[FADB] 7. La llamada al sistema execve()

FUNDAMENTOS DE LOS ATAQUES POR DESBORDAMIENTO DE BUFFER

La llamada al sistema execve() sustituye la imagen del proceso actual por otra. O lo que es lo mismo, sustituye el programa en ejecución por otro que nosotros le especifiquemos. Su estructura es execve(comando, argumentos, entorno), siendo comando una cadena de caracteres con la ruta del programa a ejecutar, argumentos un array de punteros a cadenas de caracteres con los argumentos a pasar al programa (terminado por un puntero NULL) y entorno un puntero de entorno (usaremos NULL ya que para este caso no nos interesa). Como ejemplo, el siguiente código en C se sustituye a sí mismo por una shell o línea de comandos:

#include

void main()
{
char *shell[2];

shell[0] = "/bin/sh";
shell[1] = NULL;

execve(shell[0], shell, NULL);
}

Lo guardamos como prueba3.c, lo compilamos y ejecutamos:

[bash]$ gcc -g prueba3.c -o prueba3
prueba3.c: In function `main':
prueba3.c:5: warning: return type of `main' is not `int'
[bash]$ ./prueba3
sh-2.05b$

Nuestro programa ha sido sustituído por una shell, en este caso /bin/sh, pero podríamos haber elegido cualquier otra (bash, ksh, csh...). Si salimos de la shell con exit volveremos a la shell desde la cual hemos ejecutado el programa. Veamos ahora un poco las entrañas del programa con gdb:

[bash]$ gdb -q prueba3
(gdb) disas main
Dump of assembler code for function main:
0x804833c
: push %ebp
0x804833d : mov %esp,%ebp
0x804833f : sub $0x8,%esp
0x8048342 : and $0xfffffff0,%esp
0x8048345 : mov $0x0,%eax
0x804834a : sub %eax,%esp
0x804834c : movl $0x80483b8,0xfffffff8(%ebp)
0x8048353 : movl $0x0,0xfffffffc(%ebp)
0x804835a : sub $0x4,%esp
0x804835d : push $0x0
0x804835f : lea 0xfffffff8(%ebp),%eax
0x8048362 : push %eax
0x8048363 : pushl 0xfffffff8(%ebp)
0x8048366 : call 0x8048258
0x804836b : add $0x10,%esp
0x804836e : leave
0x804836f : ret
End of assembler dump.
(gdb)

Antes de comenzar a desmenuzar el código, quisiera explicar la utilidad del registro EBP (Extended Bottom Pointer). Este registro se utiliza como referencia para acceder a las variables reservadas en la pila, ya que el uso de ESP se hace a veces muy confuso debido a las veces que cambia de valor. A EBP se le suele asignar el valor que tiene ESP nada más comenzar una función, como punto de referencia, es decir, siempre apunta al fondo de la pila de la función actual, de ahí lo de bottom.

La función main() comienza con PUSH EBP y MOV ESP,EBP. Primero salvamos el valor de EBP en la pila, para salvaguardar el valor que tuviera antes de entrar en esta función, y cargamos en él el valor de ESP. Ahora tenemos una referencia al fondo de la pila en EBP.

La instrucción SUB 0x8,ESP no debería necesitar explicación. Es la famosa asignación de memoria para los dos punteros de que consta el array shell (que es un array de punteros a cadenas de caracteres).

Las instrucciones AND $0xFFFFFF0,ESP, MOV 0x0,EAX y SUB EAX,ESP ya vimos con anterioridad que no modificaban nada en absoluto. En este caso tampoco lo hacen.

Comencemos ahora a ejecutar el programa para poder comprender mejor lo que va ocurriendo.

(gdb) b *0x804834c [break y * para especificar una dirección]
Breakpoint 1 at 0x804834c: file prueba3.c, line 7.
(gdb) r [run]
Starting program: /home/m0s/Programacion/C%2fC++/prueba3
Breakpoint 1, main () at prueba3.c:7
7 shell[0] = "/bin/sh";
(gdb) i r esp ebp [info registers esp ebp]
esp 0xbffff800 0xbffff800
ebp 0xbffff808 0xbffff808
(gdb)

MOVL 0x80483B8,0xFFFFFFF8(EBP) mueve el valor 0x80483B8 a EBP – 8 (EBP + 0xFFFFFFF8 = EBP – 0x8). ¿Qué es EBP – 8? EBP – 8 = ESP y es el segundo de los espacios reservados (recordemos SUB 0x8,ESP) para los dos punteros de shell. Ya que en la pila se meten las variables al revés, EBP – 8 es el puntero shell[0] y EBP – 4 es shell[1] ¿Y qué hay en 0x80483B8?

