逆向基础-Windows异常

初识windows异常

SEH

SEH概念:
Structured Exception Handling
SEH是windows操作系统默认的异常处理机制。

如何使用:;
在代码中使用 __try__except();
__except()小括号里填写表达式,表达式为真的时候,执行里面的内容。

int main()
{
	__try
	{
		printf("hellow 51hook");
	}
	__except(1)
	{
		printf("异常处理内容");	
	}
	return 0;
}

异常处理机制:
当我们非调试状态下运行一个程序的时候,程序如果触发了异常,那么会先判断是否存在异常处理器,如果存在则跳转到异常处理函数去执行,如果不存在则退出程序。

如果程序处于被调试状态,触发异常的时候,操作系统最先把异常抛给调试进程,也就是我们的调试器去处理。
我们看到的现象就是当触发了异常之后程序会暂停下来,也就是所谓的断下,这就是我们的断点的原理。
当异常抛给调试器之后,调试器可以选择:
  -1.修改触发异常的代码继续执行(程序会停留在触发异常的代码处,导致异常代码无法被执行)
  -2.忽略异常交给SEH执行
  
OD按下了F2,再运行可以断下来,为什么呢?
  -我们看见的是这行汇编指令下不下断点都是长这样
  -但是实际上是OD做了障眼法,实际上这一行汇编指令里面修改为了CC,也就是int 3这个中断例程
  
SEH结构:
typedef struct _EXCEPTION_REGISTRATION_RECORD {
    struct _EXCEPTION_REGISTRATION_RECORD *Next;
    PEXCEPTION_ROUTINE Handler;//异常处理器 异常处理函数
} EXCEPTION_REGISTRATION_RECORD;

创建一个SEH结构体

那么SEH链存在了哪里?操作系统又是如何使用的?
异常链存放在fs:[0]

EXCEPTION_DISPOSITION  myExceptHandler(
	struct _EXCEPTION_RECORD* ExceptionRecord,
	PVOID EstablisherFrame,
	struct _CONTEXT* pcontext,
	PVOID DispatcherContext
)

新的SEH节点每次是插入到最前面,然后SEH的处理优先级就是从第一个节点开始一直向后。

逆向基础-Windows异常_第1张图片

安装SEH

前面我们是编写了try..catch..然后编译器会自动帮我们生成SEH异常处理,并且添加到第一个节点。

那么我们现在就自己手工完成这个操作,即自己创建一个异常处理器,然后放在最前面。

__except()括号内部表达式的取值范围:
  -1.处理异常
  -0.不处理异常交给异常链的下一个去处理
  --1.继续执行
  
#include 
#include 

int main() {
	__try {
		__try {
			char* str = NULL;
			str[0] = 'a';
		}__except (1) {
			printf("触发异常了\n");
		}
	}__except (1) {
		printf("触发异常了2\n");
	}
	printf("hello 51hook\n");
	system("pause");
	return 0;
}

就是这种嵌套处理的情况,1的话表示我处理,0的话就交给下一个,即外部处理,-1的话就是继续执行。

接下来我们就开始自己添加异常处理器。
就是不通过try..except...
通过try..except是系统帮我们添加,并且放在SEH链的第一个位置。
我们手工来添加.

手工的方式记得修改连接器高级中的,影响是否具有安全异常处理程序:否

#include 
#include 

