屏蔽系统热键/关机/注入 Winlogon(下)

前言

系列文章主要讲解关于挂钩 Winlogon 回调过程,实现对任务管理器电源操作DWM 自启动控制、常见系统热键等进行编程拦截。最后,我们指出可以通过我们的方法修改系统登陆或者其他安全页面的界面样式,达到自定义的效果。这是之前编写的一篇文章,一直没有发布,最近得空完善了一下。本篇从 RPC 挂钩角度实现热键拦截。

我曾阅读过 heiheiabcd 写的通过修改具体的窗口回调中的操作 ID,将其指向无效的 ID 来拦截系统热键的方法(注:原文已经被删除,原文转载:《禁止Ctrl+Alt+Del、Win+L等任意系统热键》),但是这种方法取决于系统版本,不同的版本由于链接库的地址不同,需要分析新的内部地址,并且向后兼容性比较差,当然作者的分析具有独创性。

我一直在想,有没有什么办法能够实现对 Winlogon 的各种操作实现稳定的挂钩,从而拦截我们关注的操作呢?通过一段时间的分析,终于发现了多个利用导出函数进行拦截的路径,其中一种更是可以拦截各种消息。在接下来的文章中,我会简明扼要地介绍这几种方法。

【提示】系列文章为了有助于不了解 Winlogon 的读者初步理解我们在完成的事情,主要分为(上)、(中)、(下)三部分 ,链接会在下面列出。(需要哪个部分的请自行跳转 ☆*: .。. o(≧▽≦)o .。.:*☆)

系列文章:

屏蔽系统热键(上)传送门 ID:133801527
屏蔽系统热键(中)传送门 ID:135907307
屏蔽系统热键(下)传送门 ID:135907201

关于 RPC 拦截热键的完整代码代码参考文章:Hook 实现系统热键屏蔽(一)-CSDN博客


 一、挂钩 RPC 调用——直接过滤 Winlogon 回调

【提示】该部分涉及部分未公开的内部结构和微软不推荐使用的函数,应用到发布的软件中时,请自行遵守相关要求。

操作系统的很多过程都依赖于 LPC/RPCCOM/DCOM 组件。

下面的介绍摘选自 百度百科 和 MSDN:

进程间通信(IPC)是在多任务操作系统或联网的计算机之间运行的程序和进程所用的通信技术。有两种类型的进程间通信(IPC):

  • 本地过程调用(LPC) LPC 用在多任务操作系统中,使得同时运行的任务能互相会话。这些任务共享内存空间使任务同步和互相发送信息。
  • 远程过程调用(RPC) RPC 类似于 LPC,不是这里的重点。

本地过程调用

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

远程过程调用(略,需要的请自行去了解)

二、原理和实现

2.1 分析关键函数

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

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

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

#define __RPC_FAR
#define RPC_MGR_EPV void
#define  RPC_ENTRY __stdcall

typedef void* LI_RPC_HANDLE;
typedef LI_RPC_HANDLE LRPC_BINDING_HANDLE;


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

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

typedef struct _LRPC_MESSAGE
{
    LRPC_BINDING_HANDLE Handle;
    unsigned long DataRepresentation;// %lu
    void __RPC_FAR* Buffer;
    unsigned int BufferLength;
    unsigned int ProcNum;
    LPRPC_SYNTAX_IDENTIFIER TransferSyntax;
    void __RPC_FAR* RpcInterfaceInformation;
    void __RPC_FAR* ReservedForRuntime;
    RPC_MGR_EPV __RPC_FAR* ManagerEpv;
    void __RPC_FAR* ImportContext;
    unsigned long RpcFlags;
} LRPC_MESSAGE, __RPC_FAR* LPRPC_MESSAGE;


//--------------------------------------------------
typedef void (RPC_ENTRY* __Ndr64AsyncServerCallAll)(
    LPRPC_MESSAGE pRpcMsg
);

其中,PRC_MESSAGE 结构体的各个成员目前我理解的作用为(可能有误):

LRPC_MESSAGE 结构体

定义

