利用ASM加密Jar包字符串

ASM是一个JAVA字节码分析、创建和修改的开源应用框架。
它可以动态生成二进制格式的stub类或其它代理类,或者在类被JAVA虚拟机装入内存之前,动态修改类。

一般情况下,Class文件是通过javac编译器产生的,然后通过类加载器加载到虚拟机内,再通过执行引擎去执行。
现在我们可以通过ASM的API直接生成符合Java虚拟机规范的Class字节流,
从这个角度来看,ASM承担的工作与javac解释器的工作相似。

ASM可以从字节码的角度,分析、创建一个类,同时也可以修改一个已经被编译过的类文件。
因此,我们可以通过ASM来实现诸如代码生成、代码混淆、代码转换等等以字节码为操作目标的工作。

本篇博客就记录一下利用ASM来加密Jar中字符串的方法。
这么做的初衷是:
Proguard只能混淆代码,无法混淆代码中使用的字符串。
想要混淆字符串,可能要借助收费的DexGuard。
于是,可以利用ASM来直接加密Jar包。

P.S.:
我目前的公司,产品以SDK为主,输出为Jar包。
因此这里以加密Jar包为例。


关于ASM框架的详细内容就不再赘述了,直接上加密Jar的代码。
这里依赖的lib为asm-all-5.2.jar和commons-lang3-3.5.jar。

1、定义加密字符的方法

package com.encrypt.util;

/**
 * Created by zhangjian on 18-3-29
 */
public class Transform {
    /**
     * 将字符串编码成16进制数字,适用于所有字符(包括中文)
     */
    public static byte[] encode(byte[] bytes, String key) {
        int len = bytes.length;
        int keyLen = key.length();
        for (int i = 0; i < len; i++) {
            bytes[i] = (byte) (bytes[i] ^ key.charAt(i % keyLen));
        }

        return bytes;
    }

    /**
     * 将16进制数字解码成字符串,适用于所有字符(包括中文)
     */
    public static String decode(byte[] bytes, String key) {
        int len = bytes.length;
        int keyLen = key.length();
        for (int i = 0; i < len; i++) {
            bytes[i] = (byte) (bytes[i] ^ key.charAt(i % keyLen));
        }

        return new String(bytes);
    }
}

2、定义Main Class

/**
 * Created by zhangjian on 18-3-29
 */
public class Encrypt {
    //Transform.java对应class文件路径
    private static final String TRANSFORM_FILE = Transform.class.getName().replace(".", "/") + ".class";

    public static void main(String[] args) throws IOException {
        if (args == null || args.length < 1) {
            return;
        }

        //读取传入的待加密的Jar包路径
        String path = args[0];
        if (!path.endsWith(".jar")) {
            return;
        }

        //读取Transform.java对应的Java字节码
        byte b[] = readClass();

        //指定输入和输出文件
        int index = path.lastIndexOf(".jar");
        File jarIn = new File(path);
        File jarOut = new File(path.substring(0, index) + "obfused.jar");

        try {
            //开始处理Jar包
            processJar(jarIn, jarOut, Charset.forName("UTF-8"), Charset.forName("UTF-8"), b);
        } catch (IllegalArgumentException e) {
            if ("MALFORMED".equals(e.getMessage())) {
                processJar(jarIn, jarOut, Charset.forName("GBK"), Charset.forName("UTF-8"), b);
            } else {
                throw e;
            }
        }
    }

    private static byte[] readClass() {
        InputStream in;
        try {
            in = Transform.class.getClassLoader().getResourceAsStream(TRANSFORM_FILE);
            int len = in.available();
            byte[] b = new byte[len];
            in.read(b);
            in.close();
            return b;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static void processJar(File jarIn, File jarOut, Charset charsetIn,
                                   Charset charsetOut, byte[] out) throws IOException {
        ZipInputStream zis = null;
        ZipOutputStream zos = null;
        try {
            zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(jarIn)), charsetIn);
            zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(jarOut)), charsetOut);

            HashSet<String> processedEntryNameSet = new HashSet<>();

            ZipEntry entryIn;
            while ((entryIn = zis.getNextEntry()) != null) {
                String entryName = entryIn.getName();

                if (!processedEntryNameSet.contains(entryName)) {
                    ZipEntry entryOut = new ZipEntry(entryIn);
                    entryOut.setCompressedSize(-1);
                    zos.putNextEntry(entryOut);

                    if (!entryIn.isDirectory()) {
                        if (entryName.endsWith(".class")) {
                            //开始处理Class文件
                            processClass(zis, zos);
                        } else {
                            copy(zis, zos);
                        }
                    }
                    zos.closeEntry();
                    processedEntryNameSet.add(entryName);
                }
            }

