Visão Geral do Desafio
Este artigo detalha a análise reversa de um executável binário, denominado "re_signin", que faz parte de um desafio de CTF. O objetivo é compreender o algoritmo de criptografia implementado e reverter a operação para encontrar a entrada correta que produz um valor cifrado específico, revelando assim a flag.
Análise da Função Principal (main)
A função principle do programa inicia inicializando várias áreas de memória e definindo um array de bytes pré-determinado, que chamaremos de dados_alvo_cifrados. Este array é crucial, pois representa o valor que a entrada do usuário, após ser processada, deve igualar. O fluxo principal é o seguinte:
- Inicialização de buffers:
buffer_entrada,buffer_s1,dados_alvo_cifrados. - Preenchimento de
dados_alvo_cifradoscom 28 bytes específicos. - Leitura da entrada do usuário para
buffer_entrada(até 127 caracteres, mais o terminador nulo). - Remoção do caractere de nova linha (
\n) se presente na entrada. - Geração de uma chave de criptografia chamando
gerar_chave(877). - Chamada da função
processar_criptografiapara criptografarbuffer_entradae armazenar o resultado embuffer_s1. - Comparação de
buffer_s1comdados_alvo_cifradosusandomemcmp. - Exibição de "succsess" se forem idênticos, ou "fail" caso contrário.
O código descompilado da função main é apresentado abaixo:
int __cdecl main(int argc, const char **argv, const char **envp)
{
char dados_alvo_cifrados[144]; // Buffer para dados cifrados esperados
char buffer_s1[128]; // Buffer para armazenar o resultado da criptografia da entrada
char buffer_entrada[136]; // Buffer para a entrada do usuário
int chave_secreta; // Chave gerada para criptografia
int tamanho_entrada; // Tamanho da entrada do usuário
memset(buffer_entrada, 0, 0x80uLL);
memset(buffer_s1, 0, sizeof(buffer_s1));
memset(dados_alvo_cifrados, 0, sizeof(dados_alvo_cifrados));
// Inicialização dos 28 bytes dos dados cifrados esperados
dados_alvo_cifrados[0] = 0x9C;
dados_alvo_cifrados[1] = 0xCC;
dados_alvo_cifrados[2] = 0x88;
dados_alvo_cifrados[3] = 0x76;
dados_alvo_cifrados[4] = 0xD7;
dados_alvo_cifrados[5] = 0x89;
dados_alvo_cifrados[6] = 0x78;
dados_alvo_cifrados[7] = 0xEC;
dados_alvo_cifrados[8] = 0x7C;
dados_alvo_cifrados[9] = 0xD7;
dados_alvo_cifrados[10] = 0x89;
dados_alvo_cifrados[11] = 0x71;
dados_alvo_cifrados[12] = 0xE3;
dados_alvo_cifrados[13] = 0x6D;
dados_alvo_cifrados[14] = 0x98;
dados_alvo_cifrados[15] = 0x17;
dados_alvo_cifrados[16] = 0x94;
dados_alvo_cifrados[17] = 0xF;
dados_alvo_cifrados[18] = 0xCA;
dados_alvo_cifrados[19] = 0x9F;
dados_alvo_cifrados[20] = 0x7E;
dados_alvo_cifrados[21] = 0xD9;
dados_alvo_cifrados[22] = 0xA0;
dados_alvo_cifrados[23] = 0x8A;
dados_alvo_cifrados[24] = 0x79;
dados_alvo_cifrados[25] = 0xD1;
dados_alvo_cifrados[26] = 0x80;
dados_alvo_cifrados[27] = 0x77;
printf("Insira a sua flag: "); // Mensagem de prompt (assumindo byte_400A66 é um formato)
fgets(buffer_entrada, 128, stdin);
tamanho_entrada = strlen(buffer_entrada);
if ( buffer_entrada[tamanho_entrada - 1] == 10 )
buffer_entrada[tamanho_entrada - 1] = 0; // Remove o newline
chave_secreta = gerar_chave(877); // Geração da chave
processar_criptografia((__int64)buffer_entrada, (__int64)buffer_s1, 2024, chave_secreta);
if ( !memcmp(buffer_s1, dados_alvo_cifrados, 128uLL) )
puts("success");
else
puts("fail");
return 0;
}
Função de Criptografia (processar_criptografia)
A função processar_criptografia (originalmente encrypt) é responsável por transformar a entrada do usuário. Ela opera byte a byte, usando uma chave, uma constante e um valor de estado que é atualizado a cada iteração.
A lógica de criptografia pode ser resumida como:
__int64 __fastcall processar_criptografia(__int64 entrada_bytes, __int64 saida_cifrada, int constante_fixa, int chave_gerada)
{
__int64 resultado_temporario; // Valor temporário para cálculos
int contador_loop; // Variável de iteração
int estado_anterior_v6; // Estado acumulado do loop
unsigned int deslocamento_calculado; // Valor de deslocamento para adição
estado_anterior_v6 = 0; // Inicializa o estado com zero
for ( contador_loop = 0; contador_loop <= 127; ++contador_loop )
{
resultado_temporario = *(unsigned __int8 *)(contador_loop + entrada_bytes);
if ( (_BYTE)resultado_temporario ) // Se o byte de entrada não for nulo
{
deslocamento_calculado = chave_gerada + constante_fixa + estado_anterior_v6;
*(_BYTE *)(contador_loop + saida_cifrada) = *(unsigned __int8 *)(contador_loop + entrada_bytes) + deslocamento_calculado; // Cifra = Entrada + Deslocamento
// Atualiza o estado para a próxima iteração
// O valor *(_BYTE *)(contador_loop + saida_cifrada) é o byte cifrado atual
estado_anterior_v6 = (deslocamento_calculado >> 4) ^ *(unsigned __int8 *)(contador_loop + saida_cifrada);
}
}
return resultado_temporario;
}
A fórmula central para cada byte cifrado (C) é:
C[i] = P[i] + (chave + constante + estado_anterior)
E o estado para a próxima iteração (estado_posterior) é:
estado_posterior = ((chave + constante + estado_anterior) >> 4) ^ C[i]
Onde P[i] é o byte da entrada original no índice i.
Geração da Chave (gerar_chave)
A chave é gerada por uma função simples que pega um inteiro como semente (no caso, 877) e realiza uma operação aritmética:
__int64 __fastcall gerar_chave(int semente_inicial)
{
return (unsigned int)(1540483477 * semente_inicial + 305419896) >> 16;
}
Dados Cifrados Esperados
Os bytes que o programa espera obter após criptografar a entrada são:
valores_alvo = [
0x9C, 0xCC, 0x88, 0x76, 0xD7, 0x89, 0x78, 0xEC,
0x7C, 0xD7, 0x89, 0x71, 0xE3, 0x6D, 0x98, 0x17,
0x94, 0x0F, 0xCA, 0x9F, 0x7E, 0xD9, 0xA0, 0x8A,
0x79, 0xD1, 0x80, 0x77
]
É importante notar que o loop de criptografia vai até 127 bytes, mas os valores_alvo fornecidos são apenas 28 bytes. Isso sugere que a flag tem 28 caracteres e os bytes subsequentes (de 28 a 127) da entrada devem ser nulos (0x00) para que a condição if ( (_BYTE)resultado_temporario ) não seja ativada, mantendo buffer_s1 zerado nesses índices, o que é consistente com a inicialização de dados_alvo_cifrados com zeros.
Revertendo o Processo
Para encontrar a entrada original (a flag), precisamos reverter as operações da função processar_criptografia. Dado que:
C[i] = P[i] + (chave + constante + estado_anterior)
Podemos inferir que:
P[i] = C[i] - (chave + constante + estado_anterior)
Como estamos lidando com operações de byte (0-255), qualquer resultado negativo ou que exceda 255 deve ser tratado com um módulo 256 (ou uma operação & 0xFF).
1. Calcular a Chave
Primeiro, reproduzimos o cálculo da chave usando a semente 877:
#include <stdio.h>
unsigned int obter_chave_secreta(unsigned int semente) {
unsigned int valor_intermedio = 1540483477 * semente + 305419896;
return (valor_intermedio >> 16);
}
int main() {
unsigned int semente_usada = 877;
unsigned int chave_final = obter_chave_secreta(semente_usada);
printf("A chave hexadecimal e: 0x%X\n", chave_final); // Imprime em hexadecimal
return 0;
}
A execução deste código nos dará a chave, que é 0xA051.
2. Descriptografar a Entrada
Com a chave em mãos e a lógica de reversão, podemos escrever um script em Python para calcular a flag:
# Chave calculada a partir da função gerar_chave(877)
chave_cripto = 0xA051
# Constante fixa usada na função encrypt
constante_num = 2024
# Os bytes cifrados que a entrada deve produzir
bytes_cifrados_esperados = [
0x9C, 0xCC, 0x88, 0x76, 0xD7, 0x89, 0x78, 0xEC,
0x7C, 0xD7, 0x89, 0x71, 0xE3, 0x6D, 0x98, 0x17,
0x94, 0x0F, 0xCA, 0x9F, 0x7E, 0xD9, 0xA0, 0x8A,
0x79, 0xD1, 0x80, 0x77
]
# Variável para armazenar o estado que muda a cada iteração (o 'v6' no IDA)
estado_iterativo = 0
# Lista para armazenar os bytes da flag decifrada
flag_bytes_decifrados = []
# Iteramos sobre os bytes cifrados esperados
for i in range(len(bytes_cifrados_esperados)):
# Calculamos o valor de deslocamento (offset) usado na criptografia
# Importante: o valor é o deslocamento_calculado & 0xFF porque é um byte-offset
valor_deslocamento_total = chave_cripto + constante_num + estado_iterativo
# Revertemos a operação de criptografia: P[i] = C[i] - (deslocamento & 0xFF)
# Usamos & 0xFF para garantir que a operação se mantenha no domínio de 8 bits (byte)
byte_plano = (bytes_cifrados_esperados[i] - (valor_deslocamento_total & 0xFF)) & 0xFF
flag_bytes_decifrados.append(byte_plano)
# Atualizamos o estado iterativo para a próxima rodada
# estado_posterior = ((valor_deslocamento_total) >> 4) ^ C[i]
estado_iterativo = ((valor_deslocamento_total >> 4) ^ bytes_cifrados_esperados[i])
# Convertemos a lista de bytes para uma string
flag_final = ''.join(chr(byte) for byte in flag_bytes_decifrados)
print(f"A flag e: {flag_final}")
O resultado da execução do script Python é a flag.
Flag Obtida
ctfshow{happy_2024_jiayou_a}