XFG函数原型哈希值生成原理(一)

  • A+

原文地址:https://blog.quarkslab.com/how-the-msvc-compiler-generates-xfg-function-prototype-hashes.html

在本文中,我们将为读者详细介绍MVSC编译器是如何为XFG函数原型生成哈希值的。

目前,微软正在紧锣密鼓的开发Xtended Flow Guard(XFG),这是他们自己的控制流完整性实现(即Control Flow Guard,CFG)的提高版。XFG的工作原理是通过基于类型的函数原型哈希值来限制间接的控制流转移。本文将为读者深入讲解MSVC编译器是如何生成这些XFG函数原型哈希值的。

引言

2014年,微软推出了一种名为控制流防护(Control Flow Guard,CFG)的控制流完整性(Control Flow Integrity,CFI)解决方案。过去一段时间内,人们已经对CFG展开了广泛的研究。随着时间的推移,人们找到了许多绕过CFG的方法;其中一些绕过方法依赖于该防护机制的实现问题(例如与JIT编译器的集成,或容易被滥用的敏感API的可用性等),但后来这些漏洞都得到了修复。与之不同的是,有一个设计问题仍然挥之不去:CFG根本没有在有效的调用目标方面考虑粒度(granularity)问题,准确来说,任何受保护的间接调用都能够调用任何有效的调用目标。在大型的二进制文件中,有效的调用目标动辄多达数千个,这就给了攻击者提供了足够的灵活性,使得他们可以通过链接有效的C++虚函数来绕过CFG(例如,读者可以参见被称为Counterfeit Object-oriented Programming(COOP)的漏洞利用技术)。

近几年,微软一直在研究CFG的改进版本,即Xtended Flow Guard(XFG)。由于XFG能够通过类型签名检查来限制间接调用/跳转,从而提供了更精细的CFI解决方案。XFG背后的中心思想是,在编译时为那些可能成为间接调用/跳转目标的函数分配一个基于类型签名的哈希值。然后,在XFG安插了标记的间接调用点,进行哈希值检查:只允许调用具有预期签名哈希值的函数。

几周前,研究员Connor McGarr发表了一篇文章:“Exploit Development: Between a Rock and a (Xtended Flow) Guard Place: Examining XFG”,解释了XFG的工作原理,以及其潜在的弱点。这激发了我的好奇心,所以,我决定通过IDA Pro和Windbg软件,来弄清楚XFG哈希值到底是如何生成的。

在写这篇文章的时候,XFG已经出现在开发人员频道下的Windows 10 Insider Preview中。为了编译支持XFG功能的程序,我们需要使用Visual Studio 2019 Preview。

本文中的分析工作基于以下二进制文件,它们来自Visual Studio 2019 Preview, version 16.8.0 Preview 2.1:

c1.dll version 19.28.29213.0
c2.dll version 19.28.29213.0

本文将重点介绍如何为C源代码生成XFG哈希值。虽然C++代码的哈希算法应该与之相似,但我们还没有具体研究过。由于本文篇幅较大,所以,我们会将内容将分为多个部分:首先,我们先对XFG哈希算法进行简单介绍。然后,我们会介绍如何生成函数的哈希值,此后,我们将详细考察如何生成各种C类型的哈希值。最后,我们将考察应用于计算出的哈希值的最终变换,并以一个手工计算哈希值的例子来结束本文。

关于XFG哈希算法的简短介绍

下面,让我们从一个非常简单的C程序开始入手。该程序定义了一个名为FPTR([1])的函数指针类型,同时,还声明了一个函数,该函数接受两个浮点型参数,并返回一个浮点值。函数main声明了一个函数指针变量fptr,其类型为FPTR,其值为函数foo([2])的地址,其原型与FPTR类型相匹配。最后,在[3]处,调用fptr所指向的函数,并传递两个值(1.00001和2.00002)作为其参数。

#include <stdio.h>

[1] typedef float (* FPTR)(float, float);

float foo(float val1, float val2){
    printf("I received float values %f and %f\n", val1, val2);
    return (val2 - val1);
}


int main(int argc, char **argv){

[2] FPTR fptr = foo;

    printf("Calling function pointer...\n");

[3] fptr(1.00001, 2.00002);
return 0;
}

