You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This challenge shipped with a stripped Go binary (`challenge`) and a helper script `gen.py`. The binary expects a very specifically crafted PNG file and prints its contents only if all internal validations succeed. By reversing the RISC-V64 Go build we found a deliberately narrow, hand-rolled PNG parser whose constraints are mirrored exactly by the generator script.
774
+
#### The challenge
775
775
776
-
### Ironevil
776
+
The binary provided in the challenge, named `ironveil`, is an ELF 64-bit PIE executable built for Linux and linked against a NixOS loader. Because the interpreter path in the binary points to a non-standard location, it cannot run directly on a typical system. This is why invoking it from the shell results in the error “cannot execute: required file not found.” In practice, the solution is to manually specify the system’s own loader, usually `/lib64/ld-linux-x86-64.so.2`, in order to run the program.
777
+
778
+
The decompiled code shows that before any encryption takes place, the program spends considerable effort on initialization. It sets up signal handlers, performs poll checks on file descriptors, and interacts with `/dev/null`. It also queries thread attributes such as stack address and size, and aligns them carefully. These routines are typical of binaries hardened against debugging or sandbox analysis. However, once initialization completes, the logic converges on a relatively simple behavior: it expects a single file as input and produces an encrypted output with a `.encrypted` suffix.
779
+
780
+
The encryption routine is based on a custom virtual machine. This VM interprets thirty-two opcodes to derive a keystream of bytes. The keystream is then applied to the input file through a byte-by-byte XOR operation. Every plaintext byte is combined with the corresponding keystream byte, and the result is written to disk. The crucial detail is that the VM is deterministic: the same binary always produces the same keystream. There is no random seed, nonce, or per-file variation. This means the transformation is simply `ciphertext = plaintext ⊕ key`. Applying the transformation twice with the same key cancels it out, because `(P ⊕ K) ⊕ K = P`.
781
+
782
+
#### Solution
783
+
784
+
The challenge gave us only the binary and an already encrypted file named `flag.txt.encrypted`. The intended solution might have been to reverse the VM, study its thirty-two instructions, and regenerate the keystream in order to manually decrypt the ciphertext. However, the determinism of the algorithm offered a much simpler path. By feeding the already encrypted file back into the program, the same keystream was applied again. As a result, the double encryption inverted itself and produced the original plaintext.
777
785
778
-
TODO
786
+
Running the binary through the system loader with the encrypted flag as input created a new file named `flag.txt.encrypted.encrypted`. Opening this file immediately revealed the flag in cleartext at the beginning of the file. The remainder of the file contained garbage, which is consistent with the XOR operation continuing past the flag content into unused or irrelevant data. But the presence of the complete flag string at the start was enough to solve the challenge.
787
+
788
+
#### Final notes
789
+
790
+
The security weakness here is exactly the reuse of a static keystream. In real cryptography, stream ciphers are only secure when each encryption uses a unique nonce or initialization vector, ensuring that the keystream never repeats. Without that safeguard, the cipher degenerates into a vulnerable “many-time pad,” where multiple uses of the same keystream inevitably leak information. In this case, the leakage was so severe that a simple double invocation of the binary inverted the transformation and exposed the plaintext flag directly.
791
+
792
+
The challenge therefore could be solved in seconds without understanding the virtual machine at all, simply by re-encrypting the provided ciphertext. The unintended but valid outcome was the recovery of the flag:
793
+
794
+
### Pixel Gate
795
+
796
+
This challenge shipped with a stripped Go binary (`challenge`) and a helper script `gen.py`. The binary expects a very specifically crafted PNG file and prints its contents only if all internal validations succeed. By reversing the RISC-V64 Go build we found a deliberately narrow, hand-rolled PNG parser whose constraints are mirrored exactly by the generator script.
Questo significa che l’input dell’utente viene passato direttamente a `printf`, permettendoci di controllare la format string e di leakare valori dallo stack o scrivere in memoria arbitraria.
Il binario fornito nella challenge, chiamato `ironveil`, è 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 `/lib64/ld-linux-x86-64.so.2`, per poter eseguire il programma.
636
+
637
+
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 `poll` sui descrittori di file e interagisce con `/dev/null`. 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 `.encrypted`.
638
+
639
+
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 `ciphertext = plaintext ⊕ key`. Applicare la trasformazione due volte con la stessa chiave la annulla, perché `(P ⊕ K) ⊕ K = P`.
640
+
641
+
#### La soluzione
642
+
643
+
La challenge ci metteva a disposizione soltanto il binario e un file già cifrato, `flag.txt.encrypted`. 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.
644
+
645
+
Eseguendo il binario tramite il loader di sistema con il file cifrato come input veniva generato un nuovo file, `flag.txt.encrypted.encrypted`. 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.
646
+
647
+
#### Note finali
648
+
649
+
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.
650
+
651
+
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:
0 commit comments