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

前面几篇我们都讲解了很多有关 winlogon 挂钩的事情。拦截系统热键的非驱动方式是比较复杂的。本节就复现《禁止Ctrl+Alt+Del、Win+L等任意系统热键》一文中的方法四,实现拦截 Ctrl + Alt + Del 等热键。其实通过 heiheiabcd 给出的方法从 WMsgKMessageHandler 入手并不是最简单的方式。其他方法比如:还可以从 RPC 调用入手,有一个只要修改 RPC Asnyc 过程中 Invoke 函数派发时的参数即可完成的操作,而派发过程可以通过特殊大小的内存初始化(memset 函数)来监听,并不需要复杂的定位机制,不过分析时花了一点功夫。

屏蔽热键系列将只谈从 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. Hook RPC 实现系统热键屏蔽(二)[暂未发布]
  4. Windows 拦截系统睡眠

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

一、原理概述

Winlogon 进程通过 SignalManagerWaitForSignal 函数循环等待系统快捷键。最终通过 WMsgKMessageHandler 和  WMsgMessageHandler 等回调来实现 RPC 消息的处理。

WMsgKMessageHandler 主要负责处理由 Csrss 注册的部分 winlogon 进程的快捷键,如:Ctrl + Shift + Esc。此外,他还负责用户层发起的会话注销请求的处理。

WMsgMessageHandler 主要负责处理用户层发起的大部分会话请求的处理(不包括注销),如:重启、关机、切换用户。此外,它还联合 AppInfo Service 服务,一同处理进程的提升管理员权限会话。

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

int __fastcall WMsgKMessageHandler(
    UINT                 uMsgCSessionKey,    // unsigned int
    UINT                 uMsgWLGenericKey,
    PRPC_ASYNC_STATE     pAsync,
    int *                pReserved
)

2024/2/3 更新:纠正参数解析的类型,问题不大,在第二篇中解释原因。

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
};

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

四、更好的定位方案(通过 WMsgClntInitialize 函数)

此部分于:2024.04.13 更新,通过 WMsgClntInitialize 间接定位的思路最早在 “Windows 程序文件特征码识别定位方法” 一文中提出。此方法有助于解决利用目标函数中的代码特征进行定位容易失效的问题,因为 WMsgClntInitialize 函数在历史上很少改动,并且二进制特征明显。

对于 winlogon.exe 可执行模块,通过 IDA 我们知道要想拦截到该进程的消息回调,可以通过找到 WMsgClntInitialize 作为切入点。这个函数的反汇编如下:

int64_t __fastcall WMsgClntInitialize(WLSM_GLOBAL_CONTEXT *WSMGlobalContext, int IsPresentKey)
{
  int64_t WMsgHandlerList[9]; // [rsp+30h] [rbp-48h] BYREF
 
  memset(WMsgHandlerList, 0, 0x40);
  if ( !IsPresentKey )
    return StartWMsgServer();
  WMsgHandlerList[0] = (int64_t)WMsgMessageHandler;
  WMsgHandlerList[1] = (int64_t)WMsgKMessageHandler;
  WMsgHandlerList[5] = (int64_t)WMsgNotifyHandler;
  WMsgHandlerList[2] = (int64_t)WMsgPSPHandler;
  WMsgHandlerList[3] = (int64_t)WMsgReconnectionUpdateHandler;
  WMsgHandlerList[4] = (int64_t)WMsgGetSwitchUserLogonInfoHandler;
  RegisterWMsgServer(WMsgHandlerList);
  return StartWMsgKServer(*WSMGlobalContext + 0xCC);
}

 他有什么特征呢?就是这里的函数指针表,它的汇编非常有规律:

这里通过将栈上的函数指针通过 lea 指令传入地址到 rax 上,再通过 mov 数据传输指令来准备后面函数的调用需要用到的参数。

  • lea rax, 0x0 对应机器码序列是: { 0x48, 0x8D, 0x05, 0, 0, 0, 0 };
  • mov  [rsp + 0x0], rax 对应机器码序列是: { 0x48, 0x89, 0x44, 0x24, 0 }

你可以观察出这里的代码可以总结为一个不变的签名特征:

{ 0x48, 0x8D, 0x05, 0, 0, 0, 0, 0x48, 0x89, 0x44, 0x24, 0 }   X~3

注意:0 表示可变值, X~3 表示重复次数不小于 3 次。

因为 winlogon.exe 的代码编写习惯,所有的回调函数都在这里注册。虽然单次匹配特征码在全文中存在多处,但是这样的连续重复匹配特征码超过 3 次的有且仅有一处。