(gdb) x 0x80483b8
0x80483b8 <_io_stdin_used+4>: 0x6e69622f
(gdb) x 0x80483b8+4
0x80483bc <_io_stdin_used+8>: 0x0068732f
(gdb)

¿Y qué demonios son esos números tan feos a los que apunta 0x80483b8 (o shell[0])? ¿Qué tal si los convertimos en ASCII? 6E 69 62 2F serían respectivamente los caracteres “n”, “i”, “b”, “/”. 00 68 73 2F serían “NULL”, “h”, “s”, “/”. Recordemos que estamos trabajando en low-endian con palabras de 32 bits (el byte menos significativo en la dirección más baja). Para almacenar la cadena de caracteres “/bin/sh”, que son en total 7 caracteres + el carácter NULL (código ASCII 0) = 8 bytes, se tienen que usar 2 palabras de 4 bytes. Así que se almacenan inicialmente los primeros 4 bytes (/bin) como 0x6E69622F y luego los siguientes 4 bytes (/shNULL) como 0x0068732F. Quedaría así en memoria:

0x80483B8 N (0x6E) <-- Primera palabra
0x80483B9 I (0x69)
0x80483BA B (0x62)
0x80483BB / (0x2F)
0x80483BC NULL (0x00) <-- Segunda palabra
0x80483BD H (0x68)
0x80483BE S (0x73)
0x80483BF / (0x2F)

Como vemos, gcc ha creado un espacio en memoria (_IO_stdin_used) en la que ha almacenado la cadena “/bin/sh”. Es decir, MOVL 0x80483B8,0xFFFFFFF8(EBP) es equivalente a shell[0] = “/bin/sh” en C.

(gdb) ni [nexti / MOVL 0x80483B8,0xFFFFFFF8(EBP)]
8 shell[1] = NULL;
(gdb) i r ebp
ebp 0xbffff808 0xbffff808
(gdb) x 0xbffff808-8
0xbffff800: 0x080483b8
(gdb)

La siguiente instrucción es muy similar a la anterior. MOVL 0x0,0xFFFFFFFC(EBP) carga la dirección EBP – 4 (EBP + 0xFFFFFFFC = EBP – 4), o lo que es lo mismo, shell[1], con un puntero nulo (NULL). Equivale a shell[1] = NULL.

(gdb) ni
10 execve(shell[0], shell, NULL);
(gdb) x 0xbffff808-4
0xbffff804: 0x00000000
(gdb)

SUB 0x4,ESP no sabemos muy bien qué hace en este contexto, pero podemos suponer que son comportamientos del compilador respecto del contenido de ESP.

(gdb) ni [SUB 0x4,ESP]
0x0804835d 10 execve(shell[0], shell, NULL);
(gdb) i r esp
esp 0xbffff7fc 0xbffff7fc
(gdb) x 0xbffff7fc
0xbffff7fc: 0x08048246
(gdb) x 0x08048246
0x8048246 <_init+22>: 0x35ffc3c9
(gdb)

Vemos que ESP apunta ahora algo muy parecido a una dirección de retorno, ya que se encuentra en la pila y nos lleva a la sección <_init>, que es ejecutable y forma parte de la cabecera ELF (Executable and Linkable Format, el formato de archivos ejecutables utilizado por Linux).

(gdb) disas _init
Dump of assembler code for function _init:
0x8048230 <_init>: push %ebp
0x8048231 <_init+1>: mov %esp,%ebp
0x8048233 <_init+3>: sub $0x8,%esp
0x8048236 <_init+6>: call 0x80482a4
0x804823b <_init+11>: nop
0x804823c <_init+12>: call 0x8048310
0x8048241 <_init+17>: call 0x8048370 <__do_global_ctors_aux>
0x8048246 <_init+22>: leave
0x8048247 <_init+23>: ret
End of assembler dump.
(gdb)

Con el desensamblado podemos ya afirmar que es una dirección de retorno, pues es la posición siguiente a una llamada CALL. Pero este no es nuestro tema.

Ahora comienza el paso de parámetros por pila a la llamada al sistema execve(). Recordemos que se hace en el orden inverso al especificado en la llamada en C (execve(shell[0], shell, NULL)). PUSH 0x0 mete en la pila el NULL. LEA 0xFFFFFFF8(EBP),EAX y PUSH EAX meten en la pila la dirección del puntero al array shell (EBP – 8). Finalmente PUSHL 0xFFFFFFF8(EBP) mete en la pila el puntero a “/bin/sh”.

(gdb) ni [PUSH 0x0]
0x0804835f 10 execve(shell[0], shell, NULL);
(gdb) i r esp
esp 0xbffff7f8 0xbffff7f8
(gdb) x 0xbffff7f8
0xbffff7f8: 0x00000000

