Reverse Engineering FTL: Faster Than Light

My most recent reverse engineering project has been to learn more about the internals of the indie strategy game FTL: Faster Than Light.

Imgur

FTL: Faster THan Light is a 2012 rouge-like strategy game in a science fiction setting. You command a crew of a ship tasked with outrunning a rebel military fleet and delivering a warning message to headquarters on the other side of the galaxy. Your movement between the stars is facilitated by the FTL drive, a science fiction device that allows faster than light travel.

The game plays like an RPG and a management sim with a combination of combat, decision making, and resource management. It’s an inexpensive indie title that is worth giving a try if you’re into strategy games.

It’s also brutally hard. It might be one of the hardest games I have ever played. This is why I wanted to do some RE on the game to try to cheat a little and finally see the ending.

The game has four resources: scrap metal, fuel, missiles, and drones. Scrap metal is the currency in the game. You get this from winning space skimishes, exploring abandoned space stations, and making deals with space pirates and shopkeepers. Scrap metal lets you buy new weapons, upgrade to your ship, and hire new crew members. Fuel powers your FTL engine and lets you make the jumps. Running out of fuel is generally a pretty bad situation since you’ll be stuck as the enemy fleet approaches. Missiles are ammo for weapons systems that ignore shilds and have potenial to do more damange than conventional lasers. Drones are an interesting game mechanic that can automatically fight alongside of you, perform repair duties, etc.

If you want to get right to the cheat engine I built, look at the project on GitHub: https://github.com/austinkeeley/ftl-cheat. It lets you max out your resources (see the screenshot) and it makes the game slightly more winnable.

Imgur

Initial Notes

I’m using the Linux version purchased from Steam. Steam drops the game in the steam/steam/steamapps/common/FTL Faster Than Light/ directory. In this directory, you see the game launcher bash script FTL which just calls another script, data/FTL. This script checks your computer’s architecture and either launches the FTL.x86 or FTL.amd64 version.

There are no other shared objects and ldd just tells me that nothing else is dynamically linked in other than system libraries so we can assume that all the game’s code is in these binaries.

There’s also file called ftl.dat which I’m going to assume contains all the game’s data (graphics, music, story text and diaglogue, etc.). I’ll make a mental note to look for the code that loads this when the game initializes.

Digging into the Binary

I’m going to use the FTL.amd64 version. The first thing to notice is that the binary drumps a bunch of text to the terminal if you run it directly instead of through Steam.

[austin@localhost]$ data ./FTL.amd64
lib/SIL/src/sysdep/posix/time.c:82(sys_time_init): Using CLOCK_MONOTONIC as time source
Version: 1.6.9
Loading settings
Initializing Crash Catcher...
Starting up
Loading text
Initializing Video
Video: 1280x720, windowed
lib/SIL/src/sysdep/opengl/graphics.c:420(opengl_init): OpenGL version: 4.6.0 NVIDIA 390.116
lib/SIL/src/sysdep/opengl/graphics.c:421(opengl_init): GLSL version: 4.60 NVIDIA
lib/SIL/src/sysdep/opengl/graphics.c:422(opengl_init): OpenGL vendor: NVIDIA Corporation
###
### lots of OpenGL messages here
###
Video Initialized
Renderer: OpenGL version 4.6 (GL_VERSION: 4.6.0 NVIDIA 390.116)
Creating FBO...
Starting audio library...
lib/SIL/src/sysdep/linux/sound.c:168(sys_sound_init): Audio output rate: 48000 Hz, buffer size: 1024, period: 256
lib/SIL/src/sysdep/posix/thread.c:187(sys_thread_create): 0x6D1B70((null)): Requested priority 10 (actual -10) too high, using 0 (0)
Audio Initialized!
Resource Preload: 2.418
Initializing animations...
Animations Initialized!
Loading Ship Blueprints....
Blueprints Loaded!
Initializing Sound Data....
Generating world...
Loading achievements...
Loading score file...
Running Game!

That should give us a good idea of where to go after the entry point. There are a lot of strings in the binary that say things like:

/home/achurch/Projects/ftl/hg/lib/SIL/src/sound/core.c

Doing a little research, I discovered that is a debug string left by a software developer Andrew Church who ported the game to the iPad. I’m going to assume he also worked on the Linux port since the game uses an open source library he wrote, the System Interface Library for games (SIL). Having some of the source code is goign to make reversing the rest of the game easier.

As far as those strings dumped to the console, we can see the last one printed in the function at 0x00411ab0. We’ll call this one ftl_init. If we trace the call to this far enough, we can find the main function at 0x0040f390.

If I had to make a guess, I’d assume that there’s a data structure representing the game state (e.g. the crew, my ship’s name and health, the upgrades, etc.) and I can probably dump my memory and find it.

