Quantico CTF 2019

I did a CTF today a the Quantico Cyber Hub in Stafford, VA. I work with employees of one of the sponsors, GRIMM, and they got me an invite where they hosted this thing at their new offices outside the Washington beltway.

Even though I got there a few hours late, I tagged up with coworker Alex and we formed team flavortown and managed to get 3rd place. Coincidently, we both wore the same tshirt. It was one we both had from a conference. As far as I know, we were the only team with matching shirts.

The event featured GRIMM’s “Howdy Neighbor” CTF among others. I’ve seen this at ShmooCon but I’ve never actually attempted it. It’s a well made and fun CTF featuring IoT devices and a model house. There are enough easy challenges that beginners can jump right in. There are also plenty of hints in the documentation to point you in the right direction. If it arrives at a conference near you, I hightly recommend spending some time going for some flags and chatting up the people at the GRIMM both.

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.

Side Project: Entrance Song

I’ve been working on a side project inspired by a professional wrestling and a blog post I read about a year ago.

I can’t remember the blog’s name, but it described how the local system administrator setup their Sonos speakers to play a theme song whenever an employee entered the office. They used DHCP to detect when an employee’s mobile phone connected to the corporate WiFi and then matched the MAC address to an employee and their chosen entrance song.

The whole idea was pretty amazing and it made me think of my favorite thing about professional wrestling: getting a chance to pick an entrance song for when you enter the ring. It’s something I give a lot of thought to.

This is essentially how I wanted to feel walking in my front door every day after work and also let my friends set their own entrance theme songs.

The implementation used in the blog used tcpdump and some bash scripts, but I wanted to write a version in Python.

Here’s the link to the GitHub repo.

Sniffing the DHCP Transactions

Detecting when someone enters my house is done using DHCP, which is a weird protocol. It’s based on UDP (and implicitly, IP) but it’s used for getting an IP address lease so the device likely doesn’t have an IP address. The solution for this is to just broadcast the DHCP request and response to everyone on the network. The end result is a very noisy transaction, but that makes it easy to sniff.

  • A client sends a DHCPDISCOVER packet broadcast to 255.255.255.255
  • A nearby DHCP server detects and responds with a DHCPOFFER to 255.255.255.255 containing an IP address
  • The client device then sends back a DHCPREQUEST packet (again, to 255.255.255.255) requesting this offered IP address.
  • Finally, the DHCP server sends an acknowledgement DHCPACK message, this time just as a unicast packet.

The whole thing is like walking into a hotel lobby and shouting at everyone present until the conceirge hears you and gives you a room key.

For our entrance song to detect a device, we just need to listen for that DHCPREQUEST packet, since it’s being broadcast everywhere.

I’m using Scapy, a very awesome Python library, to sniff my network for DHCP traffic. Scapy is pretty well known for doing cyber security stuff like crafting packets and building sniffers. It’s very easy to write a basic sniffer with a callback.

from scapy.all import Ether, DHCP, sniff

sniff(prn=self.dhcp_monitor_callback, filter='udp and (port 67 or 68)', store=0)

def dhcp_monitor_callback(pkt):
    """Callback for DHCP requests"""
    if not pkt.haslayer(DHCP):
        return
    if pkt[DHCP].options[0][1] != 3:
        return

    mac_addr = pkt[Ether].src
    logging.info('DHCP request from %s', mac_addr)

Here, we’re sniffing for UDP on port 67 and 68 and passing the packet to a callback function. In the callback, we’re making sure it’s actually a DHCP packet and that it’s a DHCPREQUEST packet (the options are a list of tuples of DHCP key-value options. The first tuple is the type of DHCP message, here we’re using type 3 for DHCP requests).

After verifying that we have a DHCP request, we can easily pull the MAC address out of the Ethernet layer. We’ll use this later to compare to my friend’s names in a SQLite database and figure out what song to play.

device = data.get_device_by_mac_addr(mac_addr)
if not device:
    logging.info('This is not a device I know about... Adding it to the database')
    data.insert_device(mac_addr)
    return

