前言

在逆向中,使用Hook来解决问题非常的常见,在之前对angr的学习中,我并未关注到hook方法,现在有时间学习整理一遍,我倍感快乐,其实Hook也十分的简单,而且可以将复杂的问题简单化

初识

一个比较简单的例子defcamp_r100/r100

解题代码只有以下短短的几行:

import angr

project = angr.Project("angr-doc/examples/defcamp_r100/r100", auto_load_libs=False)

@project.hook(0x400844)
def print_flag(state):
    print("FLAG SHOULD BE:", state.posix.dumps(0))
    project.terminate_execution()

project.execute()

官方文档的介绍如下:

我们可以通过@proj.hook(proj.entry)的方式来Hook任意一个地址。

例子中使用了project.execute()方法,此方法并不常用,它往往和project.terminate_execution()结合起来使用,并且通常用在hook时

因此代码大概的执行流程如下:

  1. 初始化proj
  2. hook指定地址的函数
  3. 调用project.execute()
  4. 当遇到project.terminate_execution()符号执行结束

此时angr会执行到0x400844并打印出flag的结果。

hook符号表

这里以tumctf2016_zwiebel作为例子进行说明。首先看官方文档的说明。

hook_symbol函数可以根据所给出的符号名,在二进制文件中找寻对应的地址,并且hook该地址。

IDA载入题目

这是一个smc的题目,对于angr来说为了能在符号执行时进行自解密,需要添加support_selfmodifying_code=True参数

很明显,我们无法使用sm.explore(find=xxx,avoid=xxx)的方式来使用angr,同时注意到程序中出现了ptrace想必一定有反调试,让我们通过hook的方法来绕过反调试。

p.hook_symbol('ptrace', angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained'](return_value=0))

因为angr实现了大量的符号化函数,以此来替代程序中对库函数的外部调用,其中angr.SIM_PROCEDURESangr对符号化函数的字典调用,我们可以采用angr.SIM_PROCEDURES['模块名']['库函数名']()进行hook

而后便可以通过simulation_manager进行执行了。

state = p.factory.full_init_state(cadd_options=angr.options.unicorn)
    sm = p.factory.simulation_manager(state)

这里只能采用类似step的方法进行解决,效率很低,例子中提供的代码是这样的。

while sm.active:
        # in order to save memory, we only keep the recent 20 deadended or
        # errored states
        #print(len(sm.active))
        sm.run(n=20)
        if 'deadended' in sm.stashes and sm.deadended:
            sm.stashes['deadended'] = sm.deadended[-20:]
        if sm.errored:
            sm.errored = sm.errored[-20:]

    assert sm.deadended
    flag = sm.deadended[-1].posix.dumps(0).split(b"\n")[0]
    import ipdb; ipdb.set_trace()
    return flag

我觉得有点多此一举了,他这段代码的目的就是执行完sm.run()此时正确的输入应该保存在最后一个deadended节点的posix.dumps(0)当中,最后跑了两个小时,我也是醉了。不过至少知道了angr是如何hook,并绕过反调试的。

典型例题defcon2016quals_baby-re

这道题作为例子可能会更好一点。

这题其实不用hook也能顺利的解出,只是我们需要对结果进行处理一下,才能得到我们想要的flag。

IDA载入

其实之前我们遇到过类似的题目,不过那时我们采取的方法是:跳过输入部分,直接对内存进行存储,从而进行输入,这里当然也能这么做,只需对[rbp+var_60]内存进行操作即可

ps:本来只想简单尝试一下,没想到花了几分钟随便写的代码居然跑出结果了,因此这里顺便贴一下代码。

import angr
import claripy

# 最简单的方法,不过需要对结果进行变化
def main():
    proj = angr.Project('./baby-re', auto_load_libs=False)
    state = proj.factory.entry_state(add_options={angr.options.LAZY_SOLVES})
    sm = proj.factory.simulation_manager(state)
    sm.explore(find=0x4028E9, avoid=0x402941)

# 跳过程序自身的输入,通过内存控制输入
def main2():
    proj = angr.Project('./baby-re', auto_load_libs=False)
    flag_chars = [claripy.BVS('flag_%d' % i, 32) for i in range(13)]

    state = proj.factory.blank_state(addr=0x4028E0,add_options={angr.options.LAZY_SOLVES})
    for i in range:
        state.memory.store(state.regs.rbp-0x60+i*4,flag_chars[i])
    state.regs.rdi = state.regs.rbp-0x60
    sm = proj.factory.simulation_manager(state)
    sm.explore(find=0x4028E9, avoid=0x402941)
if __name__ == '__main__':
    main2()

当然说了这么多,这里最主要还是想说一下如何使用Hook技术,来控制输入,从而方便我们的输出。

我们可以通过这样的方式进行Hook

proj.hook_symbol('__isoc99_scanf', my_scanf(), replace=True)

我们用自己的my_scanf()来代替__isoc99_scanf,我们在保持scanf功能不变的情况下,将我们的符号变量存储进去。

class my_scanf(angr.SimProcedure):
        def run(self, fmt, ptr): # pylint: disable=arguments-differ,unused-argument
            self.state.mem[ptr].dword = flag_chars[self.state.globals['scanf_count']]
            self.state.globals['scanf_count'] += 1

这样程序每次调用scanf时,其实就是在执行my_scanf就会将flag_chars[i]存储到self.state.mem[ptr]当中,这其中ptr参数,其实就是本身scanf函数传递进来的rdi也就是[rbp+var_60]+i*4,为了控制下标,我们设置了一个全局符号变量scanf_count,相信聪明的你一定不难理解。

如此一来,只要angr执行到我们想要到达的分支,那么我们就可以通过solver.eval()的方式将其打印出来

代码如下:

import angr
import claripy

def main():
    proj = angr.Project('./baby-re', auto_load_libs=False)

    # let's provide the exact variables received through the scanf so we don't have to worry about parsing stdin into a bunch of ints.
    flag_chars = [claripy.BVS('flag_%d' % i, 32) for i in range(13)]
    class my_scanf(angr.SimProcedure):
        def run(self, fmt, ptr): # pylint: disable=arguments-differ,unused-argument
            self.state.mem[ptr].dword = flag_chars[self.state.globals['scanf_count']]
            self.state.globals['scanf_count'] += 1

    proj.hook_symbol('__isoc99_scanf', my_scanf(), replace=True)

    sm = proj.factory.simulation_manager()
    sm.one_active.options.add(angr.options.LAZY_SOLVES)
    sm.one_active.globals['scanf_count'] = 0

    # search for just before the printf("%c%c...")
    # If we get to 0x402941, "Wrong" is going to be printed out, so definitely avoid that.
    sm.explore(find=0x4028E9, avoid=0x402941)

    # evaluate each of the flag chars against the constraints on the found state to construct the flag
    flag = ''.join(chr(sm.one_found.solver.eval(c)) for c in flag_chars)
    return flag

def test():
    assert main() == 'Math is hard!'

if __name__ == '__main__':
    print(main())

总结

我感觉Hook的代码还是比较难写的,不过如果学会了,确实可以省下我们写脚本分析的时间,又是一个提高效率的方法。

源链接

Hacking more

...