Windows 下屏蔽系统睡眠

前言

在前一篇文章中,我们分析了以编程方式拦截 Winlogon 相关回调过程的具体做法,我们给出了一种拦截 RPC 异步回调的新方法------通过过滤特征码,我们可以对很多系统热键以及跟电源有关的操作做出"提前"响应。但是我们给出的代码并不能真正拦截睡眠/休眠,只能在系统唤醒时检测到用户消息。有时,系统睡眠也非常重要。当我们不想值守在设备前面,而又需要设备在一段时间内不关闭,仅仅通过拦截误操作导致的系统关闭并不是关键因素,因为这类情况不常发生。反而,系统的自动睡眠/定时睡眠会影响我们正在执行的工作,也许你正在下载一个不支持断点续传的大文件,而系统进入休眠就断网的默认特性将会给你带来困扰(你可能需要在网络恢复时重新链接文件的服务器,并从头开始)。在这里,我们将深入讨论如何降低这类事件带来的影响。一些操作或者代码将会带来很好的效果。

P.S. R0 方法比较简单,但是本文仅讨论 R3。

系列文章:

  1. 屏蔽系统热键/关机(挂钩 Winlogon 调用 键盘钩子)​​​​​​
  2. 基于 Ncalrpc 协议 NDR64 线路接口的 Hook 实现系统热键屏蔽(一)
  3. Hook 实现系统热键屏蔽(二)[暂未发布]
  4. Windows 下屏蔽系统睡眠[本文]

一、什么是系统睡眠/休眠?

系统睡眠不能笼统地认为它是一个连续过程。根据微软所说,系统状态分为六种,即 S0~S5。S0 是系统工作状态,S5 是系统关闭状态。S1~S4 都是系统睡眠状态。其中,普遍意义上的睡眠是 S1 和 S3 睡眠状态, Windows 7 时代常见的休眠是 S4 状态。大家常常通俗理解为电脑不关机时叫"待机"状态,但实际上休眠不是待机,待机状态是 S1 状态,休眠是 S4 状态,级别越高,系统软硬件处于关闭状态的越多。在 Win 10/11 上,其实它们是 S3 的混合睡眠状态, Win7 常见的睡眠是 S1 睡眠状态。混合睡眠机制是微软近期常常提倡的,办公的人常常满足于电脑不用重启就可以继续工作。

关于系统睡眠状态更为详细的内容可以看 MSDN 系统睡眠状态 - Windows drivers | Microsoft Learn。https://learn.microsoft.com/zh-cn/windows-hardware/drivers/kernel/system-sleeping-states作为理论基础,这里就不再多说了。

二、如何阻止自动睡眠?

由于对驱动层的机制目前我没去分析,所以,目前仅以在 R3 下进行有限角度的分析为主。由于这样的需求不是很多,并且微软也提供了接口用于干预睡眠过程。所以很少有资料会提及关于睡眠的较为深入的研究文章。

阻止睡眠主要包括两个部分:控制系统不要进入自动睡眠、阻止人为手动操作的睡眠。

2.1 延迟自动睡眠

首先谈谈自动睡眠,自动睡眠会产生很多问题。比如很多软件未在合适情况下请求系统不要睡眠。最简单的就是:程序还在跑着呢,结果因为自动睡眠问题,无人值守情况下陷入睡眠。恢复工作后,引发很多基于网络、内存的似乎很玄学的问题。类似的现象也很普遍,比如很多游戏或者工具不屏蔽 Alt + F4 快捷键,最离谱的是套件中有的界面会做屏蔽,有些关键的地方窗口又不做这些处理。虽然可以认为这是软件的适配没有做好,但是,我还是想通过一些分析和整理把这种容易被忽略的地方整理出来。

在写这篇文章之前,我在思考,其实这篇完全不应该叫屏蔽睡眠,而应该叫如何规避传统套路的"系统失眠"工具,似乎有些无聊中玩一些无聊事情的意思,也没有什么技术。并没有前言里面讲的那么高级,稍微跟高级沾边的我会在 Winlogon 的第二篇中讲(原本那篇才是准备写的第二篇)。

这里的目标是:如何阻止自动睡眠,并尽量不直接使用系统提供的接口,同时适当提高静态分析难度,以便于规避一般人分析我的程序(比如像直接拿到 IDA F5 上去一锅端的那种),真正的目的不是为了怎么怎么避免逆向,而是就是最简单的提高一点点分析门槛,用的方法也都是公开可见的,或者说旧方法。

首先,系统判定自动睡眠的标准是基于内部的系统空闲计时器和显示器空闲计时器,并设定一定的阈值来完成的。它们与计算机最近一次操作的时间特征有关系,比如键盘、鼠标等输入设备的输入。

系统提供了 SetThreadExecutionState 函数用于软件合理规划睡眠时间。最常见的比如视频播放程序,肯定需要播放时电脑不进入自动睡眠对吧。随随便便就睡眠了,那么大概率用户批评的是正在使用的软件方而不是系统。

这个函数其实调用起来很简单,为了避免大家跳转,我这就摘取 MSDN 上的部分解释了。

首先,这个函数只有一个参数,表示要请求的线程执行状态。这里,微软提供了多个标识符可以用于组合使用,分别用于不同的场景。

EXECUTION_STATE SetThreadExecutionState(

[in] EXECUTION_STATE esFlags

);

数值包括:

含义
ES_AWAYMODE_REQUIRED 0x00000040 启用离开模式。 必须使用 ES_CONTINUOUS 指定此值。 离开模式只能由媒体录制和媒体分发应用程序使用,这些应用程序必须在计算机似乎处于睡眠状态时在台式计算机上执行关键后台处理。 请参阅"备注"。
ES_CONTINUOUS 0x80000000 通知系统正在设置的状态应保持有效,直到使用 ES_CONTINUOUS 的下一次调用和清除其他状态标志之一。
ES_DISPLAY_REQUIRED 0x00000002 通过重置显示空闲计时器强制显示处于打开状态。
ES_SYSTEM_REQUIRED 0x00000001 通过重置系统空闲计时器强制系统处于工作状态。
ES_USER_PRESENT 0x00000004 不支持此值。 如果 ES_USER_PRESENT 与其他 esFlags 值组合使用,则调用将失败,并且不会设置任何指定的状态。

关于这些参数, MSDN 是费了好大功夫来帮助我们理解。

下面是原文中的注解:

系统自动检测本地键盘或鼠标输入、服务器活动和更改窗口焦点等活动。 未自动检测到的活动包括磁盘或 CPU 活动以及视频显示。

在不使用 ES_CONTINUOUS 的情况下调用 SetThreadExecutionState 只是重置空闲计时器;若要使显示或系统保持工作状态,线程必须定期调用 SetThreadExecutionState

若要在电源管理计算机上正确运行,传真服务器、应答计算机、备份代理和网络管理应用程序等应用程序在处理事件时必须同时使用 ES_SYSTEM_REQUIREDES_CONTINUOUS 。 多媒体应用程序(如视频播放器和演示应用程序)在长时间显示视频时,必须使用 ES_DISPLAY_REQUIRED ,而无需用户输入。 文字处理器、电子表格、浏览器和游戏等应用程序不需要调用 SetThreadExecutionState

仅当需要系统执行后台任务(例如,在系统似乎处于睡眠状态时将电视内容或流媒体录制到其他设备的媒体应用程序绝对需要)时才应使用 ES_AWAYMODE_REQUIRED 值。 不需要关键后台处理或在便携式计算机上运行的应用程序不应启用离开模式,因为它会阻止系统通过进入真正的睡眠来节省电量。

