Dumping and extracting the SpaceX Starlink User Terminal firmware

Towards the end of May 2021 Starlink launched in Belgium so we were finally able to get our hands on a Dishy McFlatface. In this blog post we will cover some initial exploration of the hardware and we will explain how we dumped and extracted the firmware.
Note that this blog post does not discuss any specific vulnerabilities, we merely document techniques that can be used by others to research the Starlink User Terminal (UT). Towards the end of this blog post we will include some interesting findings from the firmware. Note that SpaceX actively encourages people to find and report security issues through their bug bounty program: https://bugcrowd.com/spacex

We first set up our UT on a flat section of our university building’s roof and played around with it for a few hours, giving the UT and router the chance to perform a firmware update. We did run a few mandatory speed tests and were seeing as much as 268 Mbps download and 49 Mbps upload.

Teardown: Level 1

After a few hours of playing around it was time to get our hands dirty and disassemble the UT.

There have been a few teardown videos of the UT but none of them went into the details we were interested in: the main SoC and firmware. Nevertheless, these prior teardowns ([1, 2, 3]) of the dish contain a lot of useful information that allowed us to disassemble our dish without too much damage.
It appears that there a few hardware revisions of the UT out there by now, certain parts of the teardown process can differ depending on the revision, something we learned the hard way. One of the aforementioned teardown videos shows the Ethernet and motor control cables to be detached from the main board before the white plastic cover is removed. On our UT, a tug on the motor control cables pulled the entire connector from the PCB; luckily it appears we can repair the damage. In other words, do not pull on those cables but first remove the back plastic cover, for those of you in the same boat: JST BM05B-ZESS-TBT.

After removing the back plastic cover we can see a metal shield covering the PCB, with the exception of a small cut-out containing the connectors for the Ethernet cable and motor control cable. There is one additional, unpopulated connector (4 pin JST SH 1.0mm), that we assumed would contain a UART debug interface as was shown in [4]. Note that the early teardown videos had an additional connector that is no longer present on our UT.

The UART interface

After hooking up a USB to serial converter we could get some information on the UT’s boot process.
The output contains information regarding the early stage bootloaders before showing the following output.

We can see that the UT is using the U-Boot bootloader, and that typing ‘falcon’ may interrupt the boot process. While this may give us access to a U-Boot CLI we can also see that the serial input is configured as ‘nulldev’. Unsurprising, spamming the serial interface with ‘falcon’ during boot did not yield any result.

U-Boot 2020.04-gddb7afb (Apr 16 2021 - 21:10:45 +0000)

Model: Catson
DRAM:  1004 MiB
MMC:   Fast boot:eMMC: 8xbit - div2
stm-sdhci0: 0
In:    nulldev
Out:   serial
Err:   serial
CPU ID: 0x00020100 0x87082425 0xb9ca4b91
Detected Board rev: #rev2_proto2
sdhci_set_clock: Timeout to wait cmd & data inhibit
FIP1: 3 FIP2: 3
Net:   Net Initialization Skipped
No ethernet found.

                                       +    +              
                                +     +                    
                           +      +                        
+ + + + +              +     +                             
  +        +       +     +                                 
     +       + +      +                                    
        +   +      +                                       
          +      + +                                       
      +      +        +                                    
   +       +    +        +                                 
 +       +         +        +                              
+ + + + +             + + + + +                            


= Type 'falcon' to stop boot process =

Continuing through the boot process we can see that U-Boot loads a kernel, ramdisk and Flattened Device Tree (FDT) from a Flattened uImage Tree (FIT) image that is stored on an embedded MultiMediaCard (eMMC).
We can also see that the integrity (SHA256) and authenticity (RSA 2048) of the kernel, ramdisk and FDT is being checked. While we would have to perform some more tests it appears that a full trusted boot chain (TF-A) is implemented from the early stage ROM bootloader all the way down to the Linux operating system.

