Loading... > 本篇是《Hacker Disassembling Uncovered》的笔记。由于这本书出的时间比较长了,我看的版本还是08年出的,里面有很多东西没什么学习的必要,不过多读点书还是好的吧。在这里记个备忘。 ## 第13章 ### 关于X86-64体系结构 寄存器方面,额外提供了8个基础寄存器,除了原来的8个寄存器外,现在就有16个GPR了。新的寄存器的编号是从R8到R15,要访问他们的低8位,低16位和低32位可以分别使用b、w和d后缀。应用编程寄存器还包括128位的媒体控制寄存器。 关于寻址,在x64中不存在绝对寻址。 关于传参。使用fastcall调用约定,即第一个参数(最左边)用ecx,第二个用edx,第三个用r8,第四个用r9。若参数数量大于5,则使用压栈的方式。这里说的应该是windows的,linux64中的传参方式用了6个寄存器,从左到右依次是edi,esi,edx,ecx,r8,r9。 ## 第17章 首先复习一下TEB、PEB和fs寄存器。 ### TEB 线程环境块,位于用户地址空间,在比 PEB 所在地址低的地方。包含进程中运行线程的各种信息,每个线程都对应一个TEB结构体。一个进程的所有TEB都以堆栈的方式,存放在线性内存中。在用户态可通过CPU的FS寄存器来访问该段,因为FS寄存器在用户态的时候总是指向当前线程的TEB,一般存储在[FS:0]。还可以通过OS提供的API `Ntdll.NtCurrentTeb()`来访问TEB的地址。该函数原理如下: ```sh mov eax, DWORD PTR FS:[18] RETN ``` 关于TEB中的每一项的内容,我这里只介绍常用的部分。 #### +0x000 NtTib TEB的结构体的第一个成员NtTib即为我们常说的TIB线程信息块。其中0x00位置指向 `\_EXCEPTION_REGISTRATION_RECORD` 结构体的链表指针(SEH)。0x18位置为_NT_TIB结构体的self指针,即为NtCurrentTeb() 函数所读出的TEB结构体指针。 #### +0x020 ClientId 有2个变量,其中位于0x020的是UniqueProcess,为当前进程的的Pid,可用函数 GetCurrentProcessId() 访问当前结构体成员获取进程标识符。另一个变量位于0x024的是UniqueThread,为当前进程的的Tid,可用函数 GetCurrentThreadId() 访问当前结构体成员获取线程标识符。原理如下: ```sh ;获取pid mov eax, dword ptr fs:[0x18] mov eax, dword ptr [eax+0x20] ;获取tid mov eax, dword ptr fs:[0x18] mov eax, dword ptr [eax+0x24] ``` #### +0x030 ProcessEnvironmentBlock 这里存放的是PEB结构体的指针,所以说一般 fs:[0x30] 即为PEB的起始地址。 ### PEB 进程环境块,位于用户地址空间。存放进程信息,每个进程都有自己的PEB信息。PEB地址应从系统的EPROCESS结构的0x1b0偏移处获得,但访问EPROCESS需要ring0的权限。用户态可以通过TEB结构的偏移0x30处获得PEB的位置。`mov eax,fs:[0x30]` #### +0x002 BeingDebugged 表示当前进程是否处于调试状态,也就是函数 IsDebuggerPresent() 所访问的结构体成员。 ```sh mov eax, byte ptr fs:[0x30] movzx eax, byte ptr [eax+0x2] ``` #### +0x018 ProcessHeap 这个结构体成员就是进程堆的句柄,也就是指向结构体HEAP的指针。该结构体成员指向的HEAP结构体指针可用函数 GetProcessHeap()获取。其中偏移量是0x00c是Flags,0x010是ForceFlags。程序正常运行时,两个值分别是2和0。 ```sh mov eax, byte ptr fs:[0x30] movzx eax, byte ptr [eax+0x18] ``` #### +0x068 NtGlobalFlag 在调试状态时,NtGlobalFlag 的值为0x70。 ```sh mov eax, byte ptr fs:[0x30] movzx eax, byte ptr [eax+0x68] test eax, 0x70 ``` 关于PEB和TEB之间的关系,总结如下。 FS:[0x18] = FS:[0x00] (0x18处就是self指针) FS:[0x30] = &PEB FS:[0x00] = &SEH ### x64下注意点 上面讨论的包括fs寄存器在内都是在x86基础内的,在x86中,fs段寄存器用于指向TEB和处理器控制区(Processor Control Region, KPCR)。但是,到了x64的时,fs的角色已经换成了gs。gs段寄存器在用户态指向TEB,在内核态指向KPCR。然而,当运行WOW64程序中,fs存器仍然指向32位的TEB。而且偏移量也不一样了。gs+0x30是TEB的self指针。其他wiki上也没有。。 ### SEH 结构化异常处理SEH是Windows操作系统提供的强大异常处理功能。而Visual C++中的\_\_try{}/\_\_finally{}和__try{}/\_\_except{}结构本质上是对Windows提供的SEH的封装。**注意:SEH是基于线程的异常处理**。而且except和finally不像java或者python一样可以公用,在C里面是不可以一起用的。而且在try块中使用\_\_leave关键字会使程序跳转到try块的结尾,从而自然的进入finally块。 #### 封装在VC中的SEH 常见的流程如下。 ```c __try { // 受保护的代码 } __except ( /*异常过滤器exception filter*/ ) { // 异常处理程序exception handler } ``` ![SEH流程](./assets/Snipaste_2018-03-29_20-44-09.png) 其中异常过滤器只有三个可能的值(定义在Windows的Excpt.h中) `EXCEPTION_EXECUTE_HANDLER` 1 表示该异常被处理。从异常处下一条指令继续执行 `EXCEPTION_CONTINUE_SERCH` 0 表示不能处理该异常,继续搜索执行下一个EH `EXCEPTION_CONTINUE_EXECUTION` -1 表示该异常被忽略。从异常处处继续执行 两种基本的使用方式: ```c //方式一:直接使用过滤器的三个返回值之一 __try { …… } __except ( EXCEPTION_EXECUTE_HANDLER ) { …… } //方式二:自定义过滤器 __try { …… } __except ( MyFilter( GetExceptionCode() ) ) { …… } LONG MyFilter ( DWORD dwExceptionCode ) { if ( dwExceptionCode == EXCEPTION_ACCESS_VIOLATION ) return EXCEPTION_EXECUTE_HANDLER ; else return EXCEPTION_CONTINUE_SEARCH ; } ``` 显然方式2更好,对传入参数进行检查,如果异常类型是越权访问,则进行处理,否则则进行进一步搜索。 下面介绍trycatch的全局展开流程。 ![全局展开流程](./assets/Snipaste_2018-03-29_21-46-57.png) 同时上代码: ```c #include <stdio.h> #include <windows.h> #include <iostream> using namespace std; static unsigned int nStep = 1 ; void Function_B () { int x, y = 0 ; \__try { x = 5 / y ; // 引发异常 } \__finally { cout << "Step " << nStep++ << " : 执行Function_B的finally块的内容" << endl ; } } void Function_A ( ) { \__try { Function_B () ; } \__finally { cout << "Step " << nStep++ << " : 执行Function_A的finally块的内容" << endl ; } } long MyExcepteFilter ( ) { cout << "Step " << nStep++ << " : 执行main的异常过滤器" << endl ; return EXCEPTION_EXECUTE_HANDLER ; } int main () { \__try { Function_A () ; } __except ( MyExcepteFilter() ) { cout << "Step " << nStep++ << " : 执行main的except块的内容" << endl ; } return 0 ; } ``` 程序的流程如下图所示。 ![全局展开流程分析](./assets/Snipaste_2018-03-29_21-57-29.png) 总体来说,当遇到嵌套的try-except时,首先递归向上查找过滤器的值,只有值是`EXCEPTION_EXECUTE_HANDLER`的时候,执行该except的功能(或自定义函数),然后回到递归的最底层,进行全局展开,递归向上进行finally块的输出。 当未进行异常处理时,产生异常,windows将弹出异常对话框。这个功能是在UnhandledExceptionFilter中实现的,在启动进程、线程时,系统会安装一个最顶层的异常处理try-except结构。 上面就是VC封装后的SEH处理过程,下面讲下windows下的各种异常处理手段,从底层看VC是怎么封装SEH的。 ### VEH、SEH、VCH、UEF的系统实现 SEH异常回调函数定义: ```c EXCEPTION_DISPOSITION __cdecl _except_handler ( struct \_EXCEPTION_RECORD *\_ExceptionRecord, //异常记录结构指针 void * \_EstablisherFrame, //指向EXCEPTION_REGISTRATION结构,即SEH链 struct \_CONTEXT *\_ContextRecord, //Context结构指针 (线程上下文) void * \_DispatcherContext //无意义 (调度器上下文?) ); ``` 先看返回值。 ```c typedef enum _EXCEPTION_DISPOSITION { ExceptionContinueExecution, //0 重新执行异常指令 ExceptionContinueSearch, //1 搜索下一个SEH ExceptionNestedException, //2 ExceptionCollidedUnwind //3 } EXCEPTION_DISPOSITION; ``` 回调函数有4个指针,第一个是异常记录指针,在WINNT.H中定义,结构如下: ```c typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; //异常代码,说明是什么异常,比如单步、除零、断点等等 DWORD ExceptionFlags; //异常标志 struct \_EXCEPTION\_RECORD \*ExceptionRecord; //指向下一个异常记录(EXCEPTION\_RECORD)的指针 PVOID ExceptionAddress; //发生异常的地址 DWORD NumberParameters; //异常信息的个数(即数组ExceptionInformation的个数) ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //异常信息数组 } EXCEPTION_RECORD, *PEXCEPTION_RECORD; ``` 这个结构中的 ExcepitonCode成员是赋予异常的代码。通过在WINNT.H中搜索以“STATUS_”开头的#define定义,你可以得到一个异常代码列表。例如`STATUS_ACCESS_VIOLATION`的代码是0xC0000005。一个更全面的异常代码列表可以在 Windows NT DDK的NTSTATUS.H中找到。此结构的第四个成员是异常发生的地址。其它成员暂时可以忽略。 第三个参数是指向上下文的指针,在WINNT.H中定义,它代表某个特定线程的寄存器值。结构如下: ```c typedef struct _CONTEXT { DWORD ContextFlags; DWORD Dr0, Dr1, Dr2, Dr3, Dr6, Dr7; FLOATING_SAVE_AREA FloatSave; DWORD SegGs, SegFs, SegEs, SegDs; DWORD Edi, Esi, Edx, Ebx, Ecx, Eax, Ebp, Eip; DWORD SegCs; // MUST BE SANITIZED DWORD EFlags; // MUST BE SANITIZED DWORD Esp; DWORD SegSs; BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; } CONTEXT, *PCONTEXT; ``` 这里的关键是_exept_handler回调函数接收到操作系统传递过来的许多有价值的信息,例如异常的类型和发生的地址。使用这些信息,异常回调函数就能决定下一步做什么。 #### EXCEPTION_REGISTRATION ```c typedef struct _EXCEPTION_REGISTRATION { DWORD prev; //* EXCEPTION_REGISTRATION DWORD handler; // \*\_except_handler } EXCEPTION_REGISTRATION; ``` 这个结构就是fs寄存器所指向的位置,即在WINNT.H的NT_TIB结构的定义中被称为_EXCEPITON_REGISTARTION_RECORD。这里的第一个参数是前一个EXCEPTION_REGISTRATION的指针,第二个参数是指向_except_handler回调函数的指针。 当异常发生时,系统查找出错线程的TIB,获取指向EXCEPTION_REGISTRATION结构的指针。在这个结构中有一个指向_except_handler回调函数的指针。 ![异常处理过程](./assets/Snipaste_2018-03-30_13-23-42.png) 简单模拟上述过程: ```c #include <stdio.h> #include <windows.h> #include <iostream> using namespace std; DWORD scratch; EXCEPTION_DISPOSITION __cdecl _except_handler( struct \_EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct \_CONTEXT *ContextRecord, void * DispatcherContext ) { printf( "Hello from an exception handler\nscratch:%#x\n",scratch ); // 改变CONTEXT结构中EAX的值,以便它指向可以成功进写操作的位置 ContextRecord->Eax = (DWORD)&scratch; // 告诉操作系统重新执行出错的指令 return ExceptionContinueExecution; // 若返回ExceptionContinueSearch则将继续搜索下一个SEH } int main() { DWORD handler = (DWORD)\_except_handler; \__asm { // 创建EXCEPTION_REGISTRATION结构: push handler // handler函数的地址 push FS:[0] // 前一个handler函数的地址 mov FS:[0],ESP // 安装新的EXECEPTION_REGISTRATION结构 } \__asm { mov eax,0 // 将EAX清零 mov [eax], 1 // 写EAX指向的内存从而故意引发一个错误 } printf( "After writing!\n" ); \__asm { // 移去我们的EXECEPTION_REGISTRATION结构 mov eax,[ESP] // 获取前一个结构 mov FS:[0], EAX // 安装前一个结构 add esp, 8 // 将我们的EXECEPTION_REGISTRATION弹出堆栈 } return 0; } ``` 这只是一种最简单的方法,下面将说明os实现的SEH链表。 ![查找EXCEPTION_REGISTRATION结构](./assets/Snipaste_2018-03-30_22-49-27.png) ```c #include <windows.h> #include <stdio.h> EXCEPTION_DISPOSITION __cdecl _except_handler( struct \_EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct \_CONTEXT *ContextRecord, void * DispatcherContext ) { printf( "Home Grown handler: Exception Code: %08X Exception Flags %X", ExceptionRecord->ExceptionCode, ExceptionRecord->ExceptionFlags ); if ( ExceptionRecord->ExceptionFlags & 1 ) printf( " EH_NONCONTINUABLE" ); if ( ExceptionRecord->ExceptionFlags & 2 ) printf( " EH_UNWINDING" ); if ( ExceptionRecord->ExceptionFlags & 4 ) printf( " EH_EXIT_UNWIND" ); if ( ExceptionRecord->ExceptionFlags & 8 ) printf( " EH_STACK_INVALID" ); if ( ExceptionRecord->ExceptionFlags & 0x10 ) printf( " EH_NESTED_CALL" ); printf( "\n" ); // 让其它函数处理 return ExceptionContinueSearch; } void HomeGrownFrame( void ) { DWORD handler = (DWORD)\_except_handler; \__asm { // 创建EXCEPTION_REGISTRATION结构: push handler // handler函数的地址 push FS:[0] // 前一个handler函数的地址 mov FS:[0],ESP // 安装新的EXECEPTION_REGISTRATION结构 } \*(PDWORD)0 = 0; // 写入地址0,从而引发一个错误 //这句话将不会被执行,因为回调函数返回继续搜索 printf( "I should never get here!\n" ); \__asm { // 移去我们的EXECEPTION_REGISTRATION结构 mov eax,[ESP] // 获取前一个结构 mov FS:[0], EAX // 安装前一个结构 add esp, 8 // EXECEPTION_REGISTRATION结构弹出堆栈 } } int main() { \__try { HomeGrownFrame(); } __except( EXCEPTION_EXECUTE_HANDLER ) { printf( "Caught the exception in main()\n" ); } return 0; } ``` 因为_except_handler函数并没有打算修复出错的代码,因此它返回ExceptionContinueSearch。这导致操作系统继续在EXCEPTION_REGISTRATION结构链表中搜索下一个 EXCEPTION\_REGISTRATION结构。接下来安装的异常回调函数是针对main函数中的\_\_try/\_\_except块的。\_\_except块简单地打印出“Caught the exception in main()”。运行这个程序,会发现回调函数调用了2次,第二次是全局展开的原因。 UEF、VEH、VCH异常处理函数定义(UEF和VEH、VCH的函数类型名不一样,但是结构是一样的): ```c LONG NTAPI ExceptionHandler(struct \_EXCEPTION_POINTERS *ExceptionInfo); ``` 关于UEF、VEH、VCH函数的参数。参数只有一个,是指向结构_EXCEPTION_POINTERS的指针。具体结构如下: ```c typedef struct _EXCEPTION_POINTERS { PEXCEPTION_RECORD ExceptionRecord; //异常记录(EXCEPTION_RECORD)的指针 PCONTEXT ContextRecord; //线程上下文的指针 } EXCEPTION_POINTERS, *PEXCEPTION_POINTERS; ``` 这部分的详细内容就不写了,扯远了。参见参考资料。 关于VC为使用结构化异常处理的函数生成的标准异常堆栈帧如下:scopetable域指向一个scopetable_entry结构数组,而trylevel域实际上是这个数组的索引 ```sh EBP-00 _ebp EBP-04 trylevel EBP-08 scopetable数组指针 EBP-0C handler函数地址 EBP-10 指向前一个EXCEPTION_REGISTRATION结构 EBP-14 GetExceptionInformation(EXCEPTION_POINTERS) EBP-18 栈帧中的标准ESP ``` SCOPETABLE结构中的第二个成员和第三个成员分别是过滤器表达式代码的地址和相应的__except块的地址。 prviousTryLevel用于嵌套的__try块。这里的关键是函数中的每个__try块都有一个相应的SCOPETABLE结构。 ```c typedef struct _SCOPETABLE { DWORD previousTryLevel; DWORD lpfnFilter; DWORD lpfnHandler; } SCOPETABLE, *PSCOPETABLE; ``` ### 异常处理机制执行顺序 SEH(结构化异常处理,基于线程栈的异常处理) VEH(向量化异常处理,最顶端的异常处理) VCH(同上,最低端 的异常处理 ) UEF(TopLevelEH,顶级异常处理) 异常处理器处理顺序流程: 1. 交给调试器(进程必须被调试) 2. 执行VEH 3. 执行SEH 4. TopLevelEH(进程被调试时不会被执行) 5. 执行VCH 6. 交给调试器(上面的异常处理都处理不了,就再次交给调试器) 7. 调用异常端口通知csrss.exe 关于SEH和windows下的异常处理就记这么多,已经够多的了,自己也感觉很多东西有点绕。有部分东西没记,更加深入和全面的知识点可以参照以下的链接。 [参考链接1](https://blog.csdn.net/chenlycly/article/details/52575260) [参考链接2](https://bbs.pediy.com/thread-223939.htm) [参考链接3](https://bbs.pediy.com/thread-173853.htm) [参考链接4](https://blog.csdn.net/mycsersoft/article/details/33307655) [参考链接5](https://bbs.pediy.com/thread-32222.htm) ``` ``` Last modification:January 16th, 2021 at 01:35 pm © 允许规范转载 Support 确定不打赏一下支持博主吗 ×Close Appreciate the author Sweeping payments Pay by AliPay