当前位置: 首页 > news >正文

Hook 实现 Windows 系统热键屏蔽(二)

目录

前言

一、介绍用户账户控制(UAC)

1.1 什么是 UAC ?

2.2 UAC 运行机制的概述

2.3 分析 UAC 提权参数

二、 NdrAsyncServerCall 函数的分析

2.1 函数声明的解析

2.2 对 Winlogon 的逆向

2.3 对 rpcrt4 的静态分析

2.4 对 rpcrt4 的动态调试

2.5 分析可行的突破点

三、通过 Detours 挂钩实现

3.1 Buffer 参数解析代码

3.2 RpcServerTestCancel 热补丁代码

3.3 更多实例代码未来补充

四、总结

参考文献


本文出处链接:[https://blog.csdn.net/qq_59075481/article/details/135543495]。

前言

本系列包含介绍 Winlogon Message Rpc 的多篇文章,从多个方面详细分析利用 WMsg-RPC 进行热键屏蔽的方法。上一篇文章(传送门)侧重于修改 Buffer 参数的前序字节,达到拦截 Winlogon 调用,屏蔽热键的目的。接下来,我们将进一步分析  Ndr(64)AsyncServerCall(All) 函数的相关原理,从传输语法的角度对本地远程过程调用进行拦截。

在这篇文章中,讨论 winlogon.exe 进程响应应用程序请求提升管理员权限的一部分过程时所采用的 Ncalrpc 通信机制,通过挂钩技术( Hooking )注入代码来修改该线路上的关键点( Key Points),包括分支判断条件、缓冲区、函数返回值、指针对象等,来达到对相关通信过程的拦截。该方法可以扩展到所有的 WMsg LRpc 消息处理上。为了方便,文章目前只对 Windows 10/11(截至10.0.22631.3235) x64 系统下的机制进行研究。

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

关键词:Ncalrpc 协议;NDR64 接口;逆向工程;热键屏蔽;挂钩注入

系列文章:

  1. 屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)
  2. RPC-Hook 屏蔽系统热键(一)
  3. RPC-Hook 屏蔽系统热键(二)[本文]
  4. Windows 拦截系统睡眠

一、介绍用户账户控制(UAC)

1.1 什么是 UAC ?

用户帐户控制 (UAC) 是一项 Windows 安全功能,旨在保护操作系统免受未经授权的更改。当对系统的更改需要管理员级权限时,UAC 会通知用户,从而让用户有机会批准或拒绝更改。

UAC 允许所有用户使用 标准用户账户 登录到他们的计算机。使用 标准用户令牌 启动的进程可能会使用授予标准用户的访问权限来执行任务。例如,Windows 资源管理器会自动继承 标准用户级别 权限。任何使用 Windows 资源管理器启动 (例如,通过打开快捷方式) 的应用程序也 使用标准用户权限集 运行。大多数应用程序,包括操作系统附带的应用程序,都以这种方式正常工作。

其他应用程序(例如设计时未考虑安全设置的应用程序)可能需要更多权限才能成功运行。 这些类型的应用被称为 Legacy App

当用户尝试执行需要管理员权限的操作时,UAC 会触发同意提示。该提示通知用户即将发生权限更改,向用户要求获得继续操作的权限

  • 如果用户批准更改,则会使用 最高可用权限 执行该操作;
  • 如果用户未批准更改,则不会执行该操作,并且将 阻止请求更改的应用程序运行
UAC 提示界面

当应用需要使用超过标准用户权限运行时,UAC 允许用户使用其 管理员令牌 (即拥有管理员权限)而不是默认的 标准用户令牌 来运行应用。用户将继续在标准用户安全上下文中操作,同时允许某些应用在需要时以提升的权限运行

2.2 UAC 运行机制的概述

UAC 的提示界面默认运行在 安全桌面 下,此桌面名为 “Winlogon” 并与用户会话隔离。在一般情况下,UAC 处于实时就绪状态。当 UAC 激活时,用户对默认桌面的操作控制将被暂时剥夺,系统创建全屏的界面以明确询问用户是否批准应用进行权限修改

UAC 激活时是否切换到安全桌面,是否显示全局阴影背景,提示级别等由注册表项控制。系统提供的 “用户账户控制设置” 程序只是操作这些注册表项的可视化界面。由于归属的注册表子键设置了访问权限,所以依然需要管理员权限才能够进行修改。

 UAC 提权是相对复杂的过程,已经有很多研究人员分析过它,这里我只笼统概括一些重要环节。一个 UAC 过程主要由四方完成:(1)请求权限的进程(Legacy App)或者提权代理的发起者(Proxy App);(2) AIS 服务(Application Information Service);(3) 系统登陆应用程序(Winlogon);(4)用户实例

Appinfo 服务(AIS)

其中研究的相对多一些的就是 “ AIS 服务 ” 以及由 “请求方 到 处理方(主要是 AIS)” 的通路。 从上面的介绍来看, UAC 处理时离不开进程通信。研究发现 UAC 的消息传递主要通过 LRPC (本地远程过程调用)来完成。由 COM 途径代理提权模式下,部分环节又可由 DCOM 处理服务传递。LRPC 的部分实现过程还包含命名管道通信

UAC 运行过程中首先需要检验二进制文件的合法性,这依赖于二进制文件的签名证书验证、特征验证(文件路径、标记段或区块等)以及内置的白名单。如果一个进程通过了所有自动提权检验,则不会弹出 UAC 窗口;否则,将根据注册表设置的级别选择是否弹出窗口。弹出的窗口上,验证的发布者信息就是通过有效的文件签名证书和根证书颁发机构信息来识别的。验证的过程主要由 AIS 来完成,这也是 AIS 名称为 Application Information Service (应用程序信息辅助管理服务) 的原因。

从局部来看, Winlogon 在 LRPC 消息的等待过程中扮演了中转者的身份,同时他也是 WMsg Server 服务终结点。提权的消息首先会经过 AIS 服务,但是同时会有一份转发给 Winlogon,用户处理后会由 Winlogon 返回结果给 AIS 服务,AIS 服务会进行提权进程创建的后续操作。这可能与安全桌面是 Winlogon 创建的有关。在消息的响应阶段,AIS 首先拉起 consent 进程。它是 GUI 处理进程,用户看到的提示画面就是由它负责绘制的(它们是父子进程,consent.exe 通过运行时命令参数访问 AIS 进程的特定缓冲区上的数据来获知需要显示的信息)。AIS 为多个需要在同一阶段提权的进程创建等待队列,只允许一个进程进入就绪阶段,并弹出提示窗口。并且由于 AIS 的处理过程需要 Winlogon 的协助,此过程中有一个或多个死锁判定算法。例如:AIS 每隔一小段时间发送测试消息,当 Winlogon 进程在 5 分钟内每次测试均没有及时响应时,AIS 判断发生了死锁,此时自动结束 consent 进程,并回到默认桌面。

2.3 分析 UAC 提权参数

UAC 在提权时候,需要拉起的 consent 进程负责 UI 部分,启动参数格式为:

consent.exe <AIS 服务进程 PID> <参数缓冲区总大小> <参数缓冲区的首地址>

consent.exe 8312 372 0000015F3EA20AE0

缓冲区主要参数

圈起来的第一个是权限令牌。后面几个参数分别是:第一个字符串开头在这段内存中的偏移量,第二个字符串开头在这段内存中的偏移量,后面的字符串组开头在这段内存中的偏移量,以及字符串组结尾在这段内存中的偏移量。对于 exe 来说,前两个字符串在我观察到的情况中总保持一致,是文件路径,因此并不能很好地区分。而最后的字符串数组是其参数列表。
对于 dll 来说,第一个字符串有可能是其“描述”,而第二个参数,在我实验的范围内,一直是其路径。而字符串数组,则是引起 dll 加载的进程的参数列表。(其实就是 CreateProcessAsUserW 的参数列表)

早在几年前,我编写了一个解析 AIS 进程信息的工具,这是里面的一部分代码,实现了过滤并拦截特定的进程启动。代码可能写的粗糙(可读性较差),我也暂时没去重新写将就着看。

BOOL IsStrictExePath() {if (IsCSChecked == 1) {// 避免多次检查return TRUE;}else if (IsCSChecked == 2) {return FALSE;}HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE, GetCurrentProcessId()/*ProcessID*/);   // 这里如果不是注入,则需要获取 AIS 服务进程的 PIDif (hProcess == NULL)return TRUE;TCHAR* pszProcessCmd = GetProcessCommandLine(hProcess);if (pszProcessCmd[0] == L'\0') {return TRUE;}else {WCHAR seps[] = L" ";WCHAR* arg1 = wcstok(pszProcessCmd, seps);// 进程名 consent.exeWCHAR* arg2 = wcstok(NULL, seps);// 父进程 Appinfo 服务进程PIDWCHAR* arg3 = wcstok(NULL, seps);// 长度WCHAR* arg4 = wcstok(NULL, seps);// 要读取的内存地址起始位置int pid, len;void* addr;int s1 = swscanf(arg2, L"%d", &pid);int s2 = swscanf(arg3, L"%d", &len);int s3 = swscanf(arg4, L"%p", &addr);if (arg3[0] != NULL) {void* Address;HANDLE OldForeign;hProcess = OpenProcess(PROCESS_DUP_HANDLE | PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid);if (hProcess == NULL){MessageBoxW(GetForegroundWindow(), L"E1 ", L"CallBackMsg!!", MB_OK | MB_ICONINFORMATION | MB_SYSTEMMODAL);return TRUE;}char* Buffer;Buffer = reinterpret_cast<char*>(malloc(len));SIZE_T r;if (!ReadProcessMemory(hProcess, addr, Buffer, len, &r))ExitProcess(1);if (r != len){MessageBoxW(GetForegroundWindow(), L"E2 ",L"CallBackMsg!!", MB_OK | MB_ICONINFORMATION | MB_SYSTEMMODAL);return TRUE;}INT32 Byte1;memcpy(&Byte1, Buffer, sizeof(DWORD));INT32 Byte2;memcpy(&Byte2, Buffer + sizeof(DWORD), sizeof(DWORD));if (Byte1 != len){MessageBoxW(GetForegroundWindow(), L"E3 ",L"CallBackMsg!!", MB_OK | MB_ICONINFORMATION | MB_SYSTEMMODAL);return TRUE;}memcpy(&OldForeign, Buffer + 2 * sizeof(DWORD) + 2 * sizeof(void*), sizeof(void*));memcpy(&Address, Buffer + 6 * sizeof(DWORD) + 4 * sizeof(void*), sizeof(void*));DWORD BASE = 0;if (Byte2 == 0) {//ExcutableBASE = 6 * sizeof(DWORD) + 6 * sizeof(void*);}else if (Byte2 == 1) {//dllBASE = 6 * sizeof(DWORD) + 5 * sizeof(void*);}else {//Not UnderstoodINT64 t = len;memcpy(&t, Buffer + 6 * sizeof(DWORD) + 8 * sizeof(void*), sizeof(void*));if (t < len && t>0)BASE = 6 * sizeof(DWORD) + 5 * sizeof(void*);memcpy(&t, Buffer + 6 * sizeof(DWORD) + 9 * sizeof(void*), 8);if (t < len && t>0)BASE = 6 * sizeof(DWORD) + 6 * sizeof(void*);}LONG DescriptionAddr;memcpy(&DescriptionAddr, Buffer + BASE, sizeof(void*));LONG FilePathAddr;memcpy(&FilePathAddr, Buffer + BASE + sizeof(void*), sizeof(void*));LONG ParamsAddr;memcpy(&ParamsAddr, Buffer + BASE + 2 * sizeof(void*), sizeof(void*));memcpy(&ParamsAddr, Buffer + BASE + 3 * sizeof(void*), sizeof(void*));wchar_t* Description = reinterpret_cast<wchar_t*>(malloc(FilePathAddr - DescriptionAddr));if (Description != NULL) {memcpy(Description, Buffer + DescriptionAddr, FilePathAddr - DescriptionAddr);if (Description[0] == L'\"') {for (int i = 0; i < wcslen(Description) - 1; i++) {Description[i] = Description[i + 1];}Description[wcslen(Description) - 1] = L'\0';}}else{MessageBoxW(GetForegroundWindow(),L"E4 ",L"CallBackMsg!!", MB_OK | MB_ICONINFORMATION | MB_SYSTEMMODAL);return TRUE;}wchar_t* PathName = reinterpret_cast<wchar_t*>(malloc(static_cast<size_t>(ParamsAddr) - FilePathAddr));memcpy(PathName, Buffer + FilePathAddr, ParamsAddr - FilePathAddr);if (PathName != NULL) {// 中文文本std::string curLocale = setlocale(LC_ALL, "chs"); // curLocale = "C";setlocale(LC_ALL, curLocale.c_str());std::wstring  RetStr;BaseFlow::Attribute::GetFileDescription(Description, RetStr);// 忽略大小写查找if (StrStrIW(RetStr.c_str(), L"Terminal")|| StrStrIW(RetStr.c_str(), L"PowerShell")|| StrStrIW(RetStr.c_str(), L"Command")|| StrStrIW(PathName, L"cmd")|| StrStrIW(PathName, L"WindowsTerminal")|| StrStrIW(PathName, L"OpenConsole")|| StrStrIW(PathName, L"powershell")) {TCHAR szInfo[1024] = { 0 };PTSTR lpInfo = szInfo;time_t nowtime;struct tm pt[40];time(&nowtime);localtime_s(pt, &nowtime);// TODO: 造成卡顿的原因可能就是弹窗处理不应该放在这里!!!!//if (RetStr.c_str()[0] == L'\0')//wsprintf(lpInfo, //L"[Time  %d-%d-%d:%02d:%02d:%02d]\n以下程序请求提升权限: [ %s ] \n执行参数: [%s]。\n是否要跳转到 UAC 界面?\n",//1900 + pt->tm_year, 1 + pt->tm_mon, pt->tm_mday,//pt->tm_hour, pt->tm_min, pt->tm_sec, realFileName, PathName);//elsewsprintf(lpInfo, L"[Time  %d-%d-%d:%02d:%02d:%02d]\n以下程序请求提升权限: [ %s ] \n执行参数: [%s]。\n是否要跳转到 UAC 界面?\n",1900 + pt->tm_year, 1 + pt->tm_mon, pt->tm_mday,pt->tm_hour, pt->tm_min, pt->tm_sec, RetStr.c_str(), PathName);int UserMode = NULL;UserMode = MessageBoxW(NULL, szInfo, L"UAC 拦截器", MB_YESNO | MB_ICONINFORMATION | MB_TASKMODAL);if (UserMode != IDYES) {IsCSChecked = 1;return TRUE;}else {IsCSChecked = 2;return FALSE;}}else {IsCSChecked = 2;return FALSE;}}elsereturn TRUE;}}return TRUE;
}

效果如图所示:

动态注入工具截图

但是,实话实说,这种方法用于拦截启动并不靠谱,正确的做法是拦截 CreateProcessAsUser 或者 CreateProcessInternal ,具体可以看文章:https://blog.csdn.net/qq_59075481/article/details/128814911。

其实,解析该参数可以用于修改 consent.exe 我们可以自己构建一个 consent.exe,来美化提升管理员界面。但是这需要更多的代码逻辑,不过已经有人实现了相关项目:https://github.com/6ziv/CustomUAC。

6ziv 项目早期版本的截图

其实这里的提权参数还可以从其他角度细致地去分析和理解,已经有佬完成了相关工作,见文章:https://www.anquanke.com/post/id/231403。  Winlogon 和 AIS 进程之间的通信通过 LRPC 来完成,调用方进程和它们之间一般也是 LRPC。在这个过程中,有一个重要的函数 Ndr(64)AsyncServerCall(All) [x86 下是 NdrAsyncServerCall,x64 下是 Ndr64AsyncServerCallAll],作为服务端接收消息的关键步骤;而相对应的,客户端调用 Ndr(64)AsyncClientCall 发送消息

AIS 通过 LRPC 执行指定函数的参数(来自上面作者的文章)

这个系列那位作者连续写了三篇文章,都是干货满满,有兴趣可以去阅读阅读。

当开始分析这两个函数时,你会发现它并没有在 MSDN 上文档化解释。这些函数不被直接使用,往往是被一些上级的 API 调用,但是这些较为底层的函数被作为 rpcrt4.dll 的导出函数而允许我们轻松访问。作为 MS-RPC 中关键过程的封装函数,我们的方法将围绕着它来展开,无论是在第一篇还是这一篇中。(上面安全客博客作者是分析客户端代码的,没有涉及到 Winlogon 这边,本文将侧重于服务端 Winlogon 这边的分析)

二、 NdrAsyncServerCall 函数的分析

尽管 MS 刻意隐瞒这类函数的声明和作用,但结合一些逆向工作可以进一步分析出这类函数的原型。随着一些漏洞利用手法关注于 MS-RPC,一些实现细节逐渐被研究者挖掘出来。经典的如 CVE-2021-26411 远程代码执行漏洞,它是一个基于 IE 堆栈缓冲区 UAF(Use After Free) 和 RPC 的 CFG 绕过漏洞。漏洞主要利用了存在于 IE 中的一个 UAF,通过覆盖 NdrServerCall2 在 CFGBitmap 中的合法指针来绕过 CFG 检测,从而远程执行任意代码(比如使用 LoadLibrary 加载 payload)。这个漏洞涉及到一个 API :NdrServerCall2 函数。它传递了与 Ndr(64)AsyncServerCall(All) 类似的指针 —— pRpcMsg 指向 RPC_MESSAGE 结构,即 RPC 消息结构体。研究者对 RPC_MESSAGE 结构的分析对于本文来说是相当有用的。

2.1 函数声明的解析

 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,如果值不为 0x10,则 NdrConvert2 被调用

  • Buffer

类型:void *

存储函数调用中使用的参数的缓冲区(部分参数的序列化存储结构)。

  • BufferLength

类型:unsigned int

Buffer 参数指向的缓冲区的大小(以字节为单位)。 WMsg Server 处理消息包时的值一般为 12,调用完成时值被修改为 0。不同的 RPC 传递的方法不同,所需要的参数个数也不一样,缓冲区的大小就不同。Buffer 指向的缓冲区严格按照 4 字节对齐。

  • ProcNum

类型:unsigned int

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

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

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

  • TransferSyntax

类型:LPRPC_SYNTAX_IDENTIFIER

指向将写入用于编码数据的接口标识(唯一标识称作 UUID )的地址的指针。 pInterfaceId 由接口通用唯一标识符 UUID 和版本号组成。(对于测试人员,与 Rpc 有关的信息可以使用 RpcView 等工具获取)

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

  • RpcInterfaceInformation

类型:void *

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

进一步解释:在服务器端,RpcInterfaceInformation 指针指向 RPC_SERVER_INTERFACE 结构,该结构保存了服务端程序接口信息(后文将进一步分析该结构);在客户端,则指向 RPC_CLIENT_INTERFACE 结构;对于对象接口,则默认为 NULL。

  • ReservedForRuntime

类型:void *

保留用于运行时传递额外的扩展数据。(推测为指向结构体的指针,作用尚不明确)

  • ManagerEpv

类型:RPC_MGR_EPV

管理器入口点向量 (EPV) 是保存函数指针的数组。数组包含指向 IDL 文件中指定的函数实现的指针。数组中的元素数设置为 IDL 文件中指定的函数数。按照约定,包含接口和类型库定义的文件称为 IDL 文件,其文件扩展名为 .idl。接口由关键字 (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_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

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

根据相关研究,RPC_MESSAGE 结构体的重要成员已经在下图中标记出:

RPC_MESSAGE 结构体(x86)

注意:图中的偏移量是在 x86 下获得的,虽然在 x64 下结构的成员一样,但因为对齐原因偏移量不同(后文讲解如何计算实际的偏移量)。

RpcInterfaceInformation 是指向 RPC_SERVER_INTERFACE 结构体的指针。该结构体是我们下面将研究的一个重点。

2.2 对 Winlogon 的逆向

这一部分是一个很好的开始,他是本文一切研究的起点。在我的另外一篇文章中也讲过:

“屏蔽 Ctrl + Alt + Del 、Ctrl + Shift + Esc 等热键(二)”。现在我将前后工作联系起来整理一下,以便于读者对相关机制有一个清晰的认识。

相信读过本系列第一篇文章[https://blog.csdn.net/qq_59075481/article/details/135415028]的应该知道,在 Ndr64AsyncServerCallAll 函数响应的过程中,内部会调用 RpcServerTestCancel 和 RpcAsyncCompleteCall 两个导出函数。如下图所示:

典型的早期调用树

这两个函数很关键哦!服务器调用 RpcServerTestCancel 来查明客户端是否已请求取消未完成的调用。如果客户端取消了远程过程调用,则该函数返回 RPC_S_OK;否则,返回非 0 值。服务器以及客户端调用 RpcAsyncCompleteCall 函数以完成异步远程过程调用。服务器通过该调用过程 答复 指向 包含需要发送到客户端的返回值 的 缓冲区

通过 IDA 查询导入表可以看到 WMsg 接口的消息回调会调用 RpcServerTestCancel 函数:

调用 RpcServerTestCancel 的接口方法

而这些函数是 WMsg 接口消息回调的封装,比如 I_WMsgSendMessage 内部测试连接并触发间接调用。实际工作的消息回调函数为 WMsgMessageHandler 。对于实际的消息回调函数,在 Winlogon 初始化时调用 WMsgClntInitialize 注册它们的实例:

int64_t __fastcall WMsgClntInitialize(WLSM_GLOBAL_CONTEXT *WSMGlobalContext, BOOL bEnableKServer)  // int IsPresentKey
{int64_t WMsgHandlerList[9]; // [rsp+30h] [rbp-48h] BYREFmemset(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);
}

在继续分析前,我需要先谈谈 Winlogon 是怎么实现快捷键响应的。

WMsgClntInitialize 函数调用了两个比较重要的函数:StartWMsgServer 和 StartWMsgKServer 函数。

具体执行行为通过参数二来决定,参数二其实是一个 BOOL 类型,表明了是否要执行 WMsg Key Server 初始化。

在 Winlogon 启动的过程中,会调用两次 WMsgClntInitialize 函数。第一次调用参数二为 TRUE ,注册 KServer。

第一次调用 WMsgClntInitialize 函数

第二次调用在 StartLogonUI(启动 LogonUI.exe 进程,也就是登陆 UI 进程)和 WlStateMachineInitialize(状态机初始化)之后,参数为 FALSE,表明注册 Server。

第二次调用 WMsgClntInitialize 函数

 StartWMsgServer 和 StartWMsgKServer 都是启动 RPC 服务器并进行一些初始化工作。

 StartWMsgServer 伪代码:

__int64 StartWMsgServer()
{unsigned int v0; // ebxCUser *v1; // rcx__int64 v2; // rdxwchar_t pszDest[40]; // [rsp+40h] [rbp-68h] BYREFv0 = RpcServerRegisterIfEx(&unk_1400A3190, 0i64, 0i64, 0x28u, 0x4D2u, (RPC_IF_CALLBACK_FN *)WmsgRpcSecurityCallback);if ( v0 ){v1 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v2 = 21i64;goto LABEL_13;}}else{dword_1400D0658 = 1;v0 = RpcServerInqBindings(&BindingVector);if ( v0 ){v1 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v2 = 22i64;goto LABEL_13;}}else{StringCchPrintfW(pszDest, 0x25ui64, L"b08669ee-8cb5-43a5-a017-84fe%08X", NtCurrentPeb()->SessionId);v0 = UuidFromStringW(pszDest, &Uuid);if ( v0 ){v1 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v2 = 23i64;goto LABEL_13;}}else{UuidVector.Count = 1;UuidVector.Uuid[0] = &Uuid;v0 = WaitForDesiredService(L"RPCSS");if ( v0 ){v1 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v2 = 24i64;goto LABEL_13;}}else{v0 = RpcEpRegisterW(&unk_1400A3190, BindingVector, &UuidVector, 0i64);if ( v0 ){v1 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v2 = 25i64;goto LABEL_13;}}else{dword_1400D06B0 = 1;v0 = RpcServerListen(1u, 0x4D2u, 1u);if ( v0 == 1713 )v0 = 0;if ( v0 ){v1 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v2 = 26i64;
LABEL_13:WPP_SF_d(*((_QWORD *)v1 + 2), v2, &WPP_809920f00406303c4ef054ac00db20a5_Traceguids, v0);}}}}}}}if ( v0 )StopWMsgServer();return v0;
}

 StartWMsgKServer 伪代码:

