### BEGIN LICENSE ###
### Use of the CERT Basic Fuzzing Framework and related source code
### is subject to the terms of the following licenses:
###
### GNU Public License (GPL) Rights pursuant to Version 2, June 1991
### Government Purpose License Rights (GPLR) pursuant to DFARS
### 252.227.7013
###
### NO WARRANTY
###
### ANY INFORMATION, MATERIALS, SERVICES, INTELLECTUAL
### PROPERTY OR OTHER PROPERTY OR RIGHTS GRANTED OR
### PROVIDED BY CARNEGIE MELLON UNIVERSITY PURSUANT TO
### THIS LICENSE (HEREINAFTER THE "DELIVERABLES") ARE ON
### AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES
### NO WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED
### AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO,
### WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE,
### MERCHANTABILITY, INFORMATIONAL CONTENT,
### NONINFRINGEMENT, OR ERROR-FREE OPERATION. CARNEGIE
### MELLON UNIVERSITY SHALL NOT BE LIABLE FOR INDIRECT,
### SPECIAL OR CONSEQUENTIAL DAMAGES, SUCH AS LOSS OF
### PROFITS OR INABILITY TO USE SAID INTELLECTUAL
### PROPERTY, UNDER THIS LICENSE, REGARDLESS OF WHETHER
### SUCH PARTY WAS AWARE OF THE POSSIBILITY OF SUCH
### DAMAGES. LICENSEE AGREES THAT IT WILL NOT MAKE ANY
### WARRANTY ON BEHALF OF CARNEGIE MELLON UNIVERSITY,
### EXPRESS OR IMPLIED, TO ANY PERSON CONCERNING THE
### APPLICATION OF OR THE RESULTS TO BE OBTAINED WITH
### THE DELIVERABLES UNDER THIS LICENSE.
###
### Licensee hereby agrees to defend, indemnify, and hold harmless
### Carnegie Mellon University, its trustees, officers, employees,
### and agents from all claims or demands made against them (and any
### related losses, expenses, or attorney's fees) arising out of, or
### relating to Licensee's and/or its sub licensees' negligent use or
### willful misuse of or negligent conduct or willful misconduct
### regarding the Software, facilities, or other rights or assistance
### granted by Carnegie Mellon University under this License,
### including, but not limited to, any claims of product liability,
### personal injury, death, damage to property, or violation of any
### laws or regulations.
###
### Carnegie Mellon University Software Engineering Institute
### authored documents are sponsored by the U.S. Department of
### Defense under Contract F19628-00-C-0003. Carnegie Mellon
### University retains copyrights in all material produced under this
### contract. The U.S.  Government retains a non-exclusive,
### royalty-free license to publish or reproduce these documents, or
### allow others to do so, for U.S.  Government purposes only
### pursuant to the copyright license under the contract clause at
### 252.227.7013.
### END LICENSE ###

'''
Implements GDB entry point for GDB 'exploitable' command

example usage:
  (gdb) source exploitable.py
  (gdb) exploitable

'''
try:
    import gdb
except ImportError as e:
    raise ImportError("This script must be run in GDB: ", str(e))

# Code below adds directory containing this file to python path iff this script
# is being invoked standalone (i.e. not when it is imported). Allows script
# to be executed via GDB when not on GDB's PYTHONPATH without increasing 
# vulnerability to carpet bombing attacks beyond that of executing a Python script 
# outside GDB.
import sys, os
if __name__ == "__main__":
    # Code below contains a workaround for a bug in GDB's Python API: 
    # os.path.abspath returns an incorrect string when this script is sourced
    # from a path containing "~" 
    abspath = os.path.abspath(__file__)
    pos = abspath.find("/~/")
    if pos != -1:
        abspath = abspath[pos+1:]
    abspath = os.path.expanduser(abspath)
    sys.path.append(os.path.dirname(abspath))

import warnings
from optparse import OptionParser
import cPickle as pickle
import re
import lib.versions as versions
import lib.gdb_wrapper as gdb_wrapper
import lib.classifier as classifier

def check_version():
    '''
    Checks to see if current operating environment has been tested with
    this command. If not, a warning is generated.

    If the current operation environment is not Linux, an error is generated.
    '''
    if not sys.platform.startswith('linux'):
        raise NotImplementedError("unsupported platform: %s" % sys.platform)

    # check GDB ver
    if gdb_ver() < versions.min_gdb_version:
        warnings.warn("GDB v%s may not support required Python API" % gdb_ver())

ver_regex = re.compile("^.*?([\d]\.[\d]).*$")
def gdb_ver():
    '''
    Gets the GDB version number as a string.
    '''
    gdbstr = gdb.execute("show version", False, True).splitlines()[0]
    groups = re.match(ver_regex, gdbstr).groups()
    if len(groups) != 1:
        warnings.warn("Error while parsing gdb version string: %s", gdbstr)
    return groups[0]

class ExploitableCommand (gdb.Command):
    '''
    A GDB Command that determines how exploitable the current state of the
    Inferior (the program being debugged) is. Either prints the result to
    GDB's STDOUT or pickles the result to a file.

    This command is designed to be run just after the Inferior stops on
    a signal, before any commands that might change the underlying state
    of GDB have been issued. WARNING: This command may change the underlying
    state of GDB (ex: changing the disassembler flavor).

    Type <cmd> -h for options. Note specifying incorrect command options may
    cause GDB to exit.
    '''
    _cmdstr = "exploitable"
    _pc_regex = re.compile("^=>.*$")
    def __init__ (self):
        '''
        Specifies the command string that invokes this Command from the GDB
        shell. See GDB Python API documentation for details
        '''
        super (ExploitableCommand, self).__init__ (self._cmdstr, gdb.COMMAND_OBSCURE)

    def print_disassembly(self):
        '''
        Attempts to print some disassembled instructions from the function
        containing $pc to GDB's STDOUT. If GDB is unable to print the
        disassembled instructions, an error message will be printed.

        If GDB's version is less than 7.3, the entire disassembled function
        will be printed (if possible). Otherwise only a subset of the
        function will be printed.

        This behavior is due to a bug in the Python API of earlier versions
        of GDB. In these versions, the results of the 'disassemble' command
        are always printed directly to GDB's STDOUT rather than optionally
        being suppressed and passed as a return value via the Python API.
        '''
        if gdb_ver() < "7.3":
            try:
                gdb.execute("disas $pc", False, True)
            except RuntimeError as e:
                warnings.warn(e)
            return

        try:
            disas = gdb.execute("disas $pc", False, True).splitlines()
            pos = 0
            for line in disas:
                if re.match(self._pc_regex, line):
                    break
                pos += 1
            print "\n".join(disas[max(pos-5,0):pos+5])
        except RuntimeError as e:
            warnings.warn(e)

    def invoke (self, argstr, from_tty):
        '''
        Called when this Command is invoked from GDB. Prints classification of
        Inferior to GDB's STDOUT.

        Note that sys.stdout is automatically redirected to GDB's STDOUT.
        See GDB Python API documentation for details
        '''

        # Note that OptionParser is configured to work without
        # sys.argv and to minimize the cases where OptionParser
        # calls sys.exit(), which kills the parent GDB process
        op = OptionParser(prog=self._cmdstr, add_help_option=False,
                          description="type 'help exploitable' for "
                          "description. WARNING: typing an invalid "
                          "option string may cause GDB to exit.")
        op.add_option("-v", "--verbose", action="store_true",
                      dest="verbose", default=False,
                      help="print analysis info from the Inferior")
        op.add_option("-p", "--pkl-file", dest="pkl_file",
                      help="pickle exploitability classification object and "
                      "store to PKL_FILE")
        op.add_option("-h", "--help", action="store_true",
                      dest="help", default=False, help="Print this message")
        (opts, args) = op.parse_args(gdb.string_to_argv(argstr))
        if opts.help:
            # Print help manually b/c Default OptionParser help calls sys.exit
            op.print_help()
            return
        self._options = opts

        # Clear any cached signo and si_addr values rom previous runs
        gdb_wrapper.Target._si_addr = None
        gdb_wrapper.Target._si_signo = None

        try:
            target = gdb_wrapper.getTarget()
        except gdb_wrapper.GdbWrapperError as e:
            raise gdb.GdbError(e)

        c = classifier.getClassification(target)
        if self._options.pkl_file:
            path = os.path.expanduser(self._options.pkl_file)
            pickle.dump(c, file(path, "wb"))
            return

        if self._options.verbose:
            print "'exploitable' version %s" % versions.exploitable_version
            print " ".join([str(i) for i in os.uname()])
            print "Signal si_signo: %s Signal si_addr: %s" % \
             (target.si_signo(), target.si_addr())
            print "Nearby code:"
            self.print_disassembly()
            print "Stack trace:"
            print target.backtrace()
            print "Faulting frame: %s" % target.faulting_frame()

        print c

check_version()
ExploitableCommand()