switch to partitions #0, OK
mmc0(part 0) is current device

MMC read: dev # 0, block # 98304, count 49152 ... 49152 blocks read: OK
## Loading kernel from FIT Image at a2000000 ...
   Using 'rev2_proto2@1' configuration
   Verifying Hash Integrity ... sha256,rsa2048:dev+ OK
   Trying 'kernel@1' kernel subimage
     Description:  compressed kernel
     Created:      2021-04-16  21:10:45 UTC
     Type:         Kernel Image
     Compression:  lzma compressed
     Data Start:   0xa20000dc
     Data Size:    3520634 Bytes = 3.4 MiB
     Architecture: AArch64
     OS:           Linux
     Load Address: 0x80080000
     Load Size:    unavailable
     Entry Point:  0x80080000
     Hash algo:    sha256
     Hash value:   5efc55925a69298638157156bf118357e01435c9f9299743954af25a2638adc2
   Verifying Hash Integrity ... sha256+ OK
## Loading ramdisk from FIT Image at a2000000 ...
   Using 'rev2_proto2@1' configuration
   Verifying Hash Integrity ... sha256,rsa2048:dev+ OK
   Trying 'ramdisk@1' ramdisk subimage
     Description:  compressed ramdisk
     Created:      2021-04-16  21:10:45 UTC
     Type:         RAMDisk Image
     Compression:  lzma compressed
     Data Start:   0xa2427f38
     Data Size:    8093203 Bytes = 7.7 MiB
     Architecture: AArch64
     OS:           Linux
     Load Address: 0xb0000000
     Load Size:    unavailable
     Entry Point:  0xb0000000
     Hash algo:    sha256
     Hash value:   57020a8dbff20b861a4623cd73ac881e852d257b7dda3fc29ea8d795fac722aa
   Verifying Hash Integrity ... sha256+ OK
   Loading ramdisk from 0xa2427f38 to 0xb0000000
WARNING: 'compression' nodes for ramdisks are deprecated, please fix your .its file!
## Loading fdt from FIT Image at a2000000 ...
   Using 'rev2_proto2@1' configuration
   Verifying Hash Integrity ... sha256,rsa2048:dev+ OK
   Trying 'rev2_proto2_fdt@1' fdt subimage
     Description:  rev2 proto 2 device tree
     Created:      2021-04-16  21:10:45 UTC
     Type:         Flat Device Tree
     Compression:  uncompressed
     Data Start:   0xa23fc674
     Data Size:    59720 Bytes = 58.3 KiB
     Architecture: AArch64
     Load Address: 0x8f000000
     Hash algo:    sha256
     Hash value:   cca3af2e3bbaa1ef915d474eb9034a770b01d780ace925c6e82efa579334dea8
   Verifying Hash Integrity ... sha256+ OK
   Loading fdt from 0xa23fc674 to 0x8f000000
   Booting using the fdt blob at 0x8f000000
   Uncompressing Kernel Image
   Loading Ramdisk to 8f848000, end 8ffffe13 ... OK
ERROR: reserving fdt memory region failed (addr=b0000000 size=10000000)
   Loading Device Tree to 000000008f836000, end 000000008f847947 ... OK
WARNING: ethact is not set. Not including ethprime in /chosen.

Starting kernel ...

The remainder of the boot process contains some other interesting pieces of information.
For example, we can see the kernel command line arguments and with that the starting addresses and lengths of some partitions. Additionally, we can see that the SoC contains 4 CPU cores.

