內核層的三種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