
Suite de herramientas multifuncional de desofuscación Java
Threadtear es una herramienta multifuncional de desofuscación para Java. Próximamente se añadirá soporte para aplicaciones Android (actualmente trabajando en un conversor de dalvik a Java). Adecuada para un análisis de código más sencillo sin preocuparse demasiado por la ofuscación. Incluso los ofuscadores más caros como ZKM o Stringer están incluidos. Para una depuración más fácil, se incluyen otras herramientas. Inserta números de línea de depuración para entender mejor de dónde provienen las excepciones, o añade .printStackTrace() a los bloques try catch sin recompilar tu código. La compatibilidad inversa ya no es un problema, si no se utilizan métodos específicos de versión. Analiza el flujo de código en un gráfico para comprender mejor los algoritmos.





Una "ejecución" es una tarea que se ejecuta y modifica todos los archivos de clase cargados. Hay múltiples tipos de ejecuciones, que varían desde limpieza de bytecode hasta desofuscación de cadenas. Asegúrate de tenerlas en el orden correcto. Por ejemplo, las ejecuciones de limpieza deberían ejecutarse al final, pero también pueden ayudar a otras ejecuciones si se ejecutan primero. Si estás listo, haz clic en el botón "Run", y se ejecutarán en orden.
Utiliza esta herramienta bajo tu propio riesgo. Algunas ejecuciones utilizan ClassLoaders implementados para ejecutar código del archivo jar. Un atacante podría modificar el bytecode para que se ejecute código malicioso.
Las ejecuciones afectadas utilizan la clase me.nov.threadtear.asm.vm.VM.
Estas se utilizan principalmente para descifrar cadenas o recursos/ofuscación de acceso, ya que es mucho más fácil ejecutar los métodos de descifrado de forma remota.
Threadtear hace todo lo posible para protegerte de llamadas maliciosas (ejecuciones de código arbitrario) usando su propio SecurityManager, pero no hay garantía.
Especialmente con desofuscadores como los de ZKM o Stringer, debes tener mucho cuidado, ya que la reflexión debe estar permitida, de lo contrario no funcionarían.
Si descubres un ACE, por favor abre un issue. Intentaré solucionarlos lo antes posible.
Primero, ejecuta gradle build, luego gradle fatJar. En builds/libs debería haberse creado un archivo jar ejecutable. Si no deseas descargar el repositorio, puedes usar la última versión publicada.
Puedes crear fácilmente tu propia tarea de ejecución. Solo extiende me.nov.threadtear.execution.Execution:
public class MyExecution extends Execution {
public MyExecution() {
super(ExecutionCategory.CLEANING /* categoría */, "My execution" /* nombre */,
"Executes something" /* descripción, puede usar html */);
}
/**
* Este método se invoca cuando el usuario hace clic en el botón Run
* @return true si éxito, false si fallo
*/
@Override
public boolean execute(Map<String, Clazz> classes, boolean verbose) {
classes.values().stream().map(c -> c.node).forEach(c -> {
//transforma las clases aquí usando la API de árbol de ASM
});
return false;
}
}
Para cargar ClassNodes en tiempo de ejecución, usa la clase me.nov.threadtear.asm.vm.VM e implementa me.nov.threadtear.asm.vm.IVMReferenceHandler:
public class MyExecution extends Execution implements IVMReferenceHandler {
public MyExecution() {
super(ExecutionCategory.GENERIC, "My execution", "Loads ClassNodes at runtime");
}
@Override
public boolean execute(Map<String, Clazz> classes, boolean verbose) {
classes.values().stream().map(c -> c.node).forEach(c -> {
VM vm = VM.constructVM(this);
//transforma bytecode a java.lang.Class
Class<?> loadedClass = vm.loadClass(c.name.replace('/', '.'), true);
//haz cosas con tu clase aquí
loadedClass.getMethods()[0].invoke(...);
return true;
});
}
/**
* Será invocado por VM, cuando se llame a VM.loadClass
*/
@Override
public ClassNode tryClassLoad(String name) {
//intenta encontrar la clase a cargar en el archivo jar abierto
return classes.containsKey(name) ? classes.get(name).node : null;
}
}
Usando ConstantTracker (me.nov.threadtear.analysis.stack.ConstantTracker) puedes analizar métodos y rastrear valores de pila no variables.
Si, por ejemplo, se empuja iconst_0 a la pila, el valor en sí no se pierde como en el analizador ASM básico, y puedes usarlo para predecir cosas más adelante en el código.
public class MyExecution extends Execution implements IConstantReferenceHandler {
public MyExecution() {
super(ExecutionCategory.GENERIC, "My execution", "Performs stack analysis and replaces code.");
}
@Override
public boolean execute(Map<String, Clazz> classes, boolean verbose) {
classes.values().stream().map(c -> c.node).forEach(this::analyzeAndRewrite);
return true;
}
public void analyzeAndRewrite(ClassNode cn) {
cn.methods.forEach(m -> {
// este analizador mantiene los valores conocidos de la pila, p. ej., puede ser útil para la predicción de saltos
Analyzer<ConstantValue> a = new Analyzer<ConstantValue>(new ConstantTracker(this, Access.isStatic(m.access), m.maxLocals, m.desc, new Object[0]));
try {
a.analyze(cn.name, m);
} catch (AnalyzerException e) {
logger.severe("Failed stack analysis in " + cn.name + "." + m.name + ":" + e.getMessage());
return;
}
Frame<ConstantValue>[] frames = a.getFrames();
InsnList rewrittenCode = new InsnList();
Map<LabelNode, LabelNode> labels = Instructions.cloneLabels(m.instructions);
// reescribe las instrucciones del método
for (int i = 0; i < m.instructions.size(); i++) {
AbstractInsnNode ain = m.instructions.get(i);
Frame<ConstantValue> frame = frames[i];
// reemplaza / modifica instrucciones, etc...
if (frame.getStackSize() > 0) {
ConstantValue top = frame.getStack(frame.getStackSize() - 1);
if (top.isKnown() && top.isInteger()) {
int knownTopStackValue = top.getInteger();
// usa el valor conocido de la pila para eliminar saltos, simplificar código, etc...
// if(...) { rewrittenCode.add(...); }
continue;
}
}
rewrittenCode.add(ain.clone(labels));
}
// actualiza instrucciones y corrige bloques try catch, variables locales, etc...
Instructions.updateInstructions(m, labels, rewrittenCode);
});
}
/**
* Usa este método para predecir valores de pila si se cargan campos
*/
@Override
public Object getFieldValueOrNull(BasicValue v, String owner, String name, String desc) {
return null;
}
/**
* Usa este método para predecir valores de pila si se invocan métodos en objetos conocidos
*/
@Override
public Object getMethodReturnOrNull(BasicValue v, String owner, String name, String desc, List<? extends ConstantValue> values) {
if (name.equals("toCharArray") && owner.equals("java/lang/String")) {
if (!values.get(0).isKnown()) {
// el objetivo de la invocación no es conocido, no podemos calcular el valor de retorno
return null;
}
return ((String) values.get(0).getValue()).toCharArray();
}
return null;
}
}
¡No olvides añadir tu ejecución al árbol en me.nov.threadtear.execution.ExecutionLink!
Hay algunos trucos que pueden ayudarte a identificar y desofuscar archivos jar con éxito. Antes de ejecutar las ejecuciones, descompila el código para descubrir qué se necesita utilizar. Puedes usar el descompilador implementado para eso.
El mejor orden para una desofuscación es ejecuciones genéricas > desofuscación de acceso > desofuscación de cadenas > ejecuciones de limpieza.
Los ofuscadores muestran patrones que puedes usar para identificarlos. La forma más fácil de identificar un ofuscador es revisar el archivo META-INF/MANIFEST.MF.
Es posible que haya un atributo Obfuscated-By: XXX o Protected-By: XXX.
Código extremadamente ofuscado (en el flujo), a menudo notable por un método de descifrado de cadenas en el inicializador estático que contiene switches,
o métodos de descifrado de cadenas con un bloque switch muy largo (alrededor de 250 casos).
ZKM es uno de los mejores (y más antiguos) ofuscadores para Java, y muy caro. Tan antiguo como el ofuscador es su sitio web.

