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).
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#
upx -d 9b0d5e40ea39bcdb7f21c195750b010d7ebe343eefd691b27e572e6bbd740c33.exebash
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
// 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 loopcsub_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)
gcc -shared -o xor_caesar.dll -fPIC "C:\Users\r3d\Desktop\xor_caesar.c"bash
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 —
--- [*] 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 ---plaintextIndicators 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.vbscmd
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 lineplaintext- 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#
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",
}pythonBefore:
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:
python compare_decomp.py "ida_decomp.json" "ghidra_decomp.json"bash
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
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 |