腾讯玄武实验室 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 Teamhttp://sjc1-te-ftp.trendmicro.com/assets/wp/exploring-control-flow-guard-in-windows10.pdf