#!/usr/bin/python3 # This is a GDB plugin. # Usage: # gdb --batch -ex 'python exec(open("THIS_FILE").read())' -ex run -ex abrt-exploitable PROG # or # gdb --batch -ex 'python exec(open("THIS_FILE").read())' -ex 'core COREDUMP' -ex abrt-exploitable import sys import os import signal import re import gettext import locale import gdb GETTEXT_PROGNAME = "abrt" _ = gettext.gettext def init_gettext(): try: locale.setlocale(locale.LC_ALL, "") except locale.Error: os.environ['LC_ALL'] = 'C' locale.setlocale(locale.LC_ALL, "") # Defeat "AttributeError: 'module' object has no attribute 'nl_langinfo'" try: gettext.bind_textdomain_codeset(GETTEXT_PROGNAME, locale.nl_langinfo(locale.CODESET)) except AttributeError: pass gettext.bindtextdomain(GETTEXT_PROGNAME, '/usr/share/locale') gettext.textdomain(GETTEXT_PROGNAME) _WRITES_ALWAYS = -1 _WRITES_IF_MEMREF = -2 _x86_writing_instr = { # insn:N, where N: # -1: this insn always writes to memory # -2: writes to memory if any operand is a memory operand # 2: writes to memory if 2nd (or later) operand is a memory operand # # Two-operand insns "add": 2, "adc": 2, "sub": 2, "sbb": 2, "and": 2, "xor": 2, "or": 2, "xadd": 2, "cmpxchg": 2, # One-operand insns. Can use 1 or _WRITES_IF_MEMREF "inc": _WRITES_IF_MEMREF, "dec": _WRITES_IF_MEMREF, "neg": _WRITES_IF_MEMREF, "not": _WRITES_IF_MEMREF, "pop": _WRITES_IF_MEMREF, # "Set byte on condition". One-operand insns. "seta": _WRITES_IF_MEMREF, "setae": _WRITES_IF_MEMREF, "setb": _WRITES_IF_MEMREF, "setbe": _WRITES_IF_MEMREF, "setc": _WRITES_IF_MEMREF, "sete": _WRITES_IF_MEMREF, "setg": _WRITES_IF_MEMREF, "setge": _WRITES_IF_MEMREF, "setl": _WRITES_IF_MEMREF, "setle": _WRITES_IF_MEMREF, "setna": _WRITES_IF_MEMREF, "setnae": _WRITES_IF_MEMREF, "setnb": _WRITES_IF_MEMREF, "setnbe": _WRITES_IF_MEMREF, "setnc": _WRITES_IF_MEMREF, "setne": _WRITES_IF_MEMREF, "setng": _WRITES_IF_MEMREF, "setnge": _WRITES_IF_MEMREF, "setnl": _WRITES_IF_MEMREF, "setnle": _WRITES_IF_MEMREF, "setno": _WRITES_IF_MEMREF, "setnp": _WRITES_IF_MEMREF, "setns": _WRITES_IF_MEMREF, "setnz": _WRITES_IF_MEMREF, "seto": _WRITES_IF_MEMREF, "setp": _WRITES_IF_MEMREF, "setpe": _WRITES_IF_MEMREF, "setpo": _WRITES_IF_MEMREF, "sets": _WRITES_IF_MEMREF, "setz": _WRITES_IF_MEMREF, # Shifts. # sarl $2,(%rcx) # sarl (%rax) - *implicit* operand (shift count) 1. # shld 11,%ecx,(%rdi) - *third* operand is r/m. # Luckily, any memory operand is a destination, can use _WRITES_IF_MEMREF. "shl": _WRITES_IF_MEMREF, "shr": _WRITES_IF_MEMREF, "sal": _WRITES_IF_MEMREF, "sar": _WRITES_IF_MEMREF, "rol": _WRITES_IF_MEMREF, "ror": _WRITES_IF_MEMREF, "rcl": _WRITES_IF_MEMREF, "rcr": _WRITES_IF_MEMREF, "shld": _WRITES_IF_MEMREF, "shrd": _WRITES_IF_MEMREF, # Bit tests. Any memory operand is a destination, can use _WRITES_IF_MEMREF. "bts": _WRITES_IF_MEMREF, "btr": _WRITES_IF_MEMREF, "btc": _WRITES_IF_MEMREF, # One-operand (register pair is another, implicit operand). "cmpxchg8b": _WRITES_IF_MEMREF, "cmpxchg16b": _WRITES_IF_MEMREF, # Either mem operand indicates write to mem. "xchg": _WRITES_IF_MEMREF, # String store insns. # Look similar to widening signed move "movs[bwl][wlq]", # but aliasing doesn't happen since widening move has two siffixes "movs": _WRITES_ALWAYS, "stos": _WRITES_ALWAYS, # Widening moves never store to mem. # May look like we need to list them because otherwise they get caught # by "movXXX", but thankfully their 2nd operand is never a memory reference, # which "movXXX" wildcard checks. #"mov[sz][bwl][wlq]":0, # One-operand insn. # These are system insns, but they do NOT cause exception in userspace. "smsw": _WRITES_IF_MEMREF, "sgdt": _WRITES_IF_MEMREF, "sidt": _WRITES_IF_MEMREF, "sldt": _WRITES_IF_MEMREF, "str": _WRITES_IF_MEMREF, # FPU/SIMD madness follows. # FPU store insns. One-operand. "fsts": _WRITES_IF_MEMREF, "fstl": _WRITES_IF_MEMREF, #"fstt" doesn't exist "fstps": _WRITES_IF_MEMREF, "fstpl": _WRITES_IF_MEMREF, "fstpt": _WRITES_IF_MEMREF, # Saving state. One-operand insns. "fstcw": _WRITES_IF_MEMREF, "fnstcw": _WRITES_IF_MEMREF, "fstsw": _WRITES_IF_MEMREF, "fnstsw": _WRITES_IF_MEMREF, "fstenv": _WRITES_IF_MEMREF, "fnstenv": _WRITES_IF_MEMREF, "fsave": _WRITES_IF_MEMREF, "fnsave": _WRITES_IF_MEMREF, "fxsave": _WRITES_IF_MEMREF, "xsave": _WRITES_IF_MEMREF, "xsaveopt": _WRITES_IF_MEMREF, "fsave64": _WRITES_IF_MEMREF, "fnsave64": _WRITES_IF_MEMREF, "fxsave64": _WRITES_IF_MEMREF, "xsave64": _WRITES_IF_MEMREF, "xsaveopt64": _WRITES_IF_MEMREF, "stmxcsr": _WRITES_IF_MEMREF, "vstmxcsr": _WRITES_IF_MEMREF, # SIMD store insns. # Three-operand insns. Any memory operand is a destination. "vcvtps2ph": _WRITES_IF_MEMREF, "extractps": _WRITES_IF_MEMREF, "vextractps": _WRITES_IF_MEMREF, #[v]extractpd does not exist "vextractf128": _WRITES_IF_MEMREF, "vextracti128": _WRITES_IF_MEMREF, "pextr": _WRITES_IF_MEMREF, # covers pextr[bwq] "pextrd": _WRITES_IF_MEMREF, "vpextr": _WRITES_IF_MEMREF, "vpextrd": _WRITES_IF_MEMREF, "vmaskmovpd": _WRITES_IF_MEMREF, "vmaskmovps": _WRITES_IF_MEMREF, "vpmaskmovd": _WRITES_IF_MEMREF, "vpmaskmovq": _WRITES_IF_MEMREF, # These insns have implicit (%edi) dest operand: "maskmovq": _WRITES_ALWAYS, # mmx version "maskmovdqu": _WRITES_ALWAYS, "vmaskmovdqu": _WRITES_ALWAYS, # check binutils/gas/testsuite/gas/i386/* for more weird insns # Instruction Set Reference, A-M and N-Z: # http://download.intel.com/products/processor/manual/253666.pdf # http://download.intel.com/products/processor/manual/253667.pdf # SSE4: # http://software.intel.com/sites/default/files/m/0/3/c/d/4/18187-d9156103.pdf # Instruction Set Extensions: # http://download-software.intel.com/sites/default/files/319433-014.pdf # Xeon Phi: # http://download-software.intel.com/sites/default/files/forum/278102/327364001en.pdf #"[v]movXXX" - special-cased in the code "mov": 2 # Note: stack-writing instructions are omitted } _x86_pushing_instr = ( "push", "pusha", "pushf", "enter", "call", "lcall" ) _x86_intdiv_instr = ("div", "idiv") _x86_jumping_instr = ( "jmp", # indirect jumps/calls with garbage data "call", # call: also possible that stack is exhausted (infinite recursion) "ljmp", "lcall", # Yes, lret/iret isn't used in normal userspace code, # but it does work (compile with "gcc -nostartfiles -nostdlib -m32"): # #_start: .globl _start # pushf # push %cs # push $next # iret # lret or ret would work too #next: # movl $42, %ebx # movl $1, %eax # int $0x80 # exit(42) # "iret", "lret", "ret" ) # stack was smashed if we crash on one of these _x86_return_instr = ("iret", "lret", "ret") _x86_mem_op1_regex = re.compile("^((-?0x)|[(])") _x86_mem_op2_regex = re.compile("[,:]((-?0x)|[(])") def _x86_fetch_insn_from_table(ins, table): if not ins: return None if ins in table: if type(table) == dict: return table[ins] return ins # Drop common byte/word/long/quad suffix and try again if ins[-1] in ("b", "w", "l", "q"): ins = ins[:-1] if ins in table: if type(table) == dict: return table[ins] return ins return None class SignalAndInsn: def x86_instruction_is_store(self): operand = _x86_fetch_insn_from_table(self.mnemonic, _x86_writing_instr) if not operand: if not self.mnemonic: return False # There are far too many SSE store instructions, # don't want to pollute the table with them. # Special-case the check for MOVxxx # and its SIMD cousins VMOVxxx: if self.mnemonic[:3] != "mov" and self.mnemonic[:4] != "vmov": return False operand = 2 if operand == _WRITES_ALWAYS: # no need to check operands, it's a write return True # Memory operands look like this: [%seg:][[-]0xHEXNUM][(%reg[,...])] # Careful with immediate operands which are $0xHEXNUM # and FPU register references which are st(N). if _x86_mem_op1_regex.search(self.operands): mem_op_pos = 0 else: match = _x86_mem_op2_regex.search(self.operands) if not match: return False # no memory operands mem_op_pos = match.start() + 1 if operand == _WRITES_IF_MEMREF: # any mem operand indicates write return True comma = self.operands.find(",") if mem_op_pos < comma: # "%cs:0x0(%rax,%rax,1),foo" - 1st operand is memory # "%cs:0x0(%rax),foo" - 1st operand is memory memory_operand = 1 elif comma < 0: # "%cs:0x0(%rax)" - 1st operand is memory memory_operand = 1 else: # mem_op_pos is after comma # "foo,%cs:0x0(%rax,%rax,1)" - 2nd operand is memory # (It also can be a third, fourth etc operand) memory_operand = 2 if operand == memory_operand: return True return False def x86_get_instruction(self): try: # just "disassemble $pc" won't work if $pc doesn't point # inside a known function raw_instructions = gdb.execute("disassemble $pc,$pc+32", to_string=True) except gdb.error: # For example, if tracee already exited normally. # Another observed case is if $pc points to unmapped area. # We get "Python Exception No registers" return instructions = [] current = None for line in raw_instructions.split("\n"): # line can be: # "Dump of assembler code from 0xAAAA to 0xBBBB:" # "[=>] 0x00000000004004dc[ <+0>]: push %rbp" # (" <+0>" part is present when we run on a live process, # on coredump it is absent) # "End of assembler dump." # "" (empty line) if line.startswith("=>"): line = line[2:] current = len(instructions) line = line.split(":", 1) if len(line) < 2: # no ":"? continue line = line[1] # drop "foo:" line = line.strip() # drop leading/trailing whitespace if line: instructions.append(line) if current == None: # we determined that $pc points to a bad address, # which is an interesting fact. return # There can be a disasm comment: "insn op,op,op # comment"; # strip it, and whitespace on both ends: t = instructions[current].split("#", 1)[0].strip() self.current_instruction = t # Strip prefixes: while True: t = t.split(None, 1) self.mnemonic = t[0] if len(t) < 2: break if self.mnemonic.startswith("rex."): t = t[1] continue if self.mnemonic in ( "data32", "data16", "addr32", "addr16", "rex", "cs", "ds", "es", "ss", "fs", "gs", "lock", "rep", "repz", "repnz", "xacquire", "xrelease" ): t = t[1] continue # First word isn't a prefix -> we found the insn word self.operands = t[1] break self.instruction_is_pushing = (_x86_fetch_insn_from_table(self.mnemonic, _x86_pushing_instr) is not None) self.instruction_is_division = (_x86_fetch_insn_from_table(self.mnemonic, _x86_intdiv_instr) is not None) self.instruction_is_branch = (_x86_fetch_insn_from_table(self.mnemonic, _x86_jumping_instr) is not None) self.instruction_is_return = (_x86_fetch_insn_from_table(self.mnemonic, _x86_return_instr) is not None) self.instruction_is_store = self.x86_instruction_is_store() def ppc_get_instruction(self): try: # just "disassemble $pc" won't work if $pc doesn't point # inside a known function raw_instructions = gdb.execute("disassemble $pc,$pc+32", to_string=True) except gdb.error: # For example, if tracee already exited normally. # Another observed case is if $pc points to unmapped area. # We get "Python Exception No registers" return instructions = [] current = None for line in raw_instructions.split("\n"): # line can be: # "Dump of assembler code from 0xAAAA to 0xBBBB:" # "[=>] 0x00000000004004dc[ <+0>]: push %rbp" # (" <+0>" part is present when we run on a live process, # on coredump it is absent) # "End of assembler dump." # "" (empty line) if line.startswith("=>"): line = line[2:] current = len(instructions) line = line.split(":", 1) if len(line) < 2: # no ":"? continue line = line[1] # drop "foo:" line = line.strip() # drop leading/trailing whitespace if line: instructions.append(line) if current is None: # we determined that $pc points to a bad address, # which is an interesting fact. return # There can be a disasm comment: "insn op,op,op # comment"; # strip it, and whitespace on both ends: t = instructions[current].split("#", 1)[0].strip() self.current_instruction = t # Split it into mnemonic and operands t = t.split(None, 1) self.mnemonic = t[0] if len(t) > 1: self.operands = t[1] self.instruction_is_store = self.mnemonic.startswith("st") self.instruction_is_branch = self.mnemonic.startswith("b") self.instruction_is_pushing = (self.instruction_is_store and "(r1)" in self.operands) # Looks like div[o] insns on ppc don't cause exceptions # (need to check whether, and how, FPE is generated) #self.instruction_is_division = # On ppc, return insn is b[cond]lr. TODO: is cond form ever used by gcc? self.instruction_is_return = (self.mnemonic == "blr") def get_instruction(self): self.current_instruction = None self.mnemonic = None self.operands = "" self.instruction_is_division = None self.instruction_is_store = None self.instruction_is_pushing = None self.instruction_is_return = None self.instruction_is_branch = None try: arch = gdb.execute("show architecture", to_string=True) # Examples of the string we get: # The target architecture is set automatically (currently i386) # The target architecture is set automatically (currently i386:x86-64) # The target architecture is set automatically (currently powerpc:common64) if " i386" in arch: return self.x86_get_instruction() if " powerpc" in arch: return self.ppc_get_instruction() except gdb.error: return def get_signal(self): self.signo = None self.si_code = None try: # Requires new kernels which record complete siginfo # in coredumps (Linux 3.9 still don't have it), # and new gdb: sig = gdb.parse_and_eval("$_siginfo.si_signo") code = gdb.parse_and_eval("$_siginfo.si_code") # Requires patched gdb: #sig = gdb.parse_and_eval("$_signo") # # type(sig) = , convert to plain int: self.signo = int(sig) self.si_code = int(code) except gdb.error: # "Python Exception # Attempt to extract a component of a value that is not a structure" # Possible reasons why $_siginfo doesn't exist: # program is still running, program exited normally, # we work with a coredump from an old kernel. # # Lets see whether we are running from the abrt and it # provided us with signal number. Horrible hack :( # try: self.signo = int(os.environ["SIGNO_OF_THE_COREDUMP"]) except KeyError: return False return True #Our initial set of testing will use the list Apple included in their #CrashWrangler announcement: # #Exploitable if: # Crash on write instruction # Crash executing invalid address # Crash calling an invalid address # Crash accessing an uninitialized or freed pointer as indicated by # using the MallocScribble environment variable # Illegal instruction exception # Abort due to -fstack-protector, _FORTIFY_SOURCE, heap corruption # detected # Stack trace of crashing thread contains certain functions such as # malloc, free, szone_error, objc_MsgSend, etc. def is_exploitable(self): self.exploitable_rating = 3 self.exploitable_desc = "" # siginfo.si_code: # If <= 0, then it's not a crash: # SI_ASYNCNL = -60 /* asynch name lookup completion */ # SI_TKILL = -6 /* tkill (and tgkill?) */ # SI_SIGIO = -5 /* queued SIGIO */ # SI_ASYNCIO = -4 /* AIO completion */ # SI_MESGQ = -3 /* real time mesq state change */ # SI_TIMER = -2 /* timer expiration (timer_create() with SIGEV_SIGNAL) */ # SI_QUEUE = -1 /* sigqueue */ # SI_USER = 0 /* kill, sigsend */ # Crashes have si_code > 0: # testDivideByZero: SIGFPE si_code=FPE_INTDIV(1), si_addr=0x40054a # x86-64 opcode 0x62: SIGILL si_code=ILL_ILLOPN(2), si_addr=0x40053f # x86-64 priv.insn.: SIGSEGV si_code=SI_KERNEL(128), si_addr=0 # testExecuteInvalid: SIGSEGV si_code=SEGV_MAPERR(1), si_addr=0x1c2404000 # testStackBufferOverflow ("ret" to bad address): # SIGSEGV si_code=SI_KERNEL(128), si_addr=0 # testStackRecursion: SIGSEGV si_code=SEGV_MAPERR(1), si_addr=0x7fff4c216d28 # testWriteRandom: SIGSEGV si_code=SEGV_MAPERR(1), si_addr=0x1eb004004 # However: # Keyboard signals (^C INT, ^\ QUIT, ^Z TSTP) also have si_code=SI_KERNEL. # SIGWINCH has si_code=SI_KERNEL. # SIGALRM from alarm(N) has si_code=SI_KERNEL. # Surprisingly, SIGPIPE has si_code=SI_USER! if self.si_code is not None: # Filter out user-generated signals: if self.si_code == -6 or self.si_code == -1: # SI_TKILL/SI_QUEUE self.exploitable_rating = 0 self.exploitable_desc = _("Signal sent by userspace code") return if self.si_code < 0: self.exploitable_rating = 0 self.exploitable_desc = _("Signal sent by timer/IO/async event") return # Unfortunately, this isn't reliable to flag user-sent signals: # not only SIGPIPE, but some other kernel signals have SI_USER # (grep kernel sources for "send_sig(sig, ..., 0)"). # At least we know it's not a crash. if self.si_code == 0: # SI_USER self.exploitable_rating = 0 self.exploitable_desc = _("Signal has siginfo.si_code = SI_USER") # Special case (a kernel buglet?) if self.signo == signal.SIGPIPE: self.exploitable_desc = _("Signal due to write to closed pipe") return # And kernel-generated ones: if self.si_code == 0x80: # SI_KERNEL if self.signo in (signal.SIGINT, signal.SIGQUIT, signal.SIGTSTP): self.exploitable_rating = 0 self.exploitable_desc = _("Signal sent by keyboard") return if self.signo in (signal.SIGTTIN, signal.SIGTTOU, signal.SIGHUP): self.exploitable_rating = 0 self.exploitable_desc = _("Job control signal sent by kernel") return if self.signo == signal.SIGWINCH: self.exploitable_rating = 0 self.exploitable_desc = _("Signal sent by window resize") return if self.signo == signal.SIGALRM: self.exploitable_rating = 0 self.exploitable_desc = _("Signal sent by alarm(N) expiration") return # else: Can't rule out "crash" signal: may be FPE/ILL/BUS/SEGV. # Fall through into signo/insn analysis. # else: We are here if signal was not from user and not SI_KERNEL. # Fall through into signo/insn analysis. # else: We are here if si_code isn't known. # Fall through into signo/insn analysis. # Guessing here... it might be kill(2) as well. # Should I add "Likely ..." to the descriptions? if self.signo in (signal.SIGINT, signal.SIGQUIT, signal.SIGTSTP): self.exploitable_rating = 0 self.exploitable_desc = _("Signal sent by keyboard") return if self.signo in (signal.SIGTTIN, signal.SIGTTOU, signal.SIGHUP): self.exploitable_rating = 0 self.exploitable_desc = _("Job control signal sent by kernel") return if self.signo == signal.SIGPIPE: self.exploitable_rating = 0 self.exploitable_desc = _("Signal due to write to broken pipe") return if self.signo == signal.SIGWINCH: self.exploitable_rating = 0 self.exploitable_desc = _("Signal sent by window resize") return if self.signo == signal.SIGALRM: self.exploitable_rating = 0 self.exploitable_desc = _("Signal sent by alarm(N) expiration") return # Which signals can coredump? # SIGABRT Abort signal from abort(3) # SIGQUIT Quit from keyboard # SIGXCPU CPU time limit exceeded # SIGXFSZ File size limit exceeded # SIGTRAP Trace/breakpoint trap # SIGSYS Bad argument to routine (SVr4) # SIGFPE Floating point exception # SIGILL Illegal Instruction # SIGSEGV Invalid memory reference # SIGBUS Bus error (bad memory access) if self.signo == signal.SIGABRT: self.exploitable_rating = 0 self.exploitable_desc = _("ABRT signal (abort() was called?)") return # Already handled above: #if self.signo == signal.SIGQUIT: # self.exploitable_rating = 0 # self.exploitable_desc = _("QUIT signal (Ctrl-\\ pressed?)") # return if self.signo == signal.SIGXCPU: self.exploitable_rating = 0 self.exploitable_desc = _("XCPU signal (over CPU time limit)") return if self.signo == signal.SIGXFSZ: self.exploitable_rating = 0 self.exploitable_desc = _("XFSZ signal (over file size limit)") return if self.signo == signal.SIGTRAP: self.exploitable_rating = 0 self.exploitable_desc = _("TRAP signal (can be a bug in a debugger/tracer)") return if self.signo == signal.SIGSYS: self.exploitable_rating = 1 self.exploitable_desc = _("SYS signal (unknown syscall was called?)") return if self.signo == signal.SIGFPE: self.exploitable_rating = 1 self.exploitable_desc = _("Arithmetic exception") # 1 is FPE_INTDIV if self.si_code == 1 or self.instruction_is_division: self.exploitable_rating = 0 self.exploitable_desc = _("Division by zero") return if self.signo == signal.SIGILL: self.exploitable_rating = 5 self.exploitable_desc = _("Illegal instruction (jump to a random address?)") return if self.signo != signal.SIGSEGV and self.signo != signal.SIGBUS: self.exploitable_rating = 2 # Pity that we can't give a more descriptive explanation self.exploitable_desc = _("Non-crash related signal") return if self.instruction_is_pushing: self.exploitable_rating = 4 self.exploitable_desc = _("Stack overflow") return if self.instruction_is_store: self.exploitable_rating = 6 self.exploitable_desc = _("Write to an invalid address") return if self.instruction_is_return: self.exploitable_rating = 7 self.exploitable_desc = _("Subroutine return to an invalid address (corrupted stack?)") return # Note: we check "ret" first, _then_ jumps. # Corrupted stack is different from corrupted data. if self.instruction_is_branch: self.exploitable_rating = 6 self.exploitable_desc = _("Jump to an invalid address") return if not self.current_instruction: self.exploitable_rating = 6 self.exploitable_desc = _("Jump to an invalid address") return if self.signo == signal.SIGBUS: self.exploitable_rating = 5 self.exploitable_desc = _("Access past the end of mapped file, invalid address, unaligned access, etc") return # default values remain (e.g. description is "") class AbrtExploitable(gdb.Command): "Analyze a crash to determine exploitability" def __init__(self): super(AbrtExploitable, self).__init__( "abrt-exploitable", gdb.COMMAND_SUPPORT, # command class gdb.COMPLETE_NONE, # completion method False # => it's not a prefix command ) init_gettext() # Called when the command is invoked from GDB def invoke(self, args, from_tty): si = SignalAndInsn() if not si.get_signal(): sys.stderr.write(_("Can't get signal no and do exploitability analysis\n")) return si.get_instruction() min_rating = 0 if args: args = args.split(None, 1) min_rating = int(args[0]) si.is_exploitable() if si.exploitable_desc: if si.exploitable_rating >= min_rating: f = sys.stdout if args and len(args) > 1: f = open(args[1], 'w') f.write(_("Likely crash reason: ") + si.exploitable_desc + "\n") f.write(_("Exploitable rating (0-9 scale): ") + str(si.exploitable_rating) + "\n") if si.current_instruction: f.write(_("Current instruction: ") + si.current_instruction + "\n") else: sys.stderr.write(_("Exploitability analysis came up empty\n")) AbrtExploitable()