控制流防护 Control Flow Guard

😄

1 CFG 支持

微软于 2014 年在 Windows 8.1 Update 3(KB3000850)中首次发布了 CFG(Control Flow Guard),并在之后的 Windows 10 中自动启用。(参考MSDN Control Flow Guard for platform security)

CFG 主要用来解决代码中针对间接跳转的缓解,如 jmp/call/ret 等间接跳转。代码运行过程中,在间接跳转之前,系统会通过 CFG Bitmap 来动态判断跳转目的地址是否是可信赖的区域。试图在汇编级别保护间接调用。这也叫做控制流完整性(Control Flow Integrity,CFI)。分为:

  • User CFG,在用户代码中的 CFG。
  • Kernel CFG,内核代码中的 KCFG。

CFG 的启用需要系统和编译器同时支持(就像ASLR一样),从 Virtual Studio 2015 Updated 2 版本以上开始支持(项目属性—C/C++—代码生成—控制流防护—是 guard:cf)。

程序中有了 CFG 之后,间接跳转之前加了一个跳转判断:

1.png

1.1 PE中的CFG标志

当在 VS 2019 项目属性—C/C++—代码生成—控制流防护—是 guard:cf 启用 CFG 后:

  1. PE Optionnal Header.DLL characteristics 有相关签名指出程序中使用了 CFG。

  2. CFG 数据编译在 .gfids 节中,但是该节一般会被合并到 .rdata 节中。

  3. 在 .rdata 节中的 CFG 数据结构用数据目录表 _IMAGE_LOAD_CONFIG_DIRECTORY64 中相关字段来描述。

    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
    typedef struct _IMAGE_LOAD_CONFIG_DIRECTORY64 {
    DWORD Size;
    DWORD TimeDateStamp;
    WORD MajorVersion;
    WORD MinorVersion;
    DWORD GlobalFlagsClear;
    DWORD GlobalFlagsSet;
    DWORD CriticalSectionDefaultTimeout;
    ULONGLONG DeCommitFreeBlockThreshold;
    ULONGLONG DeCommitTotalFreeThreshold;
    ULONGLONG LockPrefixTable;
    ULONGLONG MaximumAllocationSize;
    ULONGLONG VirtualMemoryThreshold;
    ULONGLONG ProcessAffinityMask;
    DWORD ProcessHeapFlags;
    WORD CSDVersion;
    WORD DependentLoadFlags;
    ULONGLONG EditList;
    ULONGLONG SecurityCookie;
    ULONGLONG SEHandlerTable;
    ULONGLONG SEHandlerCount;
    ULONGLONG GuardCFCheckFunctionPointer;
    ULONGLONG GuardCFDispatchFunctionPointer;
    ULONGLONG GuardCFFunctionTable;
    ULONGLONG GuardCFFunctionCount;
    DWORD GuardFlags;
    IMAGE_LOAD_CONFIG_CODE_INTEGRITY CodeIntegrity;
    ULONGLONG GuardAddressTakenIatEntryTable;
    ULONGLONG GuardAddressTakenIatEntryCount;
    ULONGLONG GuardLongJumpTargetTable;
    ULONGLONG GuardLongJumpTargetCount;
    ULONGLONG DynamicValueRelocTable;
    ULONGLONG CHPEMetadataPointer;
    ULONGLONG GuardRFFailureRoutine;
    ULONGLONG GuardRFFailureRoutineFunctionPointer;
    DWORD DynamicValueRelocTableOffset;
    WORD DynamicValueRelocTableSection;
    WORD Reserved2;
    ULONGLONG GuardRFVerifyStackPointerFunctionPointer;
    DWORD HotPatchTableOffset;
    DWORD Reserved3;
    ULONGLONG EnclaveConfigurationPointer;
    ULONGLONG VolatileMetadataPointer;
    ULONGLONG GuardEHContinuationTable;
    ULONGLONG GuardEHContinuationCount;
    ULONGLONG GuardXFGCheckFunctionPointer;
    ULONGLONG GuardXFGDispatchFunctionPointer;
    ULONGLONG GuardXFGTableDispatchFunctionPointer;
    ULONGLONG CastGuardOsDeterminedFailureMode;
    ULONGLONG GuardMemcpyFunctionPointer;
    } IMAGE_LOAD_CONFIG_DIRECTORY64, *PIMAGE_LOAD_CONFIG_DIRECTORY64;

使用 VS 自带工具 dumpbin 查看 PE 文件头信息(工具—命令行—开发者 Power Shell):

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
dumpbin /headers /loadconfig C:\Win32\LoadDriverIO\x64\Release\LoadDriverIO.exe
...
OPTIONAL HEADER VALUES
20B magic # (PE32+)
...
C160 DLL characteristics
High Entropy Virtual Addresses
Dynamic base
NX compatible
Control Flow Guard
Terminal Server Aware

Section contains the following load config:

...
0000000140005008 Security Cookie
0000000140003240 Guard CF address of check-function pointer
0000000140003250 Guard CF address of dispatch-function pointer
00000001400032C0 Guard CF function table
D Guard CF function count
00017500 Guard Flags
CF instrumented
FID table present
Protect delayload IAT
Delayload IAT in its own section
Export suppression info present
Long jump target table present
0000 Code Integrity Flags
0000 Code Integrity Catalog
00000000 Code Integrity Catalog Offset
00000000 Code Integrity Reserved
0000000000000000 Guard CF address taken IAT entry table
0 Guard CF address taken IAT entry count
0000000000000000 Guard CF long jump target table
0 Guard CF long jump target count
0000000000000000 Dynamic value relocation table
0000000000000000 Hybrid metadata pointer
0000000000000000 Guard RF address of failure-function
0000000000000000 Guard RF address of failure-function pointer
00000000 Dynamic value relocation table offset
0000 Dynamic value relocation table section
0000 Reserved2
0000000000000000 Guard RF address of stack pointer verification function pointer
00000000 Hot patching table offset
0000 Reserved3
0000000000000000 Enclave configuration pointer
0000000140003700 Volatile metadata pointer
0000000000000000 Guard EH continuation table
0 Guard EH continuation count
0000000140003248 Guard XFG address of check-function pointer
0000000140003258 Guard XFG address of dispatch-function pointer
0000000140003260 Guard XFG address of dispatch-table-function pointer
0000000140005620 CastGuard OS determined failure mode

Guard CF Function Table

Address
--------
0000000140001070 ?OpenDriverFileThreadProc@@YAKPEAX@Z (unsigned long __cdecl OpenDriverFileThreadProc(void *))
00000001400012E0 ?DlgProc@@YAHPEAUHWND__@@I_K_J@Z (int __cdecl DlgProc(struct HWND__ *,unsigned int,unsigned __int64,__int64))
00000001400017C0
0000000140001880
0000000140001890
0000000140001A20 wWinMainCRTStartup
0000000140001A70 __report_gsfailure
0000000140001EC0 __scrt_exe_initialize_mta
0000000140001EF0 _guard_check_icall_nop
0000000140002130 __scrt_unhandled_exception_filter
00000001400021D0 _RTC_Terminate
00000001400024F0 _guard_dispatch_icall_nop
0000000140002510 _guard_xfg_dispatch_icall_nop

