实现屏蔽 Ctrl + Alt + Del 、Ctrl + Shift + Esc 等热键(一)

[温馨提示]:为了适应我们最新的开发策略,已经对此系列文档的内容进行了部分调整。如果您对技术原理的理解有什么建议,请联系我。

前面几篇我们都讲解了很多有关 winlogon 挂钩的事情。拦截系统热键的非驱动方式是比较复杂的。本节就复现《禁止Ctrl+Alt+Del、Win+L等任意系统热键》一文中的方法四,实现拦截 Ctrl + Alt + Del 等热键。其实通过 heiheiabcd 给出的方法从 WMsgKMessageHandler 入手并不是最简单的方式。


本系列文章目录:

屏蔽热键系列将只谈从 WMsgMessageHandler 切入的方法;Hook NDR-RPC 的系列(1-3篇文章)从 RPC 角度分析问题的解决方法。

编号文章标题ID 号
1实现屏蔽 Ctrl + Alt + Del 、Ctrl + Shift + Esc 等热键(一)135899525
2实现屏蔽 Ctrl + Alt + Del 、Ctrl + Shift + Esc 等热键(二)135980850

相关系列文章列表:

  1. 屏蔽系统热键/关机(上/中/下)​​​​​​
  2. Hook RPC 实现系统热键屏蔽(一)
  3. Windows 拦截系统睡眠

本文链接:https://blog.csdn.net/qq_59075481/article/details/135899525

一、原理概述

Winlogon 进程通过 SignalManagerWaitForSignal 函数循环等待系统中需要处理的一些登陆消息(如本文主要谈的系统快捷键,这里的消息是指使用 RPC 封送的信息)。最终通过 WMsgKMessageHandler 和  WMsgMessageHandler 等回调来实现 RPC 消息的处理。

WMsgKMessageHandler 函数的声明如下(与 WMsgMessageHandler 参数相同):

int __fastcall WMsgKMessageHandler(  // also WMsgMessageHandler
    UINT                 uMachineState,        // unsigned int
    UINT                 uMsgWLGenericKey,
    PRPC_ASYNC_STATE     pAsync,
    LPDWORD              lpStatus
)

第一个参数 uMsgCSessionKey 控制会话有关(CSession)的回调消息。

第二个参数 uMsgWLGenericKey 控制注册调用(WLGeneric)的回调消息,其中包含了对快捷键处理有关的函数。

这两个参数可以理解为窗口过程的 uMsg 参数,通过 switch...case 对消息进行处理。对于快捷键处理,大体上可以简化为如下伪代码过程:

