Files
HIP7CTF_Writeups/slot_machine.md
m0rph3us1987 a79656b647 Added writeups
2026-03-08 12:22:39 +01:00

199 lines
8.0 KiB
Markdown

# 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.