ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

APT组织用Rust重写PowerShell后门:攻击链演变与防御

APT组织用Rust重写PowerShell后门:攻击链演变与防御

最近圈里都在聊一件事:一个老牌APT组织,把自家用了很多年的PowerShell后门整个重写了一遍,新版本直接用Rust编译,江湖代号也跟着更新了一代。翻看时间线,这个流程非常典型——先是用PowerShell做前期试探和载荷投放,等被各家EDR盯上以后,立刻切到Rust重写核心木马,投递的样本也从几百KB的脚本变成几MB的编译产物。防御方这边最头疼的其实不是"Rust比PowerShell难分析",而是整个检测链条里,凡是基于"脚本行为"建立的规则全部失效了。这篇文章我把整个攻击链的演变逻辑、Rust版木马的核心机制、以及我们做检测和应急时真正管用的几个方向,完整拆开讲一遍。

先说清楚:任何语言本身都没有正邪之分。Rust在系统编程、基础设施工具里是顶级的可靠性选择,但当同一门语言落到恶意软件作者手里,它的内存安全、跨平台编译、以及相对干净的系统调用封装,就成了天然的免杀与混淆利器。这对攻击者是效率提升,对防守方就是实打实的新威胁面。

1. 为什么攻击者要从PowerShell转向Rust:一次系统性的语言迁移

1.1 PowerShell攻击链的局限性在哪里

任何一个长期跟踪APT样本的人都会注意到,PowerShell在攻击链里的地位非常特殊。它几乎不需要落地文件,直接通过内存加载就能执行,配合Cobalt Strike这类框架,可以在内存里完成注入、凭证窃取、横向移动一整套动作。前几年的主流攻击链几乎都是这个套路:鱼叉邮件带一个带有宏的文档,宏拉起PowerShell,随后下载后续Payload。

但PowerShell的便利性同时也是它的死穴。脚本语言的每一次执行都经过系统级脚本引擎,EDR完全可以挂钩关键函数、记录命令行参数、分析脚本块内容。到后来,很多终端安全产品对PowerShell的监控粒度已经细到"只要出现编码过的命令就开始告警"。攻击者很快发现,PowerShell脚本的存活周期越来越短:一个加载器可能三天内就被提取出IOC,整个基础设施被迫跟着换。

真正的转折点出现在两个地方。一是脚本引擎被全面监控后,基于解释型语言的后门几乎无法隐藏行为,行为分析引擎可以轻松识别"进程从网络下载内容并立即执行"这种经典链条。二是主流EDR对PowerShell的脚本块日志、AMSI(反恶意软件扫描接口)都有非常成熟的绕过检测策略,所以攻击者需要一种"不经过脚本引擎"的实现方式——编译型语言是唯一合理的选择。

1.2 Rust在攻击工具开发中的技术优势

Rust在这个时间点被攻击者选中,一点都不意外。先说内存安全。传统C/C++恶意软件最怕什么?怕崩溃、怕内存破坏导致的行为异常,一旦异常就会暴露行踪。Rust的所有权机制在编译期就排除了大量内存错误,木马可以在目标机器上长时间稳定运行,这一点对于需要持久驻留的后门来说价值极高。

再说跨平台。APT组织最烦的就是维护多套代码库:Windows一套C++、Linux一套Shell、macOS再来一套Objective-C。Rust用一套代码就能交叉编译到三大平台,标准库层面就提供了统一的网络、文件、进程操作接口。你在Windows上用std::process::Command执行命令,在Linux上几乎同等写法,连参数都不用怎么改。对于攻击者来说,这就意味着"一次编写,到处投放",运营成本直接断崖式下降。

还有一个经常被低估的点:编译产物特征。Rust和Go一样是静态编译,不需要目标机器预装运行时,但Rust的二进制文件没有Go那种非常明显的runtime.main、golang.org字符串特征。经验不够的分析师拿到一个Rust写的后门,第一眼甚至很难判断它的编译来源。我见过有人把Rust样本当成C++写的,在错误的方向上分析半天。而这种"特征不确定性"本身就是对防御方的消耗。

