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

简介: 写个代码扫描插件,再也不怕 log4j 等问题

引言

关于静态代码扫描,大家想必都非常熟悉了,比如 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步:

  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 ;

 

上述的流程如下:

我们定义了一个 字节码工厂访问器,并规定只处理非 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 之后的打开方式。

相关文章
|
SQL 存储 监控
|
监控 测试技术 Go
告别传统Log追踪!GOAT如何用HTTP接口重塑代码监控
本文介绍了GOAT(Golang Application Tracing)工具的使用方法,通过一个Echo问答服务实例,详细展示了代码埋点与追踪技术的应用。内容涵盖初始化配置、自动埋点、手动调整埋点、数据监控及清理埋点等核心功能。GOAT适用于灰度发布、功能验证、性能分析、Bug排查和代码重构等场景,助力Go项目质量保障与平稳发布。工具以轻量高效的特点,为开发团队提供数据支持,优化决策流程。
1207 90
|
存储 数据采集 监控
云上数据安全保护:敏感日志扫描与脱敏实践详解
随着企业对云服务的广泛应用,数据安全成为重要课题。通过对云上数据进行敏感数据扫描和保护,可以有效提升企业或组织的数据安全。本文主要基于阿里云的数据安全中心数据识别功能进行深入实践探索。通过对商品购买日志的模拟,分析了如何使用阿里云的工具对日志数据进行识别、脱敏(3 种模式)处理和基于 StoreView 的查询脱敏方式,从而在保障数据安全的同时满足业务需求。通过这些实践,企业可以有效降低数据泄漏风险,提升数据治理能力和系统安全性。
2535 266
云上数据安全保护:敏感日志扫描与脱敏实践详解
|
运维 监控 Cloud Native
一行代码都不改,Golang 应用链路指标日志全知道
本文将通过阿里云开源的 Golang Agent,帮助用户实现“一行代码都不改”就能获取到应用产生的各种观测数据,同时提升运维团队和研发团队的幸福感。
990 158
|
监控 测试技术 开发者
一行代码改进:Logtail的多行日志采集性能提升7倍的奥秘
一个有趣的现象引起了作者的注意:当启用行首正则表达式处理多行日志时,采集性能出现下降。究竟是什么因素导致了这种现象?本文将探索Logtail多行日志采集性能提升的秘密。
760 57
|
存储 数据采集 监控
云上数据安全保护:敏感日志扫描与脱敏实践详解
随着企业对云服务的广泛应用,数据安全成为重要课题。通过对云上数据进行敏感数据扫描和保护,可以有效提升企业或组织的数据安全。本文主要基于阿里云的数据安全中心数据识别功能进行深入实践探索。通过对商品购买日志的模拟,分析了如何使用阿里云的工具对日志数据进行识别、脱敏(3 种模式)处理和基于 StoreView 的查询脱敏方式,从而在保障数据安全的同时满足业务需求。通过这些实践,企业可以有效降低数据泄漏风险,提升数据治理能力和系统安全性。
|
SQL 监控 Java
IDEA插件-Mybatis Log Free日志替换
MyBatis Log Free 是一个免费的用于在 IntelliJ IDEA 中显示 MyBatis 日志的插件。它可以帮助您更方便地查看和分析 MyBatis 的 SQL 执行情况,以及定位潜在的性能问题,提高开发效率。
2419 0
IDEA插件-Mybatis Log Free日志替换
|
SQL 监控 Java
IDEA插件-Mybatis Log Free日志替换
MyBatis Log Free 是一个免费的用于在 IntelliJ IDEA 中显示 MyBatis 日志的插件。它可以帮助您更方便地查看和分析 MyBatis 的 SQL 执行情况,以及定位潜在的性能问题,提高开发效率。
4176 0
IDEA插件-Mybatis Log Free日志替换
08-06-06>pe_xscan 精简log分析代码 速度提升一倍
08-06-06>pe_xscan 精简log分析代码 速度提升一倍
|
SQL 安全 数据库
基于SQL Server事务日志的数据库恢复技术及实战代码详解
基于事务日志的数据库恢复技术是SQL Server中一个非常强大的功能,它能够帮助数据库管理员在数据丢失或损坏的情况下,有效地恢复数据。通过定期备份数据库和事务日志,并在需要时按照正确的步骤恢复,可以最大限度地减少数据丢失的风险。需要注意的是,恢复数据是一个需要谨慎操作的过程,建议在执行恢复操作之前,详细了解相关的操作步骤和注意事项,以确保数据的安全和完整。
912 0