初探Windows SEH(只是入门级
一些闲话
其实SEH的概念之前已经接触过几次了,第一次是在瞎翻track和含树师傅博客的时候,看到21年的miniL有涉及到这个东西,觉得还是含树师傅的方法(一个一个翻函数)最管用,所以就没太在意这个东西。直到寒假做hgame的题被教育了,后来刷buu逆向的时候做到SCTF2019的creakme又被教育了一通,觉得还是得了解这个东西吧。
怎么说呢,做题毕竟只是做题,可能这道题可以嗯翻函数,另一个题可以嗯怼汇编,即使一点也不了解这个东西也能拿到flag。但是我感觉,不知道原理的话,对一些比较屑的题还是难免无从下手吧,而且明白一个东西之后用起来感觉会更顺手一些。
最初知道寻找SEH相关的一些调用地址是在《逆向工程核心原理》这本书上,然后自己拿题目瞎调了一通。但是调出来的东西,跟书上说的东西,不能说是毫不相干,只能说是八竿子打不着(x,只好去网上找各种博客康。看了很多篇之后慢慢感觉理解了一些,感觉里面还是有挺多很有意思的东西,然后又综合《逆向工程核心原理》和《加密与解密》, 再顺便完成复现题目的任务, 自己写了一篇博客。
不过这篇文章只是一些比较简单的应用,而且只涉及了SEH,也只有静态分析的部分, (因为我太菜了,OD还用不熟练,之后也许会写一篇动调的) 对于VEH、UEH、VCH等异常处理还没有研究过,SEH栈展开之类的高级内容也许以后学了会再写一篇(逃
本文面向的读者
- 想要了解SEH的逆向er(不会读汇编也没关系,涉及到的知识和相关操作我会作简要说明)
- 吃瓜群众(因为我觉得会C语言应该就能看懂嘿嘿嘿)
初次接触很多新的概念是会感觉陌生和难以理解,翻来覆去多看几遍,对照着题目练习,慢慢就能接受了。
用到的示例题目
参考资料
winnt.h文档
https://bbs.pediy.com/thread-32222.htm
https://www.cnblogs.com/lanrenxinxin/p/4631836.html
https://bbs.pediy.com/thread-206603.htm
《逆向工程核心原理》第46~48章
《加密与解密》第8章
SEH简要说明
使用方法
SEH是Windows提供的一种异常处理机制,在源代码中使用__try
、__except
、__finally
关键字来实现。简而言之,就是当程序(__try
块中的语句)发生异常,我们可以选择使用自己定义的函数来处理异常(在__except
块中调用),而不是使用操作系统提供的异常(弹一个报错窗口然后退出程序)来处理。
SEH与逆向
静态分析
异常处理对于逆向来说最大的问题就是 __except
块中的内容不会被IDA反编译出来 ,可以据此隐藏核心代码。所以当解密过程很明确,但就是死活解出来一堆乱码,或者反编译出来的代码逻辑很迷的时候,就要考虑是不是用一些特殊的方法隐藏了核心代码……
具体而言,我寒假做hgame的时候被week2的fakeshell和creakme2折磨N久,一个是RC4,一个是xtea,都有很明确的解密流程,但解出来就是一堆乱码……我甚至尝试调试后把乱码数据写到栈上,然后用题目程序加密。结果当然是跟密文一样,所以某人直接自闭了……
后来看题解才知道,fakeshell是使用__attribute((constructor))
隐藏了一段代码逻辑,而creakme2是使用SEH隐藏了一段代码逻辑……__attribute((constructor))
是用于初始化的一段函数,在main函数之前执行,反编译结果在是在_libc_start_main
中的init
参数里。所以现在养成习惯,看main函数之前先翻一下init
段……
动态调试
进程在运行过程中发生异常时,操作系统会委托进程自身处理。如果进程自身有相关的异常处理(比如SEH),那么就由程序自身处理,否则OS启动默认的异常处理机制,终止程序,也就是上面说到的过程。
而当程序处于调试状态时,调试者拥有被调试者的所有权限(读写内存、寄存器等),所以 调试过程中的任何异常都要先交由调试者处理,而不会流转到正常的异常处理过程。 这样就增加了调试的难度。
调试遇到异常时,如果需要进入异常,一般的处理方法:
- 手动找到
__except
块,修改EIP值指向正常的异常处理过程。
在OD中使用New Origin here(Ctrl+Gray *)改变程序运行路径。 - 在OD中使用shift+f7/f8/f9直接将异常抛还给被调试者
如果无需进入异常,直接nop掉产生异常的代码即可,或者根据异常代码处理相应的内存和寄存器值也可以。
SEH简要分析
SEH具体实现
SEH异常处理程序的基本结构:
1 | __try { |
__try
__try
块中包含可能触发异常的代码。如果代码抛出异常,则交由__except
块处理。
__except
__except
块中是用户定义的处理异常的代码。
exception filter
exception filter称为异常过滤器。顾名思义,它的作用是对异常进行过滤。
异常过滤器只有三个值(定义在Windows的Excpt.h中):
- EXCEPTION_CONTINUE_EXECUTION(-1)
在发生异常的地方继续执行。 - EXCEPTION_CONTINUE_SEARCH (0)
异常无法识别。继续搜索下一个处理程序。 - EXCEPTION_EXECUTE_HANDLER (1)
异常被识别。通过执行控制转移到异常处理程序__except
复合语句,然后继续执行__except
块。
异常过滤器决定了是否处理当前异常,即是否执行__except
块中的代码(异常处理程序exception handler)。
异常过滤器的使用:
1 | __try { |
对于这段代码而言,在异常过滤器中自定义了一个函数MyFilter
,以GetExceptionCode()
的返回值作为参数,返回值是一个异常过滤器的值,所以也可以直接在__except
块的参数中写入异常过滤器的值,如__except (EXCEPTION_EXECUTE_HANDLER)
。
具体而言,GetExceptionCode()
函数返回__try
块中产生的异常值(也就是产生异常的原因),据此我们可以实现对异常的过滤。
下表列举操作系统中的异常值:
1 | EXCEPTION_ACCESS_VIOLATION 0xC0000005 |
举例来讲,如果__try
块中产生整数除零异常,那么GetExceptionCode()
函数返回0xC0000094。自定义过滤器中将这个值与预先设定好的异常值比较。在此例中,这个异常值是EXCEPTION_ACCESS_VIOLATION,即0xC0000005。如果异常值相等,那么就返回EXCEPTION_EXECUTE_HANDLER,进而执行exception handler,也就是使用当前__except
块处理异常,否则就返回EXCEPTION_CONTINUE_SEARCH,即继续搜索下一个异常处理程序。
据此,我们就使用自定义的MyFilter
完成了对异常的过滤:只有__try
块中产生读写不可访问地址异常时,__except
块才会处理该异常。也就是说,除了EXCEPTION_ACCESS_VIOLATION这个异常,__except
都不会处理。
为了便于理解以上概念,以hgame的creakme2为例说明。
hgame creakme2
这道题是一个xtea的加密,关于这种加密方式不是本文重点,我不细说,主要看SEH的部分。
IDA反编译出来的核心加密函数如下:
1 | __int64 __fastcall sub_140001070(unsigned int a1, unsigned int *a2, __int64 a3) |
正常按部就班用xtea的解密方式解密出来是一堆乱码,我们有理由怀疑此函数隐藏了一些代码逻辑。去翻一下这个函数的汇编,会发现以下代码:
1 | .text:0000000140001112 loc_140001112: ; DATA XREF: .rdata:00000001400027C4↓o |
140001112处有两个__try
块,分别属于loc_140001150和loc_140001141。
我们先去看最内层,双击loc_140001141,IDA跳转到相应地址,代码如下:
1 | .text:0000000140001141 loc_140001141: ; DATA XREF: .rdata:00000001400027C4↓o |
这里的代码即__except
块中处理异常的代码,也就是上面__except
大括号中省略号的内容。
具体而言,这段代码的作用是每次对xtea加密中的sum值(v4)异或0x1234567。
那么这个异常是如何触发的?双击__except(loc_140001DF6)
中的地址loc_140001DF6,按照上文所提及的,这里就是__except
块的异常过滤器。
1 | .text:0000000140001DF6 ; __except filter // owned by 140001112 |
可以看到,IDA标注这里是__except filter
的内容,不过实际上,这些是为了传递一些异常信息,真正的filter函数在sub_140001058中,也就是这段代码里所调用的一个函数。
跳转到这个函数:
1 | .text:0000000140001058 sub_140001058 proc near ; CODE XREF: sub_140001070+DA4↓p |
如果反编译一下,发现是retn了奇怪的值,即这里的0xC0000094。对照上文给出的操作系统异常值的表,可以发现这是一个整数除零异常,即EXCEPTION_INT_DIVIDE_BY_ZERO。
不过这样手动去对照异常找比较麻烦,我们可以使用IDA来识别相应的异常,操作如下:
选中这个值然后按M,可以选择枚举常量(IDA可能会弹窗,选yes即可),然后可以发现IDA会搜索到异常值。
选择正确的异常值(在我这里是第一个)即可,IDA会将数字转为相应的宏,如下:
1 | .text:0000000140001058 sub_140001058 proc near ; CODE XREF: sub_140001070+DA4↓p |
从名字可以看出来,这是一个整数除零异常。外层的loc_140001150处的异常处理分析过程一致,不再赘述,不过外层处理的异常值是0xC0000095,即EXCEPTION_INT_OVERFLOW。这个异常处理中__except
块中的内容是每次将0x9E3779B1赋给xtea加密中的sum值(v4)。相应的汇编代码如下:
1 | .text:0000000140001150 loc_140001150: ; DATA XREF: .rdata:00000001400027D4↓o |
分析清楚了两个__except
块中的内容,现在我们返回去看__try
中的内容。相应代码上面已经贴了,大概意思就是在v4每次加上delta(即dword_140003034)后,计算1/(v4>>31)
。这里使用一个变量v4>>31
作为分母,当v4首位为0时,就会触发除零异常,执行__except
块中的代码,对v4异或0x1234567,以此来魔改加密过程。
外层的EXCEPTION_INT_OVERFLOW并没有触发,只是出题人用来迷惑选手的。
出题人在wp中提供的题目源代码如下:
1 | int FilterFuncofDBZ(int dwExceptionCode) |
如果有难以理解的地方,可以对照源码分析。
SEH深入分析
SEH链
SEH以链的形式存在。第一个异常处理中未处理相关异常,它就会被传递到下一个异常处理器,直到得到处理。
SEH链是由_EXCEPTION_REGISTRATION_RECORD
结构体组成的链表,此结构体定义如下:
1 | typedef struct _EXCEPTION_REGISTRATION_RECORD { |
如果你有一丢丢数据结构知识,你会发现这是一个最简单的链表结构:一个是元素值,一个是下一个元素的地址。而这里的元素值Handler就存储了当前节点的异常处理函数的地址,Next则指向下一个节点。若Next成员的值为FFFFFFFF,则表示它是链表最后一个结点。
异常处理函数
异常处理函数定义如下:
1 | EXCEPTION_DISPOSITION __cdecl _except_handler |
异常处理函数接受4个参数输入,这4个参数保存着一些与异常相关的信息。
第2个参数EXCEPTION_REGISTRATION_RECORD即是上文提到过的SEH链结构体,第4个参数是OS保留,可以忽略,下面会分析第1和第3个参数。
异常处理函数返回名为EXCEPTION_DISPOSITION的枚举类型,它由系统调用,是一个回调函数。
EXCEPTION_RECORD
异常处理函数接受的第一个参数是一个指向EXCEPTION_RECORD结构体的指针。
EXCEPTION_RECORD定义如下:
1 | typedef struct _EXCEPTION_RECORD { |
ExceptionCode指出异常类型,即上文提到过的一些对应的异常值。
ExceptionAddress表示发生异常的代码地址。
CONTEXT
异常处理函数接受的第三个参数是指向CONTEXT结构体的指针,它用来备份CPU的值。
多线程环境下,每个线程内部都有一个CONTEXT结构体。CPU离开当前线程转而运行其他线程时,CPU寄存器的值被保存到当前线程的CONTEXT结构体中,当CPU返回该线程时,使用保存在CONTEXT结构体中的值来覆盖CPU各寄存器的值,以此来保证多线程安全。
CONTEXT结构体的定义如下:
1 | typedef struct _CONTEXT { |
这里只写了一部分重要的,微软的文档有更新,我还是按照《逆向工程核心原理》这本书上的定义来写,想看最新的定义可以戳这里
注意里面的Eip成员(偏移为0xb8),一般来讲Eip成员应该存储触发异常后的代码地址,即触发异常时的Eip值。
具体而言,当某一句代码触发异常,那么Eip的值应该指向这句代码的结束地址。这样当SEH处理完毕异常后,程序可以回到原来的地方,继续执行正常的代码。
但是, 在异常处理函数中可能将参数传递过来的CONTEXT.Eip设置为其他地址,然后返回处理函数。 这样之前暂停的线程会执行新的EIP地址处的代码(反调试中经常使用这个技术)。
EXCEPTION_DISPOSITION
异常处理函数的返回值是一个EXCEPTION_DISPOSITION的枚举类型,定义如下:
1 | typedef enum _EXCEPTION_DISPOSITION |
结构化异常处理内部函数
了解上述参数的意义和结构之后,如何在源代码(C++)中访问?
结构化异常处理提供了两个可用于 try-except
语句的内部函数:
GetExceptionCode
返回 (一个32位整数) 的代码,也就是上文提到过的异常原因相对应的异常值。GetExceptionInformation
返回一个指向 EXCEPTION_POINTERS 结构的指针,该结构包含有关异常的其他信息。
EXCEPTION_POINTERS结构体定义如下:
1 | typedef struct _EXCEPTION_POINTERS { |
其中ExceptionRecord是一个指向EXCEPTION_RECORD的指针。
ContextRecord是一个指向CONTEXT的指针。
同样,用miniL2021的题目0oooops来帮助理解以上概念。
miniL2021 0oooops
反编译主函数的结果:
1 | int __cdecl main_0(int argc, const char **argv, const char **envp) |
如果是在IDA里看,可以看到MEMORY[0] = 0;
被标着显眼的大红色,这其实是一个很强的提示:即程序中触发了非法内存访问的异常(MEMORY[0]一定是一个非法的地址)。出题人好温柔我真的哭死
那么开同步去看这句代码的汇编部分,可以看到显眼的__try
块:
1 | .text:00412330 loc_412330: ; CODE XREF: _main_0+15C↑j |
分析步骤在上一题中已经详细说过,此处不再赘述。
跟进loc_412377后再跟进loc_412356的__except filter
,然后跟进sub_411131
,此函数经过一次跳转到sub_411DD0
。反编译结果如下:
1 | int __cdecl sub_411DD0(int a1, int a2) |
可以看到a2也是一个奇怪的值,按照上文说过的步骤去转成枚举类型,发现这个异常类型是EXCEPTION_INT_DIVIDE_BY_ZERO,即整数除零异常。那么再去看刚开始的__try
块中的内容,发现最后两句mov edx, 0
和div edx
触发了除零异常,那么我们有理由相信这个异常中的函数确实会被调用。
观察这段加密函数,会发现对明文的运算除了一些常规的异或和减法之外,还异或了(unsigned __int8)*(_DWORD *)(*(_DWORD *)(a2 + 4) + 184)
这个值。
这个操作就是把a2转成DWORD类型后,取偏移为184的地址的值。184的十六进制是0xb8,如果对照上面说过的CONTEXT结构体后面的注释,会发现这个值实际上是Eip的值,即触发异常代码的下一句代码的地址值,取它的最后一字节来异或。
那么现在再回去看try块,try块结束的地址是0x41234B,所以这个用来异或的值就是0x4b。
或者也可以根据程序逻辑推知这一点:
在验证失败后,执行的代码是*(_DWORD *)(*(_DWORD *)(a2 + 4) + 184) += 54;
然后退出程序;验证成功后,执行的代码是*(_DWORD *)(*(_DWORD *)(a2 + 4) + 184) += 63;
然后退出程序。
上文在CONTEXT里已经强调过,可以通过修改CONTEXT的Eip来控制程序返回的地址。我们有理由相信这两个操作是为了转向不同的结果:即为我们打印失败或成功的信息。所以这里应当修改的是Eip的值,也就是控制程序返回不同的地址。有兴趣可以自行验证这一点。
当然,这样手动找或者推理很麻烦,跟上一题一样,我们还是可以借助IDA来完成这些工作。
**(_DWORD **)a2
我们已经知道是EXCEPTION_INT_DIVIDE_BY_ZERO,即_EXCEPTION_RECORD中的成员ExceptionCode。
上文已经提及过,结构化异常处理内部函数只有GetExceptionCode
和GetExceptionInformation
。代码中对a2解引用两次才得到ExceptionCode,并且此后有对a2的偏移值有一些操作,所以我们可以推知代码访问使用的函数应该是GetExceptionInformation
,而a2则是此函数的返回值类型。
具体而言,站在异常的角度来看,a2的数据类型应该是_EXCEPTION_POINTERS *
类型的。
在IDA中选中a2,摁Y修改数据类型,输入_EXCEPTION_POINTERS *a2
,会发现反编译的代码变成下面这样:
1 | int __cdecl sub_411DD0(int a1, _EXCEPTION_POINTERS *a2) |
显然,IDA已经帮我们识别出来了这个值是ContextRecord结构体中的Eip值,我们只需要去看try块的结束地址就能获取到这个值。
也许你会突然想起来,我们最开始找到try块的那个提示非法内存访问的异常呢?上面说过的只是除零异常触发的函数,那么非法内存访问异常有没有触发函数?
那当然是有的。上面那个函数里只是奇数处字符的加密函数,偶数处字符的加密函数在TlsCallback_0_0里,可以在IDA的导出表(exports)窗口中找到这个函数,非法内存访问异常触发的函数就在这里。
TLS,Thread Local Storage 线程局部存储,TLS回调函数的调用运行要先于PE代码执行,该特性使它可以作为一种反调试技术使用。TLS是各线程的独立的数据存储空间,使用TLS技术可在线程内部独立使用或修改进程的全局数据或静态数据。
这里涉及到Windows异常处理的另一种机制VEH,由于这个不是本文的重点 (其实是我太菜了不会) ,所以先不细说。对于这个题而言,点进这个函数直接分析即可。
track神的博客有讲,感兴趣可以去康康。
两道题目的比较
最后强调一下这两个题目的一点细微差异:
如果读者还记得我最开始在静态分析中说过的,那么应该知道我强调过一点:__except
块中的代码(也就是异常处理程序except handler)中的内容是不能被IDA反编译出来的。
在hgame那道题中,我们是通过阅读汇编代码获得的程序逻辑,但在miniL这道题中,我们是看反编译出来的结果,为什么会这样?(能够理解这一点的可以跳过这里)
再来回顾一下__except
块中的内容:
1 | __except ( /*异常过滤器exception filter*/ ) { |
注意__except
块中有两个重要的成员:
- 异常过滤器exception filter
- 异常处理程序exception handler
hgame的creakme2将核心代码写入exception handler,也就是__except
块大括号包裹的内容,这里的东西是不能够被IDA反编译出来的,所以我们只能通过阅读汇编获得程序逻辑。
而miniL的题目则是将核心代码写入exception filter。由于异常过滤器实际上也是一个函数,所以能够被IDA识别并且反编译。
简而言之,如果出题人想考验选手阅读汇编代码的能力,那么就将代码直接写在exception handler中。如果出题人不想为难选手嗯怼汇编,就把代码写入exception filter函数中,或者在exception handler中调用一个写入核心加密过程的函数(这种方法我们将在下一道题中看到)。
TEB
下面要介绍VC++编译器对SEH所做的增强版本,在这之前,先说明一些关于TEB(Thread Environment Block,线程环境块)的知识。在这里只讲解与SEH相关的内容。
TEB成员众多,此处我们只需要了解_NT_TIB。
_NT_TIB
TIB(Thread Information Block,线程信息块)
_NT_TIB结构体定义如下:
1 | typedef struct _NT_TIB{ |
其中ExceptionList成员指向_EXCEPTION_REGISTRATION_RECORD结构体组成的链表,也就是SEH链。
TEB访问方法
Ntdll.NtCurrentTeb()
用户模式下使用此函数访问TEB,Ntdll.NtCurrentTeb()
返回当前线程的TEB结构体的地址。
FS段寄存器
FS:[0]
指向SEH起始地址。
VC++编译器级的SEH实现
前述的SEH只是一个简单的模型,只有一个异常链表,这种机制存在很多问题。
所以现实程序设计中,大部分采用在VC++6.0中对这个体系进行扩展增强的SEH版本。
编译器提供的增强版本的结构体:
1 | struct _EXCEPTION_REGISTRATION |
原先的结构体:
1 | typedef struct _EXCEPTION_REGISTRATION_RECORD { |
既然是扩展,那么必然保留原来的结构,在此基础上增加一部分内容。进行对比可以发现,前两个结构保留,而最后三个结构是追加的。
注释中已经说明,ebp保存函数栈帧;trylevel是一个数组索引,指向scopetable数组。
_SCOPETABLE
1 | typedef struct _SCOPETABLE |
扩展异常处理机制
按照原始的设计,每一个__try/__except(__finally)
都应该对应一个 EXCEPTION_REGISTRATION。
但是VS实际实现中,每个使用 __try/__except(__finally)
的函数,不管其内部嵌套或反复使用多少 __try/__except(__finally)
,都只注册一遍,即只将一个 EXCEPTION_REGISTRATION 挂入当前线程的异常链表中。
MSC 提供一个处理函数,即 EXCEPTION_REGISTRATION::handler 被设置为 MSC 的某个函数,而不是我们自己提供的 __except
代码块,我们自己提供的多个 __except
块被存储在 EXCEPTION_REGISTRATION::scopetable 数组中。
乍看这一段话可能会觉得难以理解,我们以SCTF2019的creakme为例进行说明。
SCTF2019 creakme
主函数太长就不贴出来了,程序的大致流程是在第一个函数中触发一个断点异常,然后在except handler中调用一个用于SMC的函数,在第二个函数中调用这个经过SMC的函数对密文进行一些变换。后来就是对输入进行AES加密,与经过变换的密文比较。我们只说SEH的部分。
查看第一个函数sub_402320
的汇编代码(只贴了需要分析的核心内容):
1 | .text:00402320 sub_402320 proc near ; CODE XREF: _main+35↓p |
对照上文提到过的增强版的SEH结构体,我们可以发现入栈的参数是一一对应的:
入栈的参数是ebp、0xFFFFFFFE、stru_407B58、__except_handler4、large fs:0,分别对应_EXCEPTION_REGISTRATION中的_ebp、trylevel、scopetable、handler和prev(前面介绍TEB时已经说过,FS:[0]
指向SEH起始地址)。
那么按照scopetable的定义,这个结构体中存储了lpfnFilter(当前try块的过滤函数)和lpfnHandler(当前try块的Handler)。双击stru_407B58,查看这个结构体:
1 | .rdata:00407B58 stru_407B58 dd 0FFFFFFE4h ; GSCookieOffset |
IDA已经为我们注释出来了,loc_4023DC是FilterFunc,loc_4023EF是HandlerFunc。
先查看FilterFunc:
1 | .text:004023DC loc_4023DC: ; DATA XREF: .rdata:stru_407B58↓o |
触发异常的值是0x80000003,使用之前的方法转为枚举类型时,IDA没有搜索出来这个异常类型,但是没有关系,我们可以手动对照一下异常代码表,发现这是一个EXCEPTION_BREAKPOINT,即断点异常。
现在回过头去看sub_402320反编译出来的伪代码:
1 | void __thiscall sub_402320(_DWORD *this) |
可以发现,DebugBreak();
可以触发断点异常。看其他师傅的博客说这个是通过调试器附加的手段来触发断点异常的 这个我太菜了不会,所以暂且不谈。
总之结果就是通过这个函数触发了此处的异常。
那么我们返回去看loc_4023EF处的HandlerFunc:
1 | .text:004023EF loc_4023EF: ; DATA XREF: .rdata:stru_407B58↓o |
前面写了一堆反调(CheckRemoteDebuggerPresent
、IsDebuggerPresent
),后面调用了一个函数sub_402450
,跟进这个函数看看,F5反编译出来伪代码,可以发现就是用于SMC的代码。
这道题目的SEH分析过程到此结束,但是上面还有一个问题:MSC提供唯一一个handler处理函数到底是怎么回事?
现在再回去看sub_402320
的汇编代码,入栈的第四个参数__except_handler4,之前说过,对应_EXCEPTION_REGISTRATION中的handler,这个函数就是MSC提供的唯一一个EXCEPTION_REGISTRATION::handler处理函数。
我们返回sub_402320
,双击__except_handler4,查看汇编代码:
1 | .text:004033DE SEH_402320: |
既然这是每一个异常的EXCEPTION_REGISTRATION::handler都会被设置的函数,那么通过这个函数,我们应该可以找到程序中的所有__except
块。
选中SEH_402320
,摁X查看交叉引用,可以发现在所有向上引用中,除了我们上文提到过的sub_402320
,还有一个sub_4024A0
也引用了这个函数。
在sub_4024A0
中分析except块,过程与上面一致,此处不再说明。阅读汇编会发现,sub_4024A0
的except实际上什么也没干。对照源码(上面已经给出链接),我们可以证实这一点。
完结撒花!(bushi
第一次写这种文章,如果有错误欢迎指出~
关于本文
本文作者 云之君, 许可由 CC BY-NC 4.0.