在64位程序中,函数的前6个参数通过寄存器进行存储;但是很难找到正好符合我们需要的寄存器的gadgets,这个时候可以利用__libc_csu_init——这个函数是对libc进行初始化的函数,所以一般每个64位的程序中,都会存在。
基础知识
64位程序传参方式
64位的程序有别于32位的程序,64位的程序前六个参数会依次放置于rdi、rsi、rdx、rcx、r8、r9中,多余的参数才会和32位程序一样分别压入栈中,例如:
1 | fun1(a,b,c,d,e,f,g) |
Ret2__Libc_csu_init的利用原理
将实例程序用IDA打开,定位至__Libc_csu_init处:
1 | .text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34↑j |
通过0x400616开始,我们可以看到,寄存器rbx、rbp、r12、r13、r14可控(由于程序是像寄存器pop)。
再往上看
1 | .text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54↓j |
这段程序可以理解为四个步骤:赋值、调用、比较、跳转
1 | mov rdx, r13 -> 将r13的值赋值给rdx |
程序分析
基本功能分析
main函数
vulnerable_function函数
程序的关键函数就是上述两个,程序功能十分简单,也就是读取0x200个字符,典型的栈溢出,但是这个题目的一个难点就是没有输出,这就需要利用ret2__libc_csu_init进行getshell。
exp构造以及动态调试
通过这段代码,可以发现栈溢出的字符为0x80个
构造exp:
1 | from pwn import * |
得到动态调试:
在新弹出的terminal中输入c,直接跳转至断点处:
接下去输入命令n单步调试查看寄存器中值的变化,在所有的单步调试结束之后,获取到最终寄存器的值、以及return地址:
经过整理得到如下关系表:
1 | pop rbx 0 |
大家可能有人不知道怎么验证r12、r14是否write_got的地址,在命令中输入p write即可查看write_plt的地址:
在根据寄存器的值进行反查:
即可得到r12、r14中确实为write_got的值。
继续进行单步调试:
发现程序如同预计一样进入了地址为0x400600的地方,继续单步:
以上赋值阶段不再一一赘述,有意思的是方框中调用write的过程,在这里可能对write中的传参不是很理解,这里特意单独写出一个write函数以便说明:
1 | #include<stdio.h> |
放入gdb进行调试(具体的gdb使用方法以及技巧,会在之后的更新中说到),一直单步调试至调用write函数处:
通过动态调试的结果结合write.c源码,可以清楚地得到write压参方法。
回到正题,我们就可以知道我们进行的write函数构造为:
1 | write(1,write_got,8) |
也就是输出了write_got的地址,再根据偏移就可以获取到libc的base地址,具体的推算公式为:
1 | 泄露出的write地址 - libc.sym["write"] = libc_base |
有了libc的基址,我们就可以根据相同的方法得到system的地址,也就可以进行getshell。
在上一段payload的最后两位,分别为:
1 | "A" * (8 * 7) , main_add |
这个的意义在于栈平衡,结合ida进行观察:在确定了0x400614这一句不进行跳转后,根据程序的逻辑,程序会继续向下进行,而这并不是我们想要的,因为继续向下运行程序会导致我们布局的数据被破坏,所以我们希望直接进行main主函数的运行,所以我们要将下面的语句进行覆盖:
每一个对应的地址长度都为8字节,也就是需要八个英文字符进行覆盖,所以我采用了”A” * (8 * 7)
的方法进行覆盖,至于后面的main_add即为覆盖返回地址,将程序转为main主函数运行。
接下去的做法就和ret2libc一模一样了,就不再展开细讲,附上完整的exp:
1 | from pwn import * |
这里运用了两种方法去做ret2libc,第一种是常规的方法,第二种是one_gadget去做,这边one_gadget的做法需要栈平衡,所以并不是每一个one_gadget都可以使用,但是第一种方法是通用的。