[    0.000000] 000: Detected VIPT I-cache on CPU0
[    0.000000] 000: Built 1 zonelists, mobility grouping on.  Total pages: 193536
[    0.000000] 000: Kernel command line: rdinit=/usr/sbin/sxruntime_start mtdoops.mtddev=mtdoops console=ttyAS0,115200 quiet alloc_snapshot trace_buf_size=5M rcutree.kthread_prio=80 earlycon=stasc,mmio32,0x8850000,115200n8 uio_pdrv_genirq.of_id=generic-uio audit=1 SXRUNTIME_EXPECT_SUCCESS=true blkdevparts=mmcblk0:0x00100000@0x00000000(BOOTFIP_0),0x00100000@0x00100000(BOOTFIP_1),0x00100000@0x00200000(BOOTFIP_2),0x00100000@0x00300000(BOOTFIP_3),0x00080000@0x00400000(BOOTTERM1),0x00080000@0x00500000(BOOTTERM2),0x00100000@0x00600000(BOOT_A_0),0x00100000@0x00700000(BOOT_B_0),0x00100000@0x00800000(BOOT_A_1),0x00100000@0x00900000(BOOT_B_1),0x00100000@0x00A00000(UBOOT_TERM1),0x00100000@0x00B00000(UBOOT_TERM2),0x00050000@0x00FB0000(SXID),0x01800000@0x01000000(KERNEL_A),0x00800000@0x02800000(CONFIG_A),0x01800000@0x03000000(KERNEL_B),0x00800000@0x04800000(CONFIG_B),0x01800000@0x05000000(SX_A),0x01800000@0x06800000(SX_B),0x00020000@0x00F30000(VERSION_INFO_A),0x00020000@0x00F50000(VERSION_INFO_B),0x00020000
[    0.000000] 000: audit: enabled (after initialization)
[    0.000000] 000: Dentry cache hash table entries: 131072 (order: 9, 2097152 bytes, linear)
[    0.000000] 000: Inode-cache hash table entries: 65536 (order: 7, 524288 bytes, linear)
[    0.000000] 000: mem auto-init: stack:off, heap alloc:off, heap free:off
[    0.000000] 000: Memory: 746884K/786432K available (6718K kernel code, 854K rwdata, 1648K rodata, 704K init, 329K bss, 39548K reserved, 0K cma-reserved)
[    0.000000] 000: SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=4, Nodes=1
[    0.000000] 000: ftrace: allocating 23664 entries in 93 pages
[    0.000000] 000: rcu: Preemptible hierarchical RCU implementation.
[    0.000000] 000: rcu:        RCU event tracing is enabled.
[    0.000000] 000: rcu:        RCU restricting CPUs from NR_CPUS=8 to nr_cpu_ids=4.
[    0.000000] 000: rcu:        RCU priority boosting: priority 80 delay 500 ms.
[    0.000000] 000: rcu:        RCU_SOFTIRQ processing moved to rcuc kthreads.
[    0.000000] 000:     No expedited grace period (rcu_normal_after_boot).
[    0.000000] 000:     Tasks RCU enabled.
[    0.000000] 000: rcu: RCU calculated value of scheduler-enlistment delay is 100 jiffies.
[    0.000000] 000: rcu: Adjusting geometry for rcu_fanout_leaf=16, nr_cpu_ids=4
[    0.000000] 000: NR_IRQS: 64, nr_irqs: 64, preallocated irqs: 0
[    0.000000] 000: random: get_random_bytes called from start_kernel+0x33c/0x4b0 with crng_init=0
[    0.000000] 000: arch_timer: cp15 timer(s) running at 60.00MHz (virt).
[    0.000000] 000: clocksource: arch_sys_counter: mask: 0xffffffffffffff max_cycles: 0x1bacf917bf, max_idle_ns: 881590412290 ns
[    0.000000] 000: sched_clock: 56 bits at 60MHz, resolution 16ns, wraps every 4398046511098ns
[    0.008552] 000: Calibrating delay loop (skipped), value calculated using timer frequency.. 
[    0.016871] 000: 120.00 BogoMIPS (lpj=60000)
[    0.021129] 000: pid_max: default: 32768 minimum: 301
[    0.026307] 000: Mount-cache hash table entries: 2048 (order: 2, 16384 bytes, linear)
[    0.034005] 000: Mountpoint-cache hash table entries: 2048 (order: 2, 16384 bytes, linear)
[    0.048359] 000: ASID allocator initialised with 32768 entries
[    0.050341] 000: rcu: Hierarchical SRCU implementation.
[    0.061390] 000: smp: Bringing up secondary CPUs ...
[    0.078677] 001: Detected VIPT I-cache on CPU1
[    0.078755] 001: CPU1: Booted secondary processor 0x0000000001 [0x410fd034]
[    0.095799] 002: Detected VIPT I-cache on CPU2
[    0.095858] 002: CPU2: Booted secondary processor 0x0000000002 [0x410fd034]
[    0.112970] 003: Detected VIPT I-cache on CPU3
[    0.113025] 003: CPU3: Booted secondary processor 0x0000000003 [0x410fd034]
[    0.113160] 000: smp: Brought up 1 node, 4 CPUs
[    0.113184] 000: SMP: Total of 4 processors activated.