接下来,让我们通过x64 Native Tools Command Prompt for VS 2019 Preview编译上面的源代码,命令如下所示。注意,这里使用了/guard:xfg标志来启用XFG:

cl /Zi /guard:xfg example1.c

之后,我们对编译后的main函数进行反汇编,得到了如下所示的汇编代码:

main ; int __cdecl main(int argc, const char argv, const char envp)
main
main var_18 = qword ptr -18h
main var_10 = qword ptr -10h
main arg_0 = dword ptr 8
main arg_8 = qword ptr 10h
main
main mov [rsp+arg_8], rdx
main+5 mov [rsp+arg_0], ecx
main+9 sub rsp, 38h
main+D lea rax, foo
main+14 mov [rsp+38h+var_18], rax
main+19 lea rcx, aCallingFunctio ; "Calling function pointer...\n"
main+20 call printf
main+25 mov rax, [rsp+38h+var_18]
main+2A mov [rsp+38h+var_10], rax
main+2F mov r10, 99743F3270D52870h
main+39 movss xmm1, cs:[email protected]
main+41 movss xmm0, cs:[email protected]
main+49 mov rax, [rsp+38h+var_10]
main+4E call cs:__guard_xfg_dispatch_icall_fptr
main+54 xor eax, eax
main+56 add rsp, 38h
main+5A retn
main+5A main endp

我们可以在main+0x2F处看到,R10寄存器被设置为在main+0x4E之后的函数指针调用所需的基于类型的哈希值(0x99743F3270D52870)。通过这个函数指针调用的函数是foo,我们可以验证其原型哈希值(由函数开头部分的前8个字节给出)与预期的哈希值是否一致,也就是说函数foo是否是main+0x4E处间接调用的有效目标。好吧,准确地说,位于foo函数前8个字节的原型哈希值(0x99743F3270D52871)与我们在R10寄存器中看到的预期哈希值(0x99743F3270D52870)几乎完全一致,但第0位除外:

.text:0000000140001008 dq 99743F3270D52871h
foo
foo ; =============== S U B R O U T I N E ================================
foo ; float __fastcall foo(float val1, float val2)
foo foo proc near ; DATA XREF: main+D
foo
foo arg_0 = dword ptr 8
foo arg_8 = dword ptr 10h
foo
foo movss [rsp+arg_8], xmm1
foo+6 movss [rsp+arg_0], xmm0
foo+C sub rsp, 28h
foo+10 cvtss2sd xmm0, [rsp+28h+arg_8]
foo+16 cvtss2sd xmm1, [rsp+28h+arg_0]
foo+1C movaps xmm2, xmm0
foo+1F movq r8, xmm2
foo+24 movq rdx, xmm1
foo+29 lea rcx, _Format ; "I received float values %f and %f\n"
foo+30 call printf
foo+35 movss xmm0, [rsp+28h+arg_8]
foo+3B subss xmm0, [rsp+28h+arg_0]
foo+41 add rsp, 28h
foo+45 retn
foo+45 foo endp

不过,我们不用担心这个差异,因为在XFG派遣函数(ntdll!LdrpDispatchUserCallTargetXFG)的开头部分,就将R10的第0位设为1,导致预期的哈希值和函数哈希值在第0位上的差异是没有意义的:

