A lógica está certa. O portão está virado. Um byte resolve tudo.
Sistema de ativação. Código de 8 dígitos. Você não tem o código. E não precisa ter.
No desafio #02 você aprendeu a mentir dentro da função.
Desta vez, a função está certa — ela valida direito.
O problema está em quem interpreta o resultado.
74. Pula para outro endereço quando ZF=1 (resultado anterior foi zero). Usado após test eax, eax quando função retorna 0.75. Pula quando ZF=0 (resultado foi diferente de zero). Exatamente o oposto do je — diferem por apenas 1 bit.test eax, eax faz AND lógico de eax com si mesmo. Se eax=0, seta ZF=1. Os jumps condicionais leem esse flag pra decidir se pulam.
Abre o disassembly do main.
Procura o call validate_code.
O que vem logo depois? Se o resultado é 0 (inválido), o programa pula pra onde?
E se você fizesse ele pular pro lado errado?
objdump -d ./challenge | grep -A 10 "call.*validate_code"4012bc: e8 f5 fe ff ff call 4011b6 <validate_code> 4012c1: 85 c0 test %eax, %eax ; eax = retorno (0 ou 1) 4012c3: 74 2f je 4012f4 ; se 0 (inválido) → pula pro erro 4012c5: ... ; senão → conteúdo válido
O je em 0x4012c3 é o portão.
Se validate_code retorna 0, ZF=1, e je pula pro bloco de erro.
Se retorna 1, não pula — cai no bloco de sucesso.
A função está correta. Só o portão está do lado errado.
74 = je (pula se zero) →
75 = jne (pula se não-zero).
Diferem por exatamente 1 bit.
validate_code("12345678") → retorna 0 (código inválido) antes (je): test eax, eax → ZF = 1 74 2f → je → ZF=1, PULA → erro depois (jne): test eax, eax → ZF = 1 75 2f → jne → ZF=1, NÃO PULA → sucesso ✓
# confirma o byte atual objdump -d ./challenge | grep "4012c3" # esperado: 4012c3: 74 2f je 4012f4
r2 -w ./challenge s 0x4012c3 wx 75 ; só o opcode — offset 2f não muda q
# patch com printf + dd (funciona sem nenhuma ferramenta extra) printf '\x75' | dd of=./challenge bs=1 seek=$((0x4012c3)) conv=notrunc 2>/dev/null
# verifica: deve mostrar jne agora objdump -d ./challenge | grep "4012c3" # 4012c3: 75 2f jne 4012f4
| instrução | opcode | pula quando | par oposto |
|---|---|---|---|
| je | 74 | ZF=1 (igual / zero) | jne (75) |
| jne | 75 | ZF=0 (diferente / não-zero) | je (74) |
| jl | 7C | SF≠OF (menor, signed) | jge (7D) |
| jge | 7D | SF=OF (maior ou igual) | jl (7C) |
| jle | 7E | ZF=1 ou SF≠OF | jg (7F) |
| jg | 7F | ZF=0 e SF=OF | jle (7E) |
Cada par de opostos difere por 1 bit no opcode. 74 ↔ 75 é literalmente um toggle de bit.
| método | onde | o que faz |
|---|---|---|
| je → jne | 0x4012c3 — main | Um byte. Portão invertido. A solução mais limpa. |
| patch em validate_code | 0x401245 — função | Troca sete por setne: retorna 1 pra qualquer coisa que não seja o código correto. |
| NOP no exit | 0x401326 — main | 5 NOPs no call de exit(1). Funciona mas output mostra erro. Feio. |
Qual dessas três abordagens (je→jne, patch na função, NOP no exit) deixaria menos rastro numa análise forense do binário?