若要启用离开模式,应用程序同时使用ES_AWAYMODE_REQUIREDES_CONTINUOUS ;若要禁用离开模式,应用程序使用 ES_CONTINUOUS 调用 SetThreadExecutionState 并清除ES_AWAYMODE_REQUIRED 。 启用离开模式后,使计算机进入睡眠状态的任何操作都会将其置于离开模式。 当系统继续执行不需要用户输入的任务时,计算机似乎处于睡眠状态。 离开模式不会影响睡眠空闲计时器;若要防止系统在计时器过期时进入睡眠状态,应用程序还必须设置 ES_SYSTEM_REQUIRED 值。

SetThreadExecutionState 函数不能用于阻止用户使计算机进入睡眠状态。 应用程序应尊重用户在合上笔记本电脑的盖子或按下电源按钮时预期会出现某种行为。

此函数不会停止屏幕保护程序执行。

这个就是 ES_CONTINUOUS我们可以抓住的一个点了,因为似乎一直在强调我们加上这个值来维持状态的更改。

但是,加和不加有什么区别呢?

首先,常规的方法就是加上这个值来阻止自动睡眠,并在下一次调用该值时取消状态的修改,一种调用方式如下:

cpp 复制代码
// Television recording is beginning. Enable away mode and prevent
// the sleep idle time-out.
//
SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_AWAYMODE_REQUIRED);

//
// Wait until recording is complete...
//

//
// Clear EXECUTION_STATE flags to disable away mode and allow the system to idle to sleep normally.
//
SetThreadExecutionState(ES_CONTINUOUS);

这种方式,似乎是通知系统某个组件和设置标志位的感觉。

在第一行我们执行了之后,应该顺利请求了(大多数情况,玄学除外)失眠功能。

但是,我们能够通过 powercfg /requests 指令来查询当前占用显示器时间的程序。如下图所示:

这里也是用常见的播放器作为演示,基本上播放器都有这个功能。

那么有没有一种仅用于测试环境考虑,较为隐蔽的屏蔽系统睡眠的方式呢?那就是不加 ES_CONTINUOUS 的作用了。不加这个标志位,我们需要在相应的阶段内定期地调用该函数。因为 ES_SYSTEM_REQUIRED 和 ES_AWAYMODE_REQUIRED 只是分别地在归零系统空闲计时器和显示器空闲计时器,让它们不能够达到阈值。

当然,这种方法也是可以被发现的,因为 ETW 事件跟踪,可以有日志啊。powercfg -energy -trace 命令可以跟踪一段时间内对系统电源状态进行重要修改的程序或服务。

对于一些控制流分析或者接口监视的程序,也可以轻松发现我们调用了 SetThreadExecutionState 函数,此时,有很多迂回的方法。比如,手动加载模块,手动获取函数地址,或者甚至手动写这个实现。当然,对于这个一般功能的程序,花时间用方法都是浪费时间。

首先 SetThreadExecutionState 函数内部其实调用了 NtSetThreadExecutionState 函数,而这个函数就只是设置了一下寄存器,传递服务号调用内核函数。所以,我们也可以把机器码序列从 IDA 的汇编代码中抄下来,手动通过写内存,调用这个函数。这样就绕过了对 SetThreadExecutionState 函数的直接调用。当然,上文说的方法也可以在其他需要规避外界的代码注入修改受保护程序的项目中用到,即多用内部代码,少用 Windows Win32 的接口函数,对于函数的间接调用需要有一些校验的保护过程。

下面只是一个简单的动态调用使用该函数的例子,完善的代码见附录。

cpp 复制代码
#include <iostream>
#include <Windows.h>
#include <thread>
#include <assert.h>
#include <chrono>
#include <future>

using namespace std;

unsigned char SysCode[] = "\x4C\x8B\xD1\xB8\xBC\x01\x00\x00\xF6\x04\x25\x08\x03\xFE\x7F\x01\x75\x03\x0F\x05\xC3\xCD\x2E\xC3";

typedef NTSTATUS(NTAPI* __NtSetThreadExecutionState)
    (EXECUTION_STATE esFlags, EXECUTION_STATE* PreviousFlags);

typedef ULONG(NTAPI* __RtlNtStatusToDosErrorNoTeb)(
    NTSTATUS Status
);

LPVOID lpBaseAddress_ThreadExecution = NULL;

CRITICAL_SECTION CriticalSection;

NTSTATUS NTAPI MyNtSetThreadExecutionState(
    EXECUTION_STATE esFlags,
    EXECUTION_STATE* PreviousFlags
)
{
    // 请求临界区的所有权。
    EnterCriticalSection(&CriticalSection);

    DWORD lpOldProtect = PAGE_EXECUTE_READWRITE;

    if (!lpBaseAddress_ThreadExecution)
    {
        lpBaseAddress_ThreadExecution = VirtualAlloc(NULL, sizeof(SysCode),
            MEM_RESERVE | MEM_COMMIT, lpOldProtect);
        if (lpBaseAddress_ThreadExecution == nullptr) { return STATUS_NO_MEMORY; }
        memcpy(lpBaseAddress_ThreadExecution, SysCode, sizeof(SysCode));
    }

    if (!VirtualProtect(lpBaseAddress_ThreadExecution, sizeof(SysCode), PAGE_EXECUTE_READ, &lpOldProtect))
        return STATUS_ACCESS_VIOLATION;

    const NTSTATUS status = ((__NtSetThreadExecutionState)lpBaseAddress_ThreadExecution)(
        esFlags, PreviousFlags);

    if (!VirtualProtect(lpBaseAddress_ThreadExecution, sizeof(SysCode), PAGE_READONLY, &lpOldProtect))
        return STATUS_ACCESS_VIOLATION;

    // 释放临界区的所有权。
    LeaveCriticalSection(&CriticalSection);

    return status;
}

BOOL FreeThreadExecution()
{
    // 请求临界区的所有权。
    EnterCriticalSection(&CriticalSection);

    if (!lpBaseAddress_ThreadExecution) return TRUE;

    DWORD lpOldProtect = 0;
    if (!VirtualProtect(lpBaseAddress_ThreadExecution, sizeof(SysCode), PAGE_READWRITE, &lpOldProtect))
        return STATUS_ACCESS_VIOLATION;
    memset(lpBaseAddress_ThreadExecution, 0, sizeof(SysCode));

    if (!VirtualFree(lpBaseAddress_ThreadExecution, sizeof(SysCode), MEM_DECOMMIT)) // 虚拟地址仍然保留,物理页不保留
        return FALSE;

    if (!VirtualFree(lpBaseAddress_ThreadExecution, 0, MEM_RELEASE))  // 虚拟地址不保留 物理内存更不保留
        return FALSE;

    lpBaseAddress_ThreadExecution = NULL;

    // 释放临界区的所有权。
    LeaveCriticalSection(&CriticalSection);

    return TRUE;
}


ULONG NTAPI RtlNtStatusToDosErrorNoTeb(
    NTSTATUS Status
)
{
    HMODULE hDrv = LoadLibraryW(L"NtosKrnl.exe");
    if (hDrv)
    {
        auto func = (__RtlNtStatusToDosErrorNoTeb)GetProcAddress(hDrv, "RtlNtStatusToDosErrorNoTeb");
        if (func != NULL)
        {
            const ULONG ret = func(Status);
            FreeLibrary(hDrv);
            func = NULL;
            hDrv = NULL;
            return ret;
        }
        FreeLibrary(hDrv);
        hDrv = NULL;
        return 1626; // 无法执行函数
    }
    return 126; // 加载模块失败
}

DWORD __fastcall BaseSetLastNTError(NTSTATUS Status)
{
    ULONG dwError = 0;

    dwError = RtlNtStatusToDosErrorNoTeb(Status);
    SetLastError(dwError);
    return dwError;
}