__int64 __fastcall StartWMsgKServer(LUID *a1)
{int LocallyUniqueId; // eaxULONG v3; // ebxCUser *v4; // rcx__int64 v5; // rdxwchar_t pszDest[152]; // [rsp+40h] [rbp-148h] BYREFLocallyUniqueId = NtAllocateLocallyUniqueId(a1);if ( LocallyUniqueId < 0 ){v3 = RtlNtStatusToDosError(LocallyUniqueId);}else if ( StringCchPrintfW(pszDest,0x91ui64,L"WMsgKRpc%X%X%X",(unsigned int)a1->HighPart,a1->LowPart,NtCurrentPeb()->SessionId) < 0 ){v3 = 13;}else{v3 = RpcServerUseProtseqEpW((RPC_WSTR)L"ncalrpc", 0xAu, pszDest, 0i64);if ( v3 ){v4 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v5 = 18i64;goto LABEL_12;}}else{v3 = RpcServerRegisterIfEx(&hWmsgkSrvIfHandle, 0i64, 0i64, 0x28u, 0x4D2u, WmsgkRpcSecurityCallback);// unk_1400A3250if ( v3 ){v4 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v5 = 19i64;goto LABEL_12;}}else{dword_1400D06C0 = 1;v3 = RpcServerListen(1u, 0x4D2u, 1u);if ( v3 == 1713 )v3 = 0;if ( v3 ){v4 = WPP_GLOBAL_Control;if ( WPP_GLOBAL_Control != (CUser *)&WPP_GLOBAL_Control&& (*((_BYTE *)WPP_GLOBAL_Control + 28) & 1) != 0&& *((_BYTE *)WPP_GLOBAL_Control + 25) >= 2u ){v5 = 20i64;
LABEL_12:WPP_SF_d(*((_QWORD *)v4 + 2), v5, &WPP_809920f00406303c4ef054ac00db20a5_Traceguids, v3);}}}}}if ( v3 )StopWMsgServer();return v3;
}

他们使用 ncalrpc 协议工作,关于协议和终结点我简单解释一下。

在 RPC(Remote Procedure Call,远程过程调用)中,协议序列和终结点是两个关键概念,它们定义了客户端与服务器之间的通信方式。

(1)协议序列(Protocol Sequence)

协议序列定义了通信的协议和传输方式,即 RPC 通信时使用的底层网络协议。常见的协议序列包括:

  • ncacn_ip_tcp:基于 TCP/IP 协议的网络连接。
  • ncacn_np:基于命名管道的通信。
  • ncalrpc:本地过程调用(Local Procedure Call),用于同一台机器上的进程间通信。
  • ncacn_http:基于 HTTP 协议的通信。

协议序列决定了数据如何在网络上传输,因此选择合适的协议序列对 RPC 应用程序的性能和安全性非常重要。

(2)终结点(Endpoint)

终结点(也称为端点)是协议序列的具体实现,它定义了服务器的网络地址或命名管道名称,使得客户端能够找到并连接到服务器。终结点因协议序列的不同而不同,例如:

  • 对于 ncacn_ip_tcp,终结点通常是 IP 地址和端口号,例如 192.168.1.1:135。
  • 对于 ncacn_np,终结点是命名管道的名称,例如 \\.\pipe\mypipe。
  • 对于 ncalrpc,终结点是一个唯一的字符串名称,用于标识本地进程间通信。

下面谈谈两个函数的区别:

1. 函数签名

  • StartWMsgKServer:包含一个 LUID *lpLuid 参数,用于传递一个本地唯一标识符。
  • StartWMsgServer:不包含参数。

2. 本地唯一标识符

  • StartWMsgKServer:调用 NtAllocateLocallyUniqueId 为 lpLuid 分配一个本地唯一标识符,并基于此唯一标识符创建 pszDest 字符串 (WMsgKRpc%X%X%X)。
  • StartWMsgServer:不涉及本地唯一标识符的分配。

3. 字符串格式化

  • StartWMsgKServer:使用 StringCchPrintfW 函数格式化字符串 WMsgKRpc%X%X%X
  • StartWMsgServer:使用 StringCchPrintfW 函数格式化字符串 b08669ee-8cb5-43a5-a017-84fe%08X

4. RPC 协议序列和终结点

  • StartWMsgKServer:使用 RpcServerUseProtseqEpW 函数设置 ncalrpc 协议序列和 pszDest 终结点。
  • StartWMsgServer:涉及 RpcServerInqBindings 和 RpcEpRegisterW 函数,用于查询绑定信息和注册终结点。

5. 日志记录

  • 两个函数在错误处理部分都有日志记录逻辑,但使用的事件 ID 不同。

6. RPC 接口注册:

RPC_IF_HANDLE 为 RPC 调用中会使用到的接口句柄,其本质为 RpcInterfaceInformation,也就是 RPC 接口信息句柄,在注册 RPC 调用的时候会用到。这里的 hWmsgkSrvIfHandle 和 hWmsgSrvIfHandle 两个全局变量 RPC_IF_HANDLE 就是 RPC 接口信息句柄。

注册接口信息
  • StartWMsgKServer:注册接口 hWmsgkSrvIfHandle,并使用 WmsgkRpcSecurityCallback 回调函数。
  • StartWMsgServer:注册接口 hWmsgSrvIfHandle,并使用 WmsgRpcSecurityCallback 回调函数。

7. 其他差异

  • StartWMsgServer 包含更多的错误处理逻辑和特定的 UUID 操作。
  • 在 StartWMsgServer 函数中调用 WaitForDesiredService(L"RPCSS") 是为了确保远程过程调用 (Remote Procedure call Service) 服务 (RPCSS 服务) 已经启动并处于可用状态。这一步是必要的,因为在启动和注册 RPC 服务器时,依赖于 RPCSS 服务来处理和管理。

RPCSS 服务的角色

RPCSS服务是 COM 和 DCOM 服务器的服务控制管理器。它执行 COM 和 DCOM 服务器的对象激活请求、对象导出程序解析和分布式垃圾回收。

具体来说,RpcSs 服务主要提供以下功能:

  • 进程间通信 (Inter-process Communication, IPC):允许一个进程调用另一个进程中的函数,就像调用本地函数一样。这在分布式系统中尤为重要,因为它使得不同机器上的进程也能够通信和协作。
  • 客户端和服务器通信:RPC 服务可以用来实现客户端和服务器之间的通信,支持分布式计算,增强应用程序的灵活性和扩展性。
  • 安全和身份验证:RPC 服务包含安全特性,确保通信的安全性和数据的完整性,并提供身份验证机制。

WaitForDesiredService 函数的作用

WaitForDesiredService 函数的作用是确保指定的服务(在这里是 RPCSS 服务)已经启动并准备好接受请求。函数包含以下逻辑:

  • 检查指定服务的当前状态。
  • 如果服务尚未启动,则等待服务启动。
  • 如果服务在一段合理的时间内未启动,则返回错误。

总的来说,StartWMsgKServer 主要是针对带有本地唯一标识符 (LUID) 的 RPC 服务器的启动,使用了 ncalrpc 协议。StartWMsgServer 则使用 UUID 注册 RPC 服务器,涉及更多的绑定查询和 UUID 操作。

在 模拟发送 Ctrl+Alt+Del 快捷键 -CSDN博客 一文中,我曾解释过 WMsg 客户端部分的代码,有兴趣的可以结合一起看看。


谈完了 WMsgClntInitialize 我们还需要知道 winlogon 具体在什么阶段注册 Rpc 服务器和处理消息的: 

 Winlogon 首先会在主线程的 WinMain 函数中调用 WlStateMachineInitialize、WMsgClntInitialize 初始化全部的回调函数和事务处理线程(注意 RPC 信息的响应处理是在新线程中完成的,不是主线程)。

Winlogon 初始化的第三阶段注册相关事务的过程函数

 WlStateMachineInitialize 实际上是对 StateMachineCreate 的封装,该函数创建并初始化状态机对象。首先,为状态机和内部结构分配内存。然后,通过调用创建并初始化信号管理器SignalManager。随后,通过设置内部状态和结构完成初始化。如果任何步骤失败,请清理并返回错误代码。如果成功,则设置指向状态机的全局指针并返回 0。

初始化状态机代码片段

SignalManagerCreate 负责创建并初始化信号管理器对象。首先,为关键部分分配内存并初始化临界区。然后,创建一个事件对象,并为内部结构分配额外的内存。随后,初始化这些内部结构。如果任何步骤失败,请清理并返回错误代码;如果成功,则设置输出指针并返回 0。

初始化信号管理器对象代码片段

随后进入临界区并调用 StateMachineRun 开始监听事件。StateMachineRun 函数内部实际通过 SignalManagerWaitForSignal 循环等待同步对象(这里是设置的事件对象)。

