Skip to content

Commit 311714a

Browse files
digitalandrewclaude
andcommitted
Add desock library for network daemon fuzzing via AFL++
Cross-compiled desock.so (arm, aarch64, mips, mipsel) intercepts socket/bind/listen/accept and redirects network I/O to stdin/stdout, enabling AFL++ to fuzz network daemons that normally read from connections. Injected via AFL_PRELOAD when desock:true is passed to start_fuzzing_campaign. - fuzzing/desock/desock.c: minimal LD_PRELOAD library (~160 lines) - fuzzing/Dockerfile: cross-compilers + compile desock for 4 archs - fuzzing_service.py: AFL_PRELOAD injection with DESOCK_LIB_MAP - fuzzing.py tools: desock param on start_fuzzing_campaign, generate_fuzzing_harness recommends desock for daemon binaries, new diagnose_fuzzing_campaign tool for agent self-service debugging Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d50c836 commit 311714a

5 files changed

Lines changed: 455 additions & 19 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ The server uses a mutable `ProjectState` dataclass so all project context (proje
174174
| Security | `tools/security.py` | `check_known_cves`, `analyze_config_security`, `check_setuid_binaries`, `analyze_init_scripts`, `check_filesystem_permissions`, `analyze_certificate` |
175175
| SBOM | `tools/sbom.py` | `generate_sbom`, `get_sbom_components`, `check_component_cves`, `run_vulnerability_scan` |
176176
| Emulation | `tools/emulation.py` | `start_emulation`, `run_command_in_emulation`, `stop_emulation`, `check_emulation_status`, `get_emulation_logs`, `enumerate_emulation_services`, `diagnose_emulation_environment`, `troubleshoot_emulation`, `get_crash_dump`, `run_gdb_command`, `save_emulation_preset`, `list_emulation_presets`, `start_emulation_from_preset` |
177-
| Fuzzing | `tools/fuzzing.py` | `analyze_fuzzing_target`, `generate_fuzzing_dictionary`, `generate_seed_corpus`, `generate_fuzzing_harness`, `start_fuzzing_campaign`, `check_fuzzing_status`, `stop_fuzzing_campaign`, `triage_fuzzing_crash` |
177+
| Fuzzing | `tools/fuzzing.py` | `analyze_fuzzing_target`, `generate_fuzzing_dictionary`, `generate_seed_corpus`, `generate_fuzzing_harness`, `start_fuzzing_campaign`, `check_fuzzing_status`, `stop_fuzzing_campaign`, `triage_fuzzing_crash`, `diagnose_fuzzing_campaign` |
178178
| Comparison | `tools/comparison.py` | `list_firmware_versions`, `diff_firmware`, `diff_binary`, `diff_decompilation` |
179179
| UART | `tools/uart.py` | `uart_connect`, `uart_send_command`, `uart_read`, `uart_send_break`, `uart_send_raw`, `uart_disconnect`, `uart_status`, `uart_get_transcript` |
180180
| Reporting | `tools/reporting.py` | `add_finding`, `list_findings`, `update_finding`, `read_project_instructions`, `list_project_documents`, `read_project_document` |

backend/app/ai/tools/fuzzing.py

Lines changed: 259 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,17 @@ def register_fuzzing_tools(registry: ToolRegistry) -> None:
147147
"that need environment variable setup before execution."
148148
),
149149
},
150+
"desock": {
151+
"type": "boolean",
152+
"description": (
153+
"Enable desocketing for network daemon binaries. When true, "
154+
"intercepts socket/bind/listen/accept calls and redirects "
155+
"network I/O to stdin/stdout, allowing AFL++ to fuzz daemon "
156+
"binaries that normally read from network connections. Use "
157+
"this for binaries identified as 'network' strategy by "
158+
"analyze_fuzzing_target."
159+
),
160+
},
150161
},
151162
"required": ["binary_path"],
152163
},
@@ -254,6 +265,29 @@ def register_fuzzing_tools(registry: ToolRegistry) -> None:
254265
handler=_handle_triage_crash,
255266
)
256267

268+
registry.register(
269+
name="diagnose_fuzzing_campaign",
270+
description=(
271+
"Diagnose a fuzzing campaign that may not be performing well. "
272+
"Checks campaign status, reads AFL++ logs for startup errors, "
273+
"analyzes coverage for stalls, and provides actionable recommendations "
274+
"(e.g., enable desock for network daemons, increase timeout for hangs). "
275+
"Use this when coverage is flat, execs/sec is zero, or the campaign "
276+
"seems stuck."
277+
),
278+
input_schema={
279+
"type": "object",
280+
"properties": {
281+
"campaign_id": {
282+
"type": "string",
283+
"description": "The campaign ID to diagnose",
284+
},
285+
},
286+
"required": ["campaign_id"],
287+
},
288+
handler=_handle_diagnose_campaign,
289+
)
290+
257291

258292
# ---------------------------------------------------------------------------
259293
# Tool handlers
@@ -555,32 +589,29 @@ async def _handle_generate_harness(input: dict, context: ToolContext) -> str:
555589
"harness_script": harness,
556590
}
557591
else:
558-
lines.append("Strategy: NETWORK fuzzing (daemon-style — limited support)")
592+
lines.append("Strategy: NETWORK fuzzing (daemon-style with desock)")
559593
lines.append("")
560594
lines.append(
561-
"This binary appears to be a network daemon (uses socket/bind/listen/accept). "
562-
"Direct daemon fuzzing is NOT supported in QEMU user mode because the "
563-
"binary forks and listens on a socket rather than reading from stdin."
595+
"This binary is a network daemon (uses socket/bind/listen/accept). "
596+
"The desock library will intercept socket calls and redirect network "
597+
"I/O to stdin/stdout, allowing AFL++ to fuzz the network parsing "
598+
"code directly."
564599
)
565600
lines.append("")
566-
lines.append("Alternative approaches:")
567-
lines.append(" 1. **Stdin redirection**: If the binary has a CLI/debug mode that ")
568-
lines.append(" reads from stdin, use stdin strategy instead.")
569-
lines.append(" 2. **LD_PRELOAD desocketing**: Use a preload library (e.g., ")
570-
lines.append(" preeny's desock.so) to redirect socket I/O to stdin/stdout. ")
571-
lines.append(" Set environment: {\"LD_PRELOAD\": \"/path/to/desock.so\"}")
572-
lines.append(" 3. **CGI handler fuzzing**: If the daemon delegates to CGI handlers, ")
573-
lines.append(" fuzz those handlers directly with the CGI harness approach.")
574-
lines.append(" 4. **Function-level fuzzing**: Write a custom harness that calls ")
575-
lines.append(" specific parsing functions from the binary's shared libraries.")
576-
lines.append("")
577-
lines.append("If you want to attempt stdin redirection anyway:")
601+
lines.append("Recommended start_fuzzing_campaign parameters:")
578602
lines.append(f' binary_path: "{binary_path}"')
579-
lines.append(" (AFL++ will pipe fuzz input to stdin — may not reach network code)")
603+
lines.append(" desock: true")
604+
lines.append(" (AFL++ will pipe fuzz data through the desocketed accept() connection)")
605+
lines.append("")
606+
lines.append("Notes:")
607+
lines.append("- The daemon's accept() loop will receive one connection with AFL++ fuzz data")
608+
lines.append("- Some daemons may need arguments to skip daemonization (e.g., -f for foreground)")
609+
lines.append("- If coverage is flat, the daemon may fork after accept() — try adding")
610+
lines.append(' environment: {"AFL_NO_FORKSRV": "1"}')
580611

581612
campaign_params = {
582613
"binary_path": binary_path,
583-
"_note": "Daemon-style binary — stdin fuzzing may have limited coverage",
614+
"desock": True,
584615
}
585616

586617
lines.append("")
@@ -688,6 +719,8 @@ async def _handle_start_campaign(input: dict, context: ToolContext) -> str:
688719
config["environment"] = input["environment"]
689720
if "harness_script" in input:
690721
config["harness_script"] = input["harness_script"]
722+
if "desock" in input:
723+
config["desock"] = input["desock"]
691724

692725
svc = FuzzingService(context.db)
693726
try:
@@ -839,3 +872,211 @@ async def _handle_triage_crash(input: dict, context: ToolContext) -> str:
839872
)
840873

