Post

Taking Apart a Backdoor DLL in IDA Pro (Practical Malware Analysis Lab 5-1)

A static walk through Lab05-01.dll from Practical Malware Analysis in IDA Pro 9.1: hidden config strings, an encoded remote shell, a process lister that logs to a fake DLL, and how I followed each one from an import or string to the code that uses it.

Taking Apart a Backdoor DLL in IDA Pro (Practical Malware Analysis Lab 5-1)

Lab05-01.dll is the sample Practical Malware Analysis uses to teach IDA Pro. It’s a good first target because it’s a real backdoor with a lot of features, and none of it is hidden behind packing. I did all of this statically in IDA Pro 9.1 and never ran the DLL.

  • Sample: Lab05-01.dll, 32-bit PE DLL
  • SHA-256: eb1079bdd96bc9cc19c38b76342113a09666aad47518ff1a7536eebff8aadb4a

Rather than go through it question by question, I’ve grouped what I found by what the malware does, and for each part I show how I got from a starting point (an import, a string) to the code that matters.

Getting Oriented

DllMain is at 0x1000D02E. IDA 9.1 labels it _DllMain@12: __stdcall with 12 bytes of arguments (hinstDLL, fdwReason, lpvReserved). It isn’t the PE entry point. DllEntryPoint at 0x1001516D is the C runtime startup, which eventually calls DllMain.

At depth 1, DllMain calls only four library functions (strlen, _strnicmp, strncpy, CreateThread). Only CreateThread is a real Windows API; the other three are C runtime. Its real job is to start three threads: sub_10001074, sub_10001365 and sub_10001656. At depth 2 the xref chart grows to 69 nodes, so I listed the imports those threads reach instead:

  • Networking: WSAStartup, socket, connect, send, recv, select, closesocket, gethostbyname, inet_addr, inet_ntoa, ntohs
  • Execution and loading: WinExec, LoadLibraryA, GetProcAddress, FreeLibrary, CreateThread, ExitThread
  • Timing: Sleep, GetTickCount

From the imports alone, it starts threads that talk to a C2 server and can run programs.

Config Strings with a Tag Prefix

gethostbyname is in the IAT at 0x100163CC (WS2_32, ordinal 52). Ctrl+X shows 18 xrefs, but that’s misleading. IDA lists each call ds:gethostbyname twice, once as a read of the import pointer (r) and once as a call (p). After removing duplicates it’s 9 calls from 5 functions.

The one at 0x10001757 shows how the malware stores its config:

The gethostbyname call at 0x10001757 off_10019040 + 0Dh is passed as the hostname.

1
2
3
4
mov  eax, off_10019040    ; "[This is RDO]pics.praticalmalwareanalysis.com"
add  eax, 0Dh             ; skip 13 chars
push eax                  ; name
call ds:gethostbyname

[This is RDO] is exactly 13 characters, so add eax, 0Dh skips the tag and the lookup is for pics.praticalmalwareanalysis.com (the author’s misspelling). The same pattern appears for every config value: [This is RPO]80 is a port, and [This is CTI]30 is a sleep interval:

1
2
3
4
5
6
7
mov  eax, off_10019020    ; "[This is CTI]30"
add  eax, 0Dh             ; "30"
push eax
call ds:atoi              ; 30
imul eax, 3E8h            ; * 1000
push eax                  ; dwMilliseconds
call ds:Sleep             ; Sleep(30000)

That’s 30 seconds. Once you know the tag pattern, a strings dump shows you the C2 host, port and beacon interval in one go.

The same thread builds its socket at 0x10001701. IDA shows the arguments as 6, 1, 2. Right-click → Use standard symbolic constant makes it readable:

socket call with symbolic constants applied socket(AF_INET, SOCK_STREAM, IPPROTO_TCP): a plain IPv4 TCP connection.

The Remote Shell

The string \cmd.exe /c at 0x10095B34 sits in a custom section called xdoors_d, a name that’s a clue by itself. Its only xref is 0x100101D0 in sub_1000FF58, which turns out to be the backdoor’s main shell loop.

cmd.exe/command.exe branch followed by the recv loop The cmd.exe / command.exe choice, then the one-byte recv loop.

Going through the function:

  1. CreatePipe, then GetStartupInfoA, then hStdOutput and hStdError set to the pipe’s write end. Anything it launches writes into a pipe the malware reads.
  2. GetSystemDirectoryA plus strcat of either \cmd.exe /c or \command.exe /c.
  3. A recv(s, buf, 1, 0) loop that reads one byte at a time until CR/LF, subtracting dword_1008E5D0 from each byte. Commands arrive lightly encoded.
  4. The finished command is compared with memcmp against built-in keywords: quit, exit, enmagic, idle, uptime, language, robotwork, mbase, mhost, mmodule, minstall, inject. Anything else goes to the command interpreter.

The session opens with a greeting string at 0x10095B44: “Hi,Master [%d/%d/%d %d:%d:%d] WelCome Back…Are You Enjoying Today?”, followed by uptime, idle time and “Encrypt Magic Number For This Remote Shell Session [0x%02x]”. That magic number is the per-session byte used to encode traffic in both directions.

How it chooses cmd.exe or command.exe

The branch at 0x100101C8 tests dword_1008E5C4. Its xrefs show one write (w) and two reads. The write is at 0x10001678, right after call sub_10003695:

1
2
3
4
5
; sub_10003695
mov  [ebp+VersionInformation.dwOSVersionInfoSize], 94h
call GetVersionExA
cmp  [ebp+VersionInformation.dwPlatformId], 2   ; VER_PLATFORM_WIN32_NT
setz al

So the flag is 1 on NT-family Windows (use cmd.exe) and 0 on Windows 9x (use command.exe). The malware was written to run on both.

One command handler: robotwork

At 0x10010444, if memcmp(Buf1, "robotwork", 9) returns 0, it calls sub_100052A2(s). The decompiler makes that one quick to read:

sub_100052A2 decompiled Reads WorkTime and WorkTimes from HKLM...\CurrentVersion and sends them back.

sub_100038EE, which it calls at the end, is the send wrapper. It adds the session magic byte to every character before send(), the mirror of the subtraction on receive.

Naming a function from its callees

sub_10004E79 had no name. Xrefs From shows GetSystemDefaultLangID, sprintf, strlen, a [Language:] id:0x%x format string and the send wrapper:

Xrefs From graph for sub_10004E79 It gets the language ID, formats it and sends it.

I renamed it send_languageID. It’s the handler for the shell’s language command. Naming functions as soon as you understand them is what keeps a big binary readable, because every caller now shows the name too.

PSLIST: A Process Lister That Logs to a Fake DLL

The PSLIST export (ordinal 4, 0x10007025):

  1. Sets a global “log to file” flag (dword_1008E5BC = 1) and clears it before retn 10h.
  2. Calls sub_100036C3, which requires dwPlatformId == 2 and dwMajorVersion >= 5, so Windows 2000/XP or later. Otherwise it returns doing nothing.
  3. With no argument, it walks every process (CreateToolhelp32Snapshot → Process32First/Next → OpenProcess → EnumProcessModules → GetModuleFileNameExA). With a name argument, it keeps only processes matching _strnicmp(pe.szExeFile, Str, strlen(Str)).
  4. Each line (PID, name, thread count, path) goes through sub_1000620C, which does fopen("xinstall.dll", "a") / fprintf.

So the output goes into a text file named to look like a DLL. sub_1000664C also tries to send each line over a socket, but PSLIST passes 0 as the socket, so nothing useful goes out over the network through this export.

What I’d Write Up as IOCs

  • Network: DNS lookup of pics.praticalmalwareanalysis.com, likely TCP port 80 (from the [This is RPO]80 config string), 30-second sleep between attempts
  • Host: xinstall.dll text file in the working directory; reads HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WorkTime(s)
  • Static: [This is XXX] tagged config strings; an xdoors_d section; the “Hi,Master” banner
  • Behavior: remote shell with single-byte additive encoding, keyed per session

What I Took Away

  • Start from imports and strings, then follow the xrefs. Every finding here started with one Ctrl+X.
  • Check xref counts. The 18 vs. 9 gethostbyname count could easily become a wrong answer in a report.
  • Name and annotate as you go. Symbolic constants and renamed functions turn raw assembly into something you can read.
  • Use the decompiler where it helps. Hex-Rays made sub_100052A2 a quick read, while the shell loop was easier to follow in the graph view next to the recv calls.
This post is licensed under CC BY-NC-ND 4.0 by the author.