Last active
September 1, 2024 05:17
-
-
Save EvanMcBroom/9af396eb0624814365df96ed8c66b8b6 to your computer and use it in GitHub Desktop.
Example code that may be used in DllMain to unlock the loader lock.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (C) 2023 Evan McBroom | |
// Originally authored October 19th, 2023. | |
// | |
// Geoff Chappell first documented the format of the loader lock cookie on November 26th, 2008. | |
// His work is applied here to unlock the loader lock without knowing the original cookie that | |
// LdrLockLoaderLock returned. This same example code may be safely used in DllMain to unlock | |
// the loader lock and execute code that would otherwise deadlock the loader. | |
// Sources: | |
// - https://www.geoffchappell.com/studies/windows/win32/ntdll/api/ldrapi/lockloaderlock.htm | |
// - https://www.geoffchappell.com/studies/windows/win32/ntdll/api/ldrapi/unlockloaderlock.htm | |
#include <Windows.h> | |
#include <iostream> | |
#include <winternl.h> | |
#include <winnt.h> | |
#include <string> | |
#include <utility> | |
template<typename Function> | |
inline auto LazyLoad(HMODULE library, const std::string& procName) { | |
return (library) ? reinterpret_cast<Function*>(GetProcAddress(library, procName.data())) : nullptr; | |
} | |
template<typename Function> | |
inline std::pair<HMODULE, Function*> LazyLoad(const std::wstring& libraryName, const std::string& procName) { | |
auto library{ LoadLibraryW(libraryName.data()) }; | |
return { library, (library) ? reinterpret_cast<Function*>(GetProcAddress(library, procName.data())) : nullptr }; | |
} | |
template<typename ReturnType, typename... ArgTypes> | |
inline auto LazyLoadWithType(HMODULE library, const std::string& procName) { | |
return LazyLoad<ReturnType(*)(ArgTypes...)>(library, procName); | |
}; | |
template<typename ReturnType, typename... ArgTypes> | |
inline auto LazyLoadWithType(const std::wstring& libraryName, const std::string& procName) { | |
return LazyLoad<ReturnType(*)(ArgTypes...)>(libraryName, procName); | |
}; | |
#define LAZY_LOAD_WSTRING(_) L#_ | |
#define LAZY_LOAD_LIBRARY_AND_PROC(LIBRARY, PROC) \ | |
HMODULE Lazy##LIBRARY; \ | |
decltype(PROC)* Lazy##PROC; \ | |
std::tie(Lazy##LIBRARY, Lazy##PROC) = LazyLoad<decltype(PROC)>(LAZY_LOAD_WSTRING(LIBRARY##.dll), #PROC); | |
#define LAZY_LOAD_NATIVE_PROC(PROC) \ | |
auto Lazy##PROC{ LazyLoad<decltype(PROC)>(GetModuleHandleW(L"ntdll.dll"), #PROC) }; | |
#define LAZY_LOAD_PROC(LIBRARY, PROC) \ | |
auto Lazy##PROC{ LazyLoad<decltype(PROC)>(LIBRARY, #PROC) }; | |
// Loader lock reference: | |
// https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices | |
NTSTATUS NTAPI LdrLockLoaderLock(IN ULONG Flags, OUT ULONG* Disposition OPTIONAL, OUT PVOID* Cookie); | |
NTSTATUS NTAPI LdrUnlockLoaderLock(IN ULONG Flags, IN OUT PVOID Cookie); | |
struct NT_TEB { | |
NT_TIB NtTib; | |
PVOID EnvironmentPointer; | |
CLIENT_ID ClientId; | |
}; | |
namespace LoaderLock { | |
enum class Disposition : size_t { | |
Invalid, | |
LockAcquired, | |
LockNotAcquired | |
}; | |
enum class Flags : size_t { | |
Default, | |
RaiseOnErrors, | |
TryOnly | |
}; | |
union Cookie { | |
void* value; | |
struct { | |
size_t code : (sizeof(size_t) * 8) - 16; | |
size_t tid : 12; | |
size_t type : 4; | |
}; | |
constexpr Cookie(size_t code, size_t tid) : code(code), tid(tid), type(TypeNormal()) { | |
}; | |
constexpr Cookie(size_t tid) : Cookie(0, tid) { | |
}; | |
constexpr Cookie() : Cookie(0, (size_t)(reinterpret_cast<NT_TEB*>(NtCurrentTeb())->ClientId.UniqueThread)) { | |
}; | |
constexpr size_t TypeNormal() { | |
return 0; | |
} | |
}; | |
// LdrUnlockLoaderLock completely ignores Cookie.code and only checks Cookie.tid | |
// That allows us to unlock the loader lock without knowing what Cookie.code is | |
bool Unlock(size_t tid) { | |
LAZY_LOAD_NATIVE_PROC(LdrUnlockLoaderLock); | |
return NT_SUCCESS(LazyLdrUnlockLoaderLock(static_cast<ULONG>(Flags::Default), Cookie(tid).value)); | |
} | |
} | |
int main() { | |
LAZY_LOAD_NATIVE_PROC(LdrLockLoaderLock); | |
ULONG lockState{ 0 }; | |
PVOID lockCookie; | |
if (NT_SUCCESS(LazyLdrLockLoaderLock(0, &lockState, &lockCookie))) { | |
std::cout << "The loader has been locked and we received the cookie: 0x" << std::hex << lockCookie << std::endl; | |
// Although the value of lockCookie is partially unknown, | |
// the unknown parts of it are fully ignored by LdrUnlockLoaderLock | |
// The only part that is checked is the part we know, the value of our current thread ID | |
// LoaderLock::Cookie constructs a value with only that data, which is fully usable | |
LoaderLock::Cookie cookie; | |
std::cout << "The cookie we're going to use to unlock the loader is: 0x" << std::hex << cookie.value << std::endl; | |
LAZY_LOAD_NATIVE_PROC(LdrUnlockLoaderLock); | |
if (NT_SUCCESS(LazyLdrUnlockLoaderLock(0, cookie.value))) { | |
std::cout << "No more loader lock!" << std::endl; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment