rymga ← Volver a Lock Master

Cifrado de cadenas

Los literales de texto son la entrada más rápida a un programa: busca un mensaje en el archivo de clase y llegarás directamente a la lógica que lo utiliza. Lock Master elimina todos los literales legibles y los reconstruye, cifrados, únicamente cuando hacen falta.

Por qué importa

Java compilado conserva cada literal en claro dentro del pool de constantes. Un mensaje de licencia, un endpoint, un permiso o un error están a un solo strings comando de distancia, y cada uno señala el código que lo rodea. Cifrarlos no solo oculta el texto: elimina el mapa que utiliza un atacante para orientarse.

Se activa con un único campo en techniques:

{ "techniques": { "stringEncryption": "POOL" } }

Todos los ejemplos siguientes son salida real del motor — un método pequeño cifrado con cada modo y después descompilado de nuevo a Java con una herramienta estándar, para que veas exactamente lo que se distribuye. Este es el original:

Tu código
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}

Los cuatro modos

stringEncryption admite cuatro valores. Se diferencian por el lugar donde donde residen los datos cifrados y la rutina de descifrado; eso determina lo difícil que resulta interceptarlos o extraerlos:

ValueQué hace
NONEDesactivado (valor predeterminado). Los literales se distribuyen en claro.
PER_METHODCada cadena se convierte en una llamada a un método de descifrado propio de la clase, que recibe el texto cifrado como argumento. La clave está ligada al punto de llamada.
INLINEEl bucle de descifrado se inserta in place en cada punto de uso: no existe un método auxiliar compartido en el que colocar un breakpoint.
POOLTodas las cadenas se trasladan a una clase de pool compartida y protegida con clave, y se recuperan por índice. Es la opción más compacta y elimina por completo los literales de la clase.

Los tres modos cifrados comparten una propiedad: la clave deriva de la runtime caller (see callerBinding); por eso, copiar una cadena cifrada y su llamada a otra clase produce basura al descifrarla. No existe una única rutina que permita «descifrarlo todo».

PER_METHOD

El literal desaparece y lo sustituye una llamada a un método de descifrado generado (Sample.a aquí) que recibe el texto cifrado y parámetros propios de cada cadena. El texto legible desaparece del pool de constantes; queda una carga opaca de caracteres mostrada como \uXXXX escapes. Usa el show characters selector del bloque para alternar entre los escapes y los glifos sin procesar:

Tu código
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
Después — PER_METHOD
public String tier(int n) {
    if (n >= 500) {
        return Sample.a("_", "\u23e4\u23e5\u23fd\u23fc", 9, 2, 6);
    }
    return Sample.a("_", "\u2300\u230d\u2306\u2303\u233f\u2320\u233d\u2351", 10, 6, 29);
}
¿Por qué los caracteres parecen flechas y símbolos? El texto cifrado no pertenece a ningún idioma: contiene valores arbitrarios de 16 bits producidos directamente por el cifrado. Una cadena Java String usa UTF-16, por lo que cada char entre 0x0000 y 0xFFFF es un carácter válido aunque no forme palabras; por eso sigue siendo Java válido. Los valores aleatorios aparecen por todo Unicode —flechas, símbolos técnicos, CJK, mongol o marcas combinadas— y parecen una mezcla de escrituras. No son not emoji reales —que viven en planos superiores y necesitan dos escapes—; algunos símbolos del BMP simplemente se muestran con aspecto de emoji. La \uXXXX es únicamente la forma en que un descompilador representa un carácter no ASCII dentro de un literal.

INLINE

No existe ningún método auxiliar: el descifrado —una secuencia XOR propia de cada cadena— se escribe directamente en el método. No hay una rutina compartida que interceptar y cada punto de uso es diferente. Las cadenas en claro "gold" and "standard" are nowhere to be found:

