C程序栈帧返回地址是否指向.text段及内存段范围问题
关于函数栈帧返回地址与32位Linux内存布局的解答
对返回地址所属段的说明
常规编译运行的x86 32位C程序中,call指令压入栈的返回地址,本质是call指令下一条待执行指令的地址,绝大多数场景下属于可执行代码段范围,但不一定是主程序的.text段:
- 如果程序编译时未开启位置无关选项(
-no-pie),且关闭ASLR,主程序自身的代码会固定加载,函数调用返回到主程序自身代码时,返回地址确实落在主程序的.text段范围内,该段权限为可读、可执行、不可写。 - 如果程序开启PIE、或调用动态链接库函数、或做ROP/ret2libc利用时,返回地址可能落在动态链接器、libc等其他模块的可执行代码段,不属于主程序
.text段范围。 - 如果你做栈上shellcode注入,把返回地址改成栈上shellcode的起始地址,那返回地址就落在栈段,不属于任何
.text段。
你遇到的覆盖异常现象,恰恰说明你找错了返回地址的位置:
- 覆盖栈上保存的旧EBP值立刻崩溃是正常现象:函数收尾执行
leave指令时,会把保存的EBP值加载回EBP寄存器,后续栈寻址、ret取返回地址都依赖EBP的值,EBP被篡改后会直接访问非法内存触发段错误。 - 你覆盖自认为是返回地址的位置后,程序仅多执行一次循环就崩溃,说明你改到的是栈上存储的循环计数器、临时变量之类的数据,根本没到返回地址的存储位置——x86 32位栈帧中,保存的旧EBP位于
[EBP]位置,返回地址在旧EBP的高地址方向,即[EBP+4]的位置,你大概率是往低地址方向找错了偏移。另外你探测到的0x1f7ffd属于极低地址范围,32位Linux下该区域是未映射的保留区域,根本不可能存储合法代码地址,这是你格式化字符串读内存时偏移计算错误,把栈上的普通数值(比如局部变量、短整数)误判成了地址。
32位Linux各段地址范围说明
你之前的记忆完全混淆了32位和64位架构的地址布局:0x7fffffff是64位用户态地址空间的上限,0x55555555是64位PIE程序的默认加载基址,和32位场景完全不匹配。32位Linux用户态地址空间总大小为3G,范围是0x00000000 ~ 0xbfffffff,高于0xc0000000的地址属于内核空间,用户态无法访问。各段范围根据安全机制开启状态分为两类:
场景1:关闭ASLR、编译为非PIE程序(入门CTF题常用编译配置)
内存布局从低地址到高地址固定:
.text段(主程序代码段):从0x08048000起始,后续紧跟.rodata、.data段- BSS段:位于主程序数据段后,大致范围在
0x0804a000 ~ 0x080b0000区间 - 堆:从BSS段结束位置开始,向高地址增长
- 动态链接库(libc、ld-linux等):从
0x40000000起始映射,各库的代码段、BSS段都落在该区间 - 栈:从
0xbfffffff开始向低地址增长,栈上所有地址都落在0xbfxxxxxx区间
场景2:开启ASLR、编译为PIE程序(现代Linux默认配置)
各段基址会随机化,仅能给出大致浮动范围:
- 主程序
.text、BSS、data段:浮动在0x56555000附近的随机偏移位置 - 动态链接库:浮动在
0xb7xxxxxx附近的随机范围 - 栈:浮动在
0xbfxxxxxx区间,仍从高地址向低地址增长 - 堆、mmap匿名映射区域:分布在堆到动态库之间的随机地址范围
调试建议
定位返回地址最准确的方式是用gdb附加进程,在目标函数入口处下断点,等push ebp; mov ebp, esp两条栈帧建立指令执行完后,直接执行x/wx $ebp+4查看该地址存储的值,就是当前函数真实的返回地址,对照/proc/[进程pid]/maps里的内存映射表,就能直接确认该地址所属的内存段。
内容的提问来源于stack exchange,提问作者Nikolai Savulkin
相关产品推荐
相关产品推荐

