Nanocombattants

404CTF{fi3r_n4n0comb4ttant}

Catégorie: Reverse Difficulté: hard Flag: -

Challenge

Description


Entrez dans l'arène

Le CHAUSSURE, cette fameuse entité pionnière dans le domaine du sport de combat a ouvert un tournoi pour tous les chat-diateurs qui souhaiteraient se mesurer au reste du monde. Il est l'heure d'aller se confronter dans l'arène, à un détail près... Une vérification est opérée à l'entrée, mais impossible de vous souvenir du mot de passe ! Retrouvez-le.

Format de flag : 404CTF{mot-de-passe}

Décompilation du binaire

Pour des questions de simplicité, j'ai commenté le code et zappé 2-3 lignes inutiles pour les explications.

Commençons déjà par le main, on voit qu'un mot de passe est demandé et que sa taille doit faire 19 caractères, sans quoi on ne rentre pas dans la fonction verify.

int main() {
  // ... des trucs inutiles
  puts("=================================================================================");
  puts("Bienvenue dans l\'arène ! Pour rentrer, saisissez le mot de passe du CHAUSSURE :  ");
  puts("_____                                                                       _____");
  puts("tez de me suivre, pleutre!");
  puts(" |||                                                                         ||| ");
  puts(" |||          ~Qu\'est-ce que tu peux faire contre le 404 CROU~               ||| ");
  puts(" |||            ~on a la méthode et les XOR qui rendent fou~                 ||| ");
  puts(" |||                                                                         ||| ");
  puts(
      "(___) ≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈  (___)"
      );
  puts("");
  __printf_chk(0x1,"              >>> ");
  
  __isoc99_scanf("%255s",&password);
  size = strlen(password);
  if (size == 19) {
    verify(&password);
    code = 0x0;
  } else {
    perdu();
    code = -1;
  }
  
  return code;
}

Passons maintenant à la fonction verify mais on ne va pas s'amuser à tout décortiquer. Il suffit de voir que la boucle est parcourue 19 fois (si entre temps, on n'a pas terminé dans la fonction perdu) et qu'à chaque fois, l'adresse de notre password est incrémentée. Autrement dit, on vérifie très probablement notre mot de passe caractère par caractère.

Il faut juste retenir qu'à l'intérieur de la fonction joue_avec_les_regs, il y a un appel de ptrace avec le paramètre PTRACE_GETREGS.

void verify(char *password) {
  uint pid_00;
  uint _pid;
  __pid_t pid;
  uint uVar1;
  undefined *__dest;
  undefined *__dest_00;
  int password_address;
  long in_FS_OFFSET;
  uint status;
  
  __dest = (undefined *)mmap((void *)0x0,(ulong)DAT_00104d60,0x7,0x21,-0x1,0x0);
  memcpy(__dest,&DAT_00104ec0,(ulong)DAT_00104d60);
  __dest_00 = (undefined *)mmap((void *)0x0,(ulong)DAT_00104cd0,0x7,0x22,-0x1,0x0);
  memcpy(__dest_00,&DAT_00104ce0,(ulong)DAT_00104cd0);
  
  pid_00 = fork();
  if ((int)pid_00 < 0x0) {
    __printf_chk(0x1,"ERREUR : TRAVAIL TERMINÉ");
    exit(0x1);
  }
  
  if (pid_00 != 0x0) {
    _pid = fork();
    
    if ((int)_pid < 0x0) {
      __printf_chk(0x1,"ERREUR : TRAVAIL TERMINÉ");
      exit(0x1);
    }
    
    if (_pid != 0x0) {
      // Adresse du premier char de notre 
      password_address = &password;
      do {
        
        pid = waitpid(pid_00, &status, 0);
        if (pid == -1) goto quit;
        if ((status & 0x7f) == 0) goto quit;
        if (((char)status == '\x7f') && ((status >> 8 & 0xff) == 5)) {
          fonction_osef(_pid,(long)__dest_00, password_address,(long)__dest);
          ptrace(PTRACE_CONT, (ulong)pid_00,0, 0);
          
          // Fait de la magie noire avec les ptrace
          result = joue_avec_les_regs(pid_00,(long)__dest, password_address);
          // Passe au caractères suivant
          password_address++;
          
          if (uVar1 == -1) {
            perdu();
            goto perdu;
          }
          
          ptrace(PTRACE_CONT, pid_00, 0, 0);
        }
      } while ((int)password != 19);
      success();
quit:
      return;
    }
perdu:
    FUN_001022f5(__dest_00);
  }
  arrete_de_me_suivre(__dest, password);
}

Résolution

En lançant avec la commande strace et un mot de passe de 19 caractères, on voit 3 appels à la fonction ptrace avec PTRACE_GETREGS.

$ strace ./nanocombattant                                                                                                                            105 ⨯
execve("./nanocombattant", ["./nanocombattant"], 0x7ffe5ffce1c0 /* 56 vars */) = 0

[...]

write(1, "Bienvenue dans l'ar\303\250ne ! Pour r"..., 83Bienvenue dans l'arène ! Pour rentrer, saisissez le mot de passe du CHAUSSURE :  
read(0, "0123456789abcdef012\n", 1024)  = 20

[...]

ptrace(PTRACE_GETREGS, 16017, NULL, 0x7ffcf1e885a0) = 0
ptrace(PTRACE_SETREGS, 16017, NULL, 0x7ffcf1e885a0) = 0
ptrace(PTRACE_CONT, 16017, NULL, 0)     = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_TRAPPED, si_pid=16017, si_uid=1000, si_status=SIGTRAP, si_utime=0, si_stime=0} ---
wait4(16017, [{WIFSTOPPED(s) && WSTOPSIG(s) == SIGTRAP}], 0, NULL) = 16017
ptrace(PTRACE_GETREGS, 16017, NULL, 0x7ffcf1e885a0) = 0
ptrace(PTRACE_SETREGS, 16017, NULL, 0x7ffcf1e885a0) = 0
ptrace(PTRACE_CONT, 16017, NULL, 0)     = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_TRAPPED, si_pid=16017, si_uid=1000, si_status=SIGTRAP, si_utime=0, si_stime=0} ---
ptrace(PTRACE_CONT, 16016, NULL, 0)     = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_TRAPPED, si_pid=16016, si_uid=1000, si_status=SIGTRAP, si_utime=0, si_stime=0} ---
wait4(16016, [{WIFSTOPPED(s) && WSTOPSIG(s) == SIGTRAP}], 0, NULL) = 16016
ptrace(PTRACE_GETREGS, 16016, NULL, 0x7ffcf1e885b0) = 0
write(1, "Crois-tu pouvoir me fumister si "..., 47Crois-tu pouvoir me fumister si facilement ?!?
) = 47
lseek(0, -1, SEEK_CUR)                  = -1 ESPIPE (Illegal seek)
exit_group(105)                         = ?
+++ exited with 105 +++

En fait, chaque tour de boucle dans la fonction verify déclenche 3 appels PTRACE_GETREGS. On peut donc tester tous les caractères pour savoir lequel nous fait passer au tour de boucle suivant en comptant le nombre d'appels à ptrace.

import subprocess
import string

CHARSET = string.ascii_letters + string.digits + string.punctuation
PASSWORD_SIZE = 19

password = ''
while len(password) != PASSWORD_SIZE:
  found = False
  print(f'Password: {password}')
  for c in CHARSET:
    try:
      # Lance le binaire avec strace et complète la suite du mot de passe avec des "a"
      data = subprocess.run(['strace', './nanocombattant'], input=f'{password}{c}'.ljust(PASSWORD_SIZE, 'a'), capture_output=True, text=True, check=False)
      
      # Compte le nombre d'appels à ptrace avec PTRACE_GETREGS
      getregs_count = data.stderr.count('PTRACE_GETREGS')
      
      # Le bon caractère aura plus de PTRACE_GETREGS que les autres
      if getregs_count > (len(password) + 1) * 3 or data.returncode == 0:
        password += c
        found = True
        break
      
    except Exception as e:
      exit(1)
  if not found:
    print('No char found')
    exit(2)

print('Flag: 404CTF{' + password + '}')

Dernière mise à jour

Cet article vous a-t-il été utile ?