Finally, when the UT completes its boot process we are greeted with a login prompt:

Development login enabled: no

SpaceX User Terminal.
user1 login:

While making a few attempts at guessing valid login credentials we started realising that this UART interface would be unlikely to result in an easy win. We had to go deeper.

Teardown: Level 2

The back metal cover of the UT is glued to the assembly around the outer edge and additional glue is applied between the ribs in the metal cover and the underlying PCB. To loosen the glue at the edge of the metal cover we used a heat gun, prying tools, isopropyl alcohol and a lot of patience. Specifically, we first applied heat to a small section, used a prying tool to loosen that section, added IPA to help dissolve the glue and another round of the prying tool. Having removed the metal cover we are greeted by an enormous PCB measuring approximately 55 cm in diameter.

The parts of interest to us are shown in the picture below. The flip-chip BGA package with the metal lid is the main SoC on this board (marking: ST GLLCCOCA6BF). Unsurprisingly the SoC is connected to some volatile DRAM storage and non-volatile flash storage in the form of an eMMC chip.

Identifying eMMC test points

An embedded MultiMediaCard (eMMC) contains flash storage and a controller and is quite similar to an SD-card. The UT contains a Micron eMMC chip with package marking JY976, Micron offers a convenient tool to decode these package markings to the actual part number: https://www.micron.com/support/tools-and-utilities/fbga. The eMMC chip in question has part number MTFC4GACAJCN-1M and contains 4GB of flash storage in a BGA-153 package. In most scenarios we would desolder such an eMMC chip, reball it and dump it using a BGA socket. However, in this case we first attempted to dump the eMMC in-circuit to minimize the odds of damaging our UT and the eMMC chip.

eMMC chips are similar to SD cards in that they share a similar interface; the eMMC chip does support up to 8 data lines whereas SD-cards support up to 4 data lines. Both eMMC chips and SD-Cards support the use of only a single data line at the cost of lower read/write speeds.

To read the eMMC chip in-circuit we have to identify the clock (CLK), command (CMD) and data 0 (D0) signals. The 10 test points above the main SoC drew our attention, as 10 test points could be a CMD, CLK and 8 data lines. Additionally, all of these test points have a 30 Ohm series resistor connected to them which is relatively common for eMMC connections. We soldered a short wire to each test point, allowing us to create a logic analyser capture during the UT boot process. Using such a capture it is relatively straightforward to identify the required signals. The CLK signal will be the only repetitive signal, CMD is the signal that is first active after the clock starts toggling and D0 is the first data line to send out data. Determining the remaining 7 data lines is luckily unnecessary to dump the eMMC contents.

Dumping the eMMC in-circuit

