From 71bbe49615e9ef80614985958728f88b4f6d47d1 Mon Sep 17 00:00:00 2001 From: Decaf Code Date: Sun, 7 Oct 2018 19:47:23 -0400 Subject: [PATCH] inject: Add initial DLL injector tool --- inject/debug.c | 181 ++++++++++++++++++++++++++++++++ inject/debug.h | 7 ++ inject/main.c | 250 +++++++++++++++++++++++++++++++++++++++++++++ inject/meson.build | 12 +++ inject/options.c | 193 ++++++++++++++++++++++++++++++++++ inject/options.h | 21 ++++ meson.build | 1 + 7 files changed, 665 insertions(+) create mode 100644 inject/debug.c create mode 100644 inject/debug.h create mode 100644 inject/main.c create mode 100644 inject/meson.build create mode 100644 inject/options.c create mode 100644 inject/options.h diff --git a/inject/debug.c b/inject/debug.c new file mode 100644 index 0000000..3cb0e93 --- /dev/null +++ b/inject/debug.c @@ -0,0 +1,181 @@ +#include + +#include +#include +#include +#include + +static HRESULT debug_wstr(HANDLE process, const OUTPUT_DEBUG_STRING_INFO *odsi); +static bool debug_str(HANDLE process, const OUTPUT_DEBUG_STRING_INFO *odsi); + +HRESULT debug_main(HANDLE process, uint32_t pid) +{ + DEBUG_EVENT ev; + DWORD status; + HRESULT hr; + BOOL ok; + + for (;;) { + ok = WaitForDebugEvent(&ev, INFINITE); + + if (!ok) { + hr = HRESULT_FROM_WIN32(GetLastError()); + fprintf(stderr, "WaitForDebugEvent failed: %x\n", (int) hr); + + return hr; + } + + switch (ev.dwDebugEventCode) { + case CREATE_PROCESS_DEBUG_EVENT: + CloseHandle(ev.u.CreateProcessInfo.hFile); + + break; + + case EXIT_PROCESS_DEBUG_EVENT: + if (ev.dwProcessId == pid) { + return S_OK; + } + + break; + + case LOAD_DLL_DEBUG_EVENT: + CloseHandle(ev.u.LoadDll.hFile); + + break; + + case OUTPUT_DEBUG_STRING_EVENT: + if (ev.dwProcessId == pid) { + if (ev.u.DebugString.fUnicode) { + hr = debug_wstr(process, &ev.u.DebugString); + } else { + hr = debug_str(process, &ev.u.DebugString); + } + + if (FAILED(hr)) { + return hr; + } + } + + break; + } + + if (ev.dwDebugEventCode == OUTPUT_DEBUG_STRING_EVENT) { + status = DBG_CONTINUE; + } else { + status = DBG_EXCEPTION_NOT_HANDLED; + } + + ok = ContinueDebugEvent(ev.dwProcessId, ev.dwThreadId, status); + + if (!ok) { + hr = HRESULT_FROM_WIN32(GetLastError()); + fprintf(stderr, "ContinueDebugEvent failed: %x\n", (int) hr); + + return hr; + } + } +} + +static HRESULT debug_wstr(HANDLE process, const OUTPUT_DEBUG_STRING_INFO *odsi) +{ + char *str; + wchar_t *wstr; + int nbytes_w; + int nbytes_a; + int result; + HRESULT hr; + BOOL ok; + + str = NULL; + nbytes_w = odsi->nDebugStringLength * sizeof(wchar_t); + wstr = malloc(nbytes_w); + + if (wstr == NULL) { + hr = E_OUTOFMEMORY; + + goto end; + } + + ok = ReadProcessMemory( + process, + odsi->lpDebugStringData, + wstr, + nbytes_w, + NULL); + + if (!ok) { + hr = HRESULT_FROM_WIN32(GetLastError()); + fprintf(stderr, + "%s: ReadProcessMemory failed: %x\n", + __func__, + (int) hr); + + goto end; + } + + nbytes_a = WideCharToMultiByte(CP_ACP, 0, wstr, -1, NULL, 0, NULL, NULL); + str = malloc(nbytes_a); + + if (str == NULL) { + hr = E_OUTOFMEMORY; + + goto end; + } + + result = WideCharToMultiByte(CP_ACP, 0, wstr, -1, str, nbytes_a, NULL,NULL); + + if (result == 0) { + hr = HRESULT_FROM_WIN32(GetLastError()); + fprintf(stderr, "WideCharToMultiByte failed: %x\n", (int) hr); + + goto end; + } + + fputs(str, stdout); + + hr = S_OK; + +end: + free(str); + free(wstr); + + return hr; +} + +static bool debug_str(HANDLE process, const OUTPUT_DEBUG_STRING_INFO *odsi) +{ + char *str; + HRESULT hr; + BOOL ok; + + str = malloc(odsi->nDebugStringLength); + + if (str == NULL) { + hr = E_OUTOFMEMORY; + + goto end; + } + + ok = ReadProcessMemory( + process, + odsi->lpDebugStringData, + str, + odsi->nDebugStringLength, + NULL); + + if (!ok) { + hr = HRESULT_FROM_WIN32(GetLastError()); + fprintf(stderr, "%s: ReadProcessMemory failed: %x", __func__, (int) hr); + + goto end; + } + + fputs(str, stdout); + + hr = S_OK; + +end: + free(str); + + return hr; +} diff --git a/inject/debug.h b/inject/debug.h new file mode 100644 index 0000000..0b66437 --- /dev/null +++ b/inject/debug.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include + +HRESULT debug_main(HANDLE process, uint32_t pid); diff --git a/inject/main.c b/inject/main.c new file mode 100644 index 0000000..6025d1b --- /dev/null +++ b/inject/main.c @@ -0,0 +1,250 @@ +#include + +#include +#include +#include +#include + +#include "inject/debug.h" +#include "inject/options.h" + +static HRESULT inject_dll(HANDLE process, const char *dll_name); +static HRESULT inject_pause(HANDLE process); +static HRESULT inject_resume(HANDLE thread); + +int main(int argc, char **argv) +{ + struct options opt; + char *cmdline; + const char *hook_dll; + PROCESS_INFORMATION pi; + STARTUPINFO si; + HRESULT hr; + BOOL ok; + + hr = options_init(&opt, argc, argv); + + if (FAILED(hr) || opt.help) { + options_help(stderr); + + return EXIT_FAILURE; + } + + cmdline = NULL; + hr = options_target_cmdline(&opt, &cmdline); + + if (FAILED(hr)) { + goto end; + } + + memset(&pi, 0, sizeof(pi)); + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + + ok = CreateProcessA( + NULL, + cmdline, + NULL, + NULL, + FALSE, + CREATE_SUSPENDED, + NULL, + NULL, + &si, + &pi); + + if (!ok) { + hr = HRESULT_FROM_WIN32(GetLastError()); + fprintf(stderr, "Failed to launch executable: %x\n", (int) hr); + + goto end; + } + + while (options_next_dll(&opt, &hook_dll) == S_OK) { + hr = inject_dll(pi.hProcess, hook_dll); + + if (FAILED(hr)) { + goto end; + } + } + + if (opt.debug_pause) { + hr = inject_pause(pi.hProcess); + + if (FAILED(hr)) { + goto end; + } + } + + if (opt.debug) { + ok = DebugActiveProcess(pi.dwProcessId); + + if (!ok) { + hr = HRESULT_FROM_WIN32(GetLastError()); + fprintf(stderr, "DebugActiveProcess failed: %x\n", (int) hr); + + goto end; + } + } + + hr = inject_resume(pi.hThread); + + if (FAILED(hr)) { + goto end; + } + + if (opt.debug) { + hr = debug_main(pi.hProcess, pi.dwProcessId); + } + + if (opt.wait) { + WaitForSingleObject(pi.hProcess, INFINITE); + } + +end: + if (pi.hProcess != NULL) { + if (FAILED(hr)) { + TerminateProcess(pi.hProcess, EXIT_FAILURE); + } + + CloseHandle(pi.hProcess); + } + + if (pi.hThread != NULL) { + CloseHandle(pi.hThread); + } + + free(cmdline); + + return FAILED(hr) ? EXIT_FAILURE : EXIT_SUCCESS; +} + +static HRESULT inject_dll(HANDLE process, const char *dll_name) +{ + size_t nchars; + void *remote_addr; + HANDLE remote_thread; + DWORD found; + HRESULT hr; + BOOL ok; + + remote_addr = NULL; + remote_thread = NULL; + + found = SearchPathA(NULL, dll_name, NULL, 0, NULL, NULL); + + if (found == 0) { + hr = HRESULT_FROM_WIN32(GetLastError()); + fprintf(stderr, "%s: Hook DLL not found: %x\n", dll_name, (int) hr); + + goto end; + } + + nchars = strlen(dll_name); + + remote_addr = VirtualAllocEx( + process, + NULL, + nchars + 1, + MEM_RESERVE | MEM_COMMIT, + PAGE_READWRITE); + + if (remote_addr == NULL) { + hr = HRESULT_FROM_WIN32(GetLastError()); + fprintf(stderr, "VirtualAllocEx failed: %x\n", (int) hr); + + goto end; + } + + ok = WriteProcessMemory( + process, + remote_addr, + dll_name, + nchars + 1, + NULL); + + if (!ok) { + hr = HRESULT_FROM_WIN32(GetLastError()); + fprintf(stderr, "WriteProcessMemory failed: %x\n", (int) hr); + + goto end; + } + + remote_thread = CreateRemoteThread( + process, + NULL, + 0, + (LPTHREAD_START_ROUTINE) LoadLibraryA, + remote_addr, + 0, + NULL); + + if (remote_thread == NULL) { + hr = HRESULT_FROM_WIN32(GetLastError()); + fprintf(stderr, "CreateRemoteThread failed: %x\n", (int) hr); + + goto end; + } + + hr = S_OK; + +end: + if (remote_thread != NULL) { + WaitForSingleObject(remote_thread, INFINITE); + CloseHandle(remote_thread); + } + + if (remote_addr != NULL) { + ok = VirtualFreeEx(process, remote_addr, 0, MEM_RELEASE); + + if (!ok) { + fprintf(stderr, "VirtualFreeEx failed\n"); + } + } + + return hr; +} + +static HRESULT inject_pause(HANDLE process) +{ + HRESULT hr; + BOOL present; + BOOL ok; + + printf("Waiting for debugger to attach.\n"); + + do { + Sleep(1000); + ok = CheckRemoteDebuggerPresent(process, &present); + + if (!ok) { + hr = HRESULT_FROM_WIN32(GetLastError()); + fprintf(stderr, "CheckRemoteDebuggerPresent failed: %x\n", (int)hr); + + return hr; + } + } while (!present); + + printf("Debugger attached, resuming\n"); + + return S_OK; +} + +static HRESULT inject_resume(HANDLE thread) +{ + DWORD result; + HRESULT hr; + + result = ResumeThread(thread); + + if (result == -1) { + hr = HRESULT_FROM_WIN32(GetLastError()); + fprintf(stderr, "Failed to resume target thread: %x\n", (int) hr); + + return hr; + } + + return S_OK; +} + + diff --git a/inject/meson.build b/inject/meson.build new file mode 100644 index 0000000..65608b4 --- /dev/null +++ b/inject/meson.build @@ -0,0 +1,12 @@ +executable( + 'inject', + include_directories : inc, + c_pch : '../precompiled.h', + sources : [ + 'debug.c', + 'debug.h', + 'main.c', + 'options.c', + 'options.h', + ], +) diff --git a/inject/options.c b/inject/options.c new file mode 100644 index 0000000..2787251 --- /dev/null +++ b/inject/options.c @@ -0,0 +1,193 @@ +#include +#include +#include +#include +#include + +#include "inject/options.h" + +void options_help(FILE *f) +{ + assert(f != NULL); + + fputs( "Usage: inject [options] program args...\n" + "All options must precede the program name.\n" + "\n" + "The following options are understood:\n" + "\n" + "-h\tPrint this message.\n" + "\n" + "-d\tAttach to target as a debugger and print debug messages.\n" + "\n" + "-p\tPause the target until a debugger attaches to it.\n" + " \tCannot be used with -d.\n" + "\n" + "-w\tWait for target to terminate.\n" + " \tCannot be used with -d.\n" + "\n" + "-k dll\tInject the named DLL into the target process.\n" + " \tCan be specified more than once.\n" + "\n", + f); +} + +HRESULT options_init(struct options *opt, int argc, char **argv) +{ + int nconsumed; + const char *arg; + int i; + + assert(opt != NULL); + + memset(opt, 0, sizeof(*opt)); + nconsumed = 1; + + for (i = 1 ; i < argc && argv[i][0] == '-' ; i++) { + arg = argv[i]; + nconsumed++; + + switch (arg[1]) { + case 'h': + opt->help = true; + + break; + + case 'd': + if (opt->debug_pause || opt->wait) { + return E_FAIL; + } + + opt->debug = true; + + break; + + case 'p': + if (opt->debug) { + return E_FAIL; + } + + opt->debug_pause = true; + + break; + + case 'w': + if (opt->debug) { + return E_FAIL; + } + + opt->wait = true; + + break; + + case 'k': + if (i + 1 >= argc) { + return E_FAIL; + } + + /* These get pulled by options_next_dll. Consume its argument as + well though. */ + + nconsumed++; + i++; + + break; + + default: + return E_FAIL; + } + } + + if (nconsumed == argc) { + return E_FAIL; + } + + opt->orig_argc = argc; + opt->orig_argv = argv; + opt->target_argc = argc - nconsumed; + opt->target_argv = argv + nconsumed; + opt->dll_pos = 1; + + return S_OK; +} + +HRESULT options_target_cmdline(const struct options *opt, char **out) +{ + char *str; + char *pos; + size_t nchars; + size_t len; + size_t i; + + assert(opt != NULL); + assert(out != NULL); + + *out = NULL; + + /* Measure string. Each element requires an opening quote, a closing quote + and either a trailing space or a trailing NUL. */ + + nchars = 3 * opt->target_argc; + + for (i = 0 ; i < opt->target_argc ; i++) { + nchars += strlen(opt->target_argv[i]); + } + + str = malloc(nchars); + + if (str == NULL) { + return E_OUTOFMEMORY; + } + + /* Construct string. This doesn't escape quotes within individual args yet + but ugh I'll fix that later if it really becomes necessary, it's a pain + to deal with. */ + + for (i = 0, pos = str ; i < opt->target_argc ; i++) { + len = strlen(opt->target_argv[i]); + + *pos++ = '"'; + memcpy(pos, opt->target_argv[i], len); + pos += len; + *pos++ = '"'; + + if (i + 1 < opt->target_argc) { + *pos++ = ' '; + } else { + *pos++ = '\0'; + } + } + + *out = str; + + return S_OK; +} + +HRESULT options_next_dll(struct options *opt, const char **out) +{ + const char *arg; + + assert(opt != NULL); + assert(opt->orig_argv != NULL); + assert(out != NULL); + + *out = NULL; + + while (opt->dll_pos < opt->orig_argc) { + arg = opt->orig_argv[opt->dll_pos]; + + if (arg[0] != '-') { + break; + } + + opt->dll_pos++; + + if (arg[1] == 'k' && opt->dll_pos < opt->orig_argc) { + *out = opt->orig_argv[opt->dll_pos++]; + + return S_OK; + } + } + + return S_FALSE; +} + diff --git a/inject/options.h b/inject/options.h new file mode 100644 index 0000000..d6c17b7 --- /dev/null +++ b/inject/options.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +struct options { + bool help; + bool wait; + bool debug; + bool debug_pause; + int dll_pos; + int orig_argc; + char **orig_argv; + int target_argc; + char **target_argv; +}; + +void options_help(FILE *f); +HRESULT options_init(struct options *opt, int argc, char **argv); +HRESULT options_target_cmdline(const struct options *opt, char **str); +HRESULT options_next_dll(struct options *opt, const char **dll); diff --git a/meson.build b/meson.build index 79f7ad3..29b5511 100644 --- a/meson.build +++ b/meson.build @@ -17,3 +17,4 @@ add_project_link_arguments( inc = include_directories('.') subdir('hook') +subdir('inject')