前言

说起混淆,不得不提的是开源工具proguard了。proguard最大的优势在于它会优化你的代码,并删掉无用的类、方法、属性及代码。其次,proguard还会更改你的类名、方法名、属性名,使得反编译后变得难以看懂。但对于有经验的反编译人员或者一些静态分析工具,就变得无能为力了。

而另一款收费软件dexguard则提供了更为强大的功能,dexguard是proguard的作者针对android推出的加强版,其包含了proguard的全部功能,而且还提供了加密的功能,如常量字符串加密、入口类加密、本地库加密、资源文件加密等。此外,它还提供了隐藏敏感API的功能,这功能的实现就是通过反射调用并加密字符串来实现的。它还针对Android做了很多优化,如整合了编译流程,编译起来比proguard更快、自动去掉android的Log日志、自动分Dex等。

反射调用属性

Java的反射调用包括很多方面,如类、属性、方法、注解等,这里我介绍的是针对属性调用与方法调用的反射,下面先介绍反调调用属性。

在bytecode中,获取属性的值使用getstatic和getfield这两条指令,而设置属性的值对应为putstatic和putfield。

假设有如下Java代码:

1
2
3
4
5
6
7
package com.tencent.asm;
public class Main {
static int a = 3;
public static void main(String[] args) {
a += 2;
}
}

其中a+=2;这行代码对应的字节码如下:

1
2
3
4
GETSTATIC com/tencent/asm/Main.a : I
ICONST_2
IADD
PUTSTATIC com/tencent/asm/Main.a : I

这很好理解,那么如果改成反射调用,那么相应的Java代码又是怎样的呢?

1
2
3
4
5
6
7
8
9
package com.tencent.asm;
import java.lang.reflect.Field;
public class Main {
static int a = 3;
public static void main(String[] args) throws Exception {
Field aField = Main.class.getDeclaredField("a");
aField.setInt(null, aField.getInt(null)+2);
}
}

对应的字节码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
LDC Lcom/tencent/asm/Main;.class
LDC "a"
INVOKEVIRTUAL java/lang/Class.getDeclaredField (Ljava/lang/String;)Ljava/lang/reflect/Field;
ASTORE 1
ALOAD 1
ACONST_NULL
ALOAD 1
ACONST_NULL
INVOKEVIRTUAL java/lang/reflect/Field.getInt (Ljava/lang/Object;)I
ICONST_2
IADD
INVOKEVIRTUAL java/lang/reflect/Field.setInt (Ljava/lang/Object;I)V

需要注意的是,这些字节码都是直接通过eclipse的bytecode插件拷贝过来的,建议大家多利用下这个插件。

上面的字节码,是个比较理想的效果,但要写出这种效果,并没想像中那么简单,需要分析上下文才行,大家不妨可以试试自己能不能通过代码自动生成出这样的字节码。

写到这,或许有人听不懂为什么自动生成上面那样的字节码不简单。使用ASM开发时,最简单的就是重写MethodVisitor.visitXXX方法,根据指令来做相应的修改。就像这里,我们可以把getstatic改成对应的多条指令,从而实现反射功能。那么如果我们是使用这种写法的话,需要怎么写呢?

假设这里只处理getstatic指令,我们先看下对应的转换关系:

转换前

1
GETSTATIC com/tencent/asm/Main.a : I

转换后

1
2
3
4
5
6
7
8
9
LDC Lcom/tencent/asm/Main;.class

LDC "a"

INVOKEVIRTUAL java/lang/Class.getDeclaredField (Ljava/lang/String;)Ljava/lang/reflect/Field;

ACONST_NULL

INVOKEVIRTUAL java/lang/reflect/Field.getInt (Ljava/lang/Object;)I

好像挺简单的,但如果你试下按照上面的方式直接转换putstatic,就会发现有个小问题了——栈中元素前后顺序不对。我们先看下按照上面的方式进行转换的情况:

转换前

1
2
3
4
5
6
7
GETSTATIC com/tencent/asm/Main.a : I

ICONST_2

IADD

PUTSTATIC com/tencent/asm/Main.a : I

转换后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
LDC Lcom/tencent/asm/Main;.class

LDC "a"

INVOKEVIRTUAL java/lang/Class.getDeclaredField (Ljava/lang/String;)Ljava/lang/reflect/Field;

