1. 程式人生 > >win 64 ring0 inline hook

win 64 ring0 inline hook

防止 還要 全局 技術 png ret 寄存器 保存 ces

以下內容參考黑客防線2012合訂本316頁

1.首先要註意的問題

inline hook 不能截斷指令. 也就是說修改目標函數的指令實現跳轉到自己的函數裏面時, 不能截斷掉目標函數的指令.

因為在自己的函數裏面還要調用原來的函數,但是原來的函數如果被截斷那就沒辦法正常執行代碼

2.反匯編引擎.

用來動態解析內存中指令, 這裏就是用來獲取所需字節數的最少修改指令數所占的大小. 也就是防止出現指令截斷.

使用LDE64 (網上就有).

uchar szShellCode[12800]={...};
typedef int (*LDE_DISASM)(void *p, int dw);
LDE_DISASM LDE;

void LDE_init() { LDE=ExAllocatePool(NonPagedPool,12800); memcpy(LDE,szShellCode,12800); }

使用:

ULONG GetPatchSize(PUCHAR Address)
{
    ULONG LenCount = 0, Len = 0;
    while (LenCount <= 14)    //至少需要14字節
    {
        Len = LDE(Address, 64);
        Address = Address + Len;
        LenCount = LenCount + Len;
    }
    
return LenCount; }

關於inline hook思路:

首先聲明全局變量來存儲 A 代碼. A代碼就是原始的前n字節.

聲明全局變量存儲B代碼. 是A代碼+jmp C . 這個C是原始函數+sizeof(A)

A代碼用來unhook的 , B代碼用來從自己的函數跳回去原始函數繼續執行.

所以先獲取跳轉到自己函數的機器碼. D

這個機器碼是通過以下實現的:

  

    這裏跨4G跳轉使用的方式是jmp qword ptr [rip], 對應的機器碼是ff2500000000 6個字節
    當執行到這條指令時,假如這條指令地址是這樣
    0000000000000000   jmp qword ptr [rip]
    那麽又假如下一條指令這樣:
    
0000000000000006 cccccccccccccccc (這裏的匯編碼為int3 int3 int3...int3 共8個) 那麽指令執行完jmp指令後,將跑到地址為cccccccccccccccc處的代碼執行,而不是執行int3 指令. 簡單來說就是把rip當成普通寄存器使用了. 因此至少需要14字節的數據來跳轉任意內存處. 為什麽是至少14字節呢? 因為指令不能截斷,當hook時不能直接只改掉前14字節,這樣可能會因為 某條指令橫跨第14字節,這樣hook就會將這條指令截斷. 因此需要通過反匯編引擎對字節碼進行 解析,直到解析的指令內容>=14字節.將這些指令內容大小作為修改的大小,多余的填充nop.

然後將前n字節保存到A.

設置好B

再將D寫入到原始函數前n字節. 這樣就實現inline hook.

在自己的函數裏面可以通過調用B來執行正確的原始函數.

unhook就很簡單, 將A寫回原始函數前n字節即可. 測試結果:

技術分享

最後附上大佬的代碼:(有自己寫的一些註釋)

#include <ntddk.h>

#define kmalloc(_s) ExAllocatePoolWithTag(NonPagedPool, _s, ‘SYSQ‘)
#define kfree(_p) ExFreePool(_p)

typedef NTSTATUS(__fastcall *PSLOOKUPPROCESSBYPROCESSID)(HANDLE ProcessId, PEPROCESS *Process);
ULONG64 my_eprocess = 0;            //待保護進程的eprocess
ULONG pslp_patch_size = 0;        //PsLookupProcessByProcessId被修改了N字節
PUCHAR pslp_head_n_byte = NULL;    //PsLookupProcessByProcessId的前N字節數組
PVOID ori_pslp = NULL;            //PsLookupProcessByProcessId的原函數


KIRQL WPOFFx64()
{
    KIRQL irql = KeRaiseIrqlToDpcLevel();
    UINT64 cr0 = __readcr0();
    cr0 &= 0xfffffffffffeffff;
    __writecr0(cr0);
    _disable();
    return irql;
}

void WPONx64(KIRQL irql)
{
    UINT64 cr0 = __readcr0();
    cr0 |= 0x10000;
    _enable();
    __writecr0(cr0);
    KeLowerIrql(irql);
}

//傳入:被HOOK函數地址,原始數據,補丁長度
VOID UnhookKernelApi(IN PVOID ApiAddress, IN PVOID OriCode, IN ULONG PatchSize)
{
    KIRQL irql;
    irql = WPOFFx64();
    memcpy(ApiAddress, OriCode, PatchSize);
    WPONx64(irql);
}

NTSTATUS Proxy_PsLookupProcessByProcessId(HANDLE ProcessId, PEPROCESS *Process)
{
    NTSTATUS st;
    st = ((PSLOOKUPPROCESSBYPROCESSID)ori_pslp)(ProcessId, Process);
    if (NT_SUCCESS(st))
    {
        if (*Process == (PEPROCESS)my_eprocess)
        {
            *Process = 0;
            st = STATUS_ACCESS_DENIED;
        }
    }
    return st;
}

void *GetFunctionAddr(PCWSTR FunctionName)
{
    UNICODE_STRING UniCodeFunctionName;
    RtlInitUnicodeString(&UniCodeFunctionName, FunctionName);
    return MmGetSystemRoutineAddress(&UniCodeFunctionName);
}
/*
    這裏跨4G跳轉使用的方式是jmp qword ptr [rip], 對應的機器碼是ff2500000000 6個字節
    當執行到這條指令時,假如這條指令地址是這樣
    0000000000000000   jmp qword ptr [rip]
    那麽又假如下一條指令這樣:
    0000000000000006   cccccccccccccccc  (這裏的匯編碼為int3 int3 int3...int3 共8個)
    那麽指令執行完jmp指令後,將跑到地址為cccccccccccccccc處的代碼執行,而不是執行int3 指令.
    簡單來說就是把rip當成普通寄存器使用了. 因此至少需要14字節的數據來跳轉任意內存處.
    為什麽是至少14字節呢? 因為指令不能截斷,當hook時不能直接只改掉前14字節,這樣可能會導致
    某條指令橫跨第14字節,如果這樣hook就會將這條指令截斷. 因此需要通過反匯編引擎對字節碼進行
    解析,直到解析的指令內容>=14字節.將這些指令內容大小作為修改的大小,多余的填充nop.

*/

ULONG GetPatchSize(PUCHAR Address)
{
    ULONG LenCount = 0, Len = 0;
    while (LenCount <= 14)    //至少需要14字節
    {
        Len = LDE(Address, 64);
        Address = Address + Len;
        LenCount = LenCount + Len;
    }
    return LenCount;
}


//傳入:待HOOK函數地址,代理函數地址,接收跳回原始函數代碼內容的地址的指針,接收補丁長度的指針;返回:原來頭N字節的數據
PVOID HookKernelApi(IN PVOID ApiAddress, IN PVOID Proxy_ApiAddress, OUT PVOID *Original_ApiAddress, OUT ULONG *PatchSize)
{
    //這裏面有2個是動態分配的,需要在程序卸載時釋放掉,是head_n_byte,ori_func
    //它們被賦值到返回值和參數Original_ApiAddress了
    KIRQL irql;
    UINT64 tmpv;
    //一個是保存被hook函數前n字節碼,一個是保存代理函數調用原始函數用的代碼,不能直接調用原始函數,因為已經被改了.
    PVOID head_n_byte, ori_func;

    UCHAR jmp_code[] = "\xFF\x25\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF";
    UCHAR jmp_code_orifunc[] = "\xFF\x25\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF";
    //How many bytes shoule be patch
    *PatchSize = GetPatchSize((PUCHAR)ApiAddress);
    //step 1: Read current data
    head_n_byte = kmalloc(*PatchSize);
    irql = WPOFFx64();
    memcpy(head_n_byte, ApiAddress, *PatchSize);
    WPONx64(irql);
    //step 2: Create ori function
    ori_func = kmalloc(*PatchSize + 14);    //原始機器碼+跳轉機器碼
    RtlFillMemory(ori_func, *PatchSize + 14, 0x90);
    tmpv = (ULONG64)ApiAddress + *PatchSize;    //跳轉到沒被打補丁的那個字節
    DbgPrint("ApiAddress is %p\n", ApiAddress);
    DbgBreakPoint();
    memcpy(jmp_code_orifunc + 6, &tmpv, 8);

    memcpy((PUCHAR)ori_func, head_n_byte, *PatchSize);
    memcpy((PUCHAR)ori_func + *PatchSize, jmp_code_orifunc, 14);
    *Original_ApiAddress = ori_func;
    //step 3: fill jmp code
    tmpv = (UINT64)Proxy_ApiAddress;
    memcpy(jmp_code + 6, &tmpv, 8);
    //step 4: Fill NOP and hook
    irql = WPOFFx64();
    RtlFillMemory(ApiAddress, *PatchSize, 0x90);
    memcpy(ApiAddress, jmp_code, 14);
    WPONx64(irql);
    //return ori code
    
    return head_n_byte;
}








VOID HookPsLookupProcessByProcessId()
{
    //pslp_head_n_byte和ori_pslp 最後需要釋放掉
    pslp_head_n_byte = HookKernelApi(GetFunctionAddr(L"PsLookupProcessByProcessId"),
        (PVOID)Proxy_PsLookupProcessByProcessId,
        &ori_pslp,
        &pslp_patch_size);
}

VOID UnhookPsLookupProcessByProcessId()
{
    UnhookKernelApi(GetFunctionAddr(L"PsLookupProcessByProcessId"),
        pslp_head_n_byte,
        pslp_patch_size);
}

win 64 ring0 inline hook