rymga ← Volver a Lock Master

Ofuscación del flujo de control

Esta es la capa profunda. Reescribe cómo se bifurcan, iteran y calculan tus métodos para que no puedan reproducirse como Java limpio, aunque su ejecución sea idéntica bit por bit. Todos los fragmentos siguientes son resultados reales del motor: un método pequeño, una transformación y su descompilación posterior a Java para que puedas ver el efecto.

Ajustes predefinidos

No necesitas ocuparte de cada transformación. Configura enabled y elige un preset; Lock Master seleccionará un conjunto razonable:

{ "flow": { "enabled": true, "preset": "balanced" } }
Ajuste predefinidoQué se ejecuta
"light"Solo ofuscación estructural de bajo coste.
"balanced"Reestructuración del flujo de control e indirección, sin aritmética pesada.
"max"Todo lo descrito en esta página, además de las dos funciones que dificultan reconstruir el flujo de control: seedThreading —predicados opacos sensibles al contexto— y cfgFlattening —aplanamiento SSA real—. Es la opción más intensa; valídala primero en un servidor real.
"custom"Respeta los bloques explícitos que configures para cada técnica.

Para tener un control completo, usa "preset": "custom", an "intensity" —de 1 a 10— y activa técnicas individuales. Cada una es un objeto { "enabled": true, "weight": 5 }, donde weight —de 1 a 10— determina su intensidad. El resto de la página las agrupa según aquello que atacan.

Usa un ajuste predefinido o configura cada técnica, pero no ambas cosas. A preset selecciona el conjunto de técnicas e ignora los bloques configurados por técnica; "custom" —u omitir preset — sí los respeta. Ten en cuenta que, con sin un ajuste predefinido todas las técnicas están activadas de forma predeterminada — so "flow": { "enabled": true } por sí solo ejecuta todas , incluida reflectionObfuscation. Define siempre un ajuste predefinido o usa "custom" con exactamente las técnicas que quieras.

Predicados y código muerto

La base son instrucciones que parecen tener sentido, pero no lo tienen. Un predicado opaco es una condición cuya respuesta conoce el motor, pero no el descompilador, de modo que este no puede descartar la rama protegida.

bogusControlFlow

Intercala ramas protegidas por predicados opacos. Nunca cambian el resultado, pero quien lee el código no puede demostrarlo y debe seguirlas todas. Un método de dos líneas se convierte en una maraña:

Tu código
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
bogusControlFlow
public String tier(int n) {
    int n2 = a0;
    if (((n2 & n | n2 ^ n) - (n2 | n) | (n2 & n2) + (n2 | n2) - (n2 + n2)) != 0) {
        n2 = Integer.reverse(n2);
    }
    if (Integer.numberOfLeadingZeros(-1) != 0) {
        Integer n3 = n2;
    }
    if (n >= 500) {
        a0 ^= n2 & ~n2;
        return "gold";
    }
    a0 ^= Integer.compare(n2, n2);
    return "standard";
}

deadCode

Inyecta código verosímil que nunca se ejecuta; aquí, un valor siempre igual a cero protege un throw que nunca puede activarse. Parece una comprobación de seguridad real:

Tu código
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
deadCode
public String tier(int n) {
    int n2 = a0;
    int flags_9 = Integer.reverse(Integer.reverse(n2)) - n2;
    if (flags_9 != 0) {
        throw new IllegalArgumentException("assertion failed");
    }
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}

branchAugmentation

Divide las ramas existentes mediante condiciones opacas adicionales, de modo que un único if se convierte en un laberinto anidado con una rama muerta que parece accesible:

Tu código
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
branchAugmentation
public String tier(int n) {
    if (n >= 500) {
        int n2 = a0;
        if ((n2 | ~n2) == 0) {
            throw new AssertionError("unexpected value");
        }
        return "gold";
    }
    return "standard";
}

realisticBogus

Genera variantes realistas y convincentes de código falso en lugar de patrones aritméticos sencillos. Al mezclar sin fisuras la lógica falsa con la real, dificulta considerablemente el análisis estático.

nullInvariantPredicate

Aprovecha invariantes de nulabilidad entre bloques para crear predicados opacos potentes que un análisis sencillo del descompilador no puede simplificar en la práctica.

Destrucción de la estructura

Estas técnicas atacan la estructura del método, justo lo que reconstruye un descompilador para producir Java legible. Con suficiente intensidad, el descompilador genera basura o se rinde.

controlFlowFlattening

Disuelve la estructura del método en un bucle despachador controlado por una variable de estado. if/return desaparece y queda un switch que salta entre estados numéricos opacos. Ya no hay un flujo de control limpio que leer:

Tu código
public String tier(int n) {
    if (n >= 500) {
        return "gold";
    }
    return "standard";
}
controlFlowFlattening
public String tier(int n) {
    int n2 = a0;
    int n3 = 0x7E31B6E ^ n2;
    block5: while (true) {
        switch (n3 ^ n2) {
            case 132324206: 
            case 1894319798: {
                n3 = n >= 500 ? 0x1794DDD2 ^ n2 : 0x22892144 ^ n2;
            }
            default: {
                continue block5;
            }
            case 395632082: {
                return "gold";
            }
            case 579412292: 
        }
        break;
    }
    return "standard";
}

gotoSpaghetti

Reordena los bloques básicos, convierte cada continuación implícita en un salto explícito e intercala un bucle irreducible —un ciclo con dos puntos de entrada sin equivalente en Java estructurado—. CFR se ve obligado a emitir ** GOTO saltos y etiquetas sin estructurar —observa el // 3 sources donde confluyen las rutas y la Math.abs(sel ^ sel) != 0 guarda opaca— en vez del for bucle limpio original. Aun así, se ejecuta de forma idéntica bit por bit:

Tu código
public int score(int[] xs) {
    int total = 0;
    for (int x : xs) {
        if (x < 0) continue;
        if (x > 100) total += 100;
        else total += x;
    }
    return total;
}
gotoSpaghetti
public int score(int[] xs) {
    int total = 0;
    int[] arr = xs;
    int len = arr.length;
    int i = 0;
    int sel = 2;
    if (true) ** GOTO lbl19
    block0: while (true) {
        ++i;
        while (true) {
            if (i >= len) {
                return total;
            }
            int x = arr[i];
            if (x < 0) continue block0;
            if (x > 100) {
                total += 100;
                continue block0;
            }
            total += x;
            sel = 1;
lbl19:
            // 3 sources
            while (Math.abs(sel ^ sel) != 0) {
            }
            if (sel != 1) ** break;
            continue block0;
        }
        break;
    }
}

switchBogusCases

Envuelve el método en switch despachadores falsos con casos inaccesibles controlados por valores opacos. Un bucle trivial se convierte en tres máquinas de estados anidadas; este es el resultado real:

Tu código
public long factorial(int n) {
    long l = 1L;
    for (int i = 2; i <= n; ++i) {
        l *= (long)i;
    }
    return l;
}
switchBogusCases
public long factorial(int n) {
    int n2;
    long l;
    block12: while (true) {
        l = 1L;
        switch (new Object() instanceof String ? 1 : 0) {
            case 579412292: {
            }
            default: {
                break block12;
            }
            case 1894319798: {
                Integer.bitCount(42);
                continue block12;
            }
        }
        break;
    }
    block13: while (true) {
        n2 = 2;
        switch (n & ~n) {
            default: {
                break block13;
            }
            case 1081996856: {
                int cfr_ignored_0 = (new int[1])[n ^ n];
                continue block13;
            }
            case 1984262432: {
                int cfr_ignored_1 = (new int[1])[n ^ n];
            }
        }
        break;
    }
    block14: while (n2 <= n) {
        while (true) {
            l *= (long)n2;
            switch (~(~n2) - n2) {
                default: {
                    ++n2;
                    continue block14;
                }
                case 695721251: {
                    "".length();
                    continue block14;
                }
                case 30843304: 
            }
            int cfr_ignored_2 = (new int[1])[n ^ n];
        }
        break;
    }
    return l;
}

Bucles

Los bucles sirven de puntos de anclaje al descompilador. Estas técnicas los reescriben para eliminar ese apoyo.

loopTransforms

Reescribe los bucles con aristas de retorno y guardas opacas: condiciones adicionales dentro del cuerpo que nunca alteran la iteración, pero la amplían y la ocultan:

Tu código
public long factorial(int n) {
    long l = 1L;
    for (int i = 2; i <= n; ++i) {
        l *= (long)i;
    }
    return l;
}
loopTransforms
public long factorial(int n) {
    int n2 = 0;
    int n3 = a0;
    long l = 1L;
    for (int i = 2; i <= n; ++i) {
        l *= (long)i;
        if (((n2 += 4) ^ n2 ^ (n2 ^ n2) | Integer.highestOneBit(0)) == 0) continue;
    }
    return l;
}

