免费发布推广信息的网站,外贸推广网站哪家,建设美团网站,温州网站建设科技有限公司背景
原本是朋友在调试一个看起来比较新的变速器驱动#xff0c;整体来说支持两种变速模式#xff0c;一种是进程级#xff0c;这种用了HOOK#xff0c;中规中矩的实现#xff0c;原理网上都有。另一种是”系统级内核全局变速“#xff0c;这个模式初步看了下有些特殊整体来说支持两种变速模式一种是进程级这种用了HOOK中规中矩的实现原理网上都有。另一种是”系统级内核全局变速“这个模式初步看了下有些特殊已知的关键点没被修改也没hook。比较好奇是怎么实现的花了几天时间分析也有一些有意思的地方发个文章记录一下。
一KeQueryPerformanceCounter
写了个简单的驱动直接调用KeQueryPerformanceCounter会被加速那么从这里入手应当没问题。 KeQueryPerformanceCounter网上其他相关文章或多或少都有涉及只写一下关键调用路径 1 2 3 4 5 KeQueryPerformanceCounter HalpPerformanceCounter 0x70; HalpHvCounterQueryCounter HalpHvTimerApi HvlGetReferenceTimeUsingTscPage 核心逻辑在最后一层的HvlGetReferenceTimeUsingTscPage中主要是读__rdtsc()然后做一些运算 1 2 3 4 5 6 7 8 9 10 11 12 13 __int64 __fastcall HvlGetReferenceTimeUsingTscPage(int a1, __int64 a2) { v2 __rdtsc(); LODWORD(a2) HIDWORD(v2); v2 (unsigned int)v2; a2 (unsigned int)a2; a2 *((_QWORD *)HvlpReferenceTscPage 2) (((v2 | (a2 32)) * (unsigned __int128)*((unsigned __int64 *)HvlpReferenceTscPage 1)) 64); v5 a2; a1 *(_DWORD *)HvlpReferenceTscPage; if ( *(_DWORD *)HvlpReferenceTscPage v3 ) return v5; } 没开嵌套虚拟化所以rdtsc肯定没被动手脚调用链里涉及到的相关函数指针及代码确实都没修改。为了缩小范围及进一步排除跳过前面几层直接调用HvlGetReferenceTimeUsingTscPage甚至把代码抠出来直接执行也是被加速那么猫腻一定在这段代码里面对其逻辑做一些分析简化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct HvRTP { uint32_t unk1; uint32_t unk2; uint64_t factor; uint64_t unk3; //........ }; //上面IDA伪代码中的HvlpReferenceTscPage大致就是上面这么一个结构体指针, 核心逻辑简化后等价于 __int64 __fastcall HvlGetReferenceTimeUsingTscPage() { count HvlpReferenceTscPage-unk3 (__rdtsc() * HvlpReferenceTscPage-factor) 64; return count; } unk3固定是0那就等价于count (__rdtsc() * HvlpReferenceTscPage-factor) 64; 排除hook以及rdtsc剩下唯一的可能就是改变了factorHvlpReferenceTscPage 8。 多次观察验证
在不启动变速功能时factor值一直不变当开启加速后factor会被修改如果停止变速factor值就会被还原factor变化规律与加速倍率直接相关如果调1.2倍样本驱动就会把factor改为近似于原值*1.2变速软件本身限制最高倍率1.5但手动eq将factor改为原值的10倍后系统整体就会表现出10倍速效果。
看起来很简单但实际上分析才刚刚开始。
二蓝屏
当朋友按照这个结论去测试时发现HvlpReferenceTscPage8根本就没法改可以读但只要写入就会发生WHEA_UNCORRECTABLE_ERROR0x124蓝屏我最初一直认为是写内存方式不对但是朋友最后发现了一些规律
几乎所有写入方式比如常用的mdl/MmGetPhyAddrmap或者关WP位包括windbg的eq无一例外全部都会导致0x124蓝屏!pte检查map后的页属性没问题变速样本始终可以稳定运行而且只要通过变速样本开启一次加速即使停止加速后随便怎么写都不会触发蓝屏。蓝屏只在HyperV环境出现 当我也切换为HyperV果然出现0x124蓝屏: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 8: kd k # Child-SP RetAddr Call Site 00 ffffbf813a347918 fffff807276f7adb nt!KeBugCheckEx 01 ffffbf813a347920 fffff80724db1740 nt!HalBugCheckSystem0xeb 02 ffffbf813a347960 fffff80727826c93 PSHED!PshedBugCheckSystem0x10 03 ffffbf813a347990 fffff807276f9411 nt!WheaReportHwError0x393 04 ffffbf813a347a60 fffff807276f9858 nt!HalpMcaReportError0xb1 05 ffffbf813a347bc0 fffff807276f96f0 nt!HalpMceHandlerCore0x134 06 ffffbf813a347c20 fffff807276f9922 nt!HalpMceHandler0xe0 07 ffffbf813a347c60 fffff807276f8bd6 nt!HalpMceHandlerWithRendezvous0x62 08 ffffbf813a347c90 fffff807276fb4fb nt!HalpHandleMachineCheck0x62 09 ffffbf813a347cc0 fffff80727756e39 nt!HalHandleMcheck0x3b 0a ffffbf813a347cf0 fffff807276281be nt!KiHandleMcheck0x9 0b ffffbf813a347d20 fffff80727627de8 nt!KxMcheckAbort0x7e 0c ffffbf813a347e60 fffff807430a72eb nt!KiMcheckAbort0x2a8
从蓝屏栈可以看到是写内存时发生MCE异常VBS没开而且也不像是VBS的表现应当是hypervisor发现某些异常后主动向虚拟机内注入的MCE触发原因可能是EPT物理页属性只读。 理论上有一种简单的解决办法不要直接改HvlpReferenceTscPage8而是自己分配一块内存然后把指针替换到HvlpReferenceTscPage。但样本驱动不是这么干的应当有它的原因比如PG或者为了隐蔽具体什么原因不重要最关键同时也最让我好奇的是如果真的是因为EPT页只读触发的蓝屏那样本驱动是怎么让页变成可写的 正面硬刚VMP是下策所以首先尝试的思路是对常用内存相关函数下断点没看出变速驱动有什么特殊操作就是普普通通的IoAllocateMDL Map传的参数与自己的测试代码也是一模一样。 那么这个驱动在写内存前必然还有其他操作在配合。此时要么直面VMP要么调试HyperV没找到能用的VMP插件调HyperV现实一点。
三 Hyper-V
首先验证猜测确认是不是因为EPT不可写而注入的MCE
这里本该有图不过写这个贴子的时候距离分析已经有一段时间了懒得再重新搭建调试环境所以写一下关键思路和过程通过搜索0x4402/0x6400定位HyperV的vcpu_run_loop()及vmexit_dispatch()梳理vmexit_dispatch()内针对每种exit_reason的处理函数重点是EPT退出在ept_vio_exit_handler内合适位置下条件断点条件设置为vmread(0x2400h)GPA(HvlpReferenceTscPage)guest内尝试写入HvlpReferenceTscPage8断点会命中根据vmread(0x6400)的信息或者手动转换也能发现对应页面只有读权限再往后执行就会给子机注入MCE异常重启子机继续下第3步的断点加载变速驱动开启变速功能断点不会命中也不会蓝屏但对应内存已改写随便在vcpu_run路径上下个断点当vcpu退出命中断点根据EPTP推一遍会发现对应页已经是可读写
可以证实MCE蓝屏确实是因为物理页不可写导致接下来的关键问题是要分析变速驱动是如何让GPA从只读变成可读写的。 理论上在没有通过EFI等方式Patch hvix64.exe的情况下要从Guest内实现改变EPTP内的GPA属性就三种方式
第一种是通过vmfunc(0,)切换EPTP, 这种一般不会产生vmexitwindows内部也确实会用到但通过断点验证可以确定当时没执行过或者变速驱动自己调用了vmfunc(0,)通过指令搜索方式也基本上可以排除掉VMP后vmfunc这类指令还是以原来的形式存在在对应代码段里并不能搜索到这条指令第二种是变速驱动在guest里做了什么操作促使hypervisor修改了对应的页面属性。这类方式首先想到的是HyperV是不是有什么VMCall但是在VMCall的exit_handler下断点并没有什么线索第三种是bug可能性太小。。。。
验证完自己能想到的两种方式但没有线索后只能回到标准思路
先定位HyperVisor修改页面属性的代码然后设置条件断点判断是不是在修改HvlpReferenceTscPage所对应的GPA页属性如果断点触发结合host/guest当时的栈去分析。
这个思路肯定行得通但是仔细想一下vmfunc(0)这种唯一无VMExit的方式可以排除那么即使想不到它具体用的什么方式也可以确定过程中一定会产生vmexit。 所以最终我用了一个偷懒的思路统计未开启和开启加速时的vmexit事件对比分析差异:
在vcpu_run_loop()函数内vmread(0x4402)读取到exit_reason之后的位置下断点自动trace虚拟机的vmexit信息(主要记录reason和guest_rip)对比变速前后的vmexit统计信息如果发现某些vmexit仅在变速驱动开启加速的过程中才出现那么这些vmexit事件就很可能是突破口
反复跑几遍最后发现的规律是
变速驱动在开启变速改写HvlpReferenceTscPage8之前一定有几次rd/wrmsr而这些msr在正常运行过中系统从不访问并且通过vmexit时的guest_rip可以确认这些rd/wrmsr都是由变速驱动直接执行。
接下来就是具体分析这些rd/wrmsr直接在guest_rip或者在HyperV这边的rd/wrmsr_exit_handler处下断点再观察rd/wrmsr执行前/后VCPU ecx,edx,eax的值就可以知道读写了哪些寄存器以及读写的具体数据。
四 MSR 0x40000021
在HyperV的wrmsr_exit_handler处下断点多次观察后首先发现这样一个规律
开机后第一次开启加速时变速驱动会读取MSR 0x40000021 此时读取到的值为0xC001变速驱动对这个MSR 写入0xC000仅在第一次开启加速时才会写入后续开启加速每次都会读但不会再写入每次读取到的值也都是上一步中写入的0xC000
如果只是读MSR倒也没什么但写MSR比较可疑。搜一下MSR 0x40000021找到这样两个相对比较有用的文档
https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/tlfs/timers。Hyper-V Enlightenments — QEMU documentation
综合这些文档可以知道以下关键信息
0x40000021是HyperV自己定义的MSR相当于一个接口用于GuestOS与HyperV协商初始化Reference TSC pageReference TSC page是为了给GuestOS提供一个访问时不会产生vmexit的时间戳计数器MSR对应的读写数据格式为bit 0表示是否启用Reference TSC pagebit 12-63表示GPA PageNumber其他都是保留位默认情况下Reference TSC page功能是禁用状态值为0开机启动过程中由GuestOS主动写入这个MSR才能启用
结合这些信息和上面所说的变速驱动读写0x40000021的规律可以进一步明确其每一步的含义
第一次读取rdmsr(0x40000021)返回的是0xC001 解析0xC001 bit 0为1那就说明此时GuestOS与HyperV已经协商并初始化Reference TSC page对应GPA PageNumber为0xC对应GPA为0xC*0x10000xC000变速驱动对这个MSR写入0xC000 0xC000就是0xC001清掉bit 0其他位保持原样bit 0表示是否启用那么变速驱动这一步就是通过这个MSR接口通知HyperV禁用掉Referenct TSC page
此外还有一个最最关键的信息
上面提到“0xC001对应的GPA0xC*0x10000xC000”这个0xC000其实就是HvlpReferenceTscPage的GPA并且也正是发生EPT写入异常最终导致MCE蓝屏的GPA
结合推测变速驱动禁用reference tsc page的目的很有可能是禁用后HyperV就会在EPT中将对应Page的从只读变为读写。 调整测试代码在修改内存前也加上这个操作终于可以顺利写入HvlpReferenceTscPage同时也在HyperV这边进行验证
在wrmsr_exit_handler_0x40000021处下断点断点触发时确认0xC这个页面在EPT中属性为只读随后step out让HyperV处理完这次wrmsr当再次中断到Windbg时0xC页面就变为了可读写。此时尚未执行VMResume虚拟机又是单核VCPU退出后下一次VMResume前等同于是停转状态这期间GuestOS不可能再有任何操作也不可能再有其他vmexit因此可以断定GPA从只读变为可写100%是由wrmsr(0x40000021,0xc000)触发。
五系统假死
解决这个页面写入问题之后再次遇到新的问题
如果将factor改为原来的10倍虚拟机会假死而通过变速样本驱动进行10倍加速后系统依旧正常运行
原因是GuestOS内确实变速了但Hypervisor这边没变而GuestOS是将时钟设备配置为OneShot模式在这种模式下写到时钟设备的“到期时间”是一个绝对时间这个绝对到期时间的计算方法是HalpHvCounterQueryCounter() ClockInterval这里的HalpHvCounterQueryCounter其实等同于调用KeQueryPerfCounter()所获取到的Counter值是10倍加速后的结果而时钟设备是由Hypervisor模拟Hypervisor这边的时间是以正常速度流逝所以最终时钟到期时间会越来越晚对于GuestOS来说就是时钟中断被大幅延迟最终DPC/线程调度全部跟着延迟分时系统遇到这种情况必然假死。
这个结论的分析和验证过程如下
Hook SwapContext发现调用次数相比正常时少了很多线程切换调度肯定出了问题看看DPC是否有问题创建一个DPC Timer实际执行时机相比预期时间被延迟而不是被加速DPC Timer也有问题那么继续看时钟中断是否有问题Hook HalpTimerClockInterrupt/KeClockInterruptNotify/KeUpdateRuntime调用次数明显少于正常情况再往下就是Hypervisor了但不一定是Hypervisor导致有可能是GuestOS配置的时钟周期有问题结合IDA/Windbg顺着时钟中断一路分析发现在需要配置时钟中断到期时间时最终都会经过以下路径 HalpTimerClockArm-HalpSetTimer-HalpClockTimer0x80()-HalpHvTimerArm()在HalpHvTimerArm中就会执行上面说的HalpHvCounterQueryCounter() ClockInterval计算绝对到期时间并配置到时钟设备问题是这个时间是被加速后的尝试Hook HalpHvTimerArm将其从HalpHvCounterQueryCounter()获取到的值重新还原回加速前的正常值再写到时钟设备假死问题消失
HOOK HalpClockTimer0x80指向的HalpHvTimerArm确实也是一种解决办法但样本驱动用了另一种方案通过两个MSR接口额外启动另一个时钟设备并配置为period模式周期为1ms。怎么发现的这两个MSR绕了一圈最后还是通过VMEXIT统计对比发现的差异变速样本驱动在写了0x40000021之后会继续写0x400000B2/B3这两个MSR只是VM运行过程中vmexit本身就比较频繁这两个WRMSR退出事件和0x40000021中间还有一大堆其他vmexit导致最初没注意到。
六MSR 0x400000B2/B3
这两个MSR也是HyperV自己定义的在前面的文档中一样有描述。主要用途其实跟LAPIC Timer差不多
0x400000B2 用于配置时钟模式类似于LAPIC的LVTT(0x320) 关键bit bit 0 Enable是否启用bit 1 Period是否为周期性时钟bit 3 AutoEnable写入B3时是否自动启用Timerbit 4:11 Vector时钟到期时向GuestOS注入的中断Vectorbit 12 DirectMode如果为1, 时钟到期后通过标准的APIC向Guest注入中断通知子机如果为0则通过HyperV的SynIC注入中断通知子机0x400000B3 用于配置时钟周期/到期事件类似于LAPIC的TMICT(0x380)
变速驱动主要是执行wrmsr(0x400000B2,0x1D1A);wrmsr(0x400000B3,0x2710)其中0x1D1A表示周期性模式自动启用Vector D1DirectModeVector D1对应Windows的HalpTimerClockInterrupt0x27101ms。这样的话即使Win自己配置到时钟设备的绝对时间是加速后的但是变速驱动额外启动了1ms周期性时钟用于给OS维持稳定的心跳最终就不会卡死。 至此就知道关键流程
对40000021 的bit 0清零即禁用TSCRefPage从而使HyperV将对应GPA从只读变为读写属性通过400000B2/B3提前配置好时钟避免变速后系统假死改写factor实现变速
按照这个逻辑就可以复刻变速驱动的“全局系统级变速”模式。
七其他 有心人可能会对0x40000021这个接口感兴趣简单看了下应当不太好利用因为当写入值bit 0为0时wrmsr_handler_0x40000021其实并不是加上”W“权限而是直接”恢复”原来的页映射。在HyperV里叫Overlay Page简单理解就是贴一张新的A4纸到原来的纸上如果要禁用那就把贴上去的A4纸撕下来将原来的A4纸原样“显示”出来纸上的内容还是原来的样子。另外虽然看起来可以通过0x40000021将任一Page贴到另一个Page之上但是新Page权限以及Page中数据是由Hypervisor强制填充即使自己先写好也会被清零。当然我没有深究如果感兴趣的可以自己再调一下看看。 最后看一下(__rdtsc() * HvlpReferenceTscPage-factor) 64这个运算的意义及变速原理Windows对factor的计算逻辑是 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 unsigned __int64 unk(__int64 a1, unsigned __int64 a2, unsigned __int64 a3, __int64 *a4) { __int64 v7; // rdi __int64 v8; // rcx __int64 v9; // r9 __int64 v10; // rdx __int64 v11; // rbx v7 64i64; do { v8 2 * a2; v9 (2 * a1) | (a2 63); v10 a1 63; a1 v9 - a3; v11 2 * a2; if ( (v10 | (unsigned __int64)v9) a3 ) a1 v9; a2 v11 | 1; if ( (v10 | (unsigned __int64)v9) a3 ) a2 v8; --v7; } while ( v7 ); if ( a4 ) *a4 a1; return a2; } 传的参数是 unk(10000000i64, 0i64, 3.19xxxx, 0i64); 其中10000000是代码逻辑中写死的QPC频率3.19xxxx是tsc频率 这几行代码里面混着大数乘除法定点浮点数看着可能有点迷糊但其实等价于 factor 2^64 * qpc_freq / tsc_freq我的机器tsc频率是3.2GHZ按公式计算得到0xcccccccccccccc有点怪看到一串0xcc多少有点怀疑是不是哪搞错了其次这个值与Win算出来的确实有点差异。 实际上是正确的运算结果只是恰好等于0xcccccccccccccc而已然后Win并不会通过CPUID/MSR去获取CPU的标称tsc频率而是在启动时候用一个很短的时间去动态测算CPU的真实tsc频率算出来的频率会是3.19xxxxxxxxGHZ而不是标称3.2G那么最终多少会有点误差。 这个运算的目的则是在CPU的tsc频率与Win的QPC频率之间算一个系数用于频率转换因为QPC在新一点的机器上一般都是建立在TSC基础之上(CPU要支持iTSC)在支持iTSC的情况下QPC频率是代码写死的固定10MHZTSC频率却取决于CPUtsc频率肯定不会等于10Mhz那么必然要在中间做一层转换。至于为什么还要乘以2^64因为如果不这么干那就不可避免要涉及到浮点数运算有性能代价不想用浮点数又想保留小数点确保精度最终就引入了定点数。VMX的tsc scaling也一样只不过用的是2^48微软直接用了2^64。 按照2^64 * qpc_freq / tsc_freq计算出factor之后就可以通过逆运算 __rdtsc()*factor64 得到以10MHZ为基准的QPC计数值。将Factor调大自然也就可以实现倍速本质上跟以前的修改kuser_data差不多都是干扰参与计算的数据最终改变计算结果只是修改的位置不同 3. 以上MSR仅在虚拟机模式才有但变速驱动在物理机上用的也是类似的方式只是改了KeQueryPerfCounter所使用的另一个page中的factor与虚拟机场景有一些差异但基本也是一样的rdtsc*factor64运算逻辑而且更简单因为看了下我的物理机上本身时钟模式就是Period实测物理机上似乎也并不会出现假死问题 4. 至于检测本地检测最简单的方式就是按照公式结合tsc频率计算出原始值然后对比但要注意的是Windows是动态计算出的tsc频率理论上原始factor值本身就有一定误差所以如果仅仅只进行极低倍率的变速有可能会误判; 5. 如果直接用IDA F5去看HyperV的vcpu_run_loop函数在某些版本的HyperV上可能看不到正确逻辑因为紧跟VMResume和VMLanuch的一条跳转指令对IDA F5有一定干扰 6. vcpu_run_loop与vmexit_dispatch 搜索0x4402常量定位vcpu循环线程
vmeixt_dispatch内针对各种vmexit的处理根据switch_case定位各种exit的处理函数