以下内容参考黑客防线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);
}