[Winlogon 进程通过 SignalManagerWaitForSignal 等函数循环等待系统快捷键,而关键的消息回调是通过 RPC 完成的]

信号处理和同步用到的一些函数

在这里的代码中,可以看到混合使用了临界区(Critical Section)和 WaitForSingleObject 等函数。这种混合使用的情况通常是为了实现更复杂的同步机制,保证线程安全和同步的灵活性。

多线程同步和饥饿、死锁避免机制等

在临界区内检查特定条件,并执行相应的逻辑。这里主要是通过循环检查特定条件是否满足,如果满足则执行相应的操作。如果特定条件满足并进行了操作,则退出临界区并返回结果。

如果没有找到满足条件的情况,则退出临界区并使用WaitForSingleObject等待一个同步对象(这里可能是一个事件或信号量)变为有信号状态。如果等待失败,则获取错误码并睡眠一段时间后重试。

例如在提升管理员权限的 UAC 会话中该机制就被用于检查是否发生死锁或者超时未响应,以避免忙等干等的情况。

谈完了初始化管理器的过程,当然还需要谈处理的过程:

为了便于理解接收消息的线程的处理过程,以接收到热键消息(有 K 的函数)为例进行讲解。处理流程可以用下面的简化版理论来总结。

备注:提权等操作过程类似,只不过通过的函数没有 K 字样,在第一篇说过两个函数的功能区别。整理经 WinDbg 和 IDA Pro 的逆向分析结果,并参考了 heiheiabcd 的工作。

首先,客户端的请求通过 Ndr64AsyncClientCall 最终传递进入服务例程,服务例程通过调用 Ndr64AsyncServerCallAll 来完成所有响应过程。

而该过程是通过 rpcrt4!Invoke 函数来派发的间接调用链(了解过 MS-RPC 的应该都知道 Invoke )。

调用过程简化流程图 1

随后,进入关键调用过程:

调用过程简化流程图 2

所有任务都通过 I_WMsgkSendMessage 实现,因为此时需要调用的远程过程函数的参数已经全部在堆栈或寄存器上了。我们将过程划分为三个阶段:(1)测试远程过程,验证客户端信息来确定是否取消后续的调用;(2)验证通过后调用 WMsgKMessageHandler 也就是正真的事务处理例程,在该例程中通过 WlStateMachineSetSignal 设置事件信号,该事件会通知主线程;(3)I_WMsgkSendMessage 进行最后的处理,通过 RpcAsyncCompleteCall 通知客户端完成请求。

总的来说,整个多进程跨线程的异步机制为:客户端(调用方进程)请求某个操作时,服务器(Winlogon)通过特殊的事务处理线程接收消息并验证身份,然后通过设置事件(SetEvent)释放正在等待的 WinMain 主线程,最后事务线程通知客户端请求的操作已经完成,客户端(如果有)等待到消息后类似服务器,释放相关执行过程的线程(阻滞/非阻滞过程)。

所以,要想拦截快捷键等,一个切入点就是从 I_WMsgSendMessage 以及 I_WMsgkSendMessage 函数下手。下面以对 I_WMsgSendMessage 的逆向为例,它间接调用 WMsgMessageHandler 等函数。(前面写过的文章则以 WMsgKMessageHandler 和 WMsgMessageHandler 两个具体的消息处理过程进行拦截,其实效果差不多)

 Winlogon 进程通过 SignalManagerWaitForSignal 函数循环等待系统快捷键,关键的消息回调是通过 RPC 完成的。

注:下图中的指针实际指向的函数通过 WinDbg 分析获得。

I_WMsgSendMessage 中的间接调用

首先我们观察到函数内检测了客户端是否取消了调用:

检查客户端是否取消了调用

如果返回值为 0 也就是 RPC_S_OK 则表明客户端已经请求取消远程调用。微软文档说服务器此时可以选择是中断调用还是继续调用,只是客户端不管它返回了而已。在目前的 winlogon 处理机制下,是会立即中止调用的,因为我们观察到了关键的函数的调用 RpcAsyncAbortCall(pAsync, RPC_S_CALL_CANCELLED),这指示了调用将被服务器中止。

所以,一种思路是依赖欺骗 TestCancel 检测来绕过调用。

修改 RpcServerTestCancel 的返回值

下图展示了使用 WinDbgX 修改 RpcServerTestCancel 的返回值后的效果,可以看到在请求以管理员身份启动程序时远程过程被取消:

使用 WinDbgX 修改返回值

所以第一个挂钩点就是挂钩 RpcServerTestCancel 并在必要时候返回 RPC_S_OK。例如同时挂钩 Ndr64AsyncServerCallAll 函数,并根据第一篇文章解析的 Buffer 参数来判断当前正在进行的操作,根据操作选择是否要欺骗 WMsg 的 TestCancel 检测

远程调用过程被取消(弹窗)

接下来我们观察到了对堆栈上的一个参数的按位校验,这里应该是判断一个句柄是否有效的的校验码:

句柄校验过程

如果句柄检测后的返回值是非 0 值(a5 != 0),则按位校验会不通过(说明句柄是无效句柄),此时终止调用(走默认执行方法)。

所以,第二个思路是修改 a5 的值为一个不是 0 的值,导致执行默认方法(DefaultWMsgMessageHandler),进程创建失败(如果是提权,则会提示文件系统错误)。

为什么会失败呢?

因为默认流程不执行任何消息处理操作,只 out 错误状态以及返回完成(return 1):

__int64 __fastcall DefaultWMsgMessageHandler(__int64 a1, __int64 a2, __int64 a3, _DWORD *a4)
{*a4 = 0xC00000AF;return 1i64;
}

RpcServerTestCancel 函数是在 Rpcrt4 里面实现的,我们简单看一下它的逆向代码(注释已经十分详细了,我就不再详细展开了):

RPC_STATUS __stdcall RpcServerTestCancel(RPC_BINDING_HANDLE BindingHandle)
{int IsInvalid; // eaxRPC_BINDING_HANDLE *ThreadPointer; // raxTHREAD *lpSourceWrapThread; // raxint dwCreateEvent; // [rsp+30h] [rbp+8h] BYREF// 绑定分为静态和动态,取决于传入参数是否为空,为空则动态绑定当前线程if ( !BindingHandle ){ThreadPointer = (RPC_BINDING_HANDLE *)RpcpGetThreadPointer();// LPC/ALPC协议头。此标头具有 ClientId 字段,// 该字段同时具有发件人PID和TID。// 在接收到ALPC请求后,服务器进程中的RPC运行时将这些值保存在RPC_BINDING_HANDLE对象中,// 从中可以检索到这些值。// 向函数传递空句柄意味着使用当前线程的活动绑定,// 在这种情况下,这些API通过ReservedForNtRpc字段从当前线程的// TEB、IIRC获取RPC_BINDING_HANDLE指针。// if ( ThreadPointer )                        //   如果在 TEB 的 ClientID.UniqueThread 里面找到了已经传递的绑定句柄,//   则直接调用测试函数,而不重复创建动态绑定的句柄。goto LABEL_12;dwCreateEvent = 0;lpSourceWrapThread = (THREAD *)AllocWrapper(0xE8ui64);// 在堆上分配内存if ( lpSourceWrapThread ){ThreadPointer = (RPC_BINDING_HANDLE *)THREAD::THREAD(lpSourceWrapThread, &dwCreateEvent);// 创建绑定线程的事件对象(同步对象)if ( ThreadPointer )                      // 对线程的动态绑定到 ReservedForNtRpc 结束后,清理过程中用到的事件对象{if ( !dwCreateEvent )                   // 如果创建失败,进一步检查返回的 THREAD 对象的指针是否为空goto LABEL_11;THREAD::`scalar deleting destructor'((THREAD *)ThreadPointer);// 析构 THREAD 对象,释放内存}}ThreadPointer = 0i64;
LABEL_11:if ( !ThreadPointer )                       // 创建失败并且对象指针为空指针,则返回错误代码 1725return 1725;
LABEL_12:BindingHandle = ThreadPointer[4];           // 获取绑定句柄上的函数指针表if ( BindingHandle )                        // 测试客户端是否取消了远程过程调用。// 如果客户端没有取消调用,返回值为 1791(RPC_S_CALL_IN_PROGRESS);// 取消调用则返回 0return (*(unsigned int (__fastcall **)(RPC_BINDING_HANDLE))(*(_QWORD *)BindingHandle + 192i64))(BindingHandle) == 0? 1791: 0;                                 // 如果指针列表为空,说明绑定失败,也就是客户端没有响应这个动态绑定的句柄,// 此时返回错误代码 1725(RPC_S_NO_CALL_ACTIVE)return 1725;}                                             // 下面是测试静态绑定的句柄LOBYTE(IsInvalid) = GENERIC_OBJECT::InvalidHandle((GENERIC_OBJECT *)BindingHandle, 2105416);// 判断绑定句柄是否有效if ( !IsInvalid )return (*(unsigned int (__fastcall **)(RPC_BINDING_HANDLE))(*(_QWORD *)BindingHandle + 192i64))(BindingHandle) == 0? 1791: 0;return 1702;
}

其实就是验证 RPC 句柄是否有效以及函数指针表的完整性而已。这一部分里面很有趣,较多地使用哨兵值和有效范围进行句柄和指针的校验。所以,我们也可以修改传入的句柄或者指针在堆栈上的值,以便于触发校验失败,这种精心构造的代码也可以使得调用被取消,且不容易被发现。

2.3 对 rpcrt4 的静态分析

为了进一步解释拦截系统快捷键的方法的原理,下面将围绕一直在谈的 Ndr64AsyncServerCallAll 函数来分析。RPC 相关调用比较复杂,但是我们只需要抓住关键流程即可找到突破口。

【分析】(基于版本为 10.0.22621.3235 的 rpcrt4.dll)

 Ndr64AsyncServerCallAll 函数是导出函数,从解析的代码中可以看出它是 Ndr64AsyncServerWorker 的封装。

Ndr64AsyncServerWorker 的封装

进入  Ndr64AsyncServerWorker 可以看到这个函数很复杂。下图是函数开头的部分:

Ndr64AsyncServerWorker 的反汇编伪代码

这里的开头两句反汇编语句,是很重要的。但第一次接触的话会很难理解:

v37 = *((_QWORD *)Message->RpcInterfaceInformation + 10);
v41 = *(_QWORD **)(v37 + 8);

首先这里有个陷阱,Message->RpcInterfaceInformation 是指针地址,IDA 反汇编的指令和伪代码写法不一样,伪代码中的对数据变量地址 + 10 是要类似于数组的处理方式用数据类型乘以所参与运算的常数的,也就是说这里是(地址 + 10* 8) = (地址 +80),也就是十六进制的 ptr + 0x50。

显然,这里的 Message 变量是指向 RPC_MESSAGE 结构的指针:

Ndr64AsyncServerWorker 形参说明

我们说过,前文给出的图片解析的结构体是 x86 架构下的,我们现在研究的是 x64 下的结构,该怎么办呢?

别急,我们使用一个技巧可以轻松获取结构体成员的偏移量:

#include <stdio.h>
#include <Windows.h>
#include <rpc.h>//typedef struct _RPC_SERVER_INTERFACE
//{
//    unsigned int Length;
//    RPC_SYNTAX_IDENTIFIER InterfaceId;
//    RPC_SYNTAX_IDENTIFIER TransferSyntax;
//    PRPC_DISPATCH_TABLE DispatchTable;
//    unsigned int RpcProtseqEndpointCount;
//    PRPC_PROTSEQ_ENDPOINT RpcProtseqEndpoint;
//    RPC_MGR_EPV __RPC_FAR* DefaultManagerEpv;
//    void const __RPC_FAR* InterpreterInfo;
//    unsigned int Flags;
//} RPC_SERVER_INTERFACE, __RPC_FAR* PRPC_SERVER_INTERFACE;//typedef struct  _MIDL_SERVER_INFO_
//{
//    PMIDL_STUB_DESC                     pStubDesc;
//    const SERVER_ROUTINE* DispatchTable;
//    PFORMAT_STRING                      ProcString;
//    const unsigned short* FmtStringOffset;
//    const STUB_THUNK* ThunkTable;
//    PRPC_SYNTAX_IDENTIFIER              pTransferSyntax;
//    ULONG_PTR                           nCount;
//    PMIDL_SYNTAX_INFO                   pSyntaxInfo;
//} MIDL_SERVER_INFO, * PMIDL_SERVER_INFO;int main()
{PRPC_SERVER_INTERFACE rpcSvcInterface = nullptr;printf("RPC_SERVER_INTERFACE.Length: 0x%I64X \n", (UINT64)&rpcSvcInterface->Length);printf("RPC_SERVER_INTERFACE.InterfaceId: 0x%I64X \n", (UINT64)&rpcSvcInterface->InterfaceId);printf("RPC_SERVER_INTERFACE.TransferSyntax: 0x%I64X \n", (UINT64)&rpcSvcInterface->TransferSyntax);printf("RPC_SERVER_INTERFACE.DispatchTable: 0x%I64X \n", (UINT64)&rpcSvcInterface->DispatchTable);printf("RPC_SERVER_INTERFACE.RpcProtseqEndpointCount: 0x%I64X \n", (UINT64)&rpcSvcInterface->RpcProtseqEndpointCount);printf("RPC_SERVER_INTERFACE.RpcProtseqEndpoint: 0x%I64X \n", (UINT64)&rpcSvcInterface->RpcProtseqEndpoint);printf("RPC_SERVER_INTERFACE.DefaultManagerEpv: 0x%I64X \n", (UINT64)&rpcSvcInterface->DefaultManagerEpv);printf("RPC_SERVER_INTERFACE.InterpreterInfo: 0x%I64X \n", (UINT64)&rpcSvcInterface->InterpreterInfo);printf("RPC_SERVER_INTERFACE.Flags: 0x%I64X \n", (UINT64)&rpcSvcInterface->Flags);printf("\n\n");PMIDL_SERVER_INFO svcIdlInfo = nullptr;printf("MIDL_SERVER_INFO.pStubDesc: 0x%I64X \n", (UINT64)&svcIdlInfo->pStubDesc);printf("MIDL_SERVER_INFO.DispatchTable: 0x%I64X \n", (UINT64)&svcIdlInfo->DispatchTable);printf("MIDL_SERVER_INFO.ProcString: 0x%I64X \n", (UINT64)&svcIdlInfo->ProcString);printf("MIDL_SERVER_INFO.FmtStringOffset: 0x%I64X \n", (UINT64)&svcIdlInfo->FmtStringOffset);printf("MIDL_SERVER_INFO.ThunkTable: 0x%I64X \n", (UINT64)&svcIdlInfo->ThunkTable);printf("MIDL_SERVER_INFO.pTransferSyntax: 0x%I64X \n", (UINT64)&svcIdlInfo->pTransferSyntax);printf("MIDL_SERVER_INFO.nCount: 0x%I64X \n", (UINT64)&svcIdlInfo->nCount);printf("MIDL_SERVER_INFO.pSyntaxInfo: 0x%I64X \n", (UINT64)&svcIdlInfo->pSyntaxInfo);system("pause");return 0;
}

以 x64 平台模式编译执行程序,得到 x64 架构下结构体的成员偏移如下:

x64 结构体的成员的偏移量计算结果

当有了偏移量后,再看汇编代码就会容易理解里面的一些运算是什么了。例如下面图片展示的第 48 行的代码中 RpcInterfaceInformation + 0x50 指向的就是 RPC_SERVER_INTERFACE 结构的 InterpreterInfo 成员。

汇编代码中获取 InterpreterInfo 的地址的方法

为了便于对照理解,我们把前面一小节的图片再搬来一次:

x86 下的 ServerInterface 结构

同样地我们通过计算得到 x64 下的 MIDL_SERVER_INFO 结构体中的成员的偏移:

x64 下 MIDL_SERVER_INFO 的成员偏移

汇编代码中的 rcx + 0x28 是 RPC_MESSAGE 的 RpcInterfaceInformation 成员。这里 F5 也自动分析出来了。随后的 rax + 0x50 是 RpcInterfaceInformation 指向的结构中的成员地址,结合上文解析的 RPC_SERVER_INTERFACE 成员偏移量可知这里的 v37 是 InterpreterInfo 成员。而 InterpreterInfo 成员指向 MIDL_SERVER_INFO 结构,再根据相对于 MIDL_SERVER_INFO 起始地址的偏移量可知 v41 就是 DispatchTable 成员。

为什么我们关注这里的偏移量计算呢?往下看你就知道了,这里的 v41 在后面被赋值给 ManagerEpv,然后根据 ManagerEpv[ProcNum] 获取要执行的函数的指针,并由 rpcrt4!Invoke 执行函数(激活调用)。

