首页
社区
课程
招聘
[分享][学习]HOOK SSDT NtReadVirtualMemory学习以及遇到的问题
发表于: 1天前 407

[分享][学习]HOOK SSDT NtReadVirtualMemory学习以及遇到的问题

1天前
407

前言

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 + 0x3f
4]=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整体流程走下来了, 但是在最后一步没有成功, 我后续又想了一阵 觉得暂时没有必要深究 毕竟这是人尽皆知的技术 微软严防也是正常, 而且这个技术一用就是名牌, 相当于公开裸奔所以价值不大, 但是没搞出来还是不太爽, 有无大神帮忙解答...


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

最后于 18小时前 被mb_binusgki编辑 ,原因:
收藏
免费 1
支持
分享
最新回复 (2)
雪    币: 2958
活跃值: (6598)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2
图片的是c0000005,
20小时前
0
雪    币: 240
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
逆向爱好者 图片的是c0000005,
是c0000005 我笔误写错了 多谢提醒已修改
18小时前
0
游客
登录 | 注册 方可回帖
返回