            ZipEntry inject = new ZipEntry(TRANSFORM_FILE);
            zos.putNextEntry(inject);
            zos.write(out);
            zos.closeEntry();
        } finally {
            closeQuietly(zos);
            closeQuietly(zis);
        }
    }

    private static void processClass(InputStream classIn, OutputStream classOut) throws IOException {
        //利用ASM中的ClassReader和ClassWriter来读、写class文件
        ClassReader cr = new ClassReader(classIn);
        ClassWriter cw = new ClassWriter(1);

        //通过ASM中的ClassVistor来访问class中的各个域,并进行修改
        ClassVisitor aia = ClassVisitorFactory.create(cr.getClassName(), cw);
        cr.accept(aia, 0);

        //最后写入
        classOut.write(cw.toByteArray());
        classOut.flush();
    }

    private static void copy(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[8192];

        int c;
        while ((c = in.read(buffer)) != -1) {
            out.write(buffer, 0, c);
        }
    }

    private static void closeQuietly(Closeable target) {
        if (target != null) {
            try {
                target.close();
            } catch (Exception e) {
                // Ignored.
            }
        }
    }
}

3、定义ClassVistor

/**
 * Created by zhangjian on 18-3-29
 */
public final class ClassVisitorFactory {
    private ClassVisitorFactory() {
    }

    public static ClassVisitor create(String className, ClassWriter cw) {
        //这里指定不修改Transform对应的class文件
        if (Transform.class.getName().replace('.', '/').equals(className)) {
            return createEmpty(cw);
        }

        //也可以增加类似白名单的功能,过滤其它不需要修改的类

        //StringFieldClassVisitor就是加密String的核心代码
        return new StringFieldClassVisitor(cw);
    }

    private static ClassVisitor createEmpty(ClassWriter cw) {
        return new ClassVisitor(Opcodes.ASM5, cw) {};
    }
}

我们来看看StringFieldClassVisitor:

/**
 * Created by zhangjian on 18-3-29
 */
class StringFieldClassVisitor extends ClassVisitor {
    private static final String TRANSFORM_PATH = Transform.class.getName().replace('.', '/');

    private boolean mIsClInitExists;

    private List<ClassStringField> mStaticFinalFields = new ArrayList<>();
    private List<ClassStringField> mStaticFields = new ArrayList<>();
    private List<ClassStringField> mFinalFields = new ArrayList<>();

    private String mClassName;

    StringFieldClassVisitor(ClassWriter cw) {
        super(Opcodes.ASM5, cw);
    }

    //这其实都是访问每个class文件的回调接口

