事情的起因是在大概 2 周前看到了 v2ex 上的一篇文章《我去,发现个好玩的,现在病毒木马都开始开源提示词了?》。当时我就进行了一轮测试,感受还挺复杂,本来准备马上写两篇测评,结果被各种事情拖到现在。
简单说就是:木马本身没开源,开源的是提示词。开源提示词这事在今年年初已经有苗头了,但现在连木马都开始这么干,确实有点蔓延的趋势。本篇文章基于其中 Windows 版 Rootkit 提示词做实际测评和分析,下一篇会继续分析 Linux 版 Rootkit 提示词。

这份提示词相当工程化。从环境搭建到完成项目最初版本,前后大概花了 3 个多小时。通过投喂 AI 提示词,确实生成了一个内核版本的 Rootkit,能够伪装进程、隐藏文件、服务、注册表和网络连接,甚至会对内核文件自身进行伪装,功能齐全而且强度不低。但坑点同样存在,至少我的测试机就蓝屏了。
源码分析
样本功能
Windows 版提示词文件为 azure-wdm-agent-prompt.md,原始文档是英文的,我英语一般,先翻译成了中文。文档里并没有直接指定 AI 生成的内核项目名称,所以项目名由 AI 随机发挥。项目能力和实现原理大致如下:
| 能力 |
模块 |
实现手段 |
| 进程 PID 伪装 |
PidSpoof.c |
直接改写 EPROCESS.UniqueProcessId 为 4 |
| TCP 连接隐藏 |
NetHide.c |
Hook nsiproxy 的 IRP_MJ_DEVICE_CONTROL,在完成例程里压缩 TCP 表 |
| 文件/目录隐藏 |
PathHide.c |
MiniFilter:CREATE 返回 NOT_FOUND,目录枚举就地压缩 |
| 注册表键隐藏 |
RegHide.c |
CmRegisterCallbackEx:PreOpen 返回 NOT_FOUND,PostEnumerate 跳过子键 |
| 驱动镜像写回 |
WriteBack.c |
加载时把 .sys 缓存进非分页池,关机/休眠时写回原路径 |
| 驱动对象伪装 |
DriverObjectSpoof.c |
克隆 \Driver\Null 的 _DRIVER_OBJECT 元数据 + LDR 节点 |
先看 DriverEntry,代码很少,逻辑也很清晰。

进程伪装
这个样本并不是直接隐藏进程,而是通过修改内核 EPROCESS 的 UniqueProcessId 字段实现进程伪装。在本次测试中,被伪装进程的 PID 始终指向系统进程 PID=4。
它首先硬编码了各个 Windows 版本下 UniqueProcessId 字段在 EPROCESS 中的偏移,如果系统版本无法确定,就统一按 Win7 处理。老实说,这种写法也就是 AI 干得出来,正常人不太敢这么写,否则可能早就被公司开了。

下面这段是实际修改 EPROCESS 实现伪装的逻辑:

网络隐藏
这部分在我看来难度较大,核心是 Hook nsiproxy 的 IRP_MJ_DEVICE_CONTROL,而 NSI 这些结构本身是未公开的。
安装 Hook 时使用 ObReferenceObjectByName + *IoDriverObjectType 按名称拿到驱动对象,然后原子替换 MajorFunction[IRP_MJ_DEVICE_CONTROL]。保存旧指针是为了卸载时恢复;用 InterlockedExchangePointer 而不是直接赋值,说明作者已经考虑过“可能有其他 Hook 也在改这个槽位”的情况。

之后注入完成例程,完成例程的关键代码如下:

随后进行表压缩。TCP 表、状态表、PID 表是三个平行数组,删除一行时三张表必须同时压缩,否则 PID 会错位到别的连接上。NhRemoveRow 使用 RtlMoveMemory 把后续行整体前移。pidValid 的设计也很谨慎:当 PID 表缺失时,ByPid 规则一律不匹配,否则一张不存在的表会退化成“人人命中”,最终把整张连接表清空。
文件/目录隐藏
这部分没有什么特别复杂的地方,主要通过 MiniFilter 进行过滤。
服务/注册表隐藏
通过注册表回调实现注册表隐藏。由于系统服务同样存在于注册表中,所以也能同时实现对服务的隐藏。
配置里允许写人类习惯的 HKLM\...,但回调里看到的却是 \Registry\Machine\...。作者把映射关系做成一张表,而不是 if/else 链,这样扩展成本很低。转换函数还处理了两个边界情况。

注册表回调 API 没有提供“跳过这一项”的便利返回,因此跳过隐藏子键只能重写枚举结果。做法是:拿到当前项索引,从 Index + 1 开始自己调用 ZwEnumerateKey,并使用重入保护避免递归,找到第一个不隐藏的项后,再把它复制进调用者缓冲并修正 ResultLength。

关机回写
关机回写的前提是,需要先把驱动文件本身复制一份到内核内存,文件路径从 DriverObject->DriverSection 指向的 LDR 节点里获取。把它深拷贝成 NUL 结尾的独立缓冲是必要的,因为 UNICODE_STRING 本身不保证 NUL 结尾,而后面 ZwCreateFile 需要使用它。

写入过程中使用 InterlockedCompareExchange 确保同步。关机路径和电源回调可能同时触发,必须防止两个线程同时用 FILE_OVERWRITE_IF 打开同一个文件。

至此,源码原理分析基本结束。虽然分析看起来并不复杂,但实际代码量接近 5000 行,对个人项目来说已经算不小了。整体技术路线中规中矩,基本都是公开技术,但把它们组合起来,就形成了一个全方位隐匿的高对抗性样本。
如何检测
我在测试机里做了实验,尝试了国内 3 款杀毒软件,基本都没有明显反应,于是转向手工检测。
用 GMER 进行 Rootkit 检测,默认检测行为下完全没有发现。

比较明显的异常是:样本会把目标进程伪装成系统进程 PID=4,所以通过 Process Hacker 可以看到系统中出现了两个 System 进程。

但从属性上无法直接区分真假。

通过 XueTr,也就是 PCHunter,可以看到恶意进程。

对于内核检测来说,由于该样本会固定把自身伪装成系统的 NULL.sys,所以可以通过 PCHunter 等 ARK 工具检测到异常。伪装终究是伪装,不可能面面俱到。比如驱动对象和服务名为空,这对系统服务来说并不正常。

也可以通过按文件名排序,发现异常的内核驱动。

提示词中的坑
其实坑点很多,感兴趣的可以自行测试。我这边的蓝屏原因是文档里的 LDR 节点伪装,也就是驱动伪装,与 PatchGuard 直接冲突导致。所以不要指望这份提示词可以一键生成完全符合预期的东西。
整体来说,这组 Rootkit 提示词揭示了一个值得关注的现象:攻击者开始把工程化经验沉淀进提示词,让 AI 参与恶意驱动开发。就样本本身看,它依赖的大多是公开技术,但组合后的隐蔽性已经不可忽视。对检测侧来说,单纯依赖杀软默认策略可能不够,恶意代码分析与 ARK 工具的手工核查仍然很有必要。