前面几篇我们都讲解了很多有关 winlogon 挂钩的事情。拦截系统热键的非驱动方式是比较复杂的。本节就复现《禁止Ctrl+Alt+Del、Win+L等任意系统热键》一文中的方法四,实现拦截 Ctrl + Alt + Del 等热键。其实通过 heiheiabcd 给出的方法从 WMsgKMessageHandler 入手并不是最简单的方式。其他方法比如:还可以从 RPC 调用入手,有一个只要修改 RPC Asnyc 过程中 Invoke 函数派发时的参数即可完成的操作,而派发过程可以通过特殊大小的内存初始化(memset 函数)来监听,并不需要复杂的定位机制,不过分析时花了一点功夫。
屏蔽热键系列将只谈 WMsgKMessageHandler 入手的方法,Hook NDR-RPC 的系列(前半部分排表)将只从 RPC 角度去分析,并且都考虑到稳定性和绕过系统程序默认开启的控制流防护和缓冲区防护。
系列文章:
- 屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)
- 基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)
- Hook 实现系统热键屏蔽(二)[暂未发布]
- Windows 拦截系统睡眠
- 屏蔽 Ctrl + Alt + Del 、Ctrl + Shift + Esc 等热键(一)
- 屏蔽 Ctrl + Alt + Del 、Ctrl + Shift + Esc 等热键(二)[暂未发布]
一、原理概述
winlogon 进程通过 SignalManagerWaitForSignal 函数循环等待系统快捷键。最终通过 WMsgKMessageHandler 回调函数来实现 RPC 消息的处理。
该函数的声明应该是这样的:
cpp
int __fastcall WMsgKMessageHandler(
unsigned int uMsgCSessionKey,
unsigned int uMsgWLGenericKey,
PRPC_ASYNC_STATE pAsync,
int * pReserved
)
第一个参数 uMsgCSessionKey 控制会话有关(CSession)的回调消息,这不是我们需要关注的。
第二个参数 uMsgWLGenericKey 控制注册调用(WLGeneric)的回调消息,其中包含了对快捷键处理有关的函数。
这两个参数可以理解为窗口过程的 uMsg 参数,通过 switch...case 对消息进行处理。对于快捷键处理,大体上可以简化为如下伪代码过程:
cpp
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 消息:
如果能够挂钩该函数并且修改函数的参数使得:当 uMsgCSessionKey == 0x404 时,uMsgWLGenericKey 的值变为比较大的一个数值,这样理论上就可以绕过调用。但是猜测会导致 CFG 检测不通过,似乎这个函数通过间接调用完成,按照道理应该会有控制流防护。
heiheiabcd 给出的方法是直接修改"case ID"将每个 ID 改为很大的值。这一点我将在第二篇分去分析,本篇从定位该函数入手。
二、定位 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 的伪代码/反汇编就可以看出:
所以,只需要匹配第一次搜索结果即可,代码如下:
cpp
#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);
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; 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; 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";
BYTE pattern[] =
{ 0x48u, 0x8Bu, 0x0Du, 0, 0, 0, 0, 0x49u,
0x3Bu, 0xCCu, 0x74u , 0, 0x44, 0x84,
0x79, 0x1C , 0x74 };// ETW Trace 特征码
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 或者 0x90909090 的 Hot Patch 片段,来确定函数入口点。
但是,比较麻烦的就是 Win 11 上和之前版本还有些不同,位点之后插入了一段不明作用的数值,应该也是属于 HotPatch 里面的,有大神指点不?(已解决:见补充更新部分)
我只能想到再通过入口特征进一步定位了:入口的 mov rsp --> 48 89 特征。 一定有更好的方法。
简单编写的测试代码如下:
cpp
#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);
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; 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; 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";
BYTE pattern[] =
{ 0x48u, 0x8Bu, 0x0Du, 0, 0, 0, 0, 0x49u,
0x3Bu, 0xCCu, 0x74u , 0, 0x44, 0x84,
0x79, 0x1C , 0x74 };// ETW Trace 特征码
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 就方便了,方法将在整理好后于下一篇继续讨论。
补充更新
后来发现 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 段的代码如下:
cpp
#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;
}
运行结果如下:
计算结果一致,把这个功能整合到搜索代码中即可。
总结
本文就复现《禁止Ctrl+Alt+Del、Win+L等任意系统热键》一文中的方法四,为了实现拦截 Ctrl + Alt + Del 等热键,首先讨论了如何定位 WMsgKMessageHandler 这个关键函数。测试代码检测过 Win 8/10/11 x64 的部分版本系统,其他版本可能存在命中失败的现象。
发布于:2024.01.28,更新于:2024.01.28