REV: Packed Trojan, DLL, Rootkit & IDAPython
Overview
Five reverse engineering exercises: a UPX-packed Trojan analyzed in IDA Pro, a custom-encrypted DLL reversed in Ghidra + IDAPython, a Base64-obfuscated VBScript downloader, a rootkit kernel driver with hooked Windows APIs, and an automation pipeline comparing IDA Pro vs Ghidra decompilation output (96 vs 71 functions).
All samples are known malicious artifacts from public repositories (MalwareBazaar, Hybrid Analysis). Analysis performed in an isolated VM environment.
Part 1: Packed Trojan — tr_pack1.exe
Sample Collection
Downloaded from MalwareBazaar by searching the UPX tag.
MalwareBazaar browse — tag:UPX search results, sample 9b0d5e40... (reported name 5kidRo0t) selected from the list
Sample metadata — SHA256 9b0d5e40ea39bcdb7f21c195750b010d7ebe343eefd691b27e572e6bbd740c33, original filename Astaroth.exe, file size 23,054 bytes, first seen 2025-05-11. TrID flags it as a 52.7% match for a UPX-compressed Win32 executable
Identify the Packer
PEiD flags the packer immediately:
PEiD main window — Entrypoint 00013360 sits inside section UPX1, File Offset 00005560, First Bytes 60,BE,15,E0 (a PUSHAD/MOV ESI pair typical of UPX’s decompression stub), Linker Info 2.41
Extra Information — Detected: UPX 0.89.6-1.02 / 1.05-2.90 (Markus & Laszlo) [Overlay], Entropy: 7.88 (Packed)
Detect It Easy (DiE) cross-validates the finding:
DiE v3.10 — Packer: UPX(4.22)[NRV,brute]. Heuristic packer flag fires on the entry point, section names, and the collision between the mapped sections and their real sizes — all consistent with UPX
DiE’s PE view — three sections named UPX0 (empty header, RWE), UPX1 (holds the entry point and packed code, RWE), and UPX2 (import table, RW) — the textbook UPX section layout
Unpack
1
upx -d 9b0d5e40ea39bcdb7f21c195750b010d7ebe343eefd691b27e572e6bbd740c33.exe
UPX 3.96w successfully restores the file from 23,054 bytes back to 44,558 bytes (51.74% compression ratio) — unpacked binary ready for static analysis
IDA Pro Analysis
Loaded the unpacked binary in IDA Pro and decompiled the entry point:
IDA Pro — start() entry point, function list visible on the left, decompiled pseudocode on the right
sub_401A18 — the primary malicious routine: crypto setup, anti-debug check, and the calls into the three sub-behaviors below
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Line 16 — XOR "encrypts" a hardcoded buffer with key 0x90
for (i = 0; i < pdwDataLen - 1; ++i)
pbData[i] ^= v6; // v6 == 0x90
// Line 19-20 — acquire a CSP handle, generate an AES-256 session key
CryptAcquireContextA(hProv, 0, 0, 0x18u, 0xF0000000);
CryptGenKey(hProv[0], 0x6610u, 1u, &phKey);
// 0x6610 = CALG_AES_256, flag 1u = CRYPT_EXPORTABLE (Microsoft CryptoAPI)
// Line 25 — anti-debugging
hProv[1] = IsDebuggerPresent();
// Line 26 — drop and execute second stage, shown NORMAL (not hidden)
ShellExecuteA(0, "runas", "Astaroth.exe", 0, 0, 1); // nShowCmd = 1 = SW_SHOWNORMAL
// Line 27-29 — hand off to the three behaviors covered below
sub_40150D(); // persistence
sub_401701(); // network scanning
sub_401912(); // fork / memory exhaustion loop
sub_40150D — Persistence
Gets the current executable’s path, resolves %AppData% via SHGetFolderPathA(..., 26, ...) (CSIDL 26 = CSIDL_APPDATA), copies itself to %AppData%\5kidRo0t.exe, writes a Run-key value named 5kidRo0t under both HKCU and HKLM\...\CurrentVersion\Run, then calls SetFileAttributesA(Str, 6) — 0x6 = FILE_ATTRIBUTE_HIDDEN (2) | FILE_ATTRIBUTE_SYSTEM (4), hiding the dropped copy from normal directory listings
sub_401701 — Network Scanning
Initializes Winsock (WSAStartup), reads the local hostname and resolves it to an IP via gethostbyname — the starting point for building target addresses on the local subnet
Opens a raw socket, then loops i = 1 to 254, formatting each candidate as "%d.%d.%d.%d" to sweep the entire local /24 (e.g., a host at 192.168.1.100 scans 192.168.1.1–192.168.1.254), sending a packet to each before cleaning up with closesocket/WSACleanup
sub_401912 — Fork + Memory Exhaustion Loop
An infinite loop: spawns a fresh copy of itself via CreateProcessA, mallocs a ~400 MB block (0x17D78400), fills it with sequential integers, sleeps 500 ms (0x1F4), then repeats — steadily consuming CPU and RAM
- The write loop (filling the allocation with data) drives progressive memory pressure that can crash the system.
- The 500 ms sleep is just enough to slow the drain rate and dodge naive threshold-based resource monitors.
Part 2: Encrypted Strings DLL — xor_caesar.dll
Compile the DLL
Source for the test DLL — two hardcoded encrypted byte arrays (xor_encrypted, caesar_encrypted), a 0x2A XOR key, a Caesar shift of 3, and three exported functions: xor_decrypt(), caesar_decrypt(), and print_all() (which MessageBox-displays both decrypted strings)
1
gcc -shared -o xor_caesar.dll -fPIC "C:\Users\r3d\Desktop\xor_caesar.c"
gcc compiles xor_caesar.dll from the C source above with no errors
Ghidra Analysis
Opened the DLL in Ghidra and located the two decryption routines by their exported names:
caesar_decrypt — the decompiler shows decrypted[i] = caesar_encrypted[i] + -3, i.e. subtracting the Caesar shift of 3 from each byte
xor_decrypt — decrypted[i] = xor_encrypted[i] ^ 0x2A; XOR is symmetric, so the same key both encrypts and decrypts
IDAPython Decryption
A Python script run inside IDA Pro locates the encrypted symbols by name, decrypts them with the correct routine, and annotates the database with inline comments:
Left: IDA Pro’s disassembly of execute_cmd, which shells out to cmd.exe with ShellExecuteA. Right: the IDAPython console output —
1
2
3
4
5
6
7
8
9
--- [*] Starting Decryption Script ---
[+] XOR Encrypted @ 0x33B403010: b'rexyOIXO^'
[+] XOR Decrypted: XORSecret
[+] Caesar Encrypted @ 0x33B403020: b'FdhvduKlgghq'
[+] Caesar Decrypted: CaesarHidden
[+] Base64 Encoded @ 0x33B403030: cG93ZXJzaGVsbCAtTm9FeGl0IC1X
[+] Decoded IOC: powershell -NoExit -W
[+] C2 URL @ 0x33B40400C: http://192.168.100.50/c2
--- [*] Decryption Complete ---
Indicators of Compromise (IOCs)
| Method | Decrypted | Ciphertext | Key | Address |
|---|---|---|---|---|
| XOR | XORSecret | rexoyIXO^ | 0x2A | 0x33B403010 |
| Caesar | CaesarHidden | FdhvduKlggqh | Shift 3 | 0x33B403020 |
| Base64 | powershell -NoExit -W | cG93ZXJzaGVsbCAtTm9FeGl0IC1X | — | 0x33B403030 |
| C2 URL | http://192.168.100.50/c2 | — | — | 0x33B40400C |
Part 3: VBScript Downloader — Downloader.vbs
Sample: Hybrid Analysis — scan result: clean.
Hybrid Analysis marks the 15 KiB .vbs sample “no specific threat” / clean across its multi-scanner — obfuscation alone is enough to defeat static AV detection here
strings Downloader.vbs
The script defines its own Base64 helper functions (eb64, stb, db64, bts) built on Msxml2.DOMDocument and ADODB.Stream — a common way VBScript malware avoids relying on any single obvious “decode” API
Key techniques:
' 1. Base64 + string replacement to build a placeholder payload
pls = Replace(pls, db64("cmVwbGFjZV9wYXJhbQ=="), pr) ' → "replace_param"
' replace_plub64 is just a placeholder in this copy — in the wild the real
' payload creates a scheduled task named "chrome center", first deleting
' any old task starting with chrome + [engine|policy|tele] to camouflage itself
' 2. Hidden PowerShell via WScript.Shell
Set so = CreateObject("WScript.Shell")
setex = so.Exec(db64("Y21kLmV4ZSAvYyBwb3dlcnNoZWxsIC1XaW5kb3dTdHlsZSBIaWRkZW4gLQ=="))
' → cmd.exe /c powershell -WindowStyle Hidden -
' the decoded payload is piped in afterward via ex.StdIn.Write cts & VbCrLf,
' so PowerShell never sees the malicious command on its own command line
- Persistence: scheduled task named
"chrome center"(Chrome-themed camouflage) - C2:
rtowatchship.xyz - Fake trust signal: a
'' SIG'' Begin/Endblock appended at the end of the script to mimic a digital signature
Impact assessment — full system control, credential theft, data exfiltration, malware deployment (loader for ransomware/RATs/keyloggers), surveillance (webcam/mic), and lateral movement are all in scope if this script runs
Part 4: Rootkit Driver
Static analysis only. IDA Pro’s import table on the driver reveals the stealth + persistence architecture:
Imports from ntoskrnl.exe — ZwQuerySystemInformation, ZwSetSecurityObject, IoCreateDevice, IoCreateSymbolicLink, ObOpenObjectByPointer, RtlCreateSecurityDescriptor and friends, all resolved against the kernel image
| API | Purpose |
|---|---|
ZwQuerySystemInformation | Tampers with the data returned to user-mode tools like Task Manager, removing this driver’s PID from the process list |
ZwSetValueKey + ZwCreateKey + ZwOpenKey | Creates the autoload entry under HKLM\SYSTEM\CurrentControlSet\Services\ so the .sys loads on every boot |
IoCreateDevice / IoCreateSymbolicLink | Registers the rootkit as a device object the OS will load |
ObOpenObjectByPointer | Direct kernel object access — used to hide handles, alter permissions, or spoof object references |
ZwQueryDirectoryObject | Hides the device/driver entries from system enumeration utilities |
Loads at kernel level before any user-mode security tool initializes, giving it a persistent, hard-to-evict foothold.
Part 5: Automation & Scripting
IDAPython — Function Renaming
1
2
3
4
5
6
7
rename_map = {
"xor_decrypt": "perform_xor_decryption",
"caesar_decrypt": "perform_caesar_decryption",
"connect_to_c2": "establish_c2_connection",
"persist_in_registry": "create_persistence_entry",
"execute_cmd": "execute_shell_command",
}
Before:
Function list still carries the DLL’s original exported names: xor_decrypt, caesar_decrypt, connect_to_c2, persist_in_registry, execute_cmd, print_all
After:
Renamed to perform_xor_decryption, perform_caesar_decryption, establish_c2_connection, create_persistence_entry, execute_shell_command — console confirms “Renamed 6 functions successfully”
Highlighting Obfuscation Patterns
A follow-up script scans the renamed functions for XOR/Caesar-style constant obfuscation directly in the disassembly, color-codes each hit, and logs it: [+] Caesar cipher at 0x33B40103D0: shift = -3, [+] Caesar cipher at 0x33B4013fc: shift = -3
Decryption Stub Detection
Hashes the mnemonic instruction sequence of every function and flags ones sharing an identical pattern:
Console: “Found 2 functions with matching patterns” for several pairs (pre_c_init/__gcc_register_frame, __stregdtor/_get_output_format, __getmainargs/__wgetmainargs, tzset/_tzset_0) — each pair is highlighted light pink in the disassembly with a “Possible repeated decryption stub” comment
IDA vs Ghidra Comparison
Export from IDA Pro:
IDAPython script walks every function via idautils.Functions(), decompiles each with Hex-Rays, and dumps the result to ida_decomp.json
Export from Ghidra:
The equivalent Ghidra Script Manager job (export_decomp.py) uses DecompInterface to decompile every function in the listing and dump it to ghidra_decomp.json
First run fails with IOError: [Errno 13] Permission denied writing to Desktop\ghidra_decomp.json; re-pointing the output path to Documents\ghidra_decomp.json succeeds
Run comparison:
1
python compare_decomp.py "ida_decomp.json" "ghidra_decomp.json"
compare_decomp.py diffs both function sets: 96 decompiled by IDA vs 71 by Ghidra, with the two “only in” lists printed in full
| Metric | IDA Pro | Ghidra |
|---|---|---|
| Total functions | 96 | 71 |
| Only in this tool | 31 — includes InternetCloseHandle, InternetOpenA, InternetOpenUrlA, the _FindPESection* family, __gcc_register_frame/__gcc_deregister_frame, __getmainargs/__wgetmainargs, create_persistence_entry, establish_c2_connection, execute_all_functions, execute_shell_command, perform_caesar_decryption, perform_xor_decryption, pre_c_init, tzset | 6 — caesar_decrypt, connect_to_c2, execute_cmd, persist_in_registry, print_all, xor_decrypt |
Why the difference:
- IDA finds 31 more — more aggressive CRT-startup and Windows-API-wrapper detection, plus the rename script’s descriptive names show up as distinct entries in its own export
- Ghidra finds 6 unique — these are exactly the DLL’s original exported names (
xor_decrypt,caesar_decrypt,connect_to_c2,persist_in_registry,execute_cmd,print_all); Ghidra preserved the pre-rename symbols in this comparison pass while IDA’s export reflects the post-rename database
Cross-validation is essential — use both tools for a complete picture.
Summary
| Part | Sample | Key Finding |
|---|---|---|
| 1 | tr_pack1.exe (Astaroth.exe) | UPX-packed, AES-256 setup, anti-debug, drops %AppData%\5kidRo0t.exe persistence, /24 raw-socket scan, fork + memory-exhaustion loop |
| 2 | xor_caesar.dll | C2 192.168.100.50/c2, XOR key 0x2A, Caesar shift 3, IDAPython auto-decrypt via xor_decrypt/caesar_decrypt |
| 3 | Downloader.vbs | AV-clean but C2 rtowatchship.xyz, Chrome-named scheduled task, fake SIG block, hidden PowerShell execution |
| 4 | Rootkit .sys | Hides PID via ZwQuerySystemInformation, boots at kernel level via Services registry key, direct kernel object manipulation |
| 5 | xor_caesar.dll | IDA: 96 functions, Ghidra: 71 — always cross-validate |