EXECUTION_STATE WINAPI MySetThreadExecutionState(EXECUTION_STATE esFlags)
{
    EXECUTION_STATE PreviousFlags = 0;

    NTSTATUS status = MyNtSetThreadExecutionState(esFlags, &PreviousFlags);

    if (status >= 0)
        return PreviousFlags;

    BaseSetLastNTError(status);

    return 0;
}

/*
 * 类,该类封装 promise 和 future 对象,
 * 并提供接口函数为线程设置退出信号
 */
class Stoppable
{
    std::promise<void> exitSignal;
    std::future<void> futureObj;
public:
    Stoppable() :
        futureObj(exitSignal.get_future())
    {
    }
    Stoppable(Stoppable&& obj) : exitSignal(std::move(obj.exitSignal)), futureObj(std::move(obj.futureObj))
    {
        OutputDebugStringW(L"Move Constructor is called.\n");
    }
    Stoppable& operator=(Stoppable&& obj)
    {
        OutputDebugStringW(L"Move Assignment is called.\n");
        exitSignal = std::move(obj.exitSignal);
        futureObj = std::move(obj.futureObj);
        return *this;
    }
    // 任务需要提供此功能的定义,
    //  它将由线程函数调用
    virtual void ThreadExecutionHandler() = 0;

    // 线程要执行的线程函数
    void operator()()
    {
        ThreadExecutionHandler();
    }

    // 该函数用于检查线程是否被请求停止

    bool stopRequested()
    {
        // 检查 future 对象中的值是否可用
        if (futureObj.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout)
            return false;
        return true;
    }
    // 通过在 promise 对象中设置值来请求线程停止
    void stop()
    {
        exitSignal.set_value();
    }
};
/*
 * 扩展可停止任务的任务类
 */
class MyTask : public Stoppable
{
public:
    // 线程中要执行的任务函数
    void ThreadExecutionHandler()
    {
        OutputDebugStringW(L"Task Start.\n");

        EXECUTION_STATE PreviousFlags;
        WCHAR wsBuffer[50] = { 0 };
        // 检查线程是否被请求关闭,并在允许时候继续任务
        while (!stopRequested())
        {
            OutputDebugStringW(L"TaskAwaking.\n");
            for (int i = 0; i < 5; i++) {   // 快速归零期
                // 检查线程是否被请求关闭
                if (stopRequested()) return;
                // 执行关键函数
                PreviousFlags = MySetThreadExecutionState(ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED);
                if (!PreviousFlags)
                {
                    wmemset(wsBuffer, 0, sizeof(wsBuffer));
                    swprintf_s(wsBuffer, L"SetThreadExecutionStateFailed: %d\n", GetLastError());
                    OutputDebugStringW(wsBuffer);
                }
                wmemset(wsBuffer, 0, sizeof(wsBuffer));
                swprintf_s(wsBuffer, L"PreviousFlags: %01X\n", PreviousFlags);
                OutputDebugStringW(wsBuffer);
                std::this_thread::sleep_for(std::chrono::milliseconds(3000));
            }
            // 再次检查
            if (stopRequested()) return;
            // 休眠期
            std::this_thread::sleep_for(std::chrono::milliseconds(10000));
        }
        
        OutputDebugStringW(L"Task End.\n");
    }
};

int main()
{
    // 初始化一次临界区(仅有一次)
    if (!InitializeCriticalSectionAndSpinCount(&CriticalSection,
        0x00000400))
        return -1;

    // 创建任务实例
    MyTask ThreadExecution;
    // 创建线程用于执行任务
    std::thread th([&]()
        {
            ThreadExecution.ThreadExecutionHandler();
        });
    // 主线程等待输入,以便于退出进程
    char p = getchar();
    OutputDebugStringW(L"Asking Task to Stop.\n");
    // 结束任务
    ThreadExecution.stop();
    // 正在等待线程加入
    th.join();
    OutputDebugStringW(L"Thread Joined.\n");
    FreeThreadExecution(); // 干净地释放分配的虚拟内存
    OutputDebugStringW(L"Exiting Main Function.\n");
    // 释放临界区对象使用的资源。
    DeleteCriticalSection(&CriticalSection);

    return 0;
}

3.2 延迟手动睡眠(Win 10 之前系统)

屏蔽手动睡眠其实这里就复杂一些了。因为不同版本系统有些区别。目前,我也没找到像系统热键那样通过 RPC 秒杀的方法,在 R3 下只能通过全局钩子进行函数的挂钩,也就是挂钩电源状态设置的函数。

一般用户通过操作 explorer.exe 开始菜单中的睡眠按钮来使得电脑睡眠。通过动态的调试分析,我们知道 explorer.exe 调用 SetSuspendState 函数来请求睡眠状态。

cpp 复制代码
BOOLEAN SetSuspendState(
  [in] BOOLEAN bHibernate,
  [in] BOOLEAN bForce,
  [in] BOOLEAN bWakeupEventsDisabled
);

关于参数的解释:

[in] bHibernate

如果此参数为 TRUE ,则系统将休眠。 如果参数为 FALSE,则系统挂起。

[in] bForce

是否强制睡眠状态的立即更改。从 NT 开始,此参数不起作用。

[in] bWakeupEventsDisabled

如果此参数为 TRUE ,则系统会禁用所有唤醒事件。 如果参数为 FALSE,则任何系统唤醒事件将保持启用状态。

这个函数在调用时的参数是:

这会导致系统陷入睡眠状态,并且允许设备唤醒。

观察这个函数的调用,可以发现,其实是首先通过 AdjustTokenPrivileges 提权,然后调用了 NtInitiatePowerAction 函数,真正的功能由该函数实现。

该函数声明为:

cpp 复制代码
NTSTATUS NTAPI NtInitiatePowerAction(
    _In_     POWER_ACTION             SystemAction,
    _In_     SYSTEM_POWER_STATE       MinSystemState,
    _In_     ULONG                    Flags,
    _In_     BOOLEAN                  Asynchronous
);

关于这个参数需要解释一下它的参数:

  • SystemAction 请求的系统电源状态。 此成员必须是 POWER_ACTION 枚举类型值之一。
cpp 复制代码
typedef enum {
  PowerActionNone = 0,            // 不进行系统电源操作
  PowerActionReserved,            // 内部保留
  PowerActionSleep,               // 睡眠
  PowerActionHibernate,           // 休眠
  PowerActionShutdown,            // 关闭计算机
  PowerActionShutdownReset,       // 关闭计算机并重启
  PowerActionShutdownOff,         // 关闭计算机和切断电源
  PowerActionWarmEject,           // 热弹出?
  PowerActionDisplayOff           // 关闭显示器(推测)
} POWER_ACTION, *PPOWER_ACTION;

(MSDN 的描述很奇怪)

  • MinSystemState 电池电量低于设置的阈值时要进入的最小系统睡眠状态。 此成员必须是 SYSTEM_POWER_STATE 枚举类型值之一。
cpp 复制代码
typedef enum _SYSTEM_POWER_STATE {
  PowerSystemUnspecified = 0,        // 未指定的系统电源状态
  PowerSystemWorking = 1,            // 系统电源状态 S0 (正常运行)
  PowerSystemSleeping1 = 2,          // 系统电源状态 S1 (待机)
  PowerSystemSleeping2 = 3,          // 系统电源状态 S2
  PowerSystemSleeping3 = 4,          // 系统电源状态 S3 (混合睡眠)
  PowerSystemHibernate = 5,          // 系统电源状态 S4 (休眠)
  PowerSystemShutdown = 6,           // 系统电源状态 S5 (关机)
  PowerSystemMaximum = 7             // 指定最大枚举值
} SYSTEM_POWER_STATE, *PSYSTEM_POWER_STATE;
  • Flags 控制如何切换电源状态的标志。 此成员可以是以下一个或多个值。