Dynamic Analysis

Let’s start with doing RE to determine how fuel works. This is easy because it decrements by one each time we do a FTL jump in the game.

I used memscan to find where my fuel variable is and then set a gdb watch to break when it changes so I could get a backtrace. Not surprisingly, it’s in the heap.

[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x1dac0bc0 --> 0x801948 --> 0x4d57b0 (push   r15)
RCX: 0x0
RDX: 0x0
RSI: 0x0
RDI: 0x1dac0bc0 --> 0x801948 --> 0x4d57b0 (push   r15)
RBP: 0x1da726d8 --> 0x803a18 --> 0x51bf20 (push   r14)
RSP: 0x7ffde01a8a20 --> 0x1da726d8 --> 0x803a18 --> 0x51bf20 (push   r14)
RIP: 0x4c1d22 (mov    edx,DWORD PTR [rdi+0x700])
R8 : 0x1dec18c0 --> 0x1dff51e0 --> 0x1dfcc150 --> 0x45b0bb0 --> 0x0
R9 : 0x3
R10: 0x3
R11: 0x7
R12: 0x1da70d00 --> 0x1da70760 --> 0x0
R13: 0x1da74728 --> 0x804f00 --> 0x545cd0 (push   r12)
R14: 0x8
R15: 0x6d78 ('xm')
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4c1d14:    mov    QWORD PTR [rsp+0x28],rax
   0x4c1d19:    xor    eax,eax
   0x4c1d1b:    sub    DWORD PTR [rdi+0x700],0x1
=> 0x4c1d22:    mov    edx,DWORD PTR [rdi+0x700]
   0x4c1d28:    mov    BYTE PTR [rdi+0x764],0x1
   0x4c1d2f:    test   edx,edx
   0x4c1d31:    cmovns eax,DWORD PTR [rdi+0x700]
   0x4c1d38:    mov    rdx,QWORD PTR [rdi+0x28]
[------------------------------------stack-------------------------------------]
0000| 0x7ffde01a8a20 --> 0x1da726d8 --> 0x803a18 --> 0x51bf20 (push   r14)
0008| 0x7ffde01a8a28 --> 0x54010c (jmp    0x5400c8)
0016| 0x7ffde01a8a30 --> 0x1da70d00 --> 0x1da70760 --> 0x0
0024| 0x7ffde01a8a38 --> 0x0
0032| 0x7ffde01a8a40 --> 0x1da70d00 --> 0x1da70760 --> 0x0
0040| 0x7ffde01a8a48 --> 0x8e0a27c47f837000
0048| 0x7ffde01a8a50 --> 0x1da70760 --> 0x0
0056| 0x7ffde01a8a58 --> 0x1da726d8 --> 0x803a18 --> 0x51bf20 (push   r14)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Thread 1 "FTL.amd64" hit Hardware watchpoint 1: *0x1dac12c0

Old value = 0x25
New value = 0x24
0x00000000004c1d22 in ?? ()
gdb-peda$ bt
#0  0x00000000004c1d22 in ?? ()
#1  0x000000000055adb7 in ?? ()
#2  0x000000000040fd64 in ?? ()
#3  0x000000000041332e in ?? ()
#4  0x000000000041398e in ?? ()
#5  0x000000000068e681 in ?? ()
#6  0x000000000040f5b0 in ?? ()
#7  0x00007fd28b7d7b6b in __libc_start_main (main=0x40f390, argc=0x1, argv=0x7ffde01ad508, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffde01ad4f8) at ../csu/libc-start.c:308
#8  0x000000000040f9aa in ?? ()

Here we can see the fuel being decremented by one in the instruction sub DWORD PTR [rdi+0x700], 0x1. I repeated the same steps to find functions that change the other resources (scrap metal, missiles, drones).

Function Address Description
ftl_jump 0x4c1d00 Called when you are about to do an FTL jump
ftl_change_scrap 0x4c08a0 Called to change teh amount of scrap (both add and remove)
ftl_shoot_missile 0x66cda0 Called to shoot a missile
ftl_calculate_missiles_to_use 0x43dcf0 Caclulates how many missiles to use when shooting.
ftl_add_drones 0x4cd90 Adds drones to the game state.

Most of those functions use a pointer to a game structure that holds these values. Scrap and fuel are direct members of this structure while missiles and drones are a little trickier to interpret.

Missiles use an indirect reference using a pointer to another data structure and an offset therein. The drone count is tracked in two places. If you currently don’t have a drone system installed, it’s simply an offset in the game data structure. If you do have the drone system installed, it’s an indirect reference using another structure. The game tracks which one to use with another memory address drone_system_installed which is -1 if no system is installed.

Offset Description Size
0x700 fuel_count int
0x760 scrap_count int
0xbf0 drone_count_no_system int
*0x88 + 0x2b8 missile_count int
*0x90 + 0x290 drone_count_with_system int
*0x748 + 0x10 drone_system_installed int

There is a global pointer to the game data structure at 0x8e3500. I found this by just scaning the memory for the address in the heap that I was seeing passed into the functions.

Building the Cheat Engine

Now that I have the data structure figured out and a pointer to it in memory, I can start to figure out a good way to make changes to the running applications. I was originally going to write a C program that used ptrace to attach and modify the structure, but I took a shortcut and wrote a gdb script.

Gdb has a --command flag which lets you pass in a text file that contains gdb commands. It’s not quite a full featured scripting language, but it’s pretty close. Here’s my gdb script for giving me 9999999 scrap and 999 each of fuel, drones, and missiles.

#
# ftl-cheat.gdb
#

# Amount of resources to set
set $scrap    = 9999999
set $fuel     = 999
set $drones   = 999
set $missiles = 999

# Game state address and offsets
set $game_state_addr = 0x8e3500
set $scrap_offset = 0x760
set $drone_offset = 0xbf0
set $fuel_offset = 0x700

set $drone_system_installed_offset_1 = 0x748
set $drone_system_installed_offset_2 = 0x10

set $missile_offset_1 = 0x88
set $missile_offset_2 = 0x2b8

# There are two different locations for drones depending on if a system is installed
set $drone_offset_1 = 0x90
set $drone_offset_2 = 0x290

# Print the game state addr (useful for more debugging)
printf "-------------------------------------------------\n"
printf "Game state address: 0x%08x\n", *$game_state_addr
printf "-------------------------------------------------\n"

# Set resources
set $scrap_addr = *($game_state_addr) + $scrap_offset
printf "Scrap value (0x%08x) is currently:   %d\n", $scrap_addr, *$scrap_addr
set *$scrap_addr = $scrap

set $fuel_addr = *($game_state_addr) + $fuel_offset
printf "Fuel value (0x%08x) is currently:    %d\n", $fuel_addr, *$fuel_addr
set *$fuel_addr = $fuel

set $missiles_addr = *(*$game_state_addr + $missile_offset_1) + $missile_offset_2
printf "Missile value (0x%08x) is currently: %d\n", $missiles_addr, *$missiles_addr
set *$missiles_addr = $missiles

set $drone_system_installed_addr = *(*$game_state_addr + $drone_system_installed_offset_1) + $drone_system_installed_offset_2
if *$drone_system_installed_addr == 0xffffffff
  printf "Drone system NOT installed\n"
  set $drone_addr = *($game_state_addr) + $drone_offset
else
  printf "Drone system installed\n"
  set $drone_addr = *(*($game_state_addr) + $drone_offset_1) + $drone_offset_2
end
printf "Drones value (0x%08x) is currently:  %d\n", $drone_addr, *$drone_addr
set *$drone_addr = $drones

# Quit the debugger
detach
quit

If I combine that with the -p option to attach to a running process using it’s PID, I can attach, run the script that modifies the game data structure, and detach. I wrapped the whole thing in a bash script that makes it even easier.

#!/bin/bash

echo "[*] ftl-cheat"

CURRENT_USER=$(whoami)
if [[ $CURRENT_USER != "root" ]]
then
    echo "[!] Warning: not running as root. May not be able to connect to process"
fi

FTL_PID="$(ps -ef| grep FTL.amd64 | grep -v grep | head -n 1 | awk '{print $2}')"
if [[ $FTL_PID == "" ]]
then
    echo "[!] Could not find PID for FTL.amd64. Is the game running?"
    exit 1
fi

echo "[*] FTL game PID is $FTL_PID"
echo "[*] Attaching to process"
gdb -p $FTL_PID --command=ftl-cheat.gdb -q
echo "[*] Done"

Now all I need to do is start a new game (or continue an existing one) and run

sudo ./ftl-cheat.sh
[*] ftl-cheat
[*] FTL game PID is 16913
[*] Attaching to process
-------------------------------------------------
Game state address: 0x1dee34f0
-------------------------------------------------
Scrap value (0x1dee3c50) is currently:   30
Fuel value (0x1dee3bf0) is currently:    16
Missile value (0x04e48688) is currently: 8
Drone system NOT installed
Drones value (0x1dee40e0) is currently:  2
[Inferior 1 (process 16913) detached]
[*] Done

I removed the lines that gdb dumps on the screen so you can see that it’s attaching, running my script and detaching. After running this, I get far more resources than I can possibly use in a single game. While this should make the game easy, I still had trouble beating the final boss, even with a fully maxed out ship.

FTL: Faster Than Light is a really fun game and I encourage anyone who likes strategy and rogue-like games to purchase it and support the independent developers.