《深入研究Starlink用户终端固件》翻译

深入研究Starlink用户终端固件

这篇博客文章概述了Starlink用户终端运行时的内部结构,重点关注设备内部和与用户应用之间发生的通信,以及一些可以帮助进一步研究相同主题的工具。

介绍

Starlink是SpaceX提供的基于卫星的互联网接入服务。该服务已经在全球拥有超过150万用户,使用相同的基础设施。Starlink依赖于三个主要组件:

  • 用户终端,与卫星进行通信,是当前大多数研究关注的对象。
  • 充当网状网络的卫星舰队。
  • 将卫星连接到互联网的网关。

已经进行了许多关于Starlink的研究,主要集中在用户终端上。在我在Trento大学攻读硕士学位期间,我在Quarkslab进行了为期6个月的实习,通过对Starlink固件和其使用的各种协议进行逆向工程分析。在实习结束时,我对设备内部运作原理有了很好的了解,并开发了一套工具,可帮助其他从事相同主题研究的研究人员。这些工具将随着本博客文章一起进行描述和发布。

为了进行这项研究,我们分析了两个常规用户终端版本2(圆形的)和一个用户终端版本3(方形的),具有根访问权限(研究员访问权限),这是在我的实习结束时由SpaceX的安全团队提供的。

固件概览

第一步是转储设备的固件,因为它并非公开可用,我们借助于KU Leuven的COSIC研究组的一篇博客文章完成了这一步骤。一旦我们获取了固件,我们开始检查其内容,试图理解内存的结构。我们还开始研究由SpaceX定制的U-Boot版本,它被用作用户终端的最终引导加载程序阶段(BL33)。U-Boot许可要求对其代码的任何修改都必须以相同的许可证发布,因此您可以在GitHub上找到它。

从文件include/configs/spacex_catson_boot.h中,我们可以看到内存是如何分区的,以下是其中的一部分:

+-----------------+ 0x0000_0000
| bootfip0 (1 MB) |
+-----------------+ 0x0010_0000
[...]
+-----------------+ 0x0060_0000
| fip  a.0 (1 MB) |
+-----------------+ 0x0070_0000
[...]
+-----------------+ 0x0100_0000
| linux a (32 MB) |
+-----------------+ 0x0300_0000
| linux b (32 MB) |
+-----------------+ 0x0500_0000
[...]

这使我们能够将镜像分割成小的分区并分别分析每个分区。这个脚本可以帮助您自动完成这个过程。从这里,我们还可以看到几乎每个分区都存在多次(例如,linux a/b)。这是由于软件更新过程,该过程将覆盖当前未使用的分区,以便在发生错误时仍然存在已知为“正确”的原始分区。以下是主要分区的概览图片。

img

分区 Boot FIP 和 FIP 包含构成安全引导链的所有引导加载程序阶段,其中大多数基于 ARM TF-A 项目,该项目不使用类似GNU的许可证,而最后一个(BL33)是我们之前提到的 U-Boot 引导加载程序。

对引导过程有清晰的了解对于执行由COSIC研究组开发的故障注入攻击至关重要。引导链遵循由ARM TF-A实施的经典多阶段安全引导,下图中您可以看到它的概述。

img

引导阶段来自eMMC的不同分区,而第一个代表Root of Trust的阶段来自主处理器的内部ROM,它们可以使用ARM TF-A中的fiptool从分区镜像中提取。在引导过程结束时,BL31将驻留在Exception Level 3(EL3),充当安全监视器,而Linux内核将在Exception Level 1(EL1)中运行,正常运行用户空间应用程序在Exception Level 0(EL0)中。

接着,Linux分区如其名称所示,包含Linux内核、其ramdisk镜像和一些用于用户终端每个硬件版本的Flattened Device Trees。可以使用U-Boot项目中的dumpimage解压这个分区,ramdisk是一个cpio镜像,而FDT以Device Tree Blobs(DTBs)的形式提供,可以通过Device Tree Compiler将其“反编译”为Device Tree Sources(DTSs)文本。此分区还包含一些纠错码(ECC)信息,您需要在解压之前将其删除。SpaceX的ECC机制是定制的,您可以通过查看U-Boot中处理此验证的代码来了解其工作原理,下一部分将解释其工作原理并提供相应的工具。

SX(SX Runtime)分区包含特定于用户终端的配置文件和二进制文件。Linux的init脚本将挂载此分区到/sx/local/runtime,然后在该卷中启动二进制文件。在这种情况下,完整性验证由sxverity执行,这是SpaceX的另一个定制工具。下一节将解释其工作原理。

其他分区包括一些加密分区,使用Linux Unified Key System(LUKS),这些分区是唯一具有写入权限的分区,还有一些其他较小的分区不值得一提。

数据完整性

通过对eMMC转储内容的简要分析,我们已经了解到SpaceX正在使用一些定制的数据完整性机制,以及ARM TF-A和U-Boot中已包含的标准机制。以下是定制组件的概览。

ECC

纠错码机制仅在FIT镜像中使用,并且仅提供数据完整性,而不考虑真实性。这意味着理论上,您可以使用自己实现的ecc过程(或使用ramdisk中找到的二进制文件)提供正确格式的数据来篡改天线的某些由ECC保护的组件。但是对于FIT镜像,这是不可能的,因为真实性也会由最后的引导加载程序阶段进行检查。因此,这只是用于防止eMMC存储中的错误。

这与其原始祖先ECC RAM类似,它在内存的实际内容之间嵌入了一些附加数据 - 最初是汉明码 - 这些码是作为对它们“保护”的数据的函数而计算的。然后,当访问某些数据时,会重新计算汉明码,如果它们与存储在内存中的码不一致,则发生错误,根据错误位翻转的位数,可以进行纠正。这个版本的ECC使用Reed-Solomon纠错码(而不是汉明码)和最终哈希(即MD5)来检查正在解码的整个文件的完整性。

在这里,您可以找到一个简单的Python脚本,该脚本从文件中剥离ECC信息,而不检查数据的正确性,我们用它来解压FIT镜像。在ramdisk中,有一个二进制文件(unecc),它也执行相同的操作,同时检查并尝试纠正可能的错误。

img

ECC-保护文件的内容被组织成不同类型的块,上图显示了每个块的结构。文件以一个头块(a)开头,其中包含magic number 和协议的版本,以及一些数据和相应的控制码。然后,可以有零个或多个只包含数据和控制码的数据块(b)。最后的数据块(c)由其块类型字段($,而不是*)识别,标志着有效负载的结束,如果需要,这里会添加一些填充。最后,尾块(d)包含有效负载的大小(需要知道填充字节的数量),整个有效负载的MD5校验和,当然还有尾块本身的ECC码字。