含义
POWER_ACTION_CRITICAL 0x80000000 强制严重暂停
POWER_ACTION_DISABLE_WAKES 0x40000000 禁用所有唤醒事件
POWER_ACTION_LIGHTEST_FIRST 0x10000000 使用第一个最轻度的可用睡眠状态
POWER_ACTION_LOCK_CONSOLE 0x20000000 从某个系统待机状态恢复时,需要输入系统密码
POWER_ACTION_OVERRIDE_APPS 0x00000004 不能单独使用这个标志位,否则无效
POWER_ACTION_QUERY_ALLOWED 0x00000001 不能单独使用这个标志位,否则无效
POWER_ACTION_UI_ALLOWED 0x00000002 应用程序可以提示用户提供有关如何准备挂起的说明。 在 WM_POWERBROADCAST的 lParam 参数中传递的 Flags 参数中设置位 0
  • Asynchronous 指示更改组件条件的请求是同步执行还是异步执行。如果为 0,则表示同步执行,否则为异步执行。

在 Windows 7 SP1 7601 x64 上,可以看出该函数调用时的参数:

当前系统电源设置可以通过 powercfg /a 命令查看:

那么,我们就可以通过挂钩来拦截这个函数,这样就可以屏蔽 explorer.exe 发起的睡眠请求了

代码如下:

cpp 复制代码
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "detours.h"
#include <process.h>
#include <WtsApi32.h>
#pragma comment(lib, "WtsApi32.lib")
#pragma comment(lib, "detours.lib")

typedef _Return_type_success_(return >= 0) LONG NTSTATUS;
/*lint -save -e624 */  // Don't complain about different typedefs.
typedef NTSTATUS* PNTSTATUS;
/*lint -restore */  // Resume checking for different typedefs.


typedef NTSTATUS (NTAPI* __NtInitiatePowerAction)(
    _In_     POWER_ACTION             SystemAction,
    _In_     SYSTEM_POWER_STATE       MinSystemState,
    _In_     ULONG                    Flags,
    _In_     BOOLEAN                  Asynchronous
);

NTSTATUS NTAPI HookedNtInitiatePowerAction(
    _In_     POWER_ACTION             SystemAction,
    _In_     SYSTEM_POWER_STATE       MinSystemState,
    _In_     ULONG                    Flags,
    _In_     BOOLEAN                  Asynchronous
);

void StartHookingFunction();
void UnmappHookedFunction();

PVOID lpNtInitiatePowerAction = NULL;

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()
{
    HMODULE hModule = LoadLibraryA("ntdll.dll");
    if (hModule)
    {
        PVOID tp = 
            GetProcAddress(hModule, 
                "NtInitiatePowerAction");
    }

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

    lpNtInitiatePowerAction =
        DetourFindFunction(
            "ntdll.dll",
            "NtInitiatePowerAction");

    //将拦截的函数附加到原函数的地址上,这里可以拦截多个函数。

    DetourAttach(&(PVOID&)lpNtInitiatePowerAction,
        HookedNtInitiatePowerAction);
    //结束事务
    DetourTransactionCommit();
}


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

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

// 挂钩处理
NTSTATUS NTAPI HookedNtInitiatePowerAction(
    _In_     POWER_ACTION             SystemAction,
    _In_     SYSTEM_POWER_STATE       MinSystemState,
    _In_     ULONG                    Flags,
    _In_     BOOLEAN                  Asynchronous
)
{// 桌面图标
    WCHAR lpCap[8] = L"电源指示状态";
    WCHAR lpMsg[17] = L"请求的睡眠状态切换被系统阻止。";
    DWORD dwSessionId = WTSGetActiveConsoleSessionId();
    DWORD dwResponse = 0;
    WTSSendMessageW(
        WTS_CURRENT_SERVER_HANDLE, dwSessionId,
        lpCap, 8 * sizeof(WCHAR),
        lpMsg, 17 * sizeof(WCHAR),
        MB_OK | MB_ICONINFORMATION, 
        0, &dwResponse, FALSE);
    return STATUS_INVALID_PARAMETER;  // 直接不做任何操作返回
}

在 XP SP3 x86 注入运行效果如图(使用 RemoteDll 工具的远程线程注入):

在 Win 8 x64 运行效果如图所示(Win7以及 8.1 的类似):

三、尝试在较新系统上屏蔽

3.1 获取触发睡眠函数的目标进程

然而,相应的方法到 Win 10 上就不再适用了。这是因为 Windows 在更大范围引入了通用 Windows 平台 (UWP) 应用。UWP 是创建适用于 Windows 的客户端应用程序的众多方法之一。 UWP 应用使用 WinRT API 来提供强大的 UI 和高级异步功能,这些功能非常适用于 Internet 连接的设备。

我们用户的开始菜单操作主要由应用包程序(APPX)StartMenuExperienceHost 来完成。应用包通过匹配一个 Runtime Broker (由 svchost.exe 启动)来执行 APPX 应用的任务派发。Runtime Broker 是 Microsoft 的官方核心进程,在 Windows 8 中首次亮相,并且仍然是 Windows 10 和 Windows 11 的一部分。

我们知道 RuntimeBroker 程序和 UWP 应用一一对应,所以,系统中一般存在多个 RuntimeBroker 程序,此时,我们应该如何确认需要注入哪个 RuntimeBroker 程序呢?

微软提供了包查询 API(包查询 API - Win32 apps | Microsoft Learn)可用于获取系统上安装的应用包的相关信息。 每个应用包都包含构成 Windows 应用的文件,以及一个向 Windows 描述软件的清单文件。

其中,GetApplicationUserModelId 函数可以用于获取 UWP 进程的应用包字符串名称。

这个函数的声明如下:

LONG GetApplicationUserModelId(

[in] HANDLE hProcess,

[in, out] UINT32 *applicationUserModelIdLength,

[out] PWSTR applicationUserModelId

);

参数解释

  • [in] hProcess

进程的句柄。 此句柄必须具有 PROCESS_QUERY_LIMITED_INFORMATION 访问权限。 有关详细信息,请参阅 处理安全和访问权限。

  • [in, out] applicationUserModelIdLength

输入时, applicationUserModelId 缓冲区的大小(以宽字符为单位),应该足够大以防止失败(可以先测试调用获知需要的字符串大小)。 成功时,使用的缓冲区大小,包括 null 终止符。

  • [out] applicationUserModelId

指向接收应用程序用户模型 ID 的缓冲区的指针。

我们可以用下面的代码(由微软提供),获取指定通用桌面进程的 ID (也就是这个包名称)。

cpp 复制代码
#define _UNICODE 1
#define UNICODE 1

#include <Windows.h>
#include <appmodel.h>
#include <malloc.h>
#include <stdlib.h>
#include <stdio.h>

int ShowUsage();
void ShowProcessApplicationUserModelId(__in const UINT32 pid, __in HANDLE process);

int ShowUsage()
{
    wprintf(L"Usage: GetApplicationUserModelId <pid> [<pid>...]\n");
    return 1;
}

int __cdecl wmain(__in int argc, __in_ecount(argc) WCHAR* argv[])
{
    if (argc <= 1)
        return ShowUsage();

    for (int i = 1; i < argc; ++i)
    {
        UINT32 pid = wcstoul(argv[i], NULL, 10);
        if (pid > 0)
        {
            HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
            if (process == NULL)
                wprintf(L"Error %d in OpenProcess (pid=%u)\n", GetLastError(), pid);
            else
            {
                ShowProcessApplicationUserModelId(pid, process);
                CloseHandle(process);
            }
        }
    }
    return 0;
}

