Análise Reversa de Binário: Decifrando o Desafio re_signin

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:

  1. Inicialização de buffers: buffer_entrada, buffer_s1, dados_alvo_cifrados.
  2. Preenchimento de dados_alvo_cifrados com 28 bytes específicos.
  3. Leitura da entrada do usuário para buffer_entrada (até 127 caracteres, mais o terminador nulo).
  4. Remoção do caractere de nova linha (\n) se presente na entrada.
  5. Geração de uma chave de criptografia chamando gerar_chave(877).
  6. Chamada da função processar_criptografia para criptografar buffer_entrada e armazenar o resultado em buffer_s1.
  7. Comparação de buffer_s1 com dados_alvo_cifrados usando memcmp.
  8. 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}

Tags: CTF Reversing IDA_Pro C_language Python

Publicado em 7-23 05:39