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

  • A+

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

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

在上一篇文章中,我们为读者深入分析各种类型的哈希值是如何生成的,在本文中,我们将通过手工方式计算一个XFG型哈希值,以验证我们是否真正理解了XFG型哈希值的生成方式。

(接上文)

生成联合体/结构体/枚举类型的哈希值

当处理联合体/结构体/枚举类型时,在向std::vector中写入一个值为2的字节后,函数XFGHelper::XFGTypeHasher::compute_hash会调用XFGHelper::XFGTypeHasher::hash_tag,通过RDX传递一个指向Symbol_t对象的指针,该对象包含联合体/结构体/枚举类型的人类可读名称。

XFGHelper::XFGTypeHasher::compute_hash+AE mov rdx, [rdi+10h] ; struct Symbol_t *
XFGHelper::XFGTypeHasher::compute_hash+B2 mov rcx, rbx ; this
XFGHelper::XFGTypeHasher::compute_hash+B5 call XFGHelper::XFGTypeHasher::hash_tag(Symbol_t *)

之后,XFGHelper::XFGTypeHasher::hash_tag将调用XFGHelper::XFGHasher::add_string,将联合体/结构体/枚举变量的名称添加到std::vector中(如果联合体/结构体/枚举已经被命名的话)。相反,如果联合体/结构体/枚举是匿名的,它就会把字符串“”添加到std::vector中。

XFGHelper::XFGHasher::add_string public: void XFGHelper::XFGHasher::add_string(class Symbol_t ) proc near
XFGHelper::XFGHasher::add_string sub rsp, 38h
XFGHelper::XFGHasher::add_string+4 cmp byte ptr [rdx+11h], 4
XFGHelper::XFGHasher::add_string+8 jnz short loc_18010568B
XFGHelper::XFGHasher::add_string+A mov r8, [rdx]
XFGHelper::XFGHasher::add_string+D mov eax, [r8+10h]
XFGHelper::XFGHasher::add_string+11 shr eax, 16h
XFGHelper::XFGHasher::add_string+14 test al, 1 ; union/struct/enum is named?
XFGHelper::XFGHasher::add_string+16 jz short loc_180105674
XFGHelper::XFGHasher::add_string+18 lea r9, aUnnamed+9 ; ""
XFGHelper::XFGHasher::add_string+1F lea r8, aUnnamed ; ""
XFGHelper::XFGHasher::add_string+26
XFGHelper::XFGHasher::add_string+26 loc_180105666:
XFGHelper::XFGHasher::add_string+26 mov rdx, [rcx+8]
XFGHelper::XFGHasher::add_string+2A call std::vector::_Insert_range(std::_Vector_const_iterator>>,uchar const
,uchar const *,std::forward_iterator_tag)
XFGHelper::XFGHasher::add_string+2F add rsp, 38h
XFGHelper::XFGHasher::add_string+33 retn
XFGHelper::XFGHasher::add_string+34 ; ---------------------------------------------------------------------------
XFGHelper::XFGHasher::add_string+34
XFGHelper::XFGHasher::add_string+34 loc_180105674:
XFGHelper::XFGHasher::add_string+34 mov r8, [r8+8] ; r8 = union/struct/enum name
XFGHelper::XFGHasher::add_string+38 or r9, 0FFFFFFFFFFFFFFFFh
XFGHelper::XFGHasher::add_string+3C
XFGHelper::XFGHasher::add_string+3C loc_18010567C:
XFGHelper::XFGHasher::add_string+3C inc r9
XFGHelper::XFGHasher::add_string+3F cmp byte ptr [r8+r9], 0
XFGHelper::XFGHasher::add_string+44 jnz short loc_18010567C
XFGHelper::XFGHasher::add_string+46 add r9, r8 ; r9 points to end of string
XFGHelper::XFGHasher::add_string+49 jmp short loc_180105666

同时,在函数XFGHelper::XFGTypeHasher::hash_tag中有一个代码分支,可以在某些条件下给需要计算哈希值的数据添加字符串“”。我没有对此进行太多的研究,但它可能用于处理作用域为“local”的联合体/结构体/枚举的情况。

