Anteriormente hablamos sobre el ofuscamiento por junk bytes en el algoritmo de barrido líneal. Este caso, explicaremos en detalle como los ciberdelincuentes ofuscan los binarios para dificultar el desensamblado utilizando el algoritmo de recursive traversal.
El algoritmo de recursive traversal posee como principal fortaleza la habilidad de lidiar de forma inteligente con el flujo de control del programa. De esta manera, es capaz de desensamblar, sin errores, datos que se encuentren embebidos compartiendo la presencia de instrucciones.
Asimismo, el algoritmo recursive traversal también cuenta con algunas vulnerabilidades. En primera instancia, cuando se está decodificando un salto, este algoritmo salta a aquellos sectores del programa que sean posibles destinos del propio salto. En otras palabras, asume que el programa tendrá un funcionamiento normal. El segundo punto débil es poder identificar las direcciones de destino de un salto indirecto. Para poder resolver esto, este tipo de algoritmos utiliza jump tables para asegurarse de poder resolver los saltos correctamente. Sin embargo, es posible que algunos códigos maliciosos exploten algunas de estas vulnerabilidades con las técnicas que se detallarán a continuación.
Branch Function
Esta técnica se basa en la propiedad de que luego de ejecutar una función se retorna a la siguiente instrucción luego de la instrucción call. Por lo tanto, es posible establecer los saltos incondicionales de una forma diferente. En primera instancia se define una función que realice un mapeo, tal como se muestra a continuación:
Si se aclara que un jump permite saltar a la dirección definida como destino, en resumen el salto queda establecido como el siguiente:
Esto puede redefinirse utilizando la función de mapeo que se definió anteriormente. Específicamente se reemplaza por una instrucción call, tal como se muestra a continuación:
De esta manera, la rutina será capaz de saltar a la dirección correspondiente de acuerdo a la dirección desde la cual fue llamada. En otras palabras, el salto sigue existiendo, solo que se ejecuta a través de una función específica. Si se analiza este comportamiento desde el punto de vista de un desensamblador, puede observarse que la función ya no se comporta de forma normal. Es decir, el retorno no será la siguiente instrucción luego de la instrucción call, sino que será la dirección destino del salto. Esto dificultará en gran medida al desensamblador que utilice el algoritmo recursive traversal ya que dará lugar a confusiones para realizar un seguimiento al flujo de ejecución. El comportamiento de la función queda claro a partir del siguiente gráfico:
Analizándolo desde el código, se podría encontrar algo como lo que se visualiza en la siguiente captura (nomenclatura Intel):
Predicados opacos
Otra de las técnicas para dificultar el desensamblado utilizando recursive traversal tiene que ver con el reemplazo de los saltos incondicionales con saltos condicionales. Específicamente, se reemplazan los saltos incondicionales por condicionales pero definiendo una condición que siempre será verdadera o falsa. La evaluación de la condición se realiza en tiempo de ejecución, por lo que resulta difícil para el desensamblador discernir de forma sencilla el resultado. De esta forma, es posible insertar junk bytes en aquellas direcciones donde nunca caerá la ejecución del programa. Lo importante de esta técnica es la construcción de condiciones que son tramposas y difíciles de evaluar aunque el resultado de dicha evaluación sea siempre el mismo.
Claramente, estas son algunas de las técnicas existentes para dificultar el análisis estático de los códigos maliciosos. Día a día los ciberdelincuentes buscan nuevos métodos para dificultar el análisis. Es importante entender que incluso los algoritmos de desensamblado más poderosos y robustos poseen vulnerabilidades, y es tarea del analista poder detectarlas para llevar a cabo un análisis fidedigno.
Fernando Catoira
Analista de Seguridad