Skip to content

Latest commit

 

History

History
64 lines (46 loc) · 3.8 KB

File metadata and controls

64 lines (46 loc) · 3.8 KB

Description

This writeup documents a save game exploit in Rollercoaster Tycoon. This exploit was found as part of the Districtcon "Junkyard" competition. For more information about the contest, see https://www.districtcon.org/junkyard

The exploit detailed in this writeup allows for arbitrary code execution when a victim opens a crafted sv4 save file. Along with this writeup, the provided archive also contains the crafted map file ("calc.sv4"), and a screencapture of a successful run of the exploit.

Bug

0042556d  0fb67e04           movzx   edi, byte [esi+0x4]
00425571  ff24bd78554200     jmp     dword [edi*4+0x425578]

00425578  uint32_t jump_table_425578[0x3] = 
00425578  {
00425578      [0x0] =  0x00425584
0042557c      [0x1] =  0x0042583e
00425580      [0x2] =  0x00425a1a
00425584  }

The offset is read from user input, and is never masked or bounds checked before using it as an offset into a jumptable. This is an unusual primitive, that you wouldn't normally see in jump tables generated by a compiler. Rollercoaster Tycoon however, is famously hand written in assembly without the use of a C compiler.

So if we provide an unexpectedly large offset into this jump table, we can dereference an unrelated pointer, and then jump to the location that it points to in memory.

The byte is read from offset 0x1079c in the save file, which according to the RCTTechdepot[1] is in this range:

000010..06000F - contains the game map. See the discussion below. The spatial dimensions used in the game are: one tile has 10 square meters of area; the height marks in the game are 4/3 meters (internal units are 1/3 meters).

After further inspection, this code path is part of the rendering flow of a map element with the "ride entry/exit, Park entrance" type. The 5th byte of this element indicates the subtype - so entry, exit, or park entrance - and jumps to the corresponding rendering code using the above jumptable.

Once we replace the subtype of one of these elements, it will trigger the bug as soon as the element is rendered to the screen.

Exploitation

It might seem like we need to be exceptionally lucky to exploit this "wild" jump. But we are helped by the fact that the game is 26 years old, and all memory is executable. So one easy strategy is to find a pointer within range of the jumptable, that points to our save file. From there we can just put some shellcode in our save file (which we fully control).

The save file is run-length encoded and unpacked directly into the data section of the process. After loading it into memory, the save file runs from 0x6e4c60 to 0x8dd16c.

Using some Binary Ninja scripting, we can scan the surrounding memory for a pointer to something in this range:

savegame_start = 0x6e4c60
savegame_end = 0x8dd16c
jumptable = 0x425578
for i in range(0x100):
    target = bv.read_int(jumptable+i*4, 4, sign=False)
    if savegame_start < target < savegame_end:
        print(f"[+] Found candidate for offset 0x{i:02x} --> 0x{target:02x}")

This finds exactly one candidate:

[+] Found candidate for offset 0x75 --> 0x8884fc

This corresponds to offset 0x1a389c in the save file. If we cross-reference this with the reverse engineering efforts from RCTTechdepot[1], we find the following:

1A389C..1A389F: a game time counter - used to trigger various events

We now have everything we need to make a weaponized exploit:

  1. Take a valid save file as a base, and decompress the run-length encoding.
  2. In the decompressed file, patch one or more entry/exit elements to set the subtype to 0x75.
  3. In the decompressed file, place shellcode at offset 0x1a389c.
  4. Compress the save file using RLE and append a valid checksum.

References

  1. "SV4 FILE FORMAT": https://freerct.github.io/RCTTechDepot-Archive/SV4.html