void ShowProcessApplicationUserModelId(__in const UINT32 pid, __in HANDLE process)
{
    wprintf(L"Process %u (handle=%p)\n", pid, process);

    UINT32 length = 0;
    LONG rc = GetApplicationUserModelId(process, &length, NULL);
    if (rc != ERROR_INSUFFICIENT_BUFFER)
    {
        if (rc == APPMODEL_ERROR_NO_APPLICATION)
            wprintf(L"Desktop application\n");
        else
            wprintf(L"Error %d in GetApplicationUserModelId\n", rc);
        return;
    }

    PWSTR fullName = (PWSTR)malloc(length * sizeof(*fullName));
    if (fullName == NULL)
    {
        wprintf(L"Error allocating memory\n");
        return;
    }

    rc = GetApplicationUserModelId(process, &length, fullName);
    if (rc != ERROR_SUCCESS)
        wprintf(L"Error %d retrieving ApplicationUserModelId\n", rc);
    else
        wprintf(L"%s\n", fullName);

    free(fullName);
}

测试如下图所示,这和 Procexp 的信息一致:

procexp 显示的进程包信息:

3.2 分析如何动态注入例程

那么,知道了如何获取 Package 名称,我们怎么进行动态注入呢?

通过调试发现发现, NtInitiatePowerAction 函数的触发路径有如下两个:

(1) 当没有用户登录时,在登录界面点击右下角睡眠或者休眠,那么会通过 winlogon 进程触发PowrProf!SetSuspendState函数(内部由 NtInitiatePowerAction 实现)。

(2) 如果当前用户已经登录,在开始菜单中点击睡眠或者休眠, RuntimeBroker.exe 程序发起该过程。

3.3 分析第一种情况

对于第一种情况,处理的时候挂钩需要麻烦一些,因为 Winlogon 的操作是异步调用(通过 RPC)。

直接挂钩 SetSuspendState 一般会失败,因为它是间接调用。我们一般仍然选择挂钩 NtInitiatePowerAction 函数,这会起作用,但是不会结束 RPC 过程,这里的客户端是 LogonUI.exe 进程,也就是上图中的 GUI 界面。那么,用户会卡在 Winlogon 桌面无法登出,此时,我们就需要手动切换桌面至 Default 桌面。

在代码中,修改如红色部分所示:

NTSTATUS NTAPI HookedNtInitiatePowerAction(

In POWER_ACTION SystemAction,

In SYSTEM_POWER_STATE MinSystemState,

In ULONG Flags,

In BOOLEAN Asynchronous

)

{

DWORD dwProcessId = GetCurrentProcessId();

WCHAR lpCap[8] = L"电源指示状态";

WCHAR lpMsg[45] = { 0 };

HDESK hUser = NULL;

DWORD dwSessionId = WTSGetActiveConsoleSessionId();

DWORD dwResponse = 0;

swprintf_s(lpMsg, L"请求的睡眠状态切换被系统阻止。来自进程[%d]的消息。", dwProcessId);

WTSSendMessageW(

WTS_CURRENT_SERVER_HANDLE, dwSessionId,

lpCap, 8 * sizeof(WCHAR),

lpMsg, 45 * sizeof(WCHAR),

MB_YESNO | MB_ICONINFORMATION,

0, &dwResponse, TRUE);

if(dwResponse == IDYES)

{

return ((__NtInitiatePowerAction)lpNtInitiatePowerAction)(

SystemAction, MinSystemState, Flags, Asynchronous);

}

// 切换回用户桌面
hUser = OpenDesktopW(L"Default", 0, FALSE, GENERIC_ALL);
SwitchDesktop(hUser);

return 0;

}

但是,也由此导致了第二个问题,我们前面说 RPC 过程并没有结束,这会导致后面再进行需要在安全桌面下面完成的 RPC 过程时,客户端读取 Winlogon 的内存中特定位置的标志位,并会认为当前不需要切换桌面,例如 AIS 在启动"需要询问是否以管理员身份启动"的进程时,用户桌面未发生切换,导致提权进程死锁。此时,我们在没有找到"特定位置的标志位"前,最佳的方法就是在提权前手动切换至 Winlogon 桌面,提权完成后恢复桌面(这样的手动挡工程只需要最近一次完成即可)。

利用上一篇挂钩 Ndr64AsyncServerCallAll 的分析结论:

"00050000" // 以管理员身份启动

"01050000" // 成功以管理员身份启动

"0404000000" // Ctrl+Alt+Del KEY(安全警示页面)

"0404000004" // Ctrl+Shift+Esc KEY(任务管理器)

需要注意对于再次按下 Ctrl+Alt+Del 也会产生影响,所以也要处理这个。

我们挂钩例程可以这样写:

cpp 复制代码
void RPC_ENTRY HookedNdr64AsyncServerCallAll(
    LPRPC_MESSAGE pRpcMsg
)
{
    char bufferMask[6] = { 0 }; // 用于存储特征码
    HDESK hDsk = NULL;
    // 基址
    uint64_t iBaseAddress = reinterpret_cast<uintptr_t>(pRpcMsg->Buffer);

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

    // 从内存中复制特征码(低位 + 高位)
    memcpy(&bufferMask, reinterpret_cast<PVOID>(iBaseAddress), sizeof(char) * 5);
    
    if (bufferMask[1] == 5 || bufferMask[1] == 4) // 提权 || Ctrl + Alt + Esc || Ctrl + Shift + Esc
    {
        // 切换到 Winlogon 桌面(提权时 || Ctrl + Alt + Esc 排除 Ctrl + Shift + Esc)
        if (bufferMask[0] == 0 || (bufferMask[0] == 4  && bufferMask[4] != 4))
        {
            hDsk = OpenDesktopW(L"Winlogon", 0, FALSE, GENERIC_ALL);
            SwitchDesktop(hDsk);
        }
        else if (bufferMask[0] == 1)// 切换回用户桌面(提权完成时)
        {
            hDsk = OpenDesktopW(L"Default", 0, FALSE, GENERIC_ALL);
            SwitchDesktop(hDsk);
        }
    }
    return ((__Ndr64AsyncServerCallAll)fpNdr64AsyncServerCallAll)(pRpcMsg);
}

完整注入 Winlogon.exe 的代码如下:

cpp 复制代码
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "detours.h"
#include <WtsApi32.h>
#include <stdio.h>
#include <rpc.h>
#include <cstdint>

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

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

#define __RPC_FAR
#define RPC_MGR_EPV void
#define  RPC_ENTRY __stdcall

typedef void* LI_RPC_HANDLE;
typedef LI_RPC_HANDLE LRPC_BINDING_HANDLE;

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

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

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


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

void RPC_ENTRY HookedNdr64AsyncServerCallAll(
    LPRPC_MESSAGE pRpcMsg
);

typedef _Return_type_success_(return >= 0) LONG NTSTATUS;
/*lint -save -e624 */  // Don't complain about different typedefs.
typedef NTSTATUS* PNTSTATUS;
/*lint -restore */  // Resume checking for different typedefs.


typedef NTSTATUS (NTAPI* __NtInitiatePowerAction)(
    _In_     POWER_ACTION             SystemAction,
    _In_     SYSTEM_POWER_STATE       MinSystemState,
    _In_     ULONG                    Flags,
    _In_     BOOLEAN                  Asynchronous
);