switch(uMsgWLGenericKey) {
case Ctrl+Alt+Del:
{
    // 打开桌面安全选项();
}
break;
case Win+L:
{
    // 锁定桌面();
}
break;
case Ctrl+Shift+Esc:
{
    // 打开任务管理器();
}
break;
case Win+P:
{
    // 切换窗口();
}
break;
default:
{
    // do something...
}

当 uMsgCSessionKey == 0x404 时,才处理 WLGeneric 消息:

原始方法:heiheiabcd 给出的方法是直接修改"case  ID"将每个 ID 改为很大的值。该方法比较适合调试模式进行拦截。

方法改进:挂钩该函数并且修改函数的参数使得,当 uMsgCSessionKey == 0x404 且uMsgWLGenericKey 为测试找到的特殊值时,拦截调用并返回 TRUE(1) ,可以终止调用。

具体的挂钩方法我将在第二篇分去分析,本篇从定位该函数入手。

警告:本文第二、三、四小节讲解的方法中选取的特征码不一定向后兼容性好。已知目前在最新发布版(10.0.22631.3447)上,原始的特征码定位方法已经失效!如果你已经遇到了该问题,可以联系我。

二、定位 WMsgKMessageXX 函数

这个函数由于开放窗口很大,函数调用非常复杂。所以选取特征码时候比较麻烦,我找了很久,目前确定的一种可行的方案是定位特殊指令法。这一段是对全局变量的解引用,和获取指针引用,使用了特殊的寄存器传递数据。并且 WMsgKMessageHandler 回调函数多次使用该方法使用 ETW 记录事件日志。

所以,考虑是否可以用该特征作为特征码,提取的特征数组如下:

{ 0x48u, 0x8Bu, 0x0Du, 0, 0, 0, 0, 0x49u, 0x3Bu, 0xCCu, 0x74u , 0, 0x44, 0x84, 0x79, 0x1C , 0x74 }

其中,0 表示通配符。

使用我在这篇文章:“程序特征码识别定位方法”,提出的方法即可完成搜索操作,这里以暴力搜索(winlogon.exe 文件版本 10.0.22621.3085)为例,搜索结果如下:

可以发现结果不止一处,不用担心,经过核对,匹配项均位于 WMsgKMessageHandler 回调函数的代码中。并且第一个匹配项位于函数入口点附近,这一点看 IDA 的伪代码/反汇编就可以看出:

所以,只需要匹配第一次搜索结果即可,代码如下:

#include <stdio.h>
#include <windows.h>
#include <vector>
#include <Psapi.h>
#include <time.h>

inline int BFTracePatternInModule(
    LPCWSTR moduleName, 
    PBYTE pattern, 
    SIZE_T patternSize, 
    DWORD dwRepeat, 
    DWORD dwSelect = 1
)
{
    if (pattern == 0 || moduleName == 0 || patternSize == 0 || dwRepeat <= 0)
    {
        return 0;
    }

    HMODULE hModule = LoadLibraryW(moduleName);
    if (hModule == nullptr) {
        printf("Failed to load module: %ws.\n", moduleName);
        return 0;
    }

    MODULEINFO moduleInfo;
    if (!GetModuleInformation(GetCurrentProcess(), hModule, &moduleInfo, sizeof(moduleInfo))) {
        printf("Failed to get module information.\n");
        FreeLibrary(hModule);
        return 0;
    }

    std::vector<uint64_t> vcMachList;
    BYTE* moduleBase = reinterpret_cast<BYTE*>(hModule);
    SIZE_T moduleSize = moduleInfo.SizeOfImage;

    printf("模块基址:0x%I64X.\n", reinterpret_cast<uint64_t>(hModule));
    printf("模块大小:%I64d Bytes.\n", moduleSize);


    // 模块大小不能为 0
    if (moduleSize == 0)
    {
        printf("Failed to get module information.\n");
        NtUnMapViewModule(&MapViewInfo);
        return 0;
    }

    uint64_t thisMatch = 0;
    DWORD SelectCase = (dwSelect < 256) && dwSelect ? dwSelect: 256; // 最大结果记录次数
    SIZE_T MatchLimit = patternSize * dwRepeat - 1;  // 连续重复匹配次数限制
    int cwStart = clock();

    if (dwRepeat == 1)
    {
        for (SIZE_T i = 0; i < moduleSize - patternSize; i++)
        {
            thisMatch = 0;
            SIZE_T j = 0;

            for (j; j < patternSize - 1; j++)
            {
                if (moduleBase[i + j] != pattern[j] && pattern[j] != 0u)
                {
                    break;
                }
            }

            if (j == patternSize - 1)
            {
                if (moduleBase[i + j] == pattern[j] || pattern[j] == 0u)
                {
                    thisMatch = i;
                    SelectCase--;
                    vcMachList.push_back(thisMatch);
                    if(!SelectCase) break;
                }
            }
        }
    }
    else {
        for (SIZE_T i = 0; i < moduleSize - MatchLimit - 1; i++)
        {
            thisMatch = 0;
            SIZE_T j = 0;

            for (j; j < MatchLimit; j++)
            {
                if (moduleBase[i + j] != pattern[j % patternSize] && pattern[j % patternSize] != 0u)
                {
                    break;
                }
            }

            if (j == MatchLimit)
            {
                if (moduleBase[i + MatchLimit] == pattern[patternSize - 1] || pattern[patternSize - 1] == 0u)
                {
                    thisMatch = i;
                    SelectCase--;
                    vcMachList.push_back(thisMatch);
                    if (!SelectCase) break;
                }
            }
        }
    }

    int cwEnd = clock();
    
    for (SIZE_T i = 0; i < vcMachList.size(); i++)
    {
        printf("匹配到模式字符串位于偏移: [0x%I64X] 处,动态地址:[0x%I64X]。\n", 
            vcMachList[i], reinterpret_cast<uint64_t>(moduleBase) + vcMachList[i]);
    }

    if (vcMachList.size() == 0)
    {
        printf("No Found.\n");
    }

    FreeLibrary(hModule);
    return cwEnd - cwStart;
}


int main() {
    // 暴力算法
    const wchar_t* moduleName = L"winlogon.exe";
    // ETW Trace 特征码
    BYTE   pattern[] =
    {
        0x48u, 0x8Bu, 0x0Du, 0, 0, 0, 0, 0x49u, 
        0x3Bu, 0xCCu, 0x74u , 0, 0x44, 0x84,
        0x79, 0x1C , 0x74
    };
    SIZE_T patternSize = 17; 
    DWORD dwRepeat = 1, dwSelect = 1; // 匹配第一次完整匹配,不重复匹配
    int TimeCost = 0;
    TimeCost = BFTracePatternInModule(moduleName, 
        pattern, patternSize, dwRepeat, dwSelect);
    printf("算法耗时:%d ms.\n", TimeCost);
    return 0;
}

测试结果如图,耗时在微秒级别,当然可以用我那篇文章里面给出来的更好的匹配算法的代码:

随后,我们只需要向上搜索 0xCCCCCCCC 的 BreakSwap 片段或者 0x90909090 的 Hot Patch 片段,来确定函数入口点。

但是,比较麻烦的就是 Win 11 上和之前版本还有些不同,位点之后插入了一段不明作用的数值,应该也是属于 BreakSwap 里面的,有大神指点不?(已解决:见本文第四部分)

我只能想到再通过入口特征进一步定位了:入口的 mov rsp --> 48 89 特征。 一定有更好的方法。

简单编写的测试代码如下:

#include <stdio.h>
#include <windows.h>
#include <vector>
#include <Psapi.h>
#include <time.h>

inline int BFTracePatternInModule(
    LPCWSTR moduleName, 
    PBYTE pattern, 
    SIZE_T patternSize, 
    DWORD dwRepeat, 
    DWORD dwSelect = 1
)
{
    if (pattern == 0 || moduleName == 0 || patternSize == 0 || dwRepeat <= 0)
    {
        return 0;
    }

    HMODULE hModule = LoadLibraryW(moduleName);
    if (hModule == nullptr) {
        printf("Failed to load module: %ws.\n", moduleName);
        return 0;
    }

    MODULEINFO moduleInfo;
    if (!GetModuleInformation(GetCurrentProcess(), hModule, &moduleInfo, sizeof(moduleInfo))) {
        printf("Failed to get module information.\n");
        FreeLibrary(hModule);
        return 0;
    }

    std::vector<uint64_t> vcMachList;
    BYTE* moduleBase = reinterpret_cast<BYTE*>(hModule);
    SIZE_T moduleSize = moduleInfo.SizeOfImage;
    

    printf("模块基址:0x%I64X.\n", reinterpret_cast<uint64_t>(hModule));
    printf("模块大小:%I64d Bytes.\n", moduleSize);


    // 模块大小不能为 0
    if (moduleSize == 0)
    {
        printf("Failed to get module information.\n");
        NtUnMapViewModule(&MapViewInfo);
        return 0;
    }

    uint64_t thisMatch = 0;
    DWORD SelectCase = (dwSelect < 256) && dwSelect ? dwSelect: 256; // 最大结果记录次数
    SIZE_T MatchLimit = patternSize * dwRepeat - 1;  // 连续重复匹配次数限制
    int cwStart = clock();

    if (dwRepeat == 1)
    {
        for (SIZE_T i = 0; i < moduleSize - patternSize; i++)
        {
            thisMatch = 0;
            SIZE_T j = 0;

            for (j; j < patternSize - 1; j++)
            {
                if (moduleBase[i + j] != pattern[j] && pattern[j] != 0u)
                {
                    break;
                }
            }

            if (j == patternSize - 1)
            {
                if (moduleBase[i + j] == pattern[j] || pattern[j] == 0u)
                {
                    thisMatch = i;
                    SelectCase--;
                    vcMachList.push_back(thisMatch);
                    if(!SelectCase) break;
                }
            }
        }
    }
    else {
        for (SIZE_T i = 0; i < moduleSize - MatchLimit - 1; i++)
        {
            thisMatch = 0;
            SIZE_T j = 0;

            for (j; j < MatchLimit; j++)
            {
                if (moduleBase[i + j] != pattern[j % patternSize] && pattern[j % patternSize] != 0u)
                {
                    break;
                }
            }

            if (j == MatchLimit)
            {
                if (moduleBase[i + MatchLimit] == pattern[patternSize - 1] || pattern[patternSize - 1] == 0u)
                {
                    thisMatch = i;
                    SelectCase--;
                    vcMachList.push_back(thisMatch);
                    if (!SelectCase) break;
                }
            }
        }
    }

    /*
    * 增加:向上搜索 HotPatch 代码段
    * 
    */
    uint64_t uintPostn = NULL; // 存储偏移量

    for (SIZE_T j = vcMachList[0] - 1; j > vcMachList[0] - 1000; j--)
    {
        if (moduleBase[j] == 0xCC
            && moduleBase[j - 1] == 0xCC
            && moduleBase[j - 2] == 0xCC
            && moduleBase[j - 3] == 0xCC   // HotPatch 特征
            )
        {
            for (j; j < vcMachList[0]; j++)   // 入口点特征
            {
                if (moduleBase[j] == 0x48 && moduleBase[j + 1] == 0x89)
                {
                    uintPostn = j;  // 如果找到
                    break;
                }
            }
            break;
        }

        if (moduleBase[j] == 0x90
            && moduleBase[j - 1] == 0x90
            && moduleBase[j - 2] == 0x90
            && moduleBase[j - 3] == 0x90
            )
        {
            for (j; j < vcMachList[0]; j++)
            {
                if (moduleBase[j] == 0x48 && moduleBase[j + 1] == 0x89)
                {
                    uintPostn = j;  // 如果找到
                    break;
                }
            }
            break;
        }
    }

    if (uintPostn)
    {
        printf("匹配到函数入口点位于偏移: [0x%I64X] 处,动态地址:[0x%I64X]。\n",
            uintPostn, reinterpret_cast<uint64_t>(moduleBase) + uintPostn);
    }

    for (SIZE_T i = 1; i < vcMachList.size(); i++)
    {
        uintPostn = NULL; // 归零
        for (SIZE_T j = vcMachList[i] - 1; j > vcMachList[i - 1] - 1; j--)
        {
            if (moduleBase[j] == 0xCC
                && moduleBase[j - 1] == 0xCC
                && moduleBase[j - 2] == 0xCC
                && moduleBase[j - 3] == 0xCC
                )
            {
                for (j; j < vcMachList[i]; j++)   // 入口点特征
                {
                    if (moduleBase[j] == 0x48 && moduleBase[j + 1] == 0x89)
                    {
                        uintPostn = j;  // 如果找到
                        break;
                    }
                }
                break;
            }

            if (moduleBase[j] == 0x90
                && moduleBase[j - 1] == 0x90
                && moduleBase[j - 2] == 0x90
                && moduleBase[j - 3] == 0x90
                )
            {
                for (j; j < vcMachList[i]; j++)   // 入口点特征
                {
                    if (moduleBase[j] == 0x48 && moduleBase[j + 1] == 0x89)
                    {
                        uintPostn = j;  // 如果找到
                        break;
                    }
                }
                break;
            }
        }

        if (uintPostn)
        {
            printf("匹配到函数入口点位于偏移: [0x%I64X] 处,动态地址:[0x%I64X]。\n",
                uintPostn, reinterpret_cast<uint64_t>(moduleBase) + uintPostn);
        }

    }

    int cwEnd = clock();

    //for (SIZE_T i = 0; i < vcMachList.size(); i++)
    //{
        //printf("匹配到模式字符串位于偏移: [0x%I64X] 处,动态地址:[0x%I64X]。\n", 
            //vcMachList[i], reinterpret_cast<uint64_t>(moduleBase) + vcMachList[i]);
    //}

    if (vcMachList.size() == 0)
    {
        printf("No Found.\n");
    }
    
    FreeLibrary(hModule);
    return cwEnd - cwStart;
}


int main() {
    // 暴力算法
    const wchar_t* moduleName = L"winlogon.exe";
    // ETW Trace 特征码
    BYTE   pattern[] =
    {
        0x48u, 0x8Bu, 0x0Du, 0, 0, 0, 0, 0x49u, 
        0x3Bu, 0xCCu, 0x74u , 0, 0x44, 0x84,
        0x79, 0x1C , 0x74
    };
    SIZE_T patternSize = 17; 
    DWORD dwRepeat = 1, dwSelect = 1; // 匹配第一次完整匹配,不重复匹配
    int TimeCost = 0;
    TimeCost = BFTracePatternInModule(moduleName, 
        pattern, patternSize, dwRepeat, dwSelect);
    printf("算法耗时:%d ms.\n", TimeCost);
    return 0;
}

运行结果如图: 

和 IDA 比对:

结果正确。

定位到了之后我们使用 Hook 就方便了,方法将在整理好后于下一篇继续讨论。

三、定位 WMsgMessageXX(没K) 函数

定位 WMsgMessageHandler 回调函数时,你将遇到新的问题——函数代码节在地址上呈现分散分布特征。因为我们的目的是需要在调用时立即拦截,所以最好的方法是使用入口附近的特征来定位。

对比 Win7、Win8、Win10/11,我最终选取了下图展示的入口点特征:

为什么要选取这个特征呢?依然是因为这段特征很明显,但又为了争取对最大范围的版本兼容,所以里面有很多通配符,这导致选区的区段很小,错误匹配率可能上升。目前在测试的版本中发现 Win8 使用该特征码会同时定位到 WMsgKMessageHandler 和 WMsgMessageHandler 回调函数回调函数,并且先定位到 WMsgKMessageHandler 回调;而其他版本只能优先匹配 WMsgMessageHandler 回调,看上去兼容性还好。为了尽快测试并完成本系列文章(消耗我太多时间了),就暂时使用这一个特征码,但建议是选取其他特征码或者多特征码来验证。

特征码数组(0 解释为通配符)如下所示:

BYTE   pattern2[21] =
{
    0x8Bu, 0x0,   0x8Bu, 0x0,
    0x48,  0x8Bu, 0x0Du, 0x0,
    0x0,   0x0,   0x0,   0x0,
    0x8D,  0x0,   0x0,   0x0,
    0x0,   0x0,   0x41u, 0x0, 0x05u
};

测试过程类似上文,就不再重复解释了。

四、解决上文提到的定位问题

后来我发现 CC (INT3 软件断点)后面的数值是什么了,因为 winlogon 包含异常处理函数表,每一个内部函数都有异常处理信息,这很好用。

在目标函数之前,IDA 通过交叉引用(XREF)解析出了上一个函数的异常处理表地址

如下所示:

根据提供的 .pdata 节段中的数据,00007FF750263E28 是一个指向 RUNTIME_FUNCTION 结构的虚拟地址。RUNTIME_FUNCTION 结构通常用于异常处理和函数调用的信息,它包含了一系列函数范围和异常处理相关的信息。

在这个结构中,字段的解释如下:

00 03 96 20: 表示函数的开始地址相对于模块基址的偏移量,即函数的 RVA(相对虚拟地址)。
00 03 9A 54: 表示函数的结束地址相对于模块基址的偏移量,即函数的结束 RVA。
F6 F0: 表示异常处理信息的相对虚拟地址。
这些偏移量和地址都是相对于模块基址而言的,因此需要加上模块的基址才能得到实际的虚拟地址。通常情况下,IDA 可以通过分析二进制文件的导入表和段表等信息来确定模块的基址,并结合这些偏移量来计算实际的虚拟地址。

在这个特定的结构中,RUNTIME_FUNCTION 结构的字段通常与异常处理相关,其中包括函数的范围和异常处理的相关信息。这些信息在程序执行时由操作系统的异常处理机制使用,用于确定如何处理函数内部的异常。

根据正文第一个代码 CCCCCCCC 定位到的地址第一个 CC 的地址就是上一个函数的函数结束地址。通过遍历查找 pdata 就可以找到 WMsgKMessageHandler 的开始和结束位置,这样不管 WMsgKMessageHandler 入口是不是 mov rsp 啥的都可以准确定位了。

遍历 winlogon 模块的 pdata 段的代码如下:

#include <iostream>
#include <iostream>
#include <Windows.h>

int main() {
    const WCHAR filename[] = L"C:\\Windows\\System32\\winlogon.exe"; // winlogon 文件路径

    HANDLE hFile = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        std::cerr << "Failed to open file" << std::endl;
        return 1;
    }

    HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
    if (hMapping == NULL) {
        CloseHandle(hFile);
        std::cerr << "Failed to create file mapping" << std::endl;
        return 1;
    }

    LPVOID baseAddress = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
    if (baseAddress == NULL) {
        CloseHandle(hMapping);
        CloseHandle(hFile);
        std::cerr << "Failed to map view of file" << std::endl;
        return 1;
    }

    PIMAGE_DOS_HEADER dosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(baseAddress);
    if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
        UnmapViewOfFile(baseAddress);
        CloseHandle(hMapping);
        CloseHandle(hFile);
        std::cerr << "Not a valid DOS executable" << std::endl;
        return 1;
    }

    PIMAGE_NT_HEADERS ntHeaders = reinterpret_cast<PIMAGE_NT_HEADERS>(reinterpret_cast<BYTE*>(baseAddress) + dosHeader->e_lfanew);
    if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) {
        UnmapViewOfFile(baseAddress);
        CloseHandle(hMapping);
        CloseHandle(hFile);
        std::cerr << "Not a valid NT executable" << std::endl;
        return 1;
    }

    PIMAGE_SECTION_HEADER sectionHeader = IMAGE_FIRST_SECTION(ntHeaders);
    for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; ++i) {
        if (strcmp(reinterpret_cast<char*>(sectionHeader[i].Name), ".pdata") == 0) {
            DWORD pdataVirtualAddress = sectionHeader[i].VirtualAddress;
            DWORD pdataSize = sectionHeader[i].SizeOfRawData;

            DWORD pdataOffset = pdataVirtualAddress - sectionHeader[i].VirtualAddress + sectionHeader[i].PointerToRawData;
            PIMAGE_RUNTIME_FUNCTION_ENTRY pdata = reinterpret_cast<PIMAGE_RUNTIME_FUNCTION_ENTRY>(reinterpret_cast<BYTE*>(baseAddress) + pdataOffset);

            DWORD numEntries = pdataSize / sizeof(IMAGE_RUNTIME_FUNCTION_ENTRY);
            for (DWORD j = 0; j < numEntries; ++j) {
                std::cout << "Function " << j << ": Start RVA: 0x" << std::hex << pdata[j].BeginAddress
                    << ", End RVA: 0x" << std::hex << pdata[j].EndAddress
                    << ", Unwind Info RVA: 0x" << std::hex << pdata[j].UnwindInfoAddress << std::endl;
            }

            break; // 找到 .pdata 节段后退出循环
        }
    }

    UnmapViewOfFile(baseAddress);
    CloseHandle(hMapping);
    CloseHandle(hFile);

    return 0;
}

