diff --git a/Bootstrapper/CMakeLists.txt b/Bootstrapper/CMakeLists.txt index f03fb10..b6e62f2 100644 --- a/Bootstrapper/CMakeLists.txt +++ b/Bootstrapper/CMakeLists.txt @@ -4,11 +4,34 @@ project(Bootstrapper) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) -add_library(${PROJECT_NAME} SHARED src/library.cpp) +option(DEFAULT_LOGGING_ENABLED "Enable logging by default (overridden by -LogBootstrapper arg)" OFF) +option(DEFAULT_EXIT_HOOKS_ENABLED "Enable exit hooks by default (overridden by -HookExitProcess arg)" OFF) + +add_library(${PROJECT_NAME} SHARED src/bootstrapper.cpp src/module_dir.cpp src/logging.cpp src/exit_hooks.cpp src/process_args.cpp) target_include_directories(${PROJECT_NAME} PRIVATE include) +target_compile_definitions(${PROJECT_NAME} PRIVATE BOOTSTRAPPER_LOG_NAME="${PROJECT_NAME}.log") + +if (DEFAULT_LOGGING_ENABLED) + target_compile_definitions(${PROJECT_NAME} PRIVATE DEFAULT_LOGGING_ENABLED=1) +else () + target_compile_definitions(${PROJECT_NAME} PRIVATE DEFAULT_LOGGING_ENABLED=0) +endif () + +if (DEFAULT_EXIT_HOOKS_ENABLED) + target_compile_definitions(${PROJECT_NAME} PRIVATE DEFAULT_EXIT_HOOKS_ENABLED=1) +else () + target_compile_definitions(${PROJECT_NAME} PRIVATE DEFAULT_EXIT_HOOKS_ENABLED=0) +endif () if (NOT WIN32) target_link_libraries(${PROJECT_NAME} PRIVATE dl) endif () +if (WIN32) + add_library(VersionProxy SHARED src/version_proxy.cpp src/version_proxy.def) + set_target_properties(VersionProxy PROPERTIES OUTPUT_NAME "VERSION") + target_compile_definitions(VersionProxy PRIVATE BOOTSTRAPPER_DLL_NAME="$") + install(TARGETS VersionProxy DESTINATION ${CMAKE_BINARY_DIR}/bin) +endif () + install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_BINARY_DIR}/bin) diff --git a/Bootstrapper/build.bat b/Bootstrapper/build.bat index fb375f7..f706cea 100644 --- a/Bootstrapper/build.bat +++ b/Bootstrapper/build.bat @@ -10,7 +10,7 @@ set cmakeLookup=call %vswhere% -latest -requires Microsoft.VisualStudio.Componen for /f "tokens=*" %%i in ('%cmakeLookup%') do set cmake="%%i" -%cmake% -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON .. +%cmake% -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON -DDEFAULT_LOGGING_ENABLED=ON -DDEFAULT_EXIT_HOOKS_ENABLED=OFF .. %cmake% --build . --config %CMAKE_BUILD_TYPE% --target INSTALL cd .. diff --git a/Bootstrapper/src/bootstrapper.cpp b/Bootstrapper/src/bootstrapper.cpp new file mode 100644 index 0000000..88e06a9 --- /dev/null +++ b/Bootstrapper/src/bootstrapper.cpp @@ -0,0 +1,324 @@ +// ============================================================================= +// Platform includes & macros +// ============================================================================= + +#ifdef _WIN32 +#define EXPORT __declspec(dllexport) +#include +#else +#define EXPORT [[gnu::visibility("default")]] +#include +#endif + +#include +#include +#include +#include + +#include +#include + +#include "logging.h" +#include "module_dir.h" +#include "exit_hooks.h" +#include "process_args.h" + +// ============================================================================= +// Configuration - predefined paths for automatic initialization +// ============================================================================= + +#ifndef RUNTIME_CONFIG_PATH +#define RUNTIME_CONFIG_PATH "RuntimePatcher.runtimeconfig.json" +#endif + +#ifndef ASSEMBLY_PATH +#define ASSEMBLY_PATH "RuntimePatcher.dll" +#endif + +#ifndef TYPE_NAME +#define TYPE_NAME "RuntimePatcher.Main, RuntimePatcher" +#endif + +#ifndef METHOD_NAME +#define METHOD_NAME "InitializePatchesUnmanaged" +#endif + +#ifndef HOSTFXR_TIMEOUT_MS +#define HOSTFXR_TIMEOUT_MS 30000 +#endif + +#ifndef HOSTFXR_POLL_INTERVAL_MS +#define HOSTFXR_POLL_INTERVAL_MS 100 +#endif + +#ifdef _WIN32 +#define HOSTFXR_LIBRARY_NAME "hostfxr.dll" +#else +#define HOSTFXR_LIBRARY_NAME "libhostfxr.so" +#endif + +// ============================================================================= +// Module - cross-platform shared library helpers +// ============================================================================= + +class Module { +public: + static void *get_base_address(const char *library) { +#ifdef _WIN32 + auto base = GetModuleHandleA(library); +#else + auto base = dlopen(library, RTLD_LAZY); +#endif + return reinterpret_cast(base); + } + + static void *get_export_by_name(void *module, const char *name) { +#ifdef _WIN32 + auto address = GetProcAddress((HMODULE) module, name); +#else + auto address = dlsym(module, name); +#endif + return reinterpret_cast(address); + } + + template + static T get_function_by_name(void *module, const char *name) { + return reinterpret_cast(get_export_by_name(module, name)); + } +}; + +// ============================================================================= +// Core - .NET assembly loading via hostfxr +// ============================================================================= + +enum class InitializeResult : uint32_t { + Success, + HostFxrLoadError, + HostFxrFptrLoadError, + InitializeRuntimeConfigError, + GetRuntimeDelegateError, + EntryPointError, +}; + +extern "C" EXPORT InitializeResult bootstrapper_load_assembly( + const char_t *runtime_config_path, + const char_t *assembly_path, + const char_t *type_name, + const char_t *method_name +) { + void *module = Module::get_base_address(HOSTFXR_LIBRARY_NAME); + if (!module) { + log_printf("[-] Failed to load %s\n", HOSTFXR_LIBRARY_NAME); + return InitializeResult::HostFxrLoadError; + } + +#ifdef _WIN32 + log_printf("[*] runtime_config_path: %ls\n", runtime_config_path); + log_printf("[*] assembly_path: %ls\n", assembly_path); + log_printf("[*] type_name: %ls\n", type_name); + log_printf("[*] method_name: %ls\n", method_name); +#else + log_printf("[*] runtime_config_path: %s\n", runtime_config_path); + log_printf("[*] assembly_path: %s\n", assembly_path); + log_printf("[*] type_name: %s\n", type_name); + log_printf("[*] method_name: %s\n", method_name); +#endif + + auto hostfxr_initialize_for_runtime_config_fptr = + Module::get_function_by_name(module, "hostfxr_initialize_for_runtime_config"); + + auto hostfxr_get_runtime_delegate_fptr = + Module::get_function_by_name(module, "hostfxr_get_runtime_delegate"); + + auto hostfxr_close_fptr = + Module::get_function_by_name(module, "hostfxr_close"); + + if (!hostfxr_initialize_for_runtime_config_fptr || !hostfxr_get_runtime_delegate_fptr || !hostfxr_close_fptr) { + log_printf("[-] Failed to resolve hostfxr exports (init=%p, delegate=%p, close=%p)\n", + (void*)hostfxr_initialize_for_runtime_config_fptr, + (void*)hostfxr_get_runtime_delegate_fptr, + (void*)hostfxr_close_fptr); + return InitializeResult::HostFxrFptrLoadError; + } + + hostfxr_handle ctx = nullptr; + int rc = hostfxr_initialize_for_runtime_config_fptr(runtime_config_path, nullptr, &ctx); + log_printf("[*] hostfxr_initialize_for_runtime_config => 0x%08X\n", (unsigned int)rc); + + /// Success = 0x00000000 + /// Success_HostAlreadyInitialized = 0x00000001 + /// @see https://github.com/dotnet/runtime/blob/main/docs/design/features/host-error-codes.md + if (rc != 1 || ctx == nullptr) { + hostfxr_close_fptr(ctx); + return InitializeResult::InitializeRuntimeConfigError; + } + + void *delegate = nullptr; + int ret = hostfxr_get_runtime_delegate_fptr(ctx, hostfxr_delegate_type::hdt_load_assembly_and_get_function_pointer, + &delegate); + + if (ret != 0 || delegate == nullptr) { + log_printf("[-] hostfxr_get_runtime_delegate failed => 0x%08X\n", (unsigned int)ret); + return InitializeResult::GetRuntimeDelegateError; + } + + auto load_assembly_fptr = reinterpret_cast(delegate); + + typedef void (CORECLR_DELEGATE_CALLTYPE *custom_entry_point_fn)(); + custom_entry_point_fn custom = nullptr; + + ret = load_assembly_fptr(assembly_path, type_name, method_name, UNMANAGEDCALLERSONLY_METHOD, nullptr, + (void **) &custom); + + if (ret != 0 || custom == nullptr) { + log_printf("[-] load_assembly_and_get_function_pointer failed => 0x%08X\n", (unsigned int)ret); + return InitializeResult::EntryPointError; + } + + log_printf("[+] Invoking managed entry point\n"); + custom(); + + hostfxr_close_fptr(ctx); + + return InitializeResult::Success; +} + +// ============================================================================= +// Initialization - hostfxr polling & assembly loading orchestration +// ============================================================================= + +static bool wait_for_hostfxr(int timeout_ms) { + int elapsed = 0; + while (!Module::get_base_address(HOSTFXR_LIBRARY_NAME) && elapsed < timeout_ms) { + std::this_thread::sleep_for(std::chrono::milliseconds(HOSTFXR_POLL_INTERVAL_MS)); + elapsed += HOSTFXR_POLL_INTERVAL_MS; + } + + if (!Module::get_base_address(HOSTFXR_LIBRARY_NAME)) { + log_printf("[-] hostfxr not loaded after %dms, aborting\n", timeout_ms); + return false; + } + + log_printf("[+] hostfxr found after ~%dms\n", elapsed); + return true; +} + +static std::string get_env_var(const char *name) { + auto val = std::getenv(name); + return val == nullptr ? std::string() : std::string(val); +} + +static void initialize_from_env(int timeout_ms) { + auto runtime_config_path = get_env_var("RUNTIME_CONFIG_PATH"); + auto assembly_path = get_env_var("ASSEMBLY_PATH"); + auto type_name = get_env_var("TYPE_NAME"); + auto method_name = get_env_var("METHOD_NAME"); + + if (runtime_config_path.empty() || assembly_path.empty() || type_name.empty() || method_name.empty()) { + return; + } + + if (!wait_for_hostfxr(timeout_ms)) return; + + auto ret = bootstrapper_load_assembly( +#ifdef _WIN32 + std::wstring(runtime_config_path.begin(), runtime_config_path.end()).c_str(), + std::wstring(assembly_path.begin(), assembly_path.end()).c_str(), + std::wstring(type_name.begin(), type_name.end()).c_str(), + std::wstring(method_name.begin(), method_name.end()).c_str() +#else + runtime_config_path.c_str(), + assembly_path.c_str(), + type_name.c_str(), + method_name.c_str() +#endif + ); + log_printf("[+] bootstrapper_load_assembly() => %d\n", (uint32_t) ret); +} + +static void initialize_from_constants(int timeout_ms) { + if (!wait_for_hostfxr(timeout_ms)) return; + + auto dir = get_module_directory(); + +#ifdef _WIN32 + auto runtime_config_path = dir + L"\\" + L"" RUNTIME_CONFIG_PATH; + auto assembly_path = dir + L"\\" + L"" ASSEMBLY_PATH; +#else + auto runtime_config_path = dir + "/" + RUNTIME_CONFIG_PATH; + auto assembly_path = dir + "/" + ASSEMBLY_PATH; +#endif + + auto ret = bootstrapper_load_assembly( +#ifdef _WIN32 + runtime_config_path.c_str(), + assembly_path.c_str(), + L"" TYPE_NAME, + L"" METHOD_NAME +#else + runtime_config_path.c_str(), + assembly_path.c_str(), + TYPE_NAME, + METHOD_NAME +#endif + ); + log_printf("[+] bootstrapper_load_assembly() => %d\n", (uint32_t) ret); +} + +// ============================================================================= +// Entry point - DllMain / constructor +// ============================================================================= + +#ifndef DEFAULT_LOGGING_ENABLED +#define DEFAULT_LOGGING_ENABLED 1 // Provided by CMake; this is the IntelliSense fallback +#endif + +#ifndef DEFAULT_EXIT_HOOKS_ENABLED +#define DEFAULT_EXIT_HOOKS_ENABLED 1 // Provided by CMake; this is the IntelliSense fallback +#endif + +/// Applies CMake defaults, then overrides with command-line args if present. +static void configure_features() { + log_set_enabled(DEFAULT_LOGGING_ENABLED); + exit_hooks_set_enabled(DEFAULT_EXIT_HOOKS_ENABLED); + + if (has_process_arg("-LogBootstrapper")) log_set_enabled(true); + if (has_process_arg("-HookExitProcess")) exit_hooks_set_enabled(true); + + log_init(); + hook_exit_process(); + + // Log the command line after logging is initialized + auto args = get_process_args(); + log_printf("[*] Command line (%zu args):\n", args.size()); + for (size_t i = 0; i < args.size(); i++) { + log_printf("[*] [%zu] %s\n", i, args[i].c_str()); + } +} + +#ifdef _WIN32 +BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { + if (fdwReason == DLL_PROCESS_ATTACH) { + DisableThreadLibraryCalls(hinstDLL); + set_module_handle(hinstDLL); + + configure_features(); + + CreateThread(nullptr, 0, [](LPVOID) -> DWORD { + initialize_from_constants(HOSTFXR_TIMEOUT_MS); + return 0; + }, nullptr, 0, nullptr); + } + return TRUE; +} +#else +[[gnu::constructor]] +void initialize_library() { + configure_features(); + + std::thread thread([] { + initialize_from_constants(HOSTFXR_TIMEOUT_MS); + }); + thread.detach(); +} +#endif diff --git a/Bootstrapper/src/exit_hooks.cpp b/Bootstrapper/src/exit_hooks.cpp new file mode 100644 index 0000000..786bc8f --- /dev/null +++ b/Bootstrapper/src/exit_hooks.cpp @@ -0,0 +1,181 @@ +// ============================================================================= +// Exit hooks - prevent premature process termination (x86_64) +// ============================================================================= + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#endif + +#include +#include + +#include "exit_hooks.h" +#include "logging.h" + +static bool g_exitHooksEnabled = false; + +void exit_hooks_set_enabled(bool enabled) { g_exitHooksEnabled = enabled; } +bool exit_hooks_is_enabled() { return g_exitHooksEnabled; } + +// ============================================================================= +// Inline hook primitives +// ============================================================================= + +#ifdef _WIN32 +typedef BYTE HookByte; +#else +typedef unsigned char HookByte; +#endif + +static constexpr int HOOK_SIZE = 14; + +/// Writes a 14-byte inline hook (MOV RAX, imm64; JMP RAX) - x86_64 only +static void write_inline_hook(void* target, void* detour, HookByte* backup) { +#ifdef _WIN32 + DWORD oldProtect; + VirtualProtect(target, HOOK_SIZE, PAGE_EXECUTE_READWRITE, &oldProtect); +#else + long pagesize = sysconf(_SC_PAGESIZE); + uintptr_t pageStart = (uintptr_t)target & ~(pagesize - 1); + size_t len = ((uintptr_t)target + HOOK_SIZE) - pageStart; + mprotect((void*)pageStart, len, PROT_READ | PROT_WRITE | PROT_EXEC); +#endif + + if (backup) memcpy(backup, target, HOOK_SIZE); + + HookByte hook[HOOK_SIZE] = { 0 }; + hook[0] = 0x48; hook[1] = 0xB8; // MOV RAX, imm64 + *(uintptr_t*)(hook + 2) = (uintptr_t)detour; + hook[10] = 0xFF; hook[11] = 0xE0; // JMP RAX + memcpy(target, hook, HOOK_SIZE); + +#ifdef _WIN32 + VirtualProtect(target, HOOK_SIZE, oldProtect, &oldProtect); + FlushInstructionCache(GetCurrentProcess(), target, HOOK_SIZE); +#else + mprotect((void*)pageStart, len, PROT_READ | PROT_EXEC); + __builtin___clear_cache((char*)target, (char*)target + HOOK_SIZE); +#endif +} + +static void restore_inline_hook(void* target, const HookByte* backup) { + if (!target || !backup) return; +#ifdef _WIN32 + DWORD oldProtect; + VirtualProtect(target, HOOK_SIZE, PAGE_EXECUTE_READWRITE, &oldProtect); + memcpy(target, backup, HOOK_SIZE); + VirtualProtect(target, HOOK_SIZE, oldProtect, &oldProtect); + FlushInstructionCache(GetCurrentProcess(), target, HOOK_SIZE); +#else + long pagesize = sysconf(_SC_PAGESIZE); + uintptr_t pageStart = (uintptr_t)target & ~(pagesize - 1); + size_t len = ((uintptr_t)target + HOOK_SIZE) - pageStart; + mprotect((void*)pageStart, len, PROT_READ | PROT_WRITE | PROT_EXEC); + memcpy(target, backup, HOOK_SIZE); + mprotect((void*)pageStart, len, PROT_READ | PROT_EXEC); + __builtin___clear_cache((char*)target, (char*)target + HOOK_SIZE); +#endif +} + +// ============================================================================= +// Hook state +// ============================================================================= + +static HookByte g_originalExitBytes[HOOK_SIZE] = {}; +static HookByte g_originalTerminateBytes[HOOK_SIZE] = {}; +static void* g_exitAddr = nullptr; +static void* g_terminateAddr = nullptr; + +// ============================================================================= +// Platform-specific hooked functions & hook/unhook logic +// ============================================================================= + +#ifdef _WIN32 + +static void WINAPI hooked_exit_process(UINT uExitCode) { + log_printf("[!] ExitProcess(%u) intercepted - suspending caller thread\n", uExitCode); + SuspendThread(GetCurrentThread()); + Sleep(INFINITE); +} + +static BOOL WINAPI hooked_terminate_process(HANDLE hProcess, UINT uExitCode) { + if (hProcess == GetCurrentProcess() || hProcess == (HANDLE)-1) { + log_printf("[!] TerminateProcess(%u) intercepted - suspending caller thread\n", uExitCode); + SuspendThread(GetCurrentThread()); + Sleep(INFINITE); + return TRUE; + } + restore_inline_hook(g_terminateAddr, g_originalTerminateBytes); + BOOL result = TerminateProcess(hProcess, uExitCode); + write_inline_hook(g_terminateAddr, (void*)&hooked_terminate_process, nullptr); + return result; +} + +void hook_exit_process() { + if (!g_exitHooksEnabled) return; + g_exitAddr = (void*)GetProcAddress(GetModuleHandleA("kernel32.dll"), "ExitProcess"); + if (g_exitAddr) { + write_inline_hook(g_exitAddr, (void*)&hooked_exit_process, g_originalExitBytes); + log_printf("[+] ExitProcess hooked\n"); + } else { + log_printf("[-] Failed to find ExitProcess\n"); + } + + g_terminateAddr = (void*)GetProcAddress(GetModuleHandleA("kernel32.dll"), "TerminateProcess"); + if (g_terminateAddr) { + write_inline_hook(g_terminateAddr, (void*)&hooked_terminate_process, g_originalTerminateBytes); + log_printf("[+] TerminateProcess hooked\n"); + } else { + log_printf("[-] Failed to find TerminateProcess\n"); + } +} + +void unhook_exit_process() { + restore_inline_hook(g_exitAddr, g_originalExitBytes); + restore_inline_hook(g_terminateAddr, g_originalTerminateBytes); + log_printf("[+] Exit hooks removed\n"); +} + +#else // Linux / macOS + +static void hooked_exit(int status) { + log_printf("[!] exit(%d) intercepted - blocking caller thread\n", status); + while (true) pause(); +} + +static void hooked_underscore_exit(int status) { + log_printf("[!] _exit(%d) intercepted - blocking caller thread\n", status); + while (true) pause(); +} + +void hook_exit_process() { + if (!g_exitHooksEnabled) return; + g_exitAddr = dlsym(RTLD_DEFAULT, "exit"); + if (g_exitAddr) { + write_inline_hook(g_exitAddr, (void*)&hooked_exit, g_originalExitBytes); + log_printf("[+] exit() hooked\n"); + } else { + log_printf("[-] Failed to find exit()\n"); + } + + g_terminateAddr = dlsym(RTLD_DEFAULT, "_exit"); + if (g_terminateAddr) { + write_inline_hook(g_terminateAddr, (void*)&hooked_underscore_exit, g_originalTerminateBytes); + log_printf("[+] _exit() hooked\n"); + } else { + log_printf("[-] Failed to find _exit()\n"); + } +} + +void unhook_exit_process() { + restore_inline_hook(g_exitAddr, g_originalExitBytes); + restore_inline_hook(g_terminateAddr, g_originalTerminateBytes); + log_printf("[+] Exit hooks removed\n"); +} + +#endif diff --git a/Bootstrapper/src/exit_hooks.h b/Bootstrapper/src/exit_hooks.h new file mode 100644 index 0000000..bc9351c --- /dev/null +++ b/Bootstrapper/src/exit_hooks.h @@ -0,0 +1,11 @@ +#pragma once + +// ============================================================================= +// Exit hooks - prevent premature process termination (x86_64) +// Provides hook_exit_process() / unhook_exit_process() on all platforms. +// ============================================================================= + +void exit_hooks_set_enabled(bool enabled); +bool exit_hooks_is_enabled(); +void hook_exit_process(); +void unhook_exit_process(); diff --git a/Bootstrapper/src/library.cpp b/Bootstrapper/src/library.cpp deleted file mode 100644 index 84a3723..0000000 --- a/Bootstrapper/src/library.cpp +++ /dev/null @@ -1,144 +0,0 @@ -#ifdef _WIN32 -#define EXPORT __declspec(dllexport) -#include -#else -#define EXPORT [[gnu::visibility("default")]] -#include -#include -#endif - -/// This class helps to manage shared libraries -class Module { -public: - static void *getBaseAddress(const char *library) { -#ifdef _WIN32 - auto base = GetModuleHandleA(library); -#else - auto base = dlopen(library, RTLD_LAZY); -#endif - return reinterpret_cast(base); - } - - static void *getExportByName(void *module, const char *name) { -#ifdef _WIN32 - auto address = GetProcAddress((HMODULE) module, name); -#else - auto address = dlsym(module, name); -#endif - return reinterpret_cast(address); - } - - template - static T getFunctionByName(void *module, const char *name) { - return reinterpret_cast(getExportByName(module, name)); - } -}; - -#include -#include - -/// This enums represents possible errors to hide it from others -/// useful for debugging -enum class InitializeResult : uint32_t { - Success, - HostFxrLoadError, - InitializeRuntimeConfigError, - GetRuntimeDelegateError, - EntryPointError, -}; - -extern "C" EXPORT InitializeResult bootstrapper_load_assembly( - const char_t *runtime_config_path, - const char_t *assembly_path, - const char_t *type_name, - const char_t *method_name -) { - /// Get module base address -#ifdef _WIN32 - auto libraryName = "hostfxr.dll"; -#else - auto libraryName = "libhostfxr.so"; -#endif - void *module = Module::getBaseAddress(libraryName); - if (!module) { - return InitializeResult::HostFxrLoadError; - } - - /// Obtaining useful exports - auto hostfxr_initialize_for_runtime_config_fptr = - Module::getFunctionByName(module, "hostfxr_initialize_for_runtime_config"); - - auto hostfxr_get_runtime_delegate_fptr = - Module::getFunctionByName(module, "hostfxr_get_runtime_delegate"); - - auto hostfxr_close_fptr = - Module::getFunctionByName(module, "hostfxr_close"); - - /// Load runtime config - hostfxr_handle ctx = nullptr; - int rc = hostfxr_initialize_for_runtime_config_fptr(runtime_config_path, nullptr, &ctx); - - /// Success_HostAlreadyInitialized = 0x00000001 - /// @see https://github.com/dotnet/runtime/blob/main/docs/design/features/host-error-codes.md - if (rc != 1 || ctx == nullptr) { - hostfxr_close_fptr(ctx); - return InitializeResult::InitializeRuntimeConfigError; - } - - /// From docs: native function pointer to the requested runtime functionality - void *delegate = nullptr; - int ret = hostfxr_get_runtime_delegate_fptr(ctx, hostfxr_delegate_type::hdt_load_assembly_and_get_function_pointer, - &delegate); - - if (ret != 0 || delegate == nullptr) { - return InitializeResult::GetRuntimeDelegateError; - } - - /// `void *` -> `load_assembly_and_get_function_pointer_fn`, undocumented??? - auto load_assembly_fptr = reinterpret_cast(delegate); - - typedef void (CORECLR_DELEGATE_CALLTYPE *custom_entry_point_fn)(); - custom_entry_point_fn custom = nullptr; - - ret = load_assembly_fptr(assembly_path, type_name, method_name, UNMANAGEDCALLERSONLY_METHOD, nullptr, - (void **) &custom); - - if (ret != 0 || custom == nullptr) { - return InitializeResult::EntryPointError; - } - - custom(); - - hostfxr_close_fptr(ctx); - - return InitializeResult::Success; -} - -#ifndef _WIN32 -std::string getEnvVar(const char *name) { - auto val = std::getenv(name); - return val == nullptr ? std::string() : std::string(val); -} - -[[gnu::constructor]] -void initialize_library() { - auto runtime_config_path = getEnvVar("RUNTIME_CONFIG_PATH"); - auto assembly_path = getEnvVar("ASSEMBLY_PATH"); - auto type_name = getEnvVar("TYPE_NAME"); - auto method_name = getEnvVar("METHOD_NAME"); - - if (!runtime_config_path.empty() && !assembly_path.empty() && !type_name.empty() && !method_name.empty()) { - std::thread thread([=] { - sleep(1); - auto ret = bootstrapper_load_assembly( - runtime_config_path.c_str(), - assembly_path.c_str(), - type_name.c_str(), - method_name.c_str() - ); - printf("[+] api.inject() => %d\n", (uint32_t) ret); - }); - thread.detach(); - } -} -#endif diff --git a/Bootstrapper/src/logging.cpp b/Bootstrapper/src/logging.cpp new file mode 100644 index 0000000..9e47123 --- /dev/null +++ b/Bootstrapper/src/logging.cpp @@ -0,0 +1,69 @@ +// ============================================================================= +// Logging - file + console + OutputDebugString +// ============================================================================= + +#ifdef _WIN32 +#include +#endif + +#include +#include + +#include "logging.h" +#include "module_dir.h" + +#ifndef BOOTSTRAPPER_LOG_NAME +#define BOOTSTRAPPER_LOG_NAME "bootstrapper.log" // Provided by CMake via target_compile_definitions; this is the IntelliSense fallback +#endif + +// ============================================================================= +// Internal state +// ============================================================================= + +static bool g_loggingEnabled = false; +static FILE* g_logFile = nullptr; + +void log_set_enabled(bool enabled) { g_loggingEnabled = enabled; } +bool log_is_enabled() { return g_loggingEnabled; } + +// ============================================================================= +// Logging functions +// ============================================================================= + +void log_init() { + if (!g_loggingEnabled) return; + if (!g_logFile) { + auto dir = get_module_directory(); +#ifdef _WIN32 + auto logPath = dir + L"\\" + L"" BOOTSTRAPPER_LOG_NAME; + g_logFile = _wfopen(logPath.c_str(), L"w"); +#else + auto logPath = dir + "/" + BOOTSTRAPPER_LOG_NAME; + g_logFile = fopen(logPath.c_str(), "w"); +#endif + } +} + +void log_printf(const char* fmt, ...) { + if (!g_loggingEnabled) return; + va_list args; + + if (g_logFile) { + va_start(args, fmt); + vfprintf(g_logFile, fmt, args); + fflush(g_logFile); + va_end(args); + } + + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); + +#ifdef _WIN32 + char buf[512]; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + OutputDebugStringA(buf); +#endif +} diff --git a/Bootstrapper/src/logging.h b/Bootstrapper/src/logging.h new file mode 100644 index 0000000..d27eb5f --- /dev/null +++ b/Bootstrapper/src/logging.h @@ -0,0 +1,10 @@ +#pragma once + +// ============================================================================= +// Logging - file + console + OutputDebugString +// ============================================================================= + +void log_set_enabled(bool enabled); +bool log_is_enabled(); +void log_init(); +void log_printf(const char* fmt, ...); diff --git a/Bootstrapper/src/module_dir.cpp b/Bootstrapper/src/module_dir.cpp new file mode 100644 index 0000000..37d3b21 --- /dev/null +++ b/Bootstrapper/src/module_dir.cpp @@ -0,0 +1,50 @@ +// ============================================================================= +// Module directory resolution +// ============================================================================= + +#ifdef _WIN32 +#include +#else +#include +#endif + +#include + +#include "module_dir.h" + +// ============================================================================= +// Internal state +// ============================================================================= + +#ifdef _WIN32 +static HINSTANCE g_hInstance = nullptr; + +void set_module_handle(HINSTANCE hInstance) { + g_hInstance = hInstance; +} +#endif + +// ============================================================================= +// Directory resolution +// ============================================================================= + +#ifdef _WIN32 +std::wstring get_module_directory() { + wchar_t path[MAX_PATH]; + DWORD len = GetModuleFileNameW(g_hInstance, path, MAX_PATH); + if (len == 0) return L"."; + std::wstring dir(path, len); + auto pos = dir.find_last_of(L"\\//"); + return (pos != std::wstring::npos) ? dir.substr(0, pos) : L"."; +} +#else +std::string get_module_directory() { + Dl_info info; + if (dladdr((void*)&get_module_directory, &info) && info.dli_fname) { + std::string path(info.dli_fname); + auto pos = path.find_last_of('/'); + return (pos != std::string::npos) ? path.substr(0, pos) : "."; + } + return "."; +} +#endif diff --git a/Bootstrapper/src/module_dir.h b/Bootstrapper/src/module_dir.h new file mode 100644 index 0000000..3974b5f --- /dev/null +++ b/Bootstrapper/src/module_dir.h @@ -0,0 +1,15 @@ +#pragma once + +// ============================================================================= +// Module directory resolution - returns the directory containing this DLL/SO. +// ============================================================================= + +#ifdef _WIN32 +#include +#include +void set_module_handle(HINSTANCE hInstance); +std::wstring get_module_directory(); +#else +#include +std::string get_module_directory(); +#endif diff --git a/Bootstrapper/src/process_args.cpp b/Bootstrapper/src/process_args.cpp new file mode 100644 index 0000000..4f7a8fb --- /dev/null +++ b/Bootstrapper/src/process_args.cpp @@ -0,0 +1,55 @@ +// ============================================================================= +// Process arguments - retrieve command-line args from within a shared library. +// ============================================================================= + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +#include "process_args.h" + +std::vector get_process_args() { + std::vector args; + +#ifdef _WIN32 + int argc = 0; + LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); + if (argv) { + for (int i = 0; i < argc; i++) { + int size = WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, nullptr, 0, nullptr, nullptr); + std::string s(size - 1, '\0'); + WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, s.data(), size, nullptr, nullptr); + args.push_back(std::move(s)); + } + LocalFree(argv); + } +#else // Linux + FILE* f = fopen("/proc/self/cmdline", "r"); + if (f) { + char buf[4096]; + size_t len = fread(buf, 1, sizeof(buf) - 1, f); + fclose(f); + for (size_t i = 0; i < len;) { + args.emplace_back(buf + i); + i += args.back().size() + 1; + } + } +#endif + + return args; +} + +bool has_process_arg(const char* arg) { + for (const auto& a : get_process_args()) { +#ifdef _WIN32 + if (_stricmp(a.c_str(), arg) == 0) return true; +#else + if (strcasecmp(a.c_str(), arg) == 0) return true; +#endif + } + return false; +} diff --git a/Bootstrapper/src/process_args.h b/Bootstrapper/src/process_args.h new file mode 100644 index 0000000..f6ae0f4 --- /dev/null +++ b/Bootstrapper/src/process_args.h @@ -0,0 +1,11 @@ +#pragma once + +// ============================================================================= +// Process arguments - retrieve command-line args from within a shared library. +// ============================================================================= + +#include +#include + +std::vector get_process_args(); +bool has_process_arg(const char* arg); diff --git a/Bootstrapper/src/version_proxy.cpp b/Bootstrapper/src/version_proxy.cpp new file mode 100644 index 0000000..11f6f99 --- /dev/null +++ b/Bootstrapper/src/version_proxy.cpp @@ -0,0 +1,114 @@ +// version_proxy.cpp - compile as VERSION.dll +#define WIN32_LEAN_AND_MEAN +#define NOGDI +#include +#include + +#ifndef BOOTSTRAPPER_DLL_NAME +#define BOOTSTRAPPER_DLL_NAME "Bootstrapper.dll" // Provided by CMake via target_compile_definitions; this is the IntelliSense fallback +#endif + +static HMODULE hRealVersion = nullptr; + +static void loadRealVersion() { + if (hRealVersion) return; + char systemDir[MAX_PATH]; + GetSystemDirectoryA(systemDir, MAX_PATH); + strcat_s(systemDir, "\\version.dll"); + hRealVersion = LoadLibraryA(systemDir); +} + +static FARPROC getReal(const char *name) { + loadRealVersion(); + return GetProcAddress(hRealVersion, name); +} + +/// Each proxy function uses __VA_ARGS__ passthrough via a void* cast +/// We declare them with unique names to avoid conflicts with winver.h + +#define DEFINE_PROXY(name) \ + extern "C" __declspec(naked) void proxy_##name() { \ + __asm { jmp [pfn_##name] } \ + } + +// MSVC x64 doesn't support __asm. Use a different approach for x64: + +extern "C" { + // Pointers to real functions, resolved at load time + FARPROC pfn_GetFileVersionInfoA; + FARPROC pfn_GetFileVersionInfoByHandle; + FARPROC pfn_GetFileVersionInfoExA; + FARPROC pfn_GetFileVersionInfoExW; + FARPROC pfn_GetFileVersionInfoSizeA; + FARPROC pfn_GetFileVersionInfoSizeExA; + FARPROC pfn_GetFileVersionInfoSizeExW; + FARPROC pfn_GetFileVersionInfoSizeW; + FARPROC pfn_GetFileVersionInfoW; + FARPROC pfn_VerFindFileA; + FARPROC pfn_VerFindFileW; + FARPROC pfn_VerInstallFileA; + FARPROC pfn_VerInstallFileW; + FARPROC pfn_VerLanguageNameA; + FARPROC pfn_VerLanguageNameW; + FARPROC pfn_VerQueryValueA; + FARPROC pfn_VerQueryValueW; +} + +static void resolveAll() { + loadRealVersion(); + pfn_GetFileVersionInfoA = getReal("GetFileVersionInfoA"); + pfn_GetFileVersionInfoByHandle = getReal("GetFileVersionInfoByHandle"); + pfn_GetFileVersionInfoExA = getReal("GetFileVersionInfoExA"); + pfn_GetFileVersionInfoExW = getReal("GetFileVersionInfoExW"); + pfn_GetFileVersionInfoSizeA = getReal("GetFileVersionInfoSizeA"); + pfn_GetFileVersionInfoSizeExA = getReal("GetFileVersionInfoSizeExA"); + pfn_GetFileVersionInfoSizeExW = getReal("GetFileVersionInfoSizeExW"); + pfn_GetFileVersionInfoSizeW = getReal("GetFileVersionInfoSizeW"); + pfn_GetFileVersionInfoW = getReal("GetFileVersionInfoW"); + pfn_VerFindFileA = getReal("VerFindFileA"); + pfn_VerFindFileW = getReal("VerFindFileW"); + pfn_VerInstallFileA = getReal("VerInstallFileA"); + pfn_VerInstallFileW = getReal("VerInstallFileW"); + pfn_VerLanguageNameA = getReal("VerLanguageNameA"); + pfn_VerLanguageNameW = getReal("VerLanguageNameW"); + pfn_VerQueryValueA = getReal("VerQueryValueA"); + pfn_VerQueryValueW = getReal("VerQueryValueW"); +} + +/// Proxy functions - just call through the function pointer +/// Using void* variadic trick: on x64, args stay in registers, so a simple call-through works + +#define PROXY(name) \ + extern "C" void* __cdecl proxy_##name() { \ + return ((void*(*)())pfn_##name)(); \ + } + +PROXY(GetFileVersionInfoA) +PROXY(GetFileVersionInfoByHandle) +PROXY(GetFileVersionInfoExA) +PROXY(GetFileVersionInfoExW) +PROXY(GetFileVersionInfoSizeA) +PROXY(GetFileVersionInfoSizeExA) +PROXY(GetFileVersionInfoSizeExW) +PROXY(GetFileVersionInfoSizeW) +PROXY(GetFileVersionInfoW) +PROXY(VerFindFileA) +PROXY(VerFindFileW) +PROXY(VerInstallFileA) +PROXY(VerInstallFileW) +PROXY(VerLanguageNameA) +PROXY(VerLanguageNameW) +PROXY(VerQueryValueA) +PROXY(VerQueryValueW) + +BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { + if (fdwReason == DLL_PROCESS_ATTACH) { + DisableThreadLibraryCalls(hinstDLL); + resolveAll(); + LoadLibraryA(BOOTSTRAPPER_DLL_NAME); + } + if (fdwReason == DLL_PROCESS_DETACH) { + if (hRealVersion) FreeLibrary(hRealVersion); + } + return TRUE; +} diff --git a/Bootstrapper/src/version_proxy.def b/Bootstrapper/src/version_proxy.def new file mode 100644 index 0000000..ecfcbc6 --- /dev/null +++ b/Bootstrapper/src/version_proxy.def @@ -0,0 +1,19 @@ +LIBRARY VERSION +EXPORTS + GetFileVersionInfoA = proxy_GetFileVersionInfoA + GetFileVersionInfoByHandle = proxy_GetFileVersionInfoByHandle + GetFileVersionInfoExA = proxy_GetFileVersionInfoExA + GetFileVersionInfoExW = proxy_GetFileVersionInfoExW + GetFileVersionInfoSizeA = proxy_GetFileVersionInfoSizeA + GetFileVersionInfoSizeExA = proxy_GetFileVersionInfoSizeExA + GetFileVersionInfoSizeExW = proxy_GetFileVersionInfoSizeExW + GetFileVersionInfoSizeW = proxy_GetFileVersionInfoSizeW + GetFileVersionInfoW = proxy_GetFileVersionInfoW + VerFindFileA = proxy_VerFindFileA + VerFindFileW = proxy_VerFindFileW + VerInstallFileA = proxy_VerInstallFileA + VerInstallFileW = proxy_VerInstallFileW + VerLanguageNameA = proxy_VerLanguageNameA + VerLanguageNameW = proxy_VerLanguageNameW + VerQueryValueA = proxy_VerQueryValueA + VerQueryValueW = proxy_VerQueryValueW