基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)

前言

在前一阵子发表的“屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)”文章中,详细介绍了多年来各种对 Windows 系统热键进行屏蔽的方法,同时也囊括了对关闭计算机过程的程序化拦截方式。在文中,我提到了一种基于 MS-RPC 公开文档的或未公开文档的接口,实现对系统中和登陆应用程序( winlogon.exe )关联的主要系统热键进行精准拦截的新方法。

但是限于篇幅和时间原因,未能够提供完整的方案和技术原理的分析,在这一系列文章中,我将从多个方面详细介绍此类基于 MS-RPC 进行热键屏蔽的方法。

远程过程调用 (RPC) 是进程间通信 (IPC) 的一种形式。它允许客户端调用由 RPC 服务器公开的过程。客户端可以像执行普通过程调用一样来调用此函数,几乎不需要为远程交互的细节编码。服务器可以托管在同一台机器的不同进程中,也可以托管在远程机器上。而 MS-RPC(也称 Windows RPC),是微软推出的用于创建分布式客户端/服务器程序的强大技术。

在 Windows 操作系统中,RPC 以多种支持的协议或传输方式运作。在研究本地计算机上的多种复杂交互机制时,尤其是在需要多个进程相互协作完成一件任务的过程中,往往涉及研究 MS-RPC 的一个派生过程,即本地过程调用(LPC)。在 MS-RPC 支持的多种协议序列中,微软提供了一种 Ncalrpc 协议被较多地运用于 Windows 默认多进程或服务之间的通信。该协议是一种用于在同一台计算机上进行本地进程间通信的 RPC 协议。它使用本地命名管道(Named Pipes)来传输数据,通常用于提高本地进程之间的效率。Ncalrpc 不涉及网络通信,因为它仅在同一计算机上的进程间进行通信。

在这篇文章中,详细分析上一篇文章中给出的方法。通过挂钩技术( Hooking )注入代码来修改在 x64 Windows 10/11 系统上, winlogon.exe 进程的 Ndr64AsyncServerCallAll 函数的 Buffer 参数,从而实现屏蔽系统热键、拦截计算机电源操作等功能。

[备注:这是在 x64 系统环境下的案例,x86-32 下则需要挂钩 NdrAsyncServerCall 函数。注意系统的处理器版本。理论上本文方法适用于 Win7 及以上操作系统,但目前只测试了 Win10/11。]

关键词:热键屏蔽;挂钩注入;NDR64 接口;Winlogon 进程

一、MS-RPC 的概念和使用意义

远程过程调用(RPC)是一种通信机制,它允许程序调用其他地址空间(通常是在不同计算机上)的过程或函数,就像本地调用一样。MS-RPC(Microsoft Remote Procedure Call)是微软Windows 环境中实现的 RPC 协议。它是一种用于在分布式系统中进行进程间通信的标准。远程过程调用(RPC)是一种编程模型,允许程序在不同的地址空间执行代码,并通过网络传输数据。

MS-RPC的主要使用场景是在分布式系统中。通过 RPC,应用程序可以在网络上的远程计算机上调用服务,而无需了解底层通信细节。MS-RPC 在 Windows 操作系统中被广泛使用,是许多基础组件和服务的通信基础。例如,Active Directory、COM+、DCOM(Distributed Component Object Model)等都使用 MS-RPC 进行通信。

在 Windows 操作系统中,不同的进程可能运行在不同的用户账户下,或者在不同的计算机上。MS-RPC 提供了一种标准化的方式来实现这些进程之间的通信,使得它们能够相互调用远程过程,实现分布式应用的功能。

MS-RPC 支持安全性和认证机制,以确保通信的安全性。通过集成 Windows 安全模型,它可以使用 Kerberos 协议进行身份验证,确保只有授权的用户或系统可以调用远程过程。

总的来说,MS-RPC 在 Windows 环境中是一种关键的通信协议,为分布式系统提供了一种方便而可靠的方式,使得不同的组件和服务可以在网络上相互协作。这对于构建复杂的分布式应用和支持企业级系统的通信需求至关重要。

二、拦截系统热键的一般方法

有些安全工具需要屏蔽一些系统热键,或者接管热键响应的工作。例如:VMware 虚拟机基于热键映射对虚拟系统的热键功能进行重定向和转为其他功能,这在一定程度上避免了双机器系统的热键冲突;部分游戏优化工具会提供系统级别的按键屏蔽功能来适应多元化的玩家按键习惯。系统热键往往不能通过简单的方法进行临时性的,非破坏性的拦截或重定向。研究一种稳定高效的系统热键屏蔽方法,对有此类开发需求的编程项目尤为重要。

通常,可以通过低级键盘消息挂钩( Low Level Keyboard Proc-Hook )来获取大多数系统热键的按键消息,如 Alt + Tab、Ctrl + Esc 等,但对于安全警示序列( SAS ) 热键不具有有效的屏蔽作用。在 XP 操作系统上,安全桌面运行一个名为 “SAS Window” 的窗口。由此,一般采用挂钩该窗口的方式来拦截 winlogon.exe 的 SAS 事件消息,并联合低级键盘消息挂钩,实现对系统热键的全屏蔽。

从 Vista 和 Win7 开始,基于 SAS 窗口的拦截变得无效,因为微软出于安全考虑,已经将经 “SAS Window” 代理的安全隔离桌面改为更为安全的序列隔离方式。 heiheiabcd 对全新操作系统下系统热键拦截的工作进行了一些探索,提出一种通过修改 WMsgKMessageHandler 消息回调中不同操作的 ID,将其指向无效的 ID 号来拦截系统热键的方法(原文转载《禁止Ctrl+Alt+Del、Win+L等任意系统热键》)。在文中, heiheiabcd 采用硬编码目标函数绝对地址的方法受限于系统版本,不同的系统版本由于相关链接库中目标代码的偏移地址不同,需要分析新的内部地址,向后兼容性比较差。目前一种已知的改进方法,就是采用目标指令搜索或者模式匹配算法,可以编写出适应性较好的程序。但是此类方法对逆向分析程序的流程结构并提取特征码的需求较高,且一旦相关模块更新,则潜在早期选取的特征码失效进而导致算法命中失败的缺陷。所以,程序中,往往选取多种特征码,或者基于分支判断、行为特征等方法的组合来优化搜索算法。

三、 Winlogon 响应热键的流程

3.1 API 调用树分析

按照需求,需要拦截到 Ctrl + Alt + Del 、Ctrl + Shift + Esc 、 Win + L 等系统热键。根据微软帮助文档 MSDN 的介绍,我们可以了解到 Winlogon 是控制该类系统热键的关键程序。当 Winlogon 初始化时,它会向系统注册 CTRL+ALT+DEL 等安全注意序列 (SAS) ,然后在 WinSta0 窗口工作站中创建三个桌面。至于创建热键的具体过程, Winlogon 在进程初始化时通过 WMsgClntInitialize 函数注册基于远程过程调用的回调接口,推测在此执行期间创建了相关系统热键的响应机制。

int64_t __fastcall WMsgClntInitialize(WLSM_GLOBAL_CONTEXT *WSMGlobalContext, int IsPresentKey)
{
  int64_t WMsgHandlerList[9]; // [rsp+30h] [rbp-48h] BYREF

  memset(WMsgHandlerList, 0, 0x40);
  if ( !IsPresentKey )
    return StartWMsgServer();
  WMsgHandlerList[0] = (int64_t)WMsgMessageHandler;
  WMsgHandlerList[1] = (int64_t)WMsgKMessageHandler;
  WMsgHandlerList[5] = (int64_t)WMsgNotifyHandler;
  WMsgHandlerList[2] = (int64_t)WMsgPSPHandler;
  WMsgHandlerList[3] = (int64_t)WMsgReconnectionUpdateHandler;
  WMsgHandlerList[4] = (int64_t)WMsgGetSwitchUserLogonInfoHandler;
  RegisterWMsgServer(WMsgHandlerList);
  return StartWMsgKServer(*WSMGlobalContext + 0xCC);
}

对于热键本身,本篇文章中不做过多的分析。

首先,借助 Rohitab Batra 构建的 API Monitor(V2) 工具,可以初步分析 Winlogon 响应热键时的 API 调用树。

在第一轮测试中,我们启动一个需要请求管理员权限的进程。Winlogon 进程首先调用了 Ndr64AsyncServerCallAll 函数,随后在该函数内部执行期间有内存的初始化操作,并且使用 RpcServerTestCancel 测试客户端是否取消了远程过程调用。“LogonUI Logon Window” 窗口是由登陆 UI 程序( LogonUI.exe )创建的安全窗口,并在该窗口上绘制用户账户控制( UAC )对话框。

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第1张图片

Ndr64AsyncServerCallAll 函数执行最后,调用 RpcAsyncCompleteCall 来通知客户端远程过程调用的完成结果。在整个过程完成后(提权对话框关闭时),再次调用 Ndr64AsyncServerCallAll 函数。

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第2张图片

在第二轮测试中,我们以按下 Ctrl + Shift + Esc 按键来启动资源管理器,监视的调用树如下:

