嵌入式 Linux 安全简介

1 安全概念

安全就是降低风险。

一方面所有者,即产品或服务的受益者(用户、制造商、企业主等)。所有者希望保护资产,即产品或服务中任何有价值的东西(数据、代码、声誉等)。

另一方面威胁行为者,即能够表现出威胁的人或事物(恶意黑客等),即任何能够以可能造成伤害的方式对资产采取行动的东西。

为了体现威胁,威胁者将通过攻击矢量(威胁者用于访问或渗透目标系统的方法或途径)来探索漏洞(系统中的薄弱环节)。

归根结底,这就是所有者和威胁者之间的猫鼠游戏。所有者会在多大程度上保护资产?威胁者会在多大程度上破坏资产?这实际上取决于资产的价值。事实上,所有者和威胁行动者对价值的认识可能并不相同。

确定资产(及其价值)以降低被入侵的风险,可以通过威胁建模的过程来完成。

2 威胁建模

威胁建模是一个可以识别、列举潜在威胁并确定优先缓解措施的过程。它基本上是一个风险评估过程,在这个过程中,你要评估资产的价值和保护资产的成本。威胁建模的结果就是产品的威胁模型。

在威胁建模过程中,有几种技术和方法可以提供帮助,包括STRIDE、DREAD、VAST、OCTAVE等。

STRIDE 模型是一种非常有用的威胁分类工具。它由微软开发,其名称是六种主要威胁类型的首字母缩写: 欺骗(Spoofing)、篡改(Tampering)、抵赖(Repudiation)、信息泄露(Information disclosure)、拒绝服务(Denial of service)和权限升级(Escalation of privileges)。STRIDE 可用于识别系统资产可能面临的所有威胁。

DREAD方法是一种评估计算机安全威胁风险的工具。其名称是五类安全威胁的首字母缩写: 损害(Damage:攻击的严重程度)、可重现性(Reproducibility:重现攻击的难易程度)、可利用性(Exploitability:发起攻击的工作量)、受影响用户(Affected users:有多少人会受到影响)和可发现性(Discoverability:发现威胁的难易程度)。

STRIDE 模型有助于识别威胁,而 DREAD 方法则有助于对威胁进行排序。对于系统中的每一个威胁,您都要仔细研究每个威胁类别,并将其分为低(1 分)、中(2 分)或高(3 分)。最后,您将得到一份威胁和缓解策略的等级列表。举例说明:

我们可以看到,威胁建模将为我们提供一个非常清晰的视图,说明我们要保护什么、我们计划如何保护以及相关的成本。这是产品威胁模型的一部分,需要在每个开发周期重新评估。因此,威胁模型将提供一份需要优先处理的威胁清单,这样我们就可以集中精力实施缓解措施,提高产品的安全性。

如何保护代码的完整性和真实性?如何确保数据的私密性?在哪里存储加密密钥?如何最大限度地降低应用程序被利用的风险?让我们从安全启动开始,尝试回答所有这些问题以及更多问题!

3 安全启动

如何确保运行的代码是由值得信赖的个人或公司创建的?实施安全启动程序。

安全启动程序的目的是保护代码的完整性和真实性。

安全启动通常基于数字签名的验证。嵌入式 Linux 系统通常有三个主要组件:引导加载程序、内核和根文件系统(rootfs)。所有这些组件都有签名,并在启动过程中对签名进行检查。

让我们看看NXP iMX6 设备上的一个实际例子。

一切都始于SoC内部的ROM代码。在恩智浦iMX6上,有一个名为"High Assurance Boot"(HAB)的硬件组件,它能够验证第一阶段启动加载程序的签名,从而实现安全启动过程。iMX6设备内的高保真启动也可称为"信任根"(Root of Trust),因为如果它被破坏,所有安全启动过程也将受到破坏。

iMX6 SoC内的ROM代码使用HA 组件检查引导加载程序的签名。为此,需要生成一对密钥(公钥和私钥),用私钥签署引导加载程序,并将公钥存储在SoC 内。iMX6使用OTP保险丝来存储密钥。实际上,为了降低成本,SoC中只存储了公钥的哈希值。

引导加载程序启动时(如 U-Boot),必须检查Linux内核的签名。为此,通常使用一种名为FIT映像的映像格式。FIT映像是支持散列和签名的多个二进制文件的容器,通常包含Linux内核映像、设备树文件和初始 ramdisk。生成一对密钥后,我们需要用私钥签署FIT映像中的二进制文件,并配置U-Boot使用公钥检查FIT映像的签名。

内核启动后,将运行内存盘镜像中的启动程序。在挂载之前,ramdisk将具有验证最终根文件系统完整性的逻辑。有几种方法可以实现这一点。一个常见的选择是使用设备映射器真实性(dm-verity)内核模块。dm-verity 内核模块可对块设备进行完整性检查,并要求使用只读 rootfs(squashfs 就是一个很好的解决方案)。如果想要一个可读写的根文件系统,还可以使用IMA或dm-integrity等其他方法。

下面是整个安全启动过程的示意图:

2017年7月17日,多个恩智浦设备(i.MX6、i.MX50、i.MX53、i.MX7、i.MX28 和 Vybrid 系列)的ROM代码中的安全启动漏洞被公开披露。如果你的信任链遭到破坏,一切都会受到影响!因此,我们需要警惕这类漏洞(在这种情况下,它们已被新硅片修复)。

虽然安全启动可以确保真实性和完整性,但它并不能保护设备不被伪造或威胁者从设备中提取代码/数据。因此,如果要保护知识产权或确保数据保密性,就需要使用加密技术。

4 代码和数据加密

您可能希望对嵌入式 Linux 设备中的数据或代码进行加密。

当你需要保护用户的隐私和机密时,数据加密是一种常用的方法。数据是设备执行过程中产生的任何信息,包括数据库、配置文件等。

代码加密取决于具体情况,对整个根文件系统进行加密并不常见。通常情况下,大部分组件都是免费开源软件,因此没什么好隐藏的。此外,还有GPLv3和Tivo化的问题(使用任何GPLv3软件都会迫使你为用户提供一个更新软件的机制,如果你要对软件进行加密,这将会变得更加困难)。更常见的用例是只加密你为设备开发的应用程序。这通常是你的知识产权所在。

Linux中基本上有两种主要的加密方法:全盘加密和基于文件的加密。

全磁盘加密提供块级加密,整个磁盘或磁盘分区都会被加密。为此,我们可以使用Linux内核的设备映射器加密目标dm-crypt。

基于文件的加密提供文件系统级加密,每个目录都可以单独加密,并可选择使用不同的密钥。fscrypt是一些文件系统(如 EXT4、UBIFS 和 F2FS)上可用的API,而 eCryptFS则是一种更通用的解决方案,它是在现有文件系统上堆叠的一层。

5 加密密钥

由于在加密中使用非对称密钥算法速度太慢,因此通常使用对称密钥算法。这意味着加密和解密使用相同的密钥,而且密钥应存在于文件系统中的某个位置,这样才能解密加密的代码/数据。

但我们不能就这样把密钥留在文件系统中,对吗?

有几家公司就是因为没有意识到这一点而付出了代价。例如,下图来自第一代Xbox视频游戏机。安全研究员安德鲁-黄(Andrew "bunnie" Huang)开发了一种专门的FPGA硬件,用于嗅探通信总线,并从设备中提取加密密钥。这只是因为密钥就在那里,以明文形式在通信总线中传输,没有任何保护措施(顺便说一句,Andrew Huang 是一本非常好的硬件黑客书籍《Hacking the Xbox》的作者,你可以在他的网站上免费下载)。

结论是加密代码/数据的保护与解密密钥的保护同样安全!

6 密钥存储技术

在台式机或智能手机上,用于加密文件系统的密钥可以通过交互输入的用户密码(口令)获得。例如,安卓智能手机就是这样工作的。

在嵌入式系统中,我们通常不会在每次启动设备时通过用户交互从密码中获取密钥。因此,密钥应加密存储在文件系统或安全存储器中。为此,我们需要硬件支持。

例如NXP i.MX 处理器有一个唯一的主密钥(NXP预先编程),只有名为CAAM(加密加速器和保证模块)的特殊硬件模块才能访问该密钥。该硬件设备可用于加密密钥并将其存储在文件系统中(这必须在制造过程中完成)。在启动过程中,CAMM模块将用于解密密钥,并恢复用于解密文件系统的普通密钥。由于CAAM模块内的密钥无法访问,因此加密密钥受到了保护。

如果处理器没有安全功能,也可以通过安全元件或TPM设备等外部硬件实现相同的效果。这些外部设备通常提供安全存储,因此可以用来存储主密钥,用于加密/解密文件系统加密密钥。这些设备还提供许多安全功能,如随机数生成、散列计算、加密和签名功能等。

安全元件是一种安全计算系统。它基本上是带有自己的安全应用程序的安全存储(通常使用Java卡实现,但不是必须的)。安全元素的功能非常开放,取决于实现方式,但大多数安全元素都执行公钥密码标准11(PKCS#11)。安全元件的例子有智能卡和SIM卡。

TPM(可信平台模块)是一种规范和国际标准(ISO/IEC 11889)。TPM不是安全元件,但可以在安全元件内实施。它可以在硬件或软件中实现,但大多数是在硬件中实现的。它提供一套由标准定义的有限安全功能,包括安全存储和加密功能。

安全存储的第三种选择是使用可信执行环境。TEE(Trusted Execution Environment)是一种环境,在这种环境中,执行的代码和访问的数据是隔离的,并在保密性(任何人都无法访问数据)和完整性(任何人都无法更改代码及其行为)方面受到保护。我们身边有很多设备都在使用可信执行环境,包括智能手机、机顶盒、游戏机和智能电视。

最后,如果您希望在嵌入式Linux设备中使用加密技术,那么从项目一开始就必须考虑密钥的存储和管理问题。

7 分层安全

到目前为止我们已经讨论了如何通过安全启动和加密来提高嵌入式Linux设备的安全性。但这还不够。我们应该始终分层考虑安全问题,每一层都应用一种缓解技术,在其他层的基础上提高设备的安全性。将多种缓解技术结合起来,攻击者就更难入侵设备。

想想看。您可以通过安全启动和加密来保护您的代码和数据,但如果您运行的应用程序存在可能被利用的漏洞,您的资产仍然会面临风险。如果应用程序存在攻击向量(用户输入、配置文件、网络通信等),漏洞就有可能被利用。特别是在使用C和C++等不安全内存语言编写的程序中,缓冲区溢出等漏洞可能被用于堆栈粉碎和格式字符串等攻击。

8 安全编码

如果应用程序存在攻击向量(用户输入、配置文件、网络通信等),那么漏洞就可能被用来利用系统。

特别是使 C/C++等不安全内存语言编写的程序,缓冲区溢出等漏洞可能被用于堆栈粉碎和格式字符串等攻击。

举个例子,在 Linux 内核(从 2.6.34 版到 5.2.x)中发现了一个缓冲区溢出漏洞,该漏洞存在于vhost功能将virtqueue缓冲区转换为 IOV的方式中。在迁移过程中,拥有特权的客户机用户可以将长度无效的描述符传递给主机,从而利用这一漏洞提高他们在主机上的权限。

diff --git a/drivers/vhost/vhost.c b/drivers/vhost/vhost.c
index 34ea219936e3f..acabf20b069ef 100644
--- a/drivers/vhost/vhost.c
+++ b/drivers/vhost/vhost.c
@@ -2180,7 +2180,7 @@ static int get_indirect(struct vhost_virtqueue *vq,
	/* If this is an input descriptor, increment that count. */
	if (access == VHOST_ACCESS_WO) {
		*in_num += ret;
-			if (unlikely(log)) {
+			if (unlikely(log && ret)) {
				log[*log_num].addr = vhost64_to_cpu(vq, desc.addr);
				log[*log_num].len = vhost32_to_cpu(vq, desc.len);
				++*log_num;
@@ -2321,7 +2321,7 @@ int vhost_get_vq_desc(struct vhost_virtqueue *vq,
			/* If this is an input descriptor,
			 * increment that count. */
			*in_num += ret;
-			if (unlikely(log)) {
+			if (unlikely(log && ret)) {
				log[*log_num].addr = vhost64_to_cpu(vq, desc.addr);
				log[*log_num].len = vhost32_to_cpu(vq, desc.len);
				++*log_num;

此漏洞已在CVE-2019-14835中注册,并于2019年得到修复。实际上,虚拟(客户)机中的用户可以利用此漏洞获得主机的 root 访问权限。这个漏洞(还有许多其他漏洞)在Linux内核中存在了好几年!

归根结底,软件总会有漏洞,但我们可以尽量减少它们。为此,我们可以使用静态分析工具。

9 静态分析工具

静态分析工具能够分析源代码(无需运行程序),在运行时发现问题。这些工具可以发现空指针取消引用、内存泄漏、整数溢出、越界访问、初始化前使用等程序错误!

有许多优秀的开源工具(cppcheck、splint、clang 等)和商业选择(Coverity、PC-Lint 等)可用于静态代码分析,从编译器开始,编译器通常有内置的静态分析工具,可在编译代码时生成警告或错误。这就是为什么我们决不能忽视编译器警告的原因,对吗?)

因此,要将应用程序中存在安全漏洞的风险降至最低,第一步就是绝不忽视编译器警告并使用静态分析工具。但有些缺陷很难,有时甚至不可能在源代码层面上识别出来。因此,我们可能需要在应用程序中添加运行时保护。

10 运行时保护

运行时保护可以对应用程序进行动态分析。这意味着您的程序将具有在运行时执行的保护和检查功能。

例如,AddressSanitizer (ASan) 是Google安全研究人员创建的一个非常有趣的工具,用于识别C和C++程序中的内存访问问题。在启用AddressSanitizer的情况下编译C/C++应用程序的源代码时,程序将在运行时被检测,以识别并报告内存访问错误(不适合linux+ARM)。

更多参考: https://sergioprado.blog/finding-memory-bugs-with-addresssanitizer/。

另一个例子是ASLR(地址空间布局随机化),这是一种计算机安全技术,可随机排列进程关键数据区域(文本、堆栈、堆、库等)的地址空间位置。因此,如果你关心嵌入式 Linux 设备的安全性,就应该启用ASLR,至少是在Linux内核中。

Valgrind是另一个非常有用的工具,可以帮助检测内存相关问题,如泄露和数据竞赛。

当然,这其中也有取舍。虽然这些工具可以在运行时识别错误和安全漏洞,但它们可能会影响应用程序的性能,并增加调试系统的难度。

此外,要使用这些工具发现错误,还必须确保存在错误的代码部分能够运行。更好的办法是,应用程序的测试覆盖率应接近100%。模糊测试工具可以帮助实现这一点。

11 模糊测试工具

模糊测试是一种自动化软件测试技术,包括提供无效、意外或随机数据作为程序的输入。

然后监控程序是否出现异常,如崩溃、内置代码断言失败或潜在的内存泄漏。

目前有很多免费的开源模糊测试工具,包括AF(美国模糊测试循环)和 syzkaller(Linux 内核模糊测试工具 https://github.com/google/syzkaller)。

这确实是安全研究人员和威胁行为者用来查找软件安全漏洞的工具。有时,他们甚至自己编写模糊工具,而不是使用流行的框架。这需要付出代价,因为一些安全研究人员通过BugBounty平台发现软件中的漏洞,赚取了数百万美元(https://www.zdnet.com/article/seven-hackers-have-now-made-a-million-dollars-each-from-bug-bounties-says-hackerone/ )。

12 权限

减少漏洞利用的一种方法是不使用root(超级用户)权限运行程序!你应该利用操作系统的访问控制机制,使用非特权用户运行进程,并在只允许访问运行所需资源的组内运行进程。

这就是所谓的最小权限原则,也是设计安全系统的规则之一。应用程序在运行时,应仅拥有完成工作所需的权限。

13 Linux capabilities

Linux capabilities是一种针对以root权限运行的进程的细粒度访问控制系统。

Linux内核将与超级用户相关的权限划分为不同的单元,称为能力,这些能力可以独立启用或禁用。因此,我们的想法是编写一个能以root 身份运行的程序,但只启用其工作所需的功能。

你可以使用getcap工具列出特定程序运行所需的功能:

$ getcap /usr/bin/ping
/usr/bin/ping = cap_net_raw+ep

14 强制访问控制(Mandatory Access Control)

Linux传统上支持全权访问控制(DAC Discretionary Access Control)。DAC 是一种访问控制类型,它根据主体和/或所属组(实际上就是我们习惯的用户和组标志)的身份来限制对对象的访问。

另一种访问控制类型称为强制访问控制(MAC)。MAC 指的是一种访问控制类型,即操作系统限制主体访问对象或对对象执行某种操作的能力。

MAC是通过Linux安全模块(LSM)在内核中实现的,LSM是一个允许Linux内核支持各种计算机安全模型的框架。

实现强制访问控制的两个Linux安全模块是SELinux和AppArmor:

  • SELinux 是最流行(也最复杂)的MAC实现之一,最初由美国国家安全局开发,如今已用于 Android 和 Fedora 等大型项目。
  • AppArmor 也是一种流行且更友好的MAC实现,由Canonical提供支持,并用于Ubuntu和Debian等Linux 发行版。

因此,如果需要对进程权限进行精细控制,就应该考虑使用MAC机制。

但有时限制权限并不足以保护系统免受易受攻击应用程序的影响。而沙箱技术可以用来缓解这一问题。

15 应用程序沙箱

沙箱可以将应用程序与系统的其他部分隔离开来。

Linux内核中最古老的沙箱机制可能就是chroot。但就安全性而言,它的作用并不大,因为它只能隔离文件系统。

虚拟化是另一种应用程序沙箱的形式,但成本太高,尤其是在嵌入式系统中。

如今,嵌入式Linux中沙箱应用程序的两种可行解决方案是容器和可信执行环境(TEE)。

参考资料

16 容器

Linux容器是一个最小文件系统,只包含运行特定应用程序或一组应用程序所需的软件组件。容器的运行与系统的其他部分完全隔离,只有内核是共享的。

容器运行时实现利用了 Linux 内核提供的一些功能,包括

  • 命名空间:隔离Linux上进程的执行(PID、用户、网络连接、挂载点等)。
  • cgroups:允许按进程或进程组划分系统资源(CPU、内存、I/O)。
  • seccomp:可限制进程可执行的系统调用。

有几种工具可用于管理Linux中的容器,包括LXC、Systemd-nspawn、Podman 和Docker。

容器本身并不安全,但如果配置得当,我们可以限制容器内每个进程的权限并控制它们之间的通信,从而减少攻击面并提高系统的安全性。

与安全模块(如AppArmor或SELinux)结合使用,我们可以大大提高系统的安全性。

但在基于容器的系统中,如果内核受到攻击,所有操作系统都将面临风险。在这种情况下,可信执行环境是另一层安全保护,可以帮助防止这种情况的发生。

17 可信执行环境

可信执行环境(TEE Trusted Execution Environment)是一种环境,在这种环境中,执行的代码和访问的数据都是隔离的,并在保密性(任何人都无法访问数据)和完整性(任何人都无法更改代码及其行为)方面受到保护。

在具有TEE的系统中,我们有在富执行环境 (REE Rich Execution Environment) 上运行的不信任应用程序 (UA untrusted applications),以及在可信执行环境 (TEE) 上运行的可信应用程序 (TA trusted application)。只有在TEE上运行的可信应用程序才能完全访问主处理器、外设和内存。硬件隔离保护 TA 不受在主操作系统(非安全世界)上运行的不信任应用程序的影响。

我们需要硬件支持来实现TEE,这样就能对硬件(总线、外设、内存区域、中断等)进行分区和隔离,防止不受信任的应用程序访问受保护的资源。大多数现代处理器都内置了这一功能(如 ARM 的TrustZone、RISC-V的MultiZone和Intel SGX)。

我们身边有很多设备都在使用可信执行环境,包括智能手机、机顶盒、游戏机和智能电视。有一些商业TEE 实现,如 Kinibi、QSEE 和 iTrustee,也有一些开源实现,如Trusty 和OP-TEE。TEE可以很好地解决沙箱应用程序、存储和管理加密密钥、存储和管理凭证和敏感数据以及保护数字版权信息等问题。

18 更新系统

更新系统应在产品开发的早期阶段就设计好,如有可能,还应具备OTA(空中下载 Over-the-Air)功能。

实施良好的更新系统会给产品开发带来一些真正的挑战,包括通信协议的安全性、更新过程的原子性、断电保护、带宽和存储使用、回滚功能等。

嵌入式 Linux 的更新系统可以采用一些策略,包括

  • 基于应用程序:容易实现,但操作系统的其他部分怎么办?
  • 基于软件包:更新镜像很小,但更新是非原子的,软件包依赖性可能是个问题。
  • 基于image:使用 A/B 机制是一个很好的解决方案,问题可能在于带宽和存储空间的使用。
  • 基于容器:这是另一个不错的选择,有利于实现原子更新系统、电源故障安全、带宽使用更少、速度更快、停机时间最短、可回滚。

如果要进行 OTA 更新,设备必须有网络连接(Wi-Fi、以太网等)。这意味着网络接口将增加系统的攻击面,您需要增加更多层次的安全保护来抵御攻击。

19 网络安全

这里的规则很简单:尽可能减少攻击面。这并不意味着实施起来很容易。

例如,关闭所有不使用/不需要的 TCP/UDP 端口(nmap 等工具可以提供帮助),禁用所有不使用的协议(如 IPv6、PPP 等),制定防火墙规则以防止入站/出站连接,防止DoS攻击,防止端口扫描等。

如果需要与外部设备通信,一定要使用安全连接(VPN、反向 SSH、TLS、HTTPS 等),远程连接最好使用公钥验证,并禁止以根用户身份登录。

20 小结

我们讨论了一些安全概念、威胁建模、安全启动、代码和数据加密、加密密钥、密钥存储技术、安全编码、静态分析工具、运行时保护、模糊工具、权限、Linux 功能、强制访问控制、沙箱、更新系统和网络安全。

归根结底,没有100%安全的系统。攻击者只需要一个漏洞就能入侵设备。这只是我们希望这一过程有多难、难度有多大的问题。

因此,我们在设计时应考虑到安全性,并注意权衡利弊。系统应该 "足够安全"。我们应遵循良好的安全实践,了解可用的技术和工具,并在需要时加以使用。

让我们确保嵌入式 Linux 设备的安全!

posted @ 2023-10-01 19:36  磁石空杯  阅读(150)  评论(0编辑  收藏  举报