内核层的三种Hook技术的使用


1.前言
本文是在Win7 X86系统上进行实验,实验的内容是在内核层通过三种不同的Hook方法来实现进程保护,让运行的程序不被其他的程序关闭。这里用来被保护的程序是一个简单的HelloWord弹窗程序,程序名是demo.exe。

2.实现原理
一个程序要想关闭一个进程,首先就要获取这个进程的句柄,并且获得的这个句柄还需要具备进程关闭的访问掩码。也就是说,在用户层我们想要关闭一个进程,首先就需要使用以下的代码来获得一个进程的句柄。

HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, dwPid);

其中第一个参数PROCESS_TERMINATE就是让我们获得的进程句柄具有关闭进程的访问掩码,它在文档中的定义如下:

#define PROCESS_TERMINATE                  (0x0001)

那也就是说,如果HOOK了打开进程的函数,就可以在我们编写的函数中判断所打开的进程是否是我们要保护的进程,以及所要获得的进程句柄是否是要具备进程关闭的访问掩码,如果是的话就把它去掉。这个时候,程序如果使用这个被删除掉进程关闭访问掩码的句柄来关闭进程,就会失败,这就完成了对进程的保护作用。

而在用户层所调用的OpenProcess到内核层以后,其实在内核中是调用ZwOpenProcess,所以只要在内核中监控这个函数就可以完成实验。该函数在文档中的定义如下:

NTSTATUS
  ZwOpenProcess(
    __out PHANDLE  ProcessHandle,
    __in ACCESS_MASK  DesiredAccess,
    __in POBJECT_ATTRIBUTES  ObjectAttributes,
    __in_opt PCLIENT_ID  ClientId
    );

参数2所代表的就是要打开的这个进程要具备的访问掩码,通过对它的判断与修改就可以实现进程保护。

参数4的一个指向CLIENT_ID的指针,CLIENT_ID在文档中的定义如下:

typedef struct _CLIENT_ID {
    HANDLE UniqueProcess;
    HANDLE UniqueThread;
} CLIENT_ID;
typedef CLIENT_ID *PCLIENT_ID;

其中的UniqueProcess代表的就是进程的PID,根据它就可以用来判断是否是要保护的进程。

3.代码框架
由于三个实验只是HOOK的方法的代码不同,而其他的比如用户层与内核层的通信等等是相同的。所以为了避免重复,这里先给出代码框架,后续的HOOK代码只需要加进去就好。

用户层所要做的事情就是:

安装和卸载驱动
根据要保护的进程名获取进程PID,以传给内核层供内核层使用
根据用户输入选择安装或者卸载HOOK,向内核层发送相应的IoControlCode并把返回结果告诉用户

用户层的代码如下:


#include <cstdio>
#include <Windows.h>
#include <TlHelp32.h>

#define IOCTRL_BASE 0x800
#define MYIOCTRL_CODE(i) CTL_CODE(FILE_DEVICE_UNKNOWN, IOCTRL_BASE + i, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define CTL_HOOK MYIOCTRL_CODE(0)
#define CTL_UNHOOK MYIOCTRL_CODE(1)
#define LINK_NAME "\\\\.\\HookDeviceLink" //符号名
#define DRIVER_NAME "HookDriver"  //驱动名称
#define OUT_BUFFER_LENGTH 1        //输出缓冲区的长度
#define INPUT_BUFFER_LENGTH 4     //输入缓冲区的长度
#define PROTECTED_NAME "demo.exe" //要保护的进程名

BOOL InstallService(); //安装驱动服务
BOOL UnInstallService();   //卸载驱动
DWORD GetPid();    //获得要保护的进程PID

