* feat(plugin): add reverse-engineering plugin v0.4.0 + project-level codegraph
Bundles a multi-platform RE toolkit as a local marketplace plugin that
clone-and-enable installs in cc-haha desktop. Covers static (Ghidra,
radare2, JADX, apktool, binwalk via Bash) and the gap a previous review
called out: dynamic debugging is the lane that matters most for
AI-driven RE, but Frida alone cannot single-step or set real
breakpoints. This commit fills that gap with three non-overlapping
dynamic lanes -- Frida (instrumentation, mobile, broad surveys), GDB
(cross-arch single-step + breakpoints, embedded firmware), LLDB (Apple
platforms, ObjC/Swift).
Architecture coverage spans MIPS / ARM (incl. Cortex-M Thumb) /
PowerPC (incl. e200 VLE) / 68k (incl. Mac Toolbox A-line traps) /
SuperH / RISC-V / x86-64 / AArch64. The agent picks the right ISA,
base address, and load configuration in skills/firmware-blob, then
hands off to skills/pe-elf-macho with per-architecture decompilation
notes (MIPS delay slots, ARM Thumb interworking, PowerPC TOC/SDA,
68k register banks, etc).
Plugin layout (.claude-plugin marketplace + plugin manifests):
plugins/.claude-plugin/marketplace.json
plugins/reverse-engineering/
.claude-plugin/plugin.json v0.4.0, GHIDRA_INSTALL_DIR + ARTIFACT_DIR userConfig
agents/reverse-engineer.md triage -> static -> optional dynamic -> report
skills/triage/ file-type identification, packing, routing
skills/pe-elf-macho/ Ghidra/r2 + per-arch notes for non-x86
skills/firmware-blob/ raw blob -> ISA + base address + Ghidra/r2 load config
skills/apk-analysis/ JADX + apktool, Android attack surface
skills/ios-analysis/ IPA bundle + Mach-O, FairPlay-aware
skills/dynamic-debug-overview/ decision matrix Frida vs GDB vs LLDB
skills/frida-dynamic/ hooks + Memory + CpuContext + Stalker + watchpoints
skills/gdb-debug/ cross-arch via gdbserver / qemu-user / qemu-system
skills/lldb-debug/ macOS / iOS device / Linux, ObjC/Swift symbols
skills/crackme-keygen/ CTF / self-owned binary; ships keygen, not patch
skills/re-report/ final structured report with IOCs + confidence
commands/triage.md /reverse-engineering:triage <path>
commands/report.md /reverse-engineering:report <sample-id>
mcp/servers.json 7 MCP servers: ghidra (pyghidra-mcp PyPI),
radare2 (npm), gdb (mcp-gdb npm), lldb
(stass/lldb-mcp git), jadx, apktool, frida
(kahlo-mcp git, all @main pinned)
hooks/hooks.json empty placeholder
scripts/validate.ts offline schema check via project's
validatePluginManifest + validatePluginContents
scripts/dev-link.ts Windows mklink /J cache -> repo source so SKILL
edits don't require version bump (--restore undoes)
scripts/smoke.ts end-to-end: marketplace register -> enable ->
update -> reload -> assert detail counts.
Counts derived from on-disk source (glob),
not hardcoded; idempotent across reruns.
README.md install, prerequisites per MCP, dynamic-capabilities
matrix, architecture coverage, quickstart with
busybox sample, dev-link / smoke workflows
Project-level MCP (clone-and-go for any contributor):
.mcp.json codegraph (npx -y codegraph serve --mcp);
project scope, picked up automatically.
Pre-existing user-level codegraph install
still wins for users who have one.
userConfig of the plugin is honest -- only knobs that actually do
something (GHIDRA_INSTALL_DIR substituted into the ghidra MCP env;
ARTIFACT_DIR resolved relative to agent cwd at run time). Earlier
ENABLE_* booleans were removed because they didn't actually toggle
anything; users disable individual MCP servers from Settings -> MCP
at runtime.
End-to-end verification (server on :3456, vite on :1420):
- bun run plugins/reverse-engineering/scripts/validate.ts
-> 0 fail / 0 warn (marketplace + plugin schema both clean)
- bun run plugins/reverse-engineering/scripts/smoke.ts
-> 13 passed / 0 failed (marketplace registration, enable, update
0.3.0->0.4.0, reload, detail assertions including 11 skill ids
matching the on-disk glob)
- GET /api/plugins/detail
-> commands=2 / agents=1 / skills=11 / mcpServers=7 / errors=[]
- GET /api/mcp
-> codegraph appears with scope=project,
configLocation=.mcp.json
- Desktop UI Settings -> Plugins
-> v0.4.0 listed in enabled group, "11 skills 1 Agent 7 MCP"
External tool prerequisites (each MCP needs its underlying tool on
PATH; users install per-MCP independently -- none required all at
once): Ghidra (Java 17+), r2, GDB (gdb-multiarch for cross-arch),
LLDB, frida-tools, JADX (Java 17+), apktool. Codegraph requires
either a local global install (npm i -g codegraph) or relies on npx
to fetch on first run.
Constraint: Cannot single-step from Frida alone -- that's the gap GDB
and LLDB fill in this commit. dynamic-debug-overview SKILL is the
canonical decision guide for which lane to use.
Confidence: high
Scope-risk: narrow (additive plugin under plugins/, additive
.mcp.json; zero changes to existing source). Plugin loads via the
existing PluginManifestSchema path -- no new server-side code added.
Tested: bun run plugins/reverse-engineering/scripts/validate.ts (pass),
bun run plugins/reverse-engineering/scripts/smoke.ts (13/13 pass), live
chrome-devtools MCP smoke against running desktop UI verifying plugin
appears with correct version + counts.
Not-tested: real underlying-tool integration (Ghidra/r2/JADX/GDB/LLDB
must be installed by the user; the plugin only validates manifest
shape and component loading). Reverse-debugging (rr) flow is
documented in skills/gdb-debug but not exercised in smoke.
* fix: detect & isolate fake tool_use leaks from non-Anthropic providers
Some third-party Anthropic-compatible gateways (mimo, lgfzer, certain
proxies) don't relay native tool_use blocks: when the model wants to
call a tool, it ends up emitting an XML-style `<tool_use ...>{...}
</tool_use>` element as plain text inside content_delta. The desktop
chat renders that as markdown, marked passes the unknown element
through as raw HTML, the browser collapses whitespace, and the user
sees garbage like `<tool_useid="..."` followed by a smushed shell
command. Worse, the model has no signal the call never executed and
follows up with apologies ("工具用错了, 重来:") and retries — every
"call" is text and nothing runs.
Three layers of defense, lowest to highest in the stack:
1. **Source-side priming reduction** (server). The specialistRouter
docstring already warned that `key="value"` attribute fragments in
model-facing strings make some gateway models switch from real
tool_use blocks into XML mode. Three model-facing nudges still
embedded `subagent_type="verification"` in their reminders — fix
them to use prose ("set the subagent_type parameter to verification")
so we stop priming the failure mode:
- src/utils/messages.ts (verification_gate_reminder)
- src/tools/TodoWriteTool/TodoWriteTool.ts
- src/tools/TaskUpdateTool/TaskUpdateTool.ts
New regression test src/utils/noKeyValueNudges.test.ts pins all
three call sites so a future "typo fix" doesn't accidentally
re-introduce the attribute form.
2. **Detection + sanitization** (desktop). New
desktop/src/lib/fakeToolUseDetection.ts extracts XML-style
`<tool_use>` blocks (closed, half-open mid-stream, multiple
consecutive retries, `name`/`id` in either order, quoted or
unquoted, inside or outside fenced code blocks) and returns
`{ cleanText, blocks[] }`. AssistantMessage uses this before
handing content to MarkdownRenderer so the user never sees the
XML garbage; copyText also uses cleanContent so users don't paste
broken markup. Fenced code blocks (```xml ... ```) are preserved
so legitimate documentation examples render as-is.
3. **Provider compatibility tracking** (desktop). New
desktop/src/stores/providerCompatStore.ts counts fake tool_use
leaks per provider id, persisted to localStorage. After 3 leaks
it fires a one-time toast suggesting the user switch providers.
Settings → Provider list shows a "工具调用异常" badge on offending
providers; saving an edit on a flagged provider re-arms the
warning (clearProvider). Above-threshold detection is silent
afterward so we don't spam — the badge stays as the persistent
signal.
The new FakeToolUseNotice component renders an inline grey card
above the assistant bubble naming the attempted tool ("Bash") and
making it clear nothing ran, so the user knows not to trust any
follow-up "Done." claim that depended on the call's output.
Verified:
- desktop: bun run lint clean
- desktop: 28 new tests + existing AssistantMessage.linkrouting (15
total) pass
- server: 31 tests across verificationGate, specialistRouter, and the
new noKeyValueNudges regression all pass
Tested: bun run lint (desktop), bunx vitest run (3 desktop suites),
bun test (3 server suites)
Confidence: high
Scope-risk: narrow
---------
Co-authored-by: cc-haha <cc-haha@users.noreply.github.com>
Co-authored-by: 你的姓名 <you@example.com>
4.5 KiB
| name | description | whenToUse | allowedTools |
|---|---|---|---|
| ios-analysis | Static analysis of iOS apps (.ipa) and Mach-O frameworks. Extracts the bundle structure, Info.plist, entitlements, ObjC class layout, and routes the actual binary to native analysis. | After triage detects an IPA archive or a standalone Mach-O. Note on signature/encryption: App Store binaries arrive FairPlay-encrypted, cleartext analysis requires a decrypted dump. | Bash, Read, Grep, Glob |
ios-analysis skill
Goal: get from an .ipa (or a raw Mach-O) to a usable picture of the app's
class hierarchy, entitlements, exposed schemes, and native code.
Up-front constraint
App Store IPAs are FairPlay-encrypted — the __TEXT segment of the main
binary is unreadable until decrypted on a jailbroken device with a tool like
frida-ios-dump or bagbak. If the user gives you an App Store IPA you cannot
fully analyse it statically. State this clearly. Free or in-house IPAs are
usually unencrypted.
How to tell: in the Mach-O load commands look for LC_ENCRYPTION_INFO_64 with
cryptid=1. cryptid 0 = decrypted.
Procedure
Step 1 — Unpack the IPA
unzip -d "$SAMPLE_ID-unpacked" "$SAMPLE"
# IPAs always have: Payload/<AppName>.app/
APP_DIR=$(find "$SAMPLE_ID-unpacked/Payload" -maxdepth 1 -type d -name "*.app" | head -1)
APP_NAME=$(basename "$APP_DIR" .app)
APP_BIN="$APP_DIR/$APP_NAME"
Step 2 — Bundle metadata
Read these files from $APP_DIR:
Info.plist— bundle id, version, supported platforms, URL schemes (CFBundleURLTypes), background modes, NS*UsageDescription strings. Convert binary plist withplutil -convert xml1 -o - Info.plist.embedded.mobileprovision— provisioning profile. Extract entitlements:
Flag:security cms -D -i "$APP_DIR/embedded.mobileprovision" > /tmp/profile.plist plutil -extract Entitlements xml1 -o - /tmp/profile.plistget-task-allow=true(debuggable), keychain-access-groups,com.apple.developer.networking.networkextension.PkgInfoand presence ofWatch/,PlugIns/,Frameworks/— multi-target apps need separate analysis per target.
Step 3 — Mach-O analysis
Open the main binary with radare2 MCP (radare2 server):
radare2: open path=$APP_BIN
radare2: cmd "aaa"
radare2: cmd "iIq" # mach-o info: arch, encryption, PIE, NX
radare2: cmd "iLq" # linked libraries
radare2: cmd "iEq" # exports
Specifically check:
- encryption:
cryptidiniIoutput. If 1, halt static and state the blocker. - architecture: arm64 only, or fat binary with armv7? Usually arm64 for modern apps.
- PIE / NX / stack canary: missing PIE on a recent app is a finding.
Step 4 — ObjC / Swift class recovery
ObjC class metadata lives in __objc_classlist etc. and is recoverable even
with stripped symbols.
radare2: cmd "ic" # list classes
radare2: cmd "icj" # JSON form
radare2: cmd "ii" # imports (will include common ObjC runtime)
For Swift, symbol recovery is best-effort — Swift mangles aggressively. If r2's
demangler doesn't help, nm $APP_BIN | swift demangle (when toolchain is
available) usually does.
External tools that complement when installed (mention to the user, don't run
silently): class-dump-dyld, ipsw, Hopper Decompiler (paid), Ghidra with
the iOS loader.
Step 5 — URL schemes, deep links, network
Search the binary for:
http://,https://,wss://- Custom URL scheme strings (
myapp://) - API path templates
- AppTransportSecurity exceptions in Info.plist
Step 6 — Optional: hand off to pe-elf-macho
If the user wants a deeper decompilation of specific functions, the
pe-elf-macho skill works on Mach-O. Hand $APP_BIN to it.
Outputs
Write to ARTIFACT_DIR/<sample-id>/static-ios.md:
# Static iOS analysis — <sample-id>
## Bundle
- Bundle ID: com.example.app
- Version: ...
- URL schemes: myapp://
- Encrypted (FairPlay): yes / no
- Architecture: arm64
- PIE / NX / Canary: yes / yes / yes
## Entitlements of interest
| Key | Value | Note |
## ObjC classes of interest
| Class | Methods | Note |
## Strings of interest
| Type | Value | Where |
## Open questions / blockers
- ...
Hard rules
- Don't install the IPA on your iPhone. Static analysis is on the bundle;
dynamic is
frida-dynamicon a controlled jailbroken device. - If
cryptid=1, do not pretend to decompile the encrypted segment. State the blocker and stop the static stage.