首页
论坛
课程
招聘
[原创]Windows内核驱动Hook入门
2020-7-29 10:06 6758

[原创]Windows内核驱动Hook入门

2020-7-29 10:06
6758

目录
]

Hook框架选择

基于微软规范的框架

优势:高稳定性、开发难度降低、系统大版本升级兼容
劣势:自由度低、只知其然不知其所以然、框架中未提供功能束手无策

 

相关框架 例如:
文件系统相关:miniFilter、SFilter
进程、注册表相关:ObRegisterCallbacks注册回调
网络:TDI、WFP

微软规范以外的框架

优势:自由度高、没有做不到只有想不到,更接近底层
劣势:难度高 需要对处理的功能前后都了解、错误或不完善的处理会引发未知的错误

 

相关框架 例如:
InfinityHook、SSDTHook、IDTHook 等

 

如果选择微软规范以外的框架,win7 64位系统以后 我们碰到了一只拦路虎——PG (PatchGuard)
我们有两个选择 干掉PG 或者 绕过PG

 

干掉PG
需要找到所有检测点,每个监测点触发的条件和时间都不定,很难过掉所有的检测
绕过PG
InfinityHook 利用Windows的事件跟踪机制,可以与 PatchGuard 同时稳定运行

简单介绍一下InfinityHook

开启 Event Tracing 后,当用户层程序进行系统调用时,首先会调用PerfInfoLogSysCallEntry函数来对此次系统调用进行记录。然后才调用系统调用函数。
PerfInfoLogSysCallEntry内部又会调用驱动注册的WMI_LOGGER_CONTEXT结构中的GetCpuClock指向的函数。
这样就可以通过修改GetCpuClock指针为自己的代理函数,而且此时系统调用函数被放置在栈上,因此在该函数中能从栈中获取系统调用函数的值并进行修改为自己的hook函数。

获取内核中的函数地址

内核中导出的函数

这里我用 ZwQueryInformationProcess 举例

 

定义函数类型

typedef NTSTATUS(*PfnZwQueryInformationProcess) (
    __in HANDLE ProcessHandle,
    __in PROCESSINFOCLASS ProcessInformationClass,
    __out_bcount(ProcessInformationLength) PVOID ProcessInformation,
    __in ULONG ProcessInformationLength,
    __out_opt PULONG ReturnLength
    );

声明函数指针

PfnZwQueryInformationProcess ZwQueryInformationProcess;

获取函数地址

UNICODE_STRING UtrZwQueryInformationProcessName =
        RTL_CONSTANT_STRING(L"ZwQueryInformationProcess");

    ZwQueryInformationProcess =
        (PfnZwQueryInformationProcess)MmGetSystemRoutineAddress(&UtrZwQueryInformationProcessName);

内核未导出的函数

请注意! 调用未导出函数,可能会导致驱动不能兼容高版本;
调用导出函数,也可能会导致驱动不能兼容高版本。
例如:

/* 将当前线程附加到Process参数所指向的进程的地址空间 */
void KeAttachProcess(
  PRKPROCESS Process
);

KeAttachProcess 已过时,Win10.19041 开始不再支持
改用 KeStackAttachProcess

void KeStackAttachProcess(
  PRKPROCESS   PROCESS,
  PRKAPC_STATE ApcState
);

获取 SSDT ShadowSSDT 地址

应用程序调用WinAPI,GUI无关的对应到 ntdll.dll 中的 NtXXX函数,GUI相关的对应到 win32u.dll 中的 NtXXX函数
NtXXX函数中 通过 mov eax,0x***将系统服务号放入EAX(win32u.dll 中真实系统服务号为0x*** - 0x1000
服务分发函数 KiSystemService 根据传入的系统服务号,通过SSDT表 找到内核中的地址。

 

SSDT 系统服务描述符表 (System Services Descriptor Table)
SSDT 对应 ntoskrnl.exe 中的服务函数,这些函数实现了如文件管理、进程管理、设备管理等等相关的功能。
ShadowSSDT 对应 win32k.sys 中的服务函数,重点实现创建窗口、查找窗口、窗口绘图等 Gdi 与用户交互相关的功能。

 

Win7 32 位系统中,SSDT 在内核 Ntoskrnl.exe 中导出,直接获取导出符号 KeServiceDescriptorTable。

 

而在 64 位系统中,SSDT 表并没有在内核 Ntoskrnl.exe 中导出,我们不能像 32 位系统中那样直接获取导出符号 KeServiceDescriptorTable。

 

Win7 x64 与 Win10 64(Win10低版本)中 通过 __readmsr(0xC0000082) 获取内核函数 KiSystemCall64 的地址
KiSystemCall64 中调用了 KeServiceDescriptorTable 和 KeServiceDescriptorTableShadow
IDA查看 KiSystemCall64 的反汇编

     Win7 x64
     nt!KiSystemServiceRepeat:
     fffff800`03e8d772 4c8d15c7202300  lea     r10,[nt!KeServiceDescriptorTable (fffff800`040bf840)]
     fffff800`03e8d779 4c8d1d00212300  lea     r11,[nt!KeServiceDescriptorTableShadow (fffff800`040bf880)]

     Win10 x64
     nt!KiSystemServiceRepeat:
     00000001401906D4 4C 8D 15 A5 71 28 00        lea     r10, KeServiceDescriptorTable
     00000001401906DB 4C 8D 1D 1E 00 27 00        lea     r11, KeServiceDescriptorTableShadow

KeServiceDescriptorTable 特征码 4c8d15
KeServiceDescriptorTableShadow 特征码 4c8d1d

 

