diff --git a/src/main/inject/Module.mk b/src/main/inject/Module.mk index f2b4b2d..1f2ca1e 100644 --- a/src/main/inject/Module.mk +++ b/src/main/inject/Module.mk @@ -2,10 +2,14 @@ exes += inject ldflags_inject := \ -mconsole \ + -lpsapi \ libs_inject := \ util \ src_inject := \ + debugger.c \ + logger.c \ main.c \ options.c \ + version.c \ diff --git a/src/main/inject/debugger.c b/src/main/inject/debugger.c new file mode 100644 index 0000000..6ef6dac --- /dev/null +++ b/src/main/inject/debugger.c @@ -0,0 +1,642 @@ +#define LOG_MODULE "inject-debugger" + +#include + +#include +#include +#include +#include +#include + +#include "inject/debugger.h" +#include "inject/logger.h" + +#include "util/log.h" +#include "util/mem.h" +#include "util/str.h" + +struct debugger_thread_params { + const char *app_name; + char *cmd_line; + bool local_debugger; +}; + +static HANDLE debugger_thread_handle; +static HANDLE debugger_ready_event; + +static PROCESS_INFORMATION pi; + +// Source: +// https://docs.microsoft.com/en-us/windows/win32/memory/obtaining-a-file-name-from-a-file-handle +static bool +debugger_get_file_name_from_handle(HANDLE hFile, char *filename, size_t bufsize) +{ + HANDLE file_map; + bool success; + + success = false; + + // Create a file mapping object. + file_map = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 1, NULL); + + if (file_map) { + // Create a file mapping to get the file name. + void *mem = MapViewOfFile(file_map, FILE_MAP_READ, 0, 0, 1); + + if (mem) { + if (GetMappedFileNameA( + GetCurrentProcess(), mem, filename, MAX_PATH)) { + + // Translate path with device name to drive letters. + char tmp[bufsize]; + tmp[0] = '\0'; + + if (GetLogicalDriveStringsA(bufsize - 1, tmp)) { + char name[MAX_PATH]; + char drive[3] = " :"; + BOOL found = FALSE; + char *p = tmp; + + do { + // Copy the drive letter to the template string + *drive = *p; + + // Look up each device name + if (QueryDosDevice(drive, name, MAX_PATH)) { + size_t name_len = strlen(name); + + if (name_len < MAX_PATH) { + found = + strncmp(filename, name, name_len) == 0 && + *(filename + name_len) == '\\'; + + if (found) { + // Reconstruct filename using tmp_file + // Replace device path with DOS path + char tmp_file[MAX_PATH]; + sprintf( + tmp_file, + "%s%s", + drive, + filename + name_len); + + strcpy(filename, tmp_file); + } + } + } + + // Go to the next NULL character. + while (*p++) + ; + } while (!found && *p); // end of string + } + } + success = true; + UnmapViewOfFile(mem); + } + + CloseHandle(file_map); + } + + return success; +} + +static char * +read_debug_str(HANDLE process, const OUTPUT_DEBUG_STRING_INFO *odsi) +{ + log_assert(process); + log_assert(odsi); + + char *str; + + str = xmalloc(odsi->nDebugStringLength); + + if (ReadProcessMemory( + process, + odsi->lpDebugStringData, + str, + odsi->nDebugStringLength, + NULL)) { + str[odsi->nDebugStringLength - 1] = '\0'; + } else { + free(str); + + log_error( + "ReadProcessMemory for debug string failed: %08x", + (unsigned int) GetLastError()); + str = NULL; + } + + return str; +} + +static char * +read_debug_wstr(HANDLE process, const OUTPUT_DEBUG_STRING_INFO *odsi) +{ + log_assert(process); + log_assert(odsi); + + char *str; + wchar_t *wstr; + uint32_t nbytes; + + nbytes = odsi->nDebugStringLength * sizeof(wchar_t); + wstr = xmalloc(nbytes); + + if (ReadProcessMemory( + process, odsi->lpDebugStringData, wstr, nbytes, NULL)) { + if (wstr_narrow(wstr, &str)) { + str[odsi->nDebugStringLength - 1] = '\0'; + } else { + log_error("OutputDebugStringW: UTF-16 conversion failed"); + str = NULL; + } + } else { + log_error( + "ReadProcessMemory for debug string failed: %08x", + (unsigned int) GetLastError()); + str = NULL; + } + + free(wstr); + + return str; +} + +static bool log_debug_str(HANDLE process, const OUTPUT_DEBUG_STRING_INFO *odsi) +{ + log_assert(odsi); + + char *debug_str; + + if (odsi->fUnicode) { + debug_str = read_debug_wstr(process, odsi); + } else { + debug_str = read_debug_str(process, odsi); + } + + if (debug_str) { + logger_log(debug_str); + + free(debug_str); + return true; + } else { + return false; + } +} + +static const char *exception_code_to_str(DWORD code) +{ + switch (code) { + case EXCEPTION_ACCESS_VIOLATION: + return "EXCEPTION_ACCESS_VIOLATION"; + case EXCEPTION_DATATYPE_MISALIGNMENT: + return "EXCEPTION_DATATYPE_MISALIGNMENT"; + case EXCEPTION_BREAKPOINT: + return "EXCEPTION_BREAKPOINT"; + case EXCEPTION_SINGLE_STEP: + return "EXCEPTION_SINGLE_STEP"; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED"; + case EXCEPTION_FLT_DENORMAL_OPERAND: + return "EXCEPTION_FLT_DENORMAL_OPERAND"; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + return "EXCEPTION_FLT_DIVIDE_BY_ZERO"; + case EXCEPTION_FLT_INEXACT_RESULT: + return "EXCEPTION_FLT_INEXACT_RESULT"; + case EXCEPTION_FLT_INVALID_OPERATION: + return "EXCEPTION_FLT_INVALID_OPERATION"; + case EXCEPTION_FLT_OVERFLOW: + return "EXCEPTION_FLT_OVERFLOW"; + case EXCEPTION_FLT_STACK_CHECK: + return "EXCEPTION_FLT_STACK_CHECK"; + case EXCEPTION_FLT_UNDERFLOW: + return "EXCEPTION_FLT_UNDERFLOW"; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + return "EXCEPTION_INT_DIVIDE_BY_ZERO"; + case EXCEPTION_INT_OVERFLOW: + return "EXCEPTION_INT_OVERFLOW"; + case EXCEPTION_PRIV_INSTRUCTION: + return "EXCEPTION_PRIV_INSTRUCTION"; + case EXCEPTION_IN_PAGE_ERROR: + return "EXCEPTION_IN_PAGE_ERROR"; + case EXCEPTION_ILLEGAL_INSTRUCTION: + return "EXCEPTION_ILLEGAL_INSTRUCTION"; + case EXCEPTION_NONCONTINUABLE_EXCEPTION: + return "EXCEPTION_NONCONTINUABLE_EXCEPTION"; + case EXCEPTION_STACK_OVERFLOW: + return "EXCEPTION_STACK_OVERFLOW"; + default: + log_warning("Unknown exception code: %ld", code); + return "EXCEPTION_UNKNOWN"; + } +} + +static bool debugger_create_process( + bool local_debugger, const char *app_name, char *cmd_line) +{ + log_assert(app_name); + log_assert(cmd_line); + + STARTUPINFO si; + BOOL ok; + DWORD flags; + + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + + flags = 0; + + // CREATE_SUSPENDED that we have plenty of time to set up the debugger and + // theemote process environment with hook dlls. + flags |= CREATE_SUSPENDED; + + if (local_debugger) { + // DEBUG_PROCESS is required to make this work properly. Otherwise, + // weird things like random remote process crashing are happening. Also, + // DEBUG_ONLY_THIS_PROCESS is NOT sufficient/ correct here. Maybe I + // didn't understand the documentation properly or it and various blog + // posts I read are not explaining things well enough or are even wrong. + flags |= DEBUG_PROCESS; + } + + log_misc("Creating remote process %s...", app_name); + log_misc("Remote process cmd_line: %s", cmd_line); + + ok = CreateProcess( + app_name, cmd_line, NULL, NULL, FALSE, flags, NULL, NULL, &si, &pi); + + if (!ok) { + log_error( + "Failed to launch hooked EXE: %08x", (unsigned int) GetLastError()); + + free(cmd_line); + + return false; + } + + free(cmd_line); + + log_info("Remote process created, pid: %ld", pi.dwProcessId); + + return true; +} + +static bool debugger_loop() +{ + DEBUG_EVENT de; + DWORD continue_status; + char str_buffer[MAX_PATH + 1]; + + memset(str_buffer, 0, sizeof(str_buffer)); + + for (;;) { + if (!WaitForDebugEvent(&de, INFINITE)) { + log_error( + "WaitForDebugEvent failed: %08x", + (unsigned int) GetLastError()); + return false; + } + + // Changed if applicable, e.g. on exceptions + continue_status = DBG_CONTINUE; + + switch (de.dwDebugEventCode) { + case EXCEPTION_DEBUG_EVENT: + log_misc( + "EXCEPTION_DEBUG_EVENT(pid %ld, tid %ld): x%s 0x%p", + de.dwProcessId, + de.dwThreadId, + exception_code_to_str( + de.u.Exception.ExceptionRecord.ExceptionCode), + de.u.Exception.ExceptionRecord.ExceptionAddress); + + if (de.u.Exception.ExceptionRecord.ExceptionCode == + EXCEPTION_BREAKPOINT) { + // Handle breakpoints by ignoring them. Some modules of some + // games set breakpoints when they detect a debugger likely + // used for actual development/debugging tasks + continue_status = DBG_EXCEPTION_HANDLED; + } else { + continue_status = DBG_EXCEPTION_NOT_HANDLED; + } + + break; + + case CREATE_THREAD_DEBUG_EVENT: + log_misc( + "CREATE_THREAD_DEBUG_EVENT(pid %ld, tid %ld): hnd 0x%p, " + "addr 0x%p", + de.dwProcessId, + de.dwThreadId, + de.u.CreateThread.hThread, + de.u.CreateThread.lpStartAddress); + break; + + case CREATE_PROCESS_DEBUG_EVENT: + GetProcessImageFileNameA( + de.u.CreateProcessInfo.hProcess, + str_buffer, + sizeof(str_buffer)); + + log_misc( + "CREATE_PROCESS_DEBUG_EVENT(pid %ld, tid %ld): name %s, " + "pid %ld", + de.dwProcessId, + de.dwThreadId, + str_buffer, + GetProcessId(de.u.CreateProcessInfo.hProcess)); + + CloseHandle(de.u.CreateProcessInfo.hFile); + + break; + + case EXIT_THREAD_DEBUG_EVENT: + log_misc( + "EXIT_THREAD_DEBUG_EVENT(pid %ld, tid %ld)", + de.dwProcessId, + de.dwThreadId); + break; + + case EXIT_PROCESS_DEBUG_EVENT: + log_misc( + "EXIT_PROCESS_DEBUG_EVENT(pid %ld, tid %ld)", + de.dwProcessId, + de.dwThreadId); + return true; + + case LOAD_DLL_DEBUG_EVENT: + if (!debugger_get_file_name_from_handle( + de.u.LoadDll.hFile, str_buffer, sizeof(str_buffer))) { + strcpy(str_buffer, "--- Unknown ---"); + } + + log_misc( + "LOAD_DLL_DEBUG_EVENT(pid %ld, tid %ld): name %s, base " + "0x%p", + de.dwProcessId, + de.dwThreadId, + str_buffer, + de.u.LoadDll.lpBaseOfDll); + + CloseHandle(de.u.LoadDll.hFile); + + break; + + case UNLOAD_DLL_DEBUG_EVENT: + log_misc( + "UNLOAD_DLL_DEBUG_EVENT(pid %ld, tid %ld): base 0x%p", + de.dwProcessId, + de.dwThreadId, + de.u.UnloadDll.lpBaseOfDll); + + break; + + case OUTPUT_DEBUG_STRING_EVENT: + log_debug_str(pi.hProcess, &de.u.DebugString); + + break; + + case RIP_EVENT: + log_misc( + "RIP_EVENT(pid %ld, tid %ld)", + de.dwProcessId, + de.dwThreadId); + break; + + default: + // Ignore other events + break; + } + + if (!ContinueDebugEvent( + de.dwProcessId, de.dwThreadId, continue_status)) { + log_error( + "ContinueDebugEvent failed: %08x", + (unsigned int) GetLastError()); + return false; + } + } +} + +static DWORD WINAPI debugger_proc(LPVOID param) +{ + struct debugger_thread_params *params; + + params = (struct debugger_thread_params *) param; + + log_misc( + "Debugger thread start (local debugger: %d)", params->local_debugger); + + if (!debugger_create_process( + params->local_debugger, params->app_name, params->cmd_line)) { + return 0; + } + + SetEvent(debugger_ready_event); + + // Don't run our local debugger loop if the user wants to attach a remote + // debugger or debugger is disabled + if (params->local_debugger) { + debugger_loop(); + } + + free(params); + + log_misc("Debugger thread end"); + + return 0; +} + +bool debugger_init(bool local_debugger, const char *app_name, char *cmd_line) +{ + struct debugger_thread_params *thread_params; + + debugger_ready_event = CreateEvent(NULL, TRUE, FALSE, NULL); + + if (!debugger_ready_event) { + free(cmd_line); + + log_error( + "Creating event object failed: %08x", + (unsigned int) GetLastError()); + return false; + } + + // free'd by thread if created successfully + thread_params = xmalloc(sizeof(struct debugger_thread_params)); + + thread_params->app_name = app_name; + thread_params->cmd_line = cmd_line; + thread_params->local_debugger = local_debugger; + + debugger_thread_handle = + CreateThread(NULL, 0, debugger_proc, thread_params, 0, 0); + + if (!debugger_thread_handle) { + free(cmd_line); + free(thread_params); + + log_error( + "Creating debugger thread failed: %08x", + (unsigned int) GetLastError()); + return false; + } + + WaitForSingleObject(debugger_ready_event, INFINITE); + + log_misc("Debugger initialized"); + + return true; +} + +bool debugger_wait_for_remote_debugger() +{ + BOOL res; + + log_warning("Waiting until remote debugger attaches to remote process..."); + + while (true) { + res = FALSE; + + if (!CheckRemoteDebuggerPresent(pi.hProcess, &res)) { + log_error( + "CheckRemoteDebuggerPresent failed: %08x", + (unsigned int) GetLastError()); + return false; + } + + if (res) { + log_info("Remote debugger attached, resuming"); + break; + } + + Sleep(1000); + } + + return true; +} + +bool debugger_inject_dll(const char *path_dll) +{ + log_assert(path_dll); + + char dll_path[MAX_PATH]; + DWORD dll_path_length; + void *remote_addr; + BOOL ok; + HANDLE remote_thread; + + log_misc("Injecting: %s", path_dll); + + dll_path_length = + SearchPath(NULL, path_dll, NULL, MAX_PATH, dll_path, NULL); + + dll_path_length++; + + remote_addr = VirtualAllocEx( + pi.hProcess, + NULL, + dll_path_length, + MEM_RESERVE | MEM_COMMIT, + PAGE_READWRITE); + + if (!remote_addr) { + log_error("VirtualAllocEx failed: %08x", (unsigned int) GetLastError()); + + goto alloc_fail; + } + + ok = WriteProcessMemory( + pi.hProcess, remote_addr, dll_path, dll_path_length, NULL); + + if (!ok) { + log_error( + "WriteProcessMemory failed: %08x", (unsigned int) GetLastError()); + + goto write_fail; + } + + remote_thread = CreateRemoteThread( + pi.hProcess, + NULL, + 0, + (LPTHREAD_START_ROUTINE) LoadLibrary, + remote_addr, + 0, + NULL); + + if (remote_thread == NULL) { + log_error( + "CreateRemoteThread failed: %08x", (unsigned int) GetLastError()); + + goto inject_fail; + } + + WaitForSingleObject(remote_thread, INFINITE); + CloseHandle(remote_thread); + + ok = VirtualFreeEx(pi.hProcess, remote_addr, 0, MEM_RELEASE); + remote_addr = NULL; + + if (!ok) { + log_error("VirtualFreeEx failed: %08x", (unsigned int) GetLastError()); + } + + return true; + +inject_fail: +write_fail: + if (remote_addr != NULL) { + VirtualFreeEx(pi.hProcess, remote_addr, 0, MEM_RELEASE); + } + +alloc_fail: + return false; +} + +bool debugger_resume_process() +{ + log_info("Resuming remote process..."); + + if (ResumeThread(pi.hThread) == -1) { + log_error( + "Error resuming remote process: %08x", + (unsigned int) GetLastError()); + return false; + } + + CloseHandle(pi.hThread); + + return true; +} + +void debugger_wait_process_exit() +{ + log_misc("Waiting for remote process to exit..."); + + // Wait for the process as we might have a remote debugger attached, so our + // debugger thread exits after creating the process + WaitForSingleObject(pi.hProcess, INFINITE); + + // When the process exits, the debugger gets notified and the thread ends + WaitForSingleObject(debugger_thread_handle, INFINITE); + + log_misc("Remote process exit'd"); +} + +void debugger_finit(bool failure) +{ + log_misc("Debugger finit"); + + if (failure) { + TerminateProcess(pi.hProcess, EXIT_FAILURE); + WaitForSingleObject(debugger_thread_handle, INFINITE); + } + + CloseHandle(debugger_thread_handle); + CloseHandle(debugger_ready_event); + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); +} \ No newline at end of file diff --git a/src/main/inject/debugger.h b/src/main/inject/debugger.h new file mode 100644 index 0000000..abae20c --- /dev/null +++ b/src/main/inject/debugger.h @@ -0,0 +1,85 @@ +#pragma once + +#include + + +/** + * Initialize inject's logger backend. + * + * This takes care of hooking and merging the different log + * streams, e.g. inject's local logging and inject's debugger + * receiving remote logging events. + * + * @param log_file_path Path to the file to log to or NULL to + * disable. + */ + + +/** + * Initialize the debugger. + * + * This creates the remote process of the provided application. + * + * The remote process is created suspended. This allows you to + * to some more process setup tasks like injecting hook DLLs + * before you call debugger_resume_process to actually start + * execution of it. + * + * The actual debugging runs in a dedicated thread which spawns + * the process, waits for and dispatches debug events. + * + * However, if you want to attach a remote debugger, you have to + * set the parameter local_debugger to false. Then, the debugger + * will only create the remote process and monitor it. + * + * @param local_debugger True to attach inject's local debugger, + * false to allow attaching a remote + * debugger with enhanced features. + * @param app_name Name of the application to spawn and debug. + * @param cmd_line Command line string to pass to application. + * @return true on success, false on error. On error, no remote + * application and local debugger is started. + */ +bool debugger_init(bool local_debugger, const char *app_name, char *cmd_line); + +/** + * Inject a DLL into the remote process. + * + * @param path_dll Path to the dll to inject. + * @return true if sucessful, false on error. + */ +bool debugger_inject_dll(const char *path_dll); + +/** + * Wait/block for a remote debugger to attach to the remote process. + * + * You only need to call this if you specified local_debugger = false + * on debugger_init. + * + * @return True if successfull and a remote debugger attached, false + * on error. + */ +bool debugger_wait_for_remote_debugger(); + +/** + * Resume the remote process. + * + * Make sure to call this once you are done with setting it up. + * + * @return true on success, false on error. + */ +bool debugger_resume_process(); + +/** + * Wait for the remote process to exit. + */ +void debugger_wait_process_exit(); + +/** + * Cleanup the debugger. + * + * @param failure Set this to true if you have to cleanup due to + * a failure of another debugger function call. + * Otherwise, set this to false. + */ +void debugger_finit(bool failure); \ No newline at end of file diff --git a/src/main/inject/logger.c b/src/main/inject/logger.c new file mode 100644 index 0000000..66af67b --- /dev/null +++ b/src/main/inject/logger.c @@ -0,0 +1,194 @@ +#define LOG_MODULE "inject-logger" + +#include +#include +#include +#include +#include + +#include "inject/logger.h" +#include "inject/version.h" + +#include "util/log.h" + +static FILE *log_file; +static HANDLE log_mutex; + +static char logger_console_determine_color(const char *str) +{ + log_assert(str); + + /* Add some color to make spotting warnings/errors easier. + Based on debug output level identifier. */ + + /* Avoids colored output on strings like "Windows" */ + if (str[1] != ':') { + return 15; + } + + switch (str[0]) { + /* green */ + case 'M': + return 10; + /* blue */ + case 'I': + return 9; + /* yellow */ + case 'W': + return 14; + /* red */ + case 'F': + return 12; + /* default console color */ + default: + return 15; + } +} + +static size_t logger_msg_coloring_len(const char *str) +{ + // Expected format example: "I:boot: my log message" + + const char *ptr; + size_t len; + int colon_count; + + ptr = str; + len = 0; + colon_count = 0; + + while (true) { + // End of string = invalid log format + if (*ptr == '\0') { + return 0; + } + + if (*ptr == ':') { + colon_count++; + } + + if (colon_count == 2) { + // Skip current colon, next char is a space + return len + 1; + } + + len++; + ptr++; + } + + return 0; +} + +static void logger_console(void *ctx, const char *chars, size_t nchars) +{ + char color; + size_t color_len; + // See "util/log.c", has to align + char buffer[65536]; + char tmp; + + color_len = logger_msg_coloring_len(chars); + + // Check if we could detect which part to color, otherwise just write the + // whole log message without any coloring logic + if (color_len > 0) { + color = logger_console_determine_color(chars); + + strcpy(buffer, chars); + + // Mask start of log message for coloring + tmp = buffer[color_len]; + buffer[color_len] = '\0'; + + SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color); + printf("%s", buffer); + SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 15); + + // Write actual message non colored + buffer[color_len] = tmp; + printf("%s", buffer + color_len); + } else { + printf("%s", chars); + } +} + +static void logger_file(void *ctx, const char *chars, size_t nchars) +{ + if (ctx) { + fwrite(chars, 1, nchars, (FILE *) ctx); + fflush((FILE *) ctx); + } +} + +static void logger_writer(void *ctx, const char *chars, size_t nchars) +{ + // Different threads logging the same destination, e.g. debugger thread, + // main thread + + WaitForSingleObject(log_mutex, INFINITE); + + logger_console(ctx, chars, nchars); + logger_file(ctx, chars, nchars); + + ReleaseMutex(log_mutex); +} + +static void logger_log_header() +{ + log_info( + "\n" + " _ _ _ \n" + " (_)_ __ (_) ___ ___| |_ \n" + " | | '_ \\ | |/ _ \\/ __| __|\n" + " | | | | || | __/ (__| |_ \n" + " |_|_| |_|/ |\\___|\\___|\\__|\n" + " |__/ "); + + log_info( + "Inject build date %s, gitrev %s", inject_build_date, inject_gitrev); +} + +bool logger_init(const char *log_file_path) +{ + if (log_file_path) { + log_file = fopen(log_file_path, "w+"); + } else { + log_file = NULL; + } + + log_to_writer(logger_writer, log_file); + + logger_log_header(); + + if (log_file_path) { + log_info("Log file: %s", log_file_path); + + if (!log_file) { + log_error( + "Opening log file %s failed: %s", + log_file_path, + strerror(errno)); + return false; + } + } + + log_mutex = CreateMutex(NULL, FALSE, NULL); + + return true; +} + +void logger_log(const char *str) +{ + logger_writer(log_file, str, strlen(str)); +} + +void logger_finit() +{ + log_misc("Logger finit"); + + if (log_file) { + fclose(log_file); + } + + CloseHandle(log_mutex); +} \ No newline at end of file diff --git a/src/main/inject/logger.h b/src/main/inject/logger.h new file mode 100644 index 0000000..5702881 --- /dev/null +++ b/src/main/inject/logger.h @@ -0,0 +1,28 @@ +#include + +/** + * Initialize inject's logger backend. + * + * This takes care of hooking and merging the different log + * streams, e.g. inject's local logging and inject's debugger + * receiving remote logging events. + * + * @param log_file_path Path to the file to log to or NULL to + * disable. + */ +bool logger_init(const char *log_file_path); + +/** + * Write a message to the logging backend. + * + * This is used by inject's debugger to redirect log messages + * recevied from the remote process. + * + * @param str String to log + */ +void logger_log(const char *str); + +/** + * Shutdown and cleanup the logging backend. + */ +void logger_finit(); \ No newline at end of file diff --git a/src/main/inject/main.c b/src/main/inject/main.c index 1a03059..c057493 100644 --- a/src/main/inject/main.c +++ b/src/main/inject/main.c @@ -10,472 +10,185 @@ #include "cconfig/cconfig-util.h" #include "cconfig/cmd.h" +#include "inject/debugger.h" +#include "inject/logger.h" #include "inject/options.h" +#include "inject/version.h" #include "util/cmdline.h" +#include "util/log.h" #include "util/mem.h" +#include "util/signal.h" #include "util/str.h" -static FILE *log_file = NULL; - -static bool inject_dll(PROCESS_INFORMATION pi, const char *arg_dll); -static bool debug(HANDLE process, uint32_t pid); -static bool debug_wstr(HANDLE process, const OUTPUT_DEBUG_STRING_INFO *odsi); -static bool debug_str(HANDLE process, const OUTPUT_DEBUG_STRING_INFO *odsi); - -int main(int argc, char **argv) +static bool init_options(int argc, char **argv, struct options *options) { - struct options options; - char *cmd_line; - char dll_path[MAX_PATH]; - DWORD dll_path_length; - PROCESS_INFORMATION pi; - STARTUPINFO si; - BOOL ok; - BOOL debug_ok; - int hooks; - int exec_arg_pos; + options_init(options); - options_init(&options); - - if (argc < 3 || !options_read_cmdline(&options, argc, argv)) { + if (argc < 3 || !options_read_cmdline(options, argc, argv)) { options_print_usage(); - goto usage_fail; + return false; } - /* Open log file for logging if parameter specified */ + return true; +} - if (strlen(options.log_file) > 0) { - log_file = fopen(options.log_file, "w+"); +static bool verify_hook_dll_and_exec_args_and_count_hooks( + int argc, char **argv, uint32_t *hooks, uint32_t *exec_arg_pos) +{ + log_assert(argc >= 0); + log_assert(argv); + log_assert(hooks); + log_assert(exec_arg_pos); - if (!log_file) { - fprintf( - stderr, - "Opening log file %s failed: %s\n", - options.log_file, - strerror(errno)); - goto log_file_open_fail; - } - - printf("Log file: %s\n", options.log_file); - } - - /* Count hook dlls */ - hooks = 0; - exec_arg_pos = 0; + *hooks = 0; + *exec_arg_pos = 0; for (int i = 1; i < argc; i++) { if (str_ends_with(argv[i], "dll")) { - hooks++; + (*hooks)++; } else if (str_ends_with(argv[i], "exe")) { - exec_arg_pos = i; + *exec_arg_pos = i; break; } } - if (!hooks) { - fprintf(stderr, "No Hook DLL(s) specified before executable\n"); - - goto hook_count_fail; + if (!(*hooks)) { + log_error("No Hook DLL(s) specified before executable"); + return false; } - if (!exec_arg_pos) { - fprintf(stderr, "No executable specified\n"); - - goto find_exec_fail; + if (!*exec_arg_pos) { + log_error("No executable specified"); + return false; } - for (int i = 0; i < hooks; i++) { + log_misc("%d hook(s) dll detected", *hooks); + log_misc("Executable: %s", argv[*exec_arg_pos]); + + return true; +} + +static bool +verify_hook_dlls_exist(int argc, char **argv, uint32_t hook_dll_count) +{ + log_assert(argc >= 0); + log_assert(argv); + + char dll_path[MAX_PATH]; + DWORD dll_path_length; + + for (uint32_t i = 0; i < hook_dll_count; i++) { dll_path_length = SearchPath(NULL, argv[i + 1], NULL, MAX_PATH, dll_path, NULL); if (dll_path_length == 0) { - fprintf( - stderr, - "Hook DLL not found: %08x\n", - (unsigned int) GetLastError()); + log_error( + "Hook DLL not found: %08x", (unsigned int) GetLastError()); - goto search_fail; + return false; } } - memset(&si, 0, sizeof(si)); - si.cb = sizeof(si); - - cmd_line = args_join(argc - exec_arg_pos, argv + exec_arg_pos); - - ok = CreateProcess( - argv[exec_arg_pos], - cmd_line, - NULL, - NULL, - FALSE, - CREATE_SUSPENDED, - NULL, - NULL, - &si, - &pi); - - if (!ok) { - fprintf( - stderr, - "Failed to launch hooked EXE: %08x\n", - (unsigned int) GetLastError()); - - goto start_fail; - } - - free(cmd_line); - cmd_line = NULL; - - for (int i = 0; i < hooks; i++) { - if (!inject_dll(pi, argv[i + 1])) { - goto inject_fail; - } - } - - debug_ok = false; - - if (options.debug && !options.remote_debugger) { - debug_ok = DebugActiveProcess(pi.dwProcessId); - - if (!debug_ok) { - fprintf( - stderr, - "DebugActiveProcess failed: %08x\n", - (unsigned int) GetLastError()); - } else { - printf("Debug active process\n"); - } - } - - if (options.remote_debugger) { - printf("Waiting until debugger attaches to remote process...\n"); - - while (true) { - BOOL res = FALSE; - - if (!CheckRemoteDebuggerPresent(pi.hProcess, &res)) { - fprintf( - stderr, - "CheckRemoteDebuggerPresent failed: %08x\n", - (unsigned int) GetLastError()); - } - - if (res) { - printf("Debugger attached, resuming\n"); - break; - } - - Sleep(1000); - } - } - - printf("Resuming remote process...\n"); - - if (ResumeThread(pi.hThread) == -1) { - fprintf( - stderr, - "Error restarting hooked process: %08x\n", - (unsigned int) GetLastError()); - - goto restart_fail; - } - - CloseHandle(pi.hThread); - - if (options.debug) { - if (!debug_ok || !debug(pi.hProcess, pi.dwProcessId)) { - WaitForSingleObject(pi.hProcess, INFINITE); - } - } - - CloseHandle(pi.hProcess); - - return EXIT_SUCCESS; - -inject_fail: -restart_fail: - TerminateProcess(pi.hProcess, EXIT_FAILURE); - - CloseHandle(pi.hThread); - CloseHandle(pi.hProcess); - -start_fail: - if (cmd_line != NULL) { - free(cmd_line); - } - -hook_count_fail: -find_exec_fail: -search_fail: - if (log_file) { - fclose(log_file); - } - -log_file_open_fail: -usage_fail: - - return EXIT_FAILURE; -} - -static bool inject_dll(PROCESS_INFORMATION pi, const char *arg_dll) -{ - char dll_path[MAX_PATH]; - DWORD dll_path_length; - void *remote_addr; - BOOL ok; - HANDLE remote_thread; - - printf("Injecting: %s\n", arg_dll); - - dll_path_length = SearchPath(NULL, arg_dll, NULL, MAX_PATH, dll_path, NULL); - - dll_path_length++; - - remote_addr = VirtualAllocEx( - pi.hProcess, - NULL, - dll_path_length, - MEM_RESERVE | MEM_COMMIT, - PAGE_READWRITE); - - if (!remote_addr) { - fprintf( - stderr, - "VirtualAllocEx failed: %08x\n", - (unsigned int) GetLastError()); - - goto alloc_fail; - } - - ok = WriteProcessMemory( - pi.hProcess, remote_addr, dll_path, dll_path_length, NULL); - - if (!ok) { - fprintf( - stderr, - "WriteProcessMemory failed: %08x\n", - (unsigned int) GetLastError()); - - goto write_fail; - } - - remote_thread = CreateRemoteThread( - pi.hProcess, - NULL, - 0, - (LPTHREAD_START_ROUTINE) LoadLibrary, - remote_addr, - 0, - NULL); - - if (remote_thread == NULL) { - fprintf( - stderr, - "CreateRemoteThread failed: %08x\n", - (unsigned int) GetLastError()); - - goto inject_fail; - } - - WaitForSingleObject(remote_thread, INFINITE); - CloseHandle(remote_thread); - - ok = VirtualFreeEx(pi.hProcess, remote_addr, 0, MEM_RELEASE); - remote_addr = NULL; - - if (!ok) { - fprintf( - stderr, - "VirtualFreeEx failed: %08x\n", - (unsigned int) GetLastError()); - } - return true; - -inject_fail: -write_fail: - if (remote_addr != NULL) { - VirtualFreeEx(pi.hProcess, remote_addr, 0, MEM_RELEASE); - } - -alloc_fail: - return false; } -static bool debug(HANDLE process, uint32_t pid) +static bool inject_hook_dlls(uint32_t hooks, char **argv) { - DEBUG_EVENT de; - BOOL ok; + log_assert(argv); - for (;;) { - ok = WaitForDebugEvent(&de, INFINITE); - - if (!ok) { - fprintf( - stderr, - "WaitForDebugEvent failed: %08x\n", - (unsigned int) GetLastError()); - - return false; - } - - switch (de.dwDebugEventCode) { - case CREATE_PROCESS_DEBUG_EVENT: - CloseHandle(de.u.CreateProcessInfo.hFile); - - break; - - case EXIT_PROCESS_DEBUG_EVENT: - if (de.dwProcessId == pid) { - return true; - } - - break; - - case LOAD_DLL_DEBUG_EVENT: - CloseHandle(de.u.LoadDll.hFile); - - break; - - case OUTPUT_DEBUG_STRING_EVENT: - if (de.dwProcessId == pid) { - if (de.u.DebugString.fUnicode) { - if (!debug_wstr(process, &de.u.DebugString)) { - return false; - } - } else { - if (!debug_str(process, &de.u.DebugString)) { - return false; - } - } - } - - break; - } - - if (de.dwDebugEventCode == OUTPUT_DEBUG_STRING_EVENT) { - ok = - ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE); - } else { - ok = ContinueDebugEvent( - de.dwProcessId, de.dwThreadId, DBG_EXCEPTION_NOT_HANDLED); - } - - if (!ok) { - fprintf( - stderr, - "ContinueDebugEvent failed: %08x\n", - (unsigned int) GetLastError()); + log_info("Injecting hook DLLs..."); + for (int i = 0; i < hooks; i++) { + if (!debugger_inject_dll(argv[i + 1])) { return false; } } + + return true; } -static char console_get_color(char *str) +static void signal_shutdown_handler() { - /* Add some color to make spotting warnings/errors easier. - Based on debug output level identifier. */ - - /* Avoids colored output on strings like "Windows" */ - if (str[1] != ':') { - return 15; - } - - switch (str[0]) { - /* green */ - case 'M': - return 10; - /* blue */ - case 'I': - return 9; - /* yellow */ - case 'W': - return 14; - /* red */ - case 'F': - return 12; - /* default console color */ - default: - return 15; - } + debugger_finit(true); + logger_finit(); } -static bool debug_wstr(HANDLE process, const OUTPUT_DEBUG_STRING_INFO *odsi) +int main(int argc, char **argv) { - char *str; - wchar_t *wstr; - uint32_t nbytes; - BOOL ok; + struct options options; + uint32_t hooks; + uint32_t exec_arg_pos; + char *cmd_line; + bool local_debugger; - nbytes = odsi->nDebugStringLength * sizeof(wchar_t); - wstr = xmalloc(nbytes); + if (!init_options(argc, argv, &options)) { + goto init_options_fail; + } - ok = - ReadProcessMemory(process, odsi->lpDebugStringData, wstr, nbytes, NULL); + if (!logger_init(strlen(options.log_file) > 0 ? options.log_file : NULL)) { + goto init_logger_fail; + } - if (ok) { - if (wstr_narrow(wstr, &str)) { - str[odsi->nDebugStringLength - 1] = '\0'; + signal_exception_handler_init(); + // Cleanup remote process on CTRL+C + signal_register_shutdown_handler(signal_shutdown_handler); - SetConsoleTextAttribute( - GetStdHandle(STD_OUTPUT_HANDLE), console_get_color(str)); - printf("%s", str); - SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 15); + if (!verify_hook_dll_and_exec_args_and_count_hooks( + argc, argv, &hooks, &exec_arg_pos)) { + goto verify_fail; + } - if (log_file) { - fprintf(log_file, "%s", str); - } + if (!verify_hook_dlls_exist(argc, argv, hooks)) { + goto verify_2_fail; + } - free(str); - } else { - fprintf(stderr, "OutputDebugStringW: UTF-16 conversion failed\n"); + // buffer consumed by debugger_init + cmd_line = args_join(argc - exec_arg_pos, argv + exec_arg_pos); + + local_debugger = options.debug && !options.remote_debugger; + + if (!debugger_init(local_debugger, argv[exec_arg_pos], cmd_line)) { + goto debugger_init_fail; + } + + if (!inject_hook_dlls(hooks, argv)) { + goto inject_hook_dlls_fail; + } + + // Execute this after injecting the DLLs. Some debuggers seem to crash if we + // attach the process before DLL injection (inject's local one doesn't + // crash). However, this means the remote debugger is missing out on all + // injected DLL loads, e.g. calls to DllMain + if (options.remote_debugger) { + if (!debugger_wait_for_remote_debugger()) { + goto debugger_wait_for_remote_debugger_fail; } - } else { - fprintf( - stderr, - "ReadProcessMemory failed: %08x\n", - (unsigned int) GetLastError()); - - return false; } - free(wstr); - - return (bool) ok; -} - -static bool debug_str(HANDLE process, const OUTPUT_DEBUG_STRING_INFO *odsi) -{ - char *str; - BOOL ok; - - str = xmalloc(odsi->nDebugStringLength); - - ok = ReadProcessMemory( - process, odsi->lpDebugStringData, str, odsi->nDebugStringLength, NULL); - - if (ok) { - str[odsi->nDebugStringLength - 1] = '\0'; - - SetConsoleTextAttribute( - GetStdHandle(STD_OUTPUT_HANDLE), console_get_color(str)); - printf("%s", str); - SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 15); - - if (log_file) { - fprintf(log_file, "%s", str); - } - } else { - fprintf( - stderr, - "ReadProcessMemory failed: %08x\n", - (unsigned int) GetLastError()); + if (!debugger_resume_process()) { + goto debugger_resume_process_fail; } - free(str); + debugger_wait_process_exit(); - return (bool) ok; + debugger_finit(false); + + logger_finit(); + + return EXIT_SUCCESS; + +debugger_resume_process_fail: +debugger_wait_for_remote_debugger_fail: +inject_hook_dlls_fail: + debugger_finit(true); + +debugger_init_fail: +verify_2_fail: +verify_fail: + logger_finit(); + +init_logger_fail: +init_options_fail: + return EXIT_FAILURE; } diff --git a/src/main/inject/options.c b/src/main/inject/options.c index 39d7ebf..643e6ce 100644 --- a/src/main/inject/options.c +++ b/src/main/inject/options.c @@ -48,15 +48,15 @@ bool options_read_cmdline(struct options *options, int argc, char **argv) void options_print_usage(void) { fprintf(stderr, - "inject build " __DATE__ " " __TIME__ - ", gitrev " STRINGIFY(GITREV) "\n" - "Usage: inject hook.dll... app.exe [hooks options...]\n" - "You can specify one or multiple hook.dll files, e.g. inject.exe " - "hook1.dll hook2.dll app.exe" - "\n" - " The following options can be specified after the exe path:\n" - "\n" - " -D Enable debugging output\n" - " -R Halt the injected process until a debugger is attached\n" - " -Y [filename] Log to a file in addition to the console\n"); + "inject build " __DATE__ " " __TIME__ + ", gitrev " STRINGIFY(GITREV) "\n" + "Usage: inject hook.dll... app.exe [hooks options...]\n" + "You can specify one or multiple hook.dll files, e.g. inject.exe " + "hook1.dll hook2.dll app.exe" + "\n" + " The following options can be specified after the exe path:\n" + "\n" + " -D Enable debugging output\n" + " -R Halt the injected process until a debugger is attached\n" + " -Y [filename] Log to a file in addition to the console\n"); } diff --git a/src/main/inject/version.c b/src/main/inject/version.c new file mode 100644 index 0000000..589c0f3 --- /dev/null +++ b/src/main/inject/version.c @@ -0,0 +1,4 @@ +#include "util/defs.h" + +const char *inject_build_date = __DATE__ " " __TIME__; +const char *inject_gitrev = STRINGIFY(GITREV); \ No newline at end of file diff --git a/src/main/inject/version.h b/src/main/inject/version.h new file mode 100644 index 0000000..38b3894 --- /dev/null +++ b/src/main/inject/version.h @@ -0,0 +1,4 @@ +#pragma once + +extern const char *inject_build_date; +extern const char *inject_gitrev; \ No newline at end of file