首先,进程调用了 Ndr64AsyncServerCallAll 函数,随后在该函数内部执行期间有内存的初始化操作,并且使用 RpcServerTestCancel 测试客户端是否取消了远程过程调用,使用 RpcAsyncCompleteCall 来通知客户端远程过程调用的完成结果。

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第3张图片

随后,进入消息的响应环节,最终通过进程创建函数 CreateProcessAsUserW 来创建"launchtm.exe"进程,该进程进一步创建 TaskMgr.exe 进程:

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第4张图片

由此可以看出 Ndr64AsyncServerCallAll 函数是整个过程中的关键函数。

3.2 Ndr64AsyncServerCallAll 函数

void Ndr64AsyncServerCallAll(
  PRPC_MESSAGE pRpcMsg
);

Ndr64AsyncServerCallAll 函数用于服务端接受 RPC 消息,这个函数在 MSDN 上找不到有用的说明,它只有一个形参为指向 PRC_MESSAGE 结构体的指针,但是 PRC_MESSAGE 结构体的信息文档中解释的非常含糊、混乱。所以,本文结合一些研究和文档,将结构体的定义和参数解释整理如下:

 RPC_MESSAGE 结构体

定义

typedef struct _RPC_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;
} RPC_MESSAGE, __RPC_FAR* PRPC_MESSAGE;

参数

  • Handle

类型:RPC_BINDING_HANDLE 

服务器绑定句柄。作为内存地址,指向包含 RPC 运行时库用于访问绑定服务器信息的数据结构。服务器绑定句柄包含客户端与特定服务器建立关系所需的信息。该句柄实际指向 RPC SCALL 虚表函数的指针列表。

  • DataRepresentation

类型:unsigned long

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

  • Buffer

类型:void *

指向网络缓冲区开头的指针。用于本地 RPC 调用时传输函数的参数。

进一步解释:指向客户端和服务器共享的缓冲区,已经验证的客户端和服务器终结点的接口都可以读写该段数据区域上的信息。

  • BufferLength

类型:unsigned int

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

  • ProcNum

类型:unsigned int

即 Procedure Number,过程号的意思。ProcNum 是指定要调用的过程的数字或索引。每个接口可能有多个过程(函数),而 ProcNum 用于确定调用哪个具体的过程(函数)。

每个远程过程都有一个唯一的过程号,通过这个过程号,服务器可以确定客户端希望调用的是哪个过程(函数)。

调用过程的语法中,有多个函数在函数指针列表中,这是类似于数组的数据结构,使用 ProcNum 即可作为索引,获取需要的函数的指针。

  • TransferSyntax

类型:LPRPC_SYNTAX_IDENTIFIER

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

进一步解释:这个参数在 RPC 调用中告诉服务器要执行哪个远程接口的例程。通过查看该接口的信息,服务器可以了解要调用的接口的类型和版本等信息。同时,客户端也可以通过已知的 UUID 配对需要连接的服务器,这就是唯一标识的作用。

  • RpcInterfaceInformation

类型:void *

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

进一步解释:在服务器端,RpcInterfaceInformation 指针指向 RPC_SERVER_INTERFACE 结构,该结构保存了服务端程序接口信息;在客户端,则指向 RPC_CLIENT_INTERFACE 结构。

  • ReservedForRuntime

类型:void *

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

  • ManagerEpv

类型:RPC_MGR_EPV

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

进一步解释:ManagerEpv 是一个指向管理器(Manager)的入口点向量的指针。管理器是客户端和服务器之间通信的中介,负责将调用分派到相应的例程。
入口点向量是一个函数指针数组,其中包含管理器实现的各个例程(函数)的入口点。这个向量由 MIDL 编译器生成,它包含有关如何调用管理器函数的信息。

但是在 Vista 及以上系统中,一般不采用该字段,当 ManagerEpv 设置为 NULL 时使用 RpcInterfaceInformation 中的一个成员作为实际的 ManagerEpv。

  • ImportContext

类型:void *

推测为指向 RPC_IMPORT_CONTEXT_P 结构的指针。用于在客户端和服务器之间传递上下文信息,其中包括与名称服务相关的上下文、客户端提议的绑定句柄以及一个绑定向量,其中包含了多个绑定句柄。该字段在 Vista 及更高版本系统上不再支持,始终设置为 NULL。

  • RpcFlags

类型:unsigned long

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

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

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

以上为 RPC_MESSAGE 结构体各个成员的解释。

3.3  API Monitor 解析函数的参数

通过 API Monitor 解析函数的参数,结果如图所示。

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第5张图片

首先,需要关注 API Monitor 红色标记的在函数调用前后被修改的部分,这说明了这些参数可能与调用过程密切相关。

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

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第6张图片

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

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第7张图片

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

