Tuesday, October 28, 2014

An extension of dex2jar's string decryption utility

Dex2jar is a popular open source tool, mainly used to convert Dalvik bytecode (.dex files) to Java bytecode (.jar or .class files); it acts as a sort of bridge between Android and Java environments.

Besides the bytecode conversion feature, dex2jar offers various utilities which help to deal with obfuscated Java code. Among others, a string decryption tool is provided. In brief, the tool works by accepting a user-supplied decryption routine, and then executes the routine every time it is called within the protected application. Since the decryption routine's output is, in fact, a decrypted string, dex2jar patches the bytecode by replacing all routine's invocations with decrypted strings. Although locating the decryption routine(s) is left to the analyst, this tool helps in obtaining a functionally equivalent unencrypted Java application.

However, its capabilities are rather limited. Indeed, it is only able to manage decryption functions which accept a single String or int parameter. As a consequence, a minimal variation of the decryption routine may make this tool ineffective. For example, as for October 2014, DashO Pro obfuscator provides string encryption by using both a String and an int parameter (or, sometimes, three parameters: int, int and String); dex2jar is unable to remove the protection because of multiple parameters. Custom decryption functions may cause trouble as well.

As a solution, I extended dex2jar's string decryption utility in order to make it applicable to a wider range of cases. In particular, boolean, byte, char and double parameters are now supported, besides String and int ones. Additionally, an arbitrary number of parameters is allowed, thus making possible to deal with more complex decryption routines, such as DashO Pro's ones. All the modifications affect a single source file, DecryptStringCmd.java, and no new dependency has been added.

UPDATE: These changes have now been merged with the current version of dex2jar. An updated description of its string decryption capabilities can be found here.

