前言
SSDT HOOK已经是非常老的技术了, 但是作为入门的新手还是要走一下流程了解原理, 本次实验的对象为NtReadVirtualMemory函数, 平台为win10x64, 过程中遇到了不少问题, 也换了几种方式实现, 但总体上没有脱离SSDT HOOK, 没有使用inline Hook.
第一步: 获取NtReadVirtualMemory函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | //获取NtReadVirtualMemory函数
UINT_PTR GetNtReadVirtualMemory()
{
//获取内核基址
UINT_PTR NtoskrnlBase = GetNtoskrnlBase();
//取NtReadVirtualMemory函数
UINT_PTR NtReadVirtualMemory = NtoskrnlBase + 0x622A80;
return NtReadVirtualMemory;
}
//========调度方法=========
//获取NtReadVirtualMemory地址
SysNtReadVirtualMemory = GetNtReadVirtualMemory();
if (!SysNtReadVirtualMemory)
{
PZY_PRINT("获取NtReadVirtualMemory地址失败");
return FALSE;
}
|
我用的偷懒的方法, 直接根据内核偏移进行了硬编码定位(这种方法版本并不通用), 后续可改为特征值方式查找
第二步: 获取SSDT表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | //获取SSDT
PKSERVICE_DESCRIPTOR_TABLE GetSSDT()
{
//获取内核基址
UINT64 NtoskrnlBase = GetNtoskrnlBase();
//取KiSystemServiceRepeat函数
UINT64 KiSystemServiceRepeat = NtoskrnlBase + 0x1D2B94;
//计算偏移
INT32 offset = *(INT32*)(KiSystemServiceRepeat + 3);
UINT64 nextCode = KiSystemServiceRepeat + 7;
return nextCode + offset;
}
//========调度方法=========
//获取SSDT
PKSERVICE_DESCRIPTOR_TABLE ssdt = GetSSDT();
if (!ssdt)
{
PZY_PRINT("获取SSDT失败");
return FALSE;
}
|
ssdt表全称为系统服务表: system_service_descriptor_table, 在<ntifs.h>中就有PKSERVICE_DESCRIPTOR_TABLE类型可以直接使用.
获取ssdt方式我是通过取KiSystemServiceRepeat函数在此基础上计算偏移得出, 因为KiSystemServiceRepeat就是系统服务从r3到r0必经的函数, 所以跟踪此函数必然可得出ssdt的位置.
第三步: 在SSDT中寻找NtReadVirtualMemory函数
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | //在SSDT中寻找指定函数
BOOLEAN FindNtFunctionInSSDT(
_In_ PKSERVICE_DESCRIPTOR_TABLE KeSsdt,
_In_ PVOID TargetNtFunction,
_Out_ PULONG FoundIndex,
_Out_ PULONG_PTR* FoundEntryAddress
)
{
//检查参数是否合格
if (!KeSsdt || !TargetNtFunction)
return FALSE;
//取出服务表基址
PUCHAR serviceTableBase = (PUCHAR)KeSsdt->ServiceTableBase;
//取出服务表数量
ULONG numberOfServices = KeSsdt->NumberOfServices;
//循环遍历
for (ULONG index = 0; index < numberOfServices; index++)
{//解密算法参考KiSystemServiceRepeat
// movsxd r11, dword ptr [r10+rax*4]
INT32 entry = *(INT32*)(serviceTableBase + index * sizeof(INT32));
// sar r11, 4 例子:0x01fde701 -> 01fde70
entry >>= 4;
//add r10, r11
PUCHAR func = serviceTableBase + entry;
//判断
if ((PVOID)func == TargetNtFunction)
{
//写入index
if (FoundIndex)
*FoundIndex = index;
//写入fun地址
if (FoundEntryAddress)
*FoundEntryAddress = (PVOID)(serviceTableBase + index * sizeof(INT32));
return TRUE;
}
}
return FALSE;
}
//========调度方法=========
//在SSDT中寻找指定函数
ULONG index;
PULONG entry;
BOOLEAN FindNtFunctionInSSDTState = FindNtFunctionInSSDT(ssdt, SysNtReadVirtualMemory, &index, &entry);
if (!FindNtFunctionInSSDTState)
{
PZY_PRINT("在SSDT中寻找指定函数失败");
return FALSE;
}
|
这里特别说下ssdt与对应函数的映射算法:
首先ssdt是一个结构存储着ssdt表的信息, 里面的常用字段一般为: ServiceTableBase 这就是ssdt列表的地址, NumberOfServices 这个就是ssdt列表中有多少个服务对象 也就是多少个系统函数, 其他字段暂时没有用到.
1 2 3 4 5 | KMDFDriver2!_KSERVICE_DESCRIPTOR_TABLE
+0x000 ServiceTableBase : Ptr64 Uint8B
+0x008 ServiceCounterTableBase : Ptr64 Uint4B
+0x010 NumberOfServices : Uint8B
+0x018 ParamTableBase : Ptr64 Char
|
然后继续说, ssdt的每个服务是4个字节, 并不是8个字节, 所以取出的服务也并不是直接可以使用的地址, 需要一定的解密过程, 算法为: ServiceTableBase + [ServiceTableBase + 服务号4] >> 4,
举个例子:
ServiceTableBase: 0xfffff8072d8ccc10
服务号: 0x3f
[ServiceTableBase + 0x3f4]=46743f00
那么服务方法就是: 0xfffff8072d8ccc10+[ServiceTableBase + 0x3f*4]>>4
=0xfffff8072d8ccc10+46743f00>>4
=0xfffff8072d8ccc10+46743f0
验证一下:
1 2 3 4 5 6 | 1: kd> u 0xfffff807`2d8ccc10+46743f0
KMDFDriver2!MyNtReadVirtualMemory [D:\pzy\个人练习\C++\KMDF Driver2\KMDF Driver2\MyFunction.asm @ 9]:
fffff807`31f41000 4883ec28 sub rsp,28h
fffff807`31f41004 ff157e710000 call qword ptr [KMDFDriver2!SysNtReadVirtualMemory (fffff807`31f48188)]
fffff807`31f4100a 4883c428 add rsp,28h
fffff807`31f4100e c3 ret
|
第四步: 关闭写保护与开启保护
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //关闭写保护
VOID DisableWP()
{
__writecr0(__readcr0() & ~0x10000);
}
//开启写保护
VOID EnableWP()
{
__writecr0(__readcr0() | 0x10000);
}
//========调度方法=========
//关闭写保护
DisableWP();
//开启写保护
EnableWP();
|
这里的开关读写保护__writecr0方法也是<ntifs.h>官方提供好的 直接用即可 就不用手搓汇编了
第五步: 保存原NtReadVirtualMemory在SSDT表的值并写入写入HOOK函数
1 2 3 4 5 6 7 8 9 10 | //========调度方法=========
//保存原NtReadVirtualMemory在SSDT表的值
oldEntry = *entry;
oldIndex = index;
//写入HOOK
INT32 newEntry = (INT32)((PUCHAR)MyNtReadVirtualMemory - ssdt->ServiceTableBase);
// SSDT 要求 << 4
newEntry <<= 4;
// 写回 4 字节
*entry = newEntry;
|
这里就是保存原ssdt的值还原的时候要用, 然后将自己的方法写入ssdt, 算法也是逆向还原为ssdt服务的算法, 就不细说了
第六步: HOOK函数的构造
这一步也是坑最多的一步, 并且有些问题我也没解决
原本刚开始我是直接写C的代码的, 但是在排查问题的过程中不清晰, 所以改写汇编了(我要确保栈不出问题)
于是拥有x32编程经验的我顺手就开始写__asm{}的代码, 然后发现编译器通不过, 一查发现: 原来x64不给用, 就只能乖乖写.asm文件
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 | OPTION CASEMAP:NONE
EXTERN SysNtReadVirtualMemory:PROC
EXTERN SysMiReadWriteVirtualMemory:PROC
EXTERN HookNtReadVirtualMemoryXx:PROC
PUBLIC MyNtReadVirtualMemory
.code
MyNtReadVirtualMemory PROC
;自己的代码
sub rsp, 28h
call HookNtReadVirtualMemoryXx
add rsp, 28h
;尝试方法一
;NtReadVirtualMemory原来的代码
sub rsp, 38h
mov rax, [rsp + 60h]
mov dword ptr [rsp + 28h], 10h
mov [rsp + 20h], rax
call qword ptr [SysMiReadWriteVirtualMemory]
add rsp, 38h
ret
;尝试方法二
;直接调用NtReadVirtualMemory
;sub rsp, 28h
;call qword ptr [SysNtReadVirtualMemory]
;add rsp, 28h
;ret
MyNtReadVirtualMemory ENDP
END
|
这里简单说下.asm的语法:
EXTERN 就是要从外部导入的函数
PUBLIC 就是要导出的方法
.code 就是代码段
.data 就是数据段 但我没用到
OPTION CASEMAP:NONE是大小写关闭,防止出错
END 最后记得协商结束符号
SysNtReadVirtualMemory 这是原系统NtReadVirtualMemory方法, 为了不与系统函数重名我加了sys前缀
SysMiReadWriteVirtualMemory 这个也是原系统MiReadWriteVirtualMemory方法
HookNtReadVirtualMemoryXx 这个是我自己的方法, 我就简单的输出了日志用于观察
然后导出的方法在C中声明即可使用
1 2 3 4 5 6 7 8 | //汇编声明
extern NTSTATUS MyNtReadVirtualMemory(
HANDLE ProcessHandle,
PVOID BaseAddress,
PVOID Buffer,
SIZE_T Size,
PSIZE_T NumberOfBytesRead
);
|
nt!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 c744242810000000mov 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
|
第七步: HOOK函数的构造问题与思考
我的想法是在调用完自己的方法后直接原封不动执行NtReadVirtualMemory的代码, 确实我就是这么写的, 刚开始栈不对或者参数没传对会造成蓝屏, 但换成汇编后完全没这些担心了, 我能保证代码完全和系统的一致, 并且跟踪了调用过程也是一致的

