腾讯玄武实验室 DannyWei, lywang, FlowerCode

这是一份初步文档,当我们有新发现和更正时会进行更新。

我们分析了微软在2016年10月7日发布的Windows 10 Redstone 2 14942中加入的新安全机制Return Flow Guard。

1 保护原理

微软从Windows 8.1 Update 3之后加入了Control Flow Guard,用于阻止对间接跳转函数指针的篡改。CFG通过在每个间接跳转前检查函数指针合法性来实现,但是这种方式并不能阻止篡改栈上的返回地址或者Return Oriented Programming。

本次加入的新安全机制RFG,会在每个函数头部将返回地址保存到fs:[rsp](Thread Control Stack),并在函数返回前将其与栈上返回地址进行比较,从而有效阻止了这些攻击方式。

开启RFG需要操作系统和编译器的双重支持,在编译阶段,编译器会以nop指令的形式在目标函数中预留出相应的指令空间。当目标可执行文件在支持并开启RFG的系统上运行时,预留的指令空间会在加载阶段被替换为RFG指令,最终实现对返回地址的检测。当在不支持RFG的操作系统上运行时,这些nop指令则不会影响程序的执行流程。

RFG与GS最大的区别是,攻击者可以通过信息泄漏、暴力猜测等方式获取栈cookie从而绕过GS保护,而RFG是将当前的函数返回地址写入了攻击者不可控的Thread Control Stack,从而进一步提高了攻击难度。

2 控制开关

2.1 内核中的MmEnableRfg全局变量

该变量由注册表键值控制。该键值位于:
\Registry\Machine\SYSTEM\CurrentControlSet\Control\Session Manager\kernel
EnableRfg : REG_DWORD

2.1.1 初始化过程

KiSystemStartup -> KiInitializeKernel -> InitBootProcessor -> CmGetSystemControlValues

2.2 映像文件标志位

标志位存储在IMAGE_LOAD_CONFIG_DIRECTORY64结构中。
GuardFlags中的标志位指示该文件的RFG支持情况。

1
2
3
#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

2.3 进程标志位

2.3.1 外部读取

通过Win32 API GetProcessMitigationPolicy可以获取RFG的开启状态。

1
2
3
4
5
typedef enum _PROCESS_MITIGATION_POLICY {
// ...
ProcessReturnFlowGuardPolicy = 11
// ...
} PROCESS_MITIGATION_POLICY, *PPROCESS_MITIGATION_POLICY;

2.3.2 结构定义

