martes, 7 de agosto de 2012

Minicurso de exploiting (parte 4): Introducción a la arquitectura x86

La arquitectura x86:

Cada vez que pienso en un curso de exploiting tengo claro que hay que dedicar parte del mismo a la arquitectura sobre la que se trabaja. En nuestro caso será la de los procesadores de Intel por ser probablemente la más famosa y más extendida, aunque con la cantidad de dispositivos móviles que han ido surgiendo en los últimos años, haríamos bien en estudiar ARM.

Un repaso rápido por la historia de esta familia de procesadores. Creo recordar que Intel empezo con el microprocesador 4004 (para unas calculadoras), y que más adelante acabo diseñando el microprocesador 8086, el primero de la familia x86. Más adelante pasarían por el 8088, el 80186, el 80286 y luego llegarían al 80386 también conocido como "el 386", procesador que marcó una época. Luego llegarían el 80486 y de ahí en adelante la infinidad de procesadores de la familia Pentium (cuyas arquitecturas se siguen llamando i586, i686...).

Lo especial de la arquitectura del 386 (que es la que tocaremos) es que fué la que se usó en la mayoría de PCs alrededor del mundo y que era de 32 bits. Actualmente sigue siendo interesante estudiarla porque sigue habiendo muchos sistemas de 32 bits y además los más modernos procesadores de 64 bits permiten la ejecución de programas de 32 bits.

Los registros:

Como sabemos, los procesadores tienen en su interior ciertos tipos de memoria, la más conocida suele ser la memoria caché, en general con dos niveles L1 y L2 (creo que hay algunos procesadores que tienen incluso un tercer nivel L3, pero no suele ser lo general). Aunque esto para nosotros es transparente.

Otro tipo de memoria que tienen, aún más reducida y más rápida son los registros. Un registro es básicamente un pequeño almacén de un tamaño concreto y pequeño (1 byte, 2 bytes, 4 bytes...). Las instrucciones pueden realizar su cometido tanto con la memoria como con los registros. La arquitectura i386 divide los registros en tres categorías:
  • Registros de propósito general:
    • EAX (Extended Accumulator)
    • EBX (Extended Base index)
    • ECX (Extended Counter)
    • EDX (Extended Data index)
    • ESI (Extended Source Index)
    • EDI (Extended Destination Index)
    • ESP (Extended Stack Pointer)
    • EBP (Extended Base Pointer)
  • Registros de segmento:
    • CS (Code Segment)
    • DS (Data Segment)
    • SS (Stack Segment)
    • ES (Extra Segment)
    • FS (¿Frame? Segment)
  • Registros de control:
    • EIP (Extended Instruction Pointer)
    • EFLAGS (Extended FLAGS)
No trataré los registros de segmento ya que a la hora de programar en espacio de usuario no se suelen tocar, sino cuando se programa en espacio de kernel. Existen debido a que la arquitectura i386 permite un modelo de memoria tanto paginado como segmentado. Pero que sepamos que existen y que cuando nos metemos a nivel de kernel podríamos llegar a sufrirlos (yo los he sufrido xD).

Los interesantes a tratar son los de propósito general, estos registros son todos de 32 bits, la "e" de "extended" hace referencia a ello, pero también se pueden usar como registros de 16 bits o incluso 8 bits si nos referimos a ellos como AX (para acceder a los 16 bits menos significativos de EAX) o AH (Accumulator High) para acceder a los 8 bits más significativos de AX y AL (Accumulator Low) para los 8 bits menos significativos de AX. Y esto se repite con EBX, ECX y EDX.

Todos estos registros tienen ciertas implicaciones sobre según qué instrucciones, por lo tanto a la hora de programar en ensamblador (¿qué pasa? divertidísimo) asegúrate de tener un manual o un listado con las instrucciones que detalle todo lo que hacen. Por ejemplo, con las instrucciones de la familia rep (repetition) se repite la instrucción a la que rep hace referencia hasta que ECX llega a 0. Con las instrucciones orientadas a strings (el mnemónico comienza por la letra "s") ESI actúa como el puntero a la string de origen y EDI como el puntero a la string de destino. Esto ya hay que estudiarlo cuando te vayas a pegar con ello, no puedo tratarlo todo aquí (además de que no me lo sé todo, claro está).

