6.1 Windows驱动开发:内核枚举SSDT表基址

SSDT表(System Service Descriptor Table)是Windows操作系统内核中的关键组成部分,负责存储系统服务调用的相关信息。具体而言,SSDT表包含了系统调用的函数地址以及其他与系统服务相关的信息。每个系统调用对应SSDT表中的一个表项,其中存储了相应系统服务的函数地址。SSDT表在64位和32位系统上可能有不同的结构,但通常以数组形式存在。

对于系统调用的监控、分析或修改等高级操作,常需要内核枚举SSDT表基址。这一操作通常通过内核模块实现,涉及技术手段如逆向工程和Hooking。

看一款闭源ARK工具的枚举效果:

直接步入正题,首先SSDT表中文为系统服务描述符表,SSDT表的作用是把应用层与内核联系起来起到桥梁的作用,枚举SSDT表也是反内核工具最基本的功能,通常在64位系统中要想找到SSDT表,需要先找到KeServiceDescriptorTable这个函数,由于该函数没有被导出,所以只能动态的查找它的地址,庆幸的是我们可以通过查找msr(c0000082)这个特殊的寄存器来替代查找KeServiceDescriptorTable这一步,在新版系统中查找SSDT可以归纳为如下这几个步骤。

  • rdmsr c0000082 -> KiSystemCall64Shadow -> KiSystemServiceUser -> SSDT

首先第一步通过rdmsr C0000082 MSR寄存器得到KiSystemCall64Shadow的函数地址,计算KiSystemCall64ShadowKiSystemServiceUser偏移量,如下图所示。

  • 得到相对偏移6ed53180(KiSystemCall64Shadow) - 6ebd2a82(KiSystemServiceUser) = 1806FE
  • 也就是说 6ed53180(rdmsr) - 1806FE = KiSystemServiceUser

如上当我们找到了KiSystemServiceUser的地址以后,在KiSystemServiceUser向下搜索可找到KiSystemServiceRepeat里面就是我们要找的SSDT表基址。

其中fffff8036ef8c880则是SSDT表的基地址,紧随其后的fffff8036ef74a80则是SSSDT表的基地址。

那么如果将这个过程通过代码的方式来实现,我们还需要使用《内核枚举IoTimer定时器》中所使用的特征码定位技术,如下我们查找这段特征。

#include <ntifs.h>
#pragma intrinsic(__readmsr)

ULONGLONG ssdt_address = 0;

// 获取 KeServiceDescriptorTable 首地址
ULONGLONG GetLySharkCOMKeServiceDescriptorTable()
{
    // 设置起始位置
    PUCHAR StartSearchAddress = (PUCHAR)__readmsr(0xC0000082) - 0x1806FE;

    // 设置结束位置
    PUCHAR EndSearchAddress = StartSearchAddress + 0x100000;
    DbgPrint("[LyShark Search] 扫描起始地址: %p --> 扫描结束地址: %p \n", StartSearchAddress, EndSearchAddress);

    PUCHAR ByteCode = NULL;

    UCHAR OpCodeA = 0, OpCodeB = 0, OpCodeC = 0;
    ULONGLONG addr = 0;
    ULONG templong = 0;

    for (ByteCode = StartSearchAddress; ByteCode < EndSearchAddress; ByteCode++)
    {
        // 使用MmIsAddressValid()函数检查地址是否有页面错误
        if (MmIsAddressValid(ByteCode) && MmIsAddressValid(ByteCode + 1) && MmIsAddressValid(ByteCode + 2))
        {
            OpCodeA = *ByteCode;
            OpCodeB = *(ByteCode + 1);
            OpCodeC = *(ByteCode + 2);

            // 对比特征值 寻找 nt!KeServiceDescriptorTable 函数地址
            /*
            nt!KiSystemServiceRepeat:
            fffff803`6ebd2b94 4c8d15e59c3b00  lea     r10,[nt!KeServiceDescriptorTable (fffff803`6ef8c880)]
            fffff803`6ebd2b9b 4c8d1dde1e3a00  lea     r11,[nt!KeServiceDescriptorTableShadow (fffff803`6ef74a80)]
            fffff803`6ebd2ba2 f7437880000000  test    dword ptr [rbx+78h],80h
            fffff803`6ebd2ba9 7413            je      nt!KiSystemServiceRepeat+0x2a (fffff803`6ebd2bbe)  Branch
            */
            if (OpCodeA == 0x4c && OpCodeB == 0x8d && OpCodeC == 0x15)
            {
                // 获取高位地址fffff802
                memcpy(&templong, ByteCode + 3, 4);

                // 与低位64da4880地址相加得到完整地址
                addr = (ULONGLONG)templong + (ULONGLONG)ByteCode + 7;
                return addr;
            }
        }
    }
    return  0;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint(("驱动程序卸载成功! \n"));
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark");

    ssdt_address = GetLySharkCOMKeServiceDescriptorTable();
    DbgPrint("[LyShark] SSDT = %p \n", ssdt_address);

    DriverObject->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

如上代码中所提及的步骤我想不需要再做解释了,这段代码运行后即可输出SSDT表的基址。

如上通过调用GetLySharkCOMKeServiceDescriptorTable()得到SSDT地址以后我们就需要对该地址进行解密操作。

得到ServiceTableBase的地址后,就能得到每个服务函数的地址。但这个表存放的并不是SSDT函数的完整地址,而是其相对于ServiceTableBase[Index]>>4的数据,每个数据占四个字节,所以计算指定Index函数完整地址的公式是;

  • 在x86平台上: FuncAddress = KeServiceDescriptorTable + 4 * Index
  • 在x64平台上:FuncAddress = [KeServiceDescriptorTable+4*Index]>>4 + KeServiceDescriptorTable

如下汇编代码就是一段解密代码,代码中rcx寄存器传入SSDT的下标,而rdx寄存器则是传入SSDT表基址。

  48:8BC1                  | mov rax,rcx                             |  rcx=index
  4C:8D12                  | lea r10,qword ptr ds:[rdx]              |  rdx=ssdt
  8BF8                     | mov edi,eax                             |
  C1EF 07                  | shr edi,7                               |
  83E7 20                  | and edi,20                              |
  4E:8B1417                | mov r10,qword ptr ds:[rdi+r10]          |
  4D:631C82                | movsxd r11,dword ptr ds:[r10+rax*4]     |
  49:8BC3                  | mov rax,r11                             |
  49:C1FB 04               | sar r11,4                               |
  4D:03D3                  | add r10,r11                             |
  49:8BC2                  | mov rax,r10                             |
  C3                       | ret                                     |

有了解密公式以后代码的编写就变得很容易,如下是读取SSDT的完整代码。

#include <ntifs.h>
#pragma intrinsic(__readmsr)

typedef struct _SYSTEM_SERVICE_TABLE
{
    PVOID     ServiceTableBase;
    PVOID     ServiceCounterTableBase;
    ULONGLONG   NumberOfServices;
    PVOID     ParamTableBase;
} SYSTEM_SERVICE_TABLE, *PSYSTEM_SERVICE_TABLE;

ULONGLONG ssdt_base_aadress;
PSYSTEM_SERVICE_TABLE KeServiceDescriptorTable;

typedef UINT64(__fastcall *SCFN)(UINT64, UINT64);
SCFN scfn;

// 解密算法
VOID DecodeSSDT()
{
    UCHAR strShellCode[36] = "\x48\x8B\xC1\x4C\x8D\x12\x8B\xF8\xC1\xEF\x07\x83\xE7\x20\x4E\x8B\x14\x17\x4D\x63\x1C\x82\x49\x8B\xC3\x49\xC1\xFB\x04\x4D\x03\xD3\x49\x8B\xC2\xC3";
    /*
    48:8BC1                  | mov rax,rcx                             |  rcx=index
    4C:8D12                  | lea r10,qword ptr ds:[rdx]              |  rdx=ssdt
    8BF8                     | mov edi,eax                             |
    C1EF 07                  | shr edi,7                               |
    83E7 20                  | and edi,20                              |
    4E:8B1417                | mov r10,qword ptr ds:[rdi+r10]          |
    4D:631C82                | movsxd r11,dword ptr ds:[r10+rax*4]     |
    49:8BC3                  | mov rax,r11                             |
    49:C1FB 04               | sar r11,4                               |
    4D:03D3                  | add r10,r11                             |
    49:8BC2                  | mov rax,r10                             |
    C3                       | ret                                     |
    */
    scfn = ExAllocatePool(NonPagedPool, 36);
    memcpy(scfn, strShellCode, 36);
}

// 获取 KeServiceDescriptorTable 首地址
ULONGLONG GetKeServiceDescriptorTable()
{
    // 设置起始位置
    PUCHAR StartSearchAddress = (PUCHAR)__readmsr(0xC0000082) - 0x1806FE;

    // 设置结束位置
    PUCHAR EndSearchAddress = StartSearchAddress + 0x8192;
    DbgPrint("扫描起始地址: %p --> 扫描结束地址: %p \n", StartSearchAddress, EndSearchAddress);

    PUCHAR ByteCode = NULL;

    UCHAR OpCodeA = 0, OpCodeB = 0, OpCodeC = 0;
    ULONGLONG addr = 0;
    ULONG templong = 0;

    for (ByteCode = StartSearchAddress; ByteCode < EndSearchAddress; ByteCode++)
    {
        // 使用MmIsAddressValid()函数检查地址是否有页面错误
        if (MmIsAddressValid(ByteCode) && MmIsAddressValid(ByteCode + 1) && MmIsAddressValid(ByteCode + 2))
        {
            OpCodeA = *ByteCode;
            OpCodeB = *(ByteCode + 1);
            OpCodeC = *(ByteCode + 2);

            // 对比特征值 寻找 nt!KeServiceDescriptorTable 函数地址
            // lyshark
            // 4c 8d 15 e5 9e 3b 00  lea r10,[nt!KeServiceDescriptorTable (fffff802`64da4880)]
            // 4c 8d 1d de 20 3a 00  lea r11,[nt!KeServiceDescriptorTableShadow (fffff802`64d8ca80)]
            if (OpCodeA == 0x4c && OpCodeB == 0x8d && OpCodeC == 0x15)
            {
                // 获取高位地址fffff802
                memcpy(&templong, ByteCode + 3, 4);

                // 与低位64da4880地址相加得到完整地址
                addr = (ULONGLONG)templong + (ULONGLONG)ByteCode + 7;
                return addr;
            }
        }
    }
    return  0;
}

// 得到函数相对偏移地址
ULONG GetOffsetAddress(ULONGLONG FuncAddr)
{
    ULONG dwtmp = 0;
    PULONG ServiceTableBase = NULL;
    if (KeServiceDescriptorTable == NULL)
    {
        KeServiceDescriptorTable = (PSYSTEM_SERVICE_TABLE)GetKeServiceDescriptorTable();
    }
    ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase;
    dwtmp = (ULONG)(FuncAddr - (ULONGLONG)ServiceTableBase);
    return dwtmp << 4;
}

// 根据序号得到函数地址
ULONGLONG GetSSDTFunctionAddress(ULONGLONG NtApiIndex)
{
    ULONGLONG ret = 0;
    if (ssdt_base_aadress == 0)
    {
        // 得到ssdt基地址
        ssdt_base_aadress = GetKeServiceDescriptorTable();
    }
    if (scfn == NULL)
    {
        DecodeSSDT();
    }
    ret = scfn(NtApiIndex, ssdt_base_aadress);
    return ret;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint(("驱动程序卸载成功! \n"));
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark \n");

    ULONGLONG ssdt_address = GetKeServiceDescriptorTable();
    DbgPrint("SSDT基地址 = %p \n", ssdt_address);

    // 根据序号得到函数地址
    ULONGLONG address = GetSSDTFunctionAddress(51);
    DbgPrint("[LyShark] NtOpenFile地址 = %p \n", address);
     
    // 得到相对SSDT的偏移量
    DbgPrint("函数相对偏移地址 = %p \n", GetOffsetAddress(address));

    DriverObject->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

运行后即可得到SSDT下标为51的函数也就是得到NtOpenFile的绝对地址和相对地址。

你也可以打开ARK工具,对比一下是否一致,如下图所示,LyShark的代码是没有任何问题的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/205750.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Netty Review - 探索Channel和Pipeline的内部机制

文章目录 概念Channel Pipeline实现原理分析详解 Inbound事件和Outbound事件演示Code 概念 Netty中的Channel和Pipeline是其核心概念&#xff0c;它们在构建高性能网络应用程序时起着重要作用。 Channel&#xff1a; 在Netty中&#xff0c;Channel表示一个开放的连接&#xff…

Spring整合web环境

目录 Javaweb三大组件及环境特点 Spring整合web环境的思路及实现 Spring的web开发组件spring-web MVC框架思想及其设计思路 Javaweb三大组件及环境特点 Spring整合web环境的思路及实现 package com.xfy.listener;import com.xfy.config.SpringConfig; import org.springfra…

Scrapy框架中间件(一篇文章齐全)

1、Scrapy框架初识&#xff08;点击前往查阅&#xff09; 2、Scrapy框架持久化存储&#xff08;点击前往查阅&#xff09; 3、Scrapy框架内置管道&#xff08;点击前往查阅&#xff09; 4、Scrapy框架中间件 Scrapy 是一个开源的、基于Python的爬虫框架&#xff0c;它提供了…

业务连续性的实施步骤及价值

业务连续性计划不仅仅是面对灾难时的应急措施&#xff0c;更是一种战略性的投资。通过综合的风险评估、计划制定和实施&#xff0c;企业可以提高对各种风险的适应能力&#xff0c;确保业务在任何情况下都能够持续运营。这种全面的准备不仅有助于应对突发事件&#xff0c;还能为…

【社会网络分析第6期】Ucient实操

一、导入数据处理二、核心——边缘分析三、聚类分析四、网络密度 一、导入数据处理 将数据导入Ucinet首先需要对数据进行处理。 承接上一期的数据格式&#xff1a;【社会网络分析第5期】gephi使用指南 原先得到的数据格式如下&#xff1a; 接下来打开ucinet&#xff1a; 之后…

latex中算法的几种模板

latex中算法的几种模板_latex算法模板-CSDN博客文章浏览阅读6.2k次&#xff0c;点赞3次&#xff0c;收藏45次。latex中几种算法模板_latex算法模板https://blog.csdn.net/weixin_50514171/article/details/125136121?spm1001.2014.3001.5506 latex排版原理 常用算法排版伪代码…

「Linux」使用C语言制作简易Shell

&#x1f4bb;文章目录 &#x1f4c4;前言简易shell实现shell的概念系统环境变量shell的结构定义内建命令完整代码 &#x1f4d3;总结 &#x1f4c4;前言 对于很多学习后端的同学来讲&#xff0c;学习了C语言&#xff0c;发现除了能写出那个经典的“hello world”以外&#xff…

43.0BaseDao抽取dao公共父类

43.1. 回顾 1. 把数据库表中查询的结果封装到一个实体类中。 命名规则:类名和表名一致 类中属性和表的字段对应。 表中的一条记录对应实体的一个对象 多条记录→集合 43.2. 正文 目录 43.1. 回顾 43.2. 正文 43.3. 抽取dao公共父类。 43.4. 引入数据源 43.3. 抽取dao公共…

文件夹重命名技巧:用关键词替换文件夹名称指定内容的右侧文字

在日常生活中&#xff0c;经常要管理大量的文件夹&#xff0c;这时候掌握一些文件夹重命名的技巧就非常实用。例如文件夹重命名时&#xff0c;经常要将一些通用的文字替换成其他关键词&#xff0c;以便更好地标识和分类文件夹。而用关键词替换文件夹名称指定内容的右侧文字&…

【论文阅读】1 SkyChain:一个深度强化学习的动态区块链分片系统

SkyChain 一、文献简介二、引言及重要信息2.1 研究背景2.2 研究目的和意义2.3 文献的创新点 三、研究内容3.1模型3.2自适应分类账协议3.2.1状态块创建3.2.2合并过程3.2.3拆分过程 3.3评价框架3.3.1性能3.3.1.1共识延迟3.3.1.2重新分片延迟3.3.1.3处理事务数3.3.1.4 约束 3.3.2 …

使用RobotFramework编写BDD代码

背景 行为驱动开发&#xff08;Behavior Driven Development&#xff09;即BDD&#xff0c;是一种敏捷开发方法&#xff0c;通常应用在自动化测试中&#xff0c;或者也可称为行为驱动测试。通过使用自然描述语言确定自动化脚本&#xff0c;通过这种方式&#xff0c;能够大大促…

EI级 | Matlab实现TCN-BiLSTM-Multihead-Attention多头注意力机制多变量时间序列预测

EI级 | Matlab实现TCN-BiLSTM-Multihead-Attention多头注意力机制多变量时间序列预测 目录 EI级 | Matlab实现TCN-BiLSTM-Multihead-Attention多头注意力机制多变量时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.【EI级】Matlab实现TCN-BiLSTM-Multihead-…

Openai通用特定领域的智能语音小助手

无穷尽的Q&A 钉钉...钉钉... 双双同学刚到工位,报销答疑群的消息就万马纷沓而来。她只能咧嘴无奈的摇摇头。水都还没有喝一口就开始“人工智能”的去回复。原本很阳光心情开始蒙上一层薄薄阴影。在这无休无止的Q&A中&#xff0c;就算你对工作有磐石一般强硬&#xff0…

XXL-Job详解(一):组件架构

目录 XXL-Job特性系统组成架构图调度模块剖析任务 “运行模式” 剖析执行器 XXL-Job XXL-JOB是一个分布式任务调度平台&#xff0c;其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线&#xff0c;开箱即用。 特性 1、简单&#…

探究Kafka原理-6.CAP理论实践

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码、Kafka原理&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44…

解决keil右键Go To Definition跳转不过去的问题

解决&#xff1a; 在魔法棒中如图所示打上√

Pycharm新手开发指南

文章目录 前言一、常用功能介绍二、常用高效pycharm使用方法关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战案例③Python小游戏源码五、面试资料六、Python兼职渠道 前言…

京东秒杀之秒杀实现

1 登录判断 用户在未登录状态下可以查看商品列别以及秒杀商品详情&#xff0c;但不可以在未登录状态进行秒杀商品的操作&#xff0c;当用户点击开始秒杀时&#xff0c;进行登陆验证 <!DOCTYPE html> <head><title>商品详情</title><meta http-eq…

习题补充整理

目录 一、自己封装response 二、在响应头中放数据 HttpResponse redirect ​编辑 render JsonResponse 三、函数和方法区别 ----》绑定方法区别 四、上传图片和开启media访问 五、页面静态化(解决访问率高的问题) 一、自己封装response 第一步&#xff0c;在app01下…

使用vue脚手架创建vue项目

Vue是一个流行的前端框架&#xff0c;可以用简洁的语法和组件化的思想开发单页面应用。Vue脚手架是一个官方提供的命令行工具&#xff0c;它可以帮你快速搭建和配置vue项目的基本结构和依赖。 本文介绍如何使用vue脚手架创建一个vue2项目&#xff0c;并选择一些常用的功能和插件…