typedef struct _LRPC_MESSAGE
{
    LRPC_BINDING_HANDLE Handle;
    unsigned long DataRepresentation;
    void __RPC_FAR* Buffer;
    unsigned int BufferLength;
    unsigned int ProcNum;
    LPRPC_SYNTAX_IDENTIFIER TransferSyntax;
    void __RPC_FAR* RpcInterfaceInformation;
    void __RPC_FAR* ReservedForRuntime;
    RPC_MGR_EPV __RPC_FAR* ManagerEpv;
    void __RPC_FAR* ImportContext;
    unsigned long RpcFlags;
} LRPC_MESSAGE, __RPC_FAR* LPRPC_MESSAGE;

参数

  • Handle

类型:RPC_BINDING_HANDLE 

服务器绑定句柄。其中包含 RPC 运行时库用于访问绑定信息的信息。服务器绑定句柄包含客户端与特定服务器建立关系所需的信息。任意数量的 RPC API 运行时例程都会返回可用于进行远程过程调用的服务器绑定句柄。

  • DataRepresentation

类型:unsigned long

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

  • Buffer

类型:void *

指向网络缓冲区开头的指针。用于本地 RPC 调用时表示一段内存数据的首地址。

  • BufferLength

类型:unsigned int

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

  • ProcNum

类型:unsigned int

保留以供内部使用,一般设置为 NULL。(具体作用还未知)

  • TransferSyntax

类型:LPRPC_SYNTAX_IDENTIFIER

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

  • RpcInterfaceInformation

类型:void *

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

  • ReservedForRuntime

类型:void *

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

  • ManagerEpv

类型:RPC_MGR_EPV

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

  • ImportContext

类型:void *

保留,设置为 NULL,作用未知。

  • RpcFlags

类型:unsigned long

 RPC 调用的状态码。有时也使用 ProcNum 参数。

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

RPC_FLAGS_VALID_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

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

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

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

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

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

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

​这两个函数的定义如下:

RPC_STATUS RpcStringBindingComposeW(

RPC_WSTR ObjUuid,

RPC_WSTR ProtSeq,

RPC_WSTR NetworkAddr,

RPC_WSTR Endpoint,

RPC_WSTR Options,

RPC_WSTR *StringBinding

);

RPC_STATUS RpcBindingFromStringBindingW(

RPC_WSTR StringBinding,

RPC_BINDING_HANDLE *Binding

);

两种典型的合并/绑定方式如下:

(1)第一种是忽略 Endpoint 参数,将协议序列名称和对象的 UUID 绑定为一个字符串:

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

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

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

​仔细看地址:

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

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

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

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

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

