0x00 TL;DR
CET(CONTROL-FLOW ENFORCEMENT TECHNOLOGY)
机制是Intel
提出的用于缓解ROP/JOP/COP
的新技术。因其具备“图灵完备”的攻击效果,ROP
一直是漏洞利用领域经常使用的攻击技术,在漏洞防御方面,针对ROP
攻击技术也不断地在做新的尝试。例如微软的CFG
缓解技术,虽然能够起到一定的缓解效果,但是在复杂场景的攻击下还不足够。CET
是一项基于硬件支持的解决方案,旨在预防前向(call/jmp
)和后向(ret
)控制流指令劫持。本文将从CET
的设计理念和实际应用出发,探索CET
技术在攻防上带来的新变化。文中涉及到的代码均来自linux_cet。
0x01 Shadow Stack - 原理
特性:
shadow stack
是用于程序控制流转移的第二个栈,与数据栈是分离的,并且可以独立选择在用户模式或特权模式下启用。当shadow stack
开启时,CALL
指令会把返回地址同时压入数据栈和影子栈(shadow stack
),RET
指令会把返回地址同时从数据栈和影子栈取出,并比较。如果从两个栈中取出的返回地址不匹配,那么就会触发控制保护异常(#CP)。对影子栈的写入被严格控制在控制传输指令以及影子栈管理指令,这种来自控制传输指令以及影子栈管理指令的加载、存储(读和写)被称为shadow_stack_load
和shadow_stack_store
,以区别于其他指令,如MOV
、XSAVES
等指令执行的加载和存储。
SSP寄存器:影子栈开启时,CPU会支持一个新的寄存器,为shadow stack pointer
(SSP),SSP寄存器在指令中不能直接作为源地址、目标地址以及内存操作数。SSP
和SP
寄存器一样,指向当前影子栈的最顶端。
Supervisor Shadow Stack Token: 在特权内的far CALL
或在更高特权调用中断/异常处理的时候,会触发栈切换,如果影子栈在切换的特权中启用的话,也同样会触发影子栈的切换。这种情况下,需要管理员设置的Supervisor Shadow Stack Token
用以提供新SSP
寄存器的地址。Supervisor Shadow Stack Token
的地址存储于IA32_PLx_SSP MSR (0≤ x ≤2)
。
影子栈切换:
CET
提供了一对指令来配合实现栈切换的过程,为RSTORSSP
和SAVEPREVSSP
。RSTORSSP
指令用于验证新影子栈上的shadow-stack-restore token
,验证有效后将SSP
切换到该token
去。该token
字节格式如下:
Bit 63:2 :影子栈指针的值(当还原点被创建的时候)
Bit 1 :保留,为0
Bit 0 :为0代表传统模式的shadow-stack-restore token,为1代表64位模式下可以被RSTORSSP指令使用
shadow-stack-restore token
是被SAVEPREVSSP
指令所创建的,操作系统也可以在影子栈上创建还原点。一旦使用RSTORSSP
指令切换到新的影子栈,便可以执行SAVEPREVSSP
指令在旧的影子栈创建还原点。为了让SAVEPREVSSP
指令确定保存shadow-stack-restore token
的地址,RESTORSSP
指令会用previous-ssp token
(包含了指令调用时的SSP
的值)替换shadow-stack-restore token
。previous-ssp token
字节格式如下:
Bit 63:2 :RSTORSSP指令调用时的影子栈指针,即SSP的值
Bit 1 :设置为1
Bit 0 :模式位。为0代表可以在传统模式下被SAVEPREVSSP指令使用,为1代表可以在64位模式下被使用。
下面用图示来描述一下影子栈切换的过程:
切换前的影子栈SSP
的值为1000H
,先检查新影子栈的shadow-stack-restore token
,在3FF8H
处,保存着还原点创建时候的SSP
,这个例子中为4000H
。随后将SSP
切换到3FF8H
处,在将3FF8H
处替换为previous-ssp token
,即1000H
(RSTORSSP
调用时的SSP
值)。
为了能够切换回旧的影子栈,需要调用SAVEPREVSSP
。先找到previous-ssp token
,在3FF8H
处。随后在旧的影子栈中保存shadow-stack-restore token
,在FF8H
处,其中保存的值为记录在previous-ssp token
中的地址1000H
。最终,SAVEPREVSSP
会将当前影子栈中的previous-ssp token
给pop
(INCSSP
指令)出来,SSP
变为4000H
。
总结来说,RSTORSSP
指令包含验证shadow-stack-restore token
、切换SSP
、设置previous-ssp token
。SAVEPREVSSP
指令包含找到previous-ssp token
、设置shadow-stack-restore token
、弹出previous-ssp token
。
0x02 Shadow Stack - 代码分析
理论说再多也没有直接看代码来的实在,CET
具体在Linux
内核中是怎么实现的,下面一起来看看。
首先看shstk_setup()
函数/arch/x86/kernel/shstk.c
,这个函数会在arch_setup_elf_property
函数中被调用,用于elf
在系统中加载执行的时候设置当前进程的影子栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| struct thread_shstk { u64 base; u64 size; u64 locked:1; u64 ibt:1; };
int shstk_setup(void) { struct thread_shstk *shstk = ¤t->thread.shstk; unsigned long addr, size; int err;
size = round_up(min_t(unsigned long long, rlimit(RLIMIT_STACK), SZ_4G), PAGE_SIZE); addr = alloc_shstk(size); if (IS_ERR_VALUE(addr)) return PTR_ERR((void *)addr);
start_update_msrs(); err = wrmsrl_safe(MSR_IA32_PL3_SSP, addr + size); if (!err) wrmsrl_safe(MSR_IA32_U_CET, CET_SHSTK_EN); end_update_msrs();
if (!err) { shstk->base = addr; shstk->size = size; }
return err; }
static unsigned long alloc_shstk(unsigned long size) { int flags = MAP_ANONYMOUS | MAP_PRIVATE; unsigned long addr, populate;
addr = do_mmap(NULL, 0, size, PROT_READ, flags, VM_SHADOW_STACK, 0, &populate, NULL);
return addr; }
|
由此可见影子栈就是映射了一块只读、匿名且私有的内存。
往后再看shstk_alloc_thread_stack()
函数,该函数会被copy_thread
函数调用。用于在fork、vfork、clone
等多进程系统调用中创建新的影子栈。例如当使用fork
系统调用的时候,子进程不会与父进程共享内存,而是单独分一块内存,因此这种情况下子进程的影子栈也应当与父进程的影子栈区分开来,需要再申请一块影子栈给子进程使用。但是这里vfork
是个例外,因为vfork
会使得子进程与父进程共享同一块内存,就不存在前面这种情况了。这一块的代码也是容易理解的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| int shstk_alloc_thread_stack(struct task_struct *tsk, unsigned long clone_flags, unsigned long stack_size) { struct thread_shstk *shstk = &tsk->thread.shstk; struct cet_user_state *state; unsigned long addr;
if ((clone_flags & (CLONE_VFORK | CLONE_VM)) != CLONE_VM) return 0;
state = get_xsave_addr(&tsk->thread.fpu.state.xsave, XFEATURE_CET_USER); if (WARN_ON_ONCE(!state)) return -EINVAL;
stack_size = round_up(stack_size, PAGE_SIZE); addr = alloc_shstk(stack_size); if (IS_ERR_VALUE(addr)) { shstk->base = 0; shstk->size = 0; return PTR_ERR((void *)addr); }
state->user_ssp = (u64)(addr + stack_size); shstk->base = addr; shstk->size = stack_size; return 0; }
|
接下来还有影子栈free
以及disable
两个函数,也容易理解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| void shstk_disable(void) { struct thread_shstk *shstk = ¤t->thread.shstk; u64 msr_val;
start_update_msrs(); rdmsrl(MSR_IA32_U_CET, msr_val); wrmsrl(MSR_IA32_U_CET, msr_val & ~CET_SHSTK_EN); wrmsrl(MSR_IA32_PL3_SSP, 0); end_update_msrs();
shstk_free(current); }
void shstk_free(struct task_struct *tsk) { struct thread_shstk *shstk = &tsk->thread.shstk;
while (1) { int r;
r = vm_munmap(shstk->base, shstk->size);
if (r == -EINTR) { cond_resched(); continue; } }
shstk->base = 0; shstk->size = 0; }
|
再来看比较关键的setup_signal_shadow_stack()
和restore_signal_shadow_stack()
函数,这两个函数分别会被__setup_rt_frame
和rt_sigreturn
调用,即都应用于信号处理这一部分,分别对应着信号注册和信号返回。先看setup_signal_shadow_stack()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| int setup_signal_shadow_stack(int ia32, void __user *restorer) { struct thread_shstk *shstk = ¤t->thread.shstk; unsigned long new_ssp; int err;
err = shstk_setup_rstor_token(ia32, (unsigned long)restorer, &new_ssp);
start_update_msrs(); err = wrmsrl_safe(MSR_IA32_PL3_SSP, new_ssp); end_update_msrs();
return err; }
int shstk_setup_rstor_token(bool ia32, unsigned long ret_addr, unsigned long *new_ssp) { struct thread_shstk *shstk = ¤t->thread.shstk; unsigned long ssp, token_addr; int err;
ssp = get_user_shstk_addr();
err = create_rstor_token(ia32, ssp, &token_addr);
} else { ssp = token_addr - sizeof(u64); err = write_user_shstk_64((u64 __user *)ssp, (u64)ret_addr); }
if (!err) *new_ssp = ssp;
return err; }
static int create_rstor_token(bool ia32, unsigned long ssp, unsigned long *token_addr) { unsigned long addr;
addr = ALIGN_DOWN(ssp, 8) - 8;
if (!ia32) ssp |= BIT(0);
if (write_user_shstk_64((u64 __user *)addr, (u64)ssp)) return -EFAULT;
*token_addr = addr;
return 0; }
|
该函数整体看下来,就是实现了最开始理论部分的shadow-stack-restore token
,即创建还原点的过程。用图简单表示一下:
同样的,restore_signal_shadow_stack()
从名字上就可以大致猜到是用于还原影子栈的函数了,具体看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| int restore_signal_shadow_stack(void) { struct thread_shstk *shstk = ¤t->thread.shstk; int ia32 = in_ia32_syscall(); unsigned long new_ssp; int err;
err = shstk_check_rstor_token(ia32, &new_ssp);
start_update_msrs(); err = wrmsrl_safe(MSR_IA32_PL3_SSP, new_ssp); end_update_msrs();
return err; }
int shstk_check_rstor_token(bool proc32, unsigned long *new_ssp) { unsigned long token_addr; unsigned long token; bool shstk32;
token_addr = get_user_shstk_addr();
if (get_user(token, (unsigned long __user *)token_addr)) return -EFAULT;
shstk32 = !(token & BIT(0)); if (proc32 ^ shstk32) return -EINVAL;
*new_ssp = token;
return 0; }
|
该函数正如所猜想的,就是用于还原影子栈。拿上面表示创建shadow-stack-restore token
的图来说,就是将0x1001
去除标志位后赋值给ssp
寄存器,作为新的影子栈栈顶使用。
最后,在shadow stack
的实现里,还将创建的过程单独实现了一个API
,使得能够在用户态创建影子栈,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| SYSCALL_DEFINE2(arch_prctl, int, option, unsigned long, arg2) { long ret;
ret = do_arch_prctl_common(current, option, arg2);
return ret; }
long do_arch_prctl_common(struct task_struct *task, int option, unsigned long arg2) { return prctl_cet(option, arg2); }
int prctl_cet(int option, u64 arg2) { #ifdef CONFIG_X86_SHADOW_STACK case ARCH_X86_CET_ALLOC_SHSTK: return handle_alloc_shstk(arg2); #endif }
unsigned long cet_alloc_shstk(unsigned long len) { unsigned long token; unsigned long addr, ssp;
addr = alloc_shstk(round_up(len, PAGE_SIZE));
ssp = addr + len; token = ssp;
if (!in_ia32_syscall()) token |= BIT(0); ssp -= 8;
if (write_user_shstk_64((u64 __user *)ssp, (u64)token)) { }
return addr; }
|
也就是说我们可以在用户态用如下系统调用的语句来创建影子栈:
1 2 3 4
| uint64_t buf[3] = {0}; buf[0] = 0x1000;
syscall(SYS_arch_prctl, ARCH_X86_CET_ALLOC_SHSTK, buf);
|
以上就是Linux
中实现shadow stack
的大概了,对照着白皮书上的相关概念来看,目前只实现了一部分,还有不少需要添加的地方,例如ring0
层面的shadow stack
、系统调用的影子栈切换、进程间的影子栈切换…
0x03 Shadow Stack - 缓解效果
分析代码往往还不够,因为还并没有真正意义上的动手操作,下面就实际看看shadow stack
缓解ROP
的效果如何。
demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <stdio.h> #include <stdint.h> #include <stdlib.h>
int func(){ int b[1];
b[0] = 0x90909090; b[1] = 0x90909090; b[2] = 0x90909090; b[3] = 0x90909090; }
int main(){ func(); return 0; }
|
gdb
中的情况,返回地址已被修改,和shadow stack
中保存的返回地址并不相同,继续执行会导致崩溃:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| gdb-peda$ disassemble Dump of assembler code for function func: 0x00000000004005c6 <+0>: endbr64 0x00000000004005ca <+4>: push rbp 0x00000000004005cb <+5>: mov rbp,rsp 0x00000000004005ce <+8>: mov DWORD PTR [rbp-0x4],0x90909090 0x00000000004005d5 <+15>: mov DWORD PTR [rbp+0x0],0x90909090 0x00000000004005dc <+22>: mov DWORD PTR [rbp+0x4],0x90909090 0x00000000004005e3 <+29>: mov DWORD PTR [rbp+0x8],0x90909090 0x00000000004005ea <+36>: nop 0x00000000004005eb <+37>: pop rbp => 0x00000000004005ec <+38>: ret End of assembler dump. gdb-peda$ x/2xg $rsp 0x7fffffffe148: 0x0000000090909090 0x0000000000400610 gdb-peda$ vmmap 0x7ffff7dcfff8 Start End Perm Name 0x00007ffff75d0000 0x00007ffff7dd0000 r--p [shadow stack] gdb-peda$ x/3xg 0x00007ffff7dd0000 - 0x18 0x7ffff7dcffe8: 0x00000000004005ff 0x00007ffff722d493 0x7ffff7dcfff8: 0x000000000040050e gdb-peda$ disassemble 0x00000000004005ff Dump of assembler code for function main: 0x00000000004005ed <+0>: endbr64 0x00000000004005f1 <+4>: push rbp 0x00000000004005f2 <+5>: mov rbp,rsp 0x00000000004005f5 <+8>: mov eax,0x0 0x00000000004005fa <+13>: call 0x4005c6 <func> 0x00000000004005ff <+18>: mov eax,0x0 0x0000000000400604 <+23>: pop rbp 0x0000000000400605 <+24>: ret End of assembler dump. gdb-peda$ ni Program received signal SIGSEGV, Segmentation fault.
|
内核日志:
0x04 IBT - 原理
特性:
IBT(indirect branch tracker)
应用于间接跳转(jmp/call
指令),如果在间接跳转后的下一条指令不是ENDBR32
或ENDBR64
,就会触发#CP
异常。并不包括RIP
相对跳转、远直接jmp
跳转、call
相对跳转等,这些都是跳转到固定地址,不存在被篡改的可能,因此IBT
并不作用于这种情况。
ENDBR
:在不支持CET
的英特尔CPU
上,ENDBR32
和ENDBR64
指令有着同样的作用,可以视为和NOP
指令一样。因此无论在支持或不支持CET
的处理器上,带IBT
特性的程序执行过程都一样。
双状态机:处理器实现了两个双状态机去跟踪间接跳转,一个用于用户态,一个用于内核(特权)态。两个状态机初始都为IDLE
状态。当执行一个间接跳转(CALL
或JMP
指令)时,状态机会转变为WAIT_FOR_ENDBRANCH
状态。在WAIT_FOR_ENDBRANCH
状态时,状态机会验证下一个指令是否为ENDBR32
或ENDBR64
,不是则抛#CP
异常。
No-track前缀:
CET
允许软件指定某个间接跳转指令为”不跟踪间接跳转“,即使得IBT
暂时失效。这种情况下可以在CALL/JMP
处添加no-track
前缀。通过在IA32_U_CET/IA32_S_CET MSR
寄存器使能NO_TRACK_EN
,就可以使得带3EH
前缀(no-track
前缀)的近地址间接跳转指令(near indirect CALL/JMP
)不改变IBT
。远地址间接跳转指令(Far CALL/JMP
)始终会被IBT
跟踪且忽略3EH
前缀。当NO_TRACK_EN
控制为0
时,无论是否带3EH
前缀,近地址间接跳转也始终会被跟踪。
IBT切换:
CPL 3和CPL < 3之间:
一个在用户态(CPL == 3)执行的进程因为中断切换到内核态(CPL < 3),会导致用户态状态机切换到内核态状态机,并且用户态的IBT
会变为inactive
,内核态的IBT
会变为active
。后续的IRET
指令会将进程从中断处理(CPL < 3)返回到用户态(CPL == 3)进程,同时会导致内核态IBT
变为inactive
,用户态IBT
变为active
。具体分为下面三种情况,所有情况中源IBT
状态都变为inactive
且保持状态机的状态:
- 情况1:
Far CALL/JMP,SYSCALL/SYSENTER
目标IBT
状态变为active
,并且不受抑制,状态机转变为WAIT_FOR_ENDBRANCH
。因此这也强制要求子例程被far CALL/JMP
调用时必须要以ENDBRANCH
开头。
- 情况2:硬件中断/陷阱/异常/NMI/软件中断/Machine Checks
目标IBT
状态变为active
,并且不受抑制,状态机转变为WAIT_FOR_ENDBRANCH
。
目标IBT
状态变为active
,并保持状态机的状态。如果用户态被更高优先级的事件中断,例如在间接跳转最后的中断,那么当使用IRET
或Far RET
返回被中断用户态时,用户态IBT
会保持状态机的状态并且验证下一个指令不是ENDBR32
或ENDBR64
时触发#CP
异常。
CPL < 3内:
这种情况下在控制流程开始到结束,用的都是同一个IBT
且为active
,还是分为三种情况:
- 情况1:
Far CALL/JMP, Near indirect CALL/JMPCALL/JMP
Far CALL/JMP
:不受抑制且转变为WAIT_FOR_ENDBRANCH
。
Near indirect CALL/JMPCALL/JMP
:不受抑制且转变为WAIT_FOR_ENDBRANCH
。
- 情况2:硬件中断/陷阱/异常/NMI/软件中断/Machine Checks
不受抑制且转变为WAIT_FOR_ENDBRANCH
。
保持状态机的状态。
0x05 IBT - 代码分析
在内核实现中,有关IBT
的代码比较少,先来看初始化函数ibt_setup()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| int ibt_setup(void) { int r;
r = ibt_set_clear_msr_bits(CET_ENDBR_EN | CET_NO_TRACK_EN, 0); if (!r) current->thread.shstk.ibt = 1;
return r; }
static int ibt_set_clear_msr_bits(u64 set, u64 clear) { u64 msr; int r;
r = rdmsrl_safe(MSR_IA32_U_CET, &msr); if (!r) { msr = (msr & ~clear) | set; r = wrmsrl_safe(MSR_IA32_U_CET, msr); }
return r; }
|
初始化函数还是很简短的,同样地,关闭函数也很容易理解:
1 2 3 4 5 6
| void ibt_disable(void) { ibt_set_clear_msr_bits(0, CET_ENDBR_EN); current->thread.shstk.ibt = 0; }
|
接下去看ibt_get_clear_wait_endbr()
函数,该函数也会被__setup_rt_frame
函数调用,由此可见也是作用在信号处理过程当中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int ibt_get_clear_wait_endbr(void) { u64 msr_val = 0;
if (!test_thread_flag(TIF_NEED_FPU_LOAD)) { if (!rdmsrl_safe(MSR_IA32_U_CET, &msr_val)) wrmsrl(MSR_IA32_U_CET, msr_val & ~CET_WAIT_ENDBR); } else { }
return msr_val & CET_WAIT_ENDBR; }
|
流程比较简单,即在信号处理函数后、执行下一个函数前将状态机复位为IDLE
状态。
与之对应的是ibt_set_wait_endbr()
函数,该函数会被rt_sigreturn
调用,即返回到被信号中断的用户态函数前将状态机转变为WAIT_FOR_ENDBRANCH
:
1 2 3 4 5
| int ibt_set_wait_endbr(void) { return ibt_set_clear_msr_bits(CET_WAIT_ENDBR, 0); }
|
以上就是IBT
机制的大部分实现了,实现的地方比较少,一是因为CET
总体的实现还不完善,例如没有系统调用时IBT
跟踪ENDBR
的实现等,二是因为IBT
机制更多的是在CPU
硬件层面上的实现,例如执行间接跳转CALL/JMP
指令时会检查下一条指令是否为ENDBR32
或ENDBR64
,这种实现都是在硬件层面做的,因此IBT
机制需要在操作系统层面需要做的改动就少一些。
0x06 IBT - 缓解效果
demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| #include <stdio.h> #include <stdint.h> #include <stdlib.h> void shell(){ system("/bin/sh"); return; }
void normal(){ printf("crrocet control.\n"); return; }
typedef struct stru{ void (*ops)(void); int num; }stru;
int main(){
uint32_t over[2] = {0};
stru stru1 = {0}; stru1.ops = normal; stru1.num = 0x100;
stru1.ops();
over[0] = 0x90909090; over[1] = 0x90909090; over[-6] = 0x40068a;
if(!over[0]){ normal(); } else{ stru1.ops(); }
return 0; }
|
gdb
中的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 0x00000000004006e3 <+51>: mov rax,QWORD PTR [rbp-0x20] => 0x00000000004006e7 <+55>: call rax 0x00000000004006e9 <+57>: mov DWORD PTR [rbp-0x8],0x90909090
gdb-peda$ p stru1 $2 = { ops = 0x40068a <_dl_relocate_static_pie>, num = 0x100 }
gdb-peda$ disassemble 0x40068a Dump of assembler code for function shell: 0x0000000000400686 <+0>: endbr64 0x000000000040068a <+4>: push rbp 0x000000000040068b <+5>: mov rbp,rsp 0x000000000040068e <+8>: mov edi,0x4007b8 0x0000000000400693 <+13>: call 0x400590 <system@plt> 0x0000000000400698 <+18>: nop 0x0000000000400699 <+19>: pop rbp 0x000000000040069a <+20>: ret End of assembler dump.
gdb-peda$ ni Program received signal SIGSEGV, Segmentation fault.
|
内核日志:
0x07 CET是如何使能的?
再来深度的思考一个问题,CET
是如何使能的,为什么编译了一个ELF
文件这个文件就支持了CET
呢?答案得从编译器和内核两头中去找,先看编译器的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| void file_end_indicate_exec_stack_and_cet (void) { file_end_indicate_exec_stack (); if (flag_cf_protection == CF_NONE) return; unsigned int feature_1 = 0; if (TARGET_IBT) feature_1 |= 0x1; if (TARGET_SHSTK) feature_1 |= 0x2; if (feature_1) { int p2align = ptr_mode == SImode ? 2 : 3; switch_to_section (get_section (".note.gnu.property", SECTION_NOTYPE, NULL)); } }
|
简单来说就是会在编译阶段在ELF
相关的段上做CET
的标记,存在两个feature
:GNU_PROPERTY_X86_FEATURE_1_IBT
和GNU_PROPERTY_X86_FEATURE_1_SHSTK
。
再来看内核中的实现,直接看内核加载ELF
文件的函数load_elf_binary()
,关键在于arch_setup_elf_property
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| static int load_elf_binary(struct linux_binprm *bprm) { retval = arch_setup_elf_property(&arch_state); if (retval < 0) goto out; }
int arch_setup_elf_property(struct arch_elf_state *state) { int r = 0;
#ifdef CONFIG_X86_SHADOW_STACK memset(¤t->thread.shstk, 0, sizeof(struct thread_shstk));
if (state->gnu_property & GNU_PROPERTY_X86_FEATURE_1_SHSTK) r = shstk_setup();
if (r < 0) return r;
if (state->gnu_property & GNU_PROPERTY_X86_FEATURE_1_IBT) r = ibt_setup(); #endif
return r; }
|
从上面的函数可以看出在gcc
中添加的属性,在内核中得到了验证,若存在,则进程加载的过程中内核就为进程开启了CET
机制。由此就可以知道CET
是如何使能的大概过程了。
0x08 总结
以上就是CET
的概述了。总的来说CET
在硬件层面实现的缓解机制与以往的软件层面缓解机制有着比较大的不同,在性能上面就比软件实现的快了许多,且从目前的情况来看,还没有很有效的绕过CET
的方法,在缓解能力方面也比以往的缓解措施加强了许多。目前CET
机制还没有很广泛的使用,从Linux
上的具体实现就可以看出来,但在不久的将来,随着CET
的推广,CET
机制又会给ROP
攻击手法带来较大的困难甚至根除ROP
。道高一尺,魔高一丈,期待在未来的攻防对抗过程中又能碰撞出不一样的火花。
0x09 引用
- https://github.com/yyu168/linux_cet
- https://www.intel.com/content/www/us/en/develop/articles/technical-look-control-flow-enforcement-technology.html
- https://www.intel.com/content/dam/develop/external/us/en/documents/catc17-introduction-intel-cet-844137.pdf
- https://windows-internals.com/cet-on-windows/
- http://readm.tech/2016/11/09/cet-shadow_stacks/
- https://www.offensive-security.com/offsec/intel-cet-in-action/#cet1