目录
前言
一、介绍用户账户控制(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 接口;逆向工程;热键屏蔽;挂钩注入
系列文章:
- 屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)
- RPC-Hook 屏蔽系统热键(一)
- RPC-Hook 屏蔽系统热键(二)[本文]
- Windows 拦截系统睡眠
一、介绍用户账户控制(UAC)
1.1 什么是 UAC ?
用户帐户控制 (UAC) 是一项 Windows 安全功能,旨在保护操作系统免受未经授权的更改。当对系统的更改需要管理员级权限时,UAC 会通知用户,从而让用户有机会批准或拒绝更改。
UAC 允许所有用户使用 标准用户账户 登录到他们的计算机。使用 标准用户令牌 启动的进程可能会使用授予标准用户的访问权限来执行任务。例如,Windows 资源管理器会自动继承 标准用户级别 权限。任何使用 Windows 资源管理器启动 (例如,通过打开快捷方式) 的应用程序也 使用标准用户权限集 运行。大多数应用程序,包括操作系统附带的应用程序,都以这种方式正常工作。
其他应用程序(例如设计时未考虑安全设置的应用程序)可能需要更多权限才能成功运行。 这些类型的应用被称为 Legacy App。
当用户尝试执行需要管理员权限的操作时,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)用户实例
其中研究的相对多一些的就是 “ 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。
其实这里的提权参数还可以从其他角度细致地去分析和理解,已经有佬完成了相关工作,见文章:https://www.anquanke.com/post/id/231403。 Winlogon 和 AIS 进程之间的通信通过 LRPC 来完成,调用方进程和它们之间一般也是 LRPC。在这个过程中,有一个重要的函数 Ndr(64)AsyncServerCall(All) [x86 下是 NdrAsyncServerCall,x64 下是 Ndr64AsyncServerCallAll],作为服务端接收消息的关键步骤;而相对应的,客户端调用 Ndr(64)AsyncClientCall 发送消息。
这个系列那位作者连续写了三篇文章,都是干货满满,有兴趣可以去阅读阅读。
当开始分析这两个函数时,你会发现它并没有在 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_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 结构体各个成员的简单解释。
根据相关研究,RPC_MESSAGE 结构体的重要成员已经在下图中标记出:
注意:图中的偏移量是在 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 函数:
而这些函数是 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。
第二次调用在 StartLogonUI(启动 LogonUI.exe 进程,也就是登陆 UI 进程)和 WlStateMachineInitialize(状态机初始化)之后,参数为 FALSE,表明注册 Server。
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 信息的响应处理是在新线程中完成的,不是主线程)。
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 )。
随后,进入关键调用过程:
所有任务都通过 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 分析获得。
首先我们观察到函数内检测了客户端是否取消了调用:
如果返回值为 0 也就是 RPC_S_OK 则表明客户端已经请求取消远程调用。微软文档说服务器此时可以选择是中断调用还是继续调用,只是客户端不管它返回了而已。在目前的 winlogon 处理机制下,是会立即中止调用的,因为我们观察到了关键的函数的调用 RpcAsyncAbortCall(pAsync, RPC_S_CALL_CANCELLED),这指示了调用将被服务器中止。
所以,一种思路是依赖欺骗 TestCancel 检测来绕过调用。
下图展示了使用 WinDbgX 修改 RpcServerTestCancel 的返回值后的效果,可以看到在请求以管理员身份启动程序时远程过程被取消:
所以第一个挂钩点就是挂钩 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 可以看到这个函数很复杂。下图是函数开头的部分:
这里的开头两句反汇编语句,是很重要的。但第一次接触的话会很难理解:
v37 = *((_QWORD *)Message->RpcInterfaceInformation + 10);
v41 = *(_QWORD **)(v37 + 8);
首先这里有个陷阱,Message->RpcInterfaceInformation 是指针地址,IDA 反汇编的指令和伪代码写法不一样,伪代码中的对数据变量地址 + 10 是要类似于数组的处理方式用数据类型乘以所参与运算的常数的,也就是说这里是(地址 + 10* 8) = (地址 +80),也就是十六进制的 ptr + 0x50。
显然,这里的 Message 变量是指向 RPC_MESSAGE 结构的指针:
我们说过,前文给出的图片解析的结构体是 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 架构下结构体的成员偏移如下:
当有了偏移量后,再看汇编代码就会容易理解里面的一些运算是什么了。例如下面图片展示的第 48 行的代码中 RpcInterfaceInformation + 0x50 指向的就是 RPC_SERVER_INTERFACE 结构的 InterpreterInfo 成员。
为了便于对照理解,我们把前面一小节的图片再搬来一次:
同样地我们通过计算得到 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。
下面是节选自 /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 的内容为 PPRC_MESSAGE,我们看一下结构的参数:
随后执行直到命中断点 I_RpcBCacheAllocate,使用 F8 执行直到函数结束,查看返回值,它是指向 NDR_ASYNC_MESSAGE 结构的指针(在 memset 前,分配的缓冲区上有脏数据)。
缓冲区的分配依赖 LsaAlloc(如果无效则再尝试使用 HeapAlloc )。其中 LsaAlloc 实际为 lsasrv.dll 导出的 LsaIAllocateHeap 。
然后,我们关注到 NdrpInitializeAsyncMsg 函数,在他里面准备了 _RPC_ASYNC_STATE 结构。这个结构一般用于传递 RPC 过程的额外执行结果。
看到了一样的缓冲区分配过程,大小为 0x70,也就是 sizeof(RPC_ASYNC_STATE):
在这个函数返回时,将 RPC_ASYNC_STATE 地址保存到 a2 参数也就是 NDR_ASYNC_MESSAGE 结构偏移 0x8 位置上:
接下来就是将 Binding Handle 拷贝到参数列表上:
此时可以通过动态调试验证结论。在 call RPCRT4!NdrpInitializeAsyncMsg 后,查看偏移 NDR_ASYNC_MESSAGE 结构 0x238 位置:
跟踪这个地址被修改的位置:
这里将 PRPC_BINDING_HANDLE(地址)拷贝到了参数列表上第二个参数位置处(解组的参数列表严格按照 8 字节对齐,但 Buffer 上序列化的参数似乎是按照 4 字节对齐的)。
RPC_BINDING_HANDLE 其实和 TEB 有关,在 RPC 过程中使用 RpcServerTestCancel 测试链接需要此句柄。
例如前面提到的 I_WMsgSendMessage 过程:
第二次修改就是将 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 的前两个参数。
它们可以理解为 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 而已,没有强制终止客户端后续执行的能力。
但是,二者都可以通过在调用 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;
}
工具界面:
操作效果展示:
(1)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.