ThaySan
  • 👋Bienvenue
    • Avant-propos
  • 🚩CTF & Writeups
    • 2025 | EC2
      • Let's Crax
    • 2025 | HackDay
      • 🔑Cryptographie
        • Drunk Christmas
        • Find Eve 1
        • Well hidden message - Standard Code Encryption
      • 🔎Forensic
        • Copperwire Extraction
        • Distracted user
        • I believe you can't fly
      • 🧠Misc
        • Hello Steve
      • 💾Programmation
        • Lonely bot
        • Lonely bot #2
        • Lonely bot #3
        • Useless Animals Sorting
        • Who cares about SSI anyway ?
      • ⚙️Reverse
        • Just dig
        • Rusty_rev
        • The Cogs of Blackmail
      • 🎭Steganographie
        • It says a lot when there is no music
        • Mona LSB
        • Well hidden message - Insignificant blue
      • 🌐Web
        • Super Website Verificator 3000
        • The analytical engine leak
        • The Watchful Gears: Uncover the Secrets Within
        • Your region's finest
    • 2024 | Efrei - CyberNight
      • 🔑Cryptographie
        • Clavier spécial
        • Le futur
        • Securechain 30.0
        • Cascade
        • Shared Flag
        • Weird Algorithm
      • 🧊Minecraft
        • Introduction
        • Non aux Bouquins Trafiqués
        • Redstone Gold Block
      • 💾Programmation
        • Captcha 1.0
        • Intro à la prog
        • Captcha 2.0
      • ⚙️Reverse
        • Reverse the Reverse
        • Find me if you can
        • HuGO Decrypt
        • Kitten eXORcism
        • M30W Vault Tech
        • The Ugandan Labyrinth
      • 🎭Stéganographie
        • Le message
        • bon Le ordre
        • COD FORFLAG
        • Mauvaise note
        • Bruit
        • Un (un ?) drôle de QR code
        • Randonnée Guillerette à Bordeaux
      • 💻Système
        • Marche-pied
        • Escabot
        • régulation des Données et des Normes de Sécurité
      • 🌐Web
        • cybernight.zip
        • Mon champion préféré
        • Co(mpressed)okies
        • Gitty Mistake
        • JWT Kiddo
        • Parseur Farceur
      • L'enquête
        • L'enquête 2/6
        • L'Enquête 1/6
        • Bienvenue, enquêteur
    • 2024 | Star-Hack
      • 🔑Cryptographie
        • César, mais pas César
        • Double ennui
        • Langage secret
        • Quadratique Mystérieuse
        • ReSultAt
        • Sup3r S3cr3t
        • Warmup
      • 🔎Forensic
        • Cache-cache
        • Fichier ZIP protégé par mot de passe
        • HEADER
        • Investigation 1
        • Investigation 2
      • 🧠Misc
        • B4l4d3 Urb41n3
        • Bruhh, c'est quoi ça ?
        • Cut13
        • Pika Pika
      • 😈Pwn
        • Pwn0x01
        • Pwn0x02
        • Pwn0x03
      • ⚙️Reverse
        • Assembly
        • Rev0x00
        • Rev0x01
        • Rev0x02
      • 🌐Web
        • Clone Udemy
        • Flask
        • Guess_The_Passcode
        • PHP
        • Tickets
        • Usine de Cookies
    • 2024 | ECW
      • 🔑Cryptographie
        • Course hipPIN
    • 2024 | CTFREI Intro
      • 🔑Cryptographie
        • AES Intro
        • Aléatoire
        • Game of Luck
        • RSA Intro
        • RSA2
        • RSA3
      • 🔎Forensic
        • Mais qui est le photographe ?
        • Mais où est passé mon flag ?
        • Mémoire 1/4
        • Mémoire 2/4
        • Mémoire 3/4
        • Mémoire 4/4
        • My computer is talking
      • 📚OSINT
        • Avion ✈
        • Geoint
        • Google!
        • Googlint
        • Le pivot
        • Le temps commence maintenant
        • Sacré dossier
        • Socint
      • 💾Programmation
        • Try Me
        • Answer Me
        • Eval Me
        • Time Based
      • 😈Pwn
        • BOF Intro
        • Shop
        • BOF 2
        • BOF win()
      • ⚙️Reverse
        • CrackMe1
        • CrackMe2
        • CrackMe3
        • Hidden...
        • Something changed?
        • ZZZ
      • 🎭Stéganographie
        • Cybernight être comme
        • Joli paysage
        • Petit poisson
        • StegHide 'n' Seek
        • Un canard pas comme les autres
      • 💻Système
        • Bash Jail
        • Bash Jail Revenge
        • BrokenBin
        • GTFO Of Here
        • Pyjail
        • Pyjail Revenge
        • Strange input, right?
      • 🌐Web
        • SQLi
        • POST This Money
        • Give me my Flask PIN
        • Access
        • Render
        • RenderV2
        • Touchy
    • 2024 | DefCamp
      • 🔑Cryptographie
        • conv
        • oracle-srl
        • ctr
      • 🔎Forensic
        • i-got-a-virus
        • Alternating
        • call-me-pliz
      • 🧠Misc
        • pyterm
      • 📱Mobile
        • mobisec
      • 📚OSINT
      • 😈Pwn
      • ⚙️Reverse
      • 🕵️Traque
      • 🌐Web
        • noogle
        • production-bay
    • 2024 | 404CTF
      • 🔑Cryptographie
        • Bébé nageur
        • Le petit bain
        • Poor Random Number Generator [1/2]
        • Plongeon Rapide Super Artistique
        • J'éponge donc j'essuie
        • Poor Random Number Generator [2/2]
        • La Seine
        • J'ai glissé chef !
        • SEA - La face cachée de l'Iceberg
      • 🔎Forensic
        • Le tir aux logs
        • Darts Bank
        • Vaut mieux sécuriser que guérir
        • De bons croissants au beurre
        • Poids Raw vs. Cours Jette [1/3]
      • 🔌Hardware
        • Serial killer
        • Le soulevé de GND
        • Comment est votre modulation ? [1/2]
        • Sea side channel [1/4] - Introduction
        • Comment est votre modulation ? [2/2]
        • Sea side channel [2/4] - Reconnaissance
        • Sea side channel [3/4] - Mais où sont les triggers ?
      • 🤖IA
        • Du poison [1/2]
        • Du poison [2/2]
        • Des portes dérobées
      • 🧠Misc
        • Discord
        • De la friture sur la ligne
        • Bienvenue
        • Revers(ibl)e Engineering [0/2]
      • 📚OSINT
        • Légende
        • Not on my watch
        • Secret Training [1/2]
      • 😈Pwn
        • Pseudoverflow
        • Jean Pile
        • Mordu du 100m
        • Antismash
      • 🐈‍⬛Quantique
        • Des trains superposés
        • De l'écoute, pas très discrète
        • De la multiplicité des problèmes
      • ⚙️Reverse
        • ⭐Échauffement
        • ⭐Intronisation du CHAUSSURE
        • ⭐Bugdroid Fight [1/2]
        • ⭐Revers(ibl)e Engineering [1/2]
        • ⭐Bugdroid Fight [2/2]
        • ⭐Nanocombattants
        • ⭐Revers(ibl)e Engineering [2/2]
        • Le Tableau Noir
      • 🎭Stéganographie
        • ⭐L'absence
        • ⭐Regarder en stéréo
        • ⭐La Barre Fixe
        • ⭐Le grand écart
        • ⭐La chute
      • 🌐Web
        • ⭐Vous êtes en RETARD
        • ⭐Le match du siècle [1/2]
        • ⭐Exploit mag
        • ⭐Le match du siècle [2/2]
        • ⭐LE GORFOU 42
        • ⭐La boutique officielle
    • 2024 | CTFREI - Bordeaux
      • 🔑Cryptographie
        • zzz
      • 📚OSINT
        • Alexis Dumas
        • Back to the bureau
        • Dr Octopus
        • Folie et ambition
        • GeoGuessr
        • Hugo Nelots : prélude
        • La fin ?
        • La fuite Dumas
        • Un réseau suspect
      • 💾Programmation
        • Eval me 1
        • Eval me 2
        • Time Based
      • 💻Système
        • Broken Binary 1
        • Broken Binary 2
        • GTFO of here
        • Pyjail 1
        • Pyjail 2
        • Pyjail 3
        • Pyjail 4
      • 🌐Web
        • Au commencement était le verb
        • Becadmin
        • PHP Juggler
    • 2024 | HTB - Cyber Apocalypse Challenges
      • 🔗Blockchain
        • Lucky Faucet
        • Recovery
        • Russian Roulette
      • 🔑Cryptographie
        • Blunt
        • Dynastic
        • Iced TEA
        • Makeshift
        • Primary Knowledge
      • 🔎Forensic
        • An unusual sighting
        • Data Siege
        • Fake Boost
        • Game Invitation
        • It Has Begun
        • Phreaky
        • Pursue The Tracks
        • Urgent
      • 🔌Hardware
        • BunnyPass
        • Flash-ing Logs
        • Maze
        • Rids
        • The PROM
      • 🧠Misc
        • Character
        • Cubicle Riddle
        • Path of Survival
        • Stop Drop and Roll
        • Unbreakable
        • Were Pickle Phreaks
        • Were Pickle Phreaks Revenge
      • 😈Pwn
        • Delulu
        • Pet Companion
        • Tutorial
        • Writing on the Wall
      • ⚙️Reverse
        • BoxCutter
        • Crushing
        • FollowThePath
        • LootStash
        • MazeOfPower
        • Metagaming
        • PackedAway
        • QuickScan
      • 🌐Web
        • Flag Command
        • KORP Terminal
        • Labyrinth Linguist
        • LockTalk
        • Testimonial
        • TimeKORP
    • 2024 | UNbreakable
      • 🔑Cryptographie
        • start-enc
        • traffic-e
      • 🔎Forensic
        • easy-hide
        • password-manager-is-a-must
      • 🧠Misc
        • rfc-meta
      • 📱Mobile
        • flagen
        • improper-configuration
      • 📡Network
        • wifi-basic
        • wifiland
      • 📚OSINT
        • persistent-reccon
        • safe-password
      • 😈Pwn
        • intro-to-assembly
      • ⚙️Reverse
        • fake-add
      • 🎭Stéganographie
        • secrets-of-winter
      • 🌐Web
        • pygment
        • sided-curl
        • you-can-trust-me
    • 2023 | EFREI - CyberNight
      • 📚OSINT
        • Invest Now !
      • 😈Pwn
        • NSA Call Converter
      • ⚙️Reverse
        • CryptoVirus
        • WebChaussettes
      • 🌐Web
        • DoctoLeak
    • 2023 | Flag4All
      • 🔑Cryptographie
        • Aes IV
        • Crypt my loop
        • Kentucky fried chicken
        • RSA primes
        • Xor
    • 2022 | EFREI - CyberNight
      • 🔑Cryptographie
        • Coupé-décalé
        • ExFILEtration
        • Il s'est baissé ou pas
        • J'ai pas roté
        • Les allemands !
        • RSA Strong Prime generator
      • 🔎Forensic
        • Bomberman 1/2
        • Bomberman 2/2
        • Magic
        • Peu importe le chemin
        • Sniff sniff
        • Souvenir
        • Xray
      • 🔌Hardware
        • Class4
        • Find me 2/3
        • Identify 1/3
        • Yo listen 3/3
      • 🧠Misc
        • Et je tombe tombe tombe
        • Des yeux partout
        • RiGOLe
        • Roomba tricheur
        • Survey
        • Tinder
      • 💾Programmation
        • Repeat
        • Startup
        • Timing
      • ⚙️Reverse
        • Auth 1
        • Auth2
        • Auth3
        • Cryptoroomba
        • Tenet
      • 🎭Stéganographie
        • 50 shades of stephane legar
        • Chess master
        • Deviens champion sers toi de tout ce que tu as appris
        • Drifting in the bits
        • Pyramide
        • Spirale
      • 🌐Web
        • Ah bah c'est du propre
        • Cooking roomba
        • Leaderboard
        • vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
