PicoCTF — Guessing Game 2 Walkthrough | ret2libc, stack cookies

Sohail Saha
7 min readJan 22, 2021

I have recently started solving binary pwn challenges anywhere I can find them, and this one — ‘Guessing Game 2’ by picoCTF really proved to be a challenge, but not because of the challenge itself but rather because of some discrepancy issues.

Prerequisite Knowledge:

Tools Used:

A word of caution:

If you are just beginning to pwn this binary, DO NOT use the provided Makefile to compile your own binary, because there is a discrepancy between the program actually running on the server, and what the makefile generates. Directly use the binary they provide and its source. This discrepancy by itself made me bang my head on the wall for 2 days straight, because there simply were too many branched decision trees I had in my head, whereas it should’ve only taken a day at most to solve it.

Foreword:

All the script snippets you see below (except for the guessing script) would give you different answers every time you run them, so make sure to put everything in one script and progressively go on writing and expanding the script, because each value would somehow depend on the previously derived value.

For example, the stack cookie you found must be used in the same instance to leak the puts() address, else, it would be invalidated.

Also, you’ll find the complete exploit script at the end of this post.

Reading through the logic:

The program simply asks you to guess a number, and if your guess is correct, it asks you for your name, and echoes it back, and then it goes back to the beginning (in an endless loop).

The actual vulnerability is a BOF vulnerability in the naive gets() that asks you for your name, however, you can exploit it only after you have guessed the number correctly. If you look carefully at the source code, you will see that instead of using the value of the rand() function, it uses the value of the address of the function itself (I maybe wrong here, or maybe not). Since this address doesn’t change as long as the program isn’t restarted on the server, you can simply bruteforce all values and get the correct guess, and it would be the correct guess every time you’re asked to guess. Also note the potential format string vulnerability in the win() function, where you can directly input format strings to the printf() function and see it work because there are no value parameters passed to it.

Let’s start poking around…

Guessing the number:

As I already said, this number remains constant as long as the program is running, and since this is a forked program, you can reuse this number over and over as long as the guys there at picoCTF doesn’t restart the server. (In which case, bruteforce again :P )

Carefully examine this snippet of the source code:

