Skip to content

Commit

Permalink
i#38: Attach injection on Linux (#5019)
Browse files Browse the repository at this point in the history
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 <[email protected]>
Co-authored-by: Derek Bruening <[email protected]>
  • Loading branch information
3 people authored Aug 16, 2021
1 parent e296135 commit bf246bf
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 27 deletions.
6 changes: 6 additions & 0 deletions core/arch/x86/x86.asm
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
34 changes: 31 additions & 3 deletions core/lib/dr_inject.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
203 changes: 191 additions & 12 deletions core/unix/injector.c
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
#include <sys/user.h>
#include <sys/wait.h>
#include <unistd.h>
#include <time.h>
#ifdef MACOS
# include <spawn.h>
# include <crt_externs.h> /* _NSGetEnviron() */
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}

Expand All @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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, &regs);
if (r < 0)
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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, &regs);

/* 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.
Expand Down
Loading

0 comments on commit bf246bf

Please sign in to comment.