我们在基础知识扩展的时候,说到了为了避免有些题目供我们构造的字节数过少,以至于无法给system函数传参的时候该怎么解决
今天所要讲到的内容,也是和栈溢出字节数不够有关
当可以供我们编写的字节数仅够覆盖到ret addr时,并且该程序内并没有后门函数可以供我们利用,我们又该如何实现系统调用呢?
我们以往的简单栈溢出是通过覆盖ret addr的办法控制程序执行流导向后门函数的位置
但是其本质上 ebp和esp并没有被我们所控制,他仍然是按照原先栈底的汇编代码所运行的
所以我们换个思路?不妨劫持esp和ebp,让他们前往bss段或者其他可以供我们自由写入的区
这样我们就可以自己构建一个后门函数,并且将程序执行流引导至其
那问题就来到了如何劫持esp和ebp 我们先得清楚一下栈帧这个概念
栈帧
栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构
简单理解就是每次函数的调用,都会生成自己的栈帧
栈帧就相当于函数的调用框架,包含了函数的参数,函数的局部变量,函数执行完后的返回地址
系统是如何定义一个栈帧的?ebp指向了栈帧的栈底,esp指向了函数的栈顶
也就是说,我们把esp和ebp劫持的目的,就是让系统错以为我们写入shellcode的bss段(包括但不限)是一个栈帧
从而执行他
栈迁移原理
归根到底,就是要如何劫持esp和ebp
回到我们最开始的栈溢出,我们要溢出的字节数=变量var距离esp的字节数+一个字长
这里的一个字长覆盖的是ebp
在我们没有对ebp覆盖的时候,其保存的是上层函数的栈底地址,而ret addr保存的是上层函数执行到了哪个地方,方便子函数结束后返回父函数最后执行的地方
在一个栈帧结束的时候,eip 即将执行 leave 与 ret 两条指令恢复现场(即返回父函数)
leave指令相当于 mov esp ebp和pop ebp
他将ebp和esp指向同一地址,这一步相当于腾出了栈帧空间
随后pop ebp 将此时esp指向的old ebp(因为我们上面说过了嘛,ebp保存的是上层函数的栈底地址)赋值给真正的ebp(此时的ebp是定义栈帧栈底的ebp)
是不是有点晕?首先你要分清楚ebp保存的内容和ebp寄存器这两个概念
在子函数调用开始之前,系统会将父函数栈底的地址弹出到新的栈帧,这个值就是ebp(就是我们之前栈溢出用垃圾数据覆盖的那个嘛)
然后记录下当前父函数运行到的地址,将其弹出为ret addr,等子函数结束以后,就会返回到这个地址
所以说,如果我们覆盖ebp的时候不用垃圾数据,而是放入我们要使ebp迁移到的地址,那么ebp就会被我们挟持走
但是此时还有个esp寄存器怎么办?栈帧的空间需要这二者才能定义
你还记不记得我们构造rop链的手法?我们自己再找一个leave的汇编代码地址然后覆盖ret addr不就好了?
此时mov esp ebp会起到什么效果?ebp已经指向了我们要迁移的地址,所以esp也被挟持到了那边
但是注意,还有一句pop ebp 虽然这句没有任何作用,因为此时新的栈帧的栈顶,其保存的已经是我们要挟持到的地方的地址
但是这一句是出栈指令,此时我们的esp,他指向的地址就会增加一个字长
如图所示,HijackAddr就是我们想要劫持esp ebp到的地址
那栈迁移运作的原理我们已经搞清楚了是吧,接下来想办法构造payload
payload = cyclic(offset)+pxx(addr)+pxx(leave_addr)
这一个没有问题吧
那只剩下最后一个问题了,我们迁移到的那个地址的栈内容要怎么编写
aaaa是我们最开始的那个地址存放的垃圾数据,即上文说到的HijackAddr,因为pop ebp的原因,esp会指向高一个字长的地方
dddd则是32位情况下的传参,中间要隔个垃圾数据,这没什么好说的
下一个binsh_addr 和binsh字符串是什么意思,当程序连binsh都没给我们的话,反正我们都能自己编写一段栈帧了,我们不是可以自己写入一段binsh,然后我们也知道其地址了,不是就能调用了
后面的old_ebp和ret_addr也没什么好说的,就是一段栈帧必须的要素