引言
关于静态代码扫描,大家想必都非常熟悉了,比如 lint、detekt 等,这些也都是常用的扫描工具。但随着隐私合规在国内越来越趋于常态,我们经常需要考虑某些危险api的调用排查等等,此时上述的工具往往不容易实现现有的需求,以及后续扩展。而在这个背景下,ASM 就是解决方式的最佳手段之一。
故此,本篇我们将通过写一个代码扫描插件,从而简单玩转并入门 ASM :)
Github:Bee-AnalysisPlugin
背景
记得在前司(下厨房)的时候,我们 App 曾被报出存在漏洞问题,具体原因是:
项目中使用了log4j等api,导致存在安全漏洞。
其实当听到这个问题的时候,总感觉略有点离谱,客户端怎么会存在这个问题?
在我的印象中,log4j 似乎是21年时的一个广泛问题,当然主要影响是后端同学,团队内部也还排查过。但因为客户端和这系列库离的相对就比较远了,所以对于客户端的我们没有在意(为后面埋了伏笔)。
所以当真正收到相关部门邮件时,我们先是不相信,然后和另一个同学(化名z)开始着手排查:
结果还真是狠狠打脸了,项目历史代码中存在使用 HttpURLConnection 导致,而 HttpURLConnection 内部又引入了 Log4j 系列库,从而导致相关问题,于是就立即开始分工处理:
- z负责写代码扫描插件,全量扫项目,从而确保已经完全移除相关api;
- 我负责对代码层进行处理,对涉及到相关的
HttpURLConnection逻辑进行移除与逻辑调整;
最终在收到问题的当天晚上就提了PR流程,总耗时大概3小时,也算是比较迅速。
事后来看, 虽然问题解决了,但同时也暴漏出了一些问题,比如 客户端代码 没有相关 危险代码扫描机制 ,导致这部分隐患一直处于黑盒状态。而从技术角度来思考,实现这个check也非常简单。
如下所示:
- 定义一份线上的漏洞表(定期更新),每次
CI时拉取最新的;- 定义一个代码扫描插件,每次
PR commit时进行自动触发,并拉取最新的漏洞表,如果项目中存在相关漏洞,则中断本次打包并通知;
聊聊需求
通过上面的背景,我们大概也能知道本篇的缘由以及一些应用场景,所以如果要从练习角度入手,做一个代码扫描插件,其目的是静态扫描出相关方法的调用次数以及具体调用者,从而便于我们进行排查,应该怎么做?
此时可能会有同学抢答,我直接使用 Android Studio 全局搜索也行啊,为什么还需要专门写个插件扫描呢? 🤔
直接使用AS也能实现类似的需求,但是如果我们需要找出所有相关的调用处,这并不是一件易事,特别是对于复杂的项目而言(当然你要是没事愿意一个一个🔍,那另说了😑)。
而如果使用 ASM ,上述的需求实现起来就比较简单,而且后续的扩展也会相对成本较低,甚至我们还可以做一个调用替换等等,当然这些都是后话。
基础入门
为避免部分同学不太理解 ASM ,故这里选择先简单聊聊 ASM 基础背景,也算科普了(逃跑~)。
什么是ASM?
Java ASM(Java Bytecode Assembler)是一个用于 生成 和 修改 Java字节码的库。ASM 提供了一种灵活而强大的方式来分析、转换和生成Java类文件。使用 ASM ,我们可以在 不改变源代码 的情况下,通过操纵字节码来实现对代码的定制化需求。这种能力在许多领域中都有应用,包括 编译器 、代码优化、字节码工具、AOP(面向切面编程)框架等。
ASM与AGP关系
回到 Android 中,我们知道 Android虚拟机 是基于 Dalvik(5.0是ART),而 Dalvik 也是属于 JVM虚拟机 的一种。所以Android的开发语言是 Java (Kotlin会由编译器转为Java),而我们 Java 代码编译后的 class 文件为了便于 Dalvik 识别,故最终还需要转为dex 文件。
整个过程如下所示:
java->class->dex
常用的 AGP(Android Gradle Plugin) 插件,就是在 class -> dex 前,为开发者提供了一个时机,允许我们进行二次修改 Class ,从而实现自定义的需求,这也即是 ASM 在 AGP 中的作用由来。
ASM常见API
-
ClassReader负责对
Class进行读写,最终调用accpet加载class,由ClassVisitor开始进行处理; -
ClassVisitor负责对读取到的
Class进行操作,比如对class中某一部分信息(方法、属性等)进行修改;
ASM基础操作
总结起来通常就是三步:
- 读取class,创建
ClassReader; - 进行修改,创建
ClassVisitor(通常是ClassWriter+其他); - 保存结果,
ClassWriter.toByteArray();
伪代码如下:
val cr = ClassReader(classStream)
val cw = ClassWriter(cr, 0)
val cv = xxxClassVisitor(cw)
cr.accept(cv, ClassReader.EXPAND_FRAMES)
FileOutputStream(outClassPath).use {
it.write(cw.toByteArray())
}
ClassVisitor 提供了很多方法,比如当方法被调用时(visitMethod),开发者可以根据需求重写相应的方法,从而在 class 访问过程中,实现 class 修改。当然这些都只是最基础的操作,实际使用时我们还会使用其他更多的一些 Api ,由于本文并不是全面介绍相关 Api 的文章,故这部分就留给读者自行探索了:)
具体思路
要扫描代码,肯定是要先写一个 Plugin ,然后注册一个 Transform ,并在其中其中读取所有 class 与 jar ,从而对其进行处理。具体过程中,如果存在我们指定的方法调用,我们就将当前调用类的位置或者方法保存,最后当 ASM 处理结束后,我们再对结果进行处理。
不过需要注意的是
Transform在AGP7.0已经被标注了 废弃,AGP8.0也已经正式 移除 ,所以我们要实现上述的逻辑,还是需要做一些改动。
故我们选用的是 AndroidComponentsExtension 来进行实现,这个 API 是Android团队专门针对 ASM 做的一个 hook 时机。不过需要注意的是,其并不像 Transform,我们可以 拿到所有class以及jar直接进行处理,而是当某个 class 被处理时,我们可以有时机进行拦截并处理。故如果我们想确保收集完所有信息,就必须在相应的 Task 之后再进行汇总处理,比如在 transformxxClassesWithAsm 之后。
实现效果
我们以检测业务中 PrintStream 类的调用为例,最终实现效果如下所示:

如上图所示,业务中一共有三处使用 PrintStream 类,分别调用的都是其 print() 以及 println() 方法。
当然对于结果的处理,无论是以文件形式保存还是其他方式,都是由我们自行处理,这里只是将其打印出来。
具体流程
示例Github: Bee-AnalysisPlugin
插件配置
作为开始,我们需要定义一个自己的插件类,需要继承自 Plugin 类,具体代码如下所示:

上述的流程我们分为3步:
- 创建我们的扩展实例(用于传递配置参数);
- 注册
AsmClassVisitor,用于访问字节码; - 当字节码处理完成后,统计处理结果;
具体的配置扩展类: RuleExtension
open class RuleExtension { var classPackages: Array<String> = emptyArray() var enableLog: Boolean = false }注意:这里需要增加
open,否则编译失败;
ASM配置
在 AGP 7.0 之后,我们自定义的 ASM 访问器,需要继承自 AsmClassVisitorFactory ,并需要传入一个 InstrumentationParameters 泛型,用于确定是否需要实例化参数,因为我们需要对每个变体进行处理,所以这里传入 buildType 作为分类。当然如果并不需要传参的话,这里的工厂泛型可以直接传入 InstrumentationParameters.None ;


上述的流程如下:
我们定义了一个 字节码工厂访问器,并规定只处理非 Androidx 以及 R. 相关的 class,这样当字节码在处理时,如果当前class满足条件,就会触发 createClassVisitor() 方法,从而我们就可以创建自己的 字节码访问类,并使用这个处理类对当前字节码进行修改。
当我们在读取
class时,内部会对相关的方法、构造函数、属性等等都进行一次遍历或者调用,同时也会触发相关的回调方法,在这些回调方法里,也有对应的访问器进行处理,整体类似一个树形结构。
比如当访问 class 中的方法时,此时会调用 visitMethod() 方法,而我们本篇是希望遍历所有方法,所以需要重写该方法,并返回我们自己的方法访问器(MethodVisitor);
相应的,在具体的 MethodVisitor 里,当这个方法内部去访问其他方法时,或者访问其他对象时等,也都会再次回调相关方法。故此,我们只需要在其访问其他方法时,将其保存到我们自己池子中,从而就可以得到如下信息:
当前类、当前方法、被访问的类、被访问的方法等
而根据这些信息,我们就可以清晰的得知我们自己需要拦截的方法被谁调用了,调用了多少次,调用位置等等。
检测逻辑
具体的检测逻辑就比较简单了,我们只需要定义一个静态处理类,其内部持有一个 Map 结构的结果集(key 为变体名、value为结果集),而具体的判断规则可以存在一个Set或者List中。比如我们示例中只需要判断是否存在指定包或者类的调用,那么只需要传入 packages 即可,如果有更多的规则,比如方法等等,则可以根据逻辑进行更改。

具体逻辑如上,其中 filterAndAddMethod() 是每次当访问到相关方法时调用,如果满足条件,则将其信息缓存起来;当ASM处理完成后,也就是 transformXXXClassesWithAsm 之后,我们再调用 end() 去统计,从而按照当前 buildType 输出结果。
当然,当拿到结果后,怎么处理那都是题外话题了,比如可以直接打印,或者存储到文件里,也可以抛出异常等等,这些就留给大家自行决断吧。
使用方式
具体的使用方式,比较简单,我们直接在 application 所在的 build.gradle 添加下面的配置语句即可。
//示例
analysis {
classPackages = ["java.io.PrintStream"]
}
示例Github: Bee-AnalysisPlugin
总结
本篇到这里就结束了,严格而言,本篇其实算不上什么ASM高深技巧,只能算的上是基础操作。更多是希望,通过本篇,能使得新手同学对于 ASM 基础使用有一个了解,特别是在 AGP7.0 之后的打开方式。


