LdrpDispatchUserCallTargetXFG LdrpDispatchUserCallTargetXFG proc near
LdrpDispatchUserCallTargetXFG ; __unwind { // LdrpICallHandler
LdrpDispatchUserCallTargetXFG or r10, 1
LdrpDispatchUserCallTargetXFG+4 test al, 0Fh
LdrpDispatchUserCallTargetXFG+6 jnz short loc_180094337
LdrpDispatchUserCallTargetXFG+8 test ax, 0FFFh
LdrpDispatchUserCallTargetXFG+C jz short loc_180094337
LdrpDispatchUserCallTargetXFG+E cmp r10, [rax-8]
LdrpDispatchUserCallTargetXFG+12 jnz short loc_180094337
LdrpDispatchUserCallTargetXFG+14 jmp rax

生成函数类型的哈希值

MSVC编译器的处理过程由两个阶段组成:前端和后端。前端是特定于语言的:它首先读取源代码,然后进行词法分析、语法解析和语义分析,最后生成IL(中间语言)代码。而后端则是特定于目标架构的:它首先读取前端生成的IL,并进行优化,最后为给定的架构生成相应的代码。

函数原型的哈希值的生成任务是由特定语言的前端来完成的。这就意味着,在编译C代码时,C语言前端(c1.dll)负责生成原型的哈希值;而在编译C++代码时,则由C++语言前端(c1xx.dll)负责这项任务。

当原型哈希值被相应的语言前端生成后,编译器后端(我们这里使用的是x64后端:c2.dll)会进行最后的转换处理。在下面的章节中,我们将详细介绍编译C代码时创建原型哈希值的详细步骤。

当编译带有/guard:xfg标志的C源代码时,编译器前端会调用c1!XFGHelper__ComputeHash_1函数,以计算正在处理的函数的原型哈希值。

函数c1!XFGHelper__ComputeHash_1将创建一个类型为XFGHelper::XFGHasher的对象,该对象负责收集正在处理的函数的类型信息,并根据收集到的类型信息生成原型哈希值。同时,对象XFGHelper::XFGHasher将使用一个std::vector的实例来存储所有需要计算哈希值的类型信息,为此,该对象提供了许多方法,供计算哈希值的过程中调用。

XFGHelper::XFGHasher::add_function_type()
XFGHelper::XFGHasher::add_type()
XFGHelper::XFGHasher::get_hash()
XFGHelper::XFGTypeHasher::compute_hash()
XFGHelper::XFGTypeHasher::hash_indirection()
XFGHelper::XFGTypeHasher::hash_tag()
XFGHelper::XFGTypeHasher::hash_primitive()

在初始化XFGHelper::XFGHasher实例后,XFGHelper__ComputeHash_1函数将调用XFGHelper::XFGHasher::add_function_type()方法,同时将XFGHelper::XFGHasher实例和一个包含需要计算哈希值的函数的类型信息的Type_t对象作为参数传递给它。

XFGHelper__ComputeHash_1 XFGHelper__ComputeHash_1 proc near
XFGHelper__ComputeHash_1
XFGHelper__ComputeHash_1 arg_0 = qword ptr 8
XFGHelper__ComputeHash_1 arg_8 = qword ptr 10h
XFGHelper__ComputeHash_1 arg_10 = qword ptr 18h
[...]
XFGHelper__ComputeHash_1+79 xorps xmm0, xmm0
XFGHelper__ComputeHash_1+7C movdqu cs:xfg_hasher, xmm0 ; zero inits xfg_hasher
[...]
XFGHelper__ComputeHash_1+B1 mov rdx, rbp ; rdx = Type_t containing function information
XFGHelper__ComputeHash_1+B4 lea rbp, xfg_hasher
XFGHelper__ComputeHash_1+BB mov rcx, rbp
XFGHelper__ComputeHash_1+BE call XFGHelper::XFGHasher::add_function_type(Type_t const ,XFGHelper::VirtualInfoFromDeclspec)
XFGHelper__ComputeHash_1+C3 mov rdx, rsi ; rdx = function->return_type (struct Type_t
)
XFGHelper__ComputeHash_1+C6 mov rcx, rbp ; this
XFGHelper__ComputeHash_1+C9 call XFGHelper::XFGHasher::add_type(Type_t const *) ; (step 5)

首先,函数XFGHelper::XFGHasher::add_function_type将获取到4条关于需要计算哈希值的函数的信息,从XFGHelper::XFGHasher::add_function_type返回后,还会通过调用XFGHelper::XFGHasher::add_type再增加一条信息,正如我们在上面的汇编代码中XFGHelper__ComputeHash_1+C9处所看到的那样。这些信息将被存储到XFGHelper::XFGHasher实例所拥有的std::vector中:

4个字节,表示函数的参数数量。
8个字节/每个函数参数,存放所述参数类型的哈希值。
1个字节,表示该函数的参数数量是否可变。
4个字节,指定函数使用的调用约定。
8个字节,表示函数的返回类型的哈希值。

组成部分1:参数数量

函数XFGHelper::XFGHasher::add_function_type首先在std::vector中添加一个DWORD,表示函数的参数数量。请注意,这个数目可能会受到函数是否为变参函数的影响,或者函数是否拥有来自__declspec的虚信息的影响(我怀疑这可能是XFG在C++中实现的一些重用代码,因此,它实际上并不适用于C代码,尽管我还没有进行实证研究)。简而言之,这里考虑的参数数量将是函数原型中声明的实际参数数量,如果函数接受的参数数量是可变的,则减1;如果函数拥有来自__declspec的虚信息,则再次减1。

XFGHelper::XFGHasher::add_function_type+18 mov rsi, [rdx+10h] ; rsi = function_info->FunctionTypeInfo
XFGHelper::XFGHasher::add_function_type+1C mov rbx, rcx
XFGHelper::XFGHasher::add_function_type+1F mov rcx, rsi ; this
XFGHelper::XFGHasher::add_function_type+22 movzx r14d, r8b
XFGHelper::XFGHasher::add_function_type+26 mov r15, rdx
XFGHelper::XFGHasher::add_function_type+29 call FunctionTypeInfo_t::RealNumberOfParameters(void)
XFGHelper::XFGHasher::add_function_type+2E mov rcx, rsi ; this
XFGHelper::XFGHasher::add_function_type+31 mov r9d, eax ; r9 = real_number_of_params
XFGHelper::XFGHasher::add_function_type+34 call FunctionTypeInfo_t::IsVarArgsFunction(void)
XFGHelper::XFGHasher::add_function_type+39 mov rdx, [rbx+8]
XFGHelper::XFGHasher::add_function_type+3D lea rbp, [r9-1] ; rbp = real_number_of_params - 1
XFGHelper::XFGHasher::add_function_type+41 test al, al ; is variadic function?
XFGHelper::XFGHasher::add_function_type+43 mov rcx, rbx
XFGHelper::XFGHasher::add_function_type+46 cmovz rbp, r9 ; if not variadic, rbp = real_number_of_params
XFGHelper::XFGHasher::add_function_type+4A test r8b, r8b ; does it have virtual info from __declspec?
XFGHelper::XFGHasher::add_function_type+4D lea r9, [rsp+48h+arg_14]
XFGHelper::XFGHasher::add_function_type+52 lea r8, [rsp+48h+arg_10]
XFGHelper::XFGHasher::add_function_type+57 lea eax, [rbp-1] ; number of params = rbp - 1
XFGHelper::XFGHasher::add_function_type+5A cmovz eax, ebp ; if no virtual info from __declspec, number of params = rbp
XFGHelper::XFGHasher::add_function_type+5D mov [rsp+48h+arg_10], eax ; value to add = number of params (dword)
XFGHelper::XFGHasher::add_function_type+5D ; [step 1]
XFGHelper::XFGHasher::add_function_type+61 call std::vector::_Insert_range(std::_Vector_const_iterator>>,uchar const ,uchar const ,std::forward_iterator_tag)

组成部分2:每个参数的类型哈希值

接下来,XFGHelper::XFGHasher::add_function_type将进入一个循环,在这个循环中,它会计算每个函数参数的类型哈希值,并将每个类型哈希值(8个字节)添加到std::vector中。

除了会对几种边缘情况(type & 0x10f == 0x103, type & 0x103 == 0x101)进行特殊处理外,对于大多数参数类型来说,它将返回到loc_180105541位置处。在该位置,如果需要的话,将会调用Type_t::clearModifiersAndQualifiers方法,以清除代表被处理的参数类型的Type_t对象的限定符(如const(0x800)和volatile(0x40)),然后,通过调用XFGHelper::XFGHasher::add_type方法,将参数类型的8字节哈希值添加到std:: vector中,这些我们可以在下面的XFGHelper::XFGHasher::add_function_type+CC处看到。如果您想知道XFGHelper::XFGHasher::add_type到底是如何计算给定Type_t的哈希值的,请参阅后文中的“为各种类型生成哈希值”一节。

最后,如果还有更多的参数需要计算哈希值,它就会跳回循环代码的开头部分。

XFGHelper::XFGHasher::add_function_type+6E loc_1801054F6:
XFGHelper::XFGHasher::add_function_type+6E mov rax, [rsi] ; rax = &function_info->params
XFGHelper::XFGHasher::add_function_type+71 mov rcx, [rax+rdi8] ; rcx = function_info->params[i] (Type_t)
XFGHelper::XFGHasher::add_function_type+75 mov edx, [rcx] ; edx = params[i].type
XFGHelper::XFGHasher::add_function_type+77 mov eax, edx
XFGHelper::XFGHasher::add_function_type+79 and eax, 10Fh
XFGHelper::XFGHasher::add_function_type+7E cmp eax, 103h ; params[i].type & 0x10f == 0x103 ?
XFGHelper::XFGHasher::add_function_type+83 jnz short loc_18010552C
XFGHelper::XFGHasher::add_function_type+85 cmp edx, 8103h ; params[i].type == 0x8103 ?
XFGHelper::XFGHasher::add_function_type+8B jz short loc_18010554E
XFGHelper::XFGHasher::add_function_type+8D mov r8d, [rcx+4]
XFGHelper::XFGHasher::add_function_type+91 lea edx, [rax-1]
XFGHelper::XFGHasher::add_function_type+94 mov rcx, [rcx+8]
XFGHelper::XFGHasher::add_function_type+98 btr r8d, 1Fh
XFGHelper::XFGHasher::add_function_type+9D call Type_t::createType(Type_t const
,uint,mod_t,bool)
XFGHelper::XFGHasher::add_function_type+A2 jmp short loc_18010554B
XFGHelper::XFGHasher::add_function_type+A4 ; --------------------------------------------------------------
XFGHelper::XFGHasher::add_function_type+A4
XFGHelper::XFGHasher::add_function_type+A4 loc_18010552C:
XFGHelper::XFGHasher::add_function_type+A4 and edx, 103h
XFGHelper::XFGHasher::add_function_type+AA cmp edx, 101h ; params[i].type & 0x103 == 0x101 ?
XFGHelper::XFGHasher::add_function_type+B0 jnz short loc_180105541
XFGHelper::XFGHasher::add_function_type+B2 call Type_t::decayFunctionType(void)
XFGHelper::XFGHasher::add_function_type+B7 jmp short loc_18010554B
XFGHelper::XFGHasher::add_function_type+B9 ; --------------------------------------------------------------
XFGHelper::XFGHasher::add_function_type+B9
XFGHelper::XFGHasher::add_function_type+B9 loc_180105541:
XFGHelper::XFGHasher::add_function_type+B9 mov edx, 8C0h ; discards qualifiers 0x800 (const) | 0x80 | 0x40 (volatile)
XFGHelper::XFGHasher::add_function_type+BE call Type_t::clearModifiersAndQualifiers(mod_t)
XFGHelper::XFGHasher::add_function_type+C3
XFGHelper::XFGHasher::add_function_type+C3 loc_18010554B:
XFGHelper::XFGHasher::add_function_type+C3 ; XFGHelper::XFGHasher::add_function_type+B7↑j
XFGHelper::XFGHasher::add_function_type+C3 mov rcx, rax
XFGHelper::XFGHasher::add_function_type+C6
XFGHelper::XFGHasher::add_function_type+C6 loc_18010554E:
XFGHelper::XFGHasher::add_function_type+C6 mov rdx, rcx ; struct Type_t *
XFGHelper::XFGHasher::add_function_type+C9 mov rcx, rbx ; this
XFGHelper::XFGHasher::add_function_type+CC call XFGHelper::XFGHasher::add_type(Type_t const *) ; adds hash of params[i] type
XFGHelper::XFGHasher::add_function_type+CC ; [step 2]
XFGHelper::XFGHasher::add_function_type+D1 inc rdi
XFGHelper::XFGHasher::add_function_type+D4 cmp rdi, rbp ; counter < number_of_params ?
XFGHelper::XFGHasher::add_function_type+D7 jb short loc_1801054F6 ; if so, loop

小结
在本文中,我们首先对XFG哈希值的生成方法进行了概述,然后,深入介绍了函数类型哈希值的生成方法。在下一篇文章中,我们将继续为读者深入分析如何为各种类型生成相应的哈希值。

(未完待续)