Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

i#38: Attach injection on Linux #5019

Merged
merged 26 commits into from
Aug 16, 2021
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions core/arch/x86/x86.asm
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,11 @@ GLOBAL_LABEL(client_int_syscall:)
*/
DECLARE_FUNC(_start)
GLOBAL_LABEL(_start:)
/* i#38: attaching while in middle of blocking syscall require padded null bytes
* with number_of_null_bytes = sizeof(syscall_instr) / sizeof(nop_instr)
M3m3M4n marked this conversation as resolved.
Show resolved Hide resolved
*/
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
32 changes: 29 additions & 3 deletions core/lib/dr_inject.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,32 @@ 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 Early inject syscall handling mode.
M3m3M4n marked this conversation as resolved.
Show resolved Hide resolved
*
* \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 +272,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
183 changes: 172 additions & 11 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.
M3m3M4n marked this conversation as resolved.
Show resolved Hide resolved
*/
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,8 +838,13 @@ 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 immidiately 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);
M3m3M4n marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand Down Expand Up @@ -850,6 +895,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 +1164,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
M3m3M4n marked this conversation as resolved.
Show resolved Hide resolved
* 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 +1211,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.
derekbruening marked this conversation as resolved.
Show resolved Hide resolved
*/
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 +1471,51 @@ 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)
M3m3M4n marked this conversation as resolved.
Show resolved Hide resolved
return true;
# else
if (*(unsigned short *)instr_bytes == SYSENTER_AS_SHORT ||
*(unsigned short *)instr_bytes == INT80_AS_SHORT)
return true;
# endif
# endif
return false;
}
# endif

bool
inject_ptrace(dr_inject_info_t *info, const char *library_path)
{
Expand Down Expand Up @@ -1426,6 +1550,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.
derekbruening marked this conversation as resolved.
Show resolved Hide resolved
*/
if (!ptrace_singlestep(info->pid))
return false;
}
}

/* Open libdynamorio.so as readonly in the child. */
Expand Down Expand Up @@ -1462,10 +1594,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.
derekbruening marked this conversation as resolved.
Show resolved Hide resolved
*/
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
derekbruening marked this conversation as resolved.
Show resolved Hide resolved
*/
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