XFGHelper::XFGTypeHasher::hash_tag+4D mov rbx, [rbx+18h]
XFGHelper::XFGTypeHasher::hash_tag+51 test rbx, rbx
XFGHelper::XFGTypeHasher::hash_tag+54 jnz short loc_180105A16
XFGHelper::XFGTypeHasher::hash_tag+56 jmp short loc_180105A76
XFGHelper::XFGTypeHasher::hash_tag+58 ; ---------------------------------------------------------------------------
XFGHelper::XFGTypeHasher::hash_tag+58
XFGHelper::XFGTypeHasher::hash_tag+58 loc_180105A5C:
XFGHelper::XFGTypeHasher::hash_tag+58 mov rdx, [rdi+8]
XFGHelper::XFGTypeHasher::hash_tag+5C lea r9, aLocal+7 ; ""
XFGHelper::XFGTypeHasher::hash_tag+63 lea r8, aLocal ; ""
XFGHelper::XFGTypeHasher::hash_tag+6A mov rcx, rdi
XFGHelper::XFGTypeHasher::hash_tag+6D call std::vector::_Insert_range(std::_Vector_const_iterator>>,uchar const ,uchar const ,std::forward_iterator_tag)

生成基本类型的哈希值

在处理基本类型(那些在其Type_t值中既没有设为0x100、0x200,也没有设为0x400的类型)时,在向std::vector写入一个值为1的字节后,函数XFGHelper::XFGTypeHasher::compute_hash将会调用XFGHelper::XFGTypeHasher::hash_primitive函数。

实际上,XFGHelper::XFGTypeHasher::hash_primitive基本上就是一个大型的switch语句,用于将Type_t值映射为代表基本类型的一组常量;然后,将得到的常量(一个单字节)添加到std::vector中。例如,对于以Type_t 0x26表示的float类型,这个函数会将一个值为0x0B的字节添加到std::vector中。

XFGHelper::XFGTypeHasher::hash_primitive private: void XFGHelper::XFGTypeHasher::hash_primitive(class Type_t const ) proc near
XFGHelper::XFGTypeHasher::hash_primitive sub rsp, 38h
XFGHelper::XFGTypeHasher::hash_primitive+4 mov eax, [rdx]
XFGHelper::XFGTypeHasher::hash_primitive+6 mov r10, rcx
XFGHelper::XFGTypeHasher::hash_primitive+9 and eax, 1FFFh
XFGHelper::XFGTypeHasher::hash_primitive+E cmp eax, 40h ; '@'
XFGHelper::XFGTypeHasher::hash_primitive+11 ja loc_1801059D4
XFGHelper::XFGTypeHasher::hash_primitive+17 jz loc_1801059D0 ; case 0x40:
XFGHelper::XFGTypeHasher::hash_primitive+1D cmp eax, 1Ah
XFGHelper::XFGTypeHasher::hash_primitive+20 ja short loc_18010599E
[...]
XFGHelper::XFGTypeHasher::hash_primitive+6E loc_18010599E:
XFGHelper::XFGTypeHasher::hash_primitive+6E sub eax, 1Bh ; case 0x1B:
XFGHelper::XFGTypeHasher::hash_primitive+71 jz short loc_1801059CC
XFGHelper::XFGTypeHasher::hash_primitive+73 sub eax, 1 ; case 0x1C:
XFGHelper::XFGTypeHasher::hash_primitive+76 jz short loc_1801059C8
XFGHelper::XFGTypeHasher::hash_primitive+78 sub eax, 2 ; case 0x1E:
XFGHelper::XFGTypeHasher::hash_primitive+7B jz short loc_1801059C4
XFGHelper::XFGTypeHasher::hash_primitive+7D sub eax, 8 ; case 0x26 (float):
XFGHelper::XFGTypeHasher::hash_primitive+80 jz short loc_1801059C0
[...]
XFGHelper::XFGTypeHasher::hash_primitive+90 loc_1801059C0:
XFGHelper::XFGTypeHasher::hash_primitive+90 mov cl, 0Bh ; primitive_type = 0xB (float)
XFGHelper::XFGTypeHasher::hash_primitive+92 jmp short loc_1801059DE
[...]
XFGHelper::XFGTypeHasher::hash_primitive+AE loc_1801059DE:
XFGHelper::XFGTypeHasher::hash_primitive+AE mov rdx, [r10+8]
XFGHelper::XFGTypeHasher::hash_primitive+B2 lea r9, [rsp+38h+arg_9]
XFGHelper::XFGTypeHasher::hash_primitive+B7 mov [rsp+38h+arg_8], cl ; value to add: primitive_type
XFGHelper::XFGTypeHasher::hash_primitive+BB lea r8, [rsp+38h+arg_8]
XFGHelper::XFGTypeHasher::hash_primitive+C0 mov rcx, r10
XFGHelper::XFGTypeHasher::hash_primitive+C3 call std::vector::_Insert_range(std::_Vector_const_iterator>>,uchar const
,uchar const *,std::forward_iterator_tag)