所以,当使用少于 15 字节且没有明显对称特征的模式串进行定位的时候,可以优先使用 BF 算法。

下面代码是暴力搜索特征码的一个模板,算法支持设置只返回第一个结果:

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

// 通过 lea 传地址指令计算函数的实际地址
uintptr_t GetFunctionAddress(uintptr_t leaAddress) {
    // 从给定地址处读取偏移量数值
    int32_t offset = 0;
    ReadProcessMemory(GetCurrentProcess(), reinterpret_cast<LPCVOID>(leaAddress + 3), &offset, sizeof(offset), nullptr);

    // 计算函数地址
    uintptr_t signedOffset = static_cast<uintptr_t>(static_cast<int32_t>(offset));
    return leaAddress + 7 + signedOffset; // x64 lea 寄存器操作数指令长度为 7 字节
}


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("模块基址:%I64X.\n", reinterpret_cast<uint64_t>(hModule));
    printf("模块大小:%I64d Bytes.\n", moduleSize);


    if (moduleSize == 0)
    {
        printf("Failed to get module information.\n");
        FreeLibrary(hModule);
        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]);

        uintptr_t signedOffset = GetFunctionAddress(reinterpret_cast<uint64_t>(moduleBase) + vcMachList[i]);

        printf("第一个函数实际地址:%I64X\n", signedOffset);


        uintptr_t signedOffset2 = GetFunctionAddress(reinterpret_cast<uint64_t>(moduleBase) + vcMachList[i] + 0xc);

        printf("第二个函数实际地址:%I64X\n", signedOffset2);

    }

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

    FreeLibrary(hModule);
    return cwEnd - cwStart;
}

int main() {
    // 暴力算法
    const wchar_t* moduleName = L"C:\\Windows\\System32\\winlogon.exe";
    BYTE   pattern[] =
    { 0x48u, 0x8Du, 0x05u, 0, 0, 0, 0, 0x48u, 0x89u, 0x44u, 0x24u , 0 };
    SIZE_T patternSize = 12;
    DWORD dwRepeat = 3, dwSelect = 1;
    int TimeCost = 0;
    TimeCost = BFTracePatternInModule(moduleName, pattern, patternSize, dwRepeat, dwSelect);
    printf("算法耗时:%d ms.\n", TimeCost);
    return 0;
}

计算结果如下:

计算结果 WMsgClntInitialize

首先通过 WMsgClntInitialize 的特征找到 lea 传地址的位置:0x7FF7AF3B392C。然后,根据 lea 在 x64 寄存器上传递地址时指令长度为 7 字节,以及这里的 mov 为 5 字节。可以通过偏移量计算函数实际的入口地址。计算公式如下:

函数实际地址 = lea 指令的下一条指令的地址 + lea指令最后4字节的偏移量

这段汇编代码是 WMsg 回调函数指针表的初始化赋值过程,后一个 lea 的地址可以由前一个 lea 的地址加上 12 字节来计算。(注意:小端字节序以及负数的机器码为补码的问题)

经验证,结果正确。

WMsgMessageHandler 地址:

WMsgKMessageHandler 的地址:

而前文的特征码定位方法已经在最新系统上失效,原因是指令逻辑发生了变化:

在选取的特征区间中, test 指令现在变为源操作数与目的操作数 byte ptr[rcx + ??] 取出的值进行比较而不是直接的 test [rcx + ??], ??。这导致了搜索失败。

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

五、完整纠正目前已知的定位问题

后来我发现 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;
}

六、补充更新&已知问题

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

原始代码节:

for (DWORD j = 0; j < numEntries; ++j) {
                if (dwLastEndAddress - pdata[j].EndAddress <= 15) // 存在缺陷
                {
                    memcpy_s(dwRealAddress, sizeof(DWORD), &pdata[j].EndAddress, sizeof(DWORD));
                    return pdata[j + 1].BeginAddress;
                }
            }

修改后的代码节(已经在正文中更新):

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;
                }
            }

2024/04/13 更新:

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

2)从早期文章整理出以 WMsgClntInitialize 函数作为出发点进行定位的方法,并补充了使用调试符号计算实际地址的思路。

已知兼容性问题:IDA 是如何确定 Collapsed Function 范围的?我目前用的是模糊匹配。我猜测 IDA 先通过交叉引用确定上下文函数位置,随后处理 idata 节段表,来分析必要的折叠字节。

其他 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.

  • 16
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

涟幽516

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

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

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

打赏作者

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

抵扣说明:

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

余额充值