__int64 __fastcall WluiiStartupImpl(unsigned int a1, void *a2, DWORD *a3)
{
  int v6; // edi
  DWORD CurrentProcessId; // esi
  DWORD SharedEvents; // ebx
  __int64 dwProcessId; // rcx
  int v10; // eax
  RPC_WSTR StringBinding; // [rsp+68h] [rbp-A0h] BYREF
  DWORD cbSid; // [rsp+70h] [rbp-98h] BYREF
  DWORD cchReferencedDomainName; // [rsp+74h] [rbp-94h] BYREF
  DWORD cchName[2]; // [rsp+78h] [rbp-90h] BYREF
  struct _PROCESS_INFORMATION ProcessInformation; // [rsp+80h] [rbp-88h] BYREF
  struct _STARTUPINFOW StartupInfo; // [rsp+98h] [rbp-70h] BYREF
  RPC_SECURITY_QOS SecurityQOS; // [rsp+108h] [rbp+0h] BYREF
  WCHAR ReferencedDomainName[16]; // [rsp+118h] [rbp+10h] BYREF
  char pSid[80]; // [rsp+138h] [rbp+30h] BYREF
  unsigned __int16 ObjUuid[40]; // [rsp+188h] [rbp+80h] BYREF
  WCHAR CommandLine[256]; // [rsp+1D8h] [rbp+D0h] BYREF
  unsigned __int16 ServerPrincName[280]; // [rsp+3D8h] [rbp+2D0h] BYREF
  WCHAR Name[264]; // [rsp+608h] [rbp+500h] BYREF
  // 启动事件日志
  *a3 = 0;
  if ( *(_QWORD *)&g_TraceRegHandle
    && (unsigned __int8)EtwEventEnabled(*(_QWORD *)&g_TraceRegHandle, &WLEvt_WluiServerStartup_Start) )
  {
    EtwEventWrite(*(_QWORD *)&g_TraceRegHandle, &WLEvt_WluiServerStartup_Start, 0i64, 0i64);
  }
  if ( (a1 & 0x40000000) != 0 )
  {
    v6 = 1;
    CurrentProcessId = GetCurrentProcessId();
  }
  else
  {
    v6 = 0;
    CurrentProcessId = 0;
  }
  StringBinding = 0i64;
  // 分析需要进行的操作
  if ( a1 )
  {
    SharedEvents = WluiiCreateSharedEvents();
    if ( SharedEvents )
      goto LABEL_40;
    if ( v6 )
    {
      // 准备 LogonUI 的启动参数
      if ( (int)StringCchPrintfW(
                  CommandLine,
                  0x100ui64,
                  L"\"%s\" /flags:0x%lx /state0:0x%lx /state1:0x%lx /testclientprocessid:0x%08x",
                  L"LogonUI.exe",
                  dword_1400D1610,
                  CurrentProcessId,
                  _wnfState.Data[0],
                  _wnfState.Data[1]) >= 0 )
        goto LABEL_10;
    }
    else if ( (int)StringCchPrintfW(
                     CommandLine,
                     0x100ui64,
                     L"\"%s\" /flags:0x%lx /state0:0x%lx /state1:0x%lx",
                     L"LogonUI.exe",
                     dword_1400D1610,
                     _wnfState.Data[0],
                     _wnfState.Data[1]) >= 0 )
    {
LABEL_10:
      memset_0(&StartupInfo, 0, sizeof(StartupInfo));
      StartupInfo.cb = 104;
      StartupInfo.lpDesktop = L"Winsta0\\Winlogon";
      if ( v6 )
        StartupInfo.lpDesktop = 0i64;
      if ( a2 )
      {
        // 继承虚拟令牌,创建 SYSTEM 进程,可以是 LogonUI, 也可以是 Taskmgr
        if ( CreateProcessAsUserW(
               a2,
               0i64,
               CommandLine,
               0i64,
               0i64,
               1,  // 继承令牌
               0,
               0i64,
               0i64,
               &StartupInfo,
               &ProcessInformation) )
        {
LABEL_14:
          hTargetProcessHandle = ProcessInformation.hProcess;
          CloseHandle(ProcessInformation.hThread);
          dwProcessId = ProcessInformation.dwProcessId;
          *a3 = ProcessInformation.dwProcessId;
          RegisterLogonProcess(dwProcessId, 0i64);
          goto LABEL_15;
        }
      }   // 和当前用户桌面级别有关,此时调用 CreateProcessW 创建进程。
      else if ( CreateProcessW(0i64, CommandLine, 0i64, 0i64, 1, 0, 0i64, 0i64, &StartupInfo, &ProcessInformation) )
      {
        goto LABEL_14;
      }
      SharedEvents = GetLastError();
      if ( !SharedEvents )
        goto LABEL_27;
LABEL_40:
      WLEventWrite(&WLEvt_WluiServerStartupFailure);
      goto LABEL_27;
    }
    SharedEvents = 122;
    goto LABEL_40;
  }
LABEL_15:
  if ( !v6 )
  {
    CurrentProcessId = NtCurrentPeb()->SessionId;
    if ( CurrentProcessId == (unsigned int)RtlGetCurrentServiceSessionId() )
      CurrentProcessId = RtlGetActiveConsoleId();
  }
  v10 = StringCchPrintfW(ObjUuid, 0x25ui64, L"3BDB59A0-D736-4D44-9074-C1EE%08X", 
    CurrentProcessId); // 准备 UUID 绑定模式的参数
  if ( v10 < 0 )
  {
    SharedEvents = (unsigned __int16)v10;
    if ( (_WORD)v10 )
      goto LABEL_40;
  }
  // 绑定字符串,生成 RPC 字符串句柄
  SharedEvents = RpcStringBindingComposeW(ObjUuid, (RPC_WSTR)L"ncalrpc", 0i64, 0i64, 0i64, &StringBinding);
  if ( SharedEvents )
    goto LABEL_40;
  SharedEvents = RpcBindingFromStringBindingW(StringBinding, &Binding);
  if ( SharedEvents )
    goto LABEL_40;
  if ( !v6 )
  {
    // 查询 SID
    ServerPrincName[272] = 0;
    cbSid = 68;
    if ( CreateWellKnownSid(WinLocalSystemSid, 0i64, pSid, &cbSid) )
    {// WinLocalSystemSid == 0x16
      cchName[0] = 257;
      cchReferencedDomainName = 16;
      if ( LookupAccountSidLocalW(
             pSid,
             Name,
             cchName,
             ReferencedDomainName,
             &cchReferencedDomainName,
             (PSID_NAME_USE)&cchName[1]) )
      {
        if ( (int)StringCchPrintfW(ServerPrincName, 0x111ui64, L"%s\\%s", ReferencedDomainName, Name) >= 0 )
        {
          SecurityQOS.Version = 1;
          *(_QWORD *)&SecurityQOS.Capabilities = 1i64;
          SecurityQOS.ImpersonationType = 2;
          while ( 1 )
          {
            // 绑定 SID 和 安全属性 信息
            SharedEvents = RpcBindingSetAuthInfoExW(Binding, ServerPrincName, 6u, 0xAu, 0i64, 0, &SecurityQOS);
            if ( !SharedEvents )
              break;
            Sleep(0x12Cu);
          }
        }
      }
    }
  }
LABEL_27:
  if ( StringBinding ) // 删除字符串占用的内存
    RpcStringFreeW(&StringBinding);
  if ( SharedEvents )
    WluiiShutdownImpl(a1); // RPC 收尾工作
  if ( *(_QWORD *)&g_TraceRegHandle
    && (unsigned __int8)EtwEventEnabled(*(_QWORD *)&g_TraceRegHandle, &WLEvt_WluiServerStartup_Stop) )
  { // 事件日志记录
    EtwEventWrite(*(_QWORD *)&g_TraceRegHandle, &WLEvt_WluiServerStartup_Stop, 0i64, 0i64);
  }
  return SharedEvents;
}

