栈迁移专项学习
写在前面的话
对栈迁移还是不熟悉,还得练,所以有了这篇文章,是复习,也是重新学习,由于有点懒,没太详细些,我认为重要的还是本篇文章拓宽了一下我对程序执行流程的思路,另外让我对漏洞利用的手法有了新的态度吧。另外,本篇文章主要还是学习国资师傅的文章的时候做的笔记。引用较多,且实例较少,我就调了一个程序,感觉差不多就没多写了。
栈迁移(stack pivote)又叫**伪造栈帧(fake frame
)**,即利用leave,ret
指令将bp,sp
移动到自己想要的地方。最终目的还是控制ip
从而控制程序流
普通栈迁移
通过leave ret
逐步将栈迁移到目标位置,使用场景
- 主函数不能返回(例如 :
HITCON-Training LAB6
)ret2dlresolve
需要栈帧一般都非常长,这时候选择栈迁移能很好的省去一些不必要的麻烦。- 只能溢出1个或者2个字长,这种题目就等于把:“请栈迁移”写在脸上。这种题目的特点是两次输入,也分为两种情况。
- 迁移到栈上,第1次输入是泄露地址,第2次是布置栈帧 + 迁移。
- 迁移到非栈上,1次输入是在非栈上布置栈帧,另1次迁移至非栈上,两次顺序可以互换。
一阶栈迁移
对于溢出字长过短的情况,也可以有两种模式
- 可以溢出到返回地址自行布置
leave ret
- 只能溢出覆盖 rbp,必须利用原本函数中有的两次 leave ret 来完成迁移。
需要注意的是,第二种情况可能会出现第一次leave ret
后执行其他函数,导致栈上的数据被污染的情况。针对此种情况最好的解决办法是迁移到未被污染的地方,或者执行onegadget
二阶栈迁移
除了溢出字长过短之外,有可能栈帧长度也极短
- 栈上的回头反打
- 非栈上的二阶栈迁移
栈上的回头反打
在栈上栈帧长度不够时,把能拆的步骤拆开,不断回头多次栈迁移即可
(稍后插入练习)
非栈上的二阶栈迁移
首先插入一段引用,说明一下库函数会抬高栈帧,需要抬高的栈帧可写:
程序
bss
段毕竟长度有限,绝大多数库函数都需要一定的栈帧,尤其是system
函数需要0x200
,并且bss
段前面可能还有一些程序需要的数据,所以迁移的位置需要一定的考量
当非栈上的可控区域长度不足时,可以考虑迁移到非栈上之后二次栈迁移。通过ret2csu,仅需要0x80字节即可为所欲为
三阶栈迁移
难道0x80的长度就是极限了么?不能进一步缩小栈帧长度吗?答案是,可以!
灵魂呼叫
1. 你使用过call吗
在汇编代码中,调用其他函数一般有 2 种方式,一种是 call
,一种是 jmp(jcc)
。其中,大多是用 call
的形式,plt
表中使用的为 jmp
形式。
2. call read 移形换影
call指令会将ip
压栈,然后ret
时将ip
弹出。如果我们call的是read函数,而此时数据又能覆盖到存储ip
的位置,那么,如果我们将ip修改,就能返回到我们想要返回的地方
3. 栈帧拼接
通过call read可以将两个或多个栈帧拼接到一起,向栈上写入数据与在bss段写入数据可以有很多组合方式。
左右横跳
通过控制栈帧,最终实现任意地址写的功能
移形换影
利用call_read
时的ret
,在read结束时重新劫持程序流,在使用的时候要注意sp指针的位置。通常来说控制sp位于可控地址的最低处最好,可以防止栈帧污染数据。
爆栈之术
你该知道的,总有些时候没办法泄露关键信息的,这时候只能尝试一下覆盖栈的最后一位,试试运气啦
重启大法
略
开始抽象
手法归纳
简单归纳一下学到的手法及作用
- call_read 移形换影:结束时会有一个call的ret保存在栈上,若可向该地址写入ret,就可以在read结束后立即控制栈帧
- call_read 栈上写:通常来说调用read函数的汇编寻址都是基于bp指针偏移的,只要控制bp指针的位置就可以控制写入的位置
- 栈帧拼接:通过适当布置可以使读入内容不断向后或向前推进,可通过循环分多次读入
- 左右横跳:通过“中转地址”实现任意地址写
- sp 控位:控制sp指向读入开始的地方然后写入ROP链
手法总结
总结一下上述手法
移形换影
函数leave ret后,若再次利用call read流程,就可以布栈
此时有两种情况
若无puts等函数污染,则可以直接利用残留的寄存器值在原位置再写一次,此时,可以直接覆写read的返回地址,继续挟持程序流
观察汇编代码可以看到,read的地址多半是通过bp寄存器偏移得到的,所以控制了bp寄存器的值就可以控制读入位置。
栈帧拼接
通过跳转指令(jmp、ret),将数块ROP片段拼接到一起,达到目的
左右横跳
中转地址做“中转站”实现任意地址写
sp控位
控制sp的位置,使call_read时返回地址刚好是写入开始的地方,本次写入就可以作为ROP链。
另外,举个例子,如果有函数调用对栈上数据污染的情况,可以利用移形换影配合leave_ret避免污染,仅需要三个字长的读入,且栈迁移后还可以通过寄存器残留在原位置重新布置两字长的payload内容。
思路探析
其实回头再来看的话,似乎这个发现的过程就是对汇编指令的加深理解,是对程序执行流程的更进一步。但是我们应该看到由此可以窥见的更多手法的雏形。随着我们对程序执行过程的理解不断加深,肯定会出现各种各样的利用方式,有些的泛用性比较广泛,可以达到各种各样的效果。当然我们可以在再次见到的时候一点一点构造利用过程,最终达到目的。但是,这样必然会浪费大量的时间。而且散碎的知识也不利于我们的思考过程。若可以将大多数手法泛用化,将利用方式总结起来,让我们不必更多地思考实现细节,而是从产生条件、达到效果这两个方面来思考问题。这样我们将可以做到更快更好。
结论
对《从0到1》上的比喻有了更深的感悟,内功高手也要修炼武功招式,否则就是空有境界,始终称不上真正的高手的。
练习
call_read
整了道题,练练手,就开个nx保护得了。
#include<stdio.h> |
exp:
from pwn import * |
移形换影,左右横跳,sp控位都用到了,但是没栈帧拼接。就一次输入嘛,怎么说呢?其实感觉差不多,自己意会吧