引言
关于静态代码扫描,咱们想必都十分熟悉了,比方 lint、detekt 等,这些也都是常用的扫描东西。但随着隐私合规在国内越来越趋于常态,咱们常常需求考虑某些风险api的调用排查等等,此刻上述的东西往往不容易完成现有的需求,以及后续扩展。而在这个布景下,ASM 便是解决办法的最佳手法之一。
故此,本篇咱们将经过写一个代码扫描插件,然后简略玩转并入门 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 之后的打开办法。
当然,假如本篇能对你有所协助,那就更好了 :)
关于我
我是 Petterp ,一个 Android工程师。假如本文,你觉得写的还不错,不妨点个赞或许收藏,你的支持,是我继续创造的最大鼓励!
也欢迎重视我的 公众号(Petterp),等待与你一同 无限进步 :)





