核心Gadgets:leave; ret; 切栈

配合Gadgets:pop ebx; ret; 平衡堆栈

指令详解

leave:

mov esp,ebp
pop ebp

ret:

pop eip

详解一个ropchain的执行流

图中上方高地址,pop时从esp下往上移动

  1. 接收第一个payload后存在溢出的主调函数体运行到leave;ret;位置,leave时esp赋值为当前栈基址(图中0x100),ebp赋值为fakestack_buf1的基址。ret时相当于pop eip,eip赋值为read_plt的值,即调用到libc内的read函数,read函数体内部默认参数在调用前已经压栈,执行过程中通过偏移直接取出0x00,buf1,0x100,作为参数,即read(0,buf1,0x100); 此时接收的payload写入了我们伪造栈帧的buf1位置。
  2. 接收第二个payload后read函数退出时esp在参数上方位置,ret到gadgets执行一个leave; ret; 此时,leave时先将esp的值赋值为当前ebp值,即buf1,于是esp的值到达buf1基址的位置,然后pop ebp即从目前esp的位置取出了buf2的基址,ret到puts_plt执行puts,puts内部pop出 puts_got,即执行puts(puts_got),leak出了libc基址。puts结束,ret到pop_ebx_ret的gadgets上,pop掉puts_got这个参数,esp指向read_plt,然后一个ret将esp指向的read_plt pop取出到eip中,即通过这个堆栈平衡的gadgets,在不切栈下将eip过渡到栈帧上方。执行read(0,buf2,0x100);此时接收的payload写入了我们伪造栈帧的buf2位置。
  3. 接收第三个payload后返回地址还是一个leave; ret; 的gadgets,于是再次切栈,leave时esp的值变成当前ebp的buf2基址,ebp从esp指向的buf中pop出一个0xdeadbeef(任意地址),ret时即从此时esp指向的地址中取出了system_addr给eip。eip执行system('bin/sh'),同样返回地址任意
  4. 通过伪造栈帧进行栈迁移,ret2libc,getshell完成。

技术总结

伪造栈帧的技巧通过最初的一次ebp和ret覆盖,迁移栈空间,开辟新世界,极大拓展了ropchain的活动空间,其间ebp、esp、eip在pop leave ret的gadgets穿针引线下数据交错流动。主要是通过ebp在leave时的赋值操作了esp的位置,而在ret时操作了eip的值,实现了伪造栈帧中控制执行流的目的。

而到了x64位系统下,还需考虑函数参数时压栈的gadgets,限制条件或许会更多,但思路基本一致。

最后一步也不一定是ret2libc,可以是任意合适的getshell方式,比如dep off而pie on的情况下可通过迁移到已知地址进行ret2shellcode。或是libc一把梭的onegadgets。

适用性条件

  • 动态编译 栈溢出 且 栈空间不足
  • 存在puts等输出函数

参考

图片来源:https://xz.aliyun.com/t/4073