快速修改字节码并重打jar包

4,519 阅读5分钟

背景

不管是做Android项目还是Java后端Web项目,我们一般都会引用各种三方库。遇到特殊需求时,可能需要修改jar包中的代码。本文以实际示例讲解一些基本方法,方便大家快速入坑。

首先我们都知道直接解压jar包的话,都是class二进制文件,打开后是看不见代码的。之所以可以在开发环境中直接查看jar中的代码是因为IDE已经帮你反编译好了,才能看见Java源码。

入坑

如何修改这些jar包中的代码逻辑呢?大致有两种思路:

A、反编译class为java源文件 -> 修改Java源码 -> 重新编译成class文件 -> 将新的class文件覆盖jar包中的原有文件

B、解压得到class文件 -> 直接修改其中的二进制代码(即对应的字节码) -> 将修改后的class文件覆盖jar包中的原有文件

注: 下文基于Java 8环境。

A方法

这种方法也是最常见的方式,也相对较简单,但有一些弊端,后面会讲到。我们直接按步骤来走一遍。

1、先下载 JD-GUI 最新版,得益于Java的跨平台特性,各操作系统都能跑。如果是Windows系统,直接运行exe即可,若是Linux,可以下载deb包进行安装,也可直接java命令运行jar,macOS也是类似,命令运行就行。

2、通过JD-GUI我们打开想要修改的三方库jar包:

在这里插入图片描述

比如这里有一个Util类,其中有一个获取手机IMEI的方法,我想把它改成永远返回空字符串。先把此class文件导出成java文件(选择Save后JD-GUI将自动反编译):

在这里插入图片描述

3、修改Java代码,注意一点就是,在修改这种带参方法时,为了不影响外部调用出错,我们依然保留参数:

在这里插入图片描述

4、保存后我们将刚才的jar包和java文件放在同一个文件,执行命令:

javac -classpath test_a.jar Util.java

恭喜你,你肯定编译失败,会提示各种符号错误。因为这个类import了Android SDK中的相关包,原有的test_a.jar包是不包含的。所以我们同时需要 原jar包 和额外依赖的 android.jar (在你Android SDK目录下的platforms/android-xx文件夹中,xx表示API版本,一般选最新的就好)一起放在同一个目录。

javac -Djava.ext.dirs=/d/Downloads/test Util.java

5、编译产物就是Util.class文件,此时我们用压缩软件打开原test_a.jar包,把新的class文件复制粘贴到对应位置(一般来说用鼠标直接拖进去即可):

在这里插入图片描述

6、检验一下,用JD-GUI重新打开jar包,看看那个方法:

在这里插入图片描述

说明修改成功。

B方法

上述A方法适用于普通的jar包修改,如果一旦jar包在发布编译时, 进行了代码混淆 ,那么解压修改再重新编译就很可能失败。此时我们若能直接修改class文件中的字节码,那就可以无视混淆了。

比如这里我想修改test_b.jar中的一个方法b,让其永远返回空字符串:

在这里插入图片描述

直接解压jar包,用文本编辑器打开class文件后会发现都是十六进制字符,看不懂呀,我又不是机器:

在这里插入图片描述

所以我们需要可以翻译这些编码的工具,有很多类似的。

1、下载 JBE ,它也支持Win和Unix等系统平台,解压后运行其中的bat或sh脚本即可打开。然后我们打开需要修改的class文件:

在这里插入图片描述

从这里可以看见class文件中的各种常量池、成员变量和方法等等。找到我们需要修改的方法所对应的code。可以看见字节码指令已经被清楚地翻译出来。

2、选择Code Editor可以直接修改这些指令。这里简单地介绍一下这里涉及到的指令,ldc将int, float或String型常量值从常量池中推送至栈顶,astore将栈顶引用型数值存入指定本地变量,aload将指定的引用类型本地变量复制到栈顶。后面的数字表示操作第几个常量或变量。

所以这里我们的第0个就是入参,即context,第1个就是defaultAndroidId,以此类推。

ldc ""
astore_1 // defaultAndroidId变量赋值
ldc ""
astore_2 // androidId变量赋值
aload_0 // 开始引用context
invokevirtual android/content/Context/getContentResolver()Landroid/content/ContentResolver; // 并调用它的getContentResolver方法
ldc "android_id"
invokestatic android/provider/Settings$Secure/getString(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String; // 传入常量"android_id",并调用getString方法
astore_2 // 将上述方法调用返回结果赋值给androidId变量
aload_2 // 引用已赋值的变量,传入下面的isEmpty方法
invokestatic android/text/TextUtils/isEmpty(Ljava/lang/CharSequence;)Z
ifeq 15 // 若判断返回true则跳转第15行
ldc ""
astore_2
aload_2 // 这里是第15行指令
areturn

3、搞清楚含义之后,就可以为所欲为了。直接删掉相关逻辑,让方法直接返回空字符串:

在这里插入图片描述

4、点击Save method后,将此修改后的class粘贴到原来的jar包中覆盖旧class即可(和A方法中的最后步骤一样)。用JD-GUI打开检验一下:

在这里插入图片描述

其他注意事项

1、我最开始用的Java 13作为编译环境,但13的javac命令已经不支持-Djava.ext.dirs,目前暂时没有探究替代方法,所以又换回了Java 8。

2、在修改jar包时要确定其编译时所用的Java版本,我们需要与之一致,用JBE可以查看Major Version:

在这里插入图片描述

3、关于字节码指令大全,建议大家参考官方文档(JVM规范,见参考链接),网上搜出来的二次资料可能不保险。

参考