运行结果如下:

计算结果一致,把这个功能整合到搜索代码中即可。

整合时候还有需要注意的事情,那就是代码节在运行时会动态重定位的,所以用 LoadLibrary 加载的模块,遍历 pdata 节时会出现问题,偏移会出错。所以索性将所有代码都改为内存映射方式,在这个修改的过程中,我需要总结几点。

(1)MapView 对代码节只是映射到内存,并不会修正入口点偏移。修正偏移可以通过节区表信息完成。

将遍历完成后找到的地址 - text 节的 PointerToRawData + text 节的 VirtualAddress,就可以得到模块加载时修正映射的虚拟地址。代码如下:

// 依据 text 节映射信息,修正地址
    DWORD vcFixedOffest = 0;
    DWORD dwRealAddress = 0;
    PIMAGE_SECTION_HEADER sectionHeader = IMAGE_FIRST_SECTION(ntHeader);
    for (int i = 0; i < ntHeader->FileHeader.NumberOfSections; ++i) {
        if (strcmp(reinterpret_cast<char*>(sectionHeader[i].Name), ".text") == 0) {
            vcFixedOffest = baseEnd
                - sectionHeader[i].PointerToRawData
                + sectionHeader[i].VirtualAddress;
            break;
        }
    }

(2) 在 Win 8 测试版本的汇编上,我们观察到一个特殊点,那就是 COLLAPSED FUNCTION(折叠函数)。这是什么?

折叠函数示例[折叠]

如图所示,由于编译器优化问题,有些字节可能被废弃,这就是 IDA 解析出的折叠函数段。而这个折叠函数非常烦人,这使得我们依赖于 0x90 或者 0xCC 特征定位上一个函数的结束位置的过程变得复杂。

折叠函数示例[展开]

我想到的方法是,对最近匹配的连续 HotPatch 或 BreakSwap 区段只要找到他开始的那个字节即可,这里是 0x465E,显然这不是上一个函数的结束位置,但是我们可以设置一个误差范围的阈值限制(比如误差在 15 字节内),这允许 pdata 遍历接口函数能够模糊匹配结束位置。

(3)第三个就是优化一下正文代码中定位最接近特征码起始位置的连续 HotPatch 或 BreakSwap 区段如何确定的算法。只需要两个循环,首先确定连续,随后第二个循环找第一个不匹配的位置。

代码如下:

    /*
    * 关键算法:向上搜索跳过 HotPatch 以及 BreakSwap 代码段
    * Created By LianYou 516 at 2024.01.29.
    */
    DWORD baseStart = vcMachOffest; // 存储偏移量
    DWORD basePatch = vcMachOffest - 1;
    DWORD baseCCByte = 0, baseNopByte = 0;
    DWORD baseEnd = 0;        // 上下文的偏移
    DWORD pDataFunEntry = 0;  // 正真的入口点偏移
    const uint8_t ccByte = 0xCC;
    const uint8_t nopByte = 0x90;

    // 第一个循环找到连续的 0xCC 或者 0x90 首次出现位置
    while (baseStart - basePatch <= 0x3E8u) // 搜索域限界条件
    {
        // 检测到连续的 0xCC 标记偏移量到 baseCCByte 变量
        if (lpBaseAddress[basePatch] == ccByte
            && lpBaseAddress[basePatch - 1] == ccByte)
        {
            baseCCByte = basePatch - 1;
            break;
        }
        // 检测到连续的 0x90 标记偏移量到 baseNopByte 变量
        if (lpBaseAddress[basePatch] == nopByte &&
            lpBaseAddress[basePatch - 1] == nopByte)
        {
            baseNopByte = basePatch - 1;
            break;
        }

        --basePatch;// 递减循环
        if (basePatch == 0) break; // 防止数组越界
    }

    // 判断是否找到第一个连续 0xCC 字节的位置
    if (baseCCByte != 0)
    {
        // 循环检索 0xCC 软件断点指令,实现越过入口点软件断点区域
        while (baseStart - baseCCByte <= 0x3E8u)
        {
            if (lpBaseAddress[baseCCByte] != ccByte)
            {
                baseEnd = baseCCByte + 1;  // 找到 0xCC 最早出现的位置(低地址)
                break;
            }
            if (baseCCByte == 0) break;
            --baseCCByte;
        }
    }
    
    // 判断是否找到第一个连续 0x90 字节的位置
    if (baseNopByte != 0)
    {
        // 循环检索 0x90 NOP 指令,实现越过入口点热补丁区域
        while (baseStart - baseNopByte <= 0x3E8u)
        {
            if (lpBaseAddress[baseNopByte] != nopByte)
            {
                baseEnd = baseNopByte + 1;  // 找到 0x90 最早出现的位置(低地址)
                break;
            }
            if (baseNopByte == 0) break;
            --baseNopByte;
        }
    }