To dump the eMMC chip we can connect a reader (that supports 1.8V IO) to the identified test points. Commercial readers that are mostly aimed at phone repair exist and should work well for this purpose (e.g. easy-JTAG and Medusa Pro). Alternatively you can use a regular USB SD-card reader (one that supports 1-bit mode) with an SD-card breakout with integrated level-shifters (e.g. https://shop.exploitee.rs/shop/p/low-voltage-emmc-adapter ). You can also whip something up yourself if you have some parts laying around. The picture below shows a standard USB SD-card reader connected to a TI TXS0202EVM level-shifter breakout board.

We only provide power to the eMMC to prevent the main SoC from interfering. The eMMC can be powered through two nearby decoupling capacitors, 3.3V is provided by the SD card reader and 1.8V is provided using a lab power supply. Once everything is hooked up properly we can create a disk image for later analysis.

Note that reading eMMC in circuit is not always an easy task; wires that are slightly too long can already prevent reading from succeeding. In this case it was rather straightforward and the system appears to function normally even with these relatively long wires attached.

Unpacking the raw eMMC dump

Unfortunately, Binwalk was not able to extract the full filesystem hence a manual analysis was required.

From the boot log it was clear that U-Boot was loading 49152 blocks of data starting at block 98304. Meaning that U-Boot is reading 0x1800000 bytes (blocksize of 512 (0x200) bytes) starting from address 0x3000000. We also know from the U-Boot output that this chunk of data is a FIT image. However, when trying to read the FIT image header information using the dumpimage tool (part of the u-boot-tools package) we weren’t getting any useful information.

Luckily SpaceX released their modifications to U-Boot on Github for GPL compliance: https://github.com/SpaceExplorationTechnologies
By looking at this code it became clear that certain parts of the firmware are stored in a custom format that contains Error Correcting Code (ECC) data.

Stripping Reed-Solomon ECC words

The file spacex_catson_boot.h contains interesting information related to how the device boots.
The following snippets show how data is being read from eMMC (mmc read8) and the definition for startkernel.

	"kernel_boot_addr=" __stringify(CATS_KERNEL_BOOT_ADDR) "\0" \
    "kernel_load_addr=" __stringify(CATS_KERNEL_LOAD_ADDR) "\0" \
	"kernel_offset_a=" __stringify(CATS_KERNEL_A_OFFSET) "\0" \
	"kernel_offset_b=" __stringify(CATS_KERNEL_B_OFFSET) "\0" \
	"kernel_size=" __stringify(CATS_KERNEL_A_SIZE) "\0" \
	"setup_burn_memory=mw.q " __stringify(CATS_TERM_SCRATCH_ADDR) " 0x12345678aa640001 && " \
		"mw.l " __stringify(CATS_TERM_LOAD_ADDR) " 0xffffffff " __stringify(CATS_BOOTTERM1_SIZE) " && " \
		"mw.l " __stringify(CATS_TERM_TOC_SER_ADDR) " " __stringify(CATS_TERM_TOC_SER_VAL) "\0" \
	"startkernel=unecc $kernel_load_addr $kernel_boot_addr && bootm $kernel_boot_addr${boot_type}\0" \

	"_emmcboot=mmc dev " __stringify(CATS_MMC_BOOT_DEV) " " __stringify(CATS_MMC_BOOT_PART) " && " \
		"mmc read8 $kernel_load_addr ${_kernel_offset} $kernel_size && " \
		"run startkernel\0" \
	"emmcboot_a=setenv _kernel_offset $kernel_offset_a && run _emmcboot\0" \
	"emmcboot_b=setenv _kernel_offset $kernel_offset_b && run _emmcboot\0"

The definition of startkernel is particularly interesting as it shows how the address where the kernel was loaded is being passed to a command called unecc. From the unecc command definition it is quite clear that this functionality is performing error correction on the data read from the eMMC.

	unecc, 3, 0, do_unecc,
	"Unpacks an ECC volume; increments internal ECC error counter on error",
	"<source> <target>\n"
	"\tReturns successfully if the given source was successfully\n"
	"\tunpacked to the target. This will fail if the given source\n"
	"\tis not an ECC volume. It will succeed if bit errors were\n"
	"\tsuccessfully fixed.\n"
	"\t<source> and <target> should both be in hexadecimal.\n"

The unecc command calls the do_unecc function implemented in unecc.c. Eventually this will result in calling the ecc_decode_one_pass function defined in ecc.c.

 * Decodes an ECC protected block of memory. If the enable_correction
 * parameter is zero, it will use the MD5 checksum to detect errors and will
 * ignore the ECC bits. Otherwise, it will use the ECC bits to correct any
 * errors and still use the MD5 checksum to detect remaining problems.
 * @data:		Pointer to the input data.
 * @size:		The length of the input data, or 0 to read until the
 *			end of the ECC stream.
 * @dest:		The destination for the decoded data.
 * @decoded[out]:	An optional pointer to store the length of the decoded
 *			data.
 * @silent:		Whether to call print routines or not.
 * @enable_correction:	Indicates that the ECC data should be used
 *			to correct errors. Otherwise the MD5 checksum
 *			will be used to check for an error.
 * @error_count[out]:	Pointer to an integer that will be incremented
 *			by the number of errors found. May be NULL.
 *			Unused if !enable_correction.
 * Return: 1 if the block was successfully decoded, 0 if we had a
 * failure, -1 if the very first block didn't decode (i.e. probably
 * not an ECC file)