以下是在 Win 11 下监视并分析出的操作码(前 5 个字节):

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

"0100000009" // 关机 KEY

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

"0005000000" // 以管理员身份启动 KEY(从第 5 字节开始为可变值)

"0105000000" // 成功以管理员身份启动 KEY(从第 5 字节开始为可变值)

"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 崩溃恢复通知(从第 5 字节开始为可变值)

"0d04000000" // 注销后登陆 KEY
"f f f f f f f f f f " // 切换用户时出现该操作码

备注:该表表部分操作需要进一步的调试分析来确定具体功能。操作码 Vista 及更高版本系统上基本不变,但表格整理没有包括一些不常用的操作码。推测操作应该是按 Code 的顺序排的,具体的 Code 和操作的联系仍然在研究。

四、编写注入测试代码

4.1 初步分析分类方法

我们使用 Detours 库挂钩 Ndr64AsyncServerCallAll 函数,并在函数执行前处理 Buffer 参数指向的地址上的数据。即可实现在 Win 10 / 11 上,直接拦截 Winlogon 的远程过程,而且操作简单稳定。

经过分析操作码的按位特征,我们只需要 Buffer 偏移 0,偏移 1 以及偏移 4 的三个位来分类各种操作,下面是挂钩后对 Buffer 进行参数结组的一个样例伪代码:

/* 伪代码  Created By: lianyou_cth516 at 2024-01-08-18:57. */
const char MsgInterString[22][45] = {"", ""}; // 存储消息的描述字符串
int nCode = -1; // 消息的解释器编码(对 MsgInterString 字符串的索引)
char bufferMask[4] = { 0 }; // 用于存储特征码
// 基址
uint64_t iBaseAddress = reinterpret_cast(pRpcMsg->Buffer);
if (pRpcMsg->BufferLength == 0) // 忽略返回时状态
{
    ((__Ndr64AsyncServerCallAll)fpNdr64AsyncServerCallAll)(pRpcMsg);
    return;
}
// 从内存中复制特征码(低位 + 高位)
memcpy(&bufferMask[0], reinterpret_cast(iBaseAddress), sizeof(char));
memcpy(&bufferMask[1], reinterpret_cast(iBaseAddress + 1), sizeof(char));
memcpy(&bufferMask[2], reinterpret_cast(iBaseAddress + 4), sizeof(char));

switch(bufferMask[0]){
    case 0:
        // 请求以管理员身份启动
        break;
    case 1:
        switch(bufferMask[1]){
            case 4:
                // 重启资源管理器
            case 5:
                // 已经处理权限提升的请求
            case 0:
                switch(bufferMask[2]){
                    case 0:
                        // 注销计算机
                    case 3:
                        // 重启计算机
                    case 9:
                        // 关闭计算机
                    default: /* 处理未知操作通知 */ break;
                }
                break;
            default: /* 处理未知操作通知 */ break; // 结束分支
        }
        break;
    case 4:
        switch(bufferMask[2]){
            case 4:
                // Ctrl + Shift + Esc 快捷键被按下
            case 5:
                // Win + L 快捷键被按下
            case 0:
                switch(bufferMask[1]){
                    case 1:
                        // 从自动睡眠唤醒(实验性)
                    case 4:
                        // Ctrl + Alt + Del 快捷键被按下
                    default: /* 处理未知操作通知 */ break;
                }
                break;
            default: /* 处理未知操作通知 */ break; // 结束分支
        }
        break;
    case 3:
        switch(bufferMask[1]){
            case 1:
                 // 从深度睡眠中唤醒(L1,实验性)
            case 4:
                 // 注销用户凭证后(K1,实验性)
            default: /* 处理未知操作通知 */ break; // 结束分支
        }
        break;
    case 5:
        switch(bufferMask[1]){
            case 1:
                 // 自动睡眠
            case 2:
                 // 切换用户
            default: /* 处理未知操作通知 */ break; // 结束分支
        }
        break;
    case 6:
        switch(bufferMask[2]){
            case 0:
                // 从深度睡眠中唤醒(L2,实验性)
            case 2:
                // 进入深度睡眠(L3,实验性)
            default: /* 处理未知操作通知 */ break; // 结束分支
        }
        break;
    case 7:
        switch(bufferMask[1]){
            case 4:
                // 请求的操作已经完成。(除了提升权限的其他操作,完成处理时都会发生该消息)
            case 1:
                if(bufferMask[2] == 2) // 进入深度睡眠(L4,实验性)
                    else /* 处理未知操作通知 */ 
                default: /* 处理未知操作通知 */ break; // 结束分支
        }
        break;
    case 2:
        if(bufferMask[1] == 2 && bufferMask[2] == 0) // 注销用户凭证后(K2,实验性)
            else  /* 处理未知操作通知 */ 
        break;
    case 12:
        if(bufferMask[1] == 4) // DWM 崩溃重启
            else  /* 处理未知操作通知 */ 
        break;
    case 13:
        if(bufferMask[1] == 4 && bufferMask[2] == 0) // 注销用户凭证后 DWM 处理(K2,实验性)
            else  /* 处理未知操作通知 */ 
              break;
    case -1: /* 0xFFFFFFFF */
        if(bufferMask[1] == -1 && bufferMask[2] == -1) // 切换用户
            else  /* 处理未知操作通知 */ 
        break;
    default:
        /* 处理未知操作通知 */ 
        break;
}

