屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)

目录

前言

一、关于 Winlogon 过程

二、通过低级键盘钩子挂钩系统快捷键

2.1 Windows 挂钩

2.2 挂钩过程

2.3 键盘钩子

2.4 屏蔽/通知部分系统热键

2.5 完整代码

三、通过编程拦截 Winlogon 过程

3.1 利用注册表/组策略实现阻止用户关机

3.2 挂起 Winlogon 进程以屏蔽响应

3.3 注册原因请求接口实现关机拦截

3.4 挂钩电源接口[关机/重启]相关函数

3.5 注意事项和局限性分析

四、挂钩 RPC 调用——直接过滤 Winlogon 回调

4.1 了解 Winlogon 的 RPC 过程调用

4.2 分析关键函数

4.3 过滤方法一:拦截 RPC 的字符串句柄绑定过程

4.4 过滤方法二:拦截 Server 端异步回调过程

五、通过 ICredentialProvider 接口重写凭据提供程序

5.1 方法概述

后记


前言

本文主要讲解关于挂钩 Winlogon 回调过程,实现对任务管理器电源操作DWM 自启动控制、常见系统热键等进行编程拦截。最后,我们指出可以通过我们的方法修改系统登陆或者其他安全页面的界面样式,达到自定义的效果。这里主要涉及 R3 用户层下的挂钩,如果是 R0,有些操作可能更为简单。这是之前编写的一篇文章,一直没有发布,最近得空完善了一下。

【提示】本文为了有助于不了解 Winlogon 的读者初步理解我们在完成的事情,在第一、二、三框题下主要是对以前已知的一些方法技术进行简单的整理,而第四、五框题下,则是完整的新方法和用例,如有必要,可以直接跳转到所需要的位置阅读。

(我也不想写长文☆*: .。. o(≧▽≦)o .。.:*☆)


一、关于 Winlogon 过程

WinlogonWindows 操作系统的关键部分。此过程始终在 Windows 的后台运行,并负责一些重要的系统功能。如果没有 WinlogonWindows 将无法使用。

此过程执行与 Windows 登录过程有关的各种关键任务。例如,当登录时,winlogon.exe 进程负责将用户配置文件加载到注册表中。这使程序可以使用注册表 HKEY_CURRENT_USER 下的键,每个 Windows 用户帐户的键都不同。

Winlogon 初始化时,它会向系统注册 Ctrl + Alt + Del 安全警示序列 (SAS) [XP 上,安全序列有 SAS 窗口,在之后的系统上为了安全性取消了该机制,但是快捷键依然注册],然后在 WinSta0 窗口工作站中创建三个桌面。

注册 Ctrl + Alt + Del 会使此初始化成为第一个过程,从而确保没有其他应用程序挂钩该键序列。

WinSta0 是表示物理屏幕、键盘和鼠标的窗口站对象的名称。 WinlogonWinSta0 对象中创建以下桌面。

桌面 说明

Winlogon 安全桌面

(WinLogon 桌面)

这是 Winlogon 用于交互式标识和身份验证以及其他安全对话框的桌面。 Winlogon 在收到事件通知时会自动切换到此桌面。一般地,Winlogon 会拉起一个名为 LogonUI.exe 的 UI 进程用于交互式登陆。
应用程序桌面(Default 桌面) 每次用户成功登录时,都会为该登录会话创建一个应用程序桌面。 应用程序桌面也称为默认桌面或用户桌面。 此桌面是所有用户活动发生的地方。 应用程序桌面受到保护;只有系统和交互式登录会话才有权访问它。 请注意,只有登录用户的特定实例才能访问桌面。 如果交互式用户使用服务控制器激活进程,该服务应用程序将无权访问应用程序桌面。

屏幕保护程序桌面

(Screen-saver 桌面)

这是屏幕保护程序运行时的活动桌面。 如果用户已登录,则系统和交互式登录会话都有权访问桌面。 否则,只有系统有权访问桌面。这个桌面名叫 Screen-saver 桌面,会在系统登陆、锁屏、欢迎界面等情况下变成活动桌面。

备注:通常在登录会话处于暂停状态时(用户不是活动用户),这个用户的桌面会切换到 Disconnected 桌面

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第1张图片

Winlogon 具有以下职责:

  • 窗口工作站和桌面保护

Winlogon 设置对窗口工作站和相应桌面的保护,以确保每个桌面都可以正确访问。 通常,这意味着本地系统将拥有对这些对象的完全访问权限,并且以交互方式登录的用户将具有对窗口站对象的读取访问权限和对应用程序桌面对象的完全访问权限。

  • 标准 SAS 识别

WinlogonUser32 服务器中具有特殊的挂钩,使它可以监视 Ctrl + Alt + Del 安全警示序列 (SAS) 事件。 Winlogon 使此 SAS 事件信息用作其 SASSAS 的一部分,并启动安全序列的 UI 界面。

  • 用户配置文件加载

当用户登录时,其用户配置文件将加载到注册表中。 这样,用户的进程可以使用特殊注册表项 HKEY_CURRENT_USERWinlogon 会在成功登录后、激活新登录用户的 shell 之前自动执行此操作。

  • 将安全性分配给用户 shell

当用户登录时,Winlogon 负责为该用户创建一个或多个初始进程。 Winlogon 调用 Kernel 函数 CreateProcessAsUser,完成启动进程的任务,如启动 explorer.exe

  • 屏幕保护程序控件

Winlogon 监视键盘和鼠标活动,以确定何时激活屏幕保护程序。 激活屏幕保护程序后,Winlogon 将继续监视键盘和鼠标活动,以确定何时终止屏幕保护程序。 如果屏幕保护程序标记为安全,Winlogon 会将工作站视为已锁定。 当存在鼠标或键盘活动时,Winlogon 会恢复锁定的工作站行为。 如果屏幕保护程序不安全,则任何键盘或鼠标活动将终止屏幕保护程序。

Winlogon 的锁屏过程会启动一个名为 LockScreen 的进程。

  • 多个网络提供商支持

Windows 系统上安装的多个网络可以包含在身份验证过程和密码更新操作中。 此包含允许其他网络使用 Winlogon 的安全桌面在正常登录期间一次性收集标识和身份验证信息。 

二、通过低级键盘钩子挂钩系统快捷键

2.1 Windows 挂钩

挂钩是系统消息处理机制中的一个重要部分,应用程序可以安装子例程来监视系统中的消息流量,应用程序截获消息、鼠标操作和击键等事件,并在某些类型的消息到达目标窗口过程之前对其进行处理。 截获特定类型的事件的函数称为 挂钩过程。 挂钩过程可以对其接收的每个事件执行操作,然后修改或放弃该事件。

系统支持许多不同类型的挂钩;每种类型都提供对其消息处理机制的不同方面的访问。 例如,应用程序可以使用 WH_MOUSE 挂钩来监视鼠标消息的消息流量。

2.2 挂钩过程

系统为每种类型的挂钩维护单独的挂钩链。 挂钩链是指向应用程序定义的特殊回调函数(称为挂钩过程)的指针列表。 当发生与特定类型的挂钩关联的消息时,系统会将消息依次传递给挂钩链中引用的每个挂钩过程。 挂钩过程可以执行的操作取决于所涉及的挂钩类型。 某些类型挂钩的挂钩过程只能监视消息;其他人可以修改消息或通过链停止其进度,从而阻止它们到达下一个挂钩过程或目标窗口。

为了利用特定类型的挂钩,微软提供了一个挂钩过程,并使用 SetWindowsHookEx 函数将其安装到与挂钩关联的链中。 挂钩过程必须具有以下语法:

LRESULT CALLBACK HookProc(

int nCode,

WPARAM wParam,

LPARAM lParam

) {

// process event ...

return CallNextHookEx(NULL, nCode, wParam, lParam);

}

nCode 参数是挂钩过程用于确定要执行的操作的挂钩代码。 挂钩代码的值取决于挂钩的类型;每种类型都有自己的特征挂钩代码集。 wParam 和 lParam 参数的值取决于挂钩代码,但它们通常包含有关已发送或发布的消息的信息。

SetWindowsHookEx 函数始终在挂钩链的开头安装挂钩过程。 当发生由特定类型的挂钩监视的事件时,系统会在与挂钩关联的挂钩链的开头调用过程。 链中的每个挂钩过程确定是否将事件传递给下一个过程。 挂钩过程通过调用 CallNextHookEx 函数将事件传递给下一过程。

HHOOK SetWindowsHookExA(
  [in] int       idHook,
  [in] HOOKPROC  lpfn,
  [in] HINSTANCE hmod,
  [in] DWORD     dwThreadId
);

LRESULT CallNextHookEx(

[in, optional] HHOOK hhk,

[in] int nCode,

[in] WPARAM wParam,

[in] LPARAM lParam

);

全局挂钩监视与调用线程位于同一桌面中的所有线程的消息。 特定于线程的挂钩仅监视单个线程的消息。 全局挂钩过程可以在调用线程所在的桌面中的任何应用程序的上下文中调用,因此该过程必须位于单独的 DLL 模块中。 仅在关联线程的上下文中调用特定于线程的挂钩过程。 如果应用程序为其自己的线程之一安装挂钩过程,则挂钩过程可以位于与应用程序代码的其余部分相同的模块中,也可以位于 DLL 中。 如果应用程序为不同应用程序的线程安装挂钩过程,则该过程必须位于 DLL 中。 

2.3 键盘钩子

在这里,我们只需要了解各种类型挂钩中的两种:键盘输入钩子和低级别键盘输入钩子。这两个钩子分别对应于将 SetWindowsHookEx 函数的 idHook 参数置为以下值:

WH_KEYBOARD

2

安装用于监视击键消息的挂钩过程。  使用 KeyboardProc 挂钩过程。

WH_KEYBOARD_LL

13

安装用于监视低级别键盘输入事件的挂钩过程。 使用 LowLevelKeyboardProc 挂钩过程。

普通的键盘钩子无法挂钩系统热键,只有使用低级钩子 (Low-Level Hook) 才可以挂勾。但是由于“安全警示序列”受到桌面级挂钩保护,任何 User32 程序都无法通过系统提供的钩子函数挂钩这类热键,即 Ctrl + Alt + DeleteWin + L 等按键消息是无法挂钩的。但是有些时候,我们只挂钩一般的系统热键就可以了。下面给出了 LowLevelKeyboardProc 挂钩过程的回调:

