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

目录

前言

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

1.1 什么是 UAC ?

2.2 UAC 运行机制的概述

2.3 分析 UAC 提权参数

二、 NdrAsyncServerCall 函数的分析

2.1 函数声明的解析

三、通过 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) 函数的相关原理,从传输语法的角度对本地远程过程调用进行拦截。本篇作为部分补充。

关键词: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 服务进程的 PID
    if (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.exe
        WCHAR* arg2 = wcstok(NULL, seps);// 父进程 Appinfo 服务进程PID
        WCHAR* 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) {//Excutable
                BASE = 6 * sizeof(DWORD) + 6 * sizeof(void*);
            }
            else if (Byte2 == 1) {//dll
                BASE = 6 * sizeof(DWORD) + 5 * sizeof(void*);
            }
            else {//Not Understood
                INT64 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);
                    //else
                        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, 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;
                }
            }
            else
                return TRUE;
        }

    }
    return TRUE;
}

效果如图所示:

动态注入工具截图

但是,这个方法并不是用于拦截特定进程的启动的合适方法,正确的做法是拦截 CreateProcessAsUser 或者 CreateProcessInternal ,具体可以看文章:https://blog.csdn.net/qq_59075481/article/details/128814911

解析 AIS 的参数可以用于模拟 consent.exe 启动,我们可以自己构建一个 consent.exe 来美化 UAC 界面。这需要更多的设计和代码实现,目前已经有相关实现项目: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 执行指定函数的参数(来自上面作者的文章)

这个系列那位作者连续写了三篇文章,都是干货满满,有兴趣可以去阅读阅读。而这位作者主要分析了客户端 AIS 和 explorer 的代码,并没有涉及到 Winlogon 的分析,所以本文将侧重于对服务端 Winlogon 的分析。

当开始分析这两个函数时,你会发现它并没有在 MSDN 上文档化解释。这些函数不被直接使用,往往是被一些上级的 API 调用,但是这些较为底层的函数被作为 rpcrt4.dll 的导出函数而允许我们轻松访问。作为 MS-RPC 中关键过程的封装函数,我们的方法将围绕着它来展开,无论是在第一篇还是这一篇中。

二、 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 下结构的成员一样,但因为对齐原因偏移量不同(后文讲解如何计算实际的偏移量)。

提示:2.2~2.5 内容暂不公开。

三、通过 Detours 挂钩实现

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 __stdcall

typedef void* LI_RPC_HANDLE;
typedef LI_RPC_HANDLE LRPC_BINDING_HANDLE;

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

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

typedef struct _LRPC_MESSAGE
{
    LRPC_BINDING_HANDLE Handle;
    unsigned long DataRepresentation;
    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::wstring
    std::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,即可使得调用取消。如图所示:

调用取消过程

效果演示:

神!在 Windows 上屏蔽系统操作

工具界面:

RpcServerTestCancel 拦截器界面

操作效果展示:

(1)HotPatch 前后对比:

HotPatch 前后对比

(2)界面设计:

半透明界面设计

下载此工具:

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

提取码:6666

四、总结

最近可能考虑深耕开发一下这边,请期待。


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

转载请注明出处。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

涟幽516

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值