认识jclasslib
和Hello world
那年那个熟悉的 Hello world
还记得 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 文件
也可以直接使用 java <类全路径>
直接运行查看效果,类全路径:包名 + 类名
java 命令直接运行看着很简单,但有时候不起眼的往往适得其反;
举个栗子
Test 启动类中写的包名是 package primer
,我本地目录是这样primer/primer/Test.class
1、在 class 所在目录下运行【错误】
如果你在 Test.class 同级目录下执行命令运行,可惜报错了,大概意思是你的 Test 类全路径不对;java 命令认为在当前目录下找文件夹primer
,如果文件夹存在,再找类 Test;那就错了,文件夹(包)都没有找到。
2、在 class 所在目录下指定类名运行【错误】
根据第一点,有人可能会问:这不是在找 primer 文件夹吗?那我不去找文件夹了,直接执行字节码文件名 Test 不就好了吗?
答:不行,真不行
前面提到了,直接执行字节码是这样 java <类全路径>
,全路径是要包含包名在内的,比如我们常见的 com.tencent.mmkv.MMKV
JVM 在加载类过程中是如何确定唯一个类的? 比如有两个同名但不同包的 Test 类,JVM 如何识别? 那就需要包名限定符 + 是否同一个虚拟机
唯一确定(一般情况只运行一个虚拟机)。
3、在 class 启动类最外层包名的上一层目录运行【正确】
比如在我的例子中就是在第一个 primer 文件夹下运行即可
邂逅的 jclasslib
从这里开始就要涉及到具体的 JVM 指令操作,先奉上 ORACLE 官方文档-JVM 指令集
全部指令记住一般不会这么干,所以 jclasslib 工具可以右击跳转到官方文档特定指令位置show JVM spec
那么,我们一起上面的Hello World
为例子进行实际操作。
1、修改字符串常量
先看 main 方法的 java 代码,输出的第一个字符串常量指定是 Hello World
再看 main 方法的 class 字节码
javap 命令可以直接查看 Java 文件所对应的字节码文件哦
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 编译的过程)
思维扩散:
在我们的记忆中,字面量相同的字符串常量在常量池中是仅存在一份内容的。
如下代码"张三"
字符串在常量池中有且只有一份,但是程序中多处引用,直接修改常量池中的值那么这两个输出的都会改变了,我希望只改变name
的输出,保持aliasName
的输出。
- 直接修改常量池,确实两个都受影响,不符合期望
直接修改常量池不符合我们的期望,那如何操作满足需求呢?
夜夜失眠的,为什么总是你,想了又想,修改字符串常量池似乎不行?
字符串常量在 JVM 中是只有一份,该常量存储的位置可能就不一样,有的在堆内存、有的在栈内存。所以呢,上述例子想通过修改常量池李四
字符串是行不通的。
那我只能妥协一下在常量池中创建一个新的字符串常量并引用
,那就是通过修改 java 代码。这里我们只有一个 class文件,需要把它转换成 java 文件修改完毕之后再编译成 class 文件运行。
1、使用 JD-GUI 工具打开 class 文件,并导出为 java 文件
2、修改完毕,重新使用 javac 编译
如果你有其他方法,欢迎评论。
2、修改 for 循环次数
① 第一种类型的 for 循环
iconst_<
n
>:把值压入操作数堆栈istore_<
n
>:弹出并获取操作数堆栈栈顶顶的值,并将其值存储到本地变量iload_<
n
>:从本地变量获取值if_icmp:如果比较成功则执行后续指令
dup:复制操作数堆栈上的顶部值,并将复制的值推送到操作数堆栈上
iinc
index
byvalue
: 按照 value 自增
对于此种方式的 for 循环,我们可以修改自增量value
来减少循环执行次数
② 第二种类型的 for 循环
bipush:将值被推送到操作数堆栈上
对于此种方式的 for 循环,我们可以修改final int MAX_COUNT(bipush 的值)
来跳过或减少循环执行次数
③ 第三种类型的 for 循环
anewarray:创建数组引用
aastore:把值存储到数组列表中
对于此种方式的 for 循环,我们可以修改iinc
自增量value
来减少循环执行次数
不妨尝试修改 smail 的某个变量
上述算是实现了如何简单修改 class 文件中的某个常量,并不复杂。
但是呢?有时候反编译 apk 我们是直接使用 apktool
工具,反编译得到的是 smail
代码,难不成还想把 smail
转换成 class 再修改,可麻烦了。
不妨试试直接修改 mail ,然后回编看看效果。
工欲善其,事必先利其器
在 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
1、使用 apktool 反编译获得 smail
当遇到 dex 反编译错误时候,可以使用参数 --only-main-classes
java -jar apktool_2.6.1.jar d <apk 文件> --only-main-classes
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 源码:
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>