GDB exploitable插件源码分析

0. 背景

源码: jfoote/exploitable: The ‘exploitable’ GDB plugin (github.com)

1. exploitable命令实现

1.1 使用方式

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

1.2 exploit.py

class ExploitableCommand(gdb.Command):
    '''
    判断当前状态的可利用程度,将结果打印到stdout 或者 提取到文件中
    '''

    _cmdstr = "exploitable"
    _re_gdb_pc = re.compile(r"^=>.*$")
    def __init__(self):
        gdb.Command.__init__(self, self._cmdstr, gdb.COMMAND_OBSCURE)

    def print_disassembly(self):
        pass

    def invoke(self, argstr, from_tty):
        '''输入命令的时候被调用'''
        check_version()#检查
        op = NiceArgParser(prog=self._cmdstr, description=self.__doc__)
        op.add_argument("-v", "--verbose", action="store_true",
            help="打印分析信息")
        op.add_argument("-m", "--machine", action="store_true",
            help="以机器可解析的格式打印输出")
        op.add_argument("-p", "--pkl-file", type=argparse.FileType("wb"),
            help="打包可利用性分类对象并存储到PKL_FILE")
        op.add_argument("-a", "--asan-log", type=argparse.FileType(),
            help="符号化并分析AddressSanitizer的输出")
        op.add_argument("-b", "--backtrace-limit", type=int,
            help="将回溯中的堆栈帧数限制为所提供的值"
            "0 代表不限制.", 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()

其框架很简单,看到其重要为其中有颜色的两个步骤1. 获得目标 2.分类器分类

GDB exploitable插件源码分析_第1张图片

2. arch.getTarget()获取目标

2.1 getTarget()函数

def getTarget(asan_log_file=None, bt_limit=0):
    ''' 返回当前的Target,它是一个Python包装器,代表底层Linux GDB Inferior对象的当前状态。
    '''

    # 获得操作系统信息
    osabi = Target._re_gdb_osabi.search(str(gdb.execute("show osabi", False,
                                                        True))).group(1)
    # 尝试用python的API判断架构
    arch = gdb.selected_frame().architecture().name()
    # 反馈解析
    if arch == None:
        arch_str = str(gdb.execute("show architecture", False, True))
        # 如果我们在gdbmultiarch中设置了arch,它就是 "假定的"
        if "assumed" in arch_str:
            arch = arch_str
        else:
            arch = Target._re_gdb_arch.search(arch_str).group(1)

    # 根据参数和操作系统信息实例化一个目标。
    # ASAN + i386 + *
    if asan_log_file and arch.startswith("i386"):
        target = ASanTarget(asan_log_file.read(), bt_limit)
        target.analyzer = ASanAnalyzer(target)
        return target
    # ASAN + ARM + QNX
    elif asan_log_file and arch.lower()[:3] == "arm" and osabi == "QNX Neutrino":
        target = QnxASanTarget(asan_log_file.read(), bt_limit)
        target.analyzer = ASanAnalyzer(target)
        return target
    # ASAN + ARM + *
    elif asan_log_file and arch.lower()[:3] == "arm":
        target = ArmASanTarget(asan_log_file.read(), bt_limit)
        target.analyzer = ArmASanAnalyzer(target)
        return target
    # * + ARM + QNX
    elif arch.lower()[:3] == "arm" and osabi == "QNX Neutrino":
        target = QnxTarget(bt_limit)
        target.analyzer = ArmAnalyzer(target)
        return target
    # * + i386 + *
    elif arch.startswith("i386"):
        target = x86Target(bt_limit)
        target.analyzer = Analyzer(target)
        return target
    # * + ARM + *
    elif arch.lower()[:3] == "arm":
        target = ArmTarget(bt_limit)
        target.analyzer = ArmAnalyzer(target)
        return target
    elif "mips" in arch.lower():
        target = MipsTarget(bt_limit)
        target.analyzer = MipsAnalyzer(target)
        return target
    else:
        raise NotImplementedError("no support for arch=%s and osabi=%s" % (arch, osabi))

    return Target(bt_limit)

总结起来,这这个函数完成了以下步骤

  1. 根据gdb获得操作系统的架构arch
  2. 根据 有没有 asan_log_filearch , 返回具体的target

target类型比较多,我们其中的X86Target来分析以下

2.2 X86Target()

class x86Target(Target):
    '''
    A wrapper for an x86 Linux GDB Inferior.
    '''

    def __init__(self, bt_limit=0):
        Target.__init__(self, bt_limit)
        self._check_inferior_state()
        gdb.execute("set disassembly-flavor intel", False, True) #将汇编设置为intel格式

而target的结构如下

class Target(object):
    
    def __init__(self, bt_limit=0):
        self._check_inferior_state()
        self.bt_limit = bt_limit

    def _check_inferior_state(self):
 
    @memoized
    def backtrace(self):

    def hash(self):

    @memoized
    def procmaps(self):

    @memoized
    def faulting_frame(self):

    @staticmethod
    def sym_addr(sym):

    @memoized
    def current_instruction(self):

    def _getInstruction(self, gdbstr):

    @memoized
    def pc(self):

    @memoized
    def stack_pointer(self):

    @memoized
    def pid(self):

    @memoized
    def pointer_size(self):
    
    @memoized
    def si_signo(self):

    @memoized
    def si_addr(self):

3. classifier.Classifier().getClassification(target)分类

这部分就是exploitable插件的核心部分,即根据gdb的状态来判断是否可利用及类型。

3.1 Classfifier()分类器

class Classifier(object):
    
    _major_hash_depth = 5

    def getRules(self, target):
        '''用于分类的规则(dicts)的嵌套列表 在 rules.py 中指定的规则被组织成 AttrDicts ("规则")。
        每个规则由
            一个标签
            一个 match_function 
        组成。
        '''

        processed_rules = []
        num_rules = sum(len(rl) for (_, rl) in rules.rules)
        ranking = 1
        for cat, user_rule_list in rules.rules:
            for user_rule in user_rule_list:
                match_function = partial(getattr(target.analyzer, user_rule["match_function"])) 
                tag_data = copy.deepcopy(user_rule)
                del tag_data["match_function"]
                tag_data["ranking"] = (ranking, num_rules)
                tag_data["category"] = cat
                rule = AttrDict(matches=match_function, tag=Tag(tag_data))
                processed_rules.append(rule)
                ranking += 1

        return processed_rules

    def getClassification(self, target):
        '''
        返回目标的可利用性分类
        '''
        c = Classification(target) #获得分类器
        
        # 规则匹配
        for rule in self.getRules(target):
            try:
                match = rule.matches()
                # 如果匹配成功,则打上标签
                if match:
                    c += rule.tag
            except Exception as e:
                warnings.warn("Error while analyzing rule {}: {}\n{}".format(
                    rule.tag, e, traceback.format_exc()))

        c.hash = target.hash() #计算hash
        return c

3.2 Classification分类器

查看Classification类的代码

class Classification(AttrDict):
    ''' 描述可利用程序
    '''
    def __init__(self, target):
        AttrDict.__init__(self)
        self.tags = []

    def __add__(self, tag):
        if not issubclass(type(tag), Tag):
            raise TypeError("cannot add type {} to type {}".format(type(tag), type(self)))
        self.tags.append(tag)
        self.tags.sort()
        for k, v in self.tags[0].__dict__.items():
            self[k] = v

        return self

    # for python3
    def __lt__(self, other):

    def __cmp__(self, other):

    def __str__(self):

可以看到其继承了AttrDict,然后重载了一些方法。

class AttrDict(dict):
    def __getattribute__(self, name):
        try:
            return dict.__getattribute__(self, name)
        except AttributeError:
            if name in self:
                return self[name]
            raise
    def __setattr__(self, name, value):
        dict.__setattr__(self, name, value)
        try:
            del self[name]
        except KeyError:
            pass

AttrDict继承于字典,则Classification() 就是一个关于tag的字典。

3.3 rule.rule()判断规则

rule.py中使用字典记录了各种漏洞类型及漏洞可利用性规则,进行总结分类后如下:

A. exploitable可利用

标示 描述 判断函数 判断逻辑 (x86为例)
ReturnAv 目标在一条返回指令 上崩溃,这可能表明堆栈损坏 isReturnAv 1. 信号为 SIGSEGV, SIGBUS之一
2. 有当前指令且指令的助记符为 iret,ret 之一
UseAfterFree 目标试图访问 一个位于已经被释放的堆缓冲区内的地址。 isUseAfterFree 1. ASAN 的原因为heap-use-after-free
2. 没有ASAN直接返回
SegFaultOnPc 目标试图在一个与程序计数器相匹配的地址上访问数据 isSegFaultOnPcNotNearNull 1. 信号为 SIGSEGV, SIGBUS之一
2. 段错误的地址等于PC的值
BranchAv 目标在一条分支指令 上崩溃了,这可能表明控制流被污染了 isBranchAvNotNearNull 1. 是分支地址(ja,jae等等或者call,callq)崩溃
2. 错误地址 接近NULL
StackCodeExecution 目标在进程的堆栈区域内执行代码 时因错误而停止 isErrorWhileExecutingFromStack 1. 是良性的信号(SIGTERM等)
2. PC指在stack中
StackBufferOverflow 由于检测到堆栈缓冲区溢出 ,目标在处理由libc 产生的信号时停止 isStackBufferOverflow 1. 栈回溯中包含__fortify_fail,
__stack_chk_fail,
__GI___fortify_fail,
__stack_chk_fail
PossibleStackCorruption GDB在解开堆栈 时产生了一个错误,并且/或者堆栈中包含的返回地址没有映射到下级的进程地址空间中,并且/或者堆栈指针指向默认堆栈区域之外的位置。也就是堆栈可能遭到损坏。 isPossibleStackCorruption 1. 是良性的信号(SIGTERM等)
2. 是栈溢出
3. 是异常终止 ,且方向追踪中名字为空
4. 反向追踪没有映射区域
5. 没有栈指针或者栈指针的不指向栈
DestAv 目标在一个与指令目的操作数 相匹配的地址上因访问违规而崩溃。这很可能表明是写访问违规, isDestAvNotNearNull 1. 信号为 SIGSEGV, SIGBUS之一
2. 错误地址为dest地址
3. 错误地址 接近NULL
BadInstruction 目标试图执行一条畸形的或有特权 的指令 isMalformedInstructionSignal 1. 信号为"SIGILL, SIGSYS之一
HeapError 目标的回溯显示libc检测到一个堆错误 ,或者目标在停止时正在执行一个堆函数 isHeapError 1. 栈回溯中包含abort,__libc_message,等函数

B. probably exploitable 可能可利用

标示 描述 判断函数 判断逻辑 (x86为例)
StackOverflow 目标因访问违规 而崩溃,其中故障指令的助记符和堆栈指针似乎表明堆栈溢出 isStackOverflow 1. 信号为 SIGSEGV, SIGBUS之一
2. 由pushcall指令,且错误指令指向目标地址
3. 堆栈指针在默认的堆栈区域之外
SegFaultOnPcNearNull 类似SegFaultOnPc,但是地址接近NULL isSegFaultOnPcNearNull 1. 信号为 SIGSEGV, SIGBUS之一
2. 错误地址为dest地址
3. 错误地址接近NULL
BranchAvNearNull 类似BranchAv,但是地址接近NULL isBranchAvNearNull 1. 是分支地址(ja,jae等等或者call,callq)崩溃
2. 错误地址接近NULL
BlockMoveAv 目标在块移动 过程中崩溃,这可能表明攻击者可以控制缓冲区溢出。 isBlockMove 1. 是良性的信号(SIGTERM等)
2. 指令匹配 *mov
isDestAvNearNull 类似DestAv,但是地址接近NULL isDestAvNearNull 1. 信号为 SIGSEGV, SIGBUS之一
2. 错误地址为dest地址
3. 错误地址接近NULL

C. probably not exploitable 可能不可利用

标示 描述 判断函数 判断逻辑 (x86为例)
SourceAvNearNull 目标在一个与当前指令的源操作数相匹配的地址上,因访问违规而崩溃.这可能意味着应用程序在对数据结构进行简单的NULL解除引用时崩溃,对处理器的控制没有直接影响。 isSourceAvNearNull 1. 信号为 SIGSEGV, SIGBUS之一
2. 错误地址指向源地址
3. 地址接近NULL
FloatingPointException 目标在一个浮点异常 上崩溃了 isFloatingPointException 1. 信号为SIGFPE
BenignSignal 目标在没有显示错误或显示一般认为不可利用的错误的信号下被停止 isBenignSignal 1. 信号是良性的信号(SIGTERM等)

D. unkown 未知

标示 描述 判断函数 判断逻辑 (x86为例)
SourceAv 目标在一个与当前指令的源操作数相匹配的地址上,因访问违规而崩溃 isSourceAvNotNearNull 1. 信号为 SIGSEGV, SIGBUS之一
2. 错误地址指向源地址
3. 地址不接近NULL
AbortSignal 目标在SIGABRT中被停止 isAbortSignal 1. 信号是SIGABRT
AccessViolation 目标由于访问违规而崩溃,但没有足够的额外信息来确定可利用性。 isAccessViolationSignal 1. 信号为 SIGSEGV, SIGBUS之一
UncategorizedSignal 未分类的信号 isUncategorizedSignal 1. 没有分类的信号

4. 总结

exploitable是一个GDB中判断当前状态的分类可利用性 判断的一个工具。它的基本原理是根据信号量,ASAN信息,指令,栈回溯等信息去使用规则 来判断。

你可能感兴趣的:(gdb,深入Linux,二进制,gdb,安全漏洞)