fakeLoops

Envuelve código lineal en bucles que se ejecutan exactamente una vez, controlados por predicados opacos, de modo que una inicialización pasa a parecer una iteración condicional:

Tu código
public long factorial(int n) {
    long l = 1L;
    for (int i = 2; i <= n; ++i) {
        l *= (long)i;
    }
    return l;
}
fakeLoops
public long factorial(int n) {
    long l;
    int n2;
    do {
        l = 1L;
        n2 = a0;
    } while ((n2 & ~n2) != 0);
    for (int i = 2; i <= n; ++i) {
        l *= (long)i;
    }
    return l;
}

irreducibleFlow

Construye un flujo de control que no puede expresarse mediante bucles anidados limpios —un grafo irreducible —. Java no tiene sintaxis para representarlo, así que el descompilador recurre a saltos sin estructurar o a un bucle inaccesible que no puede eliminar:

Tu código
public String label(int n) {
    switch (n) {
        case 1: {
            return "start";
        }
        case 2: {
            return "stop";
        }
        case 3: {
            return "pause";
        }
    }
    return "unknown";
}
irreducibleFlow
public String label(int n) {
    switch (n) {
        case 1: {
            return "start";
        }
        case 2: {
            return "stop";
        }
        case 3: {
            return "pause";
        }
    }
    int n2 = a0;
    if ((n2 | ~n2) < 0) {
        return "unknown";
    }
    while (true) {
        if (!false) continue;
    }
}

Excepciones

Los manejadores de excepciones transportan un flujo de control que no aparece en el orden normal de las instrucciones. Son una cobertura perfecta y un método conocido para confundir a los descompiladores.

tryCatchAbuse

Envuelve el código en try/catch capas adicionales cuyos manejadores nunca se toman y hace pasar el control real por las tablas de excepciones:

Tu código
public int toInt(String string) {
    try {
        return Integer.parseInt(string);
    }
    catch (NumberFormatException numberFormatException) {
        return -1;
    }
}
tryCatchAbuse
public int toInt(String string) {
    try {
        try {
            return Integer.parseInt(string);
        }
        catch (NumberFormatException numberFormatException) {
            return -1;
        }
    }
    catch (UnsupportedOperationException unsupportedOperationException) {
        int n = a0;
        Integer.bitCount(n ^ n);
        throw unsupportedOperationException;
    }
}

exceptionFlow

Redirige el flujo de control normal a través de excepciones lanzadas y capturadas, y convierte una ruta de retorno sencilla en un manejador anidado:

Tu código
public int toInt(String string) {
    try {
        return Integer.parseInt(string);
    }
    catch (NumberFormatException numberFormatException) {
        return -1;
    }
}
exceptionFlow
public int toInt(String string) {
    try {
        return Integer.parseInt(string);
    }
    catch (NumberFormatException numberFormatException) {
        try {
            "".length();
            return -1;
        }
        catch (StringIndexOutOfBoundsException stringIndexOutOfBoundsException) {
            stringIndexOutOfBoundsException.getMessage();
            throw stringIndexOutOfBoundsException;
        }
    }
}

exceptionOverlap

Solapa deliberadamente rangos de excepciones de una forma que la sintaxis de Java no puede representar. Esta técnica no se limita a poner en apuros al descompilador: lo derrota: tanto CFR como Vineflower fallan al procesar el método en vez de producir Java. Como ninguna herramienta puede revertirlo, el «después» siguiente es una reconstrucción fiel creada a partir de la tabla real de excepciones del bytecode —y está etiquetada como tal—, no la salida de un descompilador:

Tu código
public long factorial(int n) {
    long l = 1L;
    for (int i = 2; i <= n; ++i) {
        l *= (long)i;
    }
    return l;
}
exceptionOverlap: reconstrucción —los descompiladores fallan—
// This method cannot be decompiled. CFR and Vineflower -- two independent,
// modern decompilers -- both crash on it with an irreducible-graph error.
// The transform installs two DELIBERATELY OVERLAPPING exception ranges over
// the same code, which Java's try/catch syntax cannot represent. That overlap
// is exactly what breaks the tools. The real bytecode exception table is:
//
//     from  to  target  type
//        1  16      25   InternalError        -> re-throw
//        2  17      25   ArithmeticException  -> re-throw   (the ranges overlap)
//
// Reconstruction below (illustrative -- NOT decompiler output, since no
// decompiler can produce one) of what that bytecode actually does:

public long factorial(int n) {
    long acc = 1L;
    /* guard A (bytes 1..16):  catch InternalError       -> re-throw */
    /* guard B (bytes 2..17):  catch ArithmeticException -> re-throw */
    /*   the two ranges overlap and share one handler -- no valid Java */
    for (int i = 2; i <= n; i++) {
        acc *= i;
    }
    return acc;
}

Constantes y datos

Aunque la estructura sea legible, los valores no tienen por qué serlo. Estas técnicas sustituyen los literales por expresiones que solo se resuelven durante la ejecución, por lo que nunca se ve el número, sino únicamente el cálculo que lo produce.

arithmeticMBA

Reescribe la aritmética como aritmética booleana mixta —MBA—: identidades construidas con ^ & | y + - que calculan el mismo resultado tras numerosas operaciones de bits. Un price * qty + 100 de una línea se convierte en esto:

Tu código
public int total(int n, int n2) {
    return n * n2 + 100;
}
arithmeticMBA
public int total(int n, int n2) {
    int n3 = 0;
    int n4 = 0;
    long l = 0L;
    long l2 = 0L;
    int n5 = 0;
    int n6 = 0;
    long l3 = 0L;
    long l4 = 0L;
    n6 = 100;
    n5 = n * n2;
    n4 = n6;
    n3 = n5;
    int n7 = ((n3 ^ n4) + (n3 & n4)) * 2;
    n4 = n6;
    n3 = n5;
    n4 = n3 + n4 - (n3 & n4) * 2;
    n3 = n7;
    return n3 + ~n4 + 1;
}

constantExpression

Sustituye una constante por una expresión equivalente. 100 desaparece; en su lugar queda algo que calcula 100 durante la ejecución sin declararlo en ningún punto:

Tu código
public int total(int n, int n2) {
    return n * n2 + 100;
}
constantExpression
public int total(int n, int n2) {
    return n * n2 + (Integer.bitCount(-1) ^ 0x44);
}

integerSplitting

Divide las constantes enteras en operaciones sobre otros valores. El umbral 500 se convierte en dos constantes combinadas mediante XOR; hay que ejecutarlo para conocer su valor:

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

Indirección

El último grupo oculta qué se comunica con qué : las llamadas, los accesos a campos y los límites de los métodos que permiten trazar el mapa de un programa.

invokeDynamic

Redirige las llamadas mediante un invokedynamic bootstrap, por lo que el archivo de clase no registra un destino directo, sino una llamada opaca con descriptores cifrados. El conmutador «mostrar caracteres» alterna entre secuencias de escape y glifos sin procesar. No es posible saber qué se invoca sin ejecutarlo:

Tu código
public int toInt(String string) {
    try {
        return Integer.parseInt(string);
    }
    catch (NumberFormatException numberFormatException) {
        return -1;
    }
}
invokeDynamic
public int toInt(String string) {
    try {
        return (int)a.bootstrap("x", 6, 1, "\u75c5\ua459\u0804\u4739\u1828\u324c\u7eda\uc98a", "\u75df\ua459\u0800\u472b\u1863\u3269\u7ed5\uc990\u8a0c\ueff3\u5f0b\ubc4a\u1dfa\u923c\uc214\ud6c7\u52ce", "\u759d\ua474\u081c\u472b\u183b\u3264\u7e9b\uc992\u8a0a\uefb3\u5f25\ubc0b\u1ddd\u922d\uc201\ud6cb\u52d2\u41ea\u5c9e\ucf8a\u7b93", -641825578, 1573233734, 318776947, 310234255, string);
    }
    catch (NumberFormatException numberFormatException) {
        return -1;
    }
}

reflectionObfuscation

Reescribe las llamadas y accesos a campos como reflexión, por lo que el archivo de clase deja de nombrar sus destinos. Una lectura y escritura sencillas se convierten en Class.forName(...).getDeclaredField(...):

Tu código
public int compute(int n) {
    int n2;
    int n3 = this.square(n);
    this.total = n2 = n3 + this.total;
    return n2;
}
reflectionObfuscation
public int compute(int n) {
    int n2;
    int n3 = this.square(n);
    Report report = this;
    Field field = Class.forName("demo.Report").getDeclaredField("total");
    field.setAccessible(true);
    int n4 = n2 = n3 + field.getInt(report);
    Report report2 = this;
    Field field2 = Class.forName("demo.Report").getDeclaredField("total");
    field2.setAccessible(true);
    field2.setInt(report2, n4);
    return n2;
}
Desactivado en todos los ajustes predefinidos. Como reescribe las llamadas mediante reflexión, puede romper código que dependa a su vez de la reflexión si no añades reglas keep adecuadas. Actívalo de forma deliberada y haz pruebas.