/* * dex2jar - Tools to work with android .dex and java .class files * Copyright (c) 2009-2012 Panxiaobo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.googlecode.dex2jar.tools; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.IntInsnNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import p.rn.util.FileOut; import p.rn.util.FileOut.OutHandler; import p.rn.util.FileWalker; import p.rn.util.FileWalker.StreamHandler; import p.rn.util.FileWalker.StreamOpener; import com.googlecode.dex2jar.tools.BaseCmd.Syntax; @Syntax(cmd = "uberdecrpyt", syntax = "[options] <jar>", desc = "Decrypts strings in a class file.\nExtended version of d2j-decrpyt-string.\n", onlineHelp = "https://code.google.com/p/dex2jar/wiki/DecryptStrings") public class DecryptStringCmd extends BaseCmd { public static void main(String[] args) { new DecryptStringCmd().doMain(args); } @Opt(opt = "f", longOpt = "force", hasArg = false, description = "force overwrite") private boolean forceOverwrite = false; @Opt(opt = "o", longOpt = "output", description = "output of .jar files, default is $current_dir/[jar-name]-decrypted.jar", argName = "out") private File output; @Opt(opt = "mo", longOpt = "decrypt-method-owner", description = "the owner of the mothed which can decrypt the stings, example: java.lang.String", argName = "owner") private String methodOwner; @Opt(opt = "mn", longOpt = "decrypt-method-name", description = "the owner of the mothed which can decrypt the stings, the method's signature must be static (type)Ljava/lang/String;", argName = "name") private String methodName; @Opt(opt = "cp", longOpt = "classpath", description = "add extra lib to classpath", argName = "cp") private String classpath; //extended parameter option: e.g. '-t int,byte,string' for decryptionRoutine(int a, byte b, String c) @Opt(opt = "t", longOpt = "arg-types", description = "comma-separated list of types:int,string,boolean,byte,char,double.Default is string", argName = "type") private String type = "string"; private String[] type_list; @Override protected void doCommandLine() throws Exception { if (remainingArgs.length != 1) { usage(); return; } File jar = new File(remainingArgs[0]); if (!jar.exists()) { System.err.println(jar + " is not exists"); return; } if (methodName == null || methodOwner == null) { System.err.println("Please set --decrypt-method-owner and --decrypt-method-name"); return; } if (output == null) { if (jar.isDirectory()) { output = new File(jar.getName() + "-decrypted.jar"); } else { output = new File(FilenameUtils.getBaseName(jar.getName()) + "-decrypted.jar"); } } if (output.exists() && !forceOverwrite) { System.err.println(output + " exists, use --force to overwrite"); return; } System.err.println(jar + " -> " + output); List<String> list = new ArrayList<String>(); if (classpath != null) { list.addAll(Arrays.asList(classpath.split(";|:"))); } list.add(jar.getAbsolutePath()); URL[] urls = new URL[list.size()]; for (int i = 0; i < list.size(); i++) { urls[i] = new File(list.get(i)).toURI().toURL(); } final Method jmethod; final String targetMethodDesc; try { //type is a comma-separated list of the decryption method's parameters type_list = type.split(","); //switch for all the supported types. String is default Class<?>[] argTypes = new Class<?>[type_list.length]; for (int i=0; i < type_list.length; i++) { switch (type_list[i]) { case "int": argTypes[i] = int.class; break; case "boolean": argTypes[i] = boolean.class; break; case "byte": argTypes[i] = byte.class; break; case "char": argTypes[i] = char.class; break; case "double": argTypes[i] = double.class; break; case "string": default: argTypes[i] = String.class; break; } } URLClassLoader cl = new URLClassLoader(urls); jmethod = cl.loadClass(methodOwner).getDeclaredMethod(methodName, argTypes); jmethod.setAccessible(true); targetMethodDesc = Type.getMethodDescriptor(jmethod); } catch (Exception ex) { System.err.println("can't load method: String " + methodOwner + "." + methodName + "(" + type + ")"); ex.printStackTrace(); return; } final String methodOwnerInternalType = this.methodOwner.replace('.', '/'); final OutHandler fo = FileOut.create(output, true); try { new FileWalker().withStreamHandler(new StreamHandler() { @Override public void handle(boolean isDir, String name, StreamOpener current, Object nameObject) throws IOException { if (isDir || !name.endsWith(".class")) { fo.write(isDir, name, current == null ? null : current.get(), nameObject); return; } ClassReader cr = new ClassReader(current.get()); ClassNode cn = new ClassNode(); cr.accept(cn, ClassReader.EXPAND_FRAMES); for (Object m0 : cn.methods) { MethodNode m = (MethodNode) m0; if (m.instructions == null) { continue; } AbstractInsnNode p = m.instructions.getFirst(); while (p != null) { if (p.getOpcode() == Opcodes.INVOKESTATIC) { MethodInsnNode mn = (MethodInsnNode) p; if (mn.name.equals(methodName) && mn.desc.equals(targetMethodDesc) && mn.owner.equals(methodOwnerInternalType)) { AbstractInsnNode q = p.getPrevious(); AbstractInsnNode next = p.getNext(); //arguments' list. it is now filled by reading bytecode backwards, //starting from the INVOKESTATIC statement Object[] arg_list = new Object[type_list.length]; //instructions' list. all the instructions to be deleted, //in order to substitute them with the decrypted string's LDC AbstractInsnNode[] instr_list = new AbstractInsnNode[arg_list.length + 1]; instr_list[instr_list.length - 1] = p; //each parameter's value is retrieved by reading bytecode backwards for (int i = arg_list.length - 1; i >= 0; i--) { //LDC: String and double cases (Opcodes.LDC comprehends LDC_W and LDC2_W. //The latter is used by double values) if (q.getOpcode() == Opcodes.LDC) { LdcInsnNode ldc = (LdcInsnNode) q; arg_list[i] = ldc.cst; instr_list[i] = q; //INT_INSN ("instruction with a single int operand", //e.g. BIPUSH and SIPUSH): int and byte cases, if the pushed value is > 5 } else if (q.getType() == AbstractInsnNode.INT_INSN) { IntInsnNode in = (IntInsnNode) q; arg_list[i] = in.operand; instr_list[i] = q; //ICONST_*: used by int, boolean and byte, if the pushed value is < 6 } else { switch (q.getOpcode()) { case Opcodes.ICONST_M1: case Opcodes.ICONST_0: case Opcodes.ICONST_1: case Opcodes.ICONST_2: case Opcodes.ICONST_3: case Opcodes.ICONST_4: case Opcodes.ICONST_5: int x = ((InsnNode) q).getOpcode() - Opcodes.ICONST_0; arg_list[i] = x; instr_list[i] = q; break; } } q = q.getPrevious(); } //All the parameters' values have been retrieved. //tryReplace invokes the decryption function and patches the bytecode tryReplace(m.instructions, instr_list, jmethod, arg_list, type_list); p = next; continue; } } p = p.getNext(); } } ClassWriter cw = new ClassWriter(0); cn.accept(cw); fo.write(false, cr.getClassName() + ".class", cw.toByteArray(), null); } }).walk(jar); } finally { IOUtils.closeQuietly(fo); } } public static AbstractInsnNode tryReplace(InsnList instructions, AbstractInsnNode[] instr_list, Method jmethod, Object[] args, String[] arg_types) { try { Object[] fixed_args = new Object[args.length]; //a cast is needed for char, boolean and byte values: //in these cases, the passed object is, in fact, an Integer. for (int i=0; i < arg_types.length; i++) { switch (arg_types[i]) { case "char": Method method = args[i].getClass().getMethod("intValue", null); fixed_args[i] = (char) (int) method.invoke(args[i], null); break; case "boolean": Method method2 = args[i].getClass().getMethod("intValue", null); fixed_args[i] = ((int) method2.invoke(args[i], null) == 0) ? false : true; break; case "byte": Method method3 = args[i].getClass().getMethod("byteValue", null); fixed_args[i] = (byte) method3.invoke(args[i], null); break; //String and double. since they are objects obtained via LDCs, no cast is needed. default: fixed_args[i] = args[i]; break; } } String newValue = (String) jmethod.invoke(null, fixed_args); LdcInsnNode nLdc = new LdcInsnNode(newValue); //insertion of the decrypted string's LDC statement, //before INVOKESTATIC statement (last element of instr_list array) instructions.insertBefore(instr_list[instr_list.length - 1], nLdc); //removal of INVOKESTATIC and previous push statements for (AbstractInsnNode instr : instr_list) { instructions.remove(instr); } return nLdc.getNext(); } catch (Exception e) { // ignore } return null; } }

No comments:

Post a Comment