def main(): global options options = get_options() dbg = PtraceDebugger() pid = int(subprocess.check_output(['pidof', "typespeed"])) process = dbg.addProcess(pid, False) process.syscall() while True: event = dbg.waitSyscall() process = event.process op_syscall(process) dbg.quit()
def debugFindRtld(pid): dbg = PtraceDebugger() process = dbg.addProcess(pid, False) for pm in readProcessMappings(process): if 'rwx' in pm.permissions: for addr in pm.search(b'\x78\x56\x34\x12'): # There are no ret instructions in the CC memory = process.readBytes(addr, 20) if b'\xC3' not in memory: process = None dbg.quit() return addr print('ERROR: Could not find the address of rtld_lock_default_lock_recursive in code cache!') exit(-1)
class Gdb(Application): def __init__(self): Application.__init__(self) # Parse self.options self.parseOptions() # Setup output (log) self.setupLog() self.last_signal = {} # We assume user wants all possible information self.syscall_options = FunctionCallOptions( write_types=True, write_argname=True, write_address=True, ) # FIXME: Remove self.breaks! self.breaks = dict() self.followterms = [] def setupLog(self): self._setupLog(stdout) def parseOptions(self): parser = OptionParser( usage="%prog [options] -- program [arg1 arg2 ...]") self.createCommonOptions(parser) self.createLogOptions(parser) self.options, self.program = parser.parse_args() if self.options.pid is None and not self.program: parser.print_help() exit(1) self.processOptions() self.show_pid = self.options.fork def _continueProcess(self, process, signum=None): if not signum and process in self.last_signal: signum = self.last_signal[process] if signum: error("Send %s to %s" % (signalName(signum), process)) process.cont(signum) try: del self.last_signal[process] except KeyError: pass else: process.cont() def cont(self, signum=None): for process in self.debugger: process.syscall_state.clear() if process == self.process: self._continueProcess(process, signum) else: self._continueProcess(process) # Wait for a process signal signal = self.debugger.waitSignals() process = signal.process # Hit breakpoint? if signal.signum == SIGTRAP: ip = self.process.getInstrPointer() if not CPU_POWERPC: # Go before "INT 3" instruction ip -= 1 breakpoint = self.process.findBreakpoint(ip) if breakpoint: error("Stopped at %s" % breakpoint) breakpoint.desinstall(set_ip=True) else: self.processSignal(signal) return None def readRegister(self, regs): name = regs.group(0)[1:] value = self.process.getreg(name) return str(value) def parseInteger(self, text): # Remove spaces and convert to lower case text = text.strip() if " " in text: raise ValueError("Space are forbidden: %r" % text) text = text.lower() # Replace registers by their value orig_text = text text = REGISTER_REGEX.sub(self.readRegister, text) # Replace hexadecimal numbers by decimal numbers def readHexadecimal(regs): text = regs.group(0) if text.startswith("0x"): text = text[2:] elif not re.search("[a-f]", text): return text value = int(text, 16) return str(value) text = re.sub(r"(?:0x)?[0-9a-f]+", readHexadecimal, text) # Reject invalid characters if not re.match(r"^[()<>+*/&0-9-]+$", text): raise ValueError("Invalid expression: %r" % orig_text) # Use integer division (a//b) instead of float division (a/b) text = text.replace("/", "//") # Finally, evaluate the expression is_pointer = text.startswith("*") if is_pointer: text = text[1:] try: value = eval(text) value = truncateWord(value) except SyntaxError: raise ValueError("Invalid expression: %r" % orig_text) if is_pointer: value = self.process.readWord(value) return value def parseIntegers(self, text): values = [] for item in text.split(): item = item.strip() value = self.parseInteger(item) values.append(value) return values def parseBytes(self, text): # FIXME: Validate input # if not BYTES_REGEX.match(text): # raise ValueError('Follow text must be enclosed in quotes!') if PY3: text = 'b' + text.lstrip() value = eval(text) if not isinstance(value, binary_type): raise TypeError("Input is not a bytes string!") return value def addFollowTerm(self, text): # Allow terms of the form 'string', "string", '\x04', "\x01\x14" term = self.parseBytes(text) self.followterms.append(term) def showFollowTerms(self): print(self.followterms) def _xray(self): for term in self.followterms: for process in self.debugger: for procmap in readProcessMappings(process): for address in procmap.search(term): yield (process, procmap, address, term) # displays the offsets of all terms found in the process memory mappings # along with possible addresses of pointers pointing to these terms def xray(self): for process, procmap, address, term in self._xray(): pointers = " ".join(formatAddress(ptr_addr) for ptr_addr in getPointers(process, address)) print("term[%s] pid[%i] %s %s pointers: %s" % ( repr(term), process.pid, procmap, formatAddress(address), pointers)) def execute(self, command): errmsg = None if command == "cont": errmsg = self.cont() elif command == "proc": self.procInfo() elif command == "proclist": self.procList() elif command.startswith("attach "): errmsg = self.attachProcess(command[7:]) elif command == "regs": self.process.dumpRegs() elif command == "stack": self.process.dumpStack() elif command == "gcore": self.gcore(self.process) elif command == "backtrace": errmsg = self.backtrace() elif command == "where" or command.startswith("where "): errmsg = self.where(command[6:]) elif command == "where2" or command.startswith("where2 "): errmsg = self.where(command[7:], manage_bp=True) elif command == "maps": self.process.dumpMaps() elif command == "dbginfo": self.debuggerInfo() elif command == "step": errmsg = self.step(False) elif command == "stepi": errmsg = self.step(True) elif command == "sys": errmsg = self.syscallTrace() elif command == "help": self.help() elif command.startswith("set "): errmsg = self.set(command) elif command.startswith("until "): errmsg = self.until(command[6:]) elif command.startswith("switch") or command == "switch": errmsg = self.switch(command[6:]) elif command.startswith("break "): errmsg = self.breakpoint(command[6:]) elif command.startswith("breakpoints"): self.displayBreakpoints() elif command.startswith("signals"): self.displaySignals() elif command.startswith("delete "): errmsg = self.delete(command[7:]) elif command.startswith("hexdump "): errmsg = self.hexdump(command[8:]) elif command.startswith("signal "): errmsg = self.signal(command[7:]) elif command.startswith("print "): errmsg = self.print_(command[6:]) elif command.startswith("follow "): errmsg = self.addFollowTerm(command[7:]) elif command == "showfollow": self.showFollowTerms() elif command == "resetfollow": self.followterms = [] elif command == "xray": self.xray() else: errmsg = "Unknown command: %r" % command if errmsg: print(errmsg, file=stderr) return False return True def parseSignum(self, command): try: return SIGNALS[command] except KeyError: pass try: return SIGNALS["SIG" + command] except KeyError: pass try: return self.parseInteger(command) except ValueError: raise ValueError("Invalid signal number: %r" % command) def signal(self, command): try: signum = self.parseSignum(command) except ValueError as err: return str(err) last_process = self.process try: errmsg = self.cont(signum) return errmsg finally: try: del self.last_signal[last_process] except KeyError: pass def print_(self, command): try: value = self.parseInteger(command) except ValueError as err: return str(err) error("Decimal: %s" % value) error("Hexadecimal: %s" % formatWordHex(value)) for map in self.process.readMappings(): if value not in map: continue error("Address is part of mapping: %s" % map) return None def gcore(self, process): import re childPid = str(process).split('#')[-1][:-1] maps_file = open("/proc/" + childPid + "/maps", 'r') mem_file = open("/proc/" + childPid + "/mem", 'r', 0) from sys import argv dump = open("/vmdump/" + argv[1] + ".dump", "wb") for line in maps_file.readlines(): # for each mapped region m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r])', line) if m.group(3) == 'r': # if this is a readable region if "/lib" not in line and "/usr" not in line: # for eliminating of shared libs start = int(m.group(1), 16) end = int(m.group(2), 16) mem_file.seek(start) # seek to region start chunk = mem_file.read(end - start) # read region contents dump.write(chunk,) # dump contents to standard output maps_file.close() mem_file.close() dump.close() def hexdump(self, command): max_line = 20 width = (terminalWidth() - len(formatAddress(1)) - 3) // 4 width = max(width, 1) limited = None parts = command.split(" ", 1) if 1 < len(parts): try: start_address = self.parseInteger(parts[0]) end_address = self.parseInteger(parts[1]) if end_address <= start_address: raise ValueError('End address (%s) is smaller than start address(%s)!' % (formatAddress(end_address), formatAddress(start_address))) except ValueError as err: return str(err) size = end_address - start_address max_size = width * max_line if max_size < size: limited = max_size end_address = start_address + max_size else: try: start_address = self.parseInteger(command) except ValueError as err: return str(err) end_address = start_address + 5 * width read_error = None address = start_address while address < end_address: size = min(end_address - address, width) try: # Read bytes memory = self.process.readBytes(address, size) # Format bytes hexa = formatHexa(memory) hexa = hexa.ljust(width * 3 - 1, ' ') ascii = formatAscii(memory) ascii = ascii.ljust(width, ' ') # Display previous read error, if any if read_error: warning("Warning: Unable to read memory %s" % ( formatAddressRange(*read_error))) read_error = None # Display line error("%s| %s| %s" % (formatAddress(address), hexa, ascii)) except PtraceError: if not read_error: read_error = [address, address + size] else: read_error[1] = address + size address += size # Display last read error, if any if read_error: warning("Warning: Unable to read memory %s" % ( formatAddressRange(*read_error))) if limited: warning("(limit to %s bytes)" % max_size) return None def backtrace(self): trace = self.process.getBacktrace() for func in trace: error(func) if trace.truncated: error("--limited to depth %s--" % len(trace)) return None def where(self, command, manage_bp=False): start = None stop = None try: values = self.parseIntegers(command) except ValueError as err: return str(err) if 1 <= len(values): start = values[0] if 2 <= len(values): stop = values[1] self.process.dumpCode(start, stop, manage_bp=manage_bp) return None def procInfo(self): dumpProcessInfo(error, self.process.pid, max_length=160) def procList(self): for process in self.debugger: text = str(process) if self.process == process: text += " (active)" error(text) def set(self, command): try: key, value = command[4:].split("=", 1) key = key.strip().lower() if not key.startswith("$"): return 'Register name (%s) have to start with "$"' % key key = key[1:] except ValueError: return "Invalid command: %r" % command try: value = self.parseInteger(value) except ValueError as err: return str(err) try: self.process.setreg(key, value) except ProcessError as err: return "Unable to set $%s=%s: %s" % (key, value, err) error("Set $%s to %s" % (key, value)) return None def displayInstr(self, prefix): try: if HAS_DISASSEMBLER: instr = self.process.disassembleOne() error("%s %s: %s" % ( prefix, formatAddress(instr.address), instr.text)) else: self.process.dumpCode() except PtraceError as err: error("Unable to read current instruction: %s" % err) def attachProcess(self, text): try: pid = self.parseInteger(text) except ValueError as err: return str(err) process = self.debugger.addProcess(pid, False) self.switchProcess(process) def step(self, enter_call, address=None): if address is None: self.displayInstr("Execute") if (not HAS_PTRACE_SINGLESTEP) or (not enter_call): if address is None: address = self.process.getInstrPointer() size = self.readInstrSize(address, default_size=None) if not size: return "Unable to read instruction size at %s" \ % formatAddress(address) address += size size = self.readInstrSize(address) # Set a breakpoint breakpoint = self.process.createBreakpoint(address, size) # Continue the process self.process.cont() else: # Use ptrace single step command self.process.singleStep() breakpoint = None # Execute processus until next TRAP try: self.process.waitSignals(SIGTRAP) if breakpoint: breakpoint.desinstall(set_ip=True) except: # noqa: E722 if breakpoint: breakpoint.desinstall() raise return None def newProcess(self, event): error("New process: %s" % event.process) # FIXME: This function doesn't work with multiple processes # especially when a parent waits for a child def syscallTrace(self): # Trace until syscall enter self.process.syscall() self.process.waitSyscall() # Process the syscall event state = self.process.syscall_state syscall = state.event(self.syscall_options) # Display syscall if syscall: if syscall.result is not None: text = "%s = %s" % (syscall.format(), syscall.result_text) if self.show_pid: text = "Process %s exits %s" % (syscall.process.pid, text) error(text) else: text = syscall.format() if self.show_pid: text = "Process %s enters %s" % (syscall.process.pid, text) error(text) return None def until(self, command): try: address = self.parseInteger(command) except ValueError as err: return str(err) errmsg = self.step(False, address) if errmsg: return errmsg self.displayInstr("Current") return None def switch(self, command): if not command: process_list = self.debugger.list if len(process_list) == 1: return "There is only one process!" index = process_list.index(self.process) index = (index + 1) % len(process_list) process = process_list[index] self.switchProcess(process) return try: pid = self.parseInteger(command) except ValueError as err: return str(err) try: process = self.debugger[pid] self.switchProcess(process) except KeyError: return "There is not process %s" % pid return None def switchProcess(self, process): if self.process == process: return error("Switch to %s" % process) self.process = process def nextProcess(self): try: process = next(iter(self.debugger)) self.switchProcess(process) except StopIteration: pass def displayBreakpoints(self): found = False for process in self.debugger: for bp in process.breakpoints.values(): found = True error("%s:%s" % (process, bp)) if not found: error("(no breakpoint)") def displaySignals(self): signals = list(SIGNAMES.items()) signals.sort(key=lambda key_value: key_value[0]) for signum, name in signals: error("% 2s: %s" % (signum, name)) def readInstrSize(self, address, default_size=None): if not HAS_DISASSEMBLER: return default_size try: # Get address and size of instruction at specified address instr = self.process.disassembleOne(address) return instr.size except PtraceError as err: warning("Warning: Unable to read instruction size at %s: %s" % ( formatAddress(address), err)) return default_size def breakpoint(self, command): try: address = self.parseInteger(command) except ValueError as err: return str(err) # Create breakpoint size = self.readInstrSize(address) try: bp = self.process.createBreakpoint(address, size) except PtraceError as err: return "Unable to set breakpoint at %s: %s" % ( formatAddress(address), err) error("New breakpoint: %s" % bp) return None def delete(self, command): try: address = self.parseInteger(command) except ValueError as err: return str(err) breakpoint = self.process.findBreakpoint(address) if not breakpoint: return "No breakpoint at %s " % formatAddress(address) breakpoint.desinstall() error("%s deleted" % breakpoint) return None def help(self): for command, description in COMMANDS: error("%s: %s" % (command, description)) error('') error("Value can be an hexadecimal/decimal number or a register name ($reg)") error("You can use operators a+b, a-b, a*b, a/b, a<<b, a>>b, a**b, and parenthesis in expressions") error( 'Use ";" to write multiple commands on the same line (e.g. "step; print $eax")') def processSignal(self, event): event.display() self.switchProcess(event.process) self.last_signal[self.process] = event.signum error("%s interrupted by %s" % (self.process, event.name)) def processExecution(self, event): error(event) self.switchProcess(event.process) self.interrupt() def debuggerInfo(self): error("Debugger process ID: %s" % getpid()) error("python-ptrace version %s" % VERSION) error("Website: %s" % WEBSITE) def interrupt(self): waitlist = [] for process in self.debugger: if process.is_stopped: continue try: if process.isTraced(): continue except NotImplementedError: pass warning("Interrupt %s (send SIGINT)" % process) process.kill(SIGINT) waitlist.append(process) for process in waitlist: info("Wait %s interruption" % process) try: process.waitSignals(SIGINT) except ProcessSignal as event: event.display() except KeyboardInterrupt: pass def deleteProcess(self, pid): try: process = self.debugger[pid] except KeyError: return event = process.processTerminated() error(str(event)) if process == self.process: self.nextProcess() def restoreTerminal(self): if enableEchoMode(): error("Terminal: restore echo mode") def mainLoop(self): # Read command try: self.restoreTerminal() command = raw_input(self.invite).strip() except EOFError: print() return True except KeyboardInterrupt: error("User interrupt!") self.interrupt() return False # If command is empty, reuse previous command if not command: if self.previous_command: command = self.previous_command info("Replay previous command: %s" % command) else: return False self.previous_command = None # User wants to quit? if command == "quit": return True # Execute the user command try: command_str = command ok = True for command in command_str.split(";"): command = command.strip() try: ok &= self.execute(command) except Exception as err: print("Command error: %s" % err) raise ok = False if not ok: break if ok: self.previous_command = command_str except KeyboardInterrupt: self.interrupt() except NewProcessEvent as event: self.newProcess(event) except ProcessSignal as event: self.processSignal(event) except ProcessExit as event: error(event) self.nextProcess() except ProcessExecution as event: self.processExecution(event) except PtraceError as err: error("ERROR: %s" % err) if err.errno == ESRCH: self.deleteProcess(err.pid) return False def runDebugger(self): self.setupDebugger() # Create new process try: self.process = self.createProcess() except ChildError as err: writeError(getLogger(), err, "Unable to create child process") return if not self.process: return # Trace syscalls self.invite = '(gdb) ' self.previous_command = None while True: if not self.debugger: # There is no more process: quit return done = self.mainLoop() if done: return def main(self): self.debugger = PtraceDebugger() try: self.runDebugger() except KeyboardInterrupt: error("Interrupt debugger: quit!") except PTRACE_ERRORS as err: writeError(getLogger(), err, "Debugger error") self.process = None self.debugger.quit() error("Quit gdb.") self.restoreTerminal()
class SyscallTracer(Application): def __init__(self): Application.__init__(self) # Parse self.options self.parseOptions() # Setup output (log) self.setupLog() def setupLog(self): if self.options.output: fd = open(self.options.output, 'w') self._output = fd else: fd = stderr self._output = None self._setupLog(fd) def parseOptions(self): parser = OptionParser(usage="%prog [options] -- program [arg1 arg2 ...]") self.createCommonOptions(parser) parser.add_option("--enter", help="Show system call enter and exit", action="store_true", default=False) parser.add_option("--profiler", help="Use profiler", action="store_true", default=False) parser.add_option("--type", help="Display arguments type and result type (default: no)", action="store_true", default=False) parser.add_option("--name", help="Display argument name (default: no)", action="store_true", default=False) parser.add_option("--string-length", "-s", help="String max length (default: 300)", type="int", default=300) parser.add_option("--array-count", help="Maximum number of array items (default: 20)", type="int", default=20) parser.add_option("--raw-socketcall", help="Raw socketcall form", action="store_true", default=False) parser.add_option("--output", "-o", help="Write output to specified log file", type="str") parser.add_option("--ignore-regex", help="Regex used to filter syscall names (eg. --ignore='^(gettimeofday|futex|f?stat)')", type="str") parser.add_option("--address", help="Display structure addressl", action="store_true", default=False) parser.add_option("--syscalls", '-e', help="Comma separated list of shown system calls (other will be skipped)", type="str", default=None) parser.add_option("--socket", help="Show only socket functions", action="store_true", default=False) parser.add_option("--filename", help="Show only syscall using filename", action="store_true", default=False) parser.add_option("--show-pid", help="Prefix line with process identifier", action="store_true", default=False) parser.add_option("--list-syscalls", help="Display system calls and exit", action="store_true", default=False) parser.add_option("-i", "--show-ip", help="print instruction pointer at time of syscall", action="store_true", default=False) self.createLogOptions(parser) self.options, self.program = parser.parse_args() if self.options.list_syscalls: syscalls = list(SYSCALL_NAMES.items()) syscalls.sort(key=lambda data: data[0]) for num, name in syscalls: print("% 3s: %s" % (num, name)) exit(0) if self.options.pid is None and not self.program: parser.print_help() exit(1) # Create "only" filter only = set() if self.options.syscalls: # split by "," and remove spaces for item in self.options.syscalls.split(","): item = item.strip() if not item or item in only: continue ok = True valid_names = list(SYSCALL_NAMES.values()) for name in only: if name not in valid_names: print("ERROR: unknow syscall %r" % name, file=stderr) ok = False if not ok: print(file=stderr) print("Use --list-syscalls options to get system calls list", file=stderr) exit(1) # remove duplicates only.add(item) if self.options.filename: for syscall, format in SYSCALL_PROTOTYPES.items(): restype, arguments = format if any(argname in FILENAME_ARGUMENTS for argtype, argname in arguments): only.add(syscall) if self.options.socket: only |= SOCKET_SYSCALL_NAMES self.only = only if self.options.ignore_regex: try: self.ignore_regex = re.compile(self.options.ignore_regex) except Exception as err: print("Invalid regular expression! %s" % err) print("(regex: %r)" % self.options.ignore_regex) exit(1) else: self.ignore_regex = None if self.options.fork: self.options.show_pid = True self.processOptions() def ignoreSyscall(self, syscall): name = syscall.name if self.only and (name not in self.only): return True if self.ignore_regex and self.ignore_regex.match(name): return True return False def displaySyscall(self, syscall): name = syscall.name text = syscall.format() if syscall.result is not None: text = "%-40s = %s" % (text, syscall.result_text) prefix = [] if self.options.show_pid: prefix.append("[%s]" % syscall.process.pid) if self.options.show_ip: prefix.append("[%s]" % formatAddress(syscall.instr_pointer)) if prefix: text = ''.join(prefix) + ' ' + text error(text) def syscallTrace(self, process): # First query to break at next syscall self.prepareProcess(process) while True: # No more process? Exit if not self.debugger: break # Wait until next syscall enter try: event = self.debugger.waitSyscall() process = event.process except ProcessExit as event: self.processExited(event) continue except ProcessSignal as event: event.display() process.syscall(event.signum) continue except NewProcessEvent as event: self.newProcess(event) continue except ProcessExecution as event: self.processExecution(event) continue # Process syscall enter or exit self.syscall(process) def syscall(self, process): state = process.syscall_state syscall = state.event(self.syscall_options) if syscall and (syscall.result is not None or self.options.enter): self.displaySyscall(syscall) # Break at next syscall process.syscall() def processExited(self, event): # Display syscall which has not exited state = event.process.syscall_state if (state.next_event == "exit") \ and (not self.options.enter) \ and state.syscall: self.displaySyscall(state.syscall) # Display exit message error("*** %s ***" % event) def prepareProcess(self, process): process.syscall() process.syscall_state.ignore_callback = self.ignoreSyscall def newProcess(self, event): process = event.process error("*** New process %s ***" % process.pid) self.prepareProcess(process) process.parent.syscall() def processExecution(self, event): process = event.process error("*** Process %s execution ***" % process.pid) process.syscall() def runDebugger(self): # Create debugger and traced process self.setupDebugger() process = self.createProcess() if not process: return self.syscall_options = FunctionCallOptions( write_types=self.options.type, write_argname=self.options.name, string_max_length=self.options.string_length, replace_socketcall=not self.options.raw_socketcall, write_address=self.options.address, max_array_count=self.options.array_count, ) self.syscall_options.instr_pointer = self.options.show_ip self.syscallTrace(process) def main(self): if self.options.profiler: from ptrace.profiler import runProfiler runProfiler(getLogger(), self._main) else: self._main() if self._output is not None: self._output.close() def _main(self): self.debugger = PtraceDebugger() try: self.runDebugger() except ProcessExit as event: self.processExited(event) except PtraceError as err: error("ptrace() error: %s" % err) except KeyboardInterrupt: error("Interrupted.") except PTRACE_ERRORS as err: writeError(getLogger(), err, "Debugger error") self.debugger.quit() def createChild(self, program): pid = Application.createChild(self, program) error("execve(%s, %s, [/* 40 vars */]) = %s" % ( program[0], program, pid)) return pid
class SyscallTracer(Application): def __init__(self): Application.__init__(self) # Parse self.options self.parseOptions() # Setup output (log) self.setupLog() def setupLog(self): if self.options.output: fd = open(self.options.output, 'w') self._output = fd else: fd = stderr self._output = None self._setupLog(fd) def parseOptions(self): parser = OptionParser( usage="%prog [options] -- program [arg1 arg2 ...]") self.createCommonOptions(parser) parser.add_option("--enter", help="Show system call enter and exit", action="store_true", default=False) parser.add_option("--profiler", help="Use profiler", action="store_true", default=False) parser.add_option( "--type", help="Display arguments type and result type (default: no)", action="store_true", default=False) parser.add_option("--name", help="Display argument name (default: no)", action="store_true", default=False) parser.add_option("--string-length", "-s", help="String max length (default: 300)", type="int", default=300) parser.add_option("--array-count", help="Maximum number of array items (default: 20)", type="int", default=20) parser.add_option("--raw-socketcall", help="Raw socketcall form", action="store_true", default=False) parser.add_option("--output", "-o", help="Write output to specified log file", type="str") parser.add_option( "--ignore-regex", help= "Regex used to filter syscall names (e.g. --ignore='^(gettimeofday|futex|f?stat)')", type="str") parser.add_option("--address", help="Display structure address", action="store_true", default=False) parser.add_option( "--syscalls", '-e', help= "Comma separated list of shown system calls (other will be skipped)", type="str", default=None) parser.add_option("--socket", help="Show only socket functions", action="store_true", default=False) parser.add_option("--filename", help="Show only syscall using filename", action="store_true", default=False) parser.add_option("--show-pid", help="Prefix line with process identifier", action="store_true", default=False) parser.add_option("--list-syscalls", help="Display system calls and exit", action="store_true", default=False) parser.add_option("-i", "--show-ip", help="print instruction pointer at time of syscall", action="store_true", default=False) self.createLogOptions(parser) self.options, self.program = parser.parse_args() if self.options.list_syscalls: syscalls = list(SYSCALL_NAMES.items()) syscalls.sort(key=lambda data: data[0]) for num, name in syscalls: print("% 3s: %s" % (num, name)) exit(0) if self.options.pid is None and not self.program: parser.print_help() exit(1) # Create "only" filter only = set() if self.options.syscalls: # split by "," and remove spaces for item in self.options.syscalls.split(","): item = item.strip() if not item or item in only: continue ok = True valid_names = list(SYSCALL_NAMES.values()) for name in only: if name not in valid_names: print("ERROR: unknown syscall %r" % name, file=stderr) ok = False if not ok: print(file=stderr) print( "Use --list-syscalls options to get system calls list", file=stderr) exit(1) # remove duplicates only.add(item) if self.options.filename: for syscall, format in SYSCALL_PROTOTYPES.items(): restype, arguments = format if any(argname in FILENAME_ARGUMENTS for argtype, argname in arguments): only.add(syscall) if self.options.socket: only |= SOCKET_SYSCALL_NAMES self.only = only if self.options.ignore_regex: try: self.ignore_regex = re.compile(self.options.ignore_regex) except Exception as err: print("Invalid regular expression! %s" % err) print("(regex: %r)" % self.options.ignore_regex) exit(1) else: self.ignore_regex = None if self.options.fork: self.options.show_pid = True self.processOptions() def ignoreSyscall(self, syscall): name = syscall.name if self.only and (name not in self.only): return True if self.ignore_regex and self.ignore_regex.match(name): return True return False def displaySyscall(self, syscall): text = syscall.format() if syscall.result is not None: text = "%-40s = %s" % (text, syscall.result_text) prefix = [] if self.options.show_pid: prefix.append("[%s]" % syscall.process.pid) if self.options.show_ip: prefix.append("[%s]" % formatAddress(syscall.instr_pointer)) if prefix: text = ''.join(prefix) + ' ' + text error(text) def syscallTrace(self, process): # First query to break at next syscall self.prepareProcess(process) while True: # No more process? Exit if not self.debugger: break # Wait until next syscall enter try: event = self.debugger.waitSyscall() except ProcessExit as event: self.processExited(event) continue except ProcessSignal as event: event.display() event.process.syscall(event.signum) continue except NewProcessEvent as event: self.newProcess(event) continue except ProcessExecution as event: self.processExecution(event) continue # Process syscall enter or exit self.syscall(event.process) def syscall(self, process): state = process.syscall_state syscall = state.event(self.syscall_options) if syscall and (syscall.result is not None or self.options.enter): self.displaySyscall(syscall) # Break at next syscall process.syscall() def processExited(self, event): # Display syscall which has not exited state = event.process.syscall_state if (state.next_event == "exit") \ and (not self.options.enter) \ and state.syscall: self.displaySyscall(state.syscall) # Display exit message error("*** %s ***" % event) def prepareProcess(self, process): process.syscall() process.syscall_state.ignore_callback = self.ignoreSyscall def newProcess(self, event): process = event.process error("*** New process %s ***" % process.pid) self.prepareProcess(process) process.parent.syscall() def processExecution(self, event): process = event.process error("*** Process %s execution ***" % process.pid) process.syscall() def runDebugger(self): # Create debugger and traced process self.setupDebugger() process = self.createProcess() if not process: return self.syscall_options = FunctionCallOptions( write_types=self.options.type, write_argname=self.options.name, string_max_length=self.options.string_length, replace_socketcall=not self.options.raw_socketcall, write_address=self.options.address, max_array_count=self.options.array_count, ) self.syscall_options.instr_pointer = self.options.show_ip self.syscallTrace(process) def main(self): if self.options.profiler: from ptrace.profiler import runProfiler runProfiler(getLogger(), self._main) else: self._main() if self._output is not None: self._output.close() def _main(self): self.debugger = PtraceDebugger() try: self.runDebugger() except ProcessExit as event: self.processExited(event) except PtraceError as err: error("ptrace() error: %s" % err) except KeyboardInterrupt: error("Interrupted.") except PTRACE_ERRORS as err: writeError(getLogger(), err, "Debugger error") self.debugger.quit() def createChild(self, program): pid = Application.createChild(self, program) error("execve(%s, %s, [/* 40 vars */]) = %s" % (program[0], program, pid)) return pid
class SyscallTracer(Application): def __init__(self): Application.__init__(self) # Parse self.options self.parseOptions() # Setup output (log) self.setupLog() def setupLog(self): if self.options.output: fd = open(self.options.output, 'w') self._output = fd else: fd = stderr self._output = None self._setupLog(fd) def parseOptions(self): parser = OptionParser(usage="%prog pid") parser.add_option( "--no-fork", "-n", help="Don't trace forks", action="store_false", dest="fork", default=True) if len(sys.argv) == 1: parser.print_help() exit(1) self.options, self.program = parser.parse_args() defaults = ( ('pid', int(sys.argv[1])), ('trace_exec', False), ('enter', False), ('profiler', False), ('type', False), ('name', False), ('string_length', 99999999), ('array_count', 20), ('raw_socketcall', False), ('output', ''), ('ignore_regex', ''), ('address', False), ('syscalls', None), ('socket', False), ('filename', False), ('show_pid', False), ('list_syscalls', False), ('show_ip', False), ('debug', False), ('verbose', False), ('quiet', False), ) for option, value in defaults: setattr(self.options, option, value) self.only = set() self.ignore_regex = None if self.options.fork: self.options.show_pid = True self.processOptions() def ignoreSyscall(self, syscall): name = syscall.name if self.only and (name not in self.only): return True if self.ignore_regex and self.ignore_regex.match(name): return True return False def displaySyscall(self, syscall): if syscall.name == 'write': fd = syscall.arguments[0].value if fd < 3: text = syscall.arguments[1].getText()[1:-1] stdout.write(text.decode('string_escape')) stdout.flush() def syscallTrace(self, process): # First query to break at next syscall self.prepareProcess(process) while True: # No more process? Exit if not self.debugger: break # Wait until next syscall enter try: event = self.debugger.waitSyscall() process = event.process except ProcessExit as event: self.processExited(event) continue except ProcessSignal as event: # event.display() process.syscall(event.signum) continue except NewProcessEvent as event: self.newProcess(event) continue except ProcessExecution as event: self.processExecution(event) continue # Process syscall enter or exit self.syscall(process) def syscall(self, process): state = process.syscall_state syscall = state.event(self.syscall_options) if syscall and (syscall.result is not None or self.options.enter): self.displaySyscall(syscall) # Break at next syscall process.syscall() def processExited(self, event): # Display syscall which has not exited state = event.process.syscall_state if (state.next_event == "exit" and (not self.options.enter) and state.syscall): self.displaySyscall(state.syscall) def prepareProcess(self, process): process.syscall() process.syscall_state.ignore_callback = self.ignoreSyscall def newProcess(self, event): process = event.process self.prepareProcess(process) process.parent.syscall() def processExecution(self, event): process = event.process process.syscall() def runDebugger(self): # Create debugger and traced process self.setupDebugger() process = self.createProcess() if not process: return self.syscall_options = FunctionCallOptions( write_types=self.options.type, write_argname=self.options.name, string_max_length=self.options.string_length, replace_socketcall=not self.options.raw_socketcall, write_address=self.options.address, max_array_count=self.options.array_count, ) self.syscall_options.instr_pointer = self.options.show_ip self.syscallTrace(process) def main(self): self._main() if self._output is not None: self._output.close() def _main(self): self.debugger = PtraceDebugger() try: self.runDebugger() except ProcessExit as event: self.processExited(event) except PtraceError as err: error("ptrace() error: %s" % err) except KeyboardInterrupt: error("Interrupted.") except PTRACE_ERRORS as err: writeError(getLogger(), err, "Debugger error") processes = list(self.debugger.list) self.debugger.quit() # python-ptrace seems iffy on actually detaching sometimes, so we make # sure we let the process continue for process in processes: subprocess.check_output(['kill', '-cont', str(process.pid)]) def createChild(self, program): pid = Application.createChild(self, program) error("execve(%s, %s, [/* 40 vars */]) = %s" % ( program[0], program, pid)) return pid
class SSHKeyExtractor(object): def __init__(self, pid): self.dbg = PtraceDebugger() self.pid = pid self.proc = psutil.Process(pid) self.network_connections = self.proc.connections() self.dbg_proc = None self.heap_map_info = None self.mem_maps = None def __repr__(self): return "<SSHKeyExtractor: {}>".format(self.pid) def _get_mem_maps(self): mem_map = open("/proc/{}/maps".format(self.pid)).read() for line in mem_map.splitlines(): regex = r"(\w+)-(\w+)\ ([\w\-]+) (\w+) ([\w\:]+) (\w+) +(.*)" match = re.search(regex, line) yield MemoryRegion(match.groups()) def _get_heap_map_info(self): #print repr(self.mem_maps) for mem_map in self.mem_maps: if mem_map.path == "[heap]": return mem_map return None def is_valid_ptr(self, ptr, allow_nullptr=True, heap_only=True): if (ptr == 0 or ptr is None): if allow_nullptr: return True else: return False if heap_only: return ptr >= self.heap_map_info.start and ptr < self.heap_map_info.end for mem_map in self.mem_maps: valid = ptr >= mem_map.start and ptr < mem_map.end if valid: return True return False def lookup_enc(self, name): return OPENSSH_ENC_ALGS_LOOKUP.get(name, None) def read_string(self, ptr, length): val = self.dbg_proc.readCString(ptr, length) if val: return val[0].decode("utf-8", errors="ignore") return None def probe_sshenc_block(self, ptr, sshenc_size): """ char *name ; 0x808e8a4 -> "*****@*****.**" Cipher *cipher; 0x808e6d0 Cipher{name=0x80989e4 -> "*****@*****.**"} int enabled; 0 u_int key_len; 8-64 *u_int iv_len; 12 u_int block_size; 8-16 u_char *key; 0x80989e4 -> "6e4a242303346ecd60209e41b03c438b" u_char *iv; 0x8088f4e -> "7e59454fbe2247d52d29bd373c3f53ae" """ mem = self.dbg_proc.readBytes(ptr, sshenc_size) enc = sshenc_62p1.from_buffer_copy(mem) sshenc_name = self.is_valid_ptr(enc.name, allow_nullptr=False) sshenc_cipher = self.is_valid_ptr(enc.cipher, allow_nullptr=False, heap_only=False) if not (sshenc_name and sshenc_cipher): return None name_str = self.read_string(enc.name, 64) enc_properties = self.lookup_enc(name_str) #print(repr(name_str), enc_properties) if not enc_properties: return None expected_key_len = enc_properties[2] key_len_valid = expected_key_len == enc.key_len if not key_len_valid: return None cipher = self.dbg_proc.readStruct(enc.cipher, sshcipher) cipher_name_valid = self.is_valid_ptr(cipher.name, allow_nullptr=False, heap_only=False) if not cipher_name_valid: return None cipher_name = self.read_string(cipher.name, 64) if cipher_name != name_str: return None #print(cipher_name) #At this point we know pretty certain this is the sshenc struct. Let's figure out which version... expected_block_size = enc_properties[1] block_size_valid = expected_block_size == enc.block_size if not block_size_valid: enc = sshenc_61p1.from_buffer_copy(mem) block_size_valid = expected_block_size == enc.block_size if not block_size_valid: # !@#$ we can't seem to properly align the structure return None sshenc_key = self.is_valid_ptr(enc.key, allow_nullptr=False) sshenc_iv = self.is_valid_ptr(enc.iv, allow_nullptr=False) if sshenc_iv and sshenc_key: return enc return None def construct_scraped_key(self, ptr, enc): key = ScrapedKey(self.pid, self.proc.name(), enc, ptr) key.network_connections = self.network_connections key.cipher_name = self.read_string(enc.name, 64) key_raw = self.dbg_proc.readBytes(enc.key, enc.key_len) key.key = key_raw.hex() if isinstance(enc, sshenc_61p1): iv_len = enc.block_size else: iv_len = enc.iv_len iv_raw = self.dbg_proc.readBytes(enc.iv, iv_len) key.iv = iv_raw.hex() return key def align_size(self, size, multiple): add = multiple - (size % multiple) return size + add def extract(self, known_addr=None): known_addr = known_addr or [] ret = [] self.dbg_proc = self.dbg.addProcess(self.pid, False) self.dbg_proc.cont() self.mem_maps = list(self._get_mem_maps()) self.heap_map_info = self._get_heap_map_info() ptr = self.heap_map_info.start sshenc_size = max(sizeof(sshenc_61p1), sizeof(sshenc_62p1)) while ptr + sshenc_size < self.heap_map_info.end: if ptr in known_addr: sshenc_aligned_size = self.align_size(sshenc_size, 4) ptr += sshenc_aligned_size #print 'skip 0x{:x}, {}'.format(ptr, sshenc_aligned_size) continue sshenc = self.probe_sshenc_block(ptr, sshenc_size) if sshenc: key = self.construct_scraped_key(ptr, sshenc) ret.append(key) ptr += 4 return ret def cleanup(self): if self.dbg_proc: from signal import SIGTRAP, SIGSTOP, SIGKILL if self.dbg_proc.read_mem_file: self.dbg_proc.read_mem_file.close() self.dbg_proc.kill(SIGSTOP) self.dbg_proc.waitSignals(SIGTRAP, SIGSTOP) self.dbg_proc.detach() if self.dbg: self.dbg.deleteProcess(self.dbg_proc) self.dbg.quit() del self.dbg_proc del self.dbg self.proc = None
class Gdb(Application): def __init__(self): Application.__init__(self) # Parse self.options self.parseOptions() # Setup output (log) self.setupLog() self.last_signal = {} # We assume user wants all possible information self.syscall_options = FunctionCallOptions( write_types=True, write_argname=True, write_address=True, ) # FIXME: Remove self.breaks! self.breaks = dict() self.followterms = [] def setupLog(self): self._setupLog(stdout) def parseOptions(self): parser = OptionParser(usage="%prog [options] -- program [arg1 arg2 ...]") self.createCommonOptions(parser) self.createLogOptions(parser) self.options, self.program = parser.parse_args() if self.options.pid is None and not self.program: parser.print_help() exit(1) self.processOptions() self.show_pid = self.options.fork def _continueProcess(self, process, signum=None): if not signum and process in self.last_signal: signum = self.last_signal[process] if signum: error("Send %s to %s" % (signalName(signum), process)) process.cont(signum) try: del self.last_signal[process] except KeyError: pass else: process.cont() def cont(self, signum=None): for process in self.debugger: process.syscall_state.clear() if process == self.process: self._continueProcess(process, signum) else: self._continueProcess(process) # Wait for a process signal signal = self.debugger.waitSignals() process = signal.process # Hit breakpoint? if signal.signum == SIGTRAP: ip = self.process.getInstrPointer() if not CPU_POWERPC: # Go before "INT 3" instruction ip -= 1 breakpoint = self.process.findBreakpoint(ip) if breakpoint: error("Stopped at %s" % breakpoint) breakpoint.desinstall(set_ip=True) else: self.processSignal(signal) return None def readRegister(self, regs): name = regs.group(0)[1:] value = self.process.getreg(name) return str(value) def parseInteger(self, text): # Remove spaces and convert to lower case text = text.strip() if " " in text: raise ValueError("Space are forbidden: %r" % text) text = text.lower() # Replace registers by their value orig_text = text text = REGISTER_REGEX.sub(self.readRegister, text) # Replace hexadecimal numbers by decimal numbers def readHexadecimal(regs): text = regs.group(0) if text.startswith("0x"): text = text[2:] elif not re.search("[a-f]", text): return text value = int(text, 16) return str(value) text = re.sub(r"(?:0x)?[0-9a-f]+", readHexadecimal, text) # Reject invalid characters if not re.match(r"^[()<>+*/&0-9-]+$", text): raise ValueError("Invalid expression: %r" % orig_text) # Use integer division (a//b) instead of float division (a/b) text = text.replace("/", "//") # Finally, evaluate the expression is_pointer = text.startswith("*") if is_pointer: text = text[1:] try: value = eval(text) value = truncateWord(value) except SyntaxError: raise ValueError("Invalid expression: %r" % orig_text) if is_pointer: value = self.process.readWord(value) return value def parseIntegers(self, text): values = [] for item in text.split(): item = item.strip() value = self.parseInteger(item) values.append(value) return values def parseBytes(self, text): # FIXME: Validate input # if not BYTES_REGEX.match(text): # raise ValueError('Follow text must be enclosed in quotes!') if PY3: text = 'b' + text.lstrip() value = eval(text) if not isinstance(value, binary_type): raise TypeError("Input is not a bytes string!") return value def addFollowTerm(self, text): # Allow terms of the form 'string', "string", '\x04', "\x01\x14" term = self.parseBytes(text) self.followterms.append(term) def showFollowTerms(self): print(self.followterms) def _xray(self): for term in self.followterms: for process in self.debugger: for procmap in readProcessMappings(process): for address in procmap.search(term): yield (process, procmap, address, term) # displays the offsets of all terms found in the process memory mappings # along with possible addresses of pointers pointing to these terms def xray(self): for process, procmap, address, term in self._xray(): pointers = " ".join(formatAddress(ptr_addr) for ptr_addr in getPointers(process, address)) print("term[%s] pid[%i] %s %s pointers: %s" % ( repr(term), process.pid, procmap, formatAddress(address), pointers)) def execute(self, command): errmsg = None if command == "cont": errmsg = self.cont() elif command == "proc": self.procInfo() elif command == "proclist": self.procList() elif command.startswith("attach "): errmsg = self.attachProcess(command[7:]) elif command == "regs": self.process.dumpRegs() elif command == "stack": self.process.dumpStack() elif command == "backtrace": errmsg = self.backtrace() elif command == "where" or command.startswith("where "): errmsg = self.where(command[6:]) elif command == "where2" or command.startswith("where2 "): errmsg = self.where(command[7:], manage_bp=True) elif command == "maps": self.process.dumpMaps() elif command == "dbginfo": self.debuggerInfo() elif command == "step": errmsg = self.step(False) elif command == "stepi": errmsg = self.step(True) elif command == "sys": errmsg = self.syscallTrace() elif command == "help": self.help() elif command.startswith("set "): errmsg = self.set(command) elif command.startswith("until "): errmsg = self.until(command[6:]) elif command.startswith("switch") or command == "switch": errmsg = self.switch(command[6:]) elif command.startswith("break "): errmsg = self.breakpoint(command[6:]) elif command.startswith("breakpoints"): self.displayBreakpoints() elif command.startswith("signals"): self.displaySignals() elif command.startswith("delete "): errmsg = self.delete(command[7:]) elif command.startswith("hexdump "): errmsg = self.hexdump(command[8:]) elif command.startswith("signal "): errmsg = self.signal(command[7:]) elif command.startswith("print "): errmsg = self.print_(command[6:]) elif command.startswith("follow "): errmsg = self.addFollowTerm(command[7:]) elif command == "showfollow": self.showFollowTerms() elif command == "resetfollow": self.followterms = [] elif command == "xray": self.xray() else: errmsg = "Unknown command: %r" % command if errmsg: print(errmsg, file=stderr) return False return True def parseSignum(self, command): try: return SIGNALS[command] except KeyError: pass try: return SIGNALS["SIG"+command] except KeyError: pass try: return self.parseInteger(command) except ValueError as err: raise ValueError("Invalid signal number: %r" % command) def signal(self, command): try: signum = self.parseSignum(command) except ValueError as err: return str(err) last_process = self.process try: errmsg = self.cont(signum) return errmsg finally: try: del self.last_signal[last_process] except KeyError: pass def print_(self, command): try: value = self.parseInteger(command) except ValueError as err: return str(err) error("Decimal: %s" % value) error("Hexadecimal: %s" % formatWordHex(value)) for map in self.process.readMappings(): if value not in map: continue error("Address is part of mapping: %s" % map) return None def hexdump(self, command): max_line = 20 width = (terminalWidth() - len(formatAddress(1)) - 3) // 4 width = max(width, 1) limited = None parts = command.split(" ", 1) if 1 < len(parts): try: start_address = self.parseInteger(parts[0]) end_address = self.parseInteger(parts[1]) if end_address <= start_address: raise ValueError('End address (%s) is smaller than start address(%s)!' % (formatAddress(end_address), formatAddress(start_address))) except ValueError as err: return str(err) size = end_address - start_address max_size = width*max_line if max_size < size: limited = max_size end_address = start_address + max_size else: try: start_address = self.parseInteger(command) except ValueError as err: return str(err) end_address = start_address + 5*width read_error = None address = start_address while address < end_address: size = min(end_address - address, width) try: # Read bytes memory = self.process.readBytes(address, size) # Format bytes hexa = formatHexa(memory) hexa = hexa.ljust(width*3-1, ' ') ascii = formatAscii(memory) ascii = ascii.ljust(width, ' ') # Display previous read error, if any if read_error: warning("Warning: Unable to read memory %s" % ( formatAddressRange(*read_error))) read_error = None # Display line error("%s| %s| %s" % (formatAddress(address), hexa, ascii)) except PtraceError: if not read_error: read_error = [address, address + size] else: read_error[1] = address + size address += size # Display last read error, if any if read_error: warning("Warning: Unable to read memory %s" % ( formatAddressRange(*read_error))) if limited: warning("(limit to %s bytes)" % max_size) return None def backtrace(self): trace = self.process.getBacktrace() for func in trace: error(func) if trace.truncated: error("--limited to depth %s--" % len(trace)) return None def where(self, command, manage_bp=False): start = None stop = None try: values = self.parseIntegers(command) except ValueError as err: return str(err) if 1 <= len(values): start = values[0] if 2 <= len(values): stop = values[1] self.process.dumpCode(start, stop, manage_bp=manage_bp) return None def procInfo(self): dumpProcessInfo(error, self.process.pid, max_length=160) def procList(self): for process in self.debugger: text = str(process) if self.process == process: text += " (active)" error(text) def set(self, command): try: key, value = command[4:].split("=", 1) key = key.strip().lower() if not key.startswith("$"): return 'Register name (%s) have to start with "$"' % key key = key[1:] except ValueError as err: return "Invalid command: %r" % command try: value = self.parseInteger(value) except ValueError as err: return str(err) try: self.process.setreg(key, value) except ProcessError as err: return "Unable to set $%s=%s: %s" % (key, value, err) error("Set $%s to %s" % (key, value)) return None def displayInstr(self, prefix): try: if HAS_DISASSEMBLER: instr = self.process.disassembleOne() error("%s %s: %s" % ( prefix, formatAddress(instr.address), instr.text)) else: self.process.dumpCode() except PtraceError as err: error("Unable to read current instruction: %s" % err) def attachProcess(self, text): try: pid = self.parseInteger(text) except ValueError as err: return str(err) process = self.debugger.addProcess(pid, False) self.switchProcess(process) def step(self, enter_call, address=None): if address is None: self.displayInstr("Execute") if (not HAS_PTRACE_SINGLESTEP) or (not enter_call): if address is None: address = self.process.getInstrPointer() size = self.readInstrSize(address, default_size=None) if not size: return "Unable to read instruction size at %s" \ % formatAddress(address) address += size size = self.readInstrSize(address) # Set a breakpoint breakpoint = self.process.createBreakpoint(address, size) # Continue the process self.process.cont() else: # Use ptrace single step command self.process.singleStep() breakpoint = None # Execute processus until next TRAP try: self.process.waitSignals(SIGTRAP) if breakpoint: breakpoint.desinstall(set_ip=True) except: if breakpoint: breakpoint.desinstall() raise return None def newProcess(self, event): error("New process: %s" % event.process) # FIXME: This function doesn't work multiple multiple processes # especially when a parent waits for a child def syscallTrace(self): # Trace until syscall enter self.process.syscall() self.process.waitSyscall() # Process the syscall event state = self.process.syscall_state syscall = state.event(self.syscall_options) # Display syscall if syscall: if syscall.result is not None: text = "%s = %s" % (syscall.format(), syscall.result_text) if self.show_pid: text = "Process %s exits %s" % (syscall.process.pid, text) error(text) else: text = syscall.format() if self.show_pid: text = "Process %s enters %s" % (syscall.process.pid, text) error(text) return None def until(self, command): try: address = self.parseInteger(command) except ValueError as err: return str(err) errmsg = self.step(False, address) if errmsg: return errmsg self.displayInstr("Current") return None def switch(self, command): if not command: process_list = self.debugger.list if len(process_list) == 1: return "There is only one process!" index = process_list.index(self.process) index = (index + 1) % len(process_list) process = process_list[index] self.switchProcess(process) return try: pid = self.parseInteger(command) except ValueError as err: return str(err) try: process = self.debugger[pid] self.switchProcess(process) except KeyError: return "There is not process %s" % pid return None def switchProcess(self, process): if self.process == process: return error("Switch to %s" % process) self.process = process def nextProcess(self): try: process = next(iter(self.debugger)) self.switchProcess(process) except StopIteration: pass def displayBreakpoints(self): found = False for process in self.debugger: for bp in process.breakpoints.values(): found = True error("%s:%s" % (process, bp)) if not found: error("(no breakpoint)") def displaySignals(self): signals = list(SIGNAMES.items()) signals.sort(key=lambda key_value: key_value[0]) for signum, name in signals: error("% 2s: %s" % (signum, name)) def readInstrSize(self, address, default_size=None): if not HAS_DISASSEMBLER: return default_size try: # Get address and size of instruction at specified address instr = self.process.disassembleOne(address) return instr.size except PtraceError as err: warning("Warning: Unable to read instruction size at %s: %s" % ( formatAddress(address), err)) return default_size def breakpoint(self, command): try: address = self.parseInteger(command) except ValueError as err: return str(err) # Create breakpoint size = self.readInstrSize(address) try: bp = self.process.createBreakpoint(address, size) except PtraceError as err: return "Unable to set breakpoint at %s: %s" % ( formatAddress(address), err) error("New breakpoint: %s" % bp) return None def delete(self, command): try: address = self.parseInteger(command) except ValueError as err: return str(err) breakpoint = self.process.findBreakpoint(address) if not breakpoint: return "No breakpoint at %s " % formatAddress(address) breakpoint.desinstall() error("%s deleted" % breakpoint) return None def help(self): for command, description in COMMANDS: error("%s: %s" % (command, description)) error('') error("Value can be an hexadecimal/decimal number or a register name ($reg)") error("You can use operators a+b, a-b, a*b, a/b, a<<b, a>>b, a**b, and parenthesis in expressions") error('Use ";" to write multiple commands on the same line (eg. "step; print $eax")') def processSignal(self, event): event.display() self.switchProcess(event.process) self.last_signal[self.process] = event.signum error("%s interrupted by %s" % (self.process, event.name)) def processExecution(self, event): error(event) self.switchProcess(event.process) self.interrupt() def debuggerInfo(self): error("Debugger process ID: %s" % getpid()) error("python-ptrace version %s" % VERSION) error("Website: %s" % WEBSITE) def interrupt(self): waitlist = [] for process in self.debugger: if process.is_stopped: continue try: if process.isTraced(): continue except NotImplementedError: pass warning("Interrupt %s (send SIGINT)" % process) process.kill(SIGINT) waitlist.append(process) for process in waitlist: info("Wait %s interruption" % process) try: process.waitSignals(SIGINT) except ProcessSignal as event: event.display() except KeyboardInterrupt: pass def deleteProcess(self, pid): try: process = self.debugger[pid] except KeyError: return event = process.processTerminated() error(str(event)) if process == self.process: self.nextProcess() def restoreTerminal(self): if enableEchoMode(): error("Terminal: restore echo mode") def mainLoop(self): # Read command try: self.restoreTerminal() command = raw_input(self.invite).strip() except EOFError: print() return True except KeyboardInterrupt: error("User interrupt!") self.interrupt() return False # If command is empty, reuse previous command if not command: if self.previous_command: command = self.previous_command info("Replay previous command: %s" % command) else: return False self.previous_command = None # User wants to quit? if command == "quit": return True # Execute the user command try: command_str = command ok = True for command in command_str.split(";"): command = command.strip() try: ok &= self.execute(command) except Exception as err: print("Command error: %s" % err) raise ok = False if not ok: break if ok: self.previous_command = command_str except KeyboardInterrupt: self.interrupt() except NewProcessEvent as event: self.newProcess(event) except ProcessSignal as event: self.processSignal(event) except ProcessExit as event: error(event) self.nextProcess() except ProcessExecution as event: self.processExecution(event) except PtraceError as err: error("ERROR: %s" % err) if err.errno == ESRCH: self.deleteProcess(err.pid) return False def runDebugger(self): self.setupDebugger() # Create new process try: self.process = self.createProcess() except ChildError as err: writeError(getLogger(), err, "Unable to create child process") return if not self.process: return # Trace syscalls self.invite = '(gdb) ' self.previous_command = None while True: if not self.debugger: # There is no more process: quit return done = self.mainLoop() if done: return def main(self): self.debugger = PtraceDebugger() try: self.runDebugger() except KeyboardInterrupt: error("Interrupt debugger: quit!") except PTRACE_ERRORS as err: writeError(getLogger(), err, "Debugger error") self.process = None self.debugger.quit() error("Quit gdb.") self.restoreTerminal()
class SyscallTracer(Application): def __init__(self, options, program, ignore_syscall_callback, syscall_callback, event_callback, quit_callback): Application.__init__(self) # Parse self.options self.options = options self.program = program self.processOptions() self.ignore_syscall_callback = ignore_syscall_callback self.syscall_callback = syscall_callback self.event_callback = event_callback self.quit_callback = quit_callback def runDebugger(self): # Create debugger and traced process self.setupDebugger() process = self.createProcess() if not process: return self.syscall_options = FunctionCallOptions( write_types=True, write_argname=True, replace_socketcall=False, string_max_length=300, write_address=False, max_array_count=20, ) self.syscall_options.instr_pointer = self.options.show_ip self.syscallTrace(process) def displaySyscall(self, syscall): self.syscall_callback(syscall) def syscall(self, process): state = process.syscall_state syscall = state.event(self.syscall_options) if syscall and (syscall.result is not None or self.options.enter): self.displaySyscall(syscall) # Break at next syscall process.syscall() def syscallTrace(self, process): # First query to break at next syscall self.prepareProcess(process) while True: # No more process? Exit if not self.debugger: break # Wait until next syscall enter try: event = self.debugger.waitSyscall() process = event.process except ProcessExit as event: self.processExited(event) continue except ProcessSignal as event: self.event_callback(event) #event.display() process.syscall(event.signum) continue except NewProcessEvent as event: self.event_callback(event) process = event.process self.prepareProcess(process) process.parent.syscall() continue except ProcessExecution as event: self.event_callback(event) process = event.process process.syscall() continue # Process syscall enter or exit self.syscall(process) def prepareProcess(self, process): process.syscall() process.syscall_state.ignore_callback = self.ignore_syscall_callback def processExited(self, event): # Display syscall which has not exited state = event.process.syscall_state if (state.next_event == "exit") and (not self.options.enter) and state.syscall: self.displaySyscall(state.syscall) self.event_callback(event) def main(self): self.debugger = PtraceDebugger() try: self.runDebugger() except ChildError as event: self.event_callback(event) except ProcessExit as event: self.processExited(event) except (KeyError, PtraceError, OSError) as error: self._handle_exceptions_during_quit(error, 'main') if self.debugger: self.debugger.quit() self.quit_callback() def quit(self): try: self.debugger.quit() except (KeyError, PtraceError, OSError) as error: self._handle_exceptions_during_quit(error, 'quit') self.quit_callback() self.debugger = None def _handle_exceptions_during_quit(self, exception, context): if isinstance(exception, KeyError): # When the debugger is waiting for a syscall and the debugger # process is closed with quit() a KeyError Exception for missing # PID is fired pass elif isinstance(exception, PtraceError): print "PtraceError from %s" % context, exception elif isinstance(exception, OSError): print 'OSError from %s' % context, exception else: print 'Unexpected exception from %s' % context
class SyscallTracer(Application): def __init__(self, options, program, ignore_syscall_callback, syscall_callback, event_callback, quit_callback): Application.__init__(self) # Parse self.options self.options = options self.program=program self.processOptions() self.ignore_syscall_callback = ignore_syscall_callback self.syscall_callback = syscall_callback self.event_callback = event_callback self.quit_callback = quit_callback def runDebugger(self): # Create debugger and traced process self.setupDebugger() process = self.createProcess() if not process: return self.syscall_options = FunctionCallOptions( write_types=True, write_argname=True, replace_socketcall=False, string_max_length=300, write_address=False, max_array_count=20, ) self.syscall_options.instr_pointer = self.options.show_ip self.syscallTrace(process) def displaySyscall(self, syscall): self.syscall_callback(syscall) def syscall(self, process): state = process.syscall_state syscall = state.event(self.syscall_options) if syscall and (syscall.result is not None or self.options.enter): self.displaySyscall(syscall) # Break at next syscall process.syscall() def syscallTrace(self, process): # First query to break at next syscall self.prepareProcess(process) while True: # No more process? Exit if not self.debugger: break # Wait until next syscall enter try: event = self.debugger.waitSyscall() process = event.process except ProcessExit as event: self.processExited(event) continue except ProcessSignal as event: self.event_callback(event) #event.display() process.syscall(event.signum) continue except NewProcessEvent as event: self.event_callback(event) process = event.process self.prepareProcess(process) process.parent.syscall() continue except ProcessExecution as event: self.event_callback(event) process = event.process process.syscall() continue # Process syscall enter or exit self.syscall(process) def prepareProcess(self, process): process.syscall() process.syscall_state.ignore_callback = self.ignore_syscall_callback def processExited(self, event): # Display syscall which has not exited state = event.process.syscall_state if (state.next_event == "exit") and (not self.options.enter) and state.syscall: self.displaySyscall(state.syscall) self.event_callback(event) def main(self): self.debugger = PtraceDebugger() try: self.runDebugger() except ChildError as event: self.event_callback(event) except ProcessExit as event: self.processExited(event) except (KeyError, PtraceError, OSError) as error: self._handle_exceptions_during_quit(error, 'main') if self.debugger: self.debugger.quit() self.quit_callback() def quit(self): try: self.debugger.quit() except (KeyError, PtraceError, OSError) as error: self._handle_exceptions_during_quit(error, 'quit') self.quit_callback() self.debugger = None def _handle_exceptions_during_quit(self, exception, context): if isinstance(exception, KeyError): # When the debugger is waiting for a syscall and the debugger process # is closed with quit() a KeyError Exception for missing PID is fired pass elif isinstance(exception, PtraceError): print "PtraceError from %s" % context, exception elif isinstance(exception, OSError): print 'OSError from %s' % context, exception else: print 'Unexpected exception from %s' % context
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()
class SyscallTracer(Application): def __init__(self): Application.__init__(self) # Parse self.options self.parseOptions() # Setup output (log) self.setupLog() def setupLog(self): if self.options.output: fd = open(self.options.output, 'w') self._output = fd else: fd = stderr self._output = None self._setupLog(fd) def parseOptions(self): parser = OptionParser(usage="%prog pid") parser.add_option("--no-fork", "-n", help="Don't trace forks", action="store_false", dest="fork", default=True) if len(sys.argv) == 1: parser.print_help() exit(1) self.options, self.program = parser.parse_args() defaults = ( ('pid', int(sys.argv[1])), ('trace_exec', False), ('enter', False), ('profiler', False), ('type', False), ('name', False), ('string_length', 99999999), ('array_count', 20), ('raw_socketcall', False), ('output', ''), ('ignore_regex', ''), ('address', False), ('syscalls', None), ('socket', False), ('filename', False), ('show_pid', False), ('list_syscalls', False), ('show_ip', False), ('debug', False), ('verbose', False), ('quiet', False), ) for option, value in defaults: setattr(self.options, option, value) self.only = set() self.ignore_regex = None if self.options.fork: self.options.show_pid = True self.processOptions() def ignoreSyscall(self, syscall): name = syscall.name if self.only and (name not in self.only): return True if self.ignore_regex and self.ignore_regex.match(name): return True return False def displaySyscall(self, syscall): if syscall.name == 'write': fd = syscall.arguments[0].value if fd < 3: text = syscall.arguments[1].getText()[1:-1] stdout.write(text.decode('string_escape')) stdout.flush() def syscallTrace(self, process): # First query to break at next syscall self.prepareProcess(process) while True: # No more process? Exit if not self.debugger: break # Wait until next syscall enter try: event = self.debugger.waitSyscall() process = event.process except ProcessExit as event: self.processExited(event) continue except ProcessSignal as event: # event.display() process.syscall(event.signum) continue except NewProcessEvent as event: self.newProcess(event) continue except ProcessExecution as event: self.processExecution(event) continue # Process syscall enter or exit self.syscall(process) def syscall(self, process): state = process.syscall_state syscall = state.event(self.syscall_options) if syscall and (syscall.result is not None or self.options.enter): self.displaySyscall(syscall) # Break at next syscall process.syscall() def processExited(self, event): # Display syscall which has not exited state = event.process.syscall_state if (state.next_event == "exit" and (not self.options.enter) and state.syscall): self.displaySyscall(state.syscall) def prepareProcess(self, process): process.syscall() process.syscall_state.ignore_callback = self.ignoreSyscall def newProcess(self, event): process = event.process self.prepareProcess(process) process.parent.syscall() def processExecution(self, event): process = event.process process.syscall() def runDebugger(self): # Create debugger and traced process self.setupDebugger() process = self.createProcess() if not process: return self.syscall_options = FunctionCallOptions( write_types=self.options.type, write_argname=self.options.name, string_max_length=self.options.string_length, replace_socketcall=not self.options.raw_socketcall, write_address=self.options.address, max_array_count=self.options.array_count, ) self.syscall_options.instr_pointer = self.options.show_ip self.syscallTrace(process) def main(self): self._main() if self._output is not None: self._output.close() def _main(self): self.debugger = PtraceDebugger() try: self.runDebugger() except ProcessExit as event: self.processExited(event) except PtraceError as err: error("ptrace() error: %s" % err) except KeyboardInterrupt: error("Interrupted.") except PTRACE_ERRORS as err: writeError(getLogger(), err, "Debugger error") processes = list(self.debugger.list) self.debugger.quit() # python-ptrace seems iffy on actually detaching sometimes, so we make # sure we let the process continue for process in processes: subprocess.check_output(['kill', '-cont', str(process.pid)]) def createChild(self, program): pid = Application.createChild(self, program) error("execve(%s, %s, [/* 40 vars */]) = %s" % (program[0], program, pid)) return pid