一篇文章带你了解SEH技术

前言:

      SEH(“Structured Exception Handling”),即结构化异常处理·是(windows)操作系统提供给程序设计者的强有力的处理程序错误或异常的武器。它不依托于设计语言,只依托于操作系统,这些并不是编译程序本身所固有的,本质上只不过是对windows内在提供的结构化异常处理的包装。
参看资料:
1.逆向工程核心原理
2.https://www.52pojie.cn/thread-16609-1-1.html
3.https://bbs.pediy.com/thread-27224.html
4.https://bbs.pediy.com/thread-12449.htm
5.https://bbs.pediy.com/thread-189193.htm

第一部分:windows系统的几种常见的异常

  • 异常:
    • 1)内存非法访问异常(Exception_Access_Violation)
      • 1.1) MOV DS:[0],123 —0号区域未被分配
      • 1.2) MOV DS:[40100],123 —.test节区起始地址只有读取权限
      • 1.3)MOV DS:[80000],123 —内核区域
      • 1.4)说明:0号地址虽然属于用户区域但是,该地址没有被分配。
    • 2)断点异常(Exception_BreakPoint)
      • 2.1)软件断点 —int 3
      • 2.2)硬件断点 —对某一个地址下断点
    • 3)CPU无法解析异常(Exception_Illegal_Instruction)
      • 3.1)某一个指令集不在cpu解析的指令之中
    • 4)除0异常
    • 5)单步异常

第二部分:SEH介绍

1.SEH链:SEH是以链状形式存在,如果异常没有被第一个异常处理函数处理,那么就会传递给下一个异常处理函数。

1
2
3
4
5
typedef struct _Exception_SEH_List
{
PException_SEH *next; //*next指针指向下一个节点,
PException_DISPOSITION handle; //handle指向一个异常处理函数。
}_Exception_SEH_List,*_Exception_SEH_List;

*next指针指向下一个节点,handle指向一个异常处理函数。

2.异常处理函数:异常处理函数是一个回调函数,接收4个参数,返回值是一个枚举类型。

  • 四个参数:
    • 1)Exception_Recode *pRecode:该参数是一个指向_Exception_Recode结构体的指针。该结构体包含关于异常的类型和地址等重要信息。
    • 2)Exception_Register_Recode *pFrame:不重要
    • 3)CONTEXT *pContext:对于IA-32 来说有效,保存CPU处理异常前的状态。
    • 4)PVOID *pViod:保留应该为1
  • 1个返回值:处理成功返回0,否则返回1

3.SEH的入口地址:在TEB.NtTib.ExceptionList,对应的段寄存器地址是FS:[0].

第三部分:SEH反调试基础:

1.首先利用异常处理例程来进行反跟踪
      很多CM都是首先安装好一个异常处理例程,然后故意制造一个异常,然后程序抛出异常.ollydebug下用shift + F7 或者 shift + F8进ntdll.KiUserExceptionDispatcher,单步跟踪后最后系统调用用户模块中的异常处理例程. 很多CM都是在异常处理例程编写一个算法来重新将EIP定位到一个会造成异常的指令地址,重复这个过程几次,这样给调试者一种很难跟踪的假象,这种一般只要用 shift + F9 (OllyDebug:该组合键是将异常交给用户程序的异常处理例程来处理),如果我们想弄清楚异常处理例程到底在做什么,我们可以在异常处理例程下个断点来查看其实现过程。

2.未处理异常用于反跟踪的原理:
      根据MSDN的描述,UnhandledExceptionFilter在没有debugger attach的时候才会被调用。所以,SetUnhandledExceptionFilter函数还有一个妙用,就是让某些敏感代码避开debugger的追踪。比如你想把一些代码保护起来,避免调试器的追踪,可以采用的方法:把代码放到SetUnhandledExceptionFilter设定的函数里面。通过人为触发一个unhandled exception来执行。由于设定的UnhandledExceptionFilter函数只有在调试器没有加载的时候才会被系统调用,这里巧妙地使用了系统的这个功能来保护代码.

第四部分:绕过SEH反调试:

      对于第一种,只需要在IsPresnetDebug()返回时修改判断即可。或者直接修改FS:[18]中的debuged成员为“0”。

      对于第二种,因为UnhandledExceptionFilter是否调用取决于系统内核的判断。用户态的调试器要想改变这个行为,要破费一番脑筋了。

2018年1月25日夜于汉阴

第五部分:利用SEH保护验证码

1.      首先,我们看到这个一个MSAM汇编编译而成的程序,如图所示,程序对输入的用户名长度做出验证,正确长度是4-30个字节,

2.      如果正确,则跳入SEH安装程序,输入注册码。输入完毕后,给出一个int 3软件异常,跳入SEH处理函数。

3.      这个SEH处理函数,是一个验证注册码正确的的函数,关于验证过程,本文不做介绍。

4.      快速到达SEH处理函数的方法,如图所是,栈顶是下一个处理函数的指针,下面红圈所是个这次处理函数的指针。

5.备注,测试用例:
https://pan.baidu.com/s/1htLY6ag

第六部分:利用SetUnhandledExceptionFilter和UnhandledExceptionFileter函数实现反调试

1.      异常时,系统处理异常的顺序

  • 发生异常时系统的处理顺序(by Jeremy Gordon, Hume):
    • 1.系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统 挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息.
    • 2.如果你的程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否安装了线程相关的异常处理例程,如果 你安装了线程相关的异常处理例程,系统就把异常发送给你的程序seh处理例程,交由其处理.
    • 3.每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处理例程, 可交由链起来的其他例程处理.
    • 4.如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知debugger.
    • 5.如果程序未处于被调试状态或者debugger没有能够处理,并且你调用SetUnhandledExceptionFilter安装了最后异 常处理例程的话,系统转向对它的调用.
    • 6.如果你没有安装最后异常处理例程或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框, 你可以选择关闭或者最后将其附加到调试器上的调试按钮.如果没有调试器能被附加于其上或者调试器也处理不了,系统 就调用ExitProcess终结程序.
    • 7.不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会.
    • 8.UnhandledExceptionFileter是系统最好一个异常 处理器(就是程序崩溃时候,出现的windows的提示框)。