methodOutlining

Extrae fragmentos en métodos auxiliares sintéticos y reparte un cuerpo legible entre muchos. Aquí un único spread método se divide en once, cada uno de los cuales recibe las técnicas anteriores:

methodOutlining: lista de métodos
// one method in...
public void spread(int[], int, int);

// ...eleven out (each fragment its own synthetic method):
  private static void a1(int[], int);
  private static void a2(int[], int);
  private static void b3(int[], int);
  private static void b4(int[], int);
  private static void a5(int[], int, int);
  private static void b6(int[], int, int);
  private static void b7(int[], int, int);
  private static void a8(int[], int, int);
  private static void b9(int[], int, int);
  private static void b10(int[], int, int);

seedMutation

Propaga por el método una seed sintética mutable que alimenta los predicados opacos utilizados por las demás transformaciones. El valor cambia mientras se ejecuta el código, por lo que el análisis estático no puede simplificar los predicados:

Tu código
public long factorial(int n) {
    long l = 1L;
    for (int i = 2; i <= n; ++i) {
        l *= (long)i;
    }
    return l;
}
seedMutation
public long factorial(int n) {
    long l = 1L;
    a0 ^= Integer.reverse(n);
    int n2 = 2;
    a0 ^= Integer.reverse(a0);
    while (n2 <= n) {
        l *= (long)n2;
        a0 += Integer.bitCount(a0) << 1 | 1;
        a0 ^= Integer.reverse(++n2);
    }
    return l;
}

Opciones de seguridad

Tres opciones controlan las transformaciones que afectan a los tiempos y al enlazado:

CampoQué hace
timingSafeIndyValor predeterminado true. Evita invokedynamic el código sensible a los tiempos entre hilos, donde el primer enlazado podría alterar una condición de carrera. La mayoría de plugins de Bukkit o Spigot pueden configurarlo como false para obtener mayor cobertura; valídalo antes en un servidor real.
seedThreadingPredicados opacos sensibles al contexto propagados por los métodos. Un descompilador no puede simplificarlos localmente. Está desactivado de forma predeterminada; el "max" ajuste lo activa.
cfgFlatteningAplanamiento real del flujo de control basado en SSA. Está desactivado de forma predeterminada; el "max" ajuste lo activa.
excludeClases o paquetes —con nombres internos— que conservarán su flujo de control original; es la exclusión específica del flujo, además de keep. Úsalo para omitir código sensible a los tiempos o que haga un uso intensivo de la reflexión sin excluirlo del renombrado ni del cifrado de cadenas.
Todo sigue funcionando. Todas las transformaciones de esta página conservan exactamente el comportamiento. Antes de entregarse, cada compilación se vuelve a comprobar con las reglas de verificación de la propia JVM. Al apilar varias, el resultado es mucho más denso que cualquiera de los ejemplos individuales. Prueba la compilación protegida antes de distribuirla.

Excluir clases de la ofuscación del flujo

De forma predeterminada, la ofuscación del flujo se aplica a todas las clases. Para excluir código sensible a los tiempos, bucles críticos o componentes con mucha reflexión, enuméralos en flow.exclude. Conservarán el flujo original, pero seguirán renombrándose y recibiendo cifrado de cadenas; esta opción solo desactiva la ofuscación del flujo en esas clases.

La lista admite clases y paquetes, combinados libremente y sin límite. Cada entrada es un nombre interno: un nombre sin más (com/you/net/PacketCodec) coincide con esa clase y sus clases internas; una barra final (com/you/scheduler/) coincide con todo un árbol de paquetes. Puedes combinar paquetes y clases individuales:

{
  "flow": {
    "enabled": true,
    "preset": "max",
    "exclude": [
      "com/you/net/PacketCodec",
      "com/you/scheduler/",
      "com/you/timing/"
    ]
  }
}
Un único patrón en todas partes. La misma sintaxis —un nombre de clase sin más o un pkg/ prefijo, combinados en una lista— es la que utilizan rename.exclude, stringEncryptionExclude, keep y classGuard.encrypt .