8.0 KiB
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):
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.
// 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):
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):
**(long **)(&DAT_001f4220 + (long)local_34 * 8) = local_30;-> The first 8 bytes of the allocated buffer storelocal_30(our "Element Count").*(code **)(*(long *)(&DAT_001f4220 + (long)local_34 * 8) + 8) = FUN_00109da4;-> The next 8 bytes (offset 8) store a function pointer (FUN_00109da4).- If you analyze the
readandwritefunctions, 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:
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.
- Initialize Slot 0 with our overflow size (
2305843009213693952). It gets a tiny 16-byte chunk. - 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!) |
- Leak the address: Read
Slot 0atEntry Index 3.- As you can see from the table,
Entry 3of Slot 0 points exactly to Slot 1'scleanupfunction pointer. - By reading this, we get the absolute address of
default_destroy(orFUN_00109da4), bypassing ASLR!
- As you can see from the table,
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.
- Write the address: Use the OOB write on
Slot 0(Entry 3) to overwriteSlot 1's cleanup pointer with our calculatedprint_flagaddress. - Trigger the win: Call the
destroycommand 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_destroyaddress. - Calculate:
target = leaked_address - 0x30. - Write Slot 0, Entry 3: Write the
targetaddress. - Destroy Slot 1: Profit!
Happy hacking! Remember, sometimes the biggest structures come from the smallest allocations.