if device.owner.song:
    song = device.owner.song
    logging.info('%s is about to enter (%s)! playing %s by %s', \
        device.owner.name,
        device.friendly_name,
        song.title,
        song.artist)
else:
    logging.info('Device owner %s does not have a song. Doing nothing.', device.owner.name)
    return

Meanwhile the SQLAlchemy models are pretty standard classes that represent a Device, an Owner, and a Song and the foreign keys between them. The data.get_device_by_mac_addr and data.insert_device are just shortcut functions for querying and inserting data into my database.

Playing Music

Once we’ve determined who’s entered my front door, we need to play their theme song. I have a Spotify premium account, so I can literally play anything. Spotify provides a pretty good web-based REST API and there’s a Python wrapper for it called Spotipy.

Spotipy hasn’t been updated in a while and the original author doesn’t seem to be merging pull requests. There are a few issues with the library so I forked it and patched them up myself.

Authenticating my premium Spotify account using Spotipy is a little weird. You have to register for their developer program and create a new project. After that, you need to allow the project to access your Spotify account with the appropriate level of access.

For playing the music, we want to do the following:

  1. Look up who entered my front door and see if they have an entrance song.
  2. If so, save the current Spotify playback so we can resume it later.
  3. Fade out the current song.
  4. Set the volume high and blast that entrance song for the next 45 seconds or so
  5. Fade out the entrance song
  6. Fade in the previously played music prior to the entrance and return everything back to normal

Sounds easy enough. There are a few interesting problems to solve though. Spotify doesn’t have a way to play just a section of music and then stop. We can fake this out by starting the music in a new thread, sleeping in that thread for the entrance duration (usually about 45 seconds to a minute) and then stop the music right before ending the thread.

class MusicThread(Thread):
    """A thread to start music and sleep. This is a cheap way to implement playing
    a duration of a song since the Spotify API doesn't include that.
    """
    def __init__(self, sp_context, mp_context, uri, position_ms, duration=45,
                 device_id=None):
        """Constructor
        Args
            sp_context - The spotify context
            mp_context - music player context
            uri - The URI to play
            position_ms - The position in ms to start from
            duration - The duration to play, in seconds
            device_id - The device to play on, or None to play on the default device
        """
        super().__init__()

        self.sp = sp_context
        self.mp = mp_context
        self.uri = uri
        self.position_ms = position_ms
        self.duration = duration
        self.device_id = device_id

    def run(self):
        """Runs the thread.
        Starts playback on the track, sleeps, and then fades out the track.
        """
        logging.info('Starting playback in new thread')
        try:
            self.sp.pause_playback()
        except SpotifyException as e:
            # This often happens if there is no current active device. We'll assume there's
            # device_id being used. The next try/catch block will handle it if not.
            pass
        try:
            self.sp.start_playback(device_id=self.device_id, uris=[self.uri],
                                   position_ms=self.position_ms)
        except SpotifyException as e:
            logging.error(e)
            return
        self.mp.set_volume(self.mp.default_volume)

        logging.info('Putting the thread to sleep for %s seconds', self.duration)
        sleep(self.duration)
        logging.info('Stopping playback')

        # Get the currently playing tack to be sure we're stopping this track and not
        # someone else's.
        current_track = self.sp.currently_playing()
        try:
            uri = current_track['item']['uri']
            if uri == self.uri:
                self.mp.fade_out()
                self.sp.pause_playback()
            else:
                logging.info('Attempted to stop song %s but it\'s not playing', self.uri)
        finally:
            return

Another problem is figuring out how to handle if someone shows up while another person’s entrance song is already playing. Should the previous person’s music be cut off? That’s disrespectful. Also the threads might start fighting over the music controls and we’d get into a weird state.

I thought about writing some state machine code to keep the threads in sync but there’s a much more elegant solution: using Python’s Queue class. These things deserve way more credit than they get. They allow you to have a thread safe way to manipulate a collection. In my case I want to add things without blocking (i.e. someone walks in the door so I need to queue up their song) while removing things with blocking (i.e. making Spotify play the song but block until the song is complete before starting the next one). No awful state machine required!