LRESULT CALLBACK LowLevelKeyboardProc(
  _In_ int    nCode,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
)
{
    if(nCode == HC_ACTION)
    {
        // process event ...
    }
    // if nCode < HC_ACTION (0), then CallNextHookEx must be used.
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

其中,wParam 是键盘消息的标识符。可以是以下消息之一: WM_KEYDOWNWM_KEYUPWM_SYSKEYDOWNWM_SYSKEYUPlParam 是指向 KBDLLHOOKSTRUCT 结构的指针。

typedef struct tagKBDLLHOOKSTRUCT {

DWORD vkCode;

DWORD scanCode;

DWORD flags;

DWORD time;

ULONG_PTR dwExtraInfo;

} KBDLLHOOKSTRUCT, *LPKBDLLHOOKSTRUCT, *PKBDLLHOOKSTRUCT;

  • vkCode 参数表示虚拟按键码。 该码必须是 1 到 254 范围内的值。
  • scanCode 是按键的硬件扫描码,关于硬件扫描码的码表有人整理出来了,可以参考这篇转载文章:键盘硬件扫描码(邓志)。
  • flags 是扩展键标志、事件注入标志、上下文代码和转换状态标志。 下表描述了此值的布局:
Bits 说明
0 指定键是扩展键,例如功能键还是数字键盘上的键。 如果键是扩展键,则值为 1;否则为 0。
1 指定事件是否是从在较低完整性级别运行的进程中注入的。 如果出现这种情况,则值为 1;否则为 0。 请注意,每当设置位 1 时,也会设置位 4。
2-3 保留。
4 指定是否注入事件。 如果出现这种情况,则值为 1;否则为 0。 请注意,设置第 4 位时不一定设置位 1。
5 上下文代码。 如果按下 Alt 键,则值为 1;否则为 0。
6 保留。
7 转换状态。 如果按下了键,则值为 0;如果释放键,则值为 1。

应用程序可以使用以下值来测试击键标志。 测试 LLKHF_INJECTED (位 4) 将告知是否已注入事件。 如果是,则测试 LLKHF_LOWER_IL_INJECTED (位 1) 会告诉你事件是否是从以较低完整性级别运行的进程注入的。

  • time 参数返回此消息的时间戳,相当于 GetMessageTime 为此消息返回的时间戳。
  • dwExtraInfo 返回与消息关联的其他信息。

有了回调函数,我们只需要在消息链中注入回调即可实现挂钩,用于设置钩子的 SetWindowsHookEx 函数可以这样设置参数:

/* WH_KEYBOARD_LL 表示使用低级键盘钩子回调;
 * KeyboardProc 是函数指针,指向回调函数;
 * 通过 GetModuleHandle(NULL) 获取挂钩进程的模块句柄
 * 这里是当前 DLL 的句柄,其中包含挂钩过程函数。 
 */
SetWindowsHookExW(WH_KEYBOARD_LL, KeyboardProc, GetModuleHandle(NULL), 0);

2.4 屏蔽/通知部分系统热键

上文通过 WindowsHook 的回调我们实现了对系统热键的捕获,但是在回调中我们并没有做任何事情。那么,如何通过回调处理具体的按键信息呢?经查阅资料,可以通过会调和函数中 lParam 返回的结构体指针来分析当前按下的低级别按键信息,并对击键消息做出响应:

// 将 lParam 的地址转换为指向适当结构体的指针
PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;

我们知道系统热键常常是组合按键,因为这样更安全有区分性,可以尽可能避免热键冲突。我们通过 KBDLLHOOKSTRUCT 结构体的成员 vkCode 确定虚拟密钥,flags 用于确定扩展按键,由于他们的组合有限,对于一些特殊的组合可能无法精确判断,故我们也会结合 GetKeyState 函数。

下表展示了 KBDLLHOOKSTRUCT 结构体的成员:

  • vkCode

类型:DWORD

虚拟密钥代码。 代码必须是 1 到 254 范围内的值。

  • scanCode

类型:DWORD

密钥的硬件扫描代码。

  • flags

类型:DWORD

扩展键标志、事件注入标志、上下文代码和转换状态标志。 此成员指定如下。 应用程序可以使用以下值来测试击键标志。 测试LLKHF_INJECTED (位 4) 将告知是否已注入事件。 如果是,则测试LLKHF_LOWER_IL_INJECTED (位 1) 会告诉你事件是否是从以较低完整性级别运行的进程注入的。

含义

LLKHF_EXTENDED

>> (KF_EXTENDED 8)

测试扩展键标志。

LLKHF_LOWER_IL_INJECTED

0x00000002

从以较低完整性级别) 标志运行的进程测试事件注入 (。

LLKHF_INJECTED

0x00000010

从任何进程) 标志测试事件注入 (。

LLKHF_ALTDOWN

>> (KF_ALTDOWN 8)

测试上下文代码。

LLKHF_UP

>> (KF_UP 8)

测试转换状态标志。

我们在消息查询线程中使用 GetKeyState 函数来获取键盘虚拟按键的状态信息,使用该函数并集合结构体返回的信息,即可做到对组合键的识别。

GetKeyState 函数的定义如下:

SHORT GetKeyState(
  [in] int nVirtKey
);

根据定义 GetKeyState 函数的返回值为 SHORT 类型,即短整型。SHORT 型是 16 位有符号的数据类型,如果要查询的键被按下,返回值最高位被置为 1,则这个数表示负数,所以可以取最高位和 0 比较来判断按键是否被击中。下面定义的宏可以实现对按键按下和弹出的判断:

#define IsKeyDown(vk_code) ((GetKeyState(vk_code) & 0x8000) ? 1 : 0)
#define IsKeyUp(vk_code) ((GetKeyState(vk_code) & 0x8000) ? 0 : 1)

系统注册的常见热键有:ALT + TABALT + ESCCTRL + ESCCTRL + SHIFT + ESCWin + LCTRL + ALT + DEL 等。通过如下所示的挂钩过程可以对大多数系统热键进行拦截或者通知:

// 低级键盘钩子回调函数
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode == HC_ACTION)
    {
        switch (wParam)
        {
        case WM_KEYDOWN:  case WM_SYSKEYDOWN:
            // 将 lParam 的地址转换为指向适当结构体的指针
            PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;

            if (p->vkCode == VK_F12)
            {
                // 实现退出/结束挂钩
                MessageBoxW(GetForegroundWindow(), L"I'm in position..", L"Hook LowLevelKey", MB_OK);
                if (hHook != NULL)
                {
                    if (!UnhookWindowsHookEx(hHook))
                    {
                        SetThreadDesktop(hCurDesk);
                        CloseHandle(hCurDesk);
                        OutputDebugString(L"Unhook failed..");
                        break;
                    }
                    OutputDebugString(L"键盘钩子成功取消");
                }
                return 0;
            }
            // 屏蔽 ALT+TAB
            else if ((p->vkCode == VK_TAB) && ((p->flags & LLKHF_ALTDOWN) != 0))
            {
                MessageBoxW(GetForegroundWindow(), L"已经拦截到 ALT+TAB 键", L"Hook LowLevelKey", MB_OK);
                return 1;
            }
            // 屏蔽 ALT+ESC
            else if ((p->vkCode == VK_ESCAPE) && 
                ((p->flags & LLKHF_ALTDOWN) != 0) && 
                ((GetKeyState(VK_SHIFT) & 0x8000) == 0))
            {
                MessageBoxW(GetForegroundWindow(), L"已经拦截到 ALT+ESC 键", L"Hook LowLevelKey", MB_OK);
                return 1;
            }
            // 屏蔽 CTRL+ESC
            else if ((p->vkCode == VK_ESCAPE) && ((GetKeyState(VK_CONTROL) & 0x8000) != 0))
            {
                MessageBoxW(GetForegroundWindow(), L"已经拦截到 CTRL+ESC 键", L"Hook LowLevelKey", MB_OK);
                return 1;
            }
            // 屏蔽 CTRL+SHIFT+ESC
            else if ((p->vkCode == VK_ESCAPE) &&
                ((GetKeyState(VK_CONTROL) & 0x8000) != 0) &&
                ((GetKeyState(VK_SHIFT) & 0x8000) != 0))
            {

                return 1;
            }
            // 屏蔽左右 Win + L 键
            else if (((GetKeyState(VK_L) & 0x8000) != 0) && ((GetKeyState(VK_LWIN) & 0x8000) != 0))
            {
                MessageBoxW(GetForegroundWindow(), L"已经拦截到 WIN+L 键", L"Hook LowLevelKey", MB_OK);
                return 1;
            }
            // 此处无法屏蔽 CTRL+ALT+DEL,但可以拦截到消息
            else if ((p->vkCode == VK_DELETE) &&
                ((GetKeyState(VK_CONTROL) & 0x8000) != 0) &&
                ((GetKeyState(VK_MENU) & 0x8000) != 0))
            {
                MessageBoxW(GetForegroundWindow(), L"已经拦截到 CTRL+ALT+DEL 键", L"Hook LowLevelKey", MB_OK);
                return 1;
            }
            break;
        }
    }
    return CallNextHookEx(hHook, nCode, wParam, lParam);
}

但对于一个低级键盘钩子,他不会加载链接库到所有窗口进程中。如果在一个窗口进程中加载钩子,则可以实现消息过滤。但现在加载的对象是 Winlogin,这个进程不是窗口进程,它的线程也不在系统应用程序所处的 Default 桌面下,因此在这个进程中加载钩子,需要注意以下几点:

1. 在需要注入的 DLL 代码中 DLL_PROCESS_ATTACH 处开启一个新线程,并在该线程中实现启用低级键盘钩子的函数。

2. 由于钩子所在的线程为非窗口的消息处理线程,因此,必须在该线程成功设置钩子以后主动接收并分发收到的消息,否则钩子将不会钩到任何消息:

MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

3. 由于该线程创建时默认与 Winlogon 同属一个桌面(Winlogon 桌面),而其它包括 explorer.exe 在内的窗口程序都处在 Default 桌面,在 Windows 中规定程序只能获得针对同一桌面上创建的窗口消息。所以,要让该线程能接收到用户在 Default 桌面下操作所产生的消息,必须在该线程中使用如下代码将它的桌面设置为 Default 桌面:

HDESK hDesk = OpenDesktop("Default",0,FALSE,MAXIMUM_ALLOWED);
SetThreadDesktop(hDesk);
CloseHandle(hDesk);

钩子例程在解决了以上问题之后,能正确设置钩子,处理回调函数。从而实现屏蔽/通知系统热键的状态。

2.5 完整代码

一个完整的利用 SetWindowsHookExW 的低级钩子库代码如下:

#include 
#include 

// 键盘钩子过程
HDESK hCurDesk = NULL;

// Dll所创建线程的句柄
HANDLE hThread = NULL;
// Dll所创建线程的ID
DWORD dwThreadId = 0;
// Dll所创建线程的线程函数
DWORD WINAPI ThreadFunc();

// 钩子句柄
HHOOK hHook = NULL;
// 低级键盘钩子回调函数
LRESULT CALLBACK KeyboardProc(int, WPARAM, LPARAM);

#define VK_L 0x4C

BOOL APIENTRY DllMain(HANDLE hMoudle, DWORD dwReason, LPVOID lpReserved)
{
    switch (dwReason)
    {
    case DLL_PROCESS_ATTACH:
        hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFunc, NULL, 0, &dwThreadId);
        break;
    case DLL_PROCESS_DETACH:
        // 卸载低级键盘钩子
        if (hHook != NULL)
        {
            if (!UnhookWindowsHookEx(hHook))
            {
                SetThreadDesktop(hCurDesk);
                CloseHandle(hCurDesk);
                OutputDebugString(L"Unhook failed..");
                break;
            }
            OutputDebugString(L"键盘钩子成功取消");
        }
        TerminateThread(hThread, 1);
        CloseHandle(hThread);

        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    }
    return TRUE;
}

// Dll所创建线程的线程函数
DWORD WINAPI ThreadFunc()
{
    hCurDesk = GetThreadDesktop(GetCurrentThreadId());
    HDESK hUserDesk = NULL;
    // 同一桌面上进程之间只能发送窗口消息。无法跨进程与其他桌面发送它们。 
    // 同样,Windows 消息是限制应用程序定义挂钩。
    // 特定桌面中运行的进程挂钩过程将〈〈只获得针对同一桌面上创建窗口消息。〉〉
    // 所以,这里必须设置钩子所在线程的桌面为Default桌面
    // 才能使得钩子所在线程能接收到 Default 桌面的消息
    hUserDesk = OpenDesktopW(L"Default", 0, FALSE, MAXIMUM_ALLOWED);
    SetThreadDesktop(hUserDesk);
    CloseHandle(hUserDesk);

    // 设置低级键盘钩子,屏蔽非SAS window的热键
    // 需要 #define _WIN32_WINNT 0x0500
    hHook = SetWindowsHookExW(WH_KEYBOARD_LL, KeyboardProc, GetModuleHandle(NULL), 0);
    if (hHook == NULL)
    {

        OutputDebugString(L"Set hook failed..");
        return 1;
    }
    OutputDebugString(L"键盘钩子成功设置");

    // 在非 GUI 线程中使用消息钩子必须主动接收并分发收到的消息
    MSG msg;
    while (GetMessageW(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }

    return 1;
}


// 低级键盘钩子回调函数
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode == HC_ACTION)
    {
        switch (wParam)
        {
        case WM_KEYDOWN:  case WM_SYSKEYDOWN:
            PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;

            if (p->vkCode == VK_F12)
            {
                // 实现模拟按键代码
                MessageBoxW(GetForegroundWindow(), L"I'm in position..", L"Hook LowLevelKey", MB_OK);
                if (hHook != NULL)
                {
                    if (!UnhookWindowsHookEx(hHook))
                    {
                        SetThreadDesktop(hCurDesk);
                        CloseHandle(hCurDesk);
                        OutputDebugString(L"Unhook failed..");
                        break;
                    }
                    OutputDebugString(L"键盘钩子成功取消");
                }
                return 0;
            }
            // 屏蔽 ALT+TAB
            else if ((p->vkCode == VK_TAB) && ((p->flags & LLKHF_ALTDOWN) != 0))
            {
                MessageBoxW(GetForegroundWindow(), L"已经拦截到 ALT+TAB 键", L"Hook LowLevelKey", MB_OK);
                return 1;
            }
            // 屏蔽 ALT+ESC
            else if ((p->vkCode == VK_ESCAPE) && 
                ((p->flags & LLKHF_ALTDOWN) != 0) && 
                ((GetKeyState(VK_SHIFT) & 0x8000) == 0))
            {
                MessageBoxW(GetForegroundWindow(), L"已经拦截到 ALT+ESC 键", L"Hook LowLevelKey", MB_OK);
                return 1;
            }
            // 屏蔽 CTRL+ESC
            else if ((p->vkCode == VK_ESCAPE) && ((GetKeyState(VK_CONTROL) & 0x8000) != 0))
            {
                MessageBoxW(GetForegroundWindow(), L"已经拦截到 CTRL+ESC 键", L"Hook LowLevelKey", MB_OK);
                return 1;
            }
            // 屏蔽 CTRL+SHIFT+ESC
            else if ((p->vkCode == VK_ESCAPE) &&
                ((GetKeyState(VK_CONTROL) & 0x8000) != 0) &&
                ((GetKeyState(VK_SHIFT) & 0x8000) != 0))
            {

                return 1;
            }
            // 屏蔽左右 Win + L 键
            else if (((GetKeyState(VK_L) & 0x8000) != 0) && ((GetKeyState(VK_LWIN) & 0x8000) != 0))
            {
                MessageBoxW(GetForegroundWindow(), L"已经拦截到 WIN+L 键", L"Hook LowLevelKey", MB_OK);
                return 1;
            }
            // 此处无法屏蔽 CTRL+ALT+DEL,但可以拦截到消息
            else if ((p->vkCode == VK_DELETE) &&
                ((GetKeyState(VK_CONTROL) & 0x8000) != 0) &&
                ((GetKeyState(VK_MENU) & 0x8000) != 0))
            {
                MessageBoxW(GetForegroundWindow(), L"已经拦截到 CTRL+ALT+DEL 键", L"Hook LowLevelKey", MB_OK);
                return 1;
            }
            break;
        }
    }
    return CallNextHookEx(hHook, nCode, wParam, lParam);
}

以上的钩子,只能拦截一般的系统热键,而且可能会失败。尤其是不能拦截 Ctrl + Alt + Delete 组合键。在 Windows XP 系统上,可以通过远程注入 DLL 到 winlogon.exe 进程,修改 Winlogon 桌面下的 SAS 窗口的回调函数,从而捕获该窗口的 WM_HOTKEY 消息,可以实现屏蔽 Ctrl + Alt + Delete。这个方法也有缺陷,那就是除了 Ctrl + Alt + Delete 外,大多数的其它系统热键,(包括Alt + Tab,Ctrl + Esc及左右 WIN 键)都无法屏蔽。一般地,我们会混合使用前面两种钩子实现在 WinXP 上的系统热键过滤。但是在 Vista 上就取消了 SAS 界面以及 GINA 组件,所以过滤 Ctrl + Alt + Delete 组合键就成为了难题。这一点我们在后面会解决。

三、通过编程拦截 Winlogon 过程

在新的内容开始前,我想整理一些旧文,这一框题展示了在以前的系统上实现在用户关机/重启/注销时弹出对话框的功能。为什么需要先讲这个部分?因为这一部分需要拦截的函数是截至 Win 8 系统,微软所采用的关机/重启等途径上的关键函数,这有助于我们理解后续的拦截任意 Winlogon 操作方法分析。

3.1 利用注册表/组策略实现阻止用户关机

WinlogonExplorer 在执行操作前会检查或设置多个注册表位置下的键值,我们常常通过 Process Monitor 等工具分析注册表操作来挖掘可用信息。

在操作系统中,用户可以通过“开始”菜单的电源按钮,打开电源选项卡。电源选项中一般有注销、重启、关机、睡眠/休眠几个选项。下图展示了在 Win 11 上电源选项卡的弹出式窗口内容:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第2张图片

如果您的电脑需要长期运行而不关机,为了防止误关机,可以手动隐藏这些按钮。下面介绍如何通过修改注册表实现隐藏电源按钮。在注册表中,我们可以分别隐藏关机、重启、睡眠、电源按钮、休眠、注销、切换用户等控件/选项。

在“运行”对话框(快捷键 WIN + R) 或开始菜单中输入 Regedit 并回车,打开注册表编辑器。定位至以下路径:

[注册表路径]

计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PolicyManager\default\Start

Start 主键下,可以看到一部分名称以 Hide 开头的子键,通过名称我们就知道它们对应的功能,比如 HideShutDown 表示隐藏关机选项、HidePowerButton 表示隐藏电源按钮。

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第3张图片

在这些以 Hide 开头的键下,有多个固定名称的值项,其中名为"value" 的值用于表示是否隐藏,值的数据为 1 表示隐藏,值的数据为 0(默认)表示始终显示。

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第4张图片

对这些项的大多数修改会立即生效,部分修改(如隐藏电源按钮)需要重启资源管理器。这里以隐藏关机选项为例,将 HideShutDown 下的 value 的值修改为 1,打开开始菜单的电源按钮,可以观察到“关机”选项已经消失:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第5张图片

并且在资源管理器的桌面或任务栏击中 Alt + F4 热键。在弹出的“关闭 Windows”对话框中,也不能找到“关机”选项:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第6张图片

此外,还可以通过组策略编辑器实现禁用电源选项:通过运行 gpedit.msc 打开组策略编辑器,展开“计算机配置 - 管理模板 - 开始菜单和任务栏”树结构,在右边的设置栏中可以找到“删除并阻止访问关机、重新启动、睡眠和休眠命令”这一项。

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第7张图片

双击编辑属性,点击“已禁用”,点击“应用”,点击确定关闭对话框:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第8张图片

 此设置将会同时隐藏关机、重新启动、睡眠和休眠命令。当然,也可以通过注册表修改组策略:

在注册表中打开如下路径

[注册表路径] —— 当前计算机

计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion

\Policies\Explorer

[注册表路径] —— 当前用户

计算机\HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion

\Policies\Explorer

找到 HidePowerOptions 值项,如果没有可以打开右键菜单,选择“新建 - DWORD(32位)值”,并命名为 HidePowerOptions,我们需要修改其数据为1。

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第9张图片

这会禁用电源按钮中的所有电源选项,但不会在 Alt+ F4 中生效。

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第10张图片

此外,通过组策略或者注册表可以禁用登陆界面的电源按钮,可以通过如下方法完成:

(1)组策略

打开“运行”对话框,并输入 gpedit.msc,打开“本地组策略编辑器”;在本地组策略编辑器的左侧窗格中,向下展开到“计算机配置”>“ Windows设置”>“安全设置”>“本地策略”>“安全选项”。 在右侧,找到“关闭:无需登录即可关闭系统”项,然后双击它。

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第11张图片

在属性设置页面点击“已启用”,点击“应用”,随后点击“确定”关闭页面,然后重启计算机即可生效。 

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第12张图片

(2)注册表

首先,点击开始并键入“regedit”,打开注册表编辑器。 按 Enter 键打开注册表编辑器,并授予其对 PC 进行更改的权限。 在注册表编辑器中,使用左侧边栏导航至以下键:

[注册表路径]

计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion

\Policies\System

在右侧的项列表中,找到 ShutdownWithoutLogon 值,然后双击它(如果没有,请创建它)。 

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第13张图片

在“数值数据”框中将值设置为0 ,然后单击“确定”。

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第14张图片

退出注册表编辑器,然后重新启动计算机以查看更改。 重新登录后,应该不再在登录屏幕的右下角看到“关机”按钮。

上面所提到的注册表修改方式可以通过编程修改,道理是一样的。

由于注册表只是让 GUI 界面的按钮隐藏,而并不是阻止相关的过程,调用和电源有关的接口的进程依然可以关闭计算机。

3.2 挂起 Winlogon 进程以屏蔽响应

一些程序通过挂起 winlogon.exe 进程来阻滞一切由其控制的操作,这方便于他们所要的功能的实现。在 Win 7 时,很多工具在后台挂起该进程达到阻止任务管理器 (Taskmgr) 启动的作用

打开 Procexp (Process Explorer) 找到登陆应用程序的进程,在右键菜单中可以看到挂起进程 ( Suspend) 选项:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第15张图片

看到进程状态被标记为 Suspended(已挂起),表示进程将暂停执行过程。

这时候,你可以尝试按下 Ctrl + Alt + Esc ,这将阻止任务管理器的启动。但是这个方法在 Win 10/11 上行不通,挂起进程会导致一切“以管理员身份启动”操作都被阻滞。并且影响 Winlogon 的稳定性。

3.3 注册原因请求接口实现关机拦截

微软提供了一些函数接口用于电源状态改变时程序能够及时得到通知。下面我们简单分析关机拦截API 的功能。

对于 GUI 应用程序,只有关闭和重新启动才被视为挂起操作。进入睡眠模式并不是一个非常重要的事件,因为它不会改变 GUI 应用程序工作中的任何内容。在某些情况下,我们可能需要检测睡眠模式,以便向其他系统组件发送 PC 将进入睡眠状态的消息。但是,大多数 GUI 应用只需要检测操作系统关闭和重启的机制。让我们看看这些机制是如何工作的:

GUI 应用程序通过窗口消息接收有关目标事件的信息。这就是为什么我们需要 WM_QUERYENDSESSION 和 WM_POWERBROADCAST 消息来使 GUI 应用程序能够检测操作系统关闭。

Windows 在用户启动用户会话关闭过程时发送 WM_QUERYENDSESSION 窗口消息。关闭计算机并重新启动也会导致用户会话结束。因此,这些事件的消息通过同一个窗口消息传递。

我们使用 WM_POWERBROADCAST 消息来获取有关系统暂停的信息。以下是我们在 GUI 应用程序中处理此消息的方式:

//...
    case WM_POWERBROADCAST:
        {
            if (wParam == PBT_APMSUSPEND)
                // 计算机正在挂起
            break;
        }
    case WM_QUERYENDSESSION:
        {
            if (lParam == 0)
                // 计算机正在关闭
            if ((lParam & ENDSESSION_LOGOFF) == ENDSESSION_LOGOFF)
                // 正在注销用户
            break;
        }
//...

WM_POWERBROADCAST 中的 lParam 参数包含各种系统事件的标识符,包括关闭。对于WM_QUERYENDSESSION 窗口消息,值为 0 表示重新启动或关闭,而其他值表示其他事件。

备注:通过模拟发送该消息和 WM_ENDSEESION ,可以实现关机前准备过程

请注意,我们单独处理关机和注销事件,因为它们不一定是关联的。

收到 WM_QUERYENDSESSION 后,我们能做什么?

case WM_QUERYENDSESSION:
{
    if (lParam == 0)
    { 
     // 计算机正在关机
       ShutdownBlockReasonCreateW(hwnd, L"Please, don't kill me");
    }      
    break;
}

如果我们不执行任何操作,Windows 将显示一条警告消息,指出这些应用程序正在阻止关闭,用户可以取消关闭或强制继续关闭,无论等待的应用程序如何。在这种情况下,我们的应用程序可以通过下面两种方式之一运行:

  • 关闭它以使系统立即关闭
  • 显示一条警告消息,向用户解释为什么他们现在不应该重新启动

这适用于 Windows Vista 和更高版本的 Windows

运行该程序,在关机时可以拉起一个等待列表:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第16张图片

如果我们不希望其他应用程序在我们的程序响应关机消息前被关闭,我们可以调用 SetProcessShutdownParameters 函数,并将 dwLevel 参数设置为 0x4FF,这对应于系统关机序列的最高优先级。在此函数中,特别注意参数 dwFlags。如果我们将其值更改为 SHUTDOWN_NORETRY,我们的 GUI 应用程序不会阻止关闭。

dwLevel 参数表示相对于系统中其他进程的进程关闭优先级。 系统会将进程从高 dwLevel 值关闭为低值。 最高和最低关闭优先级是为系统组件保留的。 此参数必须位于以下值范围内。

含义

000-0FF

系统保留上次关闭范围。

100-1FF

应用程序保留的最后一个关闭范围。

200-2FF

应用程序保留的“介于”关机范围内。

300-3FF

应用程序保留的第一个关闭范围。

400-4FF

系统保留第一个关机范围。

需要注意的是,所有用户进程都默认从 0x280 关机级别启动。

所以正确的调用方法为:

SetProcessShutdownParameters(0x4FF, 0);// 设置关机列表优先级

下面给出一个用对话框实现的电源事件拦截器实例:

resources.h

//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by ShutdownBlocker.rc
//
#define IDD_MAINDIALOG                  101
#define IDC_BUTTON_BLOCK                1001
#define IDC_BUTTON_UNBLOCK              1002
#define IDC_STATIC_STATUS               1005

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        102
#define _APS_NEXT_COMMAND_VALUE         40001
#define _APS_NEXT_CONTROL_VALUE         1006
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif

ShutdownBlocker.rc 

// Microsoft Visual C++ generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"

/
#undef APSTUDIO_READONLY_SYMBOLS

/
// 中文(简体,中国) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS)
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
#pragma code_page(936)

#ifdef APSTUDIO_INVOKED
/
//
// TEXTINCLUDE
//

1 TEXTINCLUDE 
BEGIN
    "resource.h\0"
END

2 TEXTINCLUDE 
BEGIN
    "#include ""afxres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED


/
//
// Dialog
//

IDD_MAINDIALOG DIALOGEX 0, 0, 344, 188
STYLE DS_SETFONT | DS_NOIDLEMSG | DS_SETFOREGROUND | DS_FIXEDSYS | DS_CENTER | WS_MAXIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
EXSTYLE WS_EX_APPWINDOW
CAPTION "ShutdownBlocker"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    PUSHBUTTON      "阻止关机",IDC_BUTTON_BLOCK,71,129,93,29
    PUSHBUTTON      "解除阻止",IDC_BUTTON_UNBLOCK,180,129,93,29
    CTEXT           "静态",IDC_STATIC_STATUS,100,60,139,20,SS_PATHELLIPSIS | NOT WS_GROUP
    GROUPBOX        "当前状态",IDC_STATIC,82,48,168,40
END


/
//
// DESIGNINFO
//

#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO
BEGIN
    IDD_MAINDIALOG, DIALOG
    BEGIN
        LEFTMARGIN, 7
        RIGHTMARGIN, 337
        TOPMARGIN, 5
        BOTTOMMARGIN, 181
    END
END
#endif    // APSTUDIO_INVOKED


/
//
// AFX_DIALOG_LAYOUT
//

IDD_MAINDIALOG AFX_DIALOG_LAYOUT
BEGIN
    0
END

#endif    // 中文(简体,中国) resources
/



#ifndef APSTUDIO_INVOKED
/
//
// Generated from the TEXTINCLUDE 3 resource.
//


/
#endif    // not APSTUDIO_INVOKED

ShutdownBlocker.cpp 

#include 
#include 
#include "resource.h"

// 标记是否已经阻止关机,默认为未阻止
BOOL blockedFlag = FALSE;

// 调用注册关机阻滞原因的接口
BOOL BlockShutdown(HWND hwnd)
{
	if (ShutdownBlockReasonCreate(hwnd, L"当前正在保存数据,请勿关机!"))
	{
		return TRUE;
	}
	return FALSE;
}

BOOL UnblockShutdown(HWND hwnd)
{
	if (ShutdownBlockReasonDestroy(hwnd))
	{
		return TRUE;
	}
	return FALSE;
}

INT_PTR CALLBACK MainDialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	HWND hcurwnd = NULL;
	switch(uMsg)
	{
	case WM_INITDIALOG:
		SetDlgItemTextW(hwndDlg, IDC_STATIC_STATUS, L"未阻止关机");
		return TRUE;
	case WM_CLOSE:
		if (blockedFlag)
		{
			if (IDYES == MessageBoxW(NULL,
				L"还未解除阻止,还要继续关闭程序么?",
				L"提示", MB_YESNO))
			{
				if (UnblockShutdown(hwndDlg))
				{
					EndDialog(hwndDlg, 0);
					break;
				}
			}
			else {
				return 0;
			}
		}
		EndDialog(hwndDlg, 0);
		break;
	case WM_QUERYENDSESSION:
		// 拦截 WM_QUERYENDSESSION 消息
		if (blockedFlag)
		{
			return TRUE;
		}
		return FALSE;
	case WM_COMMAND:
		switch(LOWORD(wParam))
		{
		case IDC_BUTTON_BLOCK:
			if (!blockedFlag)
			{
				if (BlockShutdown(hwndDlg))
				{
					SetDlgItemTextW(hwndDlg, IDC_STATIC_STATUS, L"已经阻止关机");
					blockedFlag = TRUE;
				}
				else
				{
					MessageBoxW(hwndDlg,L"阻止关机失败了……", L"提示", MB_OK);
				}
			}
			return TRUE;
		case IDC_BUTTON_UNBLOCK:
			if (blockedFlag)
			{
				if (UnblockShutdown(hwndDlg))
				{
					SetDlgItemTextW(hwndDlg, IDC_STATIC_STATUS, L"未阻止关机");
					blockedFlag = FALSE;
				}
				else
				{
					MessageBoxW(hwndDlg, L"解除阻止失败了……", L"提示", MB_OK);
				}
			}
			return TRUE;
		default:
			return DefWindowProcW(hwndDlg, uMsg, wParam, lParam);
		}
	default:
		return 0;
	}
	
}

int WINAPI WinMain(
	HINSTANCE hInstance, 
	HINSTANCE hPrevInstance, 
	LPSTR lpCmdLine, 
	int nShowCmd
)
{
	// 设置进程关机优先级
	SetProcessShutdownParameters(0x4FF, 0);
	DialogBoxW(hInstance, 
		MAKEINTRESOURCE(IDD_MAINDIALOG),
		NULL, 
		MainDialogProc
	);
	return 0;
}

程序执行的界面如图所示:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第17张图片

这个实例是网络上转载较多的一个,由于来源已经不可考证,所以就暂且当作是开源的,我稍做了改动。

3.4 挂钩电源接口[关机/重启]相关函数

3.4.1 初步分析 ExitWindowsEx 函数(Win XP / Win 7)

Windows 发生关机时,系统都进行了哪些操作?一直以来,这都困惑着我。我们总是猜测关机的响应和系统进程有关,而与外壳进程 (explorer.exe) 无关,因为任何程序都可以发起关机,只要它调用正确的例程。例如第三方程序 FastShutdown 可以实现多种电源操作,它是通过什么方法实现的呢?

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第18张图片

随后我们通过在 IDA 反汇编,不难定位到如下函数处:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第19张图片

可以明显地看出关机通过 ExitWindowsEx 实现,在 MSDN 上可以查找到关于这个函数的信息。

ExitWindowsEx 函数用于注销交互式用户、关闭系统、或关闭并重启系统。 它将 WM_QUERYENDSESSION 消息发送到所有应用程序,以确定它们是否可以终止。

这个函数有两个参数,uFlagsdwReason,前者表示电源操作的组合,后者表示关机/重启原因:

BOOL ExitWindowsEx(

[in] UINT uFlags,

[in] DWORD dwReason

);

我们在虚拟机中使用 API Monitor V2winlogon.exewininit.exe 等进程进行监视发现在关机时  winlogon.exe 也会调用 ExitWindowsEx 函数:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第20张图片

另外一个程序 shutdown.exe 由微软提供,位于 %systemdrive%\Windows\System32\ 下,通过对它的反汇编可以找到和电源有关的多个函数:

(1)ExitWindowsEx【关机/注销/重启】

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第21张图片

(2) NtInitiatePowerAction【睡眠/休眠】

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第22张图片

(3)InitiateShutdownW【关机/重启的高级操作】

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第23张图片

 (4)InitiateSystemShutdownExWInitiateShutdownW 的扩展】

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第24张图片

(5)此外还有 NtSetSystemPowerState 等强制关机函数:

这个函数和公开文档的 SetSystemPowerState 不是一个函数,NtSetSystemPowerState 有较多参数,下面是该函数的简单封装,其参数和 NtInitiatePowerAction 类似:

BOOL SystemPowerdown(IN POWER_ACTION SystemAction, IN SYSTEM_POWER_STATE MinSystemState, IN ULONG dwFlags)
{
    if (!NtSetSystemPowerState)
        return FALSE;

    DWORD dwRet = NtSetSystemPowerState(SystemAction, MinSystemState, dwFlags);
    if (dwRet == 0)
        return TRUE;
    else
        return FALSE;
}

综上,我们想到应该可以挂钩这类函数实现关机的拦截,下面逐一分析这些函数该如何拦截。

3.4.2 使用 Detours 内联挂钩

Detours 是一个在 Windows 平台上截获任意 Win32 函数调用的工具库。Detours 使用一个无条件转移指令来替换目标函数的最初几条指令,将控制流转移到一个用户提供的钩子拦截函数。而目标函数中的一些指令被保存在一个被称为“trampoline” (跳板)的函数中。

这些指令包括目标函数中被替换的代码以及一个重新跳转到目标函数的无条件分支。而钩子拦截函数可以替换目标函数,或者通过执行“trampoline”函数的时候将目标函数作为子程序来调用的办法来扩展功能。

Detours 定义了三个概念:

  • Target 函数:要拦截的函数,通常为 Windows 的已知 API,也可以是未导出的函数入口地址
  • Trampoline 函数:Target 函数的部分复制品。因为 Detours 将会改写 Target 函数,所以先把Target 函数的前5个字节复制保存好,一方面仍然保存 Target 函数的过程调用语义,另一方面便于以后的恢复。
  • Detour 函数:用来替代 Target 函数的钩子函数。

在 x86 平台上,DetoursTarget 函数的开头加入 JMP Address_of_ Detour_ Function 指令(共5个字节)把对 Target 函数 的调用引导到自己的 Detour 函数, 并把 Target 函数的开头的5个字节加上 JMP Address_of_ Target _ Function + 5 共10个字节作为 Trampoline 函数保存下来。

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第25张图片

在进行内联挂钩的时候,要特别注意多核 CPU 在 Hook & Replace 过程中的影响,因为多个线程有可能"同时"调用同一个函数地址,为了解决这个问题,一个好的做法是在 Inline Hook 的过程中,把当前进程的所有线程都挂起。通过 CreateToolhelp32SnapshotSuspendThread 的配合,在完成 Inline Hook 后再恢复线程。

下面以挂钩 ExitWindowsEx 为例,讲解如何进行内联挂钩。

方法一:我们可以根据 Inline Hook 的原理手动实现挂钩,例如:

/*__declspec(naked)*/ void MyExitWindowsEx(){
	__asm
	{
	    call testMsgBox;
	    jmp _ExitWindowsExAddTwoByte
	}
}


// 适用于 Win7 上 User32.dll 的内联挂钩
void hook_ExitWindowsEx() {
	HMODULE hUser32 = GetModuleHandleW(L"user32.dll");
	char* pOldExitWindowsEx = reinterpret_cast(GetProcAddress(hUser32, "ExitWindowsEx"));

	// NOP 掉 5 字节
	const int iLengthCopy = 7;
	if (pOldExitWindowsEx != nullptr) {
		_copyNtShutdownSystem = VirtualAlloc(0, 1024, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
		char* pNewAddr = reinterpret_cast(_copyNtShutdownSystem);

		char* pnop = pOldExitWindowsEx - 5;
		char aa = *pOldExitWindowsEx;
		char bb = *(pOldExitWindowsEx + 1);

		if (static_cast(0x8b) == *pOldExitWindowsEx && static_cast(0xff) == *(pOldExitWindowsEx + 1)) {
			DWORD oldshutdownProtect = 0;
			if (VirtualProtect(pOldExitWindowsEx - 5, iLengthCopy, PAGE_EXECUTE_READWRITE, &oldshutdownProtect)) {
				*pOldExitWindowsEx = static_cast(0xeB); // jmp short
				*reinterpret_cast(pOldExitWindowsEx + 1) = static_cast(-0x7); // addr
				*pnop = static_cast(0xe9); // jmp
				*reinterpret_cast(pnop + 1) = reinterpret_cast(MyExitWindowsEx) - reinterpret_cast(pnop + 5); // addr
				_ExitWindowsExAddTwoByte = pOldExitWindowsEx + 2;
				VirtualProtect(pOldExitWindowsEx - 5, iLengthCopy, oldshutdownProtect, nullptr);
			}
		}
	}
	return;
}

方法二:我们可以使用上文提到的比较成熟的第三方库,如 Detours 或者 MinHook 等,他们在实现内联挂钩方面的代码非常稳定。 首先,创建一个 Win32 动态链接库项目,然后需要在 NuGet 中为当前项目安装 detours 库。

随后,引入头文件和链接库文件:

#include "detours.h"
#pragma comment(lib, "detours.lib")

随后我们需要一个指针存放原函数地址:

PVOID fpExitWindowsEx = NULL;

随后我们需要定义一个和目标函数参数一样的函数,同时给出目标函数的定义:

// 目标函数的定义
typedef BOOL (WINAPI* __funcExitWindowsEx)(
    _In_ UINT  uFlags,
    _In_ DWORD dwReason
);

// 拦截注销/重启之类的钩子函数
BOOL WINAPI HookedExitWindowsEx(
    _In_ UINT  uFlags,
    _In_ DWORD dwReason
);

随后,我们需要编写钩子的事务过程函数:

挂钩时,首先使用 DetourFindFunction 获取目标函数的地址,并保存到 fpExitWindowsEx 中。

然后,调用 DetourAttach 将钩子打到目标函数入口处。

// 挂钩过程
void StartHookingFunction()
{
    // 开始事务
    DetourTransactionBegin();
    // 更新线程信息  
    DetourUpdateThread(GetCurrentThread());

    
    fpExitWindowsEx =
        DetourFindFunction(
            "user32.dll",
            "ExitWindowsEx");

    // 将拦截的函数附加到原函数的地址上,
    // 这里可以拦截多个函数。
    
    DetourAttach(&(PVOID&)fpExitWindowsEx,
        HookedExitWindowsEx);

    // 结束事务
    DetourTransactionCommit();
}

卸载/脱钩的过程也要写好:

脱钩主要通过 DetourDetach 并传递相同的参数来实现。

// 脱钩过程
void UnmappHookedFunction()
{
    //开始事务
    DetourTransactionBegin();
    //更新线程信息 
    DetourUpdateThread(GetCurrentThread());

    //将拦截的函数从原函数的地址上解除,这里可以解除多个函数。
    DetourDetach(&(PVOID&)fpExitWindowsEx,
        HookedExitWindowsEx);

    //结束事务
    DetourTransactionCommit();
}

随后,我们可以编写好我们的钩子函数:

BOOL WINAPI HookedExitWindowsEx(
    _In_ UINT  uFlags,
    _In_ DWORD dwReason
)
{
    WCHAR lpMsg[64]{};
    WCHAR lpCap[] = L"Windows LogonManager";
    DWORD Result = 0;
    
    /* uFlags == 65536(win8注销),什么鬼?
        win 8.1 关机 4268041, 重启 73731, 注销 65536
        win 7 关机 65545, 重启65539, 注销 65536
        Vista 开始菜单关机 65545, 注销 65536, 重启 65539
        (^ 同 Win7)
    */
    switch(uFlags){
    case 65536:
        wsprintf(lpMsg, L"正在取消注销计算机的任务计划,请稍后......\n", 
            uFlags);
        break;
    case 4268041: case 65545:
        wsprintf(lpMsg, L"正在取消关闭计算机的任务计划,请稍后......\n", 
            uFlags);
        break;
    case 73731: case 65539:
        wsprintf(lpMsg, L"正在取消重启计算机的任务计划,请稍后......\n", 
            uFlags);
        break;
    default:
        wsprintf(lpMsg, 
          L"正在取消用户发起的电源操作,请稍后......\n未知参数 uFlags [%d]\n", 
          uFlags);
        break;
    }

    // 发出阻滞对话框
    SvcMessageBox(lpCap, lpMsg, 
        MB_OK | MB_APPLMODAL | MB_ICONINFORMATION, TRUE, Result);

    SetLastError(995);//995 = 由于线程退出或应用程序请求,I/O 操作已中止。
    return FALSE;
}

其中,消息对话框不能使用 MessageBox 函数。因为 Winlogon 进程位于安全桌面, MessageBox 函数会在当前线程桌面下创建窗口,而 Winlogon 桌面是当前活动桌面,我们的窗口无法穿透桌面曾而被我们看到。窗口无法在桌面上绘制,但是对话框确实阻滞了进程,此时我们将无法继续鼠标操作。于是,我们考虑采用 WTSGetActiveConsoleSessionIdWTSSendMessageW,将对话框发送到指定的会话(Session),也就是类似于 Session 0 穿透。

#include 
#pragma comment(lib, "WtsApi32.lib")

BOOL SvcMessageBox(LPWSTR lpCap, LPWSTR lpMsg, DWORD style, BOOL bWait, DWORD& result)
{
    if (NULL == lpMsg || NULL == lpCap)
        return FALSE;
    result = 0;
    DWORD sessionXId = WTSGetActiveConsoleSessionId();
    return WTSSendMessageW(WTS_CURRENT_SERVER_HANDLE, sessionXId,
        lpCap, (DWORD)wcslen(lpCap) * sizeof(DWORD),
        lpMsg, (DWORD)wcslen(lpMsg) * sizeof(DWORD),
        style, 0, &result, bWait);
}

关于 uFlags 为什么不是公开文档中的值的组合,这可能涉及到未公开的内容。我们只需要,分析出每个系统版本上的参数的值和对应的作用,然后对不同操作进行分类即可。 

然后,我们只需要在主函数中调用钩子过程以进行拦截。

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    DisableThreadLibraryCalls(hModule);
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        StartHookingFunction();
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        UnmappHookedFunction();
        break;
    }
    return TRUE;
}