Propulsé par GitBook
Sur cette page
  • Challenge
  • Préparation
  • Analyse des binaires reçus
  • main
  • recv_functions
  • connect_and_send_token
  • recv_function
  • recv_key_and_message
  • read_password
  • verify
  • apply_functions
  • encrypt
  • Compréhension et stratégie
  • Résolution
  • Redirection des flux
  • Récupération des données
  • Serveur local
  • GDBScript et solve
  • Script complet

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

  1. CTF & Writeups
  2. 2024 | 404CTF
  3. Reverse

Revers(ibl)e Engineering [2/2]

404CTF{4df8110da3b4c5c1e87e564418fab97f}

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

Challenge

Description


Il est recommandé de finir Revers(ibl)e Engineering [1/2] avant d'essayer ce challenge.Du fait de la nature des binaires générés, vous pourriez vouloir les executer dans une VM.

"Je vois que mon enseignement a porté ses fruits, augmentons le niveau. Quand lors d'un combat anticiper devient impossible, seul celui qui sait danser sur le fil de l'imprévisible peut triompher dans ce ballet de l'inconnu."

Même principe que l'épreuve précèdente, mais cette fois vous avez dix minutes. La chance ! Par contre, les crackme sont à usage unique. Pas très eco-friendly, je sais..

Connexion : nc challenges.404ctf.fr 31990 > chall.zip nc challenges.404ctf.fr 31991

