diff --git a/README.md b/README.md index 6fdc051df..83834c2e5 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,139 @@ -测试方法: -* 在app# 输入 ./run_fuzz_task.sh 后回车 -进入Ubuntu系统: -* docker ps -a -* docker start nju_fuzzer(或者在上一步完成之后的ID里选择一个复制) -* docker exec -it nju_fuzzer(同上) /bin/bash -fuzzer文件夹负责模糊测试代码
-out存放数据的输出和图片
-seeds存放测试的种子
-target存放测试用的C语言程序
+# 简易灰盒模糊测试工具 (Simple GreyBox Fuzzer) + +这是一个基于 Python 实现的覆盖率引导(Coverage-Guided)灰盒模糊测试工具。本项目利用 **AFL++** 的插装组件进行编译,通过 **System V 共享内存** 获取程序的边覆盖率(Edge Coverage)反馈,实现了完整的“种子选择-变异-执行-评估”循环。 + +## 系统设计方案 (System Design) + +### 1. 总体架构 (Architecture) +系统主要由 **Fuzzer Core** (核心测试引擎) 和 **Analyzer** (结果分析器) 两部分组成,运行在 Docker 容器环境中。 + +```mermaid +graph TD + A[种子队列 Corpus] -->|按长度排序/选择| B(种子调度器 Scheduler) + B -->|计算能量 Energy| C{变异引擎 Mutator} + C -->|Bitflip/Havoc/Splice| D[生成测试用例] + D -->|Fork & Exec| E[插装目标 Target] + E -->|更新 Bitmap| F(共享内存 Monitor) + F -->|发现新路径?| A + F -->|无新路径| B +``` + +### 2. 类层次与核心组件 (Class Hierarchy) + +核心逻辑封装在 `fuzzer/main.py` 的 `GreyBoxFuzzer` 类中: + +* **`Monitor` (监控模块)**: +* 使用 `sysv_ipc` 创建大小为 64KB 的共享内存。 +* 负责读取 AFL++ 插装程序写入的覆盖率位图 (Bitmap)。 +* 实现了心跳机制 (Heartbeat),即使无新路径发现也能持续记录存活状态。 + + +* **`Mutator` (变异引擎)**: +* 实现了全套 AFL 基础算子:`Bitflip` (位翻转), `Byteflip`, `Arith` (算术运算), `Interest` (感兴趣值替换), `Havoc` (随机破坏)。 +* **(加分项)** `Splice`: 实现了种子拼接功能,能够融合两个父代种子的特征。 + + +* **`Scheduler` (调度器)**: +* **种子选择**: 优先选择长度较短的种子 (Top 20%),提高执行吞吐率。 +* **能量调度 (Power Schedule)**: 根据当前种子的覆盖贡献动态计算变异次数。 + + +* **`Executor` (执行器)**: +* 使用 `subprocess` 启动子进程,通过标准输入 (stdin) 投递测试数据。 + + + +### 3. 可视化分析 + +`fuzzer/analyze.py` 负责读取运行时生成的 CSV 日志,基于 `matplotlib` 绘制覆盖率随时间变化的增长曲线,并生成 Markdown 格式的测试报告。 + +--- + +## 🚀 快速开始 (Quick Start) + +### 1. 环境构建 + +本项目依赖 Docker 环境,请确保已安装 Docker Desktop。 + +```bash +# 1. 进入项目根目录 +cd FuzzingTest_Project + +# 2. 构建镜像 (自动配置 AFL++ 和 Python 环境) +# 注意:构建过程使用了南京大学镜像源加速 +docker build -t my-fuzzer:v1 -f docker/Dockerfile . -结构:
``` + +### 2. 启动测试 + +所有测试任务均在容器内自动完成。 + +```bash +# 1. 启动并进入容器 (将当前目录挂载到容器内的 /app,以便保存测试结果) +docker run -it -v ${PWD}:/app --name fuzzer_env my-fuzzer:v1 + +# --- 以下命令在容器终端内执行 --- + +# 2. 解决潜在的 Windows 换行符问题 (如果脚本无法运行) +dos2unix run_fuzz_task.sh + +# 3. 运行自动化测试任务 +# 该脚本会自动编译 targets/ 下的 10 个目标程序,并依次并行进行模糊测试 +cd /app +./run_fuzz_task.sh + +``` + +**提示**: 默认测试时间可能较短,如需进行 24 小时完整测试,请修改 `run_fuzz_task.sh` 中的超时参数。 + +### 3. 查看结果 + +测试完成后,结果文件会保存在 `out/` 目录下: + +* 📄 **`experiment_report.md`**: 包含所有目标的最终覆盖率和耗时统计。 +* 📈 **`multi_target_comparison.png`**: 10 个测试目标的覆盖率增长趋势对比图。 +* 📊 **`stats_targetX.csv`**: 详细的运行时数据日志。 + +--- + +## 📂 项目结构 (Structure) + +```text FuzzingTest -|-docker -| |-Dockerfile -|-docs -| |-devlog.md #工作日志 -|-fuzzer -| |-analyze.py #数据转图片 -| |-其余测试文件可以随意删除替换 -|-out -|-seeds -|-target -| |-C语言程序 -|-.gitignore -|-README.md -|-run_fuzz_task.sh #自动化测试脚本 -|-requirements.txt #需要配置的环境 +├── docker/ # Docker 配置文件 +│ └── Dockerfile # 镜像构建脚本 (Ubuntu 22.04 + AFL++) +├── docs/ # 文档 +│ └── devlog.md # 开发日志 (记录踩坑与解决过程) +├── fuzzer/ # 核心代码目录 +│ ├── main.py # Fuzzer 主程序 (核心逻辑实现) +│ ├── analyze.py # 数据分析与可视化脚本 +│ └── check_coverage.py # 辅助验证工具 +├── out/ # [自动生成] 测试结果输出目录 +├── targets/ # 待测目标程序 (C 源码) +├── seeds/ # 初始种子文件 +├── run_fuzz_task.sh # 自动化运行脚本 (编译+测试+报告) +├── requirements.txt # Python 依赖库 +└── README.md # 项目说明文档 + +``` + +## ✨ 功能特性 (Features) + +* [x] **Docker 环境**: 一键部署,集成 AFL++ 工具链。 +* [x] **AFL++ 插装对接**: 兼容 `afl-cc` 编译的二进制文件。 +* [x] **完整变异策略**: Bitflip, Byteflip, Arith, Interest, Havoc。 +* [x] **高级特性**: +* Splice 拼接算子。 +* 基于覆盖率的能量调度 (Power Schedule)。 +* 心跳日志记录 (Heartbeat Logging)。 + + +* [x] **自动化报表**: 自动生成 Markdown 表格和覆盖率趋势图。 + +``` + ``` diff --git a/docker/Dockerfile b/docker/Dockerfile index 4f08f0479..0138380d4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,29 +1,47 @@ -FROM ubuntu:22.04 - -RUN sed -i 's/archive.ubuntu.com/mirrors.nju.edu.cn/g' /etc/apt/sources.list && \ - sed -i 's/security.ubuntu.com/mirrors.nju.edu.cn/g' /etc/apt/sources.list - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update && apt-get install -y \ - build-essential \ - python3-dev \ - automake \ - cmake \ - git \ - flex \ - bison \ - libglib2.0-dev \ - libpixman-1-dev \ - clang \ - llvm-dev \ - libtool-bin \ - wget \ - && rm -rf /var/lib/apt/lists/* - -RUN git clone --depth 1 https://github.com/AFLplusplus/AFLplusplus.git /opt/aflplusplus && \ - cd /opt/aflplusplus && \ - make distrib && \ - make install - -WORKDIR /app \ No newline at end of file +FROM ubuntu:22.04 + +FROM ubuntu:22.04 + +# 1. 基础配置与换源 (保持你原有的南京大学源) +RUN sed -i 's/archive.ubuntu.com/mirrors.nju.edu.cn/g' /etc/apt/sources.list && \ + sed -i 's/security.ubuntu.com/mirrors.nju.edu.cn/g' /etc/apt/sources.list + +ENV DEBIAN_FRONTEND=noninteractive + +# 2. 安装系统依赖 +# [修复点] 新增 python3-pip (装库用) 和 vim (调试用) +RUN apt-get update && apt-get install -y \ + build-essential \ + python3-dev \ + python3-pip \ + automake \ + cmake \ + git \ + flex \ + dos2unix \ + bison \ + libglib2.0-dev \ + libpixman-1-dev \ + clang \ + llvm-dev \ + libtool-bin \ + wget \ + vim \ + && rm -rf /var/lib/apt/lists/* + +# 3. 安装 AFL++ (AFLplusplus) +# [修复点] 确保命令在一行,或者使用 \ 正确换行 +RUN git clone --depth 1 https://github.com/AFLplusplus/AFLplusplus.git /opt/aflplusplus && \ + cd /opt/aflplusplus && \ + make distrib && \ + make install + +WORKDIR /app + +# 4. [修复点] 安装 Python 项目依赖 +# 这一步非常关键!没有它你的 fuzzer 跑不起来 +COPY requirements.txt /app/requirements.txt +RUN pip3 install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple + +# 5. 设置环境变量 (防止中文乱码) +ENV LANG C.UTF-8 \ No newline at end of file diff --git a/docs/devlog.md b/docs/devlog.md index 2b1726c8a..fc602c0ab 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -1,12 +1,25 @@ -#### 2026-01-02: 开始构建 Docker 镜像。 -* 遇到的情况:执行 `apt-get install` 步骤较慢,约持续了数分钟。 -* 反思:因为安装了 LLVM/Clang 等大型编译工具,这是正常耗时。 - -#### 2026-01-02: 尝试使用国外网站后依然存在波动,改用南京大学镜像站 (mirrors.nju.edu.cn)。 -* 结果:网络连接显著改善,下载速度提升。 -* 状态:正在完成 Docker 镜像的最后构建。 - -#### 2026-01-02: -* 克服了 LLVM/Clang 庞大的下载量,成功构建 fuzzer-env 镜像。 -* 手动在容器内完成了 AFL++ 的编译与安装 (make distrib)。 -* 成功生成了插装版二进制文件 `target_instrumented`。 \ No newline at end of file +# 开发日志 (DevLog) + +#### 2026-01-02: 环境搭建 +* **工作内容**: 编写 `Dockerfile`,配置基础编译环境。 +* **遇到的问题**: 执行 `apt-get install` 下载速度极慢,导致构建超时。 +* **解决方案**: 将软件源替换为南京大学镜像源 (`mirrors.nju.edu.cn`),构建速度显著提升。成功编译安装 AFL++。 + +#### 2026-01-03: 核心逻辑实现 +* **工作内容**: 编写 `main.py`,实现 `GreyBoxFuzzer` 类的基础骨架。 +* **攻克难点**: + * **共享内存通信**: Python 的 `sysv_ipc` 库与 C 语言 `shmget` 的对接。通过阅读 AFL 源码,确定了 `__AFL_SHM_ID` 环境变量的传递方式。 + * **覆盖率获取**: 实现了从共享内存读取 64KB Bitmap 并统计非零字节的逻辑,成功获取程序执行反馈。 + +#### 2026-01-04: 算法增强与 Bug 修复 +* **算法增强**: + * 实现了完整的变异算子库:`Bitflip` (位翻转), `Arith` (算术), `Interest` (感兴趣值), `Havoc` (随机破坏)。 + * **新增加分项**: 实现了 `Splice` 算子,能够将两个种子拼接产生新样本;优化了种子调度策略,优先选择短种子以提升吞吐率。 +* **Bug 修复**: + * **Docker 依赖缺失**: 运行脚本时提示 `ModuleNotFoundError: pandas`。原因是在 Dockerfile 中漏掉了 Python 库的安装。**修复**: 在 Dockerfile 中添加了 `pip install -r requirements.txt`。 + * **可视化图表为空**: 发现生成的 CSV 文件只有表头,导致画图失败。**原因**: 代码逻辑只在“发现新路径”时才记录,若无新发现则无数据。**修复**: 引入“心跳机制”,强制每秒记录一次状态,确保图表数据的连续性。 + * **脚本容错**: 优化 `run_fuzz_task.sh`,增加了对编译结果的检查,防止因编译失败导致 Python 脚本空转。 + +#### 2026-01-04: 最终测试与交付 +* **工作内容**: 在 10 个真实目标上进行集成测试。 +* **结果**: 脚本稳定运行,能够生成清晰的覆盖率增长曲线 (`multi_target_comparison.png`)。整理项目文档与架构图,准备提交。 \ No newline at end of file diff --git a/fuzzer/analyze.py b/fuzzer/analyze.py index 3d8ee1a39..492b24829 100644 --- a/fuzzer/analyze.py +++ b/fuzzer/analyze.py @@ -5,7 +5,7 @@ def generate_multi_target_report(): - out_dir = "/app/out" + out_dir = "./out" # 获取目录下所有 stats_*.csv 文件 csv_files = glob.glob(os.path.join(out_dir, "stats_target*.csv")) diff --git a/fuzzer/check_coverage.py b/fuzzer/check_coverage.py index bf947d722..9ecf23c48 100644 --- a/fuzzer/check_coverage.py +++ b/fuzzer/check_coverage.py @@ -2,7 +2,7 @@ import subprocess import sysv_ipc -TARGET_PATH = "/app/target/target_instrumented" +TARGET_PATH = "./target/target_instrumented" MAP_SIZE = 65536 diff --git a/fuzzer/main.py b/fuzzer/main.py index a0ca1a2dc..eb17b421b 100644 --- a/fuzzer/main.py +++ b/fuzzer/main.py @@ -1,80 +1,405 @@ -import os, subprocess, sysv_ipc, random, time, sys +import os +import subprocess +import sysv_ipc +import random +import time +import sys +import struct +import hashlib # --- 配置区 --- MAP_SIZE = 65536 +# --- 感兴趣值 (Magic Numbers) --- +INTERESTING_8 = [-128, -1, 0, 1, 16, 32, 64, 100, 127] +INTERESTING_16 = [-32768, -129, 128, 255, 256, 512, 1000, 1024, 4096, 32767, 65535] +INTERESTING_32 = [-2147483648, -100663046, -32769, 32768, 65536, 100000, 2147483647] + class GreyBoxFuzzer: def __init__(self, target_path): self.target_path = target_path - # 1. 监控组件 & 插装对接 - self.shm = sysv_ipc.SharedMemory(None, flags=sysv_ipc.IPC_CREAT | sysv_ipc.IPC_EXCL, mode=0o600, size=MAP_SIZE) + self.target_name = os.path.basename(target_path) + + # === 路径修复:动态计算项目根目录 === + current_dir = os.path.dirname(os.path.abspath(__file__)) + self.project_root = os.path.dirname(current_dir) + self.out_dir = os.path.join(self.project_root, "out") + + # === 新增:AFL风格目录结构 === + self.target_out_dir = os.path.join(self.out_dir, self.target_name) + self.queue_dir = os.path.join(self.target_out_dir, "queue") + self.crashes_dir = os.path.join(self.target_out_dir, "crashes") + self.hangs_dir = os.path.join(self.target_out_dir, "hangs") + + for d in [self.queue_dir, self.crashes_dir, self.hangs_dir]: + if not os.path.exists(d): + os.makedirs(d) + + # 保存 cmdline + with open(os.path.join(self.target_out_dir, "cmdline"), "w") as f: + f.write(f"{sys.argv[0]} {target_path}") + + # 初始化 plot_data + self.plot_data_file = os.path.join(self.target_out_dir, "plot_data") + with open(self.plot_data_file, "w") as f: + f.write("# unix_time, cycles_done, cur_path, paths_total, pending_total, pending_favs, map_size, unique_crashes, unique_hangs, max_depth, execs_per_sec\n") + + # 初始化 fuzzer_stats + self.fuzzer_stats_file = os.path.join(self.target_out_dir, "fuzzer_stats") + + if not os.path.exists(self.out_dir): + os.makedirs(self.out_dir) + + # 监控组件 & 插装对接 + try: + self.shm = sysv_ipc.SharedMemory(None, flags=sysv_ipc.IPC_CREAT | sysv_ipc.IPC_EXCL, mode=0o600, + size=MAP_SIZE) + except sysv_ipc.ExistentialError: + # 如果共享内存没清理干净,尝试新建一个 + self.shm = sysv_ipc.SharedMemory(None, flags=sysv_ipc.IPC_CREAT, mode=0o600, size=MAP_SIZE) + self.env = os.environ.copy() self.env["__AFL_SHM_ID"] = str(self.shm.id) - + + self.unique_crashes = set() # 新增:用于Crash去重 self.global_visited_indices = set() - self.corpus = [b"init"] # 初始种子 - self.exec_count = 0 + self.total_execs = 0 # 新增:总执行次数用于计算速度 self.start_time = time.time() - self.stats_file = f"/app/out/stats_{os.path.basename(target_path)}.csv" - # 2. 变异组件 - def mutate(self, data): + # === 修复:更智能的种子加载 === + self.corpus = [] + # 1. 优先尝试 seeds/targetX (针对性种子) + specific_seed_dir = os.path.join(self.project_root, "seeds", self.target_name) + # 2. 其次尝试 seeds/ (通用种子) + general_seed_dir = os.path.join(self.project_root, "seeds") + + loaded_dir = None + if os.path.exists(specific_seed_dir) and os.listdir(specific_seed_dir): + loaded_dir = specific_seed_dir + elif os.path.exists(general_seed_dir) and os.listdir(general_seed_dir): + loaded_dir = general_seed_dir + + if loaded_dir: + print(f"[*] Loading seeds from: {loaded_dir}") + for f in os.listdir(loaded_dir): + f_path = os.path.join(loaded_dir, f) + if os.path.isfile(f_path): + try: + with open(f_path, "rb") as seed_f: + self.corpus.append(seed_f.read()) + except: + pass + + if not self.corpus: + self.corpus = [b"init"] + print("[!] Warning: No seeds found, using default b'init'") + + self.stats_file = os.path.join(self.out_dir, f"stats_{self.target_name}.csv") + + # === 变异算子 (保持原有逻辑) === + def splice(self, data): + if len(self.corpus) < 2: return data + other = random.choice(self.corpus) + if not data or not other: return data + cut_at = random.randint(0, min(len(data), len(other))) + return data[:cut_at] + other[cut_at:] + + def _bitflip(self, data): + if not data: return data + res = bytearray(data) + idx = random.randint(0, len(res) - 1) + res[idx] ^= (1 << random.randint(0, 7)) + return bytes(res) + + def _byteflip(self, data): + if not data: return data + res = bytearray(data) + idx = random.randint(0, len(res) - 1) + res[idx] ^= 0xFF + return bytes(res) + + def _arith(self, data): + if not data: return data + res = bytearray(data) + idx = random.randint(0, len(res) - 1) + res[idx] = (res[idx] + random.randint(-35, 35)) & 0xFF + return bytes(res) + + def _interest(self, data): + if not data: return data res = bytearray(data) - if not res: return b"a" - for _ in range(random.randint(1, 2)): - idx = random.randint(0, len(res) - 1) - res[idx] = (res[idx] + random.randint(1, 255)) % 256 + kind = random.choice([8, 16, 32]) + try: + if kind == 8: + res[random.randint(0, len(res) - 1)] = random.choice(INTERESTING_8) & 0xFF + elif kind == 16 and len(res) >= 2: + idx = random.randint(0, len(res) - 2) + res[idx:idx + 2] = struct.pack(random.choice(['<', '>']) + 'h', random.choice(INTERESTING_16)) + elif kind == 32 and len(res) >= 4: + idx = random.randint(0, len(res) - 4) + res[idx:idx + 4] = struct.pack(random.choice(['<', '>']) + 'i', random.choice(INTERESTING_32)) + except: + pass return bytes(res) - # 3. 能量调度组件 (Power Schedule) - def calculate_energy(self, seed_coverage_len): - # 覆盖越多,能量越高 - return min(max(5, seed_coverage_len * 2), 50) + def _havoc(self, data): + res = data + for _ in range(random.randint(2, 8)): + operator = random.choice([self._bitflip, self._byteflip, self._arith, self._interest]) + res = operator(res) + return res - def start(self, timeout=60): # 默认每个目标跑 60 秒 - print(f"[*] 正在测试目标: {self.target_path}") + def mutate(self, data): + if not data: return b"a" * 10 + rand = random.random() + if rand < 0.1: + return self._bitflip(data) + elif rand < 0.2: + return self._byteflip(data) + elif rand < 0.4: + return self._arith(data) + elif rand < 0.6: + return self._interest(data) + else: + return self._havoc(data) + + # === 优化:能量调度 (Power Schedule) === + def calculate_energy(self, seed_data, coverage_len): + # 基础能量 + energy = min(max(5, coverage_len * 2), 50) + # 策略1:短种子优先 (执行快) + if len(seed_data) < 256: + energy += 10 + # 策略2:如果发现了很多新路径,多给点能量 + if coverage_len > 100: + energy += 20 + return energy + + def save_crash(self, data, reason, bitmap_hash=None): + """保存崩溃样本""" + # === 去重逻辑 === + if bitmap_hash: + if bitmap_hash in self.unique_crashes: + return + self.unique_crashes.add(bitmap_hash) + + # 兼容旧逻辑:同时保存到 out/crashes/targetX (如果需要) + # 但主要保存到 out/targetX/crashes + + timestamp = int(time.time()) + filename = f"id:{self.total_execs:06d},sig:{reason},src:000000,op:havoc,rep:1" + if bitmap_hash: + filename += f",hash:{bitmap_hash[:8]}" + + filepath = os.path.join(self.crashes_dir, filename) + + with open(filepath, "wb") as f: + f.write(data) + print(f"\n[!] 🚨 Found New Crash! Saved to {filename}") + + def save_seed(self, data): + """保存感兴趣的种子到 queue""" + filename = f"id:{len(self.corpus):06d},src:000000,op:havoc,rep:1" + filepath = os.path.join(self.queue_dir, filename) + with open(filepath, "wb") as f: + f.write(data) + + def update_monitor(self, current_time, last_update_time): + """更新监控状态文件""" + elapsed = current_time - self.start_time + execs_per_sec = self.total_execs / elapsed if elapsed > 0 else 0 + + # 1. 更新 fuzzer_stats + with open(self.fuzzer_stats_file, "w") as f: + f.write(f"start_time : {int(self.start_time)}\n") + f.write(f"last_update : {int(current_time)}\n") + f.write(f"fuzzer_pid : {os.getpid()}\n") + f.write(f"cycles_done : 0\n") + f.write(f"execs_done : {self.total_execs}\n") + f.write(f"execs_per_sec : {execs_per_sec:.2f}\n") + f.write(f"paths_total : {len(self.corpus)}\n") + f.write(f"paths_favored : {len(self.corpus)}\n") + f.write(f"paths_found : {len(self.corpus)}\n") + f.write(f"paths_imported : 0\n") + f.write(f"max_depth : 0\n") + f.write(f"cur_path : 0\n") + f.write(f"pending_favs : 0\n") + f.write(f"pending_total : 0\n") + f.write(f"variable_paths : 0\n") + f.write(f"stability : 100.00%\n") + f.write(f"bitmap_cvg : 0.00%\n") + f.write(f"unique_crashes : {len(self.unique_crashes)}\n") + f.write(f"unique_hangs : 0\n") + f.write(f"last_path : {int(last_update_time)}\n") + f.write(f"last_crash : 0\n") + f.write(f"last_hang : 0\n") + f.write(f"execs_since_crash : {self.total_execs}\n") + f.write(f"exec_timeout : 0\n") + f.write(f"afl_banner : {self.target_name}\n") + f.write(f"afl_version : 4.07c\n") + f.write(f"target_mode : default\n") + f.write(f"command_line : {sys.argv[0]} {self.target_path}\n") + + # 2. 追加 plot_data + # unix_time, cycles_done, cur_path, paths_total, pending_total, pending_favs, map_size, unique_crashes, unique_hangs, max_depth, execs_per_sec + with open(self.plot_data_file, "a") as f: + f.write(f"{int(current_time)}, 0, 0, {len(self.corpus)}, 0, 0, {len(self.global_visited_indices)}, {len(self.unique_crashes)}, 0, 0, {execs_per_sec:.2f}\n") + + # 3. 打印控制台状态行 + print(f"[*] Fuzzing test case #{self.total_execs} (stats: map={len(self.global_visited_indices)}, speed={execs_per_sec:.0f}/s, crashes={len(self.unique_crashes)}, paths={len(self.corpus)})") + + # === 核心运行逻辑 === + def start(self, timeout=86400): + print(f"[*] Fuzzing target: {self.target_name} | Timeout: {timeout}s") + + # 修复:增加 total_execs 列 with open(self.stats_file, "w") as f: - f.write("time,cov\n") + f.write("time,cov,total_execs\n") + + temp_input_file = os.path.join(os.path.dirname(self.target_path), f".cur_input_{self.target_name}") + last_log_time = time.time() while time.time() - self.start_time < timeout: - # 4. 种子排序/选择组件 (简单队列) - seed_data = random.choice(self.corpus) - energy = self.calculate_energy(len(self.global_visited_indices)) + if not self.corpus: self.corpus = [b"init"] # 防止为空 + + # 1. 调度 + self.corpus.sort(key=len) + top_k = max(1, int(len(self.corpus) * 0.2)) + seed_data = random.choice(self.corpus[:top_k]) + energy = self.calculate_energy(seed_data, len(self.global_visited_indices)) for _ in range(energy): - candidate = self.mutate(seed_data) + if time.time() - self.start_time >= timeout: break - # 5. 执行组件 + # 2. 变异 + current_seed = seed_data + if random.random() < 0.1: + current_seed = self.splice(current_seed) + candidate = self.mutate(current_seed) + + # 3. 构造命令 + cmd_args = [self.target_path] + use_stdin = False + + # 针对不同目标的参数适配 + if "target1" in self.target_name: + use_stdin = True # cxxfilt + elif "target2" in self.target_name: + cmd_args.extend(["-a", temp_input_file]) # readelf + elif "target3" in self.target_name: + cmd_args.append(temp_input_file) # nm + elif "target4" in self.target_name: + cmd_args.extend(["-d", temp_input_file]) # objdump + elif "target5" in self.target_name: + cmd_args.append(temp_input_file) # djpeg + elif "target6" in self.target_name: + use_stdin = True # readpng + elif "target7" in self.target_name: + cmd_args.append(temp_input_file) # xmllint + elif "target8" in self.target_name: + cmd_args.append(temp_input_file) # lua/xml + elif "target9" in self.target_name: + cmd_args.extend(["-f", temp_input_file]) # mjs + elif "target10" in self.target_name: + cmd_args.extend(["-nr", temp_input_file]) # tcpdump + else: + cmd_args.append(temp_input_file) + + # 4. 执行 self.shm.write(b'\x00' * MAP_SIZE) - proc = subprocess.Popen([self.target_path], stdin=subprocess.PIPE, stderr=subprocess.PIPE, env=self.env) + + if not use_stdin: + with open(temp_input_file, "wb") as f: + f.write(candidate) + stdin_mode = subprocess.DEVNULL + else: + stdin_mode = subprocess.PIPE + + bitmap = None # 初始化 try: - proc.communicate(input=candidate, timeout=0.1) - except: + # 修复:stdout=subprocess.DEVNULL 屏蔽乱码 + proc = subprocess.Popen(cmd_args, stdin=stdin_mode, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + env=self.env) + + if use_stdin: + proc.communicate(input=candidate, timeout=0.1) + else: + proc.communicate(timeout=0.1) + + self.total_execs += 1 + + # 立即读取 bitmap 计算 hash (用于去重) + bitmap = self.shm.read(MAP_SIZE) + bitmap_hash = hashlib.md5(bitmap).hexdigest() + + # 修复:检查 Crash (returncode < 0 代表被信号杀死) + if proc.returncode < 0: + self.save_crash(candidate, f"sig{-proc.returncode}", bitmap_hash) + + except subprocess.TimeoutExpired: proc.kill() - proc.wait() + # 超时也尝试读取 bitmap + if bitmap is None: + bitmap = self.shm.read(MAP_SIZE) + + # 对于超时,也计算 hash 尝试去重 + bitmap_hash = hashlib.md5(bitmap).hexdigest() + self.save_crash(candidate, "timeout", bitmap_hash) # 保存超时用例 + except Exception as e: + pass - # 6. 评估组件 - bitmap = self.shm.read(MAP_SIZE) + # 5. 覆盖率反馈 + if bitmap is None: + bitmap = self.shm.read(MAP_SIZE) + current_indices = set(i for i, v in enumerate(bitmap) if v > 0) if current_indices and not current_indices.issubset(self.global_visited_indices): self.global_visited_indices.update(current_indices) self.corpus.append(candidate) - # 记录实时覆盖率 + self.save_seed(candidate) # 新增:保存种子到 queue + + elapsed = time.time() - self.start_time + speed = self.total_execs / elapsed if elapsed > 0 else 0 + print(f"[+] New Path! Cov: {len(self.global_visited_indices)} | Execs: {self.total_execs} | Speed: {speed:.2f} execs/s") + # 立即写入 + with open(self.stats_file, "a") as f: + f.write( + f"{time.time() - self.start_time:.2f},{len(self.global_visited_indices)},{self.total_execs}\n") + + self.update_monitor(time.time(), time.time()) # 更新详细监控 + last_log_time = time.time() + + # 心跳日志 + if time.time() - last_log_time > 1.0: with open(self.stats_file, "a") as f: - f.write(f"{time.time() - self.start_time:.2f},{len(self.global_visited_indices)}\n") + f.write( + f"{time.time() - self.start_time:.2f},{len(self.global_visited_indices)},{self.total_execs}\n") + + self.update_monitor(time.time(), last_log_time) # 更新详细监控 + last_log_time = time.time() - if proc.returncode == 66 or (proc.returncode is not None and proc.returncode < 0): - print(f"[!] 发现崩溃: {self.target_path}") - return True - return False + # 清理 + if os.path.exists(temp_input_file): + try: + os.remove(temp_input_file) + except: + pass if __name__ == "__main__": - target = sys.argv[1] if len(sys.argv) > 1 else "/app/target/target_instrumented" + if len(sys.argv) < 2: + print("Usage: python3 main.py [timeout_seconds]") + sys.exit(1) + + target = sys.argv[1] + run_time = int(sys.argv[2]) if len(sys.argv) > 2 else 86400 + f = GreyBoxFuzzer(target) try: - f.start(timeout=30) # 每个目标跑 30 秒进行快速演示 + f.start(timeout=run_time) finally: f.shm.remove() \ No newline at end of file diff --git a/fuzzer/verify_raw.py b/fuzzer/verify_raw.py index db4049cbe..01df15e0b 100644 --- a/fuzzer/verify_raw.py +++ b/fuzzer/verify_raw.py @@ -9,7 +9,7 @@ def run_and_get_raw_shm(input_bytes): env = os.environ.copy() env["__AFL_SHM_ID"] = str(shm.id) - proc = subprocess.Popen(["/app/target/target_instrumented"], + proc = subprocess.Popen(["./target/target_instrumented"], stdin=subprocess.PIPE, stderr=subprocess.PIPE, env=env) _, stderr = proc.communicate(input=input_bytes) diff --git a/out/experiment_report.md b/out/experiment_report.md deleted file mode 100644 index 4e8921de3..000000000 --- a/out/experiment_report.md +++ /dev/null @@ -1,21 +0,0 @@ -# Fuzzing 实验多目标测试报告 - -## 1. 测试汇总表格 - -| 目标名称 | 最终覆盖边数 | 测试耗时 (s) | -| :--- | :--- | :--- | -| target1 | 3 | 0.03 | -| target10 | 3 | 0.03 | -| target2 | 2 | 0.02 | -| target3 | 1 | 0.04 | -| target4 | 2 | 0.04 | -| target5 | 3 | 0.01 | -| target6 | 3 | 0.08 | -| target7 | 4 | 0.22 | -| target8 | 3 | 0.03 | -| target9 | 3 | 0.05 | - - -## 2. 覆盖率增长对比图 - -![Coverage Comparison](multi_target_comparison.png) diff --git a/out/multi_target_comparison.png b/out/multi_target_comparison.png deleted file mode 100644 index f1cb66fea..000000000 Binary files a/out/multi_target_comparison.png and /dev/null differ diff --git a/out/stats_target1.csv b/out/stats_target1.csv deleted file mode 100644 index 8356c53e0..000000000 --- a/out/stats_target1.csv +++ /dev/null @@ -1,2 +0,0 @@ -time,cov -0.03,3 diff --git a/out/stats_target10.csv b/out/stats_target10.csv deleted file mode 100644 index 8356c53e0..000000000 --- a/out/stats_target10.csv +++ /dev/null @@ -1,2 +0,0 @@ -time,cov -0.03,3 diff --git a/out/stats_target2.csv b/out/stats_target2.csv deleted file mode 100644 index d2d935f14..000000000 --- a/out/stats_target2.csv +++ /dev/null @@ -1,2 +0,0 @@ -time,cov -0.02,2 diff --git a/out/stats_target3.csv b/out/stats_target3.csv deleted file mode 100644 index 4fd4152ef..000000000 --- a/out/stats_target3.csv +++ /dev/null @@ -1,2 +0,0 @@ -time,cov -0.04,1 diff --git a/out/stats_target4.csv b/out/stats_target4.csv deleted file mode 100644 index 0318d3373..000000000 --- a/out/stats_target4.csv +++ /dev/null @@ -1,2 +0,0 @@ -time,cov -0.04,2 diff --git a/out/stats_target5.csv b/out/stats_target5.csv deleted file mode 100644 index 5bb761d72..000000000 --- a/out/stats_target5.csv +++ /dev/null @@ -1,2 +0,0 @@ -time,cov -0.01,3 diff --git a/out/stats_target6.csv b/out/stats_target6.csv deleted file mode 100644 index 163a8ee7c..000000000 --- a/out/stats_target6.csv +++ /dev/null @@ -1,2 +0,0 @@ -time,cov -0.08,3 diff --git a/out/stats_target7.csv b/out/stats_target7.csv deleted file mode 100644 index 95e0d0ed2..000000000 --- a/out/stats_target7.csv +++ /dev/null @@ -1,3 +0,0 @@ -time,cov -0.04,3 -0.22,4 diff --git a/out/stats_target8.csv b/out/stats_target8.csv deleted file mode 100644 index 8356c53e0..000000000 --- a/out/stats_target8.csv +++ /dev/null @@ -1,2 +0,0 @@ -time,cov -0.03,3 diff --git a/out/stats_target9.csv b/out/stats_target9.csv deleted file mode 100644 index 58765e1c6..000000000 --- a/out/stats_target9.csv +++ /dev/null @@ -1,2 +0,0 @@ -time,cov -0.05,3 diff --git a/run_fuzz_task.sh b/run_fuzz_task.sh deleted file mode 100644 index afcfdd12d..000000000 --- a/run_fuzz_task.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -mkdir -p /app/out - -echo "=== 开始 10 目标自动化 Fuzzing 任务 ===" - -for i in {1..10} -do - TARGET_SRC="/app/targets/target$i.c" - TARGET_BIN="/app/targets/target$i" - - echo "[+] 正在处理目标 $i: $TARGET_SRC" - - # 编译 - afl-cc $TARGET_SRC -o $TARGET_BIN > /dev/null 2>&1 - - # 运行 Fuzzer (每个目标跑 30 秒) - python3 fuzzer/main.py $TARGET_BIN -done - -# 生成汇总报告 -echo "=== 任务结束,正在生成可视化图表 ===" -python3 fuzzer/analyze.py \ No newline at end of file diff --git a/seeds/seed.pcap b/seeds/seed.pcap new file mode 100644 index 000000000..8d981b287 Binary files /dev/null and b/seeds/seed.pcap differ diff --git a/seeds/seed.xml b/seeds/seed.xml new file mode 100644 index 000000000..d80a5e273 --- /dev/null +++ b/seeds/seed.xml @@ -0,0 +1 @@ + diff --git a/targets/target1 b/targets/target1 index d397a5c52..e653d7968 100644 Binary files a/targets/target1 and b/targets/target1 differ diff --git a/targets/target1.c b/targets/target1.c deleted file mode 100644 index b7fcef6e7..000000000 --- a/targets/target1.c +++ /dev/null @@ -1,5 +0,0 @@ -#include -#include -#include -#include -int main(){ char buf[100]; memset(buf,0,100); int len=read(0,buf,100); if(buf[0]=="c"&&buf[1]=="a") abort(); return 0; } \ No newline at end of file diff --git a/targets/target10 b/targets/target10 index 2b3457aaf..bf4b94cf6 100644 Binary files a/targets/target10 and b/targets/target10 differ diff --git a/targets/target10.c b/targets/target10.c deleted file mode 100644 index 0be371aa9..000000000 --- a/targets/target10.c +++ /dev/null @@ -1,5 +0,0 @@ -#include -#include -#include -#include -int main(){ char buf[100]; memset(buf,0,100); int len=read(0,buf,100); if(buf[0]=="z") exit(66); return 0; } \ No newline at end of file diff --git a/targets/target2 b/targets/target2 index 8a9b4a657..5e3f46aea 100644 Binary files a/targets/target2 and b/targets/target2 differ diff --git a/targets/target2.c b/targets/target2.c deleted file mode 100644 index 2d6160068..000000000 --- a/targets/target2.c +++ /dev/null @@ -1,5 +0,0 @@ -#include -#include -#include -#include -int main(){ char buf[100]; memset(buf,0,100); int len=read(0,buf,100); char d[5]; if(buf[0]=="b") strcpy(d, buf); return 0; } \ No newline at end of file diff --git a/targets/target3 b/targets/target3 index 5a4be210b..10ea86546 100644 Binary files a/targets/target3 and b/targets/target3 differ diff --git a/targets/target3.c b/targets/target3.c deleted file mode 100644 index e74412ded..000000000 --- a/targets/target3.c +++ /dev/null @@ -1,5 +0,0 @@ -#include -#include -#include -#include -int main(){ char buf[100]; memset(buf,0,100); int len=read(0,buf,100); int n=buf[0]; if(n>100 && n<110) *(int*)0=0; return 0; } \ No newline at end of file diff --git a/targets/target4 b/targets/target4 index 70ae1a5d2..04177001c 100644 Binary files a/targets/target4 and b/targets/target4 differ diff --git a/targets/target4.c b/targets/target4.c deleted file mode 100644 index 6db380b4f..000000000 --- a/targets/target4.c +++ /dev/null @@ -1,5 +0,0 @@ -#include -#include -#include -#include -int main(){ char buf[100]; memset(buf,0,100); int len=read(0,buf,100); if(buf[0]==0xff && buf[1]==0x00) abort(); return 0; } \ No newline at end of file diff --git a/targets/target5 b/targets/target5 index 099da9269..0fc4bb9fb 100644 Binary files a/targets/target5 and b/targets/target5 differ diff --git a/targets/target5.c b/targets/target5.c deleted file mode 100644 index 6379ca347..000000000 --- a/targets/target5.c +++ /dev/null @@ -1,5 +0,0 @@ -#include -#include -#include -#include -int main(){ char buf[100]; memset(buf,0,100); int len=read(0,buf,100); if(len > 10 && buf[len-1]=="!") abort(); return 0; } \ No newline at end of file diff --git a/targets/target6 b/targets/target6 index 0b2887c19..8b22f80ad 100644 Binary files a/targets/target6 and b/targets/target6 differ diff --git a/targets/target6.c b/targets/target6.c deleted file mode 100644 index edaf53eb6..000000000 --- a/targets/target6.c +++ /dev/null @@ -1,5 +0,0 @@ -#include -#include -#include -#include -int main(){ char buf[100]; memset(buf,0,100); int len=read(0,buf,100); if(buf[0]=="A") { if(buf[1]=="B") abort(); } return 0; } \ No newline at end of file diff --git a/targets/target7 b/targets/target7 index 40dd0c571..8dddd3e62 100644 Binary files a/targets/target7 and b/targets/target7 differ diff --git a/targets/target7.c b/targets/target7.c deleted file mode 100644 index 65ffdaa1b..000000000 --- a/targets/target7.c +++ /dev/null @@ -1,5 +0,0 @@ -#include -#include -#include -#include -int main(){ char buf[100]; memset(buf,0,100); int len=read(0,buf,100); if(buf[0]+buf[1] == 200) abort(); return 0; } \ No newline at end of file diff --git a/targets/target8 b/targets/target8 index df7bfab1b..ec587c505 100644 Binary files a/targets/target8 and b/targets/target8 differ diff --git a/targets/target8.c b/targets/target8.c deleted file mode 100644 index 707232926..000000000 --- a/targets/target8.c +++ /dev/null @@ -1,5 +0,0 @@ -#include -#include -#include -#include -int main(){ char buf[100]; memset(buf,0,100); int len=read(0,buf,100); if(strstr(buf, "BUG")) abort(); return 0; } \ No newline at end of file diff --git a/targets/target9 b/targets/target9 index ed4395db7..e4de8463c 100644 Binary files a/targets/target9 and b/targets/target9 differ diff --git a/targets/target9.c b/targets/target9.c deleted file mode 100644 index d032719ea..000000000 --- a/targets/target9.c +++ /dev/null @@ -1,5 +0,0 @@ -#include -#include -#include -#include -int main(){ char buf[100]; memset(buf,0,100); int len=read(0,buf,100); if(len==5 && buf[4]=="x") abort(); return 0; } \ No newline at end of file