Como ocurre con la mayoría de los artículos de esta serie, estaba navegando un poco en Google y descubrí que algunas personas tenían interés en aprender a ofuscar código en Python. Naturalmente, pensé que sería un tema divertido. De ninguna manera soy un experto, pero estoy familiarizado con la idea. Como resultado, trate esto como un divertido experimento mental.

Descripción del problema

A diferencia de la mayoría de los artículos de esta serie, no busco una respuesta rápida a la ofuscación del código, el proceso de hacer que el código sea ilegible. En cambio, quiero ver varios métodos de ofuscación. Para hacer eso, necesitaremos un fragmento de código fuente bien formateado:

1
2
3
4
5
6
7
8
9
def read_solution(solution_path: str) -> list:
    """
    Reads the solution and returns it as a list of lines.
    :param solution_path: path to the solution
    :return: the solution as a list of lines
    """
    with open(solution_path, encoding="utf8") as solution:
        data = solution.readlines()
    return data

¡Frio! Aquí hay una función independiente que extraje de mi proyecto de autoevaluación . No es el mejor código del mundo, pero pensé que serviría como un buen ejemplo. Después de todo, es un fragmento corto que realiza una función simple: lee un archivo y descarga los resultados como una lista de líneas.

En este artículo, veremos algunas formas de hacer que este fragmento de código sea lo más ininteligible posible. Tenga en cuenta que no soy un experto en esto. Más bien, pensé que este sería un ejercicio divertido en el que todos podríamos aprender algo.

Soluciones

En esta sección, veremos varias formas de ofuscar código. En particular, tomaremos la solución original y la manipularemos gradualmente a lo largo de este artículo. Como resultado, cada solución no será una solución independiente. En cambio, será una adición a todas las soluciones anteriores.

Ofuscar código eliminando comentarios

Una forma segura de hacer que el código sea difícil de leer es comenzar evitando las mejores prácticas. Por ejemplo, podríamos comenzar eliminando cualquier comentario y cadena de documentación:

1
2
3
4
def read_solution(solution_path: str) -> list:
    with open(solution_path, encoding="utf8") as solution:
        data = solution.readlines()
    return data

En este caso, la solución es autodocumentada, por lo que es bastante fácil de leer. Dicho esto, la eliminación del comentario hace que sea un poco más difícil ver exactamente lo que logra este método.

Ofuscar código eliminando sugerencias de tipo

Con los comentarios fuera del camino, podemos comenzar a eliminar otras piezas útiles de sintaxis. Por ejemplo, tenemos algunos bits de sintaxis que ayudan a las personas a rastrear tipos de variables en todo el código. En particular, indicamos que el parámetro de entrada solution_pathdebería ser una cadena. Asimismo, también indicamos que la función devuelve una lista. ¿Por qué no eliminar esas sugerencias de tipo?

1
2
3
4
def read_solution(solution_path):
    with open(solution_path, encoding="utf8") as solution:
        data = solution.readlines()
    return data

Nuevamente, esta función aún es bastante manejable, por lo que no sería demasiado difícil averiguar qué hace. De hecho, casi todo el código Python se veía así en algún momento, por lo que no diría que hayamos alcanzado ningún nivel de ofuscación todavía.

Ofuscar código eliminando espacios en blanco

Otra opción para la ofuscación visual es eliminar todos los espacios en blanco superfluos. Desafortunadamente, en Python, el espacio en blanco tiene valor. De hecho, lo usamos para indicar alcance. Dicho esto, todavía hay trabajo que podemos hacer:

1
2
3
4
def read_solution(solution_path):
    with open(solution_path,encoding="utf8") as solution:
        data=solution.readlines()
    return data

Aquí, solo pudimos eliminar tres espacios: uno entre solution_pathencoding, uno entre data=, y uno entre =solution.readlines()Como resultado, el código sigue siendo bastante legible. Dicho esto, a medida que comencemos a ofuscar un poco más nuestro código, veremos que esta solución paga dividendos.

Ofuscar código abandonando las convenciones de nomenclatura

Una cosa sobre la que tenemos control total en el código son las convenciones de nombres. En otras palabras, decidimos cómo nombramos nuestras funciones y variables. Como resultado, es posible encontrar nombres que ofusquen por completo la intención de una variable o función:

1
2
3
4
def x(a):
    with open(a,encoding="utf8") as z:
        p=z.readlines()
    return p

Aquí, hemos perdido todo el valor semántico que normalmente obtenemos de los nombres de variables y funciones. Como resultado, es incluso difícil averiguar qué hace este programa.

Personalmente, no creo que esto vaya lo suficientemente lejos. Si fuéramos particularmente siniestros, generaríamos largas secuencias de texto para cada nombre, por lo que es aún más difícil de entender:

1
2
3
4
def IdDG0v5lX42t(hjqk4WN0WwxM):
    with open(hjqk4WN0WwxM,encoding="utf8") as ltZH4QOxmGy8:
        QVsxkg07bMCs=ltZH4QOxmGy8.readlines()
    return QVsxkg07bMCs

Demonios, incluso podría usar una sola cadena aleatoria de caracteres y solo modificar partes de ella. Por ejemplo, podríamos intentar usar el nombre de la función repetidamente con ligeras alteraciones (por ejemplo, 1 para l, O para 0, etc.):

1
2
3
4
def IdDG0v5lX42t(IdDG0v51X42t):
    with open(IdDG0v51X42t,encoding="utf8") as IdDGOv51X42t:
        IdDGOv51X4Rt=IdDGOv51X42t.readlines()
    return IdDGOv51X4Rt

Por supuesto, si bien esto parece más difícil de leer, nada realmente impide que el usuario use un IDE para seguir cada referencia. Del mismo modo, compilar y descompilar esta función (es decir .py -> .pyc -> .py) probablemente desharía todo nuestro trabajo duro. Como resultado, tendremos que profundizar más.

Ofuscar código manipulando cadenas

Otra forma de hacer que el código sea ininteligible es encontrar cadenas codificadas como "utf8" en nuestro ejemplo y agregarles una capa innecesaria de abstracción:

1
2
3
4
5
def IdDG0v5lX42t(IdDG0v51X42t):
    I6DGOv51X4Rt=chr(117)+chr(116)+chr(102)+chr(56)
    with open(IdDG0v51X42t,encoding=I6DGOv51X4Rt) as IdDGOv51X42t:
        IdDGOv51X4Rt=IdDGOv51X42t.readlines()
    return IdDGOv51X4Rt

Aquí, hemos construido la cadena "utf8" a partir de sus valores ordinales. En otras palabras, 'u' corresponde a 117, 't' corresponde a 116, 'f' corresponde a 102 y '8' corresponde a 56. Esta complejidad adicional todavía es bastante fácil de mapear. Como resultado, podría valer la pena introducir aún más complejidad:

1
2
3
4
5
def IdDG0v5lX42t(IdDG0v51X42t):
    I6DGOv51X4Rt="".join([chr(117),chr(116),chr(102),chr(56)])
    with open(IdDG0v51X42t,encoding=I6DGOv51X4Rt) as IdDGOv51X42t:
        IdDGOv51X4Rt=IdDGOv51X42t.readlines()
    return IdDGOv51X4Rt

En lugar de la concatenación directa, hemos introducido el método de unión. Ahora, tenemos una lista de caracteres como números. Invirtamos la lista solo para agregar un poco de entropía al sistema:

1
2
3
4
5
def IdDG0v5lX42t(IdDG0v51X42t):
    I6DGOv51X4Rt="".join(reversed([chr(56),chr(102),chr(116),chr(117)]))
    with open(IdDG0v51X42t,encoding=I6DGOv51X4Rt) as IdDGOv51X42t:
        IdDGOv51X4Rt=IdDGOv51X42t.readlines()
    return IdDGOv51X4Rt

¿Qué hay sobre eso? Ahora, tenemos aún más código que podemos comenzar a modificar.

Ofuscar código manipulando números

Con nuestra cadena "utf8" representada como una lista invertida de números, podemos comenzar a cambiar su representación numérica. Por ejemplo, 56 es realmente 28 * 2 o 14 * 2 * 2 o 7 * 2 * 2 * 2. Asimismo, Python admite varias bases, así que ¿por qué no introducir hexadecimal, octal y binario en la mezcla?

1
2
3
4
5
def IdDG0v5lX42t(IdDG0v51X42t):
    I6DGOv51X4Rt="".join(reversed([chr(2*2*7*2),chr(0x66),chr(0o164),chr(0b1110101)]))
    with open(IdDG0v51X42t,encoding=I6DGOv51X4Rt) as IdDGOv51X42t:
        IdDGOv51X4Rt=IdDGOv51X42t.readlines()
    return IdDGOv51X4Rt

De repente, no está claro con qué números estamos trabajando. Para agregar un poco de caos, pensé que sería divertido insertar un carácter de espacio en blanco:

