# 恶意弹窗技术实现拆解

1 阅读5分钟

一、注入与寄生手段

1.1 全局消息钩子

本质是通过 Windows 提供的 SetWindowsHookEx 机制安装系统级消息钩子,最常用的是 WH_GETMESSAGEWH_CALLWNDPROC。一旦钩子挂上,操作系统会自动把对应的 DLL 注入到所有拥有消息循环的 GUI 进程中。对于恶意弹窗来说,这意味着不需要自己独立创建进程和窗口,而是寄生在 Explorer、浏览器、办公软件等正常进程的地址空间里弹窗口,溯源时很难定位真正的源头程序。优点是注入覆盖面极广、几乎所有桌面进程都能渗透;缺点是全局钩子行为特征明显,很容易被主流安全软件捕获。

1.2 系统消息循环插队注入

这是一种比全局钩子更轻量的注入思路:不挂钩子,而是直接向目标线程的消息队列 "塞消息" 。典型做法是通过 PostThreadMessage 把自定义消息投递到目标进程的主线程消息队列,再配合 SetWindowsHookEx 局部钩子或 APC 机制,让目标线程在处理消息时 "顺道" 执行我们的弹窗代码。相比全局钩子,它不需要给所有进程都装 DLL,只针对特定目标进程动手,行为更隐蔽,安全软件的误报率也更低。在弹窗场景中,最终弹出的窗口隶属于正常软件的进程,用户会误以为是软件本身弹的广告。

1.3 窗口句柄寄生主线程

核心思路是不自己创建完整窗体,而是 "挂靠" 在已有窗口上。常见手段有两种:一是用 AttachThreadInput 把自己的线程输入队列和目标窗口线程绑定,共享消息循环;二是用 SetWindowLongPtr 对目标窗口做子类化(Subclass) ,替换掉原窗口的窗口过程函数,让所有窗口消息先流经我们的自定义代码。这种方式的好处是:弹窗的宿主是系统正常窗口(比如桌面、任务栏、资源管理器),没有独立的进程和窗体特征,用户用任务管理器找不到弹窗进程,用 Spy++ 查窗口归属也只会看到正常程序。

二、进程存活与自保

2.1 三互斥进程心跳保活

这是恶意软件最经典的多进程守护模型。同时启动三个独立的进程实例,每个进程都创建一个全局命名互斥体(Mutex)作为 "心跳标记",三个进程之间周期性地检测另外两个互斥体是否仍然存在。一旦发现其中一个互斥体消失(说明对应进程被用户或安全软件杀掉了),剩下的两个进程会立刻重新拉起一个新的实例补上。对普通用户来说,直观感受就是 "弹窗关不完、进程杀不掉"—— 刚结束一个,另外两个马上又生成新的,三个进程形成闭环互相兜底,手动清理几乎不可能全部杀干净。

2.2 临界区锁与双实例fork守护

在单实例运行的基础上增加了被杀分裂的机制。正常运行时通过临界区(Critical Section)或命名互斥锁保证只有一个主实例在工作,避免重复弹窗浪费资源;但一旦检测到异常终止信号(比如自己的句柄被关闭、临界区被强制释放、收到结束消息),会在退出前立刻 fork(创建)出两个全新的子进程接力运行。这种 "杀一个冒两个" 的设计专门对抗手动结束进程:用户在任务管理器里点一次结束任务,反而会让进程数量变多,越杀越难清理。

2.3 类名动态哈希混淆

窗口类名(Window Class Name)是安全软件识别恶意弹窗的重要特征指纹。普通弹窗程序往往写死一个固定的类名注册窗口,比如 PopupAdWnd 之类,安全软件只要匹配类名就能直接拦截。动态哈希混淆的做法是:程序每次启动时,用随机种子 + 时间戳生成一段哈希字符串,用这个随机字符串作为窗口类名去注册窗体。结果就是每次弹窗的窗口类名都不一样,没有固定特征,基于特征码的查杀规则直接失效,无论是安全软件还是人工用 Spy++ 排查,都很难靠类名定位到恶意窗口。

2.4 绕开标准窗体创建链路

常规的弹窗都走 CreateWindowEx 这条标准 API 路径,而这条链路上有大量安全软件的监控钩子和回调检测点。绕开标准链路的思路就是跳过 Win32 上层 API,直接走更底层的窗体创建入口—— 比如直接调用 NtUserCreateWindowEx 这类未文档化的原生 API,或者通过内核态回调、win32k.sys 直接交互来创建窗口。由于跳过了上层的检测节点,大部分主动防御软件感知不到窗口创建的行为,等窗口弹出来的时候才后知后觉。这种手段技术门槛更高,但对抗 EDR 类安全产品的效果也更显著。