说明如下:

  1. Guard CF function table:RVA,指向已识别到的程序中的所有函数地址(不仅仅是间接调用函数的地址)。且 dumpbin.exe 会将识别到的函数在最下面进行输出。
  2. Guard CF function count:表示 Guard CF function table 表中的函数个数。
  3. Guard Flags:CFG 相关标志。当包含 CF instrumented、FID table present 则表示已启用 CFG。

6.png

其中 GuardFlags 成员指定使用了哪些 CFG 缓解措施:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define IMAGE_GUARD_CF_INSTRUMENTED                    0x00000100 // Module performs control flow integrity checks using system-supplied support
#define IMAGE_GUARD_CFW_INSTRUMENTED 0x00000200 // Module performs control flow and write integrity checks
#define IMAGE_GUARD_CF_FUNCTION_TABLE_PRESENT 0x00000400 // Module contains valid control flow target metadata
#define IMAGE_GUARD_SECURITY_COOKIE_UNUSED 0x00000800 // Module does not make use of the /GS security cookie
#define IMAGE_GUARD_PROTECT_DELAYLOAD_IAT 0x00001000 // Module supports read only delay load IAT
#define IMAGE_GUARD_DELAYLOAD_IAT_IN_ITS_OWN_SECTION 0x00002000 // Delayload import table in its own .didat section (with nothing else in it) that can be freely reprotected
#define IMAGE_GUARD_CF_EXPORT_SUPPRESSION_INFO_PRESENT 0x00004000 // Module contains suppressed export information. This also infers that the address taken
// taken IAT table is also present in the load config.
#define IMAGE_GUARD_CF_ENABLE_EXPORT_SUPPRESSION 0x00008000 // Module enables suppression of exports
#define IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT 0x00010000 // Module contains longjmp target information
#define IMAGE_GUARD_RF_INSTRUMENTED 0x00020000 // Module contains return flow instrumentation and metadata
#define IMAGE_GUARD_RF_ENABLE 0x00040000 // Module requests that the OS enable return flow protection
#define IMAGE_GUARD_RF_STRICT 0x00080000 // Module requests that the OS enable return flow protection in strict mode
#define IMAGE_GUARD_RETPOLINE_PRESENT 0x00100000 // Module was built with retpoline support
#define IMAGE_GUARD_EH_CONTINUATION_TABLE_PRESENT 0x00400000 // Module contains EH continuation target information

#define IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_MASK 0xF0000000 // Stride of Guard CF function table encoded in these bits (additional count of bytes per element)
#define IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_SHIFT 28 // Shift to right-justify Guard CF function table stride

// GFIDS table entry flags.

#define IMAGE_GUARD_FLAG_FID_SUPPRESSED 0x01 // The containing GFID entry is suppressed
#define IMAGE_GUARD_FLAG_EXPORT_SUPPRESSED 0x02 // The containing GFID entry is export suppressed
PE 字段 对应函数 加载函数
Guard CF address of check-function pointer __guard_check_icall_fptr ntdll!LdrpValidateUserCallTarget
Guard CF address of dispatch-function pointer __guard_dispatch_icall_fptr ntdll!LdrpDispatchUserCallTarget
Guard CF function table 指向一个 RVA 数组的指针,这个数组包含程序中所有间接跳转/调用的函数地址的 RVA。
Guard CF function count RVA的个数。

1.2 VS开启CFG代码分析

如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

void cfgTest()
{
printf("we are testing CFG! \n");
}

void ( *cfgTest1[1] )(void) = { cfgTest };

int main()
{
cfgTest1[0]();
}
  1. 不开启 CFG 功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    .text:0000000140001510 // int __fastcall main()
    .text:0000000140001510 main proc near
    .text:0000000140001510
    .text:0000000140001510 push rdi
    .text:0000000140001512 sub rsp, 20h
    .text:0000000140001522 mov eax, 8
    .text:0000000140001527 imul rax, 0
    .text:000000014000152B lea rcx, cfgTest1 // void (*near * cfgTest1)(void)
    .text:0000000140001532 call qword ptr [rcx+rax]
    .text:0000000140001541 xor eax, eax
    .text:0000000140001543 add rsp, 20h
    .text:0000000140001547 pop rdi
    .text:0000000140001548 retn
    .text:0000000140001548 main endp
  2. 开启 CFG 功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    .text:0000000140001FC0 // int __fastcall main()
    .text:0000000140001FC0 main proc near
    .text:0000000140001FC0 var_18 = qword ptr -18h
    .text:0000000140001FC0
    .text:0000000140001FC0 push rdi
    .text:0000000140001FC2 sub rsp, 30h
    .text:0000000140001FD2 mov eax, 8
    .text:0000000140001FD7 imul rax, 0
    .text:0000000140001FDB lea rcx, cfgTest1 // void (*near * cfgTest1)(void)
    .text:0000000140001FE2 mov rax, [rcx+rax]
    .text:0000000140001FE6 mov [rsp+38h+var_18], rax
    .text:0000000140001FEB mov rax, [rsp+38h+var_18]
    .text:0000000140001FF0 call cs:__guard_dispatch_icall_fptr
    .text:0000000140002002 xor eax, eax
    .text:0000000140002004 add rsp, 30h
    .text:0000000140002008 pop rdi
    .text:0000000140002009 retn
    .text:0000000140002009 main endp

可以看到,加了 CFG 后,不再是直接调用间接地址 call qword ptr [rcx+rax],而是将间接地址交给 rax,然后调用 __guard_dispatch_icall_fptr 函数。

2 CFG的增强

2.1 CFG占位符函数

在程序中,在间接调用前,会将间接调用地址传给 __guard_check_icall_fptr/__guard_dispatch_icall_fptr 函数

  • 上述 VS 2017 以前的编译器生成的 CFG 函数为 __guard_check_icall_fptr 指向 _guard_check_icall_nop 占位符函数,该函数只是简单返回。

  • VS 2017 及之后的编译器生成的 CFG 函数为 __guard_dispatch_icall_fptr 指向 _guard_dispatch_icall_nop(占位符函数),占位符函数中仅仅跳转至目标函数。这是因为允许程序在不支持 CFG 的旧 Windows 版本上执行。Windows 10 之后才支持 dispatch 函数。

__guard_check_icall_fptr:

1
2
3
4
5
.mrdata:00000001801811E0 __guard_check_icall_fptr dq offset _guard_check_icall_nop
...
.text:000000018008C480 _guard_check_icall_nop proc near
.text:000000018008C480 retn 0
.text:000000018008C480 _guard_check_icall_nop endp

__guard_dispatch_icall_fptr:

1
2
3
4
5
.00cfg:0000000180185000 __guard_dispatch_icall_fptr dq offset _guard_dispatch_icall_nop
...
.text:00000001800A0A10 _guard_dispatch_icall_nop proc near
.text:00000001800A0A10 jmp rax
.text:00000001800A0A10 _guard_dispatch_icall_nop endp

2.2 CFG的支持

Bitmap 中使用连续的 2 位来表示连续 16 字节目标地址是否是有效地址。如下图:

