x64 异常处理

😄

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 来平衡堆栈。

21.png

x64 函数将 push 和 pop 指令分别限制在 prolog 和 epilog中使用。堆栈指针在 prolog 和 epilog 之间一定保持不变这一事实是 x64 函数的一个特征。

函数的整体结构如下:

25.png

代码如下,实际上其中函数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 // =============== S U B R O U T I N E =======================================
.text:0000000140001070
.text:0000000140001070
.text:0000000140001070 sub_140001070 proc near // CODE XREF: sub_1400011F4+107↓p
.text:0000000140001070 // DATA XREF: .pdata:000000014000400C↓o
.text:0000000140001070 push rbx
.text:0000000140001072 sub rsp, 20h //prolog 到此行
.text:0000000140001076 call sub_1400010C0
.text:000000014000107B mov rdx, rax
.text:000000014000107E lea rcx, unk_140002240
.text:0000000140001085 mov rbx, rax
.text:0000000140001088 call sub_140001010
.text:000000014000108D lea rdx, [rbx+10h]
.text:0000000140001091 lea rcx, a0xLlx // "0x%llx\n"
.text:0000000140001098 call sub_140001010
.text:000000014000109D lea rcx, Command // "pause"
.text:00000001400010A4 call cs:system
.text:00000001400010AA xor eax, eax
.text:00000001400010AC add rsp, 20h //epilog 从此行开始
.text:00000001400010B0 pop rbx
.text:00000001400010B1 retn
.text:00000001400010B1 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 节区,同时还会使用异常目录数据表。

22.png

每个非叶函数都有一个 RUNTIME_FUNCTION 结构。RUNTIME_FUNCTION 结构描述了非叶函数信息及 prolog 中相关的操作(包括堆栈提升和非叶函数是否安装了 SEH等)。按 BeginAddress 升序排列。

1
2
3
4
5
6
7
8
typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY {
ULONG BeginAddress; // RVA
ULONG EndAddress; // RVA
union {
ULONG UnwindInfoAddress;
ULONG UnwindData; // RVA
} 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!RtlUserThreadStart
Debugger function entry 00000137`838a88d8 for:
(00007ff9`40ec2680) ntdll!RtlUserThreadStart | (00007ff9`40ec26d0) ntdll!TpReleaseCleanupGroupMembers
Exact matches:

BeginAddress = 00000000`00052680
EndAddress = 00000000`000526c9
UnwindInfoAddress = 00000000`0014763c

Unwind info at 00007ff9`40fb763c, 10 bytes
version 1, flags 1, prolog 4, codes 1
handler routine: ntdll!_C_specific_handler (00007ff9`40efc720), data 1
00: offs 4, unwind op 2, op info e UWOP_ALLOC_SMALL.
  1. 查看函数的 BeginAddress。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    0: kd> lm ntdll
    Unknown option 'd'
    start end module name
    00007ff9`40e70000 00007ff9`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:
    00007ff9`40ec2680 4883ec78 sub rsp,78h
    00007ff9`40ec2684 4c8bc9 mov r9,rcx
    00007ff9`40ec2687 488b0562991100 mov rax,qword ptr [ntdll!Kernel32ThreadInitThunkFunction (00007ff9`40fdbff0)]
    00007ff9`40ec268e 4885c0 test rax,rax
    00007ff9`40ec2691 7410 je ntdll!RtlUserThreadStart+0x23 (00007ff9`40ec26a3)
    00007ff9`40ec2693 4c8bc2 mov r8,rdx
    00007ff9`40ec2696 488bd1 mov rdx,rcx
    00007ff9`40ec2699 33c9 xor ecx,ecx
  2. 查看函数的 EndAddress。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    0: kd> ub 0x00007ff9`40e70000+0x00000000`000526c9
    ntdll!RtlUserThreadStart+0x36:
    00007ff9`40ec26b6 90 nop
    00007ff9`40ec26b7 8bd0 mov edx,eax
    00007ff9`40ec26b9 4883c9ff or rcx,0FFFFFFFFFFFFFFFFh
    00007ff9`40ec26bd e8aeae0400 call ntdll!NtTerminateProcess (00007ff9`40f0d570)
    00007ff9`40ec26c2 90 nop
    00007ff9`40ec26c3 4883c478 add rsp,78h
    00007ff9`40ec26c7 c3 ret
    00007ff9`40ec26c8 cc int 3

24.png

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		// 没有安装SEH,既没有 EXCEPT_FILTER 也没有 EXCEPT_HANDLER
#define UNW_FLAG_EHANDLER 0x1 // 表示该函数有 EXCEPT_FILTER & EXCEPT_HANDLER
#define UNW_FLAG_UHANDLER 0x2 // 表示该函数有 FINALLY_HANDLER
#define UNW_FLAG_CHAININFO 0x4 // 表示该函数有多个 UNWIND_INFO,它们串接在一起(所谓的 chain链)

typedef struct _UNWIND_INFO {
/* +00 */ UCHAR Version : 3; // 低3位(版本号),目前为1
/* +00 */ UCHAR Flags : 5; // 高5位(标志)值为上述define定义中的一个
/* +01 */ UCHAR SizeOfProlog; // prolog 的大小(字节单位)
/* +02 */ UCHAR CountOfCodes; // 表示当前 UNWIND_INFO 包含多少个 UNWIND_CODE 结构
/* +03 */ UCHAR FrameRegister : 4;// 如果函数建立了栈帧,它表示栈帧的索引(相对于 CONTEXT::RAX 的偏移)。否则该成员的值为0
/* +03 */ UCHAR FrameOffset : 4; // 表示FrameRegister距离函数最初栈顶(完成prolog,没有执行其他指令时的栈顶)偏移
/* +04 */ UNWIND_CODE UnwindCode[1];// UnwindCode 数组详细记录了函数修改栈、保存非易失性寄存器的指令

//如果Flags设置了UNW_FLAG_EHANDLER或UNW_FLAG_UHANDLER,下面存放ExceptionHandler、ExceptionData
//如果Flags设置了UNW_FLAG_CHAININFO,则下面存放的是RUNTIME_FUNCTION结构
union{
struct{
/* +08 */ OPTIONAL ULONG ExceptionHandler; // RVA, Exception handler routine
/* +0C */ OPTIONAL ULONG ExceptionData[]; // RVA, Scope table structure
};

/* +08 */ OPTIONAL RUNTIME_FUNCTION RuntimeFunctionChain;
};

} UNWIND_INFO, *PUNWIND_INFO;

typedef struct _SCOPE_TABLE {
ULONG Count; // 下面 ScopeRecord 结构数组元素个数
struct
{
ULONG BeginAddress; // RVA,__try块的开始地址
ULONG EndAddress; // RVA,__try块的结束地址
ULONG HandlerAddress;// 相对于BeginAddress的RVA
// __try__except:过滤表达式,__try__finally:FINALLY_HANDLER
ULONG JumpTarget; // RVA__try__except:EXCEPT_HANDLER,__try__finally:0
} ScopeRecord[1];
} SCOPE_TABLE, *PSCOPE_TABLE;

SCOPE_TABLE:

SCOPE_TABLE 是一个变长的结构体,用来描述每一个__try__except 或 __try__finally 的位置,每一个表项对应一个 __try 语句。

  1. 在 IDA 中对安装了 SEH 的函数进行交叉引用,可以看到 UNWIND_INFO.ExceptionHandler 都是为 __C_specific_hander 函数,这相当于 x86 SEH 处理的 __except_hander3 函数,即统一的异常处理分发入口。

    26.png

    __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
    );
  2. 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;
  3. 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, // 0, 压入非易失性整数寄存器,具体压入取决于OpInfo值
UWOP_ALLOC_LARGE, // 1
UWOP_ALLOC_SMALL, // 2
UWOP_SET_FPREG, // 3
UWOP_SAVE_NONVOL, // 4
UWOP_SAVE_NONVOL_FAR, // 5
UWOP_SPARE_CODE1, // 6
UWOP_SPARE_CODE2, // 7
UWOP_SAVE_XMM128, // 8
UWOP_SAVE_XMM128_FAR, // 9
UWOP_PUSH_MACHFRAME // 10
} 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 对堆栈执行的操作是相反的。如下图。

23.png

对 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 // 没有安装SEH,既没有 EXCEPT_FILTER 也没有 EXCEPT_HANDLER
#define UNW_FLAG_EHANDLER 0x1 // 表示该函数有 EXCEPT_FILTER & EXCEPT_HANDLER
#define UNW_FLAG_UHANDLER 0x2 // 表示该函数有 FINALLY_HANDLER
#define UNW_FLAG_CHAININFO 0x4 // 表示该函数有多个 UNWIND_INFO,它们串接在一起(所谓的 chain链)

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 {
/* +00 */ UCHAR Version : 3; // 低3位(版本号),目前为1
/* +00 */ UCHAR Flags : 5; // 高5位(标志)值为上述define定义中的一个
/* +01 */ UCHAR SizeOfProlog; // prolog 的大小(字节单位)
/* +02 */ UCHAR CountOfCodes; // 表示当前 UNWIND_INFO 包含多少个 UNWIND_CODE 结构
/* +03 */ UCHAR FrameRegister : 4;// 如果函数建立了栈帧,它表示栈帧的索引(相对于 CONTEXT::RAX 的偏移)。否则该成员的值为0
/* +03 */ UCHAR FrameOffset : 4; // 表示FrameRegister距离函数最初栈顶(完成prolog,没有执行其他指令时的栈顶)偏移
/* +04 */ UNWIND_CODE UnwindCode[1];// UnwindCode 数组详细记录了函数修改栈、保存非易失性寄存器的指令

//如果Flags设置了UNW_FLAG_EHANDLER或UNW_FLAG_UHANDLER,下面存放ExceptionHandler、ExceptionData
//如果Flags设置了UNW_FLAG_CHAININFO,则下面存放的是RUNTIME_FUNCTION结构
union {
struct {
/* +08 */ OPTIONAL ULONG ExceptionHandler; // RVA, Exception handler routine
/* +0C */ OPTIONAL ULONG ExceptionData[]; // RVA, Scope table structure
};

/* +08 */ 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);
//判断是否含有有效MZ和PE标志
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;
}
//找到所有PE文件结构的头地址
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
{
//循环判断RVA在哪个节表内
DWORD i = 0;
for (; i < pNTHeader->FileHeader.NumberOfSections; i++)
{
if ((pSectionHeader[i].VirtualAddress <= RVA) && (RVA < (pSectionHeader[i].VirtualAddress + pSectionHeader[i].Misc.VirtualSize)))
{
//在某个节中,终止循环
break;
}
}
//找到RVA相对该节表的偏移,偏移+该节表在文件内的起始地址则为FOA
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 ffff998de9a560c0
Implicit process is now ffff998d`e9a560c0
WARNING: .cache forcedecodeuser is not enabled
0: kd> knf
# Memory Child-SP RetAddr Call Site
00 00000008`f531fa08 00007ff6`8306107c KERNELBASE!wil::details::DebugBreak+0x2
01 8 (Inline Function) --------`-------- First_Inline_x64!Test3Parameters+0x6
02 0 00000008`f531fa10 00007ff6`83061310 First_Inline_x64!main+0xc
03 30 (Inline Function) --------`-------- First_Inline_x64!invoke_main+0x22
04 0 00000008`f531fa40 00007fff`bad37034 First_Inline_x64!__scrt_common_main_seh+0x10c
05 40 00000008`f531fa80 00007fff`bb5426a1 KERNEL32!BaseThreadInitThunk+0x14
06 30 00000008`f531fab0 00000000`00000000 ntdll!RtlUserThreadStart+0x21
  1. Memory:子函数堆栈大小。
  2. Child-SP:当前函数 RSP 位置。
  3. RetAddr:当前函数的返回地址。
  4. Call Site:子函数的返回地址。
  5. Args to Child:当前函数的实参。
  • Call Site:调用子函数的位置。实际为子函数返回地址。需要从整个列倒序来看函数调用顺序。

    如 #01 号帧,Call Site = First_Inline_x64!Test3Parameters+0x6:

    1
    2
    3
    4
    5
    0: kd> u First_Inline_x64!Test3Parameters
    First_Inline_x64!Test3Parameters:
    00007ff6`83061076 ff15840f0000 call qword ptr [First_Inline_x64!_imp_DebugBreak (00007ff6`83062000)]
    00007ff6`8306107c e84f000000 call First_Inline_x64!GetPebLdr (00007ff6`830610d0)
    00007ff6`83061081 488d5810 lea rbx,[rax+10h]

    但是实际上 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 字节。

    1. 先来验证 #04 号帧 Memory,其对应的值为父函数帧的 0x40 字节。

      • 返回地址:8 字节。

      • 分析函数:提升堆栈为 8+0x30 = 0x38 字节。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        0: kd> u __scrt_common_main_seh
        First_Inline_x64!__scrt_common_main_seh:
        00007ff6`83061204 48895c2408 mov qword ptr [rsp+8],rbx
        00007ff6`83061209 4889742410 mov qword ptr [rsp+10h],rsi
        00007ff6`8306120e 57 push rdi
        00007ff6`8306120f 4883ec30 sub rsp,30h
        00007ff6`83061213 b901000000 mov ecx,1
        00007ff6`83061218 e82f030000 call First_Inline_x64!__scrt_initialize_crt (00007ff6`8306154c)
        00007ff6`8306121d 84c0 test al,al
      • 则总共为 8 + 8 + 0x30 = 0x40 字节,与 Memory 对应上了。

    2. 此时 #04 号帧 Child-SP:

      1
      2
      3
      4
      5
      6
      0: kd> dq 00000008`f531fa40
      00000008`f531fa40 00000000`00000000 00000000`00000000
      00000008`f531fa50 00000000`00000000 00000000`00000000
      00000008`f531fa60 00000000`00000000 00000000`00000000
      00000008`f531fa70 00000000`00000000 00007fff`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 00007ff6`8306107c KERNELBASE!wil::details::DebugBreak+0x2
01 8 (Inline Function) --------`-------- First_Inline_x64!Test3Parameters+0x6
02 0 00000008`f531fa10 00007ff6`83061310 First_Inline_x64!main+0xc
03 30 (Inline Function) --------`-------- First_Inline_x64!invoke_main+0x22
04 0 00000008`f531fa40 00007fff`bad37034 First_Inline_x64!__scrt_common_main_seh+0x10c
05 40 00000008`f531fa80 00007fff`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_seh
Debugger function entry 00000228`6b61eb38 for:
(00007ff6`83061204) First_Inline_x64!__scrt_common_main_seh | (00007ff6`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 00007ff6`83062814, 18 bytes
version 1, flags 1, prolog f, codes 6
handler routine: First_Inline_x64!_C_specific_handler (00007ff6`83061cc0), 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