在阅读和学习开源操作系统时,我偶然发现了一种极其复杂的调用"方法"的方式。在组装。它使用'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在这两种情况下实际要做的事情,人们会假设,如果一个更快,它将是"调用";变体。但自从"撤退"以来;变体被使用,想出这个首先需要很多知识,第一个变体有什么优势?(真的吗?)
随着CPU的速度越来越快,由于缓存丢失和分支错误预测等原因导致CPU停滞(以及无法做任何事情)的可能性也在增加。为了避免这些停顿,大多数现代80x86 cpu都有一堆逻辑来帮助预测控制流变化的目标地址;包括分支方向预测器,分支目标预测器,返回堆栈缓冲区等
问题是恶意攻击者(使用推测执行和测量时间)可以从CPU收集的所有信息中提取机密信息以提高性能;包括从分支方向预测器、分支目标预测器、返回堆栈缓冲区等提取机密信息
当这个问题被发现时,人们(主要是内核开发人员)争先恐后地想各种方法来缓解安全问题。具体来说,就是寻找避免、破坏或污染CPU收集的数据的方法。
更具体地说(对于您所展示的代码);如果代码使用了call rax
,那么它会将数据添加到CPU的返回堆栈缓冲区中,恶意攻击者可以通过探测来确定rax
中的原始值(如果rax
应该是机密的,那么这就构成了机密泄漏)。
一种替代方法是推送返回地址,然后使用间接跳转。在这种情况下,它只会在CPU的分支目标缓冲区中留下(机密)数据,这些数据可能会被攻击者探测,这并没有真正的帮助。
使用ret
通过不在返回堆栈缓冲区(或分支目标缓冲区)上存储任何东西来防止安全问题。作为一个副作用,它也会"去同步"。CPU的返回堆栈缓冲区;稍微混淆之前的调用/未来的返回。
可悲;所有这些都会导致性能问题——它让我们回到"随着CPU变得更快,CPU失速的机会也会增加"这个问题。并将从错误地址获取代码的成本添加到拖延成本之上。