CFG——ControlFlowGuard 控制流保护
CFG防护机制的简要分析 - 先知社区 (aliyun.com)
Exploring Control Flow Guard in Windows 10 (trendmicro.com)
本文主要来自上面两篇文章,自己做一个记录罢了。
CFG 通过在间接跳转(Indirect Call)前插入校验代码(比如 call dword ptr ss:[ebp-8] 等等 ),检查目标地址的有效性,进而可以阻止执行流跳转到预期之外的地点, 最终及时并有效的进行异常处理,避免引发相关的安全问题。
CFG 的实现需要联合编译器、操作系统用户层库和内核模块,它是一个汇编层面的保护。
首先,一个正常的函数地址(x86)的前24位会被取出,作为一个偏移值(OFFSET)。CFGBitMapBase + OFFSET*4 地址中存储着一个计算值(每4字节作为) ,_guard_check_icall_fptr 函数会对传入的参数进行计算,然后与存储着的计算值进行比较:
① 最后一字节的最后4位全为0,则取最后一字节的前5位作为一个偏移值,若对应的存储值(从0开始数)的对应位是1,则函数地址有效。 ② (windows 10 pro 2018年)最后一字节的最后4位不全为0,则取最后一字节的前5位作为一个偏移值。若对应的存储值(从0开始数)的对应位是1,则函数地址有效并返回; Win 10 1909(OS内部版本18363.1198) 中,最后一字节的最后4位不全为0,则取最后一字节的前5位并将这5位的最后一位置0,再作为一个偏移值。若对应的存储值(从0开始数)的对应位是1,则执行下一步 ③ 若前两个都不满足,或第二个满足,则将前5位的最后一位置1,作为一个偏移值。若对应的存储值(从0开始数)的对应的位是1,则函数地址有效。否则判定无效,并进行异常处理。 |
借助上面提到的文章,自己添加了一些函数进去,对编译的程序进行调试:
因为是用 C++ 写的,使用的是 __cdecl (C 规范的) 调用约定,参数从右到左入栈,由调用者负责清除栈。
可以看到 _guard_check_icall_fptr 程序实际调用了 ValidateUserCallTarget 函数
系统:Windows 10 1909 编译器:VS 2019 win32 release版
mov edx,dword ptr ds:[77D112F8] // 获得 CFGBitMapBase 地址 mov eax,ecx // ecx = 要调用的函数地址 shr eax,8 // 取函数地址的前24位作为 OFFSET mov edx,dword ptr ds:[edx+eax*4] // 到 CFGBitMapBase+OFFSET*4 取值 mov eax,ecx shr eax,3 // eax 右移三位 test cl,F // 函数地址的最后4位是否全为0
jne ntdll.77C79DBE // 不全为0则跳转到后面执行 bt edx,eax // Bit Test 指令,第一个操作数(edx)是寄存器且
// eax > 32时,将会对 eax = eax mod 32 (AX 时模数是16)
// 然后再取 edx 相对应的位的值放入 CF 中
// 因为右移了3位,最后一字节前5位能表示的最大值是31,
// 因此其实取余后的值也还是由最后一字节的前5位表示
jae ntdll.77C79DC7 // 若edx对应的位是0,则跳转,否则正常返回 ret
77C79DBE:
btr eax,0 // 取 eax 最后一位,放入 CF 并将 eax 最后一位置 0 bt edx,eax // 取 edx 对应的位放入 CF,与前面原理相同 jae ntdll.77C79DD0 // 若 CF == 0,则进入异常处理
77C79DC7:
or eax,1 // 将 eax 最后一位置 1 bt edx,eax // 取对应位 jae ntdll.77C79DD0 ret
77C79DD0: // 异常处理 push ecx lea esp,dword ptr ss:[esp-80] movups xmmword ptr ss:[esp],xmm0 movups xmmword ptr ss:[esp+10],xmm1 movups xmmword ptr ss:[esp+20],xmm2 movups xmmword ptr ss:[esp+30],xmm3 movups xmmword ptr ss:[esp+40],xmm4 movups xmmword ptr ss:[esp+50],xmm5 movups xmmword ptr ss:[esp+60],xmm6 movups xmmword ptr ss:[esp+70],xmm7 call <ntdll.@RtlpHandleInvalidUserCallTarget@4> movups xmm0,xmmword ptr ss:[esp] movups xmm1,xmmword ptr ss:[esp+10] movups xmm2,xmmword ptr ss:[esp+20] movups xmm3,xmmword ptr ss:[esp+30] movups xmm4,xmmword ptr ss:[esp+40] movups xmm5,xmmword ptr ss:[esp+50] movups xmm6,xmmword ptr ss:[esp+60] movups xmm7,xmmword ptr ss:[esp+70] lea esp,dword ptr ss:[esp+80] pop ecx ret
同时,若在编译过程中不开启 ASLR ,则自己编写的函数的 CFGBitMap 值会是 FFFFFFFF,而系统函数的值则不会是
使用不同的运行库也会导致某些函数的 CFGBitMap 值为 FFFFFFFF,猜测如下:如果代码写死在程序中,不需要调用外部 DLL 库中的文件,则值为 FFFFFFFF,如果需要调用外部 DLL 库中的文件,则值不为全 F。