不知道这个漏洞之前有没有人发现并上报,我上报国家信息安全漏洞共享平台,对方要求我提供POC。我掂量了一下自己能力,觉得在驱动里实现POC难度挺高的,所以我就放弃了(当然,我已经顺手报给微软MSRC)。于是我就移步至此,记录调试过程。实验结论基于我前两篇文章:探索Windows S3唤醒函数 (一) 探索Windows S3唤醒函数 (二)。 从Win7开始,x64位平台需要在测试模式下才能加载未认证的驱动,一般也没几个用户会开测试模式,所以限制了这个漏洞的利用。另外Win7可能不支持UEFI Bios,因此,这个漏洞可能在WinXp/Win7系统上不能奏效。
前面文章里反复提到一个内核变量: HalpLowStubPhysicalAddress,漏洞的产生全在于它:
Step 1).OS在 HalpSetupAcpiPhase0阶段,调用HalpAllocPhysicalMemory分配(可执行)物理地址,并将起始地址写入 HalpLowStubPhysicalAddress;(下文会给出调试证明。随文附件中,我提供了Win10 RS5 x64 Free Build Hal.dll和对应的Hal.i64 )
Step 1.5).在之后的某个阶段(暂时没有跟踪到),OS将初始化 HalpLowStubPhysicalAddress所指的物理内存,其起始4Byte为一个JMP跳转指令; (下文亦会给出调试证明)
Step 2).进入S3 Sleep时,OS以 HalpLowStubPhysicalAddress 作为参数,调用HalpSetupRealModeResume函数,将 HalpLowStubPhysicalAddress的值写入内核变量HalpWakeVector。前面文章提过, HalpWakeVector对应UEFI S3 WakeVector;
Step 3).外部事件触发S3 Resume,UEFI Bios执行 S3ResumeBootOs,通过SwitchStack函数跳转到OS指定的S3 WakeVector,也就是 HalpLowStubPhysicalAddress的值;(下文只能提供Intel Comet Lake RVP board bios log)
原理都写到这份上了,大家应该心知肚明了:创建驱动,仿造 Step 1)和Step 1.5)分配物理内存并在物理内存里构建JMP。然后覆盖 内核变量HalpLowStubPhysicalAddress的值为我们分配的物理地址!!!,完成后让OS进入S3,推它一把,剩下的它自己完成去吧~
2020-04-14-04:15:15.962 PROGRESS CODE: V03031006 I0 2020-04-14-04:15:15.962 2020-04-14-04:15:16.009 Transfer to 16bit OS waking vector - 1000 2020-04-14-04:15:16.009 2020-04-14-04:15:18.018 Froome: TaskSmmPanelPowerOnOffSequense set EC_CONF_DEV_En to 0x1
这段日志出自 Intel Comet Lake RVP board的串口输出日志,对应实现可以定位到下列UEFI源码(UEFI源码,大家可以从UDK2015项目中获得。我用的是IBV基于UDK的CodeBase,涉密不便于贴出):
这段日志表明了WakeVector的物理内存位于0x1000,这个物理地址由OS填写(确切的说是ACPI子系统填写)。
再证明Step 1):
a).首先查找OS为导出函数HalpSetupRealModeResume,根据IDA分析,我知道:HalpSetupRealModeResume距离OS导出函数hal!HalPerformEndOfInterrupt的偏移为0x2c35,所以我先定位运行时hal!HalPerformEndOfInterrupt函数的地址(注意,每次重启函数地址都会变,但函数间偏移不会变,不要随意下函数断点!)
;查找内核导出函数hal!HalPerformEndOfInterrupt 0: kd> x hal!*Halp* fffff800`26c0bad0 hal!HalPerformEndOfInterrupt (<no parameter info>) fffff800`26c210b0 hal!HalProcessorIdle (<no parameter info>) ;hal!HalPerformEndOfInterrupt+偏移得到HalpSetupRealModeResume的地址 ;验证一下得到的地址对应的反汇编代码是否和IDA分析的结果一致,下图为IDA HalpSetupRealModeResume的输出 ;当然一致,要不然我就不贴上来了! 0: kd> u fffff800`26c210b0+2c35 hal!HalProcessorIdle+0x2c35: fffff800`26c23ce5 488b0d64b00400 mov rcx,qword ptr [hal!HalHandleNMI+0x20c40 (fffff800`26c6ed50)] fffff800`26c23cec 8b15f6ae0400 mov edx,dword ptr [hal!HalHandleNMI+0x20ad8 (fffff800`26c6ebe8)] fffff800`26c23cf2 e829d8ffff call hal!HalProcessorIdle+0x470 (fffff800`26c21520) fffff800`26c23cf7 84c0 test al,al fffff800`26c23cf9 0f8546a1feff jne hal!HalFlushCommonBuffer+0x6f5 (fffff800`26c0de45) fffff800`26c23cff e9dca0feff jmp hal!HalFlushCommonBuffer+0x690 (fffff800`26c0dde0) fffff800`26c23d04 ffc3 inc ebx fffff800`26c23d06 851dd0b20400 test dword ptr [hal!HalHandleNMI+0x20ecc (fffff800`26c6efdc)],ebx
b). 从 HalpSetupRealModeResume函数中得到内核变量HalpWakeVector的地址:
根据IDA的分析, HalpWakeVector是全局变量,并且被 HalpSetupRealModeResume函数访问(即上图红框处),因此我从 HalpSetupRealModeResume函数中得到内核变量HalpWakeVector的地址:fffff800`26c6ebe8
fffff800`26c23cec 8b15f6ae0400 mov edx,dword ptr [hal!HalHandleNMI+0x20ad8 (fffff800`26c6ebe8)]
c).对比地址fffff800`26c6ebe8的值是否和 Intel Comet Lake RVP board输出的WakeVector相同:
0: kd> dd fffff800`26c6ebe8 fffff800`26c6ebe8 00001000 00000000 00000000 00000000 fffff800`26c6ebf8 ec50d00c ffff8600 00000000 00000000
两者值一致,但需要注意地址fffff800`26c6ebe8,也就是 HalpWakeVector存储的值是物理地址,因此要用windbg扩展命令!dd查看内容:
0: kd> !dd 00001000 # 1000 00064de9 00000001 00000001 1018003f # 1010 00000000 00000000 00000000 00000000 # 1020 00000000 00000000 00000000 00209b00 # 1030 00000000 00000000 0000ffff 00cf9300
敏感的你看了0x1000处前4B马上能意识到这是JMP指令,是的没错,用OD试试
由此可见,UEFI代码从Facs->FirmwareWakingVector跳转到 HalpWakeVector后,又会执行一次Jmp指令跳转到指定的payload。我们的实验驱动也可以这样实现,源码我就不提供了,太麻烦!