SEH在代码中使用__try, __except, __finally
关键字实现。
与c++的异常处理不同,不要混淆。微软SEH要早一些。
有处理代码则顺利处理,没有实现SEH则启动默认处理,终止进程。
调试器几乎拥有被调试者的所有权限,异常也优先由调试器处理。这时调试器会暂停,处理异常后继续调试。
下面是几种处理方法。
EXCEPTION_RECORD结构体有一个ExceptionCode成员,存有异常种类码值。
下一部分会再说这个结构体,先说下几个重要的ExceptionCode。
#define EXCEPTION_ACCESS_VIOLATION STATUS_ACCESS_VIOLATION
#define STATUS_ACCESS_VIOLATION ((DWORD )0xC0000005L)
触发:
mov dword ptr ds:[0], 1
add dword ptr ds:[401000], 1
xor dword ptr ds:[8000 0000], 1234h ;kernel address
#define EXCEPTION_BREAKPOINT STATUS_BREAKPOINT
#define STATUS_BREAKPOINT ((DWORD )0x80000003L)
调试器就是利用这个异常实现断点功能的。比如f2设置cc后会触发,但反汇编窗口并不会显示cc,用PETools将进程内存dump后就能看到cc了。
有一个调试方法,在注册表中将默认调试器改为OD,可以把一个目标文件的EP处字节改为cc,一运行就直接attach。
#define EXCEPTION_ILLEGAL_INSTRUCTION STATUS_ILLEGAL_INSTRUCTION
#define STATUS_ILLEGAL_INSTRUCTION ((DWORD )0xC000001DL)
遇到无法解析的OPCODE时会触发,比如x86没有0FFF
。
#define EXCEPTION_INT_DIVIDE_BY_ZERO STATUS_INTEGER_DIVIDE_BY_ZERO
#define STATUS_FLOAT_DIVIDE_BY_ZERO ((DWORD )0xC000008EL)
mov eax, 1
xor ebx, ebx
div ebx
#define EXCEPTION_SINGLE_STEP STATUS_SINGLE_STEP
#define STATUS_SINGLE_STEP ((DWORD )0x80000004L)
cpu在单步模式下,每执行一条指令就会触发该异常。将EFLAG的TF(trap,陷阱)置1,就进入单步模式。
SEH以链的形式存在,将异常依次传递,直到处理。
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;
若Next为-1,则说明这是链表最后一个节点。
回调处理函数原型如下:
typedef
_IRQL_requires_same_
_Function_class_(EXCEPTION_ROUTINE)
EXCEPTION_DISPOSITION
NTAPI
EXCEPTION_ROUTINE (
_Inout_ struct _EXCEPTION_RECORD *ExceptionRecord,
_In_ PVOID EstablisherFrame,
_Inout_ struct _CONTEXT *ContextRecord,
_In_ PVOID DispatcherContext
);
typedef EXCEPTION_ROUTINE *PEXCEPTION_ROUTINE;
第一个参数是刚刚整理异常时提到的结构体指针。
//
// Exception record definition.
//
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
typedef EXCEPTION_RECORD *PEXCEPTION_RECORD;
指向第一个节点(低地址)的位置。
ContextRecord指向Context结构体,用来备份寄存器的值,也就是书上常说的上下文环境。
每个线程都有这么一个结构体。
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;
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;
typedef CONTEXT *PCONTEXT;
// Exception disposition return values
typedef enum _EXCEPTION_DISPOSITION
{
ExceptionContinueExecution,
ExceptionContinueSearch,
ExceptionNestedException,
ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;
从0到3,依次为:
0. 继续执行异常代码;
2和3是在OS内部使用。
FS寄存器指向当前活动线程的TEB结构,0偏移处就是SEH第一个节点的地址,所以可以这样获得SEH链:
mov eax, fs:[0]
然后可以打开SEH chain窗口对比。
添加:
push @MyHandler
push dword ptr fs:[0]
mov dword ptr fs:[0], esp
删除:
pop dword ptr fs:[0]
add esp, 4
这里不太好理解,调试一下就懂了。
查看SEH Chain,在第一个节点地址772086D0下断点。
008FF97C ntdll.772086D0
008FF994 ntdll.772151B2
执行下面的指令:
011B2080 > 33C0 xor eax, eax
011B2082 C700 01000000 mov dword ptr [eax], 1
引发中断后,观察栈:
ESP ==> > 77215062 返回到 ntdll.77215062
ESP+4 > 008FF3F4
ESP+8 > 008FF97C
ESP+C > 008FF444
ESP+10 > 008FF37C
和普通函数调用不同,esp+4是第一个参数ExceptionRecord指针。
008FF3F4 05 00 00 C0 00 00 00 00 00 00 00 00 82 20 1B 01
c0000005就是EXCEPTION_ACCESS_VIOLATION的code值。
011b2082就是异常代码的地址。
008FF97C其实就是上面SEH Chain中的起始地址。
特别注意eip,是ContextRecord偏移B8的地方。
008FF4FC 82 20 1B 01
最后一个参数供系统内部使用,可忽略。
mov esi, dword ptr ss:[esp+c]
mov eax, dword ptr fs:[30]
cmp byte ptr ds:[eax+2], 1
jnz ...
mov dword ptr ds:[esi + B8], xxxxxxxx
xor eax, eax
retn
伪代码:
esi = &context
eax = &PEB
if(eax.BeingDebugged)
...
next_eip = xxxxxxxx;
return 0;
最后返回的0,就是ExceptionContinueExecution
。
下面介绍Debugging options - exception的几个选项。
合理配置可以在不暂停调试器的情况下基于规避SEH实现的反调试招数。
默认选中。也就是说,碰到的时候不会暂停。
这些复选框中有:
最后一个,是Float point unit,浮点运算单元,它有自己的一套指令,与普通x86结构不同。
勾选的异常会传递给被调试者处理。
这里添加的异常会发送给被调试者。