NTSTATUS NTAPI HookedNtInitiatePowerAction(
    _In_     POWER_ACTION             SystemAction,
    _In_     SYSTEM_POWER_STATE       MinSystemState,
    _In_     ULONG                    Flags,
    _In_     BOOLEAN                  Asynchronous
);



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");

    lpNtInitiatePowerAction =
        DetourFindFunction(
            "ntdll.dll",
            "NtInitiatePowerAction");

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

    DetourAttach(&(PVOID&)lpNtInitiatePowerAction,
        HookedNtInitiatePowerAction);
    //结束事务
    DetourTransactionCommit();
}


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

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


NTSTATUS NTAPI HookedNtInitiatePowerAction(
    _In_     POWER_ACTION             SystemAction,
    _In_     SYSTEM_POWER_STATE       MinSystemState,
    _In_     ULONG                    Flags,
    _In_     BOOLEAN                  Asynchronous
)
{

    DWORD dwProcessId = GetCurrentProcessId();
    WCHAR lpCap[8] = L"电源指示状态";
    WCHAR lpMsg[45] = { 0 };
    HDESK hUser = NULL;
    DWORD dwSessionId = WTSGetActiveConsoleSessionId();
    DWORD dwResponse = 0;

    swprintf_s(lpMsg, L"请求的睡眠状态切换被系统阻止。来自进程[%d]的消息。", dwProcessId);

    WTSSendMessageW(
        WTS_CURRENT_SERVER_HANDLE, dwSessionId,
        lpCap, 8 * sizeof(WCHAR),
        lpMsg, 45 * sizeof(WCHAR),
        MB_YESNO | MB_ICONINFORMATION,
        0, &dwResponse, TRUE);
    if(dwResponse == IDYES)
    {
        return ((__NtInitiatePowerAction)lpNtInitiatePowerAction)(
            SystemAction, MinSystemState, Flags, Asynchronous);
    }
    // 切换回用户桌面
    hUser = OpenDesktopW(L"Default", 0, FALSE, GENERIC_ALL);
    SwitchDesktop(hUser);
    return 0;
}


void RPC_ENTRY HookedNdr64AsyncServerCallAll(
    LPRPC_MESSAGE pRpcMsg
)
{
    char bufferMask[6] = { 0 }; // 用于存储特征码
    HDESK hDsk = NULL;
    // 基址
    uint64_t iBaseAddress = reinterpret_cast<uintptr_t>(pRpcMsg->Buffer);

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

    // 从内存中复制特征码(低位 + 高位)
    memcpy(&bufferMask, reinterpret_cast<PVOID>(iBaseAddress), sizeof(char) * 5);
    
    if (bufferMask[1] == 5 || bufferMask[1] == 4) // 提权 || Ctrl + Alt + Esc || Ctrl + Shift + Esc
    {
        // 切换到 Winlogon 桌面(提权时 || Ctrl + Alt + Esc 排除 Ctrl + Shift + Esc)
        if (bufferMask[0] == 0 || (bufferMask[0] == 4  && bufferMask[4] != 4))
        {
            hDsk = OpenDesktopW(L"Winlogon", 0, FALSE, GENERIC_ALL);
            SwitchDesktop(hDsk);
        }
        else if (bufferMask[0] == 1)// 切换回用户桌面(提权完成时)
        {
            hDsk = OpenDesktopW(L"Default", 0, FALSE, GENERIC_ALL);
            SwitchDesktop(hDsk);
        }
    }
    return ((__Ndr64AsyncServerCallAll)fpNdr64AsyncServerCallAll)(pRpcMsg);
}

测试过程不方便整理全部的截图,提供下图效果(点击是继续睡眠,点击否取消操作):

3.4 分析第二种情况

对于第二种情况,只要把 LoadLibraryTool 工具的代码改一改就行。最简单的就是循环遍历系统进程快照,确定 RuntimeBroker 进程然后注入 Dll。

首先,通过 CreateToolhelp32Snapshot 创建快照,遍历进程列表,找到所有的 RuntimeBroker 进程:

cpp 复制代码
void FindRuntimeBrokerProcess(std::vector<DWORD>* gRuntimeProcessList, DWORD dwLastId)
{
    const wchar_t exepth[] = L"RuntimeBroker.exe";
    HANDLE hp = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32W pe = { 0 };
    pe.dwSize = sizeof(PROCESSENTRY32W);

    if (Process32FirstW(hp, &pe)) {
        do {
            if (!wcscmp(pe.szExeFile, exepth) && pe.cntThreads >= 1 && dwLastId != pe.th32ProcessID) {
                // 查找 RuntimeBroker 目标进程
                (*gRuntimeProcessList).push_back(pe.th32ProcessID);
            }
        } while (Process32NextW(hp, &pe));
    }

    CloseHandle(hp);
}

利用上文提出的方法获取包名称字符串,比较是否有 StartMenuExperience 子串:

cpp 复制代码
DWORD StartMenuBrokerProcessModelIdWorker(std::vector<DWORD>* gRuntimeProcessList)
{
    if (gRuntimeProcessList == nullptr) return 0;

    std::vector<DWORD> gList = *gRuntimeProcessList;
    UINT32 globalRunProcId = NULL;
    HANDLE globalRunProcHandle = NULL;
    UINT32 bufferLength = 1024;
    LONG   dwlResponse = 0;
    WCHAR bufferFullName[1025] = { 0 };

    if (gList.size() == 0)
    {
        wprintf(L"StartMenuBrokerProcessModelId failed: NoFound TargetProcess.\n");
        return 0;
    }

    for (int i = 0; i < gList.size(); i++) {
        globalRunProcId = gList[i];
        globalRunProcHandle = OpenProcess(
            PROCESS_QUERY_LIMITED_INFORMATION, FALSE, globalRunProcId);
        if (globalRunProcHandle == NULL)
        {
            wprintf(L"Error %d in OpenProcess (pid=%u)\n",
                GetLastError(), globalRunProcId);
            globalRunProcId = 0;
            continue;
        }

        memset(bufferFullName, 0, bufferLength * sizeof(WCHAR));

        dwlResponse = GetApplicationUserModelId(globalRunProcHandle,
            &bufferLength, bufferFullName);
        if (bufferFullName[0] == 0)
        {
            CloseHandle(globalRunProcHandle);
            globalRunProcId = NULL;
            globalRunProcHandle = NULL;
            continue;
        }
        // 判断是不是 StartMenuExperienceHost 对应的 RuntimeBroker 进程
        if (wcsstr(bufferFullName, L"StartMenuExperienceHost")) {
            wprintf(L"StartMenuExp Runtime Process %u (handle=%p)\n", globalRunProcId, globalRunProcHandle);
            CloseHandle(globalRunProcHandle);
            globalRunProcHandle = NULL;
            return globalRunProcId;
        }

        CloseHandle(globalRunProcHandle);
        globalRunProcId = NULL;
        globalRunProcHandle = NULL;
    }
    return 0;
}

然后就是注入模块了(使用 ZwCreateThreadEx):

cpp 复制代码
BOOL InjectStartMenuBrokerHandler(
    LPCWSTR baseBinPath,
    PDWORD dwProcessId
)
{
    std::vector<DWORD> gRuntimeProcessList;
    FindRuntimeBrokerProcess(&gRuntimeProcessList, *dwProcessId);
    
    DWORD gNewTargetId = StartMenuBrokerProcessModelIdWorker(&gRuntimeProcessList);

    if (!gNewTargetId) {
        return FALSE;    // 未找到目标进程,静默
    }

    if (ProcessHasLoadDll(gNewTargetId, baseBinPath))
    {
        *dwProcessId = gNewTargetId;
        wprintf(L"警告:PID 为 %d 的进程已经包含目标 DLL。\n", gNewTargetId);
        
        return TRUE;
    }

    if (ZwCreateThreadExInjectDll(gNewTargetId, baseBinPath))
    {
        *dwProcessId = gNewTargetId;
        wprintf(L"已经成功注入 PID 为 %d 的进程。\n", gNewTargetId);
        return TRUE;
    }
    else{
        wprintf(L"错误:注入 PID 为 %d 的进程时失败 (Error: %d)。\n", 
            gNewTargetId, GetLastError());
        return FALSE;
    }
}

