[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