下图展示了在 Win XP 上的测试效果:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第26张图片

这是在 Win 8 的效果:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第27张图片

使用内联挂钩修改这个函数,在 Win 8 之前的操作系统上普遍适用,但是在 Win 10 / 11 上则起不到作用。起初,我以为是不再使用该函数了,随后,在反汇编中依然可以看到该函数,只不过变成了延迟加载的函数( __imp_ DelayLoadFunction )。延迟调用就是在程序启动时不自动加载函数所在的链接库,而是等到需要使用的时候再加载,也就是说在调用前,程序是没有这个函数的,所以 Detours 挂钩会失效,这时候我们考虑到使用 IAT HOOKDelay IAT Hook 挂钩模块导入表和延迟加载导入表。

下面的代码实现了 IAT Hook ExitWindowsEx

#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
// Windows Header Files

#include 
#include 
#include 

#include 
#include 
#include 
#include 

#pragma comment(lib, "Advapi32.lib")
#pragma comment(lib, "Shlwapi.lib")
#pragma warning(disable:4996)


LPVOID _copyNtShutdownSystem = NULL;
LPVOID _ExitWindowsExAddTwoByte = NULL;


typedef BOOL(WINAPI* FuncExitWindowsEx)(_In_ UINT uFlags, _In_ DWORD dwReason);
FuncExitWindowsEx _OldExitWindowsEx = NULL;


BOOL WINAPI IATHookedFun(_In_ UINT uFlags, _In_ DWORD dwReason) {
	BOOL bRet = FALSE;
	static BOOL bNeedWarning = FALSE;

	if (bNeedWarning) {
		MessageBoxW(NULL, _TEXT("弹框提示"), _TEXT("提示"), MB_ICONINFORMATION | MB_OK);
	}
	// 调用原函数
	bRet = _OldExitWindowsEx(uFlags, dwReason);
	if (bRet) {
		bNeedWarning = TRUE;
	}
	return bRet;
	//return FALSE;
}



BYTE* getNtHdrs(BYTE* pe_buffer) {
	if (pe_buffer == NULL) return NULL;

	// 将 PE 缓冲区转换为 DOS 头结构
	IMAGE_DOS_HEADER* idh = (IMAGE_DOS_HEADER*)pe_buffer;

	// 验证 DOS 头的签名,以确保它是有效的 PE 文件
	if (idh->e_magic != IMAGE_DOS_SIGNATURE) {
		return NULL;
	}

	// 定义 PE 头允许的最大偏移量
	const LONG kMaxOffset = 1024;

	// 获取从 DOS 标头到 PE 标头的偏移量
	LONG pe_offset = idh->e_lfanew;

	// 验证到PE头的偏移量是否在允许的范围内
	if (pe_offset > kMaxOffset) return NULL;

	// 将偏移后的缓冲区地址转换为指向 PE 头结构体的指针
	IMAGE_NT_HEADERS32* inh = (IMAGE_NT_HEADERS32*)((BYTE*)pe_buffer + pe_offset);
	if (inh->Signature != IMAGE_NT_SIGNATURE) return NULL;
	return (BYTE*)inh;
}

IMAGE_DATA_DIRECTORY* getPeDir(PVOID pe_buffer, size_t dir_id) {
	if (dir_id >= IMAGE_NUMBEROF_DIRECTORY_ENTRIES) return NULL;

	// 从 PE 缓冲区获取 NT 头结构体的指针
	BYTE* nt_headers = getNtHdrs((BYTE*)pe_buffer);

	// 验证是否可以获得 NT 头
	if (nt_headers == NULL) return NULL;

	// 指向 PE 文件数据目录的指针
	IMAGE_DATA_DIRECTORY* peDir = NULL;

	// 将 NT 头转换为适当的结构体指针
	IMAGE_NT_HEADERS* nt_header = (IMAGE_NT_HEADERS*)nt_headers;

	// 获取具有指定 ID 的数据表地址
	peDir = &(nt_header->OptionalHeader.DataDirectory[dir_id]);
	if (peDir->VirtualAddress == NULL) {
		return NULL;
	}
	return peDir;
}

bool FixDelayIATHook(PVOID modulePtr) {
	// 获取模块导入表的地址
	IMAGE_DATA_DIRECTORY* importsDir = getPeDir(modulePtr, IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT);
	if (importsDir == nullptr)
		return false;

	size_t maxSize = importsDir->Size;            // 导入表大小
	size_t impAddr = importsDir->VirtualAddress;  // 导入表首地址

	// 从 User32.dll 库获取 ExitWindowSex 函数的地址
	size_t addrExitWindowsEx = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"User32"), "ExitWindowsEx"));

	// 迭代延迟导入描述符
	for (size_t parsedSize = 0; parsedSize < maxSize; parsedSize += sizeof(IMAGE_DELAYLOAD_DESCRIPTOR)) {
		IMAGE_DELAYLOAD_DESCRIPTOR* lib_desc = reinterpret_cast
			(impAddr + parsedSize + reinterpret_cast(modulePtr));

		// 检查延迟导入描述符是否为空
		if (lib_desc->ImportAddressTableRVA == 0 && lib_desc->ImportNameTableRVA == 0)
			break;

		// 获取链接库名称
		LPSTR lib_name = reinterpret_cast(reinterpret_cast(modulePtr) + lib_desc->DllNameRVA);

		size_t call_via = lib_desc->ImportAddressTableRVA;
		size_t thunk_addr = lib_desc->ImportNameTableRVA;

		// 如果名称表的偏移量为0,使用地址表
		if (thunk_addr == 0)
			thunk_addr = lib_desc->ImportAddressTableRVA;

		// 迭代导入表中的字段
		for (size_t offsetField = 0, offsetThunk = 0;; offsetField += sizeof(IMAGE_THUNK_DATA), offsetThunk += sizeof(IMAGE_THUNK_DATA)) {
			IMAGE_THUNK_DATA* fieldThunk = reinterpret_cast(reinterpret_cast(modulePtr) + offsetField + call_via);
			IMAGE_THUNK_DATA* orginThunk = reinterpret_cast(reinterpret_cast(modulePtr) + offsetThunk + thunk_addr);

			// 检查两个字段是否都为空以退出循环
			if (fieldThunk->u1.Function == 0 && orginThunk->u1.Function == 0)
				break;

			// 检查是否使用序号来获取函数的地址
			if (orginThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG32 || orginThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG64) {
				// 函数的地址也可以通过获取序号的后两个字节来获得
				size_t addrOld = reinterpret_cast(GetProcAddress(LoadLibraryA(lib_name),
					reinterpret_cast(orginThunk->u1.Ordinal & 0xFFFF)));
				continue;
			}
			else { // 使用函数名获取函数地址
				PIMAGE_IMPORT_BY_NAME by_name = reinterpret_cast(
					reinterpret_cast(modulePtr) + orginThunk->u1.AddressOfData);
				LPSTR func_name = reinterpret_cast(by_name->Name);
				size_t addrOld = reinterpret_cast(GetProcAddress(LoadLibraryA(lib_name), func_name));

				// 如果函数是“ExitWindowSex”,则执行钩子并解除内存保护
				if (_stricmp(func_name, "ExitWindowsEx") == 0) {
					DWORD dOldProtect = 0;
					size_t* pFuncAddr = reinterpret_cast(&fieldThunk->u1.Function);
					if (VirtualProtect(pFuncAddr, sizeof(size_t), PAGE_EXECUTE_READWRITE, &dOldProtect)) {
						fieldThunk->u1.Function = reinterpret_cast(IATHookedFun);            // 钩子函数
						VirtualProtect(pFuncAddr, sizeof(size_t), dOldProtect, &dOldProtect);
						_OldExitWindowsEx = reinterpret_cast(addrExitWindowsEx);  // 存储原始函数地址
						return true;
					}
					break;
				}
			}
		}
	}

	return true;
}



bool FixIATHook(PVOID modulePtr) {
	// 获取模块导入表的地址
	IMAGE_DATA_DIRECTORY* importsDir = getPeDir(modulePtr, IMAGE_DIRECTORY_ENTRY_IMPORT);
	if (importsDir == NULL)
		return false;

	// 获取导入表的大小和虚拟地址
	size_t maxSize = importsDir->Size;
	size_t impAddr = importsDir->VirtualAddress;

	// 从 User32 库获取“ExitWindowSex”函数的地址
	size_t addrExitWindowsEx = (size_t)GetProcAddress(GetModuleHandleW(L"User32.dll"), "ExitWindowsEx");

	// 迭代导入表中的导入库的描述符
	for (size_t parsedSize = 0; parsedSize < maxSize; parsedSize += sizeof(IMAGE_IMPORT_DESCRIPTOR)) {
		// 获取当前导入库的描述符
		IMAGE_IMPORT_DESCRIPTOR* lib_desc = (IMAGE_IMPORT_DESCRIPTOR*)(impAddr + parsedSize + (ULONG_PTR)modulePtr);
		if (lib_desc->OriginalFirstThunk == NULL && lib_desc->FirstThunk == NULL)
			break;

		// 获取导入库的名称
		LPSTR lib_name = (LPSTR)((size_t)modulePtr + lib_desc->Name);

		// 获取 thunks 的调用地址和函数指针
		size_t call_via = lib_desc->FirstThunk;
		size_t thunk_addr = lib_desc->OriginalFirstThunk;
		if (thunk_addr == NULL)
			thunk_addr = lib_desc->FirstThunk;

		// 迭代原始 thunk 和 thunk 字段
		for (size_t offsetField = 0, offsetThunk = 0;; offsetField += sizeof(IMAGE_THUNK_DATA), offsetThunk += sizeof(IMAGE_THUNK_DATA)) {
			// 获得当前 thunks
			IMAGE_THUNK_DATA* fieldThunk = (IMAGE_THUNK_DATA*)(size_t(modulePtr) + offsetField + call_via);
			IMAGE_THUNK_DATA* orginThunk = (IMAGE_THUNK_DATA*)(size_t(modulePtr) + offsetThunk + thunk_addr);

			// 验证是否已到达 thunks 的结尾
			if (fieldThunk->u1.Function == 0 && orginThunk->u1.Function == 0)
				break;

			PIMAGE_IMPORT_BY_NAME by_name = nullptr;
			LPSTR func_name = nullptr;
			size_t addrOld = NULL;

			// 验证是否按顺序或名称导入函数
			if (orginThunk->u1.Ordinal & (IMAGE_ORDINAL_FLAG32 | IMAGE_ORDINAL_FLAG64)) {
				// 按序号导入
				addrOld = (size_t)GetProcAddress(LoadLibraryA(lib_name), (char*)(orginThunk->u1.Ordinal & 0xFFFF));
				continue;
			}
			else {
				// 按名称导入
				by_name = (PIMAGE_IMPORT_BY_NAME)(size_t(modulePtr) + orginThunk->u1.AddressOfData);
				func_name = (LPSTR)by_name->Name;
				addrOld = (size_t)GetProcAddress(LoadLibraryA(lib_name), func_name);
			}

			// HOOK
			if (strcmpi(func_name, "ExitWindowsEx") == 0) {
				// 更改为指向钩子函数的地址
				DWORD dOldProtect = 0;
				size_t* pFuncAddr = (size_t*)&fieldThunk->u1.Function;
				if (VirtualProtect(pFuncAddr, sizeof(size_t), PAGE_EXECUTE_READWRITE, &dOldProtect)) {
					fieldThunk->u1.Function = (size_t)IATHookedFun;  // 钩子函数
					VirtualProtect(pFuncAddr, sizeof(size_t), dOldProtect, &dOldProtect);
				    _OldExitWindowsEx = (FuncExitWindowsEx)addrExitWindowsEx;  // 存储原始函数地址
					return true;
				}
			}
		}
	}
	return true;
}



BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     ) {
	switch (ul_reason_for_call) {
	case DLL_PROCESS_ATTACH: {
		HMODULE exeModule = GetModuleHandleW(NULL);
        // 调用挂钩过程
		FixIATHook(exeModule);
		FixDelayIATHook(exeModule);
		break;
	}
	case DLL_THREAD_ATTACH:
		break;
	case DLL_THREAD_DETACH:
		break;
	case DLL_PROCESS_DETACH:
        break;
	}
	return TRUE;
}

然而,结果是并不能够真正拦截关机,因为在 Win 10/11 调用 ExitWindowsEx 时,已经进入了关机的中间阶段,此时你会看到正在关机的全屏界面(这是 LogonUI.exe 的界面)。所以,挂钩这个函数只能实现在关机页面短暂地弹出一个对话框,并随着关机到最后一步 Winlogon 的退出而消失。

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第28张图片

