@dsh-fly/dsh-plugin-fly-mode 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-fly/dsh-plugin-fly-mode",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Fly 模式作为一个可安装的包:**两个 preset** —— Fly 安全模式(preset-fly,8 个安全工具内联 + smart)与 Fly 标准模式(preset-fly-standard,上游 standard 的行表 + smart,无安全工具);依赖单独发布的 dsh-plugin-fly-smart。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -96,6 +96,126 @@ function requireOk(run, what) {
96
96
  * match `/\bret\b/` — so a textbook `pop rdi; ret` came back as "no gadgets
97
97
  * found" (asserted in scripts/verify-security-tools.mjs).
98
98
  */
99
+ // ── Ghidra headless(2026-10-16 实测:本机可用,但有三个坑)──────────────────────
100
+ //
101
+ // 坑一(用户踩过):`/usr/libexec/java_home` 默认指向
102
+ // `/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home` —— 那是 **x86_64 的
103
+ // Java 8**,arm64 上直接 `Bad CPU type in executable`。必须显式指 Homebrew 的 arm64 JDK:
104
+ // `JAVA_HOME=/opt/homebrew/opt/openjdk@26`(实测 26.0.1;openjdk@11 备选)。
105
+ // 坑二(本次实测):analyzeHeadless 的**项目目录必须已存在**(否则
106
+ // `ERROR Abort due to Headless analyzer error: Directory not found: …`)—— 它不替你建。
107
+ // 坑三(本次实测):`os/mac_arm_64/decompile does not exist` ⇒ **反编译的原生件没编译**,
108
+ // 这条能力要先跑 `support/buildNatives`(要 C++ 工具链)才有;在此之前只有 import/分析/函数表。
109
+
110
+ /** arm64 macOS 上唯一实测可用的 JDK(doctor 会把这条写进结果)。 */
111
+ const GHIDRA_JAVA_HOME = '/opt/homebrew/opt/openjdk@26'
112
+
113
+ /** `-postScript` 里那个脚本:把函数表打成 `FLYFN ` 前缀的 JSON 行(脚本在 scratch 里现写)。 */
114
+ function flyFunctionsScript() {
115
+ return [
116
+ '// Generated by fly-reverse (rev_ghidra) — prints one `FLYFN <json>` line per function.',
117
+ 'import ghidra.app.script.GhidraScript;',
118
+ 'import ghidra.program.model.listing.*;',
119
+ 'import java.io.*;',
120
+ 'public class FlyFunctions extends GhidraScript {',
121
+ ' @Override public void run() throws Exception {',
122
+ ' int limit = 200;',
123
+ ' if (getScriptArgs().length > 0) limit = Integer.parseInt(getScriptArgs()[0]);',
124
+ ' PrintWriter out = new PrintWriter(System.out);',
125
+ ' FunctionIterator it = currentProgram.getFunctionManager().getFunctions(true);',
126
+ ' int n = 0;',
127
+ ' while (it.hasNext() && n < limit) {',
128
+ ' Function fn = it.next();',
129
+ ' char q = 34;',
130
+ ' String json = "{" + q + "name" + q + ":" + q + fn.getName().replace(String.valueOf(q), "") + q',
131
+ ' + "," + q + "entry" + q + ":" + q + "0x" + Long.toHexString(fn.getEntryPoint().getOffset()) + q',
132
+ ' + "," + q + "size" + q + ":" + fn.getBody().getNumAddresses()',
133
+ ' + "," + q + "thunk" + q + ":" + fn.isThunk()',
134
+ ' + "," + q + "external" + q + ":" + fn.isExternal() + "}";',
135
+ ' out.println("FLYFN " + json);',
136
+ ' n += 1;',
137
+ ' }',
138
+ ' out.println("FLYFN-END " + n);',
139
+ ' out.flush();',
140
+ ' }',
141
+ '}',
142
+ ].join('\n')
143
+ }
144
+
145
+ /** 经接缝写一份文件(base64 中转 ⇒ 不引号化内容、也**不靠 stdin**:runCmd 没有 stdin,cat 会挂死)。 */
146
+ async function writeViaBase64(shell, exec, path, content) {
147
+ const encoded = Buffer.from(content, 'utf8').toString('base64')
148
+ return runCmd(shell, ['sh', '-c', 'printf %s ' + shq(encoded) + ' | base64 -d > ' + shq(path)], exec, { timeoutMs: 15_000 })
149
+ }
150
+
151
+ /** 找 analyzeHeadless:显式 GHIDRA_HOME 优先,否则候选目录(找不到就空串)。 */
152
+ /** 反编译用的 postScript(要 `os/<平台>/decompile` 原生件;没有就由 ghidraFix 给修法)。 */
153
+ function flyDecompileScript() {
154
+ return [
155
+ '// Generated by fly-reverse (rev_ghidra decompile) — prints `FLYFN-NAME` + pseudocode blocks.',
156
+ 'import ghidra.app.script.GhidraScript;',
157
+ 'import ghidra.app.decompiler.*;',
158
+ 'import ghidra.program.model.listing.*;',
159
+ 'import java.io.*;',
160
+ 'public class FlyDecompile extends GhidraScript {',
161
+ ' @Override public void run() throws Exception {',
162
+ ' String want = getScriptArgs().length > 0 ? getScriptArgs()[0] : "";',
163
+ ' DecompInterface ifc = new DecompInterface();',
164
+ ' ifc.openProgram(currentProgram);',
165
+ ' FunctionIterator it = currentProgram.getFunctionManager().getFunctions(true);',
166
+ ' PrintWriter out = new PrintWriter(System.out);',
167
+ ' int n = 0;',
168
+ ' while (it.hasNext() && n < 20) {',
169
+ ' Function fn = it.next();',
170
+ ' if (!want.isEmpty() && !fn.getName().equals(want) && !want.equals(fn.getEntryPoint().toString())) continue;',
171
+ ' DecompileResults res = ifc.decompileFunction(fn, 60, monitor);',
172
+ ' out.println("FLYFN-NAME " + fn.getName());',
173
+ ' out.println("FLYCODE-START");',
174
+ ' out.println(res.decompileCompleted() ? res.getDecompiledFunction().getC() : "/* decompile failed for this function */");',
175
+ ' out.println("FLYCODE-END");',
176
+ ' n += 1;',
177
+ ' }',
178
+ ' out.flush();',
179
+ ' ifc.dispose();',
180
+ ' }',
181
+ '}',
182
+ ].join('\n')
183
+ }
184
+
185
+
186
+ function ghidraHeadless(shell, exec) {
187
+ return runCmd(shell, ['sh', '-c', 'if [ -n "$GHIDRA_HOME" ]; then echo "$GHIDRA_HOME/support/analyzeHeadless"; else ls -d "$HOME"/tools/ghidra_*/support/analyzeHeadless 2>/dev/null | sort | tail -1; fi'], exec, { timeoutMs: 10_000 })
188
+ }
189
+
190
+ /** JDK 探针直接跑 javaHome 下的 java —— **别**写 `JAVA_HOME=x "$JAVA_HOME/bin/java"`:
191
+ * POSIX 里同一命令的变量展开发生在赋值生效**之前**,`$JAVA_HOME` 会取到旧值(= 那个 x86_64
192
+ * Java 8)⇒ doctor 会把"能用"报成"不能用"(两处口径不一致,测试当场抓到)。 */
193
+ /** 两类"跑不了"都要给**可复制的修法**(不是"工具缺失"四个字)。 */
194
+ function ghidraFix(text) {
195
+ if (/Bad CPU type|Unable to prompt user for JDK path/u.test(text)) {
196
+ return [
197
+ 'Ghidra 起不来:/usr/libexec/java_home 默认指向 /Library/Internet Plug-Ins/JavaAppletPlugin.plugin',
198
+ '(那是 **x86_64 的 Java 8**,arm64 上直接 Bad CPU type in executable)。',
199
+ '修法(arm64 macOS,实测可用):',
200
+ ' export JAVA_HOME=/opt/homebrew/opt/openjdk@26',
201
+ ' ~/tools/ghidra_12.1.4_PUBLIC/support/analyzeHeadless …',
202
+ ].join('\n')
203
+ }
204
+ if (/decompile does not exist|Building Native Components/u.test(text)) {
205
+ return [
206
+ 'Ghidra 的反编译原生件还没编译(os/mac_arm_64/decompile 不存在)——',
207
+ '这只影响**反编译**,import / 分析 / 函数表照旧能用。',
208
+ '修法(要 C++ 工具链,一次性):',
209
+ ' cd ~/tools/ghidra_12.1.4_PUBLIC/support/gradle/ && gradle buildNatives # 或自带 wrapper(需联网):./gradlew buildNatives',
210
+ ' # 编完在 <模块>/build/os/<平台>/ 下,**要装进** <模块>/os/<平台>/(release 布局里 Ghidra 找的是后者):',
211
+ ' cp Ghidra/Features/Decompiler/build/os/mac_*/decompile Ghidra/Features/Decompiler/os/mac_*/',
212
+ ].join('\n')
213
+ }
214
+ return ''
215
+ }
216
+
217
+
218
+
99
219
  function scanGadgets(disasm, filter) {
100
220
  const instruction = /^\s*([0-9a-f]+):\s+(?:[0-9a-f]{2} )+\s*([a-z][a-z0-9.]*)\s*(.*)$/u
101
221
  const wanted = filter === undefined ? null : filter.toLowerCase().split(/\s+/u).filter(Boolean)
@@ -289,4 +409,155 @@ export function apply(ctx) {
289
409
  }, null, 2)
290
410
  },
291
411
  }))
412
+ // ── rev_ghidra(Ghidra headless:doctor / functions)───────────────
413
+ ctx.tools.register(defineTool({
414
+ name: 'rev_ghidra',
415
+ description: 'Run Ghidra headless (analyzeHeadless) on a binary. doctor = probe the local install (JAVA_HOME trap, native decompiler); functions = import + analyse and list functions as JSON lines. arm64 macOS needs JAVA_HOME=/opt/homebrew/opt/openjdk@26.',
416
+ parameters: {
417
+ action: { type: 'string', required: true, description: 'doctor | functions' },
418
+ binary: { type: 'string', description: 'binary to analyse (required for functions)' },
419
+ limit: { type: 'number', description: 'max functions to list (default 200, clamped 1..2000)' },
420
+ },
421
+ output: {
422
+ schema: { type: 'string' },
423
+ render: (_a, v) => [{ type: 'text', text: v }],
424
+ },
425
+ async execute(args, exec) {
426
+ const action = String(args.action ?? '').trim()
427
+ if (action !== 'doctor' && action !== 'functions' && action !== 'decompile') {
428
+ const error = new Error('rev_ghidra 的 action 只认 doctor / functions / decompile(收到 ' + JSON.stringify(args.action) + ')')
429
+ error.name = 'ArgumentError'
430
+ throw error
431
+ }
432
+ const found = await ghidraHeadless(shell, exec)
433
+ const headless = found.stdout.trim().split('\n').filter(Boolean).at(-1) ?? ''
434
+ if (headless === '') {
435
+ const error = new Error([
436
+ 'rev_ghidra:找不到 analyzeHeadless —— 这台机器上 Ghidra 要么没装,要么没装在 ~/tools/ghidra_*/ 下。',
437
+ '两条路:设 GHIDRA_HOME=<你的 Ghidra 目录>(我只找 $GHIDRA_HOME/support/analyzeHeadless 与 ~/tools/ghidra_*/support/analyzeHeadless),',
438
+ '或者装一个:https://ghidra-sre.org/ (下载 zip 解压即可,不需要安装器)。',
439
+ ].join('\n'))
440
+ error.name = 'ToolError'
441
+ throw error
442
+ }
443
+ const javaHome = typeof process.env.GHIDRA_JAVA_HOME === 'string' && process.env.GHIDRA_JAVA_HOME.trim() !== ''
444
+ ? process.env.GHIDRA_JAVA_HOME.trim()
445
+ : GHIDRA_JAVA_HOME
446
+ const work = (process.env.TMPDIR || '/tmp').replace(/\/$/u, '') + '/fly-reverse-ghidra'
447
+ // 注意:`analyzeHeadless` 是**文件**,`<文件>/../…` 在 POSIX 里解析不了(ENOTDIR)——
448
+ // 第一版探针就是这么把"装好了"读成"不在"的(手跑同一条命令复现)。要先 `$(dirname …)`。
449
+ // 平台目录名是 `mac_arm_64`(**有下划线**):拿 `uname -m` 的 `arm64` 直接拼 `mac_${P}` 会得到
450
+ // `mac_arm64` ⇒ test -e 恒假,而 ls 明明看得见那个文件 —— 两个读数矛盾就是尺子拼错了。
451
+ const nativeProbe = headless === '' ? null : await runCmd(shell, ['sh', '-c', 'P=$(uname -m | sed s/arm64/arm_64/); D=$(dirname "' + headless + '"); test -e "$D/../Ghidra/Features/Decompiler/os/mac_${P}/decompile" || test -e "$D/../../os/mac_${P}/decompile"'], exec, { timeoutMs: 10_000 })
452
+ const javaRun = await runCmd(shell, ['sh', '-c', shq(javaHome + '/bin/java') + ' -version'], exec, { timeoutMs: 15_000 })
453
+ if (action === 'doctor') {
454
+ const probes = []
455
+ probes.push({ what: 'analyzeHeadless', ok: headless !== '', detail: headless === '' ? '不在 ~/tools/ghidra_*/support/ 下(装了的话设 GHIDRA_HOME)' : headless })
456
+ probes.push({ what: 'arm64 JDK', ok: javaRun.exitCode === 0, detail: javaRun.stdout.trim().split('\n')[0] ?? javaRun.stderr.trim().split('\n')[0] })
457
+ probes.push({ what: '反编译原生件 os/mac_arm_64/decompile', ok: nativeProbe !== null && nativeProbe.exitCode === 0, detail: nativeProbe !== null && nativeProbe.exitCode === 0 ? '在' : '不在 —— 反编译不可用,函数表照旧(修法见 limitations)' })
458
+ return JSON.stringify({
459
+ tool: 'rev_ghidra',
460
+ target: headless === '' ? '(未找到 analyzeHeadless)' : headless,
461
+ operation: '三个探针:analyzeHeadless 在不在 / arm64 JDK 能不能跑 / 反编译原生件在不在',
462
+ observed: { probes, usable: { functions: headless !== '' && javaRun.exitCode === 0, decompile: nativeProbe !== null && nativeProbe.exitCode === 0 } },
463
+ confidence: 'observed',
464
+ limitations: [
465
+ 'arm64 macOS 上**必须**显式设 JAVA_HOME=/opt/homebrew/opt/openjdk@26:'
466
+ + '/usr/libexec/java_home 默认指向 JavaAppletPlugin(x86_64 Java 8),arm64 直接 Bad CPU type',
467
+ 'analyzeHeadless 的**项目目录必须已存在**(它不替你建)—— 实测出来的形状',
468
+ '反编译原生件缺失 ≠ "这个包没有反编译":那是装机器的事,修法在下一条',
469
+ '修法(反编译,官方 GettingStarted.md §Building Native Components):cd ~/tools/ghidra_12.1.4_PUBLIC/support/gradle/ && gradle buildNatives # 或自带 wrapper(需联网):./gradlew buildNatives',
470
+ ],
471
+ }, null, 2)
472
+ }
473
+ if (action === 'decompile') {
474
+ if (typeof args.binary !== 'string' || args.binary.trim() === '') {
475
+ const error = new Error('rev_ghidra decompile 需要 binary 参数(要反编译哪个文件)')
476
+ error.name = 'ArgumentError'
477
+ throw error
478
+ }
479
+ const decBinary = safePath(args.binary.trim())
480
+ const want = typeof args.function === 'string' && args.function.trim() !== '' ? args.function.trim() : ''
481
+ await runCmd(shell, ['mkdir', '-p', work], exec, { timeoutMs: 10_000 })
482
+ await writeViaBase64(shell, exec, work + '/FlyDecompile.java', flyDecompileScript())
483
+ const run = await runCmd(shell, ['sh', '-c', [
484
+ 'JAVA_HOME=' + javaHome + ' ' + shq(headless) + ' ' + shq(work) + ' flyproj',
485
+ '-import ' + shq(decBinary) + ' -deleteProject',
486
+ '-scriptPath ' + shq(work) + ' -postScript FlyDecompile ' + shq(want),
487
+ ].join(' ')], exec, { timeoutMs: 240_000, stdoutMaxBytes: 8 * 1024 * 1024 })
488
+ const text = (run.stdout ?? '') + (run.stderr ?? '')
489
+ if (run.exitCode !== 0) {
490
+ const fix = ghidraFix(text)
491
+ throw new Error('rev_ghidra decompile 跑失败(exit ' + run.exitCode + '):'
492
+ + text.trim().split('\n').filter(Boolean).slice(0, 3).join(' | ')
493
+ + (fix === '' ? '' : '\n\n' + fix))
494
+ }
495
+ const out = run.stdout
496
+ if (!/FLYCODE-END/u.test(out)) {
497
+ throw new Error('rev_ghidra decompile:一行伪代码都没产出(退出码却是 0)—— 脚本多半没编译过或原生反编译器没装,'
498
+ + 'Ghidra 对两种情况都可能只打日志不报错;跑 rev_ghidra doctor 看反编译原生件在不在。')
499
+ }
500
+ const blocks = []
501
+ const pattern = /FLYFN-NAME (\S+)\r?\nFLYCODE-START\r?\n([\s\S]*?)\r?\nFLYCODE-END/gu
502
+ for (const match of out.matchAll(pattern)) blocks.push({ name: match[1], code: match[2] })
503
+ return JSON.stringify({
504
+ tool: 'rev_ghidra',
505
+ target: decBinary + (want === '' ? '' : ' · ' + want),
506
+ operation: 'analyzeHeadless import + 分析 → FlyDecompile postScript(DecompInterface)',
507
+ observed: { functions: blocks.length, blocks: blocks.slice(0, 20) },
508
+ confidence: 'observed',
509
+ limitations: [
510
+ '这是 Ghidra 反编译器的**伪代码**(C 风格):不是源码、不是汇编,变量名多是 Ghidra 起的(FUN_xxx 等)',
511
+ '每个函数最多等 60 秒反编译(DecompInterface 的超时);超时的会标注 decompile failed',
512
+ '需要 os/<平台>/decompile 原生件 —— 没装时 Ghidra 只在日志里说,这里把它抬成大声报错 + 修法',
513
+ '同一函数的伪代码会随 Ghidra 版本/分析深度变化,别当稳定接口用',
514
+ ],
515
+ }, null, 2)
516
+ }
517
+ if (typeof args.binary !== 'string' || args.binary.trim() === '') {
518
+ const error = new Error('rev_ghidra functions 需要 binary 参数(要分析哪个文件)')
519
+ error.name = 'ArgumentError'
520
+ throw error
521
+ }
522
+ const binary = safePath(args.binary.trim())
523
+ const limit = Math.min(2000, Math.max(1, Math.round(Number(args.limit ?? 200) || 200)))
524
+ await runCmd(shell, ['mkdir', '-p', work], exec, { timeoutMs: 10_000 })
525
+ await writeViaBase64(shell, exec, work + '/FlyFunctions.java', flyFunctionsScript())
526
+ const run = await runCmd(shell, ['sh', '-c', [
527
+ 'JAVA_HOME=' + javaHome + ' ' + shq(headless) + ' ' + shq(work) + ' flyproj',
528
+ '-import ' + shq(binary) + ' -deleteProject',
529
+ '-scriptPath ' + shq(work) + ' -postScript FlyFunctions ' + limit,
530
+ ].join(' ')], exec, { timeoutMs: 240_000, stdoutMaxBytes: 4 * 1024 * 1024 })
531
+ const text = (run.stdout ?? '') + (run.stderr ?? '')
532
+ if (run.exitCode !== 0) {
533
+ const fix = ghidraFix(text)
534
+ throw new Error('rev_ghidra functions 跑失败(exit ' + run.exitCode + '):'
535
+ + text.trim().split('\n').filter(Boolean).slice(0, 3).join(' | ')
536
+ + (fix === '' ? '' : '\n\n' + fix))
537
+ }
538
+ const rows = run.stdout.split('\n').filter(line => line.startsWith('FLYFN {')).map(line => JSON.parse(line.slice(6)))
539
+ const total = Number(/FLYFN-END (\d+)/u.exec(run.stdout)?.[1] ?? rows.length)
540
+ if (!/FLYFN-END/u.test(run.stdout)) {
541
+ // Ghidra 对**编译不过**的 postScript 是 exit 0 + 一行都不打(实测)—— 这里必须大声,
542
+ // 不许把"脚本没跑"读成"这个二进制没有函数"。
543
+ throw new Error('rev_ghidra functions:FlyFunctions 一行都没产出(退出码却是 0)—— 脚本多半没编译过。'
544
+ + 'Ghidra 对编译失败的 postScript 是静默跳过的;请看命令日志里的 javac/脚本错误,'
545
+ + '或照审计里的命令原样跑一遍看它说什么。')
546
+ }
547
+ return JSON.stringify({
548
+ tool: 'rev_ghidra',
549
+ target: binary,
550
+ operation: 'analyzeHeadless import + 分析 → FlyFunctions postScript(最多 ' + limit + ' 条)',
551
+ observed: { count: rows.length, listed: rows.length, total, functions: rows.slice(0, limit) },
552
+ confidence: 'observed',
553
+ limitations: [
554
+ '这是 Ghidra 的**函数表**(按地址顺序、最多 limit 条):不是全部符号,也不是反编译输出',
555
+ 'thunk / external 两个标志原样给出(Ghidra 的判断),没有再做二次解释',
556
+ '整个二进制**会被完整分析一遍**(headless 的语义),大文件会慢 —— 请给 limit 或等',
557
+ '项目目录与脚本写在 scratch(TMPDIR/fly-reverse-ghidra/)里,-deleteProject 用完即删,不碰仓库',
558
+ '反编译(函数体的伪代码)还没接:原生件缺失时 Ghidra 自己会报,修法见 rev_ghidra doctor',
559
+ ],
560
+ }, null, 2)
561
+ },
562
+ }))
292
563
  }