摘要
以往针对UAF和double free的防御方法 所采用的办法是推迟内存重新分配的时间
这种解决方法存在运行时和内存开销过大的问题
作者提出的解决方法为 对程序进行刨析 将不同任务的代码单元分割开 这样不同单元之间的数据互通就极少 内存的重新分配可以等到单元执行完毕后 这样就有效避免了UAF
于是设计了一款以linux为原型的 称之为PUMM 由离线的代码分析器和在线执行器组成
介绍
UAF可能导致程序崩溃和拒绝服务攻击(DOS)
甚至有可能导致任意代码执行 攻击者可以伪造缓冲区 触发UAF使程序将该缓冲区当成原始代码指针 从而劫持程序执行流
原理就是程序释放了内存后 攻击者仍然保留这块内存的指针 当这块内存被重新分配时 攻击者就可以篡改目标地址的内容
所以研究人员一开始的防御思路是设计内存分配器 以推迟内存重新分配的时间 当攻击者无法用新的内存对象覆盖以前的内存对象 就可以避免UAF
近年来 出现了两种方案(扫描和一次性分配)
扫描方法在内存释放后 会对其进行隔离 当验证不存在对该内存仍可利用的指针后 才会重新分配
一次性分配(OTA)方法则是不会重新分配内存
这两种方法虽然可以有效防止UAF 但是扫描方法会因为误报从而过度隔离内存 一次性分配会消耗过多的虚拟内存 从而加大内存开销
基于EUP 作者认为可以在执行单元上实行OTA 这样执行单元在新迭代开始时就会被释放 不会出现常规OTA所造成的虚拟地址空间耗尽
这一方案的第一个难点在于如何在缺乏源代码的程序中识别需要延迟进行重新分配的执行单元 于是作者提出了⼀种基于离线动态分析的技术来检测一个执行单元的外循环指示
这里要注意 EUP采取的是运行时行为来检测单元 而作者采取的是审计日志来检测单元
接下来的任务就是将识别到的单元转化为有效可执行的内存分配器的隔离策略
作者采取了一种算法来精确的定位程序中的释放点 通过识别执行单元开始前发生的内存管理器函数的调用者 来允许单元每次新迭代开始时安全的释放之前隔离的地址
概述
示例
如图所示 提供了一个代码示例
该代码存在UAF漏洞 代码主体逻辑为以行为单位读取文件中的内容 如果getline函数中的realloc没有开辟成功下一行的空间 那么此时的指针r就仍然指向上一行 但是上一行的空间已经被释放了 此时就得到了一个空置指针 进行了二次free
PUMM如何分析这个程序 在脱机阶段 通过测试数据的输入来分析程序的执行轨迹 会显示出一张控制流图(CFG)
针对上面的案例 PUMM检测到了四个简单周期(基本电路) 而橙色周期的头部支配其他周期的头部 所以他们合并成一个执行单元(用紫色表示)
这个执行单元的头部 也就是刚才的橙色周期 也是这个执行单元的最外层循环 这是有一定联系的 我们标识的单元具备了自主处理一个输入的所有代码
下一步 PUMM会定位内存分配器函数的调用者 并检测是否有属于某个单元的调用者
PUMM将这个调用者记录在安全配置文件中 当程序到达这个调用者 就代表执行单元新的迭代开启 就不会再访问上一次迭代的内存地址 同时 上一次迭代的悬空指针都将作废
在任何情况下 PUMM会加载安全配置文件 内存管理器函数调用的内存地址都会被隔离 同时之前隔离的内存将会被释放
这种方法仍然无法避免UAF 但是UAF访问的非法地址被隔离 所以无法访问