首页
社区
课程
招聘
[学习分享] InlineHook NtReadVirtualMemory实现过程
发表于: 18小时前 170

[学习分享] InlineHook NtReadVirtualMemory实现过程

18小时前
170

前言

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

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

第一步 获取NtReadVirtualMemory和MiReadWriteVirtualMemory地址

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//获取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原汇编代码:

1
2
3
4
5
6
7
8
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字节

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

保存原始字节以便还原

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

计算新的偏移

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

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

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

制作新偏移

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

开关保护写入偏移

1
2
3
4
5
6
7
//关闭写保护
DisableWP();
 
RtlCopyMemory(g_CallAddress, patch, 5);
 
//开启写保护
EnableWP();

第三步: 构造自己的MyNtReadVirtualMemory

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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的参数, 直接用形参取即可

1
2
3
4
5
6
7
8
//原系统函数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一个函数, 通常保护软件会拦截所有与内存 句柄 等相关的函数, 如果全部重塑实在不是好的办法...


[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

最后于 15小时前 被mb_binusgki编辑 ,原因: 补充内容
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回