13.png

  • 在支持 CFG 的进程加载期间,Windows 10 之前,不支持 dispatch 函数,系统会将 GuardCFCheckFunctionPointer 加载配置为 ntdll!LdrpValidateUserCallTarget 函数,而此时 GuardCFDispatchFunctionPointer 的值将保留为 0。
  • Windows 10 和 Windows Server 2016 开始,GuardCFDispatchFunctionPointer 加载配置为 ntdll!LdrpDispatchUserCallTarget。

需要说明: VS 2017 及之后的编译器生成的 CFG 函数在 Windows 10 之前的系统上运行时将不会起作用,因为 Windows 10 之前的系统不支持 dispatch 函数。

抑制调用(SuppressedCall)和抑制导出(Export suppression)是在 Windows 10 Anniversary Update and Windows Server 2016 引入的。因此 SuppressedCall、Export suppression 标记的函数在此之前的系统中使用时,进程的 Bitmap 会将这两类函数标记为有效的目标函数。

但是从 Windows 10 Anniversary Update and Windows Server 2016 之后,抑制调用(SuppressedCall)将会在进程的 Bitmap 中标记为无效的目标函数。

通用的CFG 增强的CFG
编译器 Visual Studio 2015 Visual Studio 2017及之后
操作系统 Windows 8.1~Windows 10 Anniversary Update
(1607,2016/8/2),Windows Server 2016 1703
Windows 10 1703, Windows Server 2016 1709 及之后
Bitmap(2 bits) ◦ Bit n:表示16个连续地址的首地址。(n mod 2 = 0,偶数位)
◦ Bit n+1:表示除首地址之外的15个连续地址。
CFG 验证时,仅使用Bitmap中的一位。
16字节对齐的目标地址,如果Offset(偶数)对应的Bitmap[Index]为0时,说明该目标函数无效,为1则有效。
非16字节首地址对齐的函数,如果Offset(奇数)对应的Bitmap[Index]为0时,说明该目标函数无效,为1则有效。
CFG 验证时,仅使用Bitmap中的两位。
{bit n, bit n+1}
{0, 0}:16个连续的地址不包含有效的目标函数。
{1, 0}:16个连续地址中,首地址是有效的目标函数。
{1, 1}:16个连续地址中,除首地址之外的15个地址中包含目标函数。
{0, 1}:16个连续地址属于抑制导出(Export suppressed)函数。
CFG(占位符函数) __guard_check_icall_fptr
_guard_check_icall_nop
__guard_dispatch_icall_fptr
_guard_dispatch_icall_nop
进程加载的CFG函数 ntdll!LdrpValidateUserCallTarget ntdll!LdrpDispatchUserCallTarget
是否支持SuppressedCall、Export-suppressed 否 是

14.png

在 PE32 上,必须先启用 DEP 才能启用 CFG。

2.3 抑制调用(SuppressedCall)

在程序中,可以使用关键字 DECLSPEC_GUARD_SUPPRESS 来注明一个函数不能被当做间接调用函数地址。如果在程序中间接调用该函数,程序将直接崩溃终止。

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
#include <windows.h>
#include <stdio.h>

typedef void(*func_t)();

void normal_function()
{
printf("This is a valid indirect call/jump target.\r\n");
}

DECLSPEC_GUARD_SUPPRESS void supressed_function()
{
printf("This can only be called directly.\r\n");
}

int main()
{
func_t indirect_normal_function = normal_function;
func_t indirect_supressed_function = supressed_function;

normal_function();
supressed_function();

indirect_normal_function();
indirect_supressed_function(); // will crash here if CFG is enabled

printf("This will not be printed.\r\n");
}

函数反汇编如下:

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
.text:00007FF63C881F70 // int __cdecl main(int argc, const char **argv, const char **envp)
.text:00007FF63C881F70 main proc near // CODE XREF: j_main↑j
.text:00007FF63C881F70 // DATA XREF: .pdata:00007FF63C88E018↓o
.text:00007FF63C881F70
.text:00007FF63C881F70 var_28 = qword ptr -28h
.text:00007FF63C881F70 var_20 = qword ptr -20h
.text:00007FF63C881F70 var_18 = qword ptr -18h
.text:00007FF63C881F70 var_10 = qword ptr -10h
.text:00007FF63C881F70
.text:00007FF63C881F70 push rdi
.text:00007FF63C881F72 sub rsp, 40h
.text:00007FF63C881F76 lea rax, j_?normal_function@@YAXXZ // normal_function(void)
.text:00007FF63C881F7D mov [rsp+48h+var_28], rax
.text:00007FF63C881F82 lea rax, j_?supressed_function@@YAXXZ // supressed_function(void)
.text:00007FF63C881F89 mov [rsp+48h+var_20], rax
.text:00007FF63C881F8E call j_?normal_function@@YAXXZ // normal_function(void)
.text:00007FF63C881F93 call j_?supressed_function@@YAXXZ // supressed_function(void)
.text:00007FF63C881F98 mov rax, [rsp+48h+var_28]
.text:00007FF63C881F9D mov [rsp+48h+var_18], rax
.text:00007FF63C881FA2 mov rax, [rsp+48h+var_18]
.text:00007FF63C881FA7 call cs:__guard_dispatch_icall_fptr
.text:00007FF63C881FAD mov rax, [rsp+48h+var_20]
.text:00007FF63C881FB2 mov [rsp+48h+var_10], rax
.text:00007FF63C881FB7 mov rax, [rsp+48h+var_10]
.text:00007FF63C881FBC call cs:__guard_dispatch_icall_fptr
.text:00007FF63C881FC2 lea rcx, aThisWillNotBeP // "This will not be printed.\r\n"
.text:00007FF63C881FC9 call j_printf
.text:00007FF63C881FCE xor eax, eax
.text:00007FF63C881FD0 add rsp, 40h
.text:00007FF63C881FD4 pop rdi
.text:00007FF63C881FD5 retn
.text:00007FF63C881FD5 main endp

分析:

  1. 当程序中调用 indirect_supressed_function(); 函数时,会将该函数的地址给 rax,然后调用 __guard_dispatch_icall_fptr。当前程序没有启用抑制导出,程序加载期间会将 __guard_dispatch_icall_fptr 加载为 LdrpDispatchUserCallTarget 函数。
  2. 在函数 LdrpDispatchUserCallTarget 中,动态跟踪 Bitmap 状态位为 {0, 0},则会调用 LdrpHandleInvalidUserCallTarget 让程序终止执行。

如下图,使用关键字 DECLSPEC_GUARD_SUPPRESS 注明的函数,在编译后,Dumpbin.exe 能够识别到会有一个 S 标志。

7.png

