记录一道ret2csu的pwn题
0x00 前言
写这道题之前, 大家首先要了解, 想要获得一个shell, 除了system("/bin/sh") 以外, 还有一种更好的方法, 就是系统调用中的 execve("/bin/sh", NULL, NULL)获得shell。我们可以在 Linxu系统调用号表中找到对应的系统调用号,进行调用, 其中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
->rsi
r13
->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
。pop
6次,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、写博客…,前前后后搞了差不多一天的时间🤣,一个字,疲惫=.=
不过总的来说,收获还是很多的,继续学吧