本文根据 tmp.0ut 第五期中 r3s1stanc3 的文章编译整理,非逐字翻译;文中观点均属于原作者。

原文:Self-Extraction Using Reachability

文章内容来自 tmpout.sh,本译文依原站 CC BY-NC-SA 4.0 许可作为改编内容发布。如有侵权请通知,我会下架文章。 全文字符数:2983

0. 引言

距离作者打磨 HandJar、写 ClassWar 已经过了一段时间。这两个病毒的工作方式相似:生成 Java 源码并编译来创建入口点,依赖 Java 编译器工具链的存在。之前的感染方式要么是把文件复制进宿主文件、用 MANIFEST.MFMain-Class 属性挂钩入口点,要么包装现有类、用反射调用旧类。作者一直懒得在 JVM 字节码层面做事:理解底层文件格式、解析修改代码、跟踪更新偏移量,工作量太大。虽然别人做过,但他从未尝试——可以说他在等一个高层 API 来做底层字节码操作。

Java 团队这次真的交付了:Java 24 在标准库里引入新 API——Class-File API,可以读、改、写 Java class 文件,而无需操心 class 文件格式的底层细节。本文探索它带来的可能性:

  • 做可达性分析,从指定起点(例如病毒入口)动态提取所有被调用的方法;
  • 把这些方法搬进现有 Java class 文件;
  • 用字节码操作在 class 文件任意现有方法里注入对入口点的调用。

作者用新 API 实现了概念验证,作为 ClassWar 的逻辑下一步,取名 Revolution。

1. Class-File API

新 API 很强大、允许许多恶作剧,但写一个病毒目前只需关注几个类:

  • ClassModel:解析后的 class 文件;
  • MethodModel:方法元数据(名称、返回类型与参数);
  • CodeModelMethodModel 实际的字节码指令;
  • ClassTransform:修改 class 文件。

获取实例很简单:ClassFile.of().parse(content) 得到 ClassModelclassModel.methods() 访问类里所有方法,methodModel.code() 访问 CodeModel

2. 摇树

树摇(tree shaking)是一种死代码消除,用于发现并移除死代码(例如从未被调用的代码)。可以把思路反过来:从指定起点搜索时,只找出真正会被执行的代码——这就是简单漂亮的可达性分析。

知道病毒入口后,从那里出发沿调用图遍历,建立病毒执行所需的全部代码的依赖树。Java 允许从运行时动态取回当前正在执行的类与方法名,所以病毒不需要存入口点的名字,可以在入口执行时现场计算。

为简单起见,PoC 对代码写法做了一些限制(再多花些功夫可以把分析扩展到所有语言特性)。当前限制:

  • 只用静态方法(最好 private,这样被感染类的使用者看不到);
  • 不对 enum 做 switch(会产生特殊的字节码,作者没时间研究);
  • 不用方法引用(如 List.of(1).forEach(System.out::println)),原因同上;
  • 不用运行时反射;
  • 入口必须是零参数的 static void 方法(这并不是可达性分析的限制,但让“在感染类里调用入口”更容易)。

限制其实不大,任何 Java 代码都能改写成这种形式:不能用类携带状态,只能把状态当方法参数传来传去(例如放进 Map);对 enum 的 switch 可以改写成 if/else;方法引用可以改写成 lambda(上例即 i -> System.out.println(i))。

按这些限制组织代码后,从任意起点对调用的方法做广度优先搜索就很简单。Java 字节码只知道几种调用方法的方式:invokedynamicinvokeinterfaceinvokespecialinvokestaticinvokevirtual。分析这五种字节码指令即可构建调用树。Class-File API 让遍历方法 CodeModel 的指令、执行可达性分析变得容易:只看这些指令、检查调用了哪些方法,若方法不是标准库的就加入依赖树。代码甚至可以跨多个类,但那样方法就不能是 private

该可达性分析只在方法级、不在字节码级,因此方法内部的死代码不会被移除。

3. 搬家

知道依赖后要把代码搬进宿主文件:基本就是把所有 MethodModel 连同 CodeModel 复制进宿主文件。由于 Java 的 static 方法仍携带对封闭类的引用(如 MyClass#foo),需要把引用改成宿主类的名字。

这里还能实现一种非常基础的多态:既然本来就在调整所有引用,可以顺便随机重命名方法。好处有二:不会与类现有方法冲突(不断生成随机名直到找到未占用的);没有确定性的方法名可作为检测病毒的模式。代码本身仍含让检测变得微不足道的模式,但真正的代码变形留作练习——作者自嘲“或者留给我,但我交稿时间不够了”。

4. 搬过去了,但没运行

前面动态提取了病毒需要的全部逻辑并搬进宿主文件,但它从未被执行。Java 里已有人探索过几种入口技术:改 MANIFEST.MFMain-Class 指向病毒并从那里调用原入口(HandJar)、用反射包装类、注入 static 块调用病毒(ClassWar)。

既然已经在 class 文件字节码层面操作,这些技术就显得有点无聊和可预测。入口是零参数 static 方法,只需在感染类某处用 invokestatic。用 ClassTransform#transformingMethodBodies 从宿主类随机挑一个方法,注入 invokestatic 指令。作者决定在每条 *return 指令前都注入调用,这样无论执行哪个分支都会调用入口。CodeBuilder 也让这件事很容易。之后,只要原代码执行到那个被随机挑中的方法,病毒也会执行。

5. 为什么这很酷

现在可以不经手底层格式的琐碎细节(重写偏移、头字段)直接修改 JVM 字节码——这些都藏在友好易用的 API 后面。同时拥有一种自包含、完全独立的方法收集属于病毒的全部方法:不需要知道代码存在哪里,只需要入口点的类名与方法名,两者都可以从 Java 运行时动态取得;然后一路摇树直到只剩病毒相关方法,再搬到下一个目标。即使扩展病毒功能,它也能原样工作。

Revolution 为针对 Java 字节码的病毒提供了可扩展的基础,可用于探索新的 EPO 技术、拆分代码并把各部分注入不同类、实现多态(例如内联/外提方法、注入垃圾代码),甚至通过把某些结构改写成等价但不同的代码实现变形。Hh86 曾用异常表做 EPO,作者计划基于该框架探索它与其他 EPO 技术。

这套方法不只对 JAR 有效,对不在归档里的 class 文件同样有效。作者甚至完全不在乎被执行的宿主文件是什么——可以用 ClassLoader#getResourceAsStream 加载 JVM 实例里任何 class 文件的内容。他认为这是感染 JVM 目标的一种优雅而酷炫的新方式。

6. 致谢

作者感谢:SPTH(多年来的导师,一直让他对自复制代码保持兴趣);TMZ 与 tmp.0ut 全体(坚持定期发布 VX ezine、投入审稿时间、给他发表文章的机会);Peter Ferrie(对 HandJar.A 的犀利分析伤害了他脆弱的自尊,但也激励他多年后重新拾起这条路——还看到 Ferrie 在担任本卷 Rootkit 竞赛评委);Java 团队(规范并实现 Class-File API,提供了一个 enterprise(TM) 级 API 来写酷炫的新 Java 感染器);以及所有在这条路上相遇、提出新点子、让工作不再无聊的人。

7. 参考

作者:r3s1stanc3(r3s1stanc3@riseup.net),2026-07。


上一页:phork:把 SHELF 重新打包成单一 ELF

下一页:RDOFF 病毒:重访一种冷门目标文件格式