完整的代码如下:

cpp 复制代码
#include <iostream>
#include <windows.h>
#include <vector>
#include <tlhelp32.h>
#include <shlwapi.h>
#include <appmodel.h>

#pragma comment(lib, "Shlwapi.lib")
#pragma comment(lib, "advapi32.lib")

BOOL ProcessHasLoadDll(
    DWORD pid,
    const TCHAR* dll
);

BOOL ZwCreateThreadExInjectDll(
    DWORD dwProcessId,
    const wchar_t* pszDllFileName
);

BOOL EnableDebugPrivilege(
    BOOL bEnablePrivilege
);

BOOL InjectStartMenuBrokerHandler(
    LPCWSTR baseBinPath,
    PDWORD dwProcessId
);

int wmain(int argc, wchar_t* argv[])
{
    SetConsoleTitleW(L"StartMenuInjectTool v.1.0");
    setlocale(NULL, "chs");
    wprintf(L"StartMenuInjectTool v.1.0\nStartMenuExperience 进程专用 DLL 注入工具;@LianYou516\n\n");

    // 检查参数是否合法
    if (argc != 2)
    {
        wprintf(L"错误:参数不合法!\n");
        std::cin.get();
        return -1;
    }

    wchar_t* dllpth = argv[1];

    const size_t dllpthlen =
        wcslen(dllpth) * sizeof(wchar_t);  // 字符串长度
    // 判断字符串长度是否超限
    if (dllpthlen < 1 || dllpthlen > 1024)
    {
        wprintf(L"错误:文件路径错误或路径长度过长!\n");
        std::cin.get();
        return -1;
    }

    BOOL dllextflag = PathFileExistsW(dllpth);
    if (FALSE == dllextflag)
    {
        wprintf(L"错误:文件不存在或者无法访问!\n");
        std::cin.get();
        return -1;
    }

    EnableDebugPrivilege(TRUE);  // 启用 Debug 权限

    // 循环检查并注入
    DWORD lpTargetProcessId = 0;
    while (true) {
        InjectStartMenuBrokerHandler(dllpth, &lpTargetProcessId);
        Sleep(1500);
    }

    std::cin.get();
    return 0;
}

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

BOOL EnableDebugPrivilege(
    BOOL bEnablePrivilege
)
{
    HANDLE hProcess = NULL;
    TOKEN_PRIVILEGES tp{};
    LUID luid;
    hProcess = GetCurrentProcess();
    HANDLE hToken = NULL;
    OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, &hToken);

    if (!LookupPrivilegeValueW(
        NULL,
        SE_DEBUG_NAME,
        &luid))
    {
        printf("LookupPrivilegeValue error: %u\n", GetLastError());
        CloseHandle(hToken);
        return FALSE;
    }

    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    if (bEnablePrivilege)
        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    else
        tp.Privileges[0].Attributes = 0;

    // 启用或禁用权限

    if (!AdjustTokenPrivileges(
        hToken,
        FALSE,
        &tp,
        sizeof(TOKEN_PRIVILEGES),
        (PTOKEN_PRIVILEGES)NULL,
        (PDWORD)NULL))
    {
        printf("AdjustTokenPrivileges error: %u\n", GetLastError());
        CloseHandle(hToken);
        return FALSE;
    }

    if (GetLastError() == ERROR_NOT_ALL_ASSIGNED)

    {
        printf("The token does not have the specified privilege. \n");
        CloseHandle(hToken);
        return FALSE;
    }
    CloseHandle(hToken);
    return TRUE;
}

BOOL ProcessHasLoadDll(DWORD pid, const TCHAR* dll) {
    /*
    * 参数为TH32CS_SNAPMODULE 或 TH32CS_SNAPMODULE32时,
    * 如果函数失败并返回ERROR_BAD_LENGTH,则重试该函数直至成功
    * 进程创建未初始化完成时,CreateToolhelp32Snapshot会返回 error 299,但其它情况下不会
    */
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
    while (INVALID_HANDLE_VALUE == hSnapshot) {
        DWORD dwError = GetLastError();
        if (dwError == ERROR_BAD_LENGTH) {
            hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
            continue;
        }
        else {
            printf("CreateToolhelp32Snapshot failed: %d\ncurrentProcessId: %d \t targetProcessId:%d\n",
                dwError, GetCurrentProcessId(), pid);
            return FALSE;
        }
    }
    MODULEENTRY32W mi{};
    mi.dwSize = sizeof(MODULEENTRY32W); // 第一次使用必须初始化成员
    BOOL bRet = Module32FirstW(hSnapshot, &mi);
    while (bRet) {
        // mi.szModule 是短路径
        if (wcsstr(dll, mi.szModule) || wcsstr(mi.szModule, dll)) {

            if (hSnapshot != NULL) CloseHandle(hSnapshot);
            return TRUE;
        }
        mi.dwSize = sizeof(MODULEENTRY32W);
        bRet = Module32NextW(hSnapshot, &mi);
    }
    if (hSnapshot != NULL) CloseHandle(hSnapshot);
    return FALSE;
}

BOOL ZwCreateThreadExInjectDll(
    DWORD dwProcessId,
    const wchar_t* pszDllFileName
)
{
    size_t pathSize = (wcslen(pszDllFileName) + 1) * sizeof(wchar_t);
    // 打开目标进程
    HANDLE hProcess = OpenProcess(
        PROCESS_ALL_ACCESS, // 打开权限
        FALSE,              // 是否继承
        dwProcessId);       // 进程PID
    if (NULL == hProcess)
    {
        wprintf(L"错误:打开目标进程失败!\n");
        return FALSE;
    }
    // 在目标进程中申请空间
    LPVOID lpPathAddr = VirtualAllocEx(
        hProcess,                   // 目标进程句柄
        0,                          // 指定申请地址
        pathSize,                   // 申请空间大小
        MEM_RESERVE | MEM_COMMIT,   // 内存的状态
        PAGE_READWRITE);            // 内存属性
    if (NULL == lpPathAddr)
    {
        wprintf(L"错误:在目标进程中申请空间失败!\n");
        CloseHandle(hProcess);
        return FALSE;
    }
    // 在目标进程中写入Dll路径
    if (FALSE == WriteProcessMemory(
        hProcess,                   // 目标进程句柄
        lpPathAddr,                 // 目标进程地址
        pszDllFileName,             // 写入的缓冲区
        pathSize,                   // 缓冲区大小
        NULL))                      // 实际写入大小
    {
        wprintf(L"错误:目标进程中写入Dll路径失败!\n");
        CloseHandle(hProcess);
        return FALSE;
    }

    // 加载ntdll.dll
    HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
    if (NULL == hNtdll)
    {
        wprintf(L"错误:加载ntdll.dll失败!\n");
        CloseHandle(hProcess);
        return FALSE;
    }

    // 获取 LoadLibraryW 的函数地址
    // FARPROC 可以自适应 32 位与 64 位
    FARPROC pFuncProcAddr = GetProcAddress(GetModuleHandleW(L"Kernel32.dll"),
        "LoadLibraryW");
    if (NULL == pFuncProcAddr)
    {
        wprintf(L"错误:获取LoadLibrary函数地址失败!\n");
        CloseHandle(hProcess);
        return FALSE;
    }

    // 获取ZwCreateThreadEx函数地址,该函数在32位与64位下原型不同
    // _WIN64 用来判断编译环境 ,_WIN32 用来判断是否是 Windows 系统
#ifdef _WIN64
    typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        ULONG CreateThreadFlags,
        SIZE_T ZeroBits,
        SIZE_T StackSize,
        SIZE_T MaximumStackSize,
        LPVOID pUnkown
        );