/*
	@function 自定义异常处理函数
	@param ExceptionRecord 记录异常的信息
	@param EstablisherFrame
	@param pcontext 寄存器环境
	@param DispatcherContext
	@return     ExceptionContinueExecution 0 交给下一个SEH
				ExceptionContinueSearch 1 自己处理
*/
EXCEPTION_DISPOSITION  myExceptHandler(
	struct _EXCEPTION_RECORD* ExceptionRecord,
	PVOID EstablisherFrame,
	struct _CONTEXT* pcontext,
	PVOID DispatcherContext
) {
	MessageBoxA(NULL, "异常处理代码执行了", "提示", MB_OK);
	// 检测是否存在调试器
	DWORD isDebuger = 0;
	__asm {
		mov eax, fs: [0x18]; // 拿到TEB 0x30是PEB
		mov eax, [eax + 0x30]; // 拿到PEB
		movzx eax, byte ptr [eax + 2]; // PEB + 2 这个位置的一个字节表示是否处于被调试状态
		mov isDebuger, eax;
	};
	if (isDebuger == 1) {
		// 处于被调试状态
		MessageBoxA(NULL, "检测到调试器", "提示", MB_OK);
		exit(0);
	}
	pcontext->Eip = pcontext->Eip + 4; // 然后跳过异常代码,就相当于处理了异常,直接执行异常的下一条指令
	return ExceptionContinueExecution;
}

int main() {
	DWORD exceptionFunAddr = (DWORD)myExceptHandler; // 异常处理器的地址
	__asm {
		push exceptionFunAddr;
		mov eax, fs: [0] ;
		push eax;
		mov fs : [0] , esp; // 就是把SEH异常处理头部移动到当前的esp位置
	};
	char* str = NULL;
	str[0] = 'a';
	printf("hello 51hook\n");
	system("pause");
	return 0;
}

逆向基础-Windows异常_第2张图片

逆向基础-Windows异常_第3张图片

向量化异常VEH

veh:向量化异常

veh和seh的区别:
SEH是基于线程的,VEH是基于进程的。
VEH以双链表的形式保存在堆中。
SEH以单链表的形式保存在栈中。
异常触发后异常的优先处理顺序:调试器->VEH->SEH

添加VEH异常:
PVOID AddVectoredExceptionHandler(
		ULONG                       First,//1添加到链表头部,0添加到尾部
		PVECTORED_EXCEPTION_HANDLER Handler//异常处理器地址
	);

PVECTORED_EXCEPTION_HANDLER PvectoredExceptionHandler;

LONG  PvectoredExceptionHandler(_EXCEPTION_POINTERS *ExceptionInfo)
{

}

