coldnight 发表于 2018-3-29 13:21

使用Java Deobfuscator对JEB Decompiler反混淆

本帖最后由 coldnight 于 2018-3-29 14:45 编辑

#简介
JEB Decompiler对关键字符串进行了混淆,直接反编译得到的源码如下:
```java
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):
```java
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`将会相应的信息保存到自身的成员中:
```java
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`,那么我们可以这样将读取到的类重新编译为字节码:
```java
ClassReader cr = new ClassReader(byteCodes);
ClassWriter cw = new ClassWriter();
cr.accept(cw, 0);
byte[] byteCode = cw.toByteArray();
```
或者,增加一些中介`ClassVisitor`,如`ClassNode`:
```java
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()`,返回值表示是否己修改类。
```java

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),主要有以下成员:
```java
public class MethodNode {
    int access;
    public String name;
    public String desc;
    // ...
}
```
对于解密函数,其成员如下:
```java
decMth.name = "QS";
decMth.desc = "([BII)Ljava/lang/String;"
```
前面我们己经提到,使用`ClassReader`可将.class文件读取为`ClassNode`。对于解密函数,有:
```java
cn.name = "LQ";
```
由于JEB每个版本都会重新混淆,导致解密函数发生变化,通过对`Licensing.getBuildTypeString()`调用的函数进行统计,即可自动得知解密函数的类(owner)、名称(name)和描述(desc)。
```java

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();
    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);
            });
}
```
这样即可找到解密函数。
##参数分析
### 字节码对栈的影响
```java
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
```
下面以表示局部变量栈,表示操作数栈,在己知var0为`StringBuilder`的情况下,初始状志下如:
```
var0(String Builder)

```
各个字节码执行时局部变量栈的变化,用表示过字节码从栈中取出的参数:
```
ALOAD 0
var0
ICONST_5
var0 5
NEWARRAY T_BYTE
5
var0 [B   // [B 为长度为5的byte数组
DUP
[B
var0 [B [B
ICONST_0
var0 [B [B 0
BIPUSH 39
var0 [B [B 0 39
BASTORE         // 将30保存到数组[B的第0位
[B 0 39
var0 [B
// 下面的都是保存数组的每一位,不重复分析
// arr = 10
DUP
ICONST_1
BIPUSH 10
BASTORE
// arr = 29
DUP
ICONST_2
BIPUSH 29
BASTORE
// arr = 22
DUP
ICONST_3
BIPUSH 22
BASTORE
// arr = 93
DUP
ICONST_4
BIPUSH 93
BASTORE
var0 {39, 10, 29, 22, 93}, 2, 86)
ICONST_2
var0 [B 2
BIPUSH 86
var0 [B 2 86
INVOKESTATIC com/pnfsoftware/jebglobal/LQ.QS ([BII)Ljava/lang/String;
[B 2 86
var0 String // 栈顶为解密结果
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
var0 String
var0 // StringBuilder.append返回自身
POP
var0

```
### JavaDeobfuscator Frame
为了获得解密函数的参数,我们需要跟踪字节码的执行流,得到构造参数的字节码,从而还原参数。
JavaDeobfuscator提供`MethodAnalyzer`可以实现对字节码执行参数的解析。
```java
// classNode为被分析的ClassNode,mthNode为被分析的MethodNode。
AnalyzerResult analyzerResult = MethodAnalyzer.analyze(classNode, mthNode);
Map<AbstractInsnNode, List<Frame>> frames = analyzerResult.getFrames();
```
`analyzerResult.getFrames()`返回了`AbstractInsnNode`(字节码指令)到`Frame`(运行时栈帧)的映射。
`Frame`最重要的成员是`children`和`parents`。怎么理解这两个东西呢?前面进行字节码执行栈分析时我们有这样几条指令:
```
var0 [B   // [B 为长度为5的byte数组
DUP
[B
var0 [B [B
ICONST_0
var0 [B [B 0
BIPUSH 39
var0 [B [B 0 39
BASTORE         // 将30保存到数组[B的第0位
[B 0 39
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 =
ICONST_0_Frame.parents = []
ICONST_0_Frame.children =
```

在JavaDeobfuscator中,与ASM类似,多个指令可能映射到同一种Frame表示,例如:
```
BIPUSH --> LdcFrame
ICONST_0 --> LdcFrame
```
部分Frame的parents特化为成员,例如`BASTORE`对应的`Frame`为`ArrayStoreFrame`。除了`ArrayStoreFrame.parents`,还有如下成员:
```
object // 保存的对象
index // 索引
array // 数组
```
有`array = 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;
    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 = (byte) value;
      }
    }
    return barr;
}
```
4. 得到解密结果
可以通过复制解密函数或者调用`MethodExecutor`得到解密结果。
5. 将解密结果插回原字节码:
```
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();
}
```

憨厚小猪 发表于 2018-4-5 00:49

话说回复大佬的帖子会不会升级快点啊{:301_978:}
https://avatar.52pojie.cn/data/avatar/000/81/89/92_avatar_small.jpg

测试中…… 发表于 2018-3-29 14:44

刚刚出炉的进来支持下~

任国富 发表于 2018-3-29 14:57

感谢大婶分享,谢谢

liyuanhang 发表于 2018-3-29 17:11

感谢分享

lthink 发表于 2018-3-29 17:19

感谢分享

澹泊明志 发表于 2018-3-29 17:46

卧槽,看不懂,反正很厉害就是了

_知鱼之乐 发表于 2018-3-29 19:00

        用心讨论,共获提升!

mayl8822 发表于 2018-3-29 19:16

感谢分享

BestMrMax 发表于 2018-3-29 19:49

谢谢分享~~~

PhysX 发表于 2018-3-29 20:38


感谢分享
页: [1] 2 3 4 5 6
查看完整版本: 使用Java Deobfuscator对JEB Decompiler反混淆