Tu código
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
Después — INLINE
public String tier(int n) {
    if (n >= 500) {
        char[] cArray = "\u2787\u2785\u2799\u279b".toCharArray();
        int n2 = cArray.length * 29;
        for (int i = 0; i < cArray.length; ++i) {
            int n3 = i;
            cArray[n3] = (char)(cArray[n3] ^ (18208 + 1653 ^ 0x6A01 ^ i * 10 ^ i >> 1 ^ n2));
        }
        return new String(cArray);
    }
    char[] cArray = "\u2ba8\u2ba7\u2baa\u2bad\u2b9f\u2b92\u2b99\u2b87".toCharArray();
    int n4 = cArray.length * 26;
    for (int i = 0; i < cArray.length; ++i) {
        int n5 = i;
        cArray[n5] = (char)(cArray[n5] ^ (12523 + 9927 ^ 0x7CB9 ^ i * 8 ^ i >> 6 ^ n4));
    }
    return new String(cArray);
}

POOL

La opción más fuerte en tamaño y separación: los literales salen por completo de la clase y pasan a un pool compartido (ab aquí), desde donde se recuperan por índice. El método no conserva datos de texto, solo una llamada de acceso:

Tu código
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
Después — POOL
public String tier(int n) {
    if (n >= 500) {
        return ab.ba("x", 0);
    }
    return ab.aa("x", 0);
}

También funciona con concatenaciones. Aquí, un saludo formado por dos literales y una variable: los literales se convierten en dos consultas al pool y solo permanece la variable recibida:

Después — POOL (una concatenación)
public String welcome(String string) {
    String string2 = string;
    return (String)((Object)ab.aa("x", 2)) + string2 + (String)((Object)ab.ab("x", 2));
}

Opciones

Los valores predeterminados sirven para casi todos los casos; estas opciones ajustan el comportamiento del descifrador:

CampoQué hace
callerBindingSTACKWALKER (predeterminado) deriva cada clave de la clase llamadora impuesta por la JVM; mover una cadena a otra clase produce una clave incorrecta. STACKTRACE es más barato, pero permite falsificar el llamador.
cacheModeFAST guarda en caché la cadena descifrada tras el primer uso. SAFE vuelve a descifrarla en cada acceso y después borra el char[] afterwards — more secure, slower.
assetPoolUbicación de los datos cifrados: INLINE (en una clase auxiliar) o EXTERNAL (in META-INF/strings.dat, completamente fuera de los archivos de clase).
stringEncryptionExcludeClases o paquetes —con nombres internos— cuyos literales deben seguir siendo legibles; es la exclusión específica del cifrado de cadenas, además de keep. Esas clases siguen sometiéndose a renombrado y ofuscación de flujo.
¿Cuál debería elegir? POOL es un excelente valor predeterminado: pequeño y limpio. Usa PER_METHOD or INLINE cuando quieras distribuir la lógica de descifrado por el código sin un único punto que atacar.

Excluir clases del cifrado de cadenas

El cifrado de cadenas se aplica a todas las clases. Para conservar algunos literales legibles —una versión que busca otra herramienta o textos que un framework compara por valor— añade esas clases a stringEncryptionExclude. Esas clases siguen renombrándose y ofuscando su flujo; solo se desactiva su cifrado de cadenas.

La lista admite clases y paquetes combinados libremente, sin límite práctico. Cada entrada es un internal name: a bare name (com/you/Version) coincide con esa clase y sus clases internas; una barra final (com/you/messages/) coincide con todo el árbol del paquete. Puedes combinar varios paquetes y clases individuales:

{
  "techniques": {
    "stringEncryption": "POOL",
    "rename": true,
    "stringEncryptionExclude": [
      "com/you/Version",
      "com/you/messages/",
      "com/you/templates/"
    ]
  }
}
Un mismo patrón en todas partes. La misma sintaxis —un nombre de clase o un pkg/ como prefijo, combinados en una sola lista— es la que utilizan rename.exclude, flow.exclude, keep and classGuard.encrypt all use.