x64驱动基础教程 18

栏目: 数据库 · SQL Server · 发布时间: 5年前

内容简介:内核INLINE HOOK是各种RK、AV和ARK最常用的手段之一了,如果让我在WIN64上使用HOOK的话,我会首推INLINE HOOK,毕竟SSDT HOOK和SHADOW SSDT 非常麻烦,不好修改。目前来说支持WIN64的INLINE HOOK引擎有很多,比如 MHOOK和MINI HOOK ENGINE,不过要移植到内核里还是很麻烦的,所以我特地做了一个适用于WIN64的RING0 INLINE HOOK ENGINE。当然,有矛必有盾,有了HOOK ENGINE,必然还有一个用于UNHOOK

内核INLINE HOOK是各种RK、AV和ARK最常用的手段之一了,如果让我在

WIN64上使用HOOK的话,我会首推INLINE HOOK,毕竟SSDT HOOK和SHADOW SSDT 非常麻烦,不好修改。目前来说支持WIN64的INLINE HOOK引擎有很多,比如 MHOOK和MINI HOOK ENGINE,不过要移植到内核里还是很麻烦的,所以我特地做了一个适用于WIN64的RING0 INLINE HOOK ENGINE。当然,有矛必有盾,有了

HOOK ENGINE,必然还有一个用于UNHOOK的玩意儿。

一、实现RING0 INLINE HOOK

无论是用户态Inline Hook还是内核级Inline Hook,都要遵循一个原则,就是指令不能截断,否则会出大错误。所以,反汇编引擎在Inline Hook引擎中的作用,就是判断指令的长度。首先介绍我找到的x64反汇编引擎,LDE64。LDE64 是Length Disassemble Engine for x64的缩写,此反汇编引擎小巧玲珑,最大的优点就是能带进驱动里使用(很多外国开源的反汇编引擎都无法带进驱动,还需要很多莫名其妙的非标准C++库)。不过,LDE64的作者也够小气的,没有直接给出源代码,但是给了一个十几 KB 的 shellcode,当你需要反汇编时,直接调用shellcode就行了。核心代码如下:

unsigned char szShellCode[12800] = {...} //详细机器码请见源码文件
typedef int (*LDE_DISASM)(void *p, int dw);
LDE_DISASM LDE;
void LDE_init()
{
    LDE=ExAllocatePool(NonPagedPool,12800);
    memcpy(LDE,szShellCode,12800);
}

需要使用时先调用 LDE_init 进行初始化,然后再调用 LDE 函数即可。 LDE 函数要求输入地址和平台类型,返回一条指令的字节长度。接下来编写一个自定义函数,返回要 Patch 的字节数目。虽然在 Win64 上写一个跨 4G 跳转指令理论上只需要 14 字节,但是 14 字节并不一定就是 N 个完整的指令,所以必须得到 N 个完整指令的长度(N 条指令的长度要大于等于 14):

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 的思路:

1.获得待 HOOK 函数的地址(Address)

2.获得要修改的字节数目(N)

3.保存这头 N 字节的机器码

4.创建『原函数』(把复制头 N 字节,再跳转到 Address+N 的地方)

5.修改函数头,跳转到代理函数里

解释一下跳转的代码。我之前使 用的跳转流程是:

MOV RAX,绝对地址

JMP RAX

后来感觉修改 RAX 不太好(虽然 RAX 是易失性寄存器),于是换了方式:

JMP QWORD PTR [本条指令结束后的地址]

以上指令的机器码是: FF 25 00 00 00 00。代码如下:

//传入:待 HOOK 函数地址,代理函数地址,接收原始函数地址的指针,接收补丁长度的指针;返回:原来头 N 字节的数据
PVOID HookKernelApi(IN PVOID ApiAddress, IN PVOID Proxy_ApiAddress, OUT PVOID
    *Original_ApiAddress, OUT ULONG *PatchSize)
{
    KIRQL irql;
    UINT64 tmpv;
    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; //跳转到没被打补丁的那个字节
    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;
}

反挂钩就简单了,直接把头 N 字节回复即可:

//传入:被 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 HookPsLookupProcessByProcessId()
{
    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);
}

效果如下:

x64驱动基础教程 18

用 WINDBG 检测一下:

挂钩前:
lkd> u pslookupprocessbyprocessid
nt!PsLookupProcessByProcessId:
fffff800`0194c750 48895c2408 mov qword ptr [rsp+8],rbx
fffff800`0194c755 48896c2410 mov qword ptr [rsp+10h],rbp
fffff800`0194c75a 4889742418 mov qword ptr [rsp+18h],rsi
fffff800`0194c75f 57 push rdi
fffff800`0194c760 4154 push r12
fffff800`0194c762 4155 push r13
fffff800`0194c764 4883ec20 sub rsp,20h
fffff800`0194c768 65488b3c2588010000 mov rdi,qword ptr gs:[188h]
挂钩后:
lkd> u pslookupprocessbyprocessid
nt!PsLookupProcessByProcessId:
fffff800`0194c750 ff2500000000 jmp qword ptr [nt!PsLookupProcessByProcessId+0x6
(fffff800`0194c756)]
fffff800`0194c756 dc50e9 fcom qword ptr [rax-17h]
fffff800`0194c759 06 ???
fffff800`0194c75a 80f8ff cmp al,0FFh
fffff800`0194c75d ff9057415441 call qword ptr [rax+41544157h]
fffff800`0194c763 55 push rbp
fffff800`0194c764 4883ec20 sub rsp,20h
fffff800`0194c768 65488b3c2588010000 mov rdi,qword ptr gs:[188h]

看样子代码好像乱了,其实不是的。因为跨 4G 跳转指令是 14 字节,而我们修改了 PsLookupProcessByProcessId 的头 15 字节(正好三条指令),前 6 字节是指令,后 9 字节并不是指令,而是数据(前 8 字节是绝对地址)和填充码(最后 1 字节没有意义)。所以这么看就对了:

lkd> u PsLookupProcessByProcessId+0xf
nt!PsLookupProcessByProcessId+0xf:
fffff800`0194c75f 57 push rdi
fffff800`0194c760 4154 push r12
fffff800`0194c762 4155 push r13
fffff800`0194c764 4883ec20 sub rsp,20h
fffff800`0194c768 65488b3c2588010000 mov rdi,qword ptr gs:[188h]
fffff800`0194c771 4533e4 xor r12d,r12d
fffff800`0194c774 488bea mov rbp,rdx
fffff800`0194c777 66ff8fc4010000 dec word ptr [rdi+1C4h]

用 WIN64AST 检测: x64驱动基础教程 18

二、恢复RING0 INLINE HOOK

首先说说清除INLINE HOOK的一般步骤(无论是X86还是X64平台,无论是用户态还是内核级的INLINE HOOK,都适用):

1.找到被挂钩函数的地址

2.获得原始的机器码

3.把原始机器码写入到指定的地址

以下表格说明了实现上述3大步骤的N个小步骤:

1.找到被挂钩函数的地址

1.1获得被HOOK函数所属模块在内存里的基址KernelBase

1.2在进程里用LoadLibraryEx加载一份被HOOK函数的所属模块,获得映射地址hKernel

1.3获得函数的实际地址:Address=KernelBase-hKernel+ GetProcAddress(hKernel,函数名称)

2.获得原始的机器码

2.1根据KernelBase和hKernel进行重定位

2.2根据计算公式OffsetAddress=Address-KernelBase+hKernel获得偏移地址

2.3使用memcpy复制指定长度的数据,这个数据就是原始机器码了

3.把原始机器码写入到指定的地址

3.1把原始机器码传到驱动里

3.2把IRQL提升到和DPC同样的级别

3.3通过修改CR0寄存器的值来关闭内存写保护

3.4使用RtlCopyMemory写内存

3.5通过修改CR0寄存器的值来打开内存写保护

3.6把IRQL降低到PASSIIVE_LEVEL

1.获得内核文件的加载基址:

ULONG64 GetKernelBase64(char *NtosName)
{
    typedef long (__stdcall *ZWQUERYSYSTEMINFORMATION)
        (
        IN ULONG SystemInformationClass,
        IN PVOID SystemInformation,
        IN ULONG SystemInformationLength,
        IN PULONG ReturnLength OPTIONAL
        );
    typedef struct _SYSTEM_MODULE_INFORMATION_ENTRY
    {
        ULONG Unknow1;
        ULONG Unknow2;
        ULONG Unknow3;
        ULONG Unknow4;
        PVOID Base;
        ULONG Size;
        ULONG Flags;
        USHORT Index;
        USHORT NameLength;
        USHORT LoadCount;
        USHORT ModuleNameOffset;
            char ImageName[256];
    } SYSTEM_MODULE_INFORMATION_ENTRY, *PSYSTEM_MODULE_INFORMATION_ENTRY;
    typedef struct _SYSTEM_MODULE_INFORMATION
    {
        ULONG Count;
        SYSTEM_MODULE_INFORMATION_ENTRY Module[1];
    } SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
#define SystemModuleInformation 11
#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)
    ZWQUERYSYSTEMINFORMATION ZwQuerySystemInformation;
    PSYSTEM_MODULE_INFORMATION pSystemModuleInformation;
    ULONG NeedSize, BufferSize = 0x5000;
    PVOID pBuffer = NULL;
    NTSTATUS Result;
    ZwQuerySystemInformation=(ZWQUERYSYSTEMINFORMATION)GetProcAddress(GetModuleHandleA("n
        tdll.dll"),"ZwQuerySystemInformation");
        do
        {
            pBuffer = malloc( BufferSize );
            if( pBuffer == NULL ) return 0;
            Result = ZwQuerySystemInformation( SystemModuleInformation, pBuffer, BufferSize,
                &NeedSize );
            if( Result == STATUS_INFO_LENGTH_MISMATCH )
            {
                free( pBuffer );
                BufferSize *= 2;
            }
            else if( !NT_SUCCESS(Result) )
            {
                free( pBuffer );
                return 0;
            }
        }
        while( Result == STATUS_INFO_LENGTH_MISMATCH );
        pSystemModuleInformation = (PSYSTEM_MODULE_INFORMATION)pBuffer;
        ULONG64 ret=(ULONG64)(pSystemModuleInformation->Module[0].Base);
        if(NtosName!=NULL)
            strcpy(NtosName,pSystemModuleInformation->Module[0].ImageName+pSystemModuleInformatio
            n->Module[0].ModuleNameOffset);
        free(pBuffer);
        return ret;
}

2.内核文件重定位:

int Reloc(ULONG64 HandleInFile, ULONG64 BaseInKernel)
{
    PIMAGE_DOS_HEADER pDosHeader;
    PIMAGE_NT_HEADERS64 pNtHeader;
    PIMAGE_BASE_RELOCATION pRelocTable;
    ULONG i,dwOldProtect;
    pDosHeader = (PIMAGE_DOS_HEADER)HandleInFile;
    if ( pDosHeader->e_magic != IMAGE_DOS_SIGNATURE )
        return 0;
    pNtHeader = (PIMAGE_NT_HEADERS64)( (ULONG64)HandleInFile + pDosHeader->e_lfanew );
    //是否存在重定位表
    if (pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size)
    {
        pRelocTable=(PIMAGE_BASE_RELOCATION)((ULONG64)HandleInFile +
            pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
        do{
            ULONG numofReloc=(pRelocTable->SizeOfBlocksizeof(IMAGE_BASE_RELOCATION))/2;
            SHORT minioffset=0;
            PUSHORT
                pRelocData=(PUSHORT)((ULONG64)pRelocTable+sizeof(IMAGE_BASE_RELOCATION));
            //循环,或直接判断*pData 是否为 0 也可以作为结束标记
            for (i=0;i<numofReloc;i++)
            {
                //需要重定位的地址
                PULONG64 RelocAddress;
                //重定位的高 4 位是重定位类型
                if (((*pRelocData)>>12)== IMAGE_REL_BASED_DIR64)//判断重定位类型
                {
                    //计算需要进行重定位的地址
                    //重定位数据的低 12 位再加上本重定位块头的 RVA 即真正需要重定位的数据的 RVA
                    minioffset=(*pRelocData)&0xFFF;//小偏移
                    //模块基址+重定位基址+每个数据表示的小偏移量
                    RelocAddress=(PULONG64)(HandleInFile+pRelocTable->VirtualAddress+minioffset);
                    //直接在 RING3 修改:原始数据+基址-IMAGE_OPTINAL_HEADER 中的基址
                    VirtualProtect((PVOID)RelocAddress, 4, PAGE_EXECUTE_READWRITE,
                        &dwOldProtect);
                    //因为是 R3 直接 LOAD 的 所以要修改一下内存权限
                    *RelocAddress=*RelocAddress+BaseInKernelpNtHeader->OptionalHeader.ImageBase;
                    VirtualProtect((PVOID)RelocAddress, 4, dwOldProtect, NULL);
                }
                    //下一个重定位数据
                    pRelocData++;
            }
            //下一个重定位块
            pRelocTable=(PIMAGE_BASE_RELOCATION)((ULONG64)pRelocTable+pRelocTable->SizeOfBlock);
        }while (pRelocTable->VirtualAddress);
        return TRUE;
    }
    return FALSE;
}

3.把内核文件加载到进程、获得函数地址和原始机器码

int InitGetOriCode(int bClear)
{
    if( bClear==0 )
    {
        KernelBase=GetKernelBase64(NtosFullName);
        //printf("KB: %llx\nKN: %s\n",KernelBase,NtosFullName);
        if(!KernelBase) return 0;
        hKernel=LoadLibraryExA(NtosFullName,0,DONT_RESOLVE_DLL_REFERENCES);
        //printf("KH: %llx\n",hKernel);
        if(!hKernel) return 0;
        if(!Reloc((ULONG64)hKernel,KernelBase)) return 0;
        return 1;
    }
    else
    {
        FreeLibrary(hKernel);
        return 1;
    }
}
void GetOriCode(ULONG64 Address, PUCHAR ba, SIZE_T Length)
{
    ULONG64 OffsetAddress=Address-KernelBase+(ULONG64)hKernel;
    memcpy(ba,(PVOID)OffsetAddress, Length);
}
ULONG64 GetSystemRoutineAddress(char *FuncName)
{
    return KernelBase+(ULONG64)GetProcAddress(hKernel,FuncName)-(ULONG64)hKernel;
}

4.获得当前在内存里的机器码和清除 INLINE HOOK(这两步本质上就是调用RtlCopyMemory 而已,只是第一个参数和第二个参数互换一下位置):

//RING3 源码:
void GetCurCode(ULONG64 Address, PUCHAR ba, SIZE_T Length)
{
    ULONG64 dat[2]={0};
    dat[0]=Address;
    dat[1]=Length;
    IoControl(hDriver ,CTL_CODE_GEN(0x800), dat, 16, ba, Length);
}
void ClearInlineHook(ULONG64 Address, PUCHAR ba, SIZE_T Length)
{
    typedef struct _KF_DATA
    {
        PVOID Address;
        ULONG64 Length;
        UCHAR data[256];
    } KF_DATA, *PKF_DATA;
    KF_DATA dat={0};
    dat.Address=(PVOID)Address;
    dat.Length=Length;
    memcpy(dat.data,ba,Length);
    IoControl(hDriver ,CTL_CODE_GEN(0x801), &dat, sizeof(KF_DATA), 0, 0);
}
//RING0 源码:
case IOCTL_GET_CUR_CODE:
{
    KF_DATA dat={0};
    memcpy(&dat,pIoBuffer,16); //只用前两个成员
    WPOFFx64();
    RtlCopyMemory(pIoBuffer,dat.Address,dat.Length);
    WPONx64();
    status = STATUS_SUCCESS;
    break;
}
case IOCTL_SET_ORI_CODE:
{
    KF_DATA dat={0};
    memcpy(&dat,pIoBuffer,sizeof(KF_DATA));
    WPOFFx64();
    RtlCopyMemory(dat.Address,dat.data,dat.Length);
    WPONx64();
    status = STATUS_SUCCESS;
    break;
}

其它界面处理的源码就不贴出来了,下面主要看一下实验效果(使用 HOOK PsLookupProcessByProcessId 为例子):

x64驱动基础教程 18


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Spark SQL内核剖析

Spark SQL内核剖析

朱锋、张韶全、黄明 / 电子工业出版社 / 2018-8 / 69.00元

Spark SQL 是 Spark 技术体系中较有影响力的应用(Killer application),也是 SQL-on-Hadoop 解决方案 中举足轻重的产品。《Spark SQL内核剖析》由 11 章构成,从源码层面深入介绍 Spark SQL 内部实现机制,以及在实际业务场 景中的开发实践,其中包括 SQL 编译实现、逻辑计划的生成与优化、物理计划的生成与优化、Aggregation 算......一起来看看 《Spark SQL内核剖析》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具