从0学习pwn【第五章】剖析ret2text64位,从函数调用到gdb调试(解决玄学栈对齐)

发布时间:2026/7/12 16:18:18
从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 [rsp0x10], xmm0movaps要求地址 16 字节对齐。栈状态错了就可能在 libc 里面崩。4.为什么正常call不会错ROP就会出错这个问题是我之前的疑惑我们还是从会汇编的角度分析正常进入backdoorcall backdoor假设call前rsp % 16 0执行call后进入 backdoorrsp % 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的影响正好相反。执行 retrip 0x400657 rsp rsp 8于是进入backdoor时rsp状态可能变成rsp % 16 0但正常函数入口应该是rsp % 16 8所以错了。5.例题测试以及动态调试我才用的题目是ctfshow中pwn38题目因为只是作为一种验证方式结合我之前的文章就先把我们需要的东西找到根据之前的文章我们需要找到backdoor地址、偏移就可以了大家可以到时候自己我这边就不写详细的步骤了相信各位师傅都能找到这是我目前写的代码当然肯定是错误的接着我们通过pwndbg调试看看会有什么首先毋庸置疑肯定是打不通来看看哪里出了问题可以看到这里目前rip里是0x400656 说明下一步会执行ret指令问题一那么进入 backdoor 函数入口时理想状态是什么呢答案 是希望rsp%160x8如果进入 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下面重新写我们的payloadfrompwnimport*context(log_leveldebug,archamd64,oslinux)ioprocess(./pwn38)elfELF(./pwn38)gdb.attach(io)padding18backdoorelf.symbols[backdoor]ret_arr0x0000000000400287payloadcyclic(padding)payloadp64(ret_arr)payloadp64(backdoor)io.sendline(payload)pause()io.interactive()下面我们才调试一下看看和我们之前的理想状态一样不一样直接进入到backdoor函数接着看一下rsp%16的值可以看到左下角屏幕那里显示了0x8说明了我们的栈对齐成功了符合了正常进入函数的要求接着一直ni就可以输入命令了所以 我们所谓的玄学栈对齐这样被解决了本篇文章写的内容目的就是让各位新手师傅们了解栈对齐、为什么要栈对齐、如何栈对齐免得各位新手师傅遇到问题解决不掉或者知其然而不知其所以然感谢各位师傅的观看并且欢迎各位师傅批评指正