    //记录当前访问的class name
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        mClassName = name;
        super.visit(version, access, name, signature, superName, interfaces);
    }

    //记录String类型的成员变量
    //ClassStringField是自定义的一个结构体
    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        if (ClassStringField.STRING_DESC.equals(desc) && name != null) {
            if ((access & Opcodes.ACC_STATIC) != 0 && (access & Opcodes.ACC_FINAL) != 0) {
                mStaticFinalFields.add(new ClassStringField(name, (String) value));
                value = null;
            }

            if ((access & Opcodes.ACC_STATIC) != 0 && (access & Opcodes.ACC_FINAL) == 0) {
                mStaticFields.add(new ClassStringField(name, (String) value));
                value = null;
            }

            if ((access & Opcodes.ACC_STATIC) == 0 && (access & Opcodes.ACC_FINAL) != 0) {
                mFinalFields.add(new ClassStringField(name, (String) value));
                value = null;
            }
        }
        return super.visitField(access, name, desc, signature, value);
    }

    //访问方法时,开始加密
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);

        if (mv != null) {
            if ("<clinit>".equals(name)) {
                mIsClInitExists = true;

                // If clinit exists meaning the static fields (not final) would have be inited here.
                mv = new MethodVisitor(Opcodes.ASM5, mv) {
                    private String lastStashCst;

                    @Override
                    public void visitCode() {
                        super.visitCode();
                        // Here init static final fields.
                        for (ClassStringField field : mStaticFinalFields) {
                            if (field.mValue == null) {
                                continue;
                            }

                            //加密StaticFinal域
                            encode(super.mv, field.mValue);

                            super.visitFieldInsn(Opcodes.PUTSTATIC, mClassName, field.mName, ClassStringField.STRING_DESC);
                        }
                    }

                    @Override
                    public void visitLdcInsn(Object cst) {
                        // Here init static or static final fields, but we must check field name int 'visitFieldInsn'
                        if (cst != null && cst instanceof String && !isEmptyAfterTrim((String) cst)) {
                            lastStashCst = (String) cst;
                            //加密
                            encode(super.mv, lastStashCst);
                        } else {
                            lastStashCst = null;
                            super.visitLdcInsn(cst);
                        }
                    }

                    @Override
                    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
                        if (mClassName.equals(owner) && lastStashCst != null) {
                            boolean isContain = false;
                            for (ClassStringField field : mStaticFields) {
                                if (field.mName.equals(name)) {
                                    isContain = true;
                                    break;
                                }
                            }
                            if (!isContain) {
                                for (ClassStringField field : mStaticFinalFields) {
                                    if (field.mName.equals(name) && field.mValue == null) {
                                        field.mValue = lastStashCst;
                                        break;
                                    }
                                }
                            }
                        }
                        lastStashCst = null;
                        super.visitFieldInsn(opcode, owner, name, desc);
                    }
                };
            } else if ("<init>".equals(name)) {

                // Here init final(not static) and normal fields
                mv = new MethodVisitor(Opcodes.ASM5, mv) {
                    @Override
                    public void visitLdcInsn(Object cst) {
                        // We don't care about whether the field is final or normal
                        if (cst != null && cst instanceof String && !isEmptyAfterTrim((String) cst)) {
                            //加密
                            encode(super.mv, (String) cst);
                        } else {
                            super.visitLdcInsn(cst);
                        }
                    }
                };
            } else {
                mv = new MethodVisitor(Opcodes.ASM5, mv) {
                    @Override
                    public void visitLdcInsn(Object cst) {
                        if (cst != null && cst instanceof String && !isEmptyAfterTrim((String) cst)) {
                            // If the value is a static final field
                            for (ClassStringField field : mStaticFinalFields) {
                                if (cst.equals(field.mValue)) {
                                    super.visitFieldInsn(Opcodes.GETSTATIC, mClassName, field.mName, ClassStringField.STRING_DESC);
                                    return;
                                }
                            }
                            // If the value is a final field (not static)
                            for (ClassStringField field : mFinalFields) {
                                // if the value of a final field is null, we ignore it
                                if (cst.equals(field.mValue)) {
                                    super.visitVarInsn(Opcodes.ALOAD, 0);
                                    super.visitFieldInsn(Opcodes.GETFIELD, mClassName, field.mName, "Ljava/lang/String;");
                                    return;
                                }
                            }
                            encode(super.mv, (String) cst);
                            return;
                        }
                        super.visitLdcInsn(cst);
                    }

                };
            }
        }
        return mv;
    }

    @Override
    public void visitEnd() {
        if (!mIsClInitExists && !mStaticFinalFields.isEmpty()) {
            MethodVisitor mv = super.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
            mv.visitCode();
            // Here init static final fields.
            for (ClassStringField field : mStaticFinalFields) {
                if (field.mValue == null) {
                    continue; // It could not be happened
                }
                encode(mv, field.mValue);
                mv.visitFieldInsn(Opcodes.PUTSTATIC, mClassName, field.mName, ClassStringField.STRING_DESC);
            }
            mv.visitInsn(Opcodes.RETURN);
            mv.visitMaxs(1, 0);
            mv.visitEnd();
        }
        super.visitEnd();
    }

    private boolean isEmptyAfterTrim(String str) {
        return str == null || str.length() == 0 || str.trim().length() == 0;
    }

    //实际的加密函数
    private void encode(MethodVisitor mv, String str) {
        String key = UUID.randomUUID().toString().replace("-", "").trim().substring(0, 6);

        //利用Transform中的函数加密
        byte[] enc = Transform.encode(str.getBytes(), key);

        int len = enc.length;
        mv.visitIntInsn(Opcodes.SIPUSH, len);
        mv.visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_BYTE);
        for (int i = 0; i < len; i++) {
            mv.visitInsn(Opcodes.DUP);
            mv.visitIntInsn(Opcodes.SIPUSH, i);
            mv.visitIntInsn(Opcodes.BIPUSH, enc[i]);
            mv.visitInsn(Opcodes.BASTORE);
        }
        mv.visitLdcInsn(key);
        //最后class中的string被替换为Transform.decode(new byte[] {字符串加密后的字符数组},  key)
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, TRANSFORM_PATH, "decode", "([BLjava/lang/String;)Ljava/lang/String;", false);
    }
}

上文中的ClassStringField就是个简单的结构体,类似于:

扫描二维码关注公众号,回复: 1999818 查看本文章
/**
 * Created by zhangjian on 18-3-29
 */
class ClassStringField {
    static final String STRING_DESC = "Ljava/lang/String;";

    String mName;
    String mValue;

    ClassStringField(String name, String value) {
        mName = name;
        mValue = value;
    }
}

4、总结
至此,主要思路记录完毕。
个人觉得为了更好地驾驭ASM,需要理解Class文件字节码的格式,
以及ASM解读Class文件的流程。

猜你喜欢

转载自blog.csdn.net/gaugamela/article/details/79747395