@dsh-fly/dsh-plugin-fly-mode 0.1.7 → 0.1.9
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/README.md +3 -3
- package/cordis.patch.yml +0 -3
- package/package.json +2 -2
- package/tools/dsh-plugin-fly-artifact/index.js +21 -2
- package/tools/dsh-plugin-fly-binary/index.js +110 -22
- package/tools/dsh-plugin-fly-blackbox/index.js +23 -4
- package/tools/dsh-plugin-fly-diag/index.js +190 -29
- package/tools/dsh-plugin-fly-pwn/index.js +91 -9
- package/tools/dsh-plugin-fly-recon/index.js +117 -16
- package/tools/dsh-plugin-fly-reverse/index.js +132 -18
|
@@ -12,6 +12,17 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
12
12
|
* Command execution goes through the host `shell` seam so the sandbox confines
|
|
13
13
|
* it (and the FLY_ALLOW_RECON gate is an ADDITIONAL authorization layer on top
|
|
14
14
|
* of the sandbox, not a replacement for it).
|
|
15
|
+
*
|
|
16
|
+
* **失败面的口径是"大声"**(Q5):
|
|
17
|
+
* · 没有 `nmap`(macOS 常态)⇒ 用系统自带的 `nc` 扫,并在结果里标明"这是 nc 扫的",且**明说
|
|
18
|
+
* `no-response` 分不清 closed 与 filtered**(要区分必须换 nmap);端口数超过内置回退的 64 上限时
|
|
19
|
+
* **一个都不扫**并说清上限与出路,绝不扫一半交差;
|
|
20
|
+
* · 目标 URL **先校验再用**(`requireHttpUrl`:只允许 http/https)—— 2026-10-13 补:此前这条只有
|
|
21
|
+
* `diag_net_throughput` 有,`recon_http_fingerprint` 会放行 `file://`,而 curl 能把本地文件读出来
|
|
22
|
+
* 回显进工具结果;Q5 那次我在本注释里先写了这条保护,**当时它并不成立**,现在它成立了;
|
|
23
|
+
* · `curl` 失败 ⇒ `requireOk` 抛 `CommandError`,不给"空指纹";
|
|
24
|
+
* · `recon_http_fingerprint` 只读响应头与 `<title>`:**不执行 JS** —— 由 JS 搭出来的界面在这把尺子里
|
|
25
|
+
* 等于不存在(AGENTS.md 里那台摄像头的负结论就是这么来的),这一点写进了它的 `limitations`。
|
|
15
26
|
*/
|
|
16
27
|
export const name = 'fly-recon'
|
|
17
28
|
export const inject = ['tools', 'shell']
|
|
@@ -58,11 +69,60 @@ async function runCmd(shell, argv, exec, opts = {}) {
|
|
|
58
69
|
* tools must say so instead of reporting an empty scan, because an empty scan
|
|
59
70
|
* reads as "target closed".
|
|
60
71
|
*/
|
|
72
|
+
/**
|
|
73
|
+
* 只接受主机名或 IP 字面量。2026-10-13 补:此前 `recon_ports` 的 `host` **没有**这道校验,
|
|
74
|
+
* 而 diag_* 三个工具都有 —— 于是 `-oN=/tmp/x` 这种"以 - 开头的目标"会被 nmap 当成**选项**
|
|
75
|
+
* (写出文件)。同族性质必须在**每个**工具上都有。
|
|
76
|
+
*/
|
|
77
|
+
function requireHost(value) {
|
|
78
|
+
const host = String(value ?? '').trim()
|
|
79
|
+
if (!/^[A-Za-z0-9](?:[A-Za-z0-9._:-]*[A-Za-z0-9])?$/u.test(host)) {
|
|
80
|
+
const error = new Error(
|
|
81
|
+
`拒绝目标 ${JSON.stringify(host)}:只接受主机名或 IP 字面量 ——`
|
|
82
|
+
+ ' 不许有空格或 shell 元字符(; | & $ ` 引号 括号),也不许以 "-" 开头'
|
|
83
|
+
+ '(会被当成命令参数:nmap、nc 会把 `-oN=file` 之类的选项当真)。',
|
|
84
|
+
)
|
|
85
|
+
error.name = 'ArgumentError'
|
|
86
|
+
throw error
|
|
87
|
+
}
|
|
88
|
+
return host
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 只允许 http/https:`file:` 会让 curl 把本地文件读出来回显进工具结果(2026-10-13 发现并补上)。 */
|
|
92
|
+
const ALLOWED_URL_SCHEMES = ['http:', 'https:']
|
|
93
|
+
function requireHttpUrl(value) {
|
|
94
|
+
const text = String(value ?? '').trim()
|
|
95
|
+
let parsed
|
|
96
|
+
try {
|
|
97
|
+
parsed = new URL(text)
|
|
98
|
+
} catch {
|
|
99
|
+
const error = new Error(`不是合法的 URL:${text}`)
|
|
100
|
+
error.name = 'ArgumentError'
|
|
101
|
+
throw error
|
|
102
|
+
}
|
|
103
|
+
if (!ALLOWED_URL_SCHEMES.includes(parsed.protocol)) {
|
|
104
|
+
const error = new Error(`拒绝 ${parsed.protocol} 目标:只允许 http/https(file: 会读本地文件)`)
|
|
105
|
+
error.name = 'ArgumentError'
|
|
106
|
+
throw error
|
|
107
|
+
}
|
|
108
|
+
return parsed.href
|
|
109
|
+
}
|
|
110
|
+
|
|
61
111
|
function requireOk(run, what) {
|
|
62
112
|
if (run.exitCode === 0) return run.stdout
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
113
|
+
// 沙箱拒绝的形状是**结果正文里的标记**(`[sandbox: file access denied under <mode> mode]`,
|
|
114
|
+
// 抄自 @deepseek-ai/dsh-sandbox/index.js:65)—— 它落在 stdout 上;只读 stderr 就会把它丢掉,
|
|
115
|
+
// 于是模型读到的是「命令失败」,而不是「你没有被授权」。所以标记必须原样带进错误文案。
|
|
116
|
+
const sandboxMark = /^\[sandbox: .*\]$/mu.exec(run.stdout)
|
|
117
|
+
const detail = [sandboxMark?.[0], ...run.stderr.trim().split('\n').map(line => line.trim()).filter(Boolean)].slice(0, 3).join(' | ')
|
|
118
|
+
const notInstalled = run.exitCode === 127 || /command not found/iu.test(run.stderr)
|
|
119
|
+
const missing = notInstalled ? ' — the command is not installed on this host' : ''
|
|
120
|
+
// 失败文案要对**读它的模型**可行动(Q6):不是裸 stderr,但也不许臆断原因 ——
|
|
121
|
+
// 只给「这条 exit 属于哪一类」与「下一步可以试什么」,具体判断仍留给读的人。
|
|
122
|
+
const next = notInstalled
|
|
123
|
+
? 'next: this host lacks that command — look for a built-in fallback in this package, or read the file another way'
|
|
124
|
+
: 'next: check the path, and that the file is what this tool expects (bin_file shows what it is); the line above is the command own reason'
|
|
125
|
+
const error = new Error(`${what} failed (exit ${run.exitCode})${missing}: ${run.command}${detail === '' ? '' : `\n${detail}\n${next}`}`)
|
|
66
126
|
error.name = 'CommandError'
|
|
67
127
|
throw error
|
|
68
128
|
}
|
|
@@ -128,38 +188,59 @@ export function apply(ctx) {
|
|
|
128
188
|
render: (_a, v) => [{ type: 'text', text: v }],
|
|
129
189
|
},
|
|
130
190
|
async execute(args, exec) {
|
|
191
|
+
// 先校验再构造命令(同族性质:diag_* 三个工具早就有这道门)。
|
|
192
|
+
const host = requireHost(args.host)
|
|
193
|
+
// 证据形状(规划 §5.1):逐分支迁移 —— 本轮只把 nc 回退分支包上。
|
|
194
|
+
const envelope = (observed, limitations) => JSON.stringify({
|
|
195
|
+
tool: 'recon_ports',
|
|
196
|
+
target: args.host,
|
|
197
|
+
operation: 'TCP connect scan of the requested ports (authorized lab targets only)',
|
|
198
|
+
observed,
|
|
199
|
+
confidence: 'observed',
|
|
200
|
+
limitations,
|
|
201
|
+
}, null, 2)
|
|
131
202
|
const requested = args.ports === undefined || String(args.ports).trim() === '' ? '22,80,443,445,3389,8080' : String(args.ports)
|
|
132
203
|
const expanded = expandPorts(requested)
|
|
133
204
|
if (expanded === null) {
|
|
134
|
-
return `unparsable port spec ${JSON.stringify(requested)} — use a comma list with optional ranges, e.g. 22,80,8000-8010`
|
|
205
|
+
return envelope({ requested, dump: `unparsable port spec ${JSON.stringify(requested)} — use a comma list with optional ranges, e.g. 22,80,8000-8010` }, ['端口规格不合法 ⇒ 一个端口都没扫:observed.dump 里是解析失败的原因与可照抄的写法'])
|
|
206
|
+
}
|
|
207
|
+
const nmap = await runCmd(shell, ['nmap', '-Pn', '--open', '-p', expanded.join(','), host], exec, { timeoutMs: 60_000 })
|
|
208
|
+
if (nmap.exitCode === 0) {
|
|
209
|
+
return envelope({ method: 'nmap', dump: nmap.stdout }, [
|
|
210
|
+
'nmap 的表格原样返回,本工具不改写它;本轮参数只带 -Pn --open(没有版本探测)',
|
|
211
|
+
'open 只说明 TCP 连得上,不说明服务身份或版本',
|
|
212
|
+
])
|
|
135
213
|
}
|
|
136
|
-
const nmap = await runCmd(shell, ['nmap', '-Pn', '--open', '-p', expanded.join(','), args.host], exec, { timeoutMs: 60_000 })
|
|
137
|
-
if (nmap.exitCode === 0) return nmap.stdout
|
|
138
214
|
if (!portMissing(nmap)) return requireOk(nmap, 'nmap')
|
|
139
215
|
// `nmap` is not installed by default on macOS. Instead of failing outright
|
|
140
216
|
// (the previous behaviour) or returning an empty scan that reads as "target
|
|
141
217
|
// closed", scan with `nc`, which ships with the base system. The result is
|
|
142
218
|
// labelled: nc cannot tell closed from filtered, and does no fingerprinting.
|
|
143
219
|
if (expanded.length > 64) {
|
|
144
|
-
return [
|
|
220
|
+
return envelope({ method: 'nc', scanned: 0, cap: 64, requested: expanded.length, dump: [
|
|
145
221
|
`nmap is not installed on this host: the built-in fallback scans with the system nc and is capped at 64 ports per call, but ${expanded.length} were requested`,
|
|
146
222
|
'narrow the range, or install nmap for a full scan (no 64-port cap, service and version detection)',
|
|
147
|
-
].join('\n')
|
|
223
|
+
].join('\n') }, [
|
|
224
|
+
'请求超过内置回退的 64 端口上限 ⇒ **一个端口都没扫**(不是"扫了、没有开着的端口")',
|
|
225
|
+
])
|
|
148
226
|
}
|
|
149
227
|
const script = expanded
|
|
150
|
-
.map((port) => `if nc -z -G 1 -w 1 ${shq(
|
|
228
|
+
.map((port) => `if nc -z -G 1 -w 1 ${shq(host)} ${port} >/dev/null 2>&1; then echo '${port}/tcp open'; else echo '${port}/tcp no-response'; fi`)
|
|
151
229
|
.join('; ')
|
|
152
230
|
const scan = await runCmd(shell, ['/bin/sh', '-c', script], exec, { timeoutMs: 120_000 })
|
|
153
231
|
if (scan.exitCode !== 0 && scan.stdout.trim() === '') return requireOk(scan, 'nc -z')
|
|
154
232
|
const rows = scan.stdout.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
155
|
-
return [
|
|
233
|
+
return envelope({ method: 'nc', scanned: expanded.length, dump: [
|
|
156
234
|
'nmap=not installed (scanned with the system nc: one TCP connect per port, no service or version detection)',
|
|
157
|
-
`host=${
|
|
235
|
+
`host=${host}`,
|
|
158
236
|
`ports=${expanded.length}`,
|
|
159
237
|
...rows,
|
|
160
238
|
`open=${rows.filter((l) => l.endsWith('/tcp open')).length}`,
|
|
161
239
|
'note: `no-response` means closed or filtered — nc cannot tell those two apart; install nmap for a real scan',
|
|
162
|
-
].join('\n')
|
|
240
|
+
].join('\n') }, [
|
|
241
|
+
'nmap 不在本机:这是系统 nc 的回退结果(每端口一次 TCP connect,无服务/版本探测)',
|
|
242
|
+
'**nc 分不清 closed 与 filtered**:no-response 两种都可能 —— 要区分必须换 nmap',
|
|
243
|
+
])
|
|
163
244
|
},
|
|
164
245
|
}))
|
|
165
246
|
|
|
@@ -174,11 +255,31 @@ export function apply(ctx) {
|
|
|
174
255
|
render: (_a, v) => [{ type: 'text', text: v }],
|
|
175
256
|
},
|
|
176
257
|
async execute(args, exec) {
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
258
|
+
// 证据形状(规划 §5.1):两支都产出信封。
|
|
259
|
+
const envelope = (observed, limitations) => JSON.stringify({
|
|
260
|
+
tool: 'recon_http_fingerprint',
|
|
261
|
+
target: args.url,
|
|
262
|
+
operation: 'one-shot HTTP fingerprint: response headers + <title> only',
|
|
263
|
+
observed,
|
|
264
|
+
confidence: 'observed',
|
|
265
|
+
limitations,
|
|
266
|
+
}, null, 2)
|
|
267
|
+
// 先校验再构造命令:non-http 目标在**命令构造之前**就被拒(不是事后清洗)。
|
|
268
|
+
const url = requireHttpUrl(args.url)
|
|
269
|
+
const headers = requireOk(await runCmd(shell, ['curl', '-sI', '-m', '8', url], exec), `curl -sI ${url}`)
|
|
270
|
+
const body = await runCmd(shell, ['curl', '-s', '-m', '8', url], exec)
|
|
271
|
+
if (body.exitCode !== 0) {
|
|
272
|
+
return envelope({ headers: headers.trim(), title: null, bodyFetched: false, dump: `${headers.trim()}\nTITLE: unknown (body fetch failed, exit ${body.exitCode})` }, [
|
|
273
|
+
'body 取不到 ⇒ 只有响应头;title 是**未知**,不是「这个页面没有标题」',
|
|
274
|
+
'curl -sI 不跟随重定向:看到的是**跳转前那一跳**的响应头',
|
|
275
|
+
])
|
|
276
|
+
}
|
|
180
277
|
const title = /<title[^>]*>([^<]*)<\/title>/iu.exec(body.stdout)?.[1]?.trim() ?? ''
|
|
181
|
-
return `${headers.trim()}\nTITLE: ${title === '' ? '(no <title> element in the body)' : title}`
|
|
278
|
+
return envelope({ headers: headers.trim(), title, bodyFetched: true, dump: `${headers.trim()}\nTITLE: ${title === '' ? '(no <title> element in the body)' : title}` }, [
|
|
279
|
+
'只读响应头与第一个 <title>:**不执行 JS** —— 由 JS 搭出来的界面(登录框、动态面板、JS 表格)在这把尺子里等于不存在',
|
|
280
|
+
'要判 JS 渲染出来的东西,得用真浏览器(仓库有 playwright-core),并断「页面实际请求了什么、状态码是什么」',
|
|
281
|
+
'标题取的是第一个 <title>;SPA 或没有 title 的页面会给 (no <title> element in the body)',
|
|
282
|
+
])
|
|
182
283
|
},
|
|
183
284
|
}))
|
|
184
285
|
}
|
|
@@ -18,7 +18,28 @@ function shq(value) {
|
|
|
18
18
|
* `TypeError: shell.run is not a function` on every call, in every engine
|
|
19
19
|
* version. `scripts/verify-security-tools.mjs` now fails loudly if a call site
|
|
20
20
|
* names a seam method the executor does not have.
|
|
21
|
+
*
|
|
22
|
+
* **失败面的口径是"大声"**(Q5):
|
|
23
|
+
* · `objdump -T` 读不出动态符号表(Mach-O 常态)⇒ 换 `nm -u` 这条**回退**,并把"这是回退路径、
|
|
24
|
+
* nm 只报未定义的名字、不说是从哪个库来的"写进结果的 `limitations`;
|
|
25
|
+
* · **两把尺子都没有** ⇒ 明说"没有可读的未定义符号",并附上两条命令的 exit 与 stderr,
|
|
26
|
+
* 绝不说成"这个程序不依赖任何外部符号"(静态链接/已 strip 正是这个形状);
|
|
27
|
+
* · `ROPgadget` 不在 ⇒ 用内置扫描器(只认 pop/leave + ret 的简单序列),并在结果里写明
|
|
28
|
+
* "**没找到 ≠ 没有**";`objdump` 缺失时 `requireOk` 抛错,不给空反汇编。
|
|
21
29
|
*/
|
|
30
|
+
/**
|
|
31
|
+
* 路径参数规范化:以 `-` 开头的路径加 `./` 前缀(2026-10-13)。
|
|
32
|
+
*
|
|
33
|
+
* 为什么不插 `--`:实测本机(macOS,发布目标平台)**objdump(-h/-d/-T)/ base64 -i / otool -hv 都不支持
|
|
34
|
+
* `--`**,插进去直接失败;而 `file -b --version` 会打印 `file-5.41`、`nm -u --version` 会打印
|
|
35
|
+
* `llvm-nm, …`,两者都 **exit 0** ⇒ 工具会把版本串当**数据**返回(`bin_sections { path: '--version' }`
|
|
36
|
+
* 真发生过)。加 `./` 之后它就是一个普通相对路径:不存在就正常报错,存在就是那个文件。
|
|
37
|
+
*/
|
|
38
|
+
function safePath(value) {
|
|
39
|
+
const text = String(value ?? '')
|
|
40
|
+
return text.startsWith('-') ? './' + text : text
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
async function runCmd(shell, argv, exec, opts = {}) {
|
|
23
44
|
const result = await (await shell.execute(shell.resolve({
|
|
24
45
|
command: argv.map(shq).join(' '),
|
|
@@ -49,9 +70,19 @@ async function runCmd(shell, argv, exec, opts = {}) {
|
|
|
49
70
|
*/
|
|
50
71
|
function requireOk(run, what) {
|
|
51
72
|
if (run.exitCode === 0) return run.stdout
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
// 沙箱拒绝的形状是**结果正文里的标记**(`[sandbox: file access denied under <mode> mode]`,
|
|
74
|
+
// 抄自 @deepseek-ai/dsh-sandbox/index.js:65)—— 它落在 stdout 上;只读 stderr 就会把它丢掉,
|
|
75
|
+
// 于是模型读到的是「命令失败」,而不是「你没有被授权」。所以标记必须原样带进错误文案。
|
|
76
|
+
const sandboxMark = /^\[sandbox: .*\]$/mu.exec(run.stdout)
|
|
77
|
+
const detail = [sandboxMark?.[0], ...run.stderr.trim().split('\n').map(line => line.trim()).filter(Boolean)].slice(0, 3).join(' | ')
|
|
78
|
+
const notInstalled = run.exitCode === 127 || /command not found/iu.test(run.stderr)
|
|
79
|
+
const missing = notInstalled ? ' — the command is not installed on this host' : ''
|
|
80
|
+
// 失败文案要对**读它的模型**可行动(Q6):不是裸 stderr,但也不许臆断原因 ——
|
|
81
|
+
// 只给「这条 exit 属于哪一类」与「下一步可以试什么」,具体判断仍留给读的人。
|
|
82
|
+
const next = notInstalled
|
|
83
|
+
? 'next: this host lacks that command — look for a built-in fallback in this package, or read the file another way'
|
|
84
|
+
: 'next: check the path, and that the file is what this tool expects (bin_file shows what it is); the line above is the command own reason'
|
|
85
|
+
const error = new Error(`${what} failed (exit ${run.exitCode})${missing}: ${run.command}${detail === '' ? '' : `\n${detail}\n${next}`}`)
|
|
55
86
|
error.name = 'CommandError'
|
|
56
87
|
throw error
|
|
57
88
|
}
|
|
@@ -103,33 +134,67 @@ export function apply(ctx) {
|
|
|
103
134
|
render: (_a, v) => [{ type: 'text', text: v }],
|
|
104
135
|
},
|
|
105
136
|
async execute(args, exec) {
|
|
106
|
-
const
|
|
137
|
+
const path = safePath(args.path)
|
|
138
|
+
const kindRun = await runCmd(shell, ['file', '-b', path], exec)
|
|
107
139
|
const kind = kindRun.exitCode === 0 ? kindRun.stdout.trim() : ''
|
|
108
|
-
const dynamic = await runCmd(shell, ['objdump', '-T',
|
|
140
|
+
const dynamic = await runCmd(shell, ['objdump', '-T', path], exec)
|
|
109
141
|
const dynamicLines = dynamic.exitCode === 0
|
|
110
142
|
? dynamic.stdout.split('\n').filter((l) => l.includes('DF') && l.includes('*UND*')).map((l) => l.trim())
|
|
111
143
|
: []
|
|
112
|
-
if (dynamicLines.length > 0)
|
|
144
|
+
if (dynamicLines.length > 0) {
|
|
145
|
+
return JSON.stringify({
|
|
146
|
+
tool: 'rev_imports',
|
|
147
|
+
target: args.path,
|
|
148
|
+
operation: 'list undefined (imported) symbols: objdump -T (DF *UND*)',
|
|
149
|
+
observed: { method: 'objdump', shown: Math.min(dynamicLines.length, 300), dump: dynamicLines.slice(0, 300).join('\n') },
|
|
150
|
+
confidence: 'observed',
|
|
151
|
+
limitations: [
|
|
152
|
+
'只列**动态**符号表(.dynsym)里的未定义项:静态链接进来的、或本地定义的符号不在里面',
|
|
153
|
+
dynamicLines.length > 300 ? '只给前 300 条(要全量就自己按符号名筛)' : '这次没有截断(未定义符号 ≤ 300)',
|
|
154
|
+
],
|
|
155
|
+
}, null, 2)
|
|
156
|
+
}
|
|
113
157
|
// `objdump -T` exits 0 with a bare table header for Mach-O, and a stripped
|
|
114
158
|
// ELF may have no dynamic symbol table at all. `nm -u` is the portable
|
|
115
159
|
// second opinion (it ships with macOS and the Xcode CLT).
|
|
116
|
-
const nm = await runCmd(shell, ['nm', '-u',
|
|
160
|
+
const nm = await runCmd(shell, ['nm', '-u', path], exec, { stdoutMaxBytes: 4194304 })
|
|
117
161
|
const undefinedSymbols = nm.exitCode === 0
|
|
118
162
|
? nm.stdout.split('\n').map((l) => l.trim()).filter((l) => /^_?[A-Za-z_][\w$.@]*$/u.test(l))
|
|
119
163
|
: []
|
|
120
164
|
if (undefinedSymbols.length > 0) {
|
|
121
|
-
return
|
|
165
|
+
return JSON.stringify({
|
|
166
|
+
tool: 'rev_imports',
|
|
167
|
+
target: args.path,
|
|
168
|
+
operation: 'list undefined (imported) symbols: nm -u (objdump -T had no dynamic table)',
|
|
169
|
+
observed: { method: 'nm -u', format: kind === '' ? null : kind, shown: Math.min(undefinedSymbols.length, 300), dump: [
|
|
122
170
|
`${kind === '' ? 'format unknown' : kind}: no ELF dynamic symbol table to read, so these are the undefined symbols from nm -u (${undefinedSymbols.length}):`,
|
|
123
171
|
...undefinedSymbols.slice(0, 300),
|
|
124
|
-
|
|
172
|
+
].join('\n') },
|
|
173
|
+
confidence: 'observed',
|
|
174
|
+
limitations: [
|
|
175
|
+
'走的是 nm -u 这条**回退**路径:objdump -T 在这个文件上没有动态符号表(Mach-O 常见)',
|
|
176
|
+
'nm -u 只报"未定义"的名字,不告诉你是从哪个库来的(要库映射得看 otool -L / readelf -d)',
|
|
177
|
+
],
|
|
178
|
+
}, null, 2)
|
|
125
179
|
}
|
|
126
|
-
return
|
|
180
|
+
return JSON.stringify({
|
|
181
|
+
tool: 'rev_imports',
|
|
182
|
+
target: args.path,
|
|
183
|
+
operation: 'list undefined (imported) symbols: both objdump -T and nm -u came back empty',
|
|
184
|
+
observed: { method: null, format: kind === '' ? null : kind, shown: 0, dump: [
|
|
127
185
|
`no undefined symbols for ${args.path}: objdump -T (exit ${dynamic.exitCode}) and nm -u (exit ${nm.exitCode}) both came back empty`,
|
|
128
186
|
`format: ${kind === '' ? 'unknown (file(1) reported nothing)' : kind}`,
|
|
129
187
|
`nm -u stderr: ${nm.stderr.trim().slice(0, 200) || '(empty)'}`,
|
|
130
188
|
`objdump -T output follows (${dynamic.stdout.trim().length} chars), so the emptiness is inspectable:`,
|
|
131
189
|
dynamic.stdout.trim().slice(0, 2000),
|
|
132
|
-
|
|
190
|
+
].join('\n') },
|
|
191
|
+
confidence: 'observed',
|
|
192
|
+
limitations: [
|
|
193
|
+
'**两把尺子都没给出符号**:这句话的前提是 objdump -T 与 nm -u **都跑过了**(两条 exit 与 nm 的 stderr 就在 dump 里)',
|
|
194
|
+
'它说的是「这份文件里没有可读的未定义符号」,不是「这个程序不依赖任何外部符号」—— 静态链接或已 strip 的二进制正是这个形状',
|
|
195
|
+
'要继续追依赖,先换个体:bin_file 看格式、bin_readelf 看动态段(.dynamic / NEEDED)',
|
|
196
|
+
],
|
|
197
|
+
}, null, 2)
|
|
133
198
|
},
|
|
134
199
|
}))
|
|
135
200
|
|
|
@@ -146,10 +211,23 @@ export function apply(ctx) {
|
|
|
146
211
|
render: (_a, v) => [{ type: 'text', text: v }],
|
|
147
212
|
},
|
|
148
213
|
async execute(args, exec) {
|
|
149
|
-
const
|
|
214
|
+
const path = safePath(args.path)
|
|
215
|
+
const argv = ['-d', path]
|
|
150
216
|
if (args.start && args.stop) argv.splice(1, 0, `--start-address=${args.start}`, `--stop-address=${args.stop}`)
|
|
151
217
|
const stdout = requireOk(await runCmd(shell, ['objdump', ...argv], exec, { stdoutMaxBytes: 1048576 }), 'objdump -d')
|
|
152
|
-
|
|
218
|
+
// 证据形状(规划 §5.1):信封 JSON,objdump 输出原样进 observed.dump。
|
|
219
|
+
return JSON.stringify({
|
|
220
|
+
tool: 'rev_disasm',
|
|
221
|
+
target: args.path,
|
|
222
|
+
operation: 'objdump -d' + (args.start && args.stop ? ` --start-address=${args.start} --stop-address=${args.stop}` : ' (whole file)'),
|
|
223
|
+
observed: { window: args.start && args.stop ? { start: args.start, stop: args.stop } : null, truncated: stdout.length > 12000, dump: stdout.slice(0, 12000) },
|
|
224
|
+
confidence: 'observed',
|
|
225
|
+
limitations: [
|
|
226
|
+
stdout.length > 12000 ? '只给前 12000 字符(truncated=true)—— 要看后面请给 start/stop 缩小窗口' : '这次没有截断(输出 ≤ 12000 字符)',
|
|
227
|
+
'反汇编是**线性扫描**的结果:数据段夹在代码里时,那些字节也会被当成指令打出来(用 start/stop 框住函数能少踩这个)',
|
|
228
|
+
'不看控制流、不做函数边界识别(要那些得换真正的反编译器)',
|
|
229
|
+
],
|
|
230
|
+
}, null, 2)
|
|
153
231
|
},
|
|
154
232
|
}))
|
|
155
233
|
|
|
@@ -165,14 +243,50 @@ export function apply(ctx) {
|
|
|
165
243
|
render: (_a, v) => [{ type: 'text', text: v }],
|
|
166
244
|
},
|
|
167
245
|
async execute(args, exec) {
|
|
168
|
-
const
|
|
246
|
+
const path = safePath(args.path)
|
|
247
|
+
const argv = ['--binary', path, '--only', 'pop|ret|leave']
|
|
169
248
|
if (args.filter) argv.push('--filter', args.filter)
|
|
170
249
|
const rop = await runCmd(shell, ['ROPgadget', ...argv], exec)
|
|
171
|
-
if (rop.exitCode === 0)
|
|
172
|
-
|
|
250
|
+
if (rop.exitCode === 0) {
|
|
251
|
+
return JSON.stringify({
|
|
252
|
+
tool: 'rev_gadget_scan',
|
|
253
|
+
target: args.path,
|
|
254
|
+
operation: 'ROPgadget --only pop|ret|leave' + (args.filter ? ` --filter ${args.filter}` : ''),
|
|
255
|
+
observed: { method: 'ROPgadget', shown: null, truncated: rop.stdout.length > 8000, dump: rop.stdout.slice(0, 8000) },
|
|
256
|
+
confidence: 'observed',
|
|
257
|
+
limitations: [
|
|
258
|
+
'ROPgadget 的输出原样返回(本工具只做截断:>8000 字符时 truncated=true)',
|
|
259
|
+
'ROPgadget 自己会做指令边界识别与去重,但**只扫 --only pop|ret|leave 这几类**(要别的指令得改本工具)',
|
|
260
|
+
],
|
|
261
|
+
}, null, 2)
|
|
262
|
+
}
|
|
263
|
+
const disasm = requireOk(await runCmd(shell, ['objdump', '-d', path], exec, { stdoutMaxBytes: 1048576 }), 'objdump -d')
|
|
173
264
|
const gadgets = scanGadgets(disasm, args.filter)
|
|
174
|
-
if (gadgets.length > 0)
|
|
175
|
-
|
|
265
|
+
if (gadgets.length > 0) {
|
|
266
|
+
return JSON.stringify({
|
|
267
|
+
tool: 'rev_gadget_scan',
|
|
268
|
+
target: args.path,
|
|
269
|
+
operation: 'built-in scanner over objdump -d (ROPgadget not installed)' + (args.filter ? ` filtered by ${args.filter}` : ''),
|
|
270
|
+
observed: { method: 'builtin', shown: Math.min(gadgets.length, 80), truncated: gadgets.length > 80, dump: gadgets.slice(0, 80).join('\n') },
|
|
271
|
+
confidence: 'observed',
|
|
272
|
+
limitations: [
|
|
273
|
+
'走的是**内置扫描器**(本机没有 ROPgadget):它只在 objdump 的反汇编里找 pop/leave + 紧跟的 ret 这类**简单序列**',
|
|
274
|
+
'不做指令边界校验、不做 gadget 去重:地址可能重叠,长链 gadget(多指令 + 中间跳转)它也不会给',
|
|
275
|
+
gadgets.length > 80 ? '只给前 80 条(observed.truncated=true)' : '这次没有截断(≤ 80 条)',
|
|
276
|
+
],
|
|
277
|
+
}, null, 2)
|
|
278
|
+
}
|
|
279
|
+
return JSON.stringify({
|
|
280
|
+
tool: 'rev_gadget_scan',
|
|
281
|
+
target: args.path,
|
|
282
|
+
operation: 'built-in scanner over objdump -d: nothing matched',
|
|
283
|
+
observed: { method: 'builtin', shown: 0, dump: `no pop/leave/ret gadgets found in ${args.path} (install ROPgadget for a deeper scan)` },
|
|
284
|
+
confidence: 'observed',
|
|
285
|
+
limitations: [
|
|
286
|
+
'**没找到 ≠ 没有**:这条走的是内置扫描器(只认 pop/leave + ret 的简单序列),装 ROPgadget 后同一条命令可能给出几十条',
|
|
287
|
+
'也不排除这个文件根本没有可执行段(比如被测的是数据文件)—— 先用 bin_file 看它是什么',
|
|
288
|
+
],
|
|
289
|
+
}, null, 2)
|
|
176
290
|
},
|
|
177
291
|
}))
|
|
178
292
|
}
|