Initial commit - 611 cybersecurity skills across all subdomains

This commit is contained in:
mukul975
2026-02-25 10:47:44 +01:00
commit 22a7ab1462
1765 changed files with 280648 additions and 0 deletions
@@ -0,0 +1,264 @@
---
name: implementing-fuzz-testing-in-cicd-with-aflplusplus
description: Integrate AFL++ coverage-guided fuzz testing into CI/CD pipelines to discover memory corruption, input handling, and logic vulnerabilities in C/C++ and compiled applications.
domain: cybersecurity
subdomain: devsecops
tags: [aflplusplus, fuzz-testing, cicd, coverage-guided-fuzzing, security-testing, vulnerability-discovery, afl]
version: "1.0"
author: mahipal
license: MIT
---
# Implementing Fuzz Testing in CI/CD with AFL++
## Overview
AFL++ (American Fuzzy Lop Plus Plus) is a community-maintained fork of AFL that provides state-of-the-art coverage-guided fuzz testing for discovering vulnerabilities in compiled applications. AFL++ uses genetic algorithms to mutate inputs, tracking code coverage to find new execution paths that trigger crashes, hangs, and undefined behavior. In CI/CD environments, AFL++ can be integrated to continuously test parsers, protocol handlers, file format processors, and any code that handles untrusted input. AFL++ supports persistent mode for high-speed fuzzing (up to 100,000+ executions per second), custom mutators, QEMU mode for binary-only fuzzing, and CmpLog/RedQueen for automatic dictionary extraction.
## Prerequisites
- Linux-based CI runners (AFL++ does not support Windows natively)
- GCC or Clang compiler toolchain
- AFL++ installed (`apt install aflplusplus` or built from source)
- Target application with harness functions isolating input processing
- Seed corpus of valid input samples
## Core Concepts
### Coverage-Guided Fuzzing
AFL++ instruments the target binary at compile time (or via QEMU/Frida for binary-only targets) to track which code paths each input exercises. When a mutated input triggers a new code path, it is saved to the corpus for further mutation. This feedback loop enables AFL++ to systematically explore program state space.
### Instrumentation Modes
| Mode | Use Case | Performance |
|------|----------|-------------|
| `afl-clang-fast` (LTO) | Source available, best performance | Highest |
| `afl-clang-fast` | Source available, standard | High |
| `afl-gcc-fast` | GCC-based projects | High |
| `QEMU mode` | Binary-only, no source | Medium |
| `Frida mode` | Binary-only, cross-platform | Medium |
| `Unicorn mode` | Firmware, embedded | Low |
### Persistent Mode
Persistent mode avoids fork overhead by fuzzing within a loop:
```c
#include <unistd.h>
__AFL_FUZZ_INIT();
int main() {
__AFL_INIT();
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
while (__AFL_LOOP(10000)) {
int len = __AFL_FUZZ_TESTCASE_LEN;
// Process buf[0..len-1]
parse_input(buf, len);
}
return 0;
}
```
## Implementation Steps
### Step 1 --- Build the Fuzzing Harness
Create a harness that feeds AFL++ input to the target function:
```c
// fuzz_harness.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "target_parser.h"
__AFL_FUZZ_INIT();
int main() {
__AFL_INIT();
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
while (__AFL_LOOP(10000)) {
int len = __AFL_FUZZ_TESTCASE_LEN;
if (len < 4) continue;
// Reset state between iterations
parser_context_t ctx;
parser_init(&ctx);
parser_process(&ctx, buf, len);
parser_cleanup(&ctx);
}
return 0;
}
```
### Step 2 --- Compile with AFL++ Instrumentation
```bash
# Standard instrumentation
export CC=afl-clang-fast
export CXX=afl-clang-fast++
# Enable AddressSanitizer for better crash detection
export AFL_USE_ASAN=1
# Build the target with instrumentation
$CC -o fuzz_harness fuzz_harness.c -ltarget_parser -fsanitize=address
# Build a CmpLog binary for better coverage
$CC -o fuzz_harness_cmplog fuzz_harness.c -ltarget_parser \
-fsanitize=address -DCMPLOG
```
### Step 3 --- Prepare Seed Corpus
```bash
mkdir -p corpus/
# Add valid input samples
cp test_inputs/* corpus/
# Minimize the corpus
afl-cmin -i corpus/ -o corpus_min/ -- ./fuzz_harness @@
# Further minimize individual inputs
mkdir -p corpus_tmin/
for f in corpus_min/*; do
afl-tmin -i "$f" -o "corpus_tmin/$(basename $f)" -- ./fuzz_harness @@
done
```
### Step 4 --- Configure CI/CD Integration
**GitHub Actions:**
```yaml
name: Fuzz Testing
on:
push:
branches: [main]
schedule:
- cron: '0 2 * * *' # Nightly fuzzing
jobs:
fuzz:
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Install AFL++
run: |
sudo apt-get update
sudo apt-get install -y aflplusplus
- name: Restore corpus cache
uses: actions/cache@v4
with:
path: corpus/
key: fuzz-corpus-${{ github.sha }}
restore-keys: fuzz-corpus-
- name: Build fuzzing harness
run: |
export CC=afl-clang-fast
export AFL_USE_ASAN=1
make fuzz_harness
- name: Run AFL++ fuzzing (CI mode)
env:
AFL_CMPLOG_ONLY_NEW: 1
AFL_FAST_CAL: 1
AFL_NO_STARTUP_CALIBRATION: 1
run: |
mkdir -p findings/
timeout 7200 afl-fuzz \
-S ci_fuzzer \
-i corpus/ \
-o findings/ \
-t 5000 \
-- ./fuzz_harness @@ || true
- name: Check for crashes
run: |
CRASHES=$(find findings/ -path "*/crashes/*" -not -name "README.txt" | wc -l)
echo "Found $CRASHES unique crashes"
if [ "$CRASHES" -gt 0 ]; then
echo "::error::AFL++ found $CRASHES crashes"
for crash in findings/*/crashes/*; do
[ -f "$crash" ] && echo "Crash: $crash ($(wc -c < $crash) bytes)"
done
exit 1
fi
- name: Update corpus cache
if: always()
run: |
afl-cmin -i findings/ci_fuzzer/queue/ -o corpus/ -- ./fuzz_harness @@
```
### Step 5 --- Parallel Fuzzing for Nightly Runs
```bash
# Launch multiple secondary instances for better coverage
for i in $(seq 1 $(nproc)); do
afl-fuzz -S fuzzer_$i \
-i corpus/ \
-o findings/ \
-- ./fuzz_harness @@ &
done
# Wait for all fuzzers
wait
# Merge and minimize corpus
afl-cmin -i findings/*/queue/ -o corpus_merged/ -- ./fuzz_harness @@
```
### Step 6 --- Crash Triage
```bash
# Reproduce and categorize crashes
for crash in findings/*/crashes/*; do
echo "=== Testing: $crash ==="
timeout 5 ./fuzz_harness_asan "$crash" 2>&1 | head -20
echo "---"
done
# Deduplicate crashes by stack trace
afl-collect findings/ crashes_deduped/ -- ./fuzz_harness @@
```
## CI/CD Best Practices for AFL++
| Setting | CI Short Run | Nightly Long Run |
|---------|-------------|-----------------|
| Duration | 30-60 min | 4-24 hours |
| Mode | `-S` (secondary only) | `-S` (no `-M` for CI) |
| `AFL_CMPLOG_ONLY_NEW` | 1 | 1 |
| `AFL_FAST_CAL` | 1 | 0 |
| `AFL_NO_STARTUP_CALIBRATION` | 1 | 0 |
| Corpus caching | Required | Required |
| Parallel instances | 1-2 | nproc |
## Monitoring Fuzzing Campaigns
```bash
# View fuzzing statistics
afl-whatsup findings/
# Key metrics to track:
# - Total paths found (code coverage indicator)
# - Unique crashes / unique hangs
# - Stability percentage (should be >90%)
# - Exec speed (execs/sec)
# - Cycles done (full corpus cycles completed)
```
## References
- [AFL++ Documentation](https://aflplus.plus/docs/)
- [AFL++ GitHub Repository](https://github.com/AFLplusplus/AFLplusplus)
- [AFL++ Fuzzing in Depth Guide](https://aflplus.plus/docs/fuzzing_in_depth/)
- [Google Testing Handbook - AFL++](https://appsec.guide/docs/fuzzing/c-cpp/aflpp/)
- [OWASP Fuzzing Guide](https://owasp.org/www-community/Fuzzing)
@@ -0,0 +1,36 @@
# Fuzz Testing Implementation Template
## Target Application
| Field | Value |
|-------|-------|
| Application Name | |
| Target Function | |
| Language | [ ] C [ ] C++ [ ] Other |
| Input Type | [ ] File [ ] Network [ ] Stdin |
## Fuzzing Configuration
| Parameter | Value |
|-----------|-------|
| Instrumentation | [ ] afl-clang-fast [ ] afl-gcc-fast [ ] QEMU |
| Sanitizer | [ ] ASan [ ] UBSan [ ] MSan [ ] TSan |
| Mode | [ ] Persistent [ ] Fork |
| CmpLog | [ ] Enabled [ ] Disabled |
| Timeout per exec | ms |
| CI run duration | minutes |
| Nightly duration | hours |
## Corpus Management
| Item | Location |
|------|----------|
| Seed corpus | |
| Minimized corpus | |
| CI cache key | |
## Crash Tracking
| Crash ID | CWE | Severity | Crash File | Stack Trace Summary | Fix Status |
|----------|-----|----------|------------|---------------------|------------|
| | | | | | |
@@ -0,0 +1,36 @@
# Standards Reference for Fuzz Testing
## NIST SP 800-53 Rev 5 Controls
| Control | Description | Fuzzing Alignment |
|---------|-------------|-------------------|
| SA-11(5) | Penetration Testing | Fuzz testing discovers vulnerabilities through automated input mutation |
| SA-11(8) | Dynamic Code Analysis | AFL++ provides runtime analysis with instrumented binaries |
| SI-10 | Information Input Validation | Fuzzing validates input handling robustness |
| SI-17 | Fail-Safe Procedures | Crash detection ensures failures are handled safely |
## OWASP Testing Guide v4.2
- **WSTG-INPV-07**: Testing for Input Validation --- AFL++ systematically tests boundary conditions
- **WSTG-ERRH-01**: Error Handling --- Crash analysis reveals improper error handling
## CWE Categories Commonly Found by Fuzzing
| CWE | Name | AFL++ Detection Method |
|-----|------|----------------------|
| CWE-120 | Buffer Overflow | ASan crash on out-of-bounds write |
| CWE-125 | Out-of-Bounds Read | ASan crash on invalid read |
| CWE-416 | Use After Free | ASan detects freed memory access |
| CWE-476 | NULL Pointer Dereference | SIGSEGV on null deref |
| CWE-190 | Integer Overflow | UBSan detects arithmetic overflow |
| CWE-787 | Out-of-Bounds Write | ASan detects heap/stack buffer overflow |
| CWE-400 | Uncontrolled Resource Consumption | Timeout detection for hangs |
## Fuzzing Maturity Levels
| Level | Description | CI Integration |
|-------|-------------|----------------|
| 1 Basic | Manual ad-hoc fuzzing | None |
| 2 Structured | Harness-based with corpus management | PR-triggered short runs |
| 3 Continuous | Nightly campaigns with crash tracking | Nightly + corpus caching |
| 4 Optimized | Multi-tool (AFL++, libFuzzer), crash dedup, coverage tracking | Full CI/CD integration with gating |
@@ -0,0 +1,64 @@
# AFL++ Fuzz Testing Workflows
## Workflow 1: CI Pipeline Integration
```
Code pushed to branch
|
Fuzzing harness compiled with afl-clang-fast + ASan
|
Corpus restored from CI cache
|
AFL++ runs in secondary mode for fixed duration
|
[No crashes] --> Corpus updated in cache, pipeline passes
[Crashes found] --> Pipeline fails, crash artifacts uploaded
|
Developer triages crashes
|
Fix applied, re-run confirms no regression
```
## Workflow 2: Nightly Fuzzing Campaign
```
Scheduled nightly trigger (cron)
|
Build instrumented binary + CmpLog binary
|
Restore merged corpus from last run
|
Launch parallel AFL++ instances (nproc count)
|
Run for 4-8 hours
|
Collect results from all instances
|
afl-cmin merges and minimizes corpus
|
Deduplicate crashes by stack hash
|
New crashes create Jira/GitHub issues automatically
|
Updated corpus cached for next run
```
## Workflow 3: Crash Triage and Fix
```
Crash file identified in findings/
|
Reproduce crash with ASan-instrumented binary
|
Capture ASan stack trace and error type
|
Minimize crash input with afl-tmin
|
Identify root cause from stack trace
|
Develop fix and add crash input as regression test
|
Verify fix by re-running AFL++ with crash input
|
Update corpus to include edge case inputs
```
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""
AFL++ Fuzzing Results Analyzer
Parses AFL++ output directories and generates reports on
crash findings, corpus growth, and coverage statistics.
"""
import json
import os
import sys
from datetime import datetime
from pathlib import Path
from collections import defaultdict
def parse_fuzzer_stats(stats_file: str) -> dict:
stats = {}
if not os.path.exists(stats_file):
return stats
with open(stats_file) as f:
for line in f:
line = line.strip()
if ":" in line:
key, value = line.split(":", 1)
stats[key.strip()] = value.strip()
return stats
def count_files_in_dir(directory: str) -> int:
if not os.path.isdir(directory):
return 0
return len([f for f in os.listdir(directory) if f != "README.txt" and os.path.isfile(os.path.join(directory, f))])
def analyze_fuzzer_instance(instance_dir: str) -> dict:
name = os.path.basename(instance_dir)
stats = parse_fuzzer_stats(os.path.join(instance_dir, "fuzzer_stats"))
return {
"name": name,
"start_time": stats.get("start_time", ""),
"last_update": stats.get("last_update", ""),
"execs_done": int(stats.get("execs_done", 0)),
"execs_per_sec": float(stats.get("execs_per_sec", 0)),
"corpus_count": count_files_in_dir(os.path.join(instance_dir, "queue")),
"crashes_total": count_files_in_dir(os.path.join(instance_dir, "crashes")),
"hangs_total": count_files_in_dir(os.path.join(instance_dir, "hangs")),
"paths_total": int(stats.get("paths_total", 0)),
"paths_found": int(stats.get("paths_found", 0)),
"stability": stats.get("stability", ""),
"cycles_done": int(stats.get("cycles_done", 0)),
"bitmap_cvg": stats.get("bitmap_cvg", ""),
"command_line": stats.get("command_line", ""),
}
def collect_crash_info(instance_dir: str) -> list:
crashes_dir = os.path.join(instance_dir, "crashes")
crashes = []
if not os.path.isdir(crashes_dir):
return crashes
for fname in sorted(os.listdir(crashes_dir)):
if fname == "README.txt":
continue
fpath = os.path.join(crashes_dir, fname)
if os.path.isfile(fpath):
crashes.append({
"file": fname,
"path": fpath,
"size": os.path.getsize(fpath),
"instance": os.path.basename(instance_dir),
})
return crashes
def analyze_campaign(findings_dir: str) -> dict:
report = {
"findings_dir": findings_dir,
"analyzed_at": datetime.utcnow().isoformat() + "Z",
"instances": [],
"total_execs": 0,
"total_crashes": 0,
"total_hangs": 0,
"total_corpus": 0,
"all_crashes": [],
"avg_execs_per_sec": 0,
}
instance_dirs = []
for entry in sorted(os.listdir(findings_dir)):
full_path = os.path.join(findings_dir, entry)
if os.path.isdir(full_path) and os.path.exists(os.path.join(full_path, "fuzzer_stats")):
instance_dirs.append(full_path)
if not instance_dirs:
print(f"No fuzzer instances found in {findings_dir}")
return report
exec_speeds = []
for inst_dir in instance_dirs:
inst = analyze_fuzzer_instance(inst_dir)
report["instances"].append(inst)
report["total_execs"] += inst["execs_done"]
report["total_crashes"] += inst["crashes_total"]
report["total_hangs"] += inst["hangs_total"]
report["total_corpus"] += inst["corpus_count"]
if inst["execs_per_sec"] > 0:
exec_speeds.append(inst["execs_per_sec"])
crashes = collect_crash_info(inst_dir)
report["all_crashes"].extend(crashes)
if exec_speeds:
report["avg_execs_per_sec"] = round(sum(exec_speeds) / len(exec_speeds), 1)
return report
def print_report(report: dict) -> None:
print(f"\n{'='*60}")
print(f"AFL++ Fuzzing Campaign Report")
print(f"{'='*60}")
print(f"Findings directory: {report['findings_dir']}")
print(f"Analyzed at: {report['analyzed_at']}")
print(f"Fuzzer instances: {len(report['instances'])}")
print(f"\nAggregate Statistics:")
print(f" Total executions: {report['total_execs']:,}")
print(f" Avg exec/sec: {report['avg_execs_per_sec']:,.1f}")
print(f" Total corpus entries: {report['total_corpus']}")
print(f" Total unique crashes: {report['total_crashes']}")
print(f" Total hangs: {report['total_hangs']}")
print(f"\nInstance Details:")
for inst in report["instances"]:
print(f" {inst['name']:20s} | Execs: {inst['execs_done']:>12,} | "
f"Speed: {inst['execs_per_sec']:>8.1f}/s | "
f"Crashes: {inst['crashes_total']:3d} | "
f"Corpus: {inst['corpus_count']:5d} | "
f"Cycles: {inst['cycles_done']}")
if report["all_crashes"]:
print(f"\nCrash Files ({len(report['all_crashes'])} total):")
for crash in report["all_crashes"][:20]:
print(f" [{crash['instance']}] {crash['file']} ({crash['size']} bytes)")
if len(report["all_crashes"]) > 20:
print(f" ... and {len(report['all_crashes']) - 20} more")
verdict = "PASS" if report["total_crashes"] == 0 else "FAIL"
print(f"\nCI Verdict: {verdict}")
def main():
if len(sys.argv) < 2:
print("Usage: python process.py <findings_directory>")
sys.exit(1)
findings_dir = sys.argv[1]
if not os.path.isdir(findings_dir):
print(f"Directory not found: {findings_dir}")
sys.exit(1)
report = analyze_campaign(findings_dir)
print_report(report)
output = os.path.join(findings_dir, "campaign_report.json")
with open(output, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"\nReport saved to: {output}")
sys.exit(1 if report["total_crashes"] > 0 else 0)
if __name__ == "__main__":
main()