Si tu archivo jar contiene algunas clases especiales con enormes algoritmos de descifrado que son utilizados por la ofuscación de cadenas y la ofuscación de acceso, probablemente sea Stringer.
La protección no es mala y Stringer es uno de los ofuscadores más caros. A diferencia de los ofuscadores normales, no incluye ofuscación de nombres.
Se usa más como "segunda capa". Probablemente el 90% de las personas que usan este ofuscador están usando una crack, ya que cuesta más que un coche.
Si tu archivo ha sido ofuscado con múltiples ofuscadores, y Stringer es uno de ellos, debes comenzar tu desofuscación con Stringer, ya que la ofuscación de Stringer no se puede sobrescribir.
(Debido a la firma JAR personalizada y al uso de nombres de métodos durante el descifrado de cadenas)

Nombres de clase como IiIlIlIiIl o aUx, cOn, PrX indican ofuscación de Allatori.
Allatori es muy común entre archivos jar ofuscados, porque ofrece una demo gratuita accesible con unos pocos clics. La ofuscación no es tan difícil de revertir.

Paramorphism es como el hermano pequeño de Stringer, ya que se parece, pero no es tan bueno. También tiene algunas características interesantes que apuntan a bloquear herramientas de ingeniería inversa, que se pueden eliminar fácilmente.
La fuerza de ofuscación es comparable a Allatori.

Para otros ofuscadores puedes probar ejecuciones genéricas o abrir un issue, y veré qué puedo hacer.
Antes de seleccionar una ejecución, revisa los textos de sugerencia al pasar el ratón. Contienen una pequeña descripción de lo que hacen, pero también etiquetas que te ayudan a entender cómo se modificará el comportamiento de tu archivo JAR.
Threadtear está bajo la licencia GNU General Public License 3.0
Esta herramienta ha sido un montón de trabajo. Si te he ahorrado tiempo y quieres invitarme a un café, puedes hacerlo aquí: Donate with Bitcoin
Usa Threadtear solo para fines legales. Threadtear no pretende ser una herramienta de cracking, sino más bien un kit de herramientas de análisis de malware.
Por favor, abre un issue o envíame un correo electrónico si un transformador no funciona correctamente y adjunta el registro.
Ten en cuenta que los archivos de salida probablemente no sean ejecutables. Si aún así quieres intentar ejecutarlos, usa -noverify como argumento de JVM.
Esta herramienta está pensada para usarse con Java 8, pero probablemente también funcione en versiones superiores.