CVE-2024-50099
CVE-2024-50099
在本次讨论中,关键的漏洞源于 LDR literal
和 LDRSW literal
指令的模拟函数 simulate_ldr_literal()
和 simulate_ldrsw_literal()
,这两个函数直接通过 C 代码访问内存,而没有使用内核的异常表(extable)来处理内存访问错误,导致内核在遇到无效内存访问时触发 BUG()
,进而导致内核崩溃或其他异常情况。
原理:LDR Literal 和 LDRSW Literal 没有使用异常表导致 BUG()
simulate_ldr_literal()
和 simulate_ldrsw_literal()
函数
这两个函数模拟了 ARM 架构下的 LDR literal(加载字面量)和 LDRSW literal(加载字面量并扩展为符号扩展)指令的执行,它们主要用来模拟从指定内存地址加载数据到寄存器。以下是两个函数的简要解析:
1. simulate_ldr_literal()
1 |
|
- 功能: 该函数模拟 ARM 指令
LDR
(literal),用于将内存中的数据加载到寄存器中。指令中的位移量(displacement)决定了加载的数据位置。 - 核心步骤:
- 通过
ldr_displacement(opcode)
获取内存地址偏移量。 - 根据指令的目标寄存器(x0-x30 或 w0-w30)从计算出的地址加载数据。
- 更新指令指针以指向下一条指令。
- 通过
2. simulate_ldrsw_literal()
1 |
|
- 功能: 该函数模拟
LDRSW
指令,用于将内存中的 32 位数据加载到寄存器,并将其符号扩展到 64 位(扩展为一个s32
类型的值)。 - 核心步骤:
- 获取位移量并计算目标地址。
- 从该地址加载 32 位的数据,并通过符号扩展将其存储在 64 位寄存器中。
- 更新指令指针,继续执行下一条指令。
没有使用异常表(extable)的问题
在 Linux 内核中,当内核访问内存时,通常会使用 异常表(extable) 来处理潜在的内存访问错误。例如,如果内存访问遇到页错误,异常表会捕获这个错误并采取适当的恢复措施(例如,重试访问、返回错误或采取其他措施)。
然而,simulate_ldr_literal()
和 simulate_ldrsw_literal()
函数存在一个问题:它们直接使用普通的 C 代码来访问用户内存,而没有设置异常表(extable)来捕获内存访问中的错误。如果这两个函数尝试访问无效的内存地址(例如,地址无效、没有权限访问或超出有效范围),它们不会捕获错误,而是导致内核触发一个硬件异常(如页面错误)。
为什么没有使用异常表(extable)?
异常表的作用: 异常表(extable)用于捕获和处理内核中的异常,尤其是内存访问异常。如果在执行指令时遇到页错误或其他内存错误,内核会查找异常表,查看是否有适当的处理机制来恢复错误或采取其他安全措施。
直接内存访问的风险:
simulate_ldr_literal()
和simulate_ldrsw_literal()
这两个函数直接通过普通 C 代码访问内存,而没有为这些内存访问设置异常表(extable)。如果访问的内存无效或无法访问(例如,用户空间内存或非分配的内存),它们会引发硬件异常,而没有任何恢复机制。- 这种做法在某些情况下可能导致 BUG() 宏被触发,内核会崩溃并终止执行。
BUG() 宏的作用:
- BUG() 是 Linux 内核中的一个宏,当遇到致命错误时会触发。它的作用是触发一个内核崩溃,通常用于开发或调试阶段,帮助开发者识别和修复问题。
- 在这种情况下,由于没有适当的异常处理机制,如果指令模拟过程中出现内存访问错误,内核会调用 BUG(),导致内核线程终止,进而可能导致系统崩溃或死锁。
修复逻辑:
为了解决这个问题,修复的关键点是 避免在 uprobe 的情况下使用 simulate_ldr_literal()
和 simulate_ldrsw_literal()
,并且增加对这些指令的处理逻辑。
- 修改
arm_kprobe_decode_insn
和arm_probe_decode_insn
中的指令处理:- 通过检查
LDR literal
和LDRSW literal
指令的类型,在arm_kprobe_decode_insn
中对这些指令的处理进行了修改:1
2
3
4
5
6
7
8
9if (aarch64_insn_is_ldr_lit(insn)) {
api->handler = simulate_ldr_literal;
decoded = INSN_GOOD_NO_SLOT;
} else if (aarch64_insn_is_ldrsw_lit(insn)) {
api->handler = simulate_ldrsw_literal;
decoded = INSN_GOOD_NO_SLOT;
} else {
decoded = arm_probe_decode_insn(insn, &asi->api);
} - 这种修改确保了
LDR literal
和LDRSW literal
指令不会使用不安全的内存访问函数,防止了错误的发生。
- 通过检查
- 拒绝不安全的 uprobe 使用:
- 在修改后的逻辑中,
simulate_ldr_literal()
和simulate_ldrsw_literal()
只允许在kprobe
的上下文中使用,而不会在uprobe
中进行模拟。 - 这意味着当
LDR literal
和LDRSW literal
被检测到时,如果它们属于uprobe
指令类型,系统会拒绝执行这些指令的处理,防止它们触发异常。
- 在修改后的逻辑中,
- 返回
INSN_REJECTED
:- 对于不支持的
LDR literal
和LDRSW literal
指令类型,arm_probe_decode_insn()
返回INSN_REJECTED
,即拒绝执行这类指令。 - 这样,内核可以避免进一步的错误,保持稳定性,防止错误访问内存并触发内核崩溃。
- 对于不支持的
总结:
修复的核心在于: - 避免不安全的内存访问:通过检查指令类型并为 LDR literal
和 LDRSW literal
指令提供适当的处理逻辑,避免了直接使用 simulate_ldr_literal()
和 simulate_ldrsw_literal()
进行内存访问,尤其是在 uprobe
中。 - 增强内核稳定性:通过使用 INSN_REJECTED
和拒绝处理不安全的指令,防止了内核因非法内存访问而触发 BUG()
。 - 修复了直接内存访问导致的问题:通过检查内存访问的合法性,确保内核的内存访问机制更加健壮。
通过这些改动,内核避免了直接通过 C 代码访问用户内存而不处理异常,从而提高了内核的稳定性,解决了 CVE-2024-50099
漏洞。