Add files via upload
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
### 工控场景的入侵检测与普通场景入侵检测的区别
|
||||
|
||||
**1. 安全目标和优先级不同**
|
||||
|
||||
- **普通场景(IT):** IT 环境的核心安全目标通常是**机密性、完整性和可用性(CIA)**。其中,机密性往往是首要考虑的。这意味着保护数据不被泄露是头等大事,其次是确保数据不被篡改,最后是保障服务的持续运行。如果发生安全事件,系统可以短暂下线进行修复
|
||||
- **工控场景(ICS/SCADA):** 工控环境的核心安全目标是**可用性、完整性和机密性(AIC)**。其首要任务是**保障物理过程的持续运行和安全**。任何中断都可能导致严重的物理后果,例如设备损坏、生产中断,甚至是人员伤亡和环境灾难。因此,可用性是压倒一切的。其次是确保控制命令的完整性,防止恶意篡改导致设备误操作。机密性(如生产配方)虽然重要,但优先级通常最低
|
||||
|
||||
**2. 网络协议和通信方式不同**
|
||||
|
||||
- **普通场景(IT):** IT 网络主要使用标准、开放的协议,如 **TCP/IP、HTTP、HTTPS、SMTP、SSH** 等。这些协议拥有成熟的加密、身份验证和安全机制,入侵检测系统(IDS)可以利用已知的签名库、异常行为模式和深度包检测(DPI)来分析流量
|
||||
- **工控场景(ICS/SCADA):** 工控网络使用大量非标准的、专有的或特定领域的协议,如 **Modbus、DNP3、Ethernet/IP、PROFINET、OPC** 等。这些协议最初设计时并未过多考虑安全性,通常是明文传输,缺乏加密和身份验证。因此,IT 领域的传统 IDS 无法理解和解析这些协议,更无法从中提取有用的信息。工控 IDS 必须具备对这些特定协议的深度解析能力
|
||||
|
||||
**3. 系统架构和设备特性不同**
|
||||
|
||||
- **普通场景(IT):** IT 系统通常由服务器、PC、路由器、交换机等标准化硬件组成,更新和打补丁相对方便。架构灵活,通常有明确的边界和分层(如DMZ区)
|
||||
- **工控场景(ICS/SCADA):** 工控系统由可编程逻辑控制器(PLC)、人机界面(HMI)、远程终端单元(RTU)、监控工作站等专用硬件组成。这些设备通常运行在实时操作系统上,计算能力和存储空间有限,**打补丁和更新极为困难,甚至是不可能的**,因为任何中断都可能影响生产。此外,工控网络通常是扁平的,设备之间直接通信,边界模糊
|
||||
|
||||
**4. 攻击类型和检测方法不同**
|
||||
|
||||
- **普通场景(IT):** IT 攻击通常针对软件漏洞、弱密码、DDoS攻击、恶意软件、钓鱼邮件等。入侵检测系统主要依靠**已知签名库(基于签名的检测)和行为模式分析(基于异常的检测)**。例如,检测到特定的恶意代码特征码,或者发现异常的登录尝试次数
|
||||
- **工控场景(ICS/SCADA):** 工控攻击不仅包括 IT 攻击手法(如针对监控工作站的恶意软件),更重要的是针对工控协议和物理过程的攻击。例如,**恶意修改PLC的逻辑控制程序、篡改HMI上的数据显示、发送恶意的控制命令**等。因此,工控IDS必须能够检测到:
|
||||
- **异常的命令和参数:** 例如,向PLC发送一个不属于正常操作范围的控制命令
|
||||
- **异常的过程值:** 例如,传感器读数突然出现与物理常识不符的剧烈波动
|
||||
- **异常的通信模式:** 例如,某个监控站突然向所有RTU发送大量广播包
|
||||
- **非法固件更新:** 检测到对PLC或RTU的非法固件上传
|
||||
- **基于物理过程的异常检测:** 结合物理过程的知识,判断网络流量是否会导致不合理的物理状态。例如,同时关闭两个互锁的阀门
|
||||
|
||||
**5. 部署方式和对系统的影响不同**
|
||||
|
||||
- **普通场景(IT):** IT IDS可以作为内联设备(inline)部署,直接串联在网络中,对流量进行阻断。或者作为旁路设备(out-of-band)部署,仅仅进行流量镜像分析。即使内联部署出现问题,通常也只会导致网络暂时中断,影响可控
|
||||
- **工控场景(ICS/SCADA):** 工控 IDS 绝大多数情况下必须以**旁路(out-of-band)**方式部署。任何在关键通信路径上串联的设备都可能引入延迟,甚至导致网络通信中断,从而引发生产事故。因此,工控IDS通常通过交换机的镜像端口(SPAN)来复制和分析流量,只进行检测,不参与控制
|
||||
@@ -0,0 +1,51 @@
|
||||
### 对比一下 QEMU 模式的 Fuzzing 和源码模式的 Fuzzing
|
||||
|
||||
**1. 源码模式 Fuzzing**
|
||||
|
||||
源码模式 Fuzzing(也称为**插桩式 Fuzzing**)是在编译时对目标程序进行修改,插入额外的代码(即“插桩”)。这些桩点会在程序运行时收集代码覆盖率等信息,并反馈给 Fuzzer,指导其生成更有效的输入
|
||||
|
||||
**工作原理**
|
||||
|
||||
- **编译插桩**:Fuzzer 使用专门的编译器前端(如 AFL-Clang 或 LLVM-sanitizer)来编译目标程序的源码
|
||||
- **插入探针**:编译器会在每个基本块(Basic Block)的开头插入一个探针。当程序执行到一个新的基本块时,探针会向 Fuzzer 反馈这个信息
|
||||
- **反馈循环**:Fuzzer 根据这些覆盖率信息,判断哪些输入探索了新的代码路径。它会保留这些有价值的输入,并对其进行变异,以期能找到更深层次的代码逻辑
|
||||
|
||||
**优点**
|
||||
|
||||
- **高效**:插桩非常轻量级,几乎不会引入额外的性能开销。Fuzzer 能够以极高的速度运行和测试样本,每秒可达数千甚至数万次
|
||||
- **精确的代码覆盖率**:由于插桩在编译时完成,Fuzzer 能够获得非常精确的、基本块级别的代码覆盖率,这使得它能够更有效地探索代码路径
|
||||
- **直接定位崩溃点**:由于 Fuzzer 能够知道输入触发了哪段代码,一旦发生崩溃,它能迅速定位到崩溃发生的基本块
|
||||
|
||||
**缺点**
|
||||
|
||||
- **需要源码**:这是最大的局限性。如果目标程序是闭源的,你就无法使用这种方法
|
||||
- **编译复杂**:对于复杂的项目,编译过程可能会很复杂,需要处理各种依赖和编译选项
|
||||
|
||||
**2. QEMU 模式 Fuzzing**
|
||||
|
||||
QEMU 模式 Fuzzing(也称为**黑盒或二进制 Fuzzing**)是在**二进制级别**进行插桩和监控。它使用 QEMU 模拟器来运行目标程序,并通过修改 QEMU 的代码来收集代码覆盖率信息。
|
||||
|
||||
**工作原理**
|
||||
|
||||
- **二进制插桩**:Fuzzer 启动一个修改过的 QEMU 用户模式模拟器
|
||||
- **动态翻译**:QEMU 在运行目标程序时,会动态地将目标程序的机器码翻译成宿主机的机器码。在翻译过程中,Fuzzer 的插桩逻辑会被嵌入到生成的代码中
|
||||
- **收集覆盖率**:当翻译后的代码执行时,插桩逻辑会收集代码覆盖率信息,并反馈给 Fuzzer。
|
||||
|
||||
**优点**
|
||||
|
||||
- **无需源码**:这是 QEMU 模式最大的优势。它能够 Fuzz 任何闭源的、可执行的二进制文件,这在分析恶意软件或商业软件时至关重要
|
||||
- **全系统覆盖**:除了用户态程序,QEMU 还可以模拟整个系统。这意味着你可以用它来 Fuzz 驱动程序或内核
|
||||
|
||||
**缺点**
|
||||
|
||||
- **性能开销大**:由于 QEMU 是一个模拟器,它会引入大量的性能开销。Fuzzing 速度比源码模式慢得多,通常每秒只能运行几十到几百次
|
||||
- **覆盖率信息粗糙**:QEMU 模式通常只能提供基本块级别的覆盖率,但可能无法像源码模式那样精确地追踪到每一条指令的执行
|
||||
- **不稳定性**:由于 QEMU 本身的复杂性,Fuzzing 过程中可能会出现一些不稳定的情况
|
||||
|
||||
| 特性 | 源码模式 Fuzzing | QEMU 模式 Fuzzing |
|
||||
| ------------ | ---------------------- | ---------------------------------------- |
|
||||
| 是否需要源码 | 是 | 否 |
|
||||
| 性能 | 极高(每秒数千次) | 较低(每秒数十次) |
|
||||
| 代码覆盖率 | 非常精确(基本块级别) | 较精确(基本块级别) |
|
||||
| 适用场景 | 开源项目、内部代码审计 | 闭源软件、恶意软件分析、驱动程序 Fuzzing |
|
||||
| 代表工具 | AFL++、LibFuzzer | AFL-QEMU |
|
||||
@@ -0,0 +1,29 @@
|
||||
### 说说 QEMU 模式的动态插桩怎么实现的,有什么优缺点
|
||||
|
||||
**QEMU 模式动态插桩的实现原理**
|
||||
|
||||
QEMU 本身是一个处理器模拟器,它通过**动态二进制翻译(Dynamic Binary Translation, DBT)**技术来执行不同架构的指令。这个过程为动态插桩提供了完美的切入点
|
||||
|
||||
简单来说,当 QEMU 运行一个目标程序时,它不是逐条解释执行指令,而是会:
|
||||
|
||||
1. **读取指令块**:QEMU 一次性读取一小段(通常是一个基本块)目标程序指令
|
||||
2. **翻译并缓存**:它将这些目标指令翻译成宿主机的机器码
|
||||
3. **插入探针(Instrumentation)**:在翻译过程中,QEMU 会在每个基本块的入口点**注入额外的指令**。这些额外的指令就是 Fuzzer 用来收集信息(如代码覆盖率)的探针
|
||||
4. **执行翻译后的代码**:QEMU 随后执行这段翻译并插桩后的代码
|
||||
|
||||
整个过程就像一个**即时编译器(JIT)**。当一个基本块被执行时,QEMU 会检查其是否已被翻译。如果未翻译,就进行翻译、插桩和缓存;如果已翻译,就直接执行缓存中的代码
|
||||
|
||||
AFL-QEMU 就是利用这种机制。它修改了 QEMU 的源码,在翻译层增加了额外的逻辑。每当 QEMU 翻译一个基本块时,AFL-QEMU 就会插入代码,将该基本块的 ID 记录在一个共享内存区域中,从而让 Fuzzer 能够实时获取代码覆盖率信息
|
||||
|
||||
**优点**
|
||||
|
||||
- **无需源码**:这是最大的优势。QEMU 模式在**二进制级别**工作,可以对任何闭源的、可执行的程序进行 Fuzzing,这对于分析商业软件、恶意软件以及驱动程序至关重要
|
||||
- **跨平台**:QEMU 能够模拟不同的 CPU 架构(如 ARM、MIPS),这意味着你可以在一个 x86_64 的 Linux 机器上 Fuzz 一个 ARM 架构的程序
|
||||
- **全系统 Fuzzing**:QEMU 可以模拟整个操作系统,包括内核。这使得它可以用于 Fuzzing 驱动程序和内核漏洞
|
||||
|
||||
**缺点**
|
||||
|
||||
- **性能开销大**:动态二进制翻译本身就会带来显著的性能开销。Fuzzing 速度比源码插桩模式慢得多,通常每秒只能运行几十到几百次,而源码模式可以达到数万次
|
||||
- **覆盖率信息粗糙**:QEMU 通常只能提供基本块级别的覆盖率,但无法像源码插桩那样精确地追踪到每一条指令的执行
|
||||
- **实现复杂且不稳定**:QEMU 模拟器本身就非常复杂,在其中进行插桩会引入更多不确定性,有时会导致模拟过程不稳定或产生非预期的行为
|
||||
- **不适合处理 I/O 密集型程序**:对于那些需要频繁进行磁盘或网络 I/O 的程序,QEMU 的模拟速度会变得更慢,Fuzzing 效率会大大降低
|
||||
@@ -0,0 +1,40 @@
|
||||
### fuzz 普通程序和数据库有哪些不同点
|
||||
|
||||
**1. 输入和协议的复杂性**
|
||||
|
||||
- **普通程序**:通常处理简单格式的输入,如文件、命令行参数或简单的网络数据包。这些输入的格式相对单一,fuzzer 很容易理解和变异。例如,对一个图片解析器进行fuzzing,输入就是图片文件
|
||||
- **数据库**:数据库的输入是复杂的、有状态的网络协议和查询语言。fuzzer 需要理解和生成符合特定数据库协议(如 MySQL 的二进制协议、PostgreSQL 的文本协议)的数据包。更重要的是,数据库的**查询语言(SQL)**本身就极为复杂,包含多种数据类型、函数、联结操作和语法结构。fuzzer 不仅要生成畸形的协议数据,还要生成语法正确但语义畸形的 SQL 查询语句,比如超长的字符串、负数、特殊字符等
|
||||
|
||||
**2. 状态管理和序列依赖**
|
||||
|
||||
- **普通程序**:大多数普通程序是**无状态**的。每次运行fuzzer,程序都从一个干净的状态开始,处理一个单独的输入。这使得fuzzer很容易追踪和重现漏洞
|
||||
- **数据库**:数据库是**有状态**的。一个查询的执行结果可能依赖于之前的查询。例如,你必须先创建一张表,然后才能向其中插入数据,最后才能查询。这意味着fuzzer需要生成一系列有逻辑关系的查询序列,而不是单个独立的输入。如果一个漏洞需要多次操作才能触发,fuzzer 必须能够管理和生成这个操作序列
|
||||
|
||||
**3. 性能和效率**
|
||||
|
||||
- **普通程序**:普通程序的fuzzing通常很快。一个文件解析器在毫秒级就能处理一个输入。这使得覆盖率引导的fuzzer(如 AFL)能够每秒测试数千甚至数万次,从而快速找到漏洞
|
||||
- **数据库**:数据库的fuzzing通常很慢。每次建立连接、发送查询、接收响应都需要时间。此外,一个查询可能需要几毫秒甚至几秒来执行。这大大降低了fuzzer的测试速度,因此需要更智能的fuzzing策略
|
||||
|
||||
**4. 漏洞类型和检测方法**
|
||||
|
||||
- **普通程序**:fuzzing通常用于寻找内存安全漏洞,如缓冲区溢出、空指针解引用等。当程序崩溃时,fuzzer会立即捕获到异常
|
||||
- **数据库**:数据库的漏洞类型更为多样,不仅包括内存安全问题,还包括:
|
||||
- **逻辑漏洞**:错误的查询结果、数据损坏等。这些漏洞不会导致程序崩溃,fuzzer需要有专门的逻辑来检测
|
||||
- **权限绕过**:在低权限下执行高权限操作
|
||||
- **SQL 注入**:这是一种特殊的逻辑漏洞,fuzzing可以用于寻找新的注入点
|
||||
|
||||
由于很多数据库漏洞不会导致程序崩溃,fuzzer需要额外的断言(assertions)或 Oracle 来检测。例如,fuzzer可以执行一个查询,然后用一个已知正确的查询来验证结果是否一致。如果不一致,就可能存在逻辑漏洞
|
||||
|
||||
**5. 架构和环境的复杂性**
|
||||
|
||||
- **普通程序**:通常在一个单一进程中运行,fuzzer可以直接附加或作为子进程运行
|
||||
- **数据库**:数据库是复杂的系统,通常是多进程或多线程的。这使得传统的fuzzer很难精确地追踪代码覆盖率。例如,一个主进程可能接受连接,然后派生出多个子进程来处理查询。fuzzer需要能够跟踪和监控这些子进程
|
||||
|
||||
| 特性 | Fuzzing 普通程序 | Fuzzing 数据库 |
|
||||
| -------- | ------------------------------ | -------------------------------------------- |
|
||||
| 输入 | 单一,通常是文件或简单网络数据 | 复杂,包含协议和查询语言 |
|
||||
| 状态 | 无状态,每个输入独立 | 有状态,需要管理查询序列 |
|
||||
| 速度 | 非常快,每秒数千次以上 | 较慢,受网络和查询执行速度影响 |
|
||||
| 漏洞类型 | 主要是内存安全漏洞(崩溃) | 内存安全、逻辑漏洞、权限问题等 |
|
||||
| 检测方法 | 捕获崩溃 | 捕获崩溃,并需要断言或Oracle来检测非崩溃漏洞 |
|
||||
| 挑战 | 探索代码路径和绕过输入验证 | 管理状态、生成复杂输入、处理低速和多进程环境 |
|
||||
@@ -0,0 +1,30 @@
|
||||
### 说说 AFL++ 和 AFL 有哪些不同
|
||||
|
||||
**AFL (American Fuzzy Lop)**
|
||||
|
||||
首先,我们回顾一下 AFL。AFL 是由 Google 的 Michał Zalewski 开发的一款**覆盖率引导的**模糊测试工具,它开创了一个时代
|
||||
|
||||
- **核心思想:** AFL 将模糊测试带入了一个新的高度,它不只是随机地变异输入,而是会**监控程序的代码覆盖率**。如果一个输入能让程序执行到之前未执行过的代码路径,AFL 就会认为这个输入“有价值”,并将其保存下来,然后基于这个输入进行更多的变异
|
||||
- **工作流程:**
|
||||
1. 从一个种子文件(或一组种子文件)开始
|
||||
2. 对种子文件进行一系列的变异操作(如位翻转、字节插入、删除)
|
||||
3. 运行变异后的输入,同时监控代码覆盖率
|
||||
4. 如果发现新的代码路径,则将该输入加入到种子队列中,作为新的变异基础
|
||||
5. 如果程序崩溃,则保存导致崩溃的输入,作为漏洞报告
|
||||
|
||||
AFL 的出现,使得模糊测试的效率和深度得到了革命性的提升
|
||||
|
||||
**AFL++ (AFLplusplus)**
|
||||
|
||||
AFL++ 是在 AFL 的基础上发展起来的一个项目。它由一群顶尖的模糊测试研究人员和开发者维护,旨在**整合所有 AFL 的优秀改进和新技术**
|
||||
|
||||
简而言之,**AFL++ 是 AFL 的超集**。它保留了 AFL 的核心思想和工作流程,但加入了大量的优化和新功能,使其在效率和能力上都远超原版 AFL
|
||||
|
||||
| 特性 | AFL | AFL++ |
|
||||
| ------------ | ------------------------------------ | ------------------------------------------------------------ |
|
||||
| 模糊测试算法 | 基础的位翻转、字节插入、随机数等。 | 更丰富、更智能的变异算法。包括 CmpLog、Redqueen 等技术,能够智能地发现和变异比较指令的魔术字节。 |
|
||||
| 覆盖率指导 | 基本的代码覆盖率指导。 | 更精细的覆盖率指导。通过 LTO、LLVM 和 GCC 等编译器插桩技术,可以获得更精确的覆盖率信息,甚至可以识别代码中的分支类型。 |
|
||||
| 字典支持 | 有简单的字典支持。 | 更强大的字典支持。可以自动从目标程序中提取字典(例如文件头、关键词等),并通过字典来加速对复杂文件格式的理解。 |
|
||||
| 代码插桩 | 基于 GCC 和 LLVM 的基本插桩。 | 多种插桩模式。除了编译器插桩,还支持 QEMU 模式的动态二进制插桩,可以对闭源程序进行模糊测试。 |
|
||||
| 性能 | 优秀 | 卓越。通过代码优化和更智能的算法,其测试速度通常比原版 AFL 更快。 |
|
||||
| 兼容性 | 仅支持 Linux 和部分 Unix-like 系统。 | 更好的兼容性。支持更多编译器、更多操作系统,并集成了更多的辅助工具。 |
|
||||
@@ -0,0 +1,69 @@
|
||||
### 怎么给 AFL 做适配去 fuzz 数据库
|
||||
|
||||
**适配器的核心任务**
|
||||
|
||||
这个适配器是一个程序,它将从 AFL 获取的**文件输入**转换成数据库能够理解的**网络协议请求**,然后发送给数据库
|
||||
|
||||
它的主要工作流程是:
|
||||
|
||||
1. 从标准输入(`stdin`)或文件中读取 AFL 提供的模糊数据
|
||||
2. 将这些数据解析成数据库协议的数据包或 SQL 查询语句
|
||||
3. 通过网络连接发送给数据库
|
||||
4. 监控数据库的响应,寻找崩溃或异常
|
||||
|
||||
**两种常见的适配方案**
|
||||
|
||||
这里有两种常用的方案,它们各有优缺点。
|
||||
|
||||
**方案一:基于文件和本地连接的适配**
|
||||
|
||||
这是最简单的方案,适用于那些支持从文件加载数据的数据库
|
||||
|
||||
**实现步骤:**
|
||||
|
||||
1. **编写适配器(`harness.c`)**:
|
||||
- 适配器会创建一个本地的数据库连接
|
||||
- 它从 AFL 的输入文件中读取数据,这些数据通常是**SQL 语句**或**数据库协议数据**
|
||||
- 适配器将读取到的内容作为 SQL 查询或协议数据发送给数据库
|
||||
2. **AFL 的运行方式**:
|
||||
- 你需要用 AFL 的编译器(`afl-clang-fast`)编译这个适配器
|
||||
- AFL 启动后,它会变异种子文件中的 SQL 语句或协议数据
|
||||
- AFL 会不断地运行你的适配器,将变异后的数据通过 `stdin` 或文件喂给它
|
||||
- 适配器在每次运行时,都会建立一个新的数据库连接,发送数据,然后退出
|
||||
|
||||
**优点:**
|
||||
|
||||
- **相对简单**:易于实现,特别是当数据库有命令行客户端时
|
||||
- **高效率**:避免了网络延迟,测试速度相对较快
|
||||
|
||||
**缺点:**
|
||||
|
||||
- **不完整**:只能测试数据库的查询解析部分,无法测试完整的网络协议栈
|
||||
- **状态问题**:难以处理需要管理状态的查询序列。
|
||||
|
||||
**方案二:基于网络和协议模拟的适配**
|
||||
|
||||
这是更复杂但更全面的方案,它能够真正地对数据库的网络协议层进行模糊测试
|
||||
|
||||
**实现步骤:**
|
||||
|
||||
1. **使用 QEMU 模式**:
|
||||
- 直接使用 `afl-fuzz -Q` 模式来对数据库服务器程序进行模糊测试
|
||||
- 编写一个自定义的**网络协议客户端**作为适配器,它会向服务器发送数据,而不是从文件读取
|
||||
2. **创建“伪文件”**:
|
||||
- 你需要一个外部程序(比如一个 Python 脚本)来生成包含数据库协议请求的“伪文件”
|
||||
- 这个脚本会根据 AFL 提供的原始模糊数据,将其封装成完整的数据库协议数据包
|
||||
- AFL 会模糊这个“伪文件”
|
||||
3. **连接和发送**:
|
||||
- 你的适配器会监听一个端口,当 AFL 的 QEMU 模式运行数据库服务器时,你的适配器会与其建立连接,并将模糊数据发送过去
|
||||
- **关键在于同步**:你需要确保 AFL 的输入文件和你的适配器发送的数据包是一致的
|
||||
|
||||
**优点:**
|
||||
|
||||
- **全面**:能够测试数据库的整个网络协议栈,包括身份验证、连接管理等
|
||||
- **真实**:更接近实际的攻击场景
|
||||
|
||||
**缺点:**
|
||||
|
||||
- **性能开销大**:QEMU 模式本身就很慢,再加上网络延迟,模糊测试速度会非常低
|
||||
- **复杂**:需要深入理解数据库的协议,并且需要处理网络连接、多进程/多线程等问题
|
||||
@@ -0,0 +1,54 @@
|
||||
### 介绍一下 fuzz 的流程,从选取目标开始
|
||||
|
||||
**1. 选取目标**
|
||||
|
||||
Fuzzing 不是盲目的,你需要选择一个合适的、有价值的目标。好的目标通常具有以下特点:
|
||||
|
||||
- **处理复杂或不受信任的输入**:比如文件解析器、网络协议栈、命令行参数处理程序。这些程序是攻击者的首要目标,因为它们直接暴露在外部输入之下
|
||||
- **高权限运行**:如果一个程序以 `root` 或 `SYSTEM` 权限运行,它将是更具吸引力的攻击目标
|
||||
- **处理多种数据格式**:例如,一个视频解码器需要处理各种容器格式(MP4, AVI)、编码格式(H.264, VP9)等
|
||||
- **有公开源码或已知的开源版本**:如果你有源码,可以使用更高效的**源码插桩**(Source-based Instrumentation)模式,如 AFL++ 或 LibFuzzer。如果没有,则需要使用**二进制插桩**(Binary Instrumentation)模式,如 QEMU
|
||||
|
||||
**2. 准备工作**
|
||||
|
||||
在开始模糊测试之前,需要做好充分的准备,这直接影响到 Fuzzing 的效率和成功率
|
||||
|
||||
- **获取种子文件(Seed Corpus)**:种子文件是模糊测试的起点。你需要收集一批高质量的、能代表正常输入的样本文件。这些样本应该尽可能地覆盖程序的不同功能。高质量的种子文件能显著提高 Fuzzing 效率
|
||||
- **编译目标程序**:如果是源码模式,你需要使用 Fuzzer 专用的编译器(例如 `afl-clang-fast`)来编译目标程序。这会在程序中植入探针,用于收集代码覆盖率信息
|
||||
- **创建 Fuzzing 脚本**:你需要编写一个脚本,作为 Fuzzer 和目标程序之间的**适配器(Harness)**。这个脚本负责将 Fuzzer 生成的输入数据传递给目标程序。对于文件输入,适配器通常很简单,可能就是 `read()` 或 `fopen()` 函数。对于更复杂的网络协议,适配器需要解析并发送数据包
|
||||
- **配置 Fuzzer**:根据你的目标和环境,你需要选择并配置一个合适的 Fuzzer(例如 AFL++)。配置参数包括:
|
||||
- Fuzzing 模式(源码、QEMU、网络等)
|
||||
- Fuzzing 进程数量
|
||||
- 字典文件(如果目标程序有特定的关键字)
|
||||
|
||||
**3. Fuzzing 运行**
|
||||
|
||||
这个阶段是 Fuzzing 的核心,Fuzzer 将持续不断地生成、变异和测试输入
|
||||
|
||||
- **启动 Fuzzer**:运行 Fuzzer 进程,将种子文件作为输入,并指定输出目录
|
||||
- **监控 Fuzzing 状态**:在 Fuzzing 过程中,需要持续监控其状态。好的 Fuzzer 都会提供一个状态界面,显示:
|
||||
- 测试速度(每秒执行次数)
|
||||
- 代码覆盖率
|
||||
- 发现的崩溃数量和异常(`timeout`)数量
|
||||
- 队列中已有的有价值的输入数量
|
||||
- **分析结果**:当 Fuzzer 发现一个崩溃或异常时,它会将导致该问题的输入文件保存在一个特定的目录中。你需要定期检查这个目录,并对新发现的崩溃进行分类和分析
|
||||
|
||||
**4. 漏洞分析与验证**
|
||||
|
||||
这个阶段是**人工分析**的过程,目的是将 Fuzzer 发现的“崩溃”转化为可利用的“漏洞”
|
||||
|
||||
- **漏洞重现**:首先,你需要用调试器(如 GDB, WinDbg)或逆向工具(如 IDA Pro)来加载导致崩溃的输入文件,并**重现崩溃**。这能帮助你确定崩溃的类型和位置
|
||||
- **漏洞分类**:将崩溃分为不同的类型,例如**栈溢出、堆溢出、空指针解引用**等。这有助于你理解漏洞的性质
|
||||
- **可利用性分析**:并非所有的崩溃都是可利用的漏洞。你需要分析崩溃的原因和上下文,判断攻击者是否能通过它来劫持程序流或执行任意代码
|
||||
- **漏洞报告**:一旦确认漏洞是可利用的,你需要编写一份详细的漏洞报告,包括:
|
||||
- 漏洞的描述和类型
|
||||
- 导致漏洞的输入文件
|
||||
- 崩溃发生时的堆栈信息
|
||||
- 漏洞的严重性评估
|
||||
|
||||
**5. 修复与回归测试**
|
||||
|
||||
这是整个流程的最后一步,也是最重要的
|
||||
|
||||
- **漏洞修复**:将漏洞报告交给开发者,由他们来修复代码中的缺陷
|
||||
- **回归测试**:在修复完成后,你需要将导致漏洞的输入文件添加到你的测试套件中,确保未来的代码修改不会再次引入这个漏洞。这个过程也被称为**回归测试**
|
||||
@@ -0,0 +1,38 @@
|
||||
### 讲一下 AFL 的插桩原理
|
||||
|
||||
**插桩的本质:反馈导向的模糊测试**
|
||||
|
||||
AFL 的插桩(instrumentation)就是一套用于收集**代码覆盖率**的探针。通过这些探针,AFL 可以知道某个输入执行了哪些代码路径
|
||||
|
||||
它的工作流程是这样的:
|
||||
|
||||
1. **编译时插桩**:用 AFL 提供的特殊编译器(`afl-clang-fast` 或 `afl-gcc`)来编译目标程序
|
||||
2. **运行时反馈**:当程序执行时,插桩代码会向 AFL 反馈代码执行路径信息
|
||||
3. **智能变异**:AFL 根据这些反馈,判断哪些输入“有价值”(即探索了新的代码路径),然后对这些有价值的输入进行更多的变异
|
||||
|
||||
这个反馈循环是 AFL 高效的关键。它使得 AFL 能够自动绕过复杂的输入校验,深入到程序更深层次的逻辑中,从而找到隐藏的漏洞
|
||||
|
||||
**插桩的原理实现**
|
||||
|
||||
AFL 的插桩非常轻量级,它采用了一种基于**基本块(Basic Block)**的简单而巧妙的方案。
|
||||
|
||||
**1. 什么是基本块?**
|
||||
|
||||
在程序中,一个基本块是一段连续的代码,它只有一个入口点(第一条指令)和一个出口点(最后一条指令),且中间没有任何分支跳转
|
||||
|
||||
你可以把基本块看作是代码中的最小“执行单元”
|
||||
|
||||
**2. AFL 的插桩步骤**
|
||||
|
||||
AFL 在编译时,会在**每个基本块的入口**插入一段代码。这段代码会做两件事:
|
||||
|
||||
- **获取当前基本块的 ID**:AFL 在编译时会给每个基本块分配一个唯一的随机 ID
|
||||
- **记录基本块的 ID**:AFL 维护一个**共享内存区域**,通常是一个大小为 64KB 的位图(bitmap)
|
||||
|
||||
当程序执行到一个新的基本块时,插入的代码会执行以下操作:
|
||||
|
||||
1. 获取当前基本块的 ID(假设是 `current_id`)
|
||||
2. 获取**上一个**执行的基本块的 ID(假设是 `prev_id`)。AFL 用一个全局变量来保存这个 `prev_id`
|
||||
3. 计算一个哈希值:`index = current_id XOR prev_id`
|
||||
4. 将这个 `index` 映射到位图的某个位置,并将该位置的值加 1
|
||||
5. 更新 `prev_id`,使其等于 `current_id`
|
||||
@@ -0,0 +1,31 @@
|
||||
### 怎么选择 fuzz 测试点
|
||||
|
||||
**1. 优先选择处理复杂格式输入的代码**
|
||||
|
||||
处理复杂、非结构化输入的代码是 Fuzzing 的首选目标。这类代码通常包含复杂的解析逻辑和状态机,容易在处理畸形数据时出错
|
||||
|
||||
- **文件解析器**:这是最典型的 Fuzzing 目标。例如,图片格式(JPEG, PNG)、视频格式(MP4, AVI)、文档格式(PDF, DOCX)的解析库或应用程序。Fuzzer 可以轻松生成无数个格式畸形的样本,测试解析器对异常情况的处理能力
|
||||
- **网络协议栈**:处理网络协议的代码是高价值目标。例如,Web 服务器、FTP 守护进程、或者物联网设备的通信协议。Fuzzer 可以向这些服务发送不符合协议规范的数据包,测试其鲁棒性
|
||||
- **编译器和解释器**:对编程语言的编译器或解释器进行 Fuzzing,可以发现它们在处理语法错误或逻辑异常的代码时的漏洞。例如,对 JavaScript 引擎的 Fuzzing 已经发现了无数高价值的漏洞
|
||||
|
||||
**2. 考虑执行权限和攻击面**
|
||||
|
||||
选择 Fuzzing 点时,要将漏洞的潜在危害考虑在内
|
||||
|
||||
- **高权限代码**:如果一个程序以 `root`、`SYSTEM` 或其他高权限运行,那么它的漏洞危害会更大。对这类程序的 Fuzzing 应该作为首要任务。例如,内核驱动、特权服务、或者安全软件
|
||||
- **暴露在外部的接口**:攻击者可以直接访问的接口是 Fuzzing 的高优先级目标。例如,通过网络监听端口、接受外部文件的服务,或者处理命令行参数的程序。这些接口是攻击者的第一道入口
|
||||
|
||||
**3. 基于代码复杂性分析**
|
||||
|
||||
如果可以访问源码,可以更精确地选择 Fuzzing 点
|
||||
|
||||
- **复杂的控制流**:在代码中寻找包含大量 `if-else`、`switch` 语句、嵌套循环或复杂状态机的函数。这些地方的代码路径多且复杂,很容易出现逻辑错误和漏洞
|
||||
- **涉及内存操作的代码**:寻找使用 `malloc`、`free`、`memcpy`、`read` 等内存相关函数的代码。这些函数是缓冲区溢出、Use-After-Free 等内存安全漏洞的常见源头
|
||||
- **缺乏边界检查的代码**:在代码中寻找缺乏对输入数据大小进行严格检查的地方。这通常是缓冲区溢出漏洞的温床
|
||||
|
||||
**4. 结合自动化工具进行决策**
|
||||
|
||||
为了避免盲目选择,可以使用自动化工具来辅助决策
|
||||
|
||||
- **代码覆盖率工具**:使用像 `gcov` 或 `llvm-profdata` 这样的工具,运行已知的测试用例,分析哪些代码区域没有被覆盖到。这些未覆盖的区域往往是 Fuzzing 的好目标
|
||||
- **静态分析工具**:使用静态分析工具(如 `Coverity`、`Clang Static Analyzer`)来扫描代码,寻找潜在的漏洞模式,比如整数溢出或空指针解引用。然后,将这些潜在的漏洞位置作为 Fuzzing 的重点
|
||||
@@ -0,0 +1,22 @@
|
||||
### 哪些漏洞可以用 fuzz 检测到
|
||||
|
||||
**内存安全漏洞**
|
||||
|
||||
这是 Fuzzing 最擅长的领域,也是 Fuzzing 历史上发现最多高价值漏洞的类型。这些漏洞通常会导致程序崩溃、数据损坏或信息泄露
|
||||
|
||||
- **缓冲区溢出(Buffer Overflows)**:当程序向一个固定大小的缓冲区写入的数据超出了其容量时,就会发生溢出。Fuzzer 可以通过生成超长字符串、大文件或超大数组等输入来触发这类漏洞
|
||||
- **整数溢出(Integer Overflows)**:当一个整数运算的结果超出其数据类型的最大值时,会发生溢出。Fuzzer 可以提供接近最大值或负数的输入,试图触发不正确的内存分配或边界检查绕过
|
||||
- **空指针解引用(Null Pointer Dereference)**:当程序试图访问一个空指针指向的内存时,会发生崩溃。Fuzzer 可以提供导致函数返回空指针的输入,例如不完整的协议数据包或畸形的文件头
|
||||
- **UAF(Use-After-Free)**:当程序在释放一块内存后,仍然使用指向这块内存的指针时,就会发生 UAF。Fuzzer 可以通过提供复杂的、有状态的输入序列来触发这种时序性漏洞
|
||||
- **双重释放(Double Free)**:当程序两次释放同一块内存时,会引发严重后果。Fuzzer 可以提供导致程序进入异常逻辑的输入,从而触发重复的内存释放操作
|
||||
- **格式化字符串漏洞(Format String Bugs)**:当程序使用 `printf` 等函数,且格式化字符串可由用户控制时,攻击者可以利用它来读写内存。Fuzzer 可以尝试在输入中插入 `%s`, `%n`, `%x` 等格式符来检测这种漏洞
|
||||
|
||||
**逻辑漏洞和异常情况**
|
||||
|
||||
除了内存安全问题,Fuzzing 也可以用来发现更复杂的逻辑漏洞,尽管这通常需要更智能的 Fuzzer 或额外的检测机制
|
||||
|
||||
- **逻辑错误(Logical Bugs)**:Fuzzer 可以通过提供畸形但不会导致崩溃的输入,来发现程序中不正确的逻辑。例如,一个输入可能导致数据库返回错误的结果,或者一个视频播放器无法正确解析视频帧
|
||||
- **拒绝服务(Denial of Service, DoS)**:当一个输入导致程序进入无限循环或消耗大量资源时,就会引发 DoS 攻击。Fuzzer 可以通过监控程序执行时间或资源消耗来检测这类问题
|
||||
- **竞态条件(Race Conditions)**:在多线程或多进程程序中,Fuzzer 可以通过随机化输入发送时间或使用多个线程来触发竞态条件,从而发现漏洞
|
||||
- **权限绕过(Privilege Escalation)**:Fuzzing 可以用来寻找程序在处理特殊输入时,是否会错误地提升权限
|
||||
- **信息泄露(Information Leakage)**:Fuzzer 可以通过分析程序的输出或返回值,来检测程序是否泄露了不应被公开的敏感信息,比如内存地址或调试信息
|
||||
@@ -0,0 +1,64 @@
|
||||
### 符号执行是如何做约束求解的
|
||||
|
||||
**1. 什么是约束求解**
|
||||
|
||||
一个约束求解器(也叫 SMT Solver,Satisfiability Modulo Theories Solver)是符号执行的“大脑”。它的工作是解决一个公式(或一组公式)是否可满足
|
||||
|
||||
例如,对于一个简单的程序:
|
||||
|
||||
```c
|
||||
int main(int x, int y) {
|
||||
if (x + y > 10) {
|
||||
printf("Branch 1");
|
||||
} else {
|
||||
printf("Branch 2");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
符号执行会将 `x` 和 `y` 变成符号 `X` 和 `Y`。如果要探索 Branch 1,它就会生成约束:`X + Y > 10`
|
||||
|
||||
约束求解器接收这个约束,然后找到满足这个条件的具体值。一个可能的解是 `X=5`,`Y=6`。符号执行器就可以用 `x=5`, `y=6` 作为输入,来验证 Branch 1 是否可达
|
||||
|
||||
**2. 约束求解的内部工作原理**
|
||||
|
||||
约束求解器本身是基于一套复杂的算法来工作的,主要包括:
|
||||
|
||||
**a. 逻辑分解**
|
||||
|
||||
求解器首先会分析给定的约束公式,将其分解为更小的、可管理的子问题。例如,一个复杂的逻辑表达式 `(A && B) || C` 会被分解成两个独立的子问题:`A && B` 和 `C`
|
||||
|
||||
**b. 理论推理**
|
||||
|
||||
SMT Solver 的“理论”部分是它的核心能力。它能够理解并处理不同领域(如整数、数组、位向量等)的约束。例如:
|
||||
|
||||
- **算术理论(Arithmetic Theory)**:处理 `+`, `-`, `*`, `>` 等数学运算
|
||||
- **位向量理论(Bitvector Theory)**:处理二进制位运算,如 `&`, `|`, `^`, `<<` 等。这对于分析底层二进制代码至关重要
|
||||
- **数组理论(Array Theory)**:处理数组的读写操作
|
||||
|
||||
当一个约束公式涉及到多个理论时,SMT Solver 会使用一种叫 **CDCL(T)**(Conflict-Driven Clause Learning with Theories)的算法,协调各个理论求解器来解决问题。
|
||||
|
||||
**c. 变量赋值与回溯**
|
||||
|
||||
求解器会尝试给变量赋值,并检查这些赋值是否满足约束
|
||||
|
||||
- **如果满足**:它会继续给其他未赋值的变量赋值,直到找到一个完整的解
|
||||
- **如果不满足**:它会回溯(backtrack),撤销之前的赋值,并尝试新的组合
|
||||
|
||||
这个过程很像解决数独,每一步的赋值都会影响后续的选择,而当发现无解时,就需要退回到上一步重新选择
|
||||
|
||||
**3. 符号执行与约束求解的结合**
|
||||
|
||||
符号执行器和约束求解器是紧密配合的
|
||||
|
||||
1. **符号执行器**:
|
||||
- 遍历程序代码,将变量抽象为符号
|
||||
- 遇到分支(`if`, `while`)时,为每个分支生成一个**路径约束**
|
||||
- 将路径约束传递给约束求解器
|
||||
2. **约束求解器**:
|
||||
- 接收路径约束
|
||||
- 尝试找到满足约束的一组具体值
|
||||
- 如果找到了解,就将解返回给符号执行器
|
||||
3. **符号执行器**:
|
||||
- 使用求解器返回的具体值作为输入,来探索新的代码路径
|
||||
- 如果求解器返回“无解”(unsatisfiable),则说明该代码路径不可达
|
||||
@@ -0,0 +1,54 @@
|
||||
### 讲讲 Linux 平台的漏洞缓解机制
|
||||
|
||||
**1. 堆栈保护(Stack Smashing Protection, SSP)**
|
||||
|
||||
这是最基础,也是最重要的堆栈溢出缓解机制
|
||||
|
||||
- **原理:** 在函数调用时,编译器会在栈上的**局部变量和返回地址之间插入一个随机的“金丝雀值”(Canary Value)**
|
||||
- **工作方式:**
|
||||
- 函数进入时,金丝雀值被推入栈中
|
||||
- 函数返回前,程序会检查这个金丝雀值是否被改变
|
||||
- 如果金丝雀值被修改,说明发生了缓冲区溢出,程序会立即终止(通常会调用 `__stack_chk_fail` 函数),而不是让攻击者控制程序流
|
||||
- **局限性:** 攻击者可以通过覆盖低地址的变量或利用其他漏洞(如格式化字符串漏洞)来泄露金丝雀值,从而绕过此保护
|
||||
|
||||
**2. 地址空间布局随机化(Address Space Layout Randomization, ASLR)**
|
||||
|
||||
ASLR 是一个非常有效的漏洞缓解机制,它让攻击者难以预测内存中关键数据的位置
|
||||
|
||||
- **原理:** 每次程序启动时,ASLR 会将程序的主要内存区域(如**可执行文件基址、堆、栈和共享库(DLL/SO)**)加载到随机的地址上
|
||||
- **工作方式:** 攻击者在利用漏洞时,通常需要知道某个函数(例如 `system` 函数)或某个数据(例如返回地址)的精确内存地址。ASLR 打破了这种确定性,使得攻击者无法在不知道这些地址的情况下构造 ROP 链(Return-Oriented Programming)
|
||||
- **局限性:**
|
||||
- **熵不足:** 早期版本的 ASLR 随机化范围有限,攻击者可以通过暴力破解或多次尝试来绕过
|
||||
- **信息泄露:** 如果程序存在信息泄露漏洞(如格式化字符串漏洞),攻击者可以泄露出某个模块的基址,从而推算出其他所有函数的地址,绕过 ASLR
|
||||
|
||||
**3. 不可执行内存(Non-Executable Memory)/ NX 位(No-eXecute)**
|
||||
|
||||
这是为了防止攻击者将恶意代码注入数据段(如堆或栈)并执行而设计的
|
||||
|
||||
- **原理:** CPU 的 MMU(内存管理单元)会根据内存页的权限来决定是否允许执行该页中的代码。NX 位被设置在页表项中,如果该位为 1,则该页不可执行
|
||||
- **工作方式:** 操作系统会将堆、栈等数据段标记为**不可执行**。当攻击者利用缓冲区溢出将 Shellcode(恶意代码)写入栈上并尝试执行时,CPU 会抛出异常,阻止代码的执行
|
||||
- **局限性:**
|
||||
- **ROP 攻击:** ROP(Return-Oriented Programming)是一种绕过 NX 位的方法。攻击者不注入代码,而是通过利用程序本身已有的代码片段(称为“gadgets”),并精心构造返回地址链来执行恶意逻辑
|
||||
- **JIT(即时编译)代码:** 对于一些需要动态生成和执行代码的应用程序(如 Java 或 JavaScript 引擎),它们需要创建可执行的内存区域,这可能会被攻击者利用
|
||||
|
||||
**4. 只读重定位(Read-Only Relocations, RELRO)**
|
||||
|
||||
RELRO 旨在保护程序在加载后不被修改,特别是全局偏移表(GOT)和过程链接表(PLT)
|
||||
|
||||
- **原理:**
|
||||
- **GOT(Global Offset Table):** 存储了外部共享库函数的实际地址
|
||||
- **PLT(Procedure Linkage Table):** 负责将函数调用重定向到 GOT
|
||||
- **工作方式:**
|
||||
- **部分 RELRO:** 在程序加载时,`.got` 段是可写的,因为加载器需要填充外部函数的地址。加载后,`.got` 段变为只读
|
||||
- **完全 RELRO:** 将 GOT 表完全设置为只读,所有重定位都在程序加载前完成
|
||||
- **局限性:** 攻击者无法再利用 GOT 覆写漏洞来劫持程序流。然而,它并不能防御所有类型的攻击
|
||||
|
||||
**5. 控制流完整性(Control Flow Integrity, CFI)**
|
||||
|
||||
CFI 是一种更高级的保护机制,它旨在确保程序执行的控制流不会被攻击者劫持
|
||||
|
||||
- **原理:** CFI 在编译和链接阶段为每个间接调用(如函数指针调用)和返回指令创建元数据,并在运行时检查这些元数据,确保控制流的跳转是合法的、预期的
|
||||
- **工作方式:**
|
||||
- **向前边沿 CFI(Forward-Edge):** 保护间接函数调用,确保函数指针只能跳转到其类型兼容的函数
|
||||
- **向后边沿 CFI(Backward-Edge):** 保护函数返回,确保返回地址不会被篡改
|
||||
- **局限性:** CFI 的实现较为复杂,并且可能引入性能开销。虽然能防御 ROP 攻击,但并不能防御所有类型的攻击,且可以被绕过
|
||||
@@ -0,0 +1,50 @@
|
||||
### NX 是怎么绕过的
|
||||
|
||||
**绕过 NX 的基本思路**
|
||||
|
||||
NX 阻止了攻击者在数据段上执行**自己的代码**。那么,绕过 NX 的基本思路就是:**不注入代码,而是利用程序本身已有的、具有执行权限的代码**
|
||||
|
||||
这个思路催生了多种绕过技术,其中最主要、最著名、最通用的就是 **ROP**
|
||||
|
||||
**ROP**
|
||||
|
||||
ROP 是目前最主流的 NX 绕过技术。它的原理是利用程序中已有的、以 `ret` 指令结尾的短小代码片段,这些片段被称为 **“gadgets”**
|
||||
|
||||
**ROP 的工作原理**
|
||||
|
||||
1. **寻找 Gadgets:** 攻击者首先在目标程序或其依赖的共享库(如 `libc`)中寻找一系列以 `ret` 指令结尾的“gadgets”。一个 gadget 可能是一条或几条汇编指令,例如:`pop edi; ret;` 或 `mov eax, [ebx]; ret;`
|
||||
2. **构建 ROP 链:** 攻击者利用漏洞(如缓冲区溢出),用一系列精心挑选的 gadget 地址来覆盖栈上的返回地址这些地址按顺序排列,形成一个 **“ROP 链”**
|
||||
3. **劫持控制流:** 当函数返回时,它不再返回到正常调用的地方,而是返回到 ROP 链中的第一个 gadget
|
||||
4. **链式执行:**
|
||||
- 第一个 gadget 执行完后,其末尾的 `ret` 指令会从栈上弹出下一个地址,也就是 ROP 链中的第二个 gadget
|
||||
- 这样,一个 gadget 接一个 gadget 地执行,每个 `ret` 指令都将控制流转移到下一个 gadget
|
||||
- 通过这种方式,攻击者可以利用程序中已有的代码,间接地执行任意恶意逻辑
|
||||
|
||||
**ROP 攻击的最终目标**
|
||||
|
||||
一个典型的 ROP 攻击,其最终目标通常是调用某个函数,比如 `system()`,并将一个指向 Shell 命令字符串(例如 `"/bin/sh"`)的指针作为参数传递给它
|
||||
|
||||
一个完整的 ROP 链通常包含:
|
||||
|
||||
- **`pop` gadget:** 用于将栈上的参数值弹出到寄存器中,为函数调用做准备
|
||||
- **`system()` 的地址:** ROP 链的末尾,用于最终调用 `system()`
|
||||
- **字符串 `/bin/sh` 的地址:** 作为 `system()` 的参数
|
||||
|
||||
**绕过 NX 的其他方法**
|
||||
|
||||
除了 ROP,还有其他一些不那么常见,但同样能绕过 NX 的技术:
|
||||
|
||||
**1. JIT Spraying(JIT 喷射)**
|
||||
|
||||
这种方法主要用于绕过浏览器中的 NX 保护
|
||||
|
||||
- **原理:** JIT(Just-In-Time)编译器会动态地生成可执行代码。攻击者可以利用 JavaScript 等语言的 JIT 特性,构造大量的 `nop` 指令(无操作指令),然后在其末尾附上 Shellcode
|
||||
- **工作方式:** JIT 引擎会将这些指令编译成原生机器码并存放在一个**可执行的**内存区域。攻击者只需找到这个可执行区域的地址,并跳转过去即可
|
||||
|
||||
**2. Return-to-libc(返回到 libc)**
|
||||
|
||||
Return-to-libc 是 ROP 的前身和简化版。它不需要复杂的 gadget 链,而是直接劫持程序流,跳转到已加载的 `libc` 库中的一个函数
|
||||
|
||||
- **原理:** 攻击者利用漏洞,用 `libc` 中 `system()` 函数的地址覆盖栈上的返回地址
|
||||
- **工作方式:** 当函数返回时,程序流会直接跳转到 `system()` 函数。攻击者在栈上预先放置好 `system()` 函数所需的参数(如 `"/bin/sh"` 的地址),即可实现代码执行
|
||||
- **局限性:** 这种方法非常简单,但它只能调用 `libc` 中已有的函数,而不能像 ROP 那样组合出更复杂的逻辑
|
||||
@@ -0,0 +1,75 @@
|
||||
### 讲讲 Linux 平台的 ELF 文件结构
|
||||
|
||||
**ELF 文件的两种视图**
|
||||
|
||||
理解 ELF 文件的关键在于,它有两个不同的、但相互关联的视图:
|
||||
|
||||
1. **链接视图(Linking View):** 供编译器和链接器使用。它由**节(Sections)**组成,主要用于编译、链接和重定位
|
||||
2. **执行视图(Execution View):** 供操作系统加载器使用。它由**段(Segments)**组成,用于将程序加载到内存中并执行
|
||||
|
||||
这两种视图由 ELF 头部中的两个表来描述:节头部表和程序头部表
|
||||
|
||||
**ELF 头部**
|
||||
|
||||
每个 ELF 文件的开头都是一个 ELF 头部,它提供了文件的基本信息,就像 PE 文件的 DOS 头部一样。它定义了文件的类型、机器架构、入口点地址等
|
||||
|
||||
ELF 头部最重要的一些字段是:
|
||||
|
||||
- `e_ident[EI_MAG0-3]`:4 字节的魔数,固定为 `0x7f, 'E', 'L', 'F'`。这是识别 ELF 文件的唯一标志
|
||||
- `e_ident[EI_CLASS]`:指定文件架构,`1` 代表 32 位,`2` 代表 64 位
|
||||
- `e_ident[EI_DATA]`:指定字节序,`1` 代表小端序,`2` 代表大端序
|
||||
- `e_type`:文件类型,如 `ET_EXEC`(可执行文件)、`ET_DYN`(共享库)、`ET_REL`(可重定位文件)
|
||||
- `e_entry`:程序的入口点地址(虚拟地址)
|
||||
- `e_phoff`:程序头部表(Program Header Table)的文件偏移量
|
||||
- `e_shoff`:节头部表(Section Header Table)的文件偏移量
|
||||
- `e_phentsize`:程序头部表中每个条目的大小
|
||||
- `e_phnum`:程序头部表的条目数量
|
||||
- `e_shentsize`:节头部表中每个条目的大小
|
||||
- `e_shnum`:节头部表的条目数量
|
||||
|
||||
**链接视图:节**
|
||||
|
||||
节是 ELF 文件的基本单元,用于组织文件中的各种数据和代码。每个节都有特定的目的
|
||||
|
||||
常见的节有:
|
||||
|
||||
- `.text`:包含可执行代码
|
||||
- `.data`:包含已初始化的全局变量和静态变量
|
||||
- `.rodata`:包含只读数据,如字符串常量
|
||||
- `.bss`:包含未初始化的全局变量和静态变量,在文件中不占用空间,加载时由加载器分配和清零
|
||||
- `.symtab`:符号表,包含了程序中所有符号(函数名、变量名)的信息
|
||||
- `.strtab`:字符串表,存储符号表中的字符串
|
||||
- `.debug`:调试信息,用于 GDB 等调试器
|
||||
- `.got`(Global Offset Table):全局偏移表,用于在运行时解析外部函数地址
|
||||
- `.plt`(Procedure Linkage Table):过程链接表,用于动态链接
|
||||
|
||||
**节头部表**
|
||||
|
||||
节头部表是一个描述所有节的数组。每个条目都是一个 `Elf64_Shdr`(对于 64 位)结构体,它包含了每个节的名称、类型、权限、文件偏移、内存地址和大小等信息。链接器和反汇编工具(如 objdump)主要依赖这个表来分析文件结构
|
||||
|
||||
**执行视图:段**
|
||||
|
||||
当程序需要被加载到内存中执行时,节会被组合成更大的逻辑单元——**段**。每个段都具有相同的内存权限(可读、可写、可执行)。这是加载器关心的内容
|
||||
|
||||
典型的段有两个:
|
||||
|
||||
- **代码段(Code Segment):** 通常包含 `.text` 和 `.rodata` 节。这个段被映射到内存中,并具有**可读和可执行**权限
|
||||
- **数据段(Data Segment):** 通常包含 `.data` 和 `.bss` 节。这个段被映射到内存中,并具有**可读和可写**权限
|
||||
|
||||
**程序头部表**
|
||||
|
||||
程序头部表是一个描述所有段的数组。每个条目都是一个 `Elf64_Phdr` 结构体,它包含了每个段的类型、文件偏移、内存地址、大小和权限等信息。加载器通过遍历这个表,将 ELF 文件中的内容映射到内存中
|
||||
|
||||
- `p_type`:段的类型,如 `PT_LOAD`(可加载到内存)
|
||||
- `p_offset`:段在文件中的偏移量
|
||||
- `p_vaddr`:段在内存中的虚拟地址
|
||||
- `p_memsz`:段在内存中的大小
|
||||
- `p_flags`:段的权限,如 `PF_R`(可读)、`PF_W`(可写)、`PF_X`(可执行)
|
||||
|
||||
**ELF 文件加载过程**
|
||||
|
||||
1. 操作系统内核的加载器读取 ELF 头部,找到程序头部表
|
||||
2. 加载器遍历程序头部表中的所有条目
|
||||
3. 对于每个类型为 `PT_LOAD` 的段,加载器将文件中的相应部分,从 `p_offset` 偏移处开始,映射到内存中的 `p_vaddr` 虚拟地址上
|
||||
4. 加载器根据 `p_flags` 设置内存页的权限(读、写、执行)
|
||||
5. 所有段加载完毕后,加载器将程序控制权交给 `e_entry` 字段指定的入口点地址,程序开始执行
|
||||
@@ -0,0 +1,87 @@
|
||||
### 讲讲 Windows 平台的 PE 文件结构
|
||||
|
||||
**PE 文件的双重性质**
|
||||
|
||||
PE 文件的结构可以看作是**DOS 文件**和 **PE 文件**的结合体。这种设计是为了保持与旧版 DOS 操作系统的兼容性。当你双击一个 PE 文件时,操作系统首先会将其作为一个 DOS 程序处理
|
||||
|
||||
PE 文件主要由以下几个核心部分组成:
|
||||
|
||||
1. **DOS 头部 (DOS Header)**
|
||||
2. **DOS Stub (DOS 存根)**
|
||||
3. **NT 头部 (NT Header)**
|
||||
4. **可选头部 (Optional Header)**
|
||||
5. **节表 (Section Table)**
|
||||
6. **节 (Sections)**
|
||||
|
||||
**1. DOS 头部 (DOS Header)**
|
||||
|
||||
这是 PE 文件的最前端,一个 `IMAGE_DOS_HEADER` 结构体
|
||||
|
||||
- **`e_magic`**:4 字节的魔数,固定为 `0x4D5A` (ASCII 字符 **"MZ"**)。这是识别 PE 文件的标志
|
||||
- **`e_lfanew`**:一个关键的字段,它是一个 4 字节的偏移量,指向 **NT 头部**的起始位置
|
||||
|
||||
**2. DOS Stub (DOS 存根)**
|
||||
|
||||
这是一个小型的 DOS 程序。当在 DOS 环境下执行这个文件时,它会打印一句经典的提示语:“This program cannot be run in DOS mode.”。它的唯一作用就是为了兼容性
|
||||
|
||||
**3. NT 头部 (NT Header)**
|
||||
|
||||
NT 头部是 PE 文件的真正核心,它是一个 `IMAGE_NT_HEADERS` 结构体,由三个部分组成:
|
||||
|
||||
- **`Signature`**:4 字节的签名,固定为 `0x50450000` (ASCII 字符 **"PE\0\0"**)。这标志着它是一个有效的 PE 文件
|
||||
- **`FileHeader` (文件头部)**:一个 `IMAGE_FILE_HEADER` 结构体,包含了文件的基本属性,比如:
|
||||
- **`Machine`**:指定文件适用的 CPU 架构,如 `0x14C` (Intel 386)
|
||||
- **`NumberOfSections`**:文件中包含的节的数量
|
||||
- **`SizeOfOptionalHeader`**:可选头部的大小
|
||||
- **`Characteristics`**:文件的特性,如是否是可执行文件、是否是 DLL 等
|
||||
- **`OptionalHeader` (可选头部)**:一个 `IMAGE_OPTIONAL_HEADER` 结构体,这部分虽然叫“可选”,但对于可执行文件来说是必需的。它包含了加载器需要的大部分信息,是理解 PE 结构的关键
|
||||
|
||||
**4. 可选头部 (Optional Header)**
|
||||
|
||||
可选头部包含了 PE 文件的加载信息,例如:
|
||||
|
||||
- **`Magic`**:标志着文件是 32 位 (`0x10B`) 还是 64 位 (`0x20B`)
|
||||
- **`AddressOfEntryPoint`**:程序的入口点地址,它是一个 **RVA (Relative Virtual Address)**。当文件加载后,加载器会将控制权交给这个地址
|
||||
- **`ImageBase`**:程序加载到内存中的首选基址
|
||||
- **`SectionAlignment`** 和 **`FileAlignment`**:内存中和文件中的节对齐粒度
|
||||
- **`SizeOfImage`**:整个文件被加载到内存后占用的总大小
|
||||
- **`DataDirectory` (数据目录)**:这是最重要的部分之一,一个 `IMAGE_DATA_DIRECTORY` 结构体数组。它包含了 PE 文件中各种重要数据结构的位置和大小,例如:
|
||||
- **`Import Table` (导入表)**:记录了程序依赖的 DLL 和从中导入的函数。加载器在运行时会根据这个表填充函数的真实地址
|
||||
- **`Export Table` (导出表)**:记录了 DLL 文件中供其他程序调用的函数
|
||||
- **`Resource Table` (资源表)**:包含了图标、光标、菜单、对话框等资源数据
|
||||
- **`Base Relocation Table` (基址重定位表)**:当文件无法加载到其首选基址时,需要进行重定位,这个表记录了所有需要修正的地址
|
||||
- **`TLS Table` (线程本地存储表)**:记录了线程相关的数据
|
||||
- **`Debug Directory` (调试目录)**:指向调试信息
|
||||
|
||||
**5. 节表 (Section Table)**
|
||||
|
||||
紧跟在可选头部后面的是节表。这是一个 `IMAGE_SECTION_HEADER` 结构体数组,数组中的每个结构体都描述了一个**节**
|
||||
|
||||
- **`Name`**:节的名称,如 `.text`, `.data`, `.rdata` 等
|
||||
- **`VirtualAddress`**:该节在内存中的 RVA
|
||||
- **`SizeOfRawData`**:该节在文件中的大小
|
||||
- **`PointerToRawData`**:该节在文件中的偏移量
|
||||
- **`Characteristics`**:节的属性,例如**可读、可写、可执行**等权限
|
||||
|
||||
**6. 节 (Sections)**
|
||||
|
||||
节是 PE 文件中包含实际数据和代码的区域。它们是根据功能和权限来划分的。常见的节有:
|
||||
|
||||
- **`.text`**:包含可执行代码和只读数据。在内存中通常具有**只读和可执行**权限
|
||||
- **`.data`**:包含已初始化的全局变量和静态变量。在内存中通常具有**可读和可写**权限
|
||||
- **`.rdata`**:包含只读数据,如字符串常量、导入表、导出表等。在内存中通常具有**只读**权限
|
||||
- **`.idata`**:导入表
|
||||
- **`.edata`**:导出表
|
||||
- **`.rsrc`**:资源数据,如图标和位图
|
||||
- **`.reloc`**:基址重定位表
|
||||
|
||||
**PE 文件加载过程**
|
||||
|
||||
当 Windows 加载器加载一个 PE 文件时,它会:
|
||||
|
||||
1. **检查 DOS 头部和 NT 头部**,确认文件是有效的 PE 格式
|
||||
2. **根据可选头部中的 `ImageBase` 和 `SizeOfImage`** 为程序在内存中分配虚拟地址空间
|
||||
3. **遍历节表**,将文件中的各个节根据其在文件中的偏移和在内存中的 RVA,映射到先前分配的内存空间中
|
||||
4. **处理导入表**,将程序依赖的 DLL 加载到内存,并填充导入表中的函数地址
|
||||
5. **如果文件无法加载到其首选基址**,加载器会处理基址重定位表,修正所有需要调整的地址
|
||||
6. **将控制权转移到入口点** (`AddressOfEntryPoint`),程序开始执行
|
||||
@@ -0,0 +1,53 @@
|
||||
### 讲讲 ASLR 怎么绕过
|
||||
|
||||
**ASLR 的核心思想**
|
||||
|
||||
首先,我们简要回顾一下 ASLR 的原理。ASLR 是一种**漏洞缓解机制**,它的核心思想是**将程序在内存中的关键区域(如可执行文件、共享库、堆和栈)加载到随机的地址**
|
||||
|
||||
没有 ASLR 时,每次运行程序,其内存布局都是固定的。攻击者可以精确地预测函数和变量的地址,然后利用漏洞(如缓冲区溢出)来劫持程序流,跳转到这些预先知道的地址
|
||||
|
||||
有了 ASLR,程序每次运行时地址都会变化,攻击者无法再依赖硬编码的地址,这大大增加了攻击的难度
|
||||
|
||||
**绕过 ASLR 的基本思路**
|
||||
|
||||
ASLR 依赖于**地址的保密性**。如果攻击者能够泄露(也就是获得)任何一个模块的真实地址,那么 ASLR 就会失效。这是因为一个模块内部的相对偏移(RVA)是固定的。一旦知道了一个函数的真实地址,攻击者就可以通过这个地址减去其 RVA,得到模块的基址,进而计算出该模块中所有其他函数和数据的位置
|
||||
|
||||
因此,绕过 ASLR 的核心思路就是**信息泄露**
|
||||
|
||||
**常见绕过 ASLR 的技术**
|
||||
|
||||
以下是一些最常见的绕过 ASLR 的技术,从简单到复杂
|
||||
|
||||
**1. 信息泄露漏洞**
|
||||
|
||||
这是最直接的绕过方式。如果程序本身存在一个信息泄露漏洞,攻击者就可以利用它来获得内存中的地址
|
||||
|
||||
- **格式化字符串漏洞(Format String Bug):** 攻击者可以利用 `%p` 格式符来打印栈上的指针。如果栈上恰好有一个指向模块基址或函数地址的指针,攻击者就可以轻松获得这些关键地址
|
||||
- **堆溢出或栈溢出:** 如果攻击者能够通过溢出漏洞读取栈或堆上的数据,他们可能会读出函数返回地址、栈指针或堆块指针,这些指针都包含有地址信息
|
||||
- **未初始化变量:** 如果一个变量没有初始化,它可能包含之前内存中的残余数据,而这些残余数据可能恰好是一个地址
|
||||
|
||||
**如何利用:**
|
||||
|
||||
1. 利用信息泄露漏洞获得某个函数的真实地址,比如 `puts()` 的地址
|
||||
2. 通过 `puts()` 的地址,计算出 `libc` 库的基址(`puts_addr - puts_libc_offset = libc_base`)
|
||||
3. 有了 `libc` 的基址,就可以计算出 `system()` 函数和字符串 `"/bin/sh"` 的地址,从而构造 ROP 链来执行 Shellcode
|
||||
|
||||
**2. 局部 ASLR 绕过**
|
||||
|
||||
某些系统或老旧的程序只对部分内存区域进行了 ASLR 随机化,或者随机化范围非常小
|
||||
|
||||
- **弱 ASLR:** 32 位系统上的 ASLR 随机化范围通常只有 16 位(64KB)。攻击者可以通过暴力破解或多次尝试来命中正确的地址,这被称为**暴力破解攻击(Brute-force Attack)**
|
||||
|
||||
**3. 非随机化区域利用**
|
||||
|
||||
有些程序或系统组件没有启用 ASLR 保护,它们总会被加载到固定的地址。攻击者可以利用这些非随机化区域作为跳板
|
||||
|
||||
- **静态编译的程序:** 如果一个程序是静态编译的,不依赖任何共享库,那么它的所有代码和数据都在一个文件中。即使开启了 ASLR,其自身的基址是随机化的,但如果攻击者能够泄露出一个地址,就能推算出所有其他地址
|
||||
- **禁用 ASLR 的模块:** 有些老旧的 DLL 可能没有启用 ASLR 编译。这些模块每次都会被加载到固定地址,攻击者可以直接利用其中的 gadgets 或函数
|
||||
|
||||
**4. 通过其他漏洞组合绕过**
|
||||
|
||||
ASLR 很少被单独绕过,通常需要与其他漏洞(如代码执行漏洞)结合使用
|
||||
|
||||
- **Return-to-PLT/GOT:** 攻击者可以利用漏洞劫持程序流,跳转到 GOT(Global Offset Table)中一个已解析的函数地址。因为 GOT 存放的是外部函数的真实地址,通过读取这个地址,攻击者就能获得 `libc` 等共享库的基址
|
||||
- **ROP Gadget 寻找:** 在没有信息泄露漏洞的情况下,攻击者可以使用**多次尝试**或**堆喷射**等技术。在现代 64 位系统上,ASLR 随机化范围很大,暴力破解几乎不可行。所以,信息泄露是绕过 ASLR 的首选方式
|
||||
@@ -0,0 +1,69 @@
|
||||
### 函数的调用约定有哪些,区别是什么
|
||||
|
||||
**1. `cdecl` (C Declaration)**
|
||||
|
||||
这是 C 语言的默认调用约定,也是最常见的
|
||||
|
||||
- **参数传递**:从**右到左**依次推入栈中
|
||||
- **栈清理**:由**调用方(caller)**负责在函数返回后清理栈
|
||||
- **寄存器**:`eax`, `ecx`, `edx` 是调用方保存的(caller-saved)寄存器。这意味着被调用函数可以自由使用这些寄存器,如果调用方需要保存它们,需要在调用前自己推入栈中
|
||||
|
||||
**优点**:支持可变参数函数,例如 `printf`
|
||||
|
||||
**缺点**:每次函数调用后都需要调用方执行额外的栈清理指令,会增加代码大小
|
||||
|
||||
**2. `stdcall` (Standard Call)**
|
||||
|
||||
这是 Windows API 的默认调用约定
|
||||
|
||||
- **参数传递**:从**右到左**依次推入栈中
|
||||
- **栈清理**:由**被调用方(callee)**负责清理栈
|
||||
- **寄存器**:与 `cdecl` 类似
|
||||
|
||||
**优点**:代码更精简。由于被调用方知道参数数量,只需一条指令即可清理栈
|
||||
|
||||
**缺点**:不支持可变参数函数,因为被调用方需要知道参数数量才能正确清理栈
|
||||
|
||||
**3. `fastcall`**
|
||||
|
||||
这是一个为了提高性能而设计的调用约定
|
||||
|
||||
- **参数传递**:前两个(或更多,具体取决于编译器和架构)参数通过**寄存器**传递,而不是通过栈。其余参数从右到左推入栈中
|
||||
- **栈清理**:由**被调用方**清理栈
|
||||
- **寄存器**:使用 `ecx` 和 `edx`(在 32 位 Windows 上)或 `rcx` 和 `rdx`(在 64 位 Windows 上)来传递前两个参数
|
||||
|
||||
**优点**:通过减少内存访问(栈操作)来提高函数调用速度
|
||||
|
||||
**缺点**:不支持可变参数函数
|
||||
|
||||
**4. `thiscall`**
|
||||
|
||||
这是 C++ 中非静态成员函数的默认调用约定
|
||||
|
||||
- **参数传递**:与 `cdecl` 或 `stdcall` 类似,但隐藏的 `this` 指针(指向对象实例)通过**寄存器**传递
|
||||
- **栈清理**:由**被调用方**清理栈
|
||||
|
||||
**优点**:优化了 C++ 成员函数的调用
|
||||
|
||||
**缺点**:只能用于 C++ 成员函数
|
||||
|
||||
**5. `pascal` (Pascal Language)**
|
||||
|
||||
在老旧的 Windows 16 位编程中很常见,现在很少使用
|
||||
|
||||
- **参数传递**:从**左到右**推入栈中
|
||||
- **栈清理**:由**被调用方**清理栈
|
||||
|
||||
**不同平台下的调用约定**
|
||||
|
||||
- **Windows (x86)**:
|
||||
- `cdecl`:C/C++ 默认
|
||||
- `stdcall`:Windows API 默认
|
||||
- `fastcall`:用于性能优化
|
||||
- `thiscall`:C++ 成员函数
|
||||
- **Windows (x64)**:
|
||||
- `__fastcall`:这是 64 位 Windows 的唯一调用约定。前四个整数或指针参数通过 `rcx`, `rdx`, `r8`, `r9` 寄存器传递。其余参数从右到左推入栈中
|
||||
- **Linux (x86)**:
|
||||
- `cdecl`:默认
|
||||
- **Linux (x64)**:
|
||||
- 前六个整数或指针参数通过 `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9` 寄存器传递。其余参数从右到左推入栈中
|
||||
@@ -0,0 +1,23 @@
|
||||
### fuzzing 主要是用来干嘛
|
||||
|
||||
**1. 探索代码路径(Code Coverage)**
|
||||
|
||||
Fuzzing 的一个核心目标是最大化代码覆盖率。传统的测试可能只会测试程序的“阳光大道”,而 fuzzing 则会试图探索那些不寻常的、可能存在漏洞的角落
|
||||
|
||||
例如,一个**覆盖率引导的 fuzzer**(如 AFL 或 LibFuzzer)会记录下每个输入所执行的代码路径。如果一个新的输入能够触发一条未曾被执行过的代码路径,fuzzer 就会将这个输入保留下来,并对其进行变异,以期能更深入地探索该路径。这种方法使得 fuzzer 能够自动绕过复杂的逻辑检查,深入到程序的核心功能中去
|
||||
|
||||
**2. 内存安全漏洞检测**
|
||||
|
||||
许多安全漏洞都与内存操作有关,比如:
|
||||
|
||||
- **缓冲区溢出**:向一个固定大小的缓冲区写入了超过其容量的数据,导致相邻的内存区域被覆盖
|
||||
- **空指针解引用**:程序试图访问一个空指针指向的内存
|
||||
- **整数溢出**:在进行算术运算时,结果超出了变量类型的最大值,导致意外的结果
|
||||
|
||||
Fuzzing 工具通过监控程序的**内存访问行为**来检测这些漏洞。例如,fuzzer 可以使用 Sanitizer 工具(如 AddressSanitizer、MemorySanitizer)来在运行时追踪内存读写,一旦发现非法访问,就会立即报告
|
||||
|
||||
**3. 绕过输入验证**
|
||||
|
||||
攻击者通常需要绕过程序的输入验证逻辑才能触发漏洞。从二进制角度来看,fuzzing 能够通过**变异输入的字节**来绕过这些验证
|
||||
|
||||
例如,一个网络协议解析器可能期望数据包的某个字段是一个特定的值。Fuzzer 会随机改变这个字段的字节,并观察解析器的行为。如果变异后的数据包导致程序进入一个异常状态,fuzzer 就找到了一个潜在的漏洞
|
||||
@@ -0,0 +1,24 @@
|
||||
### 对 Windows 平台的漏洞和保护机制了解多少
|
||||
|
||||
**常见的 Windows 漏洞类型**
|
||||
|
||||
理解 Windows 漏洞首先要从其根源——软件缺陷说起。以下是一些最常见、最危险的漏洞类型:
|
||||
|
||||
- **缓冲区溢出 (Buffer Overflows)**:这是最经典的漏洞类型。当程序向一个固定大小的缓冲区写入的数据超过其容量时,多余的数据会覆盖相邻的内存,比如函数返回地址或栈上的其他数据。攻击者可以利用这一点来劫持程序执行流程
|
||||
- **栈溢出 (Stack Overflows)**:发生在栈上,通常用于劫持函数返回地址
|
||||
- **堆溢出 (Heap Overflows)**:发生在堆上,比栈溢出更复杂,但同样危险,通常用于修改关键数据结构或函数指针
|
||||
- **整数溢出 (Integer Overflows)**:当一个整数运算的结果超出其数据类型的最大值时,会发生溢出,导致非预期的结果。攻击者可以利用整数溢出绕过大小检查,比如分配一个远小于所需内存的缓冲区,然后触发缓冲区溢出
|
||||
- **格式化字符串漏洞 (Format String Bugs)**:当程序使用 `printf` 等函数时,如果攻击者能控制格式化字符串,他们就可以利用 `%p`, `%n` 等格式符来读取栈上的数据(信息泄露)或向任意地址写入数据。这种漏洞通常用于绕过 ASLR
|
||||
- **UAF (Use-After-Free)**:当程序释放一块内存后,仍然继续使用这个指针。攻击者可以利用这个时间差,在这块内存被重新分配给其他数据后,通过旧指针访问或修改新数据,从而达到执行任意代码的目的
|
||||
- **竞态条件 (Race Conditions)**:当两个或多个线程在没有适当同步的情况下,竞争访问和修改共享资源时,可能会导致意外的结果。攻击者可以利用这种不确定性来触发漏洞,例如在程序检查完权限后,但在执行操作前,快速修改一个文件
|
||||
|
||||
**Windows 平台的漏洞保护机制**
|
||||
|
||||
为了对抗这些漏洞,微软在 Windows 操作系统和编译器中集成了一系列强大的漏洞缓解机制。这些机制不会修复漏洞本身,但会大大增加攻击的难度
|
||||
|
||||
- **数据执行保护 (Data Execution Prevention, DEP)**:这是最基本的保护机制之一,也称 **NX 位(No-eXecute)**。它将内存页标记为**不可执行**,以防止攻击者在数据段(如堆和栈)上执行恶意代码。为了绕过 DEP,攻击者必须使用**代码重用技术**,比如 ROP
|
||||
- **地址空间布局随机化 (Address Space Layout Randomization, ASLR)**:ASLR 会将可执行文件、DLL、堆和栈等关键内存区域随机加载到不同的地址。这使得攻击者无法预测函数和变量的精确位置,从而让传统的缓冲区溢出攻击失效。ASLR 的主要弱点是**信息泄露漏洞**,攻击者可以通过它来获取关键地址,从而绕过 ASLR
|
||||
- **栈保护 (Stack Canaries)**:也称为 SSP (Stack Smashing Protection)。编译器在函数返回地址之前插入一个随机的“金丝雀值”。在函数返回前,程序会检查这个值是否被修改。如果被修改,就说明发生了栈溢出,程序会立即终止
|
||||
- **控制流防护 (Control Flow Guard, CFG)**:CFG 是一个更高级的保护机制,旨在对抗 ROP 攻击。它通过在编译器和操作系统层面创建和验证一个合法的间接调用地址列表。在运行时,它会检查所有的间接函数调用,如果目标地址不在这个合法列表中,就会阻止调用
|
||||
- **SEHOP (Structured Exception Handling Overwrite Protection)**:SEHOP 是一种针对 SEH 覆盖漏洞的保护。它通过在异常处理链的末尾插入一个特殊的指针来验证链的完整性。如果攻击者试图覆盖 SEH 链,这个验证就会失败,阻止攻击
|
||||
- **SafeSEH**:在编译时,SafeSEH 会生成一个合法的异常处理函数列表。在运行时,操作系统会验证异常处理函数的地址是否在列表中。这防止了攻击者利用非法的异常处理函数来劫持程序流
|
||||
Reference in New Issue
Block a user