异常处理第一讲(SEH),筛选器异常,以及__asm的扩展
博客园IBinary原创 博客连接:http://www.cnblogs.com/iBinary/
转载请注明出处,谢谢
一丶__Asm的扩展知识
①丶使用关键字,解决局部变量申请问题
昨天已经介绍了__asm的基本用法,现在对其做个扩展(上一篇是32为汇编第七讲)
昨天我们写的裸函数,那么变量的问题需要解决
请看C的内联汇编
_declspec(naked) int MySub(int n1,int n2)
{
int nLocal1; //注意变量
int nLocal2;
_asm
{
push ebp
mov ebp,esp
sub esp,8 //申请了两个变量,那么栈就要抬高8个字节,一个变量4个字节
push 0
push 0
push 0
push 0
pop ebp
ret
}
}
那么对于上面的程序,你觉着可能没问题,但是我们想一下,如果我在定义多个变量,那么下面的指令就要多次改动
sub esp,xxx
但是这样不好,为什么,如果来个数组,来个结构体套结构体,你怎么保证我们要开辟多少个局部变量?
所以VC为我们提供了语法的支持
__LOCAL_SIZE 申请局部变量的时候自动抬栈
,ALT + 8看下反汇编窗口到底做了什么
我们看到了,我们就定义了两个局部变量,为什么是申请了48个字节,原因是我这里是Debug版本,默认编译器会帮我们申请40h(也就是64个字节)的局部空间,那么加上我们的两个局部变量正好48H个字节
如果是发布版(Release)那么则会根据你写的汇编代码的不同,申请不同的空间,为什么这样说.
因为你定义了两个局部变量,而在__asm里面你只使用了一个
例如:
__asm
{
...
mov nLocal,eax //把eax的值给局部变量
...
}
如果另一个没有使用,编译器就可能给你优化掉,只给你申请4个局部变量空间,以为不是Debug版本,所以不会在额外给你申请40H个字节了
注意,在裸函数中你定义的局部变量是不能初始化的
也就是说你可以写成我上面的那样子,但是不能初始化值,因为这个时候还没有抬栈,比如抬栈之后初始化,
而初始化就可能在__asm里面去写
当然更多的扩展的__asm语法可以MSDN查询
比如上面这个
输入__asm查询即可
②丶解决数组求大小,求数组类型大小,以及求数组/类型的问题
我们有的时候会想,我们的Sizeof()还是想使用的,很方便的
那么现在我们不能使用了,但是VC为了支持,还是提供了额外的语法支持
例如MSDN上查询到的
上面说了详细的例子
LENGTH 数组 代表了C语言的 Sizeof(数组) / sizeof(数组[0])
SIZE 数组 代表了C语言的求数组大小
TYPE 数组 代表了C语言的求数组第一项类型的大小
具体测试,请自己上机实践
③丶解决db定义数据的关键字
在VC内联汇编中,db关键字 dd dw ....等等都不可以使用了,但是提供了额外的语法
_emit指令
上面说的很清楚,如果想使用db命令则用这个指令代替
例如:
__asm
{
_emit 04ah
}
如果想使用DW 则定义两个,使用DD 着用三个即可.
注意,使用这个指令我们可以把OUT指令的二进制定义出来,还有操作码,那么汇编就是对应的OUT指令了
我们都知道,我们32位汇编下都是保护模式了,也就是说,IN OUT不管用了,(不代表不能用)我们一样可以用,只不过
IN OUT 指令是三环,所以执行这条二进制指令的时候,CPU是拒绝执行的,我们要执行就是在0环下执行,也就是常说的操作系统内部,内核执行.
博客园IBinary原创 博客连接:http://www.cnblogs.com/iBinary/
转载请注明出处,谢谢
二丶异常数处理(SEH)筛选器异常
首先我们要明白什么是异常,以及异常的作用(抱着疑问来学习,事半功倍)
什么是异常:
SEH("Structured Exception Handling"),即结构化异常处理.是操作系统提供给程序设计者的强有力的处理程序错误或异常的武器.在VISUAL C++中你或许已经熟悉了_try{} _finally{} 和_try{} _except {} 结构,这些并不是 编译程序本身所固有的,本质上只不过是对windows内在提供的结构化异常处理的包装
说白了,就是 try cath的异常和这个异常不是同一个
我们这里说的异常,是这个异常怎么产生的,以及怎么处理的,也就是说你写程序长出现的C00005这种异常,空指针异常
作用:
相信大家可能都遇到过程序崩溃的情况,或者我们有时候使用QQ 通讯工具的时候也会崩溃 :)
那么QQ处理的就是弹出一个框,让你发送错误报告什么的,为什么,因为QQ用户量很大,都是亿万级的用户量
而这些用户就是测试QQ的最好的人,比如有的用户无聊,好友列表来回点上1个小时,对,就是那么无聊.
比如王者荣耀,有人还打个游戏几个小时 :)废话好多
作用就是 让我们快速定位程序问题,如果你弹出一个崩溃框框,那么用户可能随手关了,用户也不懂这些对吧.
那么今天介绍一下筛选器异常
①丶筛选器异常
1.设置筛选器异常
啥是筛选器异常?
筛选器处理异常是由程序指定一个异常处理回调函数,当发生异常的时候,系统将调用这个回调函数,并根据回调函数的返回值决定如何进行下一步操作。 在进程范围内,筛选器异常处理回调函数是惟一的,设置了一个新的回调函数后,原来的就失效了。
啥意思,就是你提供一个函数,当程序出错了系统会调用这个函数,如果这个回调就一个,那么我们可以保存一下,当我们设置新的时候,也可以调用旧的,不过这个一般不使用
看下API 和回调函数
API,和API原型:
SetUnhandledExceptionFilter //设置一个异常处理回调
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);
MSDN图片
很简单,我们写个回调,然后传入函数的指针即可.
如果我们取消异常处理,则用
UnhandledExceptionFilter
回调函数的设置
LONG __stdcall 自己的回调函数名字(__EXCEPTION_POINTERS *ExceptionInfo)
{
MessageBoxA(NULL,NULL,NULL,NULL);
return 0;
}
且看我写一个真正的异常处理C/C++程序
其实很简单,就是给个回调,设置一下就完了.
看下C++代码
#include <stdio.h>
#include <windows.h>
LONG __stdcall MyException(_EXCEPTION_POINTERS *Ecptpoints)
{
MessageBox(NULL, NULL, NULL, NULL);
return 0;
}
int main(_In_ int _Argc, _In_reads_(_Argc) _Pre_z_ char ** _Argv, _In_z_ char ** _Env)
{
SetUnhandledExceptionFilter(MyException); //设置异常的回调函数,如果有异常,操作系统会调用我们给的回调
int *pNum = NULL;
*pNum = 1; //这里故意引发异常,空指针异常
return 0;
}
我们打开程序,看看会怎么样的结果
因为空指针异常了,所以操作系统调用了我们的回调函数,而在回调函数里面我们谢了MsgBox,所以弹框了
但是我们点击确定,又会出现系统崩溃,我们看下
为什么?,对了,我们没有调用退出函数,也就是没有调用
ExitProcess 退出进程
调用了就可以了.
但是,猜错了,固然我们调用退出进程可以解决问题,但是结果不是这样的,这和会调用的返回值有关,且看下文
详解回调返回值
2.筛选器异常回调函数的返回值问题
他有三种情况:
这里介绍两种,
1.EXCEPTION_CONTINUE_SEARCH 宏定义就是 0 意思就是处理完毕之后,不处理了,你接着处理,上文我们的代码就是这样
2.EXCEPTION_EXECUTE_HANDLER 宏定义就是1 意思就是我处理完了,不让下方处理了,也就代表这结束进程
上下一个自己MSDN查询把 :)
3.筛选器异常的反调试功能
为什么这样说,上面我们用异常输出了一个信息框,但是现在我们在里面藏着我们的代码,如果我们调试,
那么异常就会被OD接受,也就是说我们的异常函数不会到的,也就不会输出了.比如
这个是我们上面的代码,首先给eax清空,然后 又把1 给空地址写入内容,所以产生异常了.
但是注意,这个只是我们的代码,而我们利用回调函数能成功输出字符串,所以现在这里OD调试的时候,则会接受异常
我们找下我们参数在哪里
可以看出,我们压栈的参数则是 回调函数的地址,我们跳转到那里,则可以看到我们的代码了
因为我们是Debug版本,所以内部多了一程JMP跳转
具体怎么写大家自己调试
4.回调函数的参数问题
现在我们可以看下参数了,我们知道回调函数有一个参数,这个参数主要保存了错误信息
看下内容是什么
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord; //保存异常信息的结构体
PCONTEXT ContextRecord; //保存但是寄存器信息的结构体
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
可以看出内部还是嵌套了结构体
第一个结构体的作用: 主要保存异常信息,例如错误代码,发生错误的地址等等...
第二个结果提的作用: 主要保存了但是错误信息发生的时候的寄存器信息,记录了EIP,而我们知道EIP保存了当前错误地址的信息
那么具体展开看下把,如果没兴趣,则不用看了,直接看下方的目录即可.
第一个结构体信息:
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; //保存了错误代码
DWORD ExceptionFlags; //保存了错误标志
struct _EXCEPTION_RECORD *ExceptionRecord; //类似于链表,继续保存错误信息(异常链)
PVOID ExceptionAddress; //错误发生的时候的地址
DWORD NumberParameters; //个数
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD; //附加数组
现在我们只关心错误代码,以及错误的地址即可.
看下第二个结构体信息:
typedef struct _CONTEXT {
//
// The flags values within this flag control the contents of
// a CONTEXT record.
//
// If the context record is used as an input parameter, then
// for each portion of the context record controlled by a flag
// whose value is set, it is assumed that that portion of the
// context record contains valid context. If the context record
// is being used to modify a threads context, then only that
// portion of the threads context will be modified.
//
// If the context record is used as an IN OUT parameter to capture
// the context of a thread, then only those portions of the thread's
// context corresponding to set flags will be returned.
//
// The context record is never used as an OUT only parameter.
//
DWORD ContextFlags;
//
// This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
// set in ContextFlags. Note that CONTEXT_DEBUG_REGISTERS is NOT
// included in CONTEXT_FULL.
//
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3; //这些寄存器我们没见过,主要是Dr0 - 3 是我们OD中硬件断点使用的,而 6 - 7是内存断点使用,具体设置寄存器,有专门的API
DWORD Dr6;
DWORD Dr7;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
//
FLOATING_SAVE_AREA FloatSave;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_SEGMENTS.
//
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_INTEGER.
//
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_CONTROL.
//
DWORD Ebp;
DWORD Eip;
DWORD SegCs; // MUST BE SANITIZED
DWORD EFlags; // MUST BE SANITIZED
DWORD Esp;
DWORD SegSs;
//
// This section is specified/returned if the ContextFlags word
// contains the flag CONTEXT_EXTENDED_REGISTERS.
// The format and contexts are processor specific
//
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
可以看出,这个就是保存但是错误的时候的寄存器的环境
博客园IBinary原创 博客连接:http://www.cnblogs.com/iBinary/
转载请注明出处,谢谢
三丶修改寄存器,和获取寄存器的值,以及寄存器注入
简单的一场我们也理解了
这里简单提一下,我们可以使用API来设置寄存器的信息,也可以获取
分别是
SetThreadContext //设置寄存器信息
GetThreadContext //获取寄存器信息
看下SetThreadContext的效果
BOOL SetThreadContext(
HANDLE hThread, // handle to thread
CONST CONTEXT*lpContext // context structure
);
这里他需要一个线程的句柄,还需要一个ConText结构体
上面说了这个结构体中保存的寄存器,所以我们给一个结构体则可以设置
这里主要简单提一下
注意,我们可以用这个做一个注入
你可以修改EIP的值,让它变为loadlibray的地址,这样可以不需要创建远程现成
这里简单提下,因为时间关系,没时间细写了
/*
1.OpenThread 打开一个线程
2.SuspendThread 暂定这个线程
3.VirtualAllocEX 申请空间
4.WriteProcessMemory 写入DLL路径
5.修改Eip
6.ResumeThread 启动线程
....
*/
具体先看下网上的内容,时间关系,后面写
今天主要是代码没有课堂资料
博客园IBinary原创 博客连接:http://www.cnblogs.com/iBinary/
转载请注明出处,谢谢