win10 高版本中 __readmsr(0xC0000082) 返回 KiSystemCall64Shadow 函数
而 KiSystemCall64Shadow 无法直接搜索到 KeServiceDescriptorTable,IDA查看 KiSystemCall64Shadow 的反汇编

     000000014034F140 KiSystemCall64Shadow proc near
     ...
     000000014034F39A    e9631ae9ff    jmp     KiSystemServiceUser(00000001401CDC02) ; Jump
     000000014034F39F ; ---------------------------------------------------------------------------
     000000014034F39F   c3          retn                        ; Return Near from Procedure
     000000014034F39F KiSystemCall64Shadow endp

     KiSystemServiceUser 中也有
     4C 8D 15 A5 71 28 00        lea     r10, KeServiceDescriptorTable
     4C 8D 1D 1E 00 27 00        lea     r11, KeServiceDescriptorTableShadow

最终获取到的函数地址由 功能代码 变为 jmp跳转
代码从 win32k!NtXXX 跳转到了 win32kfull!NtXXX
win32k.sys 不再直接处理来自用户层的系统服务调用,而真正去处理用户的系统服务调用的函数,实际上是 win32kfull.sys 中的同名函数

 

这里贴的代码是获取 ShadowSSDT 地址,如果需要获取SSDT 地址,简单修改即可

/* 获取ShadowSSDT表地址 */
PVOID GetShadowTableAddress() {

    PUCHAR StartSearchAddress = (PUCHAR)__readmsr(0xC0000082);
    PUCHAR EndSearchAddress = StartSearchAddress + 0x500;

    ULONGLONG KeServiceDescriptorTable = 0;
    KeServiceDescriptorTable = SearchforKeServiceDescriptorTableShadow64(StartSearchAddress, EndSearchAddress);
    if (KeServiceDescriptorTable)
        return (PVOID)KeServiceDescriptorTable;

    /* msr[0xc0000082]变成了KiSystemCall64Shadow函数 */
    ULONGLONG KiSystemServiceUser = 0;
    ULONGLONG templong = 0xffffffffffffffff;
    for (PUCHAR i = StartSearchAddress; i < EndSearchAddress + 0xff; i++)
    {
        if (*(PUCHAR)i == 0xe9 && *(PUCHAR)(i + 5) == 0xc3)
        {
            //fffff803`23733383 e9631ae9ff      jmp     nt!KiSystemServiceUser(fffff803`235c4deb)
            //fffff803`23733388 c3              ret
            RtlCopyMemory(&templong, (PUCHAR)(i + 1), 4);
            KiSystemServiceUser = templong + 5 + (ULONGLONG)i;//KiSystemServiceUser
            EndSearchAddress = (PUCHAR)(KiSystemServiceUser + 0x500);
            KeServiceDescriptorTable = SearchforKeServiceDescriptorTableShadow64((PUCHAR)KiSystemServiceUser, EndSearchAddress);
            return (PVOID)KeServiceDescriptorTable;
        }
    }
    return 0;
}

/* 传入检查的地址的起始与结束位置,根据ShadowSSDT特征查找 */
ULONGLONG SearchforKeServiceDescriptorTableShadow64(PUCHAR StartSearchAddress, PUCHAR EndSearchAddress)
{
    UCHAR b1 = 0, b2 = 0, b3 = 0;
    ULONG templong = 0;
    ULONGLONG KeServiceDescriptorTable = 0;

    /*地址效验 */
    if (MmIsAddressValid(StartSearchAddress) == FALSE)return NULL;
    if (MmIsAddressValid(EndSearchAddress) == FALSE)return NULL;

    for (PUCHAR i = StartSearchAddress; i < EndSearchAddress; i++)
    {
        if (MmIsAddressValid(i) && MmIsAddressValid(i + 1) && MmIsAddressValid(i + 2))
        {
            b1 = *i;
            b2 = *(i + 1);
            b3 = *(i + 2);
            if (b1 == 0x4c && b2 == 0x8d && b3 == 0x1D)/* 4c8d1D */
            {
                memcpy(&templong, i + 3, 4);
                KeServiceDescriptorTable = (ULONGLONG)i + 7 + (ULONGLONG)templong;/* 当前地址 + 指令长度 + 偏移 */
                return KeServiceDescriptorTable;
            }
        }
    }
    return NULL;
}

定义服务描述符表

/* 服务描述符表 */
typedef struct _SERVICE_DESCRIPTOR_TABLE {
    PULONGLONG   ServiceTable;
    PVOID  CounterTable;
    ULONGLONG  TableSize;
    PVOID  ArgumentTable;
} SERVICE_DESCRIPTOR_TABLE, * PSERVICE_DESCRIPTOR_TABLE;

用法举例

PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorShadowTable;
KeServiceDescriptorShadowTable = (PSERVICE_DESCRIPTOR_TABLE)GetShadowTableAddress();

获取系统服务号

手动获取

GUI无关的 使用IDA 查看 ntdll.dll,找到 NtXXX 函数,查看反汇编
找到 mov eax,0x***0x***就是系统服务号
GUI相关的 使用IDA 查看 win32u.dll,找到 NtXXX 函数,查看反汇编
找到 mov eax,0x***0x*** - 0x1000 就是系统服务号

 

注意! 有一部分内核函数的系统服务号,会随着系统版本、补丁发生改变,导致驱动不兼容多版本,建议采用代码自动获取的方式。

获取并判断系统版本

手动获取的系统服务号不能兼容多个版本,所以还需要获取并判断系统版本

 

枚举windows版本

/* 枚举windows版本 */
typedef enum WIN_VER_DETAIL {
    WINDOWS_VERSION_NONE,/* 0 */
    WINDOWS_VERSION_7_7600_UP,
    WINDOWS_VERSION_7_7000,
    WINDOWS_VERSION_10_15063,/* win10 1703 */
    WINDOWS_VERSION_10_16299,/* win10 1709 */
    WINDOWS_VERSION_10_17134,/* win10 1803 */
    WINDOWS_VERSION_10_17763,/* win10 1809 */
    WINDOWS_VERSION_10_18362,/* win10 1903 */
    WINDOWS_VERSION_10_18363,/* win10 1909 */
    WINDOWS_VERSION_10_19041_UP/* win10 大于等于2004版本 */
} WIN_VER_DETAIL;

获取并判断系统版本

