首页
社区
课程
招聘
[分享]InlineHook NtReadVirtualMemory实现过程
发表于: 2026-2-24 13:08 1370

[分享]InlineHook NtReadVirtualMemory实现过程

2026-2-24 13:08
1370

前言

昨天做了ssdthook搞到挺晚最终还是出现一些bug导致程序无法正常打开, 于是今天修改思路使用inlineHook实现, inlineHook相比ssdthook是更加实用的技术, 也能做到更加隐蔽, 比ssdthook上限高了很多, 用起来也舒服.

大体思路为: 在NtReadVirtualMemory中的call nt!MiReadWriteVirtualMemory下手, 将其跳转到自己的方法, 实现自己的功能后在原封不动的jmpMiReadWriteVirtualMemory中, 提前透露说下今天的inlinehook相比于ssdthook非常顺利, 直接开始

第一步 获取NtReadVirtualMemory和MiReadWriteVirtualMemory地址

和ssdthook做法一模一样. 直接上代码:

//获取NtReadVirtualMemory函数
UINT_PTR GetNtReadVirtualMemory()
{
    //获取内核基址
    UINT_PTR NtoskrnlBase = GetNtoskrnlBase();
    //取NtReadVirtualMemory函数
    UINT_PTR NtReadVirtualMemory = NtoskrnlBase + 0x622A80;
    return NtReadVirtualMemory;
}

//获取MiReadWriteVirtualMemory函数
UINT_PTR GetMiReadWriteVirtualMemory()
{
    //获取内核基址
    UINT_PTR NtoskrnlBase = GetNtoskrnlBase();
    //取MiReadWriteVirtualMemory函数
    UINT_PTR MiReadWriteVirtualMemory = NtoskrnlBase + 0x622AB0;
    return MiReadWriteVirtualMemory;
}
    
//===============调度方法=================
//赋值MiReadWriteVirtualMemory
SysMiReadWriteVirtualMemory = GetMiReadWriteVirtualMemory();
if (!SysMiReadWriteVirtualMemory)
{
    PZY_PRINT("获取MiReadWriteVirtualMemory地址失败");
    return FALSE;
}

//获取NtReadVirtualMemory地址
SysNtReadVirtualMemory = GetNtReadVirtualMemory();
if (!SysNtReadVirtualMemory)
{
    PZY_PRINT("获取NtReadVirtualMemory地址失败");
    return FALSE;
}

第二步: inlineHOOK初始化过程

NtReadVirtualMemory原汇编代码:

nt!NtReadVirtualMemory:
fffff803`65ce2a80 4883ec38        sub     rsp,38h
fffff803`65ce2a84 488b442460      mov     rax,qword ptr [rsp+60h]
fffff803`65ce2a89 c744242810000000 mov     dword ptr [rsp+28h],10h
fffff803`65ce2a91 4889442420      mov     qword ptr [rsp+20h],rax
fffff803`65ce2a96 e815000000      call    nt!MiReadWriteVirtualMemory (fffff803`65ce2ab0)
fffff803`65ce2a9b 4883c438        add     rsp,38h
fffff803`65ce2a9f c3              ret

可以计算得出, 从开头到call nt!MiReadWriteVirtualMemory距离16字节

    // call 指令偏移
    g_CallAddress = (PUCHAR)SysNtReadVirtualMemory + 0x16;

保存原始字节以便还原

    // 保存原始 5 字节
    RtlCopyMemory(g_OriginalBytes, g_CallAddress, 5);

计算新的偏移

// 计算新的相对偏移
LONG64 diff = (LONG64)MyNtReadVirtualMemory - ((LONG64)g_CallAddress + 5);

插入指令是32位的 加上判断防止出错

    //判断是否超出范围
    if (diff > 0x7fffffffLL || diff < -0x80000000LL)
        return STATUS_NOT_SUPPORTED;  // 超出 rel32 范围

制作新偏移

LONG disp32 = (LONG)diff;

UCHAR patch[5];
patch[0] = 0xE8;                     // call
*(PLONG)&patch[1] = disp32;          // 写入新偏移

开关保护写入偏移

    //关闭写保护
    DisableWP();

    RtlCopyMemory(g_CallAddress, patch, 5);

    //开启写保护
    EnableWP();

第三步: 构造自己的MyNtReadVirtualMemory

MyNtReadVirtualMemory也是在ssdt写的基础上修改的, 相比之前添加了保存通用寄存器这步, 以便确保寄存器的正确性

OPTION CASEMAP:NONE

EXTERN SysNtReadVirtualMemory:PROC
EXTERN SysMiReadWriteVirtualMemory:PROC
EXTERN HookNtReadVirtualMemoryXx:PROC
PUBLIC MyNtReadVirtualMemory

.code
    MyNtReadVirtualMemory PROC
        
        ;自己的代码
        ; -------- 保存通用寄存器 --------
        push    rcx
        push    rdx
        push    r8
        push    r9
        push    r10
        push    r11

        sub rsp, 28h
        call HookNtReadVirtualMemoryXx
        add rsp, 28h

        ; -------- 还原寄存器(逆序)--------
        pop     r11
        pop     r10
        pop     r9
        pop     r8
        pop     rdx
        pop     rcx

        ;调用原MiReadWriteVirtualMemory
        jmp qword ptr [SysMiReadWriteVirtualMemory]

    MyNtReadVirtualMemory ENDP

END

HookNtReadVirtualMemoryXx这个方法就是自己的算法, 框架写好后算法都可以放上加, 可以记一下默认NtReadVirtualMemory的参数, 直接用形参取即可

//原系统函数NtReadVirtualMemory
typedef NTSTATUS(NTAPI* NtReadVirtualMemory)(
    HANDLE ProcessHandle,
    PVOID BaseAddress,
    PVOID Buffer,
    SIZE_T BufferSize,
    PSIZE_T NumberOfBytesRead
    );

大概是这样, 但好像有些版本是六个参数, 我就取了五个参数, 因为栈值都是8字节一个参数所以并不存在对齐问题, 还是很舒服的

其实我就上了个日志的功能, 大致就是取了第一个参数就是句柄, 根据句柄获取eprocess, 然后继续获取pid和程序名称, 其实推荐用pid和handle获取就好, 用文件名获取会消耗一些内存

第五步: 效果展示及总结依旧是拿ce做演示, 可以正常获取并且顺利打开了程序没有昨天的内存访问失败错误 图片描述当打开进程列表也能看到ce在访存的记录, 一切正常 图片描述

后续要添加过滤或者监控都非常方便, 只需要在r3传入pid, 就可以在r0做相应处理

最后就是对于根据句柄获取eprocess的ObReferenceObjectByHandle函数有个坑, 就是我们默认都是要进程句柄, 所以传入的参数都是PsProcessType 相当于一个过滤, 如果不是进程句柄则返回失败, 失败了切记要直接返回, 之前我就没注意这个点, 即便是返回失败了还继续用空的eprocess获取pid等后续操作, 换来的自然就是疯狂蓝屏 然后又花了时间排查..

但是总的来说 inlinehook确实非常好用 简单效果强, 并且微软也确实没有对NtReadVirtualMemory的字节码做扫描, 毕竟对NtReadVirtualMemory访问频率高的函数定时扫描那对性能损耗非常高.

对于对句柄降权方式的保护无非就是看谁hook的函数更加底层, 其实也可以直接通过映射物理地址等方式直接重塑NtReadVirtualMemory进行访存, 但是对于CE OD这些工具就不是很友好, 要在r3层进行拦截分流到自己的接口, 并且并不止NtReadVirtualMemory一个函数, 通常保护软件会拦截所有与内存 句柄 等相关的函数, 如果全部重塑实在不是好的办法...


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 4天前 被mb_binusgki编辑 ,原因: 补充内容
收藏
免费 1
支持
分享
最新回复 (2)
雪    币: 1411
活跃值: (1409)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
2
PatchGuard会保护的
2026-2-25 10:02
0
雪    币: 309
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
之前用的ssdthook似乎被PatchGuard保护了, 但直接在函数内部inlinehook似乎是成功了, 但是我是在测试模式免签名环境下调试的, 不懂正常环境是否也会被PatchGuard
2026-2-26 10:59
0
游客
登录 | 注册 方可回帖
返回