管理入口向量 - 激活过程函数

(注释:刚开始认识 RPC 时,我们并不是一开始就关注到 Invoke 函数,在他之前有很多参数初始化的细节。但是它是 MS-RPC 的关键部分,由 Invoke 向上可以轻松找到管理入口向量的解析过程)

下面我们简单介绍一下全部的过程,如果你想深入了解也可以看文末附加的参考文献:

服务器 / 客户端使用名为 RPC_MESSAGE 的结构体传递信息,其中包含发送的消息。调用的 Ndr64AsyncServerWorker 上下文只是用于设置嵌入在 AsyncMsg 中的调用。它是在这个异步调用的生命周期中使用的。

在 Ndr64AsyncServerWorker 中,首先通过 I_RpcBCacheAllocate( sizeof( NDR_ASYNC_MESSAGE) ) 构造 Async Msg 缓冲区(0x538 字节)。然后,通过 MulNdrpInitializeContextFromProc 初始化上下文(InitializeContext)。使用 NdrpInitializeAsyncMsg 将堆栈上的信息拷贝到 NDR_ASYNC_MESSAGE 结构中。

随后进入被 try-finally 包围的设置存根、上下文管理器句柄、异步句柄,反序列化参数(称为 UnMarshalling)和过程调用(称为 Invoke)代码段。Ndr64ServerInitialize 函数用于设置存根信息,而 NdrpServerUnMarshal 函数用于解析参数(反序列化)。调用的下一个例程是 Invoke,它负责调用处理请求所要求内容的本地函数以及将调用例程的参数列表。当服务器发送回复时,也会调用相同的函数(Invoke)。

上文的分析结合了逆向工程和微软 Leak 的源代码。

可以在这个神奇的网站找到 Leak 的代码:Microsoft leaked source code archive。

Microsoft Leaked 源代码目录

下面是节选自 /NT/com/rpc/ndr64/async.c  中的 Ndr64AsyncServerWorker 函数源代码(自早期代码至今,它的实现细节几乎没有变化):

void RPC_ENTRY
Ndr64AsyncServerWorker(PRPC_MESSAGE            pRpcMsg,ulong                   SyntaxIndex )
/*++
Routine Description :The server side entry point for regular asynchronous RPC procs.Arguments :pRpcMsg         - The RPC message.Return :None.
--*/
{ulong dwStubPhase = STUB_UNMARSHAL;PRPC_SERVER_INTERFACE   pServerIfInfo;PMIDL_SERVER_INFO       pServerInfo;const SERVER_ROUTINE  * DispatchTable;MIDL_SYNTAX_INFO *      pSyntaxInfo;RPC_ASYNC_HANDLE        AsyncHandle = 0;PNDR_ASYNC_MESSAGE      pAsyncMsg;ushort                  ProcNum;PMIDL_STUB_MESSAGE      pStubMsg;uchar *                 pArgBuffer;uchar *                 pArg;uchar **                ppArg;NDR64_PROC_FORMAT *     pHeader;NDR64_PARAM_FORMAT  *   Params;long                    NumberParams;NDR64_PROC_FLAGS *      pNdr64Flags;ushort                  ClientBufferSize;BOOL                    HasExplicitHandle;long                    n;// This context is just for setting up the call. embedded one in asyncmsg is the// one to be used during the life of this async call.NDR_PROC_CONTEXT        *pContext;RPC_STATUS              Status = RPC_S_OK;NDR64_PARAM_FLAGS   *       pParamFlags;NDR64_BIND_AND_NOTIFY_EXTENSION * pHeaderExts = NULL;pServerIfInfo = (PRPC_SERVER_INTERFACE)pRpcMsg->RpcInterfaceInformation;pServerInfo = (PMIDL_SERVER_INFO)pServerIfInfo->InterpreterInfo;DispatchTable = pServerInfo->DispatchTable;pSyntaxInfo = &pServerInfo->pSyntaxInfo[SyntaxIndex];NDR_ASSERT( XFER_SYNTAX_NDR64 == NdrpGetSyntaxType(&pSyntaxInfo->TransferSyntax)," invalid transfer syntax" );//// In the case of a context handle, the server side manager function has// to be called with NDRSContextValue(ctxthandle). But then we may need to// marshall the handle, so NDRSContextValue(ctxthandle) is put in the// argument buffer and the handle itself is stored in the following array.// When marshalling a context handle, we marshall from this array.//// The handle table is part of the async handle.ProcNum = (ushort) pRpcMsg->ProcNum;NDR_ASSERT( ! ((ULONG_PTR)pRpcMsg->Buffer & 0x7),"marshaling buffer misaligned at server" );AsyncHandle = 0;pAsyncMsg = (NDR_ASYNC_MESSAGE*) I_RpcBCacheAllocate( sizeof( NDR_ASYNC_MESSAGE) );if ( ! pAsyncMsg )Status = RPC_S_OUT_OF_MEMORY;else{memset( pAsyncMsg, 0, sizeof( NDR_ASYNC_MESSAGE ) );NdrServerSetupNDR64TransferSyntax(ProcNum,pSyntaxInfo,&pAsyncMsg->ProcContext );Status = NdrpInitializeAsyncMsg( 0,                 // StartofStack, serverpAsyncMsg);}if ( Status )RpcRaiseException( Status );pContext = &pAsyncMsg->ProcContext;PFORMAT_STRING pFormat = pContext->pProcFormat;pAsyncMsg->StubPhase = STUB_UNMARSHAL;pStubMsg = & pAsyncMsg->StubMsg; // same in ndr20pStubMsg->RpcMsg = pRpcMsg;// The arg buffer is zeroed out already.pArgBuffer = pContext->StartofStack;pHeader = (NDR64_PROC_FORMAT *) pFormat;pNdr64Flags = (NDR64_PROC_FLAGS *) &pHeader->Flags;HasExplicitHandle = !NDR64MAPHANDLETYPE( NDR64GETHANDLETYPE ( pNdr64Flags ) );if ( pNdr64Flags->HasOtherExtensions )pHeaderExts = (NDR64_BIND_AND_NOTIFY_EXTENSION *) (pFormat + sizeof( NDR64_PROC_FORMAT ) );if ( HasExplicitHandle ){NDR_ASSERT( pHeaderExts, "NULL extension header" );//// For a handle_t parameter we must pass the handle field of// the RPC message to the server manager.//if ( pHeaderExts->Binding.HandleType == FC64_BIND_PRIMITIVE ){pArg = pArgBuffer + pHeaderExts->Binding.StackOffset;if ( NDR64_IS_HANDLE_PTR( pHeaderExts->Binding.Flags ) )pArg = *((uchar **)pArg);*((handle_t *)pArg) = pRpcMsg->Handle;}}//// Get new interpreter info.//NumberParams = pHeader->NumberOfParams;Params = (NDR64_PARAM_FORMAT *)( (uchar *) pFormat + sizeof( NDR64_PROC_FORMAT ) + pHeader->ExtensionSize );//// Wrap the unmarshalling and the invoke call in the try block of// a try-finally. Put the free phase in the associated finally block.//BOOL        fManagerCodeInvoked = FALSE;BOOL        fErrorInInvoke = FALSE;RPC_STATUS  ExceptionCode = 0;// We abstract the level of indirection here.AsyncHandle = pAsyncMsg->AsyncHandle;RpcTryFinally{RpcTryExcept{// Put the async handle on stack.((void **)pArgBuffer)[0] = AsyncHandle;  //// Initialize the Stub message.// Note that for pipes we read non-pipe data synchronously,// and so the init routine doesn't need to know about async.//if ( ! pNdr64Flags->UsesPipes ){Ndr64ServerInitialize( pRpcMsg,pStubMsg,pServerInfo->pStubDesc );}elseNdr64ServerInitializePartial( pRpcMsg,pStubMsg,pServerInfo->pStubDesc,pHeader->ConstantClientBufferSize );// We need to set up this flag because the runtime does not know whether//   it dispatched a sync or async call to us. same as ndr20pRpcMsg->RpcFlags         |= RPC_BUFFER_ASYNC;pStubMsg->pAsyncMsg       = pAsyncMsg;pStubMsg->RpcMsg = pRpcMsg;pStubMsg->pContext       = &pAsyncMsg->ProcContext;//// Set up for context handle management.//pStubMsg->SavedContextHandles = & pAsyncMsg->CtxtHndl[0];// Raise exceptions after initializing the stub.if ( pNdr64Flags->UsesFullPtrPackage  )pStubMsg->FullPtrXlatTables = NdrFullPointerXlatInit( 0, XLAT_SERVER );if ( pNdr64Flags->ServerMustSize & pNdr64Flags->UsesPipes )RpcRaiseException( RPC_X_WRONG_PIPE_VERSION );//// Set StackTop AFTER the initialize call, since it zeros the field// out.//pStubMsg->pCorrMemory = pStubMsg->StackTop; if ( pNdr64Flags->UsesPipes )NdrpPipesInitialize64( pStubMsg,&pContext->AllocateContext,(PFORMAT_STRING) Params,(char*)pArgBuffer,NumberParams );//// We must make this check AFTER the call to ServerInitialize,// since that routine puts the stub descriptor alloc/dealloc routines// into the stub message.//if ( pNdr64Flags->UsesRpcSmPackage )NdrRpcSsEnableAllocate( pStubMsg );// Let runtime associate async handle with the call.NdrpRegisterAsyncHandle( pStubMsg, AsyncHandle );pAsyncMsg->StubPhase = NDR_ASYNC_SET_PHASE;// --------------------------------// Unmarshall all of our parameters.// --------------------------------NDR_ASSERT( pContext->StartofStack == pArgBuffer, "startofstack is not set" );Ndr64pServerUnMarshal( pStubMsg  );if ( pRpcMsg->BufferLength  <(uint)(pStubMsg->Buffer - (uchar *)pRpcMsg->Buffer) ){RpcRaiseException( RPC_X_BAD_STUB_DATA );}                                               }RpcExcept( NdrServerUnmarshallExceptionFlag(GetExceptionInformation()) ){ExceptionCode = RpcExceptionCode();if( RPC_BAD_STUB_DATA_EXCEPTION_FILTER ){ExceptionCode = RPC_X_BAD_STUB_DATA;}NdrpFreeMemoryList( pStubMsg );pAsyncMsg->Flags.BadStubData = 1;pAsyncMsg->ErrorCode = ExceptionCode;RpcRaiseException( ExceptionCode );}RpcEndExcept// Two separate blocks because the filters are different.// We need to catch exception in the manager code separately// as the model implies that there will be no other call from// the server app to clean up.RpcTryExcept{//// Do [out] initialization before the invoke.//Ndr64pServerOutInit( pStubMsg );//// Unblock the first pipe; this needs to be after unmarshalling// because the buffer may need to be changed to the secondary one.// In the out only pipes case this happens immediately.//if ( pNdr64Flags->UsesPipes )NdrMarkNextActivePipe( pContext->pPipeDesc );pAsyncMsg->StubPhase = STUB_CALL_SERVER;//// Check for a thunk.  Compiler does all the setup for us.//if ( pServerInfo->ThunkTable && pServerInfo->ThunkTable[ProcNum] ){pAsyncMsg->Flags.ValidCallPending = 1;InterlockedDecrement( & AsyncHandle->Lock );fManagerCodeInvoked = TRUE;fErrorInInvoke = TRUE;pServerInfo->ThunkTable[ProcNum]( pStubMsg );}else{//// Note that this ArgNum is not the number of arguments declared// in the function we called, but really the number of// REGISTER_TYPEs occupied by the arguments to a function.//long                ArgNum;MANAGER_FUNCTION    pFunc;REGISTER_TYPE       returnValue;if ( pRpcMsg->ManagerEpv )pFunc = ((MANAGER_FUNCTION *)pRpcMsg->ManagerEpv)[ProcNum];elsepFunc = (MANAGER_FUNCTION) DispatchTable[ProcNum];ArgNum = (long) pContext->StackSize / sizeof(REGISTER_TYPE);//// The StackSize includes the size of the return. If we want// just the number of REGISTER_TYPES, then ArgNum must be reduced// by 1 when there is a return value AND the current ArgNum count// is greater than 0.//if ( ArgNum &&  pNdr64Flags->HasReturn )ArgNum--;// Being here means that we can expect results. Note that the user// can call RpcCompleteCall from inside of the manager code.pAsyncMsg->Flags.ValidCallPending = 1;// Unlock the handle - the app is allowed to call RpCAsyncComplete//  or RpcAsyncAbort from the manager code.InterlockedDecrement( & AsyncHandle->Lock );fManagerCodeInvoked = TRUE;fErrorInInvoke = TRUE;returnValue = Invoke( pFunc,(REGISTER_TYPE *)pArgBuffer,#if defined(_IA64_)pHeader->FloatDoubleMask,#endifArgNum);// We are discarding the return value as it is not the real one.// The real return value is passed in the complete call.}fErrorInInvoke = FALSE;}RpcExcept( 1 ){ExceptionCode = RpcExceptionCode();if ( ExceptionCode == 0 )ExceptionCode = ERROR_INVALID_PARAMETER;// We may not have the async message around anymore.RpcRaiseException( ExceptionCode );}RpcEndExcept}RpcFinally{if ( fManagerCodeInvoked  &&  !fErrorInInvoke ){// Success. Just skip everything if the manager code was invoked// and returned successfully.// Note that manager code could have called Complete or Abort by now// and so the async handle may not be valid anymore.}else{// See if we can clean up;Status = RPC_S_OK;if ( fErrorInInvoke ){// After an exception in invoking, let's see if we can get a hold// of the handle. If so, we will be able to clean up.// If not, there may be a leak there that we can do nothing about.// The rule is: after an exception the app cannot call Abort or// Complete. So, we need to force complete if we can.Status = NdrValidateBothAndLockAsyncHandle( AsyncHandle );}if ( Status == RPC_S_OK ){// Something went wrong but we are able to do the cleanup.// Cleanup parameters and async message/handle.// propagate the exception.Ndr64pCleanupServerContextHandles( pStubMsg, NumberParams,Params,pArgBuffer,TRUE );     // fail before/during manager routineif (!pAsyncMsg->Flags.BadStubData){Ndr64pFreeParams( pStubMsg,NumberParams,Params,pArgBuffer );}NdrpFreeAsyncHandleAndMessage( AsyncHandle );}// else manager code invoked and we could not recover.// Exception will be raised by the EndFinally below.}}RpcEndFinally
}

通过 Leak Codes 了解到的 _NDR_ASYNC_MESSAGE 结构如下:

typedef struct _NDR_ASYNC_MESSAGE
{long                        Version;        // 0x0long                        Signature;      // 0x4RPC_ASYNC_HANDLE            AsyncHandle;    // raw and CAsyncMgr * // 0x8(size:n)NDR_ASYNC_CALL_FLAGS        Flags;          // 0x8 + n(size:2)  ----> 0x68unsigned short              StubPhase;      // 0x8 + n + 0x2(size:2) == 0x6C - 2// ----> n = 0x60  ----> 0x6Aunsigned long               ErrorCode;     // 0x6CRPC_MESSAGE                 RpcMsg;        // 0x70MIDL_STUB_MESSAGE           StubMsg;NDR_SCONTEXT                CtxtHndl[MAX_CONTEXT_HNDL_NUMBER];usigned long *              pdwStubPhase;// Note: the correlation cache needs to be sizeof(pointer) alignedNDR_PROC_CONTEXT            ProcContext;// guard at the end of the messageunsigned char               AsyncGuard[NDR_ASYNC_GUARD_SIZE]; 
}   NDR_ASYNC_MESSAGE, *PNDR_ASYNC_MESSAGE;

IDA 中的 Invoke 代码如下(具体的分析将在后面的分析中给出):

