216 lines
7.5 KiB
Markdown
216 lines
7.5 KiB
Markdown
# The Gatekeeper
|
|
|
|
`gatekeeper` ist eine Reverse-Engineering-Challenge, die eine software-simulierte Hardwareschaltung beinhaltet. Uns wird eine Binärdatei bereitgestellt und wir müssen die Eingabe finden, die den Stromkreis "vervollständigt" und die LED einschaltet.
|
|
|
|
## Informationsbeschaffung
|
|
|
|
Zuerst analysieren wir die Binärdatei:
|
|
|
|
```bash
|
|
$ file gatekeeper
|
|
gatekeeper: ELF 64-bit LSB pie executable, x86-64, ... stripped
|
|
```
|
|
|
|
Es ist eine gestrippte, statisch gelinkte 64-Bit-ELF-ausführbare Datei. Wenn sie ausgeführt wird, fragt sie nach einer Flagge.
|
|
|
|
```bash
|
|
$ ./gatekeeper
|
|
--- THE GATEKEEPER ---
|
|
Enter the flag that lights up the LED: AAAA
|
|
LED is OFF
|
|
```
|
|
|
|
## Reverse Engineering
|
|
|
|
### 1. Analyse von Main (`FUN_00108860`)
|
|
|
|
Wir öffnen die Binärdatei in Ghidra und lokalisieren die `main`-Funktion bei `0x00108860`.
|
|
|
|
```c
|
|
undefined8 main(void)
|
|
{
|
|
// ... Stack-Setup ...
|
|
|
|
FUN_00114970("--- THE GATEKEEPER ---");
|
|
do {
|
|
FUN_00153610(1, "Enter the flag that lights up the LED: ");
|
|
|
|
// Benutzereingabe lesen
|
|
lVar1 = FUN_00114410(local_1e8, 0x80, PTR_DAT_001d4d78);
|
|
if (lVar1 == 0) break;
|
|
|
|
// Längenprüfung
|
|
lVar1 = thunk_FUN_001246c0(local_1e8);
|
|
if (lVar1 == 36) {
|
|
|
|
// ... (Komplexe Logik zur Erweiterung von 36 Zeichen in 288 Bits) ...
|
|
|
|
// Löschen eines großen Arrays bei 0x1d6940 (Cache/Memoization)
|
|
puVar6 = &DAT_001d6940;
|
|
for (lVar1 = 0x1ba; lVar1 != 0; lVar1 = lVar1 + -1) {
|
|
*puVar6 = 0;
|
|
puVar6 = puVar6 + 1;
|
|
}
|
|
|
|
// Aufruf der Verifizierungsfunktion
|
|
// Sie nimmt 0x374 (884) als erstes Argument und das Bit-Array als zweites
|
|
iVar2 = FUN_001090d0(0x374, &local_168);
|
|
|
|
if (iVar2 == 1) {
|
|
FUN_00114970("LED is ON");
|
|
return 0;
|
|
}
|
|
}
|
|
FUN_00114970("LED is OFF");
|
|
} while( true );
|
|
}
|
|
```
|
|
|
|
Aus `main` lernen wir:
|
|
1. Die Flagge muss genau **36 Zeichen** lang sein.
|
|
2. Die Eingabe wird in ein Array von Bits umgewandelt.
|
|
3. Eine Verifizierungsfunktion `FUN_001090d0` wird aufgerufen, beginnend mit dem Index **884**.
|
|
|
|
### 2. Identifizierung der Gatterlogik (`FUN_001090d0`)
|
|
|
|
Die Funktion `FUN_001090d0` bestimmt, ob unsere Eingabe korrekt ist. Sie fungiert als rekursiver Auswerter für eine Logikschaltung.
|
|
|
|
Sie akzeptiert einen `gate_index` als Argument. Sie verwendet diesen Index, um eine Gatterstruktur aus einem globalen Array bei `0x001d1020` nachzuschlagen. Jede Gatterstruktur enthält einen Opcode und Indizes für andere Gatter (Eingänge).
|
|
|
|
**Der rekursive Prozess:**
|
|
Wenn die Funktion ein Gatter auswertet (z.B. ein UND-Gatter), kann sie das Ergebnis nicht sofort wissen. Stattdessen muss sie zuerst den Zustand der Eingänge bestimmen, die in dieses Gatter einspeisen.
|
|
1. Sie ruft sich selbst (`FUN_001090d0`) mit dem Index des **linken Kindes** auf.
|
|
2. Sie ruft sich selbst mit dem Index des **rechten Kindes** auf.
|
|
3. Sie führt die Logikoperation (UND/ODER/XOR) auf diesen beiden Ergebnissen aus und gibt den Wert zurück.
|
|
|
|
Diese Rekursion setzt sich tief in den Schaltungsbaum fort, bis sie auf einen "Basisfall" trifft: ein **INPUT**-Gatter (Fall 0). Das INPUT-Gatter liest einfach ein Bit aus unserer Flagge und gibt es zurück, wodurch die Rekursion für diesen Zweig gestoppt wird. Die Werte wandern dann den Baum wieder hinauf zur Wurzel.
|
|
|
|
Durch die Analyse der `switch`-Anweisung im Inneren können wir die spezifischen Operationen identifizieren:
|
|
|
|
#### Fall 1: UND-Gatter
|
|
Diese Logik repräsentiert eine UND-Operation. Beachten Sie die Rekursion: Es wertet zuerst das linke Kind aus. Wenn das 0 zurückgibt, wird kurzgeschlossen und 0 zurückgegeben. Andernfalls wird das rechte Kind ausgewertet.
|
|
```c
|
|
case 1:
|
|
// Rekursiver Aufruf für linkes Kind
|
|
if (FUN_001090d0(left_idx) == 0) {
|
|
result = 0;
|
|
} else {
|
|
// Rekursiver Aufruf für rechtes Kind
|
|
result = FUN_001090d0(right_idx);
|
|
}
|
|
return result;
|
|
```
|
|
|
|
#### Fall 2: ODER-Gatter
|
|
Ähnlich wie UND, gibt aber 1 zurück, wenn das linke Kind 1 ist.
|
|
```c
|
|
case 2:
|
|
if (FUN_001090d0(left_idx) == 1) {
|
|
result = 1;
|
|
} else {
|
|
result = FUN_001090d0(right_idx);
|
|
}
|
|
return result;
|
|
```
|
|
|
|
#### Fall 3: XOR-Gatter
|
|
Dies verwendet explizit den XOR-Operator auf den Ergebnissen der beiden rekursiven Aufrufe.
|
|
```c
|
|
case 3:
|
|
result = FUN_001090d0(left_idx) ^ FUN_001090d0(right_idx);
|
|
return result;
|
|
```
|
|
|
|
#### Fall 4: NICHT-Gatter
|
|
Dieses Gatter hat nur einen Eingang (linkes Kind). Es ruft die Funktion rekursiv auf und invertiert das Ergebnis.
|
|
```c
|
|
case 4:
|
|
result = !FUN_001090d0(left_idx);
|
|
return result;
|
|
```
|
|
|
|
#### Fall 0: INPUT-Gatter
|
|
Dies ist der Basisfall der Rekursion. Es ruft ein rohes Bit aus dem Eingabe-Array des Benutzers ab.
|
|
```c
|
|
case 0:
|
|
return input_bits[gate->bit_index];
|
|
```
|
|
|
|
**Schlussfolgerung:**
|
|
Die Binärdatei ist ein **Logikgatter-Simulator**. Der Verifizierungsmechanismus ist eine große Schaltung (885 Gatter), die im `.data`-Abschnitt gespeichert ist. Wir müssen die Eingabebits finden, die dazu führen, dass das finale "Wurzel"-Gatter (884) eine logische `1` ausgibt.
|
|
|
|
## Lösung
|
|
|
|
Wir können dies lösen, indem wir das Gatter-Array extrahieren und den **Z3 Theorem Prover** verwenden. Z3 ermöglicht es uns, die gesamte Schaltung als eine Menge von Bedingungen zu modellieren und die Eingabe zu finden, die diese erfüllt.
|
|
|
|
### Solver-Skript
|
|
|
|
1. **Gatter extrahieren**: Jedes Gatter bei `0x1d1020` besteht aus 4 Integern: `[Opcode, Left_Index, Right_Index, Bit_Index]`.
|
|
2. **Variablen definieren**: Erstelle 288 boolesche Variablen für die Flaggenbits.
|
|
3. **Logik modellieren**: Definiere rekursiv die Ausgabe jedes Gatters in Bezug auf Z3-Operatoren (`z3.And`, `z3.Or`, `z3.Xor`, `z3.Not`).
|
|
4. **Lösen**: Sage Z3, dass Gatter 884 `Wahr` sein muss.
|
|
|
|
```python
|
|
import struct
|
|
import z3
|
|
|
|
FILENAME = "gatekeeper"
|
|
OFFSET = 0xd0020 # Datei-Offset für globales Array bei 0x1d1020
|
|
GATE_COUNT = 885
|
|
FLAG_LEN = 36
|
|
|
|
# In Ghidra identifizierte Opcodes
|
|
OP_INPUT, OP_AND, OP_OR, OP_XOR, OP_NOT = range(5)
|
|
|
|
class Gate:
|
|
def __init__(self, op, left, right, val):
|
|
self.op, self.left, self.right, self.val = op, left, right, val
|
|
|
|
def solve():
|
|
# 1. Schaltung aus Binärdatei laden
|
|
gates = {}
|
|
with open(FILENAME, "rb") as f:
|
|
f.seek(OFFSET)
|
|
for i in range(GATE_COUNT):
|
|
data = f.read(16)
|
|
op, left, right, val = struct.unpack("<iiii", data)
|
|
gates[i] = Gate(op, left, right, val)
|
|
|
|
# 2. Solver einrichten
|
|
s = z3.Solver()
|
|
input_bits = [z3.Bool(f'bit_{i}') for i in range(FLAG_LEN * 8)]
|
|
gate_vars = {}
|
|
|
|
def get_var(idx):
|
|
if idx in gate_vars: return gate_vars[idx]
|
|
g = gates[idx]
|
|
if g.op == OP_INPUT: res = input_bits[g.val]
|
|
elif g.op == OP_AND: res = z3.And(get_var(g.left), get_var(g.right))
|
|
elif g.op == OP_OR: res = z3.Or(get_var(g.left), get_var(g.right))
|
|
elif g.op == OP_XOR: res = z3.Xor(get_var(g.left), get_var(g.right))
|
|
elif g.op == OP_NOT: res = z3.Not(get_var(g.left))
|
|
gate_vars[idx] = res
|
|
return res
|
|
|
|
# 3. Behaupten, dass Wurzelgatter 1 ist
|
|
s.add(get_var(GATE_COUNT - 1) == True)
|
|
|
|
# 4. Ergebnis extrahieren
|
|
if s.check() == z3.sat:
|
|
m = s.model()
|
|
bits = [1 if m.evaluate(input_bits[i]) else 0 for i in range(FLAG_LEN * 8)]
|
|
flag = ""
|
|
for i in range(FLAG_LEN):
|
|
char_val = 0
|
|
for b in range(8):
|
|
if bits[i*8 + (7-b)] == 1: char_val |= (1 << b)
|
|
flag += chr(char_val)
|
|
print(f"Flag: {flag}")
|
|
|
|
solve()
|
|
```
|
|
|
|
### Ergebnis
|
|
Das Ausführen des Skripts liefert die Flagge:
|
|
`{flag: S0ftW4r3_d3F1n3d_l0g1c_G4t3s}`
|