/* 获取系统版本 */
WIN_VER_DETAIL GetWindowsVersion()
{
    RTL_OSVERSIONINFOEXW    osverinfo;
    WIN_VER_DETAIL WinVersion = WINDOWS_VERSION_NONE;

    memset(&osverinfo, 0, sizeof(RTL_OSVERSIONINFOEXW));
    osverinfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW);

    if (RtlGetVersion((RTL_OSVERSIONINFOW*)&osverinfo) != STATUS_SUCCESS) {
        return WINDOWS_VERSION_NONE;
    }
    else if (osverinfo.dwMajorVersion == 6 && osverinfo.dwMinorVersion == 1 && osverinfo.dwBuildNumber == 7000) {
        WinVersion = WINDOWS_VERSION_7_7000;
    }
    else if (osverinfo.dwMajorVersion == 6 && osverinfo.dwMinorVersion == 1 && osverinfo.dwBuildNumber >= 7600) {
        WinVersion = WINDOWS_VERSION_7_7600_UP;
    }
    else if (osverinfo.dwMajorVersion == 10 && osverinfo.dwMinorVersion == 0 && osverinfo.dwBuildNumber == 15063) {
        WinVersion = WINDOWS_VERSION_10_15063;
    }
    else if (osverinfo.dwMajorVersion == 10 && osverinfo.dwMinorVersion == 0 && osverinfo.dwBuildNumber == 16299) {
        WinVersion = WINDOWS_VERSION_10_16299;
    }
    else if (osverinfo.dwMajorVersion == 10 && osverinfo.dwMinorVersion == 0 && osverinfo.dwBuildNumber == 17134) {
        WinVersion = WINDOWS_VERSION_10_17134;
    }
    else if (osverinfo.dwMajorVersion == 10 && osverinfo.dwMinorVersion == 0 && osverinfo.dwBuildNumber == 17763) {
        WinVersion = WINDOWS_VERSION_10_17763;
    }
    else if (osverinfo.dwMajorVersion == 10 && osverinfo.dwMinorVersion == 0 && osverinfo.dwBuildNumber == 18362) {
        WinVersion = WINDOWS_VERSION_10_18362;
    }
    else if (osverinfo.dwMajorVersion == 10 && osverinfo.dwMinorVersion == 0 && osverinfo.dwBuildNumber == 18363) {
        WinVersion = WINDOWS_VERSION_10_18363;
    }
    else if (osverinfo.dwMajorVersion == 10 && osverinfo.dwMinorVersion == 0 && osverinfo.dwBuildNumber >= 19041) {
        WinVersion = WINDOWS_VERSION_10_19041_UP;
    }
    kprintf("WinVersion: dwMajorVersion=%d; dwMinorVersion=%d; dwBuildNumber=%d;\n", osverinfo.dwMajorVersion, osverinfo.dwMinorVersion, osverinfo.dwBuildNumber);
    return WinVersion;
}

用法举例

if (WinVersion == WINDOWS_VERSION_10_19041_UP) {/* 当前的infinityhook不支持高于win10.19041的版本 */
    //return STATUS_NDIS_BAD_VERSION;/* 指定版本无效 */
    return STATUS_INCOMPATIBLE_DRIVER_BLOCKED;/* 由于与该系统不兼容,驱动已被禁止加载。请与软件供应商联系以获取驱动程序的兼容版本 */
}

代码自动获取

代码自动获取系统服务号相当于是用代码模拟了手动获取的方式

  1. 将dll读入内存中
  2. 检查是否位有效的PE文件
  3. 根据PE文件结构 PE头->扩展头->数据目录->导出表
  4. 获取以函数名字导出的函数个数、导出函数名称表,遍历找到我们要获取的函数地址
  5. 根据函数地址,找 mov eax,0x* 的特征 0xB8 注意!** Rva到文件偏移的转换

下面贴出代码

/* 一下是获取SSDT表函数索引 */
#define PE_ERROR_VALUE (ULONG)-1

NTSTATUS InitReadFile(PWCHAR Path);/* 初始 将Path读入内存中 */
int GetExportSsdtIndex(const char* ExportName);/* 获取导出表中 函数索引 */
ULONG GetExportOffset(const unsigned char* FileData, ULONG FileSize, const char* ExportName);/* 获取导出表中 函数地址 */
ULONG RvaToOffset(PIMAGE_NT_HEADERS pnth, ULONG Rva, ULONG FileSize);/* 将Rva转为文件偏移 */
void* RtlAllocateMemory(bool InZeroMemory, SIZE_T InSize);/* 分配内存 */
void RtlFreeMemory(void* InPointer);/* 释放内存 */

/* 初始 将Path读入内存中 */
NTSTATUS InitReadFile(PWCHAR Path)
{
    UNICODE_STRING FileName;
    OBJECT_ATTRIBUTES ObjectAttributes;
    RtlInitUnicodeString(&FileName, Path);/* L"\\SystemRoot\\system32\\ntdll.dll" */
    InitializeObjectAttributes(&ObjectAttributes, &FileName,
        OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
        NULL, NULL);

    if (KeGetCurrentIrql() != PASSIVE_LEVEL)
    {
#ifdef _DEBUG
        DbgPrint("[TITANHIDE] KeGetCurrentIrql != PASSIVE_LEVEL!\n");
#endif
        return STATUS_UNSUCCESSFUL;
    }

    HANDLE FileHandle;
    IO_STATUS_BLOCK IoStatusBlock;
    NTSTATUS NtStatus = ZwCreateFile(&FileHandle,
        GENERIC_READ,
        &ObjectAttributes,
        &IoStatusBlock, NULL,
        FILE_ATTRIBUTE_NORMAL,
        FILE_SHARE_READ,
        FILE_OPEN,
        FILE_SYNCHRONOUS_IO_NONALERT,
        NULL, 0);
    if (NT_SUCCESS(NtStatus))
    {
        FILE_STANDARD_INFORMATION StandardInformation = { 0 };
        NtStatus = ZwQueryInformationFile(FileHandle, &IoStatusBlock, &StandardInformation, sizeof(FILE_STANDARD_INFORMATION), FileStandardInformation);
        if (NT_SUCCESS(NtStatus))
        {
            g_FileSize = StandardInformation.EndOfFile.LowPart;
            kprintf("[TITANHIDE] FileSize of ntdll.dll is %08X!\r\n", StandardInformation.EndOfFile.LowPart);
            g_FileData = (unsigned char*)RtlAllocateMemory(true, g_FileSize);

            LARGE_INTEGER ByteOffset;
            ByteOffset.LowPart = ByteOffset.HighPart = 0;
            NtStatus = ZwReadFile(FileHandle,
                NULL, NULL, NULL,
                &IoStatusBlock,
                g_FileData,
                g_FileSize,
                &ByteOffset, NULL);

            if (!NT_SUCCESS(NtStatus))
            {
                RtlFreeMemory(g_FileData);
                kprintf("[TITANHIDE] ZwReadFile failed with status %08X...\r\n", NtStatus);
            }
        }
        else
            kprintf("[TITANHIDE] ZwQueryInformationFile failed with status %08X...\r\n", NtStatus);
        ZwClose(FileHandle);
    }
    else
        kprintf("[TITANHIDE] ZwCreateFile failed with status %08X...\r\n", NtStatus);
    return NtStatus;
}