__int64 __fastcall Invoke(__int64 (__fastcall *a1)(__int64, __int64, __int64, __int64),//  pFunconst void *a2,                      // pArgumentList__int64 a3,                          // pFloatingPointArgumentListunsigned int a4)                     // cArguments
{void *v4; // rsp__int64 vars0[4]; // [rsp+0h] [rbp+0h] BYREFv4 = alloca(8 * ((a4 + 1) & 0xFFFFFFFE));  //  当函数为堆栈分配的页面不够时,//  调用该例程分配更多页空间。//  在 X64 下局部变量超过 8K 字节时,//  由编译器插入该函数// 从缓冲区复制参数并构造参数数组qmemcpy(vars0, a2, 8 * a4);                //  dst    src    countRpcInvokeCheckICall(&a1);                  // 在调度服务例程之前,会调用 CFG //  (__guard_check_icall_fptr) // 以确保目标函数指针是 CFG 合法指针。return a1(vars0[0], vars0[1], vars0[2], vars0[3]);// 调用目标服务例程
}

Invoke 的第一个参数是要执行过程(函数)的指针,第二个参数是一般整形参数数组的首地址,第三个参数是浮点数参数数组的首地址,第四个参数是参数个数。x64 上按照 8 字节对齐解析参数列表。在通过 CFG 检查点后,执行目标过程(函数)。

2.4 对 rpcrt4 的动态调试

下面我们从动态调试角度分析有关参数传输的细节(算是对上面源代码中一些不是很透彻的地方加以分析理解)。

【分析】

首先启动 WinDbg 调试器并附加到 winlogon.exe 进程上。

然后下几个断点:

  • rpcrt4!Ndr64AsyncServerWorker
  • rpcrt4!I_RpcBCacheAllocate
  • rpcrt4!Invoke
  • rpcrt4!RpcServerTestCancel

I_RpcBCacheAllocate 是初始化分配 NDR_ASYNC_MESSAGE 的缓冲区,后续 RPC 传输语法所需要的信息都会在这个缓冲区上被序列化。

Go 进程,然后按下 Ctrl+Shift+Esc 尝试启动任务管理器,这会触发 RPC 过程。

在命中断点 0 时,我们打印一下堆栈和寄存器信息:

看 rcx 寄存器

其中,rcx 的内容为 PPRC_MESSAGE,我们看一下结构的参数:

查看 Buffer 参数

随后执行直到命中断点 I_RpcBCacheAllocate,使用 F8 执行直到函数结束,查看返回值,它是指向 NDR_ASYNC_MESSAGE 结构的指针(在 memset 前,分配的缓冲区上有脏数据)。

查看 I_RpcBCacheAllocate 返回值

缓冲区的分配依赖 LsaAlloc(如果无效则再尝试使用 HeapAlloc )。其中 LsaAlloc 实际为 lsasrv.dll 导出的 LsaIAllocateHeap 。

LsaAlloc 函数指针初始化过程

然后,我们关注到 NdrpInitializeAsyncMsg 函数,在他里面准备了 _RPC_ASYNC_STATE 结构。这个结构一般用于传递 RPC 过程的额外执行结果。

NdrpInitializeAsyncMsg 函数

看到了一样的缓冲区分配过程,大小为 0x70,也就是 sizeof(RPC_ASYNC_STATE): 

RPC_ASYNC_STATE 结构初始化

在这个函数返回时,将 RPC_ASYNC_STATE 地址保存到 a2 参数也就是 NDR_ASYNC_MESSAGE 结构偏移 0x8 位置上:

拷贝指针到 NDR_ASYNC_MESSAGE 结构

接下来就是将 Binding Handle 拷贝到参数列表上:

Binding Handle 拷贝

此时可以通过动态调试验证结论。在 call  RPCRT4!NdrpInitializeAsyncMsg 后,查看偏移 NDR_ASYNC_MESSAGE 结构 0x238 位置:

找到参数列表地址

跟踪这个地址被修改的位置:

第一次修改

这里将 PRPC_BINDING_HANDLE(地址)拷贝到了参数列表上第二个参数位置处(解组的参数列表严格按照 8 字节对齐,但 Buffer 上序列化的参数似乎是按照 4 字节对齐的)。

Binding Handle

RPC_BINDING_HANDLE 其实和 TEB 有关,在 RPC 过程中使用 RpcServerTestCancel 测试链接需要此句柄。

例如前面提到的 I_WMsgSendMessage 过程:

I_WMsgSendMessage 包含测试 RPC 句柄的过程

第二次修改就是将 RPC_ASYNC_STATE 地址复制到参数列表上:

将 RPC_ASYNC_STATE 地址复制到参数列表

继续调试到该指令附近:

第二次修改

这个就是 RPC_ASYNC_STATE 结构的地址(上面说过在 NdrpInitializeAsyncMsg 函数中被初始化)。

最后是一个关键函数,执行后续 RPC 参数的解组(反序列化)过程:

参数解组(反序列化)

继续调试到参数解组完成时:

第三次修改

主要经过三次修改,最终准备完调用 Invoke 所需要的参数列表。

值得注意的是,参数中前两个参数跟 RPC 本身有关,而后续反序列化得到的参数跟要调用的函数有关。这些后处理的参数在序列化之前是 RPC_MESSAGE 结构的 Buffer 成员指向的缓冲区内容。

根据前面文章的研究,快捷键跟 I_WMsgSendMessage 以及 I_WMsgkSendMessage 有关,所以 Buffer 上的存储了这两个函数在各自调用时的部分参数。

此外,根据我的前面写的文章(屏蔽 Ctrl + Alt + Del 、Ctrl + Shift + Esc 等热键(二)) 中对 WMsgMessageHandler 等函数的分析。上面 2.2 小节分析的 I_WMsgSendMessage 函数的形参中 a3 和 a4 其实分别对应 WMsgMessageHandler 的前两个参数。

对 I_WMsgSendMessage 函数形参的进一步分析

它们可以理解为 WMsg Rpc 消息的低位和高位。Winlogon 使用回调处理消息并执行相应的操作。所以 Buffer 开头并不是单纯的代码(Code),而是参数列表。我的前后两篇文章关系可以联系起来了[注:是 “屏蔽 CAD 热键(二)”、“Rpc-Hook 屏蔽热键(一)” 这两篇]。

我的前后两篇文章关系可以联系起来了


2.5 分析可行的突破点

这个阶段主要有三个值得我们关注的地方,一个就是管理入口向量,第二个是 UUID 在存根中的编号。最后一个是解析 Buffer,这一个也是目前最可行的突破点,方法是 hook 导出的 Ndr64AsyncServerCallAll,然后根据分析出的参数拦截你感兴趣的操作。

虽然在研究过程中,我也发现根据管理入口向量可以找到要调用函数的参数以及函数指针,可以替换函数指针或者参数来修改消息处理过程; UUID 的编号有一个规律那就是必须是奇数,如果是偶数,则会导致调用失败。但是,前者必须要结合调试信息才能直到获取到的入口向量函数名,后者的话则需要一定的 MS-RPC 的认识,你需要知道 UUID 和哪个接口过程对应,RpcViewer 也许是一个不错的选择。

注意:虽然上文提到了 I_WMsgSendMessage  里面有对一个中间返回值的验证,如下图所示。

上文提到的句柄校验过程

需要注意的是,I_WMsgkSendMessage 没有验证 a5 的过程,也不能像 I_WMsgSendMessage 一样在调用 Handler 返回失败后通过 RpcAsyncAbortCall 回滚操作。他只不过是通知客户端取消 RPC 而已,没有强制终止客户端后续执行的能力。

I_WMsgkSendMessage 没有验证 a5 的和操作回滚

但是,二者都可以通过在调用 RpcServerTestCanel 返回 NULL 之后通过 RpcAsyncAbortCall 回滚操作。

TODO: 完善分析过程,补充分析如何激发 NdrAsyncServerCall 的,看看能不能模拟发送消息。

三、通过 Detours 挂钩实现

TODO:将通过调试器验证的理论通过编程来实现,这需要在未来补充。

3.1 Buffer 参数解析代码

首先,通过 Detours 实现挂钩 Ndr64AsyncServerCallAll 函数并按照 4 字节对齐解析参数数组(Buffer 指向的缓冲区)。

#include "pch.h"
#include "detours.h"
#include <WtsApi32.h>
#include <rpc.h>
#include <cstdint>
#include <cwchar>
#include <cstdarg>
#include <string>#pragma comment(lib, "WtsApi32.lib")
#pragma comment(lib, "Rpcrt4.lib")
#pragma comment(lib, "detours.lib")PVOID fpNdr64AsyncServerCallAll = NULL;
void StartHookingFunction();
void UnmappHookedFunction();
BOOL SvcMessageBoxW(LPCWSTR lpTitleBuffer, LPCWSTR lpMsgBuffer,DWORD style, BOOL bWait, PDWORD lpdwResponse);#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;void __RPC_FAR* Buffer;unsigned int BufferLength;unsigned int ProcNum;LPRPC_SYNTAX_IDENTIFIER TransferSyntax;RPC_SERVER_INTERFACE* 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);void RPC_ENTRY HookedNdr64AsyncServerCallAll(LPRPC_MESSAGE pRpcMsg
);typedef _Return_type_success_(return >= 0) LONG NTSTATUS;typedef NTSTATUS* PNTSTATUS;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;
}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();
}std::wstring CoCreateStringBufferW(const wchar_t* wsFormat, ...) {// 使用可变参数列表来处理格式化字符串和参数va_list argsList;va_start(argsList, wsFormat);// 计算格式化后的字符串长度int nLength = _vscwprintf(wsFormat, argsList) + 1; // +1 是为了包含结尾的空字符// 分配内存来存储格式化后的字符串wchar_t* wsBuffer = new (std::nothrow) wchar_t[nLength];if (wsBuffer == nullptr) {va_end(argsList);return L"";}// 格式化字符串到缓冲区中vswprintf_s(wsBuffer, nLength, wsFormat, argsList);// 释放可变参数列表va_end(argsList);// 将 wchar_t* 转换为 std::wstringstd::wstring result(wsBuffer);// 释放临时缓冲区内存delete[] wsBuffer;return result;
}BOOL SvcMessageBoxW(LPCWSTR lpTitleBuffer, LPCWSTR lpMsgBuffer, DWORD style, BOOL bWait, PDWORD lpdwResponse)
{if (lpTitleBuffer == nullptr || lpMsgBuffer == nullptr)return FALSE;std::multiplies<size_t> multiply;DWORD dwTitleLength = (DWORD)multiply(wcslen(lpTitleBuffer) + 1u, sizeof(WCHAR));DWORD dwlpMsgLength = (DWORD)multiply(wcslen(lpMsgBuffer) + 1u, sizeof(WCHAR));if (dwTitleLength == (DWORD)-1 || dwlpMsgLength == (DWORD)-1) {OutputDebugStringW(L"Message buffer length too large.\n");return FALSE;}BOOL rStatus = 0;PWCHAR wcsTitle = new (std::nothrow) WCHAR[dwTitleLength];PWCHAR wcsMsg = new (std::nothrow) WCHAR[dwlpMsgLength];if (wcsTitle == nullptr || wcsMsg == nullptr) {OutputDebugStringW(L"Allocate memory failed.\n");return FALSE;}memset(wcsTitle, 0, dwTitleLength);memcpy_s(wcsTitle, dwTitleLength,(LPVOID)lpTitleBuffer, dwTitleLength);memcpy_s(wcsMsg, dwlpMsgLength, (LPVOID)lpMsgBuffer, dwlpMsgLength);DWORD dwCSessionId = WTSGetActiveConsoleSessionId();rStatus = WTSSendMessageW(WTS_CURRENT_SERVER_HANDLE, dwCSessionId,wcsTitle, dwTitleLength, wcsMsg, dwlpMsgLength, style, 0, lpdwResponse, bWait);delete[] wcsTitle;delete[] wcsMsg;return rStatus;
}void RPC_ENTRY HookedNdr64AsyncServerCallAll(LPRPC_MESSAGE pRpcMsg
)
{// 基址uint64_t iBufferBaseAddr = reinterpret_cast<uintptr_t>(pRpcMsg->Buffer);const UINT bufferLength = pRpcMsg->BufferLength;// 忽略零长度缓冲区(安全调用指针)if (bufferLength == 0 || pRpcMsg->Buffer == nullptr){((__Ndr64AsyncServerCallAll)fpNdr64AsyncServerCallAll)(pRpcMsg);return;}// 按照 BufferLength 解析参数列表const DWORD argsNum = bufferLength / 4u;  // 4 字节对齐参数个数// 在缓冲区上复制参数数据PDWORD argsList = (DWORD*)HeapAlloc(GetProcessHeap(), 0, bufferLength);if (argsList == nullptr) {OutputDebugStringW(L"Allocate memory failed.\n");return;}ZeroMemory(argsList, bufferLength);// 复制 buffer 指向的缓冲区memcpy(argsList, reinterpret_cast<PVOID>(iBufferBaseAddr), bufferLength);std::wstring argsDbgMsg(L"Arguments List:\n");  // 格式化输出信息文本// 遍历数组(严格按照 4 字节对齐的个数)for (UINT index = 0; index < argsNum - 1; index += 2) {argsDbgMsg += CoCreateStringBufferW(L"[%02d]: 0x%08X\t[%02d]: 0x%08X\n", index,argsList[index], index + 1, argsList[index + 1]);}if (argsNum % 2 != 0) {  // 奇数个数末尾(似乎末尾是空字节?)argsDbgMsg += CoCreateStringBufferW(L"[%02d]: 0x%08X\n", argsNum - 1,argsList[argsNum - 1]);}// 打印参数信息(非阻滞)DWORD dwResult = 0;SvcMessageBoxW(L"WMsg Information", argsDbgMsg.c_str(), MB_APPLMODAL | MB_ICONINFORMATION| MB_OK, FALSE,&dwResult);HeapFree(GetProcessHeap(), 0, argsList);return ((__Ndr64AsyncServerCallAll)fpNdr64AsyncServerCallAll)(pRpcMsg);
}

使用通用注入器注入 winlogon 进程:

通用模块注入工具

按下组合键 Ctrl + Shift + Esc ,结果如下:

特殊按键下的消息参数捕获结果

在弹出任务管理器前我们获取了参数信息,并弹出了提示框。

一个发现是,尽管大多数的 Winlogon Message (WMsg) 在处理时, BufferLength 都等于 12 字节,但依然有例外的情况。例如,在切换用户时,可以捕获到多条不同 RPC 处理的 Buffer 参数,其中一条按 4 字节对齐的参数项有 25 个,虽然对齐规则下的参数个数不等于实际的参数个数,但也说明了参数个数远超过 3 个。

参数个数的例外的情况

第二点是我认为缓冲区的末尾为占位空字节,但我选择仍然从开头输出到 BufferLength 指定的结尾。

3.2 RpcServerTestCancel 热补丁代码

通过对 RpcServerTestCancel 热补丁【不需要使用 Detours 和 Dll 模块注入】可以拦截有关 Rpc 操作(这里以不判断参数,直接拦截所有 WMsg 为例,注意:这会导致所有跟 winlogon 有关的系统操作都被禁止,虽然不影响正常运行,但可能会造成睡眠后出现异常,所以,此代码需要和 Buffer 参数解析联合使用)。

主要原理:获取 RpcServerTestCancel 函数地址,修改其指令字节,使其返回 NULL,即可使得调用取消。如图所示:

调用取消过程

修改的指令是:

Raw Hex:

33C0C3   

String Literal:

"\x33\xC0\xC3"

Array Literal:

{ 0x33, 0xC0, 0xC3 }

Disassembly:

0:  33 c0                   xor    eax,eax
2:  c3                        ret

就是异或 eax 寄存器的值,使得返回值变为 0,然后 ret 使调用返回。需要注意的是:在 x86-64 下,该函数的调用约定是 __fastcall 所以不需要被调用函数清理堆栈;而在 x86-32 下,则是 __stdcall 则需要清理和平衡堆栈,将第二条指令改为 retn 4,即 {C2, 04, 00}。

效果演示:

神!在 Windows 上屏蔽系统操作

主要代码(兼容 x32 和 x64 系统,但需要独立编译模块):

