另一种控制流防护检查:验证与调用的结合The other kind of control flow guard check: The combined validate and call
微软开发者博客深入探讨了 Windows 控制流防护(CFG)机制中的另一种检查方式:将验证与调用合二为一的机制。这种二合一的优化设计旨在提升系统安全性并减少间接分支带来的性能开销。文章剖析了该机制在底层架构中的工作原理及其如何有效防止恶意代码劫持程序的控制流。
Raymond Chen
前段时间,我讨论了如何从控制流防护检查中提取函数指针。我给出了 LdrpValidateUserCallTarget 的代码,但该函数还有另一个版本,将验证与调用结合在了一起。我猜想这个版本存在的原因是,在验证完函数指针后,你几乎总是要调用它,所以不如把这两个操作结合起来。
但这确实意味着必须改变调用约定,因为需要为最终的调用设置寄存器,这意味着验证并调用操作的参数不能与调用约定使用的寄存器重叠。(听起来很熟悉?)
这是一个 x86-64 版本。
mov r11, [ntdll!....]
mov r10,rax
shr r10,9
mov r11,qword ptr [r11+r10*8]
mov r10,rax
shr r10,3
test al,0Fh
jne @1
bt r11,r10
jae @2
jmp rax
@1: btr r10,0
bt r11,r10
jae @3
@2: or r10,1
bt r11,r10
jae @3
jmp rax
@3: xor r10d, r10d
jmp bad让我们把它和仅验证版本放在一起对比一下:
mov rdx,qword ptr [ntdll!....]
mov rax,rcx
shr rax,9
mov rdx,qword ptr [rdx+rax*8]
mov rax,rcx
shr rax,3
test cl,0Fh
jne @1
bt rdx,rax
jae @2
ret
@1: btr rax,0
bt rdx,rax
jae @3
@2: or rax,1
bt rdx,rax
jae @3
ret
@3: mov rax,rcx
xor r10d,r10d
jmp bad mov r11, [ntdll!....]
mov r10,rax
shr r10,9
mov r11,qword ptr [r11+r10*8]
mov r10,rax
shr r10,3
test al,0Fh
jne @1
bt r11,r10
jae @2
jmp rax
@1: btr r10,0
bt r11,r10
jae @3
@2: or r10,1
bt r11,r10
jae @3
jmp rax
@3:
xor r10d, r10d
jmp bad两者的逻辑是一样的;仅仅是使用的寄存器不同。
仅验证版本通过 rcx 接收地址,并将 rax 和 rdx 用作临时寄存器。验证并调用版本通过 rax 接收地址,并将 r10 和 r11 用作临时寄存器。(检测到坏指针时还有一点微小的不同:验证并调用版本的 rax 寄存器中已经存放了该坏指针,因此不需要再做任何操作将其移入其中。)
验证并调用版本将其参数和临时寄存器移至 x86-64 Windows 调用约定未使用的寄存器上,这样它就能以一条 jmp rax 指令结束,在所有函数参数完好无损的情况下跳转至已验证的函数。
对于 AArch64,情况类似。
adrp xip0,ntdll!....
ldr xip0,[xip0,#0x598]
lsr xip1,x15,#6
tst x15,#0xF
ldrb wip1,[xip0,xip1]
ubfx xip0,x15,#3,#3
bne @2
lsr xip1,xip1,xip0
tbz wip1,#0,@3
@1: ret
@2: and xip0,xip0,#-2
lsr xip1,xip1,xip0
tbz wip1,#0,@4
@3: tbnz wip1,#1,@1
@4: mov xip0,#0
b @5
@5: b bad adrp xip0,ntdll!....
ldr xip0,[xip0,#0x598]
lsr xip1,x9,#6
tst x9,#0xF
ldrb wip1,[xip0,xip1]
ubfx xip0,x9,#3,#3
bne @2
lsr xip1,xip1,xip0
tbz wip1,#0,@3
@1: br x9
@2: and xip0,xip0,#-2
lsr xip1,xip1,xip0
tbz wip1,#0,@4
@3: tbnz wip1,#1,@1
@4: mov xip0,#1
mov x15,x9
b bad同样,代码序列是相同的;区别仅在于寄存器的使用。(以及检测到坏调用时的代码序列。)仅验证版本在 x15 中接收地址,而验证并调用版本在 x9 中接收地址。(两者都使用 xip0 和 xip1 作为临时寄存器。)验证并调用版本以一条 b r9 指令结束,直接跳转至已验证的地址,而不是返回。
同样,你可以从转移后的寄存器中提取出坏指针。对于 x86-64 的验证并调用版本,它是 rax;对于 AArch64 的验证并调用版本,它是 r9。
作者
Raymond 参与了 Windows 的演进已超过 30 年。2003 年,他创办了一个名为 The Old New Thing 的网站,其受欢迎程度远远超出了他的想象,这种发展至今仍让他感到有些胆战心惊。该网站还催生了一本书,巧合的是书名也叫《The Old New Thing》(Addison Wesley 出版社 2007 年出版)。他偶尔会出现在 Windows Dev Docs 的 Twitter 账户上讲故事,但这些故事并不传达任何有用的信息。
需要完整排版与评论请前往来源站点阅读。