Skip to content

Commit

Permalink
feat: I/O
Browse files Browse the repository at this point in the history
  • Loading branch information
ndrewh committed Jul 22, 2024
1 parent 201c925 commit 720deee
Show file tree
Hide file tree
Showing 14 changed files with 313 additions and 18 deletions.
17 changes: 6 additions & 11 deletions lib/pyda/process.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
from collections import namedtuple
from dataclasses import dataclass
from .tube import ProcessTube
import pyda_core

class Process():
class Process(ProcessTube):
def __init__(self, handle, prevent_close_stdio=True):
self._p = handle

fds = self._p.capture_io()
super().__init__(fds[0], fds[1])

self._hooks = {}
self._syscall_pre_hooks = {}
self._syscall_post_hooks = {}
self._registered_syscall_pre_hook = False
self._registered_syscall_post_hook = False
self._has_run = False

def prevent_close(p, num):
if p.regs.rdi in [0, 1, 2]:
p.regs.rax = 0
return False # pre-hooks that return False will prevent the syscall from executing

return None

if prevent_close_stdio:
self.syscall_pre(3, prevent_close)

def _hook_dispatch(self, addr):
for h in self._hooks[addr]:
Expand Down
118 changes: 118 additions & 0 deletions lib/pyda/tube.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from pwnlib.tubes.tube import tube
import os
import pyda_core
import errno

# todo
class ProcessTube(tube):
def __init__(self, stdin_fd, stdout_fd, **kwargs):
super(ProcessTube, self).__init__(**kwargs)

self.closed = {"recv": False, "send": False}

self.stdin_fd = stdin_fd
self.stdout_fd = stdout_fd

# Overwritten for better usability
def recvall(self, timeout = None):
"""recvall() -> str
Receives data until the socket is closed.
"""
# todo
raise NotImplementedError("recvall() not implemented")

def recv_raw(self, numb, *a):
if self.closed["recv"]:
raise EOFError

if len(a) > 0:
raise NotImplementedError("recv_raw() with flags not implemented")

while True:
try:
data = os.read(self.stdout_fd, numb)
break
except IOError as e:
if e.errno == errno.EAGAIN:
# If we're waiting for data, let the program continue
try:
self._p.run_until_io()
continue
except Exception as e:
raise EOFError

if e.errno == errno.ETIMEDOUT or 'timed out' in e.strerror:
return None
elif e.errno in (errno.ECONNREFUSED, errno.ECONNRESET):
self.shutdown("recv")
raise EOFError
elif e.errno == errno.EINTR:
continue
else:
raise

if not data:
self.shutdown("recv")
raise EOFError

return data

# TODO: What happens when the pipe fills? This call
# will indefinitely block?
def send_raw(self, data):
if self.closed["send"]:
raise EOFError

try:
os.write(self.stdin_fd, data)
except IOError as e:
eof_numbers = (errno.EPIPE, errno.ECONNRESET, errno.ECONNREFUSED)
if e.errno in eof_numbers or 'Socket is closed' in e.args:
self.shutdown("send")
raise EOFError
else:
raise

def settimeout_raw(self, timeout):
raise NotImplementedError("settimeout_raw() not implemented")

def can_recv_raw(self, timeout):
if self.closed["recv"]:
return False

try:
if timeout is None:
return select.select([self.stdout_fd], [], []) == ([self.stdout_fd], [], [])

return select.select([self.stdout_fd], [], [], timeout) == ([self.stdout_fd], [], [])
except ValueError:
# Not sure why this isn't caught when testing self.proc.stdout.closed,
# but it's not.
#
# File "/home/user/pwntools/pwnlib/tubes/process.py", line 112, in can_recv_raw
# return select.select([self.proc.stdout], [], [], timeout) == ([self.proc.stdout], [], [])
# ValueError: I/O operation on closed file
raise EOFError
except select.error as v:
if v.args[0] == errno.EINTR:
return False

def connected_raw(self, direction):
return True

def close(self):
pass

def _close_msg(self):
self.info('Closed pyda socket')

def fileno(self):
self.error("fileno() not implemented")
return None

def shutdown_raw(self, direction):
pass

def interactive(self):
self.error("interactive() is not currently supported.")
16 changes: 16 additions & 0 deletions patches/cpython-3.10.12.patch
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ index 8d2221cfd8..1372cbc291 100644
#endif

#if defined(FAULTHANDLER_USE_ALT_STACK) && defined(HAVE_LINUX_AUXVEC_H) && defined(HAVE_SYS_AUXV_H)
diff --git a/Python/errors.c b/Python/errors.c
index bc1b55e440..78e704e332 100644
--- a/Python/errors.c
+++ b/Python/errors.c
@@ -698,10 +698,10 @@ PyErr_SetFromErrnoWithFilenameObject(PyObject *exc, PyObject *filenameObject)
PyObject *
PyErr_SetFromErrnoWithFilenameObjects(PyObject *exc, PyObject *filenameObject, PyObject *filenameObject2)
{
+ int i = errno;
PyThreadState *tstate = _PyThreadState_GET();
PyObject *message;
PyObject *v, *args;
- int i = errno;
#ifdef MS_WINDOWS
WCHAR *s_buf = NULL;
#endif /* Unix/Windows */
diff --git a/Python/thread_pthread.h b/Python/thread_pthread.h
index 35b9810aa3..130528aec3 100644
--- a/Python/thread_pthread.h
Expand Down
19 changes: 16 additions & 3 deletions patches/dynamorio-10.0.patch
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ index f5eef1e5c..91f5a16bd 100644
/* grab all_threads_synch_lock */
/* since all_threads synch doesn't give any permissions this is necessary
diff --git a/core/unix/loader.c b/core/unix/loader.c
index 10c4518b0..1a07ce582 100644
index 10c4518b0..b27284a8e 100644
--- a/core/unix/loader.c
+++ b/core/unix/loader.c
@@ -158,7 +158,7 @@ privload_locate_and_load(const char *impname, privmod_t *dependent, bool reachab
Expand Down Expand Up @@ -352,6 +352,17 @@ index 10c4518b0..1a07ce582 100644
{ "free", (app_pc)redirect_free },
{ "realloc", (app_pc)redirect_realloc },
{ "strdup", (app_pc)redirect_strdup },
@@ -1533,8 +1540,8 @@ static const redirect_import_t redirect_imports[] = {
/* These libc routines can call pthread functions and cause hangs (i#4928) so
* we use our syscall wrappers instead.
*/
- { "read", (app_pc)os_read },
- { "write", (app_pc)os_write },
+ /* { "read", (app_pc)os_read }, */
+ /* { "write", (app_pc)os_write }, */
#if defined(LINUX) && !defined(ANDROID)
{ "__tls_get_addr", (app_pc)redirect___tls_get_addr },
{ "___tls_get_addr", (app_pc)redirect____tls_get_addr },
@@ -1548,6 +1555,7 @@ static const redirect_import_t redirect_imports[] = {
{ "__gnu_Unwind_Find_exidx", (app_pc)redirect___gnu_Unwind_Find_exidx },
# endif
Expand All @@ -360,9 +371,11 @@ index 10c4518b0..1a07ce582 100644
{ "dlsym", (app_pc)redirect_dlsym },
/* We need these for clients that don't use libc (i#1747) */
{ "strlen", (app_pc)strlen },
@@ -1570,6 +1578,10 @@ static const redirect_import_t redirect_imports[] = {
@@ -1569,7 +1577,12 @@ static const redirect_import_t redirect_imports[] = {
{ "memset_chk", (app_pc)memset },
{ "memmove_chk", (app_pc)memmove },
{ "strncpy_chk", (app_pc)strncpy },
+ /* { "__errno_location", (app_pc)__errno_location } */
};
+
+DR_API redirect_import_t *client_redirect_imports = NULL;
Expand All @@ -371,7 +384,7 @@ index 10c4518b0..1a07ce582 100644
#define REDIRECT_IMPORTS_NUM (sizeof(redirect_imports) / sizeof(redirect_imports[0]))

#ifdef DEBUG
@@ -1599,6 +1611,15 @@ privload_redirect_sym(os_privmod_data_t *opd, ptr_uint_t *r_addr, const char *na
@@ -1599,6 +1612,15 @@ privload_redirect_sym(os_privmod_data_t *opd, ptr_uint_t *r_addr, const char *na
}
}
#endif
Expand Down
45 changes: 45 additions & 0 deletions pyda_core/pyda_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#include "pyda_core.h"
#include "pyda_threads.h"
#include "util.h"
#include <fcntl.h>


#ifndef PYDA_DYNAMORIO_CLIENT

Expand Down Expand Up @@ -35,6 +37,7 @@ pyda_process* pyda_mk_process() {
proc->syscall_post_hook = NULL;
proc->py_obj = NULL;

// Setup locks, etc.
pthread_condattr_t condattr;
int ret;
if (ret = pthread_condattr_setpshared(&condattr, PTHREAD_PROCESS_SHARED)) {
Expand All @@ -54,9 +57,50 @@ pyda_process* pyda_mk_process() {
dr_fprintf(STDERR, "pthread_mutex_init failed %d\n", ret);
dr_abort();
}

// Setup I/O
proc->stdin_fd = -1;
proc->stdout_fd = -1;
proc->stderr_fd = -1;

// TODO: also need to modify dynamorio printing functions
// as they use raw fd 0/1/2
return proc;
}

extern file_t our_stderr;
void pyda_capture_io(pyda_process *proc) {
int orig_in = dup(0);
int orig_out = dup(1);
int orig_err = dup(2);

int pipe1[2], pipe2[2], pipe3[2];
if (pipe(pipe1) || pipe(pipe2) || pipe(pipe3)) {
dr_fprintf(STDERR, "Failed to create pipes\n");
dr_abort();
}

dup2(pipe1[0], 0);
dup2(pipe2[1], 1);
dup2(pipe3[1], 2);

stdin = fdopen(orig_in, "r");
stdout = fdopen(orig_out, "w");
stderr = fdopen(orig_err, "w");

proc->stdin_fd = pipe1[1];
proc->stdout_fd = pipe2[0];
proc->stderr_fd = pipe3[0];

our_stderr = orig_err;

// nonblocking
if (fcntl(proc->stdout_fd, F_SETFL, O_NONBLOCK) || fcntl(proc->stderr_fd, F_SETFL, O_NONBLOCK)) {
dr_fprintf(STDERR, "Failed to set stdout to nonblocking\n");
dr_abort();
}
}

pyda_thread* pyda_mk_thread(pyda_process *proc) {
ABORT_IF_NODYNAMORIO;

Expand Down Expand Up @@ -100,6 +144,7 @@ pyda_thread* pyda_mk_thread(pyda_process *proc) {
thread->rip_updated_in_cleancall = 0;
thread->skip_next_hook = 0;
thread->python_exited = 0;
thread->app_exited = 0;
thread->errored = 0;
thread->python_blocked_on_io = 0;

Expand Down
3 changes: 3 additions & 0 deletions pyda_core/pyda_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ struct pyda_thread_s {
int skip_next_hook;

int python_exited;
int app_exited;
int errored;

int yield_count;
Expand All @@ -79,6 +80,8 @@ struct pyda_thread_s {
pyda_process* pyda_mk_process();
pyda_thread* pyda_mk_thread(pyda_process*);

void pyda_capture_io(pyda_process *p);

void pyda_process_destroy(pyda_process *p);
void pyda_thread_destroy(pyda_thread *t);
void pyda_thread_destroy_last(pyda_thread *t);
Expand Down
Loading

0 comments on commit 720deee

Please sign in to comment.