[温馨提示]:为了适应我们最新的开发策略,已经对此系列文档的内容进行了部分调整。如果您对技术原理的理解有什么建议,请联系我。
前面几篇我们都讲解了很多有关 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 |
相关系列文章列表:
本文链接: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;
}
五、更好的定位方案
通过我最新发现的一个函数可以用于定位,比上文介绍的更好。下面展示不同版本系统上,使用全新的函数进行定位的效果:

Win8.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.