ret2text的深入分析
前言:从 0.5 开始的异世界 PWN,决定从新开始温习 PWN 题目,尽量深入理解每一道做过的题目
BUUCTF_ciscn_2019_n_1
由于 ret2text 是最简单的 PWN 题目了,先从最基础的程序开始分析每个步骤:只开启了 NX 保护

rwxp 分别表示可读可写可执行,可以了解一下数据段和代码段常见的配置:
| 权限组合 | 含义 | 常见区域 |
|---|---|---|
r--p |
只读,私有 | 代码段的某些只读数据部分 (如 .rodata) |
r-xp |
可读、可执行,私有 | 程序代码段 (.text),这是存放汇编指令的地方 |
rw-p |
可读、可写,私有 | 数据段 (.data, .bss),以及堆 (Heap) 和栈 (Stack) |
rwxp |
可读、可写、可执行 | 通常意味着内存中存在可以被攻击者利用的Shellcode 执行区。现代操作系统通常会开启 NX 保护来禁止这种权限组合。 |
直接看 main 函数的汇编:
1 | .text:00000000004006DC ; =============== S U B R O U T I N E ======================================= |
1 | .text:00000000004006DC push rbp |
先建立栈帧,rbp 被保存并指向当前栈顶,这里是为了方便函数调用时进行变量寻址
连续调用两次 setvbuf 函数,设置标准输出和标准输入,之后对 rax 进行清零,调用 func 函数:
call func:
自动 Push 返回地址:
- CPU 会计算出下一条指令的地址,即 call 指令本身地址 + call 指令的长度;之后会将这个返回地址压入栈中 rsp = rsp - 8 处,然后 [rsp] = 返回地址。
跳转 (JMP):
- CPU 将 RIP 寄存器修改为“目标地址”。
func:
1 | .text:0000000000400676 ; =============== S U B R O U T I N E ======================================= |
建立栈帧之后 sub rsp,30h 为局部变量分配 0x30 大小的栈空间
把 s 赋值给 edi 然后调用 puts 函数打印出字符串
1 | lea rax, [rbp+var_30] |
rax = rbp - 0x30( var_30 的偏移是 -0x30)。- 它是在计算地址。它并不去内存里取值,它只是把这个算好的数学结果丢给 rax。
1 | jnz short loc_4006CF |
- jnz (Jump if Not Zero):如果上一次比较运算的结果不是 0,就跳转到 loc_4006CF 位置。
0x4006D9
1 | .text:00000000004006D9 loc_4006D9: ; CODE XREF: func+57↑j |
- mov rsp, rbp:将栈指针 rsp 恢复到函数开始时的位置
- pop rbp:从栈顶弹出之前保存的 rbp 值,恢复 main 函数的栈帧基址,rsp + 8
- ret: 它从栈顶弹出一个值,这个值是调用该函数时通过 call 指令压入的返回地址,并将其存入 RIP 寄存器。
- CPU 执行完这行后,程序就会跳转回 main 函数,继续执行 call func 之后的下一条指令。
回到 main 函数:
1 | .text:0000000000400721 call func |
从 func 回归 (Return)
- .text:0000000000400721 call func
- ****CPU 执行这一行时,会自动将下一行代码的地址(即 0x400726)压入栈中,然后跳转到 func 的开头。
- 当 func 执行完毕,执行 retn 后,CPU 会弹回 0x400726 这个地址,程序在这里接着往下走。
设置返回值
- .text:0000000000400726 mov eax, 0
- ****将 eax 寄存器清零。
- 在 C 语言中,main 函数的 return 0; 就是通过将 eax 置 0 来实现的。
- 操作系统通过 eax 的值来判断程序是正常退出(0)还是出错退出(非 0)。
清理 main 的栈帧
- .text:000000000040072B pop rbp
- 恢复调用 main 的函数,通常是 __libc_start_main的栈基址。此时 rbp 回到了 main 被调用前的状态。
- .text:000000000040072C retn
- 这是整个程序的最后一次返回。它会从栈中弹出 __libc_start_main 压入的返回地址,真正结束程序。
ret2text 的利用手法:
大部分人可能对 ret2text 手法利用的理解仅仅停留在覆盖 rbp/ebp 之后覆盖返回地址即可,不过通过以上分析,我们可以清楚理解到,在 get 函数的时候我们可以通过栈溢出覆盖 rbp 之后,篡改 call 函数调用的时候压入的返回地址,只要我们控制了返回地址,我们就可以对整个程序进行随意地跳转,后面要复习到的栈迁移也是这个原理,本质就是篡改返回地址触发 system(‘/bin/sh’);
更新: 2026-05-22 15:02:38
原文: https://www.yuque.com/idcm/wnemg9/ag80g47gxgzhs04s