long get_random() {
return rand;
}
int do_stuff() {
long ans = (get_random() % 4096) + 1;
...

Since rand refers to the address of it and this value is divided by a hard-coded value of 4096 and then incremented, this value can only have a range of [-4095,4096]. So if you bruteforce with this range, one of them would be correct.

Since this is a huge range, you can write a simple script that uses threads and finds out the value. Here is the script I wrote for this:

I found the number to be -31. It may differ by the time you attempt it. So use a script to find it out. Since you need to debug the program, you also need to bruteforce on the local binary too. Since this is local, you don’t need to use a script that uses threading, because the bruteforcing process would be blazing fast. I wrote this script for this:

My local guess value was -1103. Yours would be different.

Leaking the stack cookie:

Time to find a way to get the stack cookie. I threw the binary to radare2, disassembled the win() function to see where the cookie is stored. Observe this snippet carefully.

...
; var int32_t var_ch @ ebp-0xc
...
0x08048783 65a114000000 mov eax, dword gs:[0x14]
0x08048789 8945f4 mov dword [var_ch], eax
0x0804878c 31c0 xor eax, eax
...
0x080487e9 8b45f4 mov eax, dword [var_ch]
0x080487ec 653305140000. xor eax, dword gs:[0x14]
┌─< 0x080487f3 7405 je 0x80487fa
│ 0x080487f5 e816010000 call sym.__stack_chk_fail_local
└─> 0x080487fa 8b5dfc mov ebx, dword [var_4h]
0x080487fd c9 leave
0x080487fe c3 ret

Notice that a local variable is initialized at ebp-0xc (name var_ch), and then the stack cookie is stored at that location (on the stack), and at the end of the function, that value is compared (by XOR-ing) with the original cookie to see if they’re same. If they’re same, only then you can get an RCE, else the stack protection will kick in and close the connection to you. So, the stack cookie is at ebp-0xc. Now, we gotta leak it somehow.

Observe this function’s source…

void win() {
char winner[BUFSIZE];
printf("New winner!\nName? ");
gets(winner);
printf("Congrats: ");
printf(winner);
printf("\n\n");
}

See the printf(winner)? Since you control the argument, and there’s no data parameters passed to it, you can use this as a format string exploit. Next, I used gdb (pwndbg) to observe the stack when win() is running.

00:0000│ esp  0xffffcf80 ◂— 0x1
...
83:020c│ 0xffffd18c ◂— 0xc552c600 // THE COOKIE
...
86:0218│ ebp 0xffffd198 —▸ 0xffffd1b8 ◂— 0x0
...

See the cookie? How do I know that it is indeed the cookie? That’s because the cookie is supposed to be 12 bytes (0xc) above ebp, isn’t it? Pwndbg says that the cookie is at line number 0x83, which is hex for 131. So, we can now use this to pass to the vulnerable printf(winner) and get the cookie.

For some reason, I couldn’t find the cookie exactly at 131st position, so I played around a bit and found the position to be 135th. This would get you your cookie:

/* After guessing the correct answer, use '%135$p' as your name */
...
New winner!
Name: %135$p
Congrats: 0xc552c600

The cookie will vary every time you run the binary, remember this. However, its position on the stack would always be constant.

Getting the EIP offset:

I will use pwndbg again to send in a De-Bruijn (pattern) string in when asked for name and get the offsets. I assume that you know how to get this offset, so I am not explaining this step here. The offset you’d find is 528.

Making the BOF payload:

Since there’s a stack cookie sitting at ebp-0xc and EIP is at +528, the payload structure for triggering the BOF would be:

512 bytes junk + stack cookie (ebp-0xc) + 12 (0xc) bytes junk + function address (eip) + return address + function parameter/s (if any)

Take some time to understand the payload. All we are doing is overflowing the buffer while making sure to overwrite the cookie’s value where it already is (to prevent the stack protector from kicking in) so that [ebp-0xc] remains the same on stack, then jumping to any function we want by controlling the saved EIP address.

Leaking the address of puts():

Using the aforementioned payload structure, the vulnerable gets(winner) can now be used to leak the address of puts() . The payload would thus be:

512 bytes junk + stack cookie (ebp-0xc) + 12 (0xc) bytes junk + plt entry of puts() + address of win() + got entry of puts()

Note that I used the address of win() as the return address, because I wanted to use this same BOF vulnerability again to trigger system("/bin/sh") , so by returning back to win() itself, we approach the vulnerable gets(winner) again.

Getting the correct libc version:

Now that we have the address of puts() , we can use a libc database to find out which version of libc can have puts() at that address. I used the site https://libc.nullbyte.cat/.

Clicking on the first result gives me the offset of system() and "/bin/sh" relative to puts() for that version of libc . AWESOME!

Popping the shell:

With the previously found address of puts() , add these offsets, to find the addresses to the "/bin/sh" string and system() function in the libc. You’d have a payload structure like this:

512 bytes junk + stack cookie (ebp-0xc) + 12 (0xc) bytes junk + address of system() + any return address + address of "/bin/sh"

Send the payload, pop a shell, and go cat out that flag!

The complete exploit:

Here’s the complete exploit script (without the bruteforcing/guessing part) I wrote to pop a shell on the server:

Conclusion:

The takeaways from this challenge are, firstly, how to find the stack cookie through a format string exploit and overwrite the eip, and secondly, how to leak a libc function’s address, find the correct libc version used, and use it to send in the final shell-popping payload.

--

--

Sohail Saha

🚀 Frontend developer @Polygon 👨‍💻 Creator at #DevvingItWithSohail 🎮 Gamer