freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

SaBRe load-time selective binary rewriting
2022-08-04 14:25:53
所属地 河南省

摘要

目前已经存在的二进制重写办法的可靠性和性能不够高,所以SaBRe出现了。它是一个在加载时进行选择性二进制重写的工具,重写特殊的指令,尤其是系统调用和函数,并且是在程序加载到内存后,通过一个API用插件去打断这个过程(执行前)。SaBRe在x86_64 RISC-V上可行,并且实现了三个插件:a fast system call tracer(系统调用追踪器)、a muti-version executor(多版本的执行器)、a fault injector(错误注入器)。而且SaBRe开销很小,几乎在3%以下。

1. 介绍

二进制广泛应用于软件故障隔离、沙箱、多版本执行、程序优化、边界检查。
目前主流的方法有:
(1)静态二进制重写:程序执行前在磁盘中重写二进制文件、不产生任何开销、正确率很难实现。
分辨出程序中所有代码归结于halting problem的变长和间接跳转问题
(2)动态二进制重写:程序执行时在内存中重写、一次一个block然后结果存到内存中,花销很大。
SaBRe在程序被存入内存但是没有执行时来进行重写,这种结合让SaBRe变得更加高效和安全,之前这种工具已经存在了(例如Xifer等),但是他们依赖于特定的操作系统。
SaBRe安装一个无效指令处理器,这个东西能识别被PC(程序计数器)截获的指令
SaBRe也可以保留原来二进制代码的语义。
SaBRe利用被gcc和LLVM等那些好的编译器生成的代码。
SaBRe关键的优化就是用直接跳转(跳到处理程序)来重定位指令
主要贡献共五条
(1)在代码被加载到内存之后,在代码执行前来进行重写。
(2)开源、在x86_64、RISC-V架构上可以实现应用SaBRe。
(3)系统调用跟踪器
(4)新的系统调用错误注入器(在已知库中找到那些不能处理异常的情况的bug)
(5)多版本执行

2 重写

二进制重写的目标是在二进制代码中添加、删除和替换指令。

2.1 静态与动态:

在静态二进制重写中,二进制文件在程序执行之前在磁盘上重写,而在动态二进制重写中,它在程序执行时在内存中重写。
与以前的技术不同,SaBRe在加载时操作,即程序加载到内存后,但在开始执行之前。与静态二进制重写技术一样,SaBRe在原地重写代码。然而,SaBRe的翻译是在内存中完成的,类似于动态二进制重写。这种组合使SaBRe能够高效、安全地重写映射到进程中的所有代码,包括动态加载的库,同时增加程序启动时间,通常不到70毫秒。
静态的优点
1.仅在程序启动时发生的低开销 — 这使其适合生产环境。
2.在程序加载到内存中后,当有效地址已知并且动态链接和与位置无关的代码不再受关注时,重写完成。
3.重写在内存中完成,因此无需在磁盘上创建有效的可执行文件。
4.对于每次执行,可以透明且有选择地重写共享库。
静态的分类:
插入、蹦床或提升

2.2 SaBRe 中的标准拦截:

示例:
将原始指令片段 O 替换为另一个代码片段 R,其中 O 或 R 的大小都可以为 0。
设 A(o) 和 S(o) 是分别将对象 o 映射到其起始地址和大小的函数。

  • 当S(O)=0,A(O) 指示代码中应添加 R 的偏移量。

  • 当S(R)=0,则表示应删除 原始指令片段O。
    关于S(O)S(R)的不同大小情况:

  • S(O) = S(R):只用R覆盖O

  • S(O) > S(R):插入R并用nops指令填充剩余空间

  • S(O) < S(R):插入非法指令并捕获SIGILL信号
    SaBRe 安装一个处理程序来捕获由 CPU 尝试解码此非法指令而触发的 SIGILL 信号。此信号处理程序首先检查 SIGILL 的原因是否是合法的非法指令,在这种情况下,它只是将执行重定向到默认处理程序。否则,信号处理程序只需执行与插入的非法指令相对应的 R,然后返回正常执行。
    此外,如果此方案必须是通用的,解决方案是将每个R与特定的非法指令相关联,而不是与要重写的位置相关联,即引发信号的程序计数器的值。

2.3基于蹦床的优化

通过非法指令进行拦截是昂贵的
当需要绕行时,根据原始片段O和跳跃片段J的相对大小,可能需要将周围的一些指令移动到蹦床上。
出现两种情况:

  • S(O)≥S(J):插入 J 和(可能)用 nops 填充剩余空间

  • S(O)<S(J):根据需要重新定位尽可能多的相邻指令以适应J
    蹦床可能包括的六个部分:

  1. 前导码(可选):来自 O 的代码,要在处理程序之前执行,并且必须重新定位

  2. 预处理:依赖于 ABI 的代码,以确保透明度

  3. 对处理程序的调用

  4. 后处理:与 ABI 相关的代码,用于恢复原始状态

  5. Postamble(可选):来自 O 的代码,该代码将在处理程序之后执行,并且必须重新定位

  6. 跳回到主指令流
    ABI:应用程序二进制接口 ?
    并非所有指令都可以安全地重新定位。如果遇到任何此类指令,SaBRe不会执行基于蹦床的优化,而是回退到标准拦截方案,这保证了方法的合理性。
    三类指令:

  • 具有副作用的指令

  • 跳转目标

  • PC 相对地址

3.反汇编

反汇编就是机器码转换为程序语言
两种方法:

  • 线性扫描
    从第一条指令开始(指向合法指令),扫描代码直到结束。
    优点:易于实现,低开销
    缺点:未区分实际代码和嵌入的数据;忽略指令重叠

  • 递归遍历
    分析控制流递归扫描代码。
    优点:允许跳过混合数据并探索可能隐藏的执行路径
    缺点:无法处理间接控制传输。间接跳转很难静态分析,因为跳转地址都是运行时计算的。

对于负载时间的反汇编,充分反汇编的两个标准是覆盖率和性能。线性扫描只需要一次解码,所以性能优。加之现在好的编译器不会把代码和数据,所以覆盖率也可以达到100%

在此基础上,精准反汇编的条件:
充分覆盖:
1.在段内,指令被细分,指令之间没有间隙和重叠
2.可执行段的起始位置和大小已知
精确反汇编:
.充分覆盖;2.代码不可变

可行性:
1.默认情况下,gcc或clang编译器为64位x86生成细分代码,不会将数据插入到可执行段,也不会发出重叠指令
(但是在ARM体系中细分假设不可行)
2.操作系统的加载器需要所有段的位置和大小,才能将它们映射到内存中。因此,无论可执行文件格式如何,这些信息都以最终编译和链接的二进制文件的元数据形式提供。
3.大多数应用程序在执行过程中不会修改其代码,操作系统通常会限制可执行页面上的写权限,这是一种广泛使用的方案,通常称为写异或执行。而且SaBRe不支持即时编译,所以不会自修改代码。

4.实现

