XAML 当代码用:NetamLoader 的加载链拆解
原创
赛博57库
赛博57库
赛博57库
2026年10月30日 05:00
广东
去阅读
| 恶意软件 XAML 当代码用:NetamLoader 的加载链拆解 恶意软件 · 加载链 · AppDomainManager · XAML · AMSI 绕过 |
| 本文材料来自 Kaspersky GReAT 2026 年 10 月 29 日的报告《Unknown APT uses NetamLoader to attack a Win10 OS customized for governments in Asia》。原文按\"发现顺序\"写,这篇按\"攻击者怎么把东西送进去、怎么跑起来、跑起来之后干了什么\"重排,并给每一跳补上机制解释。 |
一、背景:一条告警牵出的链
事情的起点不是发现受害者,而是一条自动告警。
2026 年上半年,Kaspersky 的安全网络(KSN)在例行狩猎里报了一条:某个 .config 文件正在用 AppDomainManager 注入加载任意 .NET 程序集。这个文件被打包在一个 CAB包,放在 Windows 存放系统更新包的目录下:
C:\\Windows\\SoftwareDistribution\\Download\\e4537a1762fcafd2fd96bfc825bad34f9db9a24d |
注意这个文件名——40 位十六进制,看着像哈希。更新缓存目录里的包通常有可读的命名或 GUID,一串裸哈希本身就有点不合群。
拿到包之后的第一次分析几乎没结果。这不是分析失误,而是样本特性:CAB 里七个文件,六个是第三方补丁工具 Patch My PC 的正常组件,连被用来做注入的那个 .NET 程序集都是微软签名的。唯一的恶意文件是一个 .resx,而它当时被当成了普通资源文件。
后来重新逐一比对那个 .config 和完整的加载过程,才把链条挖出来。这条链被命名为 NetamLoader,目前没有关联到任何已知 APT。
这个背景值得单独说,因为它决定了两件事:第一,静态扫描看签名是没用的,被滥用的全是合法组件;第二,防守方的入口不在\"抓恶意 PE\",而在\"看谁在什么位置读了什么配置\"。
二、CAB 是怎么进来的
原文在这件事上非常克制,只有一句:
| We did not observe how the CAB file was delivered to the victim. |
也就是说投递环节没有观测到。这一点必须写清楚,不能替它补一个\"供应链攻击\"的结论。
已知的事实是这些:
| 事实 | 说明 |
CAB 落在 SoftwareDistribution\\Download\\ |
这是 Windows 更新(含 WSUS / 第三方补丁)的下载暂存目录 |
解包后落到 ...\\Download\\Install\\ |
同目录下的安装暂存位置 |
| 七个文件,只有一个恶意 | Resources.resx ;其余是 Patch My PC 的正常组件 |
用 PatchMyPC-ScriptRunner.exe 启动 |
参数是 -updatepackage -applicationid=<GUID> |
从这四条可以做一个推断(原文没有下这个结论):攻击者没有自己写加载器,而是借用了 Patch My PC 这条既有的第三方补丁执行链。这个工具本身就是用来把补丁包下载到 SoftwareDistribution、解到 Install、再由 ScriptRunner 跑起来的。恶意包只要混进这条流水线,剩下的动作全部是\"合法工具在做它本来就会做的事\"。
至于这个包是怎么混进去的——被篡改的补丁源、被劫持的更新通道、还是内部横向移动时手工投放——原文没有答案。这也是这条链最难防的一段:入口在更新系统里,而更新系统天生要执行外部内容。
三、执行入口:为什么选 ScriptRunner 当宿主
恶意包真正被激活,是这一行:
C:\\Windows\\SoftwareDistribution\\Download\\Install\\PatchMyPC-ScriptRunner.exe -updatepackage -applicationid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
选它当宿主有三个好处:
01 它有正当理由从更新目录里运行。 这个路径下的进程本来就在做更新,行为基线是\"正常\"的。
02 它会加载同目录的配置文件。 .NET 进程启动时会读 <exe名>.exe.config——攻击者只需要把这份配置换掉。
03 -applicationid=<GUID> 每次都不一样。 这个看起来无关紧要的参数,后面会变成解密密钥的来源。
第三条是这条链里最巧的设计,第四节会讲。
四、六跳加载链
从进程启动到插件跑起来,一共六跳。每一跳的产物都是下一跳的输入,磁盘上几乎没有中间可执行文件。
| 跳 | 载体 | 机制 | 产物 |
| 1 | PatchMyPC-ScriptRunner.exe.config |
AppDomainManager 注入 | 加载一个微软签名的程序集 |
| 2 | fabric.config |
配置类的类型实例化缺陷 | 实例化 ResXResourceSet |
| 3 | Resources.resx |
BinaryFormatter 反序列化 → TypeConfuseDelegate |
执行 XAML-1 |
| 4 | XAML-1 | 命令行哈希当密钥 | 解出 XAML-2 |
| 5 | XAML-2 | 分叉:绕 AMSI | 装 .NET 1 Loader | XAML-3 与插件就位 |
| 6 | XAML-3 | RPC + TypeConfuseDelegate 迁进程 |
插件在 dllhost.exe 里跑 |
第一跳:一个 .config 就能预设加载
CLR 启动进程时,会在 Main 之前读 <exe名>.exe.config 里的这两项:
<runtime> </runtime> |
PublicKeyToken=31bf3856ad364e35 是微软自己的公钥标记。也就是说,被指定实例化的是一个微软签名的程序集。签名校验在这里不但没拦住,反而成了掩护——出问题的不是它,是后面它去读了什么。
第二跳:把配置类当成加载器
DataCacheFactoryConfiguration 是 Windows Server AppFabric 缓存客户端的配置类。它被实例化时会去找同目录的 fabric.config,并对里面的 customType 做\"取类型 + 带参实例化\":
<section name=\"foo\"> <customType className=\"System.Resources.ResXResourceSet, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089(Resources.resx)\" /> </section> |
于是这个微软类会 Type.GetType 拿到 ResXResourceSet,用字符串 Resources.resx 当参数调它的构造函数。
这不是漏洞利用,是信任边界问题:一个配置类把\"类型名 + 构造参数\"从配置文件里读出来直接实例化,等于把类型选择权交给了文件的作者。
第三跳:.resx 里的反序列化
ResXResourceSet 解析 .resx 时会处理序列化对象字段。攻击者在 Resources.resx 里塞了一个 Base64 的二进制对象,走的是 BinaryFormatter.Deserialize()——这条路径上是经典的 TypeConfuseDelegate gadget:把委托链改写成\"先调 Process.Start、再调 XamlReader.Parse\"。
效果是:一个资源文件被解析,等价于执行了一段代码。
第四跳:命令行就是密钥
XAML-1 只做一件事——解出 XAML-2。但它同时也是反沙箱措施:
key = SHA256( lowercase( 当前进程完整命令行 ) ) blob = Base64Decode(内嵌串) iv = blob[0:16]; cipher = blob[16:] XAML-2 = AES_Decrypt(key, iv, cipher) XamlReader.Load(XAML-2) |
密钥不硬编码,而是从命令行算出来。补丁工具传的 -applicationid=<GUID> 每次不同,密钥就不同,载荷就解不开。
这正是第三节那个\"无关紧要的参数\"的用处:它既是正常业务参数,又是密钥种子。沙箱如果复现不了原始命令行,只能面对一堆解不开的密文。
解不开时的处理也很干脆:捕获异常后 Environment.Exit(0),进程静默退出,不弹错误框。
第五跳:XAML-2 分两路
XAML-2 里装着两段 Base64(各自 ZIP 压缩后再编码):
• XAML-2-1,796 行,只干一件事:绕 AMSI;
• XAML-2-2,装着 .NET 1 Loader 和一个 JSON 配置。
AMSI 这一段是全链技术含量最高的:
01 先判 IntPtr.Size == 8,不是 64 位进程就放弃——后面要按 x64 调用约定打补丁;
02 用 CLSID fdb00e52-a214-4aa1-8fba-4357bb0072ec 建出 AMSI 的 COM 对象;
03 找 Console.Clear() 的原生代码地址,把开头几个字节改成:
41 C7 00 01 00 00 00 ; mov dword ptr [r8], 1 33 C0 ; xor eax, eax C3 ; ret |
也就是\"往输出参数写 1,返回 0\"。在 AMSI 里 0 就是 AMSI_RESULT_CLEAN;
01 取 COM 对象的 IUnknown,读虚表指针,加 0x18 定位到 CAmsiAntimalware::Scan 的函数指针;
02 VirtualProtect 改成可写,把指针换成刚打过补丁的 Console.Clear() 地址,再恢复保护属性。
这一步的巧妙在于补丁打在合法模块里:改的是 Console.Clear() 的内存代码,AMSI 模块只被改了虚表项。两处都不碰文件,代码完整性校验看不到。
第六跳:换到 dllhost.exe
.NET 1 Loader 拿到 JSON 配置后,先做清扫(下一节细说),再验环境,然后用 RPC 建一个远程 COM 对象(CLSID ab8902b4-09ca-4bb6-b78d-a8f59079a8d5),借 TypeConfuseDelegate 把 XAML-3 送进去,落在 dllhost.exe 里执行。
为什么要换进程:dllhost.exe 是 COM 代理宿主,系统正常进程。载荷迁过去之后原始进程树断了,EDR 上看到的父子关系对不上恶意行为;再加上 Loader 已经把 COMPlus_ETWEnabled 置 0,进程内的 CLR 事件也没有了。
五、擦痕迹:这一步比加载更露信息
.NET 1 Loader 的第一件事不是连 C2,是打扫:
• 删掉更新记录,以及 CAB 解出来的三个文件(.config、fabric.config、Resources.resx);
• 删 CLR 使用日志 %LOCALAPPDATA%\\Microsoft\\CLR_v4.0\\UsageLogs\\PatchMyPC-ScriptRunner.exe.log;
• 删 Prefetch %WINDIR%\\Prefetch\\PATCHMYPC-SCRIPTRUNNER.EXE-*.pf;
• 遍历 %WINDIR%\\Logs\\CmitUpdateAgent\\20????\\20??????,把含 GetExtendedUpdateInfoHandler 的行过滤掉再写回;
• 清掉注册表里一套\"异常状态记录\"键。
最后两条最有情报价值。CmitUpdateAgent 这个目录名指向一套定制版 Windows 10 的更新代理——攻击者不但知道它存在,还知道要清理哪一行。而注册表那套状态记录更反常:它在 HKLM\\SOFTWARE\\Classes\\appid\\ 下按异常码建 GUID 子键,GUID 由 C2 地址推导,每个 C2 对应一组固定的九个键,异常码按二进制位决定建哪几个。用注册表当状态机、把状态藏在 CLSID 里,这种写法在普通恶意软件里见不到。
擦完之后才验环境:校验配置项(不一致就中止并上报)、检查当前用户是否管理员(不是就抛异常码 128)、用 SusClientId 拼 \\x14\\x43\\x9B\\x64 算出实例 ID。
六、插件:通信、代理与隔离域
XAML-3 在 dllhost.exe 里加载模板 __P1__(同一套 AMSI 绕过)和 XAML-P2,后者把 .NET 2 Plugin 解出来并调 m.C()。插件线程做四件事。
取指纹。 读 CLR 版本和系统 BuildLab,拼成 4.7.03190\
17763.rs5_release.180914-1434 这样的字符串——用来确认\"是不是那套定制系统\"。
保活。 调 CoLockObjectExternal 锁住一个托管对象,防止这个由 RPC 拉起来的 dllhost.exe 被系统回收。
超长休眠。 连 C2 前先随机等 20–40 分钟(沙箱等不了),起一个约 28 小时的看门狗(wi=100801 秒),到点就加载一段 Environment.Exit(0) 的 XAML 自杀。轮询间隔另一套:有响应 5–6 分钟一次,没响应 22–28 小时一次。
通信。 全流量 AES-128-CBC 加 HMAC-SHA256,Encrypt-then-MAC:
IV(16) || ciphertext || HMAC-SHA256(IV || ciphertext, mac_key) |
密钥从主密钥 ec 派生:以单字节种子生成 16 字节 HMAC 密钥,对主密钥做 HMAC-SHA256 得中间值,再用它 HMAC 常量 0x01,取前 16 字节。种子 0–3 分别派生出四个方向的加解密与认证密钥,客户端与服务端互为镜像。
请求形状固定:
https://openaigpt4v[.]com/model_v4/<实例ID>?b=&c=<si> |
证书校验被配置成 tp=\"*\",接受任意证书。上报结果另有独立配置和密钥。
代理发现三层回退:WinHttpGetDefaultProxyConfiguration() → 找 explorer.exe 并 Impersonate() 它的令牌取代理 → .NET 的 DefaultWebProxy。保证在企业代理环境里也能出网。
隔离域执行是最后一处技巧:从 C2 下载的 XAML 不在主域跑,而是新建随机命名的 AppDomain,复制配置过去,再用 MulticastDelegate 的手法改 _invocationList,把 TimeZoneInfo.ClearCachedData() 换成 XamlReader.Parse(),靠 DoCallBack() 触发。跑完卸载域,内存里不留。
七、基础设施、受害者与归因
| 项 | 值 |
| C2 域名 | openaigpt4v[.]com |
| IP | 104.18.14[.]219 (Cloudflare CDN,AS15169) |
| 首次见到 | 2023-09-27 |
| 受害者 | KSN 遥测里只有一台,位于亚洲;目标是政府专用定制 Windows 10 |
| 归因 | 无法关联任何已知 APT |
样本哈希(MD5):
F2458ED133F8D0CA19E22C10F577E98E CAB 包(文件名即 40 位十六进制) E1DC6F1ED193594F2284BE22AE08DFA4 Resources.resx AB81564BCB8CA7E710C66832DE747556 .NET 1 Loader CFB8B281BDF5D9390E64B10C312FB2C1 .NET 2 Plugin 67E3617F8709D34F806F8E02BADBA8A5 fabric.config 514AC0D73B8A1B73488EF5E2E55A75C3 PatchMyPC-ScriptRunner.exe.config |
报告把判断留在结论里:技术主权让越来越多国家自建操作系统,好处是全流程自主,代价是这套生态得自己守;成熟之前,系统和它新长出来的供应链都会成为入口。
八、防守侧能抓什么
按可落地程度排:
01 目录里的 .config。...\\SoftwareDistribution\\Download\\Install\\ 下出现非本机补丁工具产物的 .exe.config,且含 appDomainManagerType,直接高危。这个目录平时不该有手写的运行时配置。
02 异常命名的更新包。 该目录里出现 40 位十六进制之类的裸哈希文件名,且解包内容是\"正常工具 + 一个资源文件\"的组合,值得单独看。
03 appid 下的可疑键。HKLM\\SOFTWARE\\Classes\\appid\\ 里出现 DllSurrogate=\"\" 且不属于任何已装软件的子键——本例里 {ff259bcd-2f0a-44de-b34a-0092fec1d8a2} 是插件写的,用来把 COM 代理指向 dllhost.exe。
04 日志与 Prefetch 同时缺失。 CLR 使用日志和 Prefetch 本该成对出现,两者同时没有、而进程确实跑过,本身就是取证信号。
05 dllhost.exe 的行为。 它挂着装 C2 通信、且找不到对应的 COM 客户端进程,可以判异常。
06 CmitUpdateAgent 日志被整齐改写。 行数明显少于预期、或被过滤掉某一类行,值得单独比对。
07 出网形状。?b=<base64>&c=<guid> 这种两参数的 GET,参数值分别是密文和 GUID,形状很固定。
最后一点提醒:这套样本对沙箱的针对性很强——密钥绑命令行、初始 20–40 分钟抖动、28 小时看门狗。动态分析只跑几分钟,基本什么都看不到,得按它的时间尺度来,或者干脆先做静态。
| ⚠ 边界说明 ① 来源:所有数据来自一台自建隔离靶机(Windows 10 Pro / IIS 10 / .NET Framework 4.8)的实测,不是复述他人结论。② 载荷:只做「注册 + 返回固定字符串」,不含命令执行、不含加密信道,因此文中没有可直接投递的实现。③ 用途:检测与应急方法面向防守方,可自由取用;对未授权系统使用注入手法属违法行为。④ 数值:事件计数与文件哈希取自一次具体运行,换环境会有差异,方法可复用、数字不必照抄。 |
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论