搞分析的人迟早会撞上这么一堵墙明明拿到的是正经程序文件可一打开既不是熟悉的 ELF 也不是 PE而是 Java 的 class、.NET 的程序集或者浏览器里到处跑的 WebAssembly。进程起来逻辑跑得飞快静态分析时却摸不着头脑。我自己的经验是与其换来换去用好几套工具不如把 radare2 吃透——它内置的 bin 插件负责拆容器asm 插件负责解码指令anal 插件负责理控制流Java、.NET、WASM 这三种字节码它都有对应的处理路径。这篇文章就是把我实际项目里用 radare2 分析这三种字节码的完整流程、关键命令和踩过的坑整理一遍。无论你是做恶意样本分析、给老系统做审计还是单纯想搞明白某个 WASM 模块到底做了什么这份指南应该都能帮你缩短很多摸索时间。1. 先搞清楚radare2到底怎么“认识”字节码1.1 三种字节码的底子差异很多人刚接触radare2时下意识把它当成“另一个反汇编器”其实不太准确。radare2更像一个围绕二进制分析搭建的插件化框架。它的整个工作流可以拆成三步bin插件识别文件格式asm插件把字节翻译成指令anal插件恢复函数与交叉引用。你要分析的Java、.NET、WASM在这三步上的支持程度完全不同理解这一点比背命令重要得多。Java字节码算是最规整的。一个class文件由魔数、版本号、常量池、访问标志、字段表、方法表这些固定结构组成。它的指令集是JVM规范写死的基于操作数栈和执行栈帧指令大多无操作数或操作数与类文件常量池绑定。radare2的bin插件会解析常量池与方法表所以你打开文件后直接就能看到类名、方法名和字段名反汇编也基本是按JVM指令走的。这一套做得很成熟分析Java项目时体验最接近分析原生ELF。.NET程序集则复杂不少。.NET的托管程序集虽然外观看是个标准PE文件但真正的逻辑都在CLI头、元数据流和IL方法体里。元数据里有大量表类型定义表、方法定义表、字段定义表、字符串堆、GUID堆等等。radare2默认用PE解析器去读这个容器能识别出.NET相关标记但IL指令的智能解码能力没有Java那么完整。换句话说你打开一个.NET程序集不会像打开Java class那样直接跳进IL反汇编的世界更多时候是拿r2来解剖PE结构、抓元数据串、定位IL方法体的入口再配合一些IL字节知识做手工推导。WebAssembly又是另一种维度。WASM模块不是面向某个CPU架构的机器码而是一个经过压缩和编码的栈式虚拟指令流。模块由若干section组成类型、函数、内存、导出、代码、数据分别装在各自的section里。radare2的WASM bin插件会把模块结构解析出来能列函数、导入、导出、数据段asm插件也能把函数体里的字节流畅还原成类似local.get 0、i32.add、call 5这样的栈机汇编。对WASM来说r2是我用得最顺手的工具没有之一。1.2 通用命令骨架不管分析哪种字节码在radare2里的基本节奏是一致的。我用得最多的命令可以归纳成四组这里先搭个骨架后面每类字节码再往里填细节。打开文件和分析r2 -A file或不加-A进交互模式后用aa。-A表示进入后自动执行基础分析我一般习惯进去后手动敲/aa类命令因为大文件自动分析经常要等。afl列出函数af在当前位置定义函数s 地址跳转pdf反汇编当前函数。信息提取iI文件信息iS段列表is符号列表ii导入列表izz全文件字符串。这些命令在交互模式下都能加~xxx过滤比如izz~Main直接找包含 Main 的字符串。这个过滤技巧用熟练了效率能翻一倍。定位与追踪s sym.func或s 0x0001a2f0然后pd 20看20条指令。axt 地址查看谁引用了这个位置axf查看这个位置引用了谁。这是重建调用关系的核心。字节级操作px 64 地址十六进制查看p8 64 地址直接输出纯字节流配合管道和脚本很好用。pc系列可以输出C数组或Python数组写辅助脚本的时候非常省事。radare2还有一个独立的命令行工具跟r2是同一个家族叫 rabin2专门用来快速读取文件信息。很多适合在r2里做的事其实用rabin2更轻量比如rabin2 -I demo.class查看文件基本信息和格式。rabin2 -s demo.class列出符号。rabin2 -z demo.exe提取字符串加-j输出JSON方便拿脚本批量处理。1.3 从“能打开”到“能分析”的差别有一种很常见的误解r2既然能打开文件那就应该能像反编译原生程序一样把所有函数和调用关系都理出来。实际用下来你会慢慢接受一个现实打开文件只是一段旅程的开始字节码类程序的符号恢复、调用重建和语义理解往往需要额外的手工步骤尤其是在.Java和.NET这两个场景里。以我的经验Java class文件是最接近“开箱即用”的。r2的Java bin插件会把类名、方法名、字段名都解析出来这些符号基本能达到“双击即得”的程度配合常量池信息能比较快还原出一个类里的方法全貌。WASM模块则取决于它有没有保留导出符号很多在线编译器的产物是有导出名的r2能直接列出并反汇编这些函数如果被strip过就只能看到一堆FUN_xxxx占位名需要结合调用关系猜测用途。最需要心理准备的还是.NET程序集它默认进入的是“PE解析视角”你能看到段、导入、字符串但IL级别的函数边界和调用关系需要自己去摸索我会在第三部分详细展开。2. Java字节码从class文件到调用关系2.1 进入r2前先花30秒看文件Java类文件的前四个字节固定是0xCAFEBABE如果连这个都对不上基本不用往下分析了。我习惯先在终端跑一条rabin2 -I Demo.class这时能看到不少有用信息包括class文件的编译版本、文件格式标记有时还能看到主类名。版本号直接关联到你能分析的指令集范围比如较新的类文件用了更新的字节码版本某些老版本工具可能不认识新版指令。确认版本后进r2r2 Demo.class进来先iI确认格式识别成了Java类文件然后看is的输出这时你会看到类似sym.Demo.main、sym.Demo.init这样的符号。有符号和没符号在Java分析里差别不是一点半点。2.2 一条龙分析流程假设这是个测试用的Demo类包含一个main方法和一个自定义的add方法。我的操作顺序一般是[0x00000130] aa [0x00000130] aflaa会对已加载的bin信息做基础分析生成函数边界和内部引用。afl看到函数列表后直接用s跳到目标函数比如s sym.Demo.add然后pdf看反汇编。Java字节码的反汇编结果很有辨识度你会看到aload_0、getfield、invokevirtual、iadd这类JVM指令。它们操作的是局部变量表和操作数栈不像x86那样直接跟寄存器打交道所以读的时候要稍微适应一下栈机的思维方式。定位调用关系是另一个重点。在某个方法里看到invokevirtual或invokespecial指令时你想知道它到底调用了谁用鼠标对着那行指令所处的地址执行[0x00000140] axt 0x00000140这一条会列出所有引用到该位置的交叉引用通常直接显示调用方的函数名。反过来想知道当前函数引用了哪些外部目标用axf。这两个命令组合使用就是Java程序中虚方法调用的基本摸排手段。遇上混淆过的类方法名可能变成a、b或数字编号但因为常量池和JVM指令格式仍然保留靠着调用关系和字符串引用还是能一步步把行为链理出来。2.3 一个小型实战片段我用过一个非常简单的样例一个类里只写了:public class Demo { public static int add(int a, int b) { return a b; } public static void main(String[] args) { System.out.println(add(1, 2)); } }在r2里打开后is~Demo过滤符号。然后跳到sym.Demo.addpdf反汇编输出的内容大概长这样/ (fcn) sym.Demo.add 2 0x00000130 b iload_0 0x00000131 b iload_1 0x00000132 b iadd 0x00000133 b ireturn这基本就是return a b的字节码形态简单明了。接着跳到main看它如何调用add0x00000140 b iload_1 0x00000141 b iconst_2 0x00000142 b invokestatic sym.Demo.add看到invokestatic带上Demo.add符号调用目标就清楚了。这就是为什么说Java分析在r2里“手感好”——指令后的符号引用是分析器直接从常量池里解析出来的不需要人工去猜。2.4 与图形化反编译器搭配的定位有不少人会问我有了JD-GUI这类Java反编译器还用得着r2吗。我的看法是两者定位完全不同。反编译器适合把一个小方法还原成接近源代码的样子但在处理混淆、壳、动态加解密这类场景时反编译结果经常会失真或者干脆还原失败。r2保留的是指令级和结构级的原貌没有经过“还原成代码”这层脑补配合脚本批处理几百个类文件尤其方便。实际项目里我通常先用r2做批量摸底从众多类里筛出含敏感字符串、特殊调用的目标再把这些目标交给反编译器细看效果比单用哪一边都好。3. .NET字节码在元数据迷宫里的分析思路3.1 先接受一个现实这是PE视角如果你用r2打开一个.NET程序集注意不要期待它立刻变成IL指令列表。r2首先会把你带到PE的世界魔数、机器类型、节表、导入表这些。原因很简单.NET程序集本质上是PE文件PE解析优先级排在前面。你用iI看信息会看到类似“Microsoft.NET Assembly”或CLI相关的标记说明bin插件已经识别出这是托管程序集但后续的元数据解析深度取决于版本和插件实现。这并不代表.NET分析做不了。恰恰相反r2在PE容器解析和字符串提取上很扎实这两点恰好是.NET分析最需要的地基。你可以用iS看各个节的位置和大小用px看某个RVA地址的原始内容用izz把元数据里藏着的方法名、类型名、字符串全部捞出来。.NET的元数据字符串堆里常常包含了足够多的关键信息比如类型名Form1、方法名button1_Click、字符串Hello World光是这些就能帮你快速锁定关注点。3.2 为什么“找入口”比“反汇编”更实际在r2里分析.NET我的核心心法是不要死磕让r2输出漂亮的IL反汇编而是把r2当手术刀用来切PE结构、抓元数据、锁定位移最后结合IL OpCode的知识自己还原关键调用。实际操作中第一步总是找字符串。比如一个控制台程序会输出某个关键信息用[0x00000000] izz~Hello找到字符串“Hello”所在的虚拟地址和文件偏移。第二步是关键在IL语言里指令ldstr的OpCode是0x72后面跟着4字节的字符串token。也就是说在r2里看到的内存布局往往是这样的0x000020a4: 72 70 00 00 04 28 0a 00 00 06 ^^ ^^ 调用方法token ldstr0x72后面的4字节就是字符串元数据表的索引。你可以跳到这个地址往前看找到ldstr指令然后继续往下读如果遇到0x28那就是call指令后面同样跟4字节的方法token。这两种OpCode在IL里太常见了我自己甚至写过简单的r2脚本专门搜索72和28的模式把整个代码段的字符串加载和调用点全部标出来。方法token需要进一步解析成具体方法。token的高几位是表类型低几位是行号比如token0x0600000A代表MethodDef表第10行。这时候可以用r2的元数据辅助解析或者借助十六进制查看元数据堆。这个方法没法做到全自动但准确性很高尤其适合那些被混淆过、符号表被清空的.NET程序。你不需要一个完美的反编译器你只需要找到IL方法体里的关键指令序列就能逐步还原出行为逻辑。3.3 具体怎么扒一段.NET程序集假如你拿到的是一个叫SampleApp.exe的小程序我的常规操作如下先跑rabin2 -z SampleApp.exe直接列出程序集里的字符串看看有没有敏感内容。然后进入r2r2 SampleApp.exe [0x00000000] iI [0x00000000] iS接下来看代码段.text的起始地址。通常可以用[0x00000000] is~Main如果程序集没有清除公共符号Main方法入口可能直接显示出来。跳到入口附近用px 64看原始IL字节。一个典型的.NET Main方法开头可能是0x00002080: 02 72 00 00 00 70 28 0a 00 00 06 2a ^^ ^^ ^^ 入口标志 ldstr call字节02表示方法头是tiny格式72后面是字符串token28后面是方法token2a是ret。到这一步你其实已经看到了那个方法在加载字符串、调用另一个方法、然后返回的完整IL流程。结合之前抓到的字符串内容这段逻辑大概率就藏在眼前。3.4 关于.NET反汇编能力的一些补充有些人会遇到r2输出一堆类似x86的指令而非IL指令这很正常。因为当前版本的asm插件对.NET的自动识别可能没有覆盖所有场景。若你能接受在十六进制和手工解析之间切换那r2的整个PE解析能力、段管理和脚本系统就已经足够胜任多数分析场景。如果非要完整反汇编IL我会在项目里用r2把关键字节段导出成文件再用独立反编译器去还原两边对照着做。这种混合打法虽然多了一步但比纯粹依赖某个完美工具更稳定。4. WebAssembly另一种栈机的解剖4.1 WASM模块长什么样WASM模块从文件头开始就是一段明确的协议魔数是00 61 73 6d也就是字符串\0asm接着是4字节版本号01 00 00 00。后面跟着一系列section每个section有自己的id前缀和长度。section类型里1是类型段2是导入段3是函数段5是内存段7是导出段8是起始段10是代码段11是数据段。了解这个结构会很有帮助因为你在radare2里用iS看到的section列表基本就能把模块的骨架摸清楚了。WASM的指令模型基于栈没有寄存器。实际操作时你会看到类似local.get 0、i32.const 42、i32.add、call 5这样的指令序列。习惯了x86/ARM的人一开始会觉得别扭但理解了两个核心概念之后就好办了局部变量槽和操作数栈。函数参数和局部变量都按索引编号栈上的值按FIFO方式参与运算。4.2 radare2里解析WASM的实际感觉radare2对WASM的bin解析相当成熟。打开一个.wasm文件后iI会确认模块格式iS列出section及偏移ii能看到导入的外部函数is能看到导出符号。如果模块有调试信息或导出名afl能直接列出函数。接下来跳到某个导出函数上执行pdf输出的反汇编是栈机格式阅读起来比IL直白/ (fcn) sym.main 21 0x00000200 local.get 0 0x00000201 i32.const 1 0x00000202 i32.add 0x00000203 end这段代码的意思是“取第0个局部变量加1返回”。配合导出函数名和调用关系就能很快还原整个模块的意图。如果函数名被strip了那就要靠axt看谁调用了谁。WASM的函数索引是从0开始的整数但导入函数会优先占据前面的索引搞混这个很容易分析错这是个隐藏的坑。我常用的一个小技巧是先看导入段把外部导入的JS函数名称和索引记下来然后再去代码段里找call 索引这样一眼就能看出哪个本地函数调用了外部环境。特别在分析一些挖矿脚本、恶意WASM模块的时候这个做法几乎是第一优先级——外部导入函数往往是突破口。4.3 内存、数据段与全局变量WASM分析中常会被忽略的是内存和全局变量。radare2里可以用iS或rabin2 -S查看数据段多个数据段可能分布在模块的不同位置。你可以跳转到数据段地址用px直接看初始化数据。全局变量则往往藏了一些配置信息比如检测开关、密钥片段、算法参数。分析时建议大家把全局变量和数据段都过一遍不要只盯着代码。我遇到过几个WASM模块逻辑本身不复杂但全局里有一张被编码过的表代码通过i32.load从某个内存偏移读数据配合偏移计算其实是在查表解密。如果只看代码块会觉得很绕一旦把数据段当作“表”来读瞬间就通了。这个问题在Java和.NET里不太会遇到因为它们的对象布局更复杂但WASM的数据段完全暴露在眼前别浪费这个优势。5. 三种字节码都适用的踩坑与排查5.1 常见问题速查表症状可能原因处理办法r2打开Java class后没有符号bin插件未加载或类文件版本过新确认版本用-a java强制指定升级radare2.NET程序集打开后看不到IL指令r2默认按PE视角解码.NET的asm支持有限使用iSpx元数据串手工还原结合外部反编译器对照WASM打开后函数全是FUN_xxxx模块被strip导出名丢失靠axt和导入索引重建关系用iicall索引匹配aa分析大型PE文件卡死深度分析作用在超大代码段上改用aac局部分析或aab按块分析反汇编输出为原始字节而非指令asm插件识别错误用e asm.archwasm或e asm.archjava手动指定izz搜不到关键字符串字符串在数据段或元数据堆中或已被加密用iz、iZ分别看section/字符串关注元数据堆这个表里的前三条是我在项目中反复遇到的后面三条属于通用性问题遇到时不要慌。radare2的报错信息有时很含糊但绝大多数问题是“插件没识别对格式”或“分析深度没控制好”排查思路优先从这两个方向下手。5.2 让三倍速操作成为可能的小习惯用radare2分析多种字节码最大的痛点其实不是命令不会而是“同样的分析步骤每次都要重复敲一遍”。我后来养成了几个小习惯快了不少。第一写启动脚本。把常用命令集中到一个r2脚本文件里比如r2 -i setup.r2 sample.wasm脚本里写好aa、afl、izz~flag、iS这些常规操作进去就自动跑完。这不光省敲键盘的时间更重要的是每次分析的“初始画面”是标准化的不容易漏掉某一步。第二善用~过滤和管道。r2命令输出往往很冗长直接用~keyword就能在命令输出里做包含过滤。比如[0x00000000] izz~password [0x00000000] is~main这比把输出拖到屏幕末尾再肉眼找要高效得多。第三用JSON输出配合外部工具。rabin2支持-j输出JSON数据拿到后可以交给任何脚本工具做批量分析。比如我把某个目录下所有class文件的符号列表用JSON跑一遍再写个小脚本去重、分类几秒钟就能看到哪些类包含可疑的方法引用。手工人肉翻几百个符号不仅累还容易漏。第四保存会话。r2的Ps命令会把当前会话保存成脚本下次打开时恢复现场。这个在分析长样本时非常有用分析到一半关个机第二天还能从原来的状态继续。5.3 别忘了r2生态里的其他部位经常有人问我radare2是指那个r2命令本身吗。其实r2是一个工具链家族binwalk、Ghidra这些可能你更熟悉。不过就字节码分析而言常用的是两个核心组件r2本体和rabin2。rabin2特别值得多练它把文件解析能力单独抽出来了速度比进交互模式快输出也更干净。我很多“要不要打开这个文件”的判断都是先跑一条rabin2 -I完成的。如果rabin2解析都不过关那这个文件大概率不是目标格式或者文件本身有问题这时候进r2纯属浪费时间。6. 冷启动时的一些心得与小建议我仔细想了想如果只能给一个建议那就是从Java开始练手。原因是radare2对Java字节码的支持完成度最高符号解析最全反汇编结果也最好读。花一两个小时把一个class文件从打开到定位方法、追踪调用关系完整走一遍你会很快熟悉r2的基本操作节奏这个节奏是通用的。然后再转WASM感受一下栈机汇编和模块结构。最后再碰.NET到时候你已经有足够多经验来处理“PE视角手工解析IL”这种相对费神的场景了。练手素材不用专门去下载样本你自己编译几个小文件就够了。javac写个含方法调用的class用C#编译器生成个小程序集再用在线工具把一个简单C函数编译成wasm三个样本足够覆盖大部分操作了。我一直觉得分析工具的学习最忌讳“直接拿复杂样本练”因为遇到问题根本分不清是工具问题、格式问题还是自己操作问题从自己可控的简单样本开始每一步都在射程范围内学习效率反而高。我自己的体会是radare2在字节码分析里真正厉害的地方不是某一种格式支持得多么完美而是当你把它的bin解析、搜索、脚本和过滤能力组合起来后能构建出一套完全属于你自己的分析流水线。Java可以用它快速扫全量符号WASM可以用它精准定位模块细节.NET虽然要出点力气手工还原IL但r2提供的PE切片和元数据能力已经足够撑起整个分析工作。如果非要一句话收尾那就是别指望radare2替你把所有事情做完当你学会跟它配合你会发现自己能做更多以前觉得麻烦的事情。
阅读完成 · 觉得有帮助?