钩子编程(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。

简单来说就是用我们自己设计的函数去取代被 hook 的函数

一个简单的例子:

>>> @project.hook(0x1234, length=5)
... def set_rax(state):
...     state.regs.rax = 1

其中第一个参数即需要 Hook 的调用函数的地址,第二个参数 length 即指定执行引擎在完成挂钩后应跳过多少字节。具体多少字节由 Hook 处地址的指令长度确定,例如本题:

我们需要 Hook 地址的机器指令长度为 5 个字节,故最后的 hook 函数:

@project.hook(0x80486B3, length=5)

老样子先放最后 EXP,再逐一分析:

import angr
import sys
import claripy
def Go():
    path_to_binary = "./09_angr_hooks" 
    project = angr.Project(path_to_binary, auto_load_libs=False)
    initial_state = project.factory.entry_state()
 
    check_equals_called_address = 0x80486B3
    instruction_to_skip_length = 5
 
    @project.hook(check_equals_called_address, length=instruction_to_skip_length)
    def skip_check_equals_(state):
        user_input_buffer_address = 0x804A054 
        user_input_buffer_length = 16
 
        user_input_string = state.memory.load(
            user_input_buffer_address,
            user_input_buffer_length
        )
 
        check_against_string = 'XKSPZSJKJYQCQXZV'
 
        register_size_bit = 32
        state.regs.eax = claripy.If(
            user_input_string == check_against_string, 
            claripy.BVV(1, register_size_bit), 
            claripy.BVV(0, register_size_bit)
        )
 
    simulation = project.factory.simgr(initial_state)
 
    def is_successful(state):
        stdout_output = state.posix.dumps(1)
        if b'Good Job.\n' in stdout_output:
            return True
        else: 
            return False
 
    def should_abort(state):
        stdout_output = state.posix.dumps(1)
        if b'Try again.\n' in  stdout_output:
            return True
        else: 
            return False
 
    simulation.explore(find=is_successful, avoid=should_abort)
 
    if simulation.found:
        for i in simulation.found:
            solution_state = i
            solution = solution_state.posix.dumps(0)
            print("[+] Success! Solution is: {0}".format(solution.decode('utf-8')))
            #print(solution0)
    else:
        raise Exception('Could not find the solution')
    
if __name__ == "__main__":
    Go()

运行一下查看结果:

下面来逐步分析:

由于 Angr 可以处理对 scanf 的初始调用,因此我们可以从头开始

path_to_binary = "./09_angr_hooks" 
project = angr.Project(path_to_binary, auto_load_libs=False)
initial_state = project.factory.entry_state()

如之前分析的而言,首先找到需要 Hook 的函数地址 0x080486B3,然后设定指令长度

check_equals_called_address = 0x80486B3
instruction_to_skip_length = 5

然后我们需要在在 @project.hook 语句之后书写我们的模拟函数。然后如上题一致,我们利用使用 state.memory.load(addr, size) 接口读出 buffer 处的内存数据,与答案进行比较

@project.hook(check_equals_called_address, length=instruction_to_skip_length)
    def skip_check_equals_(state):
        user_input_buffer_address = 0x804A054 
        user_input_buffer_length = 16
 
        user_input_string = state.memory.load(
            user_input_buffer_address,
            user_input_buffer_length
        )
 
        check_against_string = 'XKSPZSJKJYQCQXZV'

然后这里的关键是,我们模拟一个函数就是把它视作一个黑盒,能成功模拟输入相对应的输出即可,所以我们需要处理 check 函数的返回值

不难发现这个函数是利用 EAX 寄存器作为返回值,然后成功则返回 1,不成功则返回 0,还需要注意在构建符号位向量的时候 EAX 寄存器是 32 位寄存器

register_size_bit = 32
        state.regs.eax = claripy.If(
            user_input_string == check_against_string, 
            claripy.BVV(1, register_size_bit), 
            claripy.BVV(0, register_size_bit)
        )

接下来同之前差不多,不再赘述