Análisis del código heredado cuando se pierde el código fuente: ¿hacer o no hacer?

El análisis del código binario, es decir, el código ejecutado directamente por la máquina, es una tarea no trivial. En la mayoría de los casos, si es necesario analizar el código binario, primero se restaura desarmando y luego descompilando en una representación de alto nivel, y luego analizan lo que sucedió.

Aquí debe decirse que el código que fue restaurado, de acuerdo con la representación del texto, tiene poco en común con el código que fue originalmente escrito por el programador y compilado en un archivo ejecutable. Es imposible recuperar exactamente el archivo binario recibido de lenguajes de programación compilados como C / C ++, Fortran, ya que esta es una tarea algorítmicamente no formalizada. En el proceso de convertir el código fuente que el programador escribió en el programa que ejecuta la máquina, el compilador realiza conversiones irreversibles.

En los años 90 del siglo pasado, se creía ampliamente que el compilador, como una picadora de carne, tritura el programa original, y la tarea de restaurarlo es similar a la tarea de restaurar un carnero de una salchicha.


Sin embargo, no está tan mal. En el proceso de obtención de salchichas, la oveja pierde su funcionalidad, mientras que el programa binario la guarda. Si la salchicha resultante pudiera correr y saltar, entonces las tareas serían similares.

Entonces, una vez que el programa binario ha conservado su funcionalidad, podemos decir que es posible restaurar el código ejecutable a una representación de alto nivel para que la funcionalidad del programa binario, cuya representación original no está presente, y el programa cuya representación textual recibimos, sean equivalentes.

Por definición, dos programas son funcionalmente equivalentes si, en los mismos datos de entrada, ambos completan o no completan su ejecución y, si la ejecución se completa, producen el mismo resultado.

La tarea de desmontaje generalmente se resuelve en un modo semiautomático, es decir, el especialista realiza la recuperación manual utilizando herramientas interactivas, por ejemplo, el desensamblador interactivo, radare u otra herramienta de IdaPro . Además, también en modo semiautomático, se realiza la descompilación. HexRays , SmartDecompiler u otro descompilador, que es adecuado para resolver esta tarea de descompilación, se utiliza como una herramienta de descompilación para ayudar a un especialista.

La restauración de la representación textual original del programa a partir del código de bytes puede hacerse bastante precisa. Para lenguajes interpretados como Java o lenguajes de la familia .NET, cuya traducción se realiza en código de bytes, la tarea de descompilación se resuelve de manera diferente. No estamos considerando este problema en este artículo.

Por lo tanto, puede analizar programas binarios a través de la descompilación. Por lo general, dicho análisis se realiza para comprender el comportamiento de un programa con el fin de reemplazarlo o modificarlo.

De la práctica de trabajar con programas heredados


Algún software, escrito hace 40 años en la familia de lenguaje de bajo nivel C y Fortran, controla los equipos de producción de petróleo. La falla de este equipo puede ser crítica para la producción, por lo que cambiar el software es altamente indeseable. Sin embargo, en los últimos años, los códigos fuente se perdieron.

El nuevo empleado del departamento de seguridad de la información, cuyas responsabilidades eran entender cómo funciona, descubrió que el programa de monitoreo del sensor escribe algo en el disco con cierta regularidad, y que escribe y cómo se puede utilizar esta información no está claro. También tuvo la idea de que el monitoreo del funcionamiento de los equipos se puede mostrar en una pantalla grande. Para hacer esto, era necesario entender cómo funciona el programa, qué y en qué formato escribe en el disco, cómo se puede interpretar esta información.

Para resolver el problema, se aplicó la tecnología de descompilación, seguida de un análisis del código restaurado. Primero desensamblamos los componentes del software uno por uno, luego localizamos el código responsable de la entrada / salida de la información y gradualmente comenzamos a recuperarnos de este código, dadas las dependencias. Luego, se restableció la lógica del programa, lo que permitió responder a todas las preguntas del servicio de seguridad con respecto al software analizado.

Si necesita analizar un programa binario para restaurar la lógica de su funcionamiento, restaurar parcial o completamente la lógica de convertir datos de entrada en datos de salida, etc., es conveniente hacerlo utilizando un descompilador.

Además de tales tareas, en la práctica existen problemas de análisis de programas binarios para los requisitos de seguridad de la información. Además, el cliente no siempre comprende que este análisis lleva mucho tiempo. Parecería hacer la descompilación y ejecutar el código resultante con un analizador estático. Pero como resultado de un análisis cualitativo, casi nunca tiene éxito.

Primero, las vulnerabilidades encontradas deben ser capaces no solo de encontrar, sino también de explicar. Si se encontró la vulnerabilidad en un programa en un lenguaje de alto nivel, el analista o la herramienta de análisis de código muestra qué fragmentos del código contienen ciertos defectos, cuya presencia causó la vulnerabilidad. ¿Qué pasa si no hay código fuente? ¿Cómo mostrar qué código causó la vulnerabilidad?

El descompilador recupera el código que está "lleno" con artefactos de recuperación, y es inútil mapear la vulnerabilidad revelada a dicho código, pero nada está claro. Además, el código restaurado está mal estructurado y, por lo tanto, se presta mal a las herramientas de análisis de código. Explicar la vulnerabilidad en términos de un programa binario también es difícil, porque la persona para quien se hace la explicación debe estar bien versado en la representación binaria de los programas.

En segundo lugar, un análisis binario de acuerdo con los requisitos de seguridad de la información debe llevarse a cabo con un entendimiento de qué hacer con el resultado resultante, ya que es muy difícil corregir una vulnerabilidad en un código binario, pero no hay código fuente.

A pesar de todas las características y dificultades de realizar un análisis estático de programas binarios de acuerdo con los requisitos de seguridad de la información, existen muchas situaciones en las que dicho análisis es necesario. Si por alguna razón no hay código fuente y el programa binario realiza una funcionalidad crítica para los requisitos de seguridad de la información, debe verificarse. Si se encuentran vulnerabilidades, dicha aplicación debe enviarse para su revisión, si es posible, o se debe hacer un "shell" adicional para ello, lo que permitirá controlar el movimiento de información confidencial.

Cuando la vulnerabilidad estaba oculta en un archivo binario


Si el código que ejecuta el programa tiene un alto nivel de criticidad, incluso si el código fuente del programa está en un lenguaje de alto nivel, es útil auditar el archivo binario. Esto ayudará a eliminar las características que el compilador puede introducir al realizar transformaciones de optimización. Entonces, en septiembre de 2017, la transformación de optimización realizada por el compilador de Clang fue ampliamente discutida. Su resultado fue una llamada a una función que nunca debería llamarse.

#include <stdlib.h> typedef int (*Function)(); static Function Do; static int EraseAll() { return system("rm -rf /"); } void NeverCalled() { Do = EraseAll; } int main() { return Do(); } 

Como resultado de las transformaciones de optimización, el compilador obtendrá dicho código de ensamblador. El ejemplo fue compilado bajo Linux X86 con el indicador -O2.

  .text .globl NeverCalled .align 16, 0x90 .type NeverCalled,@function NeverCalled: # @NeverCalled retl .Lfunc_end0: .size NeverCalled, .Lfunc_end0-NeverCalled .globl main .align 16, 0x90 .type main,@function main: # @main subl $12, %esp movl $.L.str, (%esp) calll system addl $12, %esp retl .Lfunc_end1: .size main, .Lfunc_end1-main .type .L.str,@object # @.str .section .rodata.str1.1,"aMS",@progbits,1 .L.str: .asciz "rm -rf /" .size .L.str, 9 

Hay un comportamiento indefinido en el código fuente. La función NeverCalled () se llama debido a las transformaciones de optimización que realiza el compilador. Durante el proceso de optimización, lo más probable es que realice un análisis de alias y, como resultado, la función Do () recibe la dirección de la función NeverCalled (). Y dado que el método main () llama a la función Do (), que no está definida, que es un comportamiento indefinido (comportamiento indefinido), el resultado es el siguiente: se llama a la función EraseAll (), que ejecuta el comando rm -rf /.

El siguiente ejemplo: como resultado de las transformaciones de optimización del compilador, perdimos la comprobación de un puntero a NULL antes de desreferenciarlo.

 #include <cstdlib> void Checker(int *P) { int deadVar = *P; if (P == 0) return; *P = 8; } 

Dado que la línea 3 hace referencia al puntero, el compilador asume que el puntero no es cero. A continuación, la línea 4 se eliminó como resultado de la optimización "eliminación de código inalcanzable" , porque la comparación se considera redundante, y después de eso, el compilador eliminó la línea 3 como resultado de la optimización " eliminación de código muerto". Solo queda la línea 5. El código del ensamblador resultante de compilar gcc 7.3 en Linux x86 con el indicador -O2 se muestra a continuación.

  .text .p2align 4,,15 .globl _Z7CheckerPi .type _Z7CheckerPi, @function _Z7CheckerPi: movl 4(%esp), %eax movl $8, (%eax) ret 

Los ejemplos anteriores del trabajo de optimización del compilador son el resultado del comportamiento indefinido de UB en el código. Sin embargo, este es un código bastante normal que la mayoría de los programadores consideraría seguro. Hoy, los programadores pasan tiempo eliminando comportamientos indefinidos en el programa, mientras que hace 10 años no le prestaban atención. Como resultado, el código heredado puede contener vulnerabilidades de UB.

La mayoría de los analizadores de código fuente estático modernos no detectan errores relacionados con UB. Por lo tanto, si el código realiza una función crítica para los requisitos de seguridad de la información, es necesario verificar tanto su código fuente como el código que se ejecutará.

Source: https://habr.com/ru/post/es415475/


All Articles