这个 S 叫做 Header flag。下面就在 PE 文件中查看 Header flag:

  1. 定位到配置目录表。得到 _IMAGE_LOAD_CONFIG_DIRECTORY64 的文件偏移地址。根据 _IMAGE_LOAD_CONFIG_DIRECTORY64 结构,GuardCFFunctionTable 偏移为 0x80,则成员的文件偏移为 0xA340 + 0x80 = 0xA3C0。

    8.png

  2. 找到 GuardCFFunctionTable,是个 VA 值 不是 RVA。如下图,可以得到该表的虚拟地址 0x00000001'40010000。

    9.png

    可以看到和 Dumpbin.exe 工具识别到的一样。

    6.png

  3. 定位到表 GuardCFFunctionTable 数据。首先看到 Imagebase = 0x00000001'40000000。则该表的 RVA = 0x10000,图下图刚好位于 .gfids 节开头,文件偏移为 0xD400。

    10.png

  4. 解析 GuardCFFunctionTable 表的 Header flag。对照 Dumpbin.exe 的解析,可以知道 GuardCFFunctionTable 表每个元素为 5 字节,最高字节就是 Header flag。

    • Header flag == 1,用 S 表示。标志该函数是 SuppressedCall 抑制调用函数。
    • Header flag == 2,用 E 表示。标志该函数是 Export-Suppressed 抑制导出函数。

    11.png

    参考 Relevance of Security Features Introduced in Modern Windows OS—P38、How Control Flow Integrity is implemented in Windows 10—GuardCFFunctionTable。

    12.png

使用 Github systeminformer 提供的 PEView.exe 工具可以查看这些函数。

Header flag 在不同编译器中所占的位长可能不同,计算公式如下:

Header flag = (GuardFlags & IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_MASK) >> IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_SHIFT。

在 Windows 10 21H2、VS 2019 下:Header flag = (0x00017500 & 0xF0000000) >> 28 = 1。

2.4 抑制导出(Export-Suppressed)

当系统开启抑制导出功能时,如果 A 程序中有导出函数,则这些导出函数都叫做抑制导出函数(Export suppressed functions)。B 进程加载了 A 程序,则 A 中的抑制导出函数在 B 进程中就开始有一些安全限制了。

如果系统用没有启用抑制导出功能,则 A 中所有的导出函数在 B 进程中都可以当做有效的间接调用目标,此时 Bitmap 中的两位状态为 {0, 1}。

抑制导出由操作系统启用和禁用(编译器没有开启/关闭开关)。

在 PowerShell 中使用如下命令启用:

1
Set-ProcessMitigation -System -Enable CFG, SuppressExports

如果系统启用了抑制导出功能,则进程在初始化期间,会将 __guard_check_icall_fptr 加载为 ntdll!LdrpValidateUserCallTargetES 函数。对于支持增强 CFG 的系统,则会将 __guard_dispatch_icall_fptr 加载为 ntdll!LdrpDispatchUserCallTargetES。

  1. 如果系统启用了抑制导出功能,在上述进程 B 中的抑制导出函数将会被视作无效的间接调用目标,此时 Bitmap 中的两位状态为 { 0, 1}(0是低位),当 ShellCode 试图在此位图状态下调用抑制导出函数时,将会触发 LdrpHandleInvalidUserCallTarget 函数。但是如果使用合法的函数如 GetProcAddress 来获取抑制导出函数的地址时,系统会将 Bitmap 中的的两位状态修改为 {1, 0},将抑制导出函数视为有效的间接目标函数,然后在进程中就可以使用该抑制导出函数了。
  2. 如果系统没有启用抑制导出功能,抑制导出函数在进程 Bitmap 中的两位状态仍然是 {0, 1},但是在 ntdll!LdrpDispatchUserCallTarget 中会将该状态的函数当做有效的目标地址。

不管是否启用抑制导出功能,抑制导出函数的地址必须是 16 字节对齐的,也就是 Bitmap 的 {0, 1} 状态的函数必须是 16 字节对齐的。可以在 ntdll!LdrpDispatchUserCallTarget 函数中进行验证。

示例:

在 3.3 的代码中增加一个导出函数:

1
extern "C" __declspec( dllexport ) int MyFunc(void);

然后使用 Dumpbin.exe 工具查看其 GuardCFFunctionTable:

15.png

  • GuardCFFunctionCount = 0x10,函数表中共 16 个函数。
  • SuppressedCall 函数的 Header flag = S,Export-Suppressed 函数的 Header flag = E。

本节参考:

3 CFG Bitmap

3.1 Bitmap原理和使用

Windows 支持两种 CFG,Windows 10 之后引入了更精细的流程(增强的CFG)。

CFG类型 使用函数 启用抑制导出(Export-Suppression) 间接地址验证流程
常规的 CFG __guard_check_icall_fptr 是:将函数 __guard_check_icall_fptr 加载为 ntdll!LdrpValidateUserCallTargetES。
否:将函数 __guard_check_icall_fptr 加载为 ntdll!LdrpValidateUserCallTarget。
验证目标,返回。
增强的 CFG __guard_dispatch_icall_fptr 是:将函数 __guard_dispatch_icall_fptr 加载为 ntdll!LdrpDispatchUserCallTargetES。
否:将函数 __guard_dispatch_icall_fptr 加载为 ntdll!LdrpDispatchUserCallTarget。
验证目标,调用目标,检查堆栈,返回。

启用抑制导出时,在 GuardFlags 下会包含 IMAGE_GUARD_CF_ENABLE_EXPORT_SUPPRESSION(0x00008000)

不管是常规还是增强的 CFG,不管是否启用抑制导出,在间接地址验证时都会使用到 Bitmap 中连续的 2 个位。

  • 如果位状态为 {0, 0},则分派可能无效。
  • 如果位状态为 {1, 0},且地址为 16 字节对齐,则分派有效。 否则,它可能无效。
  • 如果位状态为 {1, 1},并且地址不是 16 字节对齐的,则分派有效。 否则,它可能无效。
  • 如果位状态为 {0, 1},目标地址是导出抑制函数(Export-Suppressed)。

{1, 0} :1 是低位,0 是高位。

使用 2 个位的原因是:无法保证编译器生成所有函数都在 16 字节上对齐。手工编写的汇编代码和某些优化可能会违反此规则。

所以需要在原基础上增加一个位,来表示那些没有 16 字节对齐的函数,因为这些函数也是有效的间接调用函数。

所以在 CFG 位图中:

  • 偶数位对应一个 16 字节对齐的函数起始地址。
  • 奇数位对应一个非 16 字节对齐的函数起始地址( 紧接着前一个偶数位的15 个 Bytes)。

奇数位和偶数位表示同一个 16 Bytes,偶数位表示起始地址,奇数位则为起始地址后的 15 Bytes。

怎么来解释奇数位表示非 16 字节对齐的函数起始地址呢?

如函数地址是 pAddr = 0x00007ff'00402013,因为 pAddr & 0xF != 0,所以该函数起始地址不是 16 字节对齐的。

在解释这之前就必须要知道 x64 下 Windows 是如何验证函数地址是否有效的流程:

  1. Bitmap_Index = pAddr >> 9,取的函数地址的高 55 位作为定位位图的索引;
  2. Bitmap[Bitmap_Index] = Bitmap_Base + Bitmap_Index*8,定位到位图指定的条目;
  3. Offset = (pAddr >> 3) & 0x3F,取得函数地址的 bit 3 ~ bit 8 共 6 位,作为定位条目中的位,此时 Offset 为偶数;
  4. 判断函数地址是否已经 16 字节对齐,如果已经对齐的则进行如下流程;
  5. 测试 Bitmap[Bitmap_Index] 中的 bit Offset 位,如果为 1 则直接 jmp pAddr,跳转到目标函数。