(4)CreateFile 函数需要提供绝对路径

const WCHAR filename[] = L"C:\\Windows\\System32\\winlogon.exe"; // winlogon 文件路径 

最终实现效果如图所示,测试耗时(含内存映射过程)在 3ms 内。

完整的代码如下所示:

#include <stdio.h>
#include <windows.h>
#include <vector>
#include <Psapi.h>
#include <time.h>

typedef struct MAPVIEW_INFO_STRUCT
{
    HANDLE hFile = NULL;
    HANDLE hMapping = NULL;
    LPVOID lpBaseMapView = nullptr;
}MAPVIEW_INFO_STRUCT, *LPMAPVIEW_INFO_STRUCT;

// 映射模块到内存中
BYTE* NtMapViewofModule(LPCWSTR lpFileName, LPMAPVIEW_INFO_STRUCT lpMapViewInfo)
{
    HANDLE hFile = NULL;
    HANDLE hMapping = NULL;
    LPVOID lpBaseMapView = nullptr;
    PIMAGE_DOS_HEADER dosHeader = nullptr;
    PIMAGE_NT_HEADERS ntHeaders = nullptr;
    PBYTE BaseAddress = nullptr;

    hFile = CreateFileW(lpFileName, GENERIC_READ, 
        FILE_SHARE_READ, NULL, OPEN_EXISTING, 
        FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("Failed to open file.\n");
        return FALSE;
    }

    hMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
    if (hMapping == NULL) {
        printf("Failed to create file mapping.\n");
        CloseHandle(hFile);
        return FALSE;
    }

    lpBaseMapView = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
    if (lpBaseMapView == NULL) {
        printf("Failed to map view of file.\n");
        goto ErrorEndFunction;
    }

    dosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(lpBaseMapView);
    if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
        UnmapViewOfFile(lpBaseMapView);
        printf("Not a valid DOS executable.\n");
        goto ErrorEndFunction;
    }

    BaseAddress = reinterpret_cast<BYTE*>(lpBaseMapView);

    ntHeaders = reinterpret_cast<PIMAGE_NT_HEADERS>(
        BaseAddress + dosHeader->e_lfanew );
    if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) {
        UnmapViewOfFile(lpBaseMapView);
        printf("Not a valid NT executable.\n");
        goto ErrorEndFunction;
    }

    lpMapViewInfo->hFile = hFile;
    lpMapViewInfo->hMapping = hMapping;
    lpMapViewInfo->lpBaseMapView = lpBaseMapView;

    return BaseAddress;