Ce challenge tourne sur un docker et n'est pas disponible

Ce challenge est la suite de Revers(ibl)e Engineering [1/2], il est nécessaire d'avoir lu sa solution avant pour comprendre celle-ci

Préparation

Ici, nous utiliserons le même script de préparation que dans la partie 1 du challenge pour les mêmes raisons.

import socket
import os
from io import BytesIO
from zipfile import ZipFile
import typing

HOST = 'challenges.404ctf.fr'
PORT_BINARY = 31990
PORT_SOLVE = 31991

class Binary:
    FOLDER = 'out'
    
    # Sauvegarde de toutes les instances de binaires téléchargés
    INSTANCES: typing.Dict['str', 'Binary'] = {}
    
    @staticmethod
    def fetch() -> 'Binary':
        client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client.connect((HOST, PORT_BINARY))
        archive = b''
        while True:
            data = client.recv(4096)
            if len(data) == 0:
                break
            archive += data
        archive = ZipFile(BytesIO(archive))
        crackme = archive.read('crackme.bin')
        token = archive.read('token.txt').decode()
        return Binary(crackme, token)

    def __init__(self, crackme: bytes, token: str) -> None:
        self.crackme = crackme
        self.token = token
        self.filename = f'crackme_{self.token}.bin'
        self.filepath = os.path.join(self.FOLDER, self.filename)
        
        # Récupération des données envoyées par le serveur
        self.responses = self.get_server_data()
        
        # Ajout de notre objet à la liste des instances existances
        self.INSTANCES[self.token] = self
        
        self.save()
    
    def save(self):
        if not os.path.exists(self.FOLDER):
            os.mkdir(self.FOLDER)
        with open(self.filepath, 'wb') as f:
            f.write(self.crackme)
        os.system(f'chmod +x {self.filepath}')
    
    def get_server_data(self):
        # A compléter
        pass
    
    def solve(self):
        # A compléter
        pass