1
2
3
4
5
def IdDG0v5lX42t(IdDG0v51X42t):
    I6DGOv51X4Rt="".join(reversed([chr(2*2*7*2),chr(0x66),chr(0o164),chr(0b1110101),chr(0x20)])).strip()
    with open(IdDG0v51X42t,encoding=I6DGOv51X4Rt) as IdDGOv51X42t:
        IdDGOv51X4Rt=IdDGOv51X42t.readlines()
    return IdDGOv51X4Rt

Luego, podemos llamar al método strip para eliminar ese espacio extra.

Ofuscar código introduciendo Dead Code

En el ejemplo anterior, agregamos un carácter de espacio en blanco a nuestra cadena para que sea un poco más difícil de decodificar. Ahora podemos tomar esa idea y comenzar a agregar código que realmente no hace nada:

1
2
3
4
5
6
7
8
def IdDG0v5lX42t(IdDG0v51X42t):
    I6DGOv51X4Rt="".join(reversed([chr(2*2*7*2),chr(0x66),chr(0o164),chr(0b1110101),chr(0x20)])).strip()
    if len(IdDG0v51X42t*3)>-1:
        with open(IdDG0v51X42t,encoding=I6DGOv51X4Rt) as IdDGOv51X42t:
            IdDGOv51X4Rt=IdDGOv51X42t.readlines()
        return IdDGOv51X4Rt
    else:
        return list()

Aquí, presento una rama muerta. En otras palabras, operamos bajo el supuesto de que la entrada es una cadena válida . Como resultado, podemos agregar un caso tonto en el que verificamos si la cadena tiene una longitud mayor que -1, lo cual siempre es cierto. Luego, en la rama muerta, devolvemos un valor genérico.

En este punto, ¿qué nos impide escribir un bloque muerto completamente ridículo? En otras palabras, en lugar de devolver un valor basura simple, podríamos construir un valor basura complejo:

1
2
3
4
5
6
7
8
9
def IdDG0v5lX42t(IdDG0v51X42t):
    I6DGOv51X4Rt="".join(reversed([chr(2*2*7*2),chr(0x66),chr(0o164),chr(0b1110101),chr(0x20)])).strip()
    if len(IdDG0v51X42t*3)>-1:
        with open(IdDG0v51X42t,encoding=I6DGOv51X4Rt) as IdDGOv51X42t:
            IdDGOv51X4Rt=IdDGOv51X42t.readlines()
        return IdDGOv51X4Rt
    else:
        IdDG0v51X42t=IdDG0v51X42t[len(IdDG0v51X42t)/2::3]*6
        return [I6DG0v51X42t for I6DG0v51X42t in IdDG0v51X42t]

Honestamente, podría haber puesto cualquier cosa en el bloque muerto. Por diversión, decidí jugar con la cadena de entrada. Por ejemplo, construí una subcadena y la repetí. Luego, construí una lista de los caracteres en esa nueva cadena.

Ofuscar código agregando parámetros muertos

Si podemos introducir ramas muertas, podemos introducir absolutamente parámetros muertos. Sin embargo, no queremos alterar el comportamiento de la función subyacente, por lo que queremos introducir parámetros predeterminados:

1
2
3
4
5
6
7
8
9
def IdDG0v5lX42t(IdDG0v51X42t,LdDG0v51X42t=0x173):
    I6DGOv51X4Rt="".join(reversed([chr(2*2*7*2),chr(0x66),chr(0o164),chr(0b1110101),chr(0x20)])).strip()
    if len(IdDG0v51X42t*3)>-1:
        with open(IdDG0v51X42t,encoding=I6DGOv51X4Rt) as IdDGOv51X42t:
            IdDGOv51X4Rt=IdDGOv51X42t.readlines()
        return IdDGOv51X4Rt
    else:
        IdDG0v51X42t=IdDG0v51X42t[len(IdDG0v51X42t)/2::3]*6
        return [I6DG0v51X42t for I6DG0v51X42t in IdDG0v51X42t]

Por supuesto, este parámetro no sirve actualmente. En otras palabras, intentemos hacer algo con él:

1
2
3
4
5
6
7
8
9
def IdDG0v5lX42t(IdDG0v51X42t,LdDG0v51X42t=0x173):
    I6DGOv51X4Rt="".join(reversed([chr(2*2*7*2),chr(0x66),chr(0o164),chr(0b1110101),chr(0x20)])).strip()
    if LdDG0v51X42t%2!=0 or len(IdDG0v51X42t*3)>-1:
        with open(IdDG0v51X42t,encoding=I6DGOv51X4Rt) as IdDGOv51X42t:
            IdDGOv51X4Rt=IdDGOv51X42t.readlines()
        return IdDGOv51X4Rt
    else:
        IdDG0v51X42t=IdDG0v51X42t[len(IdDG0v51X42t)/2::3]*6
        return [I6DG0v51X42t for I6DG0v51X42t in IdDG0v51X42t]

