From bf246bf95baeabde567df6fa4ec38c1af825f9c8 Mon Sep 17 00:00:00 2001 From: Bepis Date: Mon, 16 Aug 2021 23:47:43 +0700 Subject: [PATCH] i#38: Attach injection on Linux (#5019) Enables attach injection on Linux. Adds an -attach option to drrun. Builds on the existing partial ptrace injection support. Handles the problem of attaching when the target was in the middle of an auto-restart syscall, where the kernel will report to the tracer that PC is at the next instruction after syscall, but will set it back to syscall instruction by subtracting PC (PC -= sizeof(syscall)) after continuation. The default handling waits for the syscall to complete by single-stepping. The -skip_syscall drrun option enables an alternate handling which aborts the syscall and returns -EINTR (adjusting the kernel's ERESTARTSYS). This may break the app as an auto-restart syscall is not expected to return, and is only implemented for x86 for now due to lack of testing in arm/aarch64. A regression test will be added separately. Co-authored-by: Yibai Zhang Co-authored-by: Derek Bruening --- core/arch/x86/x86.asm | 6 ++ core/lib/dr_inject.h | 34 ++++++- core/unix/injector.c | 203 +++++++++++++++++++++++++++++++++++++++--- tools/drdeploy.c | 75 +++++++++++++--- 4 files changed, 291 insertions(+), 27 deletions(-) diff --git a/core/arch/x86/x86.asm b/core/arch/x86/x86.asm index f7b7cb1b316..c2e52ec69d1 100644 --- a/core/arch/x86/x86.asm +++ b/core/arch/x86/x86.asm @@ -1138,6 +1138,12 @@ GLOBAL_LABEL(client_int_syscall:) */ DECLARE_FUNC(_start) GLOBAL_LABEL(_start:) + /* i#38: Attaching while in middle of blocking syscall requires padded null bytes + * with number_of_nop_instr = sizeof(syscall_instr) / sizeof(nop_instr). + * For detailed explanation see issue page. + */ + nop + nop /* i#1676, i#1708: relocate dynamorio if it is not loaded to preferred address. * We call this here to ensure it's safe to access globals once in C code * (xref i#1865). diff --git a/core/lib/dr_inject.h b/core/lib/dr_inject.h index 4514a0e7c88..d1168fbab54 100644 --- a/core/lib/dr_inject.h +++ b/core/lib/dr_inject.h @@ -136,6 +136,34 @@ DR_EXPORT int dr_inject_prepare_to_exec(const char *app_name, const char **app_cmdline, void **data); +DR_EXPORT +/** + * Prepare to ptrace(ATTACH) the provided process. Use + * dr_inject_process_inject() to perform the ptrace(ATTACH) under DR. + * + * \note Only available on Linux. + * + * \param[in] pid The pid for the target executable. The caller + * must ensure this data is valid until the + * inject data is disposed. + * + * \param[in] app_name The path to the target executable. The caller + * must ensure this data is valid until the + * inject data is disposed. + * + * \param[in] wait_syscall Syscall handling mode in inject stage. + * If true, will wait for completion of ongoing syscall. + * Else start inject immidiately. + * + * \param[out] data An opaque pointer that should be passed to + * subsequent dr_inject_* routines to refer to + * this process. + * \return Whether successful. + */ +int +dr_inject_prepare_to_attach(process_id_t pid, const char *app_name, bool wait_syscall, + void **data); + DR_EXPORT /** * Use the ptrace system call to inject into the targetted process. Must be @@ -246,9 +274,9 @@ DR_EXPORT * * \param[in] terminate If true, the process is forcibly terminated. * - * \return Returns the exit code of the process. If the caller did not wait - * for the process to finish before calling this, the code will be - * STILL_ACTIVE. + * \return Returns the exit code of the process, always returns 0 for ptraced process. + * If the caller did not wait for the process to finish before calling this, + * the code will be STILL_ACTIVE. */ int dr_inject_process_exit(void *data, bool terminate); diff --git a/core/unix/injector.c b/core/unix/injector.c index 50289b300f5..b520c61dd4c 100644 --- a/core/unix/injector.c +++ b/core/unix/injector.c @@ -78,6 +78,7 @@ #include #include #include +#include #ifdef MACOS # include # include /* _NSGetEnviron() */ @@ -145,6 +146,8 @@ typedef struct _dr_inject_info_t { int exitcode; bool no_emulate_brk; /* is -no_emulate_brk in the option string? */ + bool wait_syscall; /* valid iff -attach, handlle blocking syscalls */ + #ifdef MACOS bool spawn_32bit; #endif @@ -566,6 +569,22 @@ dr_inject_prepare_to_exec(const char *exe, const char **argv, void **data OUT) return errcode; } +DR_EXPORT +int +dr_inject_prepare_to_attach(process_id_t pid, const char *appname, bool wait_syscall, + void **data OUT) +{ + dr_inject_info_t *info = create_inject_info(appname, NULL); + int errcode = 0; + *data = info; + info->pid = pid; + info->pipe_fd = 0; /* No pipe. */ + info->exec_self = false; + info->method = INJECT_PTRACE; + info->wait_syscall = wait_syscall; + return errcode; +} + DR_EXPORT bool dr_inject_prepare_to_ptrace(void *data) @@ -742,7 +761,6 @@ bool dr_inject_wait_for_child(void *data, uint64 timeout_millis) { dr_inject_info_t *info = (dr_inject_info_t *)data; - pid_t res; timeout_expired = false; if (timeout_millis > 0) { @@ -763,12 +781,34 @@ dr_inject_wait_for_child(void *data, uint64 timeout_millis) setitimer(ITIMER_REAL, &timer, NULL); } - do { - res = waitpid(info->pid, &info->exitcode, 0); - } while (res != info->pid && res != -1 && - /* The signal handler sets this and makes waitpid return EINTR. */ - !timeout_expired); - info->exited = (res == info->pid); + if (info->method != INJECT_PTRACE) { + pid_t res; + do { + res = waitpid(info->pid, &info->exitcode, 0); + } while (res != info->pid && res != -1 && + /* The signal handler sets this and makes waitpid return EINTR. */ + !timeout_expired); + info->exited = (res == info->pid); + } else { + bool exit = false; + struct timespec t; + t.tv_sec = 1; + t.tv_nsec = 0L; + do { + /* At this point dr_inject_process_run has called PTRACE_DETACH + * For non-child target, we should poll for its exit. + * There is no standard way of getting non-child target process' exit code. + */ + if (kill(info->pid, 0) == -1) { + if (errno == ESRCH) + exit = true; + } + /* sleep might not be implemented using nanosleep */ + nanosleep(&t, 0); + } while (!exit && !timeout_expired); + info->exitcode = 0; + info->exited = (exit != false); + } return info->exited; } @@ -777,7 +817,7 @@ int dr_inject_process_exit(void *data, bool terminate) { dr_inject_info_t *info = (dr_inject_info_t *)data; - int status; + int status = 0; if (info->exited) { /* If it already exited when we waited on it above, then we *cannot* * wait on it again or try to kill it, or we might target some new @@ -798,15 +838,25 @@ dr_inject_process_exit(void *data, bool terminate) } /* Do a blocking wait to get the real status code. This shouldn't take * long since we just sent an unblockable SIGKILL. + * Return immediately if we are under INJECT_PTRACE because we can't wait + * for detached non-child process. */ - waitpid(info->pid, &status, 0); + if (info->method != INJECT_PTRACE) + waitpid(info->pid, &status, 0); + else + status = WEXITSTATUS(info->exitcode); } else { /* Use WNOHANG to match our Windows semantics, which does not block if * the child hasn't exited. The status returned is probably not useful, * but the caller shouldn't look at it if they haven't waited for the * app to terminate. + * Return immediately if we are under INJECT_PTRACE because we can't wait + * for detached non-child process. */ - waitpid(info->pid, &status, WNOHANG); + if (info->method != INJECT_PTRACE) + waitpid(info->pid, &status, WNOHANG); + else + status = WEXITSTATUS(info->exitcode); } if (info->pipe_fd != 0) close(info->pipe_fd); @@ -850,6 +900,14 @@ enum { MAX_SHELL_CODE = 4096 }; enum { REG_PC_OFFSET = offsetof(struct USER_REGS_TYPE, REG_PC_FIELD) }; # define APP instrlist_append +# define PRE instrlist_prepend + +# ifdef X86 +/* X86s are little endian */ +enum { SYSCALL_AS_SHORT = 0x050f, SYSENTER_AS_SHORT = 0x340f, INT80_AS_SHORT = 0x80cd }; +# endif + +enum { ERESTARTSYS = 512, ERESTARTNOINTR = 513, ERESTARTNOHAND = 514 }; static bool op_exec_gdb = false; @@ -1111,6 +1169,20 @@ injectee_run_get_retval(dr_inject_info_t *info, void *dc, instrlist_t *ilist) long r; ptr_int_t failure = -EUNATCH; /* Unlikely to be used by most syscalls. */ + /* For cases where we are not actally getting blocked by a syscall + * and wait_syscall is not specified + * need to pad nop everytime we restart process with PTRACE_CONT variations + * number_of_null_bytes = sizeof(syscall_instr) / sizeof(nop_instr) + */ + uint nop_times = 0; +# ifdef X86 + nop_times = SYSCALL_LENGTH; +# endif + int i; + for (i = 0; i < nop_times; i++) { + PRE(ilist, XINST_CREATE_nop(dc)); + } + /* Get register state before executing the shellcode. */ r = our_ptrace_getregs(info->pid, ®s); if (r < 0) @@ -1144,8 +1216,20 @@ injectee_run_get_retval(dr_inject_info_t *info, void *dc, instrlist_t *ilist) !ptrace_write_memory(info->pid, pc, shellcode, code_size)) return failure; - /* Run it! */ - our_ptrace(PTRACE_POKEUSER, info->pid, (void *)REG_PC_OFFSET, pc); + /* Run it! + * While under Ptrace during blocking syscall, upon continuing + * execution, tracee PC will be set back to syscall instruction + * PC = PC - sizeof(syscall). We have to add offsets to compensate. + */ + if (!info->wait_syscall) { + uint offset = 0; +# ifdef X86 + offset = SYSCALL_LENGTH; +# endif + our_ptrace(PTRACE_POKEUSER, info->pid, (void *)REG_PC_OFFSET, pc + offset); + } else { + our_ptrace(PTRACE_POKEUSER, info->pid, (void *)REG_PC_OFFSET, pc); + } if (!continue_until_break(info->pid)) return failure; @@ -1392,6 +1476,64 @@ detach_and_exec_gdb(process_id_t pid, const char *library_path) ASSERT(false && "failed to exec gdb?"); } +/* singlestep traced process + */ +static bool +ptrace_singlestep(process_id_t pid) +{ + if (our_ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL) < 0) + return false; + + if (!wait_until_signal(pid, SIGTRAP)) + return false; + + return true; +} + +/* Check if prev bytes form a syscall. + * For X86, we can't be sure if previous bytes are actually a syscall due to + * variations in instruction size. Do additional checks if that is the case. + */ +# ifdef X86 +/* Ptrace attach is only for X86 for now. + * ifdef above to statisfies compiler complains. + * These ifdef should be removed after we support new architecture. + */ +static bool +is_prev_bytes_syscall(process_id_t pid, app_pc src_pc) +{ +# ifdef X86 + /* for X86 is concerned, SYSCALL_LENGTH == INT_LENGTH == SYSENTER_LENGTH */ + app_pc syscall_pc = src_pc - SYSCALL_LENGTH; + /* ptrace_read_memory reads by multiple of sizeof(ptr_int_t) */ + byte instr_bytes[sizeof(ptr_int_t)]; + ptrace_read_memory(pid, instr_bytes, syscall_pc, sizeof(ptr_int_t)); +# ifdef X64 + if (*(unsigned short *)instr_bytes == SYSCALL_AS_SHORT) + return true; +# else + if (*(unsigned short *)instr_bytes == SYSENTER_AS_SHORT || + *(unsigned short *)instr_bytes == INT80_AS_SHORT) + return true; +# endif +# endif /* X86 */ + return false; +} +# endif /* X86 */ + +/* i#38: Quick explaination for PC offsetting and NOP sleds: + * If ptrace happens in middle of blocking syscalls, tracer will get PC address at + * the next instruction after syscall, but will set it back to previous syscall + * instruction by subtracting PC (PC -= (byte)sizeof(syscall)). + * We can then issue PTRACE_SINGLESTEP to wait for syscall completion and + * get out of syscall context to get normal ptrace PC behaviours (wait_syscall flag). + * Else we start injection immidiately. This cause PC to subtract sizeof(syscall) bytes + * every time we continue for the rest of ptrace session until PTRACE_DETACH. + * To compensate we set PC += (byte)sizeof(syscall) before PTRACE_CONTs + * and add nop sleds before our shellcodes and DR entry point. + * Errno masking also required to minimize app breakage. + * Detailed infomations in issue page. + */ bool inject_ptrace(dr_inject_info_t *info, const char *library_path) { @@ -1426,6 +1568,14 @@ inject_ptrace(dr_inject_info_t *info, const char *library_path) return false; if (!continue_until_break(info->pid)) return false; + } else { + if (info->wait_syscall) { + /* We are attached to target process, singlestep to make sure not returning + * from blocked syscall. + */ + if (!ptrace_singlestep(info->pid)) + return false; + } } /* Open libdynamorio.so as readonly in the child. */ @@ -1462,10 +1612,39 @@ inject_ptrace(dr_inject_info_t *info, const char *library_path) * XXX: Actually look up an export. */ injected_dr_start = (app_pc)loader.ehdr->e_entry + loader.load_delta; + + /* While under Ptrace during blocking syscall, upon continuing + * execution, tracee PC will be set back to syscall instruction + * PC = PC - sizeof(syscall). We have to add offsets to compensate. + */ + if (!info->wait_syscall) { + uint offset = 0; +# ifdef X86 + offset = SYSCALL_LENGTH; +# endif + injected_dr_start += offset; + } elf_loader_destroy(&loader); our_ptrace_getregs(info->pid, ®s); + /* Hijacking errno value + * After attaching with ptrace during blocking syscall, + * Errno value is leaked from kernel handling + * Mask that value into EINTR + */ + if (!info->wait_syscall) { +# ifdef X86 + if (is_prev_bytes_syscall(info->pid, (app_pc)regs.REG_PC_FIELD)) { + /* prev bytes might can match by accident, so check return value */ + if (regs.REG_RETVAL_FIELD == -ERESTARTSYS || + regs.REG_RETVAL_FIELD == -ERESTARTNOINTR || + regs.REG_RETVAL_FIELD == -ERESTARTNOHAND) + regs.REG_RETVAL_FIELD = -EINTR; + } +# endif + } + /* Create an injection context and "push" it onto the stack of the injectee. * If you need to pass more info to the injected child process, this is a * good place to put it. diff --git a/tools/drdeploy.c b/tools/drdeploy.c index 67107b30bba..6f5c6423090 100644 --- a/tools/drdeploy.c +++ b/tools/drdeploy.c @@ -296,9 +296,19 @@ const char *options_list_str = # ifndef MACOS /* XXX i#1285: private loader NYI on MacOS */ " -early Requests early injection (the default).\n" # endif - " -attach Attach to the process with the given pid. Pass 0\n" - " for pid to launch and inject into a new process.\n" " -logdir Logfiles will be stored in this directory.\n" +# endif +# ifdef UNIX + " -attach Attach to the process with the given pid.\n" + " If attach to a process which is in middle of blocking\n" + " system call, dynamorio will wait until it returns.\n" +# ifdef X86 + " Can be used with -skip_syscall.\n" + " -skip_syscall (Experimental)\n" + " Only work with -attach.\n" + " Attaching to a process will force blocking system calls\n" + " to fail with EINTR.\n" +# endif # endif " -use_dll Inject given dll instead of configured DR dll.\n" " -force Inject regardless of configuration.\n" @@ -1100,8 +1110,10 @@ _tmain(int argc, TCHAR *targv[]) time_t start_time, end_time; # else bool use_ptrace = false; + bool wait_syscall = true; bool kill_group = false; # endif + process_id_t attach_pid = 0; char *app_name = NULL; char full_app_name[MAXIMUM_PATH]; const char **app_argv; @@ -1122,6 +1134,7 @@ _tmain(int argc, TCHAR *targv[]) char native_tool[MAXIMUM_PATH]; #endif #ifdef DRRUN + char exe[MAXIMUM_PATH]; void *tofree = NULL; bool configure = true; #endif @@ -1251,20 +1264,37 @@ _tmain(int argc, TCHAR *targv[]) continue; } # endif + else if (strcmp(argv[i], "-attach") == 0) { # ifdef UNIX - else if (strcmp(argv[i], "-use_ptrace") == 0) { - /* Undocumented option for using ptrace on a fresh process. */ - use_ptrace = true; - continue; - } else if (strcmp(argv[i], "-attach") == 0) { const char *pid_str = argv[++i]; process_id_t pid = strtoul(pid_str, NULL, 10); if (pid == ULONG_MAX) - usage(false, "-attach expects an integer pid"); - if (pid != 0) - usage(false, "attaching to running processes is not yet implemented"); + usage(false, "attach expects an integer pid"); + if (pid == 0) { + usage(false, "attach to invalid pid"); + } + attach_pid = pid; +# endif +# ifdef UNIX + use_ptrace = true; +# endif +# ifdef WINDOWS + usage(false, "attach in Windows not yet implemented"); +# endif + continue; + } +# ifdef UNIX +# ifdef X86 + else if (strcmp(argv[i], "-skip_syscall") == 0) { + wait_syscall = false; + continue; + } +# endif +# endif +# ifdef UNIX + else if (strcmp(argv[i], "-use_ptrace") == 0) { + /* Undocumented option for using ptrace on a fresh process. */ use_ptrace = true; - /* FIXME: use pid below to attach. */ continue; } # ifndef MACOS /* XXX i#1285: private loader NYI on MacOS */ @@ -1531,10 +1561,28 @@ _tmain(int argc, TCHAR *targv[]) #if defined(DRRUN) || defined(DRINJECT) # ifdef DRRUN + if (attach_pid != 0) { + ssize_t size = 0; +# ifdef UNIX + char exe_str[MAXIMUM_PATH]; + _snprintf(exe_str, BUFFER_SIZE_ELEMENTS(exe_str), "/proc/%d/exe", attach_pid); + NULL_TERMINATE_BUFFER(exe_str); + size = readlink(exe_str, exe, BUFFER_SIZE_ELEMENTS(exe)); +# endif /* UNIX */ + if (size > 0) { + if (size < BUFFER_SIZE_ELEMENTS(exe)) + exe[size] = '\0'; + else + NULL_TERMINATE_BUFFER(exe); + } else { + usage(false, "attach to invalid pid"); + } + app_name = exe; + } /* Support no app if the tool has its own frontend, under the assumption * it may have post-processing or other features. */ - if (i < argc || native_tool[0] == '\0') { + if (attach_pid == 0 && (i < argc || native_tool[0] == '\0')) { # endif if (i >= argc) usage(false, "%s", "no app specified"); @@ -1737,6 +1785,9 @@ _tmain(int argc, TCHAR *targv[]) if (limit == 0 && !use_ptrace && !kill_group) { info("will exec %s", app_name); errcode = dr_inject_prepare_to_exec(app_name, app_argv, &inject_data); + } else if (attach_pid != 0) { + errcode = + dr_inject_prepare_to_attach(attach_pid, app_name, wait_syscall, &inject_data); } else # endif /* UNIX */ {