static int ecc_decode_one_pass(const void *data, unsigned long size, void *dest,
			       unsigned long *decoded, int silent,
			       int enable_correction, unsigned int *error_count)

ecc.h contains several relevant definitions:

#else /* !NPAR */
#define NPAR			32
#endif /* NPAR */

 * These options must be synchronized with the userspace "ecc"
 * utility's configuration options. See ecc/trunk/include/ecc.h in the
 * "util" submodule of the platform.
#define ECC_BLOCK_SIZE		255
#define ECC_MD5_LEN		16
#define ECC_EXTENSION		"ecc"
#define ECC_FILE_VERSION	'1'
#define ECC_FILE_MAGIC_LEN	(sizeof(ECC_FILE_MAGIC) - 1)
#define ECC_FILE_FOOTER_LEN	sizeof(file_footer_t)


In the end what it boils down to is that, in this implementation, an ECC protected block of memory starts with the magic header value SXECCv followed by a version byte (1). This magic value marks the start of the ECC protected data, but also the start of the header block. The header block itself contains (in addition to the magic value and version byte), 215 bytes of data, an asterisk (*), and 32 bytes of ECC code words.

The header block is followed by multiple data blocks. Each of these data blocks is 255 bytes long and contains 222 bytes of data followed by an asterisk symbol (*) and 32 bytes of ECC code words. The last data block contains a dollar sign ($) instead of the asterisk and is followed by a final footer block. This footer block starts with and exclamation mark (!) that is followed by the number of data bytes in the ECC protected block of memory (4-bytes) and MD5 digest over those data bytes.

At this point it should be clear why Binwalk did not succeed in extracting the kernel, initramfs and FDT. Binwalk is able to pick up on the magic values that indicate the start of a particular file, but each block of the file had additional data that prevented Binwalk from extracting it. We used a simple Python scrip to remove the extra ECC data before using Binwalk to extract the image. Similarly, we can now also use dumpimage to get more information on the FIT image.

The FIT image and board revisions

The following snippet contains some of the dumpimage output. The FIT image contains 13 boot configurations, all configurations use the same kernel and initramfs images but a different Flattened Device Tree (FDT).

FIT description: Signed dev image for catson platforms
Created:         Fri Apr 16 23:10:45 2021
 Image 0 (kernel@1)
  Description:  compressed kernel
  Created:      Fri Apr 16 23:10:45 2021
  Type:         Kernel Image
  Compression:  lzma compressed
  Data Size:    3520634 Bytes = 3438.12 KiB = 3.36 MiB
  Architecture: AArch64
  OS:           Linux
  Load Address: 0x80080000
  Entry Point:  0x80080000
  Hash algo:    sha256
  Hash value:   5efc55925a69298638157156bf118357e01435c9f9299743954af25a2638adc2

