Sneaky loading DLLs in a Windows executable
This post is a walk through on how to load DLLs “manually” without the use of the WinAPI LoadLibrary and store APIs addresses in memory in order to make it less obvious which API functions are imported (and therefore used) by the malware.
With this technique, the malware analyst cannot see which functions are imported from the Import section. It will be necessary to step through the code to find out which API functions are actually used.
The first section describes how to jump among Windows data structures to get information on DLLs loaded and where the base address is. What follows is a description on how to parse the PE structure of the DLL to get to the exported functions and their address in memory.
How to obtain the base address of a DLL
The base address is the address where the DLL has been loaded to memory. First step to obtain the base address is to get the PEB (Process Environment Block) address. There are two ways to get it:
- This value can be found adding 0x30 to the pointer stored in the FS segment register.
- Alternatively it is possible to start one step ahead and get the PEB address from the TEB (Thread Environment Block). The address of the TEB is stored at offset 0x18 from the address pointed by the FS segment register. And the PEB address is stored in the TEB structure at offset 0x30.
In pseudo-arithmetics of pointers this would translate to:
*PEB = *FS + 0x30
*PEB = *TEB + 0x30 = *(*FS + 0x18) + 0x30
PEB structure is a user-mode data structure, mostly used by the operating system, which is defined as follow.
struct _PEB {
0x000 BYTE InheritedAddressSpace;
0x001 BYTE ReadImageFileExecOptions;
0x002 BYTE BeingDebugged;
0x003 BYTE SpareBool;
0x004 void* Mutant;
0x008 void* ImageBaseAddress;
0x00c _PEB_LDR_DATA* Ldr;
0x010 _RTL_USER_PROCESS_PARAMETERS* ProcessParameters;
0x014 void* SubSystemData;
0x018 void* ProcessHeap;
0x01c _RTL_CRITICAL_SECTION* FastPebLock;
0x020 void* FastPebLockRoutine;
0x024 void* FastPebUnlockRoutine;
0x028 DWORD EnvironmentUpdateCount;
0x02c void* KernelCallbackTable;
0x030 DWORD SystemReserved[1];
0x034 DWORD ExecuteOptions:2; // bit offset: 34, len=2
0x034 DWORD SpareBits:30; // bit offset: 34, len=30
0x038 _PEB_FREE_BLOCK* FreeList;
0x03c DWORD TlsExpansionCounter;
0x040 void* TlsBitmap;
0x044 DWORD TlsBitmapBits[2];
0x04c void* ReadOnlySharedMemoryBase;
0x050 void* ReadOnlySharedMemoryHeap;
0x054 void** ReadOnlyStaticServerData;
0x058 void* AnsiCodePageData;
0x05c void* OemCodePageData;
0x060 void* UnicodeCaseTableData;
0x064 DWORD NumberOfProcessors;
0x068 DWORD NtGlobalFlag;
0x070 _LARGE_INTEGER CriticalSectionTimeout;
0x078 DWORD HeapSegmentReserve;
0x07c DWORD HeapSegmentCommit;
0x080 DWORD HeapDeCommitTotalFreeThreshold;
0x084 DWORD HeapDeCommitFreeBlockThreshold;
0x088 DWORD NumberOfHeaps;
0x08c DWORD MaximumNumberOfHeaps;
0x090 void** ProcessHeaps;
0x094 void* GdiSharedHandleTable;
0x098 void* ProcessStarterHelper;
0x09c DWORD GdiDCAttributeList;
0x0a0 void* LoaderLock;
0x0a4 DWORD OSMajorVersion;
0x0a8 DWORD OSMinorVersion;
0x0ac WORD OSBuildNumber;
0x0ae WORD OSCSDVersion;
0x0b0 DWORD OSPlatformId;
0x0b4 DWORD ImageSubsystem;
0x0b8 DWORD ImageSubsystemMajorVersion;
0x0bc DWORD ImageSubsystemMinorVersion;
0x0c0 DWORD ImageProcessAffinityMask;
0x0c4 DWORD GdiHandleBuffer[34];
0x14c void (*PostProcessInitRoutine)();
0x150 void* TlsExpansionBitmap;
0x154 DWORD TlsExpansionBitmapBits[32];
0x1d4 DWORD SessionId;
0x1d8 _ULARGE_INTEGER AppCompatFlags;
0x1e0 _ULARGE_INTEGER AppCompatFlagsUser;
0x1e8 void* pShimData;
0x1ec void* AppCompatInfo;
0x1f0 _UNICODE_STRING CSDVersion;
0x1f8 void* ActivationContextData;
0x1fc void* ProcessAssemblyStorageMap;
0x200 void* SystemDefaultActivationContextData;
0x204 void* SystemAssemblyStorageMap;
0x208 DWORD MinimumStackCommit;
);
In the PEB structure we are looking for the field Lrd at offset 0x0c, which is a pointer to the PEB LDR DATA structure. This structure contains information about all the modules loaded in the current process.
typedef struct _PEB_LDR_DATA
{
0x00 ULONG Length; /* Size of structure, used by ntdll.dll as structure version ID */
0x04 BOOLEAN Initialized; /* If set, loader data section for current process is initialized */
0x08 PVOID SsHandle;
0x0c LIST_ENTRY InLoadOrderModuleList; /* Pointer to LDR_DATA_TABLE_ENTRY structure. Previous and next module in load order */
0x14 LIST_ENTRY InMemoryOrderModuleList; /* Pointer to LDR_DATA_TABLE_ENTRY structure. Previous and next module in memory placement order */
0x1c LIST_ENTRY InInitializationOrderModuleList; /* Pointer to LDR_DATA_TABLE_ENTRY structure. Previous and next module in initialization order */
} PEB_LDR_DATA,*PPEB_LDR_DATA; // +0x24
The last three fields of this structure are pointers to the first element of a linked list, which contains information about the DLLs loaded in the process memory. The difference among the three linked lists is that they contain the same entries but in different order.
Each element of the linked list is a data structure called LDR DATA TABLE ENTRY, and its structure is defined as follows.
typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks; /* 0x00 */
LIST_ENTRY InMemoryOrderLinks; /* 0x08 */
LIST_ENTRY InInitializationOrderLinks; /* 0x10 */
PVOID DllBase; /* 0x18 */
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName; /* 0x24 */
UNICODE_STRING BaseDllName; /* 0x28 */
ULONG Flags;
WORD LoadCount;
WORD TlsIndex;
union
{
LIST_ENTRY HashLinks;
struct
{
PVOID SectionPointer;
ULONG CheckSum;
};
};
union
{
ULONG TimeDateStamp;
PVOID LoadedImports;
};
_ACTIVATION_CONTEXT * EntryPointActivationContext;
PVOID PatchInformation;
LIST_ENTRY ForwarderLinks;
LIST_ENTRY ServiceTagLinks;
LIST_ENTRY StaticLinks;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
It is important to observe here that the InInitialisationOrderModuleList of the previous element of the linked list does not point to offset 0x00 of the current list element, it points instead to the third element, i.e. offset 0x10, and might be deduced by the name of the structure field “InInitialisationOrderLinks”.
From this data structure we are interested in FullDllName at offset 0x24, to check if this is the DLL we were looking for. The field BaseAddress at offset 0x18 in a pointer to the base address of the DLL.
In pseudo code this will be:
TEB := *(FS + 18h)
PEB := *(TEB + 30h)
PEB LDR DATA := *(PEB + 0x0c)
InInitialisationOrderLinks := LDR DATA TABLE ENTRY + 0x10 = *(PEB LDR DATA + 0x1c)
FullDllName := (InInitialisationOrderLinks + 0x14)
DllBase := (InInitialisationOrderLinks + 0x08)
A useful obfuscation technique might be to hardcode the hash of the interesting DLLs in the code and once reached this point in the data structure while iterating to find the right DLL, calculate the hash of the FullDllName and compare this value with the hardcoded hash. This way no strings representing the name of the DLL will be found in the code.
Getting API addresses from DLL base address
To obtain the API address it is necessary to parse the PE structure of the DLL, specifically in the Exported Table section. Image from here.
The DLL base address points to the beginning of the PE, where the MZ signature (0x4d5a) is. Offset 0x3c contains a pointer to the offset of the PE signature, as shown in the following table.
+ ------ + ----- + ---------- + ---------------------------------- +
| Offset | Size | Member | Meaning |
+ ------ + ----- + ---------- + ---------------------------------- +
| 0x00 | WORD | emagic | Magic DOS signature MZ (0x4d 0x5A) |
| 0x02 | WORD | e_cblp | Bytes on last page of file |
| 0x04 | WORD | e_cp | Pages in file |
| 0x06 | WORD | e_crlc | Relocations |
| 0x08 | WORD | e_cparhdr | Size of header in paragraphs |
| 0x0A | WORD | e_minalloc | Minimum extra paragraphs needed |
| 0x0C | WORD | e_maxalloc | Maximum extra paragraphs needed |
| 0x0E | WORD | e_ss | Initial (relative) SS value |
| 0x10 | WORD | e_sp | Initial SP value |
| 0x12 | WORD | e_csum | Checksum |
| 0x14 | WORD | e_ip | Initial IP value |
| 0x16 | WORD | e_cs | Initial (relative) CS value |
| 0x18 | WORD | e_lfarlc | File address of relocation table |
| 0x1A | WORD | e_ovno | Overloay number |
| 0x1C | WORD | e_res[4] | Reserved words (4 WORDs) |
| 0x24 | WORD | e_oemid | OEM identifier (for e_oeminfo) |
| 0x26 | WORD | e_oeminfo | OEM information; e_oemid specific |
| 0x28 | WORD | e_res2[10] | Reserved words (10 WORDs) |
| 0x3c | DWORD | e_lfanew | Offset to start of PE header |
+ ------ + ----- + ---------- + ---------------------------------- +
At offset 0x78 starting from the PE header section, there is the Relative Virtual Address of the export table, the table containing the information about the PE exported functions.
+ ------ + ----- + ------------ + ----------------------- +
| Offset | Size | Member | Meaning |
+ ------ + ----- + ------------ + ----------------------- +
| 0x78 | DWORD | Export Table | RVA of Export Directory |
+ ------ + ----- + ------------ + ----------------------- +
This is how the export table looks like.
+ ------ + ------ + --------------------------------------------- + ------------------------------------------------------------------------------------------------------------------------------------------------------------- +
| Offset | Size | Field | Description |
+ ------ + ------ + --------------------------------------------- + ------------------------------------------------------------------------------------------------------------------------------------------------------------ +
| 0x00 | DWORD | Characteristics (Export Flags) | Reserved, must be 0. |
| 0x04 | DWORD | TimeDateStamp | The time and date that the export data was created. |
| 0x08 | WORD | MajorVersion | The major version number. The major and minor version numbers can be set by the user. |
| 0x0A | WORD | MinorVersion | The minor version number. |
| 0x0C | DWORD | Name (Name RVA) | The address of the ASCII string that contains the name of the DLL. This address is relative to the image base. |
| 0x10 | DWORD | Base (Ordinal Base) | The starting ordinal number for exports in this image. This field specifies the starting ordinal number for the export address table. It is usually set to 1. |
| 0x14 | DWORD | NumberOfFunctions (Address Table Entries) | The number of entries in the export address table. |
| 0x18 | DWORD | NumberOfNames (Number of Name Pointers) | The number of entries in the name pointer table. This is also the number of entries in the ordinal table. |
| 0x1C | DWORD | AddressOfFunctions (Export Address Table RVA) | The address of the export address table, relative to the image base. |
| 0x20 | DWORD | AddressOfNames (Name Pointer RVA) | The address of the export name pointer table, relative to the image base. The table size is given by the Number of Name Pointers field. |
| 0x24 | DWORD | AddressOfNameOrdinals (Ordinal Table RVA) | The address of the ordinal table, relative to the image base. |
+ ------ + ------ + --------------------------------------------- + ------------------------------------------------------------------------------------------------------------------------------------------------------------- +
To calculate the memory address of the API, the following structure fields are needed.
+ ------ + --------------------- +
| Offset | Field |
+ ------ + --------------------- +
| 0x18 | NumberOfNames |
| 0x1c | AddressOfFunctions |
| 0x20 | AddressOfNames |
| 0x24 | AddressOfNameOrdinals |
+ ------ + --------------------- +
The following schema shows the connections among the lists.
The Export section might contain functions that are not exported by name (so they do not have a name, they might be instead exported by ordinal).
AddressOfFunctions is a pointer to a list containing the RVA address of the exported functions. AddressOfNames is a pointer to a list which contains all the names of the functions which are exported by name (functions can also be exported by ordinal).
AddressOfNameOrdinals is a pointer to a list that contains the ordinals (or the numerical position) of the functions which are listed in AddressOfNames, but only the ones exported by name.
Example: I want to find the memory address of GetProcAddress. Since it is exported by name I will look into the AddressOfNames list to find the string GetProcAddress. I do this with a for. Once found, I use the value of the iteration parameter i
(which corresponds to the position of the string in the list) to access AddressOfNameOrdinals[i]
. This value corresponds to the ordinal in the AddressOfFunctions list. In other words the address of GetProcAddress is the value in AddressOfFunctions[i]
.
This address might now be stored in a structure in the malware code, so that when the needed API is called, the malware analyst will only see the element of the structure and no strings