如果函数地址不是 16 字节对齐的:

  1. Bitmap_Index = pAddr >> 9,取的函数地址的高 55 位作为定位位图的索引(同上);
  2. Bitmap[Bitmap_Index] = Bitmap_Base + Bitmap_Index*8,定位到位图指定的条目(同上);
  3. Offset = (pAddr >> 3) & 0x3F,取得函数地址的 bit 3 ~ bit 8 共 6 位,作为定位条目中的位(同上),此时 Offset 为奇数;
  4. 判断函数地址是否已经 16 字节对齐,如果没有对齐则进行如下流程;
  5. Offset = Offset & 3E,将最低位清零。判断上一个偶数位对应地址是否是一个有效的间接跳转地址(该偶数位为 1)。 如果是则进行下一步;
  6. Offset = Offset & 3F,测试 Bitmap[Bitmap_Index] 中的 bit Offset 位,如果为 1 则直接 jmp pAddr,跳转到目标函数。

实际上非 16 字节对齐的函数,使用的就是上面提到的 {1,1} 这个方式。

5.png

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
.text:000000018008C620 // =============== S U B R O U T I N E =======================================
.text:000000018008C620
.text:000000018008C620
.text:000000018008C620 LdrpDispatchUserCallTarget proc near // DATA XREF: LdrpCfgProcessLoadConfig+24C↑o
.text:000000018008C620 // .rdata:000000018011FD26↓o ...
.text:000000018008C620 // __unwind { // LdrpICallHandler
.text:000000018008C620 mov r11, cs:qword_1801813A8 // r11 = Bitmap数组首地址
.text:000000018008C627 mov r10, rax // rax 目标间接地址
.text:000000018008C62A shr r10, 9 // 右移9位,取得高55位,作为数组的索引
.text:000000018008C62E mov r11, [r11+r10*8] // 从数组中取得对应的元素条目 = r11
.text:000000018008C632 mov r10, rax
.text:000000018008C635 shr r10, 3 // r10 = Offset
.text:000000018008C639 test al, 0Fh // 测试地址是否已经16字节对齐
.text:000000018008C63B jnz short loc_18008C646
.text:000000018008C63D bt r11, r10
.text:000000018008C641 jnb short loc_18008C651
.text:000000018008C643 jmp rax
.text:000000018008C646 // ---------------------------------------------------------------------------
.text:000000018008C646
.text:000000018008C646 loc_18008C646: // 地址没有16字节对齐
.text:000000018008C646 btr r10, 0 // 将Offset最低位清零,然后测试上一个偶数位对应的
.text:000000018008C64B bt r11, r10 // 起始地址是否是有效的函数地址
.text:000000018008C64F jnb short loc_18008C65E // 如果上一个偶数位对应的起始地址表示的16字节不是有效地址
// 则当前奇数位表示的15字节也将不应该被设置为有效的地址
// 此时为{1,x},因为当前已经是非16字节对齐的地址,所以 x 必须为1才是有效地址
.text:000000018008C651
.text:000000018008C651 loc_18008C651: // 现在情况为:非对齐地址{1,x} 或 对齐地址{0,x}
// 对于16字节对齐地址,如果启用了抑制导出,则{0,x},x = 1,必须为1才是有效的地址
//对于非16字节对齐的地址,{1,x},x = 1,必须为1才是有效的地址
.text:000000018008C651 or r10, 1
.text:000000018008C655 bt r11, r10
.text:000000018008C659 jnb short loc_18008C65E
.text:000000018008C65B jmp rax
.text:000000018008C65E // ---------------------------------------------------------------------------
.text:000000018008C65E
.text:000000018008C65E loc_18008C65E: // CODE XREF: LdrpDispatchUserCallTarget+2F↑j
.text:000000018008C65E // LdrpDispatchUserCallTarget+39↑j
.text:000000018008C65E mov r10d, 1
.text:000000018008C664 jmp LdrpHandleInvalidUserCallTarget
.text:000000018008C664 // } // starts at 18008C620
.text:000000018008C664 LdrpDispatchUserCallTarget endp

通过以上分析,有效的函数地址:

  1. 16字节对齐的目标地址。
    • {1, x},有效地址,不会验证 x。
    • {0, 1},有效地址,启用了抑制导出。
  2. 非16字节对齐地址。
    • {1, 1},有效地址。

验证过程如下图:

3.png

解释明明 shr r10, 3 后,取得高 61 位,为什么说取得的是 Offset(6 bits) 呢?

1
2
3
4
5
6
7
.text:000000018008C632                 mov     r10, rax
.text:000000018008C635 shr r10, 3 // r10 = Offset
.text:000000018008C639 test al, 0Fh // 测试地址是否已经16字节对齐
.text:000000018008C63B jnz short loc_18008C646
.text:000000018008C63D bt r11, r10
.text:000000018008C641 jnb short loc_18008C651
.text:000000018008C643 jmp rax

是因为:bt r11, r10,是先 r10 mod 64 后再测试 r11 中的位。在 x86 下 bt ebx, eax 则会 eax mod 32,根据目的寄存器宽度来选择是模多少。

如 0101 0011b = 0x53 = 83,低 6 位为 0x13 = 19:

  • 83 mod 64 = 19

  • 19 mod 64 = 19

    所以跟直接取低 6 位来 mod 64 没有区别,6 位表示的范围为 0~63。

同理:

  • 83 mod 32 = 19

  • 19 mod 32 = 19

    跟直接取低 5 位来 mod 32 没有区别,5 位表示的范围为 0~31。

这里引出来两个问题:

  1. 为什么测试内存是否 8 字节对齐时仅使用地址低 3 位,而 16 字节对齐时仅使用地址低 4 位?

    答:8 的倍数是 8、0x10,也就是说可以判断地址 VirtualAddress mod 8 = 0 即为 8 字节对齐(只要低 3 位为 0,不管高位是多少都是 8 的整数倍)。也就是地址 VirtualAddress 低 3 位必须为 0 才表示对齐。同理 VirtualAddress mod 16 = 0 则地址的低 4 位必须为 0 才表示对齐。

    • X mod 8,等价于取 X 的低 3 位。
    • X mod 16,等价于取 X 的低 4 位。
    • X mod 32,等价于取 X 的低 5 位。
    • X mod 64,等价于取 X 的低 6 位。
  2. 为什么 Offset 在 x86 下为 5 位,在 x64 下为 6 位?

    答:在 x86 下,bt ebx, eax 是 mod 32,即取低 5 位即可,低 5 位的范围不大于 32。

    在 x64 下,bt r11, r10 是 mod 64,即取低 6 位即可,低 6 位的范围不大于 64。

CFG 通过验证间接调用的目标是否位于已知函数的开头来解决 CFl 的间接 CALL/JMP 调用。如果目标不在已知函数的开头, 则该过程将简单地终止。

  1. 常规的 CFG 流程:

    1.png

  2. 增强的 CFG 流程:

    2.png

3.2 Bitmap大小

