workspace/tags/pwn/
Pwn
- 1-0 Stack_overflow
栈溢出基础
前面的文章分别介绍了寄存器、汇编、栈帧与调用约定 现在我们把它们串起来:如果程序存在栈缓冲区越界,攻击者能把程序带到哪里去?
这篇文章的目标是让你亲手完成一次最简栈溢出——覆盖返回地址,让程序跳到不该执行的地方。读完本文后,你将能够自己编译漏洞程序、用 GDB 定位偏移、用 Python 构造输入,为后续的 ret2text、ret2shellcode 等专项文章做好准备
什么是栈溢出
当程序向栈上的局部数组写入超过其容量的数据时,多出的部分会覆盖相邻内存。如果覆盖范围到达保存的返回地址,
ret指令就会取到一个由攻击者控制的值,从而跳转到任意地址。回顾计算机组成中的栈帧布局:
高地址 +-------------------------+ | 返回地址 | 由 call 压入 +-------------------------+ | 调用者保存的 RBP | +-------------------------+ | 局部变量、数组、填充 | ← 数组从这里开始,向高地址方向写入 +-------------------------+ 低地址C 语言不检查数组边界。如果
buffer[32]的写入没有长度限制,多出的字节就会沿栈"向上"覆盖,依次破坏局部变量、旧 RBP,最终触及返回地址。一旦ret执行,CPU 就会跳到攻击者选定的位置。演示程序
这里提供一个demo
点击查看
#include <stdio.h> #include <stdlib.h> #include <string.h> /* * win — 正常情况下不会被调用的函数 * 溢出利用的目标就是让程序"意外"进入这里。 */ void win(void) { printf("\n=================================\n"); printf(" 你成功了!程序跳转到了 win 函数。\n"); printf("=================================\n\n"); exit(0); } /* * vulnerable — 包含栈溢出漏洞的函数 * * gets 不检查目标缓冲区大小,超出 buffer[32] 的输入 * 会持续写入栈上更高地址,可能覆盖: * - 其他局部变量 * - 保存的 RBP * - 返回地址 */ void vulnerable(void) { char buffer[32]; printf("请输入一些文本:"); /* gets 不会限制读取长度 */ gets(buffer); printf("你输入了:%s\n", buffer); } /* * print_info — 打印可用于偏移计算的信息 */ void print_info(void) { printf("-----------------\n"); printf("win() 地址:%p\n", (void *)win); printf("-----------------\n\n"); } int main(int argc, char **argv) { print_info(); vulnerable(); printf("\n程序正常退出。\n"); return 0; }vulnerable 中
gets会一直读取直到换行,完全不管buffer只有 32 字节。win是一个正常情况下永远不会被调用的函数,我们的目标就是通过溢出覆盖返回地址,让程序"意外"进入win。 - 0-0 计算机组成初步
从 C 语言到汇编:二进制安全需要的计算机组成基础
我们平时编写 C 语言程序时,习惯用变量、循环、判断和函数来描述程序逻辑:
int add(int a, int b) { return a + b; }但是 CPU 并不认识
int、return或函数名。源代码经过编译器处理后,会被翻译成机器指令;机器指令本质上是一串二进制数据。汇编语言则为这些机器指令提供了便于人类阅读的写法。上面的函数可能对应如下汇编逻辑:add: lea eax, [rdi + rsi] ret对于普通软件开发,我们未必需要关心翻译后的结果;但在逆向工程、漏洞分析和恶意代码分析中,拿到的往往只有编译后的程序。此时,我们必须从寄存器、内存和汇编指令出发,重新理解程序做了什么。
本文不会完整讲授数字电路、流水线或缓存一致性等计算机组成原理课程内容,而是围绕二进制安全所需的主线展开:
硬件如何协作 → CPU 如何保存数据 → 汇编指令如何操作数据和改变控制流 → 函数如何使用栈与寄存器。
文中的汇编代码采用 x86-64 Intel 语法,运行环境默认为 Linux 用户态,函数调用遵循 System V AMD64 ABI。示例以讲清原理为第一目标,实际编译结果会随着编译器版本、优化等级和源代码写法发生变化。
计算机由哪些主要硬件组成
一台计算机包含很多部件。对于分析普通用户态程序,我们首先需要认识 CPU、内存、外部存储、总线和输入输出设备。
部件 主要作用 与二进制分析的关系 CPU 解释并执行机器指令,完成运算和控制 汇编指令最终在 CPU 中执行 内存(RAM) 临时保存正在运行的程序、代码和数据 代码、栈、堆和全局变量都位于进程的虚拟地址空间中 硬盘或固态硬盘 长期保存程序和文件,断电后数据仍存在 ELF 可执行文件最初存放在这里,运行时再由系统装入内存 总线 在 CPU、内存和设备之间传输地址、数据和控制信息 决定各硬件如何交换信息,但初学阶段不必深入电气细节 输入输出设备 让计算机与用户和外界交换数据 程序通常通过操作系统提供的接口访问这些设备 1. CPU 内部有什么
为了便于理解,可以把 CPU 内部的重要部分概括为:
Terminal
C0nvR3 Lab terminal ready. Type help for commands.