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。icon-default.png?t=N7T8https://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_REQUIRED 和 ES_CONTINUOUS 。 多媒体应用程序(如视频播放器和演示应用程序)在长时间显示视频时,必须使用 ES_DISPLAY_REQUIRED ,而无需用户输入。 文字处理器、电子表格、浏览器和游戏等应用程序不需要调用 SetThreadExecutionState

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

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

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

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

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

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

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

// 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 的接口函数,对于函数的间接调用需要有一些校验的保护过程。

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

#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 函数来请求睡眠状态。

BOOLEAN SetSuspendState(
  [in] BOOLEAN bHibernate,
  [in] BOOLEAN bForce,
  [in] BOOLEAN bWakeupEventsDisabled
);

关于参数的解释:

[in] bHibernate

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

[in] bForce

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

[in] bWakeupEventsDisabled

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

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

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

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

该函数声明为:

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

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

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

(MSDN 的描述很奇怪)

  • MinSystemState 电池电量低于设置的阈值时要进入的最小系统睡眠状态。 此成员必须是 SYSTEM_POWER_STATE 枚举类型值之一。
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 发起的睡眠请求了

代码如下:

// 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 (也就是这个包名称)。

#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 名称,我们怎么进行动态注入呢?

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

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

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

参考并作了修正:通过Hook拦截Windows 睡眠及休眠操作。

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 也会产生影响,所以也要处理这个。

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

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 的代码如下:

// 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 进程:

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 子串:

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):

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;
    }
}

完整的代码如下:

#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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/337966.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

代码随想录第十八天 513 找树左下角的值 112 路径之和 106 从中序与后序遍历序列构造二叉树

LeetCode 513 找树左下角的值 题目描述 给定一个二叉树的 根节点 root&#xff0c;请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 示例 1: 输入: root [2,1,3] 输出: 1示例 2: 输入: [1,2,3,4,null,5,6,null,null,7] 输出: 7 思路 1.确定递…

matlab appdesigner系列-常用14-树(复选框)

之前系列常用9&#xff0c;为单个复选框。树&#xff0c;就是多个复选框形成的选项组 示例&#xff1a;列举湖北省的几个城市 湖北省 武汉 宜昌 襄阳 荆州 1&#xff09;将树&#xff08;复选框&#xff09;拖拽到画布上&#xff0c;方式1就是&#xff1a;文字可以在右侧…

课题学习(十九)----Allan方差:陀螺仪噪声分析

一、介绍 Allan方差是一种分析时域数据序列的方法&#xff0c;用于测量振荡器的频率稳定性。该方法还可用于确定系统中作为平均时间函数的本征噪声。该方法易于计算和理解&#xff0c;是目前最流行的识别和量化惯性传感器数据中存在的不同噪声项的方法之一。该方法的结果与适用…

131. 分割回文串 - 力扣(LeetCode)

问题描述 给你一个字符串 s&#xff0c;请你将 s 分割成一些子串&#xff0c;使每个子串都是 回文串 。返回 s 所有可能的分割方案。 回文串 是正着读和反着读都一样的字符串。 输入示例 s "aab"输出示例 [["a","a","b"],["…

PS滤镜插件:Adobe Camera Raw 16 for Mac中文激活版

Adobe Camera Raw是Adobe公司开发的一款用于处理数码相机RAW格式文件的软件插件。它可以在Adobe Photoshop、Adobe Bridge和Adobe Lightroom等软件中使用&#xff0c;用于调整RAW文件的曝光、白平衡、对比度、色彩饱和度、锐化等参数&#xff0c;从而得到更好的图像质量。 软件…

STM32之模数转换器(ADC)

一、模数转换器介绍 1、模数转换器简介 为什么使用模拟转换器&#xff1f;&#xff1f; 因为MCU只能识别01010101的数字信号&#xff0c;而外部物理信号均为模拟信号&#xff0c;如声音、光、电等&#xff0c;所以为了让计算机能够处理外部物理的信息&#xff0c;必须要通过…

增加CO气体报警、氢气报警以及烟雾报警

标题&#xff1a;增加CO气体报警、氢气报警以及烟雾报警。 内容&#xff1a;通过ADC采集通道&#xff0c;实现传感器电压的采集&#xff0c;通过对电压进行判断是否报警&#xff0c;&#xff08;理论上应该可以计算出气体浓度&#xff0c;通过气体浓度来判断是否报警&#xff…

入门分布式事务,2PC 3PC

分布式事务 什么是分布式一致性 在分布式系统中&#xff0c;为了保证数据的高可用&#xff0c;通常&#xff0c;我们会将数据保留多个副本(replica)&#xff0c;这些副本会放置在不同的物理的机器上。为了对用户提供正确的增\删\改\查等语义&#xff0c;我们需要保证这些放置…

VRRP协议负载分担

