ProGuard 进阶系列(三) Java 类文件解析

news/2024/11/23 3:52:04/

书接上文,当我们从用户的配置文件中读取到所有的配置信息后,下一步便是将配置中的指定的类文件进行读取,构建需要混淆的 Java 类文件的语法树。在阅读类文件之前,先来看一下输入输出参数中的内容,我使用的是一个 Android 项目的输出产物,使用 -injars-outjars-libraryjars 指定了相关的信息,运行起来,这些信息会放到 Configuration 中,具体信息看下图:

a10aff3f44cbddfaadbc22b06993d4c8.png
programJars 与 libraryJars

Java 代码源文件在编译后会转换成 Class 文件,格式定义是固定的,可以使用 ASM 等开源工具进行读取和解析,本文将分析 ProGuard 中,是如何进行类文件读取的。

让我们把目光拉回到 ProGuard 的 main 方法中:

f2b328d820374262342fcbe4c5915b73.png
ProGuard 的 Main 函数代码

从代码中可以看到,配置信息解析结束后,就会执行 ProGuard 的 execute 方法。继续执行下去,除去一些前置校验的操作,下一步便是本文关注的 readInput,读取 Class 文件的内容。

private void readInput() throws Exception {// Fill the program class pool and the library class pool.passRunner.run(new InputReader(configuration), appView);
}

在这几行代码中,有几个信息:passRunnerInputReaderappView

先来看passRunner,它只有很少的几行代码:

public class PassRunner {private static final Logger logger = LogManager.getLogger(PassRunner.class);private final Benchmark benchmark = new Benchmark();public void run(Pass pass, AppView appView) throws Exception {benchmark.start();pass.execute(appView);benchmark.stop();logger.debug("Pass {} completed in {}", pass::getName, () -> TimeUtil.millisecondsToMinSecReadable(benchmark.getElapsedTimeMs()));}
}

当执行 run 方法是,会执行 pass.execute 方法,并且记录其执行时间。

其次是 appView,它是一个 POJO 类,主要用来存储类信息和资源信息。

InputReader 就是本文的重点,顾名思义,它是用来读取输入信息的,就是用来读取文章开头提到的 libraryJarsprogramJars

  • libraryJars ,指的是依赖库,如在 Android 中使用的 Android SDK ,Support 包等依赖库

  • programJars,指的是我们自己编写的代码,要进行混淆的目标类文件

虽然此处叫 jar ,但其本质上不仅支持 jar 文件,还支持文件夹、war 等各种格式。

一、文件的读取

在文章的开头,programJars  里面有两个 ClassPathEntry, 分别指向了 R.jar 文件和 classes 文件夹。在 InputReader  的 execute 方法中,我们先跳过 ClassReader 相关的创建逻辑,直接来看 readInput 方法:

// InputReader.java, 省略不相关的代码
private void readInput(String messagePrefix, ClassPathEntry classPathEntry, DataEntryReader dataEntryReader) throws IOException {try {DataEntryReader reader = new DataEntryReaderFactory(configuration.android).createDataEntryReader(classPathEntry, dataEntryReader);DataEntrySource source = new DirectorySource(classPathEntry.getFile());source.pumpDataEntries(reader);} catch (IOException ex) {throw new IOException("Can't read [" + classPathEntry + "] (" + ex.getMessage() + ")", ex);}
}

此方法的入参 dataEntryReader 就是用于读取 Class 的实现,后面会讲到。从 readInput 的方法实现中可以看到,需要先创建  reader,代码中此处使用了工厂模式。先来回忆一下工厂模式:

工厂模式是一种创建型设计模式,它通过委托给一个工厂类来实例化对象,而不是直接使用 new 关键字。这一模式可以避免调用方的复杂性,提供一个抽象的接口来创建实例,让调用方不必关心创建对象的细节。使用工厂模式可以提高代码复用性,更容易维护代码,让调用方只关注业务逻辑实现细节。

DataEntryReaderFactory 中将 DataEntryReader 的创建过程封装起来,调用的时候,不需要感知创建的过程。前面提到了,programJars 支持多种格式,如 apkaabjar 等,所以在工厂方法里面会根据文件后缀名去创建不同类型的 DataEntryReader ,代码如下:

public DataEntryReader createDataEntryReader(ClassPathEntry classPathEntry, DataEntryReader reader) {// 省略部分代码// Unzip any apks, if necessary.reader = wrapInJarReader(reader, false, false, isApk, apkFilter, ".apk");if (!isApk) {// Unzip any aabs, if necessary.reader = wrapInJarReader(reader, false, false, isAab, aabFilter, ".aab");if(!isAab) {// Unzip any jars, if necessary.reader = wrapInJarReader(reader, false, false, isJar, jarFilter, ".jar");// 省略部分代码}}return reader;
}
private DataEntryReader wrapInJarReader(DataEntryReader reader,boolean stripClassesPrefix,boolean stripJmodHeader,boolean isJar,List<String> jarFilter,String jarExtension) {// 不管当前格式是什么,直接创建 JarReaderDataEntryReader jarReader = new JarReader(stripJmodHeader, reader);if (isJar) {// 如果当前需要读取的文件格式是对应后缀格式,直接返回 return jarReader;} else {// 创建一个后缀匹配器StringMatcher jarMatcher = new ExtensionMatcher(jarExtension);// 返回一个格式判断的 Readerreturn new FilteredDataEntryReader(new DataEntryNameFilter(jarMatcher),jarReader,reader);}
}

在代码中,创建 reader 的时候构建了一个嵌套的结构,此处以 Android 项目中,生成的 R.jar 文件为例,其执行创建过程如下(执行路径参考红色部分):

8fb1d1a4927e88638030126362c41be0.png
Reader 创建流程

为了验证最后的产物结构,调试可以查看最终生成的 DataEntryReader 的结构信息截图如下, 可以与上面创建的图进行对照理解:

212a858a5a91194b93f12789f4ecb147.png
DataEntryReader 示例

当你理解 Reader 的创建逻辑后,可能会有和我一样的困惑,为什么此处需要使用嵌套的结构呢?既然已经知道文件格式了,为什么不直接创建对应的 JarReader 呢?按照我个人的理解,代码可能会这样子写:

// 此处非项目中源代码,仅个人思路。
public DataEntryReader createDataEntryReader(ClassPathEntry classPathEntry, DataEntryReader reader) {// 省略部分代码// Unzip any apks, if necessary.if (isApk) {reader = wrapInJarReader(reader, false, false, isApk, apkFilter, ".apk");} else if(isAab) {reader = wrapInJarReader(reader, false, false, isAab, aabFilter, ".aab");} else if (isJar) {reader = wrapInJarReader(reader, false, false, isJar, jarFilter, ".jar");}// 省略部分代码return reader;
}

但经过多方查证,在读取文件的时候,可能会出现嵌套的问题,拿 Android 来说,在 aar 格式的文件中,会存在有 jar 格式的文件 classes.jar , 示例如下:

cef925260ca14d909a1d288d30b31532.png
example.aar 文件列表

因此,在读取内容的时候,还需要一个可以读取 jar 文件的  Reader 。虽然源代码中那样写可以正常执行逻辑,但我觉得它可能还是不够优雅,也许是我没有看懂原作者的用意,如你对此有不同的理解,欢迎与我交流。

继续回到源代码,当 Reader 创建成功后,会直接调用 sourcepumpDataEntries 方法,实现文件解析与类文件读取,从源码中可以看到,在 pumpDataEntries 中,是直接调用前面使用工厂模式创建出来的 Reader 实例中的 read 方法:

public void pumpDataEntries(DataEntryReader dataEntryReader) throws IOException {readFiles(directory, dataEntryReader);
}private void readFiles(File file, DataEntryReader dataEntryReader) throws IOException {// 直接调用 read 方法dataEntryReader.read(new FileDataEntry(directory, file));// 如果是文件夹,则遍历读取所有的子文件if (file.isDirectory()) {File[] listedFiles = file.listFiles();for (int index = 0; index < listedFiles.length; index++) {File listedFile = listedFiles[index];readFiles(listedFile, dataEntryReader);}}
}

在前面的例子中,传入的 jar 文件,最后返回的 Reader 就是 JarReader, 而它会将传入的文件进行解压读取,并使用 dataEntryReader 去读取压缩包中的其它文件,代码如下:

public void read(DataEntry dataEntry) throws IOException {// 省略部分代码FileDataEntry fileDataEntry = (FileDataEntry)dataEntry;// 处理 zip 文件ZipFile zipFile = new ZipFile(fileDataEntry.getFile(), StandardCharsets.UTF_8);try {Enumeration entries = zipFile.entries();// 读取压缩包中的所有文件while (entries.hasMoreElements()) {ZipEntry zipEntry = (ZipEntry)entries.nextElement();// 转换成真实的 reader 去读取类容。dataEntryReader.read(new ZipFileDataEntry(dataEntry, zipEntry, zipFile));}} finally {zipFile.close();}// 省略部分代码
}

在本例中,R.jar 文件包含的内容如下图所示:

ff1421aa0a1683622d72344d9d2a9fb3.png
R.jar 文件内容

当读取到此文件的第一个 ZipEntrycom/example/demo/R$style.class 文件时,源码会调用dataEntryReader 去读取内容,根据前面创建Reader 的流程,可以知道当前的 dataEntryReaderFilteredDataEntryReader ,它在执行读取时,会根据当前文件的后缀名去处理,如果后缀名匹配, 则会使用 acceptedDataEntryReader 去处理,反之会使用 rejectedDataEntryReader 去读取文件:

75c195282a2bda2356e5fd73342e2289.png
FilteredDataEntryReader 读取

因此,com/example/demo/R$style.class 文件的读取会一直嵌套调用,直到可以处理 class 文件的 ClassReader ,文件读取逻辑如下(图中红色部分):

f4ba98a48867ad2e9d4a07db2d514ff0.png
文件读取嵌套逻辑

二、CLASS 文件的读取与解析

在之前的文章 《深入 Android 混淆实践:多模块打包爬坑之旅 》中,使用了 ASM 去解析 class 文件,而在 ProGuard 中,自己实现了一套,源代码在开源库 proguard-core 中。

Java 的 class 文件格式在 JVM 规范中,有明确的定义,不论是在开源库 ASM 中,还是在 proguard-core 中,实现对 class 文件的读取与处理,都使用了访问者模式,有关访问者模式,将在后面的文章进行详细的讲解。下面在来看看 ClassReader 里面干了些什么事情。

public void read(DataEntry dataEntry) throws IOException {try {// 获取当前数据流InputStream inputStream = dataEntry.getInputStream();// 在包一层,使用 DataInputStreamDataInputStream dataInputStream = new DataInputStream(inputStream);// 创建 ProgramClassClazz clazz = new ProgramClass();// 创建访问者 ProgramClassReaderClassVisitor programClassReader = new ProgramClassReader(dataInputStream,ignoreStackMapAttributes);// 调用 accept 方法,实现派发,让 programClassReader 执行 visitProgramClass 方法clazz.accept(programClassReader);// 如果解析 class 成功String className = clazz.getName();if (className != null) {// 省略部分代码// 用过 Visistor 模式,将 ProgramClass 添加到 AppView 的 programClassPool 中clazz.accept(classVisitor);}dataEntry.closeInputStream();} catch (Exception ex) {// ......}
}

代码中,通过访问者模式,触发 ProgramClassReader  从输入数据流中读取 class 文件的内容。此处读取的逻辑相对比较简单,按照 class 格式定义,按字节读取就可以了。我将 CLASS 格式的定义和读取代码做了一个截图,可以对比看看,加深理解。

21ca2fda3e0dc5e9d68e687bb4f556bc.png
class文件读取逻辑,左测为读取代码,右测为代码结构

结语

在本文的开头,提到的 AppView 就是用来存储类数据的,代码逻辑会将输入参数中的 programJarslibraryJars 里包含的所有类解析出来,构建生成 ProgramClass,分别存储在 AppView 这个类中的 programClassPool 以及libraryClassPool 中, 用于后续混淆使用。当然,除了 class 文件,还存在一些资源文件的读取逻辑,如果你感兴趣,可以去翻翻源码。

以上为 ProGuard 中 Java 类文件的读取与解析的内容,如果本文中有描述得不清楚或不对的地方,欢迎各位朋友一起交流讨论。


http://www.ppmy.cn/news/510204.html

相关文章

MongoDB非关系型数据库

MongoDb安装部署 以下是在CentOS 7上安装部署 MongoDB 的详细步骤&#xff1a; 1. 搭建 MongoDB 的yum源。 执行以下命令添加 MongoDB 的yum源&#xff1a; sudo vi /etc/yum.repos.d/mongodb-org-4.4.repo 在文件中添加以下内容&#xff1a; [mongodb-org-4.4] nameMong…

第八章 MobileNetv3网络详解

系列文章目录 第一章 AlexNet网络详解 第二章 VGG网络详解 第三章 GoogLeNet网络详解 第四章 ResNet网络详解 第五章 ResNeXt网络详解 第六章 MobileNetv1网络详解 第七章 MobileNetv2网络详解 第八章 MobileNetv3网络详解 第九章 ShuffleNetv1网络详解 第十章…

PHP开发的简洁的导航网站源码多种主题风格切换

☑️ 编号&#xff1a;ym331 ☑️ 品牌&#xff1a;无 ☑️ 语言&#xff1a;php ☑️ 大小&#xff1a;7.9MB ☑️ 类型&#xff1a;导航网站源码 ☑️ 支持&#xff1a;PCwap &#x1f389; 欢迎关注&#xff0c;私信&#xff0c;领取 &#x1f389; ✨ 源码介绍 一套PHP开发…

美国 导航 android,首款Android智能导航手机A50现身美国市场

(4月21日&#xff0c;华盛顿讯) 全球智能导航手机领导品牌Asus(华硕集团)今日宣布&#xff0c;将与美国电信巨擘T-Mobile携手&#xff0c;推出Asus全球首款采用Android平台的智能导航手机A50合作方案&#xff0c;结合ASUS智能技术、Garmin的专业导航与T-Mobile的服务网络&#…

简约大气昼夜双色导航主题模板/WordPress导航主题模板

简约大气昼夜双色导航主题模板/WordPress导航主题模板 ☑️ 编号&#xff1a;ym442 ☑️ 品牌&#xff1a;WordPress ☑️ 语言&#xff1a;php ☑️ 大小&#xff1a;686KB ☑️ 类型&#xff1a;导航主题模板 ☑️ 支持&#xff1a;pcwap &#x1f389; 欢迎关注(发消息才不限…

WordPress导航主题/酷啦鱼导航主题模板

☑️ 编号&#xff1a;ym361 ☑️ 品牌&#xff1a;WordPress ☑️ 语言&#xff1a;php ☑️ 大小&#xff1a;3.6MB ☑️ 类型&#xff1a;导航主题 ☑️ 支持&#xff1a;pcwap &#x1f389; 欢迎关注&#xff0c;私信&#xff0c;领取 &#x1f389; ✨ 源码介绍 酷啦鱼导…

JS实现电梯导航

首先明确需求 这里我想做的是一个侧边电梯导航,可以实现点击楼层按钮文字跳转到相应的区域,同时楼层按钮高亮显示,在最顶部时,隐藏电梯导航栏,当页面滚动到下方是,显示导航栏,随着页面的滚动,导航栏按钮跟着内容变化, html部分 这里直接那之前写的网页添加一个侧边电梯导航 …

安卓导航车机root方法_上手飞歌X2 你会知道什么是真正的智能车机

​只懂车最近接触了多款不同品牌的新品车机,发现今年各大车机厂商将主攻音响皇帝位、QLED屏、360全景等几大功能,但能让小编记住的目前只有飞歌X2,因为它相比其他品牌型号,在各新功能上进行了不同程度的提升,使用上更智能化、人性化,今天就带你领略一番。 豪华的包装给小…