Ahora, hay algo hermoso en la expresión LdDG0v51X42t%2!=0Para mí, parece una contraseña, no una prueba para números impares.

Por supuesto, ¿por qué detenerse ahí? Otra cosa interesante que podemos hacer con los parámetros es aprovechar los argumentos de longitud variable:

1
2
3
4
5
6
7
8
9
def IdDG0v5lX42t(IdDG0v51X42t,LdDG0v51X42t=0x173,*LdDG0v51X42tf):
    I6DGOv51X4Rt="".join(reversed([chr(2*2*7*2),chr(0x66),chr(0o164),chr(0b1110101),chr(0x20)])).strip()
    if LdDG0v51X42t%2!=0 or len(IdDG0v51X42t*3)>-1:
        with open(IdDG0v51X42t,encoding=I6DGOv51X4Rt) as IdDGOv51X42t:
            IdDGOv51X4Rt=IdDGOv51X42t.readlines()
        return IdDGOv51X4Rt
    else:
        IdDG0v51X42t=IdDG0v51X42t[len(IdDG0v51X42t)/2::3]*6
        return [I6DG0v51X42t for I6DG0v51X42t in IdDG0v51X42t]

Ahora, hemos abierto la puerta a un número ilimitado de argumentos. Agreguemos algo de código para hacer esto interesante:

01
02
03
04
05
06
07
08
09
10
11
def IdDG0v5lX42t(IdDG0v51X42t,LdDG0v51X42t=0x173,*LdDG0v51X42tf):
    I6DGOv51X4Rt="".join(reversed([chr(2*2*7*2),chr(0x66),chr(0o164),chr(0b1110101),chr(0x20)])).strip()
    if LdDG0v51X42t%2!=0 or len(IdDG0v51X42t*3)>-1:
        with open(IdDG0v51X42t,encoding=I6DGOv51X4Rt) as IdDGOv51X42t:
            IdDGOv51X4Rt=IdDGOv51X42t.readlines()
        return IdDGOv51X4Rt
    elif LdDG0v51X42tf:
        return list()
    else:
        IdDG0v51X42t=IdDG0v51X42t[len(IdDG0v51X42t)/2::3]*6
        return [I6DG0v51X42t for I6DG0v51X42t in IdDG0v51X42t]

Nuevamente, nunca llegaremos a esta rama porque la primera condición siempre es verdadera. Por supuesto, el lector casual no lo sabe. De todos modos, divirtámonos un poco:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
def IdDG0v5lX42t(IdDG0v51X42t,LdDG0v51X42t=0x173,*LdDG0v51X42tf):
    I6DGOv51X4Rt="".join(reversed([chr(2*2*7*2),chr(0x66),chr(0o164),chr(0b1110101),chr(0x20)])).strip()
    if LdDG0v51X42t%2!=0 or len(IdDG0v51X42t*3)>-1:
        with open(IdDG0v51X42t,encoding=I6DGOv51X4Rt) as IdDGOv51X42t:
            IdDGOv51X4Rt=IdDGOv51X42t.readlines()
        return IdDGOv51X4Rt
    elif LdDG0v51X42tf:
        while LdDG0v51X42tf:
            LdDG0v51X42tx=LdDG0v51X42tf.pop()
            LdDG0v51X42tf.append(LdDG0v51X42tx)
        return LdDG0v51X42tf
    else:
        IdDG0v51X42t=IdDG0v51X42t[len(IdDG0v51X42t)/2::3]*6
        return [I6DG0v51X42t for I6DG0v51X42t in IdDG0v51X42t]

¡Sí, es un bucle infinito! Desafortunadamente, es algo obvio. Dicho esto, sospecho que los nombres de las variables oscurecerán la intención por un tiempo.

Otras formas de ofuscar el código

Una vez más, mencionaré que este artículo fue más un experimento mental para mí. Había visto código ofuscado en el pasado y pensé que sería divertido intentarlo yo mismo. Como resultado, aquí está el fragmento original y el fragmento final para comparar:

1
2
3
4
5
6
7
8
9
def read_solution(solution_path: str) -> list:
    """