Post

REV: Packed Trojan, DLL, Rootkit & IDAPython

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 — UPX tag search results MalwareBazaar browse — tag:UPX search results, sample 9b0d5e40... (reported name 5kidRo0t) selected from the list

MalwareBazaar — sample detail page 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 — Entrypoint and section info 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

PEiD — Extra Information popup 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 — UPX packer analysis 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 — PE sections view showing UPX0/UPX1/UPX2 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 decompression command 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() decompiled function IDA Pro — start() entry point, function list visible on the left, decompiled pseudocode on the right

IDA Pro — sub_401A18 core function 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

IDA Pro — sub_40150D persistence function 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

IDA Pro — sub_401701 network scanning, part 1 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

IDA Pro — sub_401701 network scanning, part 2 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.1192.168.1.254), sending a packet to each before cleaning up with closesocket/WSACleanup

sub_401912 — Fork + Memory Exhaustion Loop

IDA Pro — sub_401912 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

xor_caesar.c source 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 compile command — DLL created 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:

Ghidra — caesar_decrypt function caesar_decrypt — the decompiler shows decrypted[i] = caesar_encrypted[i] + -3, i.e. subtracting the Caesar shift of 3 from each byte

Ghidra — xor_decrypt function xor_decryptdecrypted[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:

IDA Pro + IDAPython console — full decryption output 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)

MethodDecryptedCiphertextKeyAddress
XORXORSecretrexoyIXO^0x2A0x33B403010
CaesarCaesarHiddenFdhvduKlggqhShift 30x33B403020
Base64powershell -NoExit -WcG93ZXJzaGVsbCAtTm9FeGl0IC1X0x33B403030
C2 URLhttp://192.168.100.50/c20x33B40400C

Part 3: VBScript Downloader — Downloader.vbs

Sample: Hybrid Analysis — scan result: clean.

Hybrid Analysis overview 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

Strings extraction from VBScript 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/End block appended at the end of the script to mimic a digital signature

Potential impact assessment 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:

IDA Pro — rootkit driver imports Imports from ntoskrnl.exeZwQuerySystemInformation, ZwSetSecurityObject, IoCreateDevice, IoCreateSymbolicLink, ObOpenObjectByPointer, RtlCreateSecurityDescriptor and friends, all resolved against the kernel image

APIPurpose
ZwQuerySystemInformationTampers with the data returned to user-mode tools like Task Manager, removing this driver’s PID from the process list
ZwSetValueKey + ZwCreateKey + ZwOpenKeyCreates the autoload entry under HKLM\SYSTEM\CurrentControlSet\Services\ so the .sys loads on every boot
IoCreateDevice / IoCreateSymbolicLinkRegisters the rootkit as a device object the OS will load
ObOpenObjectByPointerDirect kernel object access — used to hide handles, alter permissions, or spoof object references
ZwQueryDirectoryObjectHides 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:

IDA Pro before renaming 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:

IDA Pro after renaming 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

IDAPython — obfuscation pattern highlighting 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:

Stub detection — functions highlighted 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:

IDA Pro export to ida_decomp.json IDAPython script walks every function via idautils.Functions(), decompiles each with Hex-Rays, and dumps the result to ida_decomp.json

Export from Ghidra:

Ghidra export to ghidra_decomp.json 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

Ghidra export console — permission retry 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"

Comparison results — full function diff compare_decomp.py diffs both function sets: 96 decompiled by IDA vs 71 by Ghidra, with the two “only in” lists printed in full

MetricIDA ProGhidra
Total functions9671
Only in this tool31 — 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, tzset6caesar_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

PartSampleKey Finding
1tr_pack1.exe (Astaroth.exe)UPX-packed, AES-256 setup, anti-debug, drops %AppData%\5kidRo0t.exe persistence, /24 raw-socket scan, fork + memory-exhaustion loop
2xor_caesar.dllC2 192.168.100.50/c2, XOR key 0x2A, Caesar shift 3, IDAPython auto-decrypt via xor_decrypt/caesar_decrypt
3Downloader.vbsAV-clean but C2 rtowatchship.xyz, Chrome-named scheduled task, fake SIG block, hidden PowerShell execution
4Rootkit .sysHides PID via ZwQuerySystemInformation, boots at kernel level via Services registry key, direct kernel object manipulation
5xor_caesar.dllIDA: 96 functions, Ghidra: 71 — always cross-validate
This post is licensed under CC BY 4.0 by the author.