VRRP流量负载分担 VRRP负载分担与VRRP主备备份的基本原理和报文协商过程都是相同的。同样对于每一个VRRP备份组,都包含一个Master设备和若干Backup设备。与主备备份方式不同点在于:负载分担方式需要建立多个VRRP备份组,各备份组的Master设备可以不同;同一台VRRP设备可以加…

骑砍战团MOD开发(39)-RTS塔防保卫卡拉迪亚大陆

骑砍1战团mod开发-RTS塔防保卫卡拉迪亚大陆_哔哩哔哩bilibili_骑马与砍杀https://www.bilibili.com/video/BV1hw411E7bP/骑砍战团MOD开发(28)-骑砍联盟之RTS大规模军团竞技-CSDN博客文章浏览阅读369次&#xff0c;点赞11次&#xff0c;收藏7次。【代码】骑砍战团MOD开发(28)-骑…

SpringCloud Alibaba 深入源码 - Nacos 分级存储模型、支撑百万服务注册压力、解决并发读写问题(CopyOnWrite)

目录 一、SpringCloudAlibaba 源码分析 1.1、SpringCloud & SpringCloudAlibaba 常用组件 1.2、Nacos的服务注册表结构是怎样的&#xff1f; 1.2.1、Nacos的分级存储模型&#xff08;理论层&#xff09; 1.2.2、Nacos 源码启动&#xff08;准备工作&#xff09; 1.2.…

java steam 的使用

说steam 前看下kotlin的一个写法如果用java怎么写 fun main() {// 创建一个列表val fruits listOf("Apple", "Banana", "Cherry", "Date", "Elderberry")// 使用 Sequence 进行过滤和映射操作val uppercaseFruitLengths …

《机器学习》客户流失判断-python实现

客户流失判断 题目赛题描述数据说明赛题来源-DataCastle 问题描述解题思路Python实现读取数据并初步了解导入宏包读取数据查看数据类型检查缺失值描述性统计分析 可视化分析用户流失分析特征分析任期年数与客户流失的关系&#xff1a;服务类属性分析特征相关性分析 数据预处理类…

二叉树的直径(LeetCode 543)

文章目录 1.问题描述2.难度等级3.热门指数4.解题思路参考文献 1.问题描述 给你一棵二叉树的根节点&#xff0c;返回该树的直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的长度 。这条路径可能经过也可能不经过根节点 root 。 两节点之间路径的长度由它们之间边数…

安卓平板局域网内远程控制工控机方法

安卓平板局域网内远程控制工控机方法 将所需要远程控制的工控机通过网线连接到具有WiFi功能的路由器上&#xff0c;将安卓平板连接上WiFi&#xff0c;如下图所示 下载NoMachine远程软件安装包&#xff0c;官网地址&#xff1a;https://www.nomachine.com/ 点击Download now按钮…

C++实战:类的包含编译模型

文章目录 一、实战概述二、实战步骤&#xff08;一&#xff09;C普通类的包含编译模型1、创建普通类定义文件2、创建普通类实现文件3、创建主程序文件4、运行主程序&#xff0c;查看结果 &#xff08;二&#xff09;C模板类的包含编译模型1、创建模板类定义文件2、创建模板类实…

微前端框架篇一,了解qiankun

微前端框架篇一&#xff0c;了解qiankun ① 前置知识补充Ⅰ 什么是微前端&#xff1f;Ⅱ 什么是JS CSS沙箱&#xff1f;Ⅲ 什么是spa单页面应用&#xff1f;Ⅳ SystemJS 与 import-html-entryⅤ 现有的微前端方案 ② 了解single-spa 微前端框架③ 了解qiankun框架 ① 前置知识补…

[超级详细系列]ubuntu22.04配置深度学习环境(显卡驱动+CUDA+cuDNN+Pytorch)--[1]安装显卡驱动

[写在前面] &#x1f447;&#x1f447;&#x1f447; 如果这篇博客写的还可以的话&#xff0c;希望各位好心的读者朋友们到最下面点击关注一下Franpper的公众号&#xff0c;或者也可以直接通过名字搜索&#xff1a;Franpper的知识铺。快要过年了&#xff0c;Franpper想制作一…

腾讯云代金券如何领取?详细领取教程来了!

随着云计算的快速发展&#xff0c;越来越多的用户意识到云服务的重要性。腾讯云作为国内领先的云服务提供商&#xff0c;为广大用户提供了丰富的云计算解决方案。为了吸引用户上云&#xff0c;腾讯云推出了代金券活动&#xff0c;让用户在购买云服务时可以享受到更多的优惠。 那…

【Linux】Linux基本操作(二):rm rmdir man cp mv cat echo

承接上文&#xff1a; 【【Linux】Linux基本操作&#xff08;一&#xff09;&#xff1a;初识操作系统、ls、cd、touch、mkdir、pwd 】 目录 1.rmdir指令 && rm 指令&#xff1a; rmdir -p #当子目录被删除后如果父目录也变成空目录的话&#xff0c;就连带父目录一…