Here’s the main loop for the music player:

def player_main(self):
    logging.info('Starting music player')
    while True:
        uri, start_minute, start_second, duration = self.song_queue.get(True)
        logging.info('Found a song on the queue!')
        logging.info('Playing %s at %d:%d duration %d', uri, start_minute, start_second,
                     duration)
        self.save_current_playback()
        t = self._play_song(uri, start_minute, start_second, duration)
        logging.info('Waiting for song to end...')
        t.join()
        self.restore_playback()
        logging.info('Song over... waiting for the next song on the queue')

The get method will block the thread until it sees something on the queue. The join method on the MusicThread will also block the main thread until the song completes. The MusicPlayer itself is a Thread so the whole thing doesn’t block the rest of my application while calling these methods.

My Scapy DHCP sniffing code just needs to drop songs on the queue and they will get picked up by the music player thread.

Results and Conclusion

It’s honestly pretty awesome to walk into your own house and have an entrance song play. I now get pumped up as I enter my door and start waving my hands to an imaginary crowd. I’m not sure what my neighbors think of this.

Other Things

  • I use some NetGear wifi extenders in my house. It turns out, they change a device’s MAC address by swapping the first 3 bytes to 02:0f:b5. To get around this, I added a --virtualmac option that attempts to fall back to looking in the database for just the last 3 bytes of a MAC address.
  • I recommend using a static IP on the device hosting the Python code. I put this code on a Pine64 board I had sitting around and plugged it directly into my wifi router’s wired ethernet switch. There’s no reason it couldn’t have just run on a regular desktop or laptop, but I needed an excuse to use the Pine64.
  • It works but I’ve noticed a few bugs, especially when switching playback between output devices. I might get around to fixing them.

Future Work

  • I also own some Philips Hue lights and they are a lot of fun. I want to bring those into the entrance by having them dim as the music fades and then bringing them up at a certain point in the music.

Appendix: What my friends picked as their entrance theme songs

  • Me - Still Fly (cover) by The Devil Wears Prada
  • Alex - So Fresh, So Clean by OutKast
  • Yaro (@hokietron) - Bumble Bee Tuna Song by Mephiskapheles
  • Samantha - Hide and Seek by Imogen Heap (but at the 2’52” mark where all the memes happen)
  • Rob - Imperial March by John Williams
  • Suzie (@California_Suz) - Cotton Eye Joe by Rednex
  • Smitty - Mah Nà Mah Nà by Piero Umilian
  • Patrick - Holy Diver by Dio
  • Default song for unknown people - Frolic by Luciano Michelini (better known as the “Curb Your Enthusiasm” theme)

Switching to i3

A co-worker convinced me to try out the i3 tiling window manager. I initially wrote this off as a show of nerdy one-upmanship (I admitted to him I still run Unity on my main desktop) but I told him I’d try it out. After a few days, I have some thoughts in place.

If you’ve never used a tiling window manager, the big difference is that your windows are no longer floating and overlaping. Instead they are mutually exclusive units on the screen, usually arranged in a grid. You lose the traditional desktop metaphor, but you gain some performance and efficiency improvements.

Initial things

I installed i3 via apt-get and started a new session. There was a wave of mild paralysis when I realized I had no clue how to make anything work. Using the windows & desktop model for so long made it hard for me to even conceptualize anything else. I’m used to dragging windows around, maximizing them, minimizing them, sliding them around like physical objects at my whim.

Now I was staring at rigid compartments of cold unfeeling terminal windows with no friendly context menus or hints of what to do next. After learning a few key shortcuts and dmenu, I began to breath a sigh of relief. I could pop open my traditional desktop apps and get my footing. It felt weird to open Firefox and not be able to drag the window around. Even worse, it was frightening when I realized that my usual methods of moving files via drag-and-drop was no longer an option. Opening Nautilus was no longer an option (although if you try, you’ll just get a messed up desktop).

After a few hours of tweaking and seeing what’s available, I had a really attractive clean desktop.

desktop1 desktop2

Things I like