#include <windows.h>
#include <tlhelp32.h>
#include <richedit.h>
#include <CommCtrl.h>
#include "CustomDialog.h"
#include <mmsystem.h>
#include <dwmapi.h>
#include <unordered_map>
#include <mutex>
#include <string>
#include "resource.h"
#include <iostream>
#include <string>
#include <sstream>#pragma comment(lib, "winmm.lib")
#pragma comment(lib, "Comctl32.lib")
#pragma comment(lib, "dwmapi.lib")struct ExtStruct {        // 进程退出处理线程HWND hwndMain;DWORD dwWaitMillisecond;
};enum LogLevel { INFO, KERROR, WARNING };#pragma comment(linker,"\"/manifestdependency:type='win32' "\"name='Microsoft.Windows.Common-Controls' "\"version='6.0.0.0' processorArchitecture='*' "\"publicKeyToken='6595b64144ccf1df' language='*'\"")#define EXITTHREADDATA      L"ExitMainThread"
#define TOOLAPPTITLENAME    L"Winlogon RpcServerTestCancel Hook"
#define TOOLAPPCLASSNAME    L"WMsgTestCancelHookWindowClass"
#define TOOLABOUTSTRING     L"Winlogon RpcServerTestCancel Hook\nAuthor:\tLianYou516\nVersion:\t1.0.0.1"
#define MAX_THREAD_NAME_LENGTH 256HINSTANCE g_hInstance;
HWND hRichEdit;
HWND hButtonStart, hButtonStop, hButtonExit, hStatusbar;
bool IsEnabledHook = false;
std::unordered_map<HANDLE, std::wstring> g_ThreadDescriptions;
std::mutex g_ThreadDescriptionsMutex;void AppendText(HWND hEdit, const std::wstring& text) {// 禁用重绘SendMessageW(hEdit, WM_SETREDRAW, FALSE, 0);CHARRANGE cr = { 0 };cr.cpMin = -1;cr.cpMax = -1;SendMessageW(hEdit, EM_EXSETSEL, 0, (LPARAM)&cr);SendMessageW(hEdit, EM_REPLACESEL, FALSE, (LPARAM)text.c_str());// 滚动到文本框底部SendMessageW(hEdit, WM_VSCROLL, SB_BOTTOM, 0);// 启用重绘并强制重新绘制SendMessageW(hEdit, WM_SETREDRAW, TRUE, 0);InvalidateRect(hEdit, NULL, TRUE);SetFocus(hEdit);  // 防止失焦
}void Log(HWND hEdit, const std::wstring& message, LogLevel level) {std::wstringstream ss;switch (level) {case INFO:ss << L"[INFO] ";break;case KERROR:ss << L"[ERROR] ";break;case WARNING:ss << L"[WARNING] ";break;}ss << message << L"\r\n";AppendText(hEdit, ss.str());
}HRESULT MySetThreadDescription(HANDLE hThread, PCWSTR lpThreadDescription) {if (!lpThreadDescription) {return E_POINTER; // Null pointer passed}std::lock_guard<std::mutex> lock(g_ThreadDescriptionsMutex);try {g_ThreadDescriptions[hThread] = lpThreadDescription;}catch (const std::exception& e) {Log(hRichEdit, L"SetThreadDescription Exception: "+ std::wstring(e.what(), e.what() + strlen(e.what())) + L"\n", KERROR);return HRESULT_FROM_WIN32(ERROR_UNHANDLED_EXCEPTION); // Catch all exceptions and convert to HRESULT}return S_OK;  // Success
}HRESULT MyGetThreadDescription(HANDLE hThread, PWSTR* ppszThreadDescription) {if (!ppszThreadDescription) {return E_POINTER; // Null pointer passed}std::lock_guard<std::mutex> lock(g_ThreadDescriptionsMutex);auto it = g_ThreadDescriptions.find(hThread);if (it != g_ThreadDescriptions.end()) {// Allocate memory for the thread descriptionsize_t len = (it->second.length() + 1) * sizeof(wchar_t);*ppszThreadDescription = static_cast<PWSTR>(CoTaskMemAlloc(len));if (*ppszThreadDescription) {wcscpy_s(*ppszThreadDescription, len / sizeof(wchar_t), it->second.c_str());return S_OK;  // Success}else {return E_OUTOFMEMORY;  // Memory allocation failed}}else {return HRESULT_FROM_WIN32(ERROR_NOT_FOUND);  // Thread not found}
}void MyFreeThreadDescription(PWSTR pszThreadDescription) {if (pszThreadDescription) {CoTaskMemFree(pszThreadDescription);}
}// 检查是否以管理员权限启动
bool IsRunAsAdmin() {BOOL isRunAsAdmin = FALSE;PSID adminGroup = NULL;// 获取管理员组的 SIDSID_IDENTIFIER_AUTHORITY ntAuthority = SECURITY_NT_AUTHORITY;if (!AllocateAndInitializeSid(&ntAuthority, 2,SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS,0, 0, 0, 0, 0, 0, &adminGroup)) {return false;}// 检查当前进程的令牌是否包含管理员组的 SIDHANDLE token = NULL;if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {TOKEN_GROUPS* groupInfo = NULL;DWORD size = 0;// 获取令牌的组信息GetTokenInformation(token, TokenGroups, NULL, 0, &size);groupInfo = (TOKEN_GROUPS*)malloc(size);if (groupInfo && GetTokenInformation(token, TokenGroups, groupInfo, size, &size)) {for (DWORD i = 0; i < groupInfo->GroupCount; i++) {if (EqualSid(groupInfo->Groups[i].Sid, adminGroup)) {isRunAsAdmin = TRUE;break;}}}if (groupInfo) {free(groupInfo);}CloseHandle(token);}if (adminGroup) {FreeSid(adminGroup);}return isRunAsAdmin;
}// 启用 SE_DEBUG 权限
bool EnableDebugPrivilege() {HANDLE token;LUID luid;TOKEN_PRIVILEGES tp;if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token)) {return false;}if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) {CloseHandle(token);return false;}tp.PrivilegeCount = 1;tp.Privileges[0].Luid = luid;tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;if (!AdjustTokenPrivileges(token, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) {CloseHandle(token);return false;}CloseHandle(token);return GetLastError() == ERROR_SUCCESS;
}bool ModifyFunctionInWinlogon(bool enable, HWND hEdit) {Log(hEdit, enable ? L"Blocking Winlogon Messages..." : L"Enabling Winlogon Messages...", INFO);Log(hEdit, L"Attempting to get module handle of Rpcrt4...", INFO);auto module = GetModuleHandleW(L"rpcrt4.dll");if (!module) {Log(hEdit, L"Failed to get module handle.", KERROR);return false;}WCHAR wsModBuffer[55];ZeroMemory(wsModBuffer, 55 * sizeof(WCHAR));swprintf_s(wsModBuffer, L"Rpcrt4 Module Handle: 0x%p", module);Log(hEdit, wsModBuffer, INFO);Log(hEdit, L"Attempting to get function RpcServerTestCancel's address...", INFO);auto func = GetProcAddress(module, "RpcServerTestCancel");if (!func) {Log(hEdit, L"Failed to get function address.", KERROR);return false;}WCHAR wsAddsBuffer[55];ZeroMemory(wsAddsBuffer, 55 * sizeof(WCHAR));swprintf_s(wsAddsBuffer, L"RpcServerTestCancel's address: 0x%p", func);Log(hEdit, wsAddsBuffer, INFO);auto snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);if (snapshot == INVALID_HANDLE_VALUE) {Log(hEdit, L"Failed to create snapshot.", KERROR);return false;}PROCESSENTRY32W pe32 = { sizeof(PROCESSENTRY32W) };bool success = false;if (Process32FirstW(snapshot, &pe32)) {Log(hEdit, L"Scanning processes...", INFO);do {// 跳过系统空闲进程if (pe32.th32ProcessID <= 4u)continue;Log(hEdit, std::wstring(L"Found process: ") + pe32.szExeFile, INFO);if (!wcscmp(pe32.szExeFile, L"winlogon.exe")) {Log(hEdit, L"Target process found: winlogon.exe", INFO);WCHAR wszPID[25];swprintf_s(wszPID, L"Target PID: %u", pe32.th32ProcessID);Log(hEdit, wszPID, INFO);Log(hEdit, L"Attempting to open winlogon.exe process...", INFO);auto hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pe32.th32ProcessID);if (hProcess) {Log(hEdit, L"Opened process handle successfully.", INFO);Log(hEdit, L"Attempting to turn off memory protection...", INFO);DWORD oldProtect;if (VirtualProtectEx(hProcess, (void*)func, 0x5, PAGE_EXECUTE_READWRITE, &oldProtect)){Log(hEdit, L"Successfully turned off memory protection.", INFO);/**   在这里对函数返回值自身异或,将得到 NULL,返回值应该不为空,*   如果为空,则表明连接的一方已终止。*   其他终结点应该调用 RpcAsyncAbortCall 结束调用。*     __________________________________________________*    /                                                  \*   |  Raw Hex:                                          |*   |              33C0C3                                |*   |                                                    |*   |  String Literal:                                   |*   |              "\x33\xC0\xC3"                        |*   |                                                    |*   |  Array Literal:                                    |*   |              { 0x33, 0xC0, 0xC3 }                  |*   |                                                    |*   |  Disassembly:                                      |*   |              0 : 33 c0           xor eax, eax      |*   |              2 : c3              ret               |*    \ ________________________________________________ /*/#ifdef _WIN32#ifndef _WIN64unsigned char buf[] = { 0x33, 0xc0, 0xc2, 0x04, 0 };   // x86-32 为 stdcall 希望被调用函数清理堆栈#elseunsigned char buf[] = { 0x33, 0xc0, 0xc3 };#endif#endifif (enable) {Log(hEdit, L"Attempting to write instruction bytes for patch...", INFO);success = WriteProcessMemory(hProcess, (void*)func, buf, sizeof(buf), NULL);}else {Log(hEdit, L"Attempting to recover instruction bytes...", INFO);success = WriteProcessMemory(hProcess, (void*)func, (void*)func, sizeof(buf), NULL);}Log(hEdit, L"Attempting to turn on memory protection...", INFO);VirtualProtectEx(hProcess, (void*)func, 0x5, oldProtect, &oldProtect);if (success) {IsEnabledHook = enable;Log(hEdit, L"Memory written successfully.", INFO);}else {IsEnabledHook = false;Log(hEdit, L"Failed to write memory.", KERROR);}}else {success = false;Log(hEdit, L"Failed to change memory protection.", KERROR);}CloseHandle(hProcess);Log(hEdit, L"Closed process handle.", INFO);}else {success = false;Log(hEdit, L"Failed to open process.", KERROR);}break;}} while (Process32NextW(snapshot, &pe32));}else {Log(hEdit, L"Failed to retrieve first process.", KERROR);}CloseHandle(snapshot);return success;
}HBITMAP CreateBitmapFromIcon(HICON hIcon, int width, int height) {HDC hdcScreen = GetDC(NULL);HDC hdcMem = CreateCompatibleDC(hdcScreen);// Create a bitmap with an alpha channelBITMAPINFO bmi = { 0 };bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);bmi.bmiHeader.biWidth = width;bmi.bmiHeader.biHeight = -height; // Negative height to create a top-down DIBbmi.bmiHeader.biPlanes = 1;bmi.bmiHeader.biBitCount = 32;bmi.bmiHeader.biCompression = BI_RGB;void* pBits = NULL;HBITMAP hBitmap = CreateDIBSection(hdcScreen, &bmi, DIB_RGB_COLORS, &pBits, NULL, 0);if (hBitmap){HBITMAP hOldBitmap = (HBITMAP)SelectObject(hdcMem, hBitmap);// Fill background with transparent colorBLENDFUNCTION bf = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };GdiAlphaBlend(hdcMem, 0, 0, width, height, hdcMem, 0, 0, width, height, bf);// Draw icon onto the bitmapDrawIconEx(hdcMem, 0, 0, hIcon, width, height, 0, NULL, DI_NORMAL);SelectObject(hdcMem, hOldBitmap);DeleteDC(hdcMem);ReleaseDC(NULL, hdcScreen);//DeleteObject(hBitmap);}return hBitmap;
}// 播放背景音乐函数
void PlayBackgroundMusic()
{// 从资源中加载并播放音频PlaySoundW(MAKEINTRESOURCE(IDR_WAVE1), GetModuleHandleW(NULL), SND_RESOURCE | SND_ASYNC | SND_LOOP);
}LRESULT CALLBACK RichEditSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR, DWORD_PTR) {switch (uMsg) {case WM_KEYDOWN:switch (wParam) {case VK_DELETE:case VK_BACK:// 通过返回0阻止键盘删除操作return 0;case 'A':if (GetKeyState(VK_CONTROL) & 0x8000) {// 按下Ctrl+A时选择所有文本SendMessageW(hwnd, EM_SETSEL, 0, -1);return 0;}break;}break;case WM_CHAR:  // 阻止英文字符输入case WM_IME_COMPOSITION: // 阻止IME(输入法编辑器)合成case WM_PASTE:return 0; // 阻止文本输入(粘贴)case WM_RBUTTONDOWN: {  // 创建右键菜单HMENU hMenu = CreatePopupMenu();AppendMenuW(hMenu, MF_STRING, ID_SELECT_ALL, L"Select All");AppendMenuW(hMenu, MF_STRING, ID_CANCEL, L"Cancel");AppendMenuW(hMenu, MF_STRING, ID_COPY, L"Copy");AppendMenuW(hMenu, MF_STRING, ID_CLEAR, L"Clean Up");POINT pt;GetCursorPos(&pt);TrackPopupMenu(hMenu, TPM_RIGHTBUTTON, pt.x, pt.y, 0, hwnd, NULL);DestroyMenu(hMenu);return 0;}case WM_COMMAND:   // 添加右键菜单响应switch (LOWORD(wParam)) {case ID_SELECT_ALL:SendMessageW(hwnd, EM_SETSEL, 0, -1);break;case ID_CANCEL:SendMessageW(hwnd, EM_SETSEL, -1, 0);break;case ID_COPY:SendMessageW(hwnd, WM_COPY, 0, 0);break;case ID_CLEAR:SetWindowTextW(hwnd, L"");break;}break;}return DefSubclassProc(hwnd, uMsg, wParam, lParam);
}DWORD WINAPI OnExitMainWnd(LPVOID lpThreadParameter)
{MySetThreadDescription(GetCurrentThread(), EXITTHREADDATA);ExtStruct* extSt = (ExtStruct*)lpThreadParameter;Log(hRichEdit, L"Process is closing...", INFO);// 必须先接触挂钩才能退出进程if (IsEnabledHook){bool status = ModifyFunctionInWinlogon(false, hRichEdit);if (status)SendMessage(hStatusbar, SB_SETTEXT, 0, (LPARAM)L"Hook Disabled");}Sleep(extSt->dwWaitMillisecond);// 用户点击了确定按钮,关闭窗口if (extSt->hwndMain != NULL)PostMessageW(extSt->hwndMain, WM_DESTROY, GetCurrentThreadId(), 1);return 0;
}LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {static HDC hdcBackBuffer;static HBITMAP hbmBackBuffer;static HBITMAP hbmOldBuffer;static RECT clientRect;static HBITMAP hBackgroundBitmap = NULL;switch (uMsg) {case WM_CREATE: {LoadLibraryW(L"Msftedit.dll");// 创建字体HFONT hFont = CreateFontW(35, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET,OUT_OUTLINE_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,VARIABLE_PITCH, L"Segoe UI");// 添加控件窗口hButtonStart = CreateWindowW(L"BUTTON", L"Start", WS_VISIBLE | WS_CHILD,10, 10, 90, 45,hwnd, (HMENU)ID_START,NULL, NULL);hButtonStop = CreateWindowW(L"BUTTON", L"Stop", WS_VISIBLE | WS_CHILD | WS_DISABLED,   // 初始状态是禁止的110, 10, 90, 45,hwnd, (HMENU)ID_STOP,NULL, NULL);hButtonExit = CreateWindowW(L"BUTTON", L"Exit",WS_VISIBLE | WS_CHILD,210, 10, 90, 45,hwnd, (HMENU)ID_EXIT,NULL, NULL);SendMessageW(hButtonStart, WM_SETFONT, (WPARAM)hFont, TRUE);SendMessageW(hButtonStop, WM_SETFONT, (WPARAM)hFont, TRUE);SendMessageW(hButtonExit, WM_SETFONT, (WPARAM)hFont, TRUE);// 创建富文本框hRichEdit = CreateWindowW(MSFTEDIT_CLASS, NULL,WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE |ES_AUTOVSCROLL,10, 60, 260, 200,hwnd, NULL,NULL, NULL);// 设置背景色为暗色SendMessageW(hRichEdit, EM_SETBKGNDCOLOR, FALSE, RGB(30, 30, 30));// 设置文本颜色为亮色CHARFORMATW cf = { 0 };SendMessageW(hRichEdit, EM_SETCHARFORMAT, SCF_ALL, (LPARAM)&cf);cf.cbSize = sizeof(CHARFORMATW);cf.dwMask = CFM_COLOR;cf.crTextColor = RGB(255, 255, 255);SendMessageW(hRichEdit, EM_SETCHARFORMAT, SCF_ALL, (LPARAM)&cf);// 窗口子类化,便于对富文本框添加右键菜单和限制编辑操作SetWindowSubclass(hRichEdit, RichEditSubclassProc, 0, 0);// 添加 “关于”到系统菜单HMENU hSysMenu = GetSystemMenu(hwnd, FALSE);AppendMenuW(hSysMenu, MF_SEPARATOR, 0, NULL);AppendMenuW(hSysMenu, MF_STRING, ID_ABOUT, L"&About ...(A)");// 修改 SetMenuItemInfo 调用以包含快捷键和位图HICON hIcon = LoadIconW(NULL, IDI_ASTERISK);if (hIcon){HBITMAP hBitmap = CreateBitmapFromIcon(hIcon, 20, 20);MENUITEMINFO mii = { sizeof(MENUITEMINFO) };mii.fMask = MIIM_BITMAP | MIIM_ID | MIIM_STATE;mii.wID = ID_ABOUT;mii.hbmpItem = hBitmap;SetMenuItemInfoW(hSysMenu, ID_ABOUT, FALSE, &mii);DestroyIcon(hIcon);}// 创建状态栏hStatusbar = CreateWindowExW(0, STATUSCLASSNAME, NULL,WS_CHILD | WS_VISIBLE | SBARS_SIZEGRIP,0, 0, 0, 0, hwnd, NULL, NULL, NULL);int parts[] = { 155, -1 };SendMessageW(hStatusbar, SB_SETPARTS, sizeof(parts) / sizeof(int), (LPARAM)parts);SendMessageW(hStatusbar, SB_SETTEXT, 0, (LPARAM)L"Hook Disabled");// 启用双缓冲GetClientRect(hwnd, &clientRect);HDC hdc = GetDC(hwnd);hdcBackBuffer = CreateCompatibleDC(hdc);hbmBackBuffer = CreateCompatibleBitmap(hdc, clientRect.right, clientRect.bottom);hbmOldBuffer = (HBITMAP)SelectObject(hdcBackBuffer, hbmBackBuffer);ReleaseDC(hwnd, hdc);// 启用管理员权限if (!IsRunAsAdmin() || !EnableDebugPrivilege()){Log(hRichEdit, L"The program must be launched as an administrator.", KERROR);EnableWindow(hButtonStart, FALSE);EnableWindow(hButtonStop, FALSE);//EnableWindow(hButtonExit, FALSE);}// 最后启用背景音乐PlayBackgroundMusic();break;}case WM_GETMINMAXINFO: {MINMAXINFO* lpMinMax = (MINMAXINFO*)lParam;lpMinMax->ptMinTrackSize.x = 580; // 设置窗口的最小宽度lpMinMax->ptMinTrackSize.y = 480; // 设置窗口的最小高度return 0;}case WM_SIZE: {  // 当客户区窗口大小发生变化时,动态调整控件的位置GetClientRect(hwnd, &clientRect);//SetWindowPos(hRichEdit, NULL, 10, 65, clientRect.right - 20, clientRect.bottom - 175, SWP_NOZORDER);// 调整后缓冲区尺寸HDC hdc = GetDC(hwnd);HBITMAP hbmNewBuffer = CreateCompatibleBitmap(hdc, clientRect.right, clientRect.bottom);SelectObject(hdcBackBuffer, hbmNewBuffer);DeleteObject(hbmBackBuffer);hbmBackBuffer = hbmNewBuffer;ReleaseDC(hwnd, hdc);int btnWidth = 90;int btnHeight = 45;int spacing = 20;int totalWidth = btnWidth * 3 + spacing * 2;int startX = (clientRect.right - totalWidth) / 2;int startY = 10;SetWindowPos(hButtonStart, NULL, startX, startY, 0, 0, SWP_NOSIZE | SWP_NOZORDER);SetWindowPos(hButtonStop, NULL, startX + btnWidth + spacing, startY, 0, 0, SWP_NOSIZE | SWP_NOZORDER);SetWindowPos(hButtonExit, NULL, startX + 2 * (btnWidth + spacing), startY, 0, 0, SWP_NOSIZE | SWP_NOZORDER);int richEditHeight = clientRect.bottom - startY - btnHeight - 45;SetWindowPos(hRichEdit, NULL, 10, startY + btnHeight + 10, clientRect.right - 20, richEditHeight, SWP_NOZORDER);SetWindowPos(hStatusbar, NULL, 0, clientRect.bottom - 30, clientRect.right, 25, SWP_NOZORDER);InvalidateRect(hwnd, NULL, FALSE);break;}case WM_KEYDOWN:if (IsWindowVisible((HWND)GetSystemMenu(hwnd, FALSE)) && (wParam == 'A')) {SendMessageW(hwnd, WM_SYSCOMMAND, ID_ABOUT, 0);return 0;}break;case WM_SYSCOMMAND: {if (wParam == ID_ABOUT) {MessageBoxW(hwnd,TOOLABOUTSTRING,L"About",MB_OK | MB_ICONINFORMATION | MB_APPLMODAL);return 0;}return DefWindowProcW(hwnd, uMsg, wParam, lParam);}case WM_COMMAND: {switch (LOWORD(wParam)) {case ID_START:if (!IsEnabledHook){Log(hRichEdit, L"============== Start ==============", INFO);bool status = ModifyFunctionInWinlogon(true, hRichEdit);Log(hRichEdit, L"==================================", INFO);if (status){EnableWindow(hButtonStart, FALSE);EnableWindow(hButtonStop, TRUE);SendMessageW(hStatusbar, SB_SETTEXT, 0, (LPARAM)L"Hook Enabled");}//PlaySoundW(L"MouseClick", NULL, SND_ASYNC);}break;case ID_STOP:if (IsEnabledHook){Log(hRichEdit, L"============== Stop ==============", INFO);bool status = ModifyFunctionInWinlogon(false, hRichEdit);Log(hRichEdit, L"==================================", INFO);if (status){EnableWindow(hButtonStop, FALSE);EnableWindow(hButtonStart, TRUE);SendMessageW(hStatusbar, SB_SETTEXT, 0, (LPARAM)L"Hook Disabled");}//PlaySoundW(L"MouseClick", NULL, SND_ASYNC);}break;case ID_EXIT:PostMessageW(hwnd, WM_CLOSE, 0, 0);break;}break;}case WM_ERASEBKGND:return 1; // 通过绕过默认擦除背景防止闪烁case WM_PAINT: {PAINTSTRUCT ps;HDC hdc = BeginPaint(hwnd, &ps);// 在后缓冲区上绘制背景FillRect(hdcBackBuffer, &clientRect, (HBRUSH)(COLOR_WINDOW + 1));// 画中间分割控件空间的黑线HPEN hPen = CreatePen(PS_SOLID, 1, RGB(0, 0, 0));HPEN hOldPen = (HPEN)SelectObject(hdcBackBuffer, hPen);MoveToEx(hdcBackBuffer, 10, 60, NULL);LineTo(hdcBackBuffer, clientRect.right - 10, 60);SelectObject(hdcBackBuffer, hOldPen);DeleteObject(hPen);// 将后缓冲区覆盖到前缓冲区BitBlt(hdc, 0, 0, clientRect.right, clientRect.bottom, hdcBackBuffer, 0, 0, SRCCOPY);EndPaint(hwnd, &ps);break;}case WM_CLOSE:{// 首先备份按钮状态并禁用按钮操作bool isEnBTStart = IsWindowEnabled(hButtonStart);bool isEnBTStop = IsWindowEnabled(hButtonStop);EnableWindow(hButtonStart, FALSE);EnableWindow(hButtonStop, FALSE);// 获取光标位置POINT cursorPos;GetCursorPos(&cursorPos);ScreenToClient(hwnd, &cursorPos);// 获取窗口矩形RECT windowRect;GetWindowRect(hwnd, &windowRect);// 计算标题栏宽度int titleBarWidth = GetSystemMetrics(SM_CXSIZEFRAME);int titleBarHight = GetSystemMetrics(SM_CYSIZEFRAME);// 检查光标是否在标题栏左边区域if ((cursorPos.x < (windowRect.left + titleBarWidth) * 0.34)&& (cursorPos.y < (windowRect.top + titleBarHight) * 0.05)){// 禁止关闭窗口,此操作防止双击标题栏图标触发关闭return 0;}// 显示自定义对话框INT_PTR result = CustomDialog::Show(hwnd, g_hInstance);if (result == IDOK) {static ExtStruct ext;ext.hwndMain = hwnd;ext.dwWaitMillisecond = 1000;CreateThread(nullptr, 0, OnExitMainWnd, &ext, 0, nullptr);}else {  // 用户点击了取消按钮,不执行关闭操作// 根据备份的状态恢复按钮EnableWindow(hButtonStart, isEnBTStart);EnableWindow(hButtonStop, isEnBTStop);}break;}case WM_DESTROY: {// 关闭背景音乐PlaySoundW(NULL, GetModuleHandleW(NULL), SND_SYNC);HANDLE hRemoteThread = OpenThread(THREAD_QUERY_INFORMATION, FALSE, (DWORD)wParam);if (!hRemoteThread)break;PWSTR lpThreadData = nullptr;HRESULT hr = MyGetThreadDescription(GetCurrentThread(), &lpThreadData);if (SUCCEEDED(hr) && lpThreadData != nullptr&& !wcscmp(lpThreadData, EXITTHREADDATA)){SelectObject(hdcBackBuffer, hbmOldBuffer);DeleteObject(hbmBackBuffer);DeleteDC(hdcBackBuffer);MyFreeThreadDescription(lpThreadData);PostQuitMessage(0);}else {if(lpThreadData) MyFreeThreadDescription(lpThreadData);Log(hRichEdit, L"Verification of caller thread safety failed.", KERROR);if (IDYES == MessageBoxW(NULL, L"Are you sure you want to continue closing the window?",L"Raise Exception",MB_YESNO | MB_SYSTEMMODAL | MB_ICONEXCLAMATION)) {SelectObject(hdcBackBuffer, hbmOldBuffer);DeleteObject(hbmBackBuffer);DeleteDC(hdcBackBuffer);PostQuitMessage(0);}}break;}default:return DefWindowProcW(hwnd, uMsg, wParam, lParam);}return 0;
}int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) {g_hInstance = hInstance;//SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);const wchar_t CLASS_NAME[] = TOOLAPPCLASSNAME;WNDCLASS wc = {};wc.lpfnWndProc = WindowProc;wc.hInstance = hInstance;wc.lpszClassName = CLASS_NAME;// 加载不同尺寸的图标资源HICON hIcon48 = (HICON)LoadImageW(hInstance, MAKEINTRESOURCEW(IDI_ICON1), IMAGE_ICON, 48, 48, LR_DEFAULTCOLOR);HICON hIcon32 = (HICON)LoadImageW(hInstance, MAKEINTRESOURCEW(IDI_ICON2), IMAGE_ICON,32, 32, LR_DEFAULTCOLOR);// 设置窗口类图标wc.hIcon = hIcon32;     // 设置小图标wc.hCursor = LoadCursorW(hInstance, IDC_ARROW);wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);  //   (COLOR_WINDOW + 1);wc.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;if (!RegisterClassW(&wc)) {return 0;}HWND hwnd = CreateWindowExW(WS_EX_OVERLAPPEDWINDOW,CLASS_NAME,TOOLAPPTITLENAME,WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT, 760, 600,NULL, NULL, hInstance, NULL);if (hwnd == NULL) {return 0;}// 获取屏幕尺寸int screenWidth = GetSystemMetrics(SM_CXSCREEN);int screenHeight = GetSystemMetrics(SM_CYSCREEN);// 获取窗口尺寸RECT windowRect;GetWindowRect(hwnd, &windowRect);int windowWidth = windowRect.right - windowRect.left;int windowHeight = windowRect.bottom - windowRect.top;// 计算窗口居中位置int posX = (screenWidth - windowWidth) / 2;int posY = (screenHeight - windowHeight) / 2;// 设置窗口位置SetWindowPos(hwnd, NULL, posX, posY, 0, 0, SWP_NOSIZE | SWP_NOZORDER);// 设置窗口图标SendMessageW(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon48);   // 设置大图标SendMessageW(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hIcon32); // 设置小图标BOOL value = TRUE;::DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value));// 设置分层窗口和透明度SetWindowLongW(hwnd, GWL_EXSTYLE, GetWindowLongW(hwnd, GWL_EXSTYLE) | WS_EX_LAYERED);SetLayeredWindowAttributes(hwnd, 0, (255 * 75) / 100, LWA_ALPHA);// 显示窗口ShowWindow(hwnd, nCmdShow);UpdateWindow(hwnd);MSG msg = {};while (GetMessageW(&msg, NULL, 0, 0)) {TranslateMessage(&msg);DispatchMessageW(&msg);}return 0;
}

工具界面:

RpcServerTestCancel 拦截器界面

操作效果展示:

(1)HotPatch 前后对比:

HotPatch 前后对比

(2)界面设计:

半透明界面设计

下载此工具:

链接:https://pan.baidu.com/s/18AwCUi0IKCRzKQDsubjOyA?pwd=6666

提取码:6666

完整源代码请私信有偿获取。

3.3 更多实例代码未来补充

... ...

四、总结

本文从逆向工程的角度出发,解释了我这一系列有关系统快捷键拦截教程的实现原理。解释了之前未明确指出的 Buffer 特征字节、WMsg 消息回调参数和 NDR LPC 之间的关系。并给出了更多拦截操作的突破口。

我相信对于拦截 Ctrl + Shift + Esc 、Ctrl + Alt + Del、提权、电源操作等消息的方法不止这几篇文章所提到的方法。对于一些事物,我的分析是建立于已经掌握的信息上的,且目前我能够涉及的领域有限,必然存在疏漏的地方。我已经尽量使得这篇文章的思路完整、条理清晰,但如果发现有任何错误或容易产生混淆的地方,还请各位不吝指出。

参考文献

1.CVE-2021-26411在野样本中利用RPC绕过CFG缓解技术的研究

2.在Windbg中明查OS实现UAC验证全流程[1]-安全客

3.在Windbg中明查OS实现UAC验证全流程[2]-安全客

4.XPSP1 - ndr64 - async.c

5.NewJeans' Hyper-V Part 5 - CVE-2018-0959 Exploit(2) - hackyboiz

6.Remote Procedure Call debugging | KK's Blog

7.Marshalling 百度百科

8.基于rpc调用-动态加载ssp_ndrclientcall3-CSDN博客

9.GUID---and---UUID---and---LUID_uuid luid-CSDN博客

10.Windows RPC Study | l1nk3dHouse


本文出处链接:[https://blog.csdn.net/qq_59075481/article/details/135543495],

转载请注明出处。

撰写于:2024.01.20-2024.06.07;发布于:2024.07.03;修改于:2024.07.06.

相关文章:

Hook 实现 Windows 系统热键屏蔽(二)

目录 前言 一、介绍用户账户控制&#xff08;UAC&#xff09; 1.1 什么是 UAC &#xff1f; 2.2 UAC 运行机制的概述 2.3 分析 UAC 提权参数 二、 NdrAsyncServerCall 函数的分析 2.1 函数声明的解析 2.2 对 Winlogon 的逆向 2.3 对 rpcrt4 的静态分析 2.4 对 rpcrt4…...

SQL窗口函数详解

详细说明在sql中窗口函数是什么&#xff0c;为什么需要窗口函数&#xff0c;有普通的聚合函数了那窗口函数的意义在哪&#xff0c;窗口函数的执行逻辑是什么&#xff0c;over中的字句是如何使用和理解的&#xff08;是不是句句戳到你的痛点&#xff0c;哼哼&#xff5e;&#x…...

如何用Java写一个整理Java方法调用关系网络的程序

大家好&#xff0c;我是猿码叔叔&#xff0c;一位 Java 语言工作者&#xff0c;也是一位算法学习刚入门的小学生。很久没有为大家带来干货了。 最近遇到了一个问题&#xff0c;大致是这样的&#xff1a;如果给你一个 java 方法&#xff0c;如何找到有哪些菜单在使用。我的第一想…...

基于STM32设计的管道有害气体检测装置(ESP8266局域网)176

基于STM32设计的管道有害气体检测装置(176) 文章目录 一、前言1.1 项目介绍【1】项目功能介绍【2】项目硬件模块组成【3】ESP8266模块配置【4】上位机开发思路【5】项目模块划分【6】LCD显示屏界面布局【7】上位机界面布局1.2 项目功能需求1.3 项目开发背景1.4 开发工具的选择1…...

iCloud照片库全指南:云端存储与智能管理

iCloud照片库全指南&#xff1a;云端存储与智能管理 在数字化时代&#xff0c;照片和视频成为了我们生活中不可或缺的一部分。随着手机摄像头质量的提升&#xff0c;我们记录生活点滴的方式也越来越丰富。然而&#xff0c;这也带来了一个问题&#xff1a;如何有效管理和存储日…...

IDEA中使用Maven打包及碰到的问题

1. 项目打包 IDEA中&#xff0c;maven打包的方式有两种&#xff0c;分别是 install 和 package &#xff0c;他们的区别如下&#xff1a; install 方式 install 打包时做了两件事&#xff0c;① 将项目打包成 jar 或者 war&#xff0c;打包结果存放在项目的 target 目录下。…...

TreeMap、HashMap 和 LinkedHashMap 的区别

TreeMap、HashMap 和 LinkedHashMap 的区别 1、HashMap2、LinkedHashMap3、TreeMap4、总结 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在 Java 中&#xff0c;TreeMap、HashMap 和 LinkedHashMap 是三种常用的集合类&#xff0c;它们在…...

【跟我学K8S】45天入门到熟练详细学习计划

目录 一、什么是K8S 核心功能 架构组件 使用场景 二、入门到熟练的学习计划 第一周&#xff1a;K8s基础和概念 第二周&#xff1a;核心对象和网络 第三周&#xff1a;进阶使用和管理 第四周&#xff1a;CI/CD集成和监控 第五周&#xff1a;实战模拟和案例分析 第六周…...

ubuntu下载Nginx

一、Nginx下载安装&#xff08;Ubuntu系统&#xff09; 1.nginx下载 sudo apt-get install nginx2.nginx启动 启动命令 sudo nginx重新编译(每次更改完nginx配置文件后运行&#xff09;&#xff1a; sudo nginx -s reload3.测试nginx是否启动成功 打开浏览器访问本机80端口…...

【区分vue2和vue3下的element UI Dialog 对话框组件,分别详细介绍属性,事件,方法如何使用,并举例】

在 Vue 2 和 Vue 3 中&#xff0c;Element UI&#xff08;针对 Vue 2&#xff09;和 Element Plus&#xff08;针对 Vue 3&#xff09;提供了 Dialog 对话框组件&#xff0c;用于在页面中显示模态对话框。这两个库中的 Dialog 组件在属性、事件和方法的使用上有所相似&#xff…...

docker push 推送镜像到阿里云仓库

1.登陆阿里云 镜像服务&#xff0c;跟着指引操作就行 创建个人实例&#xff0c;创建命名空间、镜像仓库&#xff0c;绑定代码源头 2.将镜像推送到Registry $ docker login --username*** registry.cn-beijing.aliyuncs.com $ docker tag [ImageId] registry.cn-beijing.aliy…...

伯克利、斯坦福和CMU面向具身智能端到端操作联合发布开源通用机器人Policy,可支持多种机器人执行多种任务

不同于LLM或者MLLM那样用于上百亿甚至上千亿参数量的大模型&#xff0c;具身智能端到端大模型并不追求参数规模上的大&#xff0c;而是指其能吸收大量的数据&#xff0c;执行多种任务&#xff0c;并能具备一定的泛化能力&#xff0c;如笔者前博客里的RT1。目前该领域一个前沿工…...

昇思25天学习打卡营第17天(+1)|Diffusion扩散模型

1. 学习内容复盘 本文基于Hugging Face&#xff1a;The Annotated Diffusion Model一文翻译迁移而来&#xff0c;同时参考了由浅入深了解Diffusion Model一文。 本教程在Jupyter Notebook上成功运行。如您下载本文档为Python文件&#xff0c;执行Python文件时&#xff0c;请确…...

【Leetcode笔记】406.根据身高重建队列

文章目录 1. 题目要求2.解题思路 注意3.ACM模式代码 1. 题目要求 2.解题思路 首先&#xff0c;按照每个人的身高属性&#xff08;即people[i][0]&#xff09;来排队&#xff0c;顺序是从大到小降序排列&#xff0c;如果遇到同身高的&#xff0c;按照另一个属性&#xff08;即p…...

Linux 安装pdfjam (PDF文件尺寸调整)

跟Ghostscript搭配使用&#xff0c;这样就可以将不同尺寸的PDF调整到相同尺寸合并了。 在 CentOS 上安装 pdfjam 需要安装 TeX Live&#xff0c;因为 pdfjam 是基于 TeX Live 的。以下是详细的步骤来安装 pdfjam&#xff1a; ### 步骤 1: 安装 EPEL 仓库 首先&#xff0c;安…...

python+playwright 学习-90 and_ 和 or_ 定位

前言 playwright 从v1.34 版本以后支持and_ 和 or_ 定位 XPath 中的and和or xpath 语法中我们常用的有text()、contains() 、ends_with()、starts_with() //*[text()="文本"] //*[contains(@id, "xx")] //...

亲子时光里的打脸高手,贾乃亮与甜馨的父爱如山

贾乃亮这波操作&#xff0c;简直是“实力打脸”界的MVP啊&#xff01; 7月5号&#xff0c;他一甩手&#xff0c;甩出张合照&#xff0c; 瞬间让多少猜测纷飞的小伙伴直呼&#xff1a;“脸疼不&#xff1f;”带着咱家小甜心甜馨&#xff0c; 回了哈尔滨老家&#xff0c;这趟亲…...

MySQL篇-SQL优化实战

SQL优化措施 通过我们日常开发的经验可以整理出以下高效SQL的守则 表主键使用自增长bigint加适当的表索引&#xff0c;需要强关联字段建表时就加好索引&#xff0c;常见的有更新时间&#xff0c;单号等字段减少子查询&#xff0c;能用表关联的方式就不用子查询&#xff0c;可…...

【MySQL备份】Percona XtraBackup总结篇

目录 1.前言 2.问题总结 2.1.为什么在恢复备份前需要准备备份 2.1.1. 保证数据一致性 2.1.2. 完成崩溃恢复过程 2.1.3. 解决非锁定备份的特殊需求 2.1.4. 支持增量和差异备份 2.1.5. 优化恢复性能 2.2.Percona XtraBackup的工作原理 3.注意事项 1.前言 在历经了详尽…...

【Git 】规范 Git 提交信息的工具 Commitizen

Commitizen是一个用于规范Git提交信息的工具&#xff0c;它旨在帮助开发者生成符合一定规范和风格的提交信息&#xff0c;从而提高代码维护的效率&#xff0c;便于追踪和定位问题。以下是对Commitizen的详细介绍。 1、Commitizen的作用与优势 规范提交信息&#xff1a;通过提供…...

ABB PPC902AE1013BHE010751R0101控制器 处理器 模块

ABB PPC902AE1013BHE010751R0101 该模块是用于自动化和控制系统的高性能可编程控制器。它旨在与其他自动化和控制设备一起使用&#xff0c;以提供完整的系统解决方案 是一种数字输入/输出模块&#xff0c;提供了高水平的性能和可靠性。它专为苛刻的工业应用而设计&#xff0c…...

大模型AIGC转行记录(一)

自从22年11月chat gpt上线以来&#xff0c;这一轮的技术浪潮便变得不可收拾。我记得那年9月份先是在技术圈内讨论&#xff0c;然后迅速地&#xff0c;全社会在讨论&#xff0c;各个科技巨头、金融机构、政府部门快速跟进。 软件开发行业过去与现状 我19年决定转码的时候&…...

element-ui Tree之懒加载叶子节点强制设置父级半选效果

效果&#xff1a; 前言&#xff1a; 我们是先只展示一级的&#xff0c;二级的数据是通过点击之后通过服务器获取数据&#xff0c;并不是全量数据直接一起返回回来的。 问题&#xff1a; 当你设置了默认选中的子节点&#xff0c;但是由于刚进入页面此时tree中数据暂是没有这个…...

Java项目:基于SSM框架实现的高校共享单车管理系统分前后台【ssm+B/S架构+源码+数据库+开题报告+任务书+毕业论文】

一、项目简介 本项目是一套基于SSM框架实现的高校共享单车管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观、操作简单、…...

【Android】自定义换肤框架02之自定义AssetManager和Resource

ResourceId是如何变成对应Resource的 在上一章中&#xff0c;我们已经讲过&#xff0c;apk中有个资源索引文件 其中保存了每个资源对应的id&#xff0c;name&#xff0c;type&#xff0c;path 资源文件的解析&#xff0c;主要涉及两个类&#xff0c;AssetManager和Resource …...

熵权法、熵值法、熵权TOPSIS三种方法的实用场景及优劣比较

在统计分析与决策分析中,熵权法、熵值法和熵权TOPSIS是三种常用的基于信息熵理论的方法。这些方法在处理多属性或多指标决策问题时,能够客观地反映数据的内在特性和各指标的相对重要性,为科学决策提供了有力支持。 熵权法 实用场景 熵权法是一种基于信息熵的客观赋权方法…...

无人机人员搜救

人员搜救-水域救援 水域搜救&#xff1a;快速水面搜查 物资抛投&#xff1a;救生物资抛投 绳索牵引&#xff1a;牵引救援绳索 领航船艇&#xff1a;水面侦察领航 人员搜救 昼夜搜救&#xff0c;精准定位 水域搜救 经纬 M300 RTK 搭载禅思 H20T 能够满足全天候作业需求&a…...

目标检测算法

讯飞星火 目标检测算法是计算机视觉领域的一项核心技术&#xff0c;用于在数字图像或视频中识别和定位一个或多个特定对象实例。这些算法通常需要确定对象的位置&#xff08;例如通过边界框&#xff09;和类别&#xff08;例如人、汽车、狗等&#xff09;。以下是一些流行的目…...

SpringSecurity 三更草堂学习笔记

0.简介 Spring Security是Spring家族中的一个安全管理框架。相比与另外一个安全框架Shiro&#xff0c;它提供了更丰富的功能&#xff0c;社区资源也比Shiro丰富。 一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多&#xff0c;因为相比与Spring…...

鸿蒙生态应用开发白皮书V3.0

来源&#xff1a;华为&#xff1a; 近期历史回顾&#xff1a;...

CSS - 深入理解选择器的使用方式

CSS基本选择器 通配选择器元素选择器类选择器id 选择器 通配选择器 作用&#xff1a;可以选中所有HTML元素。语法&#xff1a; * {属性名&#xff1b;属性值; }举例&#xff1a; /* 选中所有元素 */ * {color: orange;font-size: 40px; }在清除样式方面有很大作用 元素选择器…...

动手学深度学习(Pytorch版)代码实践 -循环神经网络-54~55循环神经网络的从零开始实现和简洁实现

54循环神经网络的从零开始实现 import math import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2l import matplotlib.pyplot as plt import liliPytorch as lp# 读取H.G.Wells的时光机器数据集 batch_size, num_steps 32, …...

Python酷库之旅-第三方库Pandas(006)

目录 一、用法精讲 10、pandas.DataFrame.to_excel函数 10-1、语法 10-2、参数 10-3、功能 10-4、返回值 10-5、说明 10-6、用法 10-6-1、数据准备 10-6-2、代码示例 10-6-3、结果输出 11、pandas.ExcelFile类 11-1、语法 11-2、参数 11-3、功能 11-4、返回值 …...

智慧矿山:EasyCVR助力矿井视频多业务融合及视频转发服务建设

一、方案背景 随着矿井安全生产要求的不断提高&#xff0c;视频监控、数据传输、通讯联络等业务的需求日益增长。为满足矿井生产管理的多元化需求&#xff0c;提高矿井作业的安全性和效率&#xff0c;TSINGSEE青犀EasyCVR视频汇聚/安防监控综合管理平台&#xff0c;旨在构建一…...

Unix/Linux shell实用小程序1:生字本

前言 在日常工作学习中&#xff0c;我们会经常遇到一些不认识的英语单词&#xff0c;于时我们会打开翻译网站或者翻译软件进行查询&#xff0c;但是大部分工具没有生词本的功能&#xff0c;而有生字本的软件又需要注册登陆&#xff0c;免不了很麻烦&#xff0c;而且自己的数据…...

springboot2.7.6 集成swagger

在 Spring Boot 2.7.6 版本中集成 Swagger 的步骤相对直接&#xff0c;主要涉及添加依赖、编写配置以及在控制器中添加文档注解几个环节。 下面是集成 Swagger 的基本步骤&#xff1a; 1. 添加依赖 首先&#xff0c;在pom.xml文件中添加 Swagger 相关依赖。 对于 Spring Boot…...

面试篇-系统设计题总结

文章目录 1、设计一个抢红包系统1.1 高可用的解决方案&#xff1a;1.2 抢红包系统的设计1.3 其他 2、秒杀系统设计 这里记录一些有趣的系统设计类的题目&#xff0c;一般大家比较喜欢出的设计类面试题目会和高可用系统相关比如秒杀和抢红包等。欢迎大家在评论中评论自己遇到的题…...

如何摆脱反爬虫机制?

在网站设计时&#xff0c;为了保证服务器的稳定运行&#xff0c;防止非法数据访问&#xff0c;通常会引入反爬虫机制。一般来说&#xff0c;网站的反爬虫机制包括以下几种&#xff1a; 1. CAPTCHA&#xff1a;网站可能会向用户显示CAPTCHA&#xff0c;要求他们在访问网站或执行…...

68745

877454...

github仓库的基本使用-创建、上传文件、删除

1.第一步 先点击左侧菜单栏的远程仓库 2.点击NEW 3.创建仓库 然后点击右下角的 CREATE 4.点击code 点击SSH,然后我出现了You don’t have any public SSH keys in your GitHub account. You can add a new public key, or try cloning this repository via HTTPS. 1&#xff…...

[课程][原创]opencv图像在C#与C++之间交互传递

opencv图像在C#与C之间交互传递 课程地址&#xff1a;https://edu.csdn.net/course/detail/39689 无限期视频有效期 课程介绍课程目录讨论留言 你将收获 学会如何封装C的DLL 学会如何用C#调用C的DLL 掌握opencv在C#和C传递思路 学会如何配置C的opencv 适用人群 拥有C#…...

科研绘图系列:R语言双侧条形图(bar Plot)

介绍 双侧条形图上的每个条形代表一个特定的细菌属,条形的高度表示该属的LDA得分的对数值,颜色用来区分不同的分类群或组别,它具有以下优点: 可视化差异:条形图可以直观地展示不同细菌属在得分上的差异。强调重要性:较高的条形表示某些特征在区分不同组别中具有重要作用…...

计算机未来大方向的选择

选专业要了解自己的兴趣所在。 即想要学习什么样的专业&#xff0c;如果有明确的专业意向&#xff0c;就可以有针对性地选择那些专业实力较强的院校。 2.如果没有明确的专业意向&#xff0c;可以优先考虑一下院校。 确定一下自己想要选择综合性院校还是理工类院校或是像财经或者…...

AndroidKille不能用?更新apktool插件-cnblog

AndroidKiller不更新插件容易报错 找到apktool管理器 填入apktool位置&#xff0c;并输入apktool名字 选择默认的apktool版本 x掉&#xff0c;退出重启 可以看到反编译完成了...

非参数检测2——定义

定义&#xff1a;若研究二判定问题&#xff08;即判断有无信号&#xff09;的检测问题&#xff0c; 检测器的虚警概率可以由对输入数据统计特性提出微弱假设确定假设中不包含输入噪声的统计特性 则称该检测器为非参数检测器。 设计目标 在未知或时变环境下&#xff0c;有最…...

iOS多target时怎么对InfoPlist进行国际化

由于不同target要显示不同的App名称、不同的权限提示语&#xff0c;国际化InfoPlist文件必须创建名称为InfoPlist.strings的文件&#xff0c;那么多个target时怎么进行国际化呢&#xff1f;步骤如下&#xff1a; 一、首先我们在项目根目录创建不同的文件夹对应多个不同的targe…...

TZDYM001矩阵系统源码 矩阵营销系统多平台多账号一站式管理

外面稀有的TZDYM001矩阵系统源码&#xff0c;矩阵营销系统多平台多账号一站式管理&#xff0c;一键发布作品。智能标题&#xff0c;关键词优化&#xff0c;排名查询&#xff0c;混剪生成原创视频&#xff0c;账号分组&#xff0c;意向客户自动采集&#xff0c;智能回复&#xf…...

你的 Mac 废纸篓都生苍蝇啦

今天给大家推荐个免费且有趣的小工具 BananaBin&#xff0c;它可以在你的废纸篓上“长”一些可爱的苍蝇&#x1fab0;。 软件介绍 BananaBin 是 macOS 上的一款有趣实用工具&#xff0c;当你的垃圾桶满了时&#xff0c;它会提醒你清理。这个软件通过在垃圾桶上添加互动的苍蝇…...

推出新的C2000™ F28P65x 实时微控制器,专为高效控制电力电子产品而构建(F28P650DH、F28P650DK、F28P650SH)

C2000™ F28P65x 实时微控制器是集中级性能、PWM 和模拟创新与系统成本优化等优势于一身。 F28P65x 系列是 C2000™ 实时微控制器 (MCU) 系列的中级性能系列产品&#xff0c;专为高效控制电力电子产品而构建。凭借超低延迟&#xff0c;F28P65x 通过更多的模拟功能和新的 PWM 功…...

使用Java实现分布式日志系统

使用Java实现分布式日志系统 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 在分布式系统中&#xff0c;日志记录是一项至关重要的任务。它不仅用于故障排查和…...