(gdb) ni [LEA 0xFFFFFFF8(EBP),EAX]
0x08048362 10 execve(shell[0], shell, NULL);
(gdb) i r eax
eax 0xbffff800 -1073743872

(gdb) ni [PUSH EAX]
0x08048363 10 execve(shell[0], shell, NULL);
(gdb) i r esp
esp 0xbffff7f4 0xbffff7f4
(gdb) x 0xbffff7f4
0xbffff7f4: 0xbffff800

(gdb) ni [PUSHL 0XFFFFFFF8(EBP)]
0x08048366 10 execve(shell[0], shell, NULL);
(gdb) i r esp
esp 0xbffff7f0 0xbffff7f0
(gdb) x 0xbffff7f0
0xbffff7f0: 0x080483b8
(gdb)

Así que el mapa de memoria de la pila quedaría tal que así justo antes de la llamada a execve() con ESP = 0xBFFFF7F0:

0xBFFFF80C Dirección de retorno de main( )
0xBFFFF808 Antiguo EBP metido por main( )
0xBFFFF804 0x00000000 (NULL) / Puntero shell[1]
0xBFFFF800 0x080483B8 / Puntero shell[0]
0xBFFFF7FC Dirección de retorno a _init+22
0xBFFFF7F8 0x00000000 (NULL) / execve( shell[0], shell, NULL )
0xBFFFF7F4 0xBFFFF800 / execve( shell[0], shell, NULL )
0xBFFFF7F0 0x080483B8 / execve( shell[0], shell, NULL )

Después de la ejecución de CALL 0x8048258, la dirección de retorno (en este caso 0x804836B) estará metida en la pila, ESP = 0xBFFFF7EC y se salta a la entrada de execve( ), cuyo desensamblado (recortando las NOP del final) es el siguiente. Tengamos en cuenta que el código de execve( ) no está cargado en memoria hasta que hagamos la llamada dinámica a la librería, dado que no hemos usado la opción -static para gcc:

(gdb) disas execve
Dump of assembler code for function execve:
0x400bcc40 : push %ebp
0x400bcc41 : mov %esp,%ebp
0x400bcc43 : sub $0x18,%esp
0x400bcc46 : mov %ebx,0xfffffff4(%ebp)
0x400bcc49 : mov %esi,0xfffffff8(%ebp)
0x400bcc4c : mov %edi,0xfffffffc(%ebp)
0x400bcc4f : call 0x40035efd <_dl_pagesize+148473>
0x400bcc54 : add $0x7c6ec,%ebx
0x400bcc5a : mov 0x8ac(%ebx),%eax
0x400bcc60 : mov 0x8(%ebp),%edi
0x400bcc63 : test %eax,%eax
0x400bcc65 : jne 0x400bcca0
0x400bcc67 : mov 0xc(%ebp),%ecx
0x400bcc6a : mov 0x10(%ebp),%edx
0x400bcc6d : push %ebx
0x400bcc6e : mov %edi,%ebx
0x400bcc70 : mov $0xb,%eax
0x400bcc75 : int $0x80
0x400bcc77 : pop %ebx
0x400bcc78 : cmp $0xfffff000,%eax
0x400bcc7d : mov %eax,%esi
0x400bcc7f : jbe 0x400bcc8f
0x400bcc81 : neg %esi
0x400bcc83 : call 0x40035a90 <_dl_pagesize+147340>
0x400bcc88 : mov %esi,(%eax)
0x400bcc8a : mov $0xffffffff,%esi
0x400bcc8f : mov %esi,%eax
0x400bcc91 : mov 0xfffffff4(%ebp),%ebx
0x400bcc94 : mov 0xfffffff8(%ebp),%esi
0x400bcc97 : mov 0xfffffffc(%ebp),%edi
0x400bcc9a : mov %ebp,%esp
0x400bcc9c : pop %ebp
0x400bcc9d : ret
0x400bcc9e : mov %esi,%esi
0x400bcca0 : call 0x40034a70 <_dl_pagesize+143212>
0x400bcca5 : jmp 0x400bcc67
0x400bcca7 : nop
End of assembler dump.
(gdb)

Lo interesante para entender este código es que las llamadas al sistema se implementan en Linux mediante interrupciones software, concretamente la interrupción 0x80. Si nos fijamos en la línea veremos la instrucción INT 0x80. La rutina de interrupción distingue qué función es requerida por el contenido del registro AL (el byte más bajo de EAX). La llamada execve() corresponde al código de función 0x0B. Luego, los tres parámetros se pasan en los registros EBX, ECX y EDX. Vemos que ahora se usa el paso de parámetro por registros (que es más rápido pero limitado al número de registros y a su tamaño). EBX es el puntero a la cadena de caracteres que representa el comando, ECX es el puntero al array de argumentos y EDX el puntero al entorno (en nuestro caso NULL).

Comments

Popular Posts