ErrorEndFunction:
    
    CloseHandle(hMapping);
    CloseHandle(hFile);
    return FALSE;
}

// 释放映射到内存中的模块
BOOL NtUnMapViewModule(LPMAPVIEW_INFO_STRUCT lpMapViewInfo)
{
    if (lpMapViewInfo == nullptr)
    {
        return FALSE;
    }

    BOOL bResponse = FALSE;
    HANDLE hFile = lpMapViewInfo->hFile;
    HANDLE hMapping = lpMapViewInfo->hMapping;
    LPVOID lpBaseMapView = lpMapViewInfo->lpBaseMapView;

    if (hFile)
        bResponse = CloseHandle(hFile);

    if(hMapping)
        bResponse = CloseHandle(hMapping);

    if (lpBaseMapView)
        bResponse = UnmapViewOfFile(lpBaseMapView);

    lpMapViewInfo->hFile = NULL;
    lpMapViewInfo->hMapping = NULL;
    lpMapViewInfo->lpBaseMapView = nullptr;
    return bResponse;
}

// 遍历 pdata 节段信息,搜索指定函数结束地址,并返回下一个函数入口地址
DWORD TraversePDATASectionInfo(PBYTE lpBaseAddress, DWORD dwLastEndAddress, PDWORD dwRealAddress)
{
    if (dwLastEndAddress == 0) return 0;

    PIMAGE_DOS_HEADER dosHeader = 
        reinterpret_cast<PIMAGE_DOS_HEADER>(lpBaseAddress);
    PIMAGE_NT_HEADERS ntHeaders = 
        reinterpret_cast<PIMAGE_NT_HEADERS>(lpBaseAddress + dosHeader->e_lfanew);

    PIMAGE_SECTION_HEADER sectionHeader = IMAGE_FIRST_SECTION(ntHeaders);
    for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; ++i) {
        if (strcmp(reinterpret_cast<char*>(sectionHeader[i].Name), ".pdata") == 0) {
            DWORD pdataVirtualAddress = sectionHeader[i].VirtualAddress;
            DWORD pdataSize = sectionHeader[i].SizeOfRawData;
            DWORD pdataOffset = pdataVirtualAddress
                - sectionHeader[i].VirtualAddress
                + sectionHeader[i].PointerToRawData;
            PIMAGE_RUNTIME_FUNCTION_ENTRY pdata = 
                reinterpret_cast<PIMAGE_RUNTIME_FUNCTION_ENTRY>(lpBaseAddress + pdataOffset);
            DWORD numEntries = pdataSize / sizeof(IMAGE_RUNTIME_FUNCTION_ENTRY);
            for (DWORD j = 0; j < numEntries; ++j) {
                long piancha = static_cast<long>(dwLastEndAddress) - 
                    static_cast<long>(pdata[j].EndAddress); // 转为有符号数,以便于比较绝对值
                if (abs(piancha) <= 15) // 容错性,编译器优化可能存在折叠函数
                {
                    memcpy_s(dwRealAddress, sizeof(DWORD), &pdata[j].EndAddress, sizeof(DWORD));
                    return pdata[j + 1].BeginAddress;
                }
            }
            break;
        }
    }
    return FALSE;
}

