来源:香依香偎@闻道解惑
蒸米在《一步一步学ROP之linux_x64篇》中提到,在栈溢出的场景下,只要 x64 程序中调用了 libc.so,就会自带一个很好用的通用Gadget:__libc_csu_init()。
如图,先从 0x40061A 开始执行,将 rbx/rbp/r12/r13/r14/r15 这六个寄存器全部布置好,再 ret 到 0x400600 ,继续布置 rdx/rsi/rdi,最后通过 call qword ptr[r12+rbx*8] 执行目标函数。
这个通用 Gadget 好用的地方在于,不仅可以通过函数地址的指针(通常会用记录库函数真实地址的 got 表项)来控制目标函数,还可以控制目标函数的最多三个入参(rdi/rsi/rdx)的值。此外,只要设置 rbp=rbx+1而且栈空间足够,这个 Gadget 可以一直循环调用下去。
计算一下一次调用需要的空间。
可以看出,这个 Gadget 需要布置六个寄存器(rbx/rbp/r12/r13/r14/r15)加一个 ret 返回地址,x64 下至少需要 56 个字节的栈空间。如果再算上将 rip 指令跳转进来(0x40061A)的一个 ret 地址,那就是 64 字节的栈空间。
栈的布置如下:
其实,这个通用 Gadget 里,还隐藏了两个更简单的 Gadget。
将地址 0x400622 上 pop r15,ret 的三字节指令(0x41 0x5F 0xC3)拆散看,会发现后两个字节组成了一组新的指令 pop rdi,ret。
这已经足够完成单入参的函数调用了。
通常栈溢出之后,需要进行如下两步:
1、通过类似 puts(puts) 的方式,泄漏libc库函数的地址,从而通过偏移计算出 system 函数和 "/bin/sh" 字符串的地址
2、执行 sytem("bin/sh") 获得系统 shell
发现没有?大多数情况我们只需要一个入参的函数调用, __libc_csu_init() 函数最后的这个 pop rdi,ret 可以完美实现上述两个步骤。
空间上,只需要 24 个字节(一个 QWORD 存放 ret 进来的地址,两个 QWORD 作为入参和被调用函数地址)的溢出空间就足够啦。
栈的空间布置如下:
那,如果需要调用两个入参的函数呢,这个 Gadget 也行么?是的。
将地址 0x400620 上 pop r14 的两字节指令(0x41 0x5E)拆散,会发现后一个字节是单字节指令 pop rsi,可以用来控制第二个入参。
和前述的地址 0x400623 上的指令 pop rdi,ret组合起来,就可以完成两个入参的函数调用。
只需要将栈布置如下就可以啦。
1、只要Linux x64 的程序中调用了 libc.so,程序中就会自带一个很好用的通用Gadget:__libc_csu_init()。
2、__libc_csu_init() 的 0x400600 到 0x400624 其中包含了 pop rdi、pop rsi、pop rdx、ret 等指令,通过巧妙的组合可以实现调用任意单参数、双参数、三参数的函数,从而顺利泄漏libc函数地址并且获取系统 shell。
3、__libc_csu_init() 不只是一个通用 Gadget,完全就是“万能 Gadget”!
[1] 蒸米《一步一步学ROP之linux_x86篇》:https://zhuanlan.zhihu.com/p/23487280
[2] 蒸米《一步一步学ROP之linux_x64篇》:https://zhuanlan.zhihu.com/p/23537552