Image 12 (rev2_proto2_fdt@1)
  Description:  rev2 proto 2 device tree
  Created:      Fri Apr 16 23:10:45 2021
  Type:         Flat Device Tree
  Compression:  uncompressed
  Data Size:    59720 Bytes = 58.32 KiB = 0.06 MiB
  Architecture: AArch64
  Load Address: 0x8f000000
  Hash algo:    sha256
  Hash value:   cca3af2e3bbaa1ef915d474eb9034a770b01d780ace925c6e82efa579334dea8

 Image 15 (ramdisk@1)
  Description:  compressed ramdisk
  Created:      Fri Apr 16 23:10:45 2021
  Type:         RAMDisk Image
  Compression:  lzma compressed
  Data Size:    8093203 Bytes = 7903.52 KiB = 7.72 MiB
  Architecture: AArch64
  OS:           Linux
  Load Address: 0xb0000000
  Entry Point:  0xb0000000
  Hash algo:    sha256
  Hash value:   57020a8dbff20b861a4623cd73ac881e852d257b7dda3fc29ea8d795fac722aa
Default Configuration: 'rev2_proto2@1'
 Configuration 0 (utdev@1)
  Description:  default
  Kernel:       kernel@1
  Init Ramdisk: ramdisk@1
  FDT:          utdev3@1
  Sign algo:    sha256,rsa2048:dev
  Sign value:   bb34cc2512d5cd3b5ffeb5acace0c1b3dd4d960be3839c88df57c7aeb793ad73a74e87006efece4e9f1e31edbb671e2c63dc4cdcb1a2f55388d83a11f1074f21a1e48d81884a288909eb0c9015054213e5e74cbcc6a6d2617a720949dcac3166f1d01e3c2465d8e7461d14288f1a0abef22f80e2745e7f8499af46e8c007b825d72ab494f104df57433850f381be793bfe06302473269d2f45ce2ff2e8e4439017c0a94c5e7c6981b126a2768da555c86b2be136d4f5785b83193d39c9469bd24177be6ed3450b62d891a30e96d86eee33c2cbfc549d3826e6add36843f0933ced7c8e23085ee6106e3cc2af1e04d2153af5f371712854e91c8f33a4ea434269

From the U-Boot code (spacex_catson_uterm.c) it becomes clear that the boot configuration is decided based on the state of 5 GPIO pins.

     * Check board ID GPIOs to find board revision.
     * The board IDs are mapped as follows
     * id_b0:pio12[2]
     * id_b1:pio12[3]
     * id_b2:pio12[0]
     * id_b3:pio12[1]
     * id_b4:pio20[4]
    u32 pio12 = readl(BACKBONE_PIO_A_PIO2_PIN);
    u32 pio20 = readl(BACKBONE_PIO_B_PIO0_PIN);
    u32 board_id = (((pio12 >> 2) & 1) << 0) |
               (((pio12 >> 3) & 1) << 1) |
           (((pio12 >> 0) & 1) << 2) |
           (((pio12 >> 1) & 1) << 3) |
           (((pio20 >> 4) & 1) << 4);
     * https://confluence/display/satellites/User+Terminal%3A+Catson+ID+Bits
    switch (board_id)
      case 0b11111:
        board_rev_string = BOARD_REV_1_1P3;
      case 0b11100:
        board_rev_string = BOARD_REV_1_2P1;
      case 0b11000:
        board_rev_string = BOARD_REV_1_2P2;
      case 0b10100:
        board_rev_string = BOARD_REV_1_3P0;
      case 0b10000: /* rev1 pre-production */
        board_rev_string = BOARD_REV_1_PRE_PROD;
      case 0b11110: /* rev1 production */
        board_rev_string = BOARD_REV_1_PROD;
      case 0b00001:
        board_rev_string = BOARD_REV_2_0P0;
      case 0b00010:
        board_rev_string = BOARD_REV_2_1P0;
      case 0b00011:
        board_rev_string = BOARD_REV_2_2P0;

  printf("Detected Board rev: %s\n", board_rev_string);

The following picture shows where these pins are being pulled high/low to indicate the board revision. Note from the earlier serial bootlog that our UT boots using the rev2_proto2 configuration (case 0b00011). In a recent video Colin O’Flynn pulled some of these pins high/low and could observe that the UT tried booting using a different FIT configuration and thus different devicetree [5]. We compared some of the FDTs but did not spot any differences that would be interesting from a security perspective.

A first look at the firmware

The login prompt

Recall that after the boot process has completed we are greeted with a login prompt. For further research it would be useful to gain the ability to log in, allowing us to interact with the live system. However, by looking at the shadow file it becomes clear that none of the users are allowed to log in.
During boot the UT does read a fuse to determine if it is development hardware or not. If the UT is unfused it will set a password for the root user, allowing log in.
Starlink UTs that are sold to consumers are of course production fused, disabling the login prompt.


Development hardware

Development hardware often finds its way into the wrong hands [6, 7]. The engineers at SpaceX considered this scenario and appear to try to actively detect unfused development hardware that is no longer under their control. Development hardware is geofenced to only work in certain predefined areas, most of which are clearly SpaceX locations. SpaceX is likely notified if development hardware is used outside these predefined geofences.

Interestingly, some of these geofences do not seem to have a clear connection to SpaceX. While we will not disclose these locations here, I will say that the SNOW_RANCH looks like a nice location to play with development hardware.

Secure element

From references in the firmware it became clear that (our revision of) the UT contains a STMicroelectronics STSAFE secure element. The purpose of the secure element is not entirely clear yet, but it may be used to remotely authenticate the UT.

The SoC

Some people have asked which processor is being used: the answer is a Quad-Core Cortex-A53 and each core has been assigned a specific task.

# System Information
# The user terminal phased-array computers are Catson SoCs with a quad-core
# Cortex-A53.
# We dedicate one core to control, while leaving the other three to handle
# interrupts and auxiliary processes.
#   CPU 0: Control process.
#   CPU 1: Lower-MAC RX process.
#   CPU 2: Lower-MAC TX process.
#   CPU 3: PhyFW and utility core - interrupts, auxiliary processes, miscellaneous

What’s next

That’s it for now. We will likely continue looking into the Starlink UT and provide more details in future blog posts if there is interest. At the time of writing we were able to obtain a root shell on the UT, but it’s too early to publicly share more information on that matter.


[1] MikeOnSpace – Starlink Dish TEARDOWN! – Part 1 – https://youtu.be/QudtSo5tpLk
[2] Ken Keiter – Starlink Teardown: DISHY DESTROYED! – https://youtu.be/iOmdQnIlnRo
[3] The Signal Path – Starlink Dish Phased Array Design, Architecture & RF In-depth Analysis – https://youtu.be/h6MfM8EFkGg
[4] MikeOnSpace – Starlink Dish TEARDOWN! – Part 2 – https://youtu.be/38_KTq8j0Nw
[5] Colin O’Flynn – Starlink Dishy (Rev2 HW) Teardown Part 1 – UART, Reset, Boot Glitches – https://youtu.be/omScudUro3s
[6] Brendan I. Koerner – The Teens Who Hacked Microsoft’s Xbox Empire – https://www.wired.com/story/xbox-underground-videogame-hackers/
[7] Jack Rhysider – Darknet Diaries EP 45: XBOX UNDERGROUND (PART 1) – https://darknetdiaries.com/episode/45/