// 暴力搜索函数
inline int BFTracePatternInModule(
    LPCWSTR moduleName, 
    PBYTE pattern, 
    SIZE_T patternSize
)
{
    if (pattern == 0 || moduleName == 0 || patternSize == 0)
    {
        printf("Error Invalid parameter.\n");
        return 0;
    }

    int cwStartTime = 0, cwEndTime = 0; // 计时器存储时间
    PBYTE lpBaseAddress = 0;
    MAPVIEW_INFO_STRUCT MapViewInfo = { 0 };

    // 将模块映射到内存中
    lpBaseAddress = NtMapViewofModule(moduleName, &MapViewInfo);
    if (lpBaseAddress == nullptr) {
        printf("Failed to load module: %ws.\n", moduleName);
        return 0;
    }

    PIMAGE_DOS_HEADER dosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(lpBaseAddress);

    PIMAGE_NT_HEADERS ntHeader = reinterpret_cast<PIMAGE_NT_HEADERS>(
        (reinterpret_cast<BYTE*>(lpBaseAddress)) + dosHeader->e_lfanew );

    // DWORD moduleSize = ntHeader->OptionalHeader.SizeOfImage;

    // 使用 VirtualQuery 获取映射的内存地址范围
    MEMORY_BASIC_INFORMATION mbsci = { 0 };
    SIZE_T result = VirtualQuery(lpBaseAddress, &mbsci,
        sizeof(MEMORY_BASIC_INFORMATION));

    SIZE_T mapViewSize = mbsci.RegionSize;

    printf("模块基址:0x%I64X.\n", reinterpret_cast<uint64_t>(lpBaseAddress));
    printf("模块大小:%ld Bytes.\n", mapViewSize);

     模块大小不能为 0
    //if (moduleSize == 0)
    //{
    //    printf("Failed to get module information.\n");
    //    NtUnMapViewModule(&MapViewInfo);
    //    return 0;
    //}

    // 检查返回信息
    if (mapViewSize == 0 || result == 0) {
        printf("Failed to get ViewMap information.\n");
        NtUnMapViewModule(&MapViewInfo);
        return 0;
    }

    DWORD vcMachOffest = 0; // 用于记录查找的特征码偏移
    cwStartTime = clock(); // 算法开始计时
    for (DWORD i = 0; i < mapViewSize - patternSize; i++)
    {
        vcMachOffest = 0;
        DWORD j = 0;

        for (j; j < patternSize - 1; j++)
        {
            if (lpBaseAddress[i + j] != pattern[j] && pattern[j] != 0u)
            {
                break;
            }
        }

        if (j == patternSize - 1)
        {
            if (lpBaseAddress[i + j] == pattern[j] || pattern[j] == 0u)
            {
                vcMachOffest = i;
                break;
            }
        }
    }
    
    if (vcMachOffest == 0)
    {
        printf("No Found.\n");
        cwEndTime = clock();
        NtUnMapViewModule(&MapViewInfo);
        return cwEndTime - cwStartTime;
    }

    /*
    * 关键算法:向上搜索跳过 HotPatch 以及 BreakSwap 代码段
    * Created By LianYou 516 at 2024.01.29.
    */
    DWORD baseStart = vcMachOffest; // 存储偏移量
    DWORD basePatch = vcMachOffest - 1;
    DWORD baseCCByte = 0, baseNopByte = 0;
    DWORD baseEnd = 0;        // 上下文的偏移
    DWORD pDataFunEntry = 0;  // 正真的入口点偏移
    const uint8_t ccByte = 0xCC;
    const uint8_t nopByte = 0x90;

    // 第一个循环找到连续的 0xCC 或者 0x90 首次出现位置
    while (baseStart - basePatch <= 0x3E8u) // 搜索域限界条件
    {
        // 检测到连续的 0xCC 标记偏移量到 baseCCByte 变量
        if (lpBaseAddress[basePatch] == ccByte
            && lpBaseAddress[basePatch - 1] == ccByte)
        {
            baseCCByte = basePatch - 1;
            break;
        }
        // 检测到连续的 0x90 标记偏移量到 baseNopByte 变量
        if (lpBaseAddress[basePatch] == nopByte &&
            lpBaseAddress[basePatch - 1] == nopByte)
        {
            baseNopByte = basePatch - 1;
            break;
        }

        --basePatch;// 递减循环
        if (basePatch == 0) break; // 防止数组越界
    }

    // 判断是否找到第一个连续 0xCC 字节的位置
    if (baseCCByte != 0)
    {
        // 循环检索 0xCC 软件断点指令,实现越过入口点软件断点区域
        while (baseStart - baseCCByte <= 0x3E8u)
        {
            if (lpBaseAddress[baseCCByte] != ccByte)
            {
                baseEnd = baseCCByte + 1;  // 找到 0xCC 最早出现的位置(低地址)
                break;
            }
            if (baseCCByte == 0) break;
            --baseCCByte;
        }
    }
    
    // 判断是否找到第一个连续 0x90 字节的位置
    if (baseNopByte != 0)
    {
        // 循环检索 0x90 NOP 指令,实现越过入口点热补丁区域
        while (baseStart - baseNopByte <= 0x3E8u)
        {
            if (lpBaseAddress[baseNopByte] != nopByte)
            {
                baseEnd = baseNopByte + 1;  // 找到 0x90 最早出现的位置(低地址)
                break;
            }
            if (baseNopByte == 0) break;
            --baseNopByte;
        }
    }

    printf("上级函数结束点映射地址(估计):[0x%lX]\n", baseEnd);
    // 依据 text 节映射信息,修正地址
    DWORD vcFixedOffest = 0;
    DWORD dwRealAddress = 0;
    PIMAGE_SECTION_HEADER sectionHeader = IMAGE_FIRST_SECTION(ntHeader);
    for (int i = 0; i < ntHeader->FileHeader.NumberOfSections; ++i) {
        if (strcmp(reinterpret_cast<char*>(sectionHeader[i].Name), ".text") == 0) {
            vcFixedOffest = baseEnd
                - sectionHeader[i].PointerToRawData
                + sectionHeader[i].VirtualAddress;
            break;
        }
    }

    printf("上级函数结束点修正地址(估计):[0x%lX]\n", vcFixedOffest);

    // 如果正确定位,则返回地址
    pDataFunEntry = TraversePDATASectionInfo(lpBaseAddress, vcFixedOffest, &dwRealAddress);
    if (pDataFunEntry != 0)
    {
        printf("上级函数结束点实际地址(匹配):[0x%lX]\n", dwRealAddress);
        printf("匹配到函数入口点位于偏移: [0x%lX] 处,动态地址:[0x%I64X]。\n",
            pDataFunEntry, reinterpret_cast<uint64_t>(lpBaseAddress) + pDataFunEntry);
    }
    else {
        printf("No Found.[0x%lX] \n", vcFixedOffest);
    }

    cwEndTime = clock();
    NtUnMapViewModule(&MapViewInfo);
    return cwEndTime - cwStartTime;
}