For starters, this thing is fast. I can go from login screen to desktop in less than a second. Granted, most Linux window managers tend to be fast compared to Windows, but this is easily the fastest one I’ve personally used.

If you do most of your work in the terminal, then this is a fantastic experience. You can open terminals, have them cleanly arranged on your screen and never need to go searching for the last one you had open among a bunch of minimized windows.

The concept of workspaces is something I never really appreciated until I began to use them in i3. I never used the Unity workspaces because it was easier to just do all your work in one and just minimize the stuff you weren’t dealing with at the moment. In i3, workspaces are a necessity since you can’t just minimize things to a taskbar. The number of workspaces grows as you need them and they quietly disappear after you’re done. This is a fantastic feature I don’t need to dig through empty workspaces trying to find the one I was using. My current setup involves opening my background applications in their own workspaces so I can drop in on them periodically without having them take up my full attention (things like Thunderbird and Spotify). It keeps everything clean and mentally tidy.

Easily my favorite feature is the i3 status bar. It’s such a small part of the i3 experience, but it’s a lot of fun to tweak. Like the rest of i3, it’s simple and aesthetically pleasing.

I fully expected that i3 would take a hard stance on using the keyboard for everything, but I was pleasantly surprised to see the mouse as a fully capable tool. I still use it for switching workspaces.

For my (relatively simple) setup, all my tweaks use only 3 files: ~/.config/i3/config, ~/.i3status.conf, and ~/.Xdefaults. I also like that you can re-load your entire session with a few keystrokes so you can quickly play around with settings and see the results.

Things I’m not liking (yet)

Copy/pasting between windows feels weird, especially if one window supports traditional Ctrl-C/Ctrl-V but the other doesn’t. I don’t think this is an i3 issue but rather something that just isn’t configured in my current urxvt setup.

When doing front-end development, I felt slowed down, mostly because I didn’t have an easy way to view image thumbnails.

Conclusions

I like the change so far and I’m planning on sticking with i3. I haven’t had any compatibility issues with my GUI applications (even Steam runs fine). I haven’t been able to find anything that I absolutely hate.

Since I’m a command-line person, the learning curve hasn’t been too difficult. When combined with a good mechanical keyboard, i3 is one of the most satisfying ways to interact with your Unix-like system.

My setup

Getting it

sudo apt-get install i3 dmenu rxvt-unicode-256color feh

Setting urxvt to be the default terminal

Swap out the command to start a terminal to use urxvt command in ~/.config/i3/config

# start a terminal
# bindsym $mod+Return exec i3-sensible-terminal
bindsym $mod+Return exec urxvt

Get yourself a cool wallpaper

I’m using this one. Add to the end of the ~/.config/i3/config file

exec --no-startup-id feh --bg-scale ~/Downloads/pexels-photo-14676.png

Colors!

Create an .Xdefaults file. I’m using the Railscast theme exported from terminal.sexy along with some tweaks to make urxvt transparent.

urxvt.font:           xft: Dejavu Sans Mono:autohint=true:antialias=true:size=9
urxvt.background:     black
urxvt.foreground:     white
urxvt.scrollBar:      false
urxvt.tintColor:      white
urxvt.fading:         15
urxvt.fadeColor:      black
urxvt.shading:        25
urxvt.inheritPixmap:  true
urxvt.pointerColor:   black
urxvt.pointerColor2:  white

! special
*.foreground:   #e6e1dc
*.background:   #2b2b2b
*.cursorColor:  #e6e1dc

! black
*.color0:       #2b2b2b
*.color8:       #5a647e

! red
*.color1:       #da4939
*.color9:       #da4939

! green
*.color2:       #a5c261
*.color10:      #a5c261

! yellow
*.color3:       #ffc66d
*.color11:      #ffc66d

! blue
*.color4:       #6d9cbe
*.color12:      #6d9cbe

! magenta
*.color5:       #b6b3eb
*.color13:      #b6b3eb

! cyan
*.color6:       #519f50
*.color14:      #519f50

! white
*.color7:       #e6e1dc
*.color15:      #f9f7f3

