#!/usr/bin/python # instruction injector frontend # # github.com/xoreaxeaxeax/sandsifter // domas // @xoreaxeaxeax # # run as sudo for best results import signal import sys import subprocess import os from struct import * from capstone import * from collections import namedtuple from collections import deque import threading import time import curses from binascii import hexlify import re import random import argparse import code import copy from ctypes import * INJECTOR = "./injector" arch = "" OUTPUT = "./data/" LOG = OUTPUT + "log" SYNC = OUTPUT + "sync" TICK = OUTPUT + "tick" LAST = OUTPUT + "last" class ThreadState: pause = False run = True class InjectorResults(Structure): _fields_ = [('disas_length', c_int), ('disas_known', c_int), ('raw_insn', c_ubyte * 16), ('valid', c_int), ('length', c_int), ('signum', c_int), ('sicode', c_int), ('siaddr', c_int), ] class Settings: SYNTH_MODE_RANDOM = "r" SYNTH_MODE_BRUTE = "b" SYNTH_MODE_TUNNEL = "t" synth_mode = SYNTH_MODE_RANDOM root = False seed = 0 args = "" def __init__(self, args): if "-r" in args: self.synth_mode = self.SYNTH_MODE_RANDOM elif "-b" in args: self.synth_mode = self.SYNTH_MODE_BRUTE elif "-t" in args: self.synth_mode = self.SYNTH_MODE_TUNNEL self.args = args self.root = (os.geteuid() == 0) self.seed = random.getrandbits(32) def increment_synth_mode(self): if self.synth_mode == self.SYNTH_MODE_BRUTE: self.synth_mode = self.SYNTH_MODE_RANDOM elif self.synth_mode == self.SYNTH_MODE_RANDOM: self.synth_mode = self.SYNTH_MODE_TUNNEL elif self.synth_mode == self.SYNTH_MODE_TUNNEL: self.synth_mode = self.SYNTH_MODE_BRUTE class Tests: r = InjectorResults() # current result IL=20 # instruction log len UL=10 # artifact log len il = deque(maxlen=IL) # instruction log al = deque(maxlen=UL) # artifact log ad = dict() # artifact dict ic = 0 # instruction count ac = 0 # artifact count start_time = time.time() def elapsed(self): m, s = divmod(time.time() - self.start_time, 60) h, m = divmod(m, 60) return "%02d:%02d:%02d.%02d" % (h, m, int(s), int(100*(s-int(s))) ) class Tee(object): def __init__(self, name, mode): self.file = open(name, mode) self.stdout = sys.stdout sys.stdout = self def __del__(self): sys.stdout = self.stdout self.file.close() def write(self, data): self.file.write(data) self.stdout.write(data) # capstone disassembler md = None def disas_capstone(b): global md, arch if not md: if arch == "64": md = Cs(CS_ARCH_X86, CS_MODE_64) else: md = Cs(CS_ARCH_X86, CS_MODE_32) try: (address, size, mnemonic, op_str) = md.disasm_lite(b, 0, 1).next() except StopIteration: mnemonic="(unk)" op_str="" size = 0 return (mnemonic, op_str, size) # ndisasm disassembler # (ndidsasm breaks unnecessary prefixes onto its own line, which makes parsing # the output difficult. really only useful with the -P0 flag to disallow # prefixes) def disas_ndisasm(b): b = ''.join('\\x%02x' % ord(c) for c in b) if arch == "64": dis, errors = subprocess.Popen("echo -ne '%s' | ndisasm -b64 - | head -2" % b, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate() else: dis, errors = subprocess.Popen("echo -ne '%s' | ndisasm -b32 - | head -2" % b, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate() dis = dis.split("\n") extra = dis[1] dis = dis[0].split(None, 4) if extra.strip()[0] == '-': dis[1] = dis[1] + extra.strip()[1:] address = dis[0] insn = dis[1] mnemonic = dis[2] if len(dis) > 3: op_str = dis[3] else: op_str = "" if mnemonic == "db": mnemonic = "(unk)" insn = "" op_str = "" size = len(insn)/2 return (mnemonic, op_str, size) # objdump disassembler # (objdump breaks unnecessary prefixes onto its own line, which makes parsing # the output difficult. really only useful with the -P0 flag to disallow # prefixes) def disas_objdump(b): with open("/dev/shm/shifter", "w") as f: f.write(b) if arch == "64": dis, errors = subprocess.Popen("objdump -D --insn-width=256 -b binary \ -mi386 -Mx86-64 /dev/shm/shifter | head -8 | tail -1", stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate() else: dis, errors = subprocess.Popen("objdump -D --insn-width=256 -b binary \ -mi386 /dev/shm/shifter | head -8 | tail -1", stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate() dis = dis[6:] # address raw = dis[:256*3].replace(" ","") dis = dis[256*3:].strip().split(None, 2) mnemonic = dis[0] if len(dis) > 1: op_str = dis[1] else: op_str = "" if mnemonic == "(bad)": mnemonic = "(unk)" insn = "" op_str = "" size = len(raw)/2 return (mnemonic, op_str, size) def cstr2py(s): return ''.join([chr(x) for x in s]) # targeting python 2.6 support def int_to_comma(x): if type(x) not in [type(0), type(0L)]: raise TypeError("Parameter must be an integer.") if x < 0: return '-' + int_to_comma(-x) result = '' while x >= 1000: x, r = divmod(x, 1000) result = ",%03d%s" % (r, result) return "%d%s" % (x, result) def result_string(insn, result): s = "%30s %2d %2d %2d %2d (%s)\n" % ( hexlify(insn), result.valid, result.length, result.signum, result.sicode, hexlify(cstr2py(result.raw_insn))) return s class Injector: process = None settings = None command = None def __init__(self, settings): self.settings = settings def start(self): self.command = "%s %s -%c -R %s -s %d" % \ ( INJECTOR, " ".join(self.settings.args), self.settings.synth_mode, "-0" if self.settings.root else "", self.settings.seed ) self.process = subprocess.Popen( "exec %s" % self.command, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, preexec_fn=os.setsid ) def stop(self): if self.process: try: os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) except OSError: pass class Poll: SIGILL = 4 SIGSEGV = 11 SIGFPE = 8 SIGBUS = 7 SIGTRAP = 5 def __init__(self, ts, injector, tests, command_line, sync=False, low_mem=False, search_unk=True, search_len=False, search_dis=False, search_ill=False, disassembler=disas_capstone): self.ts = ts self.injector = injector self.T = tests self.poll_thread = None self.sync = sync self.low_mem = low_mem self.search_len = search_len self.search_unk = search_unk self.search_dis = search_dis self.search_ill = search_ill self.disas = disassembler if self.sync: with open(SYNC, "w") as f: f.write("#\n") f.write("# %s\n" % command_line) f.write("# %s\n" % injector.command) f.write("#\n") f.write("# cpu:\n") cpu = get_cpu_info() for l in cpu: f.write("# %s\n" % l) f.write("# %s v l s c\n" % (" " * 28)) def start(self): self.poll_thread = threading.Thread(target=self.poll) self.poll_thread.start() def stop(self): self.poll_thread.join() while self.ts.run: time.sleep(.1) def poll(self): while self.ts.run: while self.ts.pause: time.sleep(.1) bytes_polled = self.injector.process.stdout.readinto(self.T.r) if bytes_polled == sizeof(self.T.r): self.T.ic = self.T.ic + 1 error = False if self.T.r.valid: if self.search_unk and not self.T.r.disas_known and self.T.r.signum != self.SIGILL: error = True if self.search_len and self.T.r.disas_known and self.T.r.disas_length != self.T.r.length: error = True if self.search_dis and self.T.r.disas_known \ and self.T.r.disas_length != self.T.r.length and self.T.r.signum != self.SIGILL: error = True if self.search_ill and self.T.r.disas_known and self.T.r.signum == self.SIGILL: error = True if error: insn = cstr2py(self.T.r.raw_insn)[:self.T.r.length] r = copy.deepcopy(self.T.r) self.T.al.appendleft(r) if insn not in self.T.ad: if not self.low_mem: self.T.ad[insn] = r self.T.ac = self.T.ac + 1 if self.sync: with open(SYNC, "a") as f: f.write(result_string(insn, self.T.r)) else: if self.injector.process.poll() is not None: self.ts.run = False break class Gui: TIME_SLICE = .01 GRAY_BASE = 50 TICK_MASK = 0xff RATE_Q = 100 RATE_FACTOR = 1000 INDENT = 10 GRAYS = 50 BLACK = 1 WHITE = 2 BLUE = 3 RED = 4 GREEN = 5 COLOR_BLACK = 16 COLOR_WHITE = 17 COLOR_BLUE = 18 COLOR_RED = 19 COLOR_GREEN = 20 def __init__(self, ts, injector, tests, do_tick, disassembler=disas_capstone): self.ts = ts; self.injector = injector self.T = tests self.gui_thread = None self.do_tick = do_tick self.ticks = 0 self.last_ins_count = 0 self.delta_log = deque(maxlen=self.RATE_Q) self.time_log = deque(maxlen=self.RATE_Q) self.disas = disassembler self.stdscr = curses.initscr() curses.start_color() # doesn't work # self.orig_colors = [curses.color_content(x) for x in xrange(256)] curses.use_default_colors() curses.noecho() curses.cbreak() curses.curs_set(0) self.stdscr.nodelay(1) self.sx = 0 self.sy = 0 self.init_colors() self.stdscr.bkgd(curses.color_pair(self.WHITE)) self.last_time = time.time() def init_colors(self): if curses.has_colors() and curses.can_change_color(): curses.init_color(self.COLOR_BLACK, 0, 0, 0) curses.init_color(self.COLOR_WHITE, 1000, 1000, 1000) curses.init_color(self.COLOR_BLUE, 0, 0, 1000) curses.init_color(self.COLOR_RED, 1000, 0, 0) curses.init_color(self.COLOR_GREEN, 0, 1000, 0) # this will remove flicker, but gives boring colors ''' self.COLOR_BLACK = curses.COLOR_BLACK self.COLOR_WHITE = curses.COLOR_WHITE self.COLOR_BLUE = curses.COLOR_BLUE self.COLOR_RED = curses.COLOR_RED self.COLOR_GREEN = curses.COLOR_GREEN ''' for i in xrange(0, self.GRAYS): curses.init_color( self.GRAY_BASE + i, i * 1000 / (self.GRAYS - 1), i * 1000 / (self.GRAYS - 1), i * 1000 / (self.GRAYS - 1) ) curses.init_pair( self.GRAY_BASE + i, self.GRAY_BASE + i, self.COLOR_BLACK ) else: self.COLOR_BLACK = curses.COLOR_BLACK self.COLOR_WHITE = curses.COLOR_WHITE self.COLOR_BLUE = curses.COLOR_BLUE self.COLOR_RED = curses.COLOR_RED self.COLOR_GREEN = curses.COLOR_GREEN for i in xrange(0, self.GRAYS): curses.init_pair( self.GRAY_BASE + i, self.COLOR_WHITE, self.COLOR_BLACK ) curses.init_pair(self.BLACK, self.COLOR_BLACK, self.COLOR_BLACK) curses.init_pair(self.WHITE, self.COLOR_WHITE, self.COLOR_BLACK) curses.init_pair(self.BLUE, self.COLOR_BLUE, self.COLOR_BLACK) curses.init_pair(self.RED, self.COLOR_RED, self.COLOR_BLACK) curses.init_pair(self.GREEN, self.COLOR_GREEN, self.COLOR_BLACK) def gray(self, scale): if curses.can_change_color(): return curses.color_pair(self.GRAY_BASE + int(round(scale * (self.GRAYS - 1)))) else: return curses.color_pair(self.WHITE) def box(self, window, x, y, w, h, color): for i in xrange(1, w - 1): window.addch(y, x + i, curses.ACS_HLINE, color) window.addch(y + h - 1, x + i, curses.ACS_HLINE, color) for i in xrange(1, h - 1): window.addch(y + i, x, curses.ACS_VLINE, color) window.addch(y + i, x + w - 1, curses.ACS_VLINE, color) window.addch(y, x, curses.ACS_ULCORNER, color) window.addch(y, x + w - 1, curses.ACS_URCORNER, color) window.addch(y + h - 1, x, curses.ACS_LLCORNER, color) window.addch(y + h - 1, x + w - 1, curses.ACS_LRCORNER, color) def bracket(self, window, x, y, h, color): for i in xrange(1, h - 1): window.addch(y + i, x, curses.ACS_VLINE, color) window.addch(y, x, curses.ACS_ULCORNER, color) window.addch(y + h - 1, x, curses.ACS_LLCORNER, color) def vaddstr(self, window, x, y, s, color): for i in xrange(0, len(s)): window.addch(y + i, x, s[i], color) def draw(self): try: self.stdscr.erase() # constants left = self.sx + self.INDENT top = self.sy top_bracket_height = self.T.IL top_bracket_middle = self.T.IL / 2 mne_width = 10 op_width = 45 raw_width = (16*2) # render log bracket self.bracket(self.stdscr, left - 1, top, top_bracket_height + 2, self.gray(1)) # render logo self.vaddstr(self.stdscr, left - 3, top + top_bracket_middle - 5, "sand", self.gray(.2)) self.vaddstr(self.stdscr, left - 3, top + top_bracket_middle + 5, "sifter", self.gray(.2)) # refresh instruction log synth_insn = cstr2py(self.T.r.raw_insn) (mnemonic, op_str, size) = self.disas(synth_insn) self.T.il.append( ( mnemonic, op_str, self.T.r.length, "%s" % hexlify(synth_insn) ) ) # render instruction log try: for (i, r) in enumerate(self.T.il): line = i + self.T.IL - len(self.T.il) (mnemonic, op_str, length, raw) = r if i == len(self.T.il) - 1: # latest instruction # mnemonic self.stdscr.addstr( top + 1 + line, left, "%*s " % (mne_width, mnemonic), self.gray(1) ) # operands self.stdscr.addstr( top + 1 + line, left + (mne_width + 1), "%-*s " % (op_width, op_str), curses.color_pair(self.BLUE) ) # bytes if self.maxx > left + (mne_width + 1) + (op_width + 1) + (raw_width + 1): self.stdscr.addstr( top + 1 + line, left + (mne_width + 1) + (op_width + 1), "%s" % raw[0:length * 2], self.gray(.9) ) self.stdscr.addstr( top + 1 +line, left + (mne_width + 1) + (op_width + 1) + length * 2, "%s" % raw[length * 2:raw_width], self.gray(.3) ) else: # previous instructions # mnemonic, operands self.stdscr.addstr( top + 1 + line, left, "%*s %-*s" % (mne_width, mnemonic, op_width, op_str), self.gray(.5) ) # bytes if self.maxx > left + (mne_width + 1) + (op_width + 1) + (raw_width + 1): self.stdscr.addstr( top + 1 + line, left + (mne_width + 1) + (op_width + 1), "%s" % raw[0:length * 2], self.gray(.3) ) self.stdscr.addstr( top + 1 + line, left + (mne_width + 1) + (op_width + 1) + length * 2, "%s" % raw[length * 2:raw_width], self.gray(.1) ) except RuntimeError: # probably the deque was modified by the poller pass # rate calculation self.delta_log.append(self.T.ic - self.last_ins_count) self.last_ins_count = self.T.ic ctime = time.time() self.time_log.append(ctime - self.last_time) self.last_time = ctime rate = int(sum(self.delta_log)/sum(self.time_log)) # render timestamp if self.maxx > left + (mne_width + 1) + (op_width + 1) + (raw_width + 1): self.vaddstr( self.stdscr, left + (mne_width + 1) + (op_width + 1) + (raw_width + 1), top + 1, self.T.elapsed(), self.gray(.5) ) # render injection settings self.stdscr.addstr(top + 1, left - 8, "%d" % self.injector.settings.root, self.gray(.1)) self.stdscr.addstr(top + 1, left - 7, "%s" % arch, self.gray(.1)) self.stdscr.addstr(top + 1, left - 3, "%c" % self.injector.settings.synth_mode, self.gray(.5)) # render injection results self.stdscr.addstr(top + top_bracket_middle, left - 6, "v:", self.gray(.5)) self.stdscr.addstr(top + top_bracket_middle, left - 4, "%2x" % self.T.r.valid) self.stdscr.addstr(top + top_bracket_middle + 1, left - 6, "l:", self.gray(.5)) self.stdscr.addstr(top + top_bracket_middle + 1, left - 4, "%2x" % self.T.r.length) self.stdscr.addstr(top + top_bracket_middle + 2, left - 6, "s:", self.gray(.5)) self.stdscr.addstr(top + top_bracket_middle + 2, left - 4, "%2x" % self.T.r.signum) self.stdscr.addstr(top + top_bracket_middle + 3, left - 6, "c:", self.gray(.5)) self.stdscr.addstr(top + top_bracket_middle + 3, left - 4, "%2x" % self.T.r.sicode) # render instruction count self.stdscr.addstr(top + top_bracket_height + 2, left, "#", self.gray(.5)) self.stdscr.addstr(top + top_bracket_height + 2, left + 2, "%s" % (int_to_comma(self.T.ic)), self.gray(1)) # render rate self.stdscr.addstr(top + top_bracket_height + 3, left, " %d/s%s" % (rate, " " * min(rate / self.RATE_FACTOR, 100)), curses.A_REVERSE) # render artifact count self.stdscr.addstr(top + top_bracket_height + 4, left, "#", self.gray(.5)) self.stdscr.addstr(top + top_bracket_height + 4, left + 2, "%s" % (int_to_comma(self.T.ac)), curses.color_pair(self.RED)) # render artifact log if self.maxy >= top + top_bracket_height + 5 + self.T.UL + 2: # render artifact bracket self.bracket(self.stdscr, left - 1, top + top_bracket_height + 5, self.T.UL + 2, self.gray(1)) # render artifacts try: for (i, r) in enumerate(self.T.al): y = top_bracket_height + 5 + i insn_hex = hexlify(cstr2py(r.raw_insn)) # unexplainable hack to remove some of the unexplainable # flicker on my console. a bug in ncurses? doesn't # happen if using curses.COLOR_RED instead of a custom # red. doesn't happen if using a new random string each # time; doesn't happen if using a constant string each # time. only happens with the specific implementation below. #TODO: on systems with limited color settings, this # makes the background look like random characters random_string = ("%02x" % random.randint(0,100)) * (raw_width-2) self.stdscr.addstr(top + 1 + y, left, random_string, curses.color_pair(self.BLACK)) self.stdscr.addstr(top + 1 + y, left + 1, "%s" % insn_hex[0:r.length * 2], curses.color_pair(self.RED)) self.stdscr.addstr(top + 1 + y, left + 1 + r.length * 2, "%s" % insn_hex[r.length * 2:raw_width], self.gray(.25)) except RuntimeError: # probably the deque was modified by the poller pass self.stdscr.refresh() except curses.error: pass def start(self): self.gui_thread = threading.Thread(target=self.render) self.gui_thread.start() def stop(self): self.gui_thread.join() def checkkey(self): c = self.stdscr.getch() if c == ord('p'): self.ts.pause = not self.ts.pause elif c == ord('q'): self.ts.run = False elif c == ord('m'): self.ts.pause = True time.sleep(.1) self.injector.stop() self.injector.settings.increment_synth_mode() self.injector.start() self.ts.pause = False def render(self): while self.ts.run: while self.ts.pause: self.checkkey() time.sleep(.1) (self.maxy,self.maxx) = self.stdscr.getmaxyx() self.sx = 1 self.sy = max((self.maxy + 1 - (self.T.IL + self.T.UL + 5 + 2))/2, 0) self.checkkey() synth_insn = cstr2py(self.T.r.raw_insn) if synth_insn and not self.ts.pause: self.draw() if self.do_tick: self.ticks = self.ticks + 1 if self.ticks & self.TICK_MASK == 0: with open(TICK, 'w') as f: f.write("%s" % hexlify(synth_insn)) time.sleep(self.TIME_SLICE) def get_cpu_info(): with open("/proc/cpuinfo", "r") as f: cpu = [l.strip() for l in f.readlines()[:7]] return cpu def dump_artifacts(r, injector, command_line): global arch tee = Tee(LOG, "w") tee.write("#\n") tee.write("# %s\n" % command_line) tee.write("# %s\n" % injector.command) tee.write("#\n") tee.write("# insn tested: %d\n" % r.ic) tee.write("# artf found: %d\n" % r.ac) tee.write("# runtime: %s\n" % r.elapsed()) tee.write("# seed: %d\n" % injector.settings.seed) tee.write("# arch: %s\n" % arch) tee.write("# date: %s\n" % time.strftime("%Y-%m-%d %H:%M:%S")) tee.write("#\n") tee.write("# cpu:\n") cpu = get_cpu_info() for l in cpu: tee.write("# %s\n" % l) tee.write("# %s v l s c\n" % (" " * 28)) for k in sorted(list(r.ad)): v = r.ad[k] tee.write(result_string(k, v)) def cleanup(gui, poll, injector, ts, tests, command_line, args): ts.run = False if gui: gui.stop() if poll: poll.stop() if injector: injector.stop() ''' # doesn't work if gui: for (i, c) in enumerate(gui.orig_colors): curses.init_color(i, c[0], c[1], c[2]) ''' curses.nocbreak(); curses.echo() curses.endwin() dump_artifacts(tests, injector, command_line) if args.save: with open(LAST, "w") as f: f.write(hexlify(cstr2py(tests.r.raw_insn))) sys.exit(0) def main(): global arch def exit_handler(signal, frame): cleanup(gui, poll, injector, ts, tests, command_line, args) injector = None poll = None gui = None command_line = " ".join(sys.argv) parser = argparse.ArgumentParser() parser.add_argument("--len", action="store_true", default=False, help="search for length differences in all instructions (instructions\ that executed differently than the disassembler expected, or did not\ exist when the disassembler expected them to)" ) parser.add_argument("--dis", action="store_true", default=False, help="search for length differences in valid instructions (instructions\ that executed differently than the disassembler expected)" ) parser.add_argument("--unk", action="store_true", default=False, help="search for unknown instructions (instructions that the\ disassembler doesn't know about but successfully execute)" ) parser.add_argument("--ill", action="store_true", default=False, help="the inverse of --unk, search for invalid disassemblies\ (instructions that do not successfully execute but that the\ disassembler acknowledges)" ) parser.add_argument("--tick", action="store_true", default=False, help="periodically write the current instruction to disk" ) parser.add_argument("--save", action="store_true", default=False, help="save search progress on exit" ) parser.add_argument("--resume", action="store_true", default=False, help="resume search from last saved state" ) parser.add_argument("--sync", action="store_true", default=False, help="write search results to disk as they are found" ) parser.add_argument("--low-mem", action="store_true", default=False, help="do not store results in memory" ) parser.add_argument("injector_args", nargs=argparse.REMAINDER) args = parser.parse_args() injector_args = args.injector_args if "--" in injector_args: injector_args.remove("--") if not args.len and not args.unk and not args.dis and not args.ill: print "warning: no search type (--len, --unk, --dis, --ill) specified, results will not be recorded." raw_input() if args.resume: if "-i" in injector_args: print "--resume is incompatible with -i" sys.exit(1) if os.path.exists(LAST): with open(LAST, "r") as f: insn = f.read() injector_args.extend(['-i',insn]) else: print "no resume file found" sys.exit(1) if not os.path.exists(OUTPUT): os.makedirs(OUTPUT) injector_bitness, errors = \ subprocess.Popen( ['file', INJECTOR], stdout=subprocess.PIPE, stderr=subprocess.PIPE ).communicate() arch = re.search(r".*(..)-bit.*", injector_bitness).group(1) ts = ThreadState() signal.signal(signal.SIGINT, exit_handler) settings = Settings(args.injector_args) tests = Tests() injector = Injector(settings) injector.start() poll = Poll(ts, injector, tests, command_line, args.sync, args.low_mem, args.unk, args.len, args.dis, args.ill) poll.start() gui = Gui(ts, injector, tests, args.tick) gui.start() while ts.run: time.sleep(.1) cleanup(gui, poll, injector, ts, tests, command_line, args) if __name__ == '__main__': main()