修改后的ssdt:
1 2 3 4 5 6 7 8 9 10 11 | 1: kd> u 0xfffff801`08ec8c10+46583f0
KMDFDriver2!MyNtReadVirtualMemory [D:\pzy\个人练习\C++\KMDF Driver2\KMDF Driver2\MyFunction.asm @ 9]:
fffff801`0d521000 4883ec28 sub rsp,28h
fffff801`0d521004 e8b7170000 call KMDFDriver2!HookNtReadVirtualMemoryXx (fffff801`0d5227c0)
fffff801`0d521009 4883c428 add rsp,28h
fffff801`0d52100d 4883ec38 sub rsp,38h
fffff801`0d521011 488b442460 mov rax,qword ptr [rsp+60h]
fffff801`0d521016 c744242810000000 mov dword ptr [rsp+28h],10h
fffff801`0d52101e 4889442420 mov qword ptr [rsp+20h],rax
fffff801`0d521023 ff156f710000 call qword ptr [KMDFDriver2!SysMiReadWriteVirtualMemory (fffff801`0d528198)]
|
修改后可以看到日志效果: 凡是读取内存经过NtReadVirtualMemory都有记录

但是此时打开任何应用都会有c0000005错误提示:

我排除了栈不平 栈取值错误以及寄存器不对的可能(将HookNtReadVirtualMemoryXx注释掉就完全和系统函数一致了, 但错误还是存在), 唯一有可能出错的地方就在于系统检查了返回地址, 判断了返回地址是否在内核范围内, 于是我做了测试将直接调用call qword ptr [SysNtReadVirtualMemory] 这样如果检查的是NtReadVirtualMemory中的MiReadWriteVirtualMemory那么我就能通过
但结果还是失败, 那就只有一种可能: 校验是在KiSystemServiceRepeat中进行的, 后续我跟踪了MiReadWriteVirtualMemory证实了0xc0000005错误确实在MiReadWriteVirtualMemory中发生:


但判断实在KiSystemServiceRepeat中
第八: 总结
这次ssdt整体流程走下来了, 但是在最后一步没有成功, 我后续又想了一阵 觉得暂时没有必要深究 毕竟这是人尽皆知的技术 微软严防也是正常, 而且这个技术一用就是名牌, 相当于公开裸奔所以价值不大, 但是没搞出来还是不太爽, 有无大神帮忙解答...
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
最后于 19小时前
被mb_binusgki编辑
,原因: