[RFC: ABRT PATCH] Add gdb python plugin which analyzes vulnerability
Jiri Moskovcak
jmoskovc at redhat.com
Wed Jun 12 07:43:47 UTC 2013
Please, send it as a patch, reading just this email I have no clue where
to put this, how to install it, ...
Thank you,
Jirka
On 06/11/2013 05:56 PM, Denys Vlasenko wrote:
> On 06/11/2013 02:32 PM, Denys Vlasenko wrote:
>> This is an RFC, please do not commit.
>>
>> Usage:
>> Run abrt-action-analyze-vulnerability in a directory which contains
>> ./coredump file.
>> If crash looks exploitable, the tool creates ./exploitable file.
>> Example such a file:
>>
>> """
>> Program tried to write to an invalid address
>> Exploitable rating (1-10 scale):
>> 6
>> """
>>
>> This patch adds abrt-action-analyze-vulnerability invocation to
>> "EVENT=post-create analyzer=CCpp".
>>
>>
>> TODO:
>> * decide what to do if we _dont_ see particularly suspicious stuff -
>> do nothing? Or still create a file?
>> * improve error detection (e.g. what to do if gdb failed to run?)
>> * suppress stray gdb output
>> * set $SIGNO_OF_THE_COREDUMP to work around non-working $_siginfo.si_signo
>> * decide whether to push for $_signo support in gdb (I have a tested gdb patch)
>> * instruction analyzer is x86 specific now, make it per-arch
>> (how to get arch name???)
>
>
> Updated abrt-gdb-exploitable script (many more instructions added):
>
>
> #!/usr/bin/python
> # This is a GDB plugin.
> # Usage:
> # gdb --batch -ex "source THIS_FILE" -ex run -ex abrt-exploitable PROG
> # or
> # gdb --batch -ex "source THIS_FILE" -ex "core COREDUMP" -ex abrt-exploitable
>
> import gdb
> import os
> import signal
>
> _writing_instr = {
> # insn:N, where N:
> # 0: this insn never writes to memory
> # -1: this insn always writes to memory
> # -2: writes to memory if any operand is a memory operand
> # 1: writes to memory if 1st 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 -2
> "inc":-2,
> "dec":-2,
> "neg":-2,
> "not":-2,
> "pop":-2,
> # "Set byte on condition". One-operand insns.
> "seta":-2,
> "setae":-2,
> "setb":-2,
> "setbe":-2,
> "setc":-2,
> "sete":-2,
> "setg":-2,
> "setge":-2,
> "setl":-2,
> "setle":-2,
> "setna":-2,
> "setnae":-2,
> "setnb":-2,
> "setnbe":-2,
> "setnc":-2,
> "setne":-2,
> "setng":-2,
> "setnge":-2,
> "setnl":-2,
> "setnle":-2,
> "setno":-2,
> "setnp":-2,
> "setns":-2,
> "setnz":-2,
> "seto":-2,
> "setp":-2,
> "setpe":-2,
> "setpo":-2,
> "sets":-2,
> "setz":-2,
> # 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 -2.
> "shl":-2,
> "shr":-2,
> "sal":-2,
> "sar":-2,
> "rol":-2,
> "ror":-2,
> "rcl":-2,
> "rcr":-2,
> "shld":-2,
> "shrd":-2,
> # Bit tests. Any memory operand is a destination, can use -2.
> "bts":-2,
> "btr":-2,
> "btc":-2,
> # One-operand (register pair is another, implicit operand).
> "cmpxchg8b":-2,
> "cmpxchg16b":-2,
>
> # Either mem operand indicates write to mem.
> "xchg":-2,
>
> # String store insns.
> # Look similar to widening signed move "movs[bwl][wlq]",
> # but aliasing doesn't happen since widening move has two siffixes
> "movs":-1,
> "stos":-1,
> # 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,
>
> # These always write to stack:
> "push":-1,
> "pusha":-1,
> "pushf":-1,
> "enter":-1,
> #"call"?
>
> # One-operand insn.
> # These are system insns, but they do NOT cause exception in userspace.
> "smsw":-2,
> "sgdt":-2,
> "sidt":-2,
> "sldt":-2,
> "str":-2,
>
> # FPU/SIMD madness follows.
>
> # FPU store insns. One-operand.
> "fsts":-2,
> "fstl":-2,
> #"fstt" doesn't exist
> "fstps":-2,
> "fstpl":-2,
> "fstpt":-2,
> # Saving state. One-operand insns.
> "fstcw":-2,
> "fnstcw":-2,
> "fstsw":-2,
> "fnstsw":-2,
> "fstenv":-2,
> "fnstenv":-2,
> "fsave":-2,
> "fnsave":-2,
> "fxsave":-2,
> "xsave":-2,
> "xsaveopt":-2,
> "fsave64":-2,
> "fnsave64":-2,
> "fxsave64":-2,
> "xsave64":-2,
> "xsaveopt64":-2,
> "stmxcsr":-2,
> "vstmxcsr":-2,
> # SIMD store insns.
> # Three-operand insns. Any memory operand is a destination.
> "vcvtps2ph":-2,
> "extractps":-2,
> "vextractps":-2,
> #[v]extractpd does not exist
> "vextractf128":-2,
> "vextracti128":-2,
> "pextr":-2, # covers pextr[bwq]
> "pextrd":-2,
> "vpextr":-2,
> "vpextrd":-2,
> "vmaskmovpd":-2,
> "vmaskmovps":-2,
> "vpmaskmovd":-2,
> "vpmaskmovq":-2,
> # These insns have implicit (%edi) dest operand:
> "maskmovq":-1, # mmx version
> "maskmovdqu":-1,
> "vmaskmovdqu":-1,
>
> # check binutils/gas/testsuite/gas/i386/* for more weird insns
> # http://download.intel.com/products/processor/manual/253666.pdf
> # http://download.intel.com/products/processor/manual/253667.pdf
> # http://software.intel.com/sites/default/files/m/0/3/c/d/4/18187-d9156103.pdf
> # http://download-software.intel.com/sites/default/files/319433-014.pdf
>
> #"vmovXXX" - special-cased in the code
> "mov":2
> }
>
> _jumping_instr = {
> "jmp":-1, # indirect jumps/calls with garbage data
> "call":-1, # call: also possible that stack is exhausted (infinite recursion)
> #"push":-1, ?
> #"pusha":-1,
> #"enter":-1,
>
> "ret":-1 # stack smashed
> }
>
> #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 _get_signal_and_instruction(self):
> self.signo = None
> try:
> sig = gdb.parse_and_eval("$_signo") # ("$_siginfo.si_signo")
> # type(sig) = <type 'gdb.Value'>
> # sig is 8 (for SIGFPE)
> self.signo = int(sig)
> except gdb.error:
> # Python Exception <class 'gdb.error'> 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.
> #
> # HACK_ALERT: kernels before 3.?.? do not record siginfo in coredumps,
> # so $_siginfo isn't present.
> # Lets see whether we are running from the abrt and it provided us with signal number
> #
> try:
> self.signo = int(os.environ["SIGNO_OF_THE_COREDUMP"])
> except KeyError:
> return False
>
> self.current_instruction = None
> self.mnemonic = None
> self.operands = ""
> try:
> # just "disassemble $pc" won't work if $pc doesn't point
> # inside a known function
> instructions = gdb.execute("disassemble $pc,$pc+32", to_string=True)
> # type(instructions) = <type 'str'>
> except gdb.error:
> # For example, if tracee already exited normally:
> # Python Exception <class 'gdb.error'> No registers.:
> return False
>
> raw_instructions = instructions
> #print instructions
> 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:
> # not False! we determined that $pc points to a bad address,
> # which is an interesting fact.
> return True
>
> self.current_instruction = instructions[current]
> # TODO: too simplistic.
> # consider this example:
> # "data32 data32 data32 nopw %cs:0x0(%rax,%rax,1)"
> t = self.current_instruction.split(None,2)
> self.mnemonic = t[0]
> if len(t) > 1:
> self.operands = t[1]
> return True
>
> def _fetch_insn_from_table(ins, table):
> if not ins:
> return None
> if ins in table.keys():
> return table[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.keys():
> return table[ins]
> return None
>
> def _instruction_is_writing(self):
> operand = _fetch_insn_from_table(self.mnemonic, _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 == -1: # no need to check operands, it's a write
> return 1
>
> #FIXME: this is a memory operand! "add 0x7b,%eax"
> # (as opposed to "add $0x7b,%eax", where 0x7b is immediate)
> #FIXME: st(n) is not a memory operand
>
> paren = self.operands.find("(")
> if paren < 0:
> return False # no memory operands
>
> if operand == -2: # any mem operand indicates write
> return 1
>
> comma = self.operands.find(",")
> if paren < 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:
> # paren 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 False
> return True
>
> def _instruction_is_jump(self):
> if _fetch_insn_from_table(self.mnemonic, _jumping_instr):
> return True
> return False
>
>
> def _is_exploitable(self):
> self.exploitable_rating = 3
> self.exploitable_desc = ""
> if 0:
> pass
> elif self.signo == signal.SIGFPE:
> self.exploitable_rating = 1
> self.exploitable_desc = "Arithmetic exceptions (such as division by zero) are rarely exploitable"
> # TODO? look at instruction, if it is a division, lower rating to 0?
> # Or at least give a better desc ("Division by zero" and "(Other) arithmetic exception" is more informative)
> elif self.signo == signal.SIGILL:
> self.exploitable_rating = 5
> self.exploitable_desc = "SIGILL may be an indication that program jumped to a random address"
> elif not self.current_instruction: # TODO: and SIGSEGV?
> self.exploitable_rating = 6
> self.exploitable_desc = "Program jumped to an invalid address"
> elif _instruction_is_writing(self):
> self.exploitable_rating = 6
> self.exploitable_desc = "Program tried to write to an invalid address"
> #elif self.signo = signal.SIGfoo:
>
>
> 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
> )
>
> # Called when the command is invoked from GDB
> def invoke(self, arg, from_tty):
> if not _get_signal_and_instruction(self):
> return
> #print "w", _instruction_is_writing(self)
> _is_exploitable(self)
> if self.exploitable_desc and self.exploitable_rating > 3:
> f = sys.stdout
> if arg:
> f = open(arg, 'w')
> f.write(self.exploitable_desc + "\n")
> f.write("Exploitable rating (1-10 scale):\n" + str(self.exploitable_rating) + "\n")
>
> AbrtExploitable()
>
More information about the Crash-catcher
mailing list