CET为何而生?CET安全防御机制解析

描述

1 概述

CET(Control-flow Enforcement Technology)机制是 Intel提出基于硬件的⽤于缓解 ROP/JOP/COP的新技术。特别强调下,他是基于硬件⽀持的解决⽅案。从Intel的Tigerlake (11th gen),Alderlake (12th gen)/Sapphire-Rapid起,粗颗粒度地旨在预防前向( call/jmp )和后向( ret )控制流指令劫持来御防ROP的攻击。

因此针对防御对象不同,CET技术又分为CET-SS用于针对ROP的ret指令和CET-IBT用于针对JOP/COP的jmp/call指令,见图1 功守道。

操作系统

图1 功守之道

2 CET为何而生

在安全世界里,总是希望魔高一尺道高一丈。说到防御者CET,就不得不提他的进攻者ROP了。什么是ROP? 来看⼀个例⼦,假设程序中正常执行如下代码⽚段,注意此时其中不含ret或call指令。

操作系统

图2 正常执行流

但是,如果稍加偏移⼀下解释代码的地址时,就会导致出现完全不⼀样的指令,如下图所示:

操作系统

图3 RET CALL的gadget

如果按照红框中的顺序解释这些指令的时候,那么将会产⽣⾮代码预期的结果,会出现原代码中未出现过的 ret 指令以及 call 指令,这些指令序列被称为 gadget 。

通过仔细构造这些由ret指令终⽌的指令集,攻击者可以执⾏原程序中⾮预期的任意恶意代码,这种新产生ret攻击被称为 ROP 攻击,同理还有COP/JOP(call/jmp)攻击。

为了防御这种此类ROP攻击,Intel工程师在2016年起在硬件机制上推出了CET缓解机制,针对不同的攻击行为分为CET-SS(影子栈)和CET-IBT(间接跳转跟踪),即图1 功守道所示。

3 CET

CET-SS和CET-IBT 在实现机制上属于CPU内部异常,这里就涉及到中断机制。当执行启动CET发现执行执行流中没有endbr64或函数返回ret和影子栈中shandow stack保存的ret不一致时,CPU内部出发异常,这里CET占用的中断向量21号,触发#CP并归为陷阱执行中断处理程序exc_control_protection(),对CET-SS CET-IBT分情况进行报错。CET-IBT -> "traps: Missing ENDBR: xxx", CET-SS-> #CP(control protect)。

这里提一下CPU的异常,当CPU执行中发现ROP/JOP/COP后,CPU给自己发送中断信号。注意异常不一定是错误,只要是异于平常就都是异常。有些异常不但不是错误,它还是实现内核重要功能的方法。CPU异常分为3类:

1.陷阱(trap),陷阱并不是错误,而是想要陷入内核来执行一些操作,中断处理完成后继续执行之前的下一条指令;

2.故障(fault),故障是程序遇到了问题需要修复,问题不一定是错误,如果问题能够修复,那么中断处理完成后会重新执行之前的指令,如果问题无法修复那就是错误,当前进程将会被杀死;

3.中止(abort),系统遇到了很严重的错误,无法修改,一般系统会崩溃。CET归为陷阱。

操作系统

图4 CET中断异常处理流程

3.1 CET-SS(Shadow Stack)

3.1.1 原理分析

Intel 提出了⼀种基于硬件的 CET 解决⽅案,其中之⼀的 shadow stack 机制⽤于缓解 ROP 攻击。前⽂图2可以得知 ROP 依赖于 ret 指令,其中要执⾏的后续指令地址从堆栈中获得。因此 ROP 攻击的前提是攻击者能够在堆栈中构造数据。那么再来看 shadow stack 机制是怎么⼯作的。

CET 使操作系统能够创建⼀个 shadow stack (影⼦栈)。正常情况下,当执⾏ call 指令时,会将 call 指令后⼀条指令地址压栈。当启⽤了 shadow stack 后,会同时在普通数据栈和 shadow stack 中压⼊返回地址,随后在执⾏ ret 返回时,会将 shadow stack 中的返回地址和普通数据栈中的返回地址做对⽐,如匹配,则正常执⾏,如不匹配,则触发#CP(Controlflow Protection) 异常。如下图所示:

操作系统

图5 CET-SS针对ret指令触发#cp异常

3.1.2 验证方法

操作系统

图6 CET-SS test

以上内核代码以ko的形式进行加载,然后通过用户空间的app触发系统调用,这里的app被名字也命为shadow_test_fork(内核函数和app名字一致了,容易产生混淆,请大家理解)。系统调用最终调到图6中的shadow_test_fork()函数。发生如下异常报错,见图7。

操作系统

图7 CET-SS test result #CP

3.2 CET-IBT(Indirect Branch Tracking)

JOP/COP 攻击⼿法与 ROP 类似,只不过是把 ROP 中以 ret 指令做跳板的关键点替换成了 call/jmp 指令。还是拿讲述 ROP章节的两幅图(图2 图3)举例,图3中偏移解释字节码后出现了不同的指令,包含了:

操作系统

图8 CET-IBT call指令攻击

这⼀指令序列,这就是 COP 中的 gadget ,以 call 指令为跳板,也就不需要 ret 指令的辅助了。这种不需要 ret 指令的攻击场景下,前⾯所说的 shadow stack 机制就失效了。这种情况下, CET 的第⼆种机制 IBT(Indirect Branch Tracking) 就应运⽽⽣了。