Tweak the status bar

Copy the existing status bar file and make a few changes.

cp /etc/i3status.conf ~/.i3status.conf
# i3status configuration file.
# see "man i3status" for documentation.

# It is important that this file is edited as UTF-8.
# The following line should contain a sharp s:
# ß
# If the above line is not correctly displayed, fix your editor first!

general {
        colors = true
        interval = 5
        color_bad = '#da4939'
        color_good = '#a5c261'
}

order += "disk /"
order += "run_watch DHCP"
order += "wireless _first_"
order += "ethernet _first_"
order += "cpu_usage"
order += "load"
order += "cpu_temperature 0"
order += "tztime local"

wireless _first_ {
        format_up = "Wireless: (%quality at %essid) %ip"
        format_down = "Wireless: down"
}

ethernet _first_ {
        # if you use %speed, i3status requires root privileges
        format_up = "Ethernet: %ip (%speed)"
        format_down = "Ethernet: down"
}

run_watch DHCP {
        pidfile = "/var/run/dhclient*.pid"
}

tztime local {
        format = "%A, %B %e %l:%M %P"
}

load {
        format = "CPU load: %1min"
}

disk "/" {
        format = "%avail"
}

cpu_temperature 0 {
        format = "Temp: %degrees °C"
        path = "/sys/class/thermal/thermal_zone0/temp"
}

cpu_usage {
        format = "CPU usage: %usage"
}

Setuid Script Wrapper Example

The problem? I have a Node.js script that needs to run as root and Linux doesn’t like me to shoot myself in the foot.

I have a bunch of sysadmin utilities that I like to use to do various clean up tasks on my projects. I call them “agents” and they are just short Node.js scripts that run forever using PM2 (PM2 is one of my favorite utilities ever because it lets you wrap up all your microservices into one frontend, no matter what language you wrote them in – also you’re allowed to say microservices and not sound like a complete tool).

One of these agents was ping-agent.js, a Node script that uses net-ping and node-schedule to periodically send ping packets to a list of hosts and write the results back to a Redis cache. The problem is that doing a ping requires privileged access to the OS and using sudo put it in an entirely different PM2 daemon so it wasn’t appearing with my normal pm2 list results. Sad!

Most of the time this would be a job for setuid, but setuid doesn’t work for interpreted scripts for security reasons. One solution is to code up a quick-and-dirty wrapper in C. This is usually a Bad Idea™ since it’s a perfect way to do privilege escalation on a compromised system. My version of this is to make it as narrowly defined as possible and useable for only a specific case, while still being kind of portable when I need to re-deploy code in a different situation.

Here’s my solution:

The C preprocessor here requires that you explicitly give a path to the script to run but the Makefile figures that out for you so you don’t need to hardcode something.

make
pm2 start wrapper --name ping-agent
[PM2] Spawning PM2 daemon with pm2_home=/home/austin/.pm2
[PM2] PM2 Successfully daemonized
[PM2] Starting /home/austin/projects/ping-agent/wrapper in fork_mode (1 instance)
[PM2] Done.
┌────────────┬────┬──────┬───────┬────────┬─────────┬────────┬─────┬───────────┬──────────┐
│ App name   │ id │ mode │ pid   │ status │ restart │ uptime │ cpu │ mem       │ watching │
├────────────┼────┼──────┼───────┼────────┼─────────┼────────┼─────┼───────────┼──────────┤
│ ping-agent │ 0  │ fork │ 14568 │ online │ 0       │ 0s     │ 3%  │ 24.0 MB   │ disabled │
└────────────┴────┴──────┴───────┴────────┴─────────┴────────┴─────┴───────────┴──────────┘
 Use `pm2 show <id|name>` to get more details about an app

Woo! Now I can run my Node.js stuff in PM2 without needing a spearate PM2 daemon for root-ish activity. One big caveat is that you need to have a shebang line on your Node.js script (e.g. #!/usr/bin/env node).

Final warning: setuid is still pretty nasty. It’s probably not a good idea to use it for anything that gets input from the outside world.