这里主要讨论的计时API函数为QueryPerformanceCounter
原型如下
BOOL QueryPerformanceCounter(
LARGE_INTEGER *lpPerformanceCount//指向接收当前性能计数器值的变量的指针。
);
//检索性能计数器的当前值,该值是可用于时间间隔测量的高分辨率(<1us)时间戳。
也就是说能更改他返回lpPerformanceCount内的数值就能实现变速。
现在我们通过QueryPerformanceCounter编写一个以1000ms为周期的计时函数。
int main()
{
LARGE_INTEGER time1{};
LARGE_INTEGER time2{};
do
{
QueryPerformanceCounter(&time1);
Sleep(1000);
QueryPerformanceCounter(&time2);
std::cout << time2.QuadPart - time1.QuadPart << std::endl;
} while (1);
return 0;
}
可见,已经成功实现计时,周期大约在1000ms左右(出现波动的原因是Sleep并不精确)。
接下来我们测试应用层变速的效果,写入QueryPerformanceCounter HOOK
typedef BOOL (*pfnQueryPerformanceCounter)(
LARGE_INTEGER* lpPerformanceCount
);
pfnQueryPerformanceCounter fnQueryPerformanceCounter = nullptr;
PVOID g_Kernel32_QueryPerformanceCounter = nullptr;
PVOID g_Ntdll_RtlQueryPerformanceCounter = nullptr;
DWORD64 time = 0;
//山寨函数
BOOL FakeQueryPerformanceCounter(
LARGE_INTEGER* lpPerformanceCount
) {
auto ret = ((pfnQueryPerformanceCounter)g_Ntdll_RtlQueryPerformanceCounter)(lpPerformanceCount);
lpPerformanceCount->QuadPart = time + round(lpPerformanceCount->QuadPart - time) * 20;
return ret;
}
BYTE jmpcode[] = { 0XFF, 0X25, 0X00, 0X00, 0X00, 0X00, 0X00 ,0X00 ,0X00 ,0X00 ,0X00 ,0X00 ,0X00 ,0X00 };
//用于jmp到山寨函数
int main()
{
LARGE_INTEGER time1;
LARGE_INTEGER time2;
g_Kernel32_QueryPerformanceCounter = GetProcAddress(GetModuleHandleA("Kernel32.dll"),"QueryPerformanceCounter");
g_Ntdll_RtlQueryPerformanceCounter = GetProcAddress(GetModuleHandleA("ntdll.dll"), "RtlQueryPerformanceCounter");
QueryPerformanceCounter(&time1);
Sleep(1000);
QueryPerformanceCounter(&time2);
std::cout << time2.QuadPart - time1.QuadPart << std::endl;
//start to hook QueryPerformanceCounter
DWORD oldprotect;
/*KERNEL32.QueryPerformanceCounter - 48 FF 25 31C10600 - jmp qword ptr[KERNEL32.DLL + 81D48]{ ->ntdll.RtlQueryPerformanceCounter }*/
//原函数直接调用ntdll.RtlQueryPerformanceCounter
*(DWORD64*)(jmpcode + 6) = (DWORD64)FakeQueryPerformanceCounter;
VirtualProtect(QueryPerformanceCounter,14,PAGE_EXECUTE_READWRITE,&oldprotect);
memcpy(g_Kernel32_QueryPerformanceCounter,jmpcode,14);
VirtualProtect(QueryPerformanceCounter, 14, oldprotect, &oldprotect);
do
{
QueryPerformanceCounter(&time1);
Sleep(1000);
QueryPerformanceCounter(&time2);
std::cout << time2.QuadPart - time1.QuadPart << std::endl;
} while (1);
return 0;
}
首先获取到QueryPerformanceCounter与RtlQueryPerformanceCounter,其中RtlQueryPerformanceCounter的作用是当做QueryPerformanceCounter的原函数使用,无需再申请一部分内存用于执行原来的流程。
接下来测试一次HOOK之前的计时效果。
修改QueryPerformanceCounter头14字节,使其能够跳转到FakeQueryPerformanceCounter函数修改QueryPerformanceCounter中的返回参数。
FakeQueryPerformanceCounter中先执行原函数,根据公式
当前返回时间 = 上次返回时间 + (当前返回时间 - 上次返回时间) * 倍数
修改参数。
执行程序,可以明显看到计时加快了20倍。变速实现
但是现在这种基于HOOK QueryPerformanceCounter或者RtlQueryPerformanceCounter有个极大的弊端,随随便便打开一个ARK工具都能检测到安装了HOOK,非常不靠谱。
想要更隐蔽的变速,必须深入♂QueryPerformanceCounter寻找更为底层的方案。
开始分析QueryPerformanceCounter的实现。
QueryPerformanceCounter未经任何处理直接跳转到RtlQueryPerformanceCounter。
RtlQueryPerformanceCounter会判断SharedUserData+0x3C6的一字节数据中的第0位,如果是0就跳转至走ntdll.NtQueryPerformanceCounter的路子,否则就走rdtscp的路子,但rdtscp的路子不大好走,而通过ntdll.NtQueryPerformanceCounter进入内核的方式操作空间会比较大(SSDT HOOK ,inlinehook 等等),所以得控制程序流程,让这玩意朝着内核走。控制的方案有多种,例如直接将jz修改为jmp,修改SharedUserData+0x3C6的一字节数据中的第0位为0,前者会留下HOOK痕迹而后者不会。
进入内核后,通过直接inlinehook nt!NtQueryPerformanceCounter的方案就能实现变速。
#define DPRINT(...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, __VA_ARGS__)
NTSTATUS FakeNtQueryPerformanceCounter(
_Out_ PLARGE_INTEGER PerformanceCounter,
_Out_opt_ PLARGE_INTEGER PerformanceFrequency
)
{
pfnNtQueryPerformanceCounter pOldFunc = (pfnNtQueryPerformanceCounter)GetinlineHookEntry(g_NtQueryPerformanceCounter)->OriFunc;
auto name = PsGetProcessImageFileName(PsGetCurrentProcess());
auto status = pOldFunc(PerformanceCounter, PerformanceFrequency);
if (strstr(name, "test") != NULL)
{
DPRINT("FakeNtQueryPerformanceCounter\n");
PerformanceCounter->QuadPart = time.QuadPart + (PerformanceCounter->QuadPart - time.QuadPart) * 2;
}
return status;
}
void Unload(PDRIVER_OBJECT driverObject)
{
UNREFERENCED_PARAMETER(driverObject);
InstallInlineHook(g_NtQueryPerformanceCounter);
DPRINT("[+]QpcData %u\n", *(BYTE*)(&SharedUserData->QpcData));
*(BYTE*)(&SharedUserData->QpcData) = *(BYTE*)(&SharedUserData->QpcData) | 1;
DPRINT("[+]QpcData %u\n", *(BYTE*)(&SharedUserData->QpcData));
DPRINT("Unload!\n");
}
EXTERN_C NTSTATUS DriverEntry(const PDRIVER_OBJECT driverObject, const PUNICODE_STRING registryPath) {
UNREFERENCED_PARAMETER(driverObject);
UNREFERENCED_PARAMETER(registryPath);
DPRINT("[+]QpcData %u\n", *(BYTE*)(&SharedUserData->QpcData));
*(BYTE*)(&SharedUserData->QpcData) = *(BYTE*)(&SharedUserData->QpcData) & 0xFFFE;
DPRINT("[+]QpcData %u\n", *(BYTE*)(&SharedUserData->QpcData));
g_NtQueryPerformanceCounter = reinterpret_cast<PVOID>( GetSystemServiceDescriptorTableFunction(49));
InstallInlineHook(g_NtQueryPerformanceCounter,FakeNtQueryPerformanceCounter);
driverObject->DriverUnload = Unload;
return STATUS_SUCCESS;
}
验证程序
int main()
{
LARGE_INTEGER time1{};
LARGE_INTEGER time2{};
do
{
QueryPerformanceCounter(&time1);
Sleep(1000);
QueryPerformanceCounter(&time2);
std::cout << time2.QuadPart - time1.QuadPart << std::endl;
} while (1);
}
可以看到,计时器被加速了。
这种方式会受到PatchGuard的影响,无法稳定运行在Window x64 19041下,ARK工具也能很容易检测到钩子,与直接HOOK RtlQueryPerformanceCounter没有本质区别,但可以采取在Windows 19041下进行Infinityhook类似的方法绕过PatchGuard与ARK工具的检测。
我们继续看NtQueryPerformanceCounter。
函数原型:
NTSTATUS NtQueryPerformanceCounter(
_Out_ PLARGE_INTEGER PerformanceCounter,
_Out_opt_ PLARGE_INTEGER PerformanceFrequency
);
打开windbg,输入uf nt!NtQueryPerformanceCounter查看反汇编代码。
0: kd> uf nt!NtQueryPerformanceCounter
nt!NtQueryPerformanceCounter:
fffff805`5acac4c0 48895c2408 mov qword ptr [rsp+8],rbx
fffff805`5acac4c5 57 push rdi
fffff805`5acac4c6 4883ec20 sub rsp,20h
fffff805`5acac4ca 488bda mov rbx,rdx //;rbx PLARGE_INTEGER pPerformanceFrequency
fffff805`5acac4cd 488bf9 mov rdi,rcx //;rdi PLARGE_INTEGER pPerformanceCounter
fffff805`5acac4d0 488364244000 and qword ptr [rsp+40h],0
fffff805`5acac4d6 65488b042588010000 mov rax,qword ptr gs:[188h] //;_ethread
fffff805`5acac4df 80b83202000000 cmp byte ptr [rax+232h],0 //;+0x232 PreviousMode : Char
fffff805`5acac4e6 0f840a601800 je nt!NtQueryPerformanceCounter+0x186036 (fffff805`5ae324f6)
进入NtQueryPerformanceCounter后,系统或获取当前线程PreviousMode,如果是KernelMode就跳到nt!NtQueryPerformanceCounter+0x186036处执行,否则接着执行。
//KernelMode和UserMode都有这操作。
// LARGE_INTEGER NewPerformanceFrequency = {0};
// LARGE_INTEGER ret = KeQueryPerformanceCounter(&NewPerformanceFrequency);
// *pPerformanceCounter = ret;
// if(pPerformanceFrequency != 0)
// pPerformanceFrequency = &NewPerformanceFrequency;
// return STATUS_SUCCESS;
nt!NtQueryPerformanceCounter+0x186036:
fffff805`5ae324f6 488d4c2440 lea rcx,[rsp+40h]
fffff805`5ae324fb e84064acff call nt!KeQueryPerformanceCounter (fffff805`5a8f8940)
fffff805`5ae32500 488907 mov qword ptr [rdi],rax
fffff805`5ae32503 4885db test rbx,rbx
fffff805`5ae32506 0f8447a0e7ff je nt!NtQueryPerformanceCounter+0x93 (fffff805`5acac553)
nt!NtQueryPerformanceCounter+0x18604c:
fffff805`5ae3250c 488b442440 mov rax,qword ptr [rsp+40h]
fffff805`5ae32511 488903 mov qword ptr [rbx],rax
fffff805`5ae32514 e93aa0e7ff jmp nt!NtQueryPerformanceCounter+0x93 (fffff805`5acac553)
当PreviousMode == UserMode时会触碰到用户缓冲区,必须用结构化异常处理探测通过参数传过来的两个用户地址是否可写,避免操作不合法的用户缓冲区地址导致蓝屏。在try except中执行了与nt!NtQueryPerformanceCounter+0x186036相同的处理——调用KeQueryPerformanceCounter。
nt!NtQueryPerformanceCounter+0x2c:
fffff805`5acac4ec 40f6c703 test dil,3
fffff805`5acac4f0 7559 jne nt!NtQueryPerformanceCounter+0x8b (fffff805`5acac54b)
nt!NtQueryPerformanceCounter+0x32:
fffff805`5acac4f2 48ba0000ffffff7f0000 mov rdx,7FFFFFFF0000h
fffff805`5acac4fc 488bca mov rcx,rdx
fffff805`5acac4ff 483bfa cmp rdi,rdx
fffff805`5acac502 480f42cf cmovb rcx,rdi
fffff805`5acac506 8a01 mov al,byte ptr [rcx]
fffff805`5acac508 8801 mov byte ptr [rcx],al
fffff805`5acac50a 8a4107 mov al,byte ptr [rcx+7]
fffff805`5acac50d 884107 mov byte ptr [rcx+7],al
fffff805`5acac510 4885db test rbx,rbx
fffff805`5acac513 7514 jne nt!NtQueryPerformanceCounter+0x69 (fffff805`5acac529)
nt!NtQueryPerformanceCounter+0x55:
fffff805`5acac515 488d4c2440 lea rcx,[rsp+40h]
fffff805`5acac51a e821c4c4ff call nt!KeQueryPerformanceCounter (fffff805`5a8f8940)
fffff805`5acac51f 488907 mov qword ptr [rdi],rax
fffff805`5acac522 4885db test rbx,rbx
fffff805`5acac525 751a jne nt!NtQueryPerformanceCounter+0x81 (fffff805`5acac541)
nt!NtQueryPerformanceCounter+0x67:
fffff805`5acac527 eb2a jmp nt!NtQueryPerformanceCounter+0x93 (fffff805`5acac553)
nt!NtQueryPerformanceCounter+0x69:
fffff805`5acac529 f6c303 test bl,3
fffff805`5acac52c 751d jne nt!NtQueryPerformanceCounter+0x8b (fffff805`5acac54b)
nt!NtQueryPerformanceCounter+0x6e:
fffff805`5acac52e 483bda cmp rbx,rdx
fffff805`5acac531 480f42d3 cmovb rdx,rbx
fffff805`5acac535 8a02 mov al,byte ptr [rdx]
fffff805`5acac537 8802 mov byte ptr [rdx],al
fffff805`5acac539 8a4207 mov al,byte ptr [rdx+7]
fffff805`5acac53c 884207 mov byte ptr [rdx+7],al
fffff805`5acac53f ebd4 jmp nt!NtQueryPerformanceCounter+0x55 (fffff805`5acac515)
nt!NtQueryPerformanceCounter+0x81:
fffff805`5acac541 488b442440 mov rax,qword ptr [rsp+40h]
fffff805`5acac546 488903 mov qword ptr [rbx],rax
fffff805`5acac549 ebdc jmp nt!NtQueryPerformanceCounter+0x67 (fffff805`5acac527)
nt!NtQueryPerformanceCounter+0x8b:
fffff805`5acac54b e8102a0d00 call nt!ExRaiseDatatypeMisalignment (fffff805`5ad7ef60)
fffff805`5acac550 90 nop
fffff805`5acac551 eb02 jmp nt!NtQueryPerformanceCounter+0x95 (fffff805`5acac555)
nt!NtQueryPerformanceCounter+0x93:
fffff805`5acac553 33c0 xor eax,eax
nt!NtQueryPerformanceCounter+0x95:
fffff805`5acac555 488b5c2430 mov rbx,qword ptr [rsp+30h]
fffff805`5acac55a 4883c420 add rsp,20h
fffff805`5acac55e 5f pop rdi
fffff805`5acac55f c3 ret
查看KeQueryPerformanceCounter的反汇编代码
在下面代码的第八行看到了HalpPerformanceCounter,nt!KeQueryPerformanceCounter+0x56这里把[[HalpPerformanceCounter]+0x70]的值传进了rax后调用了nt!guard_dispatch_icall
控制流防护(CFG)作为Win 8.1+的新安全保护机制,其实并不知名。它被用于阻止针对可执行文件间接调用的恶意利用。CFG保护十分高效,同时它是一种编译器和操作系统相结合的防护手段。
实质上就是一个调用,调用了[[HalpPerformanceCounter]+0x70]处指向的函数。
原本调用地址nt!HalpHvCounterQueryCounter,把此处函数指针修改为指向自己的函数就能实现控制堆栈,进而控制函数返回地址至我们的函数操作NtQueryPerformanceCounter的参数实现变速。
0: kd> uf nt!KeQueryPerformanceCounter
nt!KeQueryPerformanceCounter:
fffff805`5a8f8940 48895c2420 mov qword ptr [rsp+20h],rbx
fffff805`5a8f8945 56 push rsi
fffff805`5a8f8946 4883ec20 sub rsp,20h
fffff805`5a8f894a 48897c2430 mov qword ptr [rsp+30h],rdi
fffff805`5a8f894f 488bf1 mov rsi,rcx
fffff805`5a8f8952 488b3daf549600 mov rdi,qword ptr [nt!HalpPerformanceCounter (fffff805`5b25de08)]
fffff805`5a8f8959 4c89742440 mov qword ptr [rsp+40h],r14
fffff805`5a8f895e 83bfe400000005 cmp dword ptr [rdi+0E4h],5
fffff805`5a8f8965 0f8581000000 jne nt!KeQueryPerformanceCounter+0xac (fffff805`5a8f89ec)
nt!KeQueryPerformanceCounter+0x2b:
fffff805`5a8f896b 48833da555960000 cmp qword ptr [nt!HalpTimerReferencePage (fffff805`5b25df18)],0
fffff805`5a8f8973 48c744243880969800 mov qword ptr [rsp+38h],989680h
fffff805`5a8f897c 0f84fe511100 je nt!KeQueryPerformanceCounter+0x115240 (fffff805`5aa0db80)
nt!KeQueryPerformanceCounter+0x42:
fffff805`5a8f8982 f787e000000000000100 test dword ptr [rdi+0E0h],10000h
fffff805`5a8f898c 0f8506531100 jne nt!KeQueryPerformanceCounter+0x115358 (fffff805`5aa0dc98)
nt!KeQueryPerformanceCounter+0x52:
fffff805`5a8f8992 488b4f48 mov rcx,qword ptr [rdi+48h]
nt!KeQueryPerformanceCounter+0x56:
fffff805`5a8f8996 488b4770 mov rax,qword ptr [rdi+70h]
fffff805`5a8f899a e841011000 call nt!guard_dispatch_icall (fffff805`5a9f8ae0)
fffff805`5a8f899f 488bc8 mov rcx,rax
fffff805`5a8f89a2 49b8b803000080f7ffff mov r8,0FFFFF780000003B8h
fffff805`5a8f89ac 488b0565559600 mov rax,qword ptr [nt!HalpTimerReferencePage (fffff805`5b25df18)]
fffff805`5a8f89b3 488b4008 mov rax,qword ptr [rax+8]
fffff805`5a8f89b7 4d8b00 mov r8,qword ptr [r8]
fffff805`5a8f89ba 48f7e1 mul rax,rcx
fffff805`5a8f89bd 498d0410 lea rax,[r8+rdx]
有了思路就能开始写代码了。
首先从fIappy大表哥那抄一份获取SSDT函数、HalpPerformanceCounter定位代码。
(抄,我疯狂的抄)
当然代码做了修改,原来的代码在19041.508版本下HalpPerformanceCounter会定位飞。
NTSTATUS getKernelModuleByName(const char* moduleName, std::uintptr_t* moduleStart, std::size_t* moduleSize) {
if (!moduleStart || !moduleSize)
return STATUS_INVALID_PARAMETER;
std::size_t size{};
ZwQuerySystemInformation(0xB, nullptr, size, reinterpret_cast<PULONG>(&size));/* 0xB SystemModuleInformation */
const auto listHeader = ExAllocatePool(NonPagedPool, size);
if (!listHeader)
return STATUS_MEMORY_NOT_ALLOCATED;
if (const auto status = ZwQuerySystemInformation(0xB, listHeader, size, reinterpret_cast<PULONG>(&size)))
return status;
auto currentModule = reinterpret_cast<PSYSTEM_MODULE_INFORMATION>(listHeader)->Module;
for (std::size_t i{}; i < reinterpret_cast<PSYSTEM_MODULE_INFORMATION>(listHeader)->Count; ++i, ++currentModule) {
const auto currentModuleName = reinterpret_cast<const char*>(currentModule->FullPathName + currentModule->OffsetToFileName);
if (!strcmp(moduleName, currentModuleName)) {
*moduleStart = reinterpret_cast<std::uintptr_t>(currentModule->ImageBase);
*moduleSize = currentModule->ImageSize;
return STATUS_SUCCESS;
}
}
return STATUS_NOT_FOUND;
}
std::uintptr_t getImageSectionByName(const std::uintptr_t imageBase, const char* sectionName, std::size_t* sizeOut) {
if (reinterpret_cast<PIMAGE_DOS_HEADER>(imageBase)->e_magic != 0x5A4D)
return {};
const auto ntHeader = reinterpret_cast<PIMAGE_NT_HEADERS64>(
imageBase + reinterpret_cast<PIMAGE_DOS_HEADER>(imageBase)->e_lfanew);
const auto sectionCount = ntHeader->FileHeader.NumberOfSections;
auto sectionHeader = IMAGE_FIRST_SECTION(ntHeader);
for (std::size_t i{}; i < sectionCount; ++i, ++sectionHeader) {
if (!strcmp(sectionName, reinterpret_cast<const char*>(sectionHeader->Name))) {
if (sizeOut)
*sizeOut = sectionHeader->Misc.VirtualSize;
return imageBase + sectionHeader->VirtualAddress;
}
}
return {};
}
std::uintptr_t getServiceDescriptorTable() {
std::uintptr_t ntoskrnlBase{};
std::size_t ntoskrnlSize{};
if (!NT_SUCCESS(getKernelModuleByName("ntoskrnl.exe", &ntoskrnlBase, &ntoskrnlSize)))
return {};
std::size_t ntoskrnlTextSize{};
const auto ntoskrnlText = getImageSectionByName(ntoskrnlBase, ".text", &ntoskrnlTextSize);
if (!ntoskrnlText)
return {};
auto keServiceDescriptorTableShadow = scanPattern(reinterpret_cast<std::uint8_t*>(ntoskrnlText), ntoskrnlTextSize,
"\xC1\xEF\x07\x83\xE7\x20\x25\xFF\x0F", "xxxxxxxxx");
if (!keServiceDescriptorTableShadow)
return {};
keServiceDescriptorTableShadow += 21;
keServiceDescriptorTableShadow += *reinterpret_cast<std::int32_t*>(keServiceDescriptorTableShadow) + sizeof(std::int32_t);
return keServiceDescriptorTableShadow;
}
std::uintptr_t GetSystemServiceDescriptorTableFunction(std::int32_t Index)
{
if (keServiceDescriptorTable == NULL)
keServiceDescriptorTable = getServiceDescriptorTable();
const auto serviceTable = *reinterpret_cast<std::int32_t**>(keServiceDescriptorTable);
return reinterpret_cast<std::uintptr_t>(serviceTable) + (serviceTable[Index & 0xFFF] >> 4);
}
std::uintptr_t scanPattern(std::uint8_t* base, const std::size_t size, char* pattern, char* mask) {
const auto patternSize = strlen(mask);
for (std::size_t i = {}; i < size - patternSize; i++) {
for (std::size_t j = {}; j < patternSize; j++) {
if (mask[j] != '?' && *reinterpret_cast<std::uint8_t*>(base + i + j) != static_cast<std::uint8_t>(pattern[j]))
break;
if (j == patternSize - 1)
return reinterpret_cast<std::uintptr_t>(base) + i;
}
}
return {};
}
NTSTATUS hookPerformanceCounterRoutine()
{
UNICODE_STRING keQueryPerformanceCounterUnicode = RTL_CONSTANT_STRING(L"KeQueryPerformanceCounter");
const auto keQueryPerformanceCounter = reinterpret_cast<std::uintptr_t>(
MmGetSystemRoutineAddress(&keQueryPerformanceCounterUnicode));
if (!keQueryPerformanceCounter)//如果nt!KeQueryPerformanceCounter没有定位到直接退出
return STATUS_NOT_FOUND;
//1: kd > u KeQueryPerformanceCounter
// nt!KeQueryPerformanceCounter:
// fffff805`5a8f8940 48895c2420 mov qword ptr[rsp + 20h], rbx
// fffff805`5a8f8945 56 push rsi
// fffff805`5a8f8946 4883ec20 sub rsp, 20h
// fffff805`5a8f894a 48897c2430 mov qword ptr[rsp + 30h], rdi
// fffff805`5a8f894f 488bf1 mov rsi, rcx //;rsi pPerformanceFrequency
// fffff805`5a8f8952 488b3daf549600 mov rdi, qword ptr[nt!HalpPerformanceCounter(fffff805`5b25de08)]
auto halpPerformanceCounter = scanPattern(reinterpret_cast<std::uint8_t*>(keQueryPerformanceCounter),
0x100, "\xf1\x48\x8b\x3d", "xxxx");
halpPerformanceCounter += 4;
halpPerformanceCounter = halpPerformanceCounter + *reinterpret_cast<std::int32_t*>(halpPerformanceCounter) + 4;
return STATUS_SUCCESS;
}
之后保存一份nt!HalpHvCounterQueryCounter地址,用于恢复变速,山寨函数中跳转到原函数使用。
halCounterQueryRoutine = *reinterpret_cast<std::uintptr_t*>( *reinterpret_cast<std::uintptr_t*>(halpPerformanceCounter) + 0x70);
*reinterpret_cast<std::uintptr_t*>(*reinterpret_cast<std::uintptr_t*>(halpPerformanceCounter) + 0x70) = reinterpret_cast<std::uintptr_t>(&temper);
修改指针至temper山寨函数。temper中保存rcx(keQueryPerformanceCounterHook需要用到一个参数),并将rsp作为参数传入keQueryPerformanceCounterHook。
temper PROC
push rcx
mov rcx,rsp
call keQueryPerformanceCounterHook
pop rcx
mov rax, halCounterQueryRoutine
jmp rax
temper ENDP
EXTERN_C void keQueryPerformanceCounterHook(ULONG_PTR* pStack) {
if (ExGetPreviousMode() == KernelMode) {
return;
}
//for (size_t i = 0; i < 10; i++)
//{
// if (pStack[i] - pNtQueryPerformanceCounter <= 0x100)
// {
// DPRINT("[+]%u %X\n", i, pStack[i] - pNtQueryPerformanceCounter);
// return;
// }
//}
//因windbg不能正常断在KeQueryPerformanceCounterHook,通过这种方式能方便的弄清楚堆栈。
//1: kd> g
//HalpPerformanceCounter:FFFFF8055B25DE08 halCounterQueryRoutine:FFFFF8055A98AB20
//[+]7 5F
//[+]7 5F
//[+]7 5F
//[+]7 5F
//获得了pStack[7]中返回位置为NtQueryPerformanceCounter+0x5F,可以修改pStack[7]让它返回至自己的代理函数实现变速。
auto name = PsGetProcessImageFileName(PsGetCurrentProcess());
if (strstr(name,"counter")!= NULL && pStack[7] == static_cast<std::uint64_t>(pNtQueryPerformanceCounter) + 0x5F)
{
//将返回地址重定向至自己的代理函数,用作处理KeQueryPerformanceCounter的返回值
pStack[7] = reinterpret_cast<ULONG_PTR>(handler);
}
}
在handler代理函数中,保存了rcx,rdx用于计算,将计算后的结果打入rdi(NtQueryPerformanceCounter的第二个参数pPerformanceCounter),实现修改NtQueryPerformanceCounter参数作用。最后跳转回NtQueryPerformanceCounter+0x62的位置执行NtQueryPerformanceCounter原本的流程。
nt!NtQueryPerformanceCounter+0x55:
fffff805`5acac515 488d4c2440 lea rcx,[rsp+40h]
fffff805`5acac51a e821c4c4ff call nt!KeQueryPerformanceCounter (fffff805`5a8f8940)
fffff805`5acac51f 488907 mov qword ptr [rdi],rax //*pPerformanceCounter = ret
fffff805`5acac522 4885db test rbx,rbx //;62H
handler PROC
push rcx
push rdx
mov rdx,rax ;rdx保存这次的时间
mov rcx,time ;rcx就是上次的时间
sub rax,rcx
lea rax,[rcx+rax*2] ;当前返回时间 = 上次返回时间 + (当前返回时间 - 上次返回时间) * 倍数
mov [rdi],rax
mov time,rdx
pop rdx
pop rcx
mov rax,[pNtQueryPerformanceCounter]
add rax,62h
jmp rax
handler ENDP
最终别忘了修改SharedUserData.
*(BYTE*)(&SharedUserData->QpcData) = *(BYTE*)(&SharedUserData->QpcData) & 0xFFFE;
打开counter.exe,过几秒加载驱动,可以很明显看到计时器被加速。
因为只修改了数据段,ARK工具扫不到R0和R3的钩子,也不会触发PatchGuard.
检测方案
只要检测SharedUserData->QpcData最低位,[[HalpPerformanceCounter]+0x70]处指针指向的地址是否是nt!HalpHvCounterQueryCounter就行了。
完整工程地址:
发表评论
您还未登录,请先登录。
登录