CFG(Control Flow Guard)是微软推出的漏洞利用缓解机制,从Windows 8.1开始引入,并且需要编译器的支持,编译器的版本为Virtual Studio 2015 Updated 2版本以上。
在开启了CFG支持以后,编译生成exe程序中,所有间接调用前面都会插入一个_guard_check_icall的检查函数,如果系统不支持CFG机制,则该函数不会生效。
如:
eax,[esi]
ecx, eax
call _guard_check_icall // CFG检查函数
call eax
_guard_check_icall函数的地址,在PE程序被系统加载的时候会被替换成nt!LdrpValidateUserCallTarget函数的地址
nt!LdrpValidateUserCallTarget函数的参数就是上面ecx的值,也就是间接调用的函数地址
校验逻辑如下:
1)间接调用函数地址共4字节,取高3字节的值加上CFGBitmap表的基址,得到CFGBitmap表中的值
2)判断间接调用地址是否0x10对齐:
2.1) 如果是对齐的,函数地址的第4-8位的值,就是上面获取的CFGBitmap表值的位偏移
2.2) 如果不是对齐的,函数地址的第4-8位的值,再或上1,得到的值就是CFGBitmap表值的位偏移
3)验证CFGBitmap表值的位偏移处的bit位,如果是1,则说明这个函数是有效的,否则产生异常
在PE结构的loadConfig信息中保存了:
1)_guard_check_icall的函数地址
2)CFGBitmap表的RVA,这里面是该程序的每个函数的RVA转换成1bit的值,制作成的一个CFGBitmap表
3)CFGBitmap中函数的数量
假设目标函数地址为:0x01321380
二进制表示为:
0000 0001 0011 0010 0001 0011 1000 0000
通过对目标地址右移8位,获取高3字节的值: 0x13213(0x01321380 >> 8 )
通过对目标地址右移3位,再取5位的值作为结果:0x264270(0x01321380 >> 3)
0x264270的二进制表示为:
0000 0000 0010 0110 0100 0010 0111 0000
取前5位作为offset:
1 0000
也就是等于对原目标函数地址的二进制位取前4-8位:
0000 0001 0011 0010 0001 0011 1000 0 000
等于16进制的0x10,10进制的16
现在我们得到两个值,一个是 0x13213和0x10
假设CFGBitmap的基址+0x13213*4 的位置取4字节内容是0x11010044
这个值就是对应目标函数的CFGBitmap,它的二进制位表示为:
0x11010044 = 0001 0001 0000 0001 0000 0000 0100 0100
在二进制位的第16位的位置,因为是从0开始数,也就是第17位:
0001 0001 0000 000 1 0000 0000 0100 0100
判断这一位是否为1,如果为1,就说明目标函数地址有效。否则产生异常。
通过IDA查看nt!LdrpValidateUserCallTarget函数: