引言

关于静态代码扫描,咱们想必都十分熟悉了,比方 lintdetekt 等,这些也都是常用的扫描东西。但随着隐私合规在国内越来越趋于常态,咱们常常需求考虑某些风险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 ,然后完成自界说的需求,这也便是 ASMAGP 中的效果由来。

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 ,并在其间其间读取一切 classjar ,然后对其进行处理。详细进程中,假如存在咱们指定的办法调用,咱们就将当时调用类的方位或许办法保存,最后当 ASM 处理完毕后,咱们再对成果进行处理。

不过需求留意的是 TransformAGP7.0 现已被标示了 抛弃AGP8.0 也现已正式 移除 ,所以咱们要完成上述的逻辑,还是需求做一些改动。

故咱们选用的是 AndroidComponentsExtension 来进行完成,这个 API 是Android团队专门针对 ASM 做的一个 hook 机遇。不过需求留意的是,其并不像 Transform,咱们能够 拿到一切class以及jar直接进行处理,而是当某个 class 被处理时,咱们能够有机遇进行阻拦并处理。故假如咱们想保证收集完一切信息,就必须在相应的 Task 之后再进行汇总处理,比方在 transformxxClassesWithAsm 之后。

完成效果

咱们以检测事务中 PrintStream 类的调用为例,终究完成效果如下所示:

玩转ASM | 写个代码扫描插件,再也不怕 log4j 等问题

如上图所示,事务中一共有三处运用 PrintStream 类,分别调用的都是其 print() 以及 println() 办法。

当然关于成果的处理,不管是以文件办法保存还是其他办法,都是由咱们自行处理,这儿仅仅将其打印出来。

详细流程

示例Github: Bee-AnalysisPlugin

插件装备

作为开端,咱们需求界说一个自己的插件类,需求继承自 Plugin 类,详细代码如下所示:

玩转ASM | 写个代码扫描插件,再也不怕 log4j 等问题

上述的流程咱们分为3步:

  1. 创立咱们的扩展实例(用于传递装备参数);
  2. 注册 AsmClassVisitor ,用于拜访字节码;
  3. 当字节码处理完成后,统计处理成果;

详细的装备扩展类: RuleExtension

open class RuleExtension {
 var classPackages: Array<String> = emptyArray()
 var enableLog: Boolean = false
}

留意:这儿需求增加open,不然编译失利;

ASM装备

AGP 7.0 之后,咱们自界说的 ASM 拜访器,需求继承自 AsmClassVisitorFactory ,并需求传入一个 InstrumentationParameters 泛型,用于确认是否需求实例化参数,由于咱们需求对每个变体进行处理,所以这儿传入 buildType 作为分类。当然假如并不需求传参的话,这儿的工厂泛型能够直接传入 InstrumentationParameters.None

玩转ASM | 写个代码扫描插件,再也不怕 log4j 等问题
玩转ASM | 写个代码扫描插件,再也不怕 log4j 等问题

上述的流程如下:

咱们界说了一个 字节码工厂拜访器,并规矩只处理非 Androidx 以及 R. 相关的 class,这样当字节码在处理时,假如当时class满意条件,就会触发 createClassVisitor() 办法,然后咱们就能够创立自己的 字节码拜访类,并运用这个处理类对当时字节码进行修正。

当咱们在读取 class 时,内部会对相关的办法、构造函数、特点等等都进行一次遍历或许调用,一起也会触发相关的回调办法,在这些回调办法里,也有对应的拜访器进行处理,全体相似一个树形结构。

比方当拜访 class 中的办法时,此刻会调用 visitMethod() 办法,而咱们本篇是希望遍历一切办法,所以需求重写该办法,并回来咱们自己的办法拜访器(MethodVisitor);

相应的,在详细的 MethodVisitor 里,当这个办法内部去拜访其他办法时,或许拜访其他目标时等,也都会再次回调相关办法。故此,咱们只需求在其拜访其他办法时,将其保存到咱们自己池子中,然后就能够得到如下信息:

当时类、当时办法、被拜访的类、被拜访的办法等

而依据这些信息,咱们就能够明晰的得知咱们自己需求阻拦的办法被谁调用了,调用了多少次,调用方位等等。

检测逻辑

详细的检测逻辑就比较简略了,咱们只需求界说一个静态处理类,其内部持有一个 Map 结构的成果集(key 为变体名、value为成果集),而详细的判别规矩能够存在一个Set或许List中。比方咱们示例中只需求判别是否存在指定包或许类的调用,那么只需求传入 packages 即可,假如有更多的规矩,比方办法等等,则能够依据逻辑进行更改。

玩转ASM | 写个代码扫描插件,再也不怕 log4j 等问题

详细逻辑如上,其间 filterAndAddMethod() 是每次当拜访到相关办法时调用,假如满意条件,则将其信息缓存起来;当ASM处理完成后,也便是 transformXXXClassesWithAsm 之后,咱们再调用 end() 去统计,然后依照当时 buildType 输出成果。

当然,当拿到成果后,怎样处理那都是题外话题了,比方能够直接打印,或许存储到文件里,也能够抛出反常等等,这些就留给咱们自行决断吧。

运用办法

详细的运用办法,比较简略,咱们直接在 application 地点的 build.gradle 增加下面的装备句子即可。

//示例
analysis {
   classPackages = ["java.io.PrintStream"]
}

示例Github: Bee-AnalysisPlugin

总结

本篇到这儿就完毕了,严厉而言,本篇其实算不上什么ASM深邃技巧,只能算的上是根底操作。更多是希望,经过本篇,能使得新手同学关于 ASM 根底运用有一个了解,特别是在 AGP7.0 之后的打开办法。

当然,假如本篇能对你有所协助,那就更好了 :)

关于我

我是 Petterp ,一个 Android工程师。假如本文,你觉得写的还不错,不妨点个赞或许收藏,你的支持,是我继续创造的最大鼓励!

也欢迎重视我的 公众号(Petterp),等待与你一同 无限进步 :)