[RFC: ABRT PATCH] Add gdb python plugin which analyzes vulnerability

Denys Vlasenko dvlasenk at redhat.com
Tue Jun 11 12:32:29 UTC 2013


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 at 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 &&
         (
-- 
1.8.1.4



More information about the Crash-catcher mailing list