前言

在Java开发的过程中,一般是通过javac工具把Java代码编译成字节码。在编译的过程中,每一种语法都会有一一对应的指令组合起来。只要熟悉这一对应关系,就完全能够通过字节码猜出它对应的Java代码。这时,我们可以通过指令等价替换或者增加无意义的花指令的方式,把正常的写法改成javac不会产生的写法,从而加大破解分析的难度。

指令替换

指令替换意为指令的等价替换,即把一条或多条指令替换为等价的一条或多条指令。

指令替换的方式多种多样,它有可能会增加指令的数量,也有可能会减少指令的数量。有可能会增大或减少方法代码的字节数,有可能会加快或减低代码的执行速度。

下面我讲解几种指令替换方案:

1)pop -> Xstore(Xstore指的是istore、fstore、astore等系列指令)

我们知道pop指令是用于把栈顶的元素弹出不使用,现在我们使用Xstore指令把栈顶的值存放到一个无用的临时变量中,也能达到同样的目的。

这种写法能够误导分析人员,让他们以为此临时变量后面会用到,使得分析时需要下更多的功夫。

由于POP指令比Xstore指令的字节数要少,所以会增大代码体积。另外执行效率也会降低一点,但实际影响不会很大。

另外我们需要知道当前栈顶中是什么类型的元素,以便于使用对应的Xstore指令。所以编码时需要注意这一点。

2)isub -> ineg iadd

如:a=1-a;转为a=1+(-a);

这种写法不用解释了吧。

3)ifeq -> switch case 0:

这种写法无法直接通过Java代码编译出来,因为ifeq接收的是个boolean值,而switch接收的是int值,在语法上不通过,但在bytecode中boolean与int都可作为int值来处理,所以不会有问题。

同理,switch语句也可以写成多个if_icmpeq来判断。

4)iconst_2 imul -> iconst_1 ishl

如:a=a*2;转为a=a<<1;

我们知道,a=a*(2^n)可以转换为a=a<<n,a=a/(2^n)可以转换为a=a>>n。利用这特性,我们可以对这类字节码进行等价替换。我们知道左移的执行速度是比乘法的执行速度要快很多的,所以这种方式能优化代码的执行效率。

指令替换的方案多种多样,通过指令替换,可以在不影响程序的正常执行的情况下,生成一些奇怪的写法,从而达到干扰破解分析的目的。

花指令

花指令意为无意义的指令,一般用来干扰分析。花指令需要保证堆栈平衡,且不影响程序的正常逻辑。花指令的设计因人而异,这里给一些我的思路:

1)连续且不受限制的花指令:

1
iconst_0 pop

2)连续但受限制的花指令:

假设当前栈顶有一个整型值,n为可用的本地变量,那么我们可以写出类似如下指令(随便写的,不影响堆栈平衡以及使用中的本地变量的):

1
istore_n iload_n iconst_1 iadd iload_n swap pop

3)不连续的花指令:

这里所说不连续的花指令是指一些有关联但不是全部连续在一起的多个指令。

后面我们有一章节会讲到代码乱序的原理与实现,代码乱序的原理其实就是使用了不连续的花指令。

接下来我举个非常简单的例子。

假设该方法的返回值为int,在代码开始前加入如下花指令:

1
iconst_1

在代码返回指令ireturn前加入如下指令:

1
swap pop

花指令可以写得很复杂,也可以写得很简单。

例如如果你把一个只有一个return指令的方法改写成一个执行一堆无意义逻辑的代码,甚至这些无意义的代码还产生了一个结果(假设你这个结果必然是0)保存在一个自己增加的无意义成员变量中,让人看起来这个成员变量是有用的。之后在另一个方法中判断这个成员变量是否为0,如果不为0,则执行一些会影响到正常逻辑的操作。而由于上面说必然是0,所以这个这些指令将永远执行不到。

不明白?下面我用代码举例一下:

首先假设有如下类:

1
2
3
4
5
6
7
8
9
10
public class Flower {
// 花瓣的数量,默认为5个
public int size = 5;
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
}

这个类很简单吧,就其实就一对get/set方法。我们看下如何改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Flower {
private int size = 5;
private int a;
public int getSize() {
int count = 5;
for(int i=-1;i<Math.max(size, 1);++i) {
count *= i;
}
// a本来是0,count怎么算还是0,所以a必为0
a = count - a;
return size;
}
public void setSize(int size) {
// a必为0,所以这里永远不会执行
if(a > 0) {
size = size * size;
}
this.size = size;
}
}

这样,本来很简单的一对set/get方法,分析起来也变得有些麻烦了。这样的花指令,是比较难自动化去掉的。

我们可以看到,花指令其实就是增加一些无意义的代码,使分析变得更加麻烦。当然,有攻自然就有防,有兴趣的童鞋可以试着写一个自动化去掉花指令的工具,让分析变得更简单。