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

Jakub Filak jfilak at redhat.com
Wed Jun 12 08:05:09 UTC 2013


You may find this link helpful:
http://git-scm.com/book/en/Git-Branching-Remote-Branches

----- Original Message -----
From: "Denys Vlasenko" <dvlasenk at redhat.com>
To: crash-catcher at 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()



More information about the Crash-catcher mailing list