I'm trying to create a mechanism to workaround the loader lock so I can run arbitrary code only having control of DllMain()
.
I've had some success, see here:
Demo app loads library: LoadLibraryW(L"test.dll");
DLL:
#include <Windows.h>
void func() {
// Spawn calc.exe (can't do this from DllMain but we can here)
STARTUPINFO info = { sizeof(info) };
PROCESS_INFORMATION processInfo;
wchar_t app[5] = { L'c', L'a', L'l', L'c', 0 };
CreateProcess(NULL, app, NULL, NULL, TRUE, 0, NULL, NULL, &info, &processInfo);
// We can even load libraries from here so loader lock does appear to be gone
// MessageBox... this crashes in msvcrt!__dllonexit (why!?)
// Pretty deep into the call stack to creating the message box it happens (there's only one thread)
MessageBoxW(NULL, L"hi", L"hello", 0);
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
atexit(func);
break;
}
return TRUE;
}
Please, can you help me, why am I getting that error when creating a message box? There appears to be a competing msvcrt!__dllonexit
, but I'm not sure how to utilize it (the code) or what I should do.
Microsoft's docs say calling atexit()
in a DLL should work, but according to my tests, it will work for some things and not work for others:
https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/onexit-onexit-m?view=msvc-170
https://learn.microsoft.com/en-us/cpp/c-runtime-library/dllonexit?view=msvc-170
I've done some reverse engineering work and it turns out that there are two different types of atexit
(or _onexit
) in Windows. First is the Windows loader atexit
, this is what's used if you run atexit
from a DLL by default and it is run under loader lock. Second is the CRT atexit
, this is what's used if you run atexit
from an EXE by default and it is not run under loader lock.
Our goal now is simple, call the CRT atexit
from our DLL and when the process exits we can do as we please. Here's the code:
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <process.h> // For CRT atexit functions
#include <shellapi.h> // For ShellExecute
#define DLL
#ifdef DLL
#define API __declspec(dllexport)
#define EMPTY_IMPL {}
#else
#define API __declspec(dllimport)
#define EMPTY_IMPL
#endif
// Put any necessary exports here
EXTERN_C API VOID ExampleExport(VOID) EMPTY_IMPL;
VOID atexitHandler(VOID) {
// Verify loader lock is gone:
// !critsec ntdll!LdrpLoaderLock (as shown in: https://devblogs.microsoft.com/oldnewthing/20140808-00/?p=293)
ShellExecute(NULL, L"open", L"calc", NULL, NULL, SW_SHOW);
MessageBox(NULL, L"Message", L"Test", 0);
}
BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
// Catch both normal exit and quick exit cases (just in case)
_crt_atexit(atexitHandler);
_crt_at_quick_exit(atexitHandler);
}
return TRUE;
}
I've confirmed that this works when our DLL is loaded at runtime (i.e. LoadLibrary
) or implicitly at load-time. Also, CRT atexit
handlers run before Windows loader atexit
handlers, so we're all good on that front.