Ofuscación
La ofuscación se refiere a encubrir el significado de una comunicación haciéndola más confusa y complicada de interpretar.
Informática
En computación, la ofuscación se refiere al acto deliberado de realizar un cambio no destructivo, ya sea en el código fuente de un programa informático, en el código intermedio ([[bytecode]s) o en el código máquina cuando el programa está en forma compilada o binaria. Es decir, se cambia el código se "enrevesa" manteniendo el funcionamiento original, para dificultar su entendimiento. De esta forma se dificulta los intentos de ingeniería inversa y desensamblado que tienen la intención de obtener una forma de código fuente cercana a la forma original.
A las herramientas que realizan procesos de ofuscación se les llama ofuscadores. Los ofuscadores pueden actuar sobre el código fuente, sobre el código objeto o sobre ambos.
Como un efecto lateral, la ofuscación, en ocasiones, hace que los programas resultantes sean más pequeños (aunque puede hacer que los programas sean más grandes en otros casos).
Algunos lenguajes tienden más a la ofuscación que otros. C, C++ y Perl son los más citados como fácilmente ofuscables. Las macros de preprocesador son usadas a menudo para crear código complicado de leer enmascarando la gramática y sintaxis estándar del lenguaje del cuerpo principal de código.
Además, a veces, también se puede buscar que el código fuente resulte una obra de ascii art. Existen otros programas ofuscados llamados quine que al ejecutarse la salida debe ser el código fuente del programa.
Motivación
La motivación inmediata de la ofuscación es dificultar el entendimiento del código y por tanto dificultar la ingeniería inversa. El motivo original puede ser por:[1]
- Por seguridad. No queremos que un atacante comprenda el funcionamiento del código para que no lo altere con malos propósitos. Por ejemplo, si un programa contiene un control de licencias, nos puede interesar que nadie lo modifique para saltarse dicho control. O bien nos puede interesar que un software cliente no envíe peticiones malintencionadas al servidor. Hay que tener en cuenta que esto sólo consigue seguridad por oscuridad: el sistema es seguro simplemente porque nadie sabe cómo funciona. Pero si alguien consigue entender su funcionamiento mediante el análisis, toda la seguridad se va al traste. Por tanto, usar ofuscación con este propósito sólo tiene sentido como medida adicional para poner las cosas más difíciles a un atacante, porque no es infalible en absoluto.
- Ocultar código malicioso. Esta ocultación se realiza en dos ámbitos:[2]
- Dificulta la ingeniería inversa estática de malware haciendo más difícil encontrar las secciones de código más importante.
- Hace que las firmas estáticas utilizadas por los vendedores de antivirus no puedan detectar malware, ya que las firmas se basan en secuencias de bytes específicas en el binario
- Protección de la propiedad intelectual. Quizás no quieres que tu competencia copie cierto algoritmo que has utilizado.
- Como efecto colateral de otros procesos. Un ejemplo es la minificación: la transformación del código fuente de un programa con el propósito de reducir el número de bytes que ocupa. En lenguajes interpretados como JavaScript esto reduce el espacio de almacenamiento y el tiempo necesario para descargar un script por Internet. También hay quién dice que la optimización de código de un compilador consigue, en cierto modo, un cierto nivel de ofuscación al modificar el programa para mejorar su rendimiento.
- Pura diversión. Entender cómo funciona un programa ofuscado es como un rompecabezas, y un programa difícil de entender puede llegar a ser una obra de arte. Por ejemplo, el lenguaje de programación esotérico Brainfuck fue diseñado específicamente para hacer difícil la comprensión de sus programas. Y hay un concurso muy popular, el IOCCC (International Obfuscated C Code Contest), cuyo propósito es concebir un programa «útil» de la forma más incomprensible posible.
- Evitar o dificultar la decompilación. Algunos lenguajes, como por ejemplo Java, compilan a un lenguaje intermedio de bytecodes. FrecuenteEste código intermedio pueden ser fácilmente decompilado debido a que el bytecode contiene gran parte de la información del código fuente original. Para diferentes motivos (ingeniería inversa, sabotajes, piratería de software....) a veces es necesario ofuscar el código intermedio para impedir la decompilación o que esta obtenga un código incomprensible.[3]
Ejemplos
Un ejemplo simple de ofuscación es llamar a las variables o funciones con palabras reservadas del lenguaje añadiendo algún símbolo
int int_;
Con esta línea se define una variable de tipo entero.
long int _int(int int_){return int_-int_};
Con esta línea definimos una función con un parámetro entero que devuelve un valor long int, que por otra parte siempre será 0.
_int-_int;
Esto equivale a poner 0.
(_int-_int)!;
Esto equivale a poner 1.
(((!(int_-int_)<<!(int_-int_))<<(!(int_-int_)<<!(int_-int_)))|(!(int_-int_)<<!(int_-int_)));
Esto equivale a poner 10.
Técnicas
Hay distintas técnicas para realizar ofuscación. Estas técnicas se pueden clasificar según se aplican al diseño, a datos, a instrucciones o al flujo de control del programa.[4][5]
Técnicas de ofuscación del diseño
Algunas de las técnicas de ofuscación de diseño son:
- Unión de clases (en inglés class merging). Se usa en lenguajes orientados a objetos y consiste en la unión de una o más clases del programa en una sola clase.[4]
- División de clases (en inglés class splitting).Se usa en lenguajes orientados a objetos y consiste en dividir una clase de un determinado programa en varias clases.[4]
- Tipos ocultos (en inglés type hiding).Se usan en lenguajes orientados a objetos y consiste en utilizar los interfaces para oscurecer le intención final del diseño del programa[4]
Técnicas de ofuscación de datos
Estas técnicas se basan en aplicar diversas transformaciones a las variables, las constantes y las estructuras que forman parte de un programa.[4] Algunas de las ofuscaciones de datos son:
- Cegar constantes (en inglés constant blinding). En lugar de tener un valor en claro como constante, esta técnica sustituye los valores de las constantes, generalmente mediante una XOR con un valor aleatorio, por datos aparentemente sin sentido.[5]
- Cambio de la codificación de las variables. Cambiando la codificación de las variables se busca ocultar el valor original de la variable, por ejemplo podemos cambiar la codificación de un string, por su valor equivalente en otra codificación, por ejemplo codificación a Base64 o a un cifrado sencillo mediante XOR, ROT13 etc.[5]
- Conversión de datos estáticos en procesos. Consiste en conviertir un dato en un proceso que generará ese mismo dato.[4]
- Cambio del tiempo de vida de variables Consiste en convertir variable global en variable/s local/es.[4]
- Agregación de variables (en inglés merging variable. Consiste en agrupar variables en estructuras más complejas, por ejemplo juntando enteros en un mismo struct.[5]
- Separación de variables (en inglés splitting variable). En contraposición a la agregación de variables, la separación de variables consiste en dividir las variables en unidades más pequeñas, por ejemplo transformando un int de 4 bytes en dos shorts de 2 bytes.[5]
- Agregación de arrays (en inglés merging array. Consiste en agrupar varios arrays en uno solo.[4]
- Separación de arrays (en inglés splitting array). Consiste en dividir arrays y representar un único array mediante varios de ellos.[4]
- Aplanamiento de arrays (en inglés flatten array). Consiste en utilizar una variable de array de menor dimensión para representar un array de mayor dimensión.[4]
- Despliegue de arrays (en inglés fold array). Consiste en utilizar una variable de array de mayor dimensión para representar un array de menor dimensión.[4]
Técnicas de ofuscación de flujo de control
Consisten en una serie de transformaciones que alteran el flujo de ejecución de un programa. El flujo de ejecución de un programa se puede representar por un grafo de control de flujo (CFG), en el que los nodos son los bloques básicos de ejecución del programa, y las aristas son los saltos entre bloques básicos de ejecución (conjuntos de instrucciones que se ejecutan secuencialmente, en los que la primera instrucción es el único acceso al bloque y la última instrucción es la única de salida).[4]
Algunas de las ofuscaciones de control de flujo son:
- Inserción de código muerto (en inglés dead code insertion). Consiste en insertar código en el programa que no aporta funcionalidad. Los ejemplos más sencillos son la inserción de nops, guardar el valor de una variable, hacer operaciones matemáticas con ella para luego restaurar el valor original etc.[5]
- Uso de predicados opacos. Un predicado es una expresión lógica que se evalúa a true o false, y que generalmente se usa para dirigir el flujo de un programa. Un predicado es opaco si su valor en tiempo de ejecución es conocido durante el proceso de ofuscación. Estos predicados son aprovechables para ofuscar porque un atacante que quiera ver como funciona el programa, no sabe en tiempo de compilación el valor que tendrá el predicado. De esta forma se generan más ramas en el flujo de control y se molesta en la ingeniería inversa.[5][3] Por ejemplo si tenemos un bloque de instrucciones A;B y el predicado opaco P que siempre va a ser TRUE. Podemos crear la secuencia de control equivalente "A;IF P THEN B ELSE C". De esta forma un análisis estático de código no puede saber que A y B se ejecutan siempre de forma consecutiva.[3]
- Aplanado del flujo de control (en inglés control flow flattening). Consiste en modificar y reordenar los bloques básicos de un programa (basic block, BB) de forma que, en vez de tener en el CFG una estructura de decisión if-else típica en la que se sigue la ejecución a un BB o se salta a otro BB dependiendo del valor de los flags, se pasa a una estructura aplanada, en la que un BB llamado dispatcher decide a qué BB saltar en base al valor de una variable artificial. Cada BB de las ramas de decisión tiene el dispatcher como predecesor y sucesor, dificultando así averiguar la lógica del programa detrás del CFG aplanado.[5]
- Desenroscado de bucles (en inglés loop unrolling). Consiste en transformaciones de código que evitan tener instrucciones de salto condicional y variables para emular bucles. Esta técnica, además de ser computacionalmente menos costosa, genera bloques de instrucciones muy parecidos entre ellos, lo que aumenta la confusión en el análisis estático del código.[5]
- Inlining. Consiste en sustituir una llamada a un método por una copia del código del método.[4]
- Outlining. Consiste en la creación artificial de métodos mediante secciones de código no relacionadas, las cuales añaden un nivel de fingida abstracción.[3]
- Transformación de interpretación de tabla (en inglés table interpretation). Consiste en romper una secuencia de código en múltiples trozos pequeños y construir un bucle que a través de una secuencia de condiciones decide a cual de las secuencias de código se salta en cada momento.[6]
Técnicas de ofuscación de instrucciones
Consiste en sustituir la instrucción original por otra serie de instrucciones equivalentes más complicadas, generalmente mediante el uso de operaciones matemáticas. Por ejemplo Obfuscator-LLVM es una suit de compilación sobre LLVM que permite ofuscar el programa. [5]
Técnicas de ofuscación de layout
Buscan oscurecer y alterar la estructura léxica del software introduciendo cambios en el formato del código fuente. Por ejemplo renombrando variables o borrando información del debugger.[4]
Técnicas de ofuscación de código intermedio
Algunos lenguajes, como por ejemplo Java, compilan a un lenguaje intermedio de bytecodes. Este código intermedio pueden ser fácilmente decompilado para así obtener casi el mismo código fuente original. Por diferentes motivos (ingeniería inversa, sabotajes, piratería de software....) a veces es necesario ofuscar el código intermedio para impedir la decompilación o hacer que esta obtenga un código incomprensible.[3]
Por ejemplo para los bytecodes de Java se usa las siguientes técnicas de ofuscación:[7]
- Eliminar la información de depuración, como los nombres de las variables y la información del número de línea (ofuscación de layout).
- Manipulación de nombres de variables, métodos, nombres de paquetes y clases (ofuscación de layout).
- Codificación de literales de cadena y proporcione una función para decodificarlos (ofuscación de datos). Esto no afecta la salida final del ejecutable, pero el código descompilado se ve bastante feo y no es inmediatamente comprensible. Tal codificación se puede hacer para otros literales también como enteros y flotantes literales .
- Introducir código que sea equivalente en funcionalidad pero que sea razonablemente más complicado haciendo uso de declaraciones goto, condiciones verdaderas irrazonables en las declaraciones, bucles expandidos con algunas declaraciones basura válidas entre ellas (ofuscación de flujo de control).
- Insertar declaraciones no compilables en el código de bytes que no afectan la interpretación del código de bytes pero hace que fallen los descompiladores ya que no pueden descompilar dicho código defectuoso (ofuscación de flujo de control). La ejecución del código de bytes no se ve afectada debido a la inserción de este código defectuoso porque los intérpretes de código de bytes suelen estar muy relajados en la verificación de errores, suponiendo que el compilador ya hubiera hecho esa parte.
- Insertar código extra no utilizado (ofuscación de flujo de control).
- Utilizar sobrecarga de funciones y proporcionar el mismo nombre a todas las funciones con diferentes firmas(ofuscación de layout).
- Cambiar la información del número de línea (ofuscación de layout). La información del número de línea está presente en bytecode para ayudar a depurar un programa y los decompiladores usan esta información para construir con mayor precisión el código fuente original. Entonces los ofuscadores destruyen esta información para confundir aún más a los descompiladores.
Aportando seguridad
Las técnicas de ofuscación también pueden servir para dar seguridad. Por ejemplo, PointGuard es una extensión de GCC para proteger punteros. PointGuard cifra los valores de los punteros cuando estos están en memoria aplicando una XOR con una clave generada aleatoriamente cuando el proceso del programa arranca. Cuando un puntero se va a desreferenciar se descifra el valor del puntero, de esta forma, si un atacante consigue sobrescribir el valor del puntero, cuando este se desreferencie y por PointGuard se descifre, el atacante estará accediendo a una dirección de memoria aleatoria, y muy posiblemente si el acceso no es válido hará que el programa falle, frustrando así la explotación del programa.[5]
Herramientas
Hay multitud de herramientas para ofuscar, cada una aplicable a un lenguaje distintos. Ejemplos de este tipo de herramientas son:
- Para bytecodes java: Proguard, Javaguard, Retroguard, Jobfuscate, Jshrink, Jzipper, Marvin Obfuscator, Smokescreen, Yguard, Zelix KlassMaster, CafeBabe.[3]
- Para ofuscar javascript: Thicket, Jasob 2, Javascript Obfuscator, Stunnix JavaScript Obfuscator, JCE Pro, Scripts Encryptor (ScrEnc), Shane Ng’s GPL-licenced obfuscator, Dean Edwards JavaScript Compressor/Obfuscator, ESC, Jammer, JSCruncher Pro, Strong JS, JavaScript Scrambler, Javascript Encoder from scriptasylum.com.[8]
- Para .NET: pc-guard-net64, Phoenix Protector, obfuscar, Dotfuscator
- Para python: pyobfuscate, Intensio-Obfuscator, OBFAU3 (Autoit-Obfuscator).
- Para scripts de PHP: HideMyPHP.
Otros objetivos
La ofuscación puede servir para otros propósitos. Los médicos han sido acusados de usar una jerga para encubrir hechos desagradables de un paciente. El autor y doctor Michael Crichton ha afirmado que la escritura médica es un "intento altamente capacitado y calculado de confundir al lector". [1] De forma similar, el lenguaje basado en texto, como gyaru-moji y algunas formas de leet speak es ofuscado para hacerlo incomprensible a terceras personas.
Enlaces externos
- Competición de código ofuscado en C
- Protección en línea de librerías javascript (enlace roto disponible en Internet Archive; véase el historial, la primera versión y la última).
- The International Obfuscated C Code Contest (Concurso internacional de código en C ofuscado)
- Cómo proteger código en Java por medio de la ofuscación de código
- - Ofuscador de Código PHP online
- FOPO - Free Online PHP Obfuscator
- Análisis de ofuscación de código en Javscript
- ↑ Ofuscación de código: te reto a que entiendas este programa. robert. Universitat Oberta de Catalunya. 26 de octubre de 2015
- ↑ Metodologías de ofuscación de código de malware para evasión de antivirus. Alisa Esage. noticiasseguridad.com
- ↑ a b c d e f Estudio y Prueba de una Métrica para los Ofuscadores de Java. Ángel Miralles Arévalo. Proyecto Fin de Carrera. Universidad de Sevilla. Mayo de 2005
- ↑ a b c d e f g h i j k l m n Estudio de técnicas de inserción de marcas de agua sobre software. Marc Jáimez Moruno. Universidad Politécnica de Catalunya. 10 de junio de 2008
- ↑ a b c d e f g h i j k Crea un binario ilegible con estas técnicas. Irene Díez. deusto.es. 23 de junio de 2017
- ↑ Reversing: secrets of reverse Engineering. Eldad Eilam. Ed. Wiley. 1 de abril de 2005
- ↑ Code Obfuscation
- ↑ Lista de ofuscadores para Javascript. Luis sentidoweb.com. 11 de octubre de 2006