1
2
3
4
5
6
7
8
9
10
typedef struct _PROCESS_MITIGATION_RETURN_FLOW_GUARD_POLICY {
union {
DWORD Flags;
struct {
DWORD EnableReturnFlowGuard : 1;
DWORD StrictMode : 1;
DWORD ReservedFlags : 30;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
} PROCESS_MITIGATION_RETURN_FLOW_GUARD_POLICY, *PPROCESS_MITIGATION_RETURN_FLOW_GUARD_POLICY;

3 新增的PE结构

3.1 IMAGE_LOAD_CONFIG_DIRECTORY64

启用RFG的PE文件中,Configuration Directory的IMAGE_LOAD_CONFIG_DIRECTORY64结构新增了如下字段:

1
2
3
4
ULONGLONG  GuardRFFailureRoutine; 
ULONGLONG GuardRFFailureRoutineFunctionPointer;
DWORD DynamicValueRelocTableOffset;
WORD DynamicValueRelocTableSection;

两个指针(16字节)
GuardRFFailureRoutine是_guard_ss_verify_failure函数的虚拟地址;GuardRFFailureRoutineFunctionPointer是
_guard_ss_verify_failure_fptr函数指针的虚拟地址,默认指向_guard_ss_verify_failure_default函数。

地址信息(6字节)
DynamicValueRelocTableOffset记录了动态重定位表相对重定位目录的偏移;
DynamicValueRelocTableSection记录了动态重定位表所在的节索引。

3.2 IMAGE_DYNAMIC_RELOCATION_TABLE

启用RFG的PE文件在普通的重定位表之后还有一张动态重定位表(IMAGE_DYNAMIC_RELOCATION_TABLE),结构如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _IMAGE_DYNAMIC_RELOCATION_TABLE {
DWORD Version;
DWORD Size;
// IMAGE_DYNAMIC_RELOCATION DynamicRelocations[0];
} IMAGE_DYNAMIC_RELOCATION_TABLE, *PIMAGE_DYNAMIC_RELOCATION_TABLE;

typedef struct _IMAGE_DYNAMIC_RELOCATION {
PVOID Symbol;
DWORD BaseRelocSize;
// IMAGE_BASE_RELOCATION BaseRelocations[0];
} IMAGE_DYNAMIC_RELOCATION, *PIMAGE_DYNAMIC_RELOCATION;

typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
// WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;

其中,IMAGE_BASE_RELOCATION结构的Symbol指明了存储的项目里记录的是函数头还是函数尾的信息,定义如下:

1
2
#define IMAGE_DYNAMIC_RELOCATION_GUARD_RF_PROLOGUE 0x00000001
#define IMAGE_DYNAMIC_RELOCATION_GUARD_RF_EPILOGUE 0x00000002

而最后的IMAGE_BASE_RELOCATION是常规的重定位表项,记录了需要替换的nop指令的虚拟地址和偏移,每一项的绝对地址可以通过ImageBase + VirtualAddress + TypeOffset算出。

4 指令替换

4.1 编译阶段

在启用了RFG的映像中,编译器会在目标函数的函数序和函数尾中预留出相应的指令空间,这些空间以nop指令的形式进行填充。

插入的函数头(9字节)

函数头会被插入类似如下的指令序列,长度为9字节:

1
2
xchg    ax, ax
nop dword ptr [rax+00000000h]

追加的函数尾(15字节)

函数尾会在rent指令后追加15字节指令空间,如下:

1
2
3
retn
db 0Ah dup(90h)
retn

为了减少额外开销,编译器还插入了一个名为_guard_ss_common_verify_stub的函数。编译器将大多数函数以jmp到该stub函数的形式结尾,而不是在每个函数尾部都插入nop指令。这个stub函数已经预置了会被内核在运行时替换成RFG函数尾的nop指令,最后以retn指令结尾,如下:

1
2
3
4
5
__guard_ss_common_verify_stub proc near
retn
__guard_ss_common_verify_stub endp
db 0Eh dup(90h)
retn

4.2 加载阶段

内核在加载启用了RFG的映像时,在创建映像的section过程中会通过nt!MiPerformRfgFixups,根据动态重定位表(IMAGE_DYNAMIC_RELOCATION_TABLE)中的信息,获取需要替换的起始指令地址,对映像中预留的nop指令序列进行替换。

替换的函数头(9字节)

使用MiRfgInstrumentedPrologueBytes替换函数头中的9字节nop指令,MiRfgInstrumentedPrologueBytes对应的指令序列如下:

1
2
mov     rax, [rsp]
mov fs:[rsp], rax

替换的函数尾(15字节)

使用MiRfgInstrumentedEpilogueBytes,结合目标映像IMAGE_LOAD_CONFIG_DIRECTORY64结构中的__guard_ss_verify_failure()地址,对函数尾的nop指令进行替换,长度为15字节,替换后的函数尾如下:

1
2
3
4
mov     r11, fs:[rsp]
cmp r11, [rsp]
jnz _guard_ss_verify_failure
retn

5 Thread Control Stack

为实现RFG,微软引入了Thread Control Stack概念,并在x64架构上重新使用了FS段寄存器。受保护进程的线程在执行到mov fs:[rsp], rax指令时,FS段寄存器会指向当前线程在线程控制栈上的ControlStackLimitDelta,将rax写入rsp偏移处。

进程内的所有用户模式线程使用Thread Control Stack上的不同内存区域(Shadow Stack),可以通过遍历进程的VAD自平衡二叉树(self-balancing AVL tree)获取描述进程Thread Control Stack的_MMVAD结构,索引的过程及结构体如下:

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
typedef struct _MMVAD {
/* 0x0000 */ struct _MMVAD_SHORT Core;
union {
union {
/* 0x0040 */ unsigned long LongFlags2;
/* 0x0040 */ struct _MMVAD_FLAGS2 VadFlags2;
}; /* size: 0x0004 */
} /* size: 0x0004 */ u2;
/* 0x0044 */ long Padding_;
/* 0x0048 */ struct _SUBSECTION* Subsection;
/* 0x0050 */ struct _MMPTE* FirstPrototypePte;
/* 0x0058 */ struct _MMPTE* LastContiguousPte;
/* 0x0060 */ struct _LIST_ENTRY ViewLinks;
/* 0x0070 */ struct _EPROCESS* VadsProcess;
union {
union {
/* 0x0078 */ struct _MI_VAD_SEQUENTIAL_INFO SequentialVa;
/* 0x0078 */ struct _MMEXTEND_INFO* ExtendedInfo;
}; /* size: 0x0008 */
} /* size: 0x0008 */ u4;
/* 0x0080 */ struct _FILE_OBJECT* FileObject;
} MMVAD, *PMMVAD; /* size: 0x0088 */

typedef struct _MMVAD_SHORT {
union {
/* 0x0000 */ struct _RTL_BALANCED_NODE VadNode;
/* 0x0000 */ struct _MMVAD_SHORT* NextVad;
}; /* size: 0x0018 */
/* 0x0018 */ unsigned long StartingVpn;
/* 0x001c */ unsigned long EndingVpn;
/* 0x0020 */ unsigned char StartingVpnHigh;
/* 0x0021 */ unsigned char EndingVpnHigh;
/* 0x0022 */ unsigned char CommitChargeHigh;
/* 0x0023 */ unsigned char SpareNT64VadUChar;
/* 0x0024 */ long ReferenceCount;
/* 0x0028 */ struct _EX_PUSH_LOCK PushLock;
union {
union {
/* 0x0030 */ unsigned long LongFlags;
/* 0x0030 */ struct _MMVAD_FLAGS VadFlags;
}; /* size: 0x0004 */
} /* size: 0x0004 */ u;
union {
union {
/* 0x0034 */ unsigned long LongFlags1;
/* 0x0034 */ struct _MMVAD_FLAGS1 VadFlags1;
}; /* size: 0x0004 */
} /* size: 0x0004 */ u1;
/* 0x0038 */ struct _MI_VAD_EVENT_BLOCK* EventList;
} MMVAD_SHORT, *PMMVAD_SHORT; /* size: 0x0040 */

typedef struct _RTL_BALANCED_NODE {
union {
/* 0x0000 */ struct _RTL_BALANCED_NODE* Children[2];
struct {
/* 0x0000 */ struct _RTL_BALANCED_NODE* Left;
/* 0x0008 */ struct _RTL_BALANCED_NODE* Right;
}; /* size: 0x0010 */
}; /* size: 0x0010 */
union {
/* 0x0010 */ unsigned char Red : 1; /* bit position: 0 */
/* 0x0010 */ unsigned char Balance : 2; /* bit position: 0 */
/* 0x0010 */ unsigned __int64 ParentValue;
}; /* size: 0x0008 */
} RTL_BALANCED_NODE, *PRTL_BALANCED_NODE; /* size: 0x0018 */

typedef struct _RTL_AVL_TREE {
/* 0x0000 */ struct _RTL_BALANCED_NODE* Root;
} RTL_AVL_TREE, *PRTL_AVL_TREE; /* size: 0x0008 */

typedef struct _EPROCESS {

struct _RTL_AVL_TREE VadRoot;

}

由以上可知,可以通过_EPROCESS.VadRoot遍历VAD二叉树。如果_MMVAD.Core.VadFlags.RfgControlStack标志位被置1,则当前_MMVAD描述了Thread Control Stack的虚拟内存范围(_MMVAD.Core的StartingVpn, EndingVpn, StartingVpnHigh, EndingVpnHigh),相关的结构体如下:

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
typedef struct _MMVAD_FLAGS {
struct /* bitfield */ {
/* 0x0000 */ unsigned long VadType : 3; /* bit position: 0 */
/* 0x0000 */ unsigned long Protection : 5; /* bit position: 3 */
/* 0x0000 */ unsigned long PreferredNode : 6; /* bit position: 8 */
/* 0x0000 */ unsigned long NoChange : 1; /* bit position: 14 */
/* 0x0000 */ unsigned long PrivateMemory : 1; /* bit position: 15 */
/* 0x0000 */ unsigned long PrivateFixup : 1; /* bit position: 16 */
/* 0x0000 */ unsigned long ManySubsections : 1; /* bit position: 17 */
/* 0x0000 */ unsigned long Enclave : 1; /* bit position: 18 */
/* 0x0000 */ unsigned long DeleteInProgress : 1; /* bit position: 19 */
/* 0x0000 */ unsigned long PageSize64K : 1; /* bit position: 20 */
/* 0x0000 */ unsigned long RfgControlStack : 1; /* bit position: 21 */
/* 0x0000 */ unsigned long Spare : 10; /* bit position: 22 */
}; /* bitfield */
} MMVAD_FLAGS, *PMMVAD_FLAGS; /* size: 0x0004 */

typedef struct _MI_VAD_EVENT_BLOCK {
/* 0x0000 */ struct _MI_VAD_EVENT_BLOCK* Next;
union {
/* 0x0008 */ struct _KGATE Gate;
/* 0x0008 */ struct _MMADDRESS_LIST SecureInfo;
/* 0x0008 */ struct _RTL_BITMAP_EX BitMap;
/* 0x0008 */ struct _MMINPAGE_SUPPORT* InPageSupport;
/* 0x0008 */ struct _MI_LARGEPAGE_IMAGE_INFO LargePage;
/* 0x0008 */ struct _ETHREAD* CreatingThread;
/* 0x0008 */ struct _MI_SUB64K_FREE_RANGES PebTebRfg;
/* 0x0008 */ struct _MI_RFG_PROTECTED_STACK RfgProtectedStack;
}; /* size: 0x0038 */
/* 0x0040 */ unsigned long WaitReason;
/* 0x0044 */ long __PADDING__[1];
} MI_VAD_EVENT_BLOCK, *PMI_VAD_EVENT_BLOCK; /* size: 0x0048 */

typedef struct _MI_RFG_PROTECTED_STACK {
/* 0x0000 */ void* ControlStackBase;
/* 0x0008 */ struct _MMVAD_SHORT* ControlStackVad;
} MI_RFG_PROTECTED_STACK, *PMI_RFG_PROTECTED_STACK; /* size: 0x0010 */

创建开启RFG保护的线程时,会调用 nt!MmSwapThreadControlStack设置线程的ETHREAD.UserFsBase。具体做法是通过MiLocateVadEvent检索对应的_MMVAD,然后通过如下计算设置线程的ETHREAD.UserFsBase:

1
2
3
ControlStackBase = MMVAD.Core.EventList.RfgProtectedStack.ControlStackBase
ControlStackLimitDelta = ControlStackBase - (MMVAD.Core.StartingVpnHigh * 0x100000000 + MMVAD.Core.StartingVpn ) * 0x1000
ETHREAD.UserFsBase = ControlStackLimitDelta

不同线程在Thread Control Stack上对应的Shadow Stack内存范围不同,如果当前线程对应的Shadow Stack内存范围是ControlStackBase ~ ControlStackLimit,则ControlStackLimit = _KTHREAD.StackLimit + ControlStackLimitDelta ,因此UserFsBase中实际存放的是ControlStackLimit与StackLimit的偏移值。这样,多个线程访问Shadow Stack时,使用的是Thread Control Stack上不同的内存区域,实际访问的内存地址为ETHREAD.UserFsBase + rsp。

6 实际使用

我们编写了一个简单的yara签名来检测带有RFG插桩的文件。

1
2
3
4
5
6
7
8
9
10
rule rfg {
strings:
$pe = { 4d 5a }
$a = { 66 90 0F 1F 80 00 00 00 00 }
$b = { C3 90 90 90 90 90 90 90 90 90 90 90 90 90 90 C3 }
$c = { E9 ?? ?? ?? ?? 90 90 90 90 90 90 90 90 90 90 E9 }

condition:
$pe at 0 and $a and ($b or $c)
}

用法:

1
yara64.exe -r -f rfg.yara %SystemRoot%

从结果中可以看出,在这个版本的Windows里,大部分系统文件已经带有RFG支持了。
这里我们用IDA Pro和WinDbg检查一个带RFG的calc.exe。

1
2
3
.text:000000014000176C wWinMain
.text:000000014000176C xchg ax, ax
.text:000000014000176E nop dword ptr [rax+00000000h]

动态指令替换之前的入口点

1
2
3
4
0:000> u calc!wWinMain
calc!wWinMain:
00007ff7`91ca176c 488b0424 mov rax,qword ptr [rsp]
00007ff7`91ca1770 6448890424 mov qword ptr fs:[rsp],rax

动态指令替换之后的入口点

7 参考资料

Exploring Control Flow Guard in Windows 10 Jack Tang, Trend Micro Threat Solution Team
http://sjc1-te-ftp.trendmicro.com/assets/wp/exploring-control-flow-guard-in-windows10.pdf