1.3 这次语言迁移传递了什么信号

要说清这次升级的意义,得先明白一个事实:这不是个别工具的重写,而是整个攻击链基础设施的迁移。从最初的侦察、载荷投递,到最终的后门通信,攻击者在重新评估每一个环节的语言选型。用脚本来试探、用编译型语言来做持久化、用加密流量来做通信——"每层用不同技术栈"的模式正在成为常态。

更值得警惕的是供应链层面的变化。一旦一个组织把核心木马重写成Rust,它的插件体系、加载框架、加密通信模块都会跟着换。你之前基于PowerShell版本整理的检测规则、IOC、内存特征,全部作废。如果只盯着旧样本做被动防御,等新样本大规模投放时,蓝队几乎是从零开始。这也是我在给团队做威胁建模时常说的一句话:不要只追样本,要追攻击者的技术决策路径。语言换没换、工具链换没换、混淆手段换没换,才是预判下一步动作的关键。

2. RustyWater木马的技术轮廓与核心机制

2.1 整体攻击链的组成分析

从目前能观察到的情况看,RustyWater的投放链路主要分三个阶段。初始访问阶段依然是鱼叉邮件,但附件已经从"带宏的Office文档"换成了"经过多层压缩的ISO镜像文件"——这种方式的好处是绕过网关的脚本检测,而且ISO文件可以设置只读属性,恶意内容不容易被安全措施自动清除。

第二阶段是载荷分离与落地。ISO解压后是一个经过数字签名(签名的有效性就不好说了)的可执行文件,它的任务是解码出内嵌的Rust后门并释放到指定目录。这个阶段有个细节很值得注意:释放后的路径往往伪装成公共库或者临时目录下的隐藏文件夹,文件名也尽量贴近系统进程命名习惯,比如看起来像是wmpnetwk.exe或consent.exe这种名字的变体。对于粗心的分析师来说,光看进程名称基本分辨不出来。

第三阶段就是Rust后门与C2服务器建立通信。通信协议不是简单的HTTP请求,而是伪装成正常业务流量的HTTPS请求,部分样本还会使用域前置技术,通过CDN节点转发流量,直接隐藏真实C2地址。邮件网关、流量审计设备如果只看域名白名单,根本发现不了异常。

整体看下来,这条攻击链的设计逻辑非常清晰:每一层都避免碰触发概率最高的检测点。脚本检测被绕过了,宏行为没有了,PowerShell命令行也消失了,留给防御方的有效攻击面急剧缩小。

2.2 主要功能模块的实现思路

RustyWater的核心功能没有跳出典型远控的范畴,但它的实现方式有几个值得展开的技术点。

命令执行模块,在Rust里的实现路径基本是这样的:C2下发命令字符串,后门通过std::process::Command创建子进程执行,并将输出重定向到管道返回。表面上看和PowerShell启动进程没有区别,但Rust版本不会在进程创建时触发脚本引擎的扫描钩子,EDR想要通过命令行特征抓它,就必须依赖更底层的进程监控才能做到。

文件管理模块,包括文件上传、下载、删除、枚举目录。这部分Rust生态里有walkdir这类非常成熟的目录遍历库,攻击者不用自己造轮子,扫描目标文件的效率比手搓C++高得多。另外在处理大文件传输时,Rust的异步I/O特性可以让上传不阻塞主逻辑运行,这在实际攻防中意味着"后门在传输数据时依然保持高度响应"。

持久化模块,比较常见的做法是写入注册表Run键,或者创建计划任务。Rust里调用Windows API一般走windows系列crate,写注册表的代码跟C++差别不大,但因为是纯Rust编译,不会出现PowerShell脚本里那种明显的base64编码痕迹。实际样本中还发现过通过WMI事件订阅来做持久化的变种,这样在事件日志里留下的痕迹比普通计划任务更难排查。

