1. 二进制插桩
不能移动原代码
2. 静态插桩SBI
使用二进制重写的方法永久修改磁盘上二进制程序。SBI对二进制程序进行反汇编,按需添加插桩代码并保存更新后程序。
要求:在不破坏现有代码和数据引用的前提下,添加插桩代码
优点
- 速度快
- 只需要发布修改后二进制程序即可 缺点
- SBI插桩后造成的影响是永久的
- SBI相较于DBI容易出错,因为需要先进行反汇编才可以插桩
指令替换法
将原指令替换为跳转指令,跳转到插桩代码
过程
- 将插桩代码存放在独立位置
- 当程序执行到插桩点时将控制流转移到插桩代码,执行:
- 前置插桩代码
- 被替换原指令
- 后置插桩代码
- 跳回插桩点之后的指令,恢复正常执行
缺点
- jmp指令占用多个字节,无法对短指令进行插桩
int3方法
0x03号中断:调试器暂时替换正在运行的程序中的指令以设置代码断点。
int3操作码为0xcc,指令长度1字节,可覆盖任意指令
过程
- 用int3指令覆盖原指令
- 当
SIGTRAP信号产生时,使用Linux提供的ptrace API 查找中断产生的地址 - 根据插桩点位置调用对应插桩代码
- 返回原程序
缺点
- 软中断速度慢,运行开销大
- 与使用int作为断点进行调试的程序不兼容
跳板方法
不直接对原始代码进行插桩 (但会进行修改),创建一个副本并对其进行插桩 不破坏任何代码和数据引用
主要解决间接控制流
流程
- 创建所有原始函数副本,并放在新代码段中
- 将原始代码中每个函数替换为 jmp指令+junk bytes
- 每当有调用或跳转指令将控制流转移到原始代码中,该位置的跳板立即跳转到相应的插桩代码。
attention
- 新插入的代码会导致代码移位,SBI引擎需要修补所有相对跳转指令的偏移
- SBI引擎需要替换所有2字节跳转指令(8位偏移),换为5B长指令(32bit偏移),因为代码移位可能会导致偏移超过8bit
- 重写直接调用,使其指向已插桩函数
总结 直接控制流
- 相对跳转:需要SBI引擎修补
- 绝对跳转:跳板
- 直接调用:重写指向已插桩函数 间接控制流
- 间接跳转:跳板
- 间接调用:跳板
- 跳转表:需要SBI引擎修改
- 将跳转表的值修改为新地址
- 在原始代码中的每个switch分支下放置一个跳板
1. switch跳转表实现 跳转到特定分支时,会间接跳转到原始代码,此处没有跳板会发生错误 尝试解决
- 修改跳转表
- 基本符号信息不包含switch语句布局信息
- 在原始代码每个switch分支下放置一个跳板
- 可能没有足够空间存放跳板
2. 内联数据 代码与数据混合时,跳板可能会覆盖部分内联数据
当反汇编错误时,任何改变都有可能破坏二进制程序
综上,跳板方法容易出错
3. 动态插桩DBI
监视二进制程序执行状态,在运行时将新指令插入指令流中,
优点
- 避免了代码重定位问题
- 不会破坏引用
- 更容易使用,会监视程序和库的指令
- DBI可以动态附加到进程,也可以从进程中分离,不影响程序正常使用
- DBI更不易出错 缺点
- 运行时插桩计算成本高,速度慢
- 需要同时发布 二进制程序 和 包含插桩代码的DBI平台和工具
原理 在代码缓存中运行
- DBI引擎从进程中提取一段代码,并按照DBI工具的指示对代码进行插桩
- 插桩后使用JIT编译器编译代码,并将编译后代码存储在代码缓存中
- 代码在缓存中执行,直到控制流指令要求获取新代码/在缓存中查找另一个代码块 过程中JIT编译器会控制流指令,以防止控制流转移到未插桩应用程序进程中执行 DBI引擎会模拟而不是直接运行某些指令
4. 对抗插桩
对抗动态调试
- 基于代码缓存
- 基于环境特征:插桩产生的指纹
- 基于JIT编译
- 其他反编译技术:运行开销、不支持指令