# Du tatouage

**Catégorie:** Intelligence Artificielle - **Difficulté:** Difficile

{% file src="/files/DHqWnqlDvNJomxbwE5aa" %}

{% file src="/files/kI2kkqaHK63sTsMJaGJk" %}

{% file src="/files/Zq6oXr0hZcFQWuDwnZVI" %}

{% file src="/files/RNqvRnffoyVeOySvRDcH" %}

**Description:**

<figure><img src="/files/RgZzhmL3ATyMMzoUh16r" alt=""><figcaption></figcaption></figure>

Solution:

Script de résolution :&#x20;

```python
import json
import torch
from tqdm.auto import tqdm
import transformer_lens as tl

CONFIG = {
    "TAILLE_FILTRE": 10,
    "MODULO_CHIFFREMENT": 50033,
    "FICHIERS_JOURNAUX": {
        "modele_33": "journal33.json",
        "modele_34": "journal34.json",
        "modele_35": "journal35.json"
    },
    "PREFIXE_FLAG": "404CTF{",
    "SUFFIXE_FLAG": "}"
}

class DecodeurHistoires:
    """Classe principale pour le décodage des histoires et l'extraction des clés secrètes"""
    
    def __init__(self):
        """Initialisation du décodeur et chargement du modèle"""
        self.resultats = {}
        self._initialiser_environnement()
        self._charger_modele()
    
    def _initialiser_environnement(self):
        """Configuration de l'environnement d'exécution"""
        self.appareil = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        self.precision = torch.float16 if self.appareil.type == "cuda" else torch.bfloat16
        print(f"Environnement: {self.appareil}, précision {self.precision}")
    
    def _charger_modele(self):
        """Chargement du modèle de langage pour l'analyse"""
        print("Initialisation du modèle Tiny Stories...")
        self.modele = tl.HookedTransformer.from_pretrained_no_processing(
            "tiny-stories-instruct-33M", 
            dtype=self.precision, 
            device=self.appareil
        )
        self.modele.to(self.appareil)
        self.modele.eval()
    
    def _generer_filtre(self, graine):
        """Génère un filtre binaire à partir d'une graine"""
        generateur = torch.Generator()
        generateur.manual_seed(graine)
        
        taille_double = 2 * CONFIG["TAILLE_FILTRE"]
        indices_permutation = torch.randperm(taille_double, generator=generateur)
        
        filtre_binaire = torch.zeros(taille_double, dtype=torch.bool)
        filtre_binaire[indices_permutation[:CONFIG["TAILLE_FILTRE"]]] = True
        
        return filtre_binaire
    
    def _calculer_graine(self, cle, token_precedent, token_avant_precedent):
        """Calcule une graine cryptographique à partir des tokens et d'une clé"""
        return (token_precedent * cle + token_avant_precedent) % CONFIG["MODULO_CHIFFREMENT"]
    
    def _extraire_contenu_histoire(self, texte):
        """Extrait le contenu principal de l'histoire en ignorant les métadonnées"""
        lignes = texte.splitlines()
        contenu = []
        
        for ligne in lignes:
            if any(ligne.startswith(marqueur) for marqueur in ["Words", "Story:", "Summary:", "Features:"]):
                break
            contenu.append(ligne)
            
        return " ".join(contenu).strip()
    
    def _analyser_journal(self, identifiant, chemin_fichier):
        """Analyse un fichier journal pour trouver la clé secrète"""
        print(f"\nAnalyse du journal {identifiant} ({chemin_fichier})")
        
        # Chargement des données
        with open(chemin_fichier, 'r', encoding='utf-8') as fichier:
            entrees = json.load(fichier)
        
        # Préparation des données pour l'analyse
        print("Préparation des données pour l'analyse...")
        contextes_tokens = []
        total_contextes = 0
        
        for entree in tqdm(entrees, desc=f"Préparation {identifiant}"):
            texte_histoire = self._extraire_contenu_histoire(entree)
            tokens = self.modele.to_tokens(texte_histoire).to(self.appareil)
            longueur = tokens.shape[1]
            
            # Vérification de la longueur minimale
            if longueur <= 20:
                contextes_tokens.append([])
                continue
                
            contextes_entree = []
            
            # Analyse des tokens après le 20ème
            for position in range(20, longueur):
                # Extraction du contexte
                tokens_precedents = tokens[:, :position]
                
                # Prédiction du modèle
                with torch.no_grad():
                    logits = self.modele(tokens_precedents)
                
                # Récupération des top prédictions
                logits_derniere_position = logits[0, -1, :]
                _, indices_top = logits_derniere_position.topk(2 * CONFIG["TAILLE_FILTRE"], dim=-1)
                
                # Vérification de la position du token actuel
                token_actuel = tokens[0, position].item()
                positions_trouvees = (indices_top == token_actuel).nonzero(as_tuple=False)
                
                if len(positions_trouvees) == 0:
                    continue
                    
                position_trouvee = positions_trouvees[0].item()
                token_precedent = tokens[0, position-1].item()
                token_avant_precedent = tokens[0, position-2].item()
                
                contextes_entree.append((token_precedent, token_avant_precedent, position_trouvee))
                total_contextes += 1
                
            contextes_tokens.append(contextes_entree)
            
        print(f"Total: {total_contextes} contextes de tokens à analyser")
        
        # Recherche de la clé
        meilleure_cle = None
        meilleur_score = -1
        
        for cle_candidate in tqdm(range(1, 100000), desc=f"Analyse {identifiant}", unit="clé"):
            score_actuel = 0
            
            for contextes_entree in contextes_tokens:
                if not contextes_entree:
                    continue
                    
                for (token_precedent, token_avant_precedent, position_trouvee) in contextes_entree:
                    graine = self._calculer_graine(cle_candidate, token_precedent, token_avant_precedent)
                    filtre = self._generer_filtre(graine)
                    
                    if not filtre[position_trouvee]:
                        break
                        
                    score_actuel += 1
                    
            if score_actuel > meilleur_score:
                meilleur_score = score_actuel
                meilleure_cle = cle_candidate
                
        print(f"Clé identifiée pour {identifiant}: {meilleure_cle} (score: {meilleur_score})")
        self.resultats[identifiant] = meilleure_cle
    
    def decoder_tous_journaux(self):
        """Analyse tous les fichiers journaux pour extraire les clés"""
        for identifiant, chemin in CONFIG["FICHIERS_JOURNAUX"].items():
            self._analyser_journal(identifiant, chemin)
    
    def generer_flag(self):
        """Génère le flag final à partir des clés trouvées"""
        if len(self.resultats) != 3:
            return "Erreur: toutes les clés n'ont pas été trouvées"
            
        # Extraction et formatage des clés dans l'ordre requis
        cle_35 = str(self.resultats["modele_35"]).zfill(5)
        cle_34 = str(self.resultats["modele_34"]).zfill(5)
        cle_33 = str(self.resultats["modele_33"]).zfill(5)
        
        # Construction du flag final
        flag = f"{CONFIG['PREFIXE_FLAG']}{cle_35}{cle_34}{cle_33}{CONFIG['SUFFIXE_FLAG']}"
        print(f"\nFlag final: {flag}")
        return flag

if __name__ == "__main__":
    decodeur = DecodeurHistoires()
    decodeur.decoder_tous_journaux()
    flag_final = decodeur.generer_flag()
```

Le script va donc tourner une dizaine de minute (cela dépend de la puissance de votre machine) et vous afficher le flag directement :&#x20;

<figure><img src="/files/v1WfDASmC9CIKE4qYArn" alt=""><figcaption></figcaption></figure>

<details>

<summary>🚩FLAG</summary>

`404CTF{436333370349117}`

</details>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://writeups.ayweth20.com/2025/404ctf-2025/intelligence-artificielle/du-tatouage.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
