【你 & 我 & 字节码】的一次黄昏邂逅

在这里插入图片描述

认识jclasslibHello world

那年那个熟悉的 Hello world

工具下载地址:jclasslib tool from github

还记得 N 年前,在哪夜黑风高的夜晚,自己手动完成的第一个 java 程序吗?想必很多人的第一个 java 程序都极为相似,从此踏上了一条 不归路

Test.java

package primer;

//比如 Test 类的组成:
//主版本号 + 常量池 + 访问标识 + 当前类签名 + 父类签名 + 接口集合 + 方法集合 + 字段集合 + 属性集合(仅列举部分)
public class Test{
    //方法一:<init>()V   【默认构造器】
    
    //方法二:main([Ljava/lang/String;)V   【main 方法、V 表示 void、L 表示数组、String 变成类的全路径】(仅列举部分)
    
    //比如 main 方法(仅列举部分)
    // 组成: 
    //    - 方法名 + 方法签名 + 访问标识(main + ([Ljava/lang/String;)V + public static)
    //    - 异常表
    //    - 字节码【我们讨论的重点】
    
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

javac Test.java 编译得到 Test.class 文件

image.png

也可以直接使用 java <类全路径> 直接运行查看效果,类全路径:包名 + 类名


java 命令直接运行看着很简单,但有时候不起眼的往往适得其反;

举个栗子

Test 启动类中写的包名是 package primer,我本地目录是这样primer/primer/Test.class

image.png

1、在 class 所在目录下运行【错误】

如果你在 Test.class 同级目录下执行命令运行,可惜报错了,大概意思是你的 Test 类全路径不对;java 命令认为在当前目录下找文件夹primer,如果文件夹存在,再找类 Test;那就错了,文件夹(包)都没有找到。

image.png

2、在 class 所在目录下指定类名运行【错误】

根据第一点,有人可能会问:这不是在找 primer 文件夹吗?那我不去找文件夹了,直接执行字节码文件名 Test 不就好了吗?

答:不行,真不行

image.png

前面提到了,直接执行字节码是这样 java <类全路径>,全路径是要包含包名在内的,比如我们常见的 com.tencent.mmkv.MMKV

JVM 在加载类过程中是如何确定唯一个类的? 比如有两个同名但不同包的 Test 类,JVM 如何识别? 那就需要包名限定符 + 是否同一个虚拟机唯一确定(一般情况只运行一个虚拟机)。

3、在 class 启动类最外层包名的上一层目录运行【正确】

比如在我的例子中就是在第一个 primer 文件夹下运行即可

image.png

邂逅的 jclasslib

从这里开始就要涉及到具体的 JVM 指令操作,先奉上 ORACLE 官方文档-JVM 指令集

全部指令记住一般不会这么干,所以 jclasslib 工具可以右击跳转到官方文档特定指令位置show JVM spec

那么,我们一起上面的Hello World为例子进行实际操作。

image.png

1、修改字符串常量

先看 main 方法的 java 代码,输出的第一个字符串常量指定是 Hello World

image.png

再看 main 方法的 class 字节码

javap 命令可以直接查看 Java 文件所对应的字节码文件哦

image.png

getstatic: 获取静态字段 out ;System 类中的 out 声明是 public final static PrintStream out = null;

ldc:从常量池中获取值;此处常量便是 Hello World

invokevirtual:类级别的方法调用;可以是通过类名调用方法 System.out.println()

new: 创建 Hello 类的实例

dup:

invokespecial: 调用实例方法

astore_1: 引用存储的实例

aload_1: 加载实例

invokevirtual: 调用实例方法

return: 方法退出

在字节码层面修改 ldc读取的常量值,保存并重新编译运行(工具上修改免去了手动使用 javac 编译的过程)

image.png

image.png


思维扩散:

在我们的记忆中,字面量相同的字符串常量在常量池中是仅存在一份内容的。

如下代码"张三"字符串在常量池中有且只有一份,但是程序中多处引用,直接修改常量池中的值那么这两个输出的都会改变了,我希望只改变name的输出,保持aliasName的输出。

  • 直接修改常量池,确实两个都受影响,不符合期望
    image.png

image.png

直接修改常量池不符合我们的期望,那如何操作满足需求呢?

夜夜失眠的,为什么总是你,想了又想,修改字符串常量池似乎不行?

字符串常量在 JVM 中是只有一份,该常量存储的位置可能就不一样,有的在堆内存、有的在栈内存。所以呢,上述例子想通过修改常量池李四字符串是行不通的。

那我只能妥协一下在常量池中创建一个新的字符串常量并引用,那就是通过修改 java 代码。这里我们只有一个 class文件,需要把它转换成 java 文件修改完毕之后再编译成 class 文件运行。

1、使用 JD-GUI 工具打开 class 文件,并导出为 java 文件

2、修改完毕,重新使用 javac 编译

image.png

如果你有其他方法,欢迎评论。

2、修改 for 循环次数

① 第一种类型的 for 循环

image.png

image.png

iconst_<n>:把值压入操作数堆栈

istore_<n>:弹出并获取操作数堆栈栈顶顶的值,并将其值存储到本地变量

iload_<n>:从本地变量获取值

if_icmp:如果比较成功则执行后续指令

dup:复制操作数堆栈上的顶部值,并将复制的值推送到操作数堆栈上

iinc index by value: 按照 value 自增

对于此种方式的 for 循环,我们可以修改自增量value来减少循环执行次数

② 第二种类型的 for 循环

image.png

image.png

bipush:将值被推送到操作数堆栈上

对于此种方式的 for 循环,我们可以修改final int MAX_COUNT(bipush 的值)来跳过或减少循环执行次数

③ 第三种类型的 for 循环

image.png

image.png

anewarray:创建数组引用

aastore:把值存储到数组列表中

对于此种方式的 for 循环,我们可以修改iinc自增量value来减少循环执行次数

不妨尝试修改 smail 的某个变量

上述算是实现了如何简单修改 class 文件中的某个常量,并不复杂。

但是呢?有时候反编译 apk 我们是直接使用 apktool 工具,反编译得到的是 smail 代码,难不成还想把 smail 转换成 class 再修改,可麻烦了。

不妨试试直接修改 mail ,然后回编看看效果。

工欲善其,事必先利其器

apktool 下载

JADX 反编译利器下载

VSCode 下载

VSCode smali2Java 插件

在 vscode 为 smali2Java 配置 jadx.bat 路径:

Decompile failed: The jadx executable path has not been configured

实操吧

1、找到 vscode 插件配置文件

C:\Users\YTS\.vscode\extensions

比如我本地的 smali2java 配置是在

C:\Users\YTS\.vscode\extensions\ooooonly.smali2java-1.0.1\pachage.json

2、找到 jadxPath

image.png

1、使用 apktool 反编译获得 smail

当遇到 dex 反编译错误时候,可以使用参数 --only-main-classes

java -jar apktool_2.6.1.jar d <apk 文件> --only-main-classes

image.png

image.png

3、简约分析
我们修改一下 sayHello() 延时执行时间,目前是 1000 毫秒,1000 的十六进制是 x03e8,sayHello() 所在类是 GameDemoActivity。

在 smail 代码中搜索类名、方法名、x03e8 等可定位代码。比较请求,如果我想修改成延时 5000 毫秒后执行,那把 5000(x01388) 的十六进制替换 x03e8 即可。

java 源码:

private Handler mHandle = new Handler();

private void sayHello() {
    System.out.println("invoke sayHello time = " + System.currentTimeMillis());
    mHandle.postDelayed(new Runnable() {
        @Override
        public void run() {
            System.out.println("execute sayHello time = " + System.currentTimeMillis());
            System.out.println("你好,村长");
        }
    }, 1000);
}

输出:
2022-05-23 18:14:20.134 32431-32431/com.primer.comment I/System.out: invoke sayHello time = 1653300860134
2022-05-23 18:14:21.135 32431-32431/com.primer.comment I/System.out: execute sayHello time = 1653300861135

smail 源码:

image.png

2022-05-23 18:21:48.473 1333-1333/com.primer.comment I/System.out: invoke sayHello time = 1653301308473
2022-05-23 18:21:53.478 1333-1333/com.primer.comment I/System.out: execute sayHello time = 1653301313478

4、最后打包、签名

java -jar apktool_2.6.1.jar b <打包目录>

apksigner sign --ks ****.jks --ks-key-alias <别名> --out <新生成 apk> <待签名 apk>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值