正如您之前看到的,强制程序每隔几条指令迭代一次函数调用列表是不切实际的。 因此,不是需要线性时间 O(n) 的算法,而是性能要求指示使用 O(1) 算法来代替——使用恒定查找时间的算法,而不管表中存在多少函数。 这个常量查找时间应该尽可能短。 这种要求的明显赢家将是一个可由目标函数地址索引的数组,它指示该地址是否有效(例如简单的 BOOL)。 但是,对于 128 TB 的可能地址,这样的数组本身必须是 128 TB * sizeof(BOOL),这是一个不可接受的存储大小——比地址空间本身还大。 我们能做得更好吗?

首先,我们可以利用编译器应该在 16 字节边界上生成 x64 函数代码这一事实。 这会将所需数组的大小减少到仅 128/16 TB * sizeof(BOOL)。 但是使用整个 BOOL(最坏情况下为 4 个字节,最好情况下为 1 个字节)是极其浪费的。 我们只需要一个状态,有效或无效,只需要使用 1 位。 这使得计算结果为 8 TB / 8 = 1 TB。 然而,不幸的是, 无法保证编译器生成所有函数都在 16 字节上对齐。手工编写的汇编代码和某些优化可能会违反此规则。 因此,我们必须想出一个解决办法。 一个可能的选择是简单地再使用另一位来指示函数是否开始于下一个 15 字节的某处,而不是 16 字节起始的地址本身。 因此,我们有以下可能性:

■ {0, 0} No valid function begins inside this 16-byte boundary.

■ {1, 0} A valid function begins exactly on this aligned 16-byte address.

■ {1, 1} A valid function begins somewhere inside of this 16-byte address.

由于这种设置,16 字节对齐的函数在 Bitmap 的 2 位状态将为 {1, 0}(bit 0 = 1,bit 1 = 0),而非 16 字节对齐的函数在 Bitmap 的 2 位状态将为 {1, 1}。

考虑到这一点,我们可以应用以下公式来计算 CFG 位图的大小:

  • x86 或 x64 上的 32 位应用程序: 2 GB / 16 * 2 = 32 MB。
  • 带有 /LARGEADDRESSAWARE 的 32 位应用程序,在 x86 上以 3 GB 模式启动: 3 GB / 16 *2 = 48 MB
  • 64 位应用程序: 128 TB / 16 * 2 = 2 TB
  • 带有 /LARGEADDRESSAWARE 的 32 位应用程序,在 x64 上 4 GB / 16 *2 = 64 MB,加上保护 64 位 Ntdll.dll 和 WoW64 组件所需的 64 位位图的大小,因此 2 TB + 64MB

分配和填充 2 TB 的位(每个进程执行仍然是一个难以承受的性能开销。 即使我们已经固定了间接调用本身的执行成本,进程启动也不能允许这么长时间,2 TB 的提交内存会立即耗尽提交限制。 因此,使用了两个节省内存和提高性能的技巧。

  1. 首先,内存管理器将仅 Reserve 位图(不提交Commit)。基于这样的假设:CFG 验证函数时在对 CFG 位图访问期间,如果位状态为 {0,0} 则视为异常。 因此,只要个页面区域包含 4 KB 的位状态都是 {0, 0},这个页面在内存中仅为保留状态(Reserve)。内存管理中,只需要提交至少设置了一位 {1, X} 的页面。

  2. 接下来,如第 5 章“内存管理”的 ASLR 部分所述,系统通常只在启动时执行一次库的随机化/重定位,作为避免重复重定位的性能节省措施。 因此,支持 ASLR 的库在给定地址加载一次后,将始终在同一地址加载。 因此,这也意味着一旦为该库中的函数计算了相关的位图状态,它们在所有其他也加载相同二进制文件的进程中将是相同的。 因此,内存管理器将 CFG 位图视为页面文件支持的可共享内存区域,并且对应于共享位的物理页面仅在 RAM 中存在一次。

这降低了 RAM 中已提交页面的成本,意味着只需要计算与私有内存对应的位。 在常规应用程序中,私有内存不可执行,除非有人修补了库的写时复制情况(但这不会在图像加载时发生),因此加载应用程序的成本,如果它共享相同的库 之前推出的其他应用,几乎为零。

3.3 初始化Bitmap

  1. 系统初始化时,会调用 MiInitializeCfg 函数来初始化对 CFG 的支持。 该函数创建一个或两个内存区对象(MmCreateSection)作为保留内存,其大小依赖于平台,如前所示。 对于 32 位平台,一张位图就足够了。 对于 x64 平台,需要两个位图,一个用于 64 位进程,另一个用于 Wow64 进程(32 位应用程序)。 内存区对象的指针存储在 MiState 全局变量的子结构中。
  2. 创建进程后,相应的内存区将安全地映射到进程的地址空间中。 这里的安全意味着该部分不能被进程中运行的代码取消映射或更改其保护。 (否则,恶意代码可能只是取消映射内存、重新分配并用 1 位填充所有内容,从而有效地禁用 CFG,或者通过将区域标记为读/写来简单地修改任何位。)

4 User CFG、Kernel CFG

一、User CFG

参考PE metadata

  • 用户模式CFG只能对标记为地址空间布局随机化(ASLR)兼容的图像强制执行(由与Microsoft链接器的/DYNAMICBASE选项指定)。这是由于操作系统内部如何处理CFG,它基本上连接到ASLR基础设施。一般来说,作为第一步,CFG用户应该为他们的图像启用ASLR。工具不应假设操作系统在没有ASLR设置的情况下始终忽略CFG,而通常应该同时设置两者。
  • 请注意,要执行kCFG,需要虚拟化基础安全(VBS)。然而,即使VBS被禁用,kCFG也有一些有限的功能。这超出了这篇博客文章的范围)。

只读的CFG 位图由操作系统维护,部分位图由所有进程共享。CFG 位图中:

  • 偶数位对应一个 16 字节对齐的地址。
  • 奇数位对应 15 个非 16 字节对齐的地址。

Bitmap 的核心就是根据目标地址本身,就能判断该地址是否是有效的跳转地址。

因此,内存管理器将 CFG 位图视为页面文件支持的可共享内存区域,并且对应于共享位的物理页面仅在 RAM 中存在一次。

LdrpValidateUserCallTarget:

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
.text:000000018008C580 // =============== S U B R O U T I N E =======================================
.text:000000018008C580
.text:000000018008C580
.text:000000018008C580 LdrpValidateUserCallTarget proc near // CODE XREF: RtlGuardRestoreContext+90↑p
.text:000000018008C580 // DATA XREF: LdrpCfgProcessLoadConfig+199↑o ...
.text:000000018008C580 // __unwind { // LdrpICallHandler
.text:000000018008C580 mov rdx, cs:qword_1801813A8
.text:000000018008C587 mov rax, rcx
.text:000000018008C58A shr rax, 9
.text:000000018008C58E mov rdx, [rdx+rax*8]
.text:000000018008C592 mov rax, rcx
.text:000000018008C595 shr rax, 3
.text:000000018008C599 test cl, 0Fh
.text:000000018008C59C jnz short loc_18008C5A5
.text:000000018008C59E bt rdx, rax
.text:000000018008C5A2 jnb short loc_18008C5B0
.text:000000018008C5A4 retn
.text:000000018008C5A5 // ---------------------------------------------------------------------------
.text:000000018008C5A5
.text:000000018008C5A5 loc_18008C5A5: // CODE XREF: LdrpValidateUserCallTarget+1C↑j
.text:000000018008C5A5 btr rax, 0
.text:000000018008C5AA bt rdx, rax
.text:000000018008C5AE jnb short loc_18008C5BB
.text:000000018008C5B0
.text:000000018008C5B0 loc_18008C5B0: // CODE XREF: LdrpValidateUserCallTarget+22↑j
.text:000000018008C5B0 or rax, 1
.text:000000018008C5B4 bt rdx, rax
.text:000000018008C5B8 jnb short loc_18008C5BB
.text:000000018008C5BA retn
.text:000000018008C5BB // ---------------------------------------------------------------------------
.text:000000018008C5BB
.text:000000018008C5BB loc_18008C5BB: // CODE XREF: LdrpValidateUserCallTarget+2E↑j
.text:000000018008C5BB // LdrpValidateUserCallTarget+38↑j
.text:000000018008C5BB mov rax, rcx
.text:000000018008C5BE xor r10, r10
.text:000000018008C5C1 jmp LdrpHandleInvalidUserCallTarget
.text:000000018008C5C1 // } // starts at 18008C580
.text:000000018008C5C1 LdrpValidateUserCallTarget endp