关于 LogonUI.exe 的参数一共有四个参数(NT 10),这个程序用于实现登录会话的多个 GUI 界面:

"LogonUI.exe" /testclientprocessid: /flags: /state0: /state1:

对于“/flags:”参数目前可以有两个值:0x0 和 0x4,与电源状态有关的操作为 0x4;其他操作,比如安全选项页面,参数为 0x0;

"/state0:" 和 "/state1:" 参数是 COM 调用 LogonUIController 类的虚表函数中继承的第 6 个接口,它调用了 IUserSettingManagerstate 是必要的参数,这个类的具体实现封装在 LogonUIController.dll 中 (%System32% 根目录)。具体作用暂未知,不过 "/state0:" 是运行时变化的,"/state1:" 似乎是一个几乎不变的值:0x41c64e6d。

下面是初步分析的一部分过程,如有错误望指点:

CoCreateInstance 首先检索了 CLSID_LogonUIController 关联的类对象,LPVOID ppv 返回的是一个存放多个接口地址的数组,即 int ppv[Len],据分析实际继承的有 13 个接口函数。

LogonUI.exe 准备完参数后就直接通过 COM 服务器的接口继续登录会话:

LogonUIController.dll 的虚表(部分):

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

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

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

#include "pch.h"
#include "detours.h"
#include 
#pragma comment(lib, "detours.lib")

PVOID _RpcBindingFromStringBindingW = NULL;


#define __RPC_FAR
#define RPC_NATIVE_API __stdcall
#define RPC_S_INVALID_STRING_UUID 1705L
#define RPC_S_INVALID_BINDING 1702L
#define RPC_S_OK ERROR_SUCCESS