Analyse des binaires reçus

Cette partie liste les fonctions du binaire, si vous avez la flemme, les explications de résolution commence après

Commençons par une décompilation d'un binaire (avec Ghidra pour ma part). J'ai renommé toutes les variables, modifié un peu le code pour le simplifier et ajouté des commentaires histoire de mieux comprendre ce qu'il se passe.

main

int main(void) {
  bool wrong_password;
  int code;
  long i;
  char message [0x80];
  long password [0x2];
  char key [0x10];
  long functions [0x21];  // tableau contenant les adresses de fonctions mises en mémoire
  
  // Initialisation du tableau à 0
  for (i = 0; i != 0x20; i++) functions[i] = 0x0;
  
  // Contacte le serveur pour récupérer les fonctions mises en mémoire
  code = recv_functions((long)functions);
  if (code < 0) {
    printf("Une erreur est survenue lors de la verification :(");
    exit(1);
  }
  
  while( true ) {
    // Contacte le serveur pour récupérer une clé et le message à afficher 
    code = recv_key_and_message(key,message);
    if (code < 0) {
      printf("Une erreur est survenue lors de la verification :(");
      exit(1);
    }
    if (code == 1337) break;
    
    read_password(message,password);
    wrong_password = verify(functions, key, password);
    if (wrong_password) {
      puts("Mauvaise clé, dommage..");
      exit(1);
    }
  }
  puts("C\'est bon, tu as bien mérité ton flag.. Envoie toutes les clés concaténées au serveur de vérification pour le recevoir.");
  return 0;
}

recv_functions

int recv_functions(long *functions) {
  int sock_fd;
  int recved_size;
  char *buffer;
  void *memory_dest;
  int i;
  
  i = 0;
  while( true ) {
    // Créé un buffer tampon
    buffer = (char *)malloc(0x400);
    // Alloue un espace en mémoire où sera stocké la fonction reçu
    memory_dest = mmap((void *)0, 0x400, 0x7, 0x22, -1, 0);
    if (buffer == NULL) return -1;
    
    // Se connecte au serveur et envoie le token associé au binaire
    sock_fd = connect_and_send_token();
    // Reçoit en réponse la fonction compilée
    recved_size = recv_function(sock_fd,buffer);
    // Ferme la connexion
    close(sock_fd);
    
    if (recved_size < 0) break;
    if (recved_size == 0) return 0;
    // Copie la fonction reçue dans la mémoire
    memcpy(memory_dest, buffer, recved_size);
    
    // Ajoute l'adresse mémoire à la liste des fonctions
    functions[i] = (long)memory_dest;
    // Libère le buffer tampon
    free(buffer);
    i = i + 1;
  }
  return -1;
}

connect_and_send_token