4.2 解决特殊情况

在有程序请求提升管理员权限时,不能自动提升的程序会由 AppInfo Service (AIS) 启动提权通知会话。在这个过程中, Winlogon 参与验证的部分环节(分别是验证早期,和验证完成时),这与 Winlogon 在该过程中需要两次调用 Ndr64AsyncServerCallAll 相对应。 AIS 包括它的窗口交互处理进程 consent.exe 会等待 Winlogon 的远程过程调用返回(RpcAsyncCompleteCall)。在验证早期,需要 Winlogon 正确调用 Ndr64AsyncServerCallAll 函数,否则 AIS 的线程会处于上锁的状态。

AIS 处理验证的过程是一个队列,上一个提权过程未完成时,不会继续处理新的提权过程。如果在挂钩例程中直接返回,而不做任何处理,则会导致 AIS 死锁。此时,可以尝试三种方式避免该问题。

其一,枚举默认桌面的进程,查找并终止 consent.exe 进程,当有多个 consent.exe 进程时,排除死亡的进程(由运行时崩溃导致),随后结束第一个被创建的进程(基于时间戳);

其二,修改 Buffer 指向的内存的数据,将其操作码设置为无效操作码,可以使得远程过程调用失败;

其三,逆向分析  Ndr64AsyncServerCallAll 的内部实现,结合 IDA Pro 以及 WinDbgX 调试的结果修改远程过程的传输语法,自动取消远程过程。

对于前两种方法,实现比较简单。下面函数实现在 Winlogon 桌面下,查找 Default 桌面的 consent.exe 进程(假设只有一个),并结束该进程。

BOOL ConsentUIDetectionHandler() {
    HDESK hUser = OpenDesktopW(L"Default", 0, FALSE, GENERIC_ALL);
    HDESK hWinlogon = NULL;
    hWinlogon = GetThreadDesktop(GetCurrentThreadId());

    SetThreadDesktop(hUser);
    PROCESSENTRY32 entry{};
    entry.dwSize = sizeof(PROCESSENTRY32);
    int pid = -1;
    HANDLE prehandle = NULL;
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

    if (snapshot == INVALID_HANDLE_VALUE) {
        //puts("[-] Failed to create snapshot of processes.");
        return FALSE;
    }

    if (Process32First(snapshot, &entry)) {
        while (Process32Next(snapshot, &entry)) {// 排除死锁进程: entry.cntThreads != NULL
            SetLastError(0);
            prehandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
            if (prehandle == INVALID_HANDLE_VALUE && GetLastError() == 5)
            {
                pid = -1;
                continue;
            }
            CloseHandle(prehandle);
            prehandle = NULL;
            
            if (wcscmp(entry.szExeFile, L"consent.exe") == 0 && 
                entry.cntThreads != NULL) {
                pid = entry.th32ProcessID;
                break;
            }
            pid = -1;
        }
    }
    
    CloseHandle(snapshot);

    if (pid < 0) {
        //puts("[-] Could not find consent.exe");
        return FALSE;
    }

    // 结束进程
    prehandle = OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_TERMINATE, 
        FALSE, pid);
    if (prehandle == INVALID_HANDLE_VALUE && GetLastError() == 5)
    {
        return FALSE;
    }
    SetThreadDesktop(hWinlogon);

    return TerminateProcess(prehandle, 0);
}

实际执行的效果很好,并且不影响下一次执行提权过程。

第二种方法就是修改 Buffer 指向的内存上前 8 个字节表示的操作码,使其指向无效的操作码,比如都设置为 0xff(设置为 0xff 表示无效的参数),Buffer不能设置为 0这里还有一个方法:设置 BufferLength = 0,这时候会传入 0 参数,导致远程过程失败。