typedef _Return_type_success_(return == 0) long RPC_STATUS;
typedef _Null_terminated_ wchar_t __RPC_FAR* RPC_WSTR;
typedef _Null_terminated_ const wchar_t* RPC_CWSTR;
typedef void* I_RPC_HANDLE;
typedef I_RPC_HANDLE RPC_BINDING_HANDLE;

typedef RPC_STATUS(RPC_NATIVE_API* __RpcBindingFromStringBindingW)(
    RPC_WSTR           StringBinding,
    RPC_BINDING_HANDLE* Binding
);

RPC_STATUS RPC_NATIVE_API MyRpcBindingFromStringBindingW(
    RPC_WSTR           StringBinding,
    RPC_BINDING_HANDLE* Binding
);

static void StartHookingFunction(void*);
static void UnmappHookedFunction(void*);

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    DisableThreadLibraryCalls(hModule);
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        _beginthread(StartHookingFunction, 0, NULL);
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        _beginthread(UnmappHookedFunction, 0, NULL);
        break;
    }
    return TRUE;
}


static void StartHookingFunction(void*)
{
    // 开始事务
    DetourTransactionBegin();
    // 更新线程信息  
    DetourUpdateThread(GetCurrentThread());
    // 获取函数指针
    _RpcBindingFromStringBindingW =
        DetourFindFunction(
            "rpcrt4.dll",
            "RpcBindingFromStringBindingW");

    // 将拦截的函数附加到原函数的地址上
    DetourAttach(&(PVOID&)_RpcBindingFromStringBindingW,
        MyRpcBindingFromStringBindingW);
    // 结束事务
    DetourTransactionCommit();
}


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

    //将拦截的函数从原函数的地址上解除,这里可以解除多个函数。
    DetourDetach(&(PVOID&)_RpcBindingFromStringBindingW,
        MyRpcBindingFromStringBindingW);
    //结束事务
    DetourTransactionCommit();
}


RPC_STATUS RPC_NATIVE_API MyRpcBindingFromStringBindingW(
    RPC_WSTR           StringBinding,
    RPC_BINDING_HANDLE* Binding
)
{
    if (!wcscmp(StringBinding, L"ncalrpc:[WindowsShutdown]"))
    {
        WCHAR wcstr[356]{};
        wsprintf(wcstr, L"已经阻止用户启动的关机计划!\nRPC调用关键字符串: %s\n", StringBinding);
        MessageBoxW(GetDesktopWindow(), wcstr, L"RpcStringBindingComposeW", MB_OK | MB_ICONWARNING | MB_SYSTEMMODAL);
        return RPC_S_OK;
    }
    return ((__RpcBindingFromStringBindingW)_RpcBindingFromStringBindingW)
        (StringBinding, Binding);
}

2.3 过滤方法二:拦截 Server 端异步回调过程

还记得一切的开端吗?如果从根源上掐断是否可以实现更好的效果?我们尝试去搞定送报员 Ndr64AsyncServerCallAll 函数:

​在调用 Ndr64AsyncServerCallAll 函数前后,只有三个参数发生了变化:BufferBufferLength  和 RpcFlags。送报员到底做了什么事情?

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

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

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

以下是我基于 Win 11 分析出的掩码:

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

"0100000009" // 关机 KEY

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

"0005000000" // 以管理员身份启动 KEY(其中,第5,6字节处为可变值)

"0105000000" // 成功以管理员身份启动 KEY(其中,第5,6字节处为可变值)

"0202000000" // 注销后登陆 KEY
"0301000002" // 从深度睡眠中唤醒 KEY

"0304000000" // 注销后登陆 KEY(待定)

"0401000000" // 唤醒 KEY

"0404000000" // Ctrl+Alt+Del KEY(一次调用)
"0404000004" // Ctrl+Shift+Esc KEY(一次调用)
"0404000005" // WIN+L KEY(一次调用)
"0501000000" // 自动睡眠 KEY

"0502000000" // 切换用户

"0601000000" // 从深度睡眠中唤醒 KEY
"0601000002" // S3 睡眠阶段 1 KEY

