使用BCEL库,可以查看和修改Java编译后class字节源码
新建一个Java测试工程,写下这么一个测试类:
package ck;
import java.util.HashMap;
import java.util.Map;
public class LoginAct {
public boolean login() {
String userName = "get user name from request";
String pwd = "get password name from request";
String checkLicense = "call check license ";
Map<String,String> userMapInfo = null;
String dbPwd = "select password from db where userName=?";
if( checkLicense.equals("ok") && dbPwd.equals(pwd) ) {
userMapInfo = new HashMap<String, String>();
userMapInfo.put(userName, "crazy");
//select user_info from user_table into a map where username = xxx
}
//session.addAttribute("userInfo", userMapInfo );
return userMapInfo != null;
}
}
接下来,利用Bcel库,写一个简单的操作工具Java文件,它能提取class字节码并反汇编
Bcel库Maven地址
<dependency>
<groupId>org.apache.bcel</groupId>
<artifactId>bcel</artifactId>
<version>6.2</version>
</dependency>
本人写了一个 BcelDumpClassName.java 操作工具Java文件,代码参考摘自网上。然后经过我对byteCode一番研究和探索,结合Bcel库的codeToString 函数,我对网上的代码加入了修改后的codeToString 函数,使得输出结里和反汇编语句与JBE的一样,字节码也和DyJOE一致。
package gen;
import java.io.IOException;
import java.util.Iterator;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.ClassFormatException;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.Utility;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.TargetLostException;
import org.apache.bcel.util.ByteSequence;
import org.apache.bcel.util.InstructionFinder;
public class BcelDumpClassName {
public static void main(String[] argv) {
//String className = "java.lang.String";
String className = "ck.LoginAct";
try {
JavaClass clazz = Repository.lookupClass(className);
Method[] methods = clazz.getMethods();
ConstantPoolGen cp = new ConstantPoolGen(clazz.getConstantPool());
for (int i = 0; i < methods.length; i++) {
if (!(methods[i].isAbstract() || methods[i].isNative())) {
MethodGen mg = new MethodGen(methods[i], clazz.getClassName(), cp);
Method stripped = removeNOPs(mg);
if (stripped != null) // Any NOPs stripped?
methods[i] = stripped; // Overwrite with stripped method
}
}
clazz.setConstantPool(cp.getFinalConstantPool());
//输出bytecode汇编语句
printCode(clazz.getMethods());
} catch (Exception e) {
e.printStackTrace();
}
}
public static String byte2hex(byte[] array) {
StringBuffer sb = new StringBuffer(64);
try {
for (int i = 0; i < array.length; i++) {
sb.append(Integer.toHexString((array[i] & 0xFF) | 0x100).toUpperCase().substring(1, 3));
sb.append(' ');
}
return sb.toString();
} catch (Exception e) {
return e.toString();
}
}
public static String byte2hex(byte[] array, int start, int end) {
StringBuffer sb = new StringBuffer(64);
try {
for (int i = start; i < end; i++) {
sb.append(Integer.toHexString((array[i] & 0xFF) | 0x100).toUpperCase().substring(1, 3));
sb.append(' ');
}
return sb.toString();
} catch (Exception e) {
return e.toString();
}
}
//Modify by RoadToExpert . https://blog.youkuaiyun.com/roadtotheexpert
public static String codeToString(byte[] code, ConstantPool constant_pool, int index, int length, boolean verbose) {
StringBuilder buf = new StringBuilder(code.length * 20);
int hexLen = 12;
int pre = 0;
String hexs = null;
int sindex = 0;
try {
ByteSequence stream = new ByteSequence(code);
Throwable localThrowable3 = null;
try {
for (int i = 0; i < index; i++) {
Utility.codeToString(stream, constant_pool, verbose);
}
for (int i = 0; stream.available() > 0; i++) {
if ((length < 0) || (i < length)) {
String indices = Utility.fillup("|" + stream.getIndex() + ":" + Integer.toHexString(stream.getIndex()), 8, true, ' ');
String opcodeDesc = Utility.codeToString(stream, constant_pool, verbose);
hexs = byte2hex(code, pre, stream.getIndex());
pre = sindex;
hexs = Utility.fillup(hexs, hexLen, true, ' ');
buf.append(hexs).append(indices).append(opcodeDesc).append('\n');
pre = stream.getIndex();
}
}
} catch (Throwable localThrowable1) {
localThrowable3 = localThrowable1;
// throw localThrowable1;
} finally {
if (stream != null) {
if (localThrowable3 != null) {
try {
stream.close();
} catch (Throwable localThrowable2) {
localThrowable3.addSuppressed(localThrowable2);
}
} else {
stream.close();
}
}
}
} catch (IOException e) {
throw new ClassFormatException("Byte code error: " + buf.toString(), e);
}
return buf.toString();
}
public static void printCode(Method[] methods) {
if( methods == null || methods.length == 0 ) {
return;
}
StringBuilder sb = new StringBuilder(128*methods.length);
for(int i=0; i < methods.length; i++) {
sb.append("--------------------------------------------------------------------------\n");
Code code = methods[i].getCode();
sb.append(byte2hex(code.getCode()));
sb.append('\n');
sb.append(methods[i]).append('\n');
if(code != null) {
String s = codeToString(code.getCode(), code.getConstantPool(), 0, -1, true);
sb.append(s);
}
sb.append('\n');
}
System.out.println(sb.toString());
}
private static Method removeNOPs(MethodGen mg) {
InstructionList il = mg.getInstructionList();
InstructionFinder f = new InstructionFinder(il);
String pat = "NOP+"; // Find at least one NOP
InstructionHandle next = null;
int count = 0;
for (Iterator iter = f.search(pat); iter.hasNext();) {
InstructionHandle[] match = (InstructionHandle[]) iter.next();
InstructionHandle first = match[0];
InstructionHandle last = match[match.length - 1];
if ((next = last.getNext()) == null) {
break;
}
count += match.length;
try {
il.delete(first, last);
} catch (TargetLostException e) {
for (InstructionHandle target : e.getTargets()) {
System.out.println(target.toString());
}
}
}
Method m = null;
if (count > 0) {
System.out.println("Removed " + count + " NOP instructions from method " + mg.getName());
m = mg.getMethod();
}
il.dispose(); // Reuse instruction handles
return m;
}
}
上面的 Main函数里指定了对LoginAct进行JAVA反汇编码,运行 BcelDumpClassName 工具后,可以看到控制台输出如下信息:
--------------------------------------------------------------------------
2A B7 00 08 B1
public void <init>()
2A |0:0 aload_0
B7 00 08 |1:1 invokespecial java.lang.Object.<init> ()V (8)
B1 |4:4 return
--------------------------------------------------------------------------
12 10 4C 12 12 4D 12 14 4E 01 3A 04 12 16 3A 05 2D 12 18 B6 00 1A 99 00 20 19 05 2C B6 00 1A 99 00 17 BB 00 20 59 B7 00 22 3A 04 19 04 2B 12 23 B9 00 25 03 00 57 19 04 C6 00 05 04 AC 03 AC
public boolean login()
12 10 |0:0 ldc "get user name from request" (16)
4C |2:2 astore_1
12 12 |3:3 ldc "get password name from request" (18)
4D |5:5 astore_2
12 14 |6:6 ldc "call check license " (20)
4E |8:8 astore_3
01 |9:9 aconst_null
3A 04 |10:a astore %4
12 16 |12:c ldc "select password from db" (22)
3A 05 |14:e astore %5
2D |16:10 aload_3
12 18 |17:11 ldc "ok" (24)
B6 00 1A |19:13 invokevirtual java.lang.String.equals (Ljava/lang/Object;)Z (26)
99 00 20 |22:16 ifeq #54
19 05 |25:19 aload %5
2C |27:1b aload_2
B6 00 1A |28:1c invokevirtual java.lang.String.equals (Ljava/lang/Object;)Z (26)
99 00 17 |31:1f ifeq #54
BB 00 20 |34:22 new <java.util.HashMap> (32)
59 |37:25 dup
B7 00 22 |38:26 invokespecial java.util.HashMap.<init> ()V (34)
3A 04 |41:29 astore %4
19 04 |43:2b aload %4
2B |45:2d aload_1
12 23 |46:2e ldc "crazy" (35)
B9 00 25 03 00 |48:30 invokeinterface java.util.Map.put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; (37) 3 0
57 |53:35 pop
19 04 |54:36 aload %4
C6 00 05 |56:38 ifnull #61
04 |59:3b iconst_1
AC |60:3c ireturn
03 |61:3d iconst_0
AC |62:3e ireturn
这里可以看到第|56:38行,对应的汇编是: ifnull #61
简单对比LoginAct源码,就可以知道它实际对应的就是 return userMapInfo != null;
如果把字节码 ifnull 改为 ifnonnull ,那源码不就变成 return userMapInfo == null , 这样随便输入都可以 login 通过啦!
那如何才能知道 ifnull 和 ifnonnull 对应是哪个字节码?
在org.apache.bcel.Const源码,已经给出OpcodeNames,对应如下数组
public static final String[] OPCODE_NAMES = { "nop", "aconst_null", "iconst_m1", "iconst_0", "iconst_1", "iconst_2", "iconst_3", "iconst_4", "iconst_5", "lconst_0", "lconst_1", "fconst_0", "fconst_1", "fconst_2", "dconst_0", "dconst_1", "bipush", "sipush", "ldc", "ldc_w", "ldc2_w", "iload", "lload", "fload", "dload", "aload", "iload_0", "iload_1", "iload_2", "iload_3", "lload_0", "lload_1", "lload_2", "lload_3", "fload_0", "fload_1", "fload_2", "fload_3", "dload_0", "dload_1", "dload_2", "dload_3", "aload_0", "aload_1", "aload_2", "aload_3", "iaload", "laload", "faload", "daload", "aaload", "baload", "caload", "saload", "istore", "lstore", "fstore", "dstore", "astore", "istore_0", "istore_1", "istore_2", "istore_3", "lstore_0", "lstore_1", "lstore_2", "lstore_3", "fstore_0", "fstore_1", "fstore_2", "fstore_3", "dstore_0", "dstore_1", "dstore_2", "dstore_3", "astore_0", "astore_1", "astore_2", "astore_3", "iastore", "lastore", "fastore", "dastore", "aastore", "bastore", "castore", "sastore", "pop", "pop2", "dup", "dup_x1", "dup_x2", "dup2", "dup2_x1", "dup2_x2", "swap", "iadd", "ladd", "fadd", "dadd", "isub", "lsub", "fsub", "dsub", "imul", "lmul", "fmul", "dmul", "idiv", "ldiv", "fdiv", "ddiv", "irem", "lrem", "frem", "drem", "ineg", "lneg", "fneg", "dneg", "ishl", "lshl", "ishr", "lshr", "iushr", "lushr", "iand", "land", "ior", "lor", "ixor", "lxor", "iinc", "i2l", "i2f", "i2d", "l2i", "l2f", "l2d", "f2i", "f2l", "f2d", "d2i", "d2l", "d2f", "i2b", "i2c", "i2s", "lcmp", "fcmpl", "fcmpg", "dcmpl", "dcmpg", "ifeq", "ifne", "iflt", "ifge", "ifgt", "ifle", "if_icmpeq", "if_icmpne", "if_icmplt", "if_icmpge", "if_icmpgt", "if_icmple", "if_acmpeq", "if_acmpne", "goto", "jsr", "ret", "tableswitch", "lookupswitch", "ireturn", "lreturn", "freturn", "dreturn", "areturn", "return", "getstatic", "putstatic", "getfield", "putfield", "invokevirtual", "invokespecial", "invokestatic", "invokeinterface", "invokedynamic", "new", "newarray", "anewarray", "arraylength", "athrow", "checkcast", "instanceof", "monitorenter", "monitorexit", "wide", "multianewarray", "ifnull", "ifnonnull", "goto_w", "jsr_w", "breakpoint", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "<illegal opcode>", "impdep1", "impdep2" };
查上表得知,ifnull对应索引是198,16进制是C6
ifnonnull对应索引是199,16进制是C7
找到这行
C6 00 05 |56:38 ifnull #61
没有错,就是把上面的 C6 改成 C7 ,这样源码就变成 return userMapInfo == null
。。。
但仔细想一下,因为源码中的 userMapInfo 没加载。即便 login() 返回 true 意义也不大。
再看下代码,关注点就定位到 dbPwd.equals(pwd) 这一行,查找其对应的反汇编码。分析点就落在这两行语句
B6 00 1A |28:1c invokevirtual java.lang.String.equals (Ljava/lang/Object;)Z (26)
99 00 17 |31:1f ifeq #54
就像前面的操作那样,只要把 ifeq 变成 ifne,源码就会从原来的 dbPwd.equals(pwd) 变成 !dbPwd.equals(pwd)
查上面的OPCODE_NAMES 可知 ifeq 是153(0x99 )。改成 ifne 154(0x9A)
这样,外部登陆者,只要userName写正确,dbPwd写什么都可以了。 (^_ ^)

本文介绍如何使用BCEL库对Java编译后的class文件进行字节码级的操作,包括提取和反汇编字节码,并通过修改特定字节码实现对原有程序逻辑的改变。