#else
    typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        BOOL CreateSuspended,
        DWORD dwStackSize,
        DWORD dw1,
        DWORD dw2,
        LPVOID pUnkown
        );
#endif 
    typedef_ZwCreateThreadEx ZwCreateThreadEx =
        (typedef_ZwCreateThreadEx)GetProcAddress(hNtdll, "ZwCreateThreadEx");
    if (NULL == ZwCreateThreadEx)
    {
        wprintf(L"错误:获取ZwCreateThreadEx函数地址失败!\n");
        CloseHandle(hProcess);
        return FALSE;
    }
    // 在目标进程中创建远线程
    HANDLE hRemoteThread = NULL;
    DWORD lpExitCode = 0;
    DWORD dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL,
        hProcess,
        (LPTHREAD_START_ROUTINE)pFuncProcAddr, lpPathAddr, 0, 0, 0, 0, NULL);
    if (NULL == hRemoteThread)
    {
        wprintf(L"错误:目标进程中创建线程失败!\n");
        CloseHandle(hProcess);
        return FALSE;
    }

    // 等待线程结束
    WaitForSingleObject(hRemoteThread, -1);
    GetExitCodeThread(hRemoteThread, &lpExitCode);
    if (lpExitCode == 0)
    {
        wprintf(L"错误:目标进程中注入 DLL 失败,请检查提供的 DLL 是否有效!\n");
        CloseHandle(hProcess);
        return FALSE;
    }
    // 清理环境
    VirtualFreeEx(hProcess, lpPathAddr, 0, MEM_RELEASE);
    CloseHandle(hRemoteThread);
    CloseHandle(hProcess);
    FreeLibrary(hNtdll);
    return TRUE;
}

void FindRuntimeBrokerProcess(std::vector<DWORD>* gRuntimeProcessList, DWORD dwLastId)
{
    const wchar_t exepth[] = L"RuntimeBroker.exe";
    HANDLE hp = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32W pe = { 0 };
    pe.dwSize = sizeof(PROCESSENTRY32W);

    if (Process32FirstW(hp, &pe)) {
        do {
            if (!wcscmp(pe.szExeFile, exepth) && pe.cntThreads >= 1 && dwLastId != pe.th32ProcessID) {
                // 查找 RuntimeBroker 目标进程
                (*gRuntimeProcessList).push_back(pe.th32ProcessID);
            }
        } while (Process32NextW(hp, &pe));
    }

    CloseHandle(hp);
}


DWORD StartMenuBrokerProcessModelIdWorker(std::vector<DWORD>* gRuntimeProcessList)
{
    if (gRuntimeProcessList == nullptr) return 0;

    std::vector<DWORD> gList = *gRuntimeProcessList;
    UINT32 globalRunProcId = NULL;
    HANDLE globalRunProcHandle = NULL;
    UINT32 bufferLength = 1024;
    LONG   dwlResponse = 0;
    WCHAR bufferFullName[1025] = { 0 };

    if (gList.size() == 0)
    {
        wprintf(L"StartMenuBrokerProcessModelId failed: NoFound TargetProcess.\n");
        return 0;
    }

    for (int i = 0; i < gList.size(); i++) {
        globalRunProcId = gList[i];
        globalRunProcHandle = OpenProcess(
            PROCESS_QUERY_LIMITED_INFORMATION, FALSE, globalRunProcId);
        if (globalRunProcHandle == NULL)
        {
            wprintf(L"Error %d in OpenProcess (pid=%u)\n",
                GetLastError(), globalRunProcId);
            globalRunProcId = 0;
            continue;
        }

        memset(bufferFullName, 0, bufferLength * sizeof(WCHAR));

        dwlResponse = GetApplicationUserModelId(globalRunProcHandle,
            &bufferLength, bufferFullName);
        if (bufferFullName[0] == 0)
        {
            CloseHandle(globalRunProcHandle);
            globalRunProcId = NULL;
            globalRunProcHandle = NULL;
            continue;
        }
        // 判断是不是 StartMenuExperienceHost 对应的 RuntimeBroker 进程
        if (wcsstr(bufferFullName, L"StartMenuExperienceHost")) {
            wprintf(L"StartMenuExp Runtime Process %u (handle=%p)\n", globalRunProcId, globalRunProcHandle);
            CloseHandle(globalRunProcHandle);
            globalRunProcHandle = NULL;
            return globalRunProcId;
        }

        CloseHandle(globalRunProcHandle);
        globalRunProcId = NULL;
        globalRunProcHandle = NULL;
    }
    return 0;
}


BOOL InjectStartMenuBrokerHandler(
    LPCWSTR baseBinPath,
    PDWORD dwProcessId
)
{
    std::vector<DWORD> gRuntimeProcessList;
    FindRuntimeBrokerProcess(&gRuntimeProcessList, *dwProcessId);
    
    DWORD gNewTargetId = StartMenuBrokerProcessModelIdWorker(&gRuntimeProcessList);

    if (!gNewTargetId) {
        return FALSE;    // 未找到目标进程,静默
    }

    if (ProcessHasLoadDll(gNewTargetId, baseBinPath))
    {
        *dwProcessId = gNewTargetId;
        wprintf(L"警告:PID 为 %d 的进程已经包含目标 DLL。\n", gNewTargetId);
        
        return TRUE;
    }

    if (ZwCreateThreadExInjectDll(gNewTargetId, baseBinPath))
    {
        *dwProcessId = gNewTargetId;
        wprintf(L"已经成功注入 PID 为 %d 的进程。\n", gNewTargetId);
        return TRUE;
    }
    else{
        wprintf(L"错误:注入 PID 为 %d 的进程时失败 (Error: %d)。\n", 
            gNewTargetId, GetLastError());
        return FALSE;
    }
}

效果如图:

尝试通过开始菜单睡眠,成功被拦截:

总结&后记

本文从 R3 角度分析了编程拦截睡眠的一般方法,通过上文,其实不难发现,要在 R3 下拦截任意进程发起的睡眠,必须全局注入处理 NtInitiatePowerAction、 NtSetSystemPowerState 等函数。而这样子的全局挂钩是受到很多限制的,最好的方法是在驱动中过滤这两个函数。


更新于:2024.1.21

相关推荐
Elastic 中国社区官方博客8 小时前
使用 Elastic 收集 Windows 遥测数据:ETW Filebeat 输入简介
大数据·windows·elasticsearch·搜索引擎·全文检索·可用性测试
44漏洞观察员9 小时前
windows实战-wordpress——玄机靶场
服务器·windows·web安全·网络安全·安全威胁分析
ZHOUPUYU10 小时前
最新‌VSCode保姆级安装教程(附安装包)
c语言·开发语言·c++·ide·windows·vscode·编辑器
踩着上帝的小丑13 小时前
mybatis学习(四)
windows·学习·mybatis
秦时明月之君临天下14 小时前
Windows用pm2部署node.js项目
windows·node.js
Allen Roson16 小时前
CListCtrl::InsertItem和临界区导致程序卡死
c++·windows·insertitem卡死·clistctrl插入项目·临界区死锁
麻雀12317 小时前
写时复制,读时加载
windows
Narutolxy1 天前
从 Mac 远程控制 Windows:一站式配置与实践指南20241123
windows·macos
小白一键重装系统1 天前
电脑系统重装小白教程
windows·电脑·重装系统
好开心331 天前
js高级06-ajax封装和跨域
开发语言·前端·javascript·ajax·okhttp·ecmascript·交互