@dsh-fly/dsh-plugin-fly-mode 0.0.0-stage → 0.1.3
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/LICENSE +21 -0
- package/README.md +79 -2
- package/cordis.patch.yml +399 -0
- package/package.json +32 -4
- package/tools/dsh-plugin-fly-binary/index.js +520 -0
- package/tools/dsh-plugin-fly-binary/package.json +23 -0
- package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/SKILL.md +163 -0
- package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/code/mitm_addon_template.py +302 -0
- package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/code/rules.example.json +52 -0
- package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/reference/case-ttw.md +157 -0
- package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/reference/checklist.md +133 -0
- package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/reference/playbook.md +228 -0
- package/tools/dsh-plugin-fly-blackbox/assets/client-blackbox/reference/rules.schema.md +76 -0
- package/tools/dsh-plugin-fly-blackbox/index.js +1453 -0
- package/tools/dsh-plugin-fly-blackbox/package.json +31 -0
- package/tools/dsh-plugin-fly-diag/index.js +772 -0
- package/tools/dsh-plugin-fly-diag/package.json +24 -0
- package/tools/dsh-plugin-fly-diag/wifi.js +402 -0
- package/tools/dsh-plugin-fly-guard/index.js +101 -0
- package/tools/dsh-plugin-fly-guard/package.json +26 -0
- package/tools/dsh-plugin-fly-pwn/index.js +583 -0
- package/tools/dsh-plugin-fly-pwn/package.json +23 -0
- package/tools/dsh-plugin-fly-recon/index.js +184 -0
- package/tools/dsh-plugin-fly-recon/package.json +23 -0
- package/tools/dsh-plugin-fly-reverse/index.js +178 -0
- package/tools/dsh-plugin-fly-reverse/package.json +23 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* fly-recon — policy-gated recon tools.
|
|
5
|
+
* Default posture: deny unless FLY_ALLOW_RECON=1 (authorization explicit).
|
|
6
|
+
*
|
|
7
|
+
* One apply() registers BOTH the `tools/pre-execute` guard (a Waterfall whose
|
|
8
|
+
* decision is { kind: "allow" | "deny" }) and the two recon tools. It must be
|
|
9
|
+
* mounted exactly once — as a single agent-preset row — or the tools and the
|
|
10
|
+
* guard collide with themselves in the process-global tool scope.
|
|
11
|
+
*
|
|
12
|
+
* Command execution goes through the host `shell` seam so the sandbox confines
|
|
13
|
+
* it (and the FLY_ALLOW_RECON gate is an ADDITIONAL authorization layer on top
|
|
14
|
+
* of the sandbox, not a replacement for it).
|
|
15
|
+
*/
|
|
16
|
+
export const name = 'fly-recon'
|
|
17
|
+
export const inject = ['tools', 'shell']
|
|
18
|
+
|
|
19
|
+
function shq(value) {
|
|
20
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run one confined command through the host `shell` seam.
|
|
25
|
+
*
|
|
26
|
+
* `ShellExecutor` exposes exactly `resolve()` and `execute()` (verified against
|
|
27
|
+
* 0.1.7-rc.2 and 0.2.0-rc.2): `execute()` returns a process handle whose `result`
|
|
28
|
+
* projection carries the run result. An earlier revision of this file awaited a
|
|
29
|
+
* nonexistent `shell.run()`, so every shell-backed tool threw
|
|
30
|
+
* `TypeError: shell.run is not a function` on every call, in every engine
|
|
31
|
+
* version. `scripts/verify-security-tools.mjs` now fails loudly if a call site
|
|
32
|
+
* names a seam method the executor does not have.
|
|
33
|
+
*/
|
|
34
|
+
async function runCmd(shell, argv, exec, opts = {}) {
|
|
35
|
+
const result = await (await shell.execute(shell.resolve({
|
|
36
|
+
command: argv.map(shq).join(' '),
|
|
37
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
|
|
38
|
+
...opts.stdoutMaxBytes !== void 0 ? { stdoutMaxBytes: opts.stdoutMaxBytes } : {},
|
|
39
|
+
signal: exec.signal,
|
|
40
|
+
}))).result()
|
|
41
|
+
if (result.aborted) {
|
|
42
|
+
const error = new Error('tool call aborted')
|
|
43
|
+
error.name = 'AbortError'
|
|
44
|
+
throw error
|
|
45
|
+
}
|
|
46
|
+
if (result.sandbox?.denied) {
|
|
47
|
+
const error = new Error(`[sandbox: file access denied under ${result.sandbox.mode} mode]`)
|
|
48
|
+
error.name = 'SandboxError'
|
|
49
|
+
throw error
|
|
50
|
+
}
|
|
51
|
+
return { stdout: result.stdout.text, stderr: result.stderr.text, exitCode: result.exitCode, command: argv.join(' ') }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A failed command must never come back as an empty success.
|
|
56
|
+
*
|
|
57
|
+
* `nmap` and `curl` are optional on this host; when they are missing the recon
|
|
58
|
+
* tools must say so instead of reporting an empty scan, because an empty scan
|
|
59
|
+
* reads as "target closed".
|
|
60
|
+
*/
|
|
61
|
+
function requireOk(run, what) {
|
|
62
|
+
if (run.exitCode === 0) return run.stdout
|
|
63
|
+
const detail = run.stderr.trim().split('\n').map(line => line.trim()).filter(Boolean).slice(0, 3).join(' | ')
|
|
64
|
+
const missing = run.exitCode === 127 || /command not found/iu.test(run.stderr) ? ' — the command is not installed on this host' : ''
|
|
65
|
+
const error = new Error(`${what} failed (exit ${run.exitCode})${missing}: ${run.command}${detail === '' ? '' : `\n${detail}`}`)
|
|
66
|
+
error.name = 'CommandError'
|
|
67
|
+
throw error
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Expand `22,80,8000-8010` into a port list; null when the spec is unreadable, so
|
|
72
|
+
* the tool can say that instead of scanning nothing.
|
|
73
|
+
*/
|
|
74
|
+
function expandPorts(spec) {
|
|
75
|
+
const ports = []
|
|
76
|
+
for (const part of String(spec).split(',')) {
|
|
77
|
+
const token = part.trim()
|
|
78
|
+
if (token === '') continue
|
|
79
|
+
const range = /^(\d{1,5})-(\d{1,5})$/u.exec(token)
|
|
80
|
+
if (range !== null) {
|
|
81
|
+
const from = Number(range[1])
|
|
82
|
+
const to = Number(range[2])
|
|
83
|
+
if (from < 1 || to > 65535 || from > to) return null
|
|
84
|
+
for (let port = from; port <= to; port++) ports.push(port)
|
|
85
|
+
} else if (/^\d{1,5}$/u.test(token)) {
|
|
86
|
+
const port = Number(token)
|
|
87
|
+
if (port < 1 || port > 65535) return null
|
|
88
|
+
ports.push(port)
|
|
89
|
+
} else {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
if (ports.length > 4096) return null
|
|
93
|
+
}
|
|
94
|
+
return ports.length === 0 ? null : [...new Set(ports)]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Exit 127 / "command not found": the optional tool this host would need is absent. */
|
|
98
|
+
function portMissing(run) {
|
|
99
|
+
return run.exitCode === 127 || /command not found/iu.test(run.stderr)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function reconAllowed() {
|
|
103
|
+
return process.env.FLY_ALLOW_RECON === '1'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function apply(ctx) {
|
|
107
|
+
const shell = ctx.shell
|
|
108
|
+
|
|
109
|
+
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
110
|
+
if (exec.name?.startsWith('recon_') && !reconAllowed()) {
|
|
111
|
+
return {
|
|
112
|
+
kind: 'deny',
|
|
113
|
+
reason: 'Recon tools require explicit authorization. Set FLY_ALLOW_RECON=1 for authorized lab targets only.',
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return next()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
ctx.tools.register(defineTool({
|
|
120
|
+
name: 'recon_ports',
|
|
121
|
+
description: 'TCP connect scan of ports on a lab target: nmap when installed, else the system nc (authorized use only).',
|
|
122
|
+
parameters: {
|
|
123
|
+
host: { type: 'string', required: true },
|
|
124
|
+
ports: { type: 'string', description: 'Comma list with optional ranges, default top common' },
|
|
125
|
+
},
|
|
126
|
+
output: {
|
|
127
|
+
schema: { type: 'string' },
|
|
128
|
+
render: (_a, v) => [{ type: 'text', text: v }],
|
|
129
|
+
},
|
|
130
|
+
async execute(args, exec) {
|
|
131
|
+
const requested = args.ports === undefined || String(args.ports).trim() === '' ? '22,80,443,445,3389,8080' : String(args.ports)
|
|
132
|
+
const expanded = expandPorts(requested)
|
|
133
|
+
if (expanded === null) {
|
|
134
|
+
return `unparsable port spec ${JSON.stringify(requested)} — use a comma list with optional ranges, e.g. 22,80,8000-8010`
|
|
135
|
+
}
|
|
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
|
+
if (!portMissing(nmap)) return requireOk(nmap, 'nmap')
|
|
139
|
+
// `nmap` is not installed by default on macOS. Instead of failing outright
|
|
140
|
+
// (the previous behaviour) or returning an empty scan that reads as "target
|
|
141
|
+
// closed", scan with `nc`, which ships with the base system. The result is
|
|
142
|
+
// labelled: nc cannot tell closed from filtered, and does no fingerprinting.
|
|
143
|
+
if (expanded.length > 64) {
|
|
144
|
+
return [
|
|
145
|
+
`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
|
+
'narrow the range, or install nmap for a full scan (no 64-port cap, service and version detection)',
|
|
147
|
+
].join('\n')
|
|
148
|
+
}
|
|
149
|
+
const script = expanded
|
|
150
|
+
.map((port) => `if nc -z -G 1 -w 1 ${shq(args.host)} ${port} >/dev/null 2>&1; then echo '${port}/tcp open'; else echo '${port}/tcp no-response'; fi`)
|
|
151
|
+
.join('; ')
|
|
152
|
+
const scan = await runCmd(shell, ['/bin/sh', '-c', script], exec, { timeoutMs: 120_000 })
|
|
153
|
+
if (scan.exitCode !== 0 && scan.stdout.trim() === '') return requireOk(scan, 'nc -z')
|
|
154
|
+
const rows = scan.stdout.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
155
|
+
return [
|
|
156
|
+
'nmap=not installed (scanned with the system nc: one TCP connect per port, no service or version detection)',
|
|
157
|
+
`host=${args.host}`,
|
|
158
|
+
`ports=${expanded.length}`,
|
|
159
|
+
...rows,
|
|
160
|
+
`open=${rows.filter((l) => l.endsWith('/tcp open')).length}`,
|
|
161
|
+
'note: `no-response` means closed or filtered — nc cannot tell those two apart; install nmap for a real scan',
|
|
162
|
+
].join('\n')
|
|
163
|
+
},
|
|
164
|
+
}))
|
|
165
|
+
|
|
166
|
+
ctx.tools.register(defineTool({
|
|
167
|
+
name: 'recon_http_fingerprint',
|
|
168
|
+
description: 'Fetch HTTP headers and title from a target URL (authorized lab only).',
|
|
169
|
+
parameters: {
|
|
170
|
+
url: { type: 'string', required: true },
|
|
171
|
+
},
|
|
172
|
+
output: {
|
|
173
|
+
schema: { type: 'string' },
|
|
174
|
+
render: (_a, v) => [{ type: 'text', text: v }],
|
|
175
|
+
},
|
|
176
|
+
async execute(args, exec) {
|
|
177
|
+
const headers = requireOk(await runCmd(shell, ['curl', '-sI', '-m', '8', args.url], exec), `curl -sI ${args.url}`)
|
|
178
|
+
const body = await runCmd(shell, ['curl', '-s', '-m', '8', args.url], exec)
|
|
179
|
+
if (body.exitCode !== 0) return `${headers.trim()}\nTITLE: unknown (body fetch failed, exit ${body.exitCode})`
|
|
180
|
+
const title = /<title[^>]*>([^<]*)<\/title>/iu.exec(body.stdout)?.[1]?.trim() ?? ''
|
|
181
|
+
return `${headers.trim()}\nTITLE: ${title === '' ? '(no <title> element in the body)' : title}`
|
|
182
|
+
},
|
|
183
|
+
}))
|
|
184
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-plugin-fly-recon",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js"
|
|
8
|
+
],
|
|
9
|
+
"peerDependencies": {
|
|
10
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
11
|
+
"@deepseek-ai/dsh-shell": "^0.2.0-rc.2",
|
|
12
|
+
"@deepseek-ai/dsh-tools": "^0.2.0-rc.2"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/ethanchen669/dsh-fly.git",
|
|
18
|
+
"directory": "packages/dsh-plugin-fly-recon"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"registry": "https://registry.npmjs.org/"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
|
+
|
|
3
|
+
/** fly-reverse — imports / disasm helpers feeding the Reverse Lab narrative. */
|
|
4
|
+
export const name = 'fly-reverse'
|
|
5
|
+
export const inject = ['tools', 'shell']
|
|
6
|
+
|
|
7
|
+
function shq(value) {
|
|
8
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Run one confined command through the host `shell` seam.
|
|
13
|
+
*
|
|
14
|
+
* `ShellExecutor` exposes exactly `resolve()` and `execute()` (verified against
|
|
15
|
+
* 0.1.7-rc.2 and 0.2.0-rc.2): `execute()` returns a process handle whose `result`
|
|
16
|
+
* projection carries the run result. An earlier revision of this file awaited a
|
|
17
|
+
* nonexistent `shell.run()`, so every shell-backed tool threw
|
|
18
|
+
* `TypeError: shell.run is not a function` on every call, in every engine
|
|
19
|
+
* version. `scripts/verify-security-tools.mjs` now fails loudly if a call site
|
|
20
|
+
* names a seam method the executor does not have.
|
|
21
|
+
*/
|
|
22
|
+
async function runCmd(shell, argv, exec, opts = {}) {
|
|
23
|
+
const result = await (await shell.execute(shell.resolve({
|
|
24
|
+
command: argv.map(shq).join(' '),
|
|
25
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
|
|
26
|
+
...opts.stdoutMaxBytes !== void 0 ? { stdoutMaxBytes: opts.stdoutMaxBytes } : {},
|
|
27
|
+
signal: exec.signal,
|
|
28
|
+
}))).result()
|
|
29
|
+
if (result.aborted) {
|
|
30
|
+
const error = new Error('tool call aborted')
|
|
31
|
+
error.name = 'AbortError'
|
|
32
|
+
throw error
|
|
33
|
+
}
|
|
34
|
+
if (result.sandbox?.denied) {
|
|
35
|
+
const error = new Error(`[sandbox: file access denied under ${result.sandbox.mode} mode]`)
|
|
36
|
+
error.name = 'SandboxError'
|
|
37
|
+
throw error
|
|
38
|
+
}
|
|
39
|
+
return { stdout: result.stdout.text, stderr: result.stderr.text, exitCode: result.exitCode, command: argv.join(' ') }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A failed command must never come back as an empty success.
|
|
44
|
+
*
|
|
45
|
+
* Exit 127 (and the shell's "command not found") gets its own wording, because
|
|
46
|
+
* macOS ships no ELF toolchain: `readelf`, `checksec`, `ROPgadget` and `nmap`
|
|
47
|
+
* are simply absent. The old code turned that into "no output", which read
|
|
48
|
+
* exactly like "nothing found".
|
|
49
|
+
*/
|
|
50
|
+
function requireOk(run, what) {
|
|
51
|
+
if (run.exitCode === 0) return run.stdout
|
|
52
|
+
const detail = run.stderr.trim().split('\n').map(line => line.trim()).filter(Boolean).slice(0, 3).join(' | ')
|
|
53
|
+
const missing = run.exitCode === 127 || /command not found/iu.test(run.stderr) ? ' — the command is not installed on this host' : ''
|
|
54
|
+
const error = new Error(`${what} failed (exit ${run.exitCode})${missing}: ${run.command}${detail === '' ? '' : `\n${detail}`}`)
|
|
55
|
+
error.name = 'CommandError'
|
|
56
|
+
throw error
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Gadgets ending in a return, read straight out of `objdump -d`.
|
|
61
|
+
*
|
|
62
|
+
* This is the fallback for hosts without ROPgadget (any stock macOS). The
|
|
63
|
+
* previous fallback demanded `/\bret\b/` and `pop|leave` on the SAME LINE:
|
|
64
|
+
* objdump prints `popq %rdi` and `retq` on separate lines, and `retq` does not
|
|
65
|
+
* match `/\bret\b/` — so a textbook `pop rdi; ret` came back as "no gadgets
|
|
66
|
+
* found" (asserted in scripts/verify-security-tools.mjs).
|
|
67
|
+
*/
|
|
68
|
+
function scanGadgets(disasm, filter) {
|
|
69
|
+
const instruction = /^\s*([0-9a-f]+):\s+(?:[0-9a-f]{2} )+\s*([a-z][a-z0-9.]*)\s*(.*)$/u
|
|
70
|
+
const wanted = filter === undefined ? null : filter.toLowerCase().split(/\s+/u).filter(Boolean)
|
|
71
|
+
const recent = []
|
|
72
|
+
const found = []
|
|
73
|
+
for (const line of disasm.split('\n')) {
|
|
74
|
+
const match = instruction.exec(line)
|
|
75
|
+
if (match === null) continue
|
|
76
|
+
const [, address, mnemonic, operands] = match
|
|
77
|
+
const text = `${mnemonic}${operands.trim() === '' ? '' : ` ${operands.trim()}`}`
|
|
78
|
+
if (mnemonic.startsWith('ret') === false) {
|
|
79
|
+
recent.push({ address, text })
|
|
80
|
+
if (recent.length > 5) recent.shift()
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
if (recent.length === 0) continue
|
|
84
|
+
const gadget = `0x${recent[0].address}: ${recent.map(step => step.text).join(' ; ')} ; ret`
|
|
85
|
+
if (wanted === null || wanted.every(token => gadget.toLowerCase().includes(token))) found.push(gadget)
|
|
86
|
+
recent.length = 0
|
|
87
|
+
if (found.length >= 200) break
|
|
88
|
+
}
|
|
89
|
+
return found
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function apply(ctx) {
|
|
93
|
+
const shell = ctx.shell
|
|
94
|
+
|
|
95
|
+
ctx.tools.register(defineTool({
|
|
96
|
+
name: 'rev_imports',
|
|
97
|
+
description: 'List imported (undefined) symbols: objdump -T for ELF, nm -u for Mach-O and stripped files.',
|
|
98
|
+
parameters: {
|
|
99
|
+
path: { type: 'string', required: true },
|
|
100
|
+
},
|
|
101
|
+
output: {
|
|
102
|
+
schema: { type: 'string' },
|
|
103
|
+
render: (_a, v) => [{ type: 'text', text: v }],
|
|
104
|
+
},
|
|
105
|
+
async execute(args, exec) {
|
|
106
|
+
const kindRun = await runCmd(shell, ['file', '-b', args.path], exec)
|
|
107
|
+
const kind = kindRun.exitCode === 0 ? kindRun.stdout.trim() : ''
|
|
108
|
+
const dynamic = await runCmd(shell, ['objdump', '-T', args.path], exec)
|
|
109
|
+
const dynamicLines = dynamic.exitCode === 0
|
|
110
|
+
? dynamic.stdout.split('\n').filter((l) => l.includes('DF') && l.includes('*UND*')).map((l) => l.trim())
|
|
111
|
+
: []
|
|
112
|
+
if (dynamicLines.length > 0) return dynamicLines.slice(0, 300).join('\n')
|
|
113
|
+
// `objdump -T` exits 0 with a bare table header for Mach-O, and a stripped
|
|
114
|
+
// ELF may have no dynamic symbol table at all. `nm -u` is the portable
|
|
115
|
+
// second opinion (it ships with macOS and the Xcode CLT).
|
|
116
|
+
const nm = await runCmd(shell, ['nm', '-u', args.path], exec, { stdoutMaxBytes: 4194304 })
|
|
117
|
+
const undefinedSymbols = nm.exitCode === 0
|
|
118
|
+
? nm.stdout.split('\n').map((l) => l.trim()).filter((l) => /^_?[A-Za-z_][\w$.@]*$/u.test(l))
|
|
119
|
+
: []
|
|
120
|
+
if (undefinedSymbols.length > 0) {
|
|
121
|
+
return [
|
|
122
|
+
`${kind === '' ? 'format unknown' : kind}: no ELF dynamic symbol table to read, so these are the undefined symbols from nm -u (${undefinedSymbols.length}):`,
|
|
123
|
+
...undefinedSymbols.slice(0, 300),
|
|
124
|
+
].join('\n')
|
|
125
|
+
}
|
|
126
|
+
return [
|
|
127
|
+
`no undefined symbols for ${args.path}: objdump -T (exit ${dynamic.exitCode}) and nm -u (exit ${nm.exitCode}) both came back empty`,
|
|
128
|
+
`format: ${kind === '' ? 'unknown (file(1) reported nothing)' : kind}`,
|
|
129
|
+
`nm -u stderr: ${nm.stderr.trim().slice(0, 200) || '(empty)'}`,
|
|
130
|
+
`objdump -T output follows (${dynamic.stdout.trim().length} chars), so the emptiness is inspectable:`,
|
|
131
|
+
dynamic.stdout.trim().slice(0, 2000),
|
|
132
|
+
].join('\n')
|
|
133
|
+
},
|
|
134
|
+
}))
|
|
135
|
+
|
|
136
|
+
ctx.tools.register(defineTool({
|
|
137
|
+
name: 'rev_disasm',
|
|
138
|
+
description: 'Disassemble a binary or a function window.',
|
|
139
|
+
parameters: {
|
|
140
|
+
path: { type: 'string', required: true },
|
|
141
|
+
start: { type: 'string', description: 'Optional start address, e.g. 0x4011a6' },
|
|
142
|
+
stop: { type: 'string', description: 'Optional stop address' },
|
|
143
|
+
},
|
|
144
|
+
output: {
|
|
145
|
+
schema: { type: 'string' },
|
|
146
|
+
render: (_a, v) => [{ type: 'text', text: v }],
|
|
147
|
+
},
|
|
148
|
+
async execute(args, exec) {
|
|
149
|
+
const argv = ['-d', args.path]
|
|
150
|
+
if (args.start && args.stop) argv.splice(1, 0, `--start-address=${args.start}`, `--stop-address=${args.stop}`)
|
|
151
|
+
const stdout = requireOk(await runCmd(shell, ['objdump', ...argv], exec, { stdoutMaxBytes: 1048576 }), 'objdump -d')
|
|
152
|
+
return stdout.slice(0, 12000)
|
|
153
|
+
},
|
|
154
|
+
}))
|
|
155
|
+
|
|
156
|
+
ctx.tools.register(defineTool({
|
|
157
|
+
name: 'rev_gadget_scan',
|
|
158
|
+
description: 'Scan for simple ROP gadgets with ROPgadget if installed, else a built-in objdump scanner.',
|
|
159
|
+
parameters: {
|
|
160
|
+
path: { type: 'string', required: true },
|
|
161
|
+
filter: { type: 'string', description: 'Optional gadget filter, e.g. "pop rdi"' },
|
|
162
|
+
},
|
|
163
|
+
output: {
|
|
164
|
+
schema: { type: 'string' },
|
|
165
|
+
render: (_a, v) => [{ type: 'text', text: v }],
|
|
166
|
+
},
|
|
167
|
+
async execute(args, exec) {
|
|
168
|
+
const argv = ['--binary', args.path, '--only', 'pop|ret|leave']
|
|
169
|
+
if (args.filter) argv.push('--filter', args.filter)
|
|
170
|
+
const rop = await runCmd(shell, ['ROPgadget', ...argv], exec)
|
|
171
|
+
if (rop.exitCode === 0) return rop.stdout.slice(0, 8000)
|
|
172
|
+
const disasm = requireOk(await runCmd(shell, ['objdump', '-d', args.path], exec, { stdoutMaxBytes: 1048576 }), 'objdump -d')
|
|
173
|
+
const gadgets = scanGadgets(disasm, args.filter)
|
|
174
|
+
if (gadgets.length > 0) return gadgets.slice(0, 80).join('\n')
|
|
175
|
+
return `no pop/leave/ret gadgets found in ${args.path} (install ROPgadget for a deeper scan)`
|
|
176
|
+
},
|
|
177
|
+
}))
|
|
178
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-plugin-fly-reverse",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js"
|
|
8
|
+
],
|
|
9
|
+
"peerDependencies": {
|
|
10
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
11
|
+
"@deepseek-ai/dsh-shell": "^0.2.0-rc.2",
|
|
12
|
+
"@deepseek-ai/dsh-tools": "^0.2.0-rc.2"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/ethanchen669/dsh-fly.git",
|
|
18
|
+
"directory": "packages/dsh-plugin-fly-reverse"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"registry": "https://registry.npmjs.org/"
|
|
22
|
+
}
|
|
23
|
+
}
|