5

在阅读和学习开源操作系统时,我偶然发现了一种在汇编中调用“方法”的极其复杂的方式。它使用 'ret' 指令调用库方法来执行此操作:

push rbp                ; rsp[1] = rbp
mov rbp, .continue      ; save return label to rbp
xchg rbp, QWORD [rsp]   ; restore rbp and set rsp[1] to return label

push rbp ; rsp[0] = rbp
mov rbp, 0x0000700000000000 + LIB_PTR_TABLE.funcOffset ; rbp = pointer to func pointer
mov rbp, QWORD [rbp]    ; rbp = func pointer
xchg rbp, QWORD [rsp]   ; restore rbp and set rsp[0] to func pointer

; "call" library by "returning" to the address we just planted
ret

.continue:

我添加评论是为了自己理解它,似乎我是对的或足够接近,因为我所做的所有实验都成功了。但后来我尝试这样做,这也很有效:

mov rax, 0x0000700000000000 + LIB_PTR_TABLE.funcOffset  ; rax = ptr to func ptr
mov rax, QWORD [rax]    ; rax = func ptr
call rax ; actually call the library function in a normal fashion

查看指令的数量以及 CPU 在这两种情况下实际必须做的事情,人们会假设,如果一种更快,那将是“调用”变体。但是由于使用了“ret”变体,并且首先需要大量知识,所以第一个变体有什么优势?(或者是吗?)

4

1 回答 1

5

随着 CPU 变得更快,由于缓存未命中和分支错误预测等原因导致 CPU 停顿(并且无法执行任何操作)的机会也会增加。为了帮助避免这些停顿,大多数现代 80x86 CPU 都有一堆逻辑来帮助预测控制流更改的目标地址;包括分支方向预测器、分支目标预测器、返回堆栈缓冲区等。

问题是恶意攻击者(使用推测执行和测量时间)可以从 CPU 收集的所有信息中提取机密信息以提高性能;包括从分支方向预测器、分支目标预测器、返回堆栈缓冲区等中提取机密信息。

当发现这一点时,人们(主要是内核开发人员)争先恐后地想出各种方法来缓解安全问题。具体来说,寻找避免、破坏或污染 CPU 收集的数据的方法。

更具体地说(对于您显示的代码);如果使用的代码call rax,那么它会将数据添加到 CPU 的返回堆栈缓冲区,恶意攻击者可以探测这些数据以确定有关原始值的某些内容rax(如果rax应该是机密的,那么这构成了机密性泄漏)。

一种替代方法是推送返回地址,然后使用间接跳转。在这种情况下,它只会将(机密)数据留在 CPU 的分支目标缓冲区中,攻击者可能会探测到这些数据,这并没有真正的帮助。

使用ret而不是通过不在返回堆栈缓冲区(或分支目标缓冲区)中存储任何内容来防止安全问题。作为副作用,它还会“去同步”CPU 的返回堆栈缓冲区;混淆以前的调用/未来会返回一点。

可悲的是;所有这些都会导致性能问题——它让我们回到“随着 CPU 变得更快,CPU 停顿的机会增加”,并且在停顿成本之上增加了从错误地址获取代码的成本。

于 2021-10-20T17:48:35.873 回答