news 2026/7/12 16:18:10

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从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] = rax

pop 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], xmm0

movaps要求地址 16 字节对齐。栈状态错了,就可能在 libc 里面崩。

4.为什么正常call不会错,ROP就会出错

这个问题是我之前的疑惑,我们还是从会汇编的角度分析

正常进入backdoor:

call backdoor

假设call前:

rsp % 16 == 0

执行call后进入 backdoor:

rsp % 16 == 8

backdoor开头通常是:

push rbp mov rbp, rsp

push 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就可以输入命令了

所以 我们所谓的玄学栈对齐这样被解决了

本篇文章写的内容目的就是让各位新手师傅们了解栈对齐、为什么要栈对齐、如何栈对齐,免得各位新手师傅遇到问题解决不掉或者知其然而不知其所以然

感谢各位师傅的观看,并且欢迎各位师傅批评指正

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/12 16:18:08

Nginx 编译 SSL 模块 2 大核心依赖:OpenSSL 开发库与源码路径配置详解

Nginx SSL模块编译:OpenSSL开发库与源码路径配置的深度实践在构建支持HTTPS的Nginx服务时,SSL模块的正确编译是确保安全通信的基础。本文将深入探讨OpenSSL开发库与源码路径配置的核心要点,帮助开发者从根本上理解并解决编译过程中的各类问题…

作者头像 李华
网站建设 2026/7/12 16:16:31

Keil 5 芯片包独立安装指南:离线部署 3 种主流 ARM Cortex-M 系列

Keil 5 芯片包独立安装指南:离线部署 3 种主流 ARM Cortex-M 系列 在企业研发实验室或高校教学机房中,我们常会遇到这样的困境:开发电脑因安全策略禁止连接外网,而项目又急需基于STM32F103搭建原型系统。此时,传统在线…

作者头像 李华
网站建设 2026/7/12 16:16:10

JumpServer v3.6+ 配置 VSCode Remote-SSH:2个关键参数与3步连接验证

JumpServer v3.6 服务端深度配置:解锁VSCode Remote-SSH全流程指南1. 服务端核心参数解析与配置逻辑在JumpServer v3.6版本中,要实现VSCode Remote-SSH的无缝集成,管理员需要重点关注两个关键服务端参数:ENABLE_LOCAL_PORT_FORWAR…

作者头像 李华
网站建设 2026/7/12 16:15:42

C++在AI推理引擎中不可替代的真相

AI推理引擎的幕后英雄当人们谈论大模型推理、实时图像识别或自动驾驶决策时,目光往往聚焦在模型架构和训练规模上。然而,在炫目的数字背后,将训练好的模型高效部署到生产环境并实现毫秒级响应,离不开一个低调而强大的技术栈——AI…

作者头像 李华