0.背景以及工具
背景:尝试利用变速齿轮的加速原理,提升项目运行速度。
Windows版本:Windows10 1607(14393.0)
工具:Windbg Preview、IDA Pro 7.6、VMWare、火绒剑。
1.开始
Windows有个工具叫变速齿轮,该软件能改变另一个软件的运行速度,原理是对目标进程进行DLL注入后hook一系列与Windows时钟相关的API,
如图,查看目标进程(DbgView.exe)包含的模块、进行钩子扫描得到:
(注入情况)
(挂钩情况)
注入的DLL是GearNtKe.dll,挂钩的函数是:GetTickCount、QueryPerformanceCounter、TimeGetTime、SetTimer、GetMessageTime、TimeSetEvent。这里关注与系统时间相关的函数。
用IDA打开GearNtKe.dll,查看Hook的函数:
(Hook_QueryPerformanceCounter)
其实这里就是加速的核心原理了:
当前时间戳(x倍速) = (当前时间戳(1倍数) - 上一个时间戳)* 加速倍数x
其中:qword_1001F018 = 上一个时间戳;dword_1001F020 = 速度,这两个都是全局变量,所以hook的函数都使用了互斥量来保护他们。
对于其余被hook的函数,其实逻辑都一样,就是修改当前时间戳的值来欺骗系统,让他误以为时间间隔变短/长了(速度变快/慢了)。Hook多个不同的函数是为了保证控制范围足够,避免发生时间戳不同步的问题。
(Hook_TimeGetTime)
(Hook_GetTickCount)
另外的,Cheat Engine的SpeedHack功能也是差不多这样实现的,注入SpeedHack.dll,然后hook。连核心算法都是一样的。
不过这种DLL注入+Hook函数的方式太过低级,非常容易被检测出来,不应该注入到别人的程序中,但是可以注入到自己的RPA程序中,加快鼠标点击、滚轮或拖拽的效率。另外的,这种方法只能欺骗自己的机器,对于需要联网的程序,强行改变客户端运行的速度可能会导致异常。
如果想要隐蔽地变速,不妨考虑在驱动层寻找方法。
2.内核层研究
关于Window的时钟,我们还想了解更多,比如:GetTickCount的原理是什么? QueryPerfromanceCounter为何能精确到微秒?里面是否有可以动手脚的地方?
2-1.GetTickCount的原理
观察kernel32!GetTickCount,实现非常简单,直接(ds:[0x7FFE0320] * ds:[0x7FFE004]) >> 0x24,这样就得到了自系统启动以来经过的毫秒数。
其中ds:[0x7FFE004]是一个定值,等于0xFA00000,所以随时间在增加的只有一个变量:ds:[0x7FFE0320]。
对于kernel32!GetTickCount64,其实也差不多 ((ds:[0x7FFE0004] << 32) * (ds:[0x7FFE0320] << 8)) >> 64; 本质是一样的,因为
(x*232*y*28)*2^(-64) = x*y*2^(-24),只是在这里使用了mul指令,将两个64位数相乘,能得到最高128位的数,防止溢出。
所以通过修改ds:[0x7FFE004]、ds:[0x7FFE0320]也可以达到变速的效果,具体操作的话还是利用上面的算法。但是这种做法也有非常明显的缺点,因为这个地方是内核与用户态公用的内存区域,修改之后所有使用这个API的进程的运行速度都会受到影响。
2-2. QueryPerfromanceCounter的原理
查看ntdll!RtlQueryPerformanceCounter
可以看出,当(ds:[0x7FFE03C6] & 1) == 0 或者 !RtlpHypervisorSharedUserVa || !*(_DWORD *)RtlpHypervisorSharedUserVa 成立时,才会调用NtQueryPerformanceCounter进入内核。不进入内核的话,就使用rdtsc指令或者rdtscp指令获取时间戳。
打开Intel64 手册卷1,查看指令集:处理器在每个时钟周期内单调递增时钟周期计数器MSR,并在处理器重置时将其归零,而rdtsc指令从时钟周期计数器MSR中读取数据,存入EDX:EAX中,如果CPU的主频是1GHz,那么时钟周期的持续时间就是1纳秒,所以这个API能精确到微秒也是可以理解的,类似的还有rdtscp指令,它还额外返回处理器的id到ECX中。
CR4寄存器中的时间戳禁用(TSD)标志限制了RDTSC指令的使用。当标志未设置时,RDTSC指令可以在任何特权级别执行;当标志设置时,仅能在特权级别0(内核态)下执行。所以调用NtQueryPerformanceCounter前的判断应该就是这样来的。
ntoskrnl!NtQueryPerformanceCounter:
追踪到HvlGetReferenceTimeUsingTscPage,最终还是调用了rdtsc、rdtscp这些指令:
对于时钟函数,到这里基本明确了原理,但是前面反复出现的地址0x7FFE0320、0x7FFE004等,这些到底是什么东西?是怎么来的?
2-3. KUSER_SHARED_DATA:内核-用户态共享内存
查阅资料得:
(x64内核虚拟地址空间布局)
(x86/x64 内核-用户态共享内存映射关系)
打开Windbg, 使用!kuser命令,共享内存的地址果然在0xFFFFF780`00000000
命令dt nt!_KUSER_SHARED_DATA,查看该结构体:
那么之前在各种时钟API里面出现的地址都可以看到他们的含义,同时结合代码逻辑得到:
GetTickCount:(TickCountMultiplier * TickCount ) >> 24
QueryperformanceCounter: (rdtsc() + QpcBias) >> QpcShift
其余的时钟函数也可以通过相同的方式逆向出算法,所以对于windows的时间戳函数的实现原理也基本明确了。