/* 获取导出表中  SSDT表 ShadowSSDT表 函数索引 */
int GetExportSsdtIndex(const char* ExportName)
{
    ULONG_PTR ExportOffset = GetExportOffset(g_FileData, g_FileSize, ExportName);
    if (ExportOffset == PE_ERROR_VALUE)
        return -1;

    int SsdtOffset = -1;
    unsigned char* ExportData = g_FileData + ExportOffset;
    for (int i = 0; i < 32 && ExportOffset + i < g_FileSize; i++) {
        if (ExportData[i] == 0xC2 || ExportData[i] == 0xC3)  //RET
            break;
        if (ExportData[i] == 0xB8) { //mov eax,X    
            SsdtOffset = *(int*)(ExportData + i + 1);
            break;
        }
    }

    if (SsdtOffset == -1) {
        kprintf("[TITANHIDE] SSDT Offset for %s not found...\r\n", ExportName);
    }

    if (SsdtOffset >= 0x1000) {/* ShadowSSDT表 函数索引减0x1000使用 */
        SsdtOffset -= 0x1000;
    }

    return SsdtOffset;
}

/* 获取导出表中 函数地址 */
ULONG GetExportOffset(const unsigned char* FileData, ULONG FileSize, const char* ExportName)
{
    //Verify DOS Header
    PIMAGE_DOS_HEADER pdh = (PIMAGE_DOS_HEADER)FileData;
    if (pdh->e_magic != IMAGE_DOS_SIGNATURE)
    {
        kprintf("[TITANHIDE] Invalid IMAGE_DOS_SIGNATURE!\r\n");
        return PE_ERROR_VALUE;
    }

    //Verify PE Header
    PIMAGE_NT_HEADERS pnth = (PIMAGE_NT_HEADERS)(FileData + pdh->e_lfanew);
    if (pnth->Signature != IMAGE_NT_SIGNATURE)
    {
        kprintf("[TITANHIDE] Invalid IMAGE_NT_SIGNATURE!\r\n");
        return PE_ERROR_VALUE;
    }

    //Verify Export Directory
    PIMAGE_DATA_DIRECTORY pdd = NULL;
    if (pnth->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC)
        pdd = ((PIMAGE_NT_HEADERS64)pnth)->OptionalHeader.DataDirectory;
    else
        pdd = ((PIMAGE_NT_HEADERS32)pnth)->OptionalHeader.DataDirectory;
    ULONG ExportDirRva = pdd[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    ULONG ExportDirSize = pdd[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
    ULONG ExportDirOffset = RvaToOffset(pnth, ExportDirRva, FileSize);
    if (ExportDirOffset == PE_ERROR_VALUE)
    {
        kprintf("[TITANHIDE] Invalid Export Directory!\r\n");
        return PE_ERROR_VALUE;
    }

    //Read Export Directory
    PIMAGE_EXPORT_DIRECTORY ExportDir = (PIMAGE_EXPORT_DIRECTORY)(FileData + ExportDirOffset);
    ULONG NumberOfNames = ExportDir->NumberOfNames;
    ULONG AddressOfFunctionsOffset = RvaToOffset(pnth, ExportDir->AddressOfFunctions, FileSize);
    ULONG AddressOfNameOrdinalsOffset = RvaToOffset(pnth, ExportDir->AddressOfNameOrdinals, FileSize);
    ULONG AddressOfNamesOffset = RvaToOffset(pnth, ExportDir->AddressOfNames, FileSize);
    if (AddressOfFunctionsOffset == PE_ERROR_VALUE ||
        AddressOfNameOrdinalsOffset == PE_ERROR_VALUE ||
        AddressOfNamesOffset == PE_ERROR_VALUE)
    {
        kprintf("[TITANHIDE] Invalid Export Directory Contents!\r\n");
        return PE_ERROR_VALUE;
    }
    ULONG* AddressOfFunctions = (ULONG*)(FileData + AddressOfFunctionsOffset);
    USHORT* AddressOfNameOrdinals = (USHORT*)(FileData + AddressOfNameOrdinalsOffset);
    ULONG* AddressOfNames = (ULONG*)(FileData + AddressOfNamesOffset);

    //Find Export
    ULONG ExportOffset = PE_ERROR_VALUE;
    for (ULONG i = 0; i < NumberOfNames; i++)
    {
        ULONG CurrentNameOffset = RvaToOffset(pnth, AddressOfNames[i], FileSize);
        if (CurrentNameOffset == PE_ERROR_VALUE)
            continue;
        const char* CurrentName = (const char*)(FileData + CurrentNameOffset);
        ULONG CurrentFunctionRva = AddressOfFunctions[AddressOfNameOrdinals[i]];
        if (CurrentFunctionRva >= ExportDirRva && CurrentFunctionRva < ExportDirRva + ExportDirSize)
            continue; //we ignore forwarded exports
        if (!strcmp(CurrentName, ExportName))  //compare the export name to the requested export
        {
            ExportOffset = RvaToOffset(pnth, CurrentFunctionRva, FileSize);
            break;
        }
    }

    if (ExportOffset == PE_ERROR_VALUE)
    {
        kprintf("[TITANHIDE] Export %s not found in export table!\r\n", ExportName);
    }

    return ExportOffset;
}

/* 将Rva转为文件偏移 */
ULONG RvaToOffset(PIMAGE_NT_HEADERS pnth, ULONG Rva, ULONG FileSize)
{
    PIMAGE_SECTION_HEADER psh = IMAGE_FIRST_SECTION(pnth);
    USHORT NumberOfSections = pnth->FileHeader.NumberOfSections;
    for (int i = 0; i < NumberOfSections; i++)
    {
        if (psh->VirtualAddress <= Rva)
        {
            if ((psh->VirtualAddress + psh->Misc.VirtualSize) > Rva)
            {
                Rva -= psh->VirtualAddress;
                Rva += psh->PointerToRawData;
                return Rva < FileSize ? Rva : PE_ERROR_VALUE;
            }
        }
        psh++;
    }
    return PE_ERROR_VALUE;
}

/* 分配内存 */
void* RtlAllocateMemory(bool InZeroMemory, SIZE_T InSize) {
    void* Result = ExAllocatePoolWithTag(NonPagedPool, InSize, 'HIDE');
    if (InZeroMemory && (Result != NULL))
        RtlZeroMemory(Result, InSize);
    return Result;
}

/* 释放内存 */
void RtlFreeMemory(void* InPointer) {
    ExFreePool(InPointer);
}

获取GUI相关的函数地址,还需附加GUI进程

只有在有 GUI 的线程当中,win32k.sys 的内存才可以被访问。
调用 KeAttachProcess 将当前线程附加到GUI进程的地址空间
KeAttachProcess 需要用到被附加进程的PEPROCESS

获取进程 PEPROCESS

目前最好用的是,遍历PID 对比进程名
其他方式在高版本可能不再兼容

 

这个直接贴代码

/* 通过进程名 获取进程EPROCESS */
NTSTATUS LookupProcessByName(IN PCHAR pcProcessName, OUT PEPROCESS* pEprocess) {

    PEPROCESS    pCurrentEprocess = NULL;
    ULONG uPid = 4;
    PCSZ lpcProcessName = NULL;
    ULONG uLength = 0;            /* 进程名长度 */

    if (!ARGUMENT_PRESENT(pcProcessName) || !ARGUMENT_PRESENT(pEprocess)) {
        return STATUS_INVALID_PARAMETER;
    }
    if (KeGetCurrentIrql() > PASSIVE_LEVEL) {
        return STATUS_UNSUCCESSFUL;
    }

    uLength = strlen(pcProcessName);

    while (uPid < 0x186a0) {
        if (NT_SUCCESS(PsLookupProcessByProcessId((HANDLE)uPid, &pCurrentEprocess)))
        {
            lpcProcessName = (PCSZ)PsGetProcessImageFileName(pCurrentEprocess);
            ObDereferenceObject(pCurrentEprocess);
            if (_strnicmp(pcProcessName, lpcProcessName, uLength) == 0) {
                *pEprocess = pCurrentEprocess;
                /*DbgBreakPoint();*/
                return STATUS_SUCCESS;
            }            
        }
        uPid += 4;
    }    
    return STATUS_FLT_INSTANCE_NOT_FOUND;
}

获取函数地址

用法示例

typedef NTSTATUS(*NtGdiDdDDIGetDisplayModeList)(
    _Inout_ PVOID lpParam
);

static unsigned char* g_FileData = 0;
static ULONG g_FileSize = 0;
static NtUserEnumDisplaySettings g_OriginalNtUserEnumDisplaySettings = NULL;


PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorShadowTable;
KeServiceDescriptorShadowTable = (PSERVICE_DESCRIPTOR_TABLE)GetShadowTableAddress();

NTSTATUS status = STATUS_SUCCESS;
if (KeServiceDescriptorShadowTable) {
    PEPROCESS eprocess_explorer;
    /* 我们得到一个gui进程的对象,因为我们切换进程的时候需要用到 */
    if (LookupProcessByName("explorer.exe", &eprocess_explorer) == STATUS_SUCCESS || LookupProcessByName("csrss.exe", &eprocess_explorer) == STATUS_SUCCESS)
    {
        KeAttachProcess(eprocess_explorer);/* 附加到目标进程 */

        ULONG64 shadowSSDTTable = *(ULONG64*)((PCHAR)KeServiceDescriptorShadowTable + 0x20);
        LONG32* shadowSSDTEntry = (LONG32*)shadowSSDTTable;

        if (!NT_SUCCESS(InitReadFile(L"\\SystemRoot\\system32\\win32u.dll"))) {
            kprintf("InitReadFile failed...\n");
        }
        else {
            int nNtUserEnumDisplaySettingsSerialNumber = GetExportSsdtIndex("NtUserEnumDisplaySettings");
            if ((nNtUserEnumDisplaySettingsSerialNumber == -1)|| (nNtGdiDdDDIGetDisplayModeListSerialNumber == -1)) {
                status = STATUS_UNSUCCESSFUL;
            }
            LONG32 offsetNtUserEnumDisplaySettings = shadowSSDTEntry[nNtUserEnumDisplaySettingsSerialNumber] >> 4;
            g_OriginalNtUserEnumDisplaySettings = (NtUserEnumDisplaySettings)(offsetNtUserEnumDisplaySettings + shadowSSDTTable);
            RtlFreeMemory(g_FileData);
            kprintf("NtUserEnumDisplaySettings=%d;NtGdiDdDDIGetDisplayModeList=%d;\n", nNtUserEnumDisplaySettingsSerialNumber, nNtGdiDdDDIGetDisplayModeListSerialNumber);
        }
        KeDetachProcess();/* 解除附加 */
    }
}
if (g_OriginalNtUserEnumDisplaySettings == NULL)
    return STATUS_UNSUCCESSFUL;
else
    return status;

替换被Hook的函数 的函数实现

获取函数原型

如果为内核中导出的函数,MSDN可以查到函数原型
例如:ZwQueryInformationProcess
ZwQueryInformationProcess

 

如果为内核未导出的函数

  1. 浏览器搜索,可能大佬已经先一步分享了函数原型
  2. IDA查看ntdll.dll、win32u.dll、ntoskrnl.exe、win32k.sys(IDA分析的函数原因很可能不准确,作为参考)
    在这里插入图片描述
    在这里插入图片描述
  3. 自己写一个测试程序,单独调用R3的API,OD调试,跟踪到NtXXX函数,分析参数;如果参数是堆地址等,分析NtXXX返回后 堆填充的内容,如果不需要用到所有的返回信息,不需自己定义一个结构体,参数类型给成 PVOID 即可,记录需要用到的信息所在的字节偏移。

测试代码,测试R3 APIEnumDisplaySettingsW R0 APINtUserEnumDisplaySettings

int main() {
    DEVMODEW DeviceMode = { 0 };/* SIZE 220 DC */   
    EnumDisplaySettingsW(NULL, ENUM_CURRENT_SETTINGS,&DeviceMode);
    return 0;
}

X64dbg 执行NtXXX 函数前
可以看到有四个参数,参数三是 RtlAllocateHeap 申请的堆地址
在这里插入图片描述
X64dbg 执行NtXXX 函数后,分析返回的信息,提取有用的部分
在这里插入图片描述

如果被Hook的函数是一个高频函数,如何准确定位到是自己的程序调用

思路:

  1. 获取当前线程所属进程的 PEPROCESS
  2. 通过 PEPROCESS 获取当前进程句柄
  3. 通过 进程句柄 调用 ZwQueryInformationProcess 获取 进程名

    实现代码

    /*++
    Routine Description:
    获取指定进程的完整进程名
    Arguments:
    pEproc - 指定进程的EPROCESS地址
    Return Value:
    成功则返回进程名,失败返回NULL
    Comments:
    该函数返回的进程名,由调用则负责释放(ExFreePool)
    --*/
    PUNICODE_STRING GetProcNameByEproc(IN PEPROCESS pEproc) {
    
    NTSTATUS NtStatus;
    HANDLE hProc = NULL;
    PBYTE pBuf = NULL;
    ULONG ulSize = 32;
    PAGED_CODE();
    
    // 1. pEproc --> handle
    NtStatus = ObOpenObjectByPointer(pEproc,
        OBJ_KERNEL_HANDLE,
        NULL,
        0,
        NULL,
        KernelMode,
        &hProc
    );
    if (!NT_SUCCESS(NtStatus))
        return NULL;
    
    // 2. ZwQueryInformationProcess
    while (TRUE)
    {
        pBuf = (PBYTE)ExAllocatePoolWithTag(NonPagedPool, ulSize, ALLOC_TAG);
        if (!pBuf) {
            ZwClose(hProc);
            return NULL;
        }
        NtStatus = ZwQueryInformationProcess(hProc,
            ProcessImageFileName,
            pBuf,
            ulSize,
            &ulSize);
        if (NtStatus != STATUS_INFO_LENGTH_MISMATCH)
            break;
        ExFreePool(pBuf);
    }
    ZwClose(hProc);
    if (!NT_SUCCESS(NtStatus)) {
        ExFreePool(pBuf);
        return NULL;
    }
    // 3. over
    return (PUNICODE_STRING)pBuf;
    }
    

    用法示例

    /* 调试用 */
    PUNICODE_STRING CurProcName = GetProcNameByEproc(IoGetCurrentProcess());
    if ((CurProcName->Length != 0) && (wcsstr(CurProcName->Buffer, L"test.exe"))) {
        DbgBreakPoint();
    }
    ExFreePool(CurProcName);
    

    分析不同情况下的返回值类型,调用的 R3 API 如何进行处理这些返回值

    例如:

系统设置是如何获取的显示分辨率列表

 

系统设置->显示设置->显示分辨率
设置 是通过,调用 EnumDisplaySettingsW,
参数二 从0开始,每次调用+1,每次获取一条显示信息,直到返回值为0,枚举出了所有显示信息;

 

而内核中的处理是,当参数二为0时,操作系统将初始化并缓存有关显示设备的信息。当您将参数二设置为非零值调用EnumDisplaySettings时,该函数将返回上次在参数二为0的情况下调用该函数时缓存的信息。

 

内核NtAPI 返回成功 STATUS_SUCCESS 返回值 rax = 0
而当最后一个显示信息被返回后,再次参数二+1调用,内核会返回 STATUS_INVALID_PARAMETER_2 rax = 0xC00000F0L

 

当系统枚举显示信息时,我们如果在挂钩的函数中,直接返回 STATUS_INVALID_PARAMETER_2,系统将停止枚举分辨率。

分析驱动如何返回信息到R3,返回值如何修改

还是用 NtUserEnumDisplaySettings 举例
NtUserEnumDisplaySettings 使用第三个参数,也是R3 API 申请的堆来保存要返回的显示信息
如果我们要修改R3 EnumDisplaySettingsW 拿到的显示信息,可以在挂钩的内核函数中 直接修改(当前线程与申请堆的线程所属进程一致时,可以直接修改)
第一次调用返回我们设置好的显示信息,第二次调用返回 STATUS_INVALID_PARAMETER_2,系统的设置就只能选择我们设置的分辨率,而没有其他选项。

驱动与R3通讯

驱动与R3通讯,比较简单 驱动中 只需创建设备、将设备对象和符号链接绑定;R3的应用程序打开设备调用DeviceIoControl与驱动通讯。
需要注意的是: 我们只用到了DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL],但是其他的功能函数也需要填充,否则R3的应用程序打开设备时将报错。

通讯 驱动部分代码

static UNICODE_STRING g_devName = RTL_CONSTANT_STRING(L"\\Device\\XXXXXXDevice");/* 设备对象名, 需要以`\Device\`为前缀 */
static UNICODE_STRING g_symLinkName = RTL_CONSTANT_STRING(L"\\DosDevices\\XXXXXXDeviceLink");
/* 创建R3、R0通讯 设备 符号链接 */
NTSTATUS CreateR3IOR0(_In_ PDRIVER_OBJECT DriverObject) {

    NTSTATUS status = STATUS_SUCCESS;
    DEVICE_OBJECT* pDev = NULL;/* 创建一个设备对象 用于接收3环的IO请求 */

    status = IoCreateDevice(
        DriverObject,        /* 新设备对象所属的驱动对象 */
        0,
        &g_devName,            /* 设备对象名 */
        FILE_DEVICE_UNKNOWN,/* 设备类型,默认为此值 */
        0,                    /* 默认填0 */
        FALSE,                /* 必须为FALSE */
        &pDev);                /* 输出参数, 是创建出来的新设备 */

    if (!NT_SUCCESS(status)) {
        KdPrint(("创建设备对象失败,错误:%08X\n", status));
        return status;
    }

    /*  指定设备对象使用缓冲区的方式 */
    pDev->Flags |= DO_BUFFERED_IO;/* DO_DIRECT_IO */

    /* 将设备对象和符号链接绑定. 没有符号链接,三环无法打开设备 */
    status = IoCreateSymbolicLink(
        &g_symLinkName,    /*符号链接名*/
        &g_devName);        /*设备名*/

    if (!NT_SUCCESS(status)) {
        return STATUS_UNSUCCESSFUL;
    }

    for (int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) {

        DriverObject->MajorFunction[i] = PassThroughDispatch;
    }
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = OnDeviceControl;

    return status;
}

NTSTATUS OnDeviceControl(DEVICE_OBJECT* device, IRP* irp) {

    UNREFERENCED_PARAMETER(device);
    /*DbgBreakPoint();*/
    /* 获取当前IO栈 */
    IO_STACK_LOCATION* pIoStack = IoGetCurrentIrpStackLocation(irp);
    /* 获取CTL_CODE */ 
    ULONG ctlCode = pIoStack->Parameters.DeviceIoControl.IoControlCode;

    enum DeviceCode {
        Stop_Hook = CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS),
        recovery_Hook = CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
    };

    switch (ctlCode) {

    case Stop_Hook: {
        if (g_StopHook == FALSE) {
            InterlockedIncrement(&g_StopHook);
        }
        break;
    }    
    case recovery_Hook: {
        if (g_StopHook == TRUE) {
            InterlockedDecrement(&g_StopHook);
        }
        break;
    }        
    default:
        break;
    }

    /* 需要在派遣函数中, 将IO请求设置为完成 */
    /* IoStatus - IO完成状态 */
    irp->IoStatus.Status = STATUS_SUCCESS;/* IO请求完成的状态 */
    /* irp->IoStatus.Information = 0; *//* IO请求完成的字节数 */
    /* 将irp(io请求包)设置为完成 */
    IoCompleteRequest(irp, IO_NO_INCREMENT);
    return irp->IoStatus.Status;
}

NTSTATUS PassThroughDispatch(PDEVICE_OBJECT  DeviceObject, PIRP Irp) {

    UNREFERENCED_PARAMETER(DeviceObject);
    Irp->IoStatus.Status = STATUS_SUCCESS;     //LastError()
    Irp->IoStatus.Information = 0;             //ReturnLength
    IoCompleteRequest(Irp, IO_NO_INCREMENT);   //将Irp返回给Io管理器
    return STATUS_SUCCESS;
}

用法示例 (在 DriverEntry 中调用)

NTSTATUS status = STATUS_SUCCESS;
status = CreateR3IOR0(DriverObject);/* 创建R3、R0通讯 设备 符号链接 */
if (!NT_SUCCESS(status)) {
    return status;
}

通讯 R3应用程序部分代码

enum DeviceCode {
    Stop_Hook = CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS),
    recovery_Hook = CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
};

bool OpenDevice(PHANDLE PhDev) {
    /*
        通过CreateFile打开设备
        打开设备的时候,路径以`\??\`为前缀
        后面跟着的名字就是驱动中创建的符号链接名.
    */
    *PhDev =
        CreateFile(L"\\??\\DisableResolutionDeviceLink",
            GENERIC_WRITE | GENERIC_READ,
            FILE_SHARE_READ,
            NULL,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,
            NULL);
    if (*PhDev == INVALID_HANDLE_VALUE) {
        printf("打开设备错误:%08X\n", GetLastError());
        return false;
    }
    return true;
}

通讯示例

DWORD dwBytesReturned = 0;
if (FALSE == DeviceIoControl(hDev,/*设备对象*/
    Stop_Hook, nullptr, NULL, nullptr, NULL,
    &dwBytesReturned,/*实际完成的字节数*/    /* 如果lpOverlapped(最后一个参数)为NULL,则lpBytesReturned不能为NULL。即使当操作不返回任何输出数据并且lpOutBuffer为NULL时,DeviceIoControl 也会使用lpBytesReturned。 */
    NULL)) {
    printf("Stop_Hook DeviceIoControl失败: %08X\n", GetLastError());
}

其他

声明参数未引用

UNREFERENCED_PARAMETER(参数);

中断请求级别

高IRQL的代码,可以中断(抢占)低IRQL的代码的执行过程,从而得到执行机会

 

常见的IRQL:
IRQLd等级 | 介绍
-------- | -----
PASSIVE_LEVEL | 应用层线程以及大部分内核函数处于该IRQL,可以无限制使用所有内核API,可以访问分页及非分页内存
APC_LEVEL | 异步方法调用(APC)或页错误时处于该IRQL,可以使用大部分内核API,可以访问分页及非分页内存
DISPATCH_LEVEL | 延迟方法调用(DPC)时处于该IRQL,可以使用特定的内核API,只能访问非分页内存

 

注意! 在调用任何一个内核API前,必须查看WDK文档,了解这个内核API的中断要求!

 

获取当前IRQL

NTHALAPI KIRQL KeGetCurrentIrql();

参考

      [1]: Windows内核编程
     [2]: https://bbs.pediy.com/thread-253450.htm
     [3]: https://github.com/mrexodia/TitanHide/tree/3bb2af068278ca64d67f4ca109bb56121d946131
     [4]: https://github.com/microsoft/Windows-driver-samples/tree/master/filesys/miniFilter
     [5]: https://github.com/fIappy/InfinityHook
     [6]: https://www.angelic47.com/archives/85/

[公告]《使用DCI技术进行全栈调试》训练营,硬件调试器你的,《软件调试》作者张银奎亲自授课!

最后于 2020-7-29 10:06 被随风行编辑 ,原因:
收藏
点赞18
打赏
分享
最新回复 (28)
雪    币: 682
活跃值: 活跃值 (1237)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
随风行 活跃值 1 2020-7-29 10:20
2
0

部分代码为自己实现,
也有很多思路和代码来源自大佬们分享的帖子。
自己研究win10新版本hook时,东拼西凑的资料实属不易,
为方便后学的朋友,这里做个整理。
首发在CSDN https://blog.csdn.net/weixin_43956962/article/details/107366974

最后于 2020-7-29 10:20 被随风行编辑 ,原因:
雪    币: 2715
活跃值: 活跃值 (116)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
shuyangzjg 活跃值 2020-7-29 10:35
3
0
感谢分享
雪    币: 4177
活跃值: 活跃值 (1071)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2020-7-29 10:35
4
0
mark,楼主辛苦了
雪    币: 6082
活跃值: 活跃值 (245)
能力值: ( LV5,RANK:68 )
在线值:
发帖
回帖
粉丝
万剑归宗 活跃值 1 2020-7-29 10:56
5
0
据说新版8T16T下已经炸裂了
雪    币: 164
活跃值: 活跃值 (191)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
fengyunabc 活跃值 1 2020-7-29 11:01
6
0
感谢分享!
雪    币: 682
活跃值: 活跃值 (1237)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
随风行 活跃值 1 2020-7-29 11:01
7
0
万剑归宗 据说新版8T16T下已经炸裂了
有链接吗?我瞅瞅
雪    币: 691
活跃值: 活跃值 (694)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
NONAME剑人 活跃值 3 2020-7-29 15:48
8
0
感谢大佬
雪    币: 4594
活跃值: 活跃值 (2296)
能力值: ( LV13,RANK:450 )
在线值:
发帖
回帖
粉丝
amzilun 活跃值 4 2020-7-29 16:04
9
0
干的漂亮
雪    币: 671
活跃值: 活跃值 (13)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
oyxbl 活跃值 2020-7-29 16:05
10
0
厉害
雪    币: 682
活跃值: 活跃值 (1237)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
随风行 活跃值 1 2020-7-29 16:12
11
0
NONAME剑人 感谢大佬
刚拜读了大佬的帖子,就看到大佬给我回复了 有点小激动啊
大佬的帖子 SysWOW64的奇技淫巧,我之前也有过类似的想法,在32位程序中注入64位的dll,这样注入可能更隐蔽一些
雪    币: 136
活跃值: 活跃值 (149)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
niuzuoquan 活跃值 2020-7-29 17:48
12
0
感谢分享
雪    币: 188
活跃值: 活跃值 (95)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
wujimaa 活跃值 1 2020-7-29 17:59
13
0
感谢分享!
雪    币: 2876
活跃值: 活跃值 (137)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
iamasbcx 活跃值 2020-7-29 18:26
14
0
感谢分享。InfinityHook 
雪    币: 3255
活跃值: 活跃值 (394)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Jmsrwt 活跃值 2020-7-29 20:24
15
0
感谢分享!
雪    币: 220
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
那年雨夏 活跃值 2020-7-29 22:18
16
0
感谢分享
雪    币: 294
活跃值: 活跃值 (63)
能力值: ( LV4,RANK:45 )
在线值:
发帖
回帖
粉丝
一二三六 活跃值 2020-7-29 23:21
17
0
感谢分享
雪    币: 2042
活跃值: 活跃值 (108)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
xxxxxxxxxoo 活跃值 2020-7-30 05:45
18
0
dddddd
雪    币: 3702
活跃值: 活跃值 (260)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
kxzpy 活跃值 2020-7-30 07:40
19
0
不错,虽然看不懂
雪    币: 153
活跃值: 活跃值 (91)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
人在塔在 活跃值 2020-7-31 19:05
20
0
谢谢分享
雪    币: 165
活跃值: 活跃值 (265)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
初学小潘 活跃值 2020-8-1 11:06
21
0
感谢分享,认真学习
雪    币: 2161
活跃值: 活跃值 (245)
能力值: ( LV11,RANK:185 )
在线值:
发帖
回帖
粉丝
MTRush 活跃值 1 2020-8-1 11:54
22
0
学习了
雪    币: 4434
活跃值: 活跃值 (1461)
能力值: ( LV12,RANK:270 )
在线值:
发帖
回帖
粉丝
与时尽现。 活跃值 6 2020-8-7 11:13
23
0
老哥,你秃了,也变强了
雪    币: 7000
活跃值: 活跃值 (322)
能力值: ( LV12,RANK:339 )
在线值:
发帖
回帖
粉丝
findreamwang 活跃值 6 2020-8-7 14:34
24
0
好文章!!
雪    币: 682
活跃值: 活跃值 (1237)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
随风行 活跃值 1 2020-8-8 10:50
25
0
与时尽现。 老哥,你秃了,也变强了[em_13]
再过两年 我就可以去拍三毛流浪记了
游客
登录 | 注册 方可回帖
返回