[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