ret2csu:其实就是一种攻击手法,当我们没有完整的gadget的时候,我们就可以使用csu这个手法来进行攻击,那么csu是什么呢?
这个其实就是在程序中一般都会有一段万能的控制参数的gadget,里面可以控制rbx,rbp,r12,r13,r14,r15以及rdx,rsi,edi的值,并且还可以call我们指定的地址。然后劫持程序执行流的时候,劫持到这个__libc_csu_init函数去执行(这个函数是用来初始化libc的,因此只要是动态链接的程序就都会有这个函数(可能会有特殊的情况),从而达到控制参数的目的。这样就可以进行我们想进行的攻击!
下面我们就用一道例题来进行csu的分析吧。
例题:VNCTF2022公开赛clear_got:
看上述就是__libc_csu_init,我们分为俩段,在上面那一段我们称为gadget1,下面一段称为gadget2,假设我们已经可以控制程序的返回地址,我们就可以劫持程序执行流到gadget1段,也就是0x4007EA这一段,因为rsp+8这一段指令来说对我们的攻击没有用,所以我们直接从pop rbx开始,那么现在就会把栈中的前6个数据分别弹给rbx,rbp,r12,r13,r14,r15这六个寄存器。
我们通常会把rbx的值设置成0,而rbp设置成1.这样的目的是因为我们执行完gadget1之后就会使用返回地址返回到gadget2这样在执行call qword ptr [r12+rbx8]这个指令的时候,我们仅仅把r12的值给设置成指向我们想call地址的地址即可,从而不用管rbx。
又因为这三个指令add rbx,;cmp rbx, rbp;jnz short loc_4007D0,jnz是不相等时跳转,我们通常并不想跳转到0x4007D0这个地方,因为此刻执行这三个指令的时候,我们就是从0x4008D0这个地址过来的。因此rbx加一之后,我们要让它和rbp相等,因此rbp就要提前被设置成1.
对于刚刚说的call的时候,r12存放的地址就是我们要跳转到那个地址,这里有个很重要的小技巧,如果你不想使用这个call,或者说你想call一个函数,但是你拿不到它的got地址,因此没法使用这个call,那就去call一个空函数(_term_proc函数)(并且要注意的是,r12的地址填写的并不是_term_proc的地址,而是指向这个函数的地址)。然后r13,r14,r15这三个值分别对应了rdx,rsi,edi。这里要注意的是,r15最后传给的是edi,最后rdi的高四字节都是00,而低四字节才是r15里的内容。(也就是说如果想用ret2csu去把rdi里存放成一个地址是不可行的)
接着到了gadget1的结尾ret这里,然后我们紧接着写入gadget2的地址0x4007D0,此时开始执行这部分代码,这没什么好说的了,就是把r13,r14,r15的值放入rdx,rsi,edi三个寄存器里面。
然后由于我们前面的rbx是0,加一之后等于了rbp,因此jnz不跳转。那就继续向下执行,如果我们上面call了一个空函数的话,那我们就利用下面的ret。由于继续向下执行,因此又来到了gadget1这里。如果不需要再一次控制参数的话,那我们此时把栈中的数据填充56(78你懂得,就是有6个寄存器加上rbp)个垃圾数据即可。
如果我们还需要继续控制参数的话,那就此时不填充垃圾数据,继续去控制参数,总之不管干啥呢,这里都要凑齐56字节的数据,以便我们执行最后的ret,最后ret去执行我们想要执行的函数即可。
这就是对于csu的分析,那么这道题我们怎么进行攻击呢?
做这道题,必须先掌握下面这三个点。
1、首先是call指令后面的这个地址(如果是函数名就不说了),就比如现在ret2csu中,准备执行这个gadget2的call时候我们让rbx为0,此时call r12,那怎么才能call成功呢,原本看到师傅们说是要装got地址,后来发现装一个地址(这个地址是被另一个地址所指向的),然后把r12填写成另一个地址,也可以call成功,再回想一下为什么要装got地址,而不是plt地址,原因也是出现在了got地址仅仅会跳转一次,也就是说填一个got地址,也是会从这个地址去跳到got地址所指向的地址(也就是真实地址(因为延迟绑定的原因,如果不清楚的话,这里请自行百度一下延迟绑定机制)),因此结论就出来了,要想去call去跳转到一个地址A,那就必须用一个指向地址A的地址B放到call后面。
2、如果我们仅仅是想利用ret2csu去控制参数,而并不想去用call执行,或者说是你想用call执行跳转,但是你找不到去指向你想跳转的那个地址,因此我们用最后的ret跳转(你想跳转到哪里,就填哪的地址即可)。那怎么把call的那一步忽略呢?我们可以call一个空函数(不需要参数,执行之后也不会对程序本身造成任何影响的函数),这个函数就是_term_proc(注意,这里call的是指向_term_proc的地址,而非term_proc的地址
3、怎么去修改rax的值?
这里提到了一种很巧妙的方法。我们先来看一下read函数和write函数的返回值。
可以看看这个师傅写的文章,关于ret2csu写的很好https://www.cnblogs.com/ZIKH26/articles/15910485.html
我们可以看出来read函数和write函数最后的返回值都是实际读到和写入的字节数(如果执行成功的话),而返回值最后就会放到rax里面。也就是说可以利用read和write去控制我们想要的rax。
这样就可以开始做这道题了,
它的主函数非常简单,就是一个简单的栈溢出,我们就可以控制其返回地址,但是没有发现后门函数和参数,但是在左边给我们的end1和end2明显不是系统的函数,那我们去看看汇编,发现了俩个系统调用,
但如果真的这么简单就好了,
这个代码的意思就是初始化0x601008往下的0x38的地址的东西全部初始化,那我们就看看0x601008这里有什么东西,
发现了got表,那么这样的话我们got表里的所以地址都不能使用了,但是我们能用的有什么?只剩下了系统调用,可是想用系统调用执行execve(‘/bin/sh’,0,0),我们需要做到三件事,第一是控制rax,第二是控制rdi,rsi,rdx这三个寄存器,第三是将/bin/sh写入到bss段。
控制rax?,有没有想到最开始提到的那个方法,利用read或者write去修改rax。由于我们还要写入/bin/sh,因此我们这里采用系统调用read,可是read的系统调用号是0,而程序中出现的两个系统调用没有read,怎么办?其实不用管的,因为main函数的返回值是0,在main函数的ret之前,就把rax的值给设成0了,因此我们溢出之后,始终rax都是0(在执行系统调用之前)。
既然现在可以系统调用read,那只需要控制参数,将/bin/sh写入bss段即可,怎么控制参数?用Ropgadget搜索之后发现,没有能控制rsi和rdx的寄存器,因此只能采用ret2csu的方法。
这样分析,因为我们就是由于无法调用read、puts等函数,那么只能尝试使用ret2syscall进行攻击,目标是执行execv("/bin/sh",0,0),其中execv的系统调用号是0x3b。通过ROPgadget工具发现没有类似pop rax;ret和pop rdx;ret的gadget,导致我无法控制rax寄存器和rdx寄存器。通过观察发现main函数在结束的时候rdx=0x38,rax=0(read函数的调用号),而read函数的返回值就是rax且其返回值由读取的字节数决定。因此可以尝试执行read(0,bss,0x3b)并输入0x3b字节的数据使rax=0x3b。这样就可以构造我们的exp
但是其中还有一小点我也没有怎么弄懂。还得边刷题边去总结!
总之就是ret2csu就是相当于一个工具来进行攻击的手法!
** srop**
关于srop的介绍可以看看这个博主的介绍,https://liuliuliuzy.github.io/2021-11-01-srop学习/接下来我们看一道例题来进行分析
例题:ciscn_2019_es_7
这是它的溢出的地方,很大有0x400-0x10
它给我们提供了mov rax 0x3b; ret和syscall以及pop rdi/pop rsi的gadget,但是就是没有控制rdx的gadget。所以无法直接栈溢出ret2syscall。
再考虑到这么长的溢出长度,以及mov rax 0x0f; ret的gadget(0xf号系统调用就是sigreturn),所以应该想到SROP。
但是如果想要SROP执行execve("/bin/sh", 0, 0)的话,还需要知道字符串"/bin/sh"的地址。在这道题中,可以利用sys_write输出的栈地址来获取。
第一就是泄露出我们的栈的地址
然后就是构造frame
这就是构造的frame,当触发sigreturn系统调用后,rax=59,rdi=binsh_addr,rsi=0,rdx=0,rip=0x400501。
这样就知道了吧,sigreturn系统调用的作用就是恢复之前用户态寄存器的值。然后就是最后的payload:
传入payload之后,程序会执行0x4004DA,将rax赋值为15,然后执行0x400501即syscall,程序就会去栈中找到各个寄存器的值并pop到寄存器中,然后执行我们伪造的rip即syscall,达到getshell的目的。这样就是进行了我们的srop攻击,但是并不是很熟练,如果没有wp的话根本进行不了payload的编写!所以还需要精进自己的实力!