从0学习pwn【第五章】剖析ret2text64位,从函数调用到gdb调试(解决玄学栈对齐)
文章目录
- **前言**
- 1.从汇编角度理解
- 2.什么是栈对齐
- 3. 为什么需要栈对齐
- 问题:为什么call指令执行后是8呢?
- 4.为什么正常call不会错,ROP就会出错
- 5.例题测试以及动态调试
- 问题一:那么进入 backdoor 函数入口时,理想状态是什么呢?
- 问题二:他并不是我们的理想状态,那么说明什么问题了?
- 问题三:为什么没有栈对齐呢?
- 问题四:怎么找到ret的地址呢?
前言
今日的知识更偏向于理论部分,在我刚开始学习64位的时候,我也是蒙的,很经典的问题就是payload是否加了ret的返回地址,有的时候不加ret地址就可以成功,而有的时候加了ret的地址才会成功,这就是传说中的玄学栈对齐,今天主要是理解栈对齐问题
1.从汇编角度理解
这几个寄存器已经是我们都熟悉的了,再重新复习一遍
rip = CPU 当前要执行的指令地址 rsp = 栈顶的地址 [rsp] = rsp 这个地址里面存的 8 字节内容复习一下栈是如何移动的
64 位程序里,一个地址通常是 8 字节。栈是向低地址增长的。
push rax 等价于:
rsp = rsp - 8 [rsp] = raxpop rax 等价于:
rax = [rsp] rsp = rsp + 8所以所谓“消耗 8 字节”,不是内存被删除,而是:
rsp 移动了 8 字节分析完了栈的移动,我们再重新分析一下 call和ret的底层
call backdoor 等价于:
rsp = rsp - 8 [rsp] = call 后面的返回地址 rip = backdoor所以 call 会把返回地址压到栈上
ret 等价于:
rip = [rsp] rsp = rsp + 8所以 ret会从栈顶取出一个地址放进 rip,然后 rsp 往后挪 8 字节。
这就是ROP的本质:你控制栈上的内容,让每个 ret 都从你的栈里拿下一个地址。
2.什么是栈对齐
栈对齐说的是:rsp 这个地址是不是满足某种倍数要求
在64位Linux系统常见要求是 16 字节对齐
也就是说一个地址如果能被 16 整除,就是 16 字节对齐
那么如何在pwndbg里面看呢?
指令:
p/x ((unsigned long)$rsp) & 0xf如果结果是0x8说明 rsp 比 16 字节对齐差 8,如果结果是0x0说明 rsp 是 16 字节对齐
需要注意的是,函数入口处看到 rsp & 0xf = 0x8 并不矛盾,因为 call 指令会压入 8 字节返回地址。ABI 要求的是 call 前 rsp 按 16 字节对齐,因此函数入口通常表现为 rsp & 0xf = 0x8。
3. 为什么需要栈对齐
因为 64 位 Linux 的调用约定,也就是 System V AMD64 ABI,规定了函数调用时的栈状态:
call 指令执行前:rsp % 16 == 0 call 指令执行后:rsp % 16 == 8问题:为什么call指令执行后是8呢?
答案:因为在我们刚刚复习ret和call的时候有说过,call的动作中包含了rsp = rsp - 8,因此执行后会变成8
如果 call 前:
rsp % 16 == 0那么 call压入返回地址后:
rsp % 16 == 8所以正常函数入口处,通常应该看到:
rsp % 16 == 8编译器和 libc 默认相信这个规则。某些 libc 函数内部会用 SSE 指令,比如:
movaps xmmword ptr [rsp+0x10], xmm0movaps要求地址 16 字节对齐。栈状态错了,就可能在 libc 里面崩。
4.为什么正常call不会错,ROP就会出错
这个问题是我之前的疑惑,我们还是从会汇编的角度分析
正常进入backdoor:
call backdoor假设call前:
rsp % 16 == 0执行call后进入 backdoor:
rsp % 16 == 8backdoor开头通常是:
push rbp mov rbp, rsppush rbp 会:
rsp = rsp - 8于是:
backdoor 入口:rsp % 16 == 8 push rbp 后: rsp % 16 == 0然后 backdoor里面如果要:
call system此时 call system 前正好是:
rsp % 16 == 0所以进入 system后:
rsp % 16 == 8完全符合 ABI。
如果溢出不是通过call backdoor进函数,而是通过漏洞函数最后的ret进函数。
注意:
call 是 rsp -= 8 ret 是 rsp += 8它俩对rsp的影响正好相反。
执行 ret:
rip = 0x400657 rsp = rsp + 8于是进入backdoor时,rsp状态可能变成:
rsp % 16 == 0但正常函数入口应该是:
rsp % 16 == 8所以错了。
5.例题测试以及动态调试
我才用的题目是ctfshow中pwn38题目,因为只是作为一种验证方式,结合我之前的文章就先把我们需要的东西找到,根据之前的文章,我们需要找到backdoor地址、偏移就可以了,大家可以到时候自己我这边就不写详细的步骤了,相信各位师傅都能找到
这是我目前写的代码当然肯定是错误的,接着我们通过pwndbg调试看看会有什么
首先毋庸置疑肯定是打不通,来看看哪里出了问题
可以看到这里,目前rip里是0x400656 说明下一步会执行ret指令,
问题一:那么进入 backdoor 函数入口时,理想状态是什么呢?
答案: 是希望rsp%16=0x8,如果进入 backdoor 时 rsp & 0xf == 0x8,那么执行下一条 push rbp 后,rsp 会减 8,此时 rsp & 0xf 会变成 0x0。那么我们可以来测试一下看看对不对
目前可以看到左边最下边值并不是0x8而是0x0,并且在下一步其实是跟我们想要的结果恰恰是相反的
问题二:他并不是我们的理想状态,那么说明什么问题了?
答案:说明我们写的payload并没有栈对齐
问题三:为什么没有栈对齐呢?
答案:ret 会让 rsp += 8,进入 backdoor 时 rsp & 0xf 会变成 0x0,这不符合正常函数入口 rsp & 0xf == 0x8 的状态
我们既然已经找到了问题所在,那么就可以想解决办法了解决办法的思路是什么呢,有什么指令可以再往后挪8字节接着还能跳转的呢?
此时此刻灵光乍现,那就是再来一个ret指令就可以了。
所以我们这道题目一切都了然了,只需要再加上一个栈溢出后面加上一个ret地址,让他第一次跳转到我们自己加的ret地址,接着在ret执行我们的后门函数就即可
这样既可以栈对齐又可以返回到后门函数
问题四:怎么找到ret的地址呢?
答案:使用ROPgadget 工具,ROPgadget 是一个用于在二进制文件中自动搜索 ROP 片段的工具。
例如我这个题目使用的命令:
ROPgadget --binary ./pwn38 | grep 'ret'搜索所有包含 ret 的 gadget
当然会搜索出来很多,只需要选择只有ret的地址就可以了,如图
我选择的地址是0x0000000000400287
下面重新写我们的payload:
frompwnimport*context(log_level="debug",arch="amd64",os="linux")io=process("./pwn38")elf=ELF("./pwn38")gdb.attach(io)padding=18backdoor=elf.symbols["backdoor"]ret_arr=0x0000000000400287payload=cyclic(padding)payload+=p64(ret_arr)payload+=p64(backdoor)io.sendline(payload)pause()io.interactive()下面我们才调试一下看看和我们之前的理想状态一样不一样
直接进入到backdoor函数,接着看一下rsp%16的值
可以看到左下角屏幕那里显示了0x8,说明了我们的栈对齐成功了符合了正常进入函数的要求
接着一直ni就可以输入命令了
所以 我们所谓的玄学栈对齐这样被解决了
本篇文章写的内容目的就是让各位新手师傅们了解栈对齐、为什么要栈对齐、如何栈对齐,免得各位新手师傅遇到问题解决不掉或者知其然而不知其所以然
感谢各位师傅的观看,并且欢迎各位师傅批评指正
