### BEGIN LICENSE ###
### Use of the triage tools and related source code is subject to the terms
### of the license below.
###
### ------------------------------------------------------------------------
### Copyright (C) 2011 Carnegie Mellon University. All Rights Reserved.
### ------------------------------------------------------------------------
### Redistribution and use in source and binary forms, with or without
### modification, are permitted provided that the following conditions are
### met:
###
### 1. Redistributions of source code must retain the above copyright
###    notice, this list of conditions and the following acknowledgments
###    and disclaimers.
###
### 2. Redistributions in binary form must reproduce the above copyright
###    notice, this list of conditions and the following disclaimer in the
###    documentation and/or other materials provided with the distribution.
###
### 3. The names "Department of Homeland Security," "Carnegie Mellon
###    University," "CERT" and/or "Software Engineering Institute" shall
###    not be used to endorse or promote products derived from this software
###    without prior written permission. For written permission, please
###    contact permission@sei.cmu.edu.
###
### 4. Products derived from this software may not be called "CERT" nor
###    may "CERT" appear in their names without prior written permission of
###    permission@sei.cmu.edu.
###
### 5. Redistributions of any form whatsoever must retain the following
###    acknowledgment:
###
###    "This product includes software developed by CERT with funding
###     and support from the Department of Homeland Security under
###     Contract No. FA 8721-05-C-0003."
###
### THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND
### CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, EITHER
### EXPRESS OR IMPLIED, AS TO ANY MATTER, AND ALL SUCH WARRANTIES, INCLUDING
### WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE
### EXPRESSLY DISCLAIMED. WITHOUT LIMITING THE GENERALITY OF THE FOREGOING,
### CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND
### RELATING TO EXCLUSIVITY, INFORMATIONAL CONTENT, ERROR-FREE OPERATION,
### RESULTS TO BE OBTAINED FROM USE, FREEDOM FROM PATENT, TRADEMARK AND
### COPYRIGHT INFRINGEMENT AND/OR FREEDOM FROM THEFT OF TRADE SECRETS.
### END LICENSE ###
###
### Portions Copyright 2013, BlackBerry Ltd. All Rights Reserved.
###

# Jonathan Foote
# jmfoote@loyola.edu

'''
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))

import argparse
import os
import re
import sys
import warnings

version = "1.32"

# 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 lib.classifier as classifier
import lib.arch as arch
from lib.tools import print_machine_string
from distutils.version import LooseVersion

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.
    '''

    # check GDB ver
    if LooseVersion(gdb_ver()) < LooseVersion("7.2"):
        warnings.warn("GDB v{} may not support required Python API".format(gdb_ver()))

_re_gdb_version = re.compile(r"\d+\.\d+")
def gdb_ver():
    '''
    Gets the GDB version number as a string.
    '''
    gdbstr = gdb.execute("show version", False, True).splitlines()[0]
    version = _re_gdb_version.search(gdbstr)
    if version is None:
        warnings.warn("Error while parsing gdb version string: {}".format(gdbstr))
    return version.group()

class NiceArgParserExit(RuntimeError):
    pass

class NiceArgParser(argparse.ArgumentParser):
    def exit(self, *args):
        raise NiceArgParserExit(*args)

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.
    '''

    _cmdstr = "exploitable"
    _re_gdb_pc = re.compile(r"^=>.*$")
    def __init__(self):
        '''
        Specifies the command string that invokes this Command from the GDB
        shell. See GDB Python API documentation for details
        '''
        gdb.Command.__init__(self, 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(str(e))
            return

        try:
            disas = gdb.execute("disas $pc", False, True).splitlines()
        except RuntimeError as e:
            warnings.warn(str(e))
            return

        pos = 0
        for line in disas:
            if self._re_gdb_pc.match(line):
                break
            pos += 1
        print("\n".join(disas[max(pos-5, 0):pos+5]))

    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
        '''
        check_version()

        op = NiceArgParser(prog=self._cmdstr, description=self.__doc__)
        op.add_argument("-v", "--verbose", action="store_true",
            help="print analysis info from the Inferior")
        op.add_argument("-m", "--machine", action="store_true",
            help="Print output in a machine parsable format")
        op.add_argument("-p", "--pkl-file", type=argparse.FileType("wb"),
            help="pickle exploitability classification object and store to PKL_FILE")
        op.add_argument("-a", "--asan-log", type=argparse.FileType(),
            help="Symbolize and analyze AddressSanitizer output (assumes "
            "executable is loaded) (WARNING: untested).")
        op.add_argument("-b", "--backtrace-limit", type=int,
            help="Limit number of stack frames in backtrace to supplied value. "
            "0 means no limit.", default=1000)

        try:
            args = op.parse_args(gdb.string_to_argv(argstr))
        except NiceArgParserExit:
            return

        import logging
        try:
            target = arch.getTarget(args.asan_log, args.backtrace_limit)
            c = classifier.Classifier().getClassification(target)
        except Exception as e:
            logging.exception(e)
            raise e

        if args.pkl_file:
            import pickle as pickle
            pickle.dump(c, args.pkl_file, 2)
            return

        if args.verbose:
            print("'exploitable' version {}".format(version))
            print(" ".join([str(i) for i in os.uname()]))
            print("Signal si_signo: {} Signal si_addr: {}".format(target.si_signo(), target.si_addr()))
            print("Nearby code:")
            self.print_disassembly()
            print("Stack trace:")
            print(str(target.backtrace()))
            print("Faulting frame: {}".format(target.faulting_frame()))

        if args.machine:
            print_machine_string(c, target)
        else:
            gdb.write(str(c))
        gdb.flush()

ExploitableCommand()
