# # Copyright (C) 2016-2017, Alexandre Gazet. # # Copyright (C) 2012-2015, Quarkslab. # # This file is part of ret-sync. # # ret-sync is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # #!/usr/bin/env python # -*- coding: utf-8 -*- import os import sys import time import traceback import struct import binascii import base64 import ctypes import socket import ConfigParser try: import argparse except: print "[-] please make sure python's argparse module is available\n%s" % repr(sys.exc_info()) sys.exit(0) import idc import idaapi import idautils import ida_graph import ida_kernwin from idaapi import PluginForm # Enable/disable logging JSON received in the IDA output window DEBUG_JSON = False if sys.platform == 'win32': PYTHON_BIN = 'python.exe' PYTHON_PATH = os.path.normpath("C:\\Programs\\share\\Python27") elif sys.platform.startswith('linux') or sys.platform == 'darwin': PYTHON_BIN = 'python' PYTHON_PATH = os.path.normpath("/usr/bin") else: print "[-] please fix PYTHON_PATH & PYTHON_BIN values, %s platform currently unknown" % sys.platform sys.exit(0) if not os.path.exists(os.path.join(PYTHON_PATH, PYTHON_BIN)): print "[-] please fix PYTHON_PATH value" sys.exit(0) site_packages = os.path.join(PYTHON_PATH, "lib", "site-packages") if site_packages not in sys.path: sys.path.insert(0, site_packages) try: from PyQt5 import QtCore, QtWidgets from PyQt5.QtCore import QProcess, QProcessEnvironment except: print "[-] failed to import Qt libs from PyQt5\n%s" % repr(sys.exc_info()) sys.exit(0) try: import json except: print "[-] failed to import json\n%s" % repr(sys.exc_info()) sys.exit(0) # default value is current script's path BROKER_PATH = os.path.join(os.path.normpath(os.path.dirname(__file__)), "broker.py") if not os.path.exists(BROKER_PATH): print "[-] broker path is not properly set, current value: <%s>" % BROKER_PATH sys.exit(0) IDB_PATH = os.path.dirname(os.path.realpath(idc.GetIdbPath())) CONNECT_BROKER_MAX_ATTEMPT = 4 COL_GREEN = 0x33ff00 COL_DEEP_PURPLE = 0xff44dd COL_YLW = 0x23ffff COL_BLUE_NAVY = 0x000080 COL_GRAY = 0x808080 COL_CURLINE = COL_YLW #COL_CURLINE = COL_BLUE_NAVY # renders better with some themes COL_CBTRACE = COL_GREEN NETNODE_STORE = "$ SYNC_STORE" NETNODE_INDEX = 0xFFC0DEFF DBG_DIALECTS = { 'windbg': {'prefix': '!', 'si': 't', 'so': 'p', 'go': 'g', 'bp': 'bp ', 'hbp': 'ba e 1 ', 'bp1': 'bp /1 ', 'hbp1': 'ba e 1 /1 '}, 'gdb': {'prefix': '', 'si': 'si', 'so': 'ni', 'go': 'continue', 'bp': 'b *', 'hbp': 'hb *', 'bp1': 'tb *', 'hbp1': 'thb *'}, 'ollydbg2': {'prefix': '', 'si': 'si', 'so': 'so', 'go': 'go', 'bp': 'bp ', 'hbp': 'xxx ', 'bp1': 'xxx ', 'hbp1': 'xxx '}, 'x64_dbg': {'prefix': '', 'si': 'sti', 'so': 'sto', 'go': 'go', 'bp': 'bp ', 'hbp': 'bph ', 'bp1': 'xxx ', 'hbp1': 'xxx '}, } # -------------------------------------------------------------------------- class RequestHandler(object): # color callback def cb_color(self, ea): idaapi.set_item_color(ea, COL_CBTRACE) # instruction step callback def cb_curline(self, ea): if self.prev_loc: prev_ea, prev_color = self.prev_loc cur_color = idaapi.get_item_color(prev_ea) # race condition: block/instruction's color may have been modified # after it was saved if (cur_color != prev_color) and (cur_color != COL_CURLINE): prev_color = cur_color idaapi.set_item_color(prev_ea, prev_color) self.prev_loc = [ea, idaapi.get_item_color(ea)] idaapi.set_item_color(ea, COL_CURLINE) def cb_restore_last_line(self): if self.prev_loc: ea, col = self.prev_loc idaapi.set_item_color(ea, col) # support -a / --address switch def addr_switch(self, offset, msg): if (not msg) or (msg == ''): return [offset, msg] try: args = self.parser.parse_args(msg.split()) except: print "[*] failed to parse command" return [None, msg] # no address switch supplied if not args.address: return [offset, msg] try: addr = int(''.join(args.address), 16) except: print "[*] failed to parse address, should be hex" return [None, msg] # make sure the address points to a valid instruction/data head = idaapi.get_item_head(addr) if head != addr: print "[*] ambiguous address, did you mean 0x%x ?" % head return [None, msg] return [addr, ' '.join(args.msg)] # check if address is within a valid segment def is_safe(self, offset): return not (idc.SegStart(offset) == idaapi.BADADDR) # rebase address with respect to local image base def rebase(self, base, offset): if base is not None: # check for non-compliant debugger client if base > offset: print "[sync] unsafe addr" return None if not (self.base == base): offset = (offset - base) + self.base # update base address of remote module if self.base_remote != base: self.base_remote = base if not self.is_safe(offset): print "[sync] unsafe addr" return None return offset # rebase address with respect to remote image base def rebase_remote(self, offset): if not (self.base == self.base_remote): offset = (offset - self.base) + self.base_remote return offset # demangle names def demangle(self, name): mask = idc.GetLongPrm(INF_SHORT_DN) demangled = idc.Demangle(name, mask) if demangled is None: return name else: return demangled # prevent flooding debug engine with too much commands # sync plugin does NOT wait for any sort of ack # example: "^ Debuggee already running error in 'g'" def notice_anti_flood(self): time.sleep(0.1) # append comment and handle cmt's size limitation (near 1024) def append_cmt(self, ea, cmt, rptble=False): if len(cmt) > 1024: print "[*] warning, comment needs to be splitted (from 0x%x)" % ea nh = idaapi.next_head(ea, idaapi.BADADDR) if nh == idaapi.BADADDR: print "[*] failed to find next instruction candidate" return self.append_cmt(nh, cmt[1024:], rptble) cmt = cmt[:1024] idaapi.append_cmt(ea, cmt, rptble) # location request, update disassembly IDA view def req_loc(self, hash): offset, base = hash['offset'], hash.get('base') ea = self.rebase(base, offset) if not ea: return if(self.color): self.cb_color(ea) idaapi.jumpto(ea) self.cb_curline(ea) self.gm.center() # log command output request at addr def req_cmd(self, hash): msg_b64, offset, base = hash['msg'], hash['offset'], hash['base'] msg = base64.b64decode(msg_b64) ea = self.rebase(base, offset) if not ea: return print ("[*] cmd output added at 0x%x" % ea) self.append_cmt(ea, str(msg)) # reset comment at addr def req_rcmt(self, hash): msg, offset, base = hash['msg'], hash['offset'], hash['base'] offset, msg = self.addr_switch(offset, msg) if not offset: return ea = self.rebase(base, offset) if not ea: return idaapi.set_cmt(ea, str(''), False) print ("[*] reset comment at 0x%x" % ea) # add comment request at addr def req_cmt(self, hash): msg, offset, base = hash['msg'], hash['offset'], hash['base'] offset, msg = self.addr_switch(offset, msg) if not offset: return ea = self.rebase(base, offset) if not ea: return self.append_cmt(ea, str(msg)) print ("[*] comment added at 0x%x" % ea) # add a function comment at addr def req_fcmt(self, hash): msg, offset, base = hash['msg'], hash['offset'], hash['base'] offset, msg = self.addr_switch(offset, msg) if not offset: return ea = self.rebase(base, offset) if not ea: return func = idaapi.get_func(ea) if not func: print ("[*] could not find func for 0x%x" % ea) return idaapi.set_func_cmt(func, str(msg), False) print ("[*] function comment added at 0x%x" % ea) # add an address comment request at addr def req_raddr(self, hash): raddr, rbase, offset, base = hash['raddr'], hash['rbase'], hash['offset'], hash['base'] ea = self.rebase(base, offset) if not ea: return if self.base_remote != rbase: print("[*] could not rebase this address, not in module") return addr = self.rebase(rbase, raddr) if not addr: return self.append_cmt(ea, "0x%x (rebased from 0x%x)" % (addr, raddr)) print ("[*] comment added at 0x%x" % ea) # return current cursor in IDA Pro def req_cursor(self, hash): print("[*] request IDA Pro cursor position") addr = idc.ScreenEA() self.notice_broker("cmd", "\"cmd\":\"%s\"" % addr) return # patch memory at specified address using info from debugger def req_patch(self, hash): addr, value, length = hash['addr'], hash['value'], hash['len'] if length != 4 and length != 8: print("[x] unsupported length: %d" % length) return if length == 4: prev_value = Dword(addr) if MakeDword(addr) != 1: print("[x] MakeDword failed") if PatchDword(addr, value) != 1: print("[x] PatchDword failed") if not idc.OpOff(addr, 0, 0): print("[x] OpOff failed") elif length == 8: prev_value = Qword(addr) if MakeQword(addr) != 1: print("[x] MakeQword failed") if PatchQword(addr, value) != 1: print("[x] PatchQword failed") if not idc.OpOff(addr, 0, 0): print("[x] OpOff failed") print ("[*] patched 0x%x = 0x%x (previous was 0x%x)" % (addr, value, prev_value)) # return idb's symbol for a given address def req_rln(self, hash): raddr, rbase, offset, base = hash['raddr'], hash['rbase'], hash['offset'], hash['base'] print("[*] 0x%x - 0x%x - 0x%x - 0x%x" % (raddr, rbase, offset, base)) addr = self.rebase(rbase, raddr) if not addr: print("[*] could not rebase this address (0x%x)" % raddr) return sym = idaapi.get_func_name(addr) if sym: sym = self.demangle(sym) func = idaapi.get_func(addr) if not func: print ("[*] could not find func for 0x%x" % addr) return lck = idaapi.lock_func(func) limits = idaapi.area_t() if idaapi.get_func_limits(func, limits): if limits.startEA != addr: if (addr > limits.startEA): sym = "%s%s0x%x" % (sym, "+", addr - limits.startEA) else: sym = "%s%s0x%x" % (sym, "-", limits.startEA - addr) lck = None else: sym = idc.Name(addr) if sym: sym = self.demangle(sym) if sym: self.notice_broker("cmd", "\"cmd\":\"%s\"" % sym) print ("[*] resolved symbol: %s" % sym) else: print ("[*] could not resolve symbol for address 0x%x" % addr) # return address for a given idb's symbol def req_rrln(self, hash): sym, rbase, offset, base = hash['sym'], hash['rbase'], hash['offset'], hash['base'] print("[*] %s - 0x%x - 0x%x - 0x%x" % (sym, rbase, offset, base)) addr = idc.LocByName(sym) if addr: self.notice_broker("cmd", "\"cmd\":\"%s\"" % addr) print ("[*] resolved address: %s" % addr) else: print ("[*] could not resolve address for symbol %s" % sym) # add label request at addr def req_lbl(self, hash): msg, offset, base = hash['msg'], hash['offset'], hash['base'] offset, msg = self.addr_switch(offset, msg) if not offset: return ea = self.rebase(base, offset) if not ea: return flags = False if str(msg).startswith('@@'): flags = idaapi.SN_LOCAL idaapi.set_name(ea, str(msg), flags) print ("[*] label added at 0x%x" % ea) # color request at addr def req_bc(self, hash): global COL_CBTRACE msg, offset, base = hash['msg'], hash['offset'], hash['base'] if self.is_active: ea = self.rebase(base, offset) if not ea: return else: ea = self.base if (msg == 'oneshot'): print ("[*] color oneshot added at 0x%x" % ea) # mark address as being colored self.prev_loc = [ea, COL_CBTRACE] elif (msg == 'on'): print ("[*] color start from 0x%x" % ea) self.color = True self.prev_loc = [ea, COL_CBTRACE] elif (msg == 'off'): print ("[*] color end at 0x%x" % ea) self.color = False elif (msg == 'set'): new_col = hash['rgb'] if new_col > 0xffffff: print ("[*] restoring color") new_col = COL_GREEN COL_CBTRACE = new_col print ("[*] set color to 0x%x" % COL_CBTRACE) else: print ("[*] invalid color request (%s)" % msg) # reload .bpcmds from idb def req_bps_get(self, hash): print ("[-] reload .bpcmds") node = idaapi.netnode(NETNODE_INDEX) if not node: print ("[-] failed to open netnode store") self.notice_broker("cmd", "\"cmd\":\"no blob\"") return node.create(NETNODE_STORE) blob = node.getblob(0, str(chr(1))) if not blob: print (" -> no blob") self.notice_broker("cmd", "\"cmd\":\" -> reloading .bpcmds: no blob\"") return self.notice_broker("cmd", "\"cmd\":\"%s\"" % blob) return # save .bpcmds to idb def req_bps_set(self, hash): blob = hash['msg'] print ("[-] save .bpcmds") node = idaapi.netnode(NETNODE_INDEX) if not node: print ("[-] failed to open netnode store") self.notice_broker("cmd", "\"cmd\":\" -> failed to save .bpcmds") return new = node.create(NETNODE_STORE) if new == 0: print (" -> creating new netnode store") out = node.setblob(str(blob), 0, str(chr(1))) self.notice_broker("cmd", "\"cmd\":\" -> .bpcmds saved\"") return # compare loaded module md5 with idb's input file md5 def req_modcheck(self, hash): md5, pdb = hash.get('md5'), hash.get('pdb') remote = None if md5: print ("[*] modcheck idb (md5)") local = idc.GetInputMD5() remote = (''.join(str(md5).encode("ascii").split())).upper() elif pdb: print ("[*] modcheck idb (pdb guid)") msg = base64.b64decode(pdb) local = DbgDirHlpr.read_rsds_codeview() remote = DbgDirHlpr.parse_itoldyouso_output(msg) print (" -> remote: <%s>" % remote) print (" -> local : <%s>" % local) if remote == "0": res = "[!] warning, no Debug Directory" elif local == remote: res = "[+] module successfully matched" else: res = "[!] warning, modules mismatch" print res self.notice_broker("cmd", "\"cmd\":\"%s\"" % res) return # specify debugger dialect used to send commands def req_set_dbg_dialect(self, hash): global SyncForm dialect = hash['dialect'] if dialect in DBG_DIALECTS: self.dbg_dialect = DBG_DIALECTS[dialect] print "[sync] set debugger dialect to %s, enabling hotkeys" % dialect SyncForm.init_hotkeys() else: SyncForm.uninit_hotkeys() # request from broker def req_broker(self, hash): subtype = hash['subtype'] if (subtype == 'msg'): # simple message announcement print ("[*] << broker << %s" % hash['msg']) elif(subtype == 'notice'): # notice from broker self.broker_port = int(hash['port']) print ("[*] << broker << listening on port %d" % self.broker_port) for attempt in range(CONNECT_BROKER_MAX_ATTEMPT): try: host = socket.gethostbyname('localhost') self.broker_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.broker_sock.connect((host, self.broker_port)) break except: print "[sync] failed to connect to broker" print sys.exc_info() if self.broker_sock: self.broker_sock.close() self.broker_sock = None time.sleep(0.1) if (attempt == (CONNECT_BROKER_MAX_ATTEMPT - 1)): self.announcement("[sync] failed to connect to broker (attempt %d)" % attempt) sys.exit() # enable/disable idb, if disable it drops most sync requests elif(subtype == 'enable_idb'): self.is_active = True print "[sync] idb is enabled" elif(subtype == 'disable_idb'): self.is_active = False self.cb_restore_last_line() print "[sync] idb is disabled" # parse and execute request # Note that sometimes we don't receive the whole request from the broker.py # so parsing fails. One way for fixing this would be to fix broker.py to get # everything until "\n" before proxying it but the way we do here is to read # everything until "}" is received (end of json) def parse_exec(self, req): if self.prev_req: if self.prev_req != "": if DEBUG_JSON: print "[+] JSON merge with request: \"%s\"" % req req = self.prev_req + req self.prev_req = "" if req == '': return if DEBUG_JSON: print("parse_exec -> " + str(req)) if not (req[0:6] == '[sync]'): print "\[<] bad hdr %s" % repr(req) print '[-] Request dropped due to bad header' return req_ = self.normalize(req, 6) try: hash = json.loads(req_) except: if DEBUG_JSON: print "[-] Sync failed to parse json\n '%s'. Caching for next req..." % req_ print "------------------------------------" self.prev_req = req return type = hash['type'] if type not in self.req_handlers: print ("[*] unknown request: %s" % type) return req_handler = self.req_handlers[type] # few requests are handled even though idb is not enable if type in ['broker', 'dialect', 'bc']: req_handler(hash) else: if self.is_active: req_handler(hash) else: print "[-] Drop the request because idb is not enabled" return idaapi.refresh_idaview_anyway() def normalize(self, req, taglen): req = req[taglen:] req = req.replace("\\", "\\\\") req = req.replace("\n", "") return req # send a kill notice to the broker (then forwarded to the dispatcher) def kill_notice(self): self.notice_broker("kill") # send a bp command (F2) to the debugger (via the broker and dispatcher) def bp_notice(self, oneshot=False): if not self.is_active: print "[sync] idb isn't enabled, bp can't be set" return ea = idaapi.get_screen_ea() offset = self.rebase_remote(ea) cmd = "%s0x%x" % (self.dbg_dialect['bp1' if oneshot else 'bp'], offset) self.notice_broker("cmd", "\"cmd\":\"%s\"" % cmd) print "[sync] >> set %s" % cmd # send a hardware bp command (Ctrl-F2) to the debugger (via the broker and dispatcher) def hbp_notice(self, oneshot=False): if not self.is_active: print "[sync] idb isn't enabled, hbp can't be set" return ea = idaapi.get_screen_ea() offset = self.rebase_remote(ea) cmd = "%s0x%x" % (self.dbg_dialect['hbp1' if oneshot else 'hbp'], offset) self.notice_broker("cmd", "\"cmd\":\"%s\"" % cmd) print "[sync] >> set %s" % cmd # send a oneshot bp command (F3) to the debugger (via the broker and dispatcher) def bp_oneshot_notice(self): self.bp_notice(True) # send a oneshot hardware bp command (Ctrl-F3) to the debugger (via the broker and dispatcher) def hbp_oneshot_notice(self): self.hbp_notice(True) # export IDB's breakpoint (Ctrl-F1) to the debugger (via the broker and dispatcher) def export_bp_notice(self): if not self.dbg_dialect: print "[sync] idb isn't synced yet, can't export bp" return mod = self.name.split('.')[0].strip() nbp = idc.GetBptQty() for i in range(nbp): ea = idc.GetBptEA(i) attrs = [idc.BPTATTR_TYPE, idc.BPTATTR_COND, idc.BPTATTR_FLAGS] btype, cond, flags = [idc.GetBptAttr(ea, x) for x in attrs] if cond: print "bp %d: conditional bp not supported" % i else: if ((btype in [idc.BPT_EXEC, idc.BPT_SOFT]) and ((flags & idc.BPT_ENABLED) != 0)): offset = ea - self.base bp = self.dbg_dialect['hbp' if (btype == idc.BPT_EXEC) else 'bp'] cmd = "%s%s+0x%x" % (bp, mod, offset) self.notice_broker("cmd", "\"cmd\":\"%s\"" % cmd) print "bp %d: %s" % (i, cmd) print "[sync] export done" # send a translate command (Alt-F2) to the debugger (via the broker and dispatcher) def translate_notice(self): if not self.dbg_dialect: print "[sync] idb isn't synced yet, can't translate" return ea = idaapi.get_screen_ea() mod = self.name.split('.')[0].strip() cmd = self.dbg_dialect['prefix'] + "translate 0x%x 0x%x %s" % (self.base, ea, mod) self.notice_broker("cmd", "\"cmd\":\"%s\"" % cmd) print "[sync] translate address 0x%x" % ea # send a go command (Alt-F5) to the debugger (via the broker and dispatcher) def go_notice(self): if not self.is_active: print "[sync] idb isn't enabled, can't go" return self.notice_broker("cmd", "\"cmd\":\"%s\"" % self.dbg_dialect['go']) self.notice_anti_flood() # send a single trace command (F11) to the debugger (via the broker and dispatcher) def si_notice(self): if not self.is_active: print "[sync] idb isn't enabled, can't trace" return self.notice_broker("cmd", "\"cmd\":\"%s\"" % self.dbg_dialect['si']) self.notice_anti_flood() # send a single step command (F10) to the debugger (via the broker and dispatcher) def so_notice(self): if not self.is_active: print "[sync] idb isn't enabled, can't single step" return self.notice_broker("cmd", "\"cmd\":\"%s\"" % self.dbg_dialect['so']) self.notice_anti_flood() # send a notice message to the broker process def notice_broker(self, type, args=None): if not self.broker_sock: return if args: notice = "[notice]{\"type\":\"%s\",%s}\n" % (type, args) else: notice = "[notice]{\"type\":\"%s\"}\n" % (type) try: self.broker_sock.sendall(notice) except: None def stop(self): if self.broker_sock: self.broker_sock.close() self.broker_sock = None self.cb_restore_last_line() idaapi.refresh_idaview_anyway() self.is_active = False print "[sync] idb is disabled" def __init__(self, parser): self.color = False self.prev_loc = None self.prev_node = None self.name = idaapi.get_root_filename() print "[sync] name %s" % self.name self.base = idaapi.get_imagebase() print "[sync] module base 0x%x" % self.base self.base_remote = None self.gm = GraphManager() self.parser = parser self.broker_sock = None self.is_active = False self.dbg_dialect = None self.req_handlers = { 'broker': self.req_broker, 'loc': self.req_loc, 'cmd': self.req_cmd, 'cmt': self.req_cmt, 'rcmt': self.req_rcmt, 'fcmt': self.req_fcmt, 'raddr': self.req_raddr, 'cursor': self.req_cursor, 'patch': self.req_patch, 'rln': self.req_rln, 'rrln': self.req_rrln, 'lbl': self.req_lbl, 'bc': self.req_bc, 'bps_get': self.req_bps_get, 'bps_set': self.req_bps_set, 'modcheck': self.req_modcheck, 'dialect': self.req_set_dbg_dialect } self.prev_req = "" # used as a cache if json is not completely received # -------------------------------------------------------------------------- class Broker(QtCore.QProcess): def cb_on_error(self, error): errors = ["Failed to start", "Crashed", "Timedout", "Read error", "Write Error", "Unknown Error"] print "[-] broker error: ", errors[error] def cb_broker_on_state_change(self, new_state): states = ["Not running", "Starting", "Running"] print "[*] broker new state: ", states[new_state] if states[new_state] == "Not running": print "[*] Check dispatcher.py.err if you think this is an error" def cb_broker_on_out(self): # readAllStandardOutput() returns QByteArray buffer = self.readAllStandardOutput().data().encode("ascii") batch = buffer.split('\n') for req in batch: self.worker.parse_exec(req) def __init__(self, parser): QtCore.QProcess.__init__(self) self.error.connect(self.cb_on_error) self.readyReadStandardOutput.connect(self.cb_broker_on_out) self.stateChanged.connect(self.cb_broker_on_state_change) # Create a request handler self.worker = RequestHandler(parser) # -------------------------------------------------------------------------- class DbgDirHlpr(object): @staticmethod def read_rsds_codeview(): guid = None penode = idaapi.netnode() penode.create(peutils_t.PE_NODE) fpos = penode.altval(peutils_t.PE_ALT_DBG_FPOS) if (fpos == 0): print "[*] No debug directory" return guid input_file = idc.GetInputFilePath() if not os.path.exists(input_file): print "[*] input file not available" else: with open(input_file, 'r') as fd: fd.seek(fpos) raw = fd.read(0x1C) """ typedef struct _IMAGE_DEBUG_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Type; DWORD SizeOfData; DWORD AddressOfRawData; DWORD PointerToRawData; } IMAGE_DEBUG_DIRECTORY, *PIMAGE_DEBUG_DIRECTORY; """ dbgdir = struct.unpack('LLHHLLLL', raw) # 2, IMAGE_DEBUG_TYPE_CODEVIEW if not (dbgdir[4] == 2): print "[*] not CODEVIEW data" else: fd.seek(dbgdir[7]) if not (fd.read(4) == "RSDS"): print "[*] unsupported CODEVIEW information format (%s)" % sig else: d1, d2, d3 = struct.unpack('LHH', fd.read(0x8)) d4 = struct.unpack('>H', fd.read(0x2))[0] d5 = binascii.hexlify(fd.read(0x6)).upper() guid = "%08X-%04X-%04X-%04X-%s" % (d1, d2, d3, d4, d5) return guid @staticmethod def parse_itoldyouso_output(res): for line in res.splitlines(True): line = line.strip() if line.startswith('pdb sig: '): return (line.split(':')[-1]).strip() return None # -------------------------------------------------------------------------- class GraphManager(): def __init__(self): self.prev_node = None self.graph_viewer = ida_kernwin.get_current_viewer() def center(self): curnode = ida_graph.viewer_get_curnode(self.graph_viewer) if not (self.prev_node == curnode): ida_graph.viewer_center_on(self.graph_viewer, curnode) self.prev_node = curnode return curnode # -------------------------------------------------------------------------- class SyncForm_t(PluginForm): def cb_broker_started(self): print "[*] broker started" self.btn.setText("Restart") def cb_broker_finished(self): print "[*] broker finished" if self.broker: self.broker.worker.stop() self.cb.stateChanged.disconnect(self.cb_change_state) self.cb.toggle() self.cb.stateChanged.connect(self.cb_change_state) self.btn.setText("Start") # send a kill notice to the broker # wait at most 2sec for him to gently kill itself def smooth_kill(self): self.uninit_hotkeys() if self.broker: broker = self.broker self.broker = None broker.worker.cb_restore_last_line() broker.worker.kill_notice() broker.waitForFinished(1500) def init_broker(self): print "[*] init_broker" modname = self.input.text().encode('ascii', 'replace') cmdline = u"\"%s\" -u \"%s\" --idb \"%s\"" % ( os.path.join(PYTHON_PATH, PYTHON_BIN), BROKER_PATH, modname) print "[*] init broker,", cmdline self.broker = Broker(self.parser) env = QProcessEnvironment.systemEnvironment() env.insert("IDB_PATH", IDB_PATH) env.insert("PYTHON_PATH", os.path.realpath(PYTHON_PATH)) env.insert("PYTHON_BIN", PYTHON_BIN) try: self.broker.started.connect(self.cb_broker_started) self.broker.finished.connect(self.cb_broker_finished) self.broker.setProcessEnvironment(env) self.broker.start(cmdline) except Exception as e: print "[-] failed to start broker: %s\n%s" % (str(e), traceback.format_exc()) return self.init_hotkeys() self.broker.worker.name = modname def init_hotkeys(self): if not self.hotkeys_ctx: self.init_single_hotkey("F2", self.broker.worker.bp_notice) self.init_single_hotkey("F3", self.broker.worker.bp_oneshot_notice) self.init_single_hotkey("Ctrl-F2", self.broker.worker.hbp_notice) self.init_single_hotkey("Ctrl-F3", self.broker.worker.hbp_oneshot_notice) self.init_single_hotkey("Ctrl-F1", self.broker.worker.export_bp_notice) self.init_single_hotkey("Alt-F2", self.broker.worker.translate_notice) self.init_single_hotkey("Alt-F5", self.broker.worker.go_notice) self.init_single_hotkey("F10", self.broker.worker.so_notice) self.init_single_hotkey("F11", self.broker.worker.si_notice) def init_single_hotkey(self, key, fnCb): ctx = idaapi.add_hotkey(key, fnCb) if ctx is None: print("[sync] failed to register hotkey %s", key) del ctx else: self.hotkeys_ctx.append(ctx) def uninit_hotkeys(self): if not self.hotkeys_ctx: return for ctx in self.hotkeys_ctx: if idaapi.del_hotkey(ctx): del ctx self.hotkeys_ctx = [] def cb_btn_restart(self): print "[sync] restarting broker." if self.cb.checkState() == QtCore.Qt.Checked: self.cb.toggle() time.sleep(0.1) self.cb.toggle() def cb_change_state(self, state): if state == QtCore.Qt.Checked: print "[*] sync enabled" # Restart broker self.hotkeys_ctx = [] self.init_broker() else: if self.broker: self.smooth_kill() print "[*] sync disabled\n" def OnCreate(self, form): print "[sync] form create" # Get parent widget parent = self.FormToPyQtWidget(form) # Create checkbox self.cb = QtWidgets.QCheckBox("Synchronization enable") self.cb.move(20, 20) self.cb.stateChanged.connect(self.cb_change_state) # Create label label = QtWidgets.QLabel('Overwrite idb name:') name = idaapi.get_root_filename() print "[sync] default idb name: %s" % name # Check in conf for name overwrite confpath = os.path.join(os.path.realpath(IDB_PATH), '.sync') if os.path.exists(confpath): print "[sync] found config file: %s" % confpath config = ConfigParser.SafeConfigParser() config.read(confpath) if config.has_option(name, 'name'): name = config.get(name, 'name') print "[sync] overwrite idb name with %s" % name # Create input field self.input = QtWidgets.QLineEdit(parent) self.input.setText(name) self.input.setMaxLength = 256 self.input.setFixedWidth(300) # Create restart button self.btn = QtWidgets.QPushButton('restart', parent) self.btn.setToolTip('Restart broker.') self.btn.clicked.connect(self.cb_btn_restart) # Create layout layout = QtWidgets.QGridLayout() layout.addWidget(self.cb) layout.addWidget(label) layout.addWidget(self.input) layout.addWidget(self.btn, 2, 2) layout.setColumnStretch(3, 1) layout.setRowStretch(3, 1) parent.setLayout(layout) # workaround: crash when instanciated in Broker.__init__ # weird interaction with Qtxxx libraries ? # File "C:\Python27\Lib\argparse.py", line 1584, in __init__ # self._positionals = add_group(_('positional arguments')) # File "C:\Python27\Lib\gettext.py", line 566, in gettext # return dgettext(_current_domain, message) # TypeError: 'NoneType' object is not callable self.parser = argparse.ArgumentParser() self.parser.add_argument("-a", "--address", nargs=1, action='store') self.parser.add_argument('msg', nargs=argparse.REMAINDER) # Synchronization is enabled by default self.cb.toggle() def OnClose(self, form): print "[sync] form close" self.smooth_kill() global SyncForm del SyncForm def Show(self): return PluginForm.Show(self, "ret-sync", options=PluginForm.FORM_PERSIST) # -------------------------------------------------------------------------- def main(): if not idaapi.get_root_filename(): print "[sync] please load a file/idb before" return global SyncForm try: SyncForm except: SyncForm = SyncForm_t() SyncForm.Broker = None SyncForm.Show() class SyncPlugin(idaapi.plugin_t): flags = 0 wanted_hotkey = "" comment = "ret-sync Plugin" help = "" wanted_name = "ret-sync Plugin" def init(self): return idaapi.PLUGIN_OK def term(self): pass def run(self, arg): main()