屏幕截图与键盘记录则是按需加载的插件式模块。攻击者在C2面板里可以动态下发插件,后门只保留基础功能,大幅降低初始样本被静态扫描时的可疑程度。这种"瘦客户端+胖插件"的设计在近年的APT工具里越来越流行,因为核心样本体积小、交互少,沙箱分析很难在短时间内逼出完整行为。

2.3 跨平台编译与混淆处理的实现特征

咱们把技术细节再说得深入一点。RustyWater这类跨平台木马在编译时通常会做两件事:目标平台静态编译参数和启动时的反分析逻辑。

交叉编译本身不难,关键在依赖处理。用cargo build --release --target x86_64-pc-windows-msvc就能出一版Windows样本,换target参数再出一版Linux的。但攻击者一般会调整编译参数,去掉符号信息、设置panic=abort、开启代码优化,让逆向工程师在IDA里看到的是面目全非的控制流。

反分析这块有几个惯用手法。最常见的是虚拟机检测和调试器检测:通过读取CPU指令开销、检查特定系统调用的返回行为、或者在代码里插入多态混淆层。Rust本身的一些特性也为混淆提供了便利,比如match表达式的编译结果会生成密集的分支跳转表,控制流图比C++复杂不少;再比如enum的tag匹配逻辑,静态分析时经常让人绕晕。

我见过一个样本还做过更绝的事:把C2地址用一堆无意义的字符串经过Base64和XOR的多层编码封装,真正使用时才还原。配合Rust编译出来的冗余代码,人工从二进制里提取IOC的难度直接上一个台阶。倒不是说这些手段本身有多高明,但组合在一起,确实会显著拉长蓝队的分析时间。攻防对抗的本质是时间成本博弈,攻击者花一周时间提高混淆强度,防守方可能就要花三周才能提取出第一时间可用的检测特征。

3. 针对RustyWater的检测与排查路线

3.1 静态特征与YARA规则编写思路

静态检测这块,我认为最核心的思路是必须先建立"Rust二进制基线"。Rust编译出来的程序跟C++、Go有明显差异,你能不能在拿到样本的第一时间识别出它是Rust写的,很大程度上决定了后续分析的效率。

首先看节区结构。Rust的静态编译产物通常会包含.init_array、.fini_array、.got等节区,跟C++的PE很像,但很多Rust程序在Release模式下会启用LTO和codegen-units=1,导致节区数量和排列分布与常见C++项目不太一样。用die或者blob这类工具扫描时可以重点关注。

其次看字符串。Rust程序的标准库代码路径里会残留一些标志性字符串,比如.rustc、std::rt、lang_start之类的符号引用。尽管攻击者会做strip处理,但标准库里的不少内部符号仍然暴露。实际编写YARA规则时可以围绕rust_eh_personality、_ZN4core这种mangled symbol前缀做匹配,因为即使是strip过的样本,这些符号也通常不会被完全抹掉。

还有依赖库指纹。Rust生态里常用的reqwestHTTP库、serde序列化库、windows-rs系统调用封装,都会在二进制中留下独特的字符串或导入表特征。做样本聚类时,这些指纹比文件名有用得多。你写检测规则的时候,与其死磕某一行为特征,不如先给"Rust门"做一个宽泛识别,再结合具体样本的行为特征去收敛。

3.2 动态行为监控的关键点

静态做完了就轮到动态行为。Rust木马的进程行为跟传统远控相比,有几个地方值得针对性布防。

网络通信的长时间规律性是最典型的信号。RustyWater这类成熟的远控为了躲避流量层面的检测,C2通信通常采用低频长连接的方式,心跳间隔可能是60秒、120秒,甚至更长。正常业务流量不会每小时固定打一次同一个外部域名,而远控会。流量分析系统里对这个特征做基线建模,比抓单包更有效。

进程创建链的异常。Rust后门释放出来的子进程一般不会通过常规Office链路启动,它常常由svchost.exe或explorer.exe这种系统进程间接拉起。监控时看父进程与子进程的"信任关系链"很有意义。举个例子:如果wscript.exe或msiexec.exe突然创建了一个位于临时目录的可执行文件并让它外联互联网,命中率相当高。

内存扫描也要跟上。很多团队只看磁盘文件扫描,忽略了内存里藏木马的情况。Rust后门本身有反射加载能力,可能通过进程注入常驻到合法程序中。做内存取证时,利用Volatility的modscan或apihooks功能可以快速识别异常的模块注入。我在此想强调一点:内存态的检测往往比文件态检测更能抓住编译型恶意软件,因为文件落地可能被各种打包器改头换面,但内存里的执行特征很难伪装。

3.3 一次典型的现场排查记录

说一次我在模拟攻防场景里处理过类似情况的过程,方便大家对照。

某个工作日,内部系统提示一台业务服务器反复向陌生域名发起HTTPS请求,每次间隔都在一分钟左右。从网络层看,流量体量不大,域名也和正常业务没有关系。第一反应是检查该进程的启动方式。结果发现父进程是计划任务,任务文件创建时间正好是两周之前,当时正好有一次部门级软件升级,没人注意到这个文件。

落地文件叫system_arch.exe,仔细看文件信息,没有版本信息,也没有签名。用工具查节区,看到.init_array和.rustc相关的符号残留,基本确定是Rust编译产物。接着动态跑一遍,发现请求的User-Agent伪装成Chrome浏览器的版本号,请求头里塞着一个固定的Cookie值,这就是常用的C2回连特征。

后续处理分两步:一边断开外联、隔离主机,一边提取内存镜像。内存镜像里挖出了后续的几个插件模块,包括键盘记录和屏幕截图。整个排查过程最有价值的一点在于:攻击者没有使用任何高危行为,全靠低频通信和长期驻留过日子,要不是流量基线建模敏感,这台机器可能被控制很久都不会被发觉。

4. 从RustyWater看蓝队建设的几个短板

4.1 对编译型恶意软件的关注度不足

说实话,不少蓝队在这轮语言迁移面前是准备不足的。长期以来,大家的检测重心都放在脚本类恶意软件上——毕竟是PowerShell和VBA统治了快十年。但脚本类恶意软件有一个隐形红利:它必须有对应的解释器运行环境,而这个环境本身就是天然的监控点。

编译型恶意软件把这个问题全部带走了。没有脚本引擎、没有宏、没有解释器,只有裸的进程、裸的内存。如果你的检测策略还停留在"扫描脚本内容""监控PowerShell命令行""对宏行为做沙箱分析",那等于在赛道上只修了一条车道,而攻击者已经开上另一条路跑了。

我一直认为,蓝队需要建立"语言无关"的检测思维。不要问"这是一个Rust木马还是C++木马",要问"这个进程的完整行为链是什么"。它创建了哪些子进程、访问了哪些可疑路径、外联了哪些从未出现过的域名、有没有在内核对象上做手脚。行为链是语言无关的,而行为建模才是长期有效的方向。

4.2 检测规则易失效的原因与更新策略

再聊一个更现实的问题:为什么很多检测规则定期失效?答案是规则写得太"贴样本"。早期的YARA规则喜欢匹配PowerShell里特定的编码参数、匹配Cobalt Strike的默认端口和路径,这些东西随着攻击者换版本就全部失效。RustyWater这一代样本在投递时甚至故意往自己的代码片段里插入了大量诱饵字符串,比如伪造的版本信息、伪造的版权标签,专治"想提取IOC的人"。

我个人的经验是,检测规则应该往"结构性特征"和"行为性特征"两个方向写。结构性特征指的是Rust编译产物的节区布局、导入依赖组合、加密流量载荷中的固定字段偏移,这些比具体字符串稳定得多。行为性特征则包括固定间隔的外联、子进程创建的奇异度、进程权限提升的路径等。规则要能跨样本存活,必须活在结构层和行为层,而不是活在文件名的巧合里。

另外,规则的更新节奏也要变。攻击者每次小版本迭代都可能改变字符串和混淆方式,但核心逻辑不会大变。建议每季度做一次"规则压力测试":拿最新捕获的样本回放自己的检测规则,看看哪些命中、哪些漏报、哪些误报,然后专门针对漏报部分做增量优化。规则的数量不重要,重要的是存活率。

4.3 面向未来攻击链的防御思路调整

从RustyWater这个案例能推导出一个更通用的结论:未来的攻击工具语言会越来越"系统化"。Go、Rust、Nim这类编译型语言会逐步取代脚本型语言在攻击链中的位置。攻击者下一步可能还会引入更复杂的手段,比如利用开源社区库做供应链伪装、利用可信签名的第三方组件加载恶意代码。

防御层面的应对,我认为有三个方向优先级最高。第一是端点侧的行为基线建设,把每台主机的正常行为谱系搞清楚,任何偏离基线的行为——不管是哪个语言写的——都能被发现。第二是网络侧的加密流量分析,HTTPS让流量内容不可见,但流量行为、证书指纹、JA3/JA4指纹仍然可见且难以伪造。第三是威胁情报与样本狩猎的联动,样本库不只是存放IOC的仓库,还要能主动做相似性聚类,发现同一组织的不同语言版本工具,提前建立"组织级指纹"。

5. 一些实操中的体会和建议

5.1 快速验证团队检测盲区的自测方法

讲到这里,我猜很多团队已经想检查自己的防御盲区了。我提供一个低成本的自测方法,不涉及敏感操作,谁都能做:用Rust写一个最简单的后门骨架,功能包括定期发起HTTP请求到一个可控域名、间隔采集主机名和用户名、定期写一个临时文件。这个程序不做什么恶意行为,单纯模拟RustyWater的最小行为集。

然后把这个程序放到自己的测试环境里,跑24小时,看看EDR和流量审计系统能不能发现。如果完全没有告警,说明你的检测体系对Rust家族的攻击基本是透明的。如果告警是因为"未知进程请求外联"这种粗粒度规则,也说明规则的有效性与精准度还远远不够。这类自测最好每个季度做一次版本更新,随着你的模拟工具越来越接近真实样本,检测体系的验证效果也会越来越好。

5.2 应急响应时的处置优先级

真遇到Rust后门事件,处置顺序其实和传统远控差异不大,但有几个细节必须提醒。第一要务是保留内存镜像再断网。很多人习惯先断网再分析,但Rust远控的插件模块全部在内存里,一旦断网或进程被终止,关键证据就没了。先做内存dump、再拍进程快照、最后再隔离网络,这个顺序不能乱。

第二要务是排查持久化时不要只盯注册表。RustyWater这类工具的持久化已经进化到WMI事件订阅、计划任务伪装、服务自启动等多种方式齐上。光扫一个Run键远远不够,最好用危害指标框架把启动项、服务、计划任务、WMI绑定全部过一遍。

第三要务是全盘搜索Rust编译产物的兄弟样本。攻击者往往不只投一个样本,发现一个后门后,在同一台机器上、同一段时间戳里,很可能还有未激活的备用样本。用前面提到的节区指纹和字符串指纹对整个磁盘做一次扫描,可以避免抓了一个、漏了一窝的尴尬。

5.3 威胁情报建设的一个长期方向

最后说点偏战略层面的想法。威胁情报不能只停留在"哪个IP、哪个域名、哪个文件哈希"这个层面,这些信息更新快、失效更快。我越来越倾向以"工具族谱"为单位的情报整理方式:同一个组织,从最早的VBS脚本到PowerShell再到现在的Rust版本,它们的演进路线、共享代码片段、通信协议的延续性,这些才是真正有长期价值的资产。

每当你看到一个组织换了新语言、新框架,不要只把它当成单个事件分析。把它放进工具族谱里去看,就能发现很多隐藏线索:C2端口虽然换了,但通信协议里的字段顺序没变;混淆方式变了,但解密密钥的生成逻辑还是老一套。这些线索对追踪和预测攻击行为的价值,远超十个IP黑名单。

用我自己的话说:攻击者换的是壳,但攻击者的认知模式、工程组织方式不会轻易变。你只要把"壳"下的东西看清楚,不管它换成PowerShell、换成C#、还是换成Rust,你都有机会在它真正动手之前摆好阵型。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进