回顾:针对代码注入的防护机制
1. 经典代码注入的核心思想
利用逻辑异常,在程序数据中混入代码
劫持控制流,使得指令指针从数据段读取指令
所需弱点:读取指令时,CPU无法区分目标内存区域的性质
2. 数据执行保护(Data Execution Prevention)
别名:W⊕X,NX-bit
机制:对内存页面增加一个标识bit,使之要么可改写,要么可执行
防御思路:引入新的硬件安全属性,支持CPU在执行时区分代码和数据区域(但是:若不试图执行数据区内容,则不能发现/阻止溢出,也无法防范溢出的其他后果)
目标:绕过DEP(代码重用(code reuse)攻击)
- 异常数据的注入仍然可以实施
- 代码无法直接注入,那么就利用进程空间中已经存在的代码
- 不管所利用代码原本的用途为何,通过注入的异常数据控制其为攻击者服务
return-to-libc攻击
返回导向编程(return-oriented programming)(图灵完备)
一种非常规方式将短小指令片段串联成完整的代码流
为ROP搜索可用的指令资源 --- Galileo算法
特点:
- 始于对程序控制流的篡改(控制流异常)
- 各gadget由ret指令(或者pop-jump组合)代替eip加以链接(大量控制流异常)
- 原始栈结构遭到破坏,ROP过程中栈指针单向移动(栈的行为异常)
- 有时,栈指针可能被篡改并指向不属于栈区的内存(栈的行为异常)
注:
ret指令(与栈溢出方向吻合,具有指令寻址能力)
- 根据当前栈顶内容,修改指令寄存器所指向的地址
- 将栈指针位置向栈底方向移动4字节
图灵完备的指令集应当包括
- 加载/存储
- 算术,逻辑运算,位移
- 控制流:条件跳转,系统调用
利用点1:复杂指令集(complex instruction set computing,CISC)
步骤:
- 将劫持后的控制流引导至正常指令的内部(middle of an instruction)
- 利用CISC的特点,形成意外的指令片段(unintended instruction sequence)
- Gadget资源的重要来源:因误解析而形成的“指令”(特别是0xC3字节)
需要注意的是:
- 指令变长(目的:以最短的代码长度压缩最多的指令内容)
- CPU串行地读取指令,依据上下文区分操作码/操作数等成分
- 典型:x86指令构架
- 存在问题:代码中一个字节的含义取决于上下文
a. 各种指令的使用率相差悬殊,且微码串行执行让频繁使用的简单指令也效率低下
b. 复杂指令 → 复杂的硬件结构,CISC越来越难以集成在单一芯片上
c. 许多复杂指令需要极复杂的操作,多数已可视为某种高级语言的翻版,通用性差
利用点2:精简指令集(RISC)
背景:
CISC存在许多缺点(上面说过了)
RISC的特点:
统一指令编码,如所有指令长度相等、op-code位置相同等,可快速解译
泛用的缓存器,单纯的寻址模式(用计算指令序列取代复杂寻址模式)
硬件中支持少数数据型别(如区分整数/浮点数等)
栗子
“可扩充处理器架构”(Scalable Processor ARChitecture,SPARC)
SPARC的寄存器“窗口”机制
- 32个通用寄存器,其中8个全局寄存器和一个“窗口”(包含24个寄存器)
- 支持2~32个“窗口”(取决于硬件实现),通常为7~8个——由此得名“可扩展的”
- 在任何时候,只有一个寄存器窗口是可见的
针对RISC的返回导向编程(图灵完备)
对SPARC构造返回导向编程所面临的问题:
无法利用意外的指令序列(该种情况不可能发生)
x86下返回导向编程gadget的所有构造特点在RISC中均不存在新的返回导向编程设计思路:
将函数的后缀作为gadget使用(利用其结尾处的ret-restore指令序列)
利用结构化数据流使得gadget与SPARC的函数调用惯例相吻合
构造内存-内存gadget(寄存器仅在gadget内部使用)
如何防御
核心思路:破坏返回导向编程的原子指令组件(gadget),阻止x86代码中出现意外的gadget
难点:x86编译器所生成的代码高度优化,难以随意改变指令长度
- 因此,Smashing the gadgets必须是(in-place)的
- 像上面这样抹掉0xC3字节仅仅是可用的手段之一
手段2:指令重新排序
手段3:寄存器压栈顺序随机化
手段4:寄存器重分配
ROP without return
参考论文:Return-Oriented Programming without Returns
有了这盘论文之后,之前的防御措施都没啥卵用了
核心思想:防范普遍针对ret指令(0xC3),那就想出不用ret也可以的办法
- 利用update-load-branch指令序列“pop x; jmp *x”
-
但是,update-load-branch序列并不像ret那样常见,因此要采用“蹦床”(trampoline)机制
ROP without return之图灵完整性:可用资源及trampoline
ROP without return之图灵完整性:条件分支
ROP without return之图灵完整性:函数调用
- 在esi中载入call-jmp序列的地址
- 在ebp中载入leave-jmp序列的地址
- 在eax中载入n+偏移量
- 将call-jmp序列的地址存储至地址n
- 改写esi,使其存储“返回地址”
- esi值写入result位置后,再读出至edi
- 交换使返回值存入ebp,leave指令换入edi
- 在esi中载入pop-jmp序列的地址
- 在ecx中载入函数入口地址
- 在eax中载入地址n
- 交换esp和eax,栈指针指向n(函数地址)
- edi处的leave指令将使函数“返回” 至0x7d
防御思路
由ROP的一些特点:
- 始于对程序控制流的篡改(控制流异常)
- 各gadget由ret指令(或者pop-jump组合)代替eip加以链接(大量控制流异常)
- 原始栈结构遭到破坏,ROP过程中栈指针单向移动(栈的行为异常)
- 有时,栈指针可能被篡改并指向不属于栈区的内存(栈的行为异常)
可以有如下办法:
- 要求程序按照程序猿所规定的逻辑去执行
- 当出现不应出现的控制转移行为时,阻止程序执行
- 当程序的栈结构发生异常变化时,阻止程序执行
并由此产生的返回导向编程防御思路:控制流完整性保护(CFI)技术
控制流完整性保护(CFI)技术
基本思想:
- 通过预设的运行时校验,确保程序执行与预先定义的控制流图严格吻合
- 通过对二进制码的静态分析来获取CFI所需保证的控制流图
- 藉由静态的二进制代码改写为程序添加运行时的自我校验
CFI的基本安全性假设:
- 控制转移目标的标示符(ID)具有唯一性
- 程序代码不可写
- 程序数据不可执行
实用(粗粒度)的控制流完整性保护
改进思路:借鉴SFI的思想,优化CFI的校验机制
设计效果:间接控制转移只能以Springboard段内的适当存根作为目标
基于动态优化的CFI
前述CFI方案仍然存在不尽人意之处:需要改写程序的二进制代码:
- 即使是最强大的二进制分析工具,也很难识别出程序中所有的合法控制转移目标
- 对二进制代码的修改同样困难且容易出错,且兼容性问题在一定程度上仍然存在
改进思路:在程序执行过程中加以监视和约束
设计基础:动态执行优化工具
不足:效率太低
利用硬件特性和虚拟化技术的CFI
改进思路:利用虚拟化技术
实现基础:Last Branch Record
- 一组存在于CPU内部、记录其最后n次控制转移目标的寄存器(如Intel i5中设有16个)
- 需要内核权限开启(恰好与虚拟化技术相适应)
CFI弱点
- 即使是在静态分析中,间接控制转移的合法跳转目标也既不是唯一的、也不是排他的
- 受到性能的制约,粗粒度化的CFI对控制流的约束更加松散
针对CFI的改进型返回导向编程
CFI的弱点为返回导向编程提供了新类型的gadget来源
- gadget的位置满足粗粒度CFI的约束规则
- 新约束条件下的gadgets仍然有可能形成具备图灵完整性的攻击载荷
- 副作用: gadget为了满足CFI约束而不可避免地增大了(无论是字节数还是指令数)
返回导向编程改进型1:利用以函数入口和函数调用点为起始的gadget
绕过CFI是否将导致返回导向编程的功能性有所缺损?不会
返回导向编程的变种:数据导向编程
注:Return-to-Libc攻击特点
- 篡改返回地址指向指定函数入口
- 通过溢出伪造输入参数等栈数据结构
- 诱使函数在执行中使用伪造的参数数据,实现恶意目的
从Return-to-Libc攻击可以看出:数据污染可以成为代码重用类攻击的重要成分
数据导向编程特点:
- 通过污染数据(特别是指针)改变被重用代码的实际语义
- 借助循环不断注入新的攻击载荷,使得被重用代码的实际执行效果随之改变
因此,数据导向编程所重用的代码资源主要来自串操作(或同类执行行为)
数据导向编程的循环调度
- 交互式攻击 – 允许攻击者在每轮循环中输入不同的载荷以激活不同的gadget
- 非交互式攻击 – 攻击者必须一次性输入整个攻击载荷
地址空间随机化(Address Space Layout Randomization)
基本思想:地址空间随机化
发展
粗粒度ASLR的改进:偏移量+内存区段的位置置换
细粒度的ASLR:
- 区段间 → 区段内
- 改变代码文本的具体内容
细粒度ASLR的实施
- 程序在加载时自我随机化
- 通过虚拟机进行动态随机化
- 操作系统的随机化
对ASLR的攻击
ASLR假设的攻击模型
- Case 1:攻击者无法披露目标程序的内存空间
- Case 2:攻击者可以实施内存空间披露,但只能获得一个代码指针(如果攻击者真的寻获了一个可以远程利用的内存披露漏洞,为何不反复利用之,以最大化漏洞价值?)
ASLR的无效化:Just-In-Time代码重用
Limitation:对于任何ASLR,程序在执行时不再改变其内存空间结构