3.4.3 挂钩 InitiateShutdownW 函数(Win 8/8.1)

Win 8/8.1 上,挂钩 ExitWindowsEx 会导致卡在“请稍后”页面(也是 LogonUI.exe)拉起的,而且使用 Alt+F4 等一些关机方式不会调用 ExitWindowsEx ,而是调用 InitiateShutdownW

所以我们需要把这个函数也挂钩起来即可,但需要有一个技巧,要把 LogonUI.exe 也杀死。下面是用于杀死进程的代码:

// 通过调用外部程序实现终止进程,比如taskkill.exe
inline void KillLogonUIProcess()
{
    WCHAR lpExePath[] = L"cmd.exe /c taskkill /F /IM LogonUI.exe";
    /* 根据进程名获取任意进程Id */
    DWORD  pid = 512;
    HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    /* 创建启动信息结构体 */
    STARTUPINFOEXW si;
    /* 初始化结构体 */
    ZeroMemory(&si, sizeof(si));
    /* 设置结构体成员 */
    si.StartupInfo.cb = sizeof(si);
    SIZE_T lpsize = 0;
    /* 用微软规定的特定的函数初始化结构体 */
    InitializeProcThreadAttributeList(NULL, 1, 0, &lpsize);
    /* 转换指针到正确类型 */
    char* temp = new char[lpsize];
    LPPROC_THREAD_ATTRIBUTE_LIST AttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)temp;
    /* 真正为结构体初始化属性参数 */
    InitializeProcThreadAttributeList(AttributeList, 1, 0, &lpsize);
    /* 用已构造的属性结构体更新属性表 */
    if (!UpdateProcThreadAttribute(AttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &handle, sizeof(HANDLE), NULL, NULL)) {
        return;
    }
    si.lpAttributeList = AttributeList;
    PROCESS_INFORMATION pi;
    ZeroMemory(&pi, sizeof(pi));
    ((__CreateProcessAsUserW)fpCreateProcessAsUserW)(
        NULL, 0, lpExePath, 0, 0, 0, 
        EXTENDED_STARTUPINFO_PRESENT, 
        0, 0, (LPSTARTUPINFOW)&si, &pi);
    DeleteProcThreadAttributeList(AttributeList);
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    delete[] temp;
}

那我们如何知道 LogonUI.exe 启动了呢?别急,我之前在 R3 下挂钩 AppInfo Service 的进程创建(用于创建管理员进程,他会拉起 consent.exe 进程)一文里面说到系统进程创建子进程用的是 CreateProcessAsUserW 函数,我们只要把这个函数顺便也挂钩起来即可(而且这里调用不会并发,不会出现问题):

// 全局变量用于保存进程句柄
HANDLE lpProcessHandle = NULL;

// 挂钩 CreateProcessAsUserW 监控进程创建
BOOL WINAPI HookedCreateProcessAsUserW(
    _In_opt_ HANDLE hToken,
    _In_opt_ LPCWSTR lpApplicationName,
    _Inout_opt_ LPWSTR lpCommandLine,
    _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
    _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In_ BOOL bInheritHandles,
    _In_ DWORD dwCreationFlags,
    _In_opt_ LPVOID lpEnvironment,
    _In_opt_ LPCWSTR lpCurrentDirectory,
    _In_ LPSTARTUPINFOW lpStartupInfo,
    _Out_ LPPROCESS_INFORMATION lpProcessInformation
)
{
    BOOL ret = FALSE;
    // 先调用原函数
    ret = ((__CreateProcessAsUserW)fpCreateProcessAsUserW)
        (hToken, lpApplicationName, lpCommandLine, lpProcessAttributes,
            lpThreadAttributes, bInheritHandles, dwCreationFlags,
            lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation);
    if(lpCommandLine != nullptr)
        if (wcsstr(lpCommandLine, L"LogonUI.exe"))
            lpProcessHandle = lpProcessInformation->hProcess;// 保存进程句柄
    return ret;// 返回函数
}

然后,在 HOOK InitiateShutdownW 时,只需要顺便关闭进程即可:

DWORD WINAPI HookedInitiateShutdownW(
    _In_opt_   LPWSTR lpMachineName,
    _In_opt_   LPWSTR lpMessage,
    _In_       DWORD  dwGracePeriod,
    _In_       DWORD  dwShutdownFlags,
    _In_       DWORD  dwReason
)
{
    WCHAR lpMsg[] = L"正在取消关闭/注销/重启计算机的任务计划,请稍后......";
    WCHAR lpCap[] = L"Windows LogonManager";
    DWORD Result = 0;
    BOOL IsKilled = FALSE;
    // 发出阻滞对话框
    SvcMessageBox(lpCap, lpMsg, MB_OK | MB_APPLMODAL | MB_ICONINFORMATION, TRUE, Result);
    // 尝试结束进程
    if(lpProcessHandle != INVALID_HANDLE_VALUE)
        IsKilled = TerminateProcess(lpProcessHandle, 0);

    if(!IsKilled)
    {
        KillLogonUIProcess();// 后手,如果前面失败就这个
    }
    return ERROR_INVALID_PARAMETER;
}

3.4.4 拦截强制关机/重启(WinXP/Win 7/ 8)

强制关机/重启时,winlogon.exe 不会立即处理,而是由 wininit.exe 进程首先进行处理。wininit.exe 进程通过 NtShutdownSystem 函数部署关机/重启例程,我们只需要再注入 WinInit 过程并挂钩该函数即可。

NtShutdownSystem 函数是无文档函数,它的定义如下:

typedef enum _SHUTDOWN_ACTION
{
    ShutdownNoReboot,
    ShutdownReboot,
    ShutdownPowerOff
} SHUTDOWN_ACTION, * PSHUTDOWN_ACTION;

typedef NTSTATUS (NTAPI* __NtShutdownSystem)(
    SHUTDOWN_ACTION dwAction
);

我们只需要在钩子函数中,返回 STATUS_INVALID_PARAMETER 而不做任何操作即可。

NTSTATUS NTAPI HookedNtShutdownSystem(
    SHUTDOWN_ACTION dwAction
)
{
    return STATUS_INVALID_PARAMETER;
}

3.4.5  拦截强制关机/重启(Win 10)

想要在 Win10 上注入 Wininit 进程不是一件容易的事情。 

WindowsVista 版本引入一种进程保护机制 (Process Protection),用于更进一步的控制进程的访问级别,在此之前,用户只需要使用 SeDebugPrivilege 令牌权限即可获取任意进程的所有访问权限;随后 Windows 8.1 在此进程保护的基础上,扩展引入了进程保护-轻量级机制 (Protected Process Light),简称 PPL 机制,其能提供更加细粒度化的进程访问权限控制。

关于进程保护机制的更为详细的介绍和研究可以看 itm4n 大佬的系列文章:

1.Do You Really Know About LSA Protection (RunAsPPL)?---- Posted Apr 7, 2021;

2.Bypassing LSA Protection in Userland ---- Posted Apr 22, 2021;

3.The End of PPLdump ---- Posted Jul 24, 2022;

4.Debugging Protected Processes ---- Posted Dec 4, 2022;

5.Bypassing PPL in Userland (again) ---- Posted Mar 17, 2023  Updated Jul 24, 2023.

文章最早从 LSA (lsass)进程的保护机制开始谈起,最后衍生到 Red Cursor 团队开发的 PPLKiller的技术分析,具体的限于篇幅和权限,这里就不再详细叙述。

你可以在这里获取到项目:PPLKiller(Red Cursor) ,另外 itm4n 还开发了扩展工具,可以对 PP/PPL 进行任意级别的升/降级,在 Github 就可以获取:PPLcontrol( itm4n ),下面是这个工具的演示。

C:\Temp>PPLcontrol.exe list

    PID | Level   | Signer
 -------+---------+----------------
      4 | PP  (2) | WinSystem (7)
    108 | PP  (2) | WinSystem (7)
    392 | PPL (1) | WinTcb (6)
    520 | PPL (1) | WinTcb (6)
    600 | PPL (1) | WinTcb (6)
    608 | PPL (1) | WinTcb (6)
    756 | PPL (1) | WinTcb (6)
   2092 | PP  (2) | WinSystem (7)
   3680 | PPL (1) | Antimalware (3)
   5840 | PPL (1) | Antimalware (3)
   7264 | PPL (1) | Windows (5)
   9508 | PP  (2) | WinTcb (6)
   1744 | PPL (1) | Windows (5)

[+] Enumerated 13 protected processes.

首先添加相同级别的进程保护之前,附加调试进程失败: 

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第29张图片 给普通进程添加 PPL

然后,取消保护或者添加相同级别的保护,成功附加调试进程: 

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第30张图片 取消 PPL

综上,我们在 Win 10/ 11 上,只有取消进程保护,才可以注入 wininit.exe 进程。Win 10 上可以使用利用 CVE-2019-16098 (签名漏洞移除内核回调),也就是这里 PPLKiller(Red Cursor) 版本,Win 11 上目前只能使用传统的 PPLKiller (Mattiwatti) 利用测试模式加载驱动程序在内核中取消回调,这种方法是相对复杂的,尤其是由 UEFI 安全模式启动的计算机,需要关闭 BIOS 中的安全启动,然后才能开启测试模式。

注意:PPLKiller(Red Cursor) 没有验证 DeviceIoControl 获得的地址是否为 NULL,在 Win 11 上运行会导致触发控制流防护(CFG),而蓝屏。

3.5 注意事项和局限性分析

以上的挂钩方法挂钩的函数较多,并且在 Win 10 / 11 上效果不是太好,Win 10 需要通过微星驱动程序的 I/O Read/Write 漏洞来首先 Bypass PP/PPL 保护,Win 11 由于修复了 DeviceIoControl 这个漏洞滥用 API,我们则必须使用测试模式加载驱动来干掉内核的 PPL 回调。微软推出了 PPL(Protected Process Light) 机制后,向 lsass、wininit 等进程注入代码变得极为困难。此外,过滤系统热键需要通过底层钩子来实现,有些热键依然无法捕获到。这使得我们之前的工作变得荒废。我迫切地需要一个方法来实现更高效的拦截,通过一段时间的研究,我找到了解决问题的一个切入点—— 那就是 RPC 调用,在 Win 10/ Win 11 上,甚至在之前的系统上,RPC 调用才是真正在关机时多进程调度的根源。

四、挂钩 RPC 调用——直接过滤 Winlogon 回调

【提示】该部分涉及部分未公开的内部结构和微软不推荐使用的函数,应用到发布的软件中时,请自行遵守相关要求。

我曾阅读过 heiheiabcd 写的通过修改具体的窗口回调中的操作 ID,将其指向无效的 ID 来拦截系统热键的方法(注:原文已经被删除,原文转载:《禁止Ctrl+Alt+Del、Win+L等任意系统热键》),但是这种方法取决于系统版本,不同的版本由于链接库的地址不同,需要分析新的内部地址,并且向后兼容性比较差,当然作者的分析具有独创性。我属于比较躲懒的人,我一直在想,有没有什么办法能够实现对 Winlogon 的各种操作实现稳定的挂钩,从而拦截我们关注的操作呢?通过一段时间的分析,终于发现了多个利用导出函数进行拦截的路径,其中一种更是可以拦截各种消息。在接下来的文章中,我会简明扼要地介绍这几种方法:

4.1 了解 Winlogon 的 RPC 过程调用

操作系统的很多过程都依赖于 LPC/RPCCOM/DCOM 组件。

下面的介绍摘选自 百度百科 和 MSDN:

进程间通信(IPC)是在多任务操作系统或联网的计算机之间运行的程序和进程所用的通信技术。有两种类型的进程间通信(IPC):

  • 本地过程调用(LPC) LPC 用在多任务操作系统中,使得同时运行的任务能互相会话。这些任务共享内存空间使任务同步和互相发送信息。
  • 远程过程调用(RPC) RPC 类似于 LPC,不是这里的重点。

本地过程调用

本地过程调用(LPC,Local Procedure Call,通常也被称为轻量过程调用或者本地进程间通信) 是一种由 Windows NT 内核提供的内部进程间通信方式。通过这一方式,同一计算机上的进程可以进行轻量的通信。在 Windows Vista 中,ALPC(Advanced Local Procedure Call,高级本地进程通信)替代了 LPC。ALPC 提供了一个高速可度量的通信机制,这样便于实现需要在用户模式下高速通信的用户模式驱动程序框架(UMDF,User-Mode Driver Framework)。

远程过程调用(略,需要的请自行去了解)

4.2 分析关键函数

Winlogon 通过 RPC 调用获取来自其他进程的异步调用信息,通过 API Monitor 可以看出:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第31张图片

Ndr64AsyncServerCallAll 函数用于服务端接受 RPC 消息,这个函数在 MSDN 上找不到有用的说明,它只有一个形参为指向 PRC_MESSAGE 结构体的指针,但是 PRC_MESSAGE 结构体的信息并未公开文档。

通过查阅并整理 Process Hacker(Sourceforge)ReactOS 等团队的逆向文献,我们最终得到了该函数的定义:

#define __RPC_FAR
#define RPC_MGR_EPV void
#define  RPC_ENTRY __stdcall

typedef void* LI_RPC_HANDLE;
typedef LI_RPC_HANDLE LRPC_BINDING_HANDLE;


typedef struct _LRPC_VERSION {
    unsigned short MajorVersion;
    unsigned short MinorVersion;
} LRPC_VERSION;

typedef struct _LRPC_SYNTAX_IDENTIFIER {
    GUID SyntaxGUID;
    LRPC_VERSION SyntaxVersion;
} LRPC_SYNTAX_IDENTIFIER, __RPC_FAR* LPRPC_SYNTAX_IDENTIFIER;

typedef struct _LRPC_MESSAGE
{
    LRPC_BINDING_HANDLE Handle;
    unsigned long DataRepresentation;// %lu
    void __RPC_FAR* Buffer;
    unsigned int BufferLength;
    unsigned int ProcNum;
    LPRPC_SYNTAX_IDENTIFIER TransferSyntax;
    void __RPC_FAR* RpcInterfaceInformation;
    void __RPC_FAR* ReservedForRuntime;
    RPC_MGR_EPV __RPC_FAR* ManagerEpv;
    void __RPC_FAR* ImportContext;
    unsigned long RpcFlags;
} LRPC_MESSAGE, __RPC_FAR* LPRPC_MESSAGE;


//--------------------------------------------------
typedef void (RPC_ENTRY* __Ndr64AsyncServerCallAll)(
    LPRPC_MESSAGE pRpcMsg
);

其中,PRC_MESSAGE 结构体的各个成员目前我理解的作用为(可能有误):

LRPC_MESSAGE 结构体

定义

typedef struct _LRPC_MESSAGE
{
    LRPC_BINDING_HANDLE Handle;
    unsigned long DataRepresentation;
    void __RPC_FAR* Buffer;
    unsigned int BufferLength;
    unsigned int ProcNum;
    LPRPC_SYNTAX_IDENTIFIER TransferSyntax;
    void __RPC_FAR* RpcInterfaceInformation;
    void __RPC_FAR* ReservedForRuntime;
    RPC_MGR_EPV __RPC_FAR* ManagerEpv;
    void __RPC_FAR* ImportContext;
    unsigned long RpcFlags;
} LRPC_MESSAGE, __RPC_FAR* LPRPC_MESSAGE;

参数

  • Handle

类型:RPC_BINDING_HANDLE 

服务器绑定句柄。其中包含 RPC 运行时库用于访问绑定信息的信息。服务器绑定句柄包含客户端与特定服务器建立关系所需的信息。任意数量的 RPC API 运行时例程都会返回可用于进行远程过程调用的服务器绑定句柄。

  • DataRepresentation

类型:unsigned long

NDR 规范定义的网络缓冲区的数据表示形式。系统调用默认值 0x10。

  • Buffer

类型:void *

指向网络缓冲区开头的指针。用于本地 RPC 调用时表示一段内存数据的首地址。

  • BufferLength

类型:unsigned int

Buffer 参数指向的有效数据区域的大小(以字节为单位)。

  • ProcNum

类型:unsigned int

保留以供内部使用,一般设置为 NULL。(具体作用还未知)

  • TransferSyntax

类型:LPRPC_SYNTAX_IDENTIFIER

指向将写入用于编码数据的接口标识的地址的指针。 pInterfaceId 由接口通用唯一标识符 UUID 和版本号组成。

  • RpcInterfaceInformation

类型:void *

对于服务器端的非对象 RPC 接口,它指向 RPC 服务器接口结构。 在客户端,它指向 RPC 客户端接口结构。 对于对象接口,它为 NULL。

  • ReservedForRuntime

类型:void *

保留用于运行时传递额外的扩展数据。

  • ManagerEpv

类型:RPC_MGR_EPV

管理器入口点向量 (EPV) 是保存函数指针的数组。 数组包含指向 IDL 文件中指定的函数实现的指针。 数组中的元素数设置为 IDL 文件中指定的函数数。按照约定,包含接口和类型库定义的文件称为 IDL 文件,其文件扩展名为 .idl。 实际上,MIDL 编译器将分析接口定义文件,而不考虑其扩展名。 接口由关键字 (keyword) 接口标识。

  • ImportContext

类型:void *

保留,设置为 NULL,作用未知。

  • RpcFlags

类型:unsigned long

 RPC 调用的状态码。有时也使用 ProcNum 参数。

状态码可以是下表所列举的标志位的组合:

RPC_FLAGS_VALID_BIT 0x00008000
RPC_CONTEXT_HANDLE_DEFAULT_GUARD ((void*)0xfffff00d)
RPC_CONTEXT_HANDLE_DEFAULT_FLAGS 0x00000000
RPC_CONTEXT_HANDLE_FLAGS 0x30000000
RPC_CONTEXT_HANDLE_SERIALIZE 0x10000000
RPC_CONTEXT_HANDLE_DONT_SERIALIZE 0x20000000
RPC_TYPE_STRICT_CONTEXT_HANDLE 0x40000000
RPC_NCA_FLAGS_DEFAULT 0x00000000
RPC_NCA_FLAGS_IDEMPOTENT 0x00000001
RPC_NCA_FLAGS_BROADCAST 0x00000002
RPC_NCA_FLAGS_MAYBE 0x00000004
RPC_BUFFER_COMPLETE 0x00001000
RPC_BUFFER_PARTIAL 0x00002000
RPC_BUFFER_EXTRA 0x00004000
RPC_BUFFER_ASYNC 0x00008000
RPC_BUFFER_NONOTIFY 0x00010000
RPCFLG_MESSAGE 0x01000000
RPCFLG_HAS_MULTI_SYNTAXES 0x02000000
RPCFLG_HAS_CALLBACK 0x04000000
RPCFLG_AUTO_COMPLETE 0x08000000
RPCFLG_LOCAL_CALL 0x10000000
RPCFLG_INPUT_SYNCHRONOUS 0x20000000
RPCFLG_ASYNCHRONOUS 0x40000000
RPCFLG_NON_NDR 0x80000000

例如,当 Async RPC (异步 RPC) 过程使用 Buffer 传递字符串数据时,如果信息传递成功,则返回的标志位应该是 RPC_BUFFER_COMPLETE | RPC_BUFFER_ASYNC (36864) 的组合。

在调用了函数后,开始了熟悉的 RPC 调用流程:

首先进行字符串绑定,RpcStringBindingCompose 函数用于创建字符串绑定句柄。在这个环节他会将多个表示具体操作信息的字符串进行合并,随后会调用 RpcBindingFromStringBindingW 根据合并的字符串生成 RPC 信息的绑定句柄。

(记住这两个地址对理解后面的过程很重要)

这里 0x000000d388f7ede0 地址是绑定的字符串地址。

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第32张图片

 这里 0x0000022b6ce4cd20 地址是绑定的句柄地址。

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第33张图片

这两个函数的定义如下:

RPC_STATUS RpcStringBindingComposeW(

RPC_WSTR ObjUuid,

RPC_WSTR ProtSeq,

RPC_WSTR NetworkAddr,

RPC_WSTR Endpoint,

RPC_WSTR Options,

RPC_WSTR *StringBinding

);

RPC_STATUS RpcBindingFromStringBindingW(

RPC_WSTR StringBinding,

RPC_BINDING_HANDLE *Binding

);

两种典型的合并/绑定方式如下:

(1)第一种是忽略 Endpoint 参数,将协议序列名称和对象的 UUID 绑定为一个字符串:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第34张图片

(2)第二种是忽略 ObjUuid 参数,将协议序列名称和与协议序列对应的终结点对象绑定为一个字符串:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第35张图片

上面的操作类型发生在一次过程的开始和结束。

接下来就是要为句柄设置安全对象信息,RpcBindingSetAuthInfoEx 函数设置绑定句柄的身份验证、授权和安全服务质量信息。RpcBindingSetAuthInfoEx 函数接受由 RpcBindingFromStringBindingW 绑定的句柄 (Binding 和 StringBinding 不同, StringBinding 只表示字符串)。

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第36张图片

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第37张图片

随后由 RpcStringFree 函数释放由 RPC 运行时库分配的字符串。这里是释放 RpcStringBindingCompose 创建的字符串 StringBinding。

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第38张图片

然后,程序需要确定远程应用程序是否正在侦听 RPC 调用,需要调用 RpcMgmtIsServerListening 函数,并将参数设置为为该应用程序指定服务器绑定的句柄 (Binding)。这里就是异步调用等待远程过程完成,这个函数被 Winlogon 的多个回调所采用, IDA 的交叉引用信息就可以看出 他是来自 WluiiWaitForServer 函数,看结构显然是用于等待的:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第39张图片

过程完成了,该函数才返回,最后调用 RpcBindingFree 释放绑定的句柄 (Binding)。

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第40张图片

至此,一次简单的 PRC 调用过程已经分析差不多了。

通过 IDA 可以进一步找到该过程的函数,下面是给出的伪代码:

__int64 __fastcall WluiiStartupImpl(unsigned int a1, void *a2, DWORD *a3)
{
  int v6; // edi
  DWORD CurrentProcessId; // esi
  DWORD SharedEvents; // ebx
  __int64 dwProcessId; // rcx
  int v10; // eax
  RPC_WSTR StringBinding; // [rsp+68h] [rbp-A0h] BYREF
  DWORD cbSid; // [rsp+70h] [rbp-98h] BYREF
  DWORD cchReferencedDomainName; // [rsp+74h] [rbp-94h] BYREF
  DWORD cchName[2]; // [rsp+78h] [rbp-90h] BYREF
  struct _PROCESS_INFORMATION ProcessInformation; // [rsp+80h] [rbp-88h] BYREF
  struct _STARTUPINFOW StartupInfo; // [rsp+98h] [rbp-70h] BYREF
  RPC_SECURITY_QOS SecurityQOS; // [rsp+108h] [rbp+0h] BYREF
  WCHAR ReferencedDomainName[16]; // [rsp+118h] [rbp+10h] BYREF
  char pSid[80]; // [rsp+138h] [rbp+30h] BYREF
  unsigned __int16 ObjUuid[40]; // [rsp+188h] [rbp+80h] BYREF
  WCHAR CommandLine[256]; // [rsp+1D8h] [rbp+D0h] BYREF
  unsigned __int16 ServerPrincName[280]; // [rsp+3D8h] [rbp+2D0h] BYREF
  WCHAR Name[264]; // [rsp+608h] [rbp+500h] BYREF
  // 启动事件日志
  *a3 = 0;
  if ( *(_QWORD *)&g_TraceRegHandle
    && (unsigned __int8)EtwEventEnabled(*(_QWORD *)&g_TraceRegHandle, &WLEvt_WluiServerStartup_Start) )
  {
    EtwEventWrite(*(_QWORD *)&g_TraceRegHandle, &WLEvt_WluiServerStartup_Start, 0i64, 0i64);
  }
  if ( (a1 & 0x40000000) != 0 )
  {
    v6 = 1;
    CurrentProcessId = GetCurrentProcessId();
  }
  else
  {
    v6 = 0;
    CurrentProcessId = 0;
  }
  StringBinding = 0i64;
  // 分析需要进行的操作
  if ( a1 )
  {
    SharedEvents = WluiiCreateSharedEvents();
    if ( SharedEvents )
      goto LABEL_40;
    if ( v6 )
    {
      // 准备 LogonUI 的启动参数
      if ( (int)StringCchPrintfW(
                  CommandLine,
                  0x100ui64,
                  L"\"%s\" /flags:0x%lx /state0:0x%lx /state1:0x%lx /testclientprocessid:0x%08x",
                  L"LogonUI.exe",
                  dword_1400D1610,
                  CurrentProcessId,
                  _wnfState.Data[0],
                  _wnfState.Data[1]) >= 0 )
        goto LABEL_10;
    }
    else if ( (int)StringCchPrintfW(
                     CommandLine,
                     0x100ui64,
                     L"\"%s\" /flags:0x%lx /state0:0x%lx /state1:0x%lx",
                     L"LogonUI.exe",
                     dword_1400D1610,
                     _wnfState.Data[0],
                     _wnfState.Data[1]) >= 0 )
    {
LABEL_10:
      memset_0(&StartupInfo, 0, sizeof(StartupInfo));
      StartupInfo.cb = 104;
      StartupInfo.lpDesktop = L"Winsta0\\Winlogon";
      if ( v6 )
        StartupInfo.lpDesktop = 0i64;
      if ( a2 )
      {
        // 继承虚拟令牌,创建 SYSTEM 进程,可以是 LogonUI, 也可以是 Taskmgr
        if ( CreateProcessAsUserW(
               a2,
               0i64,
               CommandLine,
               0i64,
               0i64,
               1,  // 继承令牌
               0,
               0i64,
               0i64,
               &StartupInfo,
               &ProcessInformation) )
        {
LABEL_14:
          hTargetProcessHandle = ProcessInformation.hProcess;
          CloseHandle(ProcessInformation.hThread);
          dwProcessId = ProcessInformation.dwProcessId;
          *a3 = ProcessInformation.dwProcessId;
          RegisterLogonProcess(dwProcessId, 0i64);
          goto LABEL_15;
        }
      }   // 和当前用户桌面级别有关,此时调用 CreateProcessW 创建进程。
      else if ( CreateProcessW(0i64, CommandLine, 0i64, 0i64, 1, 0, 0i64, 0i64, &StartupInfo, &ProcessInformation) )
      {
        goto LABEL_14;
      }
      SharedEvents = GetLastError();
      if ( !SharedEvents )
        goto LABEL_27;
LABEL_40:
      WLEventWrite(&WLEvt_WluiServerStartupFailure);
      goto LABEL_27;
    }
    SharedEvents = 122;
    goto LABEL_40;
  }
LABEL_15:
  if ( !v6 )
  {
    CurrentProcessId = NtCurrentPeb()->SessionId;
    if ( CurrentProcessId == (unsigned int)RtlGetCurrentServiceSessionId() )
      CurrentProcessId = RtlGetActiveConsoleId();
  }
  v10 = StringCchPrintfW(ObjUuid, 0x25ui64, L"3BDB59A0-D736-4D44-9074-C1EE%08X", 
    CurrentProcessId); // 准备 UUID 绑定模式的参数
  if ( v10 < 0 )
  {
    SharedEvents = (unsigned __int16)v10;
    if ( (_WORD)v10 )
      goto LABEL_40;
  }
  // 绑定字符串,生成 RPC 字符串句柄
  SharedEvents = RpcStringBindingComposeW(ObjUuid, (RPC_WSTR)L"ncalrpc", 0i64, 0i64, 0i64, &StringBinding);
  if ( SharedEvents )
    goto LABEL_40;
  SharedEvents = RpcBindingFromStringBindingW(StringBinding, &Binding);
  if ( SharedEvents )
    goto LABEL_40;
  if ( !v6 )
  {
    // 查询 SID
    ServerPrincName[272] = 0;
    cbSid = 68;
    if ( CreateWellKnownSid(WinLocalSystemSid, 0i64, pSid, &cbSid) )
    {// WinLocalSystemSid == 0x16
      cchName[0] = 257;
      cchReferencedDomainName = 16;
      if ( LookupAccountSidLocalW(
             pSid,
             Name,
             cchName,
             ReferencedDomainName,
             &cchReferencedDomainName,
             (PSID_NAME_USE)&cchName[1]) )
      {
        if ( (int)StringCchPrintfW(ServerPrincName, 0x111ui64, L"%s\\%s", ReferencedDomainName, Name) >= 0 )
        {
          SecurityQOS.Version = 1;
          *(_QWORD *)&SecurityQOS.Capabilities = 1i64;
          SecurityQOS.ImpersonationType = 2;
          while ( 1 )
          {
            // 绑定 SID 和 安全属性 信息
            SharedEvents = RpcBindingSetAuthInfoExW(Binding, ServerPrincName, 6u, 0xAu, 0i64, 0, &SecurityQOS);
            if ( !SharedEvents )
              break;
            Sleep(0x12Cu);
          }
        }
      }
    }
  }
LABEL_27:
  if ( StringBinding ) // 删除字符串占用的内存
    RpcStringFreeW(&StringBinding);
  if ( SharedEvents )
    WluiiShutdownImpl(a1); // RPC 收尾工作
  if ( *(_QWORD *)&g_TraceRegHandle
    && (unsigned __int8)EtwEventEnabled(*(_QWORD *)&g_TraceRegHandle, &WLEvt_WluiServerStartup_Stop) )
  { // 事件日志记录
    EtwEventWrite(*(_QWORD *)&g_TraceRegHandle, &WLEvt_WluiServerStartup_Stop, 0i64, 0i64);
  }
  return SharedEvents;
}