【更新】:将 BufferLength 设置为 0,可以更为快速地改变远程过程,并且,不改变原始的内存。此外,需要注意似乎在提权时候,UAC 对话框返回时会关闭 Winlogon 桌面下所有消息窗口,正在创建的消息窗口也会被关掉,所以,通过延时执行未挂钩的 Ndr64AsyncServerCallAll 函数可以使得消息窗口正常创建(也许应该将消息窗口单独放在一个线程)。下图是将 BufferLength 设置为 0时,终止调用的对话框。

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第8张图片

【更新2】:经多程序测试,将 BufferLength 设置为 0 的方法会导致内存不释放,和其他调试程序兼容性较差,容易导致 winlogon.exe 崩溃。故最终改为直接修改 Buffer 前 12 个字节为 0xff

4.3 完整的挂钩例程代码

下面给出挂钩例程的完整代码:

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "detours.h"
#include 
#include 
#include "rpc.h"

#pragma comment(lib, "detours.lib")
#pragma comment(lib, "WtsApi32.lib")
#pragma comment(lib, "Rpcrt4.lib")

PVOID fpNdr64AsyncServerCallAll = NULL;
void StartHookingFunction();
void UnmappHookedFunction();
BOOL SvcMessageBox(LPSTR lpCap, LPSTR lpMsg, DWORD style, BOOL bWait, DWORD& result);

#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;
    RPC_SERVER_INTERFACE* RpcInterfaceInformation; // void __RPC_FAR*
    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
);

void RPC_ENTRY HookedNdr64AsyncServerCallAll(
    LPRPC_MESSAGE pRpcMsg
);

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


// -------------------------------------------

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

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

    
    fpNdr64AsyncServerCallAll =
        DetourFindFunction(
            "rpcrt4.dll",
            "Ndr64AsyncServerCallAll");

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

    //结束事务
    DetourTransactionCommit();
}

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

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

    //结束事务
    DetourTransactionCommit();
}