对哈希值的最后一步转换

到目前为止,我们已经深入地描述了C编译器前端如何计算函数原型的哈希值,以供XFG机制使用。如果我们要用一些类似于Python的伪代码来总结的话,那么函数的哈希值的计算过程如下所示:

hash = sha256(number_of_params +

          type_hash(params[0]) +
          type_hash(params[...]) +
          type_hash(params[n]) +

          is_variadic +

          calling_convention +

          type_hash(return_type)
    )[0:8]

由于XFG使用的函数哈希值只截取了SHA256摘要的一部分(只保留前8个字节),因此,与完整的SHA256哈希相比,它们的抗碰撞能力明显下降了,但我们可以期望不同的XFG哈希值可以合理地保持哈希函数的雪崩效应,并且看起来毫无关联,对吗?

然而,如果您考察一组给定二进制代码的XFG哈希值(我选的是ntdll.dll),您就会发现,这看起来绝对不像是64位的熵:

function 0x180001a30 -> prototype hash: 0x8d952e0d365aa071
function 0x180001b50 -> prototype hash: 0xe2198f4a3c515871
function 0x180001dc0 -> prototype hash: 0xbeac2e06165fc871
function 0x180001de0 -> prototype hash: 0xfaec0e7f70d92371
function 0x180001fc0 -> prototype hash: 0xc5d11eb750d75871
function 0x180002030 -> prototype hash: 0xe8bcaf9a10586871
function 0x180002040 -> prototype hash: 0xc3110f087e584871
function 0x1800020b0 -> prototype hash: 0xdbc1261858d2f871
function 0x1800023a0 -> prototype hash: 0xda690f3e36531a71

这背后的原因是,由编译器前端(c1.dll)生成的截取型SHA256哈希值在实际写入生成的对象文件之前,会接受编译器后端(c2.dll)的最终转换处理。准确地说,c2.dll中的XfgIlVisitor::visit_I_XFG_HASH函数,会使用两个掩码对截取型SHA256哈希值做进一步的处理:

XfgIlVisitor::visit_I_XFG_HASH(tagILMAP )+5B mov rcx, 8000060010500070h
XfgIlVisitor::visit_I_XFG_HASH(tagILMAP
)+65 mov r13, 0FFFDBFFF7EDFFB70h
[...]
XfgIlVisitor::visit_I_XFG_HASH(tagILMAP )+E9 mov rdx, [rax] ; rdx = 8 bytes of SHA256 hash
XfgIlVisitor::visit_I_XFG_HASH(tagILMAP
)+EC add rax, 8
XfgIlVisitor::visit_I_XFG_HASH(tagILMAP )+F0 and rdx, r13 ; hash &= 0FFFDBFFF7EDFFB70h
XfgIlVisitor::visit_I_XFG_HASH(tagILMAP
)+F3 mov [rbx], rax
XfgIlVisitor::visit_I_XFG_HASH(tagILMAP )+F6 or rdx, rcx ; hash |= 8000060010500070h
XfgIlVisitor::visit_I_XFG_HASH(tagILMAP
)+F9 mov ecx, r9d ; this
XfgIlVisitor::visit_I_XFG_HASH(tagILMAP )+FC call XFG::TiSetHash(ulong,unsigned __int64,tagMOD )

这就是为什么XFG型哈希值看起来不完全随机的原因,尽管它是基于SHA256算法的。不过,我还不清除为什么要应用这些掩码。

“手撕”XFG型哈希值

为了验证我们是否已经正确理解了XFG型哈希值是如何产生的,让我们试着手工计算一个XFG型哈希值。假设我们想计算一个函数的哈希值,其原型如下所示:

void memcpy(
void
dest,
const void *src,
size_t count
);
我们需要找出组成函数原型的5段数据:

参数的数量;
每个参数的类型哈希值;
是否为变参函数;
调用约定;
返回类型的类型哈希值。

其中,组成部分1、3、4都很简单:

