@windypro-rourou/dsh-logcat 0.3.0 → 0.4.0

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.
Files changed (3) hide show
  1. package/README.md +7 -4
  2. package/lib/index.js +282 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,19 +18,22 @@ DSH Web GUI 的安卓实机调试面板(类似 Android Studio 的 Logcat 视
18
18
  - 暂停/继续(暂停时缓冲,恢复自动回放)、清空、复制、导出 .txt
19
19
  - 窗口化渲染 + 自动滚动(滚动手动上翻时自动停用)
20
20
  - 未授权设备提示「请在手机上点击允许 USB 调试」
21
- - **Agent 工具**(共 18 个,全部对 agent 开放,前置提示中已明示可调用):
21
+ - **Agent 工具**(共 21 个,全部对 agent 开放,前置提示中已明示可调用):
22
22
  - 设备:`logcat_devices`(列出设备)、`device_info`(型号/版本/SDK/分辨率/内存/电量)、`device_stats`(CPU/内存/电量实时采样)
23
23
  - 执行:`adb_exec`(shell)、`adb_install`(本地 APK 装真机)、`adb_pull`(拉文件)
24
24
  - 输入:`input_tap` / `input_swipe` / `input_text`(真机 UI 自动化)、`ui_dump`(界面层级 XML)
25
25
  - 日志:`logcat_recent`(按包名/级别/关键词过滤)、`logcat_crash`(崩溃/ANR 自动捕获 + 上下文)、
26
26
  `logcat_set_package`(锁定当前测试包名)
27
27
  - 逆向/内存:`proc_list`(进程列表)、`proc_maps`(内存映射 + so 模块基址)、`proc_status`(进程状态/内存摘要)、
28
- `mem_dump`(指定地址读内存 hex)、`frida_server`(frida-server 部署/启停)
28
+ `proc_smaps`(smaps 明细,Pss Top 区域)、`mem_dump`(指定地址读内存 hex)、
29
+ `mem_search`(内存搜 hex 模式/字符串)、`frida_server`(frida-server 部署/启停)、
30
+ `frida_script`(hook/trace/scan/bypass/dump 常用脚本模板生成)
29
31
  - **实机调试工作流**:构建安卓应用时,agent 的通告会动态列出当前已连接设备(serial + 型号)与当前测试包名,
30
32
  可先向用户确认后:`adb_install` 部署 APK → `adb_exec` 启动 → `logcat_set_package` 锁包 →
31
33
  `logcat_recent` / `logcat_crash` 看崩溃 → `ui_dump` + `input_*` 做界面自动化 → `adb_pull` 取证,闭环真机调试。
32
- - **逆向工作流**:`proc_list` 定位进程 → `proc_maps` 拿模块基址/权限 → `mem_dump` 读目标地址 →
33
- `frida_server` frida 做动态插桩。读其他应用内存/maps 需要 root debuggable 应用(run-as),工具会给出明确提示。
34
+ - **逆向工作流**:`proc_list` 定位进程 → `proc_maps` 拿模块基址 → `mem_dump` 读目标地址 / `mem_search` 搜特征模式
35
+ `proc_smaps` 看内存占用明细 `frida_script` 生成脚本 + `frida_server` frida 做动态插桩。
36
+ 读其他应用内存/maps 需要 root 或 debuggable 应用(run-as),工具会给出明确提示。
34
37
  - **附加能力**:`POST /api/dsh-logcat/exec` 执行 shell、`POST /api/dsh-logcat/package` 设置包名、
35
38
  `GET /api/dsh-logcat/screenshot` 截屏、`POST /api/dsh-logcat/install-adb` 一键装 adb。
36
39
 
package/lib/index.js CHANGED
@@ -17,7 +17,7 @@
17
17
  import { spawn, execFile } from 'node:child_process'
18
18
  import { existsSync } from 'node:fs'
19
19
  import { homedir } from 'node:os'
20
- import { join } from 'node:path'
20
+ import { dirname, join } from 'node:path'
21
21
  import { createInterface } from 'node:readline'
22
22
  import { WebSocket, WebSocketServer } from 'ws'
23
23
  import { defineTool } from '@deepseek-ai/dsh-tools'
@@ -1520,6 +1520,284 @@ function fridaServerTool(engine) {
1520
1520
  })
1521
1521
  }
1522
1522
 
1523
+ /** The mem_search agent tool: search process memory for a byte pattern. */
1524
+ function memSearchTool(engine) {
1525
+ return defineTool({
1526
+ name: 'mem_search',
1527
+ description: 'Search a process\'s memory for a byte pattern — plain ASCII string or hex bytes ("hex:48656c6c6f") — and return byte offsets. ' +
1528
+ 'Requires root (or a debuggable app via run-as). Scans with grep -aob over /proc/<pid>/mem; very large processes can be slow.',
1529
+ parameters: {
1530
+ serial: { type: 'string', description: 'Device serial from logcat_devices (optional; defaults to the first attached device).' },
1531
+ pid: { type: 'integer', description: 'Process id (from proc_list).' },
1532
+ pattern: { type: 'string', description: 'Pattern to search: ASCII string ("hello") or hex bytes ("hex:48656c6c6f").' },
1533
+ maxResults: { type: 'integer', description: 'Max matches to return (default 20).' },
1534
+ },
1535
+ output: {
1536
+ schema: {
1537
+ type: 'object',
1538
+ additionalProperties: false,
1539
+ properties: {
1540
+ pid: { type: 'integer', required: true },
1541
+ pattern: { type: 'string' },
1542
+ offsets: { type: 'array', required: true, items: { type: 'integer' } },
1543
+ truncated: { type: 'boolean' },
1544
+ error: { type: 'string' },
1545
+ },
1546
+ },
1547
+ render: (_args, value) => {
1548
+ if (value?.error) return [{ type: 'text', text: value.error }]
1549
+ const offs = value?.offsets ?? []
1550
+ if (offs.length === 0) return [{ type: 'text', text: `(no match for "${value?.pattern ?? ''}" in pid ${value?.pid ?? '?'})` }]
1551
+ const hex = offs.map((o) => '0x' + o.toString(16)).join('\n')
1552
+ return [{ type: 'text', text: `pattern "${value.pattern}" in pid ${value.pid}:\n${hex}${value.truncated ? '\n(truncated)' : ''}` }]
1553
+ },
1554
+ },
1555
+ async execute(args) {
1556
+ const devices = engine.deviceList()
1557
+ const serial = typeof args.serial === 'string' && args.serial !== '' ? args.serial : devices[0]?.serial ?? ''
1558
+ const pid = Number(args.pid)
1559
+ const pattern = typeof args.pattern === 'string' ? args.pattern.trim() : ''
1560
+ const max = Math.min(Math.max(Number(args.maxResults ?? 20) || 20, 1), 200)
1561
+ if (serial === '' || engine.adb === null || !Number.isInteger(pid) || pid <= 0) return { pid: 0, offsets: [], error: 'a valid pid is required' }
1562
+ if (pattern === '') return { pid, offsets: [], error: 'pattern is required (string or hex:...)' }
1563
+ // Normalize to a byte sequence emitted by printf '\xNN\xNN...'
1564
+ const bytes = pattern.startsWith('hex:')
1565
+ ? pattern.slice(4).replace(/\s+/g, '').match(/[0-9a-f]{2}/gi)?.map((b) => b.toLowerCase()) ?? []
1566
+ : [...Buffer.from(pattern, 'utf8')].map((b) => b.toString(16).padStart(2, '0'))
1567
+ if (bytes.length === 0) return { pid, offsets: [], error: 'could not parse pattern into bytes' }
1568
+ const printfArg = "\\x" + bytes.join("\\x")
1569
+ const sh = `grep -aob -F "$(printf '${printfArg}')" /proc/${pid}/mem 2>&1 | head -${max}`
1570
+ const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', sh], 30000)
1571
+ const out = result.stdout ?? ''
1572
+ if (!result.ok || /Permission denied|No such file|No such process/.test(out)) {
1573
+ return { pid, offsets: [], error: `cannot search pid ${pid} memory (need root / debuggable app); output: ${out.slice(0, 200)}` }
1574
+ }
1575
+ const offsets = []
1576
+ for (const line of out.split(/\r?\n/)) {
1577
+ const m = /^(\d+):/.exec(line)
1578
+ if (m !== null) offsets.push(Number.parseInt(m[1], 10))
1579
+ }
1580
+ return { pid, pattern, offsets, truncated: offsets.length >= max }
1581
+ },
1582
+ })
1583
+ }
1584
+
1585
+ /** The proc_smaps agent tool: per-region memory detail (top Pss regions). */
1586
+ function procSmapsTool(engine) {
1587
+ return defineTool({
1588
+ name: 'proc_smaps',
1589
+ description: 'Read /proc/<pid>/smaps and return the top memory regions by Pss (size/rss/pss per mapping) — detailed memory footprint analysis. ' +
1590
+ 'Requires root or matching permissions.',
1591
+ parameters: {
1592
+ serial: { type: 'string', description: 'Device serial from logcat_devices (optional; defaults to the first attached device).' },
1593
+ pid: { type: 'integer', description: 'Process id (from proc_list).' },
1594
+ top: { type: 'integer', description: 'Number of top regions to return (default 10, max 50).' },
1595
+ },
1596
+ output: {
1597
+ schema: {
1598
+ type: 'object',
1599
+ additionalProperties: false,
1600
+ properties: {
1601
+ pid: { type: 'integer', required: true },
1602
+ regions: {
1603
+ type: 'array',
1604
+ required: true,
1605
+ items: {
1606
+ type: 'object',
1607
+ additionalProperties: false,
1608
+ properties: {
1609
+ start: { type: 'string', required: true },
1610
+ end: { type: 'string' },
1611
+ perms: { type: 'string' },
1612
+ path: { type: 'string' },
1613
+ sizeKb: { type: 'integer' },
1614
+ rssKb: { type: 'integer' },
1615
+ pssKb: { type: 'integer' },
1616
+ },
1617
+ },
1618
+ },
1619
+ error: { type: 'string' },
1620
+ },
1621
+ },
1622
+ render: (_args, value) => {
1623
+ if (value?.error) return [{ type: 'text', text: value.error }]
1624
+ const regs = value?.regions ?? []
1625
+ if (regs.length === 0) return [{ type: 'text', text: `(pid ${value?.pid ?? '?'}: no regions)` }]
1626
+ const lines = regs.map((r) => `${r.start}${r.end ? '-' + r.end : ''} ${(r.perms ?? '').padEnd(4)} pss=${r.pssKb ?? '?'}kB rss=${r.rssKb ?? '?'}kB ${r.path ?? ''}`)
1627
+ return [{ type: 'text', text: lines.join('\n') }]
1628
+ },
1629
+ },
1630
+ async execute(args) {
1631
+ const devices = engine.deviceList()
1632
+ const serial = typeof args.serial === 'string' && args.serial !== '' ? args.serial : devices[0]?.serial ?? ''
1633
+ const pid = Number(args.pid)
1634
+ const top = Math.min(Math.max(Number(args.top ?? 10) || 10, 1), 50)
1635
+ if (serial === '' || engine.adb === null || !Number.isInteger(pid) || pid <= 0) return { pid: 0, regions: [], error: 'a valid pid is required' }
1636
+ const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', `cat /proc/${pid}/smaps 2>&1; true`], 30000)
1637
+ const text = result.stdout ?? ''
1638
+ if (!result.ok || /Permission denied|No such file|No such process/.test(text) || text.trim() === '') {
1639
+ return { pid, regions: [], error: `cannot read /proc/${pid}/smaps (need root or matching perms); output: ${text.slice(0, 200)}` }
1640
+ }
1641
+ const regions = []
1642
+ let current = null
1643
+ for (const line of text.split(/\r?\n/)) {
1644
+ const header = /^([0-9a-f]+)-([0-9a-f]+)\s+([rwxps-]{4})\s+\S+\s+\S+\s+\d+\s*(.*)$/.exec(line)
1645
+ if (header !== null) {
1646
+ current = { start: header[1], end: header[2], perms: header[3], path: header[4] ?? '', sizeKb: 0, rssKb: 0, pssKb: 0 }
1647
+ regions.push(current)
1648
+ continue
1649
+ }
1650
+ if (current === null) continue
1651
+ const field = /^(Size|Rss|Pss):\s+(\d+) kB$/.exec(line.trim())
1652
+ if (field !== null) {
1653
+ if (field[1] === 'Size') current.sizeKb = Number.parseInt(field[2], 10)
1654
+ else if (field[1] === 'Rss') current.rssKb = Number.parseInt(field[2], 10)
1655
+ else current.pssKb = Number.parseInt(field[2], 10)
1656
+ }
1657
+ }
1658
+ const topRegions = regions
1659
+ .filter((r) => r.pssKb > 0)
1660
+ .sort((a, b) => b.pssKb - a.pssKb)
1661
+ .slice(0, top)
1662
+ .map(({ start, end, perms, path, sizeKb, rssKb, pssKb }) => ({ start, end, perms, path, sizeKb, rssKb, pssKb }))
1663
+ return { pid, regions: topRegions }
1664
+ },
1665
+ })
1666
+ }
1667
+
1668
+ /** frida script templates (reverse engineering quick starts). */
1669
+ const FRIDA_TEMPLATES = {
1670
+ hook_method: (target, method) => `// hook_method: log args + return of a Java method
1671
+ // usage: frida -U -f <package> -l hook.js (or attach to a running app)
1672
+ Java.perform(function () {
1673
+ var clsName = "${target ?? 'com.example.Class'}";
1674
+ var methodName = "${method ?? 'method'}";
1675
+ var cls = Java.use(clsName);
1676
+ cls[methodName].overloads.forEach(function (ov) {
1677
+ ov.implementation = function () {
1678
+ var args = Array.prototype.slice.call(arguments).map(String).join(", ");
1679
+ console.log("[" + clsName + "." + methodName + "] called(" + args + ")");
1680
+ var ret = ov.apply(this, arguments);
1681
+ console.log("[" + clsName + "." + methodName + "] => " + ret);
1682
+ return ret;
1683
+ };
1684
+ });
1685
+ console.log("[*] hooked " + clsName + "." + methodName);
1686
+ });`,
1687
+ trace_native: (target) => `// trace_native: trace a native export, target "libfoo.so!func"
1688
+ var spec = "${target ?? 'libfoo.so!func'}";
1689
+ var sep = spec.indexOf("!");
1690
+ if (sep < 0) { console.log("usage: lib.so!func"); throw 0; }
1691
+ var moduleName = spec.slice(0, sep);
1692
+ var funcName = spec.slice(sep + 1);
1693
+ var base = Module.findBaseAddress(moduleName);
1694
+ if (!base) { console.log("[!] module not loaded: " + moduleName); throw 0; }
1695
+ var addr = Module.findExportByName(moduleName, funcName);
1696
+ if (!addr) { console.log("[!] export not found: " + funcName); throw 0; }
1697
+ Interceptor.attach(addr, {
1698
+ onEnter: function (args) {
1699
+ var a = [];
1700
+ for (var i = 0; i < 8; i++) a.push(args[i]);
1701
+ console.log("[+] " + funcName + "(" + a.join(", ") + ")");
1702
+ },
1703
+ onLeave: function (retval) { console.log("[+] " + funcName + " => " + retval); }
1704
+ });
1705
+ console.log("[*] tracing " + spec + " @ " + addr);`,
1706
+ scan_memory: (target) => `// scan_memory: scan all modules for a hex pattern ("hex:48 65 6c 6c 6f" or raw hex)
1707
+ var pattern = "${target ?? 'hex:48 65 6c 6c 6f'}";
1708
+ if (pattern.indexOf("hex:") === 0) pattern = pattern.slice(4);
1709
+ var bytes = pattern.split(/\\s+/).filter(Boolean).map(function (b) { return parseInt(b, 16); });
1710
+ if (bytes.some(function (b) { return isNaN(b); })) { console.log("[!] bad hex pattern"); throw 0; }
1711
+ Process.enumerateModules().forEach(function (m) {
1712
+ try {
1713
+ Memory.scan(m.base, m.size, bytes, {
1714
+ onMatch: function (addr, size) {
1715
+ console.log("found @ " + addr + " in " + m.name + " (+0x" + addr.sub(m.base).toString(16) + ")");
1716
+ },
1717
+ onComplete: function () {}
1718
+ });
1719
+ } catch (e) {}
1720
+ });
1721
+ console.log("[*] scanning " + Process.enumerateModules().length + " modules");`,
1722
+ bypass_debug: () => `// bypass_debug: basic anti-debug / anti-frida helpers
1723
+ // 1) observe ptrace (self-attach tricks), 2) hide common frida markers
1724
+ var ptrace = Module.findExportByName(null, "ptrace");
1725
+ if (ptrace) {
1726
+ Interceptor.attach(ptrace, {
1727
+ onEnter: function (args) { console.log("[*] ptrace called, request=" + args[0]); },
1728
+ onLeave: function (retval) { if (retval.toInt32() === -1) console.log("[*] ptrace denied (anti-debug active)"); }
1729
+ });
1730
+ }
1731
+ try {
1732
+ Java.perform(function () {
1733
+ // neutralize common "is frida running" checks by spamming hooks is out of scope;
1734
+ // this template focuses on ptrace observation.
1735
+ });
1736
+ } catch (e) {}
1737
+ console.log("[*] basic anti-debug helpers installed (ptrace observer)");`,
1738
+ dump_class: (target) => `// dump_class: list methods and fields of a Java class
1739
+ Java.perform(function () {
1740
+ var clsName = "${target ?? 'com.example.Class'}";
1741
+ var cls = Java.use(clsName);
1742
+ console.log("[*] " + clsName + " methods:");
1743
+ cls.class.getDeclaredMethods().forEach(function (m) { console.log(" " + m.toString()); });
1744
+ console.log("[*] fields:");
1745
+ cls.class.getDeclaredFields().forEach(function (f) { console.log(" " + f.toString()); });
1746
+ });`,
1747
+ }
1748
+
1749
+ /** The frida_script agent tool: generate ready-to-use frida scripts. */
1750
+ function fridaScriptTool(engine) {
1751
+ return defineTool({
1752
+ name: 'frida_script',
1753
+ description: 'Generate a ready-to-use frida JavaScript script from a built-in template (hook_method / trace_native / scan_memory / bypass_debug / dump_class) ' +
1754
+ 'and optionally write it to a local file, then use with frida_server + "frida -U -f <pkg> -l <script>".',
1755
+ parameters: {
1756
+ template: { type: 'string', enum: ['hook_method', 'trace_native', 'scan_memory', 'bypass_debug', 'dump_class'], description: 'Which template to generate.' },
1757
+ target: { type: 'string', description: 'Template-specific target: class name (hook_method/dump_class), "lib.so!func" (trace_native), hex pattern (scan_memory).' },
1758
+ method: { type: 'string', description: 'Method name for hook_method (optional; hooks the given method of the target class).' },
1759
+ output: { type: 'string', description: 'Optional local file path to write the script to, e.g. "F:/tools/hook.js".' },
1760
+ },
1761
+ output: {
1762
+ schema: {
1763
+ type: 'object',
1764
+ additionalProperties: false,
1765
+ properties: {
1766
+ script: { type: 'string', required: true },
1767
+ wroteTo: { type: 'string' },
1768
+ error: { type: 'string' },
1769
+ },
1770
+ },
1771
+ render: (_args, value) => {
1772
+ if (value?.error) return [{ type: 'text', text: value.error }]
1773
+ return [{ type: 'text', text: value?.script ?? '' }]
1774
+ },
1775
+ },
1776
+ async execute(args) {
1777
+ const template = typeof args.template === 'string' ? args.template : ''
1778
+ const target = typeof args.target === 'string' ? args.target.trim() : ''
1779
+ const method = typeof args.method === 'string' ? args.method.trim() : ''
1780
+ const generator = FRIDA_TEMPLATES[template]
1781
+ if (generator === undefined) {
1782
+ return { script: '', error: `unknown template: ${template}; pick one of ${Object.keys(FRIDA_TEMPLATES).join(' / ')}` }
1783
+ }
1784
+ const script = generator(target || undefined, method || undefined)
1785
+ const output = typeof args.output === 'string' ? args.output.trim() : ''
1786
+ if (output !== '') {
1787
+ try {
1788
+ const { writeFileSync, mkdirSync } = await import('node:fs')
1789
+ mkdirSync(dirname(output), { recursive: true })
1790
+ writeFileSync(output, script, 'utf8')
1791
+ return { script, wroteTo: output }
1792
+ } catch (error) {
1793
+ return { script, error: `failed to write ${output}: ${error instanceof Error ? error.message : String(error)}` }
1794
+ }
1795
+ }
1796
+ return { script }
1797
+ },
1798
+ })
1799
+ }
1800
+
1523
1801
  /** ------------------------------------------------------------------ */
1524
1802
 
1525
1803
  /** Mount the adb engine, routes, tool, and announcement. */
@@ -1603,6 +1881,9 @@ export function apply(ctx, config) {
1603
1881
  procStatusTool(engine),
1604
1882
  memDumpTool(engine),
1605
1883
  fridaServerTool(engine),
1884
+ memSearchTool(engine),
1885
+ procSmapsTool(engine),
1886
+ fridaScriptTool(engine),
1606
1887
  ].map((tool) => ctx.tools.register(tool))
1607
1888
  return () => { for (const dispose of disposers) dispose() }
1608
1889
  }, 'dsh-logcat: tools')
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@windypro-rourou/dsh-logcat",
3
3
  "description": "Android Logcat viewer for the dsh web GUI: auto-connects to any adb device in debug mode, live logcat stream with level/keyword filters, pause/clear/export, plus agent tools (logcat_recent). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules copy, no dsh source changes.",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {