5.9 Windows驱动开发:内核InlineHook挂钩技术

简介: 在上一章`《内核LDE64引擎计算汇编长度》`中,`LyShark`教大家如何通过`LDE64`引擎实现计算反汇编指令长度,本章将在此基础之上实现内联函数挂钩,内核中的`InlineHook`函数挂钩其实与应用层一致,都是使用`劫持执行流`并跳转到我们自己的函数上来做处理,唯一的不同的是内核`Hook`只针对`内核API`函数,但由于其身处在`最底层`所以一旦被挂钩其整个应用层都将会受到影响,这就直接决定了在内核层挂钩的效果是应用层无法比拟的,对于安全从业者来说学会使用内核挂钩也是很重要。

在上一章《内核LDE64引擎计算汇编长度》中,LyShark教大家如何通过LDE64引擎实现计算反汇编指令长度,本章将在此基础之上实现内联函数挂钩,内核中的InlineHook函数挂钩其实与应用层一致,都是使用劫持执行流并跳转到我们自己的函数上来做处理,唯一的不同的是内核Hook只针对内核API函数,但由于其身处在最底层所以一旦被挂钩其整个应用层都将会受到影响,这就直接决定了在内核层挂钩的效果是应用层无法比拟的,对于安全从业者来说学会使用内核挂钩也是很重要。

内核挂钩的原理是一种劫持系统函数调用的技术,用于在运行时对系统函数进行修改或者监控。其基本思想是先获取要被劫持的函数的地址,然后将该函数的前15个字节的指令保存下来,接着将自己的代理函数地址写入到原始函数上,这样当API被调用时,就会默认转向到自己的代理函数上执行,从而实现函数的劫持。

挂钩的具体步骤如下:

  • 1.使用MmGetSystemRoutineAddress函数获取要被劫持的函数地址。
  • 2.使用自己的代理函数取代原始函数,代理函数和原始函数具有相同的参数和返回值类型,并且在代理函数中调用原始函数。
  • 3.保存原始函数的前15个字节的指令,因为这些指令通常被认为是函数的前导码或是修饰码。
  • 4.在原始函数的前15个字节位置写入jmp MyPsLookupProcessByProcessId的指令,使得API调用会跳转到我们的代理函数。
  • 5.当代理函数被调用时,执行我们自己的逻辑,然后在适当的时候再调用原始函数,最后将其返回值返回给调用者。
  • 6.如果需要恢复原始函数的调用,将保存的前15个字节的指令写回原始函数即可。

挂钩的原理可以总结为,通过MmGetSystemRoutineAddress得到原函数地址,然后保存该函数的前15个字节的指令,将自己的MyPsLookupProcessByProcessId代理函数地址写出到原始函数上,此时如果有API被调用则默认会转向到我们自己的函数上面执行,恢复原理则是将提前保存好的前15个原始字节写回则恢复原函数的调用。

而如果需要恢复挂钩状态,则只需要还原提前保存的机器码即可,恢复内核挂钩的原理是将先前保存的原始函数前15个字节的指令写回到原始函数地址上,从而还原原始函数的调用。具体步骤如下:

  • 1.获取原函数地址,可以通过MmGetSystemRoutineAddress函数获取。
  • 2.将保存的原始函数前15个字节的指令写回到原始函数地址上,可以使用memcpy等函数实现。
  • 3.将代理函数的地址清除,可以将地址设置为NULL。

原理很简单,基本上InlineHook类的代码都是一个样子,如下是一段完整的挂钩PsLookupProcessByProcessId的驱动程序,当程序被加载时则默认会保护lyshark.exe进程,使其无法被用户使用任务管理器结束掉。

#include "lyshark_lde64.h"
#include <ntifs.h>
#include <windef.h>
#include <intrin.h>

#pragma  intrinsic(_disable)
#pragma  intrinsic(_enable)

// --------------------------------------------------------------
// 汇编计算方法
// --------------------------------------------------------------
// 计算地址处指令有多少字节
// address = 地址
// bits 32位驱动传入0 64传入64
typedef INT(*LDE_DISASM)(PVOID address, INT bits);

LDE_DISASM lde_disasm;

// 初始化引擎
VOID lde_init()
{
   
   
    lde_disasm = ExAllocatePool(NonPagedPool, 12800);
    memcpy(lde_disasm, szShellCode, 12800);
}

// 得到完整指令长度,避免截断
ULONG GetFullPatchSize(PUCHAR Address)
{
   
   
    ULONG LenCount = 0, Len = 0;

    // 至少需要14字节
    while (LenCount <= 14)
    {
   
   
        Len = lde_disasm(Address, 64);
        Address = Address + Len;
        LenCount = LenCount + Len;
    }
    return LenCount;
}

// --------------------------------------------------------------
// Hook函数封装
// --------------------------------------------------------------

// 定义指针方便调用
typedef NTSTATUS(__fastcall *PSLOOKUPPROCESSBYPROCESSID)(HANDLE ProcessId, PEPROCESS *Process);

ULONG64 protect_eprocess = 0;           // 需要保护进程的eprocess
ULONG patch_size = 0;                   // 被修改了几个字节
PUCHAR head_n_byte = NULL;              // 前几个字节数组
PVOID original_address = NULL;          // 原函数地址

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);
}

// 动态获取内存地址
PVOID GetProcessAddress(PCWSTR FunctionName)
{
   
   
    UNICODE_STRING UniCodeFunctionName;
    RtlInitUnicodeString(&UniCodeFunctionName, FunctionName);
    return MmGetSystemRoutineAddress(&UniCodeFunctionName);
}

/*
    InlineHookAPI 挂钩地址

    参数1:待HOOK函数地址
    参数2:代理函数地址
    参数3:接收原始函数地址的指针
    参数4:接收补丁长度的指针
    返回:原来头N字节的数据
*/
PVOID KernelHook(IN PVOID ApiAddress, IN PVOID Proxy_ApiAddress, OUT PVOID *Original_ApiAddress, OUT ULONG *PatchSize)
{
   
   
    KIRQL irql;
    UINT64 tmpv;
    PVOID head_n_byte, ori_func;

    // 保存跳转指令 JMP QWORD PTR [本条指令结束后的地址]
    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";

    // 获取函数地址处指令长度
    *PatchSize = GetFullPatchSize((PUCHAR)ApiAddress);

    // 分配空间
    head_n_byte = ExAllocatePoolWithTag(NonPagedPool, *PatchSize, "LyShark");

    irql = WPOFFx64();

    // 跳转地址拷贝到原函数上
    RtlCopyMemory(head_n_byte, ApiAddress, *PatchSize);
    WPONx64(irql);

    // 构建跳转

    // 1.原始机器码+跳转机器码
    ori_func = ExAllocatePoolWithTag(NonPagedPool, *PatchSize + 14, "LyShark");
    RtlFillMemory(ori_func, *PatchSize + 14, 0x90);

    // 2.跳转到没被打补丁的那个字节
    tmpv = (ULONG64)ApiAddress + *PatchSize;
    RtlCopyMemory(jmp_code_orifunc + 6, &tmpv, 8);
    RtlCopyMemory((PUCHAR)ori_func, head_n_byte, *PatchSize);
    RtlCopyMemory((PUCHAR)ori_func + *PatchSize, jmp_code_orifunc, 14);
    *Original_ApiAddress = ori_func;

    // 3.得到代理地址
    tmpv = (UINT64)Proxy_ApiAddress;
    RtlCopyMemory(jmp_code + 6, &tmpv, 8);

    //4.打补丁
    irql = WPOFFx64();
    RtlFillMemory(ApiAddress, *PatchSize, 0x90);
    RtlCopyMemory(ApiAddress, jmp_code, 14);
    WPONx64(irql);

    return head_n_byte;
}

/*
    InlineHookAPI 恢复挂钩地址

    参数1:被HOOK函数地址
    参数2:原始数据
    参数3:补丁长度
*/
VOID KernelUnHook(IN PVOID ApiAddress, IN PVOID OriCode, IN ULONG PatchSize)
{
   
   
    KIRQL irql;
    irql = WPOFFx64();
    RtlCopyMemory(ApiAddress, OriCode, PatchSize);
    WPONx64(irql);
}

// 实现我们自己的代理函数
NTSTATUS MyPsLookupProcessByProcessId(HANDLE ProcessId, PEPROCESS *Process)
{
   
   
    NTSTATUS st;
    st = ((PSLOOKUPPROCESSBYPROCESSID)original_address)(ProcessId, Process);
    if (NT_SUCCESS(st))
    {
   
   
        // 判断是否是需要保护的进程
        if (*Process == (PEPROCESS)protect_eprocess)
        {
   
   
            *Process = 0;
            DbgPrint("[lyshark] 拦截结束进程 \n");
            st = STATUS_ACCESS_DENIED;
        }
    }
    return st;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
   
   
    DbgPrint("驱动已卸载 \n");

    // 恢复Hook
    KernelUnHook(GetProcessAddress(L"PsLookupProcessByProcessId"), head_n_byte, patch_size);
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
   
   
    DbgPrint("hello lyshark \n");

    // 初始化反汇编引擎
    lde_init();

    // 设置需要保护进程EProcess
    /*
    lyshark: kd> !process 0 0 lyshark.exe
        PROCESS ffff9a0a44ec4080
            SessionId: 1  Cid: 05b8    Peb: 0034d000  ParentCid: 13f0
            DirBase: 12a7d2002  ObjectTable: ffffd60bc036f080  HandleCount: 159.
            Image: lyshark.exe
    */
    protect_eprocess = 0xffff9a0a44ec4080;

    // Hook挂钩函数
    head_n_byte = KernelHook(GetProcessAddress(L"PsLookupProcessByProcessId"), (PVOID)MyPsLookupProcessByProcessId, &original_address, &patch_size);

    DbgPrint("[lyshark] 挂钩保护完成 --> 修改字节: %d | 原函数地址: 0x%p \n", patch_size, original_address);

    for (size_t i = 0; i < patch_size; i++)
    {
   
   
        DbgPrint("[byte] = %x", head_n_byte[i]);
    }

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

运行这段驱动程序,会输出挂钩保护的具体地址信息;

使用WinDBG观察,会发现挂钩后原函数已经被替换掉了,而被替换的地址就是我们自己的MyPsLookupProcessByProcessId函数。

当你尝试使用任务管理器结束掉lyshark.exe进程时,则会提示拒绝访问。

目录
相关文章
|
4月前
|
监控 API C++
8.4 Windows驱动开发:文件微过滤驱动入门
MiniFilter 微过滤驱动是相对于`SFilter`传统过滤驱动而言的,传统文件过滤驱动相对来说较为复杂,且接口不清晰并不符合快速开发的需求,为了解决复杂的开发问题,微过滤驱动就此诞生,微过滤驱动在编写时更简单,多数`IRP`操作都由过滤管理器`(FilterManager或Fltmgr)`所接管,因为有了兼容层,所以在开发中不需要考虑底层`IRP`如何派发,更无需要考虑兼容性问题,用户只需要编写对应的回调函数处理请求即可,这极大的提高了文件过滤驱动的开发效率。
41 0
|
4月前
|
监控 Windows
7.4 Windows驱动开发:内核运用LoadImage屏蔽驱动
在笔者上一篇文章`《内核监视LoadImage映像回调》`中`LyShark`简单介绍了如何通过`PsSetLoadImageNotifyRoutine`函数注册回调来`监视驱动`模块的加载,注意我这里用的是`监视`而不是`监控`之所以是监视而不是监控那是因为`PsSetLoadImageNotifyRoutine`无法实现参数控制,而如果我们想要控制特定驱动的加载则需要自己做一些事情来实现,如下`LyShark`将解密如何实现屏蔽特定驱动的加载。
32 0
7.4 Windows驱动开发:内核运用LoadImage屏蔽驱动
|
21天前
|
监控 安全 API
7.3 Windows驱动开发:内核监视LoadImage映像回调
在笔者上一篇文章`《内核注册并监控对象回调》`介绍了如何运用`ObRegisterCallbacks`注册`进程与线程`回调,并通过该回调实现了`拦截`指定进行运行的效果,本章`LyShark`将带大家继续探索一个新的回调注册函数,`PsSetLoadImageNotifyRoutine`常用于注册`LoadImage`映像监视,当有模块被系统加载时则可以第一时间获取到加载模块信息,需要注意的是该回调函数内无法进行拦截,如需要拦截则需写入返回指令这部分内容将在下一章进行讲解,本章将主要实现对模块的监视功能。
37 0
7.3 Windows驱动开发:内核监视LoadImage映像回调
|
4月前
|
监控 安全 API
7.2 Windows驱动开发:内核注册并监控对象回调
在笔者上一篇文章`《内核枚举进程与线程ObCall回调》`简单介绍了如何枚举系统中已经存在的`进程与线程`回调,本章`LyShark`将通过对象回调实现对进程线程的`句柄`监控,在内核中提供了`ObRegisterCallbacks`回调,使用这个内核`回调`函数,可注册一个`对象`回调,不过目前该函数`只能`监控进程与线程句柄操作,通过监控进程或线程句柄,可实现保护指定进程线程不被终止的目的。
31 0
7.2 Windows驱动开发:内核注册并监控对象回调
|
4月前
|
监控 安全 API
7.6 Windows驱动开发:内核监控FileObject文件回调
本篇文章与上一篇文章`《内核注册并监控对象回调》`所使用的方式是一样的都是使用`ObRegisterCallbacks`注册回调事件,只不过上一篇博文中`LyShark`将回调结构体`OB_OPERATION_REGISTRATION`中的`ObjectType`填充为了`PsProcessType`和`PsThreadType`格式从而实现监控进程与线程,本章我们需要将该结构填充为`IoFileObjectType`以此来实现对文件的监控,文件过滤驱动不仅仅可以用来监控文件的打开,还可以用它实现对文件的保护,一旦驱动加载则文件是不可被删除和改动的。
29 1
7.6 Windows驱动开发:内核监控FileObject文件回调
|
4月前
|
监控 安全 API
6.9 Windows驱动开发:内核枚举进线程ObCall回调
在笔者上一篇文章`《内核枚举Registry注册表回调》`中我们通过特征码定位实现了对注册表回调的枚举,本篇文章`LyShark`将教大家如何枚举系统中的`ProcessObCall`进程回调以及`ThreadObCall`线程回调,之所以放在一起来讲解是因为这两中回调在枚举是都需要使用通用结构体`_OB_CALLBACK`以及`_OBJECT_TYPE`所以放在一起来讲解最好不过。
46 1
6.9 Windows驱动开发:内核枚举进线程ObCall回调
|
Windows
7、Windows驱动开发技术详解笔记(3) 基本语法回顾
    3、文件读写   在ring3 我们可以使用CreateFile、ReadFile 、WriteFile 等API,在ring0 同样很相似,不过函数变成了ZwCreateFile、ZwReadFile、ZwWriteFile 等内核函数。
940 0
|
安全 分布式数据库 API
6、Windows驱动开发技术详解笔记(2) 基本语法回顾
    1、字符串   Unicode 字符串有一个结构体定义如下: typedef struct _UNICODE_STRING { USHORT Length; // 字符串的长度(字节数) USHORT MaximumLength; // 字符串缓冲区的长度(字节数) PWSTR Buffer; // 字符串缓冲区 } UNICODE_STRING, *PUNICODE_STRING; 需要注意的是,当我们定义了一个UNICODE_STRING 变量之后,它的Buffer 域还没有分配空间,因此我们不能直接赋值,好的做法是使用微软提供的Rtl 系列函数。
1072 0
|
Windows 数据库连接 存储
8、Windows驱动开发技术详解笔记(4) 基本语法回顾
 4、注册表操作 和文件操作类似,在操作注册表之前需要首先打开注册表,获得一个句柄,这可以通过函数ZwCreateKey 完成。与ZwCreateFile函数类似,它通过一个OBJECT_ATTRIBUTES 获得需要创建或打开的路径信息,但在内核中这个路径与用户模式下不相同,实际上,因为用户模式下的应用程序总是由某个“当前用户”打开的,因此在用户模式下可以直接访问HKEY_CLASSES_ROOT 和HKEY_CURRENT_USER,但工作在内核模式下的驱动程序不属于任何一个用户,因此不能直接访问这两个根键。
821 0
|
Windows API
9、Windows驱动开发技术详解笔记(5) 基本语法回顾
    5、在驱动中获取系统时间 1)获取启动毫秒数  在ring3 我们可以通过一个GetTickCount 函数来获得自系统启动开始的毫秒数,在ring0也有一个与之对应的KeQueryTickCount 函数。
939 0