Overview
After successfully mapping the DLL image, fixing base relocations, and resolving all imported function addresses by patching the IAT, our manually loaded DLL is finally prepared structurally and contextually to run code. Just as we have a main() function entrypoint in a Go applications (and most languages for that matter), so too a DLL has a standard entry point function known as DllMain.
Note that unlike a main() function in a typical application, DllMain is optional. If present, the Windows loader calls this function automatically at specific times to notify the DLL about four key events (listed below).
Structure
For reference here is Dllmain from our very first lab… (What a long way we’ve come!)
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
BOOL WINAPI: This specifies the function’s return type and calling convention.
BOOL: Success (1) or Failure (0)WINAPI: This is a macro that defines the calling convention for the function
Inside of the function, inside of the switch statement, we have 4 conditions which may be triggered:
- Process Attach (
DLL_PROCESS_ATTACH): The DLL is being loaded into (attaching to) a process’s address space for the first time. This is the most common place for a DLL to perform one-time initializations. - Process Detach (
DLL_PROCESS_DETACH): The DLL is being unloaded from (detaching from) a process (e.g., due toFreeLibrarybeing called when the reference count hits zero, or because the process is terminating). This is where DLLs can perform cleanup tasks. - Thread Attach (
DLL_THREAD_ATTACH): A new thread is being created within the process after the DLL has already been loaded. - Thread Detach (
DLL_THREAD_DETACH): A thread within the process is exiting cleanly while the DLL is still loaded.
As mentioned above, a DLL does not require a DllMain. If a DLL doesn’t require any specific initialization or cleanup tied to these events, it can omit the function entirely. In such cases, the AddressOfEntryPoint field in the PE Optional Header will be zero. Our calc_dll.dll includes a DllMain, but it doesn’t perform any actions within its switch statement, effectively making it a placeholder.
Signature
If a DLL implements DllMain, the function must adhere to a specific signature defined by the Windows API:
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // Handle to DLL module (actually the base address)
DWORD fdwReason, // Reason for calling function
LPVOID lpvReserved // Reserved
);
hinstDLL(HINSTANCE): For a DLL loaded reflectively, this parameter should be the actual base address where the DLL was mapped in memory (theActualAllocatedBasewe obtained fromVirtualAlloc). This allows code insideDllMainto calculate absolute addresses relative to its own loaded position if needed (e.g., for accessing resources).fdwReason(DWORD): This value indicates whyDllMainis being called. It will be one of the constants mentioned earlier (DLL_PROCESS_ATTACH,DLL_PROCESS_DETACH,DLL_THREAD_ATTACH,DLL_THREAD_DETACH). For the initial call after loading, we useDLL_PROCESS_ATTACH.lpvReserved(LPVOID): This parameter provides additional context. It’s typicallyNULLfor dynamic loads (like reflective loading or calls viaLoadLibrary). It can be non-NULL during static loading or process termination under certain circumstances, but for our reflective call, passing0is appropriate.
Reflective Call to DllMain
After the IAT has been successfully patched (as we just did in Lab 4.2), the reflective loader can attempt to call DllMain to allow the DLL to initialize itself, mimicking the behavior of the standard Windows loader.
The process is:
- Find Entry Point RVA: Get the
AddressOfEntryPointvalue from theIMAGE_OPTIONAL_HEADERof the mapped DLL (which resides atActualAllocatedBase). - Check if Entry Point Exists: If
AddressOfEntryPointis zero, the DLL does not have aDllMain, so skip the call and proceed to the next step (like calling a specific exported function). - Calculate Entry Point VA: If the RVA is non-zero, calculate the absolute virtual address of
DllMain:DllMainVA = ActualAllocatedBase + AddressOfEntryPoint - Call
DllMain: Use a mechanism like Go’ssyscall.SyscallNto execute the code atDllMainVA. Pass the required arguments according to the signature:- Argument 1 (
hinstDLL):ActualAllocatedBase(cast touintptr). - Argument 2 (
fdwReason):DLL_PROCESS_ATTACH(constant value 1, cast touintptr). - Argument 3 (
lpvReserved):0(cast touintptr).
- Argument 1 (
- Check Return Value:
DllMainreturns aBOOL(non-zero for TRUE, zero for FALSE). When called withDLL_PROCESS_ATTACH, returningFALSEsignals that the DLL failed to initialize. A well-behaved loader (standard or reflective) should typically treat aFALSEreturn duringDLL_PROCESS_ATTACHas a fatal error, abort the loading process, and potentially unload the DLL or terminate. IfDllMainreturnsTRUE, initialization succeeded, and the loader can proceed.
Calling DllMain correctly allows the reflectively loaded DLL to perform any necessary setup before its exported functions are used, ensuring behavior consistent with standard loading practices.
Conclusion
Once DllMain (if present) has been called successfully, the DLL is fully initialized and ready. The next step is to locate and call a specific exported function within the DLL to trigger its main payload or functionality, in our case this is of course LaunchCalc().