3.2.1原理分析

IBT(Indirect Branch Tracking),间接跳转跟踪"希望能防止攻击者让间接跳转(例如,通过指针变量进行的函数调用)进入一个不应该走到的地方。功守道之中是为了应对JOP/COP的。

IBT 是为了防御面向跳转编程的(jump-oriented programming);工作原理是试图确保每个 indirect branch 的目标确实都是适合作为跳转目标的。IBT 的方法有很多,每一种都有自己的优势和劣势。例如,内核在 5.13 开发周期中支持了编译器实现的 IBT 机制。

在这种模式下,编译器通过一个 "jump table, 跳转表" 来完成每一个间接跳转,不仅确保目标是要供间接跳转使用的,而且要确保被调用函数的原型与调用者所期望的一致。这种方法是很有效的,但要增加很多编译、运行时的开销。

英特尔的 IBT 方法相当简单,但优点是得到了硬件的支持,因此速度更快.如果 IBT 被启用,那么 CPU 将确保每个间接跳转都落在一条特殊指令(endbr32 或 endbr64)上(见图9),该指令执行时跟 no-op 效果一致。如果发现意外,那么处理器将引发一次 control-protection(#CP)exception。

操作系统

图9 CET-IBT ENDBR标志嵌入辅助检测

3.3.2测试方法

操作系统

图10 内核测试函数

操作系统

图11 CET-IBT 运行测试结果

3.3 硬件机制.

CPU 是如何实现这种校验机制的呢?答案是⽤了 ENDBRANCH 状态机。

CPU 在⽤户态和内核态分别设有⼀个 ENDBRANCH 状态机,状态机共有两个状态,为 IDLE 和WAIT_FOR_ENDBRANCH 。初始状态都为 IDLE ,当执⾏间接跳转 call 的时候,状态机会从 IDLE变为WAIT_FOR_ENDBRANCH ,这时候就意味着下⼀条指令就必须为 endbr32/64 ,如果是 endbr32/64 ,状态机就会从 WAIT_FOR_ENDBRANCH 变回 IDLE ,如果不是则会触发 #CP 异常。

操作系统

图12 状态机

3.4 其他objtool

需要在所有的间接跳转目标地址都添加 endbr 指令,这就为今后造成了一个潜在的陷阱:开发者可能会添加汇编函数但是忘记了这个指令。如果他们在没有启用 IBT 的情况下进行测试,那么这个问题就不会被注意到,可能直到有问题的代码被合并后的某个让人非常不爽的时间点才突然暴露出来。为了防止这种情况的发生,开发者加强了内核的 objtool 工具来检查所有的间接跳转,并确保所有的目标都恰当地标注(annotated)好了。

不过,有了这种检查之后,还可以采取另一个步骤:objtool 还可以列出所有包含 endbr 指令但是永远不会通过间接跳转到达的函数。这些函数不需要进行标注,没有它们的话,内核会更安全一些。因此,内核 build 的过程会从 objtool 中获取该列表,并通过用 nop4 指令覆盖 endbr 来 "封印" 这些函数。这就减少了攻击者在启用 IBT 时仍然可以选择的攻击目标。

正如 Peter Zijlstra 所指出的,删除不需要的 endbr 指令还有一个也许是令人惊讶的好处。内核限制了 loadable module 所能使用的函数,而那些专有的(proprietary)module 则受限更多。那些专有的 module 经常使用一个技术就是在内核的符号表中查找到它们需要的、未 export 的函数,然后通过间接跳转来调用它们,从而绕过内核的限制。但是,在启用 IBT 后,任何缺乏 endbr 指令的函数都不再能以这种方式调用了。

4 性能分析

操作系统

图13 递归调用

操作系统

图14 CET性能分析

操作系统

图15 CET 性能分析

5 内核开发进度

内核开发者已经努力了一段时间,希望能让英特尔 IBT 功能进入 Linux 内核。实现这个功能的第一个 patch(针对用户空间代码而不是针对内核)是由 Yu-cheng Yu 在 2018 年发布的。然后,这项工作似乎成了那些不断穿越邮件列表的飞行荷兰人(flying-Dutchman)patch 之一,从未能够进入 mainline;第 30 版是 2021 年 8 月发布的,也没有达到能够合并的程度。类似的情况也发生在了用户空间的影子堆栈(user-space shadow-stack)patch set 上,在经历多次修订后,这些 patch 最近被 Rick Edgecombe 接手了。

2021年年底,Peter Zijlstra 决定创建一个独立的英特尔 IBT 实现来保护内核本身;第一个版是在去年 11 月发布的,Zijlstra 显然是 "在周五晚上/周六早上赶工出来的"。这项工作发展得很快,3月初发布的第四个修订版就是为 5.18 合并的版本了(链接见参考文档CET-IBT patches)

这就是目前为止的情况了。从 2020 年底上市的 Tiger Lake 一代开始,英特尔处理器就支持 IBT 了,只支持了内核代码。这并不是一个完美的工具,但它将提高攻击者攻击这类系统的门槛。同时,目前还不清楚用户空间的支持何时(或是否)会进入 mainline 内核;到目前为止发布的 30 个修订版中,有许多根本没有收到任何 review 意见。该部分已经被Rick Edgecombe接手并发布了v1版本,后续我们拭目以待,希望能在6.2中会合入全部的CET代码。





审核编辑:刘清

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分