Skip to content

Commit cc8524f

Browse files
committed
add last part of the ctf@ac writeups
1 parent 1a3b088 commit cc8524f

File tree

4 files changed

+44
-7
lines changed

4 files changed

+44
-7
lines changed

content/en/ctf/ctfatac.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ p.interactive()
387387
The vulnerability is easy to spot if you are familiar with format string bugs.
388388
In the decompiled code (see image below), the program calls `printf()` **without** specifying a format string, like `%s`:
389389

390-
![Decompiled code showing vulnerable printf](image-1.png)
390+
![Decompiled code showing vulnerable printf](/images/fini.png)
391391

392392
This means user input is passed directly to `printf`, allowing us to control the format string and leak stack values or write to arbitrary memory.
393393

@@ -769,13 +769,31 @@ With `k1, k2` fixed, decoding and assembling `a + permuted_rest + suffix` yields
769769
CTF{2944cec0c0f401a5fa538933a2f6210c279fbfc8548ca8ab912b493d03d2f5bf}
770770
```
771771
772-
### Pixel Gate
772+
### Ironevil
773773
774-
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
775775
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.
777785
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.
779797
780798
#### The algorithm
781799

content/it/ctf/ctfatac.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ p.interactive()
403403
La vulnerabilità è evidente se conosci i format string bug.
404404
Nel codice decompilato (vedi immagine), il programma chiama `printf()` **senza** specificare una format string, tipo `%s`:
405405

406-
![Decompiled code showing vulnerable printf](image-1.png)
406+
![Decompiled code showing vulnerable printf](/images/fini.png)
407407

408408
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.
409409

@@ -630,7 +630,25 @@ CTF{2944cec0c0f401a5fa538933a2f6210c279fbfc8548ca8ab912b493d03d2f5bf}
630630
631631
### Ironevil
632632
633-
TODO
633+
#### La challenge
634+
635+
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:
634652
635653
### Pixel Gate
636654

hugo.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ params:
7878
defaultTheme: auto # dark, light
7979
disableThemeToggle: false
8080

81+
math: true
8182
ShowReadingTime: true
8283
ShowShareButtons: true
8384
ShowPostNavLinks: true

static/images/fini.png

5.78 KB
Loading

0 commit comments

Comments
 (0)