Reverse Engineering FTL: Faster Than Light
29 Oct 2019My most recent reverse engineering project has been to learn more about the internals of the indie strategy game FTL: Faster Than Light.
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.
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.