sxverity

sxverity是一个用于设备映射校验(dm-verity)内核特性的自定义包装器,该特性提供块设备的透明完整性检查。该工具的源代码并未公开,因此我们不得不对已编译的二进制文件进行逆向工程以了解其内部结构。sxverity通过直接与/dev/mapper/control设备进行交互,内部使用dm-verity内核特性。

SpaceX只解决了根哈希的验证,而由内核处理的底层部分没有被重新实现,这里可以找到关于其工作原理的良好解释。

正如我们在前面的部分中看到的,sxverity用于验证存储在持久内存中的一些分区,这是为了防止持久性攻击。但正如我们将在接下来的部分中看到的那样,它还用于验证天线的软件更新。因此,它是设备整体安全性的关键组件。

img

在上面的图片中,您可以看到sxverity镜像的结构。它由一个头部组成,该头部重复4次,可能使用不同的公钥进行签名,其中包含:

  • The magic bytes "sxverity"
  • 版本和一些标志,指示已使用哪些算法进行签名和哈希。
  • 根哈希,间接覆盖整个有效负载(通过哈希树)。
  • 用于签署镜像的公钥。
  • 所有上述字段的签名,使用elliptic curve(ED25519)。

这个过程执行的解析和验证过程将在Fuzzing部分进行描述。

运行态概览

在这一部分,我们将讨论从引导加载程序链开始,从Linux的init脚本到处理用户终端运行时的进程发生了什么。

init脚本是由内核启动的第一个进程,通常具有进程标识符(PID)等于1。其主要任务是启动系统所需的所有其他运行时进程,并保持运行直到系统关闭。它将是任何其他进程的“最老”祖先,并且一旦系统启动运行后,用户还可以使用它来启动、停止和配置守护程序。在终端用户Linux发行版中,您最常见的init脚本可能是systemd,它是一套用于管理系统整个运行时的工具(例如systemctl)的集合,其中也包括init脚本。

SpaceX的开发人员喜欢实现他们自己的软件,因此他们实现了自己的init脚本,可以在ramdisk中找到,路径是/usr/sbin/sxruntime_start。该脚本使用自定义格式的配置文件,其中包含启动哪些进程、如何启动它们以及以何种顺序启动它们的指令。

#######################################
# user terminal frontend
#
# Wait until dish config partition is set up before launching.
#######################################
proc            user_terminal_frontend
apparmor        user_terminal_frontend
start_if        $(cat /proc/device-tree/model) != 'spacex_satellite_starlink_transceiver'
startup_flags   wait_on_barrier
user            sx_packet
custom_param    --log_wrapped

上面的片段显示了如何启动一个名为user_terminal_frontend的进程:

  • 只有在满足条件$(cat /proc/device-tree/model) != 'spacex_satellite_starlink_transceiver'时才会启动该进程。
  • 它在标有barrier标志的最后一个进程退出后启动。
  • 它以Linux用户sx_packet的身份执行。
  • 将命令行参数--log_wrapped传递给它。

由init脚本解析的两个配置文件可以在/etc/runtime_init(ramdisk)和/sx/local/runtime/dat/common/runtime(运行时镜像)找到。第一个处理系统级别的服务和配置,例如挂载分区(例如runtime),以及设置网络、控制台和日志记录服务。相反,第二个处理更高级别的进程和配置,例如启动运行时镜像中包含的所有进程并初始化加密设备。

此外,init脚本还通过遵循另一个配置文件中列出的一些规则,为这些进程分配优先级和特定的CPU核心,该文件位于/sx/local/runtime/dat/common/runtime_priorities

File system & mount points

首先,根文件系统由最后一个引导加载程序(U-Boot,BL33)复制到RAM中,并在/上挂载。系统可以通过文件dev/blk/mmcblk0pN访问eMMC的分区,其中mmcblk0是eMMC的名称,N是分区索引,从1开始。为了方便起见,脚本将创建一些符号链接到这些分区,以使用更明确的名称,如下所示。

# [...]
ln -s /dev/mmcblk0p1 /dev/blk/bootfip0
ln -s /dev/mmcblk0p2 /dev/blk/bootfip1
ln -s /dev/mmcblk0p3 /dev/blk/bootfip2
ln -s /dev/mmcblk0p4 /dev/blk/bootfip3
# [...]

由于几乎每个分区都是复制的,接下来的一个脚本将在文件夹 /dev/blk/current /dev/blk/other 中创建额外的链接。前者包含系统当前正在使用的分区,后者包含将来在软件更新时可能使用的其他分区。系统通过查看 /proc/device-tree/chosen/linux_boot_slot 来确定当前引导时使用了哪些分区,该信息由引导加载程序填充。

接着,使用 sxverityruntime分区进行解包,并将内容提取到 /sx/local/runtime。该分区包含两个文件夹:

  • bin 包含二进制文件和可执行脚本。
  • dat 包含许多其他数据,如 AppArmor 规则、硬件特定配置和通用配置文件,例如 dat/common/runtime,这是由 init 脚本使用的(第二个)配置文件。

在这些操作之后,使用 ro(只读)标志重新挂载根文件系统。然后挂载额外的分区,例如:

  • /dev/blk/current/version_info/mnt/version_info(通过 sxverity)。
  • /dev/blk/dish_cfg/mnt/dish_cfg(通过 LUKS)。
  • /dev/blk/edr/mnt/edr(通过 LUKS)。

Daemons

在经过详细的引导过程和系统配置后,我们最终达到了一种状态,其中一些进程正在运行,每个进程都有独特的任务,为用户提供设备构建的服务,即卫星互联网连接。正如你可能已经猜到的,许多事情在后台发生,以确保足够稳定的互联网连接,例如发送和接收与卫星之间的流量,选择连接到哪个卫星,当当前卫星移动得太远时切换卫星(而不中断互联网连接),处理来自移动应用程序的用户请求等。此外,这些进程需要不断地相互通信以同步和合作,并与Starlink的云服务进行通信以获取后端功能。

大多数二进制文件都是用C++实现的,其中一些还是静态链接的。由于这个原因,对这些二进制文件进行逆向工程是具有挑战性的,而且由于时间限制,我们无法完全理解它们。一些工作已经完成,用于识别静态链接库函数,使用二进制比较技术。这些程序可能还使用了状态机设计模式,该模式大量使用面向对象编程的特性,例如多重继承、虚方法和泛型类型。由于使用这些功能时编译器生成的复杂结构,逆向工程过程变得更加困难。

我们尝试比较用户终端的网络堆栈与已知的ISO-OSI堆栈,以下可能是一个适当的映射:

  • phyfw(可能是物理固件)处理卫星通信的物理层,包括 RF 信号的调制/解调。
  • rx_lmactx_lmac(rx/tx 低介质访问控制,可能)位于数据链路层,分别处理接收和传输的物理介质访问。
  • umac(upper Medium Access Control,可能)可能表示网络层。它在更高级别处理对介质的访问,并协调帧的传输和接收。它还可能负责选择连接到哪个卫星。
  • connection_manager 可能表示传输层,如果是这样,它处理与卫星之间的有状态连接,在其中将交换流量。
  • ut_packet_pipeline 可能用于创建一个加密通道,用户流量将在其中使用卫星上的安全元素进行握手交换。这可能与已知的协议如 TLS、DTLS、IPsec 或自定义协议有关。

除了这些与网络相关的进程外,还有一些进程处理系统遥测、软件更新、系统健康状态和故障检测/报告,最后还有一个充当所有其他进程的协调器的"control" 进程。

另一方面,其中一个二进制文件,即 user_terminal_frontend,是使用 Go 实现的,Go 是谷歌的一种开源(编译的)编程语言。Go 二进制文件是静态链接的,它们包括 Go 运行时,因此它们相当庞大,但幸运的是它们还包括由运行时用于全面报告运行时错误的符号,其中包括函数名称、源代码行号和数据结构。所有这些宝贵的信息都可以通过 Ghidra 的一个名为 GolangAnalyzer 的插件进行恢复,这相当有效。该扩展还可以恢复复杂的数据类型,并在 Ghidra 中创建相应的类似 C 的结构,在使用面向对象编程语言时非常有用。由于 Go 使用的自定义调用约定,还需要进行额外的手动分析,但在此之后,生成的反汇编 C 代码非常易读。我们的主要关注点是运行时的更高级别组件,其中包括这个进程。

img

总结这一节,你可以在上面的图片中看到运行时架构的草图(不完整),在这里你可以看到底部的进程,即靠近硬件的进程,可能出于性能原因是静态链接的,并且只与控制进程通信,而其他进程还通过 gRPC 与 Starlink 的云服务通信。在“通信”部分,我们将更详细地讨论这张图片中显示的所有通信。最后,还有一个 Go 二进制文件(技术上也是静态链接的,但仅仅是因为语言约束),它与用户使用的前端应用程序进行通信。

Runtime 模拟

由于我们的故障注入攻击的负面结果,我们无法访问一个实际的设备来检查我们的发现或进行一些动态分析。因此,我们尝试建立一个模拟环境,尽可能与真实设备相似,能够执行运行时二进制文件。我们正在模拟整个系统(全系统模拟),从内核开始,使用 QEMU 作为仿真引擎。在以下段落中,我们描述了我们在设置环境和最终结果时必须处理的每个挑战,包括我们无法解决的挑战。

首先要做的选择是希望 QEMU 模拟哪种硬件。当你想使用 QEMU 模拟 IoT 设备时,通常会寻找该设备的 QEMU 特定硬件实现,这通常适用于常见的现成设备,如 Arduino、Raspberry Pi,以及较少知名的开发板。我们设备的硬件实现当然是不可用的,因此我们使用了(aarch64)virt 机器,这是最通用的一个。正确的方法是为 QEMU 构建此机器规范,以及为板上存在的每个硬件部件实现仿真器。问题是设备上的大多数外围设备都不是开放硬件,即使它们是开放硬件,将所有这些硬件实现在 QEMU 中将是一项艰巨的工作。相反,使用 virt 机器并调整设备树要容易得多,代价是没有大多数硬件外设,因此存在一些限制。

另一个问题是选择在 QEMU 中运行的内核。我们尝试使用从固件的 FIT 中提取的原始内核,但在模拟环境中无法工作。因此,我们决定自己编译一个内核。不幸的是,SpaceX 发布的 Linux 的开源版本是 5.10.90,而在卫星上找到的版本是 5.15.55,因此我们使用了主流的 Linux 内核。为了让它启动,必须对编译时配置进行大量调整,其中一些是由 QEMU 要求的,一些是由 Starlink 的软件要求的。可以使用 Linux 内核存储库中的脚本 extract-ikconfig 从已编译的内核镜像中提取此配置,该脚本用于查找 SpaceX 配置的默认配置和 SpaceX 配置之间的差异。

设备树不仅包含有关硬件外设的信息,还包含运行时使用的数据,例如 sxverity 使用的公钥。此外,U-Boot 引导加载程序在引导 Linux 内核之前还会填充 FDT,通过添加当前引导中使用的一组分区、主要网络接口的名称等信息。所有这些信息当然都不包含在由 QEMU 为 virt 机器设置的 FDT 中,因此我们提取了此 FDT,使用 dumpdtb 标志,并添加了缺失的信息,如下所示,然后可以使用设备树编译器(dtc)重新编译,然后通过 -dtb 标志提供给 QEMU。

# ...
model = "spacex_satellite_user_terminal";
compatible = "st,gllcff";

chosen {
    linux_boot_slot = "0";
    ethprime = "eth_user";
    board_revision = "rev2_proto3";
    bootfip_slot = <0x00>;
    boot_slot = <0x0006>;
    boot_count = <0x0010>;
    # ...
};

security {
    dm-verity-pubkey = <REDACTED>;
    # ...
};
# ...

作为根文件系统,我们使用了从卫星接收的文件系统,进行了一些修改。

  • 由于我们希望能够访问模拟的卫星,它必须认为自己是开发版本,以启用密码访问,因此我们修补了 is_production_hardware 脚本。这可以通过多种方式完成,例如直接编辑 /etc/shadow 文件,或将我们的公钥添加到 SSH 的 authorized_users 文件中,但我们的做法更有效,因为模拟开发硬件还将启用其他调试功能。
  • 我们还包含了提取的运行时,它将被挂载的位置,并从 /etc/runtime_init 文件中删除了完整性验证和挂载步骤,以便还能够篡改该分区的内容。
  • /etc/runtime_init 文件中,我们还添加了一些自定义步骤,例如设置我们模拟的网络的步骤,以及将读写分区挂载为模拟卷的步骤。

其他程序在模拟环境中启动时还需要进行其他补丁。我们还包含了一些用于测试目的的附加软件,例如 gdbserver。但是为了使这些程序能够运行,我们要么必须使用相同的构建工具链交叉编译它们,要么以静态方式进行交叉编译。

尽管在 Linux 内核引导时,根文件系统和运行时都已经放置在内存中,但很多进程直接访问 eMMC 的一些分区。因此,我们还指示 QEMU 创建一个原始的虚拟块设备,其中包含从物理板上提取的原始映像。但是,由于内核未将其视为 eMMC 芯片,因此分配的名称与物理设备分配的名称不同。由于此原因,我们不得不将每个对 mmcblk0 的引用更改为 vda,这是模拟器中由内核分配的名称。幸运的是,正如我们在前一节中看到的,设备仅在一个脚本中使用设备的名称,该脚本创建了对每个分区的一些符号链接,因此我们只需修补该脚本和内核的命令行参数。以写权限挂载的分区,则映射到主机上的一个文件夹,以便后续可以检查其内容。

至于网络,没有必要复制卫星的确切网络配置(因为我们对此不是很清楚),我们只需要有正确的接口名称和互联网访问。这是通过使用 tap 接口,桥接到充当 NAT 网关的主机来完成的,如下所示。

Host:

#!/bin/bash

ifconfig $1 0.0.0.0 promisc up
brctl addbr virbr0
brctl addif virbr0 $1

ip link set dev virbr0 up
ip link set dev $1 up

ip addr add 192.168.100.2/24 dev virbr0
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A POSTROUTING -o wlp0s20f3 -j MASQUERADE

Guest:

#!/bin/sh

ip link set dev eth0 name eth_user
ip link set dev eth_user up
ip addr add 192.168.100.1/24 dev eth_user

route add -net 0.0.0.0 netmask 0.0.0.0 gw 192.168.100.2 dev eth_user

模拟结果总结:

如前面的部分所解释的,大多数硬件在模拟环境中不存在,因此每个试图使用它的组件都将失败。较低级别的进程,如 phyfw[rx/tx]_lmac,其主要任务是与 Starlink 的硬件交互,将无法在这个环境中运行。但也有其他二进制文件需要一些硬件,最常见的是安全元素,它在大多数加密交换中都在使用。因此,为了使这些二进制文件工作,我们对每个会导致程序崩溃的指令进行了修补,但如果需要硬件来执行流程的实质性部分,这种解决方案就毫无意义。最终,我们设法在模拟环境中模拟了在 Daemons 子节中讨论的主要进程中的一些,包括 user_terminal_frontendstarlink_software_updateumac 以及与遥测相关的进程,以及一些较小的进程。对这个主题的进一步研究可以逐渐添加对板上某些外设的支持,从而能够模拟越来越多的进程。以下是模拟环境中引导过程最后阶段的控制台输出。

# [...]
# kernel messages and (a lot of) errors from applications
# trying to access missing hardware peripherals
# [...]

setup_console: Development login enabled: yes

SpaceX User Terminal.
user1 login: root
Password: 

                                                                *
                                                 +               
                                       +    +                    
                                +     +                          
                           +      +                              
+ + + + +              +     +                                   
  +        +       +     +                                       
     +       + +      +                                          
        +   +      +                                             
          +      + +                                             
      +      +        +                                          
   +       +    +        +                                       
 +       +         +        +                                    
+ + + + +             + + + + +                                


The Flight Software does not log to the console. If you wish to view
the output of the binaries, you can use:

tail -f /var/log/messages

Or view the viceroy telemetry stream.

[root@user1 ~]#

以上提到的所有脚本和文件都可以在这里找到,但我们不会发布整个 UT 固件,因此您仍然需要自己提取它以运行模拟器。

通信

在物联网设备中,与其他设备的通信通常是至关重要的功能,如果不是主要功能的话。对于我们的设备也是如此,它基本上是一个互联网网关,可以想象,在这种情况下,设备与卫星之间的通信是用户终端的主要功能。我们简要分析了这种通信的物理层,但我们没有专注于它。在更高的层面上,与卫星的通信分为两个“平面”:

  • 数据平面,其中包含与互联网之间的用户流量。
  • 控制平面,其中包含与卫星之间的所有其他类型的流量,例如天线和卫星之间的控制消息(例如连接握手)。

但这并不是设备中发生的唯一通信,接下来的部分中,我们还将看到设备如何与用户前端应用程序进行交互,以及设备内部不同组件之间如何进行通信。

前端应用

与前端应用的通信由 user_terminal_frontend 进程处理,我们能够在模拟环境中运行它并进行逆向工程,这要感谢它采用的编程语言(Go)。通过前端应用,用户可以查看设备的一些统计信息,并可以更改一些高级设置,如 Wi-Fi 凭据、重新启动或折叠天线等。这些交互使用 gRPC(Google 的远程过程调用),它在底层使用 protobuf。Protobuf 定义可以通过从二进制文件中提取(使用 pbtk)或通过询问进程自身的反射服务器(例如使用 grpcurl)来获取。已经实现了一些工具作为替代前端应用程序,它们使用此协议。上述应用程序是用 Python 实现的,它们使用前端二进制文件公开的 gRPC API,为用户提供了一种查看天线统计信息的替代用户界面。这些应用程序的作者可能从移动应用程序或使用反射服务器中获取了协议定义。

在以下代码片段中,您可以看到 Request 消息的(部分)定义,其中包含一些 ID 和列出的请求中的一个特定请求。每个内部请求都有它的定义,其中包括服务器处理请求所需的参数以及将保存结果的相应。

message Request {
    uint64 id = 1;
    uint64 epoch_id = 14;
    string target_id = 13;

    oneof request {
        GetNextIdRequest get_next_id = 1006;
        AuthenticateRequest authenticate = 1005;
        EnableDebugTelemRequest enable_debug_telem = 1034;
        FactoryResetRequest factory_reset = 1011;
        GetDeviceInfoRequest get_device_info = 1008;
        GetHistoryRequest get_history = 1007;
        GetLogRequest get_log = 1012;
        GetNetworkInterfacesRequest get_network_interfaces = 1015;
        GetPingRequest get_ping = 1009;
        PingHostRequest ping_host = 1016;
        GetStatusRequest get_status = 1004;
        RebootRequest reboot = 1001;
        SetSkuRequest set_sku = 1013;
        SetTrustedKeysRequest set_trusted_keys = 1010;
        SpeedTestRequest speed_test = 1003;
        SoftwareUpdateRequest software_update = 1033;
        DishStowRequest dish_stow = 2002;
        StartDishSelfTestRequest start_dish_self_test = 2012;
        DishGetContextRequest dish_get_context = 2003;
        DishGetObstructionMapRequest dish_get_obstruction_map = 2008;
        DishSetEmcRequest dish_set_emc = 2007;
        DishGetEmcRequest dish_get_emc = 2009;
        DishSetConfigRequest dish_set_config = 2010;
        DishGetConfigRequest dish_get_config = 2011;
        // [...]
    }
}

有两种与此 gRPC 通信的方式,一种是使用不安全的通道,另一种是使用安全通道,涉及 TLS 和使用存储在安全元素中的证书进行相互身份验证。移动应用程序和 Web 界面都使用不安全的通道,因此其他某些东西必须使用加密通道。

可以向服务器发出的请求中,许多是为前端应用程序准备的,其中一些示例包括:

  • FactoryResetRequest,请求对天线进行出厂重置。
  • GetDeviceInfoRequest,返回有关设备的一些信息。
  • GetStatusRequest,请求天线的状态。
  • RebootRequest,要求天线重新启动。

但是一些请求看起来并不像是由这些应用程序使用的,比如:

  • SetTrustedKeysRequest,据说为将来由该进程或 SSH 代理使用的提供的公钥设置提供了一种方式。
message SetTrustedKeysRequest {
    repeated PublicKey keys = 1;
}
  • GetHeapDumpRequest,据说返回进程的 Heap 部分的转储。
  • SoftwareUpdateRequest,据说使用提供的更新包启动软件更新。

不幸的是,我们分析的二进制文件中没有实现大多数这些请求(例如 SetTrustedKeysRequestGetHeapDumpRequest),其中一些需要身份验证(例如 DishGetContextRequestDishGetEmcRequest)在传输层(安全的 gRPC 通道)和应用程序层(通过使用 AuthenticateRequest)。我们不完全确定这些请求应该由谁使用,以及为什么大多数请求没有在二进制文件中实现,它们可能被另一个 Starlink 产品(如 Wi-Fi 路由器)使用,或者在设备部分砖化的情况下由 Starlink 支持用于远程协助。最有趣的请求是已经实现且不需要身份验证的 SoftwareUpdateRequest,这将在Fuzzing部分中更详细地解释。

进一步研究这个主题的工作将是分析每个请求处理程序以查找错误,或者更好地对它们进行模糊测试,因为针对 protobuf 协议的有效变异器已经存在,比如 libprotobuf-mutator

进程间通信

运行时的每个进程都在不断地与其他进程共享信息,以协作并共享统计数据。正如您从显示运行时架构的图中所见,每个进程只与用户终端控制通信,该控制器充当整个运行时的协调器。它们使用的协议由SpaceX设计,称为Slate Sharing

该协议使用UDP进行传输,每个进程在回环接口上的不同端口上开始监听,并将从该端口接收来自控制进程的消息。另一方面,控制进程开始在多个端口上进行监听,每个端口对应需要与其通信的一个进程,以便通信是双向的且在进程之间没有冲突。端口号通过配置文件进行配置,该文件可在/sx/local/runtime/common/service_directory找到。在下面的代码片段中,您可以看到其中的一部分,其中列出了软件更新与控制之间以及前端与控制之间的通信端口号。

################################################################################
# Software update
software_update_to_control                localhost   27012       udp_proto
control_to_software_update                localhost   27013       udp_proto

################################################################################
# Frontend
control_to_frontend                       localhost   6500        udp_proto
frontend_to_control                       localhost   6501        udp_proto

每对进程交换不同类型的数据,而消息本身不包含有关它们传输的数据内容或结构的信息,这与其他协议(如JSON或XML)不同。因此,为了理解消息的内容,我们不得不对使用该协议的二进制文件之一进行逆向工程,而在我们的情况下,最佳选择再次是用Go实现的用户终端前端。

数据以二进制形式进行交换,通过发送原始打包的(无填充)C结构,采用大端序。每个消息都包含一个标头,其中包含有关消息的一些信息,以及一个包含要共享的实际数据的主体。在下面的代码片段中,您可以看到表示消息标头的数据结构,它来自GolangAnalyzer Ghidra插件。使用相同的技术,我们还提取了前端进程与控制之间的消息主体的结构。

**************************************************************
* Name: sx_slate.SlateHeader                                 *
* Struct:                                                    *
*   +   0x0    0x4 uint BwpType                              *
*   +   0x4    0x4 uint Crc                                  *
*   +   0x8    0x8 longlong Seq                              *
*   +  0x10    0x4 uint Frame                                *
**************************************************************    

标头包含:

  • BwpType,这是一个固定值,充当协议的“magic number”(00 00 01 20)。
  • Crc,其名称表明它是循环冗余校验,因此是消息主体的一种错误检测码,但通过逆向工程和嗅探消息,此字段似乎也是固定的,但对于每一对消息来说都是不同的。
  • Seq,这是一个顺序号,每条消息递增,但该协议不包括任何确认机制以重新发送丢失的消息。
  • Frame,在分片的情况下使用,即当消息大于MTU(最大传输单元)时,MTU通常设置为1500字节。在这种情况下,消息的主体分为多个帧,每个帧都有相同的标头,除了Frame字段,该字段从0开始,每个帧递增。

消息的主体以相同的方式编码,例如,在下面的代码片段中,您可以看到由前端进程发送到控制的消息结构的一部分。

**************************************************************
* Name: slate.FrontendToControl                              *
* Struct:                                                    *
*   +   0x0    0x1 bool AppReboot                            *
*   +   0x1    0x1 bool TiltToStowed                         *
*   +   0x4    0x4 uint StartDishCableTestRequests           *
*   +   0x8    0x1 bool IfLoopbackTest                       *
*   +  0x10    0x8 longlong Now                              *
*   +  0x18    0x8 longlong StarlinkWifiLastConnected        *
[...]    

有了这些信息,我们已经能够实现一个消息解码器(我们已经实现了),但这只能用于前端进程和控制之间的通信。要解码其他通信,我们需要手动逆向工程每个其他二进制文件,找出消息的结构,可能甚至找不到字段名称,但正如前面的部分所解释的,从C ++二进制文件中获得有用的信息是困难的。在这里,您可以看到我们如何使用Python和ctypes包解析Slate消息的标头。

class SlateHeader(BigEndianStructure):
    _pack_ = 1
    _fields_ = [
        ('BwpType', c_uint32),
        ('Crc', c_uint32),
        ('Seq', c_longlong),
        ('Frame', c_uint32)
    ]

然后,为了了解在C++二进制文件中如何处理协议,我们在控制过程中查找了一些我们知道的结构的字段(前端进程的结构),控制过程应该具有这些字段以便解码传入的消息。我们在二进制文件中找不到它们,但我们找到了一些更好的东西,在/sx/local/runtime/common文件夹中有一组配置文件,例如frontend_to_control,其中包含进程之间交换的每个消息的结构。以下是包含上述配置文件一部分的代码片段。

# Slate share message from gRPC frontend process to main control process.
app_reboot                          bool
tilt_to_stowed                      bool
start_dish_cable_test_requests      uint32
if_loopback_test.enabled            bool
now                                 int64
starlink_wifi_last_connected        int64
# [...]

有了这个,实现一个更通用的解码器就容易得多,它可以解析这些协议定义并相应地解码消息。已经开发了这样一个工具,将在下一节中讨论。

Slate sniffer & injector

Slate消息发送非常快,因此如果没有适当的可视化工具,很难理解发生了什么。这就是为什么我们实现了Slate sniffer的原因,这是一个在环回接口上嗅探UDP数据包并即时解码它们的工具,突出显示连续消息之间的差异。在下图中,您可以看到新工具的总体架构,我们将更详细地描述这个工具。这个工具是为模拟环境实现的,但我们设计它时考虑到它也需要在真实的天线上工作。因此,大部分工作都是在Sniffer中完成的,而Sniffer并不在设备上,Sniffer与天线之间的所有通信都通过SSH进行。

img

启动嗅探器时使用的第一个组件是协议定义解析器,它将解析配置文件:

  • service_directory,以了解哪些“服务”(即消息定义)可用,以及它们将在哪些UDP端口上进行通信。
  • 对于每一对可用的进程P1P2[P1]_to_[P2],以了解将要交换的消息的格式。
    此组件将创建SlateMessageParser对象,稍后将用于解码消息。解码使用struct Python包完成。

之后,通过SSH在天线上启动TCPdump,并将其侦听在环回接口上,仅捕获目标端口是解析器找到的端口之一的UDP数据包。TCPdump的输出被传送到Scapy,它将解码数据包,通过读取目标端口了解它来自哪个服务,然后提取UDP有效负载并传递给消息解码器。当消息被正确解析时,它将存储在Storage组件中,该组件是一个简单的内存数据库,仅保存最近的消息(可配置,基于可用内存)。在所有这些之上,有一个Flask服务器,提供一些API,以了解哪些服务可用,了解消息的架构,当然还可以获取消息。我们还实现了一个前端,作为一个简单的Web界面,如下图所示。从当前的前端,可以实时查看消息,通过突出显示的更改来发现差异,选择要显示的字段,并对它们进行过滤或排序。前端模块易于替换,由于暴露了API,因此可以将更复杂的接口集成到嗅探器中,以更多种方式检查获取的数据。

img

在有了查看消息的方式之后,我们认为能够注入自定义消息会很有趣,无论是通过编辑我们接收到的消息还是从头开始创建新的消息。因此,我们实现了Slate注入器,它与嗅探器共享大部分代码库。该工具的架构如下所示。为了使进程能够接收消息,消息需要来自环回接口,因此我们无法直接从注入器(外部设备)发送它们。这就是为什么注入器将在设备上启动一个socat服务器的原因,该服务器将在“外部”网络接口上侦听UDP消息,然后将其转发到正确的UDP端口,同时将源地址更改为localhost。已实现了一些API端点,以便从前端注入消息,当前界面允许您编辑和发送消息或创建新消息。

img

能够检查进程之间的消息在没有完全逆向工程的情况下帮助我们很多,此外,拥有协议定义和一种轻松注入消息的方式,项目的一个自然发展是对协议进行模糊测试,这将在“Fuzzing”部分讨论。Slate嗅探器、注入器和模糊器的完整代码可以在这里找到,但同样没有协议定义,您需要从设备中提取协议定义。

Fuzzing

在我在Quarkslab的实习的最后阶段,我们确定了已经经过足够分析以进行模糊测试的软件部分。为了找到一个适合进行模糊测试的目标,我们必须考虑多个方面:

  • 在找到漏洞的情况下,可能的攻击向量:
    • 漏洞是否可以由经过身份验证的用户触发?
    • 攻击者是否需要连接到天线盘的Wi-Fi网络?
    • 漏洞是否可以直接从互联网触发?
    • 攻击者是否需要已经访问了天线盘?在这种情况下,该漏洞可能被用于在天线盘内进行横向移动或提升权限。
  • 目标出现潜在漏洞的影响是什么?
  • 目标有多容易进行模糊测试,以及哪种类型的模糊测试最适合目标:
    • 输入是如何提供的?
    • 目标程序是否可以隔离运行?如果程序使用许多硬件外围设备和/或与运行时的其他组件进行交互,将难以在隔离环境中进行模糊测试。
    • 我们对目标的内部运作有多深入的了解?

sxverity

正如我们在通信部分所看到的,可以通过从内部网络向前端进程发送SoftwareUpdateRequest来触发软件更新。这是一个有趣的请求,因为它似乎不是为用户设计的唯一一个,而且不需要身份验证。此外,该请求的输入是更新包,可能非常大,而其他请求的输入通常为空或非常简单。在将更新包发送到设备之前,必须将其拆分为多个块。以下是发送此消息的Python脚本。

CHUNK_SIZE = 16384

def update(data):
    channel = grpc.insecure_channel('192.168.100.1:9200')
    stub = device_pb2_grpc.DeviceStub(channel)

    stream_id = int.from_bytes(os.urandom(4), byteorder='little')

    for i in range(0, len(data), CHUNK_SIZE):
        chunk = data[i:min(len(data), i + CHUNK_SIZE)]
        request = device_pb2.Request(id = 1, epoch_id = 1, target_id = "unknown",
            software_update = common_pb2.SoftwareUpdateRequest(
                stream_id = stream_id,
                data = chunk,
                open = i == 0,
                close = i + CHUNK_SIZE >= len(data)
            )
        )

每个消息都需要具有相同的 stream_id,该 ID 可以是随机生成的。第一个消息具有 open 标志,而最后一个消息具有 close 标志,而中间的消息则没有这两者。消息的接收者是前端进程,它将更新包保存在一个临时文件夹中,而不读取其内容,因此没有执行任何输入验证,仅会检查包的大小是否未达到硬编码的阈值。之后,前端进程将通知控制进程准备好了可以应用的 sideload 更新,通过 Slate 消息,控制进程将同样的消息传递给软件更新进程。一旦后者收到消息,更新就准备开始了。下图显示了 SoftwareUpdateRequest 触发的消息和操作的整体流程。

img

在通知软件更新进程准备好应用软件更新包后,更新过程开始。从这一刻起,这种软件更新与标准软件更新没有区别,后者是从 Starlink 的后端下载更新包。更新包是一个 sxverity 镜像,将由同名程序进行验证,然后会挂载内部的 rom1fs 文件系统。一旦挂载,软件更新进程将在挂载点查找分区镜像。每个分区镜像还将具有 SHA512 散列值,用于进行额外的完整性验证。最后,每个可用的分区镜像将被刷写到相应的 /dev/blk/other/* eMMC 逻辑分区。

更新包不会直接被软件更新进程访问,因此实际上读取提供的输入内容的第一个进程是 sxverity。因此,可以直接在该二进制文件上执行任何模糊测试,跳过所有之前的步骤。在下图中,您可以看到 sxverity 如何执行验证过程。可模糊测试的代码非常有限,因为签名验证是由一个库进行的,该库超出了我们的范围,一旦成功验证签名,发生在之后的任何事情都被视为我们无法达到的状态,因为如果我们能够达到那个状态,这意味着我们能够制作一个将被刷写的更新包,因此我们不需要在那里找到其他的错误。

img

代码测试的唯一会被解析的输入部分是图像的头部,因此这将是模糊测试器变异的唯一输入部分。由于程序的这一部分可以在完全隔离的环境中执行,我们在 unicorn 内进行了模糊测试,unicorn 是一个轻量级的 CPU 模拟器,可以通过 Python 进行指令控制,使用其绑定。第一步是能够模拟我们想要测试的代码,并为我们的模糊测试器设置测试环境,其中包括:

  • 加载二进制文件。
  • 在代码中找到一个良好的起始点,在此轻松设置整个环境,例如将输入放置在正确的内存位置,并设置将由测试代码使用的所有其他内存结构。例如,以下代码片段显示了如何将输入放置在内存中以及如何设置保存输入位置地址的寄存器。
def place_input_cb(mu: Uc, content: bytes, persistent_round, data):
    content_size = len(content)

    if content_size < INPUT_MIN_LEN:
        return False

    pubkey, header = key_and_header_from_data(content)

    # write data in memory
    mu.mem_write(PUBKEY_ADDR, pubkey)
    mu.mem_write(HEADER_ADDR, header)

    # prepare function arguments
    mu.reg_write(UC_ARM64_REG_X2, PUBKEY_ADDR) # pubkey address
    mu.reg_write(UC_ARM64_REG_X3, 0x40) # nblocks
    mu.reg_write(UC_ARM64_REG_X4, HEADER_ADDR) # header buffer address

    return True
  • 识别函数调用,挂钩它们并在 Python 中进行模拟,这样我们就不需要测试库代码,也不必将它们加载到内存中并处理动态加载的库。以下是一个示例,展示了如何挂钩和模拟 libc 函数 memcpy
if address == MEMCPY_ADDR:
    # read arguments from registers
    dest = mu.reg_read(UC_ARM64_REG_X0)
    src = mu.reg_read(UC_ARM64_REG_X1)
    n = mu.reg_read(UC_ARM64_REG_X2)

    # read the data from src
    data = mu.mem_read(src, n)
    # write data in dst
    mu.mem_write(dest, bytes(data))
    # return the address of dest 
    mu.reg_write(UC_ARM64_REG_X0, dest)
    # jump to the return address
    lr = mu.reg_read(UC_ARM64_REG_LR)
    mu.reg_write(UC_ARM64_REG_PC, lr)
  • 识别一个结束点,这个点在程序中必须是一个我们停止模拟的地方,因为运行是成功的(没有错误bug)。

使用 Unicorn 的最好之处,除了配置和指导相当容易之外,还在于它与 AFL++(American Fuzzy Lop plus plus)的无缝支持,我们将其用作模糊测试工具。AFL++与 Unicorn 配合使用可以检测崩溃,更重要的是可以以透明的方式收集覆盖信息,以便进行基于覆盖率的突变。使用 Unicorn 设置模糊器相当简单。模糊器还需要一些初始测试用例,称为种子,我们使用了一些有效的头部(从在设备中找到的 sxverity 映像中提取)和一些随机生成的头部。

模糊器运行了大约 24 小时,执行了超过一百万次,但不幸的是,没有记录到崩溃。这是预期的,因为被测试的代码库非常有限,输入的结构非常简单,没有复杂的数据结构或可变长度字段,因此大多数与内存相关的常见错误都被规避了。

Slate 消息

我们用模糊测试进行测试的另一个组件是进程间通信(IPC)——在前面的部分中进行了深入分析——因为我们已经开发了一套工具来分析和篡改这种通信。在这种情况下,我们不会对单个二进制文件进行模糊测试,而是对组成设备运行时的整套进程进行测试,因为它们每个都在使用 Slate 消息进行通信。与我们用于 sxverity 的灰盒模糊测试方法完全不同,因为:

  • 我们试图测试的代码库非常庞大。
  • 我们无法精确定位处理每个二进制文件中的 Slate 消息的代码,而且更重要的是,由于对输入的错误解释导致程序状态的一些不一致,错误也可能出现在此代码之外。
  • 二进制文件需要在类似于设备的环境中运行,因为它们不断与系统的其他组件进行交互,其中大多数甚至不在我们的模拟环境中运行。
  • 此外,由于我们没有源代码重新编译它们,要记录覆盖率将是具有挑战性的,而且模糊器需要在设备上运行。

出于上述原因,我们使用了黑盒模糊测试,没有覆盖率引导的突变,通常被称为“愚蠢”的模糊测试。我们使用了 Boofuzz 作为模糊器,它是一个简单易用的专为网络协议设计的模糊器,非常适合我们所寻找的内容。Boofuzz 不会以完全随机的方式生成输入,因为您会向其提供用于通信的协议定义,还可以使用有限状态机定义消息的序列。在我们的情况下,每个消息与其他消息无关(除了序列号),因此定义消息的格式就足够了。模糊器将突变消息的每个字段,尝试一些可能触发错误的值,例如对于 int32,模糊器将尝试值,如 {0, 1, -1, INT_MAX, -INT_MAX, ...}。例如,以下是 Slate 消息的一些字段如何在 Boofuzz 协议定义中被“转换”的示例。

if param.dtype.name == "BOOL":
    return Simple(
        name=param.name,
        default_value=b"\x00\x00\x00\x00",
        fuzz_values=[b"\x00\x00\x00\x00", b"\x00\x00\x00\x01"],
    )
if param.dtype.name == "INT8" or param.dtype.name == "UINT8":
    return Byte(name=param.name)
if param.dtype.name == "INT32" or param.dtype.name == "UINT32" or param.dtype.name == "FLOAT":
    return DWord(name=param.name)

Slate 消息协议中使用的每种数据类型都可以使用 Boofuzz 的标准类型进行编码,除了 Sequence number,它需要存储内部状态以在每次迭代时递增自身,您可以在以下代码片段中看到其实现。像 BwptypeCrc 这样的静态字段可以使用 Boofuzz 的 Static 类型进行编码。

class SequenceNumber(Fuzzable):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs, fuzzable=False, default_value=0)
        self._curr = 0

    def encode(self, value: bytes = None, mutation_context=None) -> bytes:
        curr = self._curr
        self._curr += 1
        return int.to_bytes(curr, length=8, byteorder="big")

一旦消息结构被定义,fuzzer 就可以使用 Slate 注入器中的代码发送消息。此时唯一需要实现的组件是能够检测在发送消息后程序是否崩溃的内容。起初,我们通过 SSH 发出 pgrep 命令,但这会增加减缓 fuzzer 速度的开销。因此,我们实现了一个简单的脚本,运行在 dish 上,打开一个 TCP 套接字并等待连接,然后用于与 fuzzer 直接通信。将运行在客户端(fuzzer 机器)上的进程监视器的部分可以集成到 fuzzer 中,通过继承 Boofuzz 的 BaseMonitor 并实现其方法,如 alive(检查目标进程是否仍然存活)和 restart_target(重新启动目标进程)。下图显示了最终的体系结构。

img

通过对 control_to_frontend 协议进行 fuzzer,发现了一些崩溃问题,但似乎没有发现可以以其他方式利用的漏洞,除了简单地导致程序崩溃,对前端应用程序造成拒绝服务。这是因为前端进程是一个 Go 二进制文件,Go 运行时通过 panic 函数使进程崩溃,因为它检测到有可疑的事情正在发生。

例如,以下是其中一个崩溃的详细信息。在以下代码片段中,你可以看到 Go 运行时在崩溃时生成的部分堆栈跟踪,从中可以理解到崩溃是由 UpdateObstructionMap 函数引起的,该函数尝试分配过多的内存。

fatal error: runtime: out of memory

goroutine 5 [running]:
[...]
runtime.(*mheap).alloc(0x5046800?, 0x28234?, 0xd0?)
[...]
main.(*DishControl).UpdateObstructionMap(0x40003be000, {0x7a90f8?, 0x4000580380?})
[...]

通过进一步检查这个函数,我们了解到障碍图是如何传输到前端进程的。首先,障碍图是天线上方的天空的三维地图,指示天线是否可以清晰看到天空,或者是否被障碍物(如树木或其他建筑物)阻挡,用户可以从前端应用程序中看到这些信息。这张地图不是由前端进程生成的,因此必须通过 Slate 消息发送给它。

obstruction_map.config.num_rows                                                 uint32
obstruction_map.config.num_cols                                                 uint32
obstruction_map.current.obstructed                                              bool
obstruction_map.current.index                                                   uint32

在上面的代码片段中,您可以看到携带有关障碍图信息的消息结构定义的一部分。障碍图在内存中表示为一个矩阵,其中每个点可以被阻挡或不阻挡。控制进程通过为矩阵中的每个点设置正确的index并将 obstructed 设置为 truefalse,通过发送多个 Slate 消息来发送这些信息。矩阵的大小不是固定的,其维度可以由控制进程使用消息中的 num_rowsnum_cols 字段进行设置。这就是 bug 的所在之处,事实上,在这两个字段中发送大的值时,程序会尝试为矩阵分配足够的内存,并因此而发生 panic。

len = ObstructionMapConfigNumCols * ObstructionMapConfigNumRows;
if (len == (this->obstructionMap).snr.__count)
    goto LAB_0050b7f4;
(this->obstructionMap).numRows = ObstructionMapConfigNumRows;
(this->obstructionMap).numCols = ObstructionMapConfigNumCols;
puVar5 = runtime.makeslice(&datatype.Float32.float32,len,len);

上面的代码片段显示了处理 Slate 消息时前端二进制文件的反编译和注释代码。第1行计算矩阵的大小,第2行将其与程序当前在内存中的矩阵大小进行比较,如果两者不同,则在第4和第5行更新内部内存结构中的维度,然后使用 Go 运行时的 makeslice 方法分配新矩阵,该方法出现在第6行。正如您所看到的,对要分配的大小以及两个给定维度之间的乘法结果都没有进行检查。在 C 语言中,这可能非常危险,但是 Go 运行时会自动处理所有边界情况,通过检查所请求内存的大小是否为正且不太大。Go 运行时还会检查每个数组访问,否则,通过操作矩阵的索引和大小,可能会导致任意写入。

请注意,此 bug 仅可通过向绑定到仅限本地主机的服务发送精心制作的 UDP 数据包触发。因此,无法从外部网络触发它。此外,UT 的 iptables 配置会过滤掉传入的 UDP 数据包,因此使用伪造的具有 localhost 源 IP 的数据包也行不通。因此,我们并未将此视为漏洞,而只是一个 bug。

在我们实施了模糊测试并在模拟器中使用后,Starlink为我们提供了一台已 root 的 UT,然后我们确认了真实设备上存在上述 bug,并对在模拟器中无法运行的一些其他进程进行了模糊测试。

结论

你可以在我即将于年底发表的硕士论文中找到更多详细信息,敬请期待!提供的工具和脚本可以在此存储库中找到。

这项工作和我们发布的工具旨在供进一步研究 Starlink 用户终端使用。不幸的是,由于一些技术问题和时间限制,我们未能完全检查卫星通信中使用的网络堆栈和协议,但希望将来可以利用运行时高级管理功能的这个知识库来协助进行该方面的研究。

我鼓励对这个主题进行更多的研究,因为 SpaceX 的安全团队愿意提供帮助,并且他们提供了一些丰厚的赏金。

特别感谢:

  • 我的实习导师Maxime Rossi Bellom,在这项研究中给予我的指导。
  • Lennert Wouters,他是关于转储固件和对Starlink用户终端进行故障注入攻击的博客文章的作者,在我们研究的早期阶段为我们提供帮助。
  • 来自SpaceX安全团队的Tim Ferrell,为我们提供了一台带有root访问权限的测试设备。
  • Ivan Arce、Salwa Souaf和Guillaume Valadon对我的博客文章进行审查。
  • 许多其他出色的同事,在他们的专业领域帮助我解决问题。

原文链接:Diving into Starlink’s User Terminal Firmware

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值