参数数量:用DWORD表示,其值为3;

是否为变参函数:用1个字节表示,其值为0;
调用约定:默认值(用DWORD表示,其值为0x201 & 0xF == 0x1)。

下面,让我们来计算一下比较复杂的部分:每个参数的类型哈希值,以及返回类型的类型哈希值。

参数1的类型哈希值

第一个参数的类型是void *。该类型用Type_t表示,具体如下所示:

00000102 00000200 [+ pointer to referenced Type_t]

我们需要找出3段数据来生成一个类型哈希值:

类型限定符:值为0的字节;
类型组:它是一个指向值为3的字节的指针;
特定于类型的数据:它是一个“泛型”指针,保存的地址为引用类型的哈希值(我们这里使用了递归)+ 1个字节(其值为2)处。

为了递归计算引用类型(void)的哈希值,这里的类型将使用Type_t来表示,具体如下所示:

00000040 00000000

我们需要的数据如下所示:

类型限定符:值为0的字节;
类型组:它是一个基本类型,用值为1的字节表示。
特定于类型的数据:对于Type_t 0x40(void),XFGHelper::XFGTypeHasher::hash_primitive将写入一个值为0x0E的字节。

参数2的类型哈希值

第二个参数的类型是const void *。该类型用Type_t表示,具体如下所示:

00000102 00000200 [+ pointer to referenced Type_t]

我们需要的数据构建如下:

 类型限定符:值为0的字节;
 类型组:它是一个指针,用值为3的字节表示;
 特定于类型的数据:这是一个“泛型”指针,保存的地址为引用类型的哈希值(在这里我们使用了递归)+ 一个字节(取值为2)处。

为了递归计算引用类型的哈希值(const void),该类型用Type_t表示,其内容如下所示:

00000040 00000800

我们需要的数据构建如下:

 类型限定符:const限定符,编码为值为1的字节;
 类型组:它是一种基本类型,用值为1的字节表示;
 类型特定的数据:对于Type_t 0x40 (void) 类型,XFGHelper::XFGTypeHasher::hash_primitive将写入一个值为0x0E的字节。

参数3的类型哈希值

第三个参数的类型为size_t。该类型用Type_t表示,具体如下所示:

00004019 00000000

我们需要的数据构建如下:

 类型限定符:用值为0的字节表示;
 类型组:它是一种基本类型,用值为1的字节表示;
 特定于类型的数据:对于Type_t 0x4019(unsigned long long)类型,XFGHelper::XFGTypeHasher::hash_primitive将写入一个值0x88的字节。

返回类型的类型哈希值

返回类型为void *,与该函数的第一个参数相同,因此,在这里我们直接复制之前获得的内容。

 类型限定符:用值为0的字节表示;
 类型组:指向值为3的字节的指针;
 特定于类型的数据:这是一个“泛型”指针,其中保存的地址为引用类型的哈希值(在这里我们使用了递归)+ 一个字节(值为2)。

为了递归计算引用类型(void)的哈希值,我们需要:

 类型限定符:用值为0的字节表示;
 类型组:它是一种基本类型,用值为1的字节表示;
 特定于类型的数据:对于Type_t 0x40 (void)类型,XFGHelper::XFGTypeHasher::hash_primitive将写入一个值为0x0E的字节。

把所有的东西放在一起

让我们把所有的数据组合在一起:

Number of params

03 00 00 00

type hash of param 1 (void *)

SHA256(
00 #qualifiers
03 # type group: pointer
# type hash of referenced type (void)
SHA256(
00 # qualifiers
01 # type group: primitive type
0E # hash of primitive type: void -> 0x0E
)[0:8]
02 # regular pointer
)[0:8]

type hash of param 2 (const void *)

SHA256(
00 # qualifiers
03 # type group: pointer
# type hash of referenced type (const void)
SHA256(
01 # qualifiers: const
01 # type group: primitive type
0E # hash of primitive type: void -> 0x0E
)[0:8]
02 # regular pointer
)[0:8]

type hash of param 3 (size_t)

SHA256(
00 # qualifiers
01 # type group: primitive type
88 # hash of primitive type: unsigned long long -> 0x88
)[0:8]

is variadic

00

calling convention

01 00 00 00

type hash of return value (void *)

