首页
社区
课程
招聘
[分享]HOOK SSDT NtReadVirtualMemory实操以及遇到的问题
发表于: 2026-2-23 20:11 1337

[分享]HOOK SSDT NtReadVirtualMemory实操以及遇到的问题

2026-2-23 20:11
1337

前言

SSDT HOOK已经是非常老的技术了, 但是作为入门的新手还是要走一下流程了解原理, 本次实验的对象为NtReadVirtualMemory函数, 平台为win10x64, 过程中遇到了不少问题, 也换了几种方式实现, 但总体上没有脱离SSDT HOOK, 没有使用inline Hook.

第一步: 获取NtReadVirtualMemory函数

    //获取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表


    //获取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函数

    //在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列表中有多少个服务对象 也就是多少个系统函数, 其他字段暂时没有用到.

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: 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

第四步: 关闭写保护与开启保护

    //关闭写保护
    VOID DisableWP()
    {
        __writecr0(__readcr0() & ~0x10000);
    }
    //开启写保护
    VOID EnableWP()
    {
        __writecr0(__readcr0() | 0x10000);
    }

    //========调度方法=========
    //关闭写保护
    DisableWP();
    //开启写保护
    EnableWP();

这里的开关读写保护__writecr0方法也是&lt;ntifs.h&gt;官方提供好的 直接用即可 就不用手搓汇编了

第五步: 保存原NtReadVirtualMemory在SSDT表的值并写入写入HOOK函数


    //========调度方法=========
    //保存原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文件

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中声明即可使用

//汇编声明
extern NTSTATUS MyNtReadVirtualMemory(
    HANDLE ProcessHandle,
    PVOID BaseAddress,
    PVOID Buffer,
    SIZE_T Size,
    PSIZE_T NumberOfBytesRead
);

nt!NtReadVirtualMemory

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: 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实战!

最后于 5天前 被mb_binusgki编辑 ,原因: 补充内容
收藏
免费 1
支持
分享
最新回复 (2)
雪    币: 3032
活跃值: (6783)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2
图片的是c0000005,
2026-2-24 09:44
0
雪    币: 309
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
逆向爱好者 图片的是c0000005,
是c0000005 我笔误写错了 多谢提醒已修改
2026-2-24 12:31
0
游客
登录 | 注册 方可回帖
返回