int connect_and_send_token() {
  int sock_fd;
  int success;
  size_t size;
  sockaddr_in serv_addr;
  
  // Définis un socket(2 (AF_INET), 1 (SOCK_STREAM), 0 (protocol par défaut - TCP))
  sock_fd = socket(2, 1, 0);
  if (sock_fd < 0) {
    printf("Impossible de se connecter au serveur..");
    exit(1);
  }
  
  // Configure les infos du serveur
  serv_addr.sin_family = 2;
  serv_addr.sin_port = htons(31992);
  serv_addr.sin_addr = inet_addr("162.19.101.153");
  
  // Se connecte
  success = connect(sock_fd, (sockaddr *)&serv_addr, 0x10);
  if (success < 0) {
    printf("Impossible de se connecter au serveur..");
    exit(1);
  }
  
  // Envoie le token
  size = strlen("55b20a291d201497d871b76063dbd74e\n");
  send(sock_fd,"55b20a291d201497d871b76063dbd74e\n", size, 0);
  return sock_fd;
}

recv_function

int recv_function(int sock_fd,char *buff) {
  char recved [0x408];
  char *found_at_index;
  size_t recved_size;
  size_t total_recved_size;
  
  total_recved_size = 0;
  do {
    // Récupère 0x400 (1024) octets sur le socket
    recved_size = recv(sock_fd,recved,0x400,0x0);
    if ((long)recved_size < 0x0) return -1;
    if (recved_size == 0) break;
    
    // Copies à la suite les données reçues dans le buffer
    memcpy(buff + total_recved_size,recved, recved_size);
    total_recved_size = total_recved_size + recved_size;
    
    // Si les données reçues sont uniquement "endfunc", alors la boucle s'arrête
    found_at_index = strstr(recved, "endfunc");
  } while (found_at_index == (char *)0);
  
  // Retourne
  return (int)total_recved_size + -0x7;
}

recv_key_and_message

int recv_key_and_message(void *key,void *message) {
  int sock_fd;
  ssize_t recved_size;
  
  sock_fd = connect_and_send_token();
  // Reçoit la clé de 16 octets 
  recved_size = recv(sock_fd,key,0x10,0x0);
  if (recved_size < 1) return -1;  // Erreur de réception du message
  
  memset(message,0x0,0x80);
  // Reçoit le message pour demander le mot de passe à l'utilisateur
  recved_size = recv(sock_fd,message,0x80,0x0);
  if (recved_size < 1) return -1;  // Erreur de réception du message
  
  // Si le message reçu est "done", alors nous avons passé toutes les vérifs
  if (memcmp(message,"done",0x4) == 0) return 1337;

  return 0;
}

read_password

void read_password(char *message, char *password) {
  // Affiche le message pour demander le mot de passe à l'utilisateur
  printf("%s",message);
  // Récupère notre mot de passe dans le STDIN, max 32 octets
  fgets(password, 0x20, stdin);
  return;
}

verify

bool verify(long *functions, char *key, char *password) {
    int diff;
    void *key_encrypted;
    
    // Applique les fonctions spécifiées sur le mot de passe
    apply_functions(functions, password);
    
    // Chiffre la clé (key) pour obtenir une version chiffrée
    key_encrypted = encrypt(key);
    
    // Compare les 16 premiers octets de password avec key_encrypted
    diff = memcmp(password, key_encrypted, 16);
    
    // Si les 16 premiers octets de password ne correspondent pas à key_encrypted,
    // retourne true (c'est-à-dire que la vérification a échoué)
    return diff != 0;
}

apply_functions

void apply_functions(long *functions, char *password) {
    int i;
    
    // Parcourt le tableau de fonctions jusqu'à trouver la sentinelle (0x0)
    for (i = 0; (code *)functions[i] != (code *)0x0; i = i + 1) {
        // Appelle chaque fonction sur le mot de passe
        (*(code *)functions[i])(password);
    }

    return;
}

encrypt

void *encrypt(char *data) {
    unsigned char *encrypted = malloc(16); // Alloue de la mémoire pour stocker les données chiffrées (16 octets)
    
    for (int i = 0; i < 16; i++) {
        unsigned char n2 = data[i]; // Récupère le i-ème caractère de la chaîne
        unsigned char n1;

        // Algorithme de chiffrement (change à chaque binaire téléchargé)
        n1 = n2 ^ ((n2 >> 5 & 0x01) << 3);
        n1 = n1 ^ (((n1 >> 2) & (n1 >> 6) & 0x01) << 1) ^ 0x40;
        n2 = n1 ^ (((n1 & (n1 >> 1) & 0x01) << 6)) ^ (n2 << 7);
        n2 = n2 ^ (((n2 >> 7) & (n2 >> 4) & 0x01) << 5) ^ 0x40 ^ ((n2 >> 7) << 5);
        n2 = n2 ^ (((n2 >> 3) & (n2 >> 2) & 0x01) << 5) ^ 0x08;
        n2 = n2 ^ (((n2 >> 2) & n2 & 0x01) << 7);
        n1 = n2 ^ 0x20 ^ (((n2 ^ 0x20) >> 2) & 0x01) << 5;
        n1 = n1 ^ (((n1 >> 5) & (n1 >> 3) & 0x01) << 1) ^ ((n2 >> 7) << 2);
        n1 = n1 ^ (n1 >> 1 & 0x01) ^ (((n2 >> 6) & 0x01) << 4) ^ ((n2 >> 7) << 6);

        // Stocke le caractère chiffré dans le tableau alloué
        encrypted[i] = n1;
    }
    return encrypted;
}