void RPC_ENTRY HookedNdr64AsyncServerCallAll(
    LPRPC_MESSAGE pRpcMsg
)
{
    CHAR lpMsgCap[] = "Windows LogonManager";
    
    int nCode = -1; // 消息的解释器编码(对 MsgInterStr 字符串的索引)
    DWORD dwMsgResult = 0;     // WTSSendMessage 的返回值
    char bufferMask[4] = { 0 }; // 用于存储特征码
    const char MsgInterStr[23][45] = { 
        "有程序请求以管理员身份启动",    // 0
        "检测到资源管理器重启",          // 1
        "已经处理权限提升的请求",        // 2
        "正在请求注销这台计算机",        // 3
        "正在请求重启这台计算机",        // 4
        "正在请求关闭这台计算机",        // 5
        "Ctrl+Shift+Esc 快捷键被按下",   // 6
        "Win+L 快捷键被按下",            // 7
        "已经从自动睡眠状态唤醒",        // 8
        "Ctrl+Alt+Del 快捷键被按下",     // 9
        "已经从深度睡眠状态唤醒",        // 10
        "正在进行切换用户操作 1",        // 11
        "正在进入自动睡眠状态",          // 12
        "正在进行切换用户操作 2",        // 13
        "已经从深度睡眠状态唤醒",        // 14
        "正在进入深度睡眠状态 1",        // 15
        "请求的操作已经完成",            // 16
        "正在进入深度睡眠状态 2",        // 17
        "正在进行切换用户操作 3",        // 18
        "检测到 DWM 重启",               // 19
        "正在向 DWM 发送状态请求",       // 20
        "当前不进行有效操作",            // 21
        "目前未知的处理操作码"           // 22
    }; // 存储消息的描述字符串

    BOOL bWaitForCloseBox = FALSE; // 标识是否等待消息对话框的返回

    // 基址
    uint64_t iBaseAddress = reinterpret_cast(pRpcMsg->Buffer);
    
    // 忽略零长度缓冲区(更新:安全调用指针)
    if (pRpcMsg->BufferLength == 0 || pRpcMsg->Buffer == nullptr)
    {
        ((__Ndr64AsyncServerCallAll)fpNdr64AsyncServerCallAll)(pRpcMsg);
        return;
    }

    // 从内存中复制特征码(低位 + 高位)
    memcpy(&bufferMask[0], reinterpret_cast(iBaseAddress), sizeof(char));
    memcpy(&bufferMask[1], reinterpret_cast(iBaseAddress + 1), sizeof(char));
    memcpy(&bufferMask[2], reinterpret_cast(iBaseAddress + 4), sizeof(char));

    switch (bufferMask[0]) {
    case 0:
        // 请求以管理员身份启动
        bWaitForCloseBox = TRUE;
        nCode = 0;
        break;
    case 1:
        switch (bufferMask[1]) {
        case 4:
            // 重启资源管理器
            bWaitForCloseBox = TRUE;
            nCode = 1;
            break;
        case 5:
            // 已经处理权限提升的请求
            bWaitForCloseBox = FALSE;
            nCode = 2;
            break;
        case 0:
            switch (bufferMask[2]) {
            case 0:
                // 注销计算机
                bWaitForCloseBox = TRUE;
                nCode = 3;
                break;
            case 3:
                // 重启计算机
                bWaitForCloseBox = TRUE;
                nCode = 4;
                break;
            case 9:
                // 关闭计算机
                bWaitForCloseBox = TRUE;
                nCode = 5;
                break;
            default: /* 处理未知操作通知 */ 
                bWaitForCloseBox = FALSE;
                nCode = -1001;
                break;
            }
            break;
        default: /* 处理未知操作通知 */ 
            bWaitForCloseBox = FALSE;
            nCode = -1000;
            break; // 结束分支
        }
        break;
    case 4:
        switch (bufferMask[2]) {
        case 4:
            // Ctrl + Shift + Esc 快捷键被按下
            bWaitForCloseBox = TRUE;
            nCode = 6;
            break;
        case 5:
            // Win + L 快捷键被按下
            bWaitForCloseBox = TRUE;
            nCode = 7;
            break;
        case 0:
            switch (bufferMask[1]) {
            case 1:
                // 从自动睡眠唤醒(实验性)
                bWaitForCloseBox = FALSE;
                nCode = 8;
                break;
            case 4:
                // Ctrl + Alt + Del 快捷键被按下
                bWaitForCloseBox = TRUE;
                nCode = 9;
                break;
            default: /* 处理未知操作通知 */ 
                bWaitForCloseBox = FALSE;
                nCode = -1003;
                break;
            }
            break;
        default: /* 处理未知操作通知 */ 
            bWaitForCloseBox = FALSE;
            nCode = -1002;
            break; // 结束分支
        }
        break;
    case 3:
        switch (bufferMask[1]) {
        case 1:
            // 从深度睡眠中唤醒(L1,实验性)
            bWaitForCloseBox = FALSE;
            nCode = 10;
            break;
        case 4:
            // 注销用户凭证后(K1,实验性)
            bWaitForCloseBox = FALSE;
            nCode = 11;
            break;
        default: /* 处理未知操作通知 */ 
            bWaitForCloseBox = FALSE;
            nCode = -1004;
            break; // 结束分支
        }
        break;
    case 5:
        switch (bufferMask[1]) {
        case 1:
            // 自动睡眠
            bWaitForCloseBox = FALSE;
            nCode = 12;
            break;
        case 2:
            // 切换用户
            bWaitForCloseBox = FALSE;
            nCode = 13;
            break;
        default: /* 处理未知操作通知 */ 
            bWaitForCloseBox = FALSE;
            nCode = -1006;
            break; // 结束分支
        }
        break;
    case 6:
        switch (bufferMask[2]) {
        case 0:
            // 从深度睡眠中唤醒(L2,实验性)
            bWaitForCloseBox = FALSE;
            nCode = 14;
            break;
        case 2:
            // 进入深度睡眠(L3,实验性)
            bWaitForCloseBox = FALSE;
            nCode = 15;
            break;
        default: /* 处理未知操作通知 */ 
            bWaitForCloseBox = FALSE;
            nCode = -1008;
            break; // 结束分支
        }
        break;
    case 7:
        switch (bufferMask[1]) {
        case 4:
            // 请求的操作已经完成。(除了提升权限的其他操作,完成处理时都会发生该消息)
            bWaitForCloseBox = FALSE;  // TODO: Patch(20240111001) -- 将该值设置为 TRUE,导致注销时进程退出失败,若设置为 FALSE ,弹窗未能显示,但进程正常退出(判断为多线程处理问题)
            nCode = 16;
            break;
        case 1:
            if (bufferMask[2] == 2) // 进入深度睡眠(L4,实验性)
            {
                bWaitForCloseBox = FALSE;
                nCode = 17;
            }
            else /* 处理未知操作通知 */
            {
                bWaitForCloseBox = FALSE;
                nCode = -1011;
            }
            break;
        default: /* 处理未知操作通知 */
            bWaitForCloseBox = FALSE;
            nCode = -1010; 
            break; // 结束分支
        }
        break;
    case 2:
        if (bufferMask[1] == 2 && bufferMask[2] == 0) // 注销用户凭证后(K2,实验性)
        {
            bWaitForCloseBox = FALSE;
            nCode = 18;
        }
        else  /* 处理未知操作通知 */
        {
            bWaitForCloseBox = FALSE;
            nCode = -1012;
        }
        break;
    case 12:
        if (bufferMask[1] == 4) // DWM 崩溃重启
        {
            bWaitForCloseBox = TRUE;
            nCode = 19;
        }
        else  /* 处理未知操作通知 */
        {
            bWaitForCloseBox = FALSE;
            nCode = -1014;
        }
        break;
    case 13:
        if (bufferMask[1] == 4 && bufferMask[2] == 0) // 注销用户凭证后 DWM 处理(K2,实验性)
        {
            bWaitForCloseBox = FALSE;
            nCode = 20;
        }
        else  /* 处理未知操作通知 */
        {
            bWaitForCloseBox = FALSE;
            nCode = -1016;
        }
        break;
    case -1: /* 0xFFFFFFFF */
        if (bufferMask[1] == -1 && bufferMask[2] == -1) // 切换用户
        {
            bWaitForCloseBox = FALSE;
            nCode = 21;
        }
        else  /* 处理未知操作通知 */
        {
            bWaitForCloseBox = FALSE;
            nCode = -1018;
        }
        break;
    default:
        /* 处理未知操作通知 */
        bWaitForCloseBox = FALSE;
        nCode = -1020;
        break;
    }

    // 弹窗显示结果
    char lpMsgStr[45] = { 0 };
    if (nCode >= 0) strcpy_s(lpMsgStr, MsgInterStr[nCode]);
    else sprintf_s(lpMsgStr, "%s:%d,%d,%d\n", MsgInterStr[22],
        bufferMask[0], bufferMask[1], bufferMask[2]);

    DWORD MsgStyle = MB_APPLMODAL | MB_ICONINFORMATION | 
        (bWaitForCloseBox ? MB_YESNO : MB_OK);

    // 弹窗处理
    if (SvcMessageBox(lpMsgCap, lpMsgStr,
        MsgStyle,
        bWaitForCloseBox, dwMsgResult))
    {
        switch (dwMsgResult) {
        case IDASYNC:
        case IDYES:
            if (nCode == 2 || nCode == 16) Sleep(300); // 等待 UAC 窗口关闭
            break;
        default:
            /*
            * 修改 12 字节提权信息,导致提权失败,
            * 否则会一直等待提权,导致调用方死锁
            * 经过测试,任何调用,前 12 字节
            * 每个字节修改为 0xff 或者 0x0f 都可以阻止调用
            * 不建议直接将 BufferLength 设置为 0,
            * 那样的话和其他调试程序兼容性较差。
            * 2022.01.12 
            */
            memset(pRpcMsg->Buffer, 0xff, 12);
            break;
        }
        // 调用原函数
        return ((__Ndr64AsyncServerCallAll)fpNdr64AsyncServerCallAll)(pRpcMsg);
    }
}