image.png
三层架构:插件/主体/后端
backbone包括一个加载器和一个重写器
后端收集所有特定于isa的代码:包括反汇编器和二进制码发射器
插件实现了重写的额外目的例如跟踪系统调用,故障注入(插入非法指令)。
Fault-injection故障注入,基于故障注入的测试是评测计算机系统可靠性的重要方法之一
主体
加载器是一个动态链接的可执行文件,它提供了SaBRe执行的入口点,并且只在加载时运行。第一个任务是加载插件并执行它特定的初始化,
接着,加载器扫描它自己进程的内存映射(避免重写自己的进程)
再接下来,将用户应用程序(以后称为客户端)加载到同一个进程中,并再次扫描内存映射,重写客户端二进制文件和已知有系统调用的动态链接库的可执行段
最后,加载器为客户端重写堆栈,并跳转到它的入口点。
客户端
客户端如果是静态连接,入口点就是二进制文件本身。当客户端从加载器接受控制时,所有系统调用都已经被重写。客户端动态链接的话,入口点时操作系统的动态加载器(在linux上就是ld.so)SaBRe的加载器重写ld.so来拦截加载
重写器
重写器内置在加载器的二进制文件中,并且在加载时只调用一次
首先,重写器处理虚拟动态共享对象(vDSO),vDSO是针对一些频繁安全的系统调用的空间优化(重写vDSO是安全的因为每个进程都有私有副本)
接下来,检查包含系统调用或选定函数的库(通过搜索符号表)来获取需要拦截的函数名
注:SaBRe依靠符号表将函数名映射到其第一个指令的地址。客户端二进制文件被剥夺了它的符号表,或者当使用内联时,函数拦截是不可能的
在上述两种情况下patching的工作机制如下:
重写器扫描目标内存范围,首先收集分支目标地址(这个其实可以参考重写部分),在内存中保留最后几条指令的缓冲区(为了能够在系统调用时重新定位它们)。搜索到重新定位的对象后,如果没有足够的空间容纳detour,,目标指令替换为一个非法指令,之后通过异常处理捕捉后进行相关处理

后端
对于每个截获的指令I,反汇编器向SaBRe提供以下信息:
1.指向后面一个指令的指针,用来计算指令大小
2.寻址模式,特别关心是否时PC相对寻址
3.指令的副作用(这个参考上面的内容)
4.操作码类别:控制流还是系统调用
后端还包括
(1)跳到蹦床(指令),(2)蹦床本身,(3)预处理程序。
预处理程序:从蹦床调用的手写组装函数,依次执行以下操作:保存机器状态(寄存器和标志),调用插件特定的处理程序,以及在插件返回时恢复机器状态。
特殊处理两个系统调用 clone&rt_sigreturn
clone是因为在内核发出系统调用后,子线程是由一个新的堆栈创建的,但是这个堆栈不包含如何返回到libc库的信息(其他的系统调用的堆栈都包含返回地址的信息)
解决方法是将返回地址传递给发出系统调用的蹦床,再通过蹦床跳转回重写的库的硬编码地址
rt_sigreturn会返回到应用程序提供的指针(这是什么奇怪的套路),rt_sigreturn 永远不会返回,当它发出时,内核直接将执行恢复到应用程序提供的指针。这就产生了问题,因为堆栈在sabre调用时就已经改变了(不明觉厉)
处理方法:
SaBRe将堆栈指针恢复到跳到蹦床之前的值,即发出rt_sigreturn之前的值。
x86_64和risc-V的跳转指令字节:x86_64 5个字节就够了
risc-V分条件
插件
插件被编译为共享对象,在运行时通过dlopen动态加载。插件通过api实现与主干的交互,api只需要实现了两个函数:初始化,在加载时调用一次;(初始化函数的作用:1处理插件的命令行参数2.向SaBRe注册系统调用和函数处理程序)
系统调用处理程序,在运行时拦截系统调用时调用
此外,还提供了三种额外的可选函数类型:1.vDSO调用的特殊处理程序
2.截取特定函数(用户可以决定拦截属于某个库的函数(在动态链接的情况下)或二进制文件本身,方法是用函数名和库或二进制文件的名称填充一个专用的数据结构。)
3.定义初始化后的函数(可能可以用于在加载时和运行时之间拥有不同的系统调用处理程序。)
限制
1 不适用于程序自修改 (Self-modifying code):程序在运行期间 (Run time)修改自身指令。
2.默认情况下,只支持系统调用、vDSO和函数序言
3.SaBRe依赖于符号表(前面提到过了)
4.静态链接的二进制文件在运行时(通过dlopen)加载的库不会被重写器看到
5.在动态连接,栈展开信息丢失
还有就是一些操作可能变得不安全(论文里以malloc()函数为例说明)

论文链接:https://link.springer.com/article/10.1007/s10009-021-00644-w
团队gitee仓库:https://gitee.com/metetor/da-chuang--resource-code

# 二进制 # 二进制代码重写
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录