/*
|
|
* main.cpp -- The main "entry point" and the main logic of the DLL.
|
|
*
|
|
* Here, we define and initialize struct Main that contains the main code of this DLL.
|
|
*
|
|
* The main procedure goes as follows:
|
|
* 1. The loader checks that PatchLoader.dll and mono.dll exist
|
|
* 2. mono.dll is loaded into memory and some of its functions are looked up
|
|
* 3. mono_jit_init_version is hooked with the help of MinHook
|
|
*
|
|
* Then, the loader waits until Unity creates its root domain for mono (which is done with mono_jit_init_version).
|
|
*
|
|
* Inside mono_jit_init_version hook:
|
|
* 1. Call the original mono_jit_init_version to get the Unity root domain
|
|
* 2. Load PatchLoader.dll into the root domain
|
|
* 3. Find and invoke PatchLoader.Loader.Run()
|
|
*
|
|
* Rest of the work is done on the managed side.
|
|
*
|
|
*/
|
|
|
|
#pragma warning( disable : 4267 100 152 6387 4456 6011 )
|
|
|
|
#include "winapi_util.h"
|
|
#include <Windows.h>
|
|
|
|
#include "config.h"
|
|
#include "mono.h"
|
|
#include "hook.h"
|
|
#include "assert_util.h"
|
|
#include "proxy.h"
|
|
#include <synchapi.h>
|
|
|
|
#include <intrin.h>
|
|
|
|
EXTERN_C IMAGE_DOS_HEADER __ImageBase; // This is provided by MSVC with the infomration about this DLL
|
|
|
|
HANDLE unhandledMutex;
|
|
void ownMonoJitParseOptions(int argc, char * argv[]);
|
|
BOOL setOptions = FALSE;
|
|
|
|
void unhandledException(void* exc, void* data)
|
|
{
|
|
WaitForSingleObject(unhandledMutex, INFINITE);
|
|
|
|
void* exception = NULL;
|
|
void* mstr = mono_object_to_string(exc, &exception);
|
|
|
|
if (exception != NULL)
|
|
{
|
|
#ifdef _VERBOSE
|
|
void* monostr = mono_object_to_string(exception, &exception);
|
|
if (exception != NULL)
|
|
{
|
|
DEBUG_BREAK;
|
|
LOG("An error occurred while stringifying uncaught error, but the error could not be stringified.\n");
|
|
ASSERT(FALSE, L"Uncaught exception; could not stringify");
|
|
}
|
|
else
|
|
{
|
|
char* str = mono_string_to_utf8(monostr);
|
|
DEBUG_BREAK;
|
|
LOG("An error occurred stringifying uncaught error: %s\n", str);
|
|
|
|
size_t len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
|
|
wchar_t* wstr = memalloc(sizeof(wchar_t) * len);
|
|
MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, len);
|
|
|
|
ASSERT_F(FALSE, L"Uncaught exception; stringify failed: %s", wstr);
|
|
|
|
memfree(wstr);
|
|
mono_free(str);
|
|
}
|
|
#else
|
|
ASSERT(FALSE, L"Could not stringify uncaught exception");
|
|
#endif
|
|
}
|
|
|
|
char* str = mono_string_to_utf8(mstr);
|
|
DEBUG_BREAK;
|
|
LOG("Uncaught exception: %s\n", str);
|
|
|
|
size_t len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
|
|
wchar_t* wstr = memalloc(sizeof(wchar_t) * len);
|
|
MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, len);
|
|
|
|
#ifdef _VERBOSE
|
|
ASSERT(FALSE, L"Uncaught exception; see doorstop.log for details");
|
|
#else
|
|
ASSERT_F(FALSE, L"Uncaught exception: %s", wstr);
|
|
#endif
|
|
|
|
memfree(wstr);
|
|
mono_free(str);
|
|
|
|
ReleaseMutex(unhandledMutex);
|
|
}
|
|
|
|
// The hook for mono_jit_init_version
|
|
// We use this since it will always be called once to initialize Mono's JIT
|
|
void *ownMonoJitInitVersion(const char *root_domain_name, const char *runtime_version)
|
|
{
|
|
// Call the original mono_jit_init_version to initialize the Unity Root Domain
|
|
if (debug) {
|
|
char* opts[1];
|
|
opts[0] = "";
|
|
ownMonoJitParseOptions(0, opts);
|
|
}
|
|
#ifdef WIN32
|
|
if (debug_info) {
|
|
mono_debug_init(MONO_DEBUG_FORMAT_MONO);
|
|
}
|
|
#endif
|
|
|
|
void *domain = mono_jit_init_version(root_domain_name, runtime_version);
|
|
|
|
if (debug_info) {
|
|
#ifdef WIN64
|
|
mono_debug_init(MONO_DEBUG_FORMAT_MONO);
|
|
#endif
|
|
mono_debug_domain_create(domain);
|
|
}
|
|
|
|
size_t len = WideCharToMultiByte(CP_UTF8, 0, targetAssembly, -1, NULL, 0, NULL, NULL);
|
|
char *dll_path = memalloc(sizeof(char) * len);
|
|
WideCharToMultiByte(CP_UTF8, 0, targetAssembly, -1, dll_path, len, NULL, NULL);
|
|
|
|
LOG("Loading assembly: %s\n", dll_path);
|
|
// Load our custom assembly into the domain
|
|
void *assembly = mono_domain_assembly_open(domain, dll_path);
|
|
|
|
if (assembly == NULL)
|
|
LOG("Failed to load assembly\n");
|
|
|
|
memfree(dll_path);
|
|
ASSERT_SOFT(assembly != NULL, domain);
|
|
|
|
// Get assembly's image that contains CIL code
|
|
void *image = mono_assembly_get_image(assembly);
|
|
ASSERT_SOFT(image != NULL, domain);
|
|
|
|
// Note: we use the runtime_invoke route since jit_exec will not work on DLLs
|
|
|
|
// Create a descriptor for a random Main method
|
|
void *desc = mono_method_desc_new("*:Main", FALSE);
|
|
|
|
// Find the first possible Main method in the assembly
|
|
void *method = mono_method_desc_search_in_image(desc, image);
|
|
ASSERT_SOFT(method != NULL, domain);
|
|
|
|
void *signature = mono_method_signature(method);
|
|
|
|
// Get the number of parameters in the signature
|
|
UINT32 params = mono_signature_get_param_count(signature);
|
|
|
|
void **args = NULL;
|
|
wchar_t *app_path = NULL;
|
|
if (params == 1)
|
|
{
|
|
// If there is a parameter, it's most likely a string[].
|
|
// Populate it as follows
|
|
// 0 => path to the game's executable
|
|
// 1 => --doorstop-invoke
|
|
|
|
get_module_path(NULL, &app_path, NULL, 0);
|
|
|
|
void *exe_path = MONO_STRING(app_path);
|
|
void *doorstop_handle = MONO_STRING(L"--doorstop-invoke");
|
|
|
|
void *args_array = mono_array_new(domain, mono_get_string_class(), 2);
|
|
|
|
SET_ARRAY_REF(args_array, 0, exe_path);
|
|
SET_ARRAY_REF(args_array, 1, doorstop_handle);
|
|
|
|
args = memalloc(sizeof(void*) * 1);
|
|
_ASSERTE(args != nullptr);
|
|
args[0] = args_array;
|
|
}
|
|
|
|
LOG("Installing uncaught exception handler\n");
|
|
|
|
mono_install_unhandled_exception_hook(unhandledException, NULL);
|
|
|
|
wchar_t* dll_path_w; // self path
|
|
size_t dll_path_len = get_module_path((HINSTANCE)&__ImageBase, &dll_path_w, NULL, 0);
|
|
char* self_dll_path = memalloc(dll_path_len + 1);
|
|
WideCharToMultiByte(CP_UTF8, 0, dll_path_w, -1, self_dll_path, dll_path_len + 1, NULL, NULL);
|
|
|
|
mono_dllmap_insert(NULL, "i:bsipa-doorstop", NULL, self_dll_path, NULL); // remap `bsipa-doorstop` to this assembly
|
|
|
|
memfree(self_dll_path);
|
|
memfree(dll_path_w);
|
|
|
|
|
|
unhandledMutex = CreateMutexW(NULL, FALSE, NULL);
|
|
|
|
LOG("Invoking method!\n");
|
|
|
|
void* exception = NULL;
|
|
mono_runtime_invoke(method, NULL, args, &exception);
|
|
|
|
WaitForSingleObject(unhandledMutex, INFINITE); // if the EH is triggered, wait for it
|
|
|
|
if (args != NULL)
|
|
{
|
|
memfree(app_path);
|
|
memfree(args);
|
|
NULL;
|
|
}
|
|
|
|
#ifdef _VERBOSE
|
|
if (exception != NULL)
|
|
{
|
|
void* monostr = mono_object_to_string(exception, &exception);
|
|
if (exception != NULL)
|
|
LOG("An error occurred while invoking the injector, but the error could not be stringified.\n")
|
|
else
|
|
{
|
|
char* str = mono_string_to_utf8(monostr);
|
|
LOG("An error occurred invoking the injector: %s\n", str);
|
|
mono_free(str);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
cleanupConfig();
|
|
|
|
free_logger();
|
|
|
|
ReleaseMutex(unhandledMutex);
|
|
|
|
return domain;
|
|
}
|
|
|
|
void ownMonoJitParseOptions(int argc, char * argv[])
|
|
{
|
|
setOptions = TRUE;
|
|
|
|
int size = argc;
|
|
#ifdef WIN64
|
|
if (debug) size += 2;
|
|
#elif defined(WIN32)
|
|
if (debug) size += 1;
|
|
#endif
|
|
|
|
char** arguments = memalloc(sizeof(char*) * size);
|
|
_ASSERTE(arguments != nullptr);
|
|
memcpy(arguments, argv, sizeof(char*) * argc);
|
|
if (debug) {
|
|
//arguments[argc++] = "--debug";
|
|
#ifdef WIN64
|
|
arguments[argc++] = "--soft-breakpoints";
|
|
#endif
|
|
if (debug_server)
|
|
arguments[argc] = "--debugger-agent=transport=dt_socket,address=0.0.0.0:10000,server=y";
|
|
else
|
|
arguments[argc] = "--debugger-agent=transport=dt_socket,address=127.0.0.1:10000,server=n";
|
|
}
|
|
|
|
mono_jit_parse_options(size, arguments);
|
|
|
|
memfree(arguments);
|
|
}
|
|
|
|
BOOL initialized = FALSE;
|
|
|
|
void init(HMODULE module)
|
|
{
|
|
if (!initialized)
|
|
{
|
|
initialized = TRUE;
|
|
LOG("Got mono.dll at %p\n", module);
|
|
loadMonoFunctions(module);
|
|
}
|
|
}
|
|
|
|
void * WINAPI hookGetProcAddress(HMODULE module, char const *name)
|
|
{
|
|
if (lstrcmpA(name, "mono_jit_init_version") == 0)
|
|
{
|
|
init(module);
|
|
return (void*)&ownMonoJitInitVersion;
|
|
}
|
|
if (lstrcmpA(name, "mono_jit_parse_options") == 0 && debug)
|
|
{
|
|
init(module);
|
|
return (void*)&ownMonoJitParseOptions;
|
|
}
|
|
return (void*)GetProcAddress(module, name);
|
|
}
|
|
|
|
BOOL hookGetMessage(
|
|
BOOL isW,
|
|
LPMSG msg,
|
|
HWND hwnd,
|
|
UINT wMsgFilterMin,
|
|
UINT wMsgFilterMax
|
|
);
|
|
|
|
BOOL WINAPI hookGetMessageA(LPMSG msg, HWND hwnd, UINT wMsgFilterMin, UINT wMsgFilterMax)
|
|
{
|
|
return hookGetMessage(FALSE, msg, hwnd, wMsgFilterMin, wMsgFilterMax);
|
|
}
|
|
BOOL WINAPI hookGetMessageW(LPMSG msg, HWND hwnd, UINT wMsgFilterMin, UINT wMsgFilterMax)
|
|
{
|
|
return hookGetMessage(TRUE, msg, hwnd, wMsgFilterMin, wMsgFilterMax);
|
|
}
|
|
|
|
typedef BOOL(*GetMessageHook)(BOOL isW, BOOL result, LPMSG msg, HWND hwnd, UINT filterMin, UINT filterMax);
|
|
|
|
GetMessageHook getMessageHook = NULL;
|
|
|
|
__declspec(dllexport) void __stdcall SetGetMessageHook(GetMessageHook hook) {
|
|
getMessageHook = hook;
|
|
}
|
|
|
|
BOOL hookGetMessage(
|
|
BOOL isW,
|
|
LPMSG msg,
|
|
HWND hwnd,
|
|
UINT wMsgFilterMin,
|
|
UINT wMsgFilterMax
|
|
)
|
|
{
|
|
BOOL loop = FALSE;
|
|
|
|
BOOL result;
|
|
|
|
do {
|
|
if (isW) {
|
|
result = GetMessageW(msg, hwnd, wMsgFilterMin, wMsgFilterMax);
|
|
} else {
|
|
result = GetMessageA(msg, hwnd, wMsgFilterMin, wMsgFilterMax);
|
|
}
|
|
|
|
if (getMessageHook) {
|
|
loop = getMessageHook(isW, result, msg, hwnd, wMsgFilterMin, wMsgFilterMax);
|
|
}
|
|
} while (loop);
|
|
|
|
return result;
|
|
}
|
|
|
|
BOOL hookPeekMessage(
|
|
BOOL isW,
|
|
LPMSG msg,
|
|
HWND hwnd,
|
|
UINT wMsgFilterMin,
|
|
UINT wMsgFilterMax,
|
|
UINT wRemoveMsg
|
|
);
|
|
|
|
BOOL WINAPI hookPeekMessageA(LPMSG msg, HWND hwnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg)
|
|
{
|
|
return hookPeekMessage(FALSE, msg, hwnd, wMsgFilterMin, wMsgFilterMax, wRemoveMsg);
|
|
}
|
|
BOOL WINAPI hookPeekMessageW(LPMSG msg, HWND hwnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg)
|
|
{
|
|
return hookPeekMessage(TRUE, msg, hwnd, wMsgFilterMin, wMsgFilterMax, wRemoveMsg);
|
|
}
|
|
|
|
typedef BOOL(*PeekMessageHook)(BOOL isW, BOOL result, LPMSG msg, HWND hwnd, UINT filterMin, UINT filterMax, UINT* wRemoveMsg);
|
|
|
|
PeekMessageHook peekMessageHook = NULL;
|
|
|
|
__declspec(dllexport) void __stdcall SetPeekMessageHook(PeekMessageHook hook) {
|
|
peekMessageHook = hook;
|
|
}
|
|
|
|
BOOL hookPeekMessage(
|
|
BOOL isW,
|
|
LPMSG msg,
|
|
HWND hwnd,
|
|
UINT wMsgFilterMin,
|
|
UINT wMsgFilterMax,
|
|
UINT wRemoveMsg
|
|
)
|
|
{
|
|
BOOL loop = FALSE;
|
|
|
|
BOOL result;
|
|
|
|
do {
|
|
if (isW) {
|
|
result = PeekMessageW(msg, hwnd, wMsgFilterMin, wMsgFilterMax, wRemoveMsg);
|
|
}
|
|
else {
|
|
result = PeekMessageA(msg, hwnd, wMsgFilterMin, wMsgFilterMax, wRemoveMsg);
|
|
}
|
|
|
|
if (peekMessageHook) {
|
|
loop = peekMessageHook(isW, result, msg, hwnd, wMsgFilterMin, wMsgFilterMax, &wRemoveMsg);
|
|
}
|
|
} while (loop);
|
|
|
|
return result;
|
|
}
|
|
|
|
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD reasonForDllLoad, LPVOID reserved)
|
|
{
|
|
if (reasonForDllLoad != DLL_PROCESS_ATTACH)
|
|
return TRUE;
|
|
|
|
hHeap = GetProcessHeap();
|
|
|
|
init_logger();
|
|
|
|
LOG("Doorstop started!\n");
|
|
|
|
wchar_t *dll_path = NULL;
|
|
size_t dll_path_len = get_module_path((HINSTANCE)&__ImageBase, &dll_path, NULL, 0);
|
|
|
|
LOG("DLL Path: %S\n", dll_path);
|
|
|
|
wchar_t *dll_name = get_file_name_no_ext(dll_path, dll_path_len);
|
|
|
|
LOG("Doorstop DLL Name: %S\n", dll_name);
|
|
|
|
loadProxy(dll_name);
|
|
loadConfig();
|
|
|
|
// If the loader is disabled, don't inject anything.
|
|
if (enabled)
|
|
{
|
|
LOG("Doorstop enabled!\n");
|
|
ASSERT_SOFT(GetFileAttributesW(targetAssembly) != INVALID_FILE_ATTRIBUTES, TRUE);
|
|
|
|
HMODULE targetModule = GetModuleHandleA("UnityPlayer");
|
|
|
|
if(targetModule == NULL)
|
|
{
|
|
LOG("No UnityPlayer.dll; using EXE as the hook target.");
|
|
targetModule = GetModuleHandleA(NULL);
|
|
}
|
|
|
|
LOG("Installing IAT hook\n");
|
|
if (!iat_hook(targetModule, "kernel32.dll", &GetProcAddress, &hookGetProcAddress))
|
|
{
|
|
LOG("Failed to install IAT hook!\n");
|
|
free_logger();
|
|
}
|
|
|
|
LOG("Hook installed!\n");
|
|
|
|
LOG("Attempting to install GetMessageA and GetMessageW hooks\n");
|
|
|
|
if (!iat_hook(targetModule, "user32.dll", &GetMessageA, &hookGetMessageA)) {
|
|
LOG("Could not hook GetMessageA! (not an error)\n");
|
|
}
|
|
if (!iat_hook(targetModule, "user32.dll", &GetMessageW, &hookGetMessageW)) {
|
|
LOG("Could not hook GetMessageW! (not an error)\n");
|
|
}
|
|
|
|
LOG("Attempting to install PeekMessageA and PeekMessageW hooks\n");
|
|
|
|
if (!iat_hook(targetModule, "user32.dll", &PeekMessageA, &hookPeekMessageA)) {
|
|
LOG("Could not hook PeekMessageA! (not an error)\n");
|
|
}
|
|
if (!iat_hook(targetModule, "user32.dll", &PeekMessageW, &hookPeekMessageW)) {
|
|
LOG("Could not hook PeekMessageW! (not an error)\n");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LOG("Doorstop disabled! memfreeing resources\n");
|
|
free_logger();
|
|
}
|
|
|
|
memfree(dll_name);
|
|
memfree(dll_path);
|
|
|
|
return TRUE;
|
|
}
|