LdrpValidateUserCallTargetES:

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
.text:000000018008C5D0 // =============== S U B R O U T I N E =======================================
.text:000000018008C5D0
.text:000000018008C5D0
.text:000000018008C5D0 LdrpValidateUserCallTargetES proc near // CODE XREF: RtlGuardRestoreContext:loc_18006DD9B↑p
.text:000000018008C5D0 // DATA XREF: LdrpCfgProcessLoadConfig+192↑o ...
.text:000000018008C5D0 // __unwind { // LdrpICallHandler
.text:000000018008C5D0 mov rdx, cs:qword_1801813A8
.text:000000018008C5D7 mov rax, rcx
.text:000000018008C5DA shr rax, 9
.text:000000018008C5DE mov rdx, [rdx+rax*8]
.text:000000018008C5E2 mov rax, rcx
.text:000000018008C5E5 shr rax, 3
.text:000000018008C5E9 test cl, 0Fh
.text:000000018008C5EC jnz short loc_18008C5F5
.text:000000018008C5EE bt rdx, rax
.text:000000018008C5F2 jnb short loc_18008C60B
.text:000000018008C5F4 retn
.text:000000018008C5F5 // ---------------------------------------------------------------------------
.text:000000018008C5F5
.text:000000018008C5F5 loc_18008C5F5: // CODE XREF: LdrpValidateUserCallTargetES+1C↑j
.text:000000018008C5F5 btr rax, 0
.text:000000018008C5FA bt rdx, rax
.text:000000018008C5FE jnb short loc_18008C60B
.text:000000018008C600 or rax, 1
.text:000000018008C604 bt rdx, rax
.text:000000018008C608 jnb short loc_18008C60B
.text:000000018008C60A retn
.text:000000018008C60B // ---------------------------------------------------------------------------
.text:000000018008C60B
.text:000000018008C60B loc_18008C60B: // CODE XREF: LdrpValidateUserCallTargetES+22↑j
.text:000000018008C60B // LdrpValidateUserCallTargetES+2E↑j ...
.text:000000018008C60B mov rax, rcx
.text:000000018008C60E xor r10, r10
.text:000000018008C611 jmp LdrpHandleInvalidUserCallTarget
.text:000000018008C611 // } // starts at 18008C5D0
.text:000000018008C611 LdrpValidateUserCallTargetES endp

LdrpDispatchUserCallTarget:

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
.text:000000018008C620 // =============== S U B R O U T I N E =======================================
.text:000000018008C620
.text:000000018008C620
.text:000000018008C620 LdrpDispatchUserCallTarget proc near // DATA XREF: LdrpCfgProcessLoadConfig+24C↑o
.text:000000018008C620 // .rdata:000000018011FD26↓o ...
.text:000000018008C620 // __unwind { // LdrpICallHandler
.text:000000018008C620 mov r11, cs:qword_1801813A8 // r11 = Bitmap数组首地址
.text:000000018008C627 mov r10, rax // rax 目标间接地址
.text:000000018008C62A shr r10, 9 // 右移9位,取得高55位,作为数组的索引
.text:000000018008C62E mov r11, [r11+r10*8] // 从数组中取得对应的元素条目 = r11
.text:000000018008C632 mov r10, rax
.text:000000018008C635 shr r10, 3 // r10 = Offset
.text:000000018008C639 test al, 0Fh // 测试地址是否已经16字节对齐
.text:000000018008C63B jnz short loc_18008C646
.text:000000018008C63D bt r11, r10
.text:000000018008C641 jnb short loc_18008C651
.text:000000018008C643 jmp rax
.text:000000018008C646 // ---------------------------------------------------------------------------
.text:000000018008C646
.text:000000018008C646 loc_18008C646: // 地址没有16字节对齐
.text:000000018008C646 btr r10, 0 // 将Offset最低位清零,然后测试上一个偶数位对应的
.text:000000018008C64B bt r11, r10 // 起始地址是否是有效的函数地址
.text:000000018008C64F jnb short loc_18008C65E
.text:000000018008C651
.text:000000018008C651 loc_18008C651: // CODE XREF: LdrpDispatchUserCallTarget+21↑j
.text:000000018008C651 or r10, 1
.text:000000018008C655 bt r11, r10
.text:000000018008C659 jnb short loc_18008C65E
.text:000000018008C65B jmp rax
.text:000000018008C65E // ---------------------------------------------------------------------------
.text:000000018008C65E
.text:000000018008C65E loc_18008C65E: // CODE XREF: LdrpDispatchUserCallTarget+2F↑j
.text:000000018008C65E // LdrpDispatchUserCallTarget+39↑j
.text:000000018008C65E mov r10d, 1
.text:000000018008C664 jmp LdrpHandleInvalidUserCallTarget
.text:000000018008C664 // } // starts at 18008C620
.text:000000018008C664 LdrpDispatchUserCallTarget endp

LdrpDispatchUserCallTargetES:

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
.text:000000018008C670 // =============== S U B R O U T I N E =======================================
.text:000000018008C670
.text:000000018008C670
.text:000000018008C670 LdrpDispatchUserCallTargetES proc near // DATA XREF: LdrpCfgProcessLoadConfig+245↑o
.text:000000018008C670 // .rdata:000000018011FD2B↓o ...
.text:000000018008C670 // __unwind { // LdrpICallHandler
.text:000000018008C670 mov r11, cs:qword_1801813A8
.text:000000018008C677 mov r10, rax
.text:000000018008C67A shr r10, 9
.text:000000018008C67E mov r11, [r11+r10*8]
.text:000000018008C682 mov r10, rax
.text:000000018008C685 shr r10, 3
.text:000000018008C689 test al, 0Fh
.text:000000018008C68B jnz short loc_18008C696
.text:000000018008C68D bt r11, r10
.text:000000018008C691 jnb short loc_18008C6AE
.text:000000018008C693 jmp rax
.text:000000018008C696 // ---------------------------------------------------------------------------
.text:000000018008C696
.text:000000018008C696 loc_18008C696: // CODE XREF: LdrpDispatchUserCallTargetES+1B↑j
.text:000000018008C696 btr r10, 0
.text:000000018008C69B bt r11, r10
.text:000000018008C69F jnb short loc_18008C6AE
.text:000000018008C6A1 or r10, 1
.text:000000018008C6A5 bt r11, r10
.text:000000018008C6A9 jnb short loc_18008C6AE
.text:000000018008C6AB jmp rax
.text:000000018008C6AE // ---------------------------------------------------------------------------
.text:000000018008C6AE
.text:000000018008C6AE loc_18008C6AE: // CODE XREF: LdrpDispatchUserCallTargetES+21↑j
.text:000000018008C6AE // LdrpDispatchUserCallTargetES+2F↑j ...
.text:000000018008C6AE mov r10d, 1
.text:000000018008C6B4 jmp LdrpHandleInvalidUserCallTarget
.text:000000018008C6B4 // } // starts at 18008C670
.text:000000018008C6B4 LdrpDispatchUserCallTargetES endp

