Added writeups
This commit is contained in:
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.
|
||||
|
||||
Reference in New Issue
Block a user