"0701000002" // S3 睡眠阶段 2 KEY

"0704000000" // 操作已完成 KEY

"0c04000000" // DWM 崩溃恢复通知,第五位可变

"0d04000000" // 注销后登陆 KEY
"f f f f f f f f f f " // 切换用户

备注:这个表大部分都是已经确定了具体操作了的,在以前的系统上基本不变,但可能有几个发生变化。我推测操作应该是按 Code 的顺序排的,估计应该编码了一个表在内部,具体的 Code 和操作的联系仍然在研究。目前通过调试获得的这个表不全面。

通过过滤该函数的参数即可实现在 Win 10 / 11 上,直接拦截 Winlogon 回调,而且操作简单。

依然使用 Detours 库,钩子函数内的方法可以仿照下面的来写:

// 钩子例程
if (pRpcMsg->BufferLength == 0)
{
    ((__Ndr64AsyncServerCallAll)fpNdr64AsyncServerCallAll)(pRpcMsg);
    return;
}

char p[5] = { 0 };
char Strr[45] = { 0 };
int BufferMask[5] = { 0 };
DWORD Result = 0;

// 读取前 5 字节数据
if (!memcpy_s(BufferMask, (PVOID)iBaseAddress,
        , static_cast(5)))
    {
        sprintf_s(p, "%02x", BufferMask[i]);
        Buffer = Buffer + p;
    }
// ... do something to compare string Codes.

......

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

二、通过 ICredentialProvider 接口重写凭据提供程序

微软提供了凭据提供程序接口 (Vista 及以上) 和 GINA (XP或更早) 等途径来自定义凭据处理对话。我们就可以通过凭据提供程序接口,ICredentialProvider 这个接口接口继承自 IUnknown 接口,调用它可以实现定制自己的登陆界面,就像 Wallpaper Engine/Lively Wallpaper 可以实现动态壁纸一样。

方法概述

MSDN 上,ICredentialProvider 接口具有以下方法:

ICredentialProvider::Advise

允许凭据提供程序通过回调接口在登录 UI 或凭据 UI 中启动事件。
ICredentialProvider::GetCredentialAt

获取特定凭据。
ICredentialProvider::GetCredentialCount

获取此凭据提供程序下的可用凭据数。
ICredentialProvider::GetFieldDescriptorAt

获取描述指定字段的元数据。
ICredentialProvider::GetFieldDescriptorCount

检索显示此提供程序凭据所需的 中的字段计数。
ICredentialProvider::SetSerialization

设置凭据提供程序的序列化特征。
ICredentialProvider::SetUsageScenario

定义凭据提供程序有效的方案。 每当初始化凭据提供程序时调用。
ICredentialProvider::UnAdvise

登录 UI 或凭据 UI 用于通知凭据提供程序不再接受事件回调。

可以通过该方法和之前的 RPC 检测技术配合,来自定义凭据提供程序(自定义登陆界面版面和动画)。有时间的话,我会单独写一篇利用该接口实现自定义登陆界面的博客。


后记

本文被拆解为上中下三部分,这是系列的第三部分。

更多关于 RPC 拦截热键的代码参考最新的文章:

实现屏蔽 Ctrl + Alt + Del 、Ctrl + Shift + Esc 等热键(一)-CSDN博客文章浏览阅读634次,点赞11次,收藏20次。winlogon 进程通过 SignalManagerWaitForSignal 函数循环等待系统快捷键。最终通过 WMsgKMessageHandler 回调函数来实现 RPC 消息的处理。第一个参数 uMsgCSessionKey 控制会话有关(CSession)的回调消息,这不是我们需要关注的。第二个参数 uMsgWLGenericKey 控制注册调用(WLGeneric)的回调消息,其中包含了对快捷键处理有关的函数。https://blog.csdn.net/qq_59075481/article/details/135899525版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:https://blog.csdn.net/qq_59075481/article/details/135907201

文章发布于:2024.01.29 文章更新于:2024.01.29

你可能感兴趣的:(Windows,基础编程,快捷键机制系列文章,windows,微软,测试工具)