关于 LogonUI.exe 的参数一共有四个参数(NT 10),这个程序用于实现登录会话的多个 GUI 界面:

"LogonUI.exe" /testclientprocessid: /flags: /state0: /state1:

对于“/flags:”参数目前可以有两个值:0x0 和 0x4,与电源状态有关的操作为 0x4;其他操作,比如安全选项页面,参数为 0x0;

"/state0:" 和 "/state1:" 参数是 COM 调用 LogonUIController 类的虚表函数中继承的第 6 个接口,它调用了 IUserSettingManagerstate 是必要的参数,这个类的具体实现封装在 LogonUIController.dll 中 (%System32% 根目录)。具体作用暂未知,不过 "/state0:" 是运行时变化的,"/state1:" 似乎是一个几乎不变的值:0x41c64e6d。

下面是初步分析的一部分过程,如有错误望指点:

CoCreateInstance 首先检索了 CLSID_LogonUIController 关联的类对象,LPVOID ppv 返回的是一个存放多个接口地址的数组,即 int ppv[Len],据分析实际继承的有 13 个接口函数。

LogonUI.exe 准备完参数后就直接通过 COM 服务器的接口继续登录会话:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第41张图片

LogonUIController.dll 的虚表(部分):

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第42张图片

以上只是简单对 RPC 过程的分析,可能多有不足。

4.3 过滤方法一:拦截 RPC 的字符串句柄绑定过程

RPC 字符串句柄绑定的过程中可以通过拦截 RpcStringBindingComposeW 函数实现 RPC 操作的拦截。但是由于这个函数绑定的参数情况较多,绑定失败会采用其他方式传递参数,就像上文列举的那样,以 WindowsShutdown 等终结点模式传递失败时候,会改用 UUID 模式,这会导致挂钩的难度上升。下文给出的是拦截 WindowsShutdown 终结点的挂钩代码,在实践中发现,如果同时挂钩 RpcAsyncCompleteCall ,并等待 RpcStringBindingComposeW 函数返回空值之后,调用 RpcAsyncCompleteCall 并返回操作被取消的信息,可以避免系统改用其他模式的尝试。

#include "pch.h"
#include "detours.h"
#include 
#pragma comment(lib, "detours.lib")

PVOID _RpcBindingFromStringBindingW = NULL;


#define __RPC_FAR
#define RPC_NATIVE_API __stdcall
#define RPC_S_INVALID_STRING_UUID 1705L
#define RPC_S_INVALID_BINDING 1702L
#define RPC_S_OK ERROR_SUCCESS

typedef _Return_type_success_(return == 0) long RPC_STATUS;
typedef _Null_terminated_ wchar_t __RPC_FAR* RPC_WSTR;
typedef _Null_terminated_ const wchar_t* RPC_CWSTR;
typedef void* I_RPC_HANDLE;
typedef I_RPC_HANDLE RPC_BINDING_HANDLE;

typedef RPC_STATUS(RPC_NATIVE_API* __RpcBindingFromStringBindingW)(
    RPC_WSTR           StringBinding,
    RPC_BINDING_HANDLE* Binding
);

RPC_STATUS RPC_NATIVE_API MyRpcBindingFromStringBindingW(
    RPC_WSTR           StringBinding,
    RPC_BINDING_HANDLE* Binding
);

static void StartHookingFunction(void*);
static void UnmappHookedFunction(void*);

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    DisableThreadLibraryCalls(hModule);
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        _beginthread(StartHookingFunction, 0, NULL);
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        _beginthread(UnmappHookedFunction, 0, NULL);
        break;
    }
    return TRUE;
}


static void StartHookingFunction(void*)
{
    // 开始事务
    DetourTransactionBegin();
    // 更新线程信息  
    DetourUpdateThread(GetCurrentThread());
    // 获取函数指针
    _RpcBindingFromStringBindingW =
        DetourFindFunction(
            "rpcrt4.dll",
            "RpcBindingFromStringBindingW");

    // 将拦截的函数附加到原函数的地址上
    DetourAttach(&(PVOID&)_RpcBindingFromStringBindingW,
        MyRpcBindingFromStringBindingW);
    // 结束事务
    DetourTransactionCommit();
}


static void UnmappHookedFunction(void*)
{
    //开始事务
    DetourTransactionBegin();
    //更新线程信息 
    DetourUpdateThread(GetCurrentThread());

    //将拦截的函数从原函数的地址上解除,这里可以解除多个函数。
    DetourDetach(&(PVOID&)_RpcBindingFromStringBindingW,
        MyRpcBindingFromStringBindingW);
    //结束事务
    DetourTransactionCommit();
}


RPC_STATUS RPC_NATIVE_API MyRpcBindingFromStringBindingW(
    RPC_WSTR           StringBinding,
    RPC_BINDING_HANDLE* Binding
)
{
    if (!wcscmp(StringBinding, L"ncalrpc:[WindowsShutdown]"))
    {
        WCHAR wcstr[356]{};
        wsprintf(wcstr, L"已经阻止用户启动的关机计划!\nRPC调用关键字符串: %s\n", StringBinding);
        MessageBoxW(GetDesktopWindow(), wcstr, L"RpcStringBindingComposeW", MB_OK | MB_ICONWARNING | MB_SYSTEMMODAL);
        return RPC_S_OK;
    }
    return ((__RpcBindingFromStringBindingW)_RpcBindingFromStringBindingW)
        (StringBinding, Binding);
}

4.4 过滤方法二:拦截 Server 端异步回调过程

还记得一切的开端吗?如果从根源上掐断是否可以实现更好的效果?我们尝试去搞定送报员 Ndr64AsyncServerCallAll 函数:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第43张图片

在调用 Ndr64AsyncServerCallAll 函数前后,只有三个参数发生了变化:BufferBufferLength  和 RpcFlags。送报员到底做了什么事情?

Ndr64AsyncServerCallAll 函数下断点,打开内存编辑器,并在尝试相应的操作,比如按下 Ctrl + Alt + Del 时,观察触发断点时窗口中 Buffer 参数的值:

在内存编辑器中找到对应的地址,可以看到地址的开头 4~5 字节为一个 Code,这里是 04 04 00 00 00:

通过分析多个操作发现这里的前 12 字节都是有意义的(和 BufferLength = 12 一致),开头几个字节的掩码表示要进行的操作的代码。

以下是我基于 Win 11 分析出的掩码:

[ Windows 11 22H2 / 10.0.22621.XXXX ]
"0100000000" // 注销 KEY
"0100000003" // 重启 KEY

"0100000009" // 关机 KEY

"0104000000" // 资源管理器崩溃重启

"0005000000" // 以管理员身份启动 KEY(其中,第5,6字节处为可变值)

"0105000000" // 成功以管理员身份启动 KEY(其中,第5,6字节处为可变值)

"0202000000" // 注销后登陆 KEY
"0301000002" // 从深度睡眠中唤醒 KEY

"0304000000" // 注销后登陆 KEY(待定)

"0401000000" // 唤醒 KEY

"0404000000" // Ctrl+Alt+Del KEY(一次调用)
"0404000004" // Ctrl+Shift+Esc KEY(一次调用)
"0404000005" // WIN+L KEY(一次调用)
"0501000000" // 自动睡眠 KEY

"0502000000" // 切换用户

"0601000000" // 从深度睡眠中唤醒 KEY
"0601000002" // S3 睡眠阶段 1 KEY

"0701000002" // S3 睡眠阶段 2 KEY

"0704000000" // 操作已完成 KEY

"0c04000000" // DWM 崩溃恢复通知,第五位可变

"0d04000000" // 注销后登陆 KEY
"f f f f f f f f f f " // 切换用户

备注:这个表大部分都是已经确定了具体操作了的,在以前的系统上基本不变,但可能有几个发生变化。我推测操作应该是按 Code 的顺序排的,估计应该编码了一个表在内部,具体的 Code 和操作的联系仍然在研究。目前通过调试获得的这个表不全面。

通过过滤该函数的参数即可实现在 Win 10 / 11 上,直接拦截 Winlogon 回调,而且操作简单。

依然使用 Detours 库,钩子函数内的方法可以仿照下面的来写:

// 钩子例程
if (pRpcMsg->BufferLength == 0)
{
    ((__Ndr64AsyncServerCallAll)fpNdr64AsyncServerCallAll)(pRpcMsg);
    return;
}

char p[5] = { 0 };
char Strr[45] = { 0 };
int BufferMask[5] = { 0 };
DWORD Result = 0;

// 读取前 5 字节数据
if (!memcpy_s(BufferMask, (PVOID)iBaseAddress,
        , static_cast(5)))
    {
        sprintf_s(p, "%02x", BufferMask[i]);
        Buffer = Buffer + p;
    }
// ... do something to compare string Codes.

......

 下图展示了使用该技术实现的,电源选项拦截/通知的效果:

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第44张图片

屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)_第45张图片

五、通过 ICredentialProvider 接口重写凭据提供程序

微软提供了凭据提供程序接口 (Vista 及以上) 和 GINA (XP或更早) 等途径来自定义凭据处理对话。我们就可以通过凭据提供程序接口,ICredentialProvider 这个接口接口继承自 IUnknown 接口,调用它可以实现定制自己的登陆界面,就像 Wallpaper Engine/Lively Wallpaper 可以实现动态壁纸一样。

5.1 方法概述

MSDN 上,ICredentialProvider 接口具有以下方法:

ICredentialProvider::Advise

允许凭据提供程序通过回调接口在登录 UI 或凭据 UI 中启动事件。
ICredentialProvider::GetCredentialAt

获取特定凭据。
ICredentialProvider::GetCredentialCount

获取此凭据提供程序下的可用凭据数。
ICredentialProvider::GetFieldDescriptorAt

获取描述指定字段的元数据。
ICredentialProvider::GetFieldDescriptorCount

检索显示此提供程序凭据所需的 中的字段计数。
ICredentialProvider::SetSerialization

设置凭据提供程序的序列化特征。
ICredentialProvider::SetUsageScenario

定义凭据提供程序有效的方案。 每当初始化凭据提供程序时调用。
ICredentialProvider::UnAdvise

登录 UI 或凭据 UI 用于通知凭据提供程序不再接受事件回调。

可以通过该方法和之前的 RPC 检测技术配合,来自定义凭据提供程序(自定义登陆界面版面和动画)。有时间的话,我会单独写一篇利用该接口实现自定义登陆界面的博客。


后记

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

文章更新于:2023.10.29

你可能感兴趣的:(Windows,基础编程,windows,测试工具,微软,1024程序员节)