Compréhension et stratégie

Pour résumer, voici les étapes depuis le lancement du binaire :

  1. Une connexion s'ouvre vers le serveur 162.19.101.153:31992 pour récupérer des fonctions compilées jusqu'à recevoir le message endfunc

  2. Tant que la boucle n'est pas cassée

    1. Une nouvelle connexion s'ouvre pour récupérer une clé de 16 octets et un message de taille variable

    2. Demande le mot de passe en affichant le message récupéré

    3. Applique toutes les fonctions sur notre mot de passe

    4. Chiffre la clé

    5. Compare la clé chiffrée avec notre mot de passe modifié

La petite chose à noter est que le serveur auquel se connecte notre binaire n'envoie les réponses qu'une seule fois. Autrement dit, on ne peut lancer le binaire qu'une seule fois.


L'astuce utilisée ici est de contacter le serveur distant qui envoie les fonctions et clés en amont, son IP et son port ne changent d'un binaire à l'autre.

Ainsi, avant de lancer notre binaire, on va récupérer les réponses qu'il était censé recevoir du serveur et les enregistrer. Ensuite, on viendra lancer un serveur local qui imitera le comportement du serveur distant en renvoyant ces données dans le même ordre. Il ne reste alors plus qu'à rediriger les requêtes faites par le binaire vers le serveur distant sur notre serveur local pour qu'il n'y voie que feu et qu'on puisse le lancer sans limites.

Résolution

Redirection des flux

L'objectif est de capturer ce qui part vers le serveur distant sur le port 31192 et de le rediriger vers notre serveur local sur le port 3000. Mais nous devons aussi pouvoir récupérer les données du serveur distant, pour ça, on peut mapper un autre port libre (ici le 40000) vers le vrai port 31992 car si on utilise directement le vrai port, il sera capturé par notre première règle.

Il nous faut donc 2 règles iptables :

  • 162.19.101.153:31992 redirigé vers localhost:3000

  • 162.19.101.153:xxxx redirigé vers 162.19.101.153:31992

Voici un petit script qui permet d'ajouter les règles et les supprimer (tout en vérifiant si elles sont déjà en place).

from subprocess import getstatusoutput

EXTERNAL_IP = '162.19.101.153' # IP du serveur distant
EXTERNAL_PORT = 31992          # Port du serveur distant
FAKE_EXTERNAL_PORT = 40000     # Nouveau port du serveur distant
LOCAL_PORT = 3000              # Port du serveur local

def apply_rules(option: str):
    rules = [
        f'sudo iptables -t nat {option} OUTPUT -p tcp -d {EXTERNAL_IP} --dport {EXTERNAL_PORT} -j DNAT --to-destination 127.0.0.1:{LOCAL_PORT}',
        f'sudo iptables -t nat {option} OUTPUT -p tcp -d {EXTERNAL_IP} --dport {FAKE_EXTERNAL_PORT} -j DNAT --to-destination {EXTERNAL_IP}:{EXTERNAL_PORT}'
    ]
    for rule in rules:
        if getstatusoutput(rule)[0] != 0:
            raise Exception('La règle n\'existe pas')

def add_rules():
    try:
        apply_rules('-C')
    except:
        apply_rules('-A')

def delete_rules():
    apply_rules('-D')

Récupération des données

On va compléter la fonction get_server_data de notre classe Binary, elle va servir à récupérer les données envoyées par le serveur distant en se faisant passer pour le binaire.

class Binary    
    def get_server_data(self):
        # Liste des réponses reçues
        responses = []
        while True:
            # Connexion au serveur distant via le port de redirigé avec iptables
            client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            client.connect((EXTERNAL_IP, FAKE_EXTERNAL_PORT))
            # Envoi du token
            client.send(f'{self.token}\n'.encode())
            # Réception des données
            data = client.recv(4096)
            client.close()
            # Ajout des données à la liste
            responses.append(data)
            if b'done' in data:
                break
        return responses

