Obtención de información semántica de alto nivel a partir de código binario
- Escalada Gómez, Javier
- Francisco Ortín Soler Director
Universidade de defensa: Universidad de Oviedo
Fecha de defensa: 07 de xuño de 2021
- Daniel Fernández Lanvín Presidente/a
- María Puerto Paule Ruiz Secretario/a
- José Baltasar García Pérez-Schofield Vogal
- Ted Scully Vogal
- María Lourdes Borrajo Diz Vogal
Tipo: Tese
Resumo
La decompilación es el proceso dentro de la ingeniería inversa de software encargado de recuperar el código fuente de alto nivel a partir de un fichero binario. Aunque el código fuente obtenido tiene el mismo comportamiento que el programa original, su legibilidad suele ser inferior y casi siempre es distinto al código escrito por el programador. La obtención del programa original a partir de un binario es considerado como un problema indecidible puesto que, cuando se compila un programa, el compilador descarta mucha información de alto nivel no necesaria en el binario generado. Para tratar de reconstruir esta información, los decompiladores asocian patrones de instrucciones ensamblador a construcciones de alto nivel, permitiendo obtener construcciones de alto nivel a partir de patrones binarios. No obstante, la obtención de los patrones binarios es una tarea compleja y éstos dependen de variables tales como el compilador utilizado, opciones de compilación, el lenguaje de alto nivel de origen y el procesador para el que se genera el binario, entre otros. Si alguna de estas variables cambia, los patrones binarios a detectar también cambian. En los últimos años se están utilizando técnicas de aprendizaje automático y big data para construir herramientas orientadas a mejorar el desarrollo de software. La línea de investigación big code obtiene grandes volúmenes de código fuente a partir de repositorios de código fuente para entrenar modelos probabilísticos predictores. Con este tipo de técnicas se han construido herramientas de diversa índole, tales como desofuscadores de código, traductores automáticos o detectores de errores y vulnerabilidades en código fuente. Esta tesis plantea en qué medida este tipo de técnicas pueden ser utilizadas para resolver el problema indecidible de la decompilación. Esta tesis describe un método que, utilizando aprendizaje automático supervisado, mejora la extracción de información de alto nivel en comparación con los decompiladores existentes en el mercado. Inicialmente, el método plantea la creación de distintos modelos en función de las variables influyentes en la detección de los patrones binarios, eligiendo uno de los modelos creados una vez se hayan detectado los valores de las variables influyentes. Otro de los retos abordados por el método es la enorme variabilidad en la representación binaria de instrucciones ensamblador. Para ello, se define proceso de generalización de patrones en el que el aprendizaje automático es utilizado junto con un experto de dominio para reducir la variabilidad de dichos patrones. Finalmente, un proceso iterativo es el encargado de ayudar al experto en la creación de modelos que puedan inferir nuevas construcciones en lenguajes de alto nivel. El método propuesto no se limita a utilizar aprendizaje automático para entrenar los modelos, sino que también lo emplea como un mecanismo de descubrimiento de características (feature engineering). Para entrenar los modelos predictivos de manera automática, diseñamos e implementamos una plataforma altamente parametrizable encargada de generar los conjuntos de datos usados en el entrenamiento. Para ello, primero permite instrumentar el código fuente de alto nivel para relacionar las construcciones de alto nivel y su representación binaria. Posteriormente se extraen estos patrones binarios iniciales y se aplican las generalizaciones previamente identificadas por el método descrito en el párrafo anterior. Finalmente se compone el conjunto de datos, caracterizando cada instancia, fila o individuo mediante los patrones binarios obtenidos y etiquetándolo con la construcción de alto nivel oportuna. La implementación ha sido altamente paralelizada y por ello es capaz de obtener una mejora de 3,5 factores sobre un máximo teórico de 4, cuando se ejecuta en un procesador con 4 núcleos. Nuestros modelos se entrenan con código fuente C, pero es difícil obtener grandes volúmenes de código C estándar que pueda compilarse en diversos compiladores. Por ello implementamos un generador de código estocástico llamado Cnerator. Este generador, nos permite generar grandes volúmenes de código fuente con descripciones probabilísticas de las distintas construcciones del lenguaje, así como asegurar que se cumplen determinadas reglas especificadas por el usuario. El código sintético generado por Cnerator es enriquecido con código real obtenido de repositorios de código fuente. Así conseguimos cubrir un elevado espacio de búsqueda (código sintético) y representar las construcciones típicas mas utilizadas por los programadores (código real). Con todo ello, se construyó un modelo predictivo orientado a detectar el tipo de alto nivel retornado por todas las funciones existentes en código binario. Tras construir el conjunto de datos, se crearon 14 modelos clasificadores con distintos algoritmos de aprendizaje automáticos, evaluándolos con 3 métodos que consideran su capacidad predictiva ante código no utilizado en el entrenamiento. Todos los modelos obtuvieron mejores resultados que todos los decompiladores existentes, para los 3 métodos de evaluación. Comparando la capacidad predictiva del mejor modelo y el mejor decompilador, nuestro sistema obtuvo un F1-score del 79,1 % frente al 30 % F1-score del mejor decompilador del mercado. Tras la creación de los conjuntos de datos para entrenar los modelos, realizamos un análisis de los primeros para documentar los patrones utilizados en la clasificación del tipo de retorno de las funciones. Con este fin obtuvimos y analizamos reglas que asocian patrones binarios a los tipos de retorno de alto nivel. Dichas reglas de asociación combinan patrones binarios obtenidos dentro del cuerpo de una función con patrones de utilización del valor devuelto tras las invocaciones a la función. Las reglas combinan la información binaria de cómo una función retorna un valor con información de cómo se usa dicho valor, para detectar los tipos de retorno de alto nivel. Estos patrones pueden ser incluidos en los decompiladores actuales y así mejorar la inferencia que hacen de los tipos de retorno de las funciones decompiladas.