841874
return "\n".join(lines)
875+
876+
877+
async def _handle_diagnose_campaign(input: dict, context: ToolContext) -> str:
878+
"""Diagnose a fuzzing campaign for performance issues."""
879+
campaign_id = input.get("campaign_id")
880+
if not campaign_id:
881+
return "Error: campaign_id is required."
882+
883+
from uuid import UUID
884+
import docker
885+
import docker.errors
886+
887+
svc = FuzzingService(context.db)
888+
try:
889+
campaign = await svc.get_campaign_status(UUID(campaign_id), context.project_id)
890+
except ValueError as exc:
891+
return f"Error: {exc}"
892+
893+
config = campaign.config or {}
894+
stats = campaign.stats or {}
895+
lines: list[str] = [
896+
f"Fuzzing Campaign Diagnostics: {campaign.id}",
897+
f" Binary: {campaign.binary_path}",
898+
f" Status: {campaign.status}",
899+
f" Desock: {'enabled' if config.get('desock') else 'disabled'}",
900+
"",
901+
]
902+
903+
issues: list[str] = []
904+
recommendations: list[str] = []
905+
906+
# --- Check 1: Campaign status ---
907+
if campaign.status == "error":
908+
lines.append(f" ERROR: {campaign.error_message or 'unknown error'}")
909+
issues.append("Campaign is in error state")
910+
recommendations.append(
911+
"Check the error message above. The campaign may need to be "
912+
"recreated with different parameters."
913+
)
914+
915+
if campaign.status in ("stopped", "completed"):
916+
lines.append(" Campaign is not running.")
917+
issues.append("Campaign is stopped — no live data available")
918+
919+
# --- Check 2: AFL++ log from container ---
920+
afl_log = ""
921+
afl_process_running = False
922+
if campaign.container_id and campaign.status == "running":
923+
try:
924+
client = docker.from_env()
925+
container = client.containers.get(campaign.container_id)
926+
927+
# Read AFL++ log
928+
log_result = container.exec_run(
929+
["tail", "-100", "/opt/fuzzing/afl.log"]
930+
)
931+
if log_result.exit_code == 0:
932+
afl_log = log_result.output.decode("utf-8", errors="replace")
933+
934+
# Check if AFL++ process is running
935+
ps_result = container.exec_run(["sh", "-c", "pgrep -f afl-fuzz"])
936+
afl_process_running = ps_result.exit_code == 0
937+
938+
# Check if QEMU trace process is running
939+
qemu_result = container.exec_run(
940+
["sh", "-c", "pgrep -f afl-qemu-trace"]
941+
)
942+
qemu_running = qemu_result.exit_code == 0
943+
944+
lines.append(" Container: running")
945+
lines.append(
946+
f" AFL++ process: {'running' if afl_process_running else 'NOT running'}"
947+
)
948+
lines.append(
949+
f" QEMU trace: {'running' if qemu_running else 'NOT running'}"
950+
)
951+
952+
if not afl_process_running:
953+
issues.append("AFL++ process is not running in the container")
954+
recommendations.append(
955+
"AFL++ crashed or failed to start. Check the log output below "
956+
"for details. You may need to stop and restart with different settings."
957+
)
958+
959+
except docker.errors.NotFound:
960+
lines.append(" Container: NOT FOUND (may have been removed)")
961+
issues.append("Container no longer exists")
962+
except Exception as exc:
963+
lines.append(f" Container check failed: {exc}")
964+
965+
# --- Check 3: Coverage analysis ---
966+
total_execs = stats.get("total_execs", 0)
967+
execs_per_sec = stats.get("execs_per_sec", 0)
968+
bitmap_cvg = stats.get("bitmap_cvg", "N/A")
969+
saved_crashes = stats.get("saved_crashes", 0)
970+
saved_hangs = stats.get("saved_hangs", 0)
971+
corpus_count = stats.get("corpus_count", 0)
972+
973+
if stats:
974+
lines.append("")
975+
lines.append(" Live statistics:")
976+
lines.append(f" Execs/sec: {execs_per_sec}")
977+
lines.append(f" Total execs: {total_execs}")
978+
lines.append(f" Coverage: {bitmap_cvg}")
979+
lines.append(f" Corpus: {corpus_count}")
980+
lines.append(f" Crashes: {saved_crashes}")
981+
lines.append(f" Hangs: {saved_hangs}")
982+
983+
# Zero executions
984+
if campaign.status == "running" and total_execs == 0:
985+
issues.append("Zero executions — AFL++ may have failed to start")
986+
recommendations.append(
987+
"AFL++ produced no executions. Check the log below for startup errors. "
988+
"Common causes: binary not found, missing shared libraries (check "
989+
"QEMU_LD_PREFIX), or architecture mismatch."
990+
)
991+
992+
# Flat coverage detection (100 execs is enough to detect a problem)
993+
if total_execs > 100:
994+
cvg_str = str(bitmap_cvg).replace("%", "").strip()
995+
try:
996+
cvg_pct = float(cvg_str)
997+
if cvg_pct < 5.0:
998+
issues.append(
999+
f"Very low coverage ({bitmap_cvg}) after {total_execs} executions"
1000+
)
1001+
if not config.get("desock"):
1002+
# Check if this might be a network binary
1003+
fw_result = await context.db.execute(
1004+
select(Firmware).where(Firmware.id == campaign.firmware_id)
1005+
)
1006+
firmware = fw_result.scalar_one_or_none()
1007+
if firmware:
1008+
try:
1009+
analysis = await svc.analyze_target(
1010+
firmware, campaign.binary_path
1011+
)
1012+
if analysis.get("recommended_strategy") == "network":
1013+
recommendations.append(
1014+
"This binary is a NETWORK DAEMON but desock is disabled. "
1015+
"AFL++ fuzz data never reaches the network parsing code. "
1016+
"ACTION: Stop this campaign and restart with desock: true"
1017+
)
1018+
except Exception:
1019+
pass
1020+
if not recommendations:
1021+
recommendations.append(
1022+
"Coverage is very low. The binary may not be processing "
1023+
"AFL++ input effectively. Consider enabling desock (if it's "
1024+
"a network daemon) or using a harness script."
1025+
)
1026+
else:
1027+
recommendations.append(
1028+
"Coverage is very low even with desock. The daemon may fork "
1029+
"after accept() — try environment: {\"AFL_NO_FORKSRV\": \"1\"}. "
1030+
"Or the binary may exit before reading stdin."
1031+
)
1032+
except (ValueError, TypeError):
1033+
pass
1034+
1035+
# High hang count
1036+
if saved_hangs > 10 and saved_hangs > saved_crashes:
1037+
issues.append(f"High hang count ({saved_hangs}) — binary may be timing out")
1038+
recommendations.append(
1039+
f"Many hangs detected. Current timeout: "
1040+
f"{config.get('timeout_per_exec', 1000)}ms. "
1041+
"Try increasing timeout_per_exec (e.g., 5000 or 10000)."
1042+
)
1043+
1044+
# --- Check 4: AFL++ log output ---
1045+
if afl_log:
1046+
# Look for known error patterns
1047+
if "PROGRAM ABORT" in afl_log:
1048+
issues.append("AFL++ aborted — see log for details")
1049+
if "No instrumentation detected" in afl_log:
1050+
issues.append("No instrumentation — QEMU mode may not be working")
1051+
recommendations.append(
1052+
"AFL++ reports no instrumentation. Ensure the binary architecture "
1053+
"matches the QEMU trace binary."
1054+
)
1055+
if "can't find" in afl_log.lower() or "not found" in afl_log.lower():
1056+
issues.append("Binary or dependency not found")
1057+
recommendations.append(
1058+
"A file was not found. Check that binary_path is correct and "
1059+
"all shared libraries exist under the firmware root."
1060+
)
1061+
1062+
lines.append("")
1063+
lines.append(" AFL++ log (last lines):")
1064+
for log_line in afl_log.strip().split("\n")[-30:]:
1065+
lines.append(f" {log_line}")
1066+
1067+
# --- Summary ---
1068+
lines.append("")
1069+
if issues:
1070+
lines.append("ISSUES FOUND:")
1071+
for i, issue in enumerate(issues, 1):
1072+
lines.append(f" {i}. {issue}")
1073+
else:
1074+
lines.append("No issues detected — campaign appears healthy.")
1075+
1076+
if recommendations:
1077+
lines.append("")
1078+
lines.append("RECOMMENDATIONS:")
1079+
for i, rec in enumerate(recommendations, 1):
1080+
lines.append(f" {i}. {rec}")
1081+
1082+
return "\n".join(lines)

