背景:
公司要求修改以前的项目调用的代码,但是发现代码已经丢失了只剩下jar包了,想起来以前学习JVM的Javap,已经反编译jar包在此我都尝试了一下做一下记录方便以后遇到
一、常规Jar包修改流程
1、定位问题
- 通过通过procmon监控相关软件,查看程序都访问了些啥。
- 用反编译软件如jd-gui、或者idea自带的反编译jar包得到源码
- 搜索关键词去进行定位
2、修改文件
- 用dex2jar将JAR包转成Dex文件
- 再将Dex解出Smali
- 修改Smali代码
- 将修改以后的文件重新打包成Dex文件
- 最后转成JAR包
二、直接修改.class字节码的方式
首先我们需要一个工具就是jclasslib 地址:https://link.zhihu.com/?target=https%3A//github.com/ingokegel/jclasslib
或者我门在idea中可以直接搜索jclasslib bytecode viewer、如果对javap感觉很熟悉的话javap也可以
jclasslib可以通过直接点击字节码查看当前字节码是什么,如果有兴趣的话可以看看《Java虚拟机规范》、《深入理解Java虚拟机》
学习下面代码之前我们需要先补充一下虚拟机的一个基础
我们都知道程序计数器是标记代码执行的行号方便在线程切换后恢复到正确的执行位置,这一点在Javap生成的以及classlib中有体现
如果我们使用javap的话
$ javac Demo.java
$ javap -p -v Demo
如果文件太大的话可能控制台打印不下,我们这时候可以用shell将内容添加到文本文件中去
javap -p -v Main >> Main.txt
有时,class文件中不会生成linenumberable或localvariabletable,编译期间,可以使用以下参数强制生成:
javac -g:lines 强制生成LineNumberTable。
javac -g:vars 强制生成LocalVariableTable。
javac -g 生成所有的debug信息。
LocalVariableTable就是栈帧中的局部变量表。
Linenumbertable描述源代码行号和字节码行号(字节码偏移量)之间的对应关系。通过这些信息,在debug时,就能够获取到发生异常的源代码行号
如果嫌弃麻烦的话,可以直接使用classlib比如我们想分分析下面代码对应的源文件
public class Demo {
private int a = 1111;
static long C = 2222;
public long test(long num) {
long ret = this.a + num + C;
return ret;
}
public static void main(String[] args) {
new Demo().test(3333);
}
}
test方法的执行过程
test方法同时使用了成员变量a、静态变量C,以及输入参数num。我们此时说的方法执行,内存其实就是在虚拟机栈上分配的。下面这些内容,就是test方法的字节码。
test方法同时使用了成员变量a、静态变量C,以及输入参数num。我们此时说的方法执行,内存其实就是在虚拟机栈上分配的。下面这些内容,就是test方法的字节码。
public long test(long);
descriptor: (J)J
flags: ACC_PUBLIC
Code:
stack=4, locals=5, args_size=2
0: aload_0
1: getfield #2 // Field a:I
4: i2l
5: lload_1
6: ladd
7: getstatic #3 // Field C:J
10: ladd
11: lstore_3
12: lload_3
13: lreturn
LineNumberTable:
line 7: 0
line 8: 12
说明:
- Stack=4:表示test方法的最大操作数栈深度为4。当JVM运行时,它将根据这个值在栈框架中分配操作栈的深度。
- Locales=5:可以重用以slot为单位的局部变量的存储空间。内容包括:this、方法参数、异常处理程序参数和方法体中定义的局部变量。
- args_size=2:方法的参数个数。因为每个实例方法都有一个隐藏参数this(静态方法没有this),所以这里的数字是2。
字节码执行过程
0: aload_0
将第1个引用的局部变量推到操作数栈,这意味着将this加载到操作数栈中。
对于static方法,aload_0表示对该方法的第一个参数的操作。
1: getfield #2
将指定对象的第2个实例域(Field)的值压入栈顶。#2就是我们的成员变量a。
4: i2l
将栈顶int类型的数据转换为long类型。这里就涉及我们的隐式类型转换了。
6: ladd
将堆栈顶的两个long型数值相加,并将结果放入栈中。
7: getstatic #3
根据偏移获取静态属性的值,并将该值push到操作数栈中,即静态变量C。
10: ladd
再次执行ladd。
11: lstore_3
将栈顶long型数值存储到第4个局部变量中,一个long和double类型会占用2个slot。
在这里,我们为什么要将栈顶的变量存储到局部变量表中,然后又将它们取出并放到栈中呢?原因是我们定义了RET变量。JVM不知道它以后是否会使用这个变量,所以它只好按照傻瓜的顺序执行。
我们都知道代码的运行速度和编译成汇编之后的的多少成正比,所以我们有必要去了解java相关虚拟机的知识,并且知道我们怎么写会造成隐形类型转换等导致的运行速度慢等问题
为了看到差异,我们可以稍微修改一下代码,然后直接返回:
public long test(long num) {
return this.a + num + C;
}
对应的字节码如下:
public long test(long);
descriptor: (J)J
flags: ACC_PUBLIC
Code:
stack=4, locals=3, args_size=2
0: aload_0
1: getfield #2 // Field a:I
4: i2l
5: lload_1
6: ladd
7: getstatic #3 // Field C:J
10: ladd
11: lreturn
LineNumberTable:
line 7: 0
12: lload_3
把第3个局部变量放入栈,这是我们的参数num,其中l代表long。
13: lreturn
从当前方法返回long。
修改字节码
当我们可以读懂他的过程的适合我们就可以通过修改他JVM指令来达到我们修改函数目的,如果只是简单替换字符串的话我推荐使用classlib在这个工具里面我们可以清楚的看到他值的储存位置
如果我们需要修改字符串或者一些变量的话一般都会放在常量池中
这种方法其实比较适合修改一些字符串等,但是我们可以借助jclasslib自带的一些方法来针对指定行号进行修改
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import org.gjt.jclasslib.io.ClassFileWriter;
import org.gjt.jclasslib.structures.CPInfo;
import org.gjt.jclasslib.structures.ClassFile;
import org.gjt.jclasslib.structures.InvalidByteCodeException;
import org.gjt.jclasslib.structures.constants.ConstantUtf8Info;
public class Client
{
@SuppressWarnings("deprecation")
public static void main(String[] args){
String filePath = "C:\\Users\\zw\\Desktop\\Client.class";
FileInputStream fis = null;
try {
fis = new FileInputStream(filePath);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
DataInput di = new DataInputStream(fis);
ClassFile cf = new ClassFile();
try {
cf.read(di);
} catch (InvalidByteCodeException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
CPInfo[] infos = cf.getConstantPool();
int count = infos.length;
for (int i = 0; i < count; i++) {
if (infos[i] != null) {
System.out.print(i);
System.out.print(" = ");
try {
System.out.print(infos[i].getVerbose());
} catch (InvalidByteCodeException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.print(" = ");
System.out.println(infos[i].getTagVerbose());
if(i == 174){ //修改第174行
ConstantUtf8Info uInfo = (ConstantUtf8Info)infos[i];
uInfo.setBytes("aaa".getBytes()); //找到字符为aaa的
infos[i]=uInfo;
}
}
}
cf.setConstantPool(infos);
try {
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
File f = new File(filePath);
try {
ClassFileWriter.writeToFile(f, cf);
} catch (InvalidByteCodeException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
相关jar包在插件目录下的lib中引入就可以了
除了这种我们还可以直接修改反编译的,反编译代码少量修改再将其环境搭建起来运行再编译成class直接解包jar文件,替换完再打包也可以。少量反编译还是可以的
除了这两种还可以直接修改class文件的二进制编码也能达到类似的效果