我一直在试图理解由GCC生成的汇编语言代码,并经常在许多函数的开头(包括_start()
)遇到这条指令,但找不到任何说明其目的的指南:
31-0000000000001040 <_start>:
32: 1040: f3 0f 1e fa endbr64
33- 1044: 31 ed xor ebp,ebp
我一直在试图理解由GCC生成的汇编语言代码,并经常在许多函数的开头(包括_start()
)遇到这条指令,但找不到任何说明其目的的指南:
31-0000000000001040 <_start>:
32: 1040: f3 0f 1e fa endbr64
33- 1044: 31 ed xor ebp,ebp
它代表着“End Branch 64位”(还有32位的对应部分)-- 或者更准确地说,64位下的终止间接分支。
以下是操作:
IF EndbranchEnabled(CPL) & EFER.LMA = 1 & CS.L = 1
IF CPL = 3
THEN
IA32_U_CET.TRACKER = IDLE
IA32_U_CET.SUPPRESS = 0
ELSE
IA32_S_CET.TRACKER = IDLE
IA32_S_CET.SUPPRESS = 0
FI
FI;
endbr64
(以及endbr32
)是英特尔控制流强制技术(CET)的一部分(另请参见英特尔软件开发人员手册,第1卷,第18章)。
英特尔CET提供硬件保护,可防御返回导向编程(ROP)和跳转/调用导向编程(JOP/COP)攻击,这些攻击通过操纵控制流来重复使用现有代码进行恶意用途。
它的两个主要特点是:
endbr64
。虽然CET在当前处理器世代中仍在缓慢推出,但随着GCC 8的发布,它已经得到支持,默认情况下会插入endbrXX
指令。该操作码被选择为在旧处理器上不执行任何操作,因此如果不支持CET,则该指令将被忽略;在支持CET的处理器上,如果间接分支跟踪被禁用,则也将被忽略。
endbr64
具体是如何工作的呢?
前提条件:
CR4.CET
为1来启用CET。IA32_U_CET
(用户模式)或IA32_S_CET
(监管者模式)MSR中的间接分支跟踪适当标志。CPU设置一个小状态机,用于跟踪上一个分支的类型。请看以下示例:
some_function:
mov rax, qword [vtable+8]
call rax
...
check_login:
endbr64
...
authenticated:
mov byte [is_admin], 1
...
ret
现在让我们简要地看一下两个场景。
无攻击:
some_function
从 虚方法表 vtable
中检索虚拟方法 check_login
的地址,并调用它。TRACKER = WAIT_FOR_ENDBRANCH
)上触发。endbr64
,因此间接调用被认为是“安全的”,执行继续进行(endbr64
仍然表现为 no-op)。状态机被重置(TRACKER = IDLE
)。攻击:
攻击者以某种方式成功操作了 vtable
,使得 vtable+8
现在指向 authenticated
。
some_function
从虚方法表 vtable
中检索 authenticated
的地址,并调用它。TRACKER = WAIT_FOR_ENDBRANCH
)上触发。mov byte [is_admin], 1
,而不是预期的 endbr64
指令。CET状态机推断控制流被操纵,并引发一个 #CP
错误,终止程序。如果没有CET,则控制流操作将不会被注意到,攻击者将获得管理员权限。
总之,Intel CET的间接分支跟踪功能确保间接调用和跳转只能重定向到以 endbr64
指令开头的函数。
请注意,这并不保证调用正确的函数-如果攻击者改变控制流以跳转到另一个以 endbr64
开头的函数,状态机将不会报错并继续执行程序。但是,这仍然极大地减少了攻击面,因为大多数JOP / COP攻击针对函数中的指令(甚至直接跳转“进入”指令)。
_start
这样的代码中才会发现这种情况,这些代码已经以机器码形式存在,gcc 将其与您的可执行文件链接起来(从crt0.o
或其他文件),而不是从 C 源代码中生成。 - Peter Cordes