ACONST_NULL

INVOKEVIRTUAL java/lang/reflect/Field.getInt (Ljava/lang/Object;)I

ICONST_2

IADD

LDC Lcom/tencent/asm/Main;.class

LDC "a"

INVOKEVIRTUAL java/lang/Class.getDeclaredField (Ljava/lang/String;)Ljava/lang/reflect/Field;

ACONST_NULL

INVOKEVIRTUAL java/lang/reflect/Field.setInt (Ljava/lang/Object;I)V

大家对比下,是不是发现问题了?我们这里调用Field.setInt方法时,要求栈中有3个元素,从栈底到栈顶依次是Field对象、null、要设置的值。如果我们还是按上面的转换方法,那么栈中就变成要设置的值、Field对象、null了,这样位置就不对了。

有人说,我直接把ICONST_2和IADD直接插入到ACONST_NULL下面不就行了吗?这样当然可以,但如果想写个自动化程序,这种方案就有点难办了。

我们换个思路,既然我们知道调用putstatic前,栈中必然已经有要设置的值了,那么我们是否可以先把它保存到本地变量中,再根据需要把它加载到栈中呢。这样我们的字节码就变成下面这样:

转换前

1
2
3
4
5
6
7
GETSTATIC com/tencent/asm/Main.a : I

ICONST_2

IADD

PUTSTATIC com/tencent/asm/Main.a : I

转换后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
LDC Lcom/tencent/asm/Main;.class

LDC "a"

INVOKEVIRTUAL java/lang/Class.getDeclaredField (Ljava/lang/String;)Ljava/lang/reflect/Field;

ACONST_NULL

INVOKEVIRTUAL java/lang/reflect/Field.getInt (Ljava/lang/Object;)I

ICONST_2

IADD

ISTORE 0

LDC Lcom/tencent/asm/Main;.class

LDC "a"

INVOKEVIRTUAL java/lang/Class.getDeclaredField (Ljava/lang/String;)Ljava/lang/reflect/Field;

ACONST_NULL

ILOAD 0

INVOKEVIRTUAL java/lang/reflect/Field.setInt (Ljava/lang/Object;I)V

好了,现在可以正常跑了。

问题又来了,上面的字节码中,获取Field功能的字节码是重复的,是不是可以优化掉呢?于是字节码应该变成这样子:

转换前

1
2
3
4
5
6
7
GETSTATIC com/tencent/asm/Main.a : I

ICONST_2

IADD

PUTSTATIC com/tencent/asm/Main.a : I

转换后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
LDC Lcom/tencent/asm/Main;.class

LDC "a"

INVOKEVIRTUAL java/lang/Class.getDeclaredField (Ljava/lang/String;)Ljava/lang/reflect/Field;

ASTORE 1

ALOAD 1

ACONST_NULL

INVOKEVIRTUAL java/lang/reflect/Field.getInt (Ljava/lang/Object;)I

ICONST_2

IADD

ISTORE 0

ALOAD 1

ACONST_NULL

ILOAD 0

INVOKEVIRTUAL java/lang/reflect/Field.setInt (Ljava/lang/Object;I)V

好了,这样就与我们最理想的情况比较接近了。如果要优化到最理想的情况,就需要分析上下文了。优化又是另外一个话题了,proguard就实现了代码优化,大家可以借鉴下。

下面给出只实现putstatic的ASM代码,其它情况大家自己试着处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
public class ReflectFieldMain implements Opcodes {  
public static void main(String[] args) throws Exception {
ClassReader cr = new ClassReader(new FileInputStream("a.class"));
ClassNode newClassNode = new ClassNode();
cr.accept(new ClassVisitor(ASM5, newClassNode) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return new ReflectGetStaticField(ASM5, (MethodNode) super.visitMethod(access, name, desc, signature, exceptions));
}
}, 0);
// 代码中会新增本地变量,ClassWriter.COMPUTE_MAXS可以让ASM框架帮我们重新计算真正的maxLocals
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
newClassNode.accept(cw);
// ..保存的代码我就不写了
}

