Full text
Escola Tècnica Superior d’Enginyeria Informàtica Universitat Politècnica de València Trabajo Fin de Grado Grado en Ingeniería Informática Autor: Fernando Vañó García Tutor: José Ismael Ripoll Ripoll Director Experimental: Hector Marco Gisbert 2014/2015
Buscador de ‘gadgets’ ROP para la construcci´on de ‘payloads’ para ARM 2
Resumen A partir de la aparici´on de la t´ecnica de protecci´on NX (Non eXecutable), la inyecci´on de c´odigo como m´etodo de ejecuci´on de payloads se vio seriamente limitada. Como consecuencia, los atacantes desarrollaron nuevas estrategias para explotar las vulnerabilidades de los procesos remotos. Una de las t´ecnicas m´as importantes es ROP (Return Oriented Programming), la cual permite “reorganizar” el c´odigo del propio proceso que est´a en ejecuci´on para ejecutar lo que el atacante desea. El presente trabajo aborda la implementaci´on de un programa en lenguaje C que, dado un fichero ejecutable ELF de la arquitectura ARM, localice, en el mismo, todos aquellos fragmentos de c´odigo (denominados Gadgets en ROP) que pueden ser utilizados para elaborar un payload. El objetivo, por tanto, es ofrecer una herramienta que muestre los gadgets disponibles para la elaboraci´on de payloads, as´ı como la automatizaci´on de un payload espec´ıfico que ejecute un shell de Linux. Se ponen en pr´actica conocimientos avanzados de los sistemas operativos (convenio de llamadas a funciones), el lenguaje ensamblador de la arquitectura ARM, la explotaci´on de fallos de programaci´on (buffer overflow) y la estructura de los ficheros ejecutables ELF. Palabras clave: ARM, ROP, Return, Oriented, Programming, ELF, Overflow, Exploit
Resum A partir de l’aparici´o de la t`ecnica de protecci´o NX (Non eXecutable), la injecci´o de codi com a m`etode d’execuci´o de payloads va resultar seriosament limitada. Com a conseq¨u`encia, els atacants van desenvolupar noves estrat`egies per a explotar les vulnerabilitats dels procesos remots. Una de les t`ecniques m´es importants es ROP (Return Oriented Programming), la qual permet “reorganitzar” el codi del propi proc´es que est`a en execuci´o per a executar all`o que l’atacant desitge. El present treball aborda la implementaci´o d’un programa en llenguaje C que, donat un fitxer executable ELF de l’arquitectura ARM, localitze, en el mateix, tots aquells fragments de codi (denominats Gadgets en ROP) que puguen ser utilitzats per a elaborar un payload. L’objectiu, per tant, ´es oferir una ferramenta que mostre els gadgets disponibles per a l’elaboraci´o de payloads, aix´ı com l’automatitzaci´o d’un payload espec´ıfic que execute un shell de Linux. Es posen en pr`actica coneixements avan¸cats dels sistemes operatius (conveni de crida de funcions), el llenguatge d’assemblador de l’arquitectura ARM, l’explotaci´o d’errors de programari (buffer overflow) i l’estructura dels fitxers executables ELF. Paraules clau: ARM, ROP, Return, Oriented, Programming, ELF, Overflow, Exploit
Abstract From the appearance of protection technique NX (Non eXecutable), code injection as a method of payloads execution was seriously limited. As a consequence, attackers developed new strategies to exploit the vulnerabilities of remote processes. One of the most important techniques is ROP (Return Oriented Programming), which allows attackers to reorganize the code of the process itself that is running in order to execute what the attacker wants. This paper deals with the implementation of a program written in C language that, given a ELF executable file of the ARM architecture, locates in it all those code snippets (called Gadgets in ROP) which can be used to prepare a payload. The goal, therefore, is to provide a tool which shows all available gadgets for the production of payloads, as well as the automation of an specific payload which executes a Linux shell. Advanced knowledge of operating systems (calling convention), the assembly language of the ARM architecture, exploitation of programming errors (buffer overflow), and the ELF executable files structure are put into practice. Key words: ARM, ROP, Return, Oriented, Programming, ELF, Overflow, Exploit
Agradecimientos Este trabajo no habr´ıa sido posible si no hubiera sido por el equipo de Cibersecurity UPV Research Group; tanto a Ismael Ripoll, que adem´as de ense˜narme cuantosos conocimientos acerca de la seguridad inform´atica, es el tutor que ha guiado este trabajo; como a Hector Marco, el cual me di´o la idea de realizar esta herramienta y me ha aconsejado a lo largo del desarrollo. Tambi´en urge la necesidad de nombrar a mi compa˜nero Vicente Ferrer Garc´ıa, ya que fu´e quien me di´o la brillante idea de c´omo implementar las listas gen´ericas en lenguaje C, las cuales han sido fundamentales en la implementaci´on del presente trabajo. Por ´ultimo y no por ello menos importante, debo agradecer a David Puente Castro, aunque no nos conozcamos personalmente, su excelente labor de compartir sus conocimientos a todo el mundo de manera desinteresada. Durante los ´ultimos a˜nos, en mi tiempo libre, he aprendido much´ısimo de los documentos y experimentos que ha publicado este se˜nor. Definitivamente, el mundo necesita personas como estas. A todos ellos, gracias. 1
Buscador de ‘gadgets’ ROP para la construcci´on de ‘payloads’ para ARM 2
´ Indice general 1. Introducci´on 7 1.1. Motivaci´on................................... 8 1.2. Objetivos ................................... 9 1.3. Organizaci´on de la tesis . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2. Base te´orica 11 2.1. Estadodelarte ................................ 11 2.2. FicherosELF ................................. 12 2.2.1. Formato................................ 12 2.2.2. ELFHeader.............................. 13 2.2.3. Program Header Table . . . . . . . . . . . . . . . . . . . . . . . . 13 2.3. ArquitecturaARM .............................. 14 2.3.1. Juego de instrucciones ARM . . . . . . . . . . . . . . . . . . . . . 14 2.3.2. Subconjunto de instrucciones para programaci´on ROP . . . . . . 15 2.3.3. Convenio de llamada a funci´on . . . . . . . . . . . . . . . . . . . 17 2.4. Evoluci´on de las t´ecnicas de ataque . . . . . . . . . . . . . . . . . . . . . 18 2.4.1. BufferOverflow............................ 18 2.4.2. Return-into-library . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.4.3. Return Oriented Programming . . . . . . . . . . . . . . . . . . . 20 2.4.4. Otras variaciones de ROP . . . . . . . . . . . . . . . . . . . . . . 23 3. Implementaci´on 24 3.1. Descodificaci´on de instrucciones . . . . . . . . . . . . . . . . . . . . . . . 24 3.2. Recolecci´on de Gadgets . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 3.3. Obtenci´on del juego de instrucciones ROP . . . . . . . . . . . . . . . . . 29 3.3.1. Operaciones y efectos . . . . . . . . . . . . . . . . . . . . . . . . . 29 3.3.2. Selecci´on de gadgets . . . . . . . . . . . . . . . . . . . . . . . . . 30 3.4. Generaci´on del payload . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 3.4.1. Proceso de construcci´on . . . . . . . . . . . . . . . . . . . . . . . 32 3.4.2. Resoluci´on de dependencias . . . . . . . . . . . . . . . . . . . . . 33 3.4.3. Resultadofinal ............................ 34 4. Evaluaci´on 35 4.1. Entornodepruebas.............................. 35 4.2. Utilizaci´on de la herramienta . . . . . . . . . . . . . . . . . . . . . . . . . 36 3
5. Conclusiones y trabajo futuro 42 A. Exploit utilizado en la evaluaci´on 44 B. Glosario de t´erminos 47 4
Cap´ıtulo 2 Base te´orica En este cap´ıtulo, tras contemplar algunas herramientas que existen actualmente en el ´ambito del Return Oriented Programming, explicaremos todos aquellos conceptos que son necesarios para el completo entendimiento del problema que se plantea, empezando por el formato que utilizan los archivos ejecutables ELF, pasando por la arquitectura ARM y acabando por la propia t´ecnica ROP. 2.1. Estado del arte En el momento en el que se escribe este documento, existen sendos programas que ofrecen este tipo de funcionalidad (y muchas m´as). No obstante, hay muy pocos que ofrecen la funcionalidad de generaci´on autom´atica de un payload para la arquitectura ARM; la gran mayor´ıa solamente lo ofrecen para la arquitectura x86. Estos son algunos ejemplos: ROPgadget: Herramienta la cual nos hemos inspirado en este trabajo. Desarrollada en Python y basada en la libreria Capstone para des-ensamblar instrucciones, busca gadgets en binarios de varias arquitecturas (x86, x64, ARM, ARM64, PowerPC, SPARC, MIPS) y genera payloads para x86. Tiene una interfaz f´acil de utilizar. URL: github.com/JonathanSalwan/ROPgadget (´ Ultimo acceso: 10/06/2015). ROPchain: Esta herramienta trabaja ´unicamente sobre la arquitectura x86, tambi´en basada en la librer´ıa Capstone, pero ofrece una caracter´ıstica importante: provee una API para programar payloads. URL: github.com/SQLab/ropchain (´ Ultimo acceso: 10/06/2015). Ropc: Otra herramienta que trabaja sobre arquitecturas de Intel. Tambi´en ofrece una API y asegura ofrecer un lenguaje turing completo de alto nivel para implementar programas ROP. Da soporte a saltos condicionales, funciones recursivas, variables locales, punteros, etc. 11
URL: github.com/pakt/ropc (´ Ultimo acceso: 10/06/2015). Ropper: Esta herramienta s´ı es capaz de producir un payload automatizado para la arquitectura ARM, adem´as de x86, x86 64, MIPS y PowerPC. Ofrece tambi´en payloads para ARM en modo thumb. De entre los payloads que ofrece est´a la llamada al sistema execve() ymprotect. Adicionalmente proporciona otras utilidades como leer archivos en hexadecimal, filtrar bytes, etc. URL: scoding.de/ropper (´ Ultimo acceso: 24/06/2015). 2.2. Ficheros ELF ELF es el acr´onimo de Executable and Linkable Format; es decir, es un formato que se utiliza tanto en archivos ejecutables como en archivos de c´odigo objeto, entre otros. Necesitamos conocer dicho formato puesto que de estos ficheros leeremos los opcodes para descodificar las instrucciones, adem´as de otros datos que veremos a continuaci´on. Para la extracci´on de informaci´on de los ficheros binarios ELF se ha utilizado la especificaci´on 1.2 [10]. A continuaci´on explicaremos las distintas partes de las que consta un archivo ELF. 2.2.1. Formato Estos archivos tienen una estructura tal que se puede ver desde dos puntos de vista diferentes: la vista de enlace (Linking View) y la vista de ejecuci´on (Execution View). Figura 2.1: Formato de un binario ELF Desde la perspectiva de enlace, el archivo est´a estructurado en secciones (Section Header), todas ellas indexadas desde una tabla llamada Section Header Table; contienen informaci´on relevante para el enlazado y relocalizaci´on. Algunos ejemplos de secciones son las siguientes: 12
.plt: Tabla de saltos que se utiliza al llamar a las funciones de la biblioteca compartida. .text: C´odigo (instrucciones) del programa. .dynamic: Informaci´on de enlace din´amica. .data: Contiene variables inicializadas que contribuyen a la imagen de la memoria del programa. .strtab: Cadenas de car´acteres utilizados por los s´ımbolos del programa. Si vemos el archivo desde la perspectiva de ejecuci´on, este se compone de segmentos (Program Header), indexados desde la tabla Program Header Table, que ser´an cargados en la memoria del proceso en tiempo de ejecuci´on. Un segmento puede contener varias secciones. Dependiendo del archivo, es posible que tenga Section Header Table sin Program Header Table, o viceversa; la ´unica parte que siempre debe estar presente es la cabecera ELF Header, la cual explicaremos en la siguiente secci´on. Para lo que a nosotros nos concierne, obviaremos la vista de enlace por dos razones: por una parte, queremos extraer instrucciones de ficheros binarios que contengan alg´un segmento ejecutable, por lo que si se proporciona como entrada alg´un fichero que no contenga segmentos cargables en memoria (es decir, que no tenga Program Header Table) no nos intersa. Por otra parte, recientemente se ha demostrado[19] que se puede manipular la Section Header Table de modo que herramientas como objdump ogdb muestren informaci´on falseada acerca de s´ımbolos tales como nombres de funciones. Por estas razones, solamente analizaremos la vista de ejecuci´on y sus segmentos. 2.2.2. ELF Header La cabecera ELF Header reside al principio del archivo y describe una especie de mapa del contenido. La estructura que representa los datos de esta cabecera es Elf32 Ehdr. En ella se almacenan tama˜nos y desplazamientos (offsets) relativos al inicio del archivo para acceder a las tablas. Adem´as, contiene informaci´on acerca de la arquitectura para la cual est´a compilado el binario (e machine), la direcci´on de entrada (e entry), y otros datos ´utiles. El vector e ident[EI NIDENT] contiene informaci´on independiente de la arquitectura, entre ellos el magic number, si es little-endian obig-endian, etc. Dado que queremos leer la Program Header Table, debemos desplazarnos por el archivo tantos bytes como nos indique el campo e phoff. El n´umero de entradas en esta y el tama˜no en bytes de cada entrada viene indicado en los campos e phnum ye phentsize, respect´ıvamente. 2.2.3. Program Header Table El Program Header Table de un ejecutable o de un objeto compartido es un vector de estructuras del tipo Elf32 Phdr, cada una de las cuales describe un segmento que el 13
cargador de programa necesitar´a para crear el programa y lanzarlo a ejecuci´on. Nos interesan especialmente los siguientes campos: p flags: Nos permite conocer atributos del segmento; de este modo podemos extraer segmentos ejecutables y segmentos de datos con permiso de escritura. p memsz: N´umero de bytes que ocupa el segmento en la imagen de memoria. Por una parte, nos guardaremos aquellos segmentos que contengan instrucciones y, por otra, de los segmentos que tengan permiso de lectura y escritura, nos quedaremos con el de mayor longitud; esto nos servir´a para escribir cadenas de car´acteres, tales como ‘/bin/sh’, teniendo referencia a ellas. 2.3. Arquitectura ARM ARM ha crecido vertiginosamente[4] en pocos a˜nos, hasta convertirse en la arquitectura de facto cuando se quiere potencia a un coste energ´etico bajo. Se trata de una arquitectura ‘RISC’ (Computador con Conjunto de Instrucciones Reducidas); de hecho, en sus inicios, ARM fueron las siglas de Advanced RISC Machines[3]. Antes de proseguir, debemos aclarar que ARM no es una ´unica arquitectura en s´ı; es m´as bien un conjunto de microarquitecturas que se agrupan por familias (e.g. ARM1, ARM11, Cortex-A), cada una de las cuales puede contener una o m´as microarquitecturas (e.g. ARMv1, ARMv6, ARMv7-A). Adem´as, cada una de estas ´ultimas, puede contener uno o m´as cores (e.g. ARM1, ARM1176JZF-S, Cortex-A7)[21]. La empresa ‘ARM Holdings’ es quien dise˜na estas arquitecturas; no obstante, no fabrica los chips, si no que proporciona el dise˜no de la arquitectura y el juego de instrucciones a empresas que fabrican y/o dise˜nan sus propios productos y que implementar´an la arquitectura comprada. Este tipo de procesadores tienen distintos ‘estados de operaci´on’[7]: ARM: Set tradicional con instrucciones de 32 bits. Thumb: Set m´as compacto, de 16 bits, orientado a dispositivos peque˜nos (permite mayor densidad de c´odigo). Jazelle: El procesador es capaz de ejecutar java bytecode. El estado est´a marcado con un flag (espec´ıficamente, el ‘T’) en el registro ‘CPSR’. Nosotros nos centraremos ´unicamente en el primer estado, con instrucciones de 32 bits, ya que los dem´as quedan fuera del alcance del trabajo. 2.3.1. Juego de instrucciones ARM Somos conscientes de que hay distintas familias, con distintas arquitecturas y distintos cores en cada una. Dos implementaciones diferentes de la misma arquitectura pueden variar en detalles, tales como las extensiones que ofrecen, distinto pipeline, etc; pero todas 14
ellas respetan el juego de instrucciones de la arquitectura, es m´as, todas las arquitecturas parten de un set b´asico (primera implementaci´on, ARMv1) de modo que cada arquitectura posterior soporta (o contiene) las instrucciones de la anterior, pudiendo a˜nadir mejoras o incluso instrucciones nuevas. En el cap´ıtulo 4 veremos que disponemos de un procesador ARM1176JZF-S (de la familia ARM11 y arquitectura ARMv7) para realizar pruebas; podr´ıamos utilizar el juego de instrucciones ARMv7, pero para aumentar la compatibilidad utilizaremos el juego de instrucciones ARMv6; el manual t´ecnico de referencia de esta arquitectura se puede consultar en la documentaci´on oficial[5]. Como inciso adicional, aunque no entremos en detalle, a partir de la arquitectura ARMv7 se definieron tres perfiles: ARMv7-A: Application ARMv7-R: Real-Time ARMv7-M: Microcontroller Esto es debido a que una ´unica arquitectura no es capaz de satisfacer las necesidades de la amplia gama de dispositivos, todos ellos diferentes, que implementan ARM[18]. 2.3.2. Subconjunto de instrucciones para programaci´on ROP Para alcanzar nuestro objetivo, tenemos distintas opciones a la hora de dise˜nar una soluci´on para construir el payload en base a las instrucciones que contenga el ejecutable recibido a trav´es de la entrada. Para una primera aproximaci´on, limitaremos el juego de instrucciones; de este modo, si no se presentan dichas instrucciones en la entrada, el programa no ser´a capaz de construir el payload. Para que esto no limite demasiado nuestras opciones, hemos analizado varios binarios del sistema ‘raspbian’ [15] (entre ellos: bash, dash, cat, netstat, etc), obteniendo as´ı una cuenta global de cada instrucci´on. Esto nos servir´a para decidir qu´e instrucciones ser´an necesarias y cuales no. Podemos observar en la Figura 2.2 que la instrucci´on que m´as se repite es ldr, instrucci´on de carga de memoria en registro, la cual, para nuestro prop´osito, no nos interesa porque, a priori, no tenemos control de toda la memoria mapeada por el proceso que ejecutar´a el binario que estamos analizando, solamente tenemos acceso a la pila, as´ı que si usamos esta instrucci´on corremos el grave riesgo de leer una zona de memoria que no pertenece al propio proceso, resultando el final de este con un precioso segfault. La instrucci´on pop no est´a entre las m´as comunes y esto podr´ıa parecernos un problema. No obstante, hay que tener en cuenta que estas instrucciones, a la par que las push, escriben (o leen) en una serie de registros, que son la lista finita de entre r0 yr15 (pc), por tanto, dada una lista de registros, solamente hay una combinaci´on posible. En otro caso, no es as´ı. Como instancia, con la instrucci´on sub y los tres registros r4, r5 yr6 tenemos seis posibles combinaciones; en el caso de la instrucci´on pop solamente tenemos una posible combinaci´on: pop {r4, r5, r6}. Por esta raz´on, si necesitamos una 15
Figura 2.2: Frecuencia de aparici´on por instrucci´on instrucci´on que cargue una palabra de la pila en el registro rX y tenemos una instrucci´on pop que escriba en este registro, no necesitamos nada m´as. Continuando con el an´alisis, la instrucci´on mov est´a en segunda posici´on y la frecuencia de aparici´on no es nada despreciable. Esto nos brindar´a un gran abanico de posibilidades y combinaciones entre registros y valores inmediatos, as´ı que la utilizaremos para copiar datos de un registro a otro. Esto podr´ıa parecer poco ´util, pero ser´a de gran ayuda dado que los registros de prop´osito general (r4 –r12) son muy comunes en instrucciones pop. Teniendo estas instrucciones en nuestro set particular, solamente nos quedar´ıa ser capaces de escribir en una zona de memoria controlada y para ello tenemos la instrucci´on str. El problema que se nos plantea en las instrucciones de almacenamiento es similar al que hemos comentado anteriormente en las instrucciones de carga: el acceso a zonas de memoria prohibidas. Para que esto no ocurra, antes de ejecutar una instrucci´on de almacenamiento deberemos asegurarnos de que el registro situado en el par´ametro de direcci´on (entre corchetes) est´e bajo nuestro control y este contenga una direcci´on de memoria v´alida. Para la elecci´on de estas instrucciones se ha seguido el mismo procedimiento: se ha analizado el flujo de instrucciones, filtrando aquellas que son de almacenamiento, observando cu´ales se repiten m´as veces. Si observamos la Figura 2.3, nos percatamos de que la instrucci´on ‘str r2, [r3]’ prepondera de entre las dem´as combinaciones. A pesar de ello, hemos notado que el registro r2 es muy poco com´un e incluso en muchos binarios no aparece instrucci´on que escriba en ´el, asi que nos quedaremos con la siguiente m´as com´un para conseguir nuestro fin. Con esto, tenemos todas las acciones necesarias para escribir en registros y en memoria. Cabe destacar que en esta arquitectura no existe como tal la instrucci´on ‘ret’ (x86); no obstante, disponemos de instrucciones equivalentes, como ‘pop {pc}’, que nos permiten escribir la direcci´on de la nueva instrucci´on directamente en el contador de programa. A lo largo del documento nos referiremos a este tipo de instrucciones como return . 16
Figura 2.3: Frecuencia de aparici´on de la instrucci´on de almacenamiento 2.3.3. Convenio de llamada a funci´on En los lenguajes de alto nivel es com´un utilizar funciones que realicen ciertas operaciones finitas. Es posible que nos devuelvan un resultado esperado o que, simplemente, realicen alg´un trabajo sin ofrecer feedback. Incluso si nos vamos hasta un nivel m´as bajo, el propio lenguaje ensamblador nos permite utilizar subrutinas. El mecanismo que subyace es el siguiente: cuando realizamos una llamada a una funci´on, el procesador guarda el valor del contador de programa correspondiente a la siguiente instrucci´on, de forma que, cuando la funci´on termine y ejecute la instrucci´on “return”, el programa pueda seguir por su flujo de ejecuci´on. En cuanto a los argumentos, pueden ser transferidos mediante los registros del procesador o mediante la memoria; qui´en hace qu´e depende del convenio de llamada a funci´on. En nuestro caso (ARM) sigue un convenio semejante al fastcall: el invocante es el encargado de transferir los argumentos al invocado mediante los registros r0 –r3; si la funci´on requiere m´as de cuatro argumentos, podemos transferir los adicionales a trav´es de la pila. El invocado es responsable de devolver un c´odigo de salida mediante los registros r0 –r3 (seg´un los bytes que requiera), as´ı como de devolver el flujo de ejecuci´on a la direcci´on guardada. Este ´ultimo paso, puede realizarse de distintas formas: Si la direcci´on de retorno se encuentra en el registro lr (link register): •Copiando el contenido directamente al contador de programa: mov pc, lr •Realizando un salto indirecto: bx lr Si la direcci´on de retorno se encuentra en la pila: •pop {pc} •ldr pc, [sp], #4 (Ambas son equivalentes) 17
Siguiendo los est´andares de esta arquitectura, el registro lr sirve para almacenar la direcci´on de retorno justo antes de invocar a una subrutina, pero... ¿Qu´e ocurre si dentro de la subrutina se llama a otra subrutina?; en este caso la primera deber´a guardar el valor de lr en la pila antes de invocar a la segunda. Figura 2.4: Estado de la pila tras invocar una subrutina En la figura 2.4 podemos ver un ejemplo de estos casos; representa el estado de la pila (stack) tras invocar una subrutina, quedando la direcci´on de retorno de lr almacenada en la primera posici´on del marco de pila (stack frame) de la subrutina. 2.4. Evoluci´on de las t´ecnicas de ataque A continuaci´on vamos a realizar un peque˜no recorrido hist´orico para contemplar la evoluci´on que han experimentado las t´ecnicas de ataque m´as importantes a ra´ız de las contramedidas dise˜nadas por los profesionales de la seguridad. 2.4.1. Buffer Overflow Tal y como hemos dicho en la introducci´on, desde los inicios de la propia inform´atica la acompa˜nan los errores de software. En la jerga, a un error se le suele llamar bug (bicho en castellano) por razones hist´oricas; existe la leyenda del primer error de programaci´on, el 9 de septiembre del a˜no 1947, en el ordenador ‘Mark II’ del Laboratorio de Computaci´on de la Universidad de Harvard, el cual tuvo un fallo en un rel´e electromagn´etico debido a que una polilla se hab´ıa enganchado a ´el[1]. 18
Hay distintos tipos (divisi´on por cero, deadlock, etc), pero nosotros nos vamos a centrar en un error espec´ıfico muy cometido por los programadores: el desbordamiento de buffer (buffer overflow). Dicho error se produce cuando en un programa se escribe en un buffer local sin comprobar previamente si la longitud del contenido a escribir es mayor que la capacidad del buffer. Si esto ocurre y la entrada es proporcionada por el usuario, puede desencadenar en fatales consecuencias, como veremos en la siguiente secci´on. Para entender las consecuencias de un desbordamiento es necesario entender el funcionamiento de la ABI de llamada a funci´on, explicado en la secci´on 2.3.3. Haciendo referencia a lo comentado en los dos ´ultimos p´arrafos de dicha secci´on, vamos a asumir que la direcci´on de retorno est´a guardada en la pila (en la pr´actica, es muy probable que esto ocurra). Lo curioso de este asunto se debe a la naturaleza de dos elementos: el buffer y la propia pila. La pila se sit´ua en direcciones altas del proceso y crece hacia direcciones m´as bajas; esto es, cada vez que insertamos un elemento en la pila (push), la direcci´on apuntada por el registro sp se decrementa en cuatro unidades (la pila aumenta), mientras que si extraemos un elemento, a sp se le suma cuatro. Por el contrario, el primer elemento de un buffer que se encuentra en la pila est´a situado en la direcci´on m´as baja de este, y crece hacia direcciones m´as altas. Figura 2.5: Ejemplo de desbordamiento de buffer Como podemos apreciar en la figura 2.5, si sobrepasamos los l´ımites del buffer local, se sobreescribe todo aquello que est´e en direcciones m´as altas de la memoria del proceso. Ya sabemos que la funci´on carga el valor almacenado correspondiente en el contador de programa cuando realiza el ep´ılogo; ahora bien, si este valor corresponde con una direcci´on v´alida del proceso, el procesador tratar´a, como instrucci´on, el opcode almacenado en esa direcci´on y continuar´a su ejecuci´on; si, por el contrario, no es v´alida, el programa terminar´a por ‘violaci´on del segmento’. Desde el punto de vista de la seguridad, si ya se ha dado el caso en el que la direcci´on de retorno se ha sobreescrito, obviamente 19
es preferible que este valor no sea una direcci´on v´alida ya que, en ese caso, el error se transforma en un una simple denegaci´on de servicio (DOS). El peligro real est´a en la posibilidad de escribir una direcci´on v´alida que, adem´as, corresponda con una instrucci´on real, controlada por el atacante. En un desbordamiento de buffer ‘clasico’, adem´as de sobreescribir concienzudamente la direcci´on de retorno, se dise˜na un shellcode que se inserta en el buffer local, de modo que la direcci´on de retorno apunta a una zona del buffer, ejecut´andose as´ı la secuencia de instrucciones insertada por el atacante. Para que esto sea posible, la zona de memoria correspondiente a la pila debe tener permiso de ejecuci´on. Afortunadamente, los procesadores actuales gozan de una extensi´on llamada ‘NX’ (Non eXecutable) la cual impide que se ejecuten instrucciones en zonas de memoria determinadas (a.k.a. el stack). Cabe destacar que incluso si el procesador no provee esta caracter´ıstica, el parche de Linux ‘PaX’ implementa mecanismos que emulan muy bien esta protecci´on. 2.4.2. Return-into-library En aquellos casos en los que la protecci´on NX est´a presente, aunque sea posible insertar un shellcode y redireccionar el flujo de ejecuci´on hacia ´el, el programa finalizar´a, frustando los intentos del atacante. La t´ecnica return-into-library consiste en redireccionar el flujo de ejecuci´on a funciones presentes en el propio ejecutable (obviamente en zonas de memoria con permisos de ejecuci´on). Estas funciones suelen pertenecer a la libreria del sistema, como system() o execve(), situando los argumentos convenientemente (dependiendo del convenio de llamada), de forma que el atacante puede ejecutar comandos arbitrarios. En este caso, la primera limitaci´on que se puede encontrar una persona que realice este ataque es la protecci´on ASLR (Address space layout randomization). ASLR se encarga de que las librerias din´amicas (e.g. libc) se carguen en una direcci´on aleatoria en cada ejecuci´on; de este modo, por cada vez distinta que se lance el programa, una funci´on determinada se situa en distintas posiciones de la memoria, dificultando la t´ecnica returninto-library ya que, a priori, no se conoce d´onde estar´a la funci´on que se desee ejecutar. 2.4.3. Return Oriented Programming Llegamos a la t´ecnica ROP (tambi´en conocida como borrowed code chunks), la cual es la clave de este trabajo. Esta t´ecnica es ´util si tanto NX como ASLR est´an habilitados ya que, si un binario ha sido compilado sin opciones especiales, el c´odigo de este se sit´ua en posiciones est´aticas. Esta t´ecnica es una generalizaci´on de return-into-library; en lugar de ejecutar funciones enteras, se ejecutan trozos (chunk) de estas (generalmente correspondientes a los ep´ılogos de las funciones) los cuales est´an situados en el propio c´odigo del proceso. En la literatura, la arquitectura m´as com´unmente utilizada para estudiar esta t´ecnica es x86. De hecho, tal y como hemos visto en la secci´on 2.1, existen pocas herramientas p´ublicas que trabajen sobre la arquitectura ARM y construyan un payload; la mayor´ıa trabajan sobre x86. Esta es una arquitectura CISC con una gran densidad de c´odigo, adem´as de tener una 20
building <- 0 ## Indica si estamos construyendo un gadget for ´ultima_instrucci´on -> primera do if !building then ## Buscando instrucciones return if instrucci´on_es_return then Crear nuevo gadget a~nadir instrucci´on a gadget if MAX_LENGTH == 1 then A~nadir gadget a la lista else building <- 1 end if; end if else ## Estamos construyendo un gadget if instrucci´on_es_return || instrucci´on_no_valida then building <- 0 ## Fin del gadget A~nadir gadget a la lista if instrucci´on_es_return then retroceder una instrucci´on end if else if instrucci´on_valida then a~nadir instrucci´on a gadget if longitud(gadget) == MAX_LENGTH then building <- 0 ## Fin del gadget A~nadir gadget a la lista end if end if end for Listing 3.5: Pseudo-c´odigo del algoritmo de recolecci´on de gadgets La lista de gadgets es en realidad una lista de listas, cada sublista de la cual corresponde con un gadget del algoritmo (cuando creamos un nuevo gadget, en realidad, estamos creando una sublista). Al salir del bucle tendremos la lista de gadgets construida. La figura 3.2 muestra un peque˜no ejemplo del resultado del algoritmo. Las flechas grises sombreadas representan las cargas ´utiles de cada nodo de una lista; aunque, en realidad, cada nodo de cada sublista de ‘Gadgets’ tiene una carga ´util del tipo ‘Gadget t’, no obstante, hemos preferido representarlo as´ı en la figura 3.2 puesto que el atributo importante es el puntero a ‘instr obj 32’ de la lista de instrucciones, representado por las flechas negras. En cuanto al c´odigo de colores de las instrucciones, aquellas coloreadas de verde ser´an las incluidas dentro de los gadgets. 27
typedef struct { instr_obj_32 *instruction; union { // ‘return’ node (tail) of each sublist in ‘gadgets’ // struct list *effects_list; // other nodes // struct Lnode *effects_node; } pointer; int Inputs[15]; int Outputs[15]; } Gadget_t; Listing 3.6: Estructura ‘Gadget t’ Gadget t: Representa un gadget. El puntero ‘instruction’ hace referencia a la instrucci´on de la cabeza del gadget; la uni´on ‘pointer’ hace referencia a una lista, si ‘instruction’ apunta a una instrucci´on return, o a un nodo de la lista ‘effects list’, en caso contrario. Figura 3.2: Recolecci´on de gadgets a partir de la lista de instrucciones Cuando tenemos la lista de gadgets completa tenemos tantos gadgets como nodos totales dentro de todas las sublistas de la lista ‘Gadgets’. Poniendo como ejemplo el mostrado en la figura 3.2, la secuencia mov r0, r4; pop {r5, pc}son dos gadgets funcionales: por un lado, la propia secuencia mov r0, r4; pop {r5, pc}; por otro, pop {r5, pc}. As´ı que, si las instrucciones mostradas en la figura fueran las ´unicas disponibles, tendr´ıamos tres gadgets funcionales. 28
Llegados a este punto, tenemos todo lo necesario para mostrar por pantalla todos los gadgets disponibles; lo ´unico que tenemos que hacer es recorrer la lista de gadgets imprimiendo, por cada elemento, toda la lista de instrucciones que lo conforman. En el cap´ıtulo 4 podemos ver un ejemplo de la salida del programa. 3.3. Obtenci´on del juego de instrucciones ROP Uno de nuestros objetivos principales es generar un juego de instrucciones en base a las operaciones disponibles para poder recompilar el c´odigo del programa en tiempo de ejecuci´on. Adem´as de que nuestro juego de instrucciones es desconocido, no siempre es el mismo ya que, evidentemente, var´ıa para dos archivos binarios distintos. A lo largo de esta secci´on vamos a ver c´omo conocer las operaciones que tenemos disponibles tras obtener la lista de gadgets. 3.3.1. Operaciones y efectos Ya sabemos que cuando implementamos un programa ROP (payload) nuestras ‘instrucciones’ son aquellos gadgets que est´en disponibles en el binario. Es evidente que se puede presentar el caso en el cual un gadget realice acciones que ‘contaminen’ el estado; en otras palabras, puede que un determinado gadget deshaga acciones realizadas previamente porque entre la instrucci´on de nuestro inter´es y el return hay otra instrucci´on que escribe en un registro que ten´ıamos escrito previamente. Es interesante abstraer la unidad de gadget y determinar los efectos sobre los registros y la memoria que producen el conjunto de instrucciones que lo conforman. Tim Kornau realiz´o un estudio[11] muy te´orico acerca de la abstracci´on de operaciones de los gadgets, usando REIL (The Reverse Engineering Intermediate Language)[8], un lenguaje intermedio independiente de la plataforma que tiene como objetivo simplificar los algoritmos de an´alisis de c´odigo est´atico, como el de la b´usqueda de gadgets para ROP[9]. Nosotros hemos determinado tres campos: Entradas: Registros (normalmente de prop´osito general) que el gadget copiar´a a otros, por tanto, se debe escribir el contenido previamente. Salidas: Registros sobrescritos por el gadget. Efectos: Relaciones de escrituras. Pueden ser de varios tipos: •Registro a Registro. •Valor Inmediato a Registro. •Registro a Memoria. •Valor Inmediato a Memoria. Con estas tres caracter´ısticas tenemos toda la informaci´on necesaria qued´andonos con un juego de instrucciones representado, por una parte, por efectos en registros y memoria; 29
y por otra, por entradas que un gadget necesita que se cumplan a priori para que pueda ejercer su funci´on. En la figura 4.1 podemos ver dos ejemplos. Figura 3.3: Entradas, Salidas y Efectos Si, como instancia, queremos escribir en el registro rX, tan solo necesitamos buscar entre aquellos gadgets que lo tengan como salida. Si rX se encuentra en la lista de la instrucci´on pop del return, no necesitaremos m´as gadgets; si no, deberemos buscar en los efectos para conocer qu´e se escribe en ´el: si se escribe un valor inmediato, podemos aceptar el gadget (si el valor es el deseado) o descartarlo; si, por el contrario, se transfiere el valor desde otro registro, sabremos qu´e entrada le transfrir´a el valor a rX. Como hemos visto en la secci´on 2.3.2 usualmente tenemos suficientes instrucciones return que contienen registros de prop´osito general, as´ı que no tendremos problemas en encontrar este tipo de operaciones, las cuales solamente tienen salidas, no requieren entradas y carecen de efectos colaterales. Estas ser´an las primeras en aparecer en nuestro programa ROP, como veremos en la secci´on 3.4.2. 3.3.2. Selecci´on de gadgets Una vez identificadas las operaciones que realiza cada gadget, deberemos evaluarlos y catalogarlos en funci´on de las salidas. Para ello hemos dise˜nado dos elementos, donde almacenaremos gadgets en bruto y gadgets usables, respectivamente. El primero es fabricado mediante el siguiente procedimiento: recorremos la lista de gadgets cuyas operaciones han sido previamente procesadas y los filtramos a trav´es de una funci´on de evaluaci´on que permite ordenar los distintos gadgets en base a los registros que tiene como salidas y el tama˜no de frame de pila que genera, obteniendo un valor por cada gadget. Si el gadget evaluado tiene efectos colaterales prohibitivos (escribe en sp, etc) obtendr´a un valor negativo, por lo que se prescindir´a de ´el. En caso de obtener un valor positivo, cuanto mayor sea, mejor ser´a el gadget. A medida que vamos evaluando los gadgets, los iremos posicionando en los ‘gadgets en bruto’, indexando cada uno en funci´on de los registros que escribe. Cuando acabamos de recorrer todos los gadgets, en esta estructura tenemos, por cada registro, una lista de gadgets la cual primer elemento es aquel con mejor puntuaci´on. El n´umero de elementos 30
por registro es controlado por una directiva #define del preprocesador; as´ı podemos ajustar el tama˜no de las listas. Con este almacenamiento de gadgets en bruto accesible, ya podemos fabricar nuestro juego de instrucciones (gadgets usables). Centr´andonos en nuestro payload (llamada al sistema execve()), necesitamos escribir en los registros r0,r1,r2 as´ı que tendremos esas tres entradas junto con el gadget de almacenamiento (estos cuatro gadgets ser´an los principales), la instrucci´on svc (para realizar la interrupci´on) y un vector de gadgets auxiliares. Los gadgets auxiliares son aquellos que escriben en los registros de prop´osito general (pueden ser entradas requeridas por los gadgets principales o registros que necesitamos escribir por otra raz´on, como r7 para introducir el n´umero de llamada). Figura 3.4: Obtenci´on de nuestro set de instrucciones ROP Puesto que nuestra instrucci´on de almacenamiento es fija, sabemos que requerimos, al menos, de gadgets auxiliares para r3,r4 (por la str r3, [r4]) y r7. As´ı que si no disponemos de estos, no tenemos suficientes instrucciones en nuestro set y nos ser´a imposible escribir un programa ROP. Lo mismo ocurre con los gadgets principales y con la instrucci´on svc que nos permite realizar la interrupci´on; si esta no est´a presente, no podemos continuar, si est´a, la a˜nadimos al set que estamos elaborando. Para finalizar esta etapa, debemos comprobar que tenemos disponibles los gadgets auxiliares para toda entrada requerida; para ello hemos dise˜nado un algoritmo recusivo que, para cada registro principal junto con la instrucci´on de almacenamiento, comprueba si se pueden abastecer las entradas. La funci´on que se encarga de ello recibe como par´ametro una lista, correspondiente con las listas almacenadas en los gadgets en bruto; de esta extraemos el primer elemento y verificamos si todas las entradas tienen su respectivo gadget auxiliar de modo que, si no se cumple, se elimina el elemento de la lista y se llama recursivamente a la funci´on (la lista tendr´a un elemento menos). Si, por el contrario, 31
tenemos disponibles los gagdets necesarios, la funci´on devuelve el puntero al elemento actual (gadget principal), el cual ser´a almacenado en la estructura de gadgets usables. Si llegamos al final, devuelve NULL. Si durante el procedimiento alguno de estos elementos es NULL, significa que las dependencias de este gadget principal no se pueden cumplir. En caso contrario, cuando finalicemos todas las comprobaciones, tendremos todos los gadgets usables, tanto principales como auxiliares, que nos permitir´an escribir el programa ROP, conectando las salidas con las entradas sin que nos falte ninguna instrucci´on. este ser´a nuestro propio set de instrucciones ROP para el programa que vamos a automatizar. 3.4. Generaci´on del payload Una vez tenemos disponible nuestro set de instrucciones (gadgets usables) vamos a ver c´omo elaborar el programa ROP (o payload) para realizar la llamada al sistema execve(‘/bin/sh’). 3.4.1. Proceso de construcci´on Para la elaboraci´on del payload hemos implementado otra lista, cada nodo de la cual representa una palabra de 32 bits que se situar´a en la pila del proceso. Esto nos permitir´a insertar palabras donde queramos y guardar informaci´on adicional como las cadenas de car´acteres correspondientes con las instrucciones de un gadget. Iremos construyendo la lista en orden inverso, desde el final hacia el inicio, as´ı conoceremos las entradas que se requieren por aquellos gadgets que vayamos insertando. Hemos diferenciado los nodos que contienen gadgets (ya sean principales o auxiliares) de los nodos que contienen valores (aquellos que se transferir´an a los registros) aunque, evidentemente, el valor de los gadgets se tranferir´a al registro pc. La informaci´on que contienen estos nodos son del tipo payload gadget t los cuales, si contienen un gadget, utilizan todos los valores almacenando la direcci´on de la primera instrucci´on, un puntero al gadget y un puntero a las cadenas de car´acteres de estas (de car´acter informativo); si son datos, ´unicamente utilizan el primer campo. typedef struct { uint32_t value; // Address or Value to the stack // struct Lnode *gadget; // type Gadget_t // char *strings[MAX_GADGET_LENGTH]; } payload_gadget_t; Listing 3.7: Estructura ‘payload gadget t’ El procedimiento que seguimos es el siguiente: se realiza una primera etapa escribiendo la instrucci´on svc (que ser´a la ´ultima en ejecutarse) y los gadgets principales. Hay que tener en consideraci´on dos asuntos importantes: En primer lugar, tras estudiar las apariciones de las instrucciones que modifican los registros r0,r1 yr2, hemos decidido establecer un orden, de modo que en primer lugar 32
siempre se ejecutar´a el gadget que escribe en el registro r2. Tras ´el, se ejecutar´an los gadgets que escriben en r0 yr1, en este orden. Por esta raz´on, tenemos en cuenta las salidas de los dos ´ultimos ya que si, por ejemplo, el gadget de r1 escribe en los tres registros, no necesitamos usar los otros dos. En segundo lugar, tal y como hemos visto en la secci´on 2.3.2, hay muchos programas que no contienen ningun gadget ´util que modifique el registro r2. Dependiendo del programa ROP que queramos implementar, esto ser´a un problema o no. En nuestro caso, este registro debe contener el valor nulo o bien una direcci´on de memoria que contenga este valor. Por tanto, si no se dispone de gadget que escriba en ´el, la ejecuci´on exitosa del payload depende del estado en el que se encuentre el procesador en el momento en el que se ejecuta. Si se da este caso, aunque las estad´ısticas no est´en de la parte del usuario, hemos decidido que se contin´ue con la automatizaci´on del payload avisando al usuario de la adversidad. A medida que vamos a˜nadiendo gadgets, mantenemos el estado global anotando las entradas requeridas pendientes, elimin´andolas si alg´un gadget insertado posteriormente (debemos tener claro que el orden de ejecuci´on es el inverso) las proporciona en sus salidas. Tras esta primera etapa, en el estado global tendremos aquellas dependencias pendientes que no se han podido aprovisionar; efectuaremos tres etapas m´as con las que resolveremos las dependencias pendientes, a˜nadiremos las instrucciones de almacenamiento para tener la cadena ‘/bin/sh’ en la memoria del proceso y a˜nadiremos los datos que acabar´an escribi´endose en los registros. 3.4.2. Resoluci´on de dependencias Aprovechando que tenemos aquellas dependencias que no han sido suministradas en el estado global, nos guardaremos dichas dependencias y realizaremos otra pasada, desde el final hasta el principio del payoad, anot´andolas a medida que van apareciendo. Si aparece una dependencia que est´a en el que nos hemos guardado previamente, sabemos con seguridad que no ser´a proporcionada por ning´un gadget. Imaginemos que tenemos guardado como pendiente el registro r4; si estamos procesando un gadget que utiliza este registro y anteriormente hemos procesado un gadget que tambi´en lo utiliza (por tanto lo tenemos marcado tanto en el estado anterior como en el nuevo) necesitamos insertar un gadget auxiliar que escriba en r4 que se ejecute entre el gadget que estamos procesando y el procesado previamente (por tanto, insertar´ıamos el nodo antes del nodo actual). El prop´osito de esto es conseguir elegancia en el programa ROP ya que nos permite posponer entradas y a˜nadirlas cuando sea estr´ıctamente necesario (o bien hemos llegado al principio del payload). Si durante la resoluci´on de otras dependencias se dota una que hab´ıamos aplazado, nos ahorraremos insertar un nuevo gadget ganando tiempo de c´omputo y espacio en el payload (muy importante ya que ser´a inyectado en la pila del proceso en ejecuci´on). En cuanto a las instrucciones de almacenamiento, hemos decidido separarlas de las dem´as; son las primeras instrucciones que se ejecutar´an en el payload. Cuando hemos resuelto todas las dependencias, procedemos a a˜nadir las instrucciones de almacenamiento. Los 33
primeros gadgets ser´an instrucciones return (gadgets auxiliares) que escriban en los registros r3 yr4. Por ´ultimo, cuando ya tenemos todos los gadgets de instrucciones, insertamos los datos acorde a nuestro objetivo. 3.4.3. Resultado final Adem´as de las opciones (comentadas en el apartado anterior) que nos brinda una lista para el almacenamiento del payload, nos permite flexibilidad para el lenguaje en el que vamos a implementar el programa ROP. Nosotros hemos elegido el lenguaje Python; no obstante, se puede ampliar f´acilmente la herramienta para mostrar el payload con otros lenguajes. La tarea final, por tanto, es recorrer esta lista que contiene el programa ROP e imprimirla con la sintaxis pertinente. Para Python, cada palabra va encapsulada mediante la funci´on struct.pack() con el formato ‘L’ (unsigned long), que nos permite almacenar el valor en 32 bits. Figura 3.5: Visi´on global La se˜nal con el s´ımbolo de exclamaci´on significa que en este punto, dependiendo de la entrada, el flujo del programa puede terminar si alguna instrucci´on necesaria no est´a disponible. En la figura 3.5 podemos ver como la lista que contiene el payload hace referencia a los gadgets; los cuales, a su vez, hacen referencia a la lista de instrucciones, de modo que en ning´un momento se replican datos. 34
Cap´ıtulo 4 Evaluaci´on Para finalizar el documento es necesario poner a prueba nuestra herramienta para comprobar que funciona correctamente. A continuaci´on se expone, por una parte, una serie de pruebas realizadas tanto durante el desarrollo como sobre la herramienta final; y por otra, la utilizaci´on de la herramienta mediante capturas de pantalla. 4.1. Entorno de pruebas Durante el desarrollo de la herramienta, se han realizado muchos tests desde la funci´on de desensamblar c´odigo m´aquina hasta la correcta ejecuci´on de la llamada al sistema. Las pruebas de desarrollo con programas de lenguaje ensamblador no se van a mostrar puesto que, adem´as del hecho de que se han realizado incontables pruebas, durante todo el proceso se trabaja con el resultado de dicha funci´on (esto es, las instrucciones descodificadas) as´ı que lo podemos ver impl´ıcitamente. Figura 4.1: Ejemplo de Entradas, Salidas y Efectos en el desarrollo 35
Para ilustrar las tres caracter´ısticas de las que hablamos en la secci´on 3.3.1, podemos observar la figura 4.1 donde se muestran dos gadgets de ejemplo. El operador ‘op: 4’ hace referencia al atributo OP ADD. En resumen, la sem´antica de los efectos del primer gadget es que tanto al registro r4 como al r0 se va a escribir la suma de r5 yr6. En el segundo gadget, en el registro r3 se escribe un inmediato de valor nulo, al igual que en memoria, en la posici´on indexada por el registro r4. Figura 4.2: Salida del servidor de ‘echo’ Para las pruebas de los programas ROP se ha dise˜nado un programa simple en lenguaje C que abre un socket y espera conexiones. Cuando recibe una conexi´on, simplemente retorna al cliente la misma petici´on recibida (servidor de echo). La parte importante es la funci´on echo(), que contiene una vulnerabilidad de buffer overflow. conn_fd = accept(server_fd, (struct sockaddr*) &serv_addr, &addrlen); bytes_read = read(conn_fd, recv_data, 1024); echo(conn_fd, recv_data, bytes_read); Listing 4.1: Funcionamiento b´asico del servidor de prueba void echo(int fd, char *str, int len){ char buff[16]; memcpy(buff, str, len); write(fd, buff, len); return; } Listing 4.2: Funci´on vulnerable a buffer overflow La funci´on echo() es francamente innecesaria, al igual que la copia de los datos al buffer local, pero nos sirve como analog´ıa de una funci´on vulnerable real. Al fin y al cabo, lo que verdaderamente nos interesa es lo que ocurre por debajo. En la figura 4.2 podemos ver un ejemplo del funcionamiento, por si quedaba alguna duda. En cuanto a los tests de nuestro programa, frop, tanto para el desarrollo como para las pruebas finales hemos usado una Raspberry Pi, con un procesador ARM1176JZF-S a 700MHz. En la figura 4.3 podemos ver informaci´on acerca del procesador y las extensiones que implementa. 4.2. Utilizaci´on de la herramienta A continuaci´on se exponen las distintas opciones que proporciona la herramienta. En primer lugar, en la figura 4.5 se muestra la salida de la funcionalidad de mostrar toda la lista de gadgets disponibles. Recordamos que esta funcionalidad es importante ya que, para implementar cualquier programa ROP, necesitamos conocer cuales son los gadgets que est´an disponibles en el c´odigo de un binario. 36
sesgada sobre la que trabajamos. Esto es debido a la enorme complejidad que supone dise˜nar un programa ROP gen´erico a partir de los gadgets disponibles, ya que estos no son conocidos hasta que se analiza el binario. An´alogamente, ser´ıa como generar un compilador con un juego de instrucciones que no es conocido a priori. Podr´ıamos a˜nadir mecanismos de inteligencia artificial que cubra toda la sem´antica del juego de instrucciones. De este modo podr´ıamos conseguir una generaci´on (completamente autom´atica) de programas ROP gen´ericos; esto nos permitir´ıa, adem´as de ofrecer m´as posibilidades en el lenguaje (bucles, subrutinas, etc), producir payloads m´as cortos y eficientes. Por ´ultimo, animamos a cualquier persona que lea este documento a adentrarse (o continuar) en el mundo de la seguridad, sea cual sea el area, del lado de los buenos con el fin de conseguir un mundo mejor y poder disfrutar de las ventajas que nos ofrece la tecnolog´ıa de la que tan dependientes somos. “We’ve arranged a global civilization in which most crucial elements profoundly depend on science and technology. We have also arranged things so that almost no one understands science and technology. This is a prescription for disaster.” (Carl Sagan) 43
Ap´endice A Exploit utilizado en la evaluaci´on #!/usr/bin/python ## Fernando Vanyo Garcia ## import argparse; import sys; import socket; import select; from struct import pack; target = ’’; port = ’’; ######################## offset = # to complete # ######################## def interact(s): input_list = [sys.stdin, s]; while True: try: select_res = select.select(input_list, [], []); except: s.close(); exit(0); for i in select_res[0]: if i is s: # Server -> Client readed = s.recv(4096); if readed == "": print "[+] Connection closed by remote host"; exit(0); else: sys.stdout.write(readed); 44
sys.stdout.flush(); elif i is sys.stdin: # Server <- Client command = sys.stdin.readline(); s.send(command); def getConnection(): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.connect((target, port)); except: sys.stderr.write("[-] Sorry... I can’t connect to "\ + target + ":" + str(port) + "\n"); exit(-3); print "[+] Connection established"; return s; def send2server(payload): print "[+] Connecting to " + target + ":" + str(port) + "..."; s = getConnection(); print "[+] Sending exploit..."; s.send(payload); return s; def usage(): sys.stderr.write("Usage: " + str(sys.argv[0]) + " target port payload"); exit(-1); def main(): global target; global port; parser = argparse.ArgumentParser(epilog = \ ’Fernando Vanyo Garcia ([email protected])’, \ usage=’ %(prog)s -t Target -p Port’, \ conflict_handler=’resolve’); parser.add_argument(’-t’, nargs = 1, type = str, required = True, \ metavar = ’Target’, help = \ ’target of the %(prog)s program’); parser.add_argument(’-p’, nargs = 1, type = int, required = True, \ metavar = ’Port’, help=’port listening’); args = vars(parser.parse_args()); target = args[’t’][0]; port = args[’p’][0]; if __name__ == ’__main__’: 45
main(); try: target = socket.gethostbyname(target); except: sys.stderr.write("[-] Sorry... I can’t connect to " + target + "\n"); exit(-1); if (port < 1) or (port > 65535): sys.stderr.write("[-] " + str(port) + " is not a valid port\n"); exit(-2); payload = "A" * offset; ## insert payload ## s = send2server(payload); interact(s); Listing A.1: Exploit utilizado en la prueba de concepto 46
Ap´endice B Glosario de t´erminos ARM: Famlia de microarquitecturas de computadores de bajo coste. Arquitectura: Estructura l´ogica y f´ısica de un sistema de computadoras. ELF: Acr´onimo en ingl´es de “Formato Ejecutable y Vinculable”. Exploit: Dispositivo l´ogico (software) o f´ısico (hardware) cuyo objetivo es atacar una vulnerabilidad dada en cierto sistema. Gadget: Secuencia de instrucciones que act´uan como unidad operacional inherente. Payload: Componente que se encarga de ejecutar una acci´on determinada. En t´erminos de Return Oriented Programming, dada una vulnerabilidad, el exploit la aprovecha, ejecutando el payload (siendo este independiente del exploit). Root: Usuario con permisos totales sobre un sistema basado en Unix. Shell: Int´erprete de comandos que le permite a un usuario ejecutar ´ordenes sobre el sistema operativo. Shellcode: Programa software (usualmente escrito en lenguaje ensamblador) representado mediante los c´odigos binarios de las instrucciones, inyectado por un atacante en el espacio de direcciones de un proceso. Vulnerabilidad: Caso particular de error de software en el cual cabe la posibilidad de aprovechar la debilidad para comprometer la seguridad del sistema (confidencialidad, integridad, disponibilidad y autenticidad). 47
Bibliograf´ıa [1] Abad´ıa Digital. Este fue el primer bug inform´atico. http://www.abadiadigital.com/este-fue-el-primer-bug-informatico (Consulta: 12 de junio de 2015) [2] Adobe Security Bulletin. Ficha para la vulnerabilidad CVE-2015-3113. https://helpx.adobe.com/security/products/flash-player/apsb15-14.html (Consulta: 4 de julio de 2015) [3] ARM. Hitos de la compa˜nia ARM. http://www.arm.com/about/company-profile/milestones.php (Consulta: 11 de junio de 2015) [4] ARM Connected Community. ARM from zero to billions in 25 short years. https://community.arm.com/groups/internet-of-things/blog/2010/05/11/ arm-from-zero-to-billions-in-25-short-years (Consulta: 11 de junio de 2015) [5] ARM Inforcenter. P´agina web oficial. http://infocenter.arm.com/help/index.jsp (Consulta: 11 de junio de 2015) [6] CNET. ARMed for the living room. http://news.cnet.com/ARMed-for-the-living-room/2100-1006_3-6056729.html (Consulta: 11 de junio de 2015) [7] Caprile, S. (2013) Desarrollo con microcontroladores ARM Cortex-M3. Buenos Aires. Puntolibro. P´agina 73. [8] Dullien, T y Porst, S. REIL: A platform-independent intermediate representation of disassembled code for static code analysis. http://www.zynamics.com/downloads/csw09.pdf (Consulta: 20 de junio de 2015) [9] Dullien, T; Kornau, T. y Weinmann, R. A framework for automated architectureindependent gadget search https://www.usenix.org/legacy/event/woot10/tech/full_papers/Dullien.pdf (Consulta: 21 de junio de 2015) [10] Especificaci´on ELF Versi´on 1.2 http://pdos.csail.mit.edu/6.828/2005/readings/elf.pdf (Consulta: 15 de junio de 2015) 48
[11] Kornau, T. (2009). Return Oriented Programming for the ARM Architecture Bochum: Ruhr-Universit¨at Bochum, www.zynamics.com/downloads/kornau-tim--diplomarbeit--rop.pdf (Consulta: 19 de junio de 2015) [12] Kovacs, E. (2015). “Flash Player Flaw Used by APT3 Group Added to Magnitude Exploit Kit”. Disponible en http://www.securityweek.com/flash-player-flaw-used-apt3-group-added-magnitude-exploit-kit (Consulta: 4 de julio de 2015) [13] Marco Ramilli’s Blog. From ROP to JOP. Disponible en http://marcoramilli.blogspot.com.es/2011/12/from-rop-to-jop. html (Consulta: 24 de junio de 2015) [14] Nazar, I. ARM7 and ARM9 opcode map. http://imrannazar.com/ARM-Opcode-Map (Consulta: 12 de junio de 2015) [15] Raspbian. Sistema operativo libre basado en Debian. https://www.raspbian.org (Consulta: 11 de junio de 2015) [16] Puente Castri, D. (2013). Linux Exploiting. M´ostoles (Madrid): 0xWORD. [17] SCS Standord. Blind Return Oriented Programming http://www.scs.stanford.edu/brop (Consulta: 24 de junio de 2015) [18] Shore, C. Navigating the Cortex Maze. Disponible en https://community.arm.com/docs/DOC-7033 (Consulta: 12 de junio de 2015) [19] Squall’s blog. ELF obfuscation: let analysis tools show wrong external symbol calls. Disponible en http://h4des.org/blog/index.php?/archives/ 346-ELF-obfuscation-let-analysis-tools-show-wrong-external-symbol-calls. html (Consulta: 13 de junio de 2015) [20] The Register. ARM Holdings eager for PC and server expansion. http://www.theregister.co.uk/2011/02/01/arm_holdings_q4_2010_numbers (Consulta: 11 de junio de 2015) [21] Wikipedia. List of ARM microarchitectures. https://en.wikipedia.org/wiki/List_of_ARM_cores (Consulta: 11 de junio de 2015) [22] Wikipedia. Instruction set architecture. https://en.wikipedia.org/wiki/Instruction_set_architecture (Consulta: 11 de junio de 2015) 49