def start_trace(cmd): new_pid = createChild(cmd, no_stdout=False) debugger = PtraceDebugger() debugger.traceFork() debugger.traceExec() debugger.addProcess(new_pid, is_attached=True) return debugger
def startDebugger(self, args): debugger = PtraceDebugger() debugger.traceFork() debugger.traceExec() debugger.enableSysgood( ) # to differentiate between traps raised by syscall, no syscall newProcess = ProcessWrapper(args=args, debugger=debugger, redirect=True) # first process newProcess.syscalls_to_trace = self.syscalls_to_trace self.addProcess(newProcess) return debugger
class TracingThread(Thread): """ tracing thread that can be cancelled """ def __init__(self, manager, pid, record, old_debugger=None, is_thread=False): """ Create a new tracing thread :param manager: overall tracing management instance :param pid: Process to trace :param record: Action recording instance :param old_debugger: Old debugger the process might be attached to at the moment :param is_thread: True if the pid is a thread else False """ super().__init__() self.manager = manager # Save tracing record self.record = record self.record.runtime_log(RuntimeActionRecord.TYPE_STARTED) self._debugger_options = FunctionCallOptions(replace_socketcall=False) self.is_thread = is_thread self.process = psutil.Process(pid) self.debugger = None self.traced_item = None self.stopped = False # Check if the process is attached to the debugger if old_debugger is not None and old_debugger.dict.get( self.process.pid): old_debugger.dict.get(self.process.pid).detach() posix.kill(self.process.pid, signal.SIGSTOP) def stop(self): """ Mark this thread to stop tracing :return: None """ self.stopped = True def run(self): """ Trace the given process or thread for one syscall """ finished = False self.record.log("Attaching debugger") try: self.debugger = PtraceDebugger() self.debugger.traceFork() self.debugger.traceExec() self.debugger.traceClone() self.traced_item = self.debugger.addProcess( self.process.pid, False, is_thread=self.is_thread) self.record.log("PTrace debugger attached successfully") TracingThread._trace_continue(self.traced_item) except Exception as e: self.record.log( "PTrace debugger attachment failed with reason: {}".format(e)) finished = True # Trace process until finished while not finished and not self.stopped: finished = self._trace(self.traced_item, self.record) if self.traced_item: self.traced_item.detach() # Keep in mind that the process maybe already gone try: if self.process.status() == psutil.STATUS_STOPPED: posix.kill(self.process.pid, signal.SIGCONT) except psutil.NoSuchProcess: pass self.record.log("Tracee appears to have ended and thread will finish") def _trace(self, traced_item, record): """ Trace the given process or thread for one syscall :param traced_item: Process or thread to trace :param record: Action recording object :return: True if the item was terminated else False """ finished = False continue_tracing = True signum = 0 try: # Wait for the next syscall event = traced_item.debugger.waitSyscall(traced_item) if not event: return False # Fetch the syscall state if self.manager.is_syscall_tracing_enabled( ) or self.manager.is_file_access_tracing_enabled(): state = traced_item.syscall_state syscall = state.event(self._debugger_options) # Trace the syscall in the process record if it is not filtered out if self.manager.should_record_syscall(syscall): record.syscall_log(syscall) # Trace file access if the syscall is a file access syscall and not filtered out if self.manager.should_record_file_access(syscall): record.file_access_log(syscall) except NewProcessEvent as event: # Trace new process and continue parent self._trace_new_item(event, record) continue_tracing = False except ProcessExecution: record.runtime_log(RuntimeActionRecord.TYPE_EXEC) except ProcessExit as event: # Finish process tracing TracingThread._trace_finish(event, record) finished = True continue_tracing = False except ProcessSignal as event: record.runtime_log(RuntimeActionRecord.TYPE_SIGNAL_RECEIVED, signal=event.signum) signum = event.signum # Continue process execution if continue_tracing: TracingThread._trace_continue(traced_item, signum) return finished @staticmethod def _trace_continue(traced_item, signum=0): """ Move the process/thread to the next execution step / syscall :param traced_item: Process or thread to proceed :param signum: Signal to emit to the traced item :return: None """ traced_item.syscall(signum) @staticmethod def _trace_finish(event, record): """ Handle process or thread completion Determine exit code and termination signal and return them :param event: ProcessExit event that was raised :param record: Action recording object :return: None """ process = event.process record.runtime_log(RuntimeActionRecord.TYPE_EXITED, signal=event.signum, exit_code=event.exitcode) # Detach from tracing process.detach() def _trace_new_item(self, event, record): """ Setup tracing the given new process or thread :param event: new process/thread event :param record: Action recording object :return: None """ parent = event.process.parent process = event.process record.runtime_log(RuntimeActionRecord.TYPE_SPAWN_CHILD, child_pid=process.pid) TracingThread._trace_continue(parent) # Start new tracing thread for the new process if not self.stopped: self.manager.trace_create_thread(process.pid, process.debugger, process.is_thread) else: TracingThread._trace_continue(process)
class PythonPtraceBackend(Backend): def __init__(self): self.debugger = PtraceDebugger() self.root = None self.stop_requested = False self.syscalls = {} self.backtracer = NullBacktracer() self.debugger.traceClone() self.debugger.traceExec() self.debugger.traceFork() def attach_process(self, pid): self.root = self.debugger.addProcess(pid, is_attached=False) def create_process(self, arguments): pid = createChild(arguments, no_stdout=False) self.root = self.debugger.addProcess(pid, is_attached=True) return self.root def get_argument(self, pid, num): return self.syscalls[pid].arguments[num].value def get_syscall_result(self, pid): if self.syscalls[pid]: return self.syscalls[pid].result return None def get_arguments_str(self, pid): self.syscalls[pid].format() return ", ".join([ "{}={}".format(i.name, i.text) for i in self.syscalls[pid].arguments ]) def read_cstring(self, pid, address): try: return self.debugger[pid].readCString(address, 255)[0].decode('utf-8') except PtraceError as e: # TODO: ptrace's PREFORMAT_ARGUMENTS, why they are lost? for arg in self.syscalls[pid].arguments: if arg.value == address: return arg.text raise e def read_bytes(self, pid, address, size): return self.debugger[pid].readBytes(address, size) def write_bytes(self, pid, address, data): return self.debugger[pid].writeBytes(address, data) def create_backtrace(self, pid): return self.backtracer.create_backtrace(self.debugger[pid]) def start(self): # First query to break at next syscall self.root.syscall() while not self.stop_requested and self.debugger: try: try: # FIXME: better mechanism to stop debugger # debugger._waitPid(..., blocking=False) may help # actually debugger receives SIGTERM, terminates all remaining process # then this method is unblocked and fails with KeyError event = self.debugger.waitSyscall() except: if self.stop_requested: return raise state = event.process.syscall_state syscall = state.event(FunctionCallOptions()) self.syscalls[event.process.pid] = syscall yield SyscallEvent(event.process.pid, syscall.name) # FIXME: # when exit_group is called, it seems that thread is still monitored # in next event we area accessing pid that is not running anymore # so when exit_group occurs, remove all processes with same Tgid and detach from them if syscall.name == 'exit_group': # result is None, never return! def read_group(pid): with open('/proc/%d/status' % pid) as f: return int({ i: j.strip() for i, j in [ i.split(':', 1) for i in f.read().splitlines() ] }['Tgid']) me = read_group(syscall.process.pid) for process in self.debugger: if read_group(process.pid) == me: process.detach() self.debugger.deleteProcess(pid=process.pid) yield ProcessExited(process.pid, syscall.arguments[0].value) else: event.process.syscall() except ProcessExit as event: # Display syscall which has not exited state = event.process.syscall_state if (state.next_event == "exit") and state.syscall: # self.syscall(state.process) TODO: pass yield ProcessExited(event.process.pid, event.exitcode) except ProcessSignal as event: # event.display() event.process.syscall(event.signum) except NewProcessEvent as event: process = event.process logging.info("*** New process %s ***", process.pid) yield ProcessCreated(pid=process.pid, parent_pid=process.parent.pid, is_thread=process.is_thread) process.syscall() process.parent.syscall() except ProcessExecution as event: logging.info("*** Process %s execution ***", event.process.pid) event.process.syscall() except DebuggerError: if self.stop_requested: return raise except PtraceError as e: if e.errno == 3: # FIXME: same problem as exit_group above ? logging.warning("Process dead?") else: raise def quit(self): self.stop_requested = True self.debugger.quit()