😄
1 异常处理基础 1.1 叶函数与非叶函数
非叶函数 :在函数中调用其他函数(子函数)、在函数中开辟栈空间、保存非易失寄存器、使用 SEH 等行为的函数(每个“、”表示”或“)。
叶函数 :既不调用函数,又没有修改栈指针 RSP,也没有使用 SEH 的函数就叫做叶函数。
易失寄存器与非失变寄存器寄存器:
易失寄存器:RAX, RCX, RDX, R8-R11
非失变寄存器:RBP, RBX, RSI, RDI, R12-R15
1.2 prolog 和 epilog x64 中的函数被分成三个结构:prolog — 函数体 — epilog 。
x64 的函数内在一开始使用 prolog 来开辟堆栈,在最后时才使用 epilog 来平衡堆栈。而不像是 x86 中的 __cdecl 调用约定那样每次调用完函数立即要平衡堆栈。x64 是在函数最后的 epilog 来平衡堆栈 。
x64 函数将 push 和 pop 指令分别限制在 prolog 和 epilog中使用。堆栈指针在 prolog 和 epilog 之间一定保持不变这一事实是 x64 函数的一个特征。
函数的整体结构如下:
代码如下,实际上其中函数1调用2,但是都已经优化在 main 函数中进行调用,这样的好处是调用者和被调用者共享同一个栈帧,减少开销。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 .text:0000000140001070 .text:0000000140001070 .text:0000000140001070 .text:0000000140001070 sub_140001070 proc near .text:0000000140001070 .text:0000000140001070 push rbx .text:0000000140001072 sub rsp, 20 h .text:0000000140001076 call sub_1400010C0 .text:000000014000107B mov rdx, rax .text:000000014000107 E lea rcx, unk_140002240 .text:0000000140001085 mov rbx, rax .text:0000000140001088 call sub_140001010 .text:000000014000108 D lea rdx, [rbx+10 h] .text:0000000140001091 lea rcx, a0xLlx .text:0000000140001098 call sub_140001010 .text:000000014000109 D lea rcx, Command .text:00000001400010 A4 call cs:system .text:00000001400010 AA xor eax, eax .text:00000001400010 AC add rsp, 20 h .text:00000001400010B 0 pop rbx .text:00000001400010B 1 retn .text:00000001400010B 1 sub_140001070 endp
在 x86 CPU 上,FS 段寄存器指向线程环境块 (TEB) 和处理器控制区域 (KPCR),但在 x64 上,GS 寄存器在用户模式下指向 TEB,在内核中指向 KPCR 的新模式。然而,当运行 WOW64 应用程序(即 x64 系统上的 32 位应用程序)时,FS 寄存器继续指向 32 位版本的 TEB。x64上的陷阱帧结构体 nt!_KTRAP_FRAME 不包含非易失性寄存器的有效内容 。如果打算修改非易失性寄存器,X64函数的prolog会保存非易失性寄存器的值。调试器始终可以从堆栈中提取这些非易失性寄存器的保存值,而不必从陷阱帧中获取它们。 在x64上的内核模式调试期间,.trap命令的输出会打印一条注释,突出显示从陷阱中检索到的所有寄存器的值可能不准确的事实,如下所示。此规则有例外,例如,为用户到内核模式转换生成的陷阱帧确实包含所有寄存器的正确值。
2 RUNTIME_FUNCTION x64 的 PE32+ 可执行文件中比 32 位的 PE 文件多一个 .pdata 节区,同时还会使用异常目录 数据表。
每个非叶函数都有一个 RUNTIME_FUNCTION 结构 。RUNTIME_FUNCTION 结构描述了非叶函数信息及 prolog 中相关的操作(包括堆栈提升和非叶函数是否安装了 SEH等)。按 BeginAddress 升序排列。
1 2 3 4 5 6 7 8 typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY { ULONG BeginAddress; ULONG EndAddress; union { ULONG UnwindInfoAddress; ULONG UnwindData; } DUMMYUNIONNAME; } RUNTIME_FUNCTION, *PRUNTIME_FUNCTION, _IMAGE_RUNTIME_FUNCTION_ENTRY, *_PIMAGE_RUNTIME_FUNCTION_ENTRY;
BeginAddress:当前非叶函数第一条指令相对于 ImageBase 的 RVA 起始地址。
EndAddress:当前非叶函数最后一条指令相对于 ImageBase 的 RVA 终止地址。
UnwindInfoAddress:一个指向展开信息结构(UNWIND_INFO )的指针 RVA,该结构描述了在发生异常时如何展开函数的调用堆栈。
当函数发生异常时,系统会使用当前 RIP 到 RUNTIME_FUNCTION 表中使用二分查找定位到当前函数的栈展开信息 UNWIND_INFO。
练习:查看指定函数的 RUNTIME_FUNCTION
.fnent 命令显示有关给定函数的 RUNTIME_FUNCTION 结构的信息。
用户模式的顶层处理函数 SEH 安装在 ntdll!RtlUserThreadStart 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 0 : kd> .fnent ntdll!RtlUserThreadStartDebugger function entry 00000137 `838 a88d8 for : (00007f f9`40 ec2680) ntdll!RtlUserThreadStart | (00007f f9`40 ec26d0) ntdll!TpReleaseCleanupGroupMembers Exact matches: BeginAddress = 00000000 `00052680 EndAddress = 00000000 `000526 c9 UnwindInfoAddress = 00000000 `0014763 c Unwind info at 00007f f9`40f b763c, 10 bytes version 1 , flags 1 , prolog 4 , codes 1 handler routine: ntdll!_C_specific_handler (00007f f9`40 efc720), data 1 00 : offs 4 , unwind op 2 , op info e UWOP_ALLOC_SMALL.
查看函数的 BeginAddress。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 0 : kd> lm ntdllUnknown option 'd' start end module name 00007f f9`40e70000 00007f f9`41068000 ntdll ntdll.dll FE96C48E (This is a reproducible build file hash, not a timestamp) fffff801`78600000 fffff801`79646000 nt ntkrnlmp.exe 2FF0D722 (This is a reproducible build file hash, not a timestamp) 0 : kd> u 0x00007ff9 `40e70000 +0x00000000 `00052680 ntdll!RtlUserThreadStart: 00007f f9`40 ec2680 4883 ec78 sub rsp,78 h00007f f9`40 ec2684 4 c8bc9 mov r9,rcx00007f f9`40 ec2687 488b 0562991100 mov rax,qword ptr [ntdll!Kernel32ThreadInitThunkFunction (00007f f9`40f dbff0)]00007f f9`40 ec268e 4885 c0 test rax,rax00007f f9`40 ec2691 7410 je ntdll!RtlUserThreadStart+0x23 (00007f f9`40 ec26a3)00007f f9`40 ec2693 4 c8bc2 mov r8,rdx00007f f9`40 ec2696 488b d1 mov rdx,rcx00007f f9`40 ec2699 33 c9 xor ecx,ecx
查看函数的 EndAddress。
1 2 3 4 5 6 7 8 9 10 0 : kd> ub 0x00007ff9 `40e70000 +0x00000000 `000526 c9ntdll!RtlUserThreadStart+0x36 : 00007f f9`40 ec26b6 90 nop00007f f9`40 ec26b7 8b d0 mov edx,eax00007f f9`40 ec26b9 4883 c9ff or rcx,0F FFFFFFFFFFFFFFFh00007f f9`40 ec26bd e8aeae0400 call ntdll!NtTerminateProcess (00007f f9`40f 0d570)00007f f9`40 ec26c2 90 nop00007f f9`40 ec26c3 4883 c478 add rsp,78 h00007f f9`40 ec26c7 c3 ret00007f f9`40 ec26c8 cc int 3
2.1 UNWIND_INFO RUNTIME_FUNCTION 结构的 UnwindInfoAddress 字段是一个 UNWIND_INFO 结构的偏移量,它告诉操作系统运行时它应该如何展开堆栈。UNWIND_INFO 结构包含数量不定的 UNWIND_CODE 结构,每个结构都存储着对应非叶函数 prolog 对堆栈相关操作的信息。
UNWIND_INFO 结构体保存了异常处理的信息 。
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 35 36 37 38 #define UNW_FLAG_NHANDLER 0x0 #define UNW_FLAG_EHANDLER 0x1 #define UNW_FLAG_UHANDLER 0x2 #define UNW_FLAG_CHAININFO 0x4 typedef struct _UNWIND_INFO { UCHAR Version : 3 ; UCHAR Flags : 5 ; UCHAR SizeOfProlog; UCHAR CountOfCodes; UCHAR FrameRegister : 4 ; UCHAR FrameOffset : 4 ; UNWIND_CODE UnwindCode[1 ]; union { struct { OPTIONAL ULONG ExceptionHandler; OPTIONAL ULONG ExceptionData[]; }; OPTIONAL RUNTIME_FUNCTION RuntimeFunctionChain; }; } UNWIND_INFO, *PUNWIND_INFO; typedef struct _SCOPE_TABLE { ULONG Count; struct { ULONG BeginAddress; ULONG EndAddress; ULONG HandlerAddress; ULONG JumpTarget; } ScopeRecord[1 ]; } SCOPE_TABLE, *PSCOPE_TABLE;
SCOPE_TABLE :
SCOPE_TABLE 是一个变长的结构体,用来描述每一个__try__except 或 __try__finally 的位置,每一个表项对应一个 __try 语句。
在 IDA 中对安装了 SEH 的函数进行交叉引用,可以看到 UNWIND_INFO.ExceptionHandler 都是为 __C_specific_hander 函数,这相当于 x86 SEH 处理的 __except_hander3 函数,即统一的异常处理分发入口。
__C_specific_handler 函数:
1 2 3 4 5 6 _CRTIMP __C_specific_handler( _In_ struct _EXCEPTION_RECORD *ExceptionRecord, _In_ void *EstablisherFrame, _Inout_ struct _CONTEXT *ContextRecord, _Inout_ struct _DISPATCHER_CONTEXT *DispatcherContext );
SCOPE_TABLE.JumpTarget 对应的就是 __except 处理函数。
参考MSDN-x64 异常处理 函数原型如下:
1 2 3 4 5 6 typedef EXCEPTION_DISPOSITION (*PEXCEPTION_ROUTINE) ( IN PEXCEPTION_RECORD ExceptionRecord; IN ULONG64 EstablisherFrame, IN OUT PCONTEXT ContextRecord, IN OUT PDISPATCHER_CONTEXT Dispatchercontext )
ExceptionRecord:异常采集信息,该指针指向具有标准 Win64 定义的异常记录。
EstablisherFrame:是此异常发生函数的固定堆栈分配的基址。
ContextRecord:指向引发异常时的异常上下文(在异常处理程序的情况下)或当前的展开上下文(在终止处理程序的情况下)。
DispatcherContext:指向此函数的调度程序上下文。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef struct _EXCEPTION_RECORD64 { NTSTATUS ExceptionCode; ULONG ExceptionFlags; ULONG64 ExceptionRecord; ULONG64 ExceptionAddress; ULONG NumberParameters; ULONG __unusedAlignment; ULONG64 ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD64, *PEXCEPTION_RECORD64; typedef struct _EXCEPTION_POINTERS { PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord; } EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
PEXCEPTION_ROUTINE 的 Dispatchercontext(PDISPATCHER_CONTEXT) 参数:
1 2 3 4 5 6 7 8 9 10 typedef struct _DISPATCHER_CONTEXT { ULONG64 ControlPc; ULONG64 ImageBase; PRUNTIME_FUNCTION FunctionEntry; ULONG64 EstablisherFrame; ULONG64 TargetIp; PCONTEXT ContextRecord; PEXCEPTION_ROUTINE LanguageHandler; PVOID HandlerData; } DISPATCHER_CONTEXT, *PDISPATCHER_CONTEXT;
ControlPc:是此函数中的 RIP 值(异常触发点)。此值可以是异常地址,也可以是控制离开建立函数的地址。
ImageBase:是包含此函数的模块的映像基(加载地址),将其添加到函数入口使用的32位偏移,以记录相对地址。
FunctionEntry:提供一个指针,该指针指向保存此函数的函数和展开信息图像基相对地址的 RUNTIME_FUNCTION 函数项。
EstablisherFrame:ControlPc 所在函数的栈帧(如果建立了栈帧)或 RSP。
TargetIp:处理异常的 __except 代码块(由 __C_specific_hander 来负责调用)。如果未指定 EstablisherFrame,则忽略此地址。
ContextRecord:指向异常上下文,供系统异常调度/展开代码使用。
LanguageHandler:指向调用的异常处理程序例程(UNWIND_INFO.ExceptionHandler,即 __C_specific_hander)。
HandlerData:指向此函数的异常处理程序数据(UNWIND_INFO.ExceptionData 中 SCOPE_TABLE.ScopeRecord)。
2.2 UNWIND_CODE UNWIND_CODE 结构用来恢复函数 prolog 对堆栈执行的操作,每个 UNWIND_CODE 结构仅对应一个操作,要么是开辟堆栈空间、要么是 push 非易失寄存器。
UNWIND_CODE 结构的内容是包含在 UNWIND_INFO 里面的,不是通过 RVA 得到的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 typedef enum _UNWIND_OP_CODES { UWOP_PUSH_NONVOL = 0 , UWOP_ALLOC_LARGE, UWOP_ALLOC_SMALL, UWOP_SET_FPREG, UWOP_SAVE_NONVOL, UWOP_SAVE_NONVOL_FAR, UWOP_SPARE_CODE1, UWOP_SPARE_CODE2, UWOP_SAVE_XMM128, UWOP_SAVE_XMM128_FAR, UWOP_PUSH_MACHFRAME } UNWIND_OP_CODES, *PUNWIND_OP_CODES; typedef union _UNWIND_CODE { struct { UCHAR CodeOffset; UCHAR UnwindOp : 4 ; UCHAR OpInfo : 4 ; }; USHORT FrameOffset; } UNWIND_CODE, *PUNWIND_CODE;
注意 :在每一个 UNWIND_INFO 下的多个 UNWIND_CODE 中,UNWIND_CODE 表示的操作与实际 prolog 对堆栈执行的操作是相反的 。如下图。
对 Exception Directory 的解析:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 #include <iostream> #include <windows.h> #include <tchar.h> using namespace std ;#ifndef DEBUG #define DEBUG #endif #define UNW_FLAG_NHANDLER 0x0 #define UNW_FLAG_EHANDLER 0x1 #define UNW_FLAG_UHANDLER 0x2 #define UNW_FLAG_CHAININFO 0x4 CHAR File_Name[] = "C:\\Users\\alvin\\Desktop\\VS2019项目文件夹\\x64\\First_Inline_x64\\x64\\Release\\First_Inline_x64.exe" ; typedef union _UNWIND_CODE { struct { UCHAR CodeOffset; UCHAR UnwindOp : 4 ; UCHAR OpInfo : 4 ; }; USHORT FrameOffset; } UNWIND_CODE, * PUNWIND_CODE; typedef struct _UNWIND_INFO { UCHAR Version : 3 ; UCHAR Flags : 5 ; UCHAR SizeOfProlog; UCHAR CountOfCodes; UCHAR FrameRegister : 4 ; UCHAR FrameOffset : 4 ; UNWIND_CODE UnwindCode[1 ]; union { struct { OPTIONAL ULONG ExceptionHandler; OPTIONAL ULONG ExceptionData[]; }; OPTIONAL RUNTIME_FUNCTION RuntimeFunctionChain; }; } UNWIND_INFO, * PUNWIND_INFO; BOOL ReadPEFile (IN LPCSTR lpszFile, OUT PVOID64* pFileBuffer, OUT PULONG64 FileSize) { FILE* pFile = NULL ; ULONG uError = 0 ; ULONG64 fileSize = 0 ; PVOID64 pTempFileBuffer = NULL ; uError = fopen_s(&pFile, lpszFile, "rb" ); if (uError || pFile == NULL ) { printf (" 无法打开 EXE 文件! uError = %d, pFile = %llx\n" , uError, (ULONG64)pFile); return FALSE; } fseek(pFile, 0 , SEEK_END); fileSize = ftell(pFile); fseek(pFile, 0 , SEEK_SET); #ifdef DEBUG printf ("读取的文件大小:0x%llx字节\n" , fileSize); #endif if (fileSize <= 0 ) { printf (" 文件读取失败! " ); return FALSE; } pTempFileBuffer = malloc (fileSize); if (!pTempFileBuffer) { printf (" 分配空间失败! " ); fclose(pFile); return FALSE; } memset (pTempFileBuffer, 0 , fileSize); ULONG64 n = fread(pTempFileBuffer, fileSize, 1 , pFile); if (!n) { printf (" 读取数据失败! " ); free (pTempFileBuffer); fclose(pFile); return FALSE; } *pFileBuffer = pTempFileBuffer; *FileSize = fileSize; fclose(pFile); return TRUE; } ULONG64 RVAToFOA (IN ULONG64 RVA, IN PVOID64 pFileBuffer) { PIMAGE_DOS_HEADER pDosHeader = NULL ; PIMAGE_NT_HEADERS64 pNTHeader = NULL ; PIMAGE_FILE_HEADER pFileHeader = NULL ; PIMAGE_OPTIONAL_HEADER64 pOptionHeader64 = NULL ; PIMAGE_SECTION_HEADER pSectionHeader = NULL ; ULONG dwSizeOfHeaders = 0 ; BYTE uSignature[4 ] = { 0 }; if (pFileBuffer == NULL ) { printf ("pImageBuffer缓冲区指针出错...\n" ); return FALSE; } uSignature[0 ] = *(PBYTE)pFileBuffer; uSignature[1 ] = *(PBYTE)((BYTE*)pFileBuffer+1 ); if (*(PUSHORT)uSignature != IMAGE_DOS_SIGNATURE) { printf ("非有效的MZ标志\n" ); return FALSE; } pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer; if (*((PDWORD)((BYTE*)pDosHeader + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE) { printf ("非有效的PE标志\n" ); return FALSE; } pNTHeader = (PIMAGE_NT_HEADERS64)((BYTE*)pDosHeader + pDosHeader->e_lfanew); pFileHeader = (PIMAGE_FILE_HEADER)((BYTE*)pNTHeader + sizeof (DWORD)); pOptionHeader64 = (PIMAGE_OPTIONAL_HEADER64)((BYTE*)pFileHeader + IMAGE_SIZEOF_FILE_HEADER); pSectionHeader = (PIMAGE_SECTION_HEADER)((BYTE*)pOptionHeader64 + pFileHeader->SizeOfOptionalHeader); dwSizeOfHeaders = pOptionHeader64->SizeOfHeaders; if (RVA < dwSizeOfHeaders) { return RVA; } else { DWORD i = 0 ; for (; i < pNTHeader->FileHeader.NumberOfSections; i++) { if ((pSectionHeader[i].VirtualAddress <= RVA) && (RVA < (pSectionHeader[i].VirtualAddress + pSectionHeader[i].Misc.VirtualSize))) { break ; } } return RVA - pSectionHeader[i].VirtualAddress + pSectionHeader[i].PointerToRawData; } } BOOL ParserExceptionDirectory (PVOID64 pFileBuffer) { PIMAGE_DOS_HEADER pDosHeader = NULL ; PIMAGE_NT_HEADERS64 pNTHeader = NULL ; PIMAGE_FILE_HEADER pFileHeader = NULL ; PIMAGE_OPTIONAL_HEADER64 pOptionHeader64 = NULL ; PIMAGE_SECTION_HEADER pSectionHeader = NULL ; PIMAGE_DATA_DIRECTORY pDataDirectory = NULL ; PRUNTIME_FUNCTION pRuntimeFunction = NULL ; PUNWIND_INFO pUnwindInfo = NULL ; PUNWIND_CODE pUnwindCode = NULL ; pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer; pNTHeader = (PIMAGE_NT_HEADERS64)((BYTE*)pFileBuffer + pDosHeader->e_lfanew); pOptionHeader64 = (PIMAGE_OPTIONAL_HEADER64)((BYTE*)pNTHeader + IMAGE_SIZEOF_FILE_HEADER + sizeof (DWORD)); pSectionHeader = (PIMAGE_SECTION_HEADER)((BYTE*)pOptionHeader64 + pNTHeader->FileHeader.SizeOfOptionalHeader); pDataDirectory = (PIMAGE_DATA_DIRECTORY)(pOptionHeader64->DataDirectory); pRuntimeFunction = (PRUNTIME_FUNCTION)((BYTE*)pDosHeader + RVAToFOA(pDataDirectory[3 ].VirtualAddress, pFileBuffer)); ULONG j = 0 ; while (pRuntimeFunction[j].BeginAddress) { j++; } cout << --j << endl ; while (pRuntimeFunction->BeginAddress) { pUnwindInfo = (PUNWIND_INFO)((BYTE*)pDosHeader + RVAToFOA(pRuntimeFunction->UnwindInfoAddress, pFileBuffer)); pUnwindCode = (PUNWIND_CODE)(pUnwindInfo->UnwindCode); cout << "-------------------------------------------------------------------------" << endl ; cout << "UNWIND_INFO:" << endl ; cout << "Version = " << (ULONG)(pUnwindInfo->Version) << endl ; cout << "Flags = " << (ULONG)(pUnwindInfo->Flags) << endl ; cout << "SizeOfProlog = " << (ULONG)(pUnwindInfo->SizeOfProlog) << endl ; cout << "CountOfCodes = " << (ULONG)(pUnwindInfo->CountOfCodes) << endl ; cout << "FrameRegister = " << (ULONG)(pUnwindInfo->FrameRegister) << endl ; cout << "FrameOffset = " << (ULONG)(pUnwindInfo->FrameOffset) << endl ; cout << "UnwindCode = " << hex << (ULONG)(pUnwindInfo->UnwindCode) << endl ; if ((ULONG)(pUnwindInfo->Flags) & (UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER)) { cout << "ExceptionHandler(RVA) = " << (ULONG)(pUnwindInfo->ExceptionHandler) << endl ; cout << "ExceptionData(RVA,Scopetable) = " << (ULONG)(pUnwindInfo->ExceptionData) << endl ; } if ((ULONG)(pUnwindInfo->Flags) == UNW_FLAG_CHAININFO) { RUNTIME_FUNCTION RuntimeFuntion = pUnwindInfo->RuntimeFunctionChain; cout << "BeginAddress(RVA) = " << (ULONG64)(RuntimeFuntion.BeginAddress) << endl ; cout << "EndAddress(RVA) = " << (ULONG64)(RuntimeFuntion.EndAddress) << endl ; cout << "UnwindInfoAddress(RVA) = " << (ULONG64)(RuntimeFuntion.UnwindInfoAddress) << endl ; } cout << "-------------------------------------------------------------------------" << endl ; for (ULONG i = 0 ; i < (ULONG)(pUnwindInfo->CountOfCodes); i++) { cout << "UNWIND_CODE:" << i + 1 << endl ; cout << "CodeOffset = " << (ULONG)(pUnwindCode[i].CodeOffset) << endl ; cout << "UnwindOp = " << (ULONG)(pUnwindCode[i].UnwindOp) << endl ; cout << "OpInfo = " << (ULONG)(pUnwindCode[i].OpInfo) << endl ; cout << "FrameOffset = " << (ULONG)(pUnwindCode[i].FrameOffset) << endl << endl ; } cout << "-------------------------------------------------------------------------" << endl ; pRuntimeFunction++; } return TRUE; } int _tmain(int argc, char ** argv){ PVOID64 pFileBuffer = NULL ; ULONG64 uFileSize = 0 ; BOOL bRet = FALSE; bRet = ReadPEFile(File_Name, &pFileBuffer, &uFileSize); printf ("The file buffer : 0x%llx\n" , (ULONG64)pFileBuffer); bRet = ParserPEFile(pFileBuffer); getchar(); }
3 x64堆栈回滚 3.1 Windbg调试堆栈回滚 如下代码:
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 #include <iostream> #include <windows.h> extern "C" PVOID64 _fastcall GetPebLdr (void ) ;__int64 Test3Parameters (__int64* arg_1, __int64 arg_2, __int64 arg_3, OUT PVOID64* OutAddress) { __int64 iLocal = 0 ; iLocal = *arg_1; DebugBreak(); *OutAddress = GetPebLdr(); return iLocal + arg_2 + arg_3 + (ULONG64)*OutAddress; } int main () { PVOID64 OutAddress = NULL ; INT64 Temp = 2 ; __try { INT64 Test = Test3Parameters(&Temp, 5 , 9 , &OutAddress); printf ("%p\n" , OutAddress); printf ("0x%llx\n" , Test); } __except(1 ) { printf ("%s\n" , TEXT("发生异常" )); } system("pause" ); }
函数调用关系:main — Test3Parameters — DebugBreak(int 3)。
在 Release 版本下面查看此时断点断下来后的堆栈:
knf 命令显示调用堆栈以及堆栈中每一帧使用的堆栈空间量 Memory。
1 2 3 4 5 6 7 8 9 10 11 12 0 : kd> .process ffff998de9a560c0Implicit process is now ffff998d`e9a560c0 WARNING: .cache forcedecodeuser is not enabled 0 : kd> knf # Memory Child-SP RetAddr Call Site 00 00000008 `f531fa08 00007f f6`8306107 c KERNELBASE!wil::details::DebugBreak+0x2 01 8 (Inline Function) --------`-------- First_Inline_x64!Test3Parameters+0x6 02 0 00000008 `f531fa10 00007f f6`83061310 First_Inline_x64!main+0xc 03 30 (Inline Function) --------`-------- First_Inline_x64!invoke_main+0x22 04 0 00000008 `f531fa40 00007f ff`bad37034 First_Inline_x64!__scrt_common_main_seh+0x10c 05 40 00000008 `f531fa80 00007f ff`bb5426a1 KERNEL32!BaseThreadInitThunk+0x14 06 30 00000008 `f531fab0 00000000 `00000000 ntdll!RtlUserThreadStart+0x21
Memory:子函数堆栈大小。
Child-SP:当前函数 RSP 位置。
RetAddr:当前函数的返回地址。
Call Site:子函数的返回地址。
Args to Child:当前函数的实参。
Call Site :调用子函数的位置。实际为子函数返回地址 。需要从整个列倒序来看函数调用顺序 。
如 #01 号帧,Call Site = First_Inline_x64!Test3Parameters+0x6:
1 2 3 4 5 0 : kd> u First_Inline_x64!Test3ParametersFirst_Inline_x64!Test3Parameters: 00007f f6`83061076 ff15840f0000 call qword ptr [First_Inline_x64!_imp_DebugBreak (00007f f6`83062000 )]00007f f6`8306107 c e84f000000 call First_Inline_x64!GetPebLdr (00007f f6`830610 d0)00007f f6`83061081 488 d5810 lea rbx,[rax+10 h]
但是实际上 00007ff6'83061076 + 6 = 00007ff6'8306107c 为子函数的返回地址。
Memory :子函数 栈帧的大小,单位是字节, 十六进制。需要从上下两行来看子函数堆栈使用了多少字节,如 #05 号帧的 0x40 字节表示子函数的堆栈使用情况 。
$函数栈帧的大小\ =\ 返回地址(8 个字节)+ 非易失变寄存器 + 局部变量 + 基于栈的参数 + 预留的 0x20 字节(shadow space)$ $函数栈帧的大小\ =\ 返回地址(8个字节)+\ unwind\_code$
公式的返回地址是调用者函数的返回地址,其余值都是子函数的。
结论:当前函数栈帧的大小位于下一帧的 Memory 中 。
Child-SP :表示当前函数 初始化完成后的 RSP 状态值,当前函数中此时的 RSP 已经完成 prolog 操作。
如 #04 号帧,此时 Child-SP 指针指向的内容就表示函数 __scrt_common_main_seh 完成 prolog 后的 RSP 的位置。需要从整个列来看 RSP 实际变化情况 。如 #04 号帧到 #05 号帧的 RSP 变化为 0x40 字节,表示 #04 号帧用了 0x40 字节。
先来验证 #04 号帧 Memory,其对应的值为父函数帧的 0x40 字节。
此时 #04 号帧 Child-SP:
1 2 3 4 5 6 0 : kd> dq 00000008 `f531fa4000000008 `f531fa40 00000000 `00000000 00000000 `00000000 00000008 `f531fa50 00000000 `00000000 00000000 `00000000 00000008 `f531fa60 00000000 `00000000 00000000 `00000000 00000008 `f531fa70 00000000 `00000000 00007f ff`bad37034 <--RetAddr 00000008 `f531fa80 00000000 `00000000 00000000 `00000000
注意 :.frame /r 02 表示列出 #02 号帧的寄存器的值,此时的寄存器保存的值是从 #01 号帧 prolog 保存非易失寄存器的时得到的。也就是说 .frame /r 02 的寄存器状态是调用子函数时的寄存器,不是进入 #02 号帧时的寄存器。
3.2 UNWIND_INFO堆栈回滚 x64 的函数栈回溯,无需借助程序的符号表。只需要借助 RUNTIME_FUNCTION、UNWIND_INFO 和 UNWIND_CODE 结构即可从 PE 文件本身中的信息来得出每个函数堆栈上分配的空间。
$函数栈帧的大小\ =\ 返回地址(8 个字节)+ 非易失变寄存器 + 局部变量 + 基于栈的参数 + 预留的 0x20 字节(shadow space)$ $函数栈帧的大小\ =\ 返回地址(8个字节)+\ unwind\_code$
本节实验还是借助 Windbg,在 IDA 中其实更好分析。参考《深入X64架构(翻译) 》、《深入 x64 》。
本节还是以上面提到的 #04 号帧做例子:
1 2 3 4 5 6 7 8 9 0 : kd> knf # Memory Child-SP RetAddr Call Site 00 00000008 `f531fa08 00007f f6`8306107 c KERNELBASE!wil::details::DebugBreak+0x2 01 8 (Inline Function) --------`-------- First_Inline_x64!Test3Parameters+0x6 02 0 00000008 `f531fa10 00007f f6`83061310 First_Inline_x64!main+0xc 03 30 (Inline Function) --------`-------- First_Inline_x64!invoke_main+0x22 04 0 00000008 `f531fa40 00007f ff`bad37034 First_Inline_x64!__scrt_common_main_seh+0x10c 05 40 00000008 `f531fa80 00007f ff`bb5426a1 KERNEL32!BaseThreadInitThunk+0x14 06 30 00000008 `f531fab0 00000000 `00000000 ntdll!RtlUserThreadStart+0x21
查看函数 UNWIND_INFO 情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 0 : kd> .fnent First_Inline_x64!__scrt_common_main_sehDebugger function entry 00000228 `6b 61eb38 for : (00007f f6`83061204 ) First_Inline_x64!__scrt_common_main_seh | (00007f f6`83061380 ) First_Inline_x64!mainCRTStartup Exact matches: First_Inline_x64!__scrt_common_main_seh (void ) BeginAddress = 00000000 `00001204 EndAddress = 00000000 `00001380 UnwindInfoAddress = 00000000 `00002814 Unwind info at 00007f f6`83062814 , 18 bytes version 1 , flags 1 , prolog f, codes 6 handler routine: First_Inline_x64!_C_specific_handler (00007f f6`83061 cc0), data 2 00 : offs f, unwind op 4 , op info 6 UWOP_SAVE_NONVOL FrameOffset: 48 reg: rsi. 02 : offs f, unwind op 4 , op info 3 UWOP_SAVE_NONVOL FrameOffset: 40 reg: rbx. 04 : offs f, unwind op 2 , op info 5 UWOP_ALLOC_SMALL. 05 : offs b, unwind op 0 , op info 7 UWOP_PUSH_NONVOL reg: rdi.
正常来说 UNWIND_CODE 应该是逆序排序的。
UWOP_SAVE_NONVOL:mov qword ptr[rsp+0x8], rbx,0 bytes。
UWOP_SAVE_NONVOL:mov qword ptr[rsp+0x10], rsi,0 bytes。
UWOP_PUSH_NONVOL:push rdi,8 bytes。
UWOP_ALLOC_SMALL:sub rsp, 5*8+8,0x30 bytes。
函数栈帧的大小 = 返回地址(8 bytes)+unwind_code(0x38) = 0x40字节。和 #05 帧的 Memory 对上。
参考:
4 异常处理
4.1 异常产生与采集 4.2 异常分发 4.3 内核异常处理 4.4 用户异常处理 5 #DB 调试异常 5.1 #DB 异常的类型 当遇到一个硬件断点(指令执行、数据读写)或 single-step(单步调试)时,处理器将组织一个 #DB 异常,并提交处理器执行。
在 x86/x64 体系上的 #DB(调试)异常使用 1 号中断向量,分为 fault 和 trap 两类,共有 6 个触发方式。
fault 类型的 #DB 有 2 类触发方式:
由执行断点触发。
由访问调试寄存器触发(DR7.GD = 1 时)。当断点对应的 DR7.R/Wx = 00 时,使用执行断点,在断点寄存器 DR0-DR3 里设置断点的地址。当 DR7.GD = 1 时,使用 DR 寄存器访问触发方式。在后续的指令中访问任何一个 DR 寄存器将产生 #DB 异常。
trap 类型 #DB 异常有 4 类触发方式:
读写硬件断点(根据 DR7 的 R/Wx 位来区分)。
单步调试断点,设置 Rflags.TF = 1 时产生。单步调试分为指令单步调试及分支单步调试两种(单步调试在将 eflags.TF 置为 1 后,下一条指令执行完毕后触发,分支单步调试则在目标指令执行前触)
I/O 断点,I/O 断点是指令访问的 I/O 端口地址。在 CR4.DE = 1 时,DR7 的 R/Wx = 10,产生的就是 I/O 读写断点。(见 卷 3A—Chapter 17 Debug, Branch Profile, TSC, and Intel® Resource Director Technology. (Intel® RDT) Features—17.2 Debug Registers 。)
在任务切换时,任务切换的 #DB 异常在执行完任务切换后,处理器检测到 TSS.T 为 1 时触发。(在 VMX 架构下,并不支持在 non-root operation 环境中使用任务切换机制。一旦发生任务切换,将产生 VM-exit 行为。因此,在 pending debug exception 字段中并不支持记录任务切换时产生的 #DB 异常。)
fault 和 trap 两类异常触发的位置不同,fault 类型如 DR 奇存器访问的 #DB 异常,在执行 MOV-DR 指令前触发,异常处理后还要继续执行该指令;trap 类型的异常,在执行完每一条指令后触发,异常处理后执行下一条指令。
5.2 #DB 异常的处理 《处理器虚拟化技术》3.8.7.1