|
110 | 110 | "ctfatac", "ctf", "binary", "crypto", "web", "ctfatac2025" |
111 | 111 | ], |
112 | 112 | "articleBody": "CTF@AC 2025 Web 🌐 money Analisi La challenge espone una dashboard minimale che supporta plugin di terze parti. Quando carichiamo un plugin, la piattaforma ci permette anche di scaricare quelli esistenti (incluso l’ufficiale flag.plugin).\nExploit Dopo aver scaricato flag.plugin, notiamo che è cifrato. Il file server.py contiene sia la chiave sia la funzione per decifrarlo, quindi possiamo decifrarlo in locale usando decrypt_file.\nKEY = b\"SECRET_KEY!123456XXXXXXXXXXXXXXX\" def decrypt_file(input_path, output_path, key): with open(input_path, \"rb\") as f: data = f.read() iv = data[:16] ciphertext = data[16:] cipher = AES.new(key, AES.MODE_CBC, iv) plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size) with open(output_path, \"wb\") as f: f.write(plaintext) Il init.py del flag.plugin decifrato contiene il seguente codice:\nimport json, sqlite3, pathlib, time, uuid import os plugin_dir = pathlib.Path(__file__).resolve().parent manifest_path = plugin_dir / \"plugin_manifest.json\" name, version = \"Widget\", \"1.0.0\" if manifest_path.exists(): try: m = json.loads(manifest_path.read_text()) name = m.get(\"name\", name) version = m.get(\"version\", version) except Exception: pass thumb = thumb = f''' ", |
113 | | - "wordCount" : "5583", |
| 113 | + "wordCount" : "6161", |
114 | 114 | "inLanguage": "it", |
115 | 115 | "image":"https://opengraph.githubassets.com/eccdc445364e4f9dcbece7bb7f178f0756be13a48717c78ec94bf78c35861b9a/PascalCTF/PascalCTF.github.io","datePublished": "2025-09-16T00:00:00Z", |
116 | 116 | "dateModified": "2025-09-16T00:00:00Z", |
@@ -211,7 +211,7 @@ <h1 class="post-title entry-hint-parent"> |
211 | 211 | <div class="post-description"> |
212 | 212 | Alcune writeup della ctf CTF@AC edizione 2025. |
213 | 213 | </div> |
214 | | - <div class="post-meta"><span title='2025-09-16 00:00:00 +0000 UTC'>settembre 16, 2025</span> · 27 minuti · 5583 parole · Paolo | Traduzioni: |
| 214 | + <div class="post-meta"><span title='2025-09-16 00:00:00 +0000 UTC'>settembre 16, 2025</span> · 29 minuti · 6161 parole · Paolo | Traduzioni: |
215 | 215 | <ul class="i18n_list"> |
216 | 216 | <li> |
217 | 217 | <a href="https://pascalctf.github.io/en/ctf/ctfatac/">🇬🇧 En</a> |
@@ -725,7 +725,16 @@ <h3 id="pythonese">Pythonese<a hidden class="anchor" aria-hidden="true" href="#p |
725 | 725 | <h4 id="flag">Flag<a hidden class="anchor" aria-hidden="true" href="#flag">#</a></h4> |
726 | 726 | <pre tabindex="0"><code>CTF{2944cec0c0f401a5fa538933a2f6210c279fbfc8548ca8ab912b493d03d2f5bf} |
727 | 727 | </code></pre><h3 id="ironevil">Ironevil<a hidden class="anchor" aria-hidden="true" href="#ironevil">#</a></h3> |
728 | | -<p>TODO</p> |
| 728 | +<h4 id="la-challenge">La challenge<a hidden class="anchor" aria-hidden="true" href="#la-challenge">#</a></h4> |
| 729 | +<p>Il binario fornito nella challenge, chiamato <code>ironveil</code>, è un eseguibile ELF 64-bit PIE compilato per Linux e collegato a un loader NixOS. Poiché il percorso dell’interprete indicato nel binario punta a una posizione non standard, il programma non può essere eseguito direttamente su un sistema tipico. È per questo che, lanciandolo da shell, compare l’errore “cannot execute: required file not found.” In pratica, la soluzione è specificare manualmente il loader del sistema, di solito <code>/lib64/ld-linux-x86-64.so.2</code>, per poter eseguire il programma.</p> |
| 730 | +<p>Il codice decompilato mostra che, prima di qualsiasi operazione di cifratura, il programma dedica molto tempo all’inizializzazione. Imposta gestori di segnali, esegue controlli con <code>poll</code> sui descrittori di file e interagisce con <code>/dev/null</code>. Inoltre interroga attributi dei thread, come indirizzo e dimensione dello stack, e li riallinea con precisione. Queste procedure sono tipiche di binari resi più resistenti a tecniche di debugging o all’esecuzione in sandbox. Una volta completata l’inizializzazione, però, la logica si riduce a un comportamento piuttosto semplice: il programma si aspetta un file come input e produce un output cifrato con il suffisso <code>.encrypted</code>.</p> |
| 731 | +<p>La routine di cifratura è basata su una macchina virtuale personalizzata. Questa VM interpreta trentadue opcode per generare uno stream di byte che funge da chiave. Lo stream viene poi applicato al file in input con un’operazione di XOR byte per byte. Ogni byte di plaintext viene combinato con il corrispondente byte della chiave e il risultato viene scritto su disco. Il dettaglio cruciale è che la VM è deterministica: lo stesso binario produce sempre lo stesso keystream. Non esiste alcun seed casuale, nonce o variazione per file. Ciò significa che la trasformazione è semplicemente <code>ciphertext = plaintext ⊕ key</code>. Applicare la trasformazione due volte con la stessa chiave la annulla, perché <code>(P ⊕ K) ⊕ K = P</code>.</p> |
| 732 | +<h4 id="la-soluzione-1">La soluzione<a hidden class="anchor" aria-hidden="true" href="#la-soluzione-1">#</a></h4> |
| 733 | +<p>La challenge ci metteva a disposizione soltanto il binario e un file già cifrato, <code>flag.txt.encrypted</code>. La soluzione pensata dagli autori probabilmente era quella di invertire la VM, studiarne le trentadue istruzioni e rigenerare lo stream di chiave per decifrare manualmente il ciphertext. Tuttavia, la natura deterministica dell’algoritmo offriva una via molto più semplice. Dando in pasto al programma il file già cifrato, lo stesso keystream veniva applicato di nuovo. Di conseguenza, la doppia cifratura si annullava e restituiva il plaintext originale.</p> |
| 734 | +<p>Eseguendo il binario tramite il loader di sistema con il file cifrato come input veniva generato un nuovo file, <code>flag.txt.encrypted.encrypted</code>. Aprendolo si poteva vedere immediatamente la flag in chiaro all’inizio del file. Il resto conteneva byte spazzatura, coerenti con l’operazione XOR che prosegue oltre la flag su dati inutilizzati o irrilevanti. Ma la presenza della flag completa all’inizio era sufficiente per risolvere la challenge.</p> |
| 735 | +<h4 id="note-finali">Note finali<a hidden class="anchor" aria-hidden="true" href="#note-finali">#</a></h4> |
| 736 | +<p>La debolezza di sicurezza qui risiede proprio nel riutilizzo di uno stream di chiave statico. Nella crittografia reale, i cifrari a flusso sono sicuri solo se ogni cifratura usa un nonce o un vettore di inizializzazione univoco, così da garantire che lo stream non si ripeta mai. In assenza di questa misura, il cifrario si riduce a un insicuro “many-time pad”, in cui l’uso ripetuto dello stesso keystream porta inevitabilmente a perdite di informazione. In questo caso, la falla era talmente grave che una semplice doppia esecuzione del binario invertiva la trasformazione ed esponeva direttamente la flag in chiaro.</p> |
| 737 | +<p>La challenge quindi poteva essere risolta in pochi secondi senza comprendere affatto il funzionamento della macchina virtuale, semplicemente ri-cifrando il ciphertext fornito. Il risultato inatteso ma valido è stato il recupero della flag:</p> |
729 | 738 | <h3 id="pixel-gate">Pixel Gate<a hidden class="anchor" aria-hidden="true" href="#pixel-gate">#</a></h3> |
730 | 739 | <p>Questa challenge includeva un eseguibile Go <code>challenge</code> strip-pato e uno script helper <code>gen.py</code>. Il binario si aspetta un PNG con un formato molto specifico e stampa i contenuti solo se tutte le validazioni interne vanno a buon fine. Facendo reverse del build Go RISC-V64 troviamo un parser PNG volutamente minimale e fatto a mano, i cui vincoli sono rispecchiati esattamente dallo script generatore.</p> |
731 | 740 | <h4 id="lalgoritmo">L’algoritmo<a hidden class="anchor" aria-hidden="true" href="#lalgoritmo">#</a></h4> |
@@ -763,7 +772,7 @@ <h5 id="generatore-png-in-python">Generatore PNG in Python<a hidden class="ancho |
763 | 772 | </span></span><span class="line"><span class="cl"> <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"pass.png"</span><span class="p">,</span> <span class="s2">"wb"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span> |
764 | 773 | </span></span><span class="line"><span class="cl"> <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">data</span><span class="p">)</span> |
765 | 774 | </span></span><span class="line"><span class="cl"> <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Wrote pass.png (</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">)</span><span class="si">}</span><span class="s2"> bytes)"</span><span class="p">)</span> |
766 | | -</span></span></code></pre></div><h4 id="note-finali">Note finali<a hidden class="anchor" aria-hidden="true" href="#note-finali">#</a></h4> |
| 775 | +</span></span></code></pre></div><h4 id="note-finali-1">Note finali<a hidden class="anchor" aria-hidden="true" href="#note-finali-1">#</a></h4> |
767 | 776 | <p>Le stringhe che sembrano esadecimali non vengono mai interpretate—il confronto avviene sui byte ASCII letterali. Consentire il color type 6 qui non aggiunge valore, quindi il type 2 evita complicazioni su palette e alpha. Poiché non avviene decompressione, un <code>IDAT</code> vuoto basta. Qualunque deviazione—lunghezze errate, costante alterata, chunk mancante o CRC non corrispondente—provoca l’aborto prima dell’output. Il risultato è un PNG volutamente minuscolo e deterministico, usato come “token” di accesso.</p> |
768 | 777 | <h2 id="cryptography-">Cryptography 🔑<a hidden class="anchor" aria-hidden="true" href="#cryptography-">#</a></h2> |
769 | 778 | <h3 id="repeated-rsa">Repeated RSA<a hidden class="anchor" aria-hidden="true" href="#repeated-rsa">#</a></h3> |
|
0 commit comments