int main() {
    // 暴力算法
    const wchar_t* moduleName = L"C:\\Windows\\System32\\winlogon.exe"; // winlogon 文件完整路径
    // ETW Trace 特征码
    BYTE   pattern[] = 
    {
        0x48u, 0x8Bu, 0x0Du, 0, 0, 0, 0, 0x49u,
        0x3Bu, 0xCCu, 0x74u , 0, 0x44, 0x84,
        0x79, 0x1C , 0x74
    };
    SIZE_T patternSize = 17; 
    int TimeCost = 0;
    TimeCost = BFTracePatternInModule(moduleName, pattern, patternSize);
    printf("算法耗时:%d ms.\n", TimeCost);
    return 0;
}

五、更好的定位方案

通过我最新发现的一个函数可以用于定位,比上文介绍的更好。下面展示不同版本系统上,使用全新的函数进行定位的效果:

Win 7 系统上拦截此函数

Win8.1 系统拦截效果:

Win 8.1 系统上拦截此函数

注意:除了本文重点讲解的特征码搜索定位方法,还可以使用微软符号服务器下载 PDB 程序调试数据库文件,使用调试帮助 API 获取函数地址,只不过要联网下载符号数据。 

六、补充更新&已知问题

使用 LoadLibrary 时候,可以用原本模块大小获取方式。但使用映射模块的缓冲区时大小不等于模块理论计算的大小。需要使用官方要求的 VirtualQuery 函数获取视图的信息来计算映射地址的结束位置,否则可能出现越界访问问题。

2024/02/03 更新:已知在判断函数结束位置(pdata 分析)的模糊机制上,用绝对值是更稳定安全的做法,而不是简单作差,并且注意无符号数小减去大会出问题,所以先转为有符号的。

2024/04/13 更新:

1)修复存在的末尾字节越界访问和映射范围计算错误问题。

2)从早期文章整理出定位的方法,并补充了使用调试符号计算实际地址的思路。

其他 BUG:暂未知

总结

本文就复现《禁止Ctrl+Alt+Del、Win+L等任意系统热键》一文中的方法四,为了实现拦截 Ctrl + Alt + Del 等热键,首先讨论了如何定位 WMsgKMessageHandler 等关键函数。测试代码检测过 Win 8/10/11 x64 的部分版本系统,其他版本可能存在命中失败的现象,此时可以尝试修改两个阈值:其一是遍历连续热补丁位置时最大允许搜索附近 1000 字节,其二是 pdata 匹配过程存在两种可能造成失败情况,一方面模糊匹配的误差设置的是 15 字节之内,如果微软的编译器抽风出现很大范围的折叠函数算法就会命中失败,另一方面不清楚微软未来会不会删除 pdata 节,如果删除就需要找其他方法。


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_59075481/article/details/135899525

发布于:2024.01.28,更新于:2024.02.03, 2024.03.15, 2024.04.13, 2024.06.02.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

涟幽516

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值