Window 19041下绕过PatchGuard内核变速的实现

阅读量434512

|评论3

|

发布时间 : 2020-11-02 14:30:24

 

这里主要讨论的计时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;
}

image-20201028233953728

可见,已经成功实现计时,周期大约在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倍。变速实现

image-20201028235727560

但是现在这种基于HOOK QueryPerformanceCounter或者RtlQueryPerformanceCounter有个极大的弊端,随随便便打开一个ARK工具都能检测到安装了HOOK,非常不靠谱。

image-20201029000241014

image-20201029184142815

想要更隐蔽的变速,必须深入♂QueryPerformanceCounter寻找更为底层的方案。

开始分析QueryPerformanceCounter的实现。

image-20201029001050692

QueryPerformanceCounter未经任何处理直接跳转到RtlQueryPerformanceCounter。

image-20201029001537270

​ RtlQueryPerformanceCounter会判断SharedUserData+0x3C6的一字节数据中的第0位,如果是0就跳转至走ntdll.NtQueryPerformanceCounter的路子,否则就走rdtscp的路子,但rdtscp的路子不大好走,而通过ntdll.NtQueryPerformanceCounter进入内核的方式操作空间会比较大(SSDT HOOK ,inlinehook 等等),所以得控制程序流程,让这玩意朝着内核走。控制的方案有多种,例如直接将jz修改为jmp,修改SharedUserData+0x3C6的一字节数据中的第0位为0,前者会留下HOOK痕迹而后者不会。

image-20201029184419349

image-20201029001642673

进入内核后,通过直接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);

}

image-20201029003714110

可以看到,计时器被加速了。

这种方式会受到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定位代码。

image-20201029184649991

(抄,我疯狂的抄)

https://github.com/fIappy/infhook19041

当然代码做了修改,原来的代码在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;

image-20201029122642260

打开counter.exe,过几秒加载驱动,可以很明显看到计时器被加速。

因为只修改了数据段,ARK工具扫不到R0和R3的钩子,也不会触发PatchGuard.

image-20201029123007957

检测方案

​ 只要检测SharedUserData->QpcData最低位,[[HalpPerformanceCounter]+0x70]处指针指向的地址是否是nt!HalpHvCounterQueryCounter就行了。

完整工程地址:

https://gitee.com/thereason/ChangeSpeed

img

本文由嫌我烦了是叭原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/221116

安全客 - 有思想的安全新媒体

分享到:微信
+15赞
收藏
嫌我烦了是叭
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66