backend/app/services/fuzzing_service.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@
6868
"select", "poll", "epoll_wait",
6969
}
7070

71+
# Architecture → desock shared library path inside the fuzzing container
72+
DESOCK_LIB_MAP: dict[str, str] = {
73+
"arm": "/opt/desock/desock_arm.so",
74+
"aarch64": "/opt/desock/desock_aarch64.so",
75+
"mips": "/opt/desock/desock_mips.so",
76+
"mipsel": "/opt/desock/desock_mipsel.so",
77+
}
78+
7179

7280
class FuzzingService:
7381
"""Manages AFL++ fuzzing campaign lifecycle via Docker containers."""
@@ -453,6 +461,18 @@ async def start_campaign(self, campaign_id: UUID, project_id: UUID) -> FuzzingCa
453461
f"QEMU_LD_PREFIX=/firmware "
454462
)
455463

464+
# Inject desock library via AFL_PRELOAD to redirect socket
465+
# I/O to stdin/stdout for network daemon fuzzing
466+
desock = config.get("desock", False)
467+
if desock:
468+
desock_lib = DESOCK_LIB_MAP.get(arch)
469+
if desock_lib:
470+
afl_cmd += f"AFL_PRELOAD={desock_lib} "
471+
else:
472+
logger.warning(
473+
"Desock requested but no library for arch %s", arch
474+
)
475+
456476
if env_prefix:
457477
afl_cmd += f"{env_prefix} "
458478

fuzzing/Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ RUN apt-get update && apt-get install -y \
1717
libtool libtool-bin automake bison flex \
1818
libglib2.0-dev libpixman-1-dev \
1919
ninja-build meson pkg-config wget ca-certificates \
20+
gcc-mipsel-linux-gnu gcc-mips-linux-gnu \
21+
gcc-arm-linux-gnueabi gcc-aarch64-linux-gnu \
2022
&& rm -rf /var/lib/apt/lists/*
2123

2224
# Clone AFL++ stable branch
@@ -52,6 +54,16 @@ RUN cd qemu_mode && CPU_TARGET=aarch64 ./build_qemu_support.sh \
5254
RUN cd qemu_mode && CPU_TARGET=i386 ./build_qemu_support.sh \
5355
&& cp ../afl-qemu-trace /usr/local/bin/afl-qemu-trace-i386
5456

57+
# Cross-compile desock library for all target architectures.
58+
# This LD_PRELOAD library intercepts socket calls and redirects network I/O
59+
# to stdin/stdout, allowing AFL++ to fuzz network daemons directly.
60+
COPY desock/ /desock/
61+
RUN mkdir -p /opt/desock && \
62+
mipsel-linux-gnu-gcc -fPIC -shared -Wl,--hash-style=sysv -o /opt/desock/desock_mipsel.so /desock/desock.c && \
63+
mips-linux-gnu-gcc -fPIC -shared -Wl,--hash-style=sysv -o /opt/desock/desock_mips.so /desock/desock.c && \
64+
arm-linux-gnueabi-gcc -fPIC -shared -Wl,--hash-style=sysv -o /opt/desock/desock_arm.so /desock/desock.c && \
65+
aarch64-linux-gnu-gcc -fPIC -shared -Wl,--hash-style=sysv -o /opt/desock/desock_aarch64.so /desock/desock.c
66+
5567
# ---------------------------------------------------------------------------
5668
# Stage 2: Lean runtime image
5769
# ---------------------------------------------------------------------------
@@ -88,6 +100,9 @@ COPY --from=builder /usr/local/bin/afl-qemu-trace-mipsel /usr/local/bin/
88100
COPY --from=builder /usr/local/bin/afl-qemu-trace-aarch64 /usr/local/bin/
89101
COPY --from=builder /usr/local/bin/afl-qemu-trace-i386 /usr/local/bin/
90102

103+
# Copy cross-compiled desock libraries
104+
COPY --from=builder /opt/desock/ /opt/desock/
105+
91106
# Create working directories
92107
RUN mkdir -p /data/firmware /opt/fuzzing /opt/dictionaries
93108

0 commit comments

Comments
 (0)