LdrpHandleInvalidUserCallTarget:

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
.text:000000018008C4C0 // =============== S U B R O U T I N E =======================================
.text:000000018008C4C0
.text:000000018008C4C0
.text:000000018008C4C0 LdrpHandleInvalidUserCallTarget proc near
.text:000000018008C4C0 // CODE XREF: LdrpValidateUserCallTarget+41↓j
.text:000000018008C4C0 // LdrpValidateUserCallTargetES+41↓j ...
.text:000000018008C4C0
.text:000000018008C4C0 var_98 = xmmword ptr -98h
.text:000000018008C4C0 var_88 = xmmword ptr -88h
.text:000000018008C4C0 var_78 = xmmword ptr -78h
.text:000000018008C4C0 var_68 = xmmword ptr -68h
.text:000000018008C4C0 var_58 = xmmword ptr -58h
.text:000000018008C4C0 var_48 = xmmword ptr -48h
.text:000000018008C4C0 var_10 = qword ptr -10h
.text:000000018008C4C0
.text:000000018008C4C0 push r11
.text:000000018008C4C2 push r10
.text:000000018008C4C4 push r9
.text:000000018008C4C6 push r8
.text:000000018008C4C8 push rcx
.text:000000018008C4C9 push rdx
.text:000000018008C4CA push rax
.text:000000018008C4CB sub rsp, 80h
.text:000000018008C4D2 movaps [rsp+0B8h+var_98], xmm0
.text:000000018008C4D7 movaps [rsp+0B8h+var_88], xmm1
.text:000000018008C4DC movaps [rsp+0B8h+var_78], xmm2
.text:000000018008C4E1 movaps [rsp+0B8h+var_68], xmm3
.text:000000018008C4E6 movaps [rsp+0B8h+var_58], xmm4
.text:000000018008C4EB movaps [rsp+0B8h+var_48], xmm5
.text:000000018008C4F0 mov rcx, rax
.text:000000018008C4F3 call RtlpHandleInvalidUserCallTarget
.text:000000018008C4F8 movaps xmm3, [rsp+0B8h+var_68]
.text:000000018008C4FD movaps xmm2, [rsp+0B8h+var_78]
.text:000000018008C502 movaps xmm1, [rsp+0B8h+var_88]
.text:000000018008C507 movaps xmm0, [rsp+0B8h+var_98]
.text:000000018008C50C mov r10, [rsp+0B8h+var_10]
.text:000000018008C514 test r10, r10
.text:000000018008C517 jz short loc_18008C52E
.text:000000018008C519 add rsp, 80h
.text:000000018008C520 pop rax
.text:000000018008C521 pop rdx
.text:000000018008C522 pop rcx
.text:000000018008C523 pop r8
.text:000000018008C525 pop r9
.text:000000018008C527 pop r10
.text:000000018008C529 pop r11
.text:000000018008C52B jmp rax
.text:000000018008C52E // ---------------------------------------------------------------------------
.text:000000018008C52E
.text:000000018008C52E loc_18008C52E: // CODE XREF: LdrpHandleInvalidUserCallTarget+57↑j
.text:000000018008C52E movaps xmm5, [rsp+0B8h+var_48]
.text:000000018008C533 movaps xmm4, [rsp+0B8h+var_58]
.text:000000018008C538 add rsp, 80h
.text:000000018008C53F pop rax
.text:000000018008C540 pop rdx
.text:000000018008C541 pop rcx
.text:000000018008C542 pop r8
.text:000000018008C544 pop r9
.text:000000018008C546 pop r10
.text:000000018008C548 pop r11
.text:000000018008C54A retn
.text:000000018008C54A LdrpHandleInvalidUserCallTarget endp

二、 Kernel CFG

尽管使用 Visual Studio 和 /guard:cf 编译的驱动程序最终也具有与用户模式图像相同的二进制属性,但 Windows 10 的第一个版本并未对这些数据执行任何操作。与受保护的用户模式 CFG 位图不同 如果要创建一个更高、更可信的实体(内核),则没有任何东西可以真正“保护”内核 CFG 位图。 恶意利用可以简单地编辑与包含要修改的所需位的页面对应的 PTE,将其标记为读/写,然后继续进行间接调用或跳转。 因此,设置这种微不足道的可绕过缓解措施的开销根本不值得。

随着越来越多的用户启用 VBS 功能,可以再次利用 VTL 1 提供的更高安全边界。 SLAT 页表条目通过提供第二个边界来防止 PTE 页面保护更改来解决这个问题。 虽然位图对于 VTL 0 是可读的,因为 SLAT 条目被标记为只读,但如果内核攻击者试图更改 PTE 以将其标记为可读/可写,则他们无法对 SLAT 条目执行相同的操作。 因此,这将被检测为 HyperGuard 可以对其采取行动的无效 KCFG 位图访问(仅出于遥测原因——因为这些位无论如何都无法更改)。

KCFG 的实现几乎与常规 CFG 相同,除了未启用导出抑制、longjmp 支持以及为 JIT 目的动态请求额外位的能力之外。 内核驱动程序不应该做任何这些事情。 取而代之的是,这些位是根据“地址获取 IAT 表”条目(如果已设置)在位图中设置的; 每次加载驱动程序映像时,通过保护表中的常用函数条目; 以及由 MiInitializeKernelCfg 引导期间的 HAL 和内核。 如果未启用虚拟机管理程序,并且不存在 SLAT 支持,那么这些都不会被初始化,并且 Kernel CFG 将保持禁用状态。

就像在用户模式下一样,加载配置数据目录中的动态指针被更新,在启用的情况下,它将指向 __guard_check_icall 用于检查功能,而 __guard_dispatch_icall 用于增强 CFG 模式下的调度功能。 此外,名为 guard_icall_bitmap 的变量将保存位图的虚拟地址。

关于内核 CFG 的最后一个细节是,不幸的是,动态驱动程序验证程序设置将不可配置(有关驱动程序验证程序的更多信息,请参阅第 6 章,“/O 系统”),因为这需要添加动态内核挂钩并将执行重定向到 可能不在位图中的功能。在这种情况下,将返回 STATUS_VRF_CFG_ENABLED (0xC000049F),并且需要重新启动(此时可以使用 Verifier 驱动程序挂钩构建位图)。