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

news2024/11/19 17:19:49

书接上文,当我们从用户的配置文件中读取到所有的配置信息后,下一步便是将配置中的指定的类文件进行读取,构建需要混淆的 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) {
 // 不管当前格式是什么,直接创建 JarReader
  DataEntryReader jarReader = new JarReader(stripJmodHeader, reader);

  if (isJar) {
    // 如果当前需要读取的文件格式是对应后缀格式,直接返回 
    return jarReader;
  } else {
    // 创建一个后缀匹配器
    StringMatcher jarMatcher = new ExtensionMatcher(jarExtension);
  // 返回一个格式判断的 Reader
    return 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();

        // 在包一层,使用 DataInputStream
        DataInputStream dataInputStream = new DataInputStream(inputStream);
    // 创建 ProgramClass
        Clazz clazz = new ProgramClass();
        // 创建访问者 ProgramClassReader
        ClassVisitor 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.coloradmin.cn/o/669789.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大一下暑期计划 + 2023字节青训营预告直播

目录 &#x1f33c;前言 &#x1f339;后端学习方法 &#x1f333;1&#xff0c;层次 &#x1f333;2&#xff0c;体系 &#x1f333;3&#xff0c;算法和数据结构 &#x1f333;4&#xff0c;总结 &#x1f339;前端学习方法 &#x1f333;基础 &#x1f339;求职中如…

如何在Microsoft Excel中使用LEN函数

如果你曾经想尝试查找一行文本中的字符数&#xff0c;你可以使用Microsoft Excel来查找&#xff0c;这要归功于LEN函数。以下是如何使用它。 一、什么是 LEN 函数 LEN函数是一个简单的计算函数&#xff0c;用于计算给定文本字符串中的所有字符&#xff0c;包括数字、字母、特…

【数据库课设】图书馆资源管理系统 源码+流程图+结构设计(借还图书 逾期罚款 图书管理 读者管理 信息查询)python实现

文章目录 一 实现功能&#xff08;1&#xff09;管理员功能&#xff08;2&#xff09;读者功能 二 数据流图三 概念结构设计四 文件目录五 源码&#xff1a;main.py六 运行截图 一 实现功能 &#xff08;1&#xff09;管理员功能 一个管理员编号对应一个密码&#xff0c;且需…

redis—安装以及可视化

前言 redis 是一种非关系型数据库&#xff0c;什么是非关系型数据库&#xff0c;之前我们在mysql专栏 也有提到过&#xff0c;这边就不再过多的赘述&#xff0c;忘记了的小伙伴可以再次阅读这篇文章 终于明白了数据库的【关系型】与【非关系型】 其实这还是挺重要的&#xff…

ruoyi+vue回显数字的问题,解决方案

在项目中用ruoyi框架和前端vue进行开发&#xff0c; 需求是在前端生成下拉框&#xff0c;下拉框中的内容需要调用后端接口进行数据返回&#xff0c; 现在新增的时候&#xff0c;数据已经返回了&#xff0c;但是再修改的时候&#xff0c;进行回显数据导致前端列表中展示出来的…

城市排水监测系统为城市排水防涝提质增效

城市化进程中&#xff0c;城市排水系统成为城市基础设施建设的重要组成部分。然而&#xff0c;随着气候变化和城市建设规模的扩大&#xff0c;极端天气和内涝风险不断增加&#xff0c;城市的排水系统面临巨大挑战。因此&#xff0c;建立一套智能化城市排水监测系统&#xff0c;…

【python】一些常用的pandas技巧

有了gpt之后&#xff0c;确实很多代码都可以让gpt给改错。嘎嘎香 merge多个dateframe https://stackoverflow.com/questions/44327999/how-to-merge-multiple-dataframes data_net [a,b,c,d] net_merged reduce(lambda left,right: pd.merge(left,right,on[key column],ho…

小程序安全指南:保护用户数据的最佳实践

第一章&#xff1a;引言 近年来&#xff0c;小程序已成为移动应用开发的重要组成部分。它们为用户提供了方便的功能和个性化的体验&#xff0c;然而&#xff0c;与此同时&#xff0c;小程序安全问题也引起了广泛的关注。保护用户数据是开发者应该高度重视的问题。在本指南中&a…

JavaScript ES11新特性

文章目录 导文可选链操作符&#xff08;Optional Chaining Operator&#xff09;空值合并操作符&#xff08;Nullish Coalescing Operator&#xff09;动态 import() 函数BigInt 类型Promise.allSettled() 导文 JavaScript ES11&#xff0c;也被称为ES2020&#xff0c;引入了一…

经纬度、时差知识整理(理解与应用)

经纬度是经度与纬度的合称组成一个坐标系统&#xff0c;称为地理坐标系统&#xff0c;它是一种利用三度空间的球面来定义地球上的空间的球面坐标系统&#xff0c;能够标示地球上的任何一个位置。初一的同学在学地理这门课的时候&#xff0c;一上来很快就会学到经纬度这个概念。…

PC市场寒冬,大众还需要PC吗?

PC市场寒冬&#xff0c;大众还需要PC吗&#xff1f; PC&#xff08;个人电脑&#xff09;市场从2016年智能手机兴起之时便进入下滑态势&#xff0c;到2020年疫情发生后&#xff0c;居家办公、在线教育等需求曾给PC市场带来连续六个季度的增长。⁴ 好景不长&#xff0c;进入202…

mybatisplus分页total总数为0

mybatisplus分页total总数为0 背景&#xff1a;最近初始化新项目时&#xff0c;使用mybatisplus分页功能发现 records 有记录&#xff0c;但是 total 总是为0&#xff0c;于是开启了一顿“知识寻求”之路SpringBoot版本 <parent><groupId>org.springframework.boo…

Makerbase CANable V2.0 固件升级或替换

第1部分 应用软件与固件 应用软件CANable V2.0CANable V1.0cangaroocandleLight/slcan(支持CAN FD)candleLight/slcan/cantactBUSMASTER V3.2.2candleLightcandleLight/pcan/cantactTSMastercandleLightcandleLight/pcan/cantactPCAN-Explorer 5、pcan view不支持pcancantacts…

一文带你全面理解 MySQL 中的常用函数

0️⃣前言 MySQL是一种常用的关系型数据库管理系统&#xff0c;它提供了许多内置函数来处理数据。本文将介绍MySQL中的各种常用函数&#xff0c;包括字符串函数、日期函数、数学函数、聚合函数等。 文章目录 0️⃣前言1️⃣字符串函数1.1CONCAT函数1.2SUBSTRING函数1.3REPLACE函…

建设一个网站需要多长时间?

&#x1f482; 个人网站:【海拥】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 目录 前言网站建设的基本步骤…

【Leetcode60天带刷】day10栈与队列—— 232.用栈实现队列,225. 用队列实现栈

题目&#xff1a; 232. 用栈实现队列 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作&#xff08;push、pop、peek、empty&#xff09;&#xff1a; 实现 MyQueue 类&#xff1a; void push(int x) 将元素 x 推到队列的末尾int pop() 从队列的开头移…

Spring Boot 集成 Redisson分布式锁

Redisson 是一种基于 Redis 的 Java 驻留集群的分布式对象和服务库&#xff0c;可以为我们提供丰富的分布式锁和线程安全集合的实现。在 Spring Boot 应用程序中使用 Redisson 可以方便地实现分布式应用程序的某些方面&#xff0c;例如分布式锁、分布式集合、分布式事件发布和订…

旷视研究院获 CVPR 2023 自动驾驶国际挑战赛 OpenLane Topology 赛道冠军

近日&#xff0c;为期三个月的 CVPR 2023 自动驾驶国际挑战赛比赛结果揭晓。旷视研究院在OpenLane Topology 赛道中击败 30 余支国内外队伍&#xff0c;夺得冠军。 自动驾驶技术已经渗透到人们的日常生活中&#xff0c;但是传统的感知方法仍不能满足大家对 L5 级自动驾驶的追逐…

傻瓜式教程--基于FPGA的CYUSB3014双向通信

一、概述 CYUSB3014是赛普拉斯在近几年推出的新一代USB3.0的外设控制器&#xff0c;可以解决USB2.0带宽限制&#xff0c;或者单独开发USB协议和驱动的难题。赛普拉斯将CYUSB3014简称为EZ-USB FX3&#xff0c;具有高度的灵活特性&#xff0c;开发人员只需要下载FX3的固件库&…

电脑死机怎么办?试试这5个方法!

案例&#xff1a;我的电脑用着用着突然死机了&#xff0c;我的文档写到了一半没有保存&#xff0c;不知道还能不能找回&#xff1f;电脑死机了怎么办&#xff1f;有没有小伙伴有应对的方法&#xff1f; 随着电脑在我们生活中的普及和广泛应用&#xff0c;电脑死机问题也成为了…