SHA256(
00 # qualifiers
03 # type group: pointer
# type hash of referenced type (void)
SHA256(
00 # qualifiers
01 # type group: primitive type
0E # hash of primitive type: void -> 0x0E
)[0:8]
02 # regular pointer
)[0:8]

下面的Python代码用于计算该数据的SHA256摘要,并截取前8个字节,从而得到一个与编译器前端给出的哈希值完全相同的哈希值。最后,它会利用编译器后端的两个掩码进行相应的处理,从而获得最终形式的XFG型哈希值:

import struct
import hashlib

def truncated_hash(data):
return hashlib.sha256(data).digest()[0:8]

def apply_backend_masks(hash):
hash = hash & 0xFFFDBFFF7EDFFB70
hash = hash | 0x8000060010500070
return hash

def main():
# number of params
data = struct.pack('<L', 3)
# type hash of first param (void )
data += truncated_hash(b'\x00\x03' + truncated_hash(b'\x00\x01\x0e') + b'\x02')
# type hash of second param (const void
)
data += truncated_hash(b'\x00\x03' + truncated_hash(b'\x01\x01\x0e') + b'\x02')
# type hash of third param (size_t)
data += truncated_hash(b'\x00\x01\x88')
# is variadic
data += struct.pack('<B', 0x0)
# calling convention (default)
data += struct.pack('<L', 0x201 & 0x0F)
# type hash of return type (void *)
data += truncated_hash(b'\x00\x03' + truncated_hash(b'\x00\x01\x0e') + b'\x02')

print(f'Data to be hashed: {data} ({len(data)} bytes)')
frontend_hash = struct.unpack('<Q', truncated_hash(data))[0]
print(f'Hash generated by the frontend: 0x{frontend_hash:x}')

final_hash = apply_backend_masks(frontend_hash)
print(f'[*] Final XFG hash: 0x{final_hash:x}')

上述Python代码的输出结果如下所示:

python test.py

Data to be hashed: b'\x03\x00\x00\x00\xf5\x97x>[J\xb0\x17\x80\xb8\xc0[\x1b\xd0\xd8#\x14\xb4\xba\x91\xc7\xf6j\x00\x01\x00\x00\x00\xf5\x97x>[J\xb0' (41 bytes)

Hash generated by the frontend: 0x1da7d393d6b63a72

[*] Final XFG hash: 0x9da5979356d63a70

如果我们使用函数指针编译一些代码来调用其原型与我们在本节中一直讨论的函数相匹配的函数,我们可以看到,我们手工计算的XFG型哈希值与MSVC生成的XFG型哈希值完全匹配(参见下面反汇编中分配给main+0x8E处寄存器R10的值):

main+1C lea rax, my_memcpy
main+23 mov [rsp+78h+var_50], rax
[...]
main+6A lea rcx, aCallingFunctio ; "Calling function pointer...\n"
main+71 call printf
main+76 lea rcx, Str ; "a test"
main+7D call strlen
main+82 cdqe
main+84 mov rcx, [rsp+78h+var_50]
main+89 mov [rsp+78h+var_48], rcx
main+8E mov r10, 9DA5979356D63A70h
main+98 mov r8, rax
main+9B lea rdx, aATest_0 ; "a test"
main+A2 lea rcx, [rsp+78h+var_28]
main+A7 mov rax, [rsp+78h+var_48]
main+AC call cs:__guard_xfg_dispatch_icall_fptr

小结

在本文中,我们深入考察了MSVC编译器为C程序生成XFG型哈希值的所有细节。除了探讨即将到来的这种漏洞利用缓解措施的细节外,我们还顺路挖掘一下编译器的内部运行机制。

请记住,目前,XFG只存在于Windows Insider Preview版本中,所以,在这个CFI解决方案进入Windows 10正式版之前,我们在这里描述的内容仍然可能会发生变化。

此外,有些问题暂时还没有答案,比如为什么编译器后端会对前端生成的哈希值应用两个掩码,以及为什么在函数启动前,保存的哈希值的第0位被设置为1,但在XFG检测的调用点处,哈希值的第0位却为0。

最后,还有一件非常有意思的事情:看看C++编译器前端(c1xx.dll)计算XFG型哈希值的方式有什么不同。快速查看这个二进制文件,我们就会发现,这里的哈希值算法看起来与用于C语言的算法相当相似,但它可能进行相应的调整,以适应C++的相关概念,如继承、C++类型限定符以及修饰符等。