Table of Contents

Question

In the example walk-through, we did a nearly one-to-one translation of the assembly code to C. As an exercise, re-decompile this whole function so that it looks more natural. What can you say about the developer’s skill level/experience? Explain your reasons. Can you do a better job?

Answer

We already saw a raw decompilation of the sample’s DllMain routine in the last exercise using the Hex-Rays decompiler but let’s further clean it up and polish it.

typedef unsigned __int16 WORD;  // 2 byte
typedef unsigned __int32 DWORD; // 4 byte

// 0x6 bytes(sizeof)
// Seems original developer(s) was/were unaware of structure packing?
#pragma pack(push, 1)
typedef struct _IDTR {
    WORD  Limit; // 0x0
    // WORD Pad; // 0x2, padding inserted by compiler for alignment in absence of byte packing
    DWORD Base;  // 0x2
} IDTR, *PIDTR;
#pragma pack(pop)

BOOL __stdcall DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
  // Init local variables
  IDTR idtr = { 0 };
  PROCESSENTRY32 processentry32;
  HANDLE hProcessSnapshot = NULL;
  DWORD dwParentPid = 0;
  DWORD dwPid = 0;
  BOOL bRet = FALSE;

  // Store the contents of the IDTR to memory
  __sidt(&idtr);

  // Check if IDT base address falls within range = 0x8003F400 to 0x80047400, if TRUE then return FALSE to fail DLL load else proceed with execution
  // On x86 Windows XP, IDT is located at KVA = 0x8003f400 for CPU 0
  if ((idtr.Base > 0x8003F400) && (idtr.Base < 0x80047400))
    return FALSE;

  // Zero initialize PROCESSENTRY32 structure(in a weird manner for some reason), also why?
  processentry32.dwSize = 0;
  __stosd(&processentry32.cntUsage, 0, (sizeof(processentry32) - 4));

  // Take a snapshot of all the processes in the system
  hProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  if (hProcessSnapshot == INVALID_HANDLE_VALUE)
    return FALSE;

  // Set the size of the structure before using it
  processentry32.dwSize = sizeof(PROCESSENTRY32); // 0x128

  // Retrieve information about the first process and exit if unsuccessful
  if (Process32First(hProcessSnapshot, &processentry32) == 0)
    return FALSE; // memory leak here

  // Now, walk the snapshot of processes and check if a process with image name = "explorer.exe" exists
  do {
    if (_stricmp(processentry32.szExeFile, "explorer.exe") == 0)
      break;
  } while(Process32Next(hProcessSnapshot, &processentry32));

  // Get parent process ID and process ID
  dwParentPid = processentry32.th32ParentProcessID;
  dwPid = processentry32.th32ProcessID;
  if (dwPid == dwParentPid) // why?
    return FALSE; // memory leak here

  // Perform actions based on the reason for calling
  switch(fdwReason) {
    // The DLL is being loaded into the UVAS of the current process
    case DLL_PROCESS_ATTACH:
      // Create a new thread in local process with start address = 0x100032D0
      CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)0x100032D0, NULL, 0, NULL);
      bRet = TRUE;
      break;
    // The current process is creating a new thread
    case DLL_THREAD_ATTACH:
    // A thread is exiting cleanly
    case DLL_THREAD_DETACH:
      bRet = TRUE;
      break;
    // The DLL is being unloaded from the UVAS of the calling process
    case DLL_PROCESS_DETACH:
      bRet = FALSE;
      break;
  }

  return bRet; // memory leak here
}

Regarding the developer’s skill level/experience, we can infer that they were probably not very familiar with Win32 API programming and didn’t have a lot of experience in it(or writing software targeting multi-processor systems for that matter) based on the following points:

  1. Most modern CPUs are multi-core and each logical processor has its own IDTR but the code doesn’t account for that. Instead, it just assumes that it will always run on CPU 0 for what appears to be detection of Windows XP-based VMs. What happens if the thread runs the code on a different processor? Furthermore, it doesn’t perform any Windows version checks for this purpose rendering it quite ineffective.
  2. The PROCESSENTRY32 structure is zero-initialized(in a weird manner nonetheless) when there’s no requirement for that.
  3. There exists some handle leaks due to the fact that the developer failed to close the handle to the process snapshot object as returned by kernel32!CreateToolhelp32Snapshot() as well as the handle to the thread object that is returned on a successful call to kernel32!CreateThread() while returning from the DllMain routine.
  4. Finally, it is a bit unclear to us as to why the malware might be checking whether explorer.exe process exists yet not taking any definitive action based on the fact.

It should be worth mentioning that is hard to deduce the developer’s skill level based on the limited data that we have, however, it is no mystery that this malware sample is not particularly sophisticated. Au contraire, it’s of the garden variety replete with programming and design mistakes.

Could we have done a better job than this?

Yes. ;)