Serveur local

A chaque fois que le binaire fait une requête avec son token, on doit envoyer les données suivantes. Le serveur va être lancé dans un thread pour ne pas bloquer l'exécution du binaire ensuite.

# Décorateur pour lancer automatiquement une fonction dans un thread
def threaded(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        thread = threading.Thread(target=func, args=args, kwargs=kwargs)
        thread.start()
        return thread
    return wrapper

class Server:
    @threaded
    @staticmethod
    def run():
        # Compteurs pour savoir quelle données à envoyer en fonction du token
        counters = {}

        # Création du serveur local et écoute
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server.bind(('127.0.0.1', LOCAL_PORT))
        server.listen()
        while True:
            conn, _ = server.accept()
            with conn:
                # Récupération du message envoyé par le binaire
                token = conn.recv(1024).strip().decode()

                # Permet de réinitialiser tous les compteurs
                if token.startswith('reset'):
                    counters = {}
                    conn.send(b'Reset done !\n')
                    continue
                
                # Si l'on a les réponses associées au token 
                if token in Binary.INSTANCES:
                    binary = Binary.INSTANCES[token]
                    if token not in counters:
                        counters[token] = 0
                    data = binary.responses[counters[token]]
                    conn.sendall(data)
                    # Incrémentation du compteur
                    counters[token] = (counters[token] + 1) % len(binary.responses)

GDBScript et solve

On fait la même technique que dans la version 1 du challenge, on met un breakpoint sur le memcmp@plt et l'on regarde les registres. Petite variation sur le fait qu'au total on nous demande 3 mots de passe à la suite.

import json
import socket

# Les caractères possibles pour le mot de passe
CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
KEY_SIZE = 16


# Reset les compteurs du serveur local
def reset() -> None:
	client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	client.connect(('162.19.101.153', 31992))
	client.send(b'reset\n')


# Récupère la valeur 16 bytes d'une adresse dans un registre
def get_register(register: str) -> bytes:
	return b''.join([bytes.fromhex(n.strip())[::-1] for n in gdb.execute(f'x/2xg ${register}', False, True).split('\t0x')[1:]])


# Lancer le binaires avec un mot de passe
def run_password(password: str, skip: int) -> tuple[bytes, dict]:
	reset()
	# Ajoute le mot de passe dans le STDIN, où il est récupéré par le fgets dans le binaire
	gdb.execute(f'r <<< {password}')
	
	# Skip le nombre de breakpoint
	for _ in range(skip):
		gdb.execute('c')
	key = get_register('rcx')
	rax = get_register('rax')
	# Retourne le tuple (clé en mémoire, mapping partiel)
	return key, {rax[i]: password[i] for i in range(KEY_SIZE)}


# Utilise le mapping pour retrouver le bon mot de passe
def solve_key(target: bytes, mapping: dict):
	return ''.join([mapping[n] for n in target])


# Break avant la comparaison
gdb.execute('b memcmp@plt')

# Map contenant le couple {chiffré: caractère}
mapping = {}

# Coupe notre charset en bloc de 16 caractères
for i in range(0, len(CHARSET), KEY_SIZE):
	# Complète au cas où il n'y en a pas assez avec des 'a'
	password = CHARSET[i:i+KEY_SIZE].ljust(KEY_SIZE, 'a')
	# skip le premier breakpoint, ce qui n'est pas ce qui nous intéresse
	target1, partial_mapping = run_password(password, skip=1)
	mapping.update(partial_mapping)

key1 = solve_key(target1, mapping)

target2, _ = run_password(key1.ljust(31, 'A') + '0000111122223333', skip=3)
key2 = solve_key(target2, mapping)

target3, _ = run_password(key1.ljust(31, 'A') + key2.ljust(31, 'A') + '0000111122223333', skip=5)
key3 = solve_key(target3, mapping)

# Concaténation des mots de passe et le print
print(f'{key1+key2+key3}')

# Quit GDB
gdb.execute(f'quit')

Script complet

import socket
import os
from io import BytesIO
from zipfile import ZipFile
from subprocess import getstatusoutput
import typing
import threading
import functools
from subprocess import check_output

HOST = 'challenges.404ctf.fr'
PORT_BINARY = 31990
PORT_SOLVE = 31991
EXTERNAL_IP = '162.19.101.153' # IP du serveur distant
EXTERNAL_PORT = 31992          # Port du serveur distant
FAKE_EXTERNAL_PORT = 40000     # Nouveau port du serveur distant
LOCAL_PORT = 3000              # Port du serveur local

############################################################################

def apply_rules(option: str):
    rules = [
        f'sudo iptables -t nat {option} OUTPUT -p tcp -d {EXTERNAL_IP} --dport {EXTERNAL_PORT} -j DNAT --to-destination 127.0.0.1:{LOCAL_PORT}',
        f'sudo iptables -t nat {option} OUTPUT -p tcp -d {EXTERNAL_IP} --dport {FAKE_EXTERNAL_PORT} -j DNAT --to-destination {EXTERNAL_IP}:{EXTERNAL_PORT}'
    ]
    for rule in rules:
        if getstatusoutput(rule)[0] != 0:
            raise Exception('La règle n\'existe pas')

def add_rules():
    try:
        apply_rules('-C')
    except:
        apply_rules('-A')

def delete_rules():
    apply_rules('-D')

############################################################################

class Binary:
    FOLDER = 'out'
    INSTANCES: typing.Dict['str', 'Binary'] = {}
    
    @staticmethod
    def fetch() -> 'Binary':
        client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client.connect((HOST, PORT_BINARY))
        archive = b''
        while True:
            data = client.recv(4096)
            if len(data) == 0:
                break
            archive += data
        archive = ZipFile(BytesIO(archive))
        crackme = archive.read('crackme.bin')
        token = archive.read('token.txt').decode()
        return Binary(crackme, token)

    def __init__(self, crackme: bytes, token: str) -> None:
        self.crackme = crackme
        self.token = token
        self.filename = f'crackme_{self.token}.bin'
        self.filepath = os.path.join(self.FOLDER, self.filename)
        self.responses = self.get_server_data()
        self.INSTANCES[self.token] = self
        self.save()
    
    def save(self):
        if not os.path.exists(self.FOLDER):
            os.mkdir(self.FOLDER)
        with open(self.filepath, 'wb') as f:
            f.write(self.crackme)
        os.system(f'chmod +x {self.filepath}')
    
    def get_server_data(self):
        # Liste des réponses reçues
        responses = []
        while True:
            # Connexion au serveur distant via le port de redirigé avec iptables
            client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            client.connect((EXTERNAL_IP, FAKE_EXTERNAL_PORT))
            # Envoi du token
            client.send(f'{self.token}\n'.encode())
            # Réception des données
            data = client.recv(4096)
            client.close()
            # Ajout des données à la liste
            responses.append(data)
            if b'done' in data:
                break
        return responses
    
    def solve(self) -> str:
        return check_output(['gdb', '--command', 'gdbscript.py', self.filepath]).splitlines()[-1].strip().decode()

############################################################################

# Décorateur pour lancer automatiquement une fonction dans un thread
def threaded(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        thread = threading.Thread(target=func, args=args, kwargs=kwargs)
        thread.start()
        return thread
    return wrapper

class Server:
    @threaded
    @staticmethod
    def run():
        # Compteurs pour savoir quelle données à envoyer en fonction du token
        counters = {}

        # Création du serveur local et écoute
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server.bind(('127.0.0.1', LOCAL_PORT))
        server.listen()
        while True:
            conn, _ = server.accept()
            with conn:
                # Récupération du message envoyé par le binaire
                token = conn.recv(1024).strip().decode()

                # Permet de réinitialiser tous les compteurs
                if token.startswith('reset'):
                    counters = {}
                    conn.send(b'Reset done !\n')
                    continue
                
                # Si l'on a les réponses associées au token 
                if token in Binary.INSTANCES:
                    binary = Binary.INSTANCES[token]
                    if token not in counters:
                        counters[token] = 0
                    data = binary.responses[counters[token]]
                    conn.sendall(data)
                    # Incrémentation du compteur
                    counters[token] = (counters[token] + 1) % len(binary.responses)

############################################################################

def main() -> None:
    # Création des règles iptables
    add_rules()
    
    # Lancement du serveur local
    Server.run()

    # Récupération d'un binaire
    binary = Binary.fetch()
    print(f'Token: {binary.token}')
    
    # Lancement de la résolution du binaire avec gdb
    password = binary.solve()
    print(f'Password: {password}')

    # Envoi de la solution sur le serveur
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect((HOST, PORT_SOLVE))
    client.recv(2048)
    client.send(f'{binary.token}\n'.encode())
    client.recv(2048)
    client.send(f'{password}\n'.encode())
    # Récupération du flag
    print(client.recv(2048).decode())
    
    # Suppression des règles iptables
    delete_rules()
    
    # Quitte tous les threads
    os._exit(0)


if __name__ == '__main__':
    main()
PrécédentNanocombattantsSuivantLe Tableau Noir

Dernière mise à jour il y a 1 an

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

🚩
⚙️
⭐