控制流劫持技术 (Control-Flow Hijacking)
篡改程序的正常执行流,来执行恶意代码(执行恶意的 shellcode 或代码复用攻击,如 ROP)
常见的实现方式:
- 栈溢出修改返回地址
- 修改函数指针,间接跳转劫持
简单形式:代码注入攻击-执行shellcode
进阶:代码重用攻击-ROP-返回式编程
用处:绕过DEP(数据执行保护)
- Gadget:以
ret
指令为结尾的代码片段(程序中合法的代码片段)- ROP链:精心设计的gadgets
进阶:代码重用攻击-JOP-面向跳转的编程
控制流完整性(Control-Flow Integrity)
防御技术
栈canary
在函数栈帧的局部变量之后插入一个特殊的值,称为
canary
在函数返回前检查这个值是否被篡改。若变化,说明可能发生了缓冲区溢出攻击,程序会中止执行
Windows平台下canary的编译支持与检验
- 微软MSVC编译器开启
/GS
(Stack Guard)选项 - 地址
FS:[0x28]
(FS:段寄存器):表示当前线程的stack cookie
,即canary__readfsqword(0x28u)
:Windows平台下读取canary的方法
- 检验:
// 贴近编译器生成的伪代码 unsigned __int64 callme() { unsigned __int64 __stack_canary = __readfsqword(0x28u);// 双下划线开头的变量,属于编译器变量 // 函数主体 // ... if (__readfsqword(0x28u) != __stack_canary) { __security_check_cookie_fail(); // Canary 值被篡改,可能存在栈溢出 } return 0; // 正常退出 }
Linux平台下canary的编译支持与检验
- GCC编译器指定
-fstack-protector
- 类似的,
glibc
使用符号变量__stack_chk_guard
作为当前线程的canary - 检验:
$ gcc -fstack-protector-all -o test test.c
$ objdump -d test | grep __stack_chk_guard
mov rax,QWORD PTR fs:0x28
mov QWORD PTR [rbp-0x8],rax ; 保存 canary 到局部变量
...
mov rax,QWORD PTR fs:0x28
xor rax,QWORD PTR [rbp-0x8]
je .Lreturn_ok
call __stack_chk_fail ; Canary 被破坏,执行终止
ASLR 地址空间布局随机化
启动ASLR选项
默认情况下,ASLR选项是关闭的🚫
- 方法1️⃣:在系统进行初始程序装入(IPL)时,使用DIAGxx指定ASLR选项
- 方法2️⃣:在IPL之后,设置
SET DIAG=xx
命令- 此时只会影响在启动ASLR选项之后启动的程序,不会影响先前已经启动的
DEP 数据执行保护
- 限定内存页不能同时具有执行权限和写权限
- 预防shellcode注入
基于硬件–控制流强制技术 CET
- Control-flow Enforcement Technology
- 基于硬件的
- 预防控制流劫持攻击
- 包括前向控制流劫持(JMP CALL)
- 后向控制流劫持(RET)
目前,64位内核中,仅支持用户空间的
影子栈
技术和内核空间的IBT
技术
Shadow Stack-影子栈
在内存中分配第二个栈。影子栈是不能被应用直接修改的。 在执行函数调用(
CALL
指令)时,处理器将返回地址
同时push
到原栈
和影子栈
上。等函数返回时,比较影子栈
的返回地址
与原栈
是否一致。若不同,则触发control protection fault
影子栈
的实质是映射一块私有、匿名、只读的内存- 私有。相对的是共享
- 匿名页面。相对的是文件页面,即映射到文件的页面。
- 只读:影子栈只读,只用来存储返回地址。
IBT(Indirect-branch-tracking)-间接分支追踪
- 间接分支
- 依赖动态计算目标地址
- 包括函数调用
call
、跳转jmp
、RET
- 目标地址通常保存在指针里
- 一旦攻击者恶意填充目标地址内容,即可劫持控制流。
- 间接分支追踪
验证间接调用(
CALL
)或跳转(JMP
)的目标是否使用endbr
操作码标记。
endbr64
用于64位;endbr32
用于32位- 用于间接分支保护。编译器在每个可间接调用的函数入口插入
endbr64
指令。如果jmp
call
跳转到非endbr
开头的函数,则CPU触发异常。