前言
昨天做了ssdthook搞到挺晚最终还是出现一些bug导致程序无法正常打开, 于是今天修改思路使用inlineHook实现, inlineHook相比ssdthook是更加实用的技术, 也能做到更加隐蔽, 比ssdthook上限高了很多, 用起来也舒服.
大体思路为: 在NtReadVirtualMemory中的call nt!MiReadWriteVirtualMemory下手, 将其跳转到自己的方法, 实现自己的功能后在原封不动的jmp到MiReadWriteVirtualMemory中, 提前透露说下今天的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编辑
,原因: 补充内容