Sobre los registros de control, pues EIP no puede ser accedido directamente, pero si indirectamente con instrucciones de la familia call y ret. EFLAGS es un registro de flags como su nombre indica. Mantiene información de si la última instrucción ha producido acarreo, un cero, un positivo o un negativo, la paridad, etc...

Instrucciones x86:

La arquitectura x86 es CISC (Complex Instruction Set Computer), esto quiere decir que estos procesadores tienen un mogollón de instrucciones, a cual más compleja y extravagante, cuyos cometidos pueden ir desde incrementar en 1 un registro hasta freir un huevo sin necesidad de aceite (instrucción "feggwo" Fry EGG Without Oil). ¿Y qué quiero decir al soltar esto al principio? Pues que no voy a hacer un listado de todas, ni de lo que hacen ni nada de nada xD. Tampoco hablaré de todos los modos de direccionamiento de la memoria que pueden usar estas instrucciones ni de como se encodean a binario. ¿Para qué una sección de instrucciones si no voy a hablar de ellas?, pues para poner un par de ejemplos y así vayamos viendo un poco las instrucciones que más vamos a utilizar en este curso... porque programaremos en ensamblador, ya lo adelanto.

Veamos un ejemplo de código para una shellcode (para Linux) escrito con sintaxis AT&T.

.globl _start
_start:
 jmp  trick
shellcode:
 pop  %esi
 xor  %eax, %eax
 movb %al, 0x7(%esi)
 movl %esi, 0x8(%esi)
 movl %eax, 0xc(%esi)
 movl %esi, %ebx
 movl %esi, %ecx
 addb $0x8, %cl
 xor  %edx, %edx
 xor  %esi, %esi
 movb $0xb, %al
 int  $0x80

trick:
 call  shellcode

.string "/bin/shABBBBCCCC"

Ahora el mismo ejemplo con sintaxis Intel.

 jmp  trick

shellcode:
 pop  esi
 xor  eax, eax
 mov  [esi + 0x7], al
 mov  [esi + 0x8], esi
 mov  [esi + 0xc], eax
 mov  ebx, esi
 mov  ecx, esi
 add  cl, 8
 xor  edx, edx
 xor  esi, esi
 mov  al, 0xb
 int  0x80

trick:
 call shellcode

db "/bin/shABBBBCCCC"
  
Lo que quiero hacer notar con esto es que existen dos (no sé si más) sintaxis distintas y muy extendidas para escribir en ensamblador de x86, la de AT&T y la de Intel. Y tienen bastante diferencias que debemos tener en cuenta. Las principales son las siguientes:
  • En AT&T se pone el registro destino a la derecha, Intel a la izquierda.
  • En AT&T para referenciar a un registro le tenemos que poner un '%' delante, en Intel no.
  • En AT&T los literales (números directamente en un operando) se marcan con un '$' al principio, en Intel no.
  • En AT&T los direccionamientos van entre paréntesis, en Intel van entre corchetes.
  • En AT&T los desplazamientos en un direccionamiento van fuera de los paréntesis, en Intel van dentro de los corchetes.
  • En AT&T el mnemónico de la instrucción hace referencia al tamaño del tipo de dato que utiliza b para byte, w para word (2 bytes), l para long (4 bytes) y no estoy seguro si d para double (8 bytes), en Intel no se usan estas letras.
Para el primer ejemplo podríamos usar gcc para compilar, para el segundo podemos usar nasm... aunque no estoy seguro si se le puede decir a gcc que compile un código con sintaxis Intel, creo que sí.

Conclusión, aunque x86 tiene infinidad de instrucciones a la hora de programar no usaremos demasiadas (aunque usaremos más que las vistas en estos ejemplos). Donde sí nos hará más falta conocer las instrucciones y tener los manuales preparados al lado será a la hora del reversing.

Espero con esto haber alimentado un poco la curiosidad sobre x86. Esto sólo es un comienzo, yo no puedo ponerme aquí a dar un curso completo de x86 (porque necesitaría mucho tiempo y porque no soy un gurú de esta arquitectura). Esto es un curso de exploiting, ahora te toca a tí currártelo. Además aprenderemos bastante según avance el curso a base de pegarnos con estas cosas.

Saludos.

No hay comentarios:

Publicar un comentario