ASM - 分支目标处的堆栈映射帧不一致
ASM - Inconsistent stackmap frames at branch target
我正在尝试做一个简单的 Java 字节码混淆器,它通过用简单的条件跳转替换 GOTO
指令来工作,比如 if 10 != 15 GOTO else throw IllegalStateException
。我当前的代码是:
final AbstractInsnNode[] insns = method.instructions.toArray().clone();
for (final AbstractInsnNode insn : insns) {
final int op = insn.getOpcode();
if ((op == GOTO) || (op == IFLE) || (op == IFGE)) {
LabelNode l0 = new LabelNode();
LabelNode l1 = new LabelNode();
LabelNode l2 = new LabelNode();
int locals = (method.localVariables == null) ? 0 : method.localVariables.size();
int params = (method.parameters == null) ? 0 : method.parameters.size();
int v0index = locals + params;
int v1index = v0index + 1;
int exindex = v1index + 1;
// Init fake conditional fields
method.instructions.insertBefore(insn, new LdcInsnNode(10F));
method.instructions.insertBefore(insn, new VarInsnNode(FSTORE, v0index));
method.instructions.insertBefore(insn, new LdcInsnNode(45F));
method.instructions.insertBefore(insn, new VarInsnNode(FSTORE, v1index));
// Crossing jumps
method.instructions.insertBefore(insn, l1);
method.instructions.insert(insn, l0);
method.instructions.insert(l0, l2);
LabelNode l3 = new LabelNode();
LabelNode l4 = new LabelNode();
method.instructions.insert(l2, l3);
method.instructions.insert(l3, l4);
// If 'v0!=v1', jump to l0, otherwise goto l3
method.instructions.insertBefore(l1, new VarInsnNode(FLOAD, v0index));
method.instructions.insertBefore(l1, new VarInsnNode(FLOAD, v1index));
method.instructions.insertBefore(l1, new InsnNode(FCMPG));
method.instructions.insertBefore(l1, new JumpInsnNode(IFNE, l0));
method.instructions.insertBefore(l1, new JumpInsnNode(GOTO, l3));
// Jump to l3 results in throwing an exception
// Create and throw the exception
method.instructions.insertBefore(l4, new TypeInsnNode(NEW, "java/lang/IllegalStateException"));
method.instructions.insertBefore(l4, new InsnNode(DUP));
method.instructions.insertBefore(l4, new MethodInsnNode(INVOKESPECIAL, "java/lang/IllegalStateException", "<init>", "()V", false));
method.instructions.insertBefore(l4, new InsnNode(ATHROW));
method.instructions.insertBefore(l0, new JumpInsnNode(GOTO, l2));
method.instructions.insertBefore(l2, new JumpInsnNode(GOTO, l1));
// Exception handler
LabelNode start = new LabelNode();
LabelNode handler = new LabelNode();
LabelNode end = new LabelNode();
method.instructions.insertBefore(l0, start);
method.instructions.insert(l2, end);
method.instructions.insert(end, handler);
// Just throw the exception again
LabelNode l5 = new LabelNode();
method.instructions.insert(handler, l5);
method.instructions.insertBefore(l5, new TypeInsnNode(NEW, "java/lang/IllegalStateException"));
method.instructions.insertBefore(l5, new InsnNode(DUP));
method.instructions.insertBefore(l5, new MethodInsnNode(INVOKESPECIAL, "java/lang/IllegalStateException", "<init>", "()V", false));
method.instructions.insertBefore(l5, new InsnNode(ATHROW));
// Try/catch
TryCatchBlockNode tryBlock = new TryCatchBlockNode(start, end, handler, "java/lang/IllegalStateException");
method.tryCatchBlocks.add(tryBlock);
// Init local variables
method.visitLocalVariable("_v0_" + Rand.alphaNumeric(5), "F", null, l0.getLabel(), l2.getLabel(), v0index);
method.visitLocalVariable("_v1_" + Rand.alphaNumeric(5), "F", null, l0.getLabel(), l2.getLabel(), v1index);
method.visitLocalVariable("_ex_" + Rand.alphaNumeric(5), "Ljava/lang/IllegalArgumentException;", null, start.getLabel(), handler.getLabel(), exindex);
}
}
其中method
为MethodNode
类型的混淆方法参数,class实现接口Opcodes
.
这很好用,但不是对所有方法都有效(我对字节码很陌生,所以不知道确切的情况)。例如,它适用于 main
方法:
原Java代码(在Procyon中反编译): https://p.reflex.rip/DLMT.cs
原始字节码: https://p.reflex.rip/ywJt.go
混淆的Java代码(在Procyon中反编译): https://p.reflex.rip/Er9V.cs
混淆字节码: https://p.reflex.rip/JBAb.go
但是,它破坏了其他方法之一,divMinByMax
,方法:
原Java代码(在Procyon中反编译): https://p.reflex.rip/AW9W.java
原始字节码: https://p.reflex.rip/GX2k.cpp
混淆Java代码(在Procyon中反编译,FAILED): https://p.reflex.rip/Eqju.java
混淆字节码: https://p.reflex.rip/isiX.cpp
当我尝试使用 java -jar
:
运行 混淆 JAR 时,此方法会导致 VerifyError
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.VerifyError: Inconsistent stackmap frames at branch target 27
Exception Details:
Location:
test/one/HelloRandom.divMinByMax(DD)D @21: goto
Reason:
Current frame's stack size doesn't match stackmap.
Current Frame:
bci: @21
flags: { }
locals: { double, double_2nd, float, float }
stack: { }
Stackmap Frame:
bci: @27
flags: { }
locals: { double, double_2nd, float, float }
stack: { 'java/lang/IllegalStateException' }
Bytecode:
0x0000000: 2826 9712 7145 1272 4624 2596 9a00 0ca7
0x0000010: 0014 9e00 48a7 0006 a7ff fabb 0016 59b7
0x0000020: 0073 bfbb 0016 59b7 0073 bf00 0000 0000
0x0000030: 00bf 0000 00bf 0000 0000 0000 00bf 0000
0x0000040: bf00 00bf 0000 bf00 00bf 0000 0000 0000
0x0000050: 00bf 0000 0000 0000 00bf 2826 6faf
Exception Handler Table:
bci [24, 27] => handler: 27
Stackmap Table:
full_frame(@18,{Double,Float,Float},{Integer})
same_locals_1_stack_item_frame(@24,Integer)
same_locals_1_stack_item_frame(@27,Object[#22])
same_locals_1_stack_item_frame(@35,Integer)
full_frame(@43,{},{Object[#159]})
same_locals_1_stack_item_frame(@50,Object[#159])
same_locals_1_stack_item_frame(@54,Object[#159])
same_locals_1_stack_item_frame(@62,Object[#159])
same_locals_1_stack_item_frame(@65,Object[#159])
same_locals_1_stack_item_frame(@68,Object[#159])
same_locals_1_stack_item_frame(@71,Object[#159])
same_locals_1_stack_item_frame(@74,Object[#159])
same_locals_1_stack_item_frame(@82,Object[#159])
append_frame(@90,Double,Float,Float)
same_locals_1_stack_item_frame(@93,Double)
我做了很多研究,我唯一找到的是原因:据我了解,问题是 @21
上的堆栈(GOTO
跳转到标签throw new IllegalStateException
):
stack: { }
(为空)与跳转目标标签 @27
处的堆栈不匹配:
stack: { 'java/lang/IllegalStateException' }
(其中包含它应该 "throw" 的异常)。
所以基本上,据我所知,错误发生在我尝试执行 GOTO <n>
跳转时,其中 <n>
是 "throws" 一个标签的编号IllegalStateException
.
我该如何解决这个问题?也许有一种方法可以让 @21
处的堆栈在跳跃之前也包含 java/lang/IllegalStateException
(这样这两个堆栈,一个在跳跃之前,一个在跳跃之后,匹配)?或者我可以用它做些什么?
您正在指令后插入异常处理程序,但是当指令是 条件 分支时,即 IFLE
或 IFGE
,分支可能不被采用并且代码流在指令之后继续,运行 进入异常处理程序。
这会造成不一致的状态,因为异常处理程序期望堆栈上有一个 Throwable
,而当代码流在检测指令之后继续时,堆栈上并不存在。但是当然,在那种情况下你不想执行异常处理程序,所以你必须插入另一个 GOTO
,从 l2
到 l5
如果我没看错的话。
检测 GOTO
指令时这不是问题,这些指令永远不会在指令之后继续。
在这个地方,我推荐一种不同的编码风格。在不同的参考节点之前和之后插入使得在阅读代码时无法预测实际代码结构。如果您只使用一个参考节点插入一个线性指令列表,它会更易于维护。
我正在尝试做一个简单的 Java 字节码混淆器,它通过用简单的条件跳转替换 GOTO
指令来工作,比如 if 10 != 15 GOTO else throw IllegalStateException
。我当前的代码是:
final AbstractInsnNode[] insns = method.instructions.toArray().clone();
for (final AbstractInsnNode insn : insns) {
final int op = insn.getOpcode();
if ((op == GOTO) || (op == IFLE) || (op == IFGE)) {
LabelNode l0 = new LabelNode();
LabelNode l1 = new LabelNode();
LabelNode l2 = new LabelNode();
int locals = (method.localVariables == null) ? 0 : method.localVariables.size();
int params = (method.parameters == null) ? 0 : method.parameters.size();
int v0index = locals + params;
int v1index = v0index + 1;
int exindex = v1index + 1;
// Init fake conditional fields
method.instructions.insertBefore(insn, new LdcInsnNode(10F));
method.instructions.insertBefore(insn, new VarInsnNode(FSTORE, v0index));
method.instructions.insertBefore(insn, new LdcInsnNode(45F));
method.instructions.insertBefore(insn, new VarInsnNode(FSTORE, v1index));
// Crossing jumps
method.instructions.insertBefore(insn, l1);
method.instructions.insert(insn, l0);
method.instructions.insert(l0, l2);
LabelNode l3 = new LabelNode();
LabelNode l4 = new LabelNode();
method.instructions.insert(l2, l3);
method.instructions.insert(l3, l4);
// If 'v0!=v1', jump to l0, otherwise goto l3
method.instructions.insertBefore(l1, new VarInsnNode(FLOAD, v0index));
method.instructions.insertBefore(l1, new VarInsnNode(FLOAD, v1index));
method.instructions.insertBefore(l1, new InsnNode(FCMPG));
method.instructions.insertBefore(l1, new JumpInsnNode(IFNE, l0));
method.instructions.insertBefore(l1, new JumpInsnNode(GOTO, l3));
// Jump to l3 results in throwing an exception
// Create and throw the exception
method.instructions.insertBefore(l4, new TypeInsnNode(NEW, "java/lang/IllegalStateException"));
method.instructions.insertBefore(l4, new InsnNode(DUP));
method.instructions.insertBefore(l4, new MethodInsnNode(INVOKESPECIAL, "java/lang/IllegalStateException", "<init>", "()V", false));
method.instructions.insertBefore(l4, new InsnNode(ATHROW));
method.instructions.insertBefore(l0, new JumpInsnNode(GOTO, l2));
method.instructions.insertBefore(l2, new JumpInsnNode(GOTO, l1));
// Exception handler
LabelNode start = new LabelNode();
LabelNode handler = new LabelNode();
LabelNode end = new LabelNode();
method.instructions.insertBefore(l0, start);
method.instructions.insert(l2, end);
method.instructions.insert(end, handler);
// Just throw the exception again
LabelNode l5 = new LabelNode();
method.instructions.insert(handler, l5);
method.instructions.insertBefore(l5, new TypeInsnNode(NEW, "java/lang/IllegalStateException"));
method.instructions.insertBefore(l5, new InsnNode(DUP));
method.instructions.insertBefore(l5, new MethodInsnNode(INVOKESPECIAL, "java/lang/IllegalStateException", "<init>", "()V", false));
method.instructions.insertBefore(l5, new InsnNode(ATHROW));
// Try/catch
TryCatchBlockNode tryBlock = new TryCatchBlockNode(start, end, handler, "java/lang/IllegalStateException");
method.tryCatchBlocks.add(tryBlock);
// Init local variables
method.visitLocalVariable("_v0_" + Rand.alphaNumeric(5), "F", null, l0.getLabel(), l2.getLabel(), v0index);
method.visitLocalVariable("_v1_" + Rand.alphaNumeric(5), "F", null, l0.getLabel(), l2.getLabel(), v1index);
method.visitLocalVariable("_ex_" + Rand.alphaNumeric(5), "Ljava/lang/IllegalArgumentException;", null, start.getLabel(), handler.getLabel(), exindex);
}
}
其中method
为MethodNode
类型的混淆方法参数,class实现接口Opcodes
.
这很好用,但不是对所有方法都有效(我对字节码很陌生,所以不知道确切的情况)。例如,它适用于 main
方法:
原Java代码(在Procyon中反编译): https://p.reflex.rip/DLMT.cs
原始字节码: https://p.reflex.rip/ywJt.go
混淆的Java代码(在Procyon中反编译): https://p.reflex.rip/Er9V.cs
混淆字节码: https://p.reflex.rip/JBAb.go
但是,它破坏了其他方法之一,divMinByMax
,方法:
原Java代码(在Procyon中反编译): https://p.reflex.rip/AW9W.java
原始字节码: https://p.reflex.rip/GX2k.cpp
混淆Java代码(在Procyon中反编译,FAILED): https://p.reflex.rip/Eqju.java
混淆字节码: https://p.reflex.rip/isiX.cpp
当我尝试使用 java -jar
:
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.VerifyError: Inconsistent stackmap frames at branch target 27
Exception Details:
Location:
test/one/HelloRandom.divMinByMax(DD)D @21: goto
Reason:
Current frame's stack size doesn't match stackmap.
Current Frame:
bci: @21
flags: { }
locals: { double, double_2nd, float, float }
stack: { }
Stackmap Frame:
bci: @27
flags: { }
locals: { double, double_2nd, float, float }
stack: { 'java/lang/IllegalStateException' }
Bytecode:
0x0000000: 2826 9712 7145 1272 4624 2596 9a00 0ca7
0x0000010: 0014 9e00 48a7 0006 a7ff fabb 0016 59b7
0x0000020: 0073 bfbb 0016 59b7 0073 bf00 0000 0000
0x0000030: 00bf 0000 00bf 0000 0000 0000 00bf 0000
0x0000040: bf00 00bf 0000 bf00 00bf 0000 0000 0000
0x0000050: 00bf 0000 0000 0000 00bf 2826 6faf
Exception Handler Table:
bci [24, 27] => handler: 27
Stackmap Table:
full_frame(@18,{Double,Float,Float},{Integer})
same_locals_1_stack_item_frame(@24,Integer)
same_locals_1_stack_item_frame(@27,Object[#22])
same_locals_1_stack_item_frame(@35,Integer)
full_frame(@43,{},{Object[#159]})
same_locals_1_stack_item_frame(@50,Object[#159])
same_locals_1_stack_item_frame(@54,Object[#159])
same_locals_1_stack_item_frame(@62,Object[#159])
same_locals_1_stack_item_frame(@65,Object[#159])
same_locals_1_stack_item_frame(@68,Object[#159])
same_locals_1_stack_item_frame(@71,Object[#159])
same_locals_1_stack_item_frame(@74,Object[#159])
same_locals_1_stack_item_frame(@82,Object[#159])
append_frame(@90,Double,Float,Float)
same_locals_1_stack_item_frame(@93,Double)
我做了很多研究,我唯一找到的是原因:据我了解,问题是 @21
上的堆栈(GOTO
跳转到标签throw new IllegalStateException
):
stack: { }
(为空)与跳转目标标签 @27
处的堆栈不匹配:
stack: { 'java/lang/IllegalStateException' }
(其中包含它应该 "throw" 的异常)。
所以基本上,据我所知,错误发生在我尝试执行 GOTO <n>
跳转时,其中 <n>
是 "throws" 一个标签的编号IllegalStateException
.
我该如何解决这个问题?也许有一种方法可以让 @21
处的堆栈在跳跃之前也包含 java/lang/IllegalStateException
(这样这两个堆栈,一个在跳跃之前,一个在跳跃之后,匹配)?或者我可以用它做些什么?
您正在指令后插入异常处理程序,但是当指令是 条件 分支时,即 IFLE
或 IFGE
,分支可能不被采用并且代码流在指令之后继续,运行 进入异常处理程序。
这会造成不一致的状态,因为异常处理程序期望堆栈上有一个 Throwable
,而当代码流在检测指令之后继续时,堆栈上并不存在。但是当然,在那种情况下你不想执行异常处理程序,所以你必须插入另一个 GOTO
,从 l2
到 l5
如果我没看错的话。
检测 GOTO
指令时这不是问题,这些指令永远不会在指令之后继续。
在这个地方,我推荐一种不同的编码风格。在不同的参考节点之前和之后插入使得在阅读代码时无法预测实际代码结构。如果您只使用一个参考节点插入一个线性指令列表,它会更易于维护。