endbr64指令实际上是做什么的?

101

我一直在试图理解由GCC生成的汇编语言代码,并经常在许多函数的开头(包括_start())遇到这条指令,但找不到任何说明其目的的指南:

31-0000000000001040 <_start>:
32:    1040:    f3 0f 1e fa             endbr64 
33-    1044:    31 ed                   xor    ebp,ebp

6
请查看英特尔的此PDF文件 - Jester
1
通常只有在像 _start 这样的代码中才会发现这种情况,这些代码已经以机器码形式存在,gcc 将其与您的可执行文件链接起来(从 crt0.o 或其他文件),而不是从 C 源代码中生成。 - Peter Cordes
除非您的GCC默认配置为使用“-fcf-protection = branch”,或者您手动使用该选项。请参见https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html和https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html中的“-mmanual-endbr”。 - Peter Cordes
2个回答

85

它代表着“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;

如果指令无法清除TRACKER标志,则CPU会生成#CP异常。换句话说,如果黑客能够更改间接跳转的目标地址,即使目标是合法的汇编代码,程序也很可能终止。
该指令在其他情况下被视为NOP。
换句话说,CET功能用于确保您的间接分支实际上到达有效位置。这提供了额外的安全性。以下是Intel关于此功能的段落。
ENDBRANCH(详见第73节)是一条新指令,用于标记程序中间的间接调用和跳转的有效目标地址。该指令的操作码被选择为在传统机器上是NOP指令,以便使用ENDBRANCH新指令编译的程序可以在没有CET强制执行的旧机器上继续运行。在支持CET的处理器上,ENDBRANCH仍然是一个NOP指令,并且主要用作处理器流水线的标记指令,用于检测控制流违规。CPU实现了一个状态机来跟踪间接jmp和call指令。当其中一个指令出现时,状态机从IDLE状态转移到WAIT_FOR_ENDBRANCH状态。在WAIT_FOR_ENDBRANCH状态下,程序流中的下一条指令必须是ENDBRANCH指令。如果没有看到ENDBRANCH指令,处理器将引发控制保护异常(#CP),否则状态机将返回IDLE状态。
顺便提一下,可以告诉处理器不要使用ENDBR64。这可以通过前缀(3Eh)来实现。这在某些情况下非常有用,比如开关语句中的地址存储在只读内存中的表中。然而,CPU在许多情况下会忽略该前缀。

答案中提到的《第11代英特尔® Core™处理器数据手册,卷1/2,2021年6月修订版006》的地址无效。本月有效的链接为https://cdrdv2.intel.com/v1/dl/getContent/631121。 - vitsoft
@vitsoft 有趣的是,你提到的文档中包含了我这里提供的链接,而该链接确实已经失效。由于我有一份解释该指令的段落副本,所以我认为即使没有链接也没问题... - Alexis Wilke

75

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

现在让我们简要地看一下两个场景。

无攻击:

  1. some_function虚方法表 vtable 中检索虚拟方法 check_login 的地址,并调用它。
  2. 由于这是一个间接调用,CET状态机被激活并设置为在下一条指令(TRACKER = WAIT_FOR_ENDBRANCH)上触发。
  3. 下一条指令是 endbr64,因此间接调用被认为是“安全的”,执行继续进行(endbr64 仍然表现为 no-op)。状态机被重置(TRACKER = IDLE)。

攻击:
攻击者以某种方式成功操作了 vtable,使得 vtable+8 现在指向 authenticated

  1. some_function 从虚方法表 vtable 中检索 authenticated 的地址,并调用它。
  2. 由于这是一个间接调用,CET状态机被激活并设置为在下一条指令(TRACKER = WAIT_FOR_ENDBRANCH)上触发。
  3. 下一条指令是 mov byte [is_admin], 1,而不是预期的 endbr64 指令。CET状态机推断控制流被操纵,并引发一个 #CP 错误,终止程序。

如果没有CET,则控制流操作将不会被注意到,攻击者将获得管理员权限。


总之,Intel CET的间接分支跟踪功能确保间接调用和跳转只能重定向到以 endbr64 指令开头的函数。

请注意,这并不保证调用正确的函数-如果攻击者改变控制流以跳转到另一个以 endbr64 开头的函数,状态机将不会报错并继续执行程序。但是,这仍然极大地减少了攻击面,因为大多数JOP / COP攻击针对函数中的指令(甚至直接跳转“进入”指令)。


4
CR4 只能在 Ring 0 中更改,因此需要内核支持。 - janw
1
分支/跳转/调用是LOAD指令--它们直到要跳转的代码被分页、检查页面执行权限后才会退出。 - William Cushing
@WilliamCushing - 我很困惑,因为一系列的连续跳跃(没有使用CET)并不能防止中断。 - TLW
2
我不明白在没有额外的状态需要保存和恢复的情况下,这两种场景之间的行为如何会有所不同。 - TLW
1
@TLW 在你描述的情况下,操作系统确实需要保存状态。英特尔手册中描述了 CET 的状态,包括间接分支跟踪器和影子堆栈指针的状态可以使用 XSAVE 和 XSTORE 进行保存。具体来说,跟踪器位于 IA32_U_CET MSR 中。 - Dougvj
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接