记录一道ret2csu的pwn题
0x00 前言
写这道题之前, 大家首先要了解, 想要获得一个shell, 除了system("/bin/sh") 以外, 还有一种更好的方法, 就是系统调用中的 execve("/bin/sh", NULL, NULL)获得shell。我们可以在 Linux系统调用号表中找到对应的系统调用号,进行调用, 其中32位程序系统调用号用 eax 储存, 第一 、 二 、 三参数分别在 ebx 、ecx 、edx中储存, 可以用 int 80汇编指令调用。64位程序系统调用号用 rax 储存, 第一 、 二 、 三参数分别在 rdi 、rsi 、rdx中储存, 可以用syscall 汇编指令调用。
这题我是不会做的🤡,至少在看别人的博客之前是这样。而且,在查阅了众多资料以及自己跟着gdb调试之后,才终于弄懂了这题的一种解法。所以,为了加深理解,在这里记录一下假装是自己做出来的自己复现的解题过程吧。😅
0x01 开始分析
首先checksec
|
|
64位,没开PIE,没有cannary,开了NX(说明不能直接写shellcode)。
IDA看一下,vuln()和gadgets()很显眼啊,看着这函数名当时心想这估计又是一道简简单单的基础rop训练,且看我10分钟拿下🤣那就分别去看一下这两个函数的内容吧。
首先是vuln()。发现其中调用了sys_read()和sys_write(),都是系统调用的形式。而且sys_read()向大小为0x10的buf写入最多0x400个数,这显然存在溢出。
然后是gadgets()。其中存在两个设置rax寄存器的gadget,查一下64位Linux系统调用表,0x0f是sigreturn,0x3b是execve。再结合vuln()中存在syscall语句,就可以确定最明显的思路了,那就是想办法调用execve("/bin/sh", NULL, NULL)来获取shell。
根据前言提到的64位Linux系统调用的传参方式,我们在调用syscall之前需要完成两点:
rax寄存器置为0x3b(59)rdi设为/bin/sh字符串的地址,rsi/rdx设为0
0x02 execve/ret2csu解法
首先要想一下怎么获取指向字符串/bin/sh的地址。IDA view->open subviews->strings查看,发现没有/bin/sh。所以只能调sys_read()自己写了。同时,注意到**vuln()函数的结尾是没有leave指令的**,所以vlun()被调用完之后其函数栈并没有被清空,于是我们可以写/bin/sh在栈上,而且覆盖的时候覆盖的rbp就直接是返回地址。
那么第二个问题来了,如何知道vuln()函数栈的地址呢,答案就是利用vuln()中的sys_write()调用。该调用输出0x30个字节的内容,在调试的过程中可以看到,buf在栈上的地址为0xdf30,而从源码可知它与rbp的距离为0x10个字节,所以sys_write()输出的第0x21~0x28个字节是可执行文件名的地址,输出内容与buf地址的偏移为0xe048-0xdf30=0x118=280。所以,我们可以通过泄露可执行文件名地址的方式来获取buf的地址。
需要注意的是,题目说明了该题的远程环境为Ubuntu18,而我一开始是用Ubuntu20调试的,所以得到的偏移为0x128,多了0x10个字节,我说怎么一直不对,后来再装了个ubuntu18的wsl,发现果然如此,估计是系统的地址对齐之类的原因。有趣的是,在这一过程中还学习了一些wsl的新操作,也算是需求导向性学习了,记录在了这里。
Ubuntu20上面看到的就是0xdf58-0xde30=0x128=296。
OK,地址知道了,execve()的3个参数的值我们都能够确定了,所以第一步就是泄露’buf’的地址。
|
|
上面的payload1发完之后,又进入了vuln()。接下来的就是通过第二个payload想办法构造ROP链来实现控制寄存器与函数跳转的过程,这又是这题的一个难点(因为我做这题之前不知道ret2csu😶)。最简单的思路那肯定是用ROPgadget找到能够pop三个寄存器、然后ret的gadget。但是ROPgadget只能找到pop rdi和pop rsi的gadget(为什么pop rsi; pop r15; ret这条指令从中间开始取就是pop rdi; ret?这是指令设计的原因吗?),还差一个,所以这种方法行不通。
因此,我们得用一种新的ret方式,ret2csu,就是利用_libc_csu_init()的pop和mov两个gadget来实现控制寄存器的操作,一般适用于64位的题目。ret2csu的参考资料网上都有,感觉这篇写的挺清晰的。
两段gadget为
|
|
|
|
总结下来就是:
r15d->edi(一般来说rdi寄存器高8位都是0,所以这里虽然只控制了低8位,但实际上相当于可以控制rdi的值)r14->rsir13->rdx- 还有,
rbx设为0,然后call [r12+rbx*8];就会以r12存储的地址为起点取指令,并且每次向后跳8位,因为call指令后会将rbx加1,然后对比rbp,如果不相等则再次循环
于是,理论上,我们就能够构造一段极其巧妙的payload2:
|
|
对应的执行流程为:
- 写完’/bin/sh’,
vuln()内执行retn,进而执行pop_rbx_rbp_r12_r13_r14_r15_ret。pop6次,rbx/rbp/r12/r13/r14/r15分别被设为0/0/binsh_addr+0x50/0/0/0/,rsp此时指向mov_rdx_r13_mov_rsi_r14_mov_edi_r15_call_r12,接在再retn,执行mov_rdx_r13_mov_rsi_r14_mov_edi_r15_call_r12,此时rsp指向mov_rax_59_ret,注意,这刚好是前面的binsh_addr_0x50😬 mov_rdx_r13_mov_rsi_r14_mov_edi_r15_call_r12指令执行,rsi/rdx被设置为0,第一次call [r12];。- 众所周知,
call指令的执行过程是先push再jump,然后jump的目的指令段最后一般都有个ret回来。所以,第一次执行call [r12];,就相当于执行mov_rax_59_ret,把rax设为了59,然后ret。经过一番push/ret之后,rsp又回到了mov_rax_59_ret的位置,但此时rbx已经被加了1,值变为了1。然后通过后面的cmp判断,jnz再次跳回mov_rdx_r13_mov_rsi_r14_mov_edi_r15_call_r12指令开头。 - 再次执行
mov_rdx_r13_mov_rsi_r14_mov_edi_r15_call_r12,不同的是,此时rbx为1,所以call指令变为了call [r12+8];,所以pop_rdi_ret被执行:call首先push,rsp指向call之后的下一条指令,然后jump;jump之后pop rdi,rdi变为了call的下一条指令地址,rsp指向mov_rax_59_ret;再接着ret,rsp指向pop_rdi_ret,rip指令寄存器的内容为mov_rax_59_ret,所以系统执行的下一条指令为mov_rax_59_ret。 - 执行
mov_rax_59_ret,rsp指向binsh_addr,rip指令寄存器的内容为pop_rdi_ret,所以系统执行的下一条指令为pop_rdi_ret。 pop rdi;将binsh_addr写入rdi,然后ret执行syscall。此时,rax为59,且三个寄存器rdi/rsi/rdx分别为binsh_addr/0/0,相当于执行execve("/bin/sh", NULL, NULL),拿到shell。
感觉这篇博客里的执行流程分析好像写错了一步…
最终完整exp如下。
|
|
执行截图
0x03 sigreturn/SROP解法
贴一下别人的exp,等我学习一下SROP再回过头来看。
|
|
0x04 总结
这题从不会,到看懂一种解法,以及查资料、gdb调试、尝试gdb+wsl2+pwntools联合调试、弄wsl ubuntu18、迁移占用c盘空间太多的wsl2-ubuntu20、写博客…,前前后后搞了差不多一天的时间🤣,一个字,疲惫=.=
不过总的来说,收获还是很多的,继续学吧