Return Flow Guard | xxxReturn Flow Guard – xxx
菜单

Return Flow Guard

十一月 2, 2016 - 腾讯玄武实验室

腾讯玄武实验室 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支持情况。

#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的开启状态。

typedef enum _PROCESS_MITIGATION_POLICY { // ...     ProcessReturnFlowGuardPolicy = 11 // ... } PROCESS_MITIGATION_POLICY, *PPROCESS_MITIGATION_POLICY; 

2.3.2 结构定义

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结构新增了如下字段:

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),结构如下。

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指明了存储的项目里记录的是函数头还是函数尾的信息,定义如下:

#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字节:

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

追加的函数尾(15字节)

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

retn db 0Ah dup(90h) retn 

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

__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对应的指令序列如下:

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

替换的函数尾(15字节)

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

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结构,索引的过程及结构体如下:

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),相关的结构体如下:

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:

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插桩的文件。

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) } 

用法:

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

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

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

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

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


Notice: Undefined variable: canUpdate in /var/www/html/wordpress/wp-content/plugins/wp-autopost-pro/wp-autopost-function.php on line 51