public static class ReflectGetStaticField extends MethodVisitor {
Map<String, Integer> fieldIndexMap = new HashMap<String, Integer>();
int newLocalIndex;
public ReflectGetStaticField(int api, MethodNode mn) {
super(api, mn);
// 用于新增变量
newLocalIndex = mn.maxLocals;
}
@Override
public void visitCode() {
super.visitCode();
Map<String, FieldInsnNode> invokeFields = new HashMap<String, FieldInsnNode>();
InsnList instructions = ((MethodNode)mv).instructions;
if(instructions == null || instructions.size() == 0) {
return;
}
for(int i=0;i<instructions.size();++i) {
AbstractInsnNode ainode = instructions.get(i);
if(ainode.getType() == AbstractInsnNode.FIELD_INSN) {
FieldInsnNode finode = (FieldInsnNode) ainode;
// 去重
invokeFields.put(toKey(finode.owner,finode.name,finode.desc), finode);
}
}
if(invokeFields.isEmpty()) {
return;
}
for(FieldInsnNode finode : invokeFields.values()) {
// 提前保存到本地变量中
fieldIndexMap.put(toKey(finode.owner,finode.name,finode.desc), newLocalIndex);
super.visitLdcInsn(Type.getObjectType(finode.owner));
super.visitLdcInsn(finode.name);
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getDeclaredField", "(Ljava/lang/String;)Ljava/lang/reflect/Field;", false);
super.visitVarInsn(ASTORE, newLocalIndex++);
}
}
private String toKey(String owner, String name, String desc) {
return owner + name + desc;
}
@Override
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
if(opcode != PUTSTATIC) {
super.visitFieldInsn(opcode, owner, name, desc);
return;
}
Type type = Type.getType(desc);
// 把栈中的值保存下
// type.getOpcode(ISTORE)的作用为根据类型获取相应的操作码
super.visitVarInsn(type.getOpcode(ISTORE), newLocalIndex);
super.visitVarInsn(ALOAD, fieldIndexMap.get(toKey(owner, name, desc)));
super.visitInsn(ACONST_NULL);
super.visitVarInsn(type.getOpcode(ILOAD), newLocalIndex);
if(type.getSort() >= Type.BOOLEAN && type.getSort() <= Type.DOUBLE) {
// 基本数据类型,则使用对应的getXXX方法,这样可以省去对象到基本数据类型的转换
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/reflect/Field", "get" + getTypeName(type), "(Ljava/lang/Object;)" + type.getDescriptor(), false);
} else {
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/reflect/Field", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", false);
// 对象类型,需要进行强制类型转换
super.visitTypeInsn(CHECKCAST, type.getInternalName());
}
}
private String getTypeName(Type type) {
switch(type.getSort()) {
case Type.VOID:
return "Void";
case Type.BOOLEAN:
return "Boolean";
case Type.BYTE:
return "Byte";
case Type.SHORT:
return "Short";
case Type.INT:
return "Int";
case Type.FLOAT:
return "Float";
case Type.LONG:
return "Long";
case Type.DOUBLE:
return "Double";
case Type.OBJECT:
case Type.ARRAY:
return "Object";
}
throw new IllegalArgumentException();
}
}
}

注意下,上面我使用的是Class.getDeclaredField来获取对应的Field,getDeclaredField获取的是类中声明的属性。如果调用的属性是父类的,且它的权限为public的,则可以使用getField方法。非public的父类属性,则可以通过该父类的Class对象的getDeclaredField来查找。

我们平时使用反射时,很多时候会遇到属性没权限访问,这时,就需要调用到相应的setAccessible(true)方法。但这里并不需要,我想这一点不用解释了吧。

另外,复杂一点的方法,都会有很多跳转,如果把获取Field对象的功能都放到最开始的话,运行效率会非常低。当然这功能本来就会非常影响效率,因此建议只在关键地方使用。

反射调用方法

有了上面反射调用属性的经验,反射调用方法也变得简单很多了。

在bytecode中,调用方法包含多条指令,下面一一解释下:

invokevirtual:调用虚方法指令,此指令可以调用可能有继承关系的方法,除构造方法。

invokestatic:调用静态方法指令。

invokespecial:调用特殊方法指令,如调用构造方法、调用私有实例方法

invokeinterface:调用接口方法指令。

invokedynamic:调用动态方法指令,用于支持动态语言。

需要注意的是,invokedynamic指令在dalvik bytecode目前没有对应的指令,因此不用考虑。

除了invokestatic外,其它指令均至少有一个对象实例的参数,即Java中的this对象。另外,调用的方法中也可以有多个参数。因此,我们首先需要先把栈中相应的内容全部保存到本地变量中,然后通过反射得到对应的Method对象,接着把刚才保存的参数重新压回栈中并调用Method.invoke()方法,最后当然是类型转换拉。

下面是转换前后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ReflectMethodMain {
public static void add(int a, int b) {
System.out.println(a+b);
}
public static void main(String[] args) throws Exception {
oldWay();
newWay();
}
public static void oldWay() {
add(3, 7);
}
public static void newWay() throws Exception {
Method addMethod = ReflectMethodMain.class.getDeclaredMethod("add", int.class, int.class);
addMethod.invoke(null, 3, 7);
}
}

其中oldWay是转换前的,newWay是转换后的。接下来我们对比下转换前后的字节码变化:

转换前

1
2
3
4
5
6
7
ICONST_3

BIPUSH 7

INVOKESTATIC ReflectMethodMain.add (II)V

RETURN

转换后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
LDC LReflectMethodMain;.class

LDC "add"

ICONST_2

ANEWARRAY java/lang/Class

DUP

ICONST_0

GETSTATIC java/lang/Integer.TYPE : Ljava/lang/Class;

AASTORE

DUP

ICONST_1

GETSTATIC java/lang/Integer.TYPE : Ljava/lang/Class;

AASTORE

INVOKEVIRTUAL java/lang/Class.getDeclaredMethod (Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;

ASTORE 0

ALOAD 0

ACONST_NULL

ICONST_2

ANEWARRAY java/lang/Object

DUP

ICONST_0

ICONST_3

INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;

AASTORE

DUP

ICONST_1

BIPUSH 7

INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;

AASTORE

INVOKEVIRTUAL java/lang/reflect/Method.invoke (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;

POP

RETURN

上面我把一些关键点进行了加粗,我们可以看到,我在Java代码中使用的是可变参数的写法,我们知道可变参数实际上是通过数组来实现的,所以bytecode中,使用了数组的方式。

另外,invoke方法接收的是Object对象,因而需要对基本数据类型进行转换。还有一点要注意的是,如果反射调用的方法是void(无返回值),则需要调用一下POP指令,这是因为invoke方法会有一个返回值,如果反射调用的方法返回值为void,那么invoke方法会返回一个null值。

这个是比较理想情况下的转换,但与上面反射调用属性类似,要达到这种效果,需要进行优化。这里同样采用较为简单的方法,下面给出相应的步骤:

1)保存栈中的参数

2)获取待调用方法的Method对象,并压入栈顶

3)产生一个与参数个数同等大小的Object数组,并压入栈顶

4)把参数依次放入到Object数组中,并把参数中的基本数据类型全部转换为相应的对象类型。

5)调用Method.invoke()方法

6)如果返回值是void,则直接调用pop。如果返回值为基本数据类型,则转换为对应的基本数据类型。否则直接强制类型转换为相应的对象类型。

由于这功能简化后代码还是比较多,这里就不贴出来了,大家自己试着实现下,如果有什么不理解的,可联系下我。

我们关注下返回值为基本数据类型的情况。如果返回值就int的,则调用invoke方法后返回的是Integer对象,因此需要先强制类型转换为Integer对象,然后调用Integer.intValue()方法来得到相应的int值。

字符串加密

这里的字符串加密指的是常量字符串的加密。像上面介绍的反射调用中,类名、属性名、方法名就是常量字符串,此外,开发者在编码时也会经常使用常量字符串。对字符串进行加密,可以隐藏代码中的一些明文,同时也可以结合反射调用来隐藏关键的API调用,增加破解和分析的难度。

下面是加密前的Java代码:

1
2
3
4
5
public class Main {
public static void main(String[] args) {
System.out.println("Hello World!!");
}
}

接下来是加密后的Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main(String[] args) {
System.out.println(decrypt("Idmmn!Vnsme"));
}
public static String decrypt(String value) {
if(value == null || value.equals("")) {
return value;
}
int code = 1;
char[] oldBytes = value.toCharArray();
char[] newBytes = new char[oldBytes.length];
for(int i=0;i<oldBytes.length;++i) {
newBytes[i] = (char) (oldBytes[i] ^ code);
}
return new String(newBytes);
}
}

大家在测试时,可能会遇到一些加密后不可打印的甚至是无意义的字符,上面为了能够正常显示,我选了个可以打印的加密结果。

我们看到,加密后的代码多了一个解密方法,另外常量字符串也变为了一个加密过无意义的常量字符串了,而在使用常量字符串前,使用了解密方法进行解密。

上面使用了一个简单的异或加密方案,此方案加密与解密代码是完全一样的,即为可逆算法。有兴趣的童鞋可以把它改为更加复杂的算法,下面给出对应的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
package com.tencent.asm;
import java.io.FileInputStream;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
public class EncryptStringMain implements Opcodes {
public static void main(String[] args) throws Exception {
ReflectFieldMain.class.getDeclaredField("").setAccessible(true);
ClassReader cr = new ClassReader(new FileInputStream("EncryptStringMain.class"));
ClassNode oldClassNode = new ClassNode();
cr.accept(oldClassNode, 0);
final ClassNode newClassNode = new ClassNode();
final MethodNode decryptMethodNode = newDecryptMethod();
oldClassNode.accept(new ClassVisitor(ASM5, newClassNode) {
@Override
public MethodVisitor visitMethod(int access, final String name, final String desc, String signature, String[] exceptions) {
return new MethodVisitor(ASM5, super.visitMethod(access, name, desc, signature, exceptions)) {
@Override
public void visitLdcInsn(Object cst) {
if(!(cst instanceof String)) {
super.visitLdcInsn(cst);
return;
}
// 这行代码在这里是永远不会执行到,这里只是为了提醒大家,注意不要生成递归调用了
if(name.equals(decryptMethodNode.name) && desc.equals(decryptMethodNode.desc)) {
super.visitLdcInsn(cst);
return;
}
// 加载加密后的字符串
super.visitLdcInsn(encrypt((String)cst));
// 调用解密方法进行解密
super.visitMethodInsn(INVOKESTATIC, newClassNode.name, decryptMethodNode.name, decryptMethodNode.desc, false);
}
};
}
});
newClassNode.methods.add(decryptMethodNode);

}
private static MethodNode newDecryptMethod() {
// 这里代码很长,建议不要自己写,而是直接使用eclipse的bytecode插件的ASM功能来拷
MethodNode decryptMethodNode = new MethodNode(ACC_PUBLIC + ACC_STATIC, "_" + System.currentTimeMillis(), "(Ljava/lang/String;)Ljava/lang/String;", null, null);
decryptMethodNode.visitCode();
decryptMethodNode.visitVarInsn(ALOAD, 0);
Label label0 = new Label();
decryptMethodNode.visitJumpInsn(IFNULL, label0);
decryptMethodNode.visitVarInsn(ALOAD, 0);
decryptMethodNode.visitLdcInsn("");
decryptMethodNode.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
Label label1 = new Label();
decryptMethodNode.visitJumpInsn(IFEQ, label1);
decryptMethodNode.visitLabel(label0);
decryptMethodNode.visitVarInsn(ALOAD, 0);
decryptMethodNode.visitInsn(ARETURN);
decryptMethodNode.visitLabel(label1);
decryptMethodNode.visitInsn(ICONST_1);
decryptMethodNode.visitVarInsn(ISTORE, 1);
decryptMethodNode.visitVarInsn(ALOAD, 0);
decryptMethodNode.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "toCharArray", "()[C", false);
decryptMethodNode.visitVarInsn(ASTORE, 2);
decryptMethodNode.visitVarInsn(ALOAD, 2);
decryptMethodNode.visitInsn(ARRAYLENGTH);
decryptMethodNode.visitIntInsn(NEWARRAY, T_CHAR);
decryptMethodNode.visitVarInsn(ASTORE, 3);
decryptMethodNode.visitInsn(ICONST_0);
decryptMethodNode.visitVarInsn(ISTORE, 4);
Label label2 = new Label();
decryptMethodNode.visitJumpInsn(GOTO, label2);
Label label3 = new Label();
decryptMethodNode.visitLabel(label3);
decryptMethodNode.visitVarInsn(ALOAD, 3);
decryptMethodNode.visitVarInsn(ILOAD, 4);
decryptMethodNode.visitVarInsn(ALOAD, 2);
decryptMethodNode.visitVarInsn(ILOAD, 4);
decryptMethodNode.visitInsn(CALOAD);
decryptMethodNode.visitVarInsn(ILOAD, 1);
decryptMethodNode.visitInsn(IXOR);
decryptMethodNode.visitInsn(I2C);
decryptMethodNode.visitInsn(CASTORE);
decryptMethodNode.visitIincInsn(4, 1);
decryptMethodNode.visitLabel(label2);
decryptMethodNode.visitVarInsn(ILOAD, 4);
decryptMethodNode.visitVarInsn(ALOAD, 2);
decryptMethodNode.visitInsn(ARRAYLENGTH);
decryptMethodNode.visitJumpInsn(IF_ICMPLT, label3);
decryptMethodNode.visitTypeInsn(NEW, "java/lang/String");
decryptMethodNode.visitInsn(DUP);
decryptMethodNode.visitVarInsn(ALOAD, 3);
decryptMethodNode.visitMethodInsn(INVOKESPECIAL, "java/lang/String", "<init>", "([C)V", false);
decryptMethodNode.visitInsn(ARETURN);
decryptMethodNode.visitEnd();
return decryptMethodNode;
}

public static String encrypt(String value) {
if(value == null || value.equals("")) {
return value;
}
int code = 1;
char[] oldBytes = value.toCharArray();
char[] newBytes = new char[oldBytes.length];
for(int i=0;i<oldBytes.length;++i) {
newBytes[i] = (char) (oldBytes[i] ^ code);
}
return new String(newBytes);
}
}

代码很简单,我就不解释了。结合前面的反射调用功能,就可以实现针对性地隐藏关键系统API调用。

方法指令上限问题

首先,无论是正常调用改为反射调用还是字符串加密,必然会增加字节码,而Java每个方法的代码是不能超过65536个字节的,这一点我们可以从ASM框架中的MethodWriter.getSize()方法而看出来。下面是MethodWriter的关键代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.objectweb.asm;
...
class MethodWriter extends MethodVisitor {
...
private ByteVector code = new ByteVector();
...
MethodWriter(final ClassWriter cw, final int access, final String name, final String desc, final String signature, final String[] exceptions, final boolean computeMaxs, final boolean computeFrames) {
...
}
...
final int getSize() {
...
if (code.length > 0) {
if (code.length > 65536) {
throw new RuntimeException("Method code too large!");
}
...
}
...
}
...
}

也就是说,当前方法的长度可以通过code.length来得到,不过有点麻烦的是,code属性的权限是private的,而MethodWriter类是package的,还有的是,MethodWriter的构造方法也是package的,且参数中要求一个ClassWriter类的实例。

下面提供一种获取当前方法的长度的方案以及获取某条指令占用的字节数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.objectweb.asm;
import java.lang.reflect.Field;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.MethodNode;
public class CodeLengthUtil {
// 获取方法的长度
public static int getCodeLength(MethodNode mn) throws Exception {
MethodWriter mw = new MethodWriter(new ClassWriter(ClassWriter.COMPUTE_MAXS), mn.access, mn.name, mn.desc, mn.signature, mn.exceptions.toArray(new String[0]), true, false);
mn.accept(mw);
return getLength(mw);
}
// 获取某条指令的长度
public static int getCodeLength(AbstractInsnNode ainode) throws Exception {
MethodWriter mw = new MethodWriter(new ClassWriter(ClassWriter.COMPUTE_MAXS), 0, "a", "()V", null, null, true, false);
ainode.accept(mw);
return getLength(mw);
}
private static int getLength(MethodWriter mw) throws Exception {
Field codeField = MethodWriter.class.getDeclaredField("code");
codeField.setAccessible(true);
ByteVector bv = (ByteVector) codeField.get(mw);
return bv.length;
}
}

需要注意的是,上面CodeLengthUtil所在的包为org.objectweb.asm,即与MethodWriter在同一个包,如果不在同一个包中,则会更多的地方会需要使用反射。当然,ASM是开源的,大家直接改下它的源码也行,甚至直接修改ASM库也是可以的。

有了上面的工具,我们可以先获取原方法的长度,然后获得需要增加的指令的总长度,再加起来与65536比较,如果超了,就看着处理哈。