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 toFreeLibrary
being 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 (theActualAllocatedBase
we obtained fromVirtualAlloc
). This allows code insideDllMain
to calculate absolute addresses relative to its own loaded position if needed (e.g., for accessing resources).fdwReason
(DWORD): This value indicates whyDllMain
is 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 typicallyNULL
for 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, passing0
is 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
AddressOfEntryPoint
value from theIMAGE_OPTIONAL_HEADER
of the mapped DLL (which resides atActualAllocatedBase
). - Check if Entry Point Exists: If
AddressOfEntryPoint
is 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.SyscallN
to 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:
DllMain
returns aBOOL
(non-zero for TRUE, zero for FALSE). When called withDLL_PROCESS_ATTACH
, returningFALSE
signals that the DLL failed to initialize. A well-behaved loader (standard or reflective) should typically treat aFALSE
return duringDLL_PROCESS_ATTACH
as a fatal error, abort the loading process, and potentially unload the DLL or terminate. IfDllMain
returnsTRUE
, 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()
.