吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 25743|回复: 59
收起左侧

[Android 原创] 使用Java Deobfuscator对JEB Decompiler反混淆

  [复制链接]
coldnight 发表于 2018-3-29 13:21
本帖最后由 coldnight 于 2018-3-29 14:45 编辑

简介

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最重要的成员是childrenparents。怎么理解这两个东西呢?前面进行字节码执行栈分析时我们有这样几条指令:

[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对应的FrameArrayStoreFrame。除了ArrayStoreFrame.parents,还有如下成员:

object // 保存的对象
index // 索引
array // 数组

array[index] = object
NEWARRAY对应的NewArrayFrame有成员lengthntype
部分Frame进行特殊处理,DUP的源和结果是同一个对象,因此可以省略。例如BASTORE <= [B <= DUP <= [B <= NEWARRAY[BDUP生成,实际上ArrayStoreFrame.arrayNewArrayFrame,而不是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个数组参数,可以通过查找NewArrayFramechildren获得,因为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;
}
  1. 得到解密结果
    可以通过复制解密函数或者调用MethodExecutor得到解密结果。
  2. 将解密结果插回原字节码:
    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();
    }

免费评分

参与人数 24吾爱币 +26 热心值 +23 收起 理由
176915785 + 1 + 1 我很赞同!
堂前燕 + 1 + 1 谢谢@Thanks!
wushaominkk + 3 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
nbhonghong + 1 + 1 用心讨论,共获提升!
code2018 + 1 + 1 谢谢@Thanks!
十立 + 1 + 1 谢谢@Thanks!
22222 + 1 + 1 热心回复!
第三世界 + 1 + 1 热心回复!
liwenhui0921 + 1 + 1 谢谢@Thanks!
vince991 + 1 + 1 热心回复!
sunnylds7 + 1 + 1 用心讨论,共获提升!
gink + 1 + 1 热心回复!
Mint_Grass + 1 + 1 热心回复!
liminghui168 + 1 + 1 谢谢@Thanks!
笙若 + 1 + 1 谢谢@Thanks!
叶冰瑶 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
_洲 + 1 我很赞同!
noth + 1 + 1 我很赞同!
poppig + 1 + 1 我很赞同!另外什么时候Patch一下最新的JEB demo
SomnusXZY + 1 + 1 热心回复!
tylerxi + 1 + 1 用心讨论,共获提升!
xinkui + 1 + 1 谢谢@Thanks!
夏雨微凉 + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
测试中…… + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

头像被屏蔽
憨厚小猪 发表于 2018-4-5 00:49
话说回复大佬的帖子会不会升级快点啊
[img=99999999999999999999,99999999999999999999]https://avatar.52pojie.cn/data/avatar/000/81/89/92_avatar_small.jpg[/img]
测试中…… 发表于 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

感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-25 01:32

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表