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???)
Signed-off-by: Denys Vlasenko dvlasenk@redhat.com --- abrt.spec.in | 2 + src/plugins/Makefile.am | 10 +- src/plugins/abrt-action-analyze-vulnerability | 9 + src/plugins/abrt-gdb-exploitable | 268 ++++++++++++++++++++++++++ src/plugins/ccpp_event.conf | 5 +- 5 files changed, 291 insertions(+), 3 deletions(-) create mode 100755 src/plugins/abrt-action-analyze-vulnerability create mode 100755 src/plugins/abrt-gdb-exploitable
diff --git a/abrt.spec.in b/abrt.spec.in index fec5b56..ce5f944 100644 --- a/abrt.spec.in +++ b/abrt.spec.in @@ -670,6 +670,7 @@ gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || : %{_initrddir}/abrt-ccpp %endif %{_libexecdir}/abrt-hook-ccpp +%{_libexecdir}/abrt-gdb-exploitable
# attr(6755) ~= SETUID|SETGID %attr(6755, abrt, abrt) %{_libexecdir}/abrt-action-install-debuginfo-to-abrt-cache @@ -677,6 +678,7 @@ gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || : %{_bindir}/abrt-action-analyze-c %{_bindir}/abrt-action-trim-files %{_bindir}/abrt-action-analyze-core +%{_bindir}/abrt-action-analyze-vulnerability %{_bindir}/abrt-action-install-debuginfo %{_bindir}/abrt-action-generate-backtrace %{_bindir}/abrt-action-generate-core-backtrace diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am index 767c045..20e0297 100644 --- a/src/plugins/Makefile.am +++ b/src/plugins/Makefile.am @@ -3,6 +3,7 @@ bin_SCRIPTS = \ abrt-action-install-debuginfo \ abrt-action-analyze-core \ + abrt-action-analyze-vulnerability \ abrt-action-analyze-vmcore \ abrt-action-list-dsos \ abrt-action-perform-ccpp-analysis \ @@ -29,9 +30,12 @@ bin_PROGRAMS += \ abrt-bodhi endif
-libexec_PROGRAMS = abrt-action-install-debuginfo-to-abrt-cache +libexec_PROGRAMS = \ + abrt-action-install-debuginfo-to-abrt-cache
-libexec_SCRIPTS = abrt-action-ureport +libexec_SCRIPTS = \ + abrt-action-ureport \ + abrt-gdb-exploitable
#dist_pluginsconf_DATA = Python.conf
@@ -68,6 +72,7 @@ PYTHON_FILES = \ abrt-action-install-debuginfo.in \ abrt-action-list-dsos \ abrt-action-analyze-core \ + abrt-action-analyze-vulnerability \ abrt-action-analyze-vmcore.in \ abrt-action-perform-ccpp-analysis.in
@@ -84,6 +89,7 @@ EXTRA_DIST = \ abrt-action-analyze-vmcore \ abrt-action-save-kernel-data \ abrt-action-ureport \ + abrt-gdb-exploitable \ https-utils.h \ post_report.xml.in \ abrt-action-analyze-ccpp-local diff --git a/src/plugins/abrt-action-analyze-vulnerability b/src/plugins/abrt-action-analyze-vulnerability new file mode 100755 index 0000000..47d2ce4 --- /dev/null +++ b/src/plugins/abrt-action-analyze-vulnerability @@ -0,0 +1,9 @@ +#!/bin/sh + +if type gdb >/dev/null 2>&1; then + # gdb is avaliable + gdb --batch \ + -ex 'python execfile("/usr/libexec/abrt-gdb-exploitable")' \ + -ex 'core-file ./coredump' \ + -ex 'abrt-exploitable ./exploitable' +fi diff --git a/src/plugins/abrt-gdb-exploitable b/src/plugins/abrt-gdb-exploitable new file mode 100755 index 0000000..116d9db --- /dev/null +++ b/src/plugins/abrt-gdb-exploitable @@ -0,0 +1,268 @@ +#!/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:which operand to check for mem ref" + "add":2, + "adc":2, + "sub":2, + "sbb":2, + "and":2, + "xor":2, + "or":2, + "inc":1, + "dec":1, + "neg":1, + "not":1, + # sarl $2,(%rcx), BUT: + # FIXME: shifts by 1 have one operand!!!- sarb (%rax) + "shl":2, + "shr":2, + "sal":2, + "sar":2, + "rol":2, + "ror":2, + "rcl":2, + "rcr":2, + # shld $0xc6,%ecx,(%rdi) + #"shld":3, + #"shrd":3, + "bts":1, + "btr":1, + "btc":1, + "pop":1, + + "xchg":-2, # either mem operand indicates write to mem + + # FIXME: aliased to widening move "movs[bwl][wlq]" which never stores to mem + "movs":-1, # don't check operands, this insn always writes to mem + "stos":-1, + # these always write to stack: + #"push":-1, + #"pusha":-1, + #"pushf":-1, + #"enter":-1, + + "cmpxchg":2, + "xadd":2, + # with binutils-like disasm, implicit register operands aren't shown + "cmpxchg8b":1, + "cmpxchg16b":1, + + #"f[x]save":1, + + # check binutils/gas/testsuite/gas/i386/* for more weird insns + + "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 + + 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() diff --git a/src/plugins/ccpp_event.conf b/src/plugins/ccpp_event.conf index dfc4908..aa8cdb3 100644 --- a/src/plugins/ccpp_event.conf +++ b/src/plugins/ccpp_event.conf @@ -15,10 +15,13 @@ EVENT=post-create analyzer=CCpp exit 1 fi # Try generating backtrace, if it fails we can still use - # the UUID generated by abrt-action-analyze-c + # the hash generated by abrt-action-analyze-c ##satyr migration: #satyr abrt-create-core-stacktrace "$DUMP_DIR" abrt-action-generate-core-backtrace + # Run GDB plugin to see if crash looks exploitable + abrt-action-analyze-vulnerability + # Generate hash abrt-action-analyze-c && abrt-action-list-dsos -m maps -o dso_list && (
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()
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()
You may find this link helpful: http://git-scm.com/book/en/Git-Branching-Remote-Branches
----- Original Message ----- From: "Denys Vlasenko" dvlasenk@redhat.com To: crash-catcher@lists.fedorahosted.org Sent: Tuesday, June 11, 2013 5:56:58 PM Subject: Re: [RFC: ABRT PATCH] Add gdb python plugin which analyzes vulnerability
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()
crash-catcher@lists.fedorahosted.org