2021年8月5日,来自微软Azure Defender for IoT 团队的研究员Omri Ben-Bassat 和 Tamir Ariel,在 BlackHat USA 2021 上分享了以内存分配程序漏洞BadAlloc为主题的演讲。本文将和大家一起来看看议题中涉及的内容。
一、漏洞简介
“BadAlloc”其实是一类漏洞的代号,这些漏洞均与整型溢出相关,具体来说漏洞存在于各类流行的内存分配程序的核心函数(如malloc和calloc)中。BadAlloc漏洞影响诸多广泛使用的实时操作系统(如 VxWorks, FreeRTOS, eCos),标准C库(如,newlib, uClibc, Linux klibc),物联网设备SDK(如,谷歌云物联网SDK, 德州仪器 SimpleLink SDK)和其他自内存管理应用(如,Redis)。其中一些漏洞最早可追溯到90年代早期,它们共同影响着全球数百万台设备,而且尤其是物联网和嵌入式设备。
据微软博客上介绍,攻击者利用这些漏洞可绕过安全控制,在受影响系统上执行恶意代码或导致系统崩溃。这些远程代码执行 (RCE) 漏洞涵盖超过 25 个 CVE,并影响广泛的领域,从消费者、医疗物联网到工业物联网、运营技术 (OT) 和工业控制系统。鉴于 IoT 和 OT 设备的普遍性,这些漏洞如果被成功利用,将成为各种组织的重大潜在风险。
二、受影响产品
通过CISA 发布的通告整理得到如下受影响设备的信息,具体厂商修复情况详见:https://us-cert.cisa.gov/ics/advisories/icsa-21-119-04。
三、基于堆的整型溢出简述
3.1 整型溢出
整数分为有符号和无符号两种类型,有符号数以最高位作为其符号位,即正整数最高位为0,负整数最高位为1,而无符号数无此类情况,它的取值范围是非负数。不同类型的整数在内存中均有不同的固定取值范围,当我们向其存储的数值超过该类型整数的最大值,就会导致整型溢出,比如 unsigned short 的存储范围是 0 ~ 65535 ,但当存储的值超过 65535 时,数据就会截断,例如输入 65536,系统就会识别为 0。
VC 6.0 中定义的整数变量取值范围:
类型 | 占用字节数 | 取值范围 |
int | 4 | -2147483648 ~ 2147483647 |
short int | 2 | -32768 ~ 32767 |
long int | 4 | -2147483648 ~ 2147483647 |
unsigned int | 4 | 0 ~ 4294967295 |
unsigned short int | 2 | 0 ~ 65535 |
unsigned long int | 4 | 0 ~ 4294967295 |
对于无符号整型unsigned int来说,如果它被赋予了最大值 4294967295(0xFFFFFFFF),那么如果程序中有其他操作使其再加上一个 8,那么最终实际上将会得到 7。乘法也是类似的:
3.2 堆内存分配
通常在动态开辟内存时,会使用 malloc 等函数在堆区开辟空间,分配器将通过特定的数据结构组织堆区的数据块。上图所展示的是分配器通过单/双链表管理空闲内存。
假如我们通过 malloc 申请一块 1024 字节大小的内存,那么实际分配到的块大小是多少呢?是 Size 的长度 8 加上申请空间大小 1024,块大小是 1032。再考虑另一种情况,假如我们现在申请一块非常大的空间呢,比如申请大小4,294,967,295(= 232 − 1),最终实际得到的块大小会是多少呢?
最终只会得到 7。那么这样会产生什么问题呢,下面看一个例子:
这是一个简单的程序,其中会根据用户所提供的参数 size 利用 malloc 申请一块空间,如果申请成功,则会执行 memcpy ,从用户提供的数据 user_data 中复制 size 长度的内容到申请好的空间中。
正常用户传入的参数如上图所示,经过程序处理会返回 OK。
那假如一个用户传入的 size 很大,比如想要申请 4294967295字节的空间,根据不同的系统,例如在Windows下32位程序如果单纯看地址空间能有4G左右的内存可用,不过实际上系统会把其中2G的地址留给内核使用,所以理想情况下程序最大能用2G的内存。那么程序执行时内存申请会失败,最终返回 Sorry。
那是否可能存在恶意用户申请巨大空间时成功呢?答案是肯定的,这也就是本次漏洞的关键点所在,一些产品中的内存分配函数由于存在整型溢出漏洞,当用户申请不合理的巨大内存空间时,最终会返回给用户一个指向极小内存空间的指针。那么当程序中对该块内存进行操作时,就极有可能造成堆溢出从而使程序崩溃或被执行代码。
下面就以 FreeRTOS 中的漏洞为例,来看看 BadAlloc的实际情况。
四、FreeRTOS漏洞实例
CVE-2021-31571和CVE-2021-31572是存在于Amazon Free RTOS 10.4.3之前版本中的整型溢出漏洞。问题出现在它的内核代码:https://github.com/FreeRTOS/FreeRTOS-Kernel/pull/224。
4.1 补丁代码分析
https://github.com/FreeRTOS/FreeRTOS-Kernel/commit/c7a9a01c94987082b223d3e59969ede64363da63 中显示总共有 4 个文件做了改动,分别是 heap_1.c,heap_2.c, heap_4.c和heap_5.c。
FreeRTOS对于pvPortMalloc()和vPortFree()提供了5种实现。FreeRTOS应用程序可以使用其中的一种,或者使用自己的实现。5种实现分别在heap_1.c,heap_2.c,heap_3.c,heap_4.c和heap_5.c文件中,都存在于文件夹FreeRTOS/Source/portable/MemMang下。为了向后兼容性,Heap_2保留在FreeRTOS发行版中,但是不推荐在新设计使用它。可以考虑使用heap_4,heap_4是heap_2增强版。
Heap_2.c还是通过configTOTAL_HEAP_SIZE来定义堆的大小,它使用最佳匹配算法来分配内存,并且与heap_1不同,它允许释放内存。同样,数组是静态声明的,因此会使应用程序看起来消耗大量RAM。
接下来看代码中为了修复漏洞做出的改动:
在 portable/MemMang/heap_1.c 文件中pvPortMalloc()函数里的两处修改:
- 可能出现溢出的变量是 xWantedSize,因为在程序中会给这个变量做加法,修改之后,会在它做自加之前,判断一下它加上 ( portBYTE_ALIGNMENT – ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ) 之后会不会溢出。
- 还是和 xWantedSize 相关,去检查它是否大于 0。
那么现在的疑问是:
在 portable/MemMang/heap_2.c 文件中pvPortMalloc()函数里的两处修改:
- 在执行 xWantedSize += heapSTRUCT_SIZE; 之前增加了校验检查 ( xWantedSize + heapSTRUCT_SIZE ) > xWantedSize,防止 xWantedSize 越界。
- 执行内存字节数对齐前,增加xWantedSize + ( portBYTE_ALIGNMENT – ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ) > xWantedSize 的校验。
在 portable/MemMang/heap_4.c 文件中pvPortMalloc()函数里的两处修改:
- 在执行 xWantedSize += xHeapStructSize; 之前增加了校验检查 ( xWantedSize + xHeapStructSize ) > xWantedSize ,防止 xWantedSize 越界。
- 执行内存字节数对齐前,增加 xWantedSize + ( portBYTE_ALIGNMENT – ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ) > xWantedSize 的校验。
在 portable/MemMang/heap_5.c 文件中 pvPortMalloc() 函数里的两处修改:
- 在执行 xWantedSize += xHeapStructSize; 之前增加了校验检查 ( xWantedSize + xHeapStructSize ) > xWantedSize ,防止 xWantedSize 越界。
- 执行内存字节数对齐前,增加 xWantedSize + ( portBYTE_ALIGNMENT – ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ) > xWantedSize 的校验。
根据 https://github.com/FreeRTOS/FreeRTOS-Kernel/pull/224 中的描述,修改增加了对 heap 的边界检查,如果请求的内存块的大小介于 4,294,967,288 和 4,294,967,296 字节之间,则有可能发生溢出。
FreeRTOS官网上安全更新中的通告:https://www.freertos.org/security/security_updates.html。
在queue.c 中查找调用了 pvPortMalloc() 函数的地方,只有一处,是当 configSUPPORT_DYNAMIC_ALLOCATION 为1的时候,xQueueGenericCreate 函数中调用了 pvPortMalloc。
这样看来应该就可以触发 pvPortMalloc 中的整型溢出漏洞了,接下来想要验证申请超大size 内存时程序会出现什么异常,于是决定从搞清以下几个问题来入手:
- xQueueGenericCreate 是什么作用;
- 它调用 pvPortMalloc 时传入的参数从哪里来且如何控制。
静态代码分析的差不多了,现在知道了整型溢出漏洞存在的位置,还知道了要触发该漏洞需要调用的函数。接下来通过动态调试来进一步分析,不过要调试 FreeRTOS 的代码,首先来对 FreeRTOS 大致了解一下。
4.2 FreeRTOS简介
RTOS 全称是 Real Time Operating System,中文就是实时操作系统。RTOS 不是指某一个确定的系统,而是指一类系统。比如 uC/OS,FreeRTOS,RTX,RT-Thread 等这些都是 RTOS 类操作系统。
FreeRTOS 是 RTOS 系统的一种,FreeRTOS 十分小巧,可以在资源有限的微控制器中运行,当然,FreeRTOS 不仅局限于在微控制器中使用。但从文件数量上来看 FreeRTOS 要比uC/OSII 和 uC/OSIII 小的多。通常情况下内核占用 4k-9k 字节的空间。许多半导体厂商产品的 SDK(Software Development Kit—软件开发工具包)包就使用 FreeRTOS 作为其操作系统,尤其是 WIFI、蓝牙这些带协议栈的芯片或模块。
而且FreeRTOS 免费、开放源码,文档齐全。FreeRTOS可以被移植到很多不同架构的处理器和编译器上。每一个RTOS移植都附带一个已经配置好的演示例程,可以方便快速启动开发。更好的是,每个演示例程都附带一个说明网页,提供如何定位RTOS演示工程源代码、如何编译演示例程、如何配置硬件平台的全部信息。在 https://github.com/FreeRTOS/FreeRTOS/tree/main/FreeRTOS/Demo 中可以看到。
4.3 FreeRTOS案例调试
本次为了方便,选取了可以在 Windows 平台上运行的 FreeRTOS 示例程序。示例详见https://github.com/FreeRTOS/FreeRTOS/tree/main/FreeRTOS/Demo/WIN32-MSVC 。
首先从 github 上下载了 FreeRTOS v10.4.1的源码,这个版本存在漏洞,而且离最新版本也比较近。下载之后,解压缩,然后在…\FreeRTOS\Demo\WIN32-MSVC目录中,即可看到 Win32-MSVC 的例子。用 Visual Studio 2019(2010及以上的版本都可以)打开 WIN32.sln文件。
官网上有提供 Demo 的介绍:在https://www.freertos.org/a00090.html 选择设备 x86 / Windows Simulator ,在 https://www.freertos.org/FreeRTOS-Windows-Simulator-Emulator-for-Visual-Studio-and-Eclipse-MingW.html 中即可看到对整个示例程序的完整说明。
大致来说,程序中main 函数会根据mainCREATE_SIMPLE_BLINKY_DEMO_ONLY 的取值来决定是执行 main_blinky() 还是 main_full() 函数。我们其实只关注怎么在程序中调用 xQueueCreate,所以先看比较简单的 main_blinky()。main_blinky() 中部分代码如下图,其中就有调用到和漏洞相关的 xQueueCreate 函数,在调试的时候它的参数我们可控。
其中的参数 mainQUEUE_LENGTH表示队列一次可以容纳的项目数。
根据官网安全通告提示,heap_2.c 中存在可利用问题,所以将原先项目中的heap_5.c 移除,从 FreeRTOS\Source\portable\MemMang 中找到 heap_2.c 替换进去。看下图,在heap_2.c 中,传入的参数会加上 heapSTRUCT_SIZE,通过调试可知道其值是7。
那么思路就清晰了:为了使在p vPortMalloc 中 xWantedSize + 7 >= 4294967296,就需要保证 xWantedSize >= 4294967289;
也就是需要sizeof( Queue_t ) + xQueueSizeInBytes >= 4294967289;sizeof( Queue_t ) 经过调试得到是 84,所以需要 xQueueSizeInBytes >= 4,294,967,205;
最终只要xQueueSizeInBytes = uxQueueLength * uxItemSize >= 4,294,967,205即可。如果 uxItemSize 保持 sizeof( uint32_t ) 不变,可为 uxQueueLength 赋值为 1,073,741,802。当uxQueueLength为1,073,741,802时, pvPortMalloc中申请内存时获取到的大小实际为 4。
如此编译好的程序在 vs 中调试运行会报错。将程序编译 Release 版本,在 windbg 中打开并运行,在命令行输入 g 命令并执行,进程出现异常:
五、受影响的其他产品
5.1 VxWorks
看 VxWorks 5.1 中的漏洞代码:
和 calloc 这个内存分配程序中的处理流程有关。
5.2 Klibc
5.3 德州仪器“SimpleLink”SDK
可以看到常用SimpleLink组件中包含了之前介绍过的 FreeRTOS。
在执行内存分配时,最终调用到的 FreeRTOS 中的 heap_4.c。
SimpleLink 中的 Calloc 是安全的,它里面有执行对溢出的检查,但是Malloc 是不安全的:
六、漏洞缓解措施
BadAlloc 系列漏洞影响范围广泛,必须被用户和受影响软件厂商重视起来,以下是微软提供的一些缓解措施:
- 按照供应商的说明,及时为受影响的产品打上补丁。
- 如果无法修补,请加强监控。由于大多数老版 IoT 和 OT 设备不支持代理,因此请使用 IoT/OT 感知网络检测和响应 (NDR) 解决方案来自动发现和持续监控设备异常或未经授权的行为,例如发现到与不熟悉的本地或远程主机通信。这些是为 IoT/OT 实施零信任策略的基本要素。
- 通过消除与 OT 控制系统不必要的互联网连接来减少攻击面,并在需要远程访问时使用多因素身份验证 (MFA) 实施 VPN 访问。当然VPN 设备也可能存在漏洞,应更新到可用的最新版本。
- 网络分段对于零信任很重要,因为它限制了攻击者在初始入侵后横向移动并破坏系统的能力。特别是物联网设备和 OT 网络应使用防火墙与企业 IT 网络隔离。
- 建议检查程序中使用到的如下函数的实现:malloc、calloc、realloc、memalign、valloc、pvalloc、aligned_alloc。
一些标准C库中缓解技术案例如下:
参考链接
[1] blackhat官网议题页面
[2] 微软博客
[3] 后端 FreeRTOS 队列
https://www.dazhuanlan.com/jiangbindtc/topics/1004098
https://blog.csdn.net/qq_37634122/article/details/104283673
[4] FreeRTOS基础篇系列
https://blog.csdn.net/zhzht19861011/category_9265276.html
版权声明
本站“技术博客”所有内容的版权持有者为绿盟科技集团股份有限公司(“绿盟科技”)。作为分享技术资讯的平台,绿盟科技期待与广大用户互动交流,并欢迎在标明出处(绿盟科技-技术博客)及网址的情形下,全文转发。
上述情形之外的任何使用形式,均需提前向绿盟科技(010-68438880-5462)申请版权授权。如擅自使用,绿盟科技保留追责权利。同时,如因擅自使用博客内容引发法律纠纷,由使用者自行承担全部法律责任,与绿盟科技无关。