typedef struct _EXCEPTION_POINTERS {
  PEXCEPTION_RECORD ExceptionRecord;
  PCONTEXT          ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

#include 
#include 

/*
	@function 自定义异常处理函数
	@param ExceptionRecord 记录异常的信息
	@param EstablisherFrame
	@param pcontext 寄存器环境
	@param DispatcherContext
	@return     ExceptionContinueExecution 0 交给下一个SEH
				ExceptionContinueSearch 1 自己处理
*/
EXCEPTION_DISPOSITION  myExceptHandler(
	struct _EXCEPTION_RECORD* ExceptionRecord,
	PVOID EstablisherFrame,
	struct _CONTEXT* pcontext,
	PVOID DispatcherContext
) {
	MessageBoxA(NULL, "异常处理代码执行了", "提示", MB_OK);
	// 检测是否存在调试器
	DWORD isDebuger = 0;
	__asm {
		mov eax, fs: [0x18]; // 拿到TEB 0x30是PEB
		mov eax, [eax + 0x30]; // 拿到PEB
		movzx eax, byte ptr [eax + 2]; // PEB + 2 这个位置的一个字节表示是否处于被调试状态
		mov isDebuger, eax;
	};
	if (isDebuger == 1) {
		// 处于被调试状态
		MessageBoxA(NULL, "检测到调试器", "提示", MB_OK);
		exit(0);
	}
	pcontext->Eip = pcontext->Eip + 4; // 然后跳过异常代码,就相当于处理了异常,直接执行异常的下一条指令
	return ExceptionContinueExecution;
}

/*
	typedef struct _EXCEPTION_POINTERS {
	  PEXCEPTION_RECORD ExceptionRecord; 异常日志
	  PCONTEXT          ContextRecord; 寄存器环境
	} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

	@return 返回-1表示继续执行,0的话交给下一个执行
*/
LONG WINAPI PvectoredExceptionHandler(_EXCEPTION_POINTERS* ExceptionInfo)
{
	MessageBoxA(NULL, "veh处理了异常", "提示", MB_OK);
	ExceptionInfo->ContextRecord->Eip += 4; // 我们跳过出现异常的代码,让她继续执行 
	// 那么继续执行,因为我们跳过了异常所以 SEH就不会被执行了
	return EXCEPTION_CONTINUE_EXECUTION;
}

int main() {
	AddVectoredExceptionHandler(1, PvectoredExceptionHandler);

	DWORD exceptionFunAddr = (DWORD)myExceptHandler; // 异常处理器的地址
	__asm {
		push exceptionFunAddr;
		mov eax, fs: [0] ;
		push eax;
		mov fs : [0] , esp; // 就是把SEH异常处理头部移动到当前的esp位置
	};
	char* str = NULL;
	str[0] = 'a';
	printf("hello 51hook\n");
	system("pause");
	return 0;
}

VEH HOOK

硬件断点的原理:
MessageBoxA
将需要hook的地址设置硬件执行断点,一旦执行到该程序就会触发异常,这个时候异常处理优先级按照调试器-VEH-SEH去处理。
在程序处于非调试状态下,可以通过硬件断点+异常处理函数对某个地址进行HOOK。不过最多只能HOOK4个地址。

如何设置硬件断点?
CONTEXT_DEBUG_REGISTERS
API:
SetThreadContext

那么我们就可以搞事情了,因为CONTEXT存放的是触发异常的线程上下文的寄存器环境。
这个时候刚进来,还没有push ebp,所以
ESP = 返回地址
ESP + 4 = 第一个参数

硬件执行断点依赖于硬件,可以避免对软件进行修改,这样可以绕开很多检测。

Context中:
  -DR0~DR3 存放着我们要下断点的地址
  -DR6 存放着异常信息
  -DR7 不同位有不同意思
    -L0~L3 对应DR0~DR3的断点是否有效(局部断点)
    -G0~G3 对应DR0~DR3的断点是否有效(全局断点)Windows下没用
    -LEN0~LEN3 对应DR0~DR3的断点长度,多少个字节
      -00 表示一个字节  01 表示两个字节 11 表示四个字节
    -RW0~RW3 对应DR0~DR3断点的类型
      -00 执行断点     01 写入断点    11 读写断点
    
AddVectoredExceptionHandler

SEH HOOK
当异常没有处理的时候,系统就会调用SetUnhandledExceptionFilter所设置异常处理函数

// Test_Console_1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
#include 

using namespace std;

// 如果有调试器,则不会执行这个函数
BOOL bIsBeinDbg = TRUE;
LONG WINAPI UnhandledExcepFilter(PEXCEPTION_POINTERS pExcepPointers){
    bIsBeinDbg = FALSE;
    return EXCEPTION_CONTINUE_EXECUTION;
}

int main()
{
    // 注册异常处理函数
    LPTOP_LEVEL_EXCEPTION_FILTER Top = SetUnhandledExceptionFilter(UnhandledExcepFilter);

    // 主动抛出一个异常
    RaiseException(EXCEPTION_FLT_DIVIDE_BY_ZERO, 0, 0, NULL);

    if (bIsBeinDbg == TRUE) {
        cout << "发现调试器!" << endl;
    }
    else {
        cout << "没有调试器!" << endl;
    }

main_end:
    getchar();
    return 0;
}

带断点调试检测的壳

脱壳三部曲:
  -1.找OEP
    -按照我们加壳的思路,区段被加密肯定会去解密,解密就一定会访问数据,可以在区段下内存访问断点,一步一步跟踪到OEP
    【按照我们最单纯的想法,解密代码执行完毕以后会跳转到OEP】
  -2.修复IAT
    -思路1:IAT被加密后根据IAT表地址下访问或者写入断点,断下后在附近观察找到关键指令
    -思路2:无论IAT如何加密如何周转最终一定得调用真实的API可以观察中转函数特点创远程线程修复IAT
  -3.DUMP
  
1.加壳一定会加密代码段,如果加密代码段,我们可以下一个内存写入断点。当解密代码执行完毕以后,下一步再下一个内存访问断点,因为解密完了,我们肯定会执行代码段的内容。
【解密区段,所以我们下了写入断点
解密完成以后,我们肯定会jmp oep去执行代码段里面的代码,那肯定访问了,所以下访问断点,你执行代码段代码之前就会停下了】
【上面的方法太麻烦,有一个投机取巧的方法就是我们去猜测第一个调用的API是什么,即壳的编译器版本是什么】

2.接着我们就要修复IAT
  -因为他对IAT进行了加密,所以肯定会对IAT进行填充内容,所以我们就可以下断点了。
    -但是我们发现我们的硬件断点不好使
      -说明它应该检测到我们的硬件断点了。
    -那么我们试一下我们的内存断点
      -发现程序直接崩溃了,所以它应该也是进行了检测
  -那么就是说现在对IAT下断点的方式不起作用了,无论是硬件还是内存断点
  -那么我们知道它是因为异常触发的崩溃,所以我们猜测它有反调试。
  
内存断点原理:比如你要下一个内存访问断点,那么我就把这个内存的可访问属性给去掉,那你访问就会发生异常了。
那为什么OD我们设置的内存访问可以停下来,它程序触发的异常不能停下来,因为OD对自己设置的断点是有记录的。
不过我们可以对OD进行设置,就是如果发生了异常我们会停下来,而不会直接崩溃,即OD就不会对软件发生的异常不处理。

然后我们OD没有处理,它程序的SEH异常处理机制肯定也没有处理,因为处理了就不会崩溃了。

断点原理:
1.OD的F2断点:
INT3指令触发异常
2.内存断点:
设置内存属性触发异常
3.硬件断点
在寄存器中,有这些一些寄存器,他们用于调试,人们称之为调试寄存器,调试寄存器一共有8个名字Dr0~Dr7,对于Dr0~Dr3的四个调试寄存器,他们的作用是存放中断寄存器的地址。
对于Dr4,Dr5寄存器我们一般不使用,对于Dr6,Dr7是用来记录我们Dr0~Dr3下段地址的属性的。

所以说白了,我们前面的硬件断点失效就是因为他修改了硬件断点寄存器的值。

清除硬件断点的两种方法:
  -1.通过异常处理函数中的PCONTEXT设置
  -2.通过SetThreadContext设置
  
VAR getApiAddr
VAR setIatAddr
VAR exceptionAddr
VAR dwOepAddr
VAR realApiAddr

MOV getApiAddr,0042FFA1 // 这行执行完eax保存真实地址
MOV  setIatAddr,004301A0 
MOV exceptionAddr,00431FA1
MOV dwOepAddr,00402680

// 清除硬件断点和内存断点
BPHWC
BC 

// 设置硬件断点
BPHWS getApiAddr,"x"
BPHWS setIatAddr,"x"
BPHWS exceptionAddr,"x"
BPHWS dwOepAddr,"x"

BEGIN:
RUN 

CMP eip,getApiAddr
JNZ TAG1
MOV realApiAddr,eax // 转存真实地址
JMP BEGIN


TAG1:
CMP eip,setIatAddr
JNZ TAG2
MOV eax,realApiAddr
JMP BEGIN

TAG2:
CMP eip,exceptionAddr
JNZ TAG3
FILL 00431FA1,1A,90
JMP BEGIN

TAG3:
CMP eip,eip,dwOepAddr
JNZ BEGIN
MSG "脚本执行完毕"

你可能感兴趣的:(逆向,安全)