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 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()