seh to fasm 研究
作者:小鱼
为啥有此文呢,可能你会说seh文章很多啊。那请你去各大搜索引擎搜索下相关fasm的seh文章以及官方搜索
下更是甚少,几乎没有。所以就有了此文,希望能帮到一些和我一样喜欢fasm的朋友们。
1.概念
SEH的英文全称是"Structured Exception Handling", 即"结构化异常处理",是windows提供给我们异常
处理机制。
在高级语言中的_try{} _finally{} 和 _try{} _except {} 也就是利用seh机制。只是它们是将本身的s
eh机制进行了包装,其实如果你想,我们汇编语言也可以完全通过宏来进行包装。但是我们既然是程序爱好者
,那么我们就应该对各种底层机制非常了解,所以今天就让我带领大家进入seh的天堂吧!!!!!
引用:
发生异常时系统的处理顺序(by Jeremy Gordon):
1.系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统
挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息.呵呵,这不是正好可以用来探测调试器的存在吗?
2.如果你的程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否安装了线程相关的异常处理例程,
如果你安装了线程相关的异常处理例程,系统就把异常发送给你的程序seh处理例程,交由其处理.
3.每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处
理例程,可交由链起来的其他例程处理.
4.如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知debugger.
5.如果程序未处于被调试状态或者debugger没有能够处理,并且你调用SetUnhandledExceptionFilter安装了
最后异常处理例程的话,系统转向对它的调用.
6.如果你没有安装最后异常处理例程或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个
对话框, 你可以选择关闭或者最后将其附加到调试器上的调试按钮.如果没有调试器能被附加于其上或者调试
器也处理不了,系统就调用ExitProcess终结程序.
7.不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会.
如果你看了上面的步骤一头雾水的话,别着急,化点时间慢慢理解或者进入下一部分实例操作.
Windows下的异常处理有两种方式:
1.finally型
finally类型是你的异常在未得到线程相关处理例程的处理,在操作系统即将结束程序前调用的处理例程,
所以它是与进程相关的而不是和线程相关的。因为无论哪个是哪个线程发生异常未能被处理都会调用这个例程。
实战演练:
format PE GUI 4.0
entry start
include 'win32ax.inc'
SIZE_OF_80387_REGISTERS1 equ 80
MAXIMUM_SUPPORTED_EXTENSION1 equ 512
ExceptionContinueExecution1 equ 0
EXCEPTION_MAXIMUM_PARAMETERS equ 15
struct EXCEPTION_RECORD
ExceptionCode dd ?
ExceptionFlags dd ?
pExceptionRecord dd ?
ExceptionAddress dd ?
NumberParameters dd ?
ExceptionInformation dd EXCEPTION_MAXIMUM_PARAMETERS dup(?)
ends
struct FLOATING_SAVE_AREA1
ControlWord dd ?
StatusWord dd ?
TagWord dd ? ; **
ErrorOffset dd ?
ErrorSelector dd ? ; **
DataOffset dd ?
DataSelector dd ?
RegisterArea db SIZE_OF_80387_REGISTERS1 dup(?)
Cr0NpxState dd ?
ends
struct CONTEXT1
ContextFlags dd ?
iDr0 dd ?
iDr1 dd ?
iDr2 dd ?
iDr3 dd ?
iDr6 dd ?
iDr7 dd ?
FloatSave FLOATING_SAVE_AREA1 <>
reg_Gs dd ? ; gs register
reg_Fs dd ? ; fs register
reg_Es dd ? ; es register
reg_Ds dd ? ; ds register
reg_Edi dd ?
reg_Esi dd ?
reg_Ebx dd ?
reg_Edx dd ?
reg_Ecx dd ?
reg_Eax dd ?
reg_Ebp dd ? ; SEH
reg_Eip dd ? ; SEH
reg_Cs dd ? ; cs register
reg_Flag dd ? ; eflags register
reg_Esp dd ? ; esp register ; SEH
reg_Ss dd ? ; ss register
ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION1 dup(?)
ends
.data
szMsg db '异常发生的位置: %08x, 异常代码: %08x, 标志: %08x', 0
szText db '嘿嘿俺安全了', 0
szCap db 'Finally 异常处理例子', 0
lpOldExcep rd 1
.code
proc Finally_Handler lpExp:DWORD
locals
szBuffer db 200 dup (?)
endl
pushad
mov eax,[lpExp]
virtual at eax
.lpExcept dd ?
.lpContext dd ?
end virtual
mov esi, [.lpExcept]
mov edi, [.lpContext]
callw wsprintf, addr szBuffer, szMsg, [edi+CONTEXT1.reg_Eip],/
[esi+EXCEPTION_RECORD.ExceptionCode], [esi+EXCEPTION_RECORD.ExceptionFlags]
add esp, 14h
mov [edi+CONTEXT1.reg_Eip], _safe
callw MessageBox, NULL, addr szBuffer, szCap, MB_OK
popad
xor eax, eax ;使eax = -1,因为windows 就是根据返回值来决定下一步如何处理的。
dec eax ; eax = -1 表示将恢复之前保存的环境快,然后继续执行
ret ; eax = 1 表示结束这个进程,不弹出对话框
endp ; eax = 0 表示按照默认的异常处理例程去处理,也就是弹出错误对话框,然后结束进程
start:
callw SetUnhandledExceptionFilter, Finally_Handler
mov [lpOldExcep], eax
; Exception code
xor ecx, ecx
mov eax, 200
cdq
div ecx
_safe:
callw MessageBox, NULL, szText, szCap, MB_OK
callw SetUnhandledExceptionFilter, [lpOldExcep]
callw ExitProcess, NULL
.import
library kernel32, 'kernel32.dll',/
user32, 'user32.dll'
include 'api/kernel32.inc'
include 'api/user32.inc'
我们来分析下这段代码:
这句程序首先通过SetUnhandledExceptionFilter函数来设置异常处理例程,这个函数只有一个参数
那就是异常处理例程的地址。 我们的异常处理函数也只有一个参数,这个参数是指向EXCEPTION_POINTERS结构的指针。
struct EXCEPTION_POINTERS
.lpExcept dd ?
.lpContext dd ?
ends
这个结构中的第一个成员是指向EXCEPTION_RECORD结构的指针。 第二个成员是指向CONTEXT结构的指针。
EXCEPTION_RECORD结构保存了异常产生的原因、产生的位置等情况。CONTEXT结构保存了异常产生时刻的运行环境。
首先我们通过 mov eax,[lpExp] 获得我们这个回调函数的参数值。
紧接着我们通过我们fasm的宏 virtual 来建立虚拟数据,这个虚拟数据仅仅包含在源代码中,并不包含在
编译的程序中。fasm也提供了assume,但是我觉得virtual更爽些,(*^__^*) 嘻嘻……
virtual宏的格式:
virtual at 偏移地址
;定义虚拟数据
end virtual
这样我们就可以直接引用这些虚拟数据,而这些虚拟数据的地址就是virtual at后面的地址 + 数据字节大小。因为
eax寄存器中保存的是我们的EXCEPTION_POINTERS结构的指针,所以此时我们就可以通过建立EXCEPTION_POINTERS结构虚拟
数据,然后通过这些数据标号名 我们就可以来引用了,这样给我们的源代码的可读性带来好处,否则我们必须通过raw asm。
很多搞病毒的朋友就喜欢raw asm,它们貌似就喜欢别人看不懂自己的代码,以此为乐,好了不扯了。
然后我们通过
mov esi, [.lpExcept]
mov edi, [.lpContext]
来获得指向 EXCEPTION_RECORD结构的指针,和CONTEXT结构的指针,接下来我们就可以通过+结构中偏移来进行变址寻址
了。
然后我们通过mov [edi+CONTEXT1.reg_Eip], _safe ; [edi+结构偏移],这样通过变址寻址就可以访问结构中各个
成员。
这里通过改写context结构的中的eip寄存器,这样我们等下返回-1,windows就会恢复我们之前的contexe结构,然后去
执行,由于这里我们改写了eip寄存器,所以我们就可以跳过发生异常错误的地址,从而跳到安全的地址中执行。
返回 1 表示我已经处理了异常,可以优雅地结束了
返回 equ 0 表示我不处理,其他人来吧,于是windows调用默认的处理
程序显示一个错误框,并结束
返回 -1 表示错误已经被修复,请从异常发生处继续执行 。你可以试着让程序返回0和-1然后编译程序,就会理解我
所有苍白无力的语言...
;---------------------------------------------------------------------------------------------------------------
SHE
2. Thread Exception Handler, 线程相关的异常处理,通常每个线程的fs段寄存器指向一个TIB结构。该结构如下:
struct NT_TIB
ExceptionList dd ? ;seh 链入口
StackBase dd ? ;堆栈基地址
StackLimit dd ? ;堆栈大小
SubSystemTib dd ?
FiberData dd ?
AribitraryUserPointer dd ?
Self dd ? ; 本nt_tib结构的线性地址
ends
由于fs:[0] 中就是[NT_TIB.ExceptionList], ExceptionList成员的值指向的是EXCEPTION_REGISTRATION。该结构如下:
struct EXCEPTION_REGISTRATION
prev dd ? ;前一个EXCEPTION_REGISTRATION结构的地址
handler dd ? ;异常处理回调函数的地址
ends
第一个成员是之前EXCEPTION_REGISTRATION 结构的地址,handler是我们异常处理例程的地址。。既然是这样的话,
我们来思考,我们只要自己建立一个EXCEPTION_REGISTRATION结构,然后修改TIB结构中的ExceptionList成员为我们
的结构,这样我们的就将关联了此线程的异常处理例程,只要有异常发生,windows就会首先调用线程相关的异常处理
例程,如果失败,或者是不存在线程相关的异常处理例程的话,再去调用进程相关的异常处理例程,也就是上面我们说
的finally型的。
好,因为最常用的是用堆栈来建立,所以我就采用堆栈来建立吧,当然静态内存也是可以的。
例子:
format PE GUI 4.0
entry start
include 'win32ax.inc'
.data
szMsg db 'seh测试', 0
szCap db '线程相关异常处理例程', 0
.code
Thread_Handler:
callw MessageBox, NULL, szMsg, szCap, MB_OK
mov eax, 1 ; eax = 1表示由其他例程进行处理 , eax = 0表示已经修复异常,恢复context继续执行。
ret
start:
push Thread_Handler
push dword [fs:0]
mov dword [fs:0], esp
;Exception Code
xor eax, eax
xchg [eax], eax
callw MessageBeep, MB_OK
callw ExitProcess, 0
.import
library kernel32, 'kernel32.dll',/
user32, 'user32.dll'
include 'api/kernel32.inc'
include 'api/user32.inc'
哈哈,这个够简单吧。由于我只是想让大家先了解下框架,等下我们再来深入的探究。由于我们没有
获得这个回调函数的参数的相信信息,所以我们不能修改相应的寄存器值,所以我们的这个程序运行仅仅是
弹出一个对话框,紧接由系统默认的异常处理例程处理,自然就有那个讨厌的对话框。
注意:这个返回值和finally型可不一样。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、
好像目前而来我们并没有得到很多的好处,除了在异常发生后,执行一点我们那微不足道的代码,事实上seh
可以修复我们的代码并且可以让我们从我们想执行的地方开始执行。好了,我们继续来深入的探析seh机制。
3.深入学习
首先Thread Exception Handler回调函数的参数定义和finally型不同,其函数格式不是stdcall 的形式,
而是c的格式。也就是需要我们自己修正堆栈
这个回调函数有四个参数,但是一般是前3个参数是必须的,最后一个参数没有什么用处。
proc Thread_Handler lpExceptRecord:DWORD, lpSeh:DWORD, lpContext:DWORD, lpDispatcherContext:DWORD
endp
第一个参数lpExceptRecord的是EXCEPTION_RECORD结构, 其实我们在学习finally型的异常处理例程时候已经学习了。
EXCEPTION_RECORD这个结构中包含了我们异常的相关信息,例如异常产生的原因,异常的发生位置等信息。
第二个参数lpSeh指向的注册回调函数时使用的EXCEPTION_REGISTRATION结构的地址。也就是在上面哪个例子中
我们在堆栈中构造的这个结构的地址。
第三个参数lpContext指向的是我们的contexe结构,这里我就不多说了和我们上面的finally型介绍的是一样的。
第四个参数没有什么用,所以这里我就不说了。
由于这个调用例程是的c的约定形式,所以就不能用proc伪指令了。我们直接通过标号,然后通过esp做堆栈指针,
来访问参数。
例子:
format PE GUI 4.0
entry start
include 'win32ax.inc'
SIZE_OF_80387_REGISTERS1 equ 80
MAXIMUM_SUPPORTED_EXTENSION1 equ 512
ExceptionContinueExecution1 equ 0
struct FLOATING_SAVE_AREA1
ControlWord dd ?
StatusWord dd ?
TagWord dd ? ; **
ErrorOffset dd ?
ErrorSelector dd ? ; **
DataOffset dd ?
DataSelector dd ?
RegisterArea db SIZE_OF_80387_REGISTERS1 dup(?)
Cr0NpxState dd ?
ends
struct CONTEXT1
ContextFlags dd ?
iDr0 dd ?
iDr1 dd ?
iDr2 dd ?
iDr3 dd ?
iDr6 dd ?
iDr7 dd ?
FloatSave FLOATING_SAVE_AREA1 <>
reg_Gs dd ? ; gs register
reg_Fs dd ? ; fs register
reg_Es dd ? ; es register
reg_Ds dd ? ; ds register
reg_Edi dd ?
reg_Esi dd ?
reg_Ebx dd ?
reg_Edx dd ?
reg_Ecx dd ?
reg_Eax dd ?
reg_Ebp dd ? ; SEH
reg_Eip dd ? ; SEH
reg_Cs dd ? ; cs register
reg_Flag dd ? ; eflags register
reg_Esp dd ? ; esp register ; SEH
reg_Ss dd ? ; ss register
ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION1 dup(?)
ends
struct SEH
PrevLink dd ? ;the address of the previous seh structure
CurrentHandler dd ? ;the address of the exception handler
SafeOffset dd ? ;The offset where it's safe to continue execution
PrevEsp dd ? ;the old value in esp
PrevEbp dd ? ;The old value in ebp
ends
.data
szMsg db '除法的商是%d', 0
szCap db '异常处理例程', 0
szBuf db 200 dup (?)
.code
Theard_Handler:
virtual at esp+4
.lpExceptRecord dd ?
.lpSeh dd ?
.lpContext dd ?
.lpDisPatcherContext dd ?
end virtual
mov edi, [.lpContext]
mov [edi+CONTEXT1.reg_Ecx], 20
lea eax, [Execute]
mov [edi+CONTEXT1.reg_Eip], Execute ;从偶们指定的地方开始执行
sub eax, eax ;返回0,表示context修复,恢复context执行。
ret 16
start:
push Theard_Handler
push dword [fs:0]
mov dword [fs:0], esp
xor ecx, ecx
mov eax, 200
cdq
Execute:
div ecx
callw wsprintf, szBuf, szMsg, ecx
add esp, 12
callw MessageBox, NULL, szBuf, szCap, MB_OK
callw ExitProcess, NULL
.import
library kernel32, 'kernel32.dll',/
user32, 'user32.dll'
include 'api/kernel32.inc'
include 'api/user32.inc'
这个例子中我们通过在异常处理例程中恢复ecx寄存器的值和eip的值,使程序继续执行,并显示除法的结果。
好,到这里这篇文章已经基本介绍完了,当然本文不可能把seh的文章能全部的介绍完,只能带领大家进入seh的领域,把文章中的代码和分析多多读下,你会发现的更多。。
参考文献: 冷雨飘心的SEH in ASM研究