将上面的代码编译成 DLL,并使用 LoadLibraryTool (V1) [代码见:利用 ZwCreateThreadEx 注入 DLL(Article-133710618)] 对 winlogon.exe 进行注入:

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第9张图片

然后,尝试以管理员身份启动程序,成功弹出对话框,我们点击“否”终止调用。

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第10张图片

此时,远程过程调用停止,资源管理器弹出对话框(设置无效的 Buffer 信息 0xff ):

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第11张图片

此外,测试其他功能热键,都可以被拦截到。

Ctrl + Shift + Esc (任务管理器)拦截效果:

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第12张图片

 Ctrl + Alt + Del(SAS 安全警示页面):

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第13张图片

电源操作:

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第14张图片

等等。 

总结&后记

通过挂钩技术( Hooking )注入代码来修改 winlogon.exe 的 Ndr64AsyncServerCallAll 函数的 Buffer 参数,从而实现屏蔽系统热键、拦截计算机电源操作等功能,可以实现在多个版本操作系统上的系统热键屏蔽。

上面提到的第三种方法需要对  PRC_MESSAGE 结构体的 RpcInterfaceInformation、ProcNum、Buffer 作进一步的分析。这涉及到基于 WinDbg 和 IDA Pro 的分析。将在下一篇博客中详细解释。

(先放一张图:)下一篇中,需要准备好 Procexp、WinDbg、IDA Pro、虚拟机等工具,需要有 x64 调试和逆向工程的基础。先透露一下,主要是针对  RpcInterfaceInformation 参数包含的信息进行深入研究。

基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)_第15张图片


涟幽516-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_59075481?type=blog

更新于:2024.1.11

你可能感兴趣的:(Windows,基础编程,rpc,windows,微软,交互,算法,c++)