Pela primeira vez, você não patcha nada. Você entende o algoritmo e escreve o gerador.
Ativador de software. Pede nome de usuário e uma chave no formato XXXX-XXXX.
Até aqui você só precisou patchear. Desta vez o objetivo é diferente:
entender o que a função generate_key faz e escrever um keygen que produza a chave certa para qualquer nome.
imul multiplica dois operandos com sinal. Frequentemente aparece em loops de hash como acc += byte * (pos + 1).% 9973 com multiplicação por recíproco. O resultado é equivalente ao módulo — mas parece horrível no disassembly.atoi. Então o valor gerado é comparado direto. Formatar com :08d dá o XXXX-XXXX certo.
Abre o disassembly de generate_key.
Procura o loop: tem um imul dentro dele. O que é multiplicado pelo quê?
Depois do loop, procura um imul com uma constante grande — isso é o módulo otimizado.
Reproduz essa operação em Python. Gera a chave. Testa.
objdump -d ./challenge | grep -A 40 "<generate_key>:"4012db: 8b 45 f8 mov -0x8(%rbp),%eax ; i 4012de: 48 63 d0 movslq %eax,%rdx 4012e1: 48 8b 45 e8 mov -0x18(%rbp),%rax ; name 4012e5: 48 01 d0 add %rdx,%rax 4012e8: 0f b6 00 movzbl (%rax),%eax ; byte = name[i] 4012eb: 0f b6 c0 movzbl %al,%eax 4012ee: 8b 55 f8 mov -0x8(%rbp),%edx ; i 4012f1: 83 c2 01 add $0x1,%edx ; i + 1 4012f4: 0f af c2 imul %edx,%eax ; byte * (i+1) 4012f7: 01 45 fc add %eax,-0x4(%rbp) ; acc += ...
O loop soma byte × (posição + 1) para cada caractere do nome.
Isso é a função hash — acc = Σ name[i] × (i+1).
Depois do loop vem o módulo por 9973 (otimizado com recíproco) e uma segunda operação com 1337.
div — usa multiplicação por um magic number. No disassembly após o loop:40130b: b8 55 70 48 d2 mov $0xd2487055,%eax 401310: 48 0f af c2 imul %rdx,%rax ; acc * magic 401314: 48 c1 e8 20 shr $0x20,%rax ; >> 32 401318: c1 e8 0d shr $0xd,%eax ; >> 13 = quociente 40131b: 69 d0 f5 26 00 00 imul $0x26f5,%eax,%edx ; q * 9973 401321: 29 d1 sub %edx,%ecx ; acc - q*9973 = acc % 9973
% 9973. O código-fonte deixa claro. Agora o main aplica mais uma transformação:401476: e8 31 fe ff ff call 4012ac <generate_key> 40147b: 89 45 fc mov %eax,-0x4(%rbp) ; expected = generate_key(name) 401481: 69 d0 39 05 00 00 imul $0x539,%eax,%edx ; expected * 1337 401489: 48 69 c0 f7... imul ... ; % 99991 (magic) 4014a8: 8b 45 8c mov -0x74(%rbp),%eax ; provided (da entrada) 4014ab: 39 45 f8 cmp %eax,-0x8(%rbp) ; key_val == provided? 4014ae: 75 4d jne 4014fd ; não bate → erro
A fórmula completa: key_val = (generate_key(name) * 1337) % 99991
#!/usr/bin/env python3 # keygen.py — CRACKLAB #06 import sys def generate_key(name: str) -> int: acc = 0 for i, c in enumerate(name): acc += ord(c) * (i + 1) return acc % 9973 def keygen(name: str) -> str: expected = generate_key(name) key_val = (expected * 1337) % 99991 s = f"{key_val:08d}" return f"{s[:4]}-{s[4:]}" name = sys.argv[1] if len(sys.argv) > 1 else input("Nome: ") print(f"Chave para '{name}': {keygen(name)}")
# testa com um nome qualquer KEY=$(python3 keygen.py alice) printf "alice\n$KEY\n" | ./challenge
Por que isso é mais importante que patching? Você acabou de fazer engenharia reversa de verdade.
Não corrigiu um byte — reconstruiu o algoritmo original em outra linguagem.
A partir daqui, patches ficam mais difíceis (anti-debug, checksums). Mas keygen nunca falha.
| variação | o que faz |
|---|---|
| patch clássico | Trocar jne 0x4014ae → NOP. Funciona, mas não entende o algoritmo. |
| patch retorno | Forçar cmp a sempre ser igual. Igualmente burro e mais difícil de localizar. |
| keygen (correto) | Reproduz a lógica. Funciona pra qualquer nome, qualquer instância, sem tocar no binário. |
O módulo é 9973 (primo). Por que usar um número primo como módulo numa função hash ajuda a distribuir melhor os valores? E o que aconteceria se o módulo fosse uma potência de 2?