int main()
{
    DWORD dwAns;                 //用来输入控制程序执行
    HANDLE hDevice = NULL;
    BOOLEAN bRes = FALSE;       //输出缓冲区,用来判断是否Hook成功
    DWORD dwRetLength = 0;       //接受缓冲区长度

    if (InstallService())
    {
        // 获取设备句柄
        hDevice = CreateFile(LINK_NAME, GENERIC_READ | GENERIC_WRITE,
            0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
        if (hDevice == INVALID_HANDLE_VALUE)
        {
            printf("CreateFile Error %d.\n", GetLastError());
            goto exit;
        }
     
        while (TRUE)
        {
            dwAns = 0;
            bRes = TRUE;
     
            printf("选择操作,(输入1 2 3):\n");
            printf("1是开启HOOK,2是关闭HOOK,3是退出程序\n");
            scanf("%d", &dwAns);
     
            if (dwAns == 3) break;
            else if (dwAns == 1)
            {
                DWORD dwPid = GetPid();
                if (dwPid == 0)
                {
                    printf("没有找到进程\n");
                    continue;
                }
                //开启HOOK
                if (!DeviceIoControl(hDevice, CTL_HOOK, &dwPid, INPUT_BUFFER_LENGTH,
                                     &bRes, OUT_BUFFER_LENGTH, &dwRetLength, NULL))
                {
                    printf("DeviceIoControl CTL_HOOK Error\n");
                    break;
                }
     
                if (bRes) printf("Hook 成功\n");
                else printf("Hook 失败\n");
            }
            else if (dwAns == 2)
            {
                //关闭HOOK
                if (!DeviceIoControl(hDevice, CTL_UNHOOK, NULL, 0,
                              &bRes, OUT_BUFFER_LENGTH, &dwRetLength, NULL))
                {
                    printf("DeviceIoControl CTL_UNHOOK Error\n");
                    break;
                }
     
                if (bRes) printf("UnHook 成功\n");
                else printf("UnHook 失败\n");
            }
        }
    exit:
        if (hDevice) CloseHandle(hDevice);
        if (!UnInstallService())
        {
            printf("卸载驱动失败\n");
        }
    }
    else
    {
        printf("驱动加载失败\n");
    }
    system("pause");
     
    return 0;
}

BOOL UnInstallService()
{
    BOOL bRet = TRUE;
    SC_HANDLE hService = NULL, hSCMHandle = NULL;
    SERVICE_STATUS ServiceStatus = { 0 };

    hSCMHandle = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);        //建立服务控制管理器连接
    if (hSCMHandle == NULL)
    {
        printf("OpenSCManager error %d\n", GetLastError());
        bRet = FALSE;
        goto exit;
    }
         
    hService = OpenService(hSCMHandle, DRIVER_NAME, SERVICE_ALL_ACCESS);
    if (hService == NULL)
    {
        printf("CreateService error %d\n", GetLastError());
        bRet = FALSE;
        goto exit;
    }
     
    if (!ControlService(hService, SERVICE_CONTROL_STOP, &ServiceStatus))        //停止驱动服务
    {
        printf("ControlService error %d\n", GetLastError());
        bRet = FALSE;
        goto exit;
    }
     
    if (!DeleteService(hService))        //删除驱动
    {
        printf("DeleteService error %d\n", GetLastError());
        bRet = FALSE;
        goto exit;
    }
     
    printf("驱动卸载成功\n");
exit:
    if (hSCMHandle)        CloseServiceHandle(hSCMHandle);
    if (hService) CloseServiceHandle(hService);

    return bRet;
}

BOOL InstallService()
{
    SC_HANDLE hSCMHandle = NULL, hService = NULL;
    BOOL bRet = TRUE;
    CHAR szDriverPath[MAX_PATH] = { 0 };

    hSCMHandle = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);        //建立服务控制管理器连接
    if (hSCMHandle == NULL)
    {
        printf("OpenSCManager error %d\n", GetLastError());
        bRet = FALSE;
        goto exit;
    }
         
        //获取当前程序运行目录
    GetCurrentDirectory(MAX_PATH, szDriverPath);
    strcat(szDriverPath, "\\");
    strcat(szDriverPath, DRIVER_NAME);
    strcat(szDriverPath, ".sys");
    //创建一个服务对象
    hService = CreateService(hSCMHandle,
        DRIVER_NAME,                        //驱动名称
        DRIVER_NAME,                        //显示的名称
        SERVICE_ALL_ACCESS,
        SERVICE_KERNEL_DRIVER,        //指定内核驱动程序
        SERVICE_DEMAND_START,            //需要时候开启
        SERVICE_ERROR_NORMAL,
        szDriverPath,        //驱动的路径
        NULL, NULL, NULL, NULL, NULL);
    if (hService == NULL)
    {
        printf("CreateService error %d\n", GetLastError());
        bRet = FALSE;
        goto exit;
    }
     
    if (!StartService(hService, 0, NULL))        //启动服务
    {
        printf("StartService error %d\n", GetLastError());
        bRet = FALSE;
        goto exit;
    }
     
    printf("驱动安装成功\n");
