初识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的处理优先级就是从第一个节点开始一直向后。
安装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;
}
向量化异常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 "脚本执行完毕"