2.      本例样本网络上找不到了,截取前辈的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
77C01269 > 55 push ebp////以下用F8走。
77C0126A 8BEC mov ebp,esp
77C0126C 51 push ecx
77C0126D 53 push ebx
77C0126E 56 push esi
77C0126F 57 push edi
77C01270 E8 DD680000 call 77C07B52 ; 77C07B52
77C01275 8B7D 08 mov edi,dword ptr ss:[ebp+8]
77C01278 8BF0 mov esi,eax
77C0127A 8B56 54 mov edx,dword ptr ds:[esi+54]
77C0127D A1 94A7C277 mov eax,dword ptr ds:[77C2A794]
77C01282 8BCA mov ecx,edx
77C01284 3939 cmp dword ptr ds:[ecx],edi
77C01286 74 0D je short 77C01295 ; 77C01295
77C01288 8D1C40 lea ebx,dword ptr ds:[eax+eax*2]
77C0128B 83C1 0C add ecx,0C
77C0128E 8D1C9A lea ebx,dword ptr ds:[edx+ebx*4]
77C01291 3BCB cmp ecx,ebx
77C01293 ^ 72 EF jb short 77C01284 ; 77C01284
77C01295 8D0440 lea eax,dword ptr ds:[eax+eax*2]////F4下来。
77C01298 8D0482 lea eax,dword ptr ds:[edx+eax*4]
77C0129B 3BC8 cmp ecx,eax
77C0129D 73 04 jnb short 77C012A3 ; 77C012A3
77C0129F 3939 cmp dword ptr ds:[ecx],edi
77C012A1 74 02 je short 77C012A5 ; 77C012A5
77C012A3 33C9 xor ecx,ecx
77C012A5 85C9 test ecx,ecx
77C012A7 0F84 12010000 je 77C013BF ////这里跳!
77C012AD 8B59 08 mov ebx,dword ptr ds:[ecx+8]
77C012B0 85DB test ebx,ebx
77C012B2 895D 08 mov dword ptr ss:[ebp+8],ebx
77C012B5 0F84 04010000 je 77C013BF ; 77C013BF
77C012BB 83FB 05 cmp ebx,5
77C012BE 75 0C jnz short 77C012CC ; 77C012CC

跳到:

1
2
3
4
5
6
7
77C013BF FF75 0C push dword ptr ss:[ebp+C]////跳到这里。
77C013C2 FF15 C011BE77 call dword ptr ds:[77BE11C0] ; kernel32.UnhandledExceptionFilter
77C013C8 5F pop edi
77C013C9 5E pop esi
77C013CA 5B pop ebx
77C013CB C9 leave
77C013CC C3 retn

跳到77C013BF后就可以发现开始调用UnhandledExceptionFilter了!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
77E730C0 > 68 00050000 push 500
77E730C5 68 F852E777 push 77E752F8
77E730CA E8 0972FEFF call 77E5A2D8 ; 77E5A2D8
77E730CF C745 E4 0400000>mov dword ptr ss:[ebp-1C],4
77E730D6 8B7D 08 mov edi,dword ptr ss:[ebp+8]
77E730D9 8B07 mov eax,dword ptr ds:[edi]
77E730DB BB 050000C0 mov ebx,C0000005
77E730E0 33F6 xor esi,esi
77E730E2 3918 cmp dword ptr ds:[eax],ebx
77E730E4 75 09 jnz short 77E730EF ; 77E730EF
77E730E6 3970 14 cmp dword ptr ds:[eax+14],esi
77E730E9 0F85 77DC0000 jnz 77E80D66 ; 77E80D66
77E730EF 8975 E0 mov dword ptr ss:[ebp-20],esi
77E730F2 56 push esi
77E730F3 6A 04 push 4
77E730F5 8D45 E0 lea eax,dword ptr ss:[ebp-20]
77E730F8 50 push eax
77E730F9 6A 07 push 7
77E730FB E8 B9B5FEFF call 77E5E6B9////调用GetCurrentProcess,返回hProcess。
77E73100 50 push eax
77E73101 FF15 AC10E477 call dword ptr ds:[77E410AC]////调用ntdll.ZwQueryInformationProcess。晕,调用好多次!
77E73107 85C0 test eax,eax////EAX=0则表示成功,否则失败。
77E73109 7C 09 jl short 77E73114
77E7310B 3975 E0 cmp dword ptr ss:[ebp-20],esi////[ebp-20]=-1,则表示有调试器,=0表示没调试器。
77E7310E 0F85 C5060000 jnz 77E737D9////这里不能跳,跳则表示有调试器。干脆NOP掉!
77E73114 A1 B473EB77 mov eax,dword ptr ds:[77EB73B4]
77E73119 3BC6 cmp eax,esi
77E7311B 74 15 je short 77E73132
77E7311D 57 push edi
77E7311E FFD0 call eax////F7进入!
77E73120 83F8 01 cmp eax,1
77E73123 0F84 E9030000 je 77E73512 ; 77E73512

3.      总结:在第四部分中我说了,对于第二种方法,属于内核级调试,不容易绕过,但是这个例子给了我们一个思路:由于反调试代码UnhandledExceptionFilter函数内部,我们可以跟进函数内部,把里面的判断给误导了,就可以顺利通过反调试。

2018年1月26日夜于汉阴