exit:
    if (hSCMHandle)        CloseServiceHandle(hSCMHandle);
    if (hService) CloseServiceHandle(hService);

    return bRet;
}

DWORD GetPid()
{
    DWORD dwPid = 0;
    PROCESSENTRY32 pe32 = { 0 };
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    BOOL bRet = FALSE;

    if (hSnap == INVALID_HANDLE_VALUE)
    {
        printf("CreateToolhelp32Snapshot process %d\n", GetLastError());
        return 0;
    }
     
    pe32.dwSize = sizeof(pe32);
    bRet = Process32First(hSnap, &pe32);
    while (bRet)
    {
        if (lstrcmp(pe32.szExeFile, PROTECTED_NAME) == 0)
        {
            dwPid = pe32.th32ProcessID;
            break;
        }
        bRet = Process32Next(hSnap, &pe32);
    }
     
    CloseHandle(hSnap);
     
    return dwPid;
}

而驱动程序就要接受用户层传下来的IoControlCode来进行Hook或者UnHook,并把返回结果传回用户层,具体代码如下:

#include <ntifs.h>

#define IOCTRL_BASE 0x800
#define MYIOCTRL_CODE(i) CTL_CODE(FILE_DEVICE_UNKNOWN, IOCTRL_BASE + i, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define CTL_HOOK MYIOCTRL_CODE(0)
#define CTL_UNHOOK MYIOCTRL_CODE(1)
#define DEVICE_NAME L"\\Device\\HookDevice"
#define SYMBOL_LINK_XP L"\\DosDevices\\HookDeviceLink"
#define SYMBOL_LINK_WIN7 L"\\DosDevices\\Global\\HookDeviceLink"

// 进程访问权限,允许关闭进程
#define PROCESS_TERMINATE  (0x0001) 

VOID DriverUnload(IN PDRIVER_OBJECT driverObject);
NTSTATUS DispatchCommon(PDEVICE_OBJECT pObj, PIRP pIrp);
NTSTATUS DispatchIoctrl(PDEVICE_OBJECT pObj, PIRP pIrp);
BOOLEAN Hook();
BOOLEAN UnHook();

ULONG g_uPID = 0;            //要保护的进程的PID
ULONG g_uOrgFuncAddr = 0;    //保存原函数地址

NTSTATUS DriverEntry(IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING registryPath)
{
    NTSTATUS status = STATUS_SUCCESS;
    UNICODE_STRING uDeviceName, uSymbolLinkName;
    ULONG i = 0;
    PDEVICE_OBJECT pDeviceOjb = NULL;

    //创建设备
    RtlInitUnicodeString(&uDeviceName, DEVICE_NAME);
    status = IoCreateDevice(driverObject, NULL, &uDeviceName, FILE_DEVICE_UNKNOWN, 
                            FILE_DEVICE_SECURE_OPEN, FALSE, &pDeviceOjb);
    if (!NT_SUCCESS(status))
    {
        DbgPrint("IoCreateDevice %X\r\n", status);
        goto exit;
    }
     
    //设置数据交互方式
    pDeviceOjb->Flags |= DO_BUFFERED_IO;
     
    //创建符号链接
    if (IoIsWdmVersionAvailable(1, 0x10)) //根据操作系统版本来初始化符号名
    {
        RtlInitUnicodeString(&uSymbolLinkName, SYMBOL_LINK_WIN7);
    }
    else
    {
        RtlInitUnicodeString(&uSymbolLinkName, SYMBOL_LINK_XP);
    }
     
    status = IoCreateSymbolicLink(&uSymbolLinkName, &uDeviceName);
    if (!NT_SUCCESS(status))
    {
        DbgPrint("IoCreateSymbolicLink %X\r\n", status);
        goto exit;
    }
     
    for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
    {
        driverObject->MajorFunction[i] = DispatchCommon;
    }
    driverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoctrl;
    DbgPrint("驱动成功加载\r\n");
exit:
    driverObject->DriverUnload = DriverUnload;
    return STATUS_SUCCESS;
}

NTSTATUS DispatchIoctrl(PDEVICE_OBJECT pObj, PIRP pIrp)
{
    BOOLEAN bRes = FALSE;
    NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
    PIO_STACK_LOCATION pIrpStack;
    ULONG uIoControlCode = 0, uInformation = 0;
    PVOID pIoBuffer = NULL;

    //获取设备栈
    pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
    //获取控制码
    uIoControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;
    //获取输入输出缓冲区
    pIoBuffer = pIrp->AssociatedIrp.SystemBuffer;
     
    switch(uIoControlCode)
    {
        case CTL_HOOK:
        {
            memcpy(&g_uPID, pIoBuffer, sizeof(g_uPID));       //保存用户层传入的进程PID
            bRes = Hook();
            memcpy(pIoBuffer, &bRes, sizeof(bRes));
            uInformation = sizeof(bRes);
            if (bRes)
            {
                DbgPrint("Hook 成功\r\n");
                DbgPrint("要保护的进程PID是:%d\r\n", g_uPID);
                status = STATUS_SUCCESS;
            }
            break; 
        }
        case CTL_UNHOOK:
        {
            bRes = UnHook();
            g_uPID = 0;
            memcpy(pIoBuffer, &bRes, sizeof(bRes));
            uInformation = sizeof(bRes);
            status = STATUS_SUCCESS;
            if (bRes) DbgPrint("UnHook 成功\r\n");
            break;
        }
        default:
        {
            uInformation = 0;
            DbgPrint("Unknown IoControlCode\r\n");
            status = STATUS_SUCCESS;
            break;
        }  
    }
     
    pIrp->IoStatus.Status = status;
    pIrp->IoStatus.Information = uInformation;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
     
    return STATUS_SUCCESS;
}

BOOLEAN Hook()
{
    BOOLEAN bRes = TRUE;

    return bRes;
}

BOOLEAN UnHook()
{
    BOOLEAN bRes = TRUE;

    return bRes;
}

NTSTATUS DispatchCommon(PDEVICE_OBJECT pObj, PIRP pIrp)
{
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;

    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
     
    return STATUS_SUCCESS;
}

VOID DriverUnload(IN PDRIVER_OBJECT driverObject)
{
    UNICODE_STRING uSymbolLinkName;

    if (driverObject->DeviceObject)
    {
        IoDeleteDevice(driverObject->DeviceObject);
        if (IoIsWdmVersionAvailable(1, 0x10))
        {
            RtlInitUnicodeString(&uSymbolLinkName, SYMBOL_LINK_WIN7);
        }
        else
        {
            RtlInitUnicodeString(&uSymbolLinkName, SYMBOL_LINK_XP);
        }
        IoDeleteSymbolicLink(&uSymbolLinkName);
    }
    DbgPrint("驱动卸载完成\r\n");
}

4.InlineHook
InlineHook的实现原理是:将原函数要执行的代码改为jmp跳转指令,跳转到我们要执行的代码中执行中。而在我们的代码中,为了保证Hook以后原本的功能可以实现就需要恢复被修改的字节然后再次调用,可是在多线程的环境中频繁的修改与删除很容易带来错误,造成蓝屏。

为了避免这种情况,可以采取热补丁的技术来实现,首先看看NtOpenProcess函数的反汇编结果。

可以看到在函数最开始执行了一句mov edi, edi这么一个无意义的指令,而这个指令对应的硬编码是两字节0x8BFF,而在这个函数上面有5个字节的0x90。所以可以通过修改最开始的两个字节实现短跳转跳到上面的5个字节的0x90的开始地址执行,而这五个字节的0x90就可以修改为跳转到要真正执行的我们的函数的地址。

而在我们的函数中,如果要执行原来的代码,只需要直接跳转到NtOpenProcess函数2个字节偏移处,也就是略过最开始的mov edi, edi这条指令,直接从push ebp开始执行,这样就不用频繁的对原来的函数进行修改。

这里还有一点需要注意,由于页保护的存在,所以不能直接通过函数地址对函数中的字节进行修改。需要通过内存描述符(MDL)描述一块包含该内存区域的起始地址,拥有者进程,字节数量,标记等信息的内存区域,并通过将它修改具有可写的属性来实现对函数的读写。

具体实现代码如下:

VOID NtMyOpenProcess(PHANDLE ProcessHandle,
             ACCESS_MASK DesiredAccess, 
             POBJECT_ATTRIBUTES ObjectAttributes, 
             PCLIENT_ID ClientId);    //Hook以后要执行的函数

PVOID g_pNtOpenProcessMap_5 = NULL;      //NtOpenProcess函数地址-5的地址映射得指针
PMDL g_pMDL = NULL;  //内存描述符指针
UCHAR g_szOrgBytes[7]; //存储HOOK之前函数中的七个字节

BOOLEAN Hook()
{
    BOOLEAN bRes = TRUE;
    UCHAR szShellCode[7] = { 0xE9, 0x0, 0x0, 0x0, 0x0, 0xEB, 0xF9 }; //构造热补丁
    PVOID pNtOpenProcessAddr_5 = NULL;       //NtOpenProcess-5的地址
    UNICODE_STRING uStrFuncName = RTL_CONSTANT_STRING(L"NtOpenProcess");
    ULONG i = 0;


    pNtOpenProcessAddr_5 = (PVOID)((UINT32)MmGetSystemRoutineAddress(&uStrFuncName) - 5); //获得NtOpenProcess的函数地址-5的数
    g_uOrgFuncAddr = (PVOID)((UINT32)pNtOpenProcessAddr_5 + 7); //取得要执行得原函数得代码地址
    //创建MDL,并为内存属性添加可写属性
    g_pMDL = MmCreateMdl(NULL, pNtOpenProcessAddr_5, sizeof(szShellCode));
     
    if (g_pMDL == NULL)
    {
        bRes = FALSE;
        goto exit;
    }
     
    MmBuildMdlForNonPagedPool(g_pMDL);  //建立内存页的MDL描述
    g_pMDL->MdlFlags |= MDL_MAPPED_TO_SYSTEM_VA;   //改变MDL的标记为可写
    g_pNtOpenProcessMap_5 = MmMapLockedPages(g_pMDL, KernelMode);    //映射MDL空间
     
    *(PUINT32)(szShellCode + 1) = (UINT32)NtMyOpenProcess - ((UINT32)pNtOpenProcessAddr_5 + 5); //填充szShellCode使其跳转到目标地址
    __asm cli;
    for (i = 0; i < 7; i++)
    {
        g_szOrgBytes[i] = *((PUCHAR)g_pNtOpenProcessMap_5 + i); //将原来的字节保存起来
        *((PUCHAR)g_pNtOpenProcessMap_5 + i) = szShellCode[i];  //将ShellCode写入到地址中
    }
    __asm sti;
exit:

    return bRes;
}

BOOLEAN UnHook()
{
    BOOLEAN bRes = TRUE;
    ULONG  i = 0;

    if (g_pNtOpenProcessMap_5)
    {
        __asm cli;
        for (i = 0; i < 7; i++) 
        {
            *((PUCHAR)g_pNtOpenProcessMap_5 + i) = g_szOrgBytes[i]; //恢复原来的字节
            g_szOrgBytes[i] = 0;
        }
        __asm sti;
    }
     
    //释放MDL,将函数的内存空间改为初始状态
    if (g_pMDL)
    {
        MmUnmapLockedPages(g_pNtOpenProcessMap_5, g_pMDL);
        IoFreeMdl(g_pMDL);
        g_pMDL = NULL;
    }

 



    return bRes;
}

VOID __declspec(naked) NtMyOpenProcess(PHANDLE ProcessHandle,
                                       ACCESS_MASK DesiredAccess, 
                                       POBJECT_ATTRIBUTES ObjectAttributes, 
                                       PCLIENT_ID ClientId)
{
    __asm
    {
        push ebp
        mov ebp, esp  //把ebp挪上来,为了后面方便使用参数
        pushfd          //保存寄存器环境
        pushad
    }

    if (ClientId)
    {
        if ((UINT32)ClientId->UniqueProcess == g_uPID)    //操作得是否是要保护得进程
        {
            if (DesiredAccess & PROCESS_TERMINATE)   //是否是要关闭进程 如果是删掉它
            {
                DesiredAccess &= ~PROCESS_TERMINATE;
            }
        }
    }
     
    __asm
    {
        popad       //恢复寄存器环境
        popfd
        mov esp, ebp
        pop ebp
        jmp g_uOrgFuncAddr
    }
}

5.SysenterHook
程序员使用OpenProcess打开进程的时候,这个函数其实是Kernel32.dll的导出函数,而在Kernel32.dll中这个函数的作用就是检测参数,随后将参数入栈以后在调用ntdll.dll中的ZwOpenProcess,该函数的具体实现如下:

.text:77F05D88 ; NTSTATUS __stdcall ZwOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId)
.text:77F05D88 public ZwOpenProcess
.text:77F05D88 ZwOpenProcess   proc near               ; CODE XREF: RtlQueryProcessDebugInformation+8F↑p
.text:77F05D88                                         ; sub_77EDD774+64C3A↓p ...
.text:77F05D88
.text:77F05D88 ProcessHandle   = dword ptr  4
.text:77F05D88 DesiredAccess   = dword ptr  8
.text:77F05D88 ObjectAttributes= dword ptr  0Ch
.text:77F05D88 ClientId        = dword ptr  10h
.text:77F05D88
.text:77F05D88     mov     eax, 0BEh       ; NtOpenProcess
.text:77F05D8D     mov     edx, 7FFE0300h
.text:77F05D92     call    dword ptr [edx]
.text:77F05D94     retn    10h
.text:77F05D94 ZwOpenProcess   endp

可以看到这个函数首先将0xBE赋给eax,此时这个0xBE就叫做调用号,它用来指定在内核中你要调用的那个内核函数是哪个。随后对0x7FFE0300这个地址中保存的地址进行了调用。

而0x7FFE03000这个地址中究竟保存的是什么就要取决于你的CPU是否支持快速调用,如果支持这个地址中保存的就是ntdll.dll中的KiFastCallSystemCall,如果不支持那么保存的就是ntdll.dll中的KiIntSystemCall。

而检测系统是否支持快速调用的方法是,为eax赋值为1,随后调用cpuid指令,那么处理器的信息就会被放到ecx和edx寄存器中,此时判断edx的第11位是否为1,如果为1那么当前CPU就支持快速调用,否则不支持。检测代码如下:

BOOL CanSysenter()
{
    BOOL bRet = FALSE;

    __asm
    {
        mov eax, 1
        cpuid
        and edx, 0x0800   //与第11位为1的二进制进行与
        mov bRet, edx
    }
     
    return bRet;
}

而要实现SysenterHook的电脑,它的CPU是要支持快速调用的,所以此时继续跟进ntdll.dll中的KiFastCallSystemCall,函数实现如下:

.text:77F070B0 public KiFastSystemCall
.text:77F070B0 KiFastSystemCall proc near ; DATA XREF: .text:off_77EF61B8↑o
.text:77F070B0    mov     edx, esp
.text:77F070B2    sysenter
.text:77F070B2 KiFastSystemCall endp

可以看到,函数首先将esp赋值给edx,也就是说此时edx指向的是包含返回地址和参数的栈地址。但要注意,由于调用了ZwOpenProcess以后又调用了KiFastSystemCall。所以此时edx所指的地址保存了两个返回地址,具体如下图所示。

随后函数会调用sysenter指令,此时这个指令就会从特殊模块寄存器(MSRs)寄存器中取出在内核中要运行的代码地址以及要设置的代码段等信息取出,并将它们赋给相应的寄存器。MSRs中包含的不同功能的寄存器有上百种,但是与sysenter配合的MSRs一共有下表的三个:

名称 偏移 功能
SYSENTER_CS_MSR 0x174 指定切换到内核层的CS
SYSENTER_ESP_MSR 0x175 指定切换到内核层的ESP
SYSENTER_EIP_MSR 0x176 指定切换到内核层的EIP

那也就是说,进入内核层要执行的代码是有SYSENTER_EIP_MSR来指定的,而程序员可以用rdmsr/wrmsr来分别实现对SYSENTER_EIP_MSR的读取和修改。当使用rdmsr指令的时候,程序会将ecx所指的MSRs的指定寄存器中的值写入edx:eax,而调用wrmsr的时候,程序会将edx:eax的值写入到ecx所指的MSRs寄存器中。

那也就是说可以通过将ecx赋值为0x176,再将eax赋值为要调用的函数,就可以实现SysenterHook。当程序进入内核的时候,首先执行的就是我们所指定的函数。具体的实现代码如下:

VOID MyKiFastCallEntry();  //Hook以后要执行的函数
ULONG g_uSSDT_Index = 0; //保存调用的SSDT函数的下标
PACCESS_MASK g_pAccessMask = 0;  //保存调用ZwOpenProcess时候传入的访问权限
PCLIENT_ID g_ClientId = 0;       //保存调用ZwOpenProcess时候传入的

//edx保存的参数和返回地址的栈地址,但是注意这时候有两个返回地址,详细如上图所示
__declspec(naked) VOID MyKiFastCallEntry()
{
    __asm
    {
        mov g_uSSDT_Index, eax;   //获取调用号
        push EDX
        add DWORD PTR [ESP], 0x0C
        pop g_pAccessMask          //取得保存AccessMask的栈地址
        push DWORD PTR [EDX + 0x14]   
        pop  g_ClientId;          //取出参数ClientId
    }

    __asm
    {
        pushfd //保存寄存器环境
        pushad
    }
     
    if (g_uSSDT_Index == 0xBE)   //根据调用号判断调用的是否是ZwOpenProcess
    {
        if (g_ClientId)
        {
            if ((ULONG)g_ClientId->UniqueProcess == g_uPID ) //是否是要保护的进程
            {
                if (*g_pAccessMask & PROCESS_TERMINATE)
                {
                    *g_pAccessMask = (*g_pAccessMask) & (~PROCESS_TERMINATE);   //具有关闭进程的权限,删除它
                }
            }
        }
    }
     
    __asm
    {
        popad   //恢复寄存器环境
        popfd
        jmp g_uOrgFuncAddr //跳回原来的代码执行
    }
}

BOOLEAN Hook()
{
    BOOLEAN bRes = TRUE;

    __asm
    {
        cli
        mov ecx, 0x176    //指向SYSENTER_EIP_MSR
        rdmsr
        mov g_uOrgFuncAddr, eax       //将原来要执行得代码地址保存起来
        mov eax, MyKiFastCallEntry    //Hook为要新执行的代码
        wrmsr
        sti
    }
     
    return bRes;
}

BOOLEAN UnHook()
{
    BOOLEAN bRes = TRUE;
    __asm
    {
        cli
        mov ecx, 0x176    //指向SYSENTER_EIP_MSR
        xor edx, edx
        mov eax, g_uOrgFuncAddr       //将原来执行的代码的地址恢复回去
        wrmsr
        sti
    }

    return bRes;
}

6.SSDT Hook
上面说过,进入内核的之前,程序会给eax赋值一个调用号,用来在进入内核以后找到要执行的函数。而找到的办法其实是通过找系统服务描述服表来找到的,在这张表中就保存了系统的所有函数,系统就是通过这张表来调用相应的函数。这张表的地址在32位的系统中是导出的,可以直接获取。而获取了该表所保存的内容如下:

typedef struct _KSERVICE_TABLE_DESCRIPTOR
{
    KSYSTEM_SERVICE_TABLE ntoskrnl;       
    KSYSTEM_SERVICE_TABLE win32k; 
    KSYSTEM_SERVICE_TABLE NotUsed1;    //未使用
    KSYSTEM_SERVICE_TABLE NotUsed2;
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;

打开进程的函数在ntoskrnl中,称为系统服务表,这是一个KSYSTEM_SERVICE_TABLE的变量,定义如下:

typedef struct _KSYSTEM_SERVICE_TABLE
{
    PULONG pServiceTableBase;          //保存函数地址的表
    ULONG pServiceCounterTable;            //通常为0
    ULONG  uNumberFunc;               //保存函数个数
    PUCHAR  pParamTableBase;          //保存了参数个数的表,以字节为单位(除4得到参数个数)
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;

其中的pServiceTableBase指向的地址就是一张保存了各个函数地址的表,通过替换表中相应位置的内容就可以实现HOOK。因为当用户层的函数调用进入到内核的时候,系统通过这张表找到的函数地址就是我们指定的地址。

另外这张表同样是因为页保护的存在而不可写,上面介绍了通过MDL的方式映射一块内存来进行写操作。其实还有另一个办法,那就是修改CR0寄存器中的WP位(第16位)。当它为0的时候,页保护就会被关闭,这样就可以实现对SSDT表的修改。

以下是具体的代码实现:

#pragma pack(1)
// 系统服务表
typedef struct _KSYSTEM_SERVICE_TABLE
{
    PULONG pServiceTableBase;          //保存函数地址的表
    ULONG pServiceCounterTable;            //通常为0
    ULONG  uNumberFunc;                   //保存函数个数
    PUCHAR  pParamTableBase;          //保存了参数个数的表,以字节为单位(除4得到参数个数)
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;

typedef struct _KSERVICE_TABLE_DESCRIPTOR
{
    KSYSTEM_SERVICE_TABLE ntoskrnl;       
    KSYSTEM_SERVICE_TABLE win32k; 
    KSYSTEM_SERVICE_TABLE unUsedTable1;    //系统未使用
    KSYSTEM_SERVICE_TABLE unUsedTable2;    //系统未使用
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
#pragma pack()

VOID OpenPageProtect();    //开起页保护
VOID ClosePageProtect();   //关闭页保护
typedef NTSTATUS (*pFnOpenProcess)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PCLIENT_ID);
NTSTATUS MyZwOpenProcess(PHANDLE  ProcessHandle,
                         ACCESS_MASK  DesiredAccess, 
                         POBJECT_ATTRIBUTES  ObjectAttributes, 
                         PCLIENT_ID  ClientId);    //HOOK以后要执行的函数
extern PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;       //获取导出的地址

BOOLEAN Hook()
{
    BOOLEAN bRes = TRUE;

    ClosePageProtect(); //关闭页保护
    g_uOrgFuncAddr = KeServiceDescriptorTable->ntoskrnl.pServiceTableBase[0xBE];   //原函数进行备份
    KeServiceDescriptorTable->ntoskrnl.pServiceTableBase[0xBE] = (ULONG)MyZwOpenProcess;    //修改为要执行的函数地址
    OpenPageProtect();  //开启页保护
     
    return bRes;
}

BOOLEAN UnHook()
{
    BOOLEAN bRes = TRUE;

    ClosePageProtect(); //关闭页保护
    KeServiceDescriptorTable->ntoskrnl.pServiceTableBase[0xBE] = g_uOrgFuncAddr;    //还原成原来的函数
    OpenPageProtect();  //开启页保护
     
    return bRes;
}

NTSTATUS MyZwOpenProcess(PHANDLE  ProcessHandle, ACCESS_MASK  DesiredAccess, POBJECT_ATTRIBUTES  ObjectAttributes, PCLIENT_ID  ClientId)
{
    if ((ULONG)ClientId->UniqueProcess == g_uPID) //是否是要保护的进程
    {
        if (DesiredAccess & PROCESS_TERMINATE)   //是否具有关闭进程的权限
        {
            DesiredAccess &= ~PROCESS_TERMINATE;
        }
    }

       return ((pFnOpenProcess)g_uOrgFuncAddr)(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);    //调用原函数
}

// 关闭页保护
VOID ClosePageProtect()
{
    __asm
    {
        cli
        mov eax, cr0     //将cr0中的数值赋给eax
        and eax, ~0x10000  // 将第16位,也就是WP位置0
        mov cr0, eax      
    }
}

// 开起页保护
VOID OpenPageProtect()
{
    __asm
    {
        mov eax, cr0    // 将cr0中的数值赋给eax
        or eax, 0x10000   // WP位置1
        mov cr0, eax
        sti
    }
}

7.实验结果

而当卸载了钩子以后就可以顺利关闭进程。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM