内存保护单元(MPU)是一种硬件机制,通过只允许代码访问需要的内存和外设来提高嵌入式设备的安全性。应用程序可以组织为进程(process),每个进程访问自己的内存和外设。MPU不仅阻止应用程序代码访问其指定区域以外的内存或外设,而且还可以用于检测堆栈溢出。
我们基于ARM Cortex-M MCU中的MPU,讨论一下MPU所提供的一些特性。
什么是MPU?
MPU:Memory Protection Unit,内存保护单元。它是一种硬件机制,只允许需要访问某些资源的代码访问相应的内存和外设。 MPU常用于安全关键应用,如医疗设备、航空电子设备、工业控制、核电站等,提高嵌入式应用的稳定性和安全性。
在IoT应用中,也可以通过MPU限制对内存和外设的访问,提高产品的安全性。例如,可以通过MPU机制隐藏加密密钥以拒绝攻击者访问;使用MPU隔离Flash控制器也可以防止攻击者更改应用程序,只允许受信任的代码执行代码更新。
通过MPU将RTOS任务划分为进程,如图1所示。每个进程可以包含任意数量的任务。进程内的任务可以访问分配给该进程的内存和外设。增加MPU时,从任务的角度几乎不用更改,除非任务之间存在交互。
图1 应用隔离为进程
进程间可以通过共享内存进行通信,两个进程的MPU配置表中将出现相同的区域。
应用中也可以包含具有完全权限的系统级任务和ISR,允许它们访问所有内存、外设或CPU。
当违规发生时,系统的行为取决于应用程序,是哪个任务违规。例如,如果违规是由图形用户界面(GUI)造成的,可以终止并重新启动GUI,并且不会影响系统的其它部分。但是,如果违规任务控制一个制动器,则异常处理程序可能需要在重启任务之前立即停止制动器。理想情况下,在产品开发过程中会捕获并纠正访问违规,否则,系统设计人员将需要评估所有可能的结果,并决定发生这种情况时该做什么。
使用MPU检测堆栈溢出
在基于RTOS的应用中,每个任务都需要单独的堆栈空间。堆栈溢出可能是基于RTOS的系统开发人员所面临的最常见的问题之一。如果没有硬件帮助,堆栈溢出检测可以由软件实现,但软件方式不能及时捕获溢出,可能导致产品不稳定。MPU可以帮助防止堆栈溢出。
如图2所示,MPU域可用于堆栈溢出检测。使用一个小的域(RedZone)来覆盖每个任务堆栈的底部。配置MPU属性,如果有任何代码尝试写入该区域,将触发MPU异常。域的大小决定了该方法在捕获堆栈溢出方面的效率。区域越大,堆栈溢出捕获的可能性就越大,同时堆栈可用的RAM就越少。
换言之,RedZone域被认为是不可用的内存,它被用来检测非法写入。开始时可以将RedZone大小设置为32个字节,如果任务堆栈为512个字节,那么32个字节约占用6%,剩余480个字节的可用堆栈空间。
图2 使用MPU域检测堆栈溢出
另一种检测方法是将整个任务堆栈封装为一个MPU区域,允许读写操作。该方法有两个问题:首先,在Cortex-M(ARMv7M架构)上,堆栈的大小需为2的幂,并且必须在幂边界(即32、64、128、256、512等)上对齐。如果嵌入式应用有足够的RAM,那么这不是问题;然而,在资源受限的应用 (如Cortex-M MCU)中,在减少浪费的同时设置合适的堆栈内存会很复杂。其次,该方法不允许写同一进程中的其它任务堆栈,不允许进程中的任务通过堆栈传递信息。
进程表
进程表中的条目数量取决于MPU。Cortex-M架构MCU可以有8个或16个MPU区域。由于可用的区域数量有限,通常更多设置区域保护RAM中的数据访问。但是,如果应用程序没有用完所有区域,也可以通过区域限制对代码的访问来提高安全性。
图3展示了一个包含四个任务的进程的区域定义,所有进程将共享相同的Flash代码空间。
图3 一个拥有4个任务的进程MPU域
1、一个MPU域提供对代码空间的读取和执行访问。因为常量通常存储在Flash中(ASCII字符串、查找表、常量等),所以读访问是必须的。
2、一个区域用于提供进程相关的外设访问,例如,以太网控制器、USB控制器等。MPU区域设置为读写访问,但不允许执行代码。如果进程不需要访问外设,则不需要此MPU区域。
3、一个MPU区域封装进程全局变量以及堆(heap)空间。MPU区域将设置为读写权限,但同样不允许执行代码。
4、一个MPU区域用于检测堆栈溢出。此方法假设进程中的任务不会通过其堆栈共享数据。同样,在此区域中不允许执行代码。RTOS负责选择运行哪个任务,相应任务的堆栈将被封装在MPU区域中。
5、该区域显示了由于MPU区域大小和对齐限制而可能导致的RAM浪费。资源受限的嵌入式应用中应尝试尽量减少浪费。
6、此MPU区域用于建立多个进程共享的RAM。如果进程不需要共享数据,则不需要此MPU区域。
进程表由“N”个条目组成,每个条目包含两个字段:区域的基地址和指定区域属性(区域大小,允许读、写或执行等)。
进程表在创建任务时被分配给任务。RTOS只是在任务的控制块(TCB)中保留一个指向进程表的指针。RTOS在上下文切换时增加更新MPU进程表的代码,如图4所示。在切换任务时不需要保存MPU配置。
图4 RTOS上下文切换时,更新MPU配置
Cortex-M 特权等级
上电后,Cortex-M运行在特权模式,可以访问CPU的所有资源,访问任何内存或I/O地址,启用/禁用中断,设置嵌套向量中断控制器(NVIC),配置FPU和MPU等。
为了保证系统的安全,特权模式代码保留给经过完全测试并受信任的代码。由于大多数RTOS都经过了完整的测试,通常被认为是值得信任的,而应用代码是不可信的。也有例外,例如,只要ISR保持尽可能短而不被滥用,ISR通常被认为是受信任的,因此也以特权模式运行。这是大多数RTOS供应商的建议。
应用代码在非特权模式下运行,从而限制了代码可以做的事情。具体来说,非特权模式可以防止代码关中断、更改嵌套向量中断控制器(NVIC)的设置、将运行模式更改为特权等级、修改MPU设置等。这是一个理想的特性,因为我们不希望不受信任的代码赋予自己特权,从而更改系统设计者提供的保护。
由于CPU总是以特权模式开始运行,任务需要从创建时就以非特权模式运行,或者在启动后通过API调用,切换到非特权模式。一旦进入非特权模式,CPU只有在中断或异常服务中才能切换回特权模式。
在用户模式访问RTOS服务
由于非特权代码不能禁用中断,因此应用代码被迫使用RTOS服务来访问共享资源。由于RTOS服务在特权模式下运行,因此非特权任务必须通过Cortex-M提供的SVC机制切换回特权模式。SVC的行为类似中断,但由CPU指令触发。
在Cortex-M上,SVC指令使用一个8位参数来指定256个可能的RTOS服务。设计者决定非特权代码可以使用的RTOS服务。例如,你可能不希望允许非特权任务终止其它任务(或其本身)。此外,这些服务都不允许禁用中断,因为这将破坏在非特权模式下运行代码的目的。一旦调用,SVC指令跳转到SVC异常处理程序。
SVC处理过程如图5所示。
图5 用户代码调用RTOS服务
在Cortex-M3上,SVC处理程序将增加约1k字节的代码,需要执行75~125条CPU指令。因此,相同RTOS服务,在非特权代码中调用比特权模式调用需要更多的处理时间。
在非特权模式下运行代码还可以防止用户代码禁用中断,从而减少了锁定系统的机会。当然,如果用户代码进入无限循环,特别是在高优先级任务或ISR中时,锁定仍然可能发生。在这种情况下,通过使用看门狗可以恢复。
进程间通信
图6展示了进程之间的通信方式。这些只是一些可能的场景,实际上,应用程序可以使用这些技术的组合。
图6 进程通信方式
1、互斥量用于确保两个进程不同时访问相同的数据。注意,互斥量驻留在RTOS内存空间中,通过RTOS API,所有进程可以访问该互斥量。
2、需要访问受保护资源的任务必须首先获取互斥量。当任务完成共享资源访问后,互斥量将被释放。沙漏表示可选超时,当任务不希望永久等待所有者释放互斥量时,可以使用超时机制。
3、信号量也可以用来指示数据可用。
4、进程中的任务将数据存储到共享内存中,然后发出信号。
5、进程B中的任务等待进程A的信号。沙漏表示一个可选的超时,以避免永远等待信号。如果该信号未在规定的时间内发生,则RTOS将恢复该任务。在这种情况下,任务知道共享区域中没有存入任何东西。
6、如果没有发生超时,进程B确认数据处理完成。
7、进程A发信号量后,等待具有可选超时的确认。
8、通信也可以使用RTOS的消息队列机制。此时,从共享RAM区域动态分配缓冲区,进程A中的发送任务填充缓冲区,并将指针发送给进程B中的任务。
9、与信号量情况类似,等待进程B中的任务确认,并指定一个可选的超时。
内存和I/O访问错误处理
进程间可以通过共享内存进行通信,两个进程的MPU配置表中将出现相同的区域。
应用中也可以包含具有完全权限的系统级任务和ISR,允许它们访问所有内存、外设或CPU。
当违规发生时,系统的行为取决于应用程序,是哪个任务违规。例如,如果违规是由图形用户界面(GUI)造成的,可以终止并重新启动GUI,并且不会影响系统的其它部分。但是,如果违规任务控制一个制动器,则异常处理程序可能需要在重启任务之前立即停止制动器。理想情况下,在产品开发过程中会捕获并纠正访问违规,否则,系统设计人员将需要评估所有可能的结果,并决定发生这种情况时该做什么。
MPU的工作是确保进程中的任务只能访问分配给它的内存和外设。但是,如果任务试图访问允许区域以外的数据呢?MPU会触发一个称为内存管理(MemManage)故障的CPU异常。
当故障发生时,系统行为取决于应用程序,但如何处理故障可能是很难确定的事情。首先,这些类型的故障应该在开发过程中被检测和纠正。然而,使用MPU的原因之一是为了防止发生的无效内存或外设访问,要么是因为系统验证期间未捕获某些偶发情况,或者是未经授权的访问。
MemManage故障通常由RTOS处理。理想情况下,嵌入式系统有一些机制可以记录和报告故障,以便在产品的下一个版本中修正。文件系统是记录这些故障的好地方,当然,还取决于故障处理程序。
发生故障时,故障处理程序可以执行以下操作序列(伪代码):
void OS_MPU_FaultHandler (void) { // Terminate the offending task/process (1) // Release resources owned by the task/process (2) // Run a user provided ‘callback’ (based on the offending task) (3) // If we have a file system: (4) // Store information about the cause // Do we restart the task/process? (5) // Yes, Restart the task/process // Alert a user (6) // No, Reset the system (7) }
(1)当故障发生时,设计者需要确定如何操作。至少必须终止违规的任务,但我们是否还需要终止此进程中的其他任务?没有一个确定的答案,事实上,这可能取决于是哪个任务造成了故障。因此,MPU故障处理程序应根据触发它的任务或进程执行不同的操作。
(2)被终止的违规任务(或进程)可能拥有内核对象、缓冲区、I/O等资源。这些资源需要被释放,以避免影响其他任务/进程。
(3)导致故障的任务可能会控制制动器或其他类型的输出,需将任务置于安全状态,以避免对人员或资产造成伤害。嵌入式系统设计者应提供用户定义的回调函数,以处理系统特定的操作。在任务创建过程中,将回调函数存储在任务的控制块(TCB)中。为提高系统安全性,只能在系统启动时创建任务,此时CPU处于特权模式;运行时只能在故障时删除任务。由于TCB位于RTOS空间中,因此无法从用户代码访问回调函数,从而防止潜在的不安全和不可靠的代码无意中或恶意地调用回调函数。
(4)如果嵌入式系统具有数据存储功能,则可以记录故障相关的信息,如违规任务的性质、CPU寄存器的值、所采取的操作等。
(5)根据导致故障的任务,可以重新启动,使系统可以错误中恢复。
(6)如果系统能够恢复,并且如果系统包含显示,则警告提示非常有用。此外,如果系统具有网络连接,则通知服务部门和开发团队可以在将来的版本中避免此问题。
(7)如果系统无法恢复,除了重置系统之外,可能没有其他选择。
可以更改MPU进程表,使其包含每个任务的回调函数,以便检测到故障时从RTOS上下文切换代码调用。如果所有的任务都需要对故障执行相同的操作,那么可以不使用此功能,或者让所有MPU进程表都指向相同的回调函数。后一种选择更最灵活,因为它为未来的版本提供了更大的灵活性。但你可能需要咨询RTOS供应商,以确定此功能是否可用。
建议
以非特权模式运行用户代码
使用MPU时,以特权模式运行应用代码。这意味着应用程序代码将能够更改MPU设置,并破坏使用MPU的目的。以特权模式运行应用可能更容易地迁移应用代码。在某些时候,大多数应用程序代码将需要在非特权模式下运行,用户需要添加SVC处理程序。
ISR具有完全访问权限
当识别到中断并且启动ISR时,处理器将切换到特权模式。由于MPU控制寄存器的PRIVDEFENA为1,因此ISR可以访问所有I/O内存。
此外,ISR应该尽可能短,并简单地发信号给任务,由任务执行中断设备所需的大部分工作。当然,这假设ISR是内核感知的ISR,并且该中断设备有相当多的工作处理。例如,不应在ISR中上处理以太网数据包。然而,闪烁LED或更新脉冲宽度调制(PWM)计时器的占空比可以直接在ISR中完成。
防止在RAM中执行代码
大多数MPU可以防止从RAM执行代码,从而限制代码注入攻击。防止外设执行代码可能看起来很奇怪,但可以防止想方设法进入系统的黑客。
限制进程对外设的访问
应该留出一个或多个MPU区域,以限制进程只能访问自己的外设。换言之,如果一个进程管理USB端口,那么它应该只能访问USB外设或与USB控制器的需求相关的外设,例如DMA。
限制可用的RTOS API
设计者必须确定应用代码可使用哪些RTOS API。例如,是否禁止应用代码在系统初始化后创建和删除任务或RTOS对象?换言之,RTOS对象是否只能系统启动时创建,而非运行时创建?如果是,SVC处理程序查找表应只包含向应用公开的API。即使ISR在特权模式下运行,可以访问所有的RTOS API,一个好的RTOS也会阻止从ISR中创建和删除RTOS对象。
在RTOS空间中分配RTOS对象
任务堆栈位于进程空间内,但是,RTOS对象(信号量、队列、任务控制块等)应在内核空间中分配,并通过引用进行访问。不应在进程空间中分配RTOS对象,因为这意味着应用代码可以不调用RTOS API,即可有意或意外地修改内核对象。
保护对代码的访问
虽然MPU区域通常用于提供或限制对RAM和外设的访问,但如果有空闲区域,并且能够通过进程组织代码,那么限制对代码的访问非常有用,可以防止某些类型的安全攻击,如Return-to-libc攻击。
减少进程间的通信
正如设计时,尽可能保持任务独立一样,进程也应该遵循同样的规则。因此,进程间应尽量不交互,或者将进程间通信保持在最低限度。如果必须与其它进程通信,只需留出一个包含Out和In缓冲区的共享区域。发送者将其数据放入Out缓冲区中,然后触发一个中断来唤醒接收进程。一旦数据被处理,响应可以放在发送者的In缓冲区中,并且可以使用中断来通知发送进程。
确定出现MPU故障时应如何操作
理想情况下,在开发过程中检测并纠正所有MPU故障。你还需考虑由于意外故障或错误或系统受到安全攻击而出现的故障处理。在大多数情况下,建议对每个任务或每个进程设置一个可控的关机顺序。是否重新启动违规任务、进程内或整个系统内的所有任务都取决于故障的严重程度。
记录和报告故障
理想情况下,系统应该能够记录(可能是文件系统)并显示故障原因,以便开发人员解决问题。
大多数RTOS在特权模式下运行应用代码,使得应用程序完全控制CPU及其资源。所有任务和ISR都可以不受限制地访问内存和外设。这意味着应用程序代码可能会意外或故意损坏其他任务的堆栈或变量。通过MPU机制,可以限制应用代码的访问权限,极大提供应用的功能安全。
审核编辑:刘清
全部0条评论
快来发表一下你的评论吧 !