34

Return Flow Guard – 腾讯玄武实验室

 6 years ago
source link: http://xlab.tencent.com/cn/2016/11/02/return-flow-guard/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

腾讯玄武实验室 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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK