简介
JEB Decompiler对关键字符串进行了混淆,直接反编译得到的源码如下:
public static final String getBuildTypeString() {
StringBuilder var0 = new StringBuilder();
if (isReleaseBuild()) {
var0.append(LQ.QS(new byte[]{-125, 23, 9, 9, 4, 18, 22, 74}, 1, 241));
} else {
var0.append(LQ.QS(new byte[]{-118, 1, 7, 23, 18, 72}, 1, 238));
}
if (isFullBuild()) {
var0.append(LQ.QS(new byte[]{46, 19, 25, 0, 67}, 1, 72));
} else {
var0.append(LQ.QS(new byte[]{39, 10, 29, 22, 93}, 2, 86));
}
...
}
本文介绍了使用Java Deobfuscator对JEB Decompiler进行自动化字符串反混淆的流程。
Java Deobfuscator 是基于ASM的用于Java反混淆的开源项目,采用模块化架构。
ASM
ASM库提供两类API用于生成和转换字节码:基于事件的核心API和基于对象的树API。这两类API可以类比于对XML进行读取的两种API: SAX和DOM。
基于事件API的核心类是ClassVisitor
。当一个类的方法、注解、成员被遍历时,ClassVisitor
类的对应方法将被调用。
例子:ClassReader
类可以读取.class文件,但是怎样才能得到类对应的内容呢?我们可以通过以下代码将字节码转变为基于对象的表示(类似于DOM):
ClassReader cr = new ClassReader(byteCodes);
ClassNode cn = new ClassNode();
cr.accept(cn, 0);
ClassNode
继承于ClassVisitor
。当调用cr.accept(cn, 0)
时,ClassReader
遍历类时,将调用ClassVisitor.visit()
方法,即ClassNode.visit()
,而ClassNode
将会相应的信息保存到自身的成员中:
public void ClassNode.visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.version = version;
this.access = access;
this.name = name;
this.signature = signature;
this.superName = superName;
if (interfaces != null) {
this.interfaces.addAll(Arrays.asList(interfaces));
}
。。。
}
注意到ClassWriter
也继承于ClassVisitor
,那么我们可以这样将读取到的类重新编译为字节码:
ClassReader cr = new ClassReader(byteCodes);
ClassWriter cw = new ClassWriter();
cr.accept(cw, 0);
byte[] byteCode = cw.toByteArray();
或者,增加一些中介ClassVisitor
,如ClassNode
:
ClassReader cr = new ClassReader(byteCodes);
ClassNode cn = new ClassNode();
cr.accept(cn, 0);
// 对cn进行修改
ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
classfileBuffer = cw.toByteArray();
我们可以在调用cn.accept(cw)
前,对cn
进行修改,之后传递给ClassWriter
保存。
Java 字节码简介
这里简要介绍反混淆时用到的知识。
字节码
Java源代码编译后生成.class文件。该文件包含Java运行所需字节码、元数据等。Java字节码是基于栈的指令集。代码运行时分为两个栈(Stack),局部变量栈(Local Variables Stack)保存函数运行时的临时变量,可以使用Index随机访问;操作数栈(Operand Stack)保存指令运行时需要的参数,先进先出,类似于C++的stack数据结构。
ASM将字节码指令映射到多个指令类,所有指令类均继承于AbstractInsnNode
:
LdcInsnNode:读取常量到操作数栈顶。
MethodInsnNode:调用指定函数,函数的所有参数(包含this指针)均从操作数栈上弹出,并压入返回值到栈顶。
VarInsnNode: 从局部变量栈上读取数据到操作数栈上,或者从操作数栈顶的数据保存到局部变量。
举例
字节码
SIPUSH 397
对应的指令类为:
new IntInsnNode(Opcodes.SIPUSH, 397);
该字节码将把常量397到操作数栈栈顶。
LDC "string"
对应的指令类为
new LdcInsnNode("string");
注意不同的字节码(Opcode)可能映射到同一个指令类:
BIPUSH 18
对应的指令类为:
new IntInsnNode(Opcodes.BIPUSH, 18);
和SIPUSH
使用同一个指令类。
类型描述
在.class中,不保存类信息,所有类均以全名保存。此外,类型表示和源文件有所区别:
类:以String类为例,全名为java.lang.String,表示为Ljava/lang/String
java.lang.Object -> Ljava/lang/Object
byte -> B
int -> I
short -> S
byte[] -> [B
bool -> Z
配置
根据官方文档使用方式有两种,一种是通过命令行启动,另一种是将Jar包作为库,使用其他Java代码调用。
这里我们使用第二种方式。
Java Deobfuscator针对不同的混淆器使用不同的Transformer。此外,还可在配置文件中使用"Detect: True",自动检测混淆器类型。
针对JEB的字符串混淆,我们自行编写一个Transformer
,该类继承自Transformer<TransformerConfig>
,并需要重写public boolean transform()
,返回值表示是否己修改类。
public class JEBDeobfuscator extends Transformer<TransformerConfig> {
public static void main(String[] args) {
Configuration config = new Configuration();
config.setInput(new File("template\\jeb2312.jar"));
config.setOutput(new File("output.jar"));
final String jdk_path = "xx\\lib\\"; // java 路径
ArrayList<File> path = new ArrayList<>(Arrays.asList(
new File(jdk_path + "rt.jar"),
new File(jdk_path + "jce.jar"),
new File(jdk_path + "ext\\jfxrt.jar"),
new File(jdk_path + "tools.jar")
));
File[] files = new File("lib\\jeb\\").listFiles();
path.addAll(Arrays.asList(files));
config.setPath(path);
config.setTransformers(Arrays.asList(
TransformerConfig.configFor(JEBDeobfuscator.class)
));
new Deobfuscator(config).start();
}
@Override
public boolean transform() {
ClassNode decryptorClz = classes.get(xxx);
}
}
首先,我们需要添加可能被使用的类路径,目前Java Deobfuscator只能识别Jar包里的classes,对于Java 9的模块文件支持有限。
之后,在transform()
方法中,可以对Map<String, ClassNode> classes
(即要反混淆的输入)中的类进行修改。
编写
确定解密函数
JEB Decompiler注册信息类com.pnfsoftware.jeb.client.Licensing的getBuildTypeString()多次调用了解密函数:
public static final String getBuildTypeString() {
StringBuilder var0 = new StringBuilder();
if (isReleaseBuild()) {
var0.append(LQ.QS(new byte[]{-125, 23, 9, 9, 4, 18, 22, 74}, 1, 241));
} else {
var0.append(LQ.QS(new byte[]{-118, 1, 7, 23, 18, 72}, 1, 238));
}
if (isFullBuild()) {
var0.append(LQ.QS(new byte[]{46, 19, 25, 0, 67}, 1, 72));
} else {
var0.append(LQ.QS(new byte[]{39, 10, 29, 22, 93}, 2, 86));
}
...
}
LQ.QS
即为解密函数,函数原型为:
static String LQ.QS(byte[] arr, int mode, int key)
在ASM中,使用MethodNode表示一个方法(基于对象的API),主要有以下成员:
public class MethodNode {
int access;
public String name;
public String desc;
// ...
}
对于解密函数,其成员如下:
decMth.name = "QS";
decMth.desc = "([BII)Ljava/lang/String;"
前面我们己经提到,使用ClassReader
可将.class文件读取为ClassNode
。对于解密函数,有:
cn.name = "LQ";
由于JEB每个版本都会重新混淆,导致解密函数发生变化,通过对Licensing.getBuildTypeString()
调用的函数进行统计,即可自动得知解密函数的类(owner)、名称(name)和描述(desc)。
private void findDecMth() {
Collection<ClassNode> cns = classNodes();
// 得到Licensing类
ClassNode licNode = cns.parallelStream()
.filter(classNode -> classNode.name.equals("com/pnfsoftware/jeb/client/Licensing"))
.findAny().orElse(null);
// 得到getBuildTypeString方法
MethodNode decMthNode = (MethodNode) licNode.methods.stream().filter(methodNode -> methodNode.name.equals("getBuildTypeString")).toArray()[0];
Arrays.stream(decMthNode.instructions.toArray())
.filter(ins -> ins.getOpcode() == Opcodes.INVOKESTATIC
&& ((MethodInsnNode) ins).desc.equals("([BII)Ljava/lang/String;"))
.forEach(ins -> {
MethodInsnNode mthNode = (MethodInsnNode) ins;
System.out.println(mthNode.owner + "." + mthNode.name + mthNode.desc);
});
}
这样即可找到解密函数。
参数分析
字节码对栈的影响
var0.append(LQ.QS(new byte[]{39, 10, 29, 22, 93}, 2, 86));
对应的字节码如下:
ALOAD 0
ICONST_5
NEWARRAY T_BYTE
DUP
ICONST_0
BIPUSH 39
BASTORE
DUP
ICONST_1
BIPUSH 10
BASTORE
DUP
ICONST_2
BIPUSH 29
BASTORE
DUP
ICONST_3
BIPUSH 22
BASTORE
DUP
ICONST_4
BIPUSH 93
BASTORE
ICONST_2
BIPUSH 86
INVOKESTATIC com/pnfsoftware/jebglobal/LQ.QS ([BII)Ljava/lang/String;
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
POP
下面以[Loc]表示局部变量栈,[Opn]表示操作数栈,在己知var0为StringBuilder
的情况下,初始状志下如:
[Loc] var0(String Builder)
[Opn]
各个字节码执行时局部变量栈的变化,用[arg]表示过字节码从栈中取出的参数:
ALOAD 0
[Opn] var0
ICONST_5
[Opn] var0 5
NEWARRAY T_BYTE
[arg] 5
[Opn] var0 [B // [B 为长度为5的byte数组
DUP
[arg] [B
[Opn] var0 [B [B
ICONST_0
[Opn] var0 [B [B 0
BIPUSH 39
[Opn] var0 [B [B 0 39
BASTORE // 将30保存到数组[B的第0位
[arg] [B 0 39
[Opn] var0 [B
// 下面的都是保存数组的每一位,不重复分析
// arr[1] = 10
DUP
ICONST_1
BIPUSH 10
BASTORE
// arr[2] = 29
DUP
ICONST_2
BIPUSH 29
BASTORE
// arr[3] = 22
DUP
ICONST_3
BIPUSH 22
BASTORE
// arr[4] = 93
DUP
ICONST_4
BIPUSH 93
BASTORE
[Opn] var0 [B // 栈顶为数组 new byte[]{39, 10, 29, 22, 93}, 2, 86)
ICONST_2
[Opn] var0 [B 2
BIPUSH 86
[Opn] var0 [B 2 86
INVOKESTATIC com/pnfsoftware/jebglobal/LQ.QS ([BII)Ljava/lang/String;
[arg] [B 2 86
[Opn] var0 String // 栈顶为解密结果
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
[arg] var0 String
[Opn] var0 // StringBuilder.append返回自身
POP
[arg] var0
[Opn]
JavaDeobfuscator Frame
为了获得解密函数的参数,我们需要跟踪字节码的执行流,得到构造参数的字节码,从而还原参数。
JavaDeobfuscator提供MethodAnalyzer
可以实现对字节码执行参数的解析。
// classNode为被分析的ClassNode,mthNode为被分析的MethodNode。
AnalyzerResult analyzerResult = MethodAnalyzer.analyze(classNode, mthNode);
Map<AbstractInsnNode, List<Frame>> frames = analyzerResult.getFrames();
analyzerResult.getFrames()
返回了AbstractInsnNode
(字节码指令)到Frame
(运行时栈帧)的映射。
Frame
最重要的成员是children
和parents
。怎么理解这两个东西呢?前面进行字节码执行栈分析时我们有这样几条指令:
[Opn] var0 [B // [B 为长度为5的byte数组
DUP
[arg] [B
[Opn] var0 [B [B
ICONST_0
[Opn] var0 [B [B 0
BIPUSH 39
[Opn] var0 [B [B 0 39
BASTORE // 将30保存到数组[B的第0位
[arg] [B 0 39
[Opn] var0 [B
以BASTORE
为例,他的参数有3个:byte数组、0、39,这三个参数是由其他指令生成的,如:
BASTORE <= [B <= DUP <= [B
BASTORE <= 0 <= ICONST_0
BASTORE <= 39 <= BIPUSH 39
箭头表示数据生成和传递的方向。可以看到DUP指令还接收了[B
作为指令参数(arg)。
对于Frame,A => B
就表示A对应的Frame(A_Frame)是B对应的Frame(B_Frame)的parents,B_Frame是A_Frame的children。
那么我们有
BASTORE_Frame.parents = [DUP_Frame ICONST_0_Frame BIPUSH_Frame]
ICONST_0_Frame.parents = []
ICONST_0_Frame.children = [BAStore_Frame]
在JavaDeobfuscator中,与ASM类似,多个指令可能映射到同一种Frame表示,例如:
BIPUSH --> LdcFrame
ICONST_0 --> LdcFrame
部分Frame的parents特化为成员,例如BASTORE
对应的Frame
为ArrayStoreFrame
。除了ArrayStoreFrame.parents
,还有如下成员:
object // 保存的对象
index // 索引
array // 数组
有array[index] = object
。
NEWARRAY
对应的NewArrayFrame
有成员length
、ntype
。
部分Frame
进行特殊处理,DUP
的源和结果是同一个对象,因此可以省略。例如BASTORE <= [B <= DUP <= [B <= NEWARRAY
的[B
由DUP
生成,实际上ArrayStoreFrame.array
为NewArrayFrame
,而不是DupFrame
。
提取解密函数参数
我们己经知道解密函数形参为(byte[]arr, int mode, int key)
,并且可以从字节码对应的Frame
中提取。
分以下步骤:
1.遍历所有类和方法,找到调用了解密函数的指令:
List<MethodInsnNode> insnNodes = new LinkedList<>();
for (int i = 0; i < mthNode.instructions.size(); ++i) {
AbstractInsnNode ins = mthNode.instructions.get(i);
if (ins.getOpcode() == INVOKESTATIC) {
MethodInsnNode mthInsNode = ((MethodInsnNode) ins);
if (mthInsNode.owner.equals(owner)
&& mthInsNode.desc.equals(desc)
&& mthInsNode.name.equals(name)) {
insnNodes.add(mthInsNode);
}
}
}
2.将指令映射为Frame:
Map<AbstractInsnNode, List<Frame>> frames = analyzerResult.getFrames();
List<MethodFrame> invoke_dec_frames = insnNodes.stream()
.map(frames::get)
.flatMap(Collection::stream)
.map(frame -> (MethodFrame)frame)
.collect(Collectors.toList());
3.从MethodFrame
中取得解密函数的参数:
List<Frame> frameArgs = mthFram.getArgs();
byte[] arg0 = getByteArray(((NewArrayFrame) frameArgs.get(0)));
int arg1 = ((Number) ((LdcFrame) frameArgs.get(1)).getConstant()).intValue();
int arg2 = ((Number) ((LdcFrame) frameArgs.get(2)).getConstant()).intValue();
第2个和第3个参数可以直接从LdcFrame.getConstant().intValue()
获得。
对于第1个数组参数,可以通过查找NewArrayFrame
的children
获得,因为NewArrayFrame
生成的数组引用被DUP
指令和BASTORE
指令使用。使用如下函数获得数组:
private byte[] getByteArray(NewArrayFrame newArrayFrame) {
if (newArrayFrame == null) {
return null;
}
int length = ((Number) ((LdcFrame) newArrayFrame.getLength()).getConstant()).intValue();
byte[] barr = new byte[length];
for (Frame frame : newArrayFrame.getChildren()) {
if (frame instanceof ArrayStoreFrame) {
int idx = ((Number) ((LdcFrame) ((ArrayStoreFrame) frame).getIndex()).getConstant()).intValue();
int value = ((Number) ((LdcFrame) (((ArrayStoreFrame) frame).getObject())).getConstant()).intValue();
barr[idx] = (byte) value;
}
}
return barr;
}
- 得到解密结果
可以通过复制解密函数或者调用MethodExecutor
得到解密结果。
- 将解密结果插回原字节码:
Map<Frame, AbstractInsnNode> frame2NodeMap = analyzerResult.getMapping();
MethodInsnNode invokeStaticNode = ((MethodInsnNode) frame2NodeMap.get(mthFram));
mthNode.instructions.insert(invokeStaticNode, new LdcInsnNode(str)); //插入解密结果
mthNode.instructions.insert(invokeStaticNode, new InsnNode(POP))); // 删除原计算结果
效果
public static final String getBuildTypeString() {
final StringBuilder sb = new StringBuilder();
if (isReleaseBuild()) {
final StringBuilder sb2 = sb;
wp.ox(new byte[] { 49, 10, 28, 28, 19, 26, 2, 71 }, 2, 211);
sb2.append("release/");
}
...
return sb.toString();
}