diff --git a/sdk/agent/config.json b/sdk/agent/config.json index 96f3ab4fe1..6282badc44 100644 --- a/sdk/agent/config.json +++ b/sdk/agent/config.json @@ -1,9 +1,8 @@ { - "device_id": "", - "server_url": "ws://192.168.0.5:8899/ws/device", + "device_id": "xgfe65eimrrofyws", + "server_url": "ws://127.0.0.1:8899/ws/device", "heartbeat_interval": 10, "project_id": "cunkebao", - "ai_brain": { "enabled": false, "api_url": "http://localhost:3102", diff --git a/sdk/agent/hook/hook_executor.py b/sdk/agent/hook/hook_executor.py index fdce48b745..ce20afa5d9 100644 --- a/sdk/agent/hook/hook_executor.py +++ b/sdk/agent/hook/hook_executor.py @@ -177,8 +177,118 @@ ACTION_TO_RPC: Dict[str, str] = { "get_process_info": "getProcessInfo", "get_wechat_version": "getWechatVersion", "batch_execute": "batchExecute", + + # ── 矩阵 v8.0.56 补全(Frida RPC 直映射)── + "get_safety_center": "getSafetyCenter", + "check_restrictions": "checkRestrictions", + "get_top_stories": "getTopStories", + "get_wechat_steps": "getWechatSteps", + "get_sticker_list": "getStickerList", + "show_payment_code": "showPaymentCode", + "like_wechat_steps": "likeWechatSteps", + "clear_cache": "clearCache", + "check_for_update": "checkForUpdate", } +# unified / ADB 引擎方法名 → HookExecutor 标准 action(矩阵 v8.0.56 真机验收) +ACTION_ALIASES: Dict[str, str] = { + "recall_message": "revoke_message", + "search_contact": "search_contacts", + "get_friend_info": "get_contact_info", + "account_status": "check_account_status", + "safety_center": "get_safety_center", + "set_chat_top": "pin_chat", + "set_mute_chat": "set_do_not_disturb", + "get_video_list": "browse_channels", + "show_my_qr": "generate_my_qr_code", + "view_wallet": "get_wallet_balance", + "view_transactions": "get_transaction_history", + "wechat_search": "global_search", + "top_stories": "get_top_stories", + "get_steps": "get_wechat_steps", + "get_tags": "get_labels", + "create_tag": "create_label", + "delete_tag": "delete_label", + "set_remark": "set_friend_remark", + "add_to_favorites": "add_favorite", + "clear_history": "clear_chat_history", + "like_video": "like_channel_video", + "send_voice_message": "send_voice", + "set_group_notice": "set_group_announcement", + "transfer": "send_transfer", + "receive_payment": "receive_transfer", + "unblock_account": "unblock_self", + "unblock_appeal": "unblock_self", + "appeal_restriction": "unblock_self", + "unblock_with_sms": "unblock_self", + "set_gender": "set_sex", + "set_moments_cover": "set_privacy", + "set_moments_privacy": "set_privacy", + "forward_moments_link": "post_moments", + "open_miniprogram": "open_mini_program", + "do_not_disturb": "set_do_not_disturb", + "like_steps": "like_wechat_steps", + "toggle_do_not_disturb": "set_do_not_disturb", + "clear_cache": "clear_cache", + "check_for_update": "check_for_update", + "send_file_from_chat": "send_file", + "send_voice_message": "send_voice", + "add_to_favorites": "add_favorite", +} + + +def resolve_action(action: str) -> str: + return ACTION_ALIASES.get(action, action) + + +def normalize_params(action: str, params: Dict[str, Any]) -> Dict[str, Any]: + """统一 API 参数 → Frida RPC 参数(user_id→wxid 等)""" + p = dict(params or {}) + resolved = resolve_action(action) + # 文件传输助手显示名 → wxid + for key in ("user_id", "wxid", "to_id", "conversation_id"): + if p.get(key) in ("文件传输助手", "File Transfer"): + p[key] = "filehelper" + if p.get("user_id") and not p.get("wxid"): + p["wxid"] = p["user_id"] + if resolved == "pin_chat": + if "enable" in p and "pin" not in p: + p["pin"] = p["enable"] + if resolved == "get_contact_info" and p.get("user_id") and not p.get("wxid"): + p["wxid"] = p["user_id"] + if resolved == "send_message" and "msg_type" not in p: + p["msg_type"] = "text" + if resolved == "get_contacts_by_label" and p.get("tag_name") and not p.get("label_name"): + p["label_name"] = p["tag_name"] + if resolved == "create_label" and p.get("tag_name") and not p.get("name"): + p["name"] = p["tag_name"] + if p.get("to_id") and not p.get("user_id"): + p["user_id"] = p["to_id"] + if p.get("user_id") and not p.get("to_id") and resolved in ( + "send_location", "send_voice", "send_file", "send_emoji", "send_image", "send_video" + ): + p["to_id"] = p["user_id"] + if p.get("name") and not p.get("label"): + p["label"] = p["name"] + if p.get("emoji_name") and not p.get("emoji_md5"): + p["emoji_md5"] = p["emoji_name"] + if p.get("name") and not p.get("app_id") and resolved == "open_mini_program": + p["app_id"] = p["name"] + if p.get("index") is not None and not p.get("video_id") and resolved == "like_channel_video": + p["video_id"] = f"index_{p['index']}" + if p.get("privacy_type") and not p.get("setting"): + p["setting"] = "moments_privacy" + p["value"] = p["privacy_type"] + if resolved == "set_privacy" and p.get("days") and not p.get("setting"): + p["setting"] = "moments_days" + p["value"] = p["days"] + if p.get("content") and not p.get("content_desc"): + p["content_desc"] = p["content"] + if resolved == "send_voice" and not p.get("voice_path") and p.get("duration"): + p["voice_path"] = f"/sdcard/workphone/voice_{int(p['duration'])}s.amr" + return p + + MODULE_NAMES = { "H15": "消息接收", "H16": "联系人", "H17": "消息发送", "H18": "好友请求", "H19": "好友管理", "H20": "朋友圈发布", @@ -206,17 +316,20 @@ class HookExecutor: if not self.available: return {"success": False, "error": "Hook 通道不可用(Frida 未连接)", "channel": "hook"} - rpc_method = ACTION_TO_RPC.get(action) + canonical = resolve_action(action) + norm = normalize_params(action, params) + rpc_method = ACTION_TO_RPC.get(canonical) if not rpc_method: return {"success": False, "error": f"Hook 不支持该动作: {action}", "channel": "hook"} - logger.info(f"[HookExecutor] {action} → rpc.{rpc_method}") - result = self.frida.call_rpc(rpc_method, params or {}) + logger.info(f"[HookExecutor] {action} → {canonical} → rpc.{rpc_method}") + result = self.frida.call_rpc(rpc_method, norm) result["channel"] = "hook" + result["action_resolved"] = canonical return result def supports(self, action: str) -> bool: - return action in ACTION_TO_RPC + return resolve_action(action) in ACTION_TO_RPC def get_supported_actions(self) -> list: return sorted(ACTION_TO_RPC.keys()) diff --git a/sdk/agent/hook/scripts/common.js b/sdk/agent/hook/scripts/common.js new file mode 100644 index 0000000000..d03fcb9fe8 --- /dev/null +++ b/sdk/agent/hook/scripts/common.js @@ -0,0 +1,332 @@ +/** + * common.js — 机擎 SDK Hook 脚本公共框架 + * + * 所有平台 Hook 脚本(微信/抖音/小红书等)共用的工具函数和基础设施。 + * 由各平台脚本通过 Frida evaluate 或内联引用使用。 + * + * @version 1.0.0 + */ +'use strict'; + +// ============================================================ +// § 1 日志与事件 +// ============================================================ + +var LOG_LEVEL_MAP = { debug: 0, info: 1, warn: 2, error: 3 }; +var _currentLogLevel = 'info'; + +function setLogLevel(level) { + if (LOG_LEVEL_MAP[level] !== undefined) _currentLogLevel = level; +} + +function log(level, tag, message, extra) { + if ((LOG_LEVEL_MAP[level] || 0) < (LOG_LEVEL_MAP[_currentLogLevel] || 0)) return; + send({ + type: 'log', + level: level, + tag: String(tag || 'hook'), + message: String(message || ''), + extra: extra || {}, + timestamp: Date.now(), + }); +} + +function emitEvent(eventType, payload) { + send({ + type: 'hook_event', + event_type: String(eventType), + platform: (typeof PLATFORM !== 'undefined') ? PLATFORM : 'unknown', + payload: payload || {}, + timestamp: new Date().toISOString(), + }); +} + +// ============================================================ +// § 2 安全字符串/类型工具 +// ============================================================ + +function safeStr(v) { + if (v === null || v === undefined) return ''; + try { return v.toString(); } catch (_) { return ''; } +} + +function safeInt(v, fallback) { + if (v === null || v === undefined) return fallback || 0; + try { + var n = parseInt(v, 10); + return isNaN(n) ? (fallback || 0) : n; + } catch (_) { return fallback || 0; } +} + +function safeJSON(v) { + try { return JSON.parse(v); } catch (_) { return null; } +} + +function clamp(val, min, max) { + return Math.max(min, Math.min(max, val)); +} + +function generateId(prefix) { + return (prefix || 'id') + '_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6); +} + +// ============================================================ +// § 3 Java / Android 辅助 +// ============================================================ + +function javaString(str) { + return Java.use('java.lang.String').$new(String(str)); +} + +function getContext() { + try { + return Java.use('android.app.ActivityThread').currentApplication().getApplicationContext(); + } catch (e) { + log('warn', 'ctx', '获取 Context 失败: ' + e); + return null; + } +} + +function getPackageVersion(packageName) { + try { + var ctx = getContext(); + if (!ctx) return ''; + var pm = ctx.getPackageManager(); + var info = pm.getPackageInfo(javaString(packageName), 0); + return safeStr(info.versionName.value); + } catch (_) { + return ''; + } +} + +function isServiceRunning(serviceClass) { + try { + var ctx = getContext(); + if (!ctx) return false; + var am = Java.cast( + ctx.getSystemService(javaString('activity')), + Java.use('android.app.ActivityManager') + ); + var services = am.getRunningServices(100); + for (var i = 0; i < services.size(); i++) { + if (safeStr(services.get(i).service.value.getClassName()) === serviceClass) return true; + } + } catch (_) {} + return false; +} + +// ============================================================ +// § 4 SQLite 通用查询 +// ============================================================ + +function execSQL(dbPath, sql, limit) { + limit = limit || 500; + var results = []; + try { + var SQLiteDatabase = Java.use('android.database.sqlite.SQLiteDatabase'); + var db = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY.value); + var cursor = db.rawQuery(sql, null); + var count = 0; + while (cursor.moveToNext() && count < limit) { + var row = {}; + for (var c = 0; c < cursor.getColumnCount(); c++) { + row[cursor.getColumnName(c)] = safeStr(cursor.getString(c)); + } + results.push(row); + count++; + } + cursor.close(); + db.close(); + } catch (e) { + log('warn', 'db', 'execSQL 失败: ' + e + ' | sql=' + sql); + } + return results; +} + +function findDatabase(basePath, dbName) { + try { + var File = Java.use('java.io.File'); + var dir = File.$new(basePath); + if (!dir.exists()) return null; + var subs = dir.listFiles(); + if (!subs) return null; + for (var i = 0; i < subs.length; i++) { + var candidate = subs[i].getAbsolutePath() + '/' + dbName; + var f = File.$new(candidate); + if (f.exists()) return candidate; + } + } catch (_) {} + return null; +} + +// ============================================================ +// § 5 Intent 辅助 +// ============================================================ + +function sendBroadcast(action, extras) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getContext(); + if (!ctx) return false; + var intent = Intent.$new(action); + if (extras) { + var keys = Object.keys(extras); + for (var i = 0; i < keys.length; i++) { + intent.putExtra(keys[i], String(extras[keys[i]])); + } + } + ctx.sendBroadcast(intent); + return true; + } catch (e) { + log('error', 'intent', 'sendBroadcast 失败: ' + e); + return false; + } +} + +function startActivity(componentPkg, componentCls, extras) { + try { + var Intent = Java.use('android.content.Intent'); + var ComponentName = Java.use('android.content.ComponentName'); + var ctx = getContext(); + if (!ctx) return false; + var intent = Intent.$new(); + intent.setComponent(ComponentName.$new(componentPkg, componentCls)); + intent.addFlags(0x10000000); // FLAG_ACTIVITY_NEW_TASK + if (extras) { + var keys = Object.keys(extras); + for (var i = 0; i < keys.length; i++) { + intent.putExtra(keys[i], String(extras[keys[i]])); + } + } + ctx.startActivity(intent); + return true; + } catch (e) { + log('error', 'intent', 'startActivity 失败: ' + e); + return false; + } +} + +// ============================================================ +// § 6 Hook 辅助(安全包装 Java.use / overload) +// ============================================================ + +function tryJavaUse(className) { + try { return Java.use(className); } catch (_) { return null; } +} + +function hookMethod(className, methodName, overloads, callbacks) { + try { + var Cls = Java.use(className); + var method = overloads ? Cls[methodName].overload.apply(Cls[methodName], overloads) : Cls[methodName]; + method.implementation = callbacks.replace || function () { + if (callbacks.before) callbacks.before.apply(this, arguments); + var ret = method.apply(this, arguments); + if (callbacks.after) ret = callbacks.after.call(this, ret, arguments) || ret; + return ret; + }; + log('info', 'hook', '已 Hook: ' + className + '.' + methodName); + return true; + } catch (e) { + log('warn', 'hook', 'Hook 失败: ' + className + '.' + methodName + ' → ' + e); + return false; + } +} + +function hookConstructor(className, overloads, callback) { + try { + var Cls = Java.use(className); + var ctor = overloads ? Cls.$init.overload.apply(Cls.$init, overloads) : Cls.$init; + ctor.implementation = function () { + ctor.apply(this, arguments); + if (callback) callback.apply(this, arguments); + }; + log('info', 'hook', '已 Hook 构造: ' + className); + return true; + } catch (e) { + log('warn', 'hook', 'Hook 构造失败: ' + className + ' → ' + e); + return false; + } +} + +// ============================================================ +// § 7 文件系统辅助 +// ============================================================ + +function readFile(path) { + try { + var File = Java.use('java.io.File'); + var FileInputStream = Java.use('java.io.FileInputStream'); + var BufferedReader = Java.use('java.io.BufferedReader'); + var InputStreamReader = Java.use('java.io.InputStreamReader'); + var f = File.$new(path); + if (!f.exists()) return null; + var reader = BufferedReader.$new(InputStreamReader.$new(FileInputStream.$new(f))); + var sb = []; + var line; + while ((line = reader.readLine()) !== null) sb.push(safeStr(line)); + reader.close(); + return sb.join('\n'); + } catch (_) { return null; } +} + +function fileExists(path) { + try { return Java.use('java.io.File').$new(path).exists(); } catch (_) { return false; } +} + +// ============================================================ +// § 8 缓冲与节流 +// ============================================================ + +function createThrottle(minIntervalMs) { + var lastFire = 0; + return function (fn) { + var now = Date.now(); + if (now - lastFire >= minIntervalMs) { + lastFire = now; + fn(); + } + }; +} + +function createRingBuffer(capacity) { + var buf = []; + return { + push: function (item) { + buf.push(item); + if (buf.length > capacity) buf = buf.slice(-Math.floor(capacity * 0.6)); + }, + slice: function (start, end) { return buf.slice(start, end); }, + get length() { return buf.length; }, + clear: function () { buf = []; }, + }; +} + +// ============================================================ +// § 9 导出(供其它脚本引用) +// ============================================================ + +var CommonUtils = { + log: log, + setLogLevel: setLogLevel, + emitEvent: emitEvent, + safeStr: safeStr, + safeInt: safeInt, + safeJSON: safeJSON, + clamp: clamp, + generateId: generateId, + javaString: javaString, + getContext: getContext, + getPackageVersion: getPackageVersion, + isServiceRunning: isServiceRunning, + execSQL: execSQL, + findDatabase: findDatabase, + sendBroadcast: sendBroadcast, + startActivity: startActivity, + tryJavaUse: tryJavaUse, + hookMethod: hookMethod, + hookConstructor: hookConstructor, + readFile: readFile, + fileExists: fileExists, + createThrottle: createThrottle, + createRingBuffer: createRingBuffer, +}; diff --git a/sdk/agent/hook/scripts/syscall_hook.js b/sdk/agent/hook/scripts/syscall_hook.js new file mode 100644 index 0000000000..af43cd322f --- /dev/null +++ b/sdk/agent/hook/scripts/syscall_hook.js @@ -0,0 +1,312 @@ +/** + * syscall_hook.js — 系统调用层网络拦截(机擎 SDK v3.0 · H23) + * + * 功能: + * 1. 拦截 libc connect/sendto/recvfrom — 监控所有网络连接 + * 2. 拦截 SSL_read/SSL_write — HTTPS 明文抓取 + * 3. DNS 解析拦截 — 域名映射追踪 + * 4. 可选:SSL Pinning Bypass + * + * 使用方式: + * 由 FridaManager 单独加载或与 wechat_hook_v2.js 并行 + * + * @version 1.0.0 + */ +'use strict'; + +var PLATFORM = 'syscall'; + +var SYSCALL_CONFIG = { + HOOK_CONNECT: true, + HOOK_SSL: true, + HOOK_DNS: true, + SSL_BYPASS: false, + LOG_LEVEL: 'info', + CAPTURE_PAYLOAD: false, + MAX_PAYLOAD_SIZE: 4096, + MONITORED_DOMAINS: [ + 'weixin.qq.com', 'wechat.com', 'wx.qq.com', + 'long.weixin.qq.com', 'short.weixin.qq.com', + 'szlong.weixin.qq.com', 'szshort.weixin.qq.com', + ], + MONITORED_PORTS: [80, 443, 8080, 8443], +}; + +// ============================================================ +// 工具 +// ============================================================ + +function log(level, tag, message, extra) { + send({ type: 'log', level: level, tag: 'syscall.' + tag, message: String(message || ''), extra: extra || {}, timestamp: Date.now() }); +} + +function emitEvent(eventType, payload) { + send({ type: 'hook_event', event_type: eventType, platform: PLATFORM, payload: payload || {}, timestamp: new Date().toISOString() }); +} + +function ipFromSockaddr(ptr) { + if (ptr.isNull()) return { ip: '0.0.0.0', port: 0, family: 0 }; + var family = ptr.readU16(); + if (family === 2) { + var port = (ptr.add(2).readU8() << 8) | ptr.add(3).readU8(); + var ip = ptr.add(4).readU8() + '.' + ptr.add(5).readU8() + '.' + ptr.add(6).readU8() + '.' + ptr.add(7).readU8(); + return { ip: ip, port: port, family: 2 }; + } + if (family === 10) { + var port6 = (ptr.add(2).readU8() << 8) | ptr.add(3).readU8(); + var bytes = []; + for (var i = 0; i < 16; i++) bytes.push(ptr.add(8 + i).readU8()); + if (bytes[10] === 0xff && bytes[11] === 0xff && bytes.slice(0, 10).every(function (b) { return b === 0; })) { + return { ip: bytes[12] + '.' + bytes[13] + '.' + bytes[14] + '.' + bytes[15], port: port6, family: 10 }; + } + return { ip: '::ipv6', port: port6, family: 10 }; + } + return { ip: 'unknown', port: 0, family: family }; +} + +// ============================================================ +// 统计 +// ============================================================ + +var _stats = { + connections: 0, + ssl_reads: 0, + ssl_writes: 0, + dns_queries: 0, + bytes_sent: 0, + bytes_received: 0, +}; + +var _connectionMap = {}; + +// ============================================================ +// rpc.exports +// ============================================================ + +rpc.exports = { + ping: function () { return 'pong from syscall_hook'; }, + + getStats: function () { return _stats; }, + + getConnections: function (params) { + var limit = (params && params.limit) || 50; + var keys = Object.keys(_connectionMap).slice(-limit); + return { + success: true, + connections: keys.map(function (k) { return _connectionMap[k]; }), + count: keys.length, + total: Object.keys(_connectionMap).length, + }; + }, + + setConfig: function (params) { + if (params) { + if (params.capture_payload !== undefined) SYSCALL_CONFIG.CAPTURE_PAYLOAD = !!params.capture_payload; + if (params.ssl_bypass !== undefined) SYSCALL_CONFIG.SSL_BYPASS = !!params.ssl_bypass; + if (params.log_level) SYSCALL_CONFIG.LOG_LEVEL = params.log_level; + } + return { success: true, config: SYSCALL_CONFIG }; + }, +}; + +// ============================================================ +// Hook 1: libc connect — 捕获所有 TCP 连接 +// ============================================================ + +if (SYSCALL_CONFIG.HOOK_CONNECT) { + try { + var connectPtr = Module.findExportByName('libc.so', 'connect'); + if (connectPtr) { + Interceptor.attach(connectPtr, { + onEnter: function (args) { + this.fd = args[0].toInt32(); + this.addr = args[1]; + this.addrlen = args[2].toInt32(); + }, + onLeave: function (retval) { + if (retval.toInt32() === 0 || retval.toInt32() === -1) { + try { + var info = ipFromSockaddr(this.addr); + if (info.family === 2 || info.family === 10) { + _stats.connections++; + var connId = 'fd_' + this.fd; + _connectionMap[connId] = { + fd: this.fd, + ip: info.ip, + port: info.port, + timestamp: new Date().toISOString(), + pid: Process.id, + tid: Process.getCurrentThreadId(), + }; + + var isMonitored = SYSCALL_CONFIG.MONITORED_PORTS.indexOf(info.port) !== -1; + if (isMonitored) { + emitEvent('tcp_connect', { fd: this.fd, ip: info.ip, port: info.port }); + } + } + } catch (_) {} + } + }, + }); + log('info', 'connect', 'libc connect 拦截已启用'); + } + } catch (e) { + log('warn', 'connect', 'connect Hook 失败: ' + e); + } +} + +// ============================================================ +// Hook 2: SSL_read / SSL_write — HTTPS 明文读写 +// ============================================================ + +if (SYSCALL_CONFIG.HOOK_SSL) { + var sslLibNames = ['libssl.so', 'libssl.so.1.1', 'libssl.so.3']; + + function hookSSL(libName) { + try { + var SSL_read = Module.findExportByName(libName, 'SSL_read'); + var SSL_write = Module.findExportByName(libName, 'SSL_write'); + + if (SSL_read) { + Interceptor.attach(SSL_read, { + onEnter: function (args) { + this.ssl = args[0]; + this.buf = args[1]; + this.num = args[2].toInt32(); + }, + onLeave: function (retval) { + var len = retval.toInt32(); + if (len > 0) { + _stats.ssl_reads++; + _stats.bytes_received += len; + if (SYSCALL_CONFIG.CAPTURE_PAYLOAD && len <= SYSCALL_CONFIG.MAX_PAYLOAD_SIZE) { + try { + var data = this.buf.readByteArray(Math.min(len, 512)); + emitEvent('ssl_read', { length: len, preview: _bytesToHex(data, 64) }); + } catch (_) {} + } + } + }, + }); + } + + if (SSL_write) { + Interceptor.attach(SSL_write, { + onEnter: function (args) { + this.ssl = args[0]; + this.buf = args[1]; + this.num = args[2].toInt32(); + _stats.ssl_writes++; + _stats.bytes_sent += this.num; + if (SYSCALL_CONFIG.CAPTURE_PAYLOAD && this.num <= SYSCALL_CONFIG.MAX_PAYLOAD_SIZE) { + try { + var data = this.buf.readByteArray(Math.min(this.num, 512)); + emitEvent('ssl_write', { length: this.num, preview: _bytesToHex(data, 64) }); + } catch (_) {} + } + }, + }); + } + + if (SSL_read || SSL_write) { + log('info', 'ssl', 'SSL Hook 已启用: ' + libName); + return true; + } + } catch (_) {} + return false; + } + + var sslHooked = false; + for (var li = 0; li < sslLibNames.length && !sslHooked; li++) { + sslHooked = hookSSL(sslLibNames[li]); + } + if (!sslHooked) { + log('warn', 'ssl', 'SSL 库未找到,跳过 SSL Hook'); + } +} + +// ============================================================ +// Hook 3: DNS 解析拦截 +// ============================================================ + +if (SYSCALL_CONFIG.HOOK_DNS) { + try { + var getaddrinfoPtr = Module.findExportByName('libc.so', 'getaddrinfo'); + if (getaddrinfoPtr) { + Interceptor.attach(getaddrinfoPtr, { + onEnter: function (args) { + this.hostname = args[0].isNull() ? '' : args[0].readCString(); + }, + onLeave: function (retval) { + if (this.hostname && retval.toInt32() === 0) { + _stats.dns_queries++; + var isMonitored = false; + for (var i = 0; i < SYSCALL_CONFIG.MONITORED_DOMAINS.length; i++) { + if (this.hostname.indexOf(SYSCALL_CONFIG.MONITORED_DOMAINS[i]) !== -1) { + isMonitored = true; + break; + } + } + if (isMonitored) { + emitEvent('dns_resolve', { hostname: this.hostname }); + } + } + }, + }); + log('info', 'dns', 'DNS 拦截已启用'); + } + } catch (e) { + log('warn', 'dns', 'DNS Hook 失败: ' + e); + } +} + +// ============================================================ +// 可选: SSL Pinning Bypass +// ============================================================ + +if (SYSCALL_CONFIG.SSL_BYPASS) { + Java.perform(function () { + // TrustManager bypass + try { + var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl'); + TrustManagerImpl.verifyChain.implementation = function () { + return arguments[0]; + }; + log('info', 'bypass', 'TrustManagerImpl bypass 已启用'); + } catch (_) {} + + // OkHttp CertificatePinner bypass + try { + var CertPinner = Java.use('okhttp3.CertificatePinner'); + CertPinner.check.overload('java.lang.String', 'java.util.List').implementation = function () {}; + log('info', 'bypass', 'OkHttp CertificatePinner bypass 已启用'); + } catch (_) {} + + // WebViewClient SSL bypass + try { + var WebViewClient = Java.use('android.webkit.WebViewClient'); + WebViewClient.onReceivedSslError.implementation = function (view, handler, error) { + handler.proceed(); + }; + log('info', 'bypass', 'WebViewClient SSL bypass 已启用'); + } catch (_) {} + }); +} + +// ============================================================ +// 辅助 +// ============================================================ + +function _bytesToHex(arr, maxLen) { + if (!arr) return ''; + var bytes = new Uint8Array(arr); + var hex = []; + var limit = Math.min(bytes.length, maxLen || 64); + for (var i = 0; i < limit; i++) { + hex.push(('0' + bytes[i].toString(16)).slice(-2)); + } + return hex.join(' ') + (bytes.length > limit ? '...' : ''); +} + +log('info', 'init', 'syscall_hook 初始化完成', _stats); +emitEvent('syscall_hook_initialized', { config: SYSCALL_CONFIG, stats: _stats }); diff --git a/sdk/agent/hook/wechat_hook_v2.js b/sdk/agent/hook/wechat_hook_v2.js index 02c289dd43..05bd4905d9 100644 --- a/sdk/agent/hook/wechat_hook_v2.js +++ b/sdk/agent/hook/wechat_hook_v2.js @@ -683,13 +683,10 @@ rpc.exports = { var content = (params && params.content) || ''; var imageUrls = (params && params.image_urls) || []; if (!content && imageUrls.length === 0) return { success: false, error: '朋友圈内容不能为空' }; - try { - return Java.performNow(function () { - return _postMomentsInternal(content, imageUrls); - }); - } catch (e) { - return { success: false, error: String(e) }; - } + return _intentAction('com.workphone.POST_MOMENTS', { + content: content, + image_urls: (imageUrls || []).join(','), + }, 'moments_posted', { content: content }); }, getMoments: function (params) { @@ -902,6 +899,59 @@ rpc.exports = { return _intentAction('com.workphone.GET_TRANSACTIONS', { limit: String(limit) }, null, {}); }, + showPaymentCode: function () { + return _intentAction('com.workphone.SHOW_PAYMENT_CODE', {}, null, {}); + }, + + getSafetyCenter: function () { + return _intentAction('com.workphone.GET_SAFETY_CENTER', {}, null, {}); + }, + + checkRestrictions: function () { + try { + return Java.performNow(function () { + var kws = ['账号暂时无法使用聊天功能', '功能受限', '账号异常', '暂时限制', '封禁', '被限制']; + var sql = 'SELECT content FROM message ORDER BY createTime DESC LIMIT 200'; + var rows = _execSQL(sql); + var hits = []; + for (var i = 0; i < rows.length; i++) { + var c = String(rows[i].content || ''); + for (var j = 0; j < kws.length; j++) { + if (c.indexOf(kws[j]) >= 0 && hits.indexOf(kws[j]) < 0) hits.push(kws[j]); + } + } + return { success: true, restricted: hits.length > 0, restrictions: hits }; + }); + } catch (e) { + return { success: false, error: String(e), restricted: false, restrictions: [] }; + } + }, + + getTopStories: function () { + return _intentAction('com.workphone.GET_TOP_STORIES', {}, null, {}); + }, + + getWechatSteps: function () { + return _intentAction('com.workphone.GET_WECHAT_STEPS', {}, null, {}); + }, + + getStickerList: function () { + return _intentAction('com.workphone.GET_STICKER_LIST', {}, null, {}); + }, + + likeWechatSteps: function (params) { + var userId = (params && params.user_id) || ''; + return _intentAction('com.workphone.LIKE_WECHAT_STEPS', { user_id: userId }, 'steps_liked', { user_id: userId }); + }, + + clearCache: function () { + return _intentAction('com.workphone.CLEAR_CACHE', {}, 'cache_cleared', {}); + }, + + checkForUpdate: function () { + return _intentAction('com.workphone.CHECK_UPDATE', {}, 'update_checked', {}); + }, + // ==================== H26 二维码 ==================== scanQrCode: function (params) { @@ -934,6 +984,9 @@ rpc.exports = { likeChannelVideo: function (params) { var videoId = (params && params.video_id) || ''; + if (!videoId && params && params.index !== undefined) { + videoId = 'index_' + params.index; + } if (!videoId) return { success: false, error: '缺少 video_id' }; return _intentAction('com.workphone.LIKE_CHANNEL', { video_id: videoId }, 'channel_liked', { video_id: videoId }); }, @@ -1041,7 +1094,11 @@ rpc.exports = { addFavorite: function (params) { var msgSvrId = (params && params.msg_svr_id) || ''; var type = (params && params.type) || 'message'; - if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id' }; + var content = (params && params.content) || (params && params.content_desc) || ''; + if (!msgSvrId && content) { + return _intentAction('com.workphone.ADD_FAVORITE_TEXT', { content: content, type: type }, 'favorite_added', { content: content }); + } + if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id 或 content' }; return _intentAction('com.workphone.ADD_FAVORITE', { msg_svr_id: msgSvrId, type: type }, 'favorite_added', { msg_svr_id: msgSvrId }); }, @@ -1081,7 +1138,9 @@ rpc.exports = { setDoNotDisturb: function (params) { var wxid = (params && params.wxid) || ''; var enable = params && params.enable !== undefined ? params.enable : true; - if (!wxid) return { success: false, error: '缺少 wxid' }; + if (!wxid) { + return _intentAction('com.workphone.SET_GLOBAL_DND', { enable: String(enable) }, 'dnd_set', { enable: enable }); + } return _intentAction('com.workphone.SET_DND', { wxid: wxid, enable: String(enable) }, 'dnd_set', { wxid: wxid, enable: enable }); }, @@ -1119,7 +1178,7 @@ rpc.exports = { // ==================== H32 小程序 ==================== openMiniProgram: function (params) { - var appId = (params && params.app_id) || ''; + var appId = (params && params.app_id) || (params && params.name) || ''; var path = (params && params.path) || ''; if (!appId) return { success: false, error: '缺少 app_id' }; return _intentAction('com.workphone.OPEN_MINI_PROGRAM', { app_id: appId, path: path }, 'mini_program_opened', { app_id: appId }); @@ -1161,10 +1220,14 @@ rpc.exports = { }, sendVoice: function (params) { - var toId = (params && params.to_id) || ''; + var toId = (params && params.to_id) || (params && params.user_id) || ''; var voicePath = (params && params.voice_path) || ''; var duration = (params && params.duration) || 0; - if (!toId || !voicePath) return { success: false, error: '缺少 to_id 或 voice_path' }; + if (!toId) return { success: false, error: '缺少 to_id' }; + if (!voicePath && duration) { + return _intentAction('com.workphone.SEND_VOICE_RECORD', { to_id: toId, duration: String(duration) }, 'voice_sent', { to_id: toId, duration: duration }); + } + if (!voicePath) return { success: false, error: '缺少 voice_path' }; return _intentAction('com.workphone.SEND_VOICE', { to_id: toId, voice_path: voicePath, duration: String(duration) }, 'voice_sent', { to_id: toId }); }, @@ -1339,8 +1402,8 @@ rpc.exports = { // ==================== H37 表情管理 ==================== sendEmoji: function (params) { - var toId = (params && params.to_id) || ''; - var emojiMd5 = (params && params.emoji_md5) || ''; + var toId = (params && params.to_id) || (params && params.user_id) || ''; + var emojiMd5 = (params && params.emoji_md5) || (params && params.emoji_name) || ''; if (!toId || !emojiMd5) return { success: false, error: '缺少 to_id 或 emoji_md5' }; return _intentAction('com.workphone.SEND_EMOJI', { to_id: toId, emoji_md5: emojiMd5 }, 'emoji_sent', { to_id: toId }); }, @@ -1428,86 +1491,8 @@ rpc.exports = { } }, - // ==================== 调试/统计接口 ==================== - rawSql: function (params) { - var sql = (params && params.sql) || ""; - if (!sql) return { success: false, error: "缺少 sql 参数" }; - try { - return Java.performNow(function () { - var rows = _execSQL(sql); - return { success: true, rows: rows, count: rows.length }; - }); - } catch (e) { return { success: false, error: String(e) }; } - }, - getContactStats: function () { - try { - return Java.performNow(function () { - var total = _execSQL("SELECT COUNT(*) as cnt FROM rcontact"); - var typeDist = _execSQL("SELECT type, COUNT(*) as cnt FROM rcontact GROUP BY type ORDER BY cnt DESC LIMIT 20"); - var flagDist = _execSQL("SELECT verifyFlag, COUNT(*) as cnt FROM rcontact GROUP BY verifyFlag ORDER BY cnt DESC"); - var type3 = _execSQL("SELECT COUNT(*) as cnt FROM rcontact WHERE type=3"); - var relaxed = _execSQL("SELECT COUNT(*) as cnt FROM rcontact WHERE type NOT IN (0,33) AND username NOT LIKE \"%@chatroom\" AND username NOT LIKE \"gh_%\""); - var groupsRcontact = _execSQL("SELECT COUNT(*) as cnt FROM rcontact WHERE username LIKE \"%@chatroom\""); - var groupsChatroom = _execSQL("SELECT COUNT(*) as cnt FROM chatroom"); - var sampleType3 = _execSQL("SELECT username, nickname, conRemark, type, verifyFlag FROM rcontact WHERE type=3 LIMIT 30"); - var sampleRelaxed = _execSQL("SELECT username, nickname, conRemark, type, verifyFlag FROM rcontact WHERE type NOT IN (0,33) AND username NOT LIKE \"%@chatroom\" AND username NOT LIKE \"gh_%\" LIMIT 30"); - var groupSample = _execSQL("SELECT username, nickname FROM rcontact WHERE username LIKE \"%@chatroom\" LIMIT 20"); - var chatroomSample = _execSQL("SELECT chatroomname, chatroomnick FROM chatroom LIMIT 20"); - return { - success: true, - total_rcontact: total[0] ? total[0].cnt : 0, - type_distribution: typeDist, - verifyflag_distribution: flagDist, - type3_count: type3[0] ? type3[0].cnt : 0, - relaxed_count: relaxed[0] ? relaxed[0].cnt : 0, - groups_in_rcontact: groupsRcontact[0] ? groupsRcontact[0].cnt : 0, - groups_in_chatroom: groupsChatroom[0] ? groupsChatroom[0].cnt : 0, - sample_type3: sampleType3, - sample_relaxed: sampleRelaxed, - group_sample: groupSample, - chatroom_sample: chatroomSample, - }; - }); - } catch (e) { return { success: false, error: String(e) }; } - }, - getContactsFull: function (params) { - var limit = (params && params.limit) || 500; - var offset = (params && params.offset) || 0; - var filterType = (params && params.filter_type); - try { - return Java.performNow(function () { - var sql; - if (filterType !== undefined && filterType !== null) { - sql = "SELECT username, nickname, conRemark, type, verifyFlag FROM rcontact WHERE type=" + parseInt(filterType) + " AND username NOT LIKE \"%@chatroom\" LIMIT " + limit + " OFFSET " + offset; - } else { - sql = "SELECT username, nickname, conRemark, type, verifyFlag FROM rcontact WHERE username NOT LIKE \"%@chatroom\" AND username NOT LIKE \"gh_%\" AND type NOT IN (0) LIMIT " + limit + " OFFSET " + offset; - } - var rows = _execSQL(sql); - return { success: true, contacts: rows, count: rows.length, sql: sql }; - }); - } catch (e) { return { success: false, error: String(e) }; } - }, - getGroupsFull: function (params) { - var limit = (params && params.limit) || 500; - try { - return Java.performNow(function () { - var rows = _execSQL("SELECT chatroomname, chatroomnick, roomowner, membercount FROM chatroom LIMIT " + limit); - if (rows.length > 0 && !rows[0].error) { - return { success: true, source: "chatroom_table", groups: rows.map(function (r) { - return { group_id: r.chatroomname, name: r.chatroomnick || r.chatroomname, owner: r.roomowner, member_count: r.membercount }; - }), count: rows.length }; - } - var rows2 = _execSQL("SELECT username, nickname FROM rcontact WHERE username LIKE \"%@chatroom\" LIMIT " + limit); - return { success: true, source: "rcontact_table", groups: rows2.map(function (r) { - return { group_id: r.username, name: r.nickname }; - }), count: rows2.length }; - }); - } catch (e) { return { success: false, error: String(e) }; } - }, // ==================== 批量执行 ==================== - - batchExecute: function (params) { var actions = (params && params.actions) || []; if (actions.length === 0) return { success: false, error: '缺少 actions' }; @@ -1524,160 +1509,8 @@ rpc.exports = { } return { success: true, results: results, count: results.length }; }, - - // ==================== 系统控制 (纯Frida无线) ==================== - - takeScreenshot: function (params) { - try { - return Java.performNow(function () { - var savePath = (params && params.path) || '/sdcard/frida_sc_' + Date.now() + '.png'; - var Runtime = Java.use('java.lang.Runtime'); - var process = Runtime.getRuntime().exec(['sh', '-c', 'screencap -p ' + savePath]); - process.waitFor(); - var File = Java.use('java.io.File'); - var file = File.$new(savePath); - if (!file.exists()) return { success: false, error: '截图失败' }; - return { success: true, path: savePath, size: parseInt(file.length()) }; - }); - } catch (e) { return { success: false, error: String(e) }; } - }, - - getScreenshotBase64: function (params) { - try { - return Java.performNow(function () { - var filePath = (params && params.path) || '/sdcard/frida_sc.png'; - var File = Java.use('java.io.File'); - var file = File.$new(filePath); - if (!file.exists()) return { success: false, error: '文件不存在' }; - var fileSize = parseInt(file.length()); - if (fileSize > 3145728) return { success: false, error: '文件过大:' + fileSize }; - var FileInputStream = Java.use('java.io.FileInputStream'); - var fis = FileInputStream.$new(file); - var bytes = Java.array('byte', new Array(fileSize).fill(0)); - fis.read(bytes); - fis.close(); - var Base64 = Java.use('android.util.Base64'); - var b64 = Base64.encodeToString(bytes, 2); - return { success: true, base64: b64, size: fileSize }; - }); - } catch (e) { return { success: false, error: String(e) }; } - }, - - getCurrentActivity: function () { - try { - return Java.performNow(function () { - var activities = []; - Java.choose('android.app.Activity', { - onMatch: function (act) { - if (!act.isFinishing() && !act.isDestroyed()) { - activities.push({ cls: act.getClass().getName(), title: act.getTitle() ? act.getTitle().toString() : '', focus: act.hasWindowFocus() }); - } - }, - onComplete: function () {}, - }); - return { success: true, activities: activities, foreground: activities.length > 0 ? activities[activities.length - 1].cls : 'none' }; - }); - } catch (e) { return { success: false, error: String(e) }; } - }, - - navigateToChat: function (params) { - try { - return Java.performNow(function () { - var wxid = (params && params.wxid) || 'filehelper'; - var ctx = getCtx(); - var Intent = Java.use('android.content.Intent'); - var intent = Intent.$new(); - intent.setClassName(jStr('com.tencent.mm'), jStr('com.tencent.mm.ui.chatting.ChattingUI')); - intent.putExtra(jStr('Chat_User'), jStr(wxid)); - intent.addFlags(0x10000000); - ctx.startActivity(intent); - return { success: true, target: 'ChattingUI', wxid: wxid }; - }); - } catch (e) { return { success: false, error: String(e) }; } - }, - - navigateToMain: function () { - try { - return Java.performNow(function () { - var ctx = getCtx(); - var Intent = Java.use('android.content.Intent'); - var intent = Intent.$new(); - intent.setClassName(jStr('com.tencent.mm'), jStr('com.tencent.mm.ui.LauncherUI')); - intent.addFlags(0x10000000); - ctx.startActivity(intent); - return { success: true, target: 'LauncherUI' }; - }); - } catch (e) { return { success: false, error: String(e) }; } - }, - - navigateToMoments: function () { - try { - return Java.performNow(function () { - var ctx = getCtx(); - var Intent = Java.use('android.content.Intent'); - var intent = Intent.$new(); - intent.setClassName(jStr('com.tencent.mm'), jStr('com.tencent.mm.plugin.sns.ui.SnsTimeLineUI')); - intent.addFlags(0x10000000); - ctx.startActivity(intent); - return { success: true, target: 'SnsTimeLineUI' }; - }); - } catch (e) { return { success: false, error: String(e) }; } - }, - - simulateTap: function (params) { - try { - return Java.performNow(function () { - var x = (params && params.x) || 0; - var y = (params && params.y) || 0; - var Runtime = Java.use('java.lang.Runtime'); - Runtime.getRuntime().exec(['sh', '-c', 'input tap ' + x + ' ' + y]).waitFor(); - return { success: true, x: x, y: y }; - }); - } catch (e) { return { success: false, error: String(e) }; } - }, - - simulateBack: function () { - try { - return Java.performNow(function () { - var Runtime = Java.use('java.lang.Runtime'); - Runtime.getRuntime().exec(['sh', '-c', 'input keyevent 4']).waitFor(); - return { success: true, action: 'back' }; - }); - } catch (e) { return { success: false, error: String(e) }; } - }, - - simulateInput: function (params) { - try { - return Java.performNow(function () { - var text = (params && params.text) || ''; - var Runtime = Java.use('java.lang.Runtime'); - Runtime.getRuntime().exec(['sh', '-c', 'input text "' + text.replace(/"/g, '\\"') + '"']).waitFor(); - return { success: true, text: text }; - }); - } catch (e) { return { success: false, error: String(e) }; } - }, - - getConnectionStatus: function () { - return { success: true, mode: 'frida_wireless_tcp', pid: Process.id, arch: Process.arch, platform: Process.platform, timestamp: Date.now() }; - }, }; -// ============================================================ -// § AUTO-ALIAS: 为所有camelCase方法注册全小写别名 -// 解决 Frida Python 3.14 自动将方法名转小写的问题 -// ============================================================ -(function () { - var exports = rpc.exports; - var keys = Object.keys(exports); - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - var lower = key.toLowerCase(); - if (lower !== key && !exports[lower]) { - exports[lower] = exports[key]; - } - } -})(); - // ============================================================ // § 5 内部实现 — 消息发送 (H17) // ============================================================ diff --git a/sdk/app/agent/hook/hook_executor.py b/sdk/app/agent/hook/hook_executor.py index fdce48b745..ce20afa5d9 100644 --- a/sdk/app/agent/hook/hook_executor.py +++ b/sdk/app/agent/hook/hook_executor.py @@ -177,8 +177,118 @@ ACTION_TO_RPC: Dict[str, str] = { "get_process_info": "getProcessInfo", "get_wechat_version": "getWechatVersion", "batch_execute": "batchExecute", + + # ── 矩阵 v8.0.56 补全(Frida RPC 直映射)── + "get_safety_center": "getSafetyCenter", + "check_restrictions": "checkRestrictions", + "get_top_stories": "getTopStories", + "get_wechat_steps": "getWechatSteps", + "get_sticker_list": "getStickerList", + "show_payment_code": "showPaymentCode", + "like_wechat_steps": "likeWechatSteps", + "clear_cache": "clearCache", + "check_for_update": "checkForUpdate", } +# unified / ADB 引擎方法名 → HookExecutor 标准 action(矩阵 v8.0.56 真机验收) +ACTION_ALIASES: Dict[str, str] = { + "recall_message": "revoke_message", + "search_contact": "search_contacts", + "get_friend_info": "get_contact_info", + "account_status": "check_account_status", + "safety_center": "get_safety_center", + "set_chat_top": "pin_chat", + "set_mute_chat": "set_do_not_disturb", + "get_video_list": "browse_channels", + "show_my_qr": "generate_my_qr_code", + "view_wallet": "get_wallet_balance", + "view_transactions": "get_transaction_history", + "wechat_search": "global_search", + "top_stories": "get_top_stories", + "get_steps": "get_wechat_steps", + "get_tags": "get_labels", + "create_tag": "create_label", + "delete_tag": "delete_label", + "set_remark": "set_friend_remark", + "add_to_favorites": "add_favorite", + "clear_history": "clear_chat_history", + "like_video": "like_channel_video", + "send_voice_message": "send_voice", + "set_group_notice": "set_group_announcement", + "transfer": "send_transfer", + "receive_payment": "receive_transfer", + "unblock_account": "unblock_self", + "unblock_appeal": "unblock_self", + "appeal_restriction": "unblock_self", + "unblock_with_sms": "unblock_self", + "set_gender": "set_sex", + "set_moments_cover": "set_privacy", + "set_moments_privacy": "set_privacy", + "forward_moments_link": "post_moments", + "open_miniprogram": "open_mini_program", + "do_not_disturb": "set_do_not_disturb", + "like_steps": "like_wechat_steps", + "toggle_do_not_disturb": "set_do_not_disturb", + "clear_cache": "clear_cache", + "check_for_update": "check_for_update", + "send_file_from_chat": "send_file", + "send_voice_message": "send_voice", + "add_to_favorites": "add_favorite", +} + + +def resolve_action(action: str) -> str: + return ACTION_ALIASES.get(action, action) + + +def normalize_params(action: str, params: Dict[str, Any]) -> Dict[str, Any]: + """统一 API 参数 → Frida RPC 参数(user_id→wxid 等)""" + p = dict(params or {}) + resolved = resolve_action(action) + # 文件传输助手显示名 → wxid + for key in ("user_id", "wxid", "to_id", "conversation_id"): + if p.get(key) in ("文件传输助手", "File Transfer"): + p[key] = "filehelper" + if p.get("user_id") and not p.get("wxid"): + p["wxid"] = p["user_id"] + if resolved == "pin_chat": + if "enable" in p and "pin" not in p: + p["pin"] = p["enable"] + if resolved == "get_contact_info" and p.get("user_id") and not p.get("wxid"): + p["wxid"] = p["user_id"] + if resolved == "send_message" and "msg_type" not in p: + p["msg_type"] = "text" + if resolved == "get_contacts_by_label" and p.get("tag_name") and not p.get("label_name"): + p["label_name"] = p["tag_name"] + if resolved == "create_label" and p.get("tag_name") and not p.get("name"): + p["name"] = p["tag_name"] + if p.get("to_id") and not p.get("user_id"): + p["user_id"] = p["to_id"] + if p.get("user_id") and not p.get("to_id") and resolved in ( + "send_location", "send_voice", "send_file", "send_emoji", "send_image", "send_video" + ): + p["to_id"] = p["user_id"] + if p.get("name") and not p.get("label"): + p["label"] = p["name"] + if p.get("emoji_name") and not p.get("emoji_md5"): + p["emoji_md5"] = p["emoji_name"] + if p.get("name") and not p.get("app_id") and resolved == "open_mini_program": + p["app_id"] = p["name"] + if p.get("index") is not None and not p.get("video_id") and resolved == "like_channel_video": + p["video_id"] = f"index_{p['index']}" + if p.get("privacy_type") and not p.get("setting"): + p["setting"] = "moments_privacy" + p["value"] = p["privacy_type"] + if resolved == "set_privacy" and p.get("days") and not p.get("setting"): + p["setting"] = "moments_days" + p["value"] = p["days"] + if p.get("content") and not p.get("content_desc"): + p["content_desc"] = p["content"] + if resolved == "send_voice" and not p.get("voice_path") and p.get("duration"): + p["voice_path"] = f"/sdcard/workphone/voice_{int(p['duration'])}s.amr" + return p + + MODULE_NAMES = { "H15": "消息接收", "H16": "联系人", "H17": "消息发送", "H18": "好友请求", "H19": "好友管理", "H20": "朋友圈发布", @@ -206,17 +316,20 @@ class HookExecutor: if not self.available: return {"success": False, "error": "Hook 通道不可用(Frida 未连接)", "channel": "hook"} - rpc_method = ACTION_TO_RPC.get(action) + canonical = resolve_action(action) + norm = normalize_params(action, params) + rpc_method = ACTION_TO_RPC.get(canonical) if not rpc_method: return {"success": False, "error": f"Hook 不支持该动作: {action}", "channel": "hook"} - logger.info(f"[HookExecutor] {action} → rpc.{rpc_method}") - result = self.frida.call_rpc(rpc_method, params or {}) + logger.info(f"[HookExecutor] {action} → {canonical} → rpc.{rpc_method}") + result = self.frida.call_rpc(rpc_method, norm) result["channel"] = "hook" + result["action_resolved"] = canonical return result def supports(self, action: str) -> bool: - return action in ACTION_TO_RPC + return resolve_action(action) in ACTION_TO_RPC def get_supported_actions(self) -> list: return sorted(ACTION_TO_RPC.keys()) diff --git a/sdk/app/agent/hook/scripts/common.js b/sdk/app/agent/hook/scripts/common.js new file mode 100644 index 0000000000..d03fcb9fe8 --- /dev/null +++ b/sdk/app/agent/hook/scripts/common.js @@ -0,0 +1,332 @@ +/** + * common.js — 机擎 SDK Hook 脚本公共框架 + * + * 所有平台 Hook 脚本(微信/抖音/小红书等)共用的工具函数和基础设施。 + * 由各平台脚本通过 Frida evaluate 或内联引用使用。 + * + * @version 1.0.0 + */ +'use strict'; + +// ============================================================ +// § 1 日志与事件 +// ============================================================ + +var LOG_LEVEL_MAP = { debug: 0, info: 1, warn: 2, error: 3 }; +var _currentLogLevel = 'info'; + +function setLogLevel(level) { + if (LOG_LEVEL_MAP[level] !== undefined) _currentLogLevel = level; +} + +function log(level, tag, message, extra) { + if ((LOG_LEVEL_MAP[level] || 0) < (LOG_LEVEL_MAP[_currentLogLevel] || 0)) return; + send({ + type: 'log', + level: level, + tag: String(tag || 'hook'), + message: String(message || ''), + extra: extra || {}, + timestamp: Date.now(), + }); +} + +function emitEvent(eventType, payload) { + send({ + type: 'hook_event', + event_type: String(eventType), + platform: (typeof PLATFORM !== 'undefined') ? PLATFORM : 'unknown', + payload: payload || {}, + timestamp: new Date().toISOString(), + }); +} + +// ============================================================ +// § 2 安全字符串/类型工具 +// ============================================================ + +function safeStr(v) { + if (v === null || v === undefined) return ''; + try { return v.toString(); } catch (_) { return ''; } +} + +function safeInt(v, fallback) { + if (v === null || v === undefined) return fallback || 0; + try { + var n = parseInt(v, 10); + return isNaN(n) ? (fallback || 0) : n; + } catch (_) { return fallback || 0; } +} + +function safeJSON(v) { + try { return JSON.parse(v); } catch (_) { return null; } +} + +function clamp(val, min, max) { + return Math.max(min, Math.min(max, val)); +} + +function generateId(prefix) { + return (prefix || 'id') + '_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6); +} + +// ============================================================ +// § 3 Java / Android 辅助 +// ============================================================ + +function javaString(str) { + return Java.use('java.lang.String').$new(String(str)); +} + +function getContext() { + try { + return Java.use('android.app.ActivityThread').currentApplication().getApplicationContext(); + } catch (e) { + log('warn', 'ctx', '获取 Context 失败: ' + e); + return null; + } +} + +function getPackageVersion(packageName) { + try { + var ctx = getContext(); + if (!ctx) return ''; + var pm = ctx.getPackageManager(); + var info = pm.getPackageInfo(javaString(packageName), 0); + return safeStr(info.versionName.value); + } catch (_) { + return ''; + } +} + +function isServiceRunning(serviceClass) { + try { + var ctx = getContext(); + if (!ctx) return false; + var am = Java.cast( + ctx.getSystemService(javaString('activity')), + Java.use('android.app.ActivityManager') + ); + var services = am.getRunningServices(100); + for (var i = 0; i < services.size(); i++) { + if (safeStr(services.get(i).service.value.getClassName()) === serviceClass) return true; + } + } catch (_) {} + return false; +} + +// ============================================================ +// § 4 SQLite 通用查询 +// ============================================================ + +function execSQL(dbPath, sql, limit) { + limit = limit || 500; + var results = []; + try { + var SQLiteDatabase = Java.use('android.database.sqlite.SQLiteDatabase'); + var db = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY.value); + var cursor = db.rawQuery(sql, null); + var count = 0; + while (cursor.moveToNext() && count < limit) { + var row = {}; + for (var c = 0; c < cursor.getColumnCount(); c++) { + row[cursor.getColumnName(c)] = safeStr(cursor.getString(c)); + } + results.push(row); + count++; + } + cursor.close(); + db.close(); + } catch (e) { + log('warn', 'db', 'execSQL 失败: ' + e + ' | sql=' + sql); + } + return results; +} + +function findDatabase(basePath, dbName) { + try { + var File = Java.use('java.io.File'); + var dir = File.$new(basePath); + if (!dir.exists()) return null; + var subs = dir.listFiles(); + if (!subs) return null; + for (var i = 0; i < subs.length; i++) { + var candidate = subs[i].getAbsolutePath() + '/' + dbName; + var f = File.$new(candidate); + if (f.exists()) return candidate; + } + } catch (_) {} + return null; +} + +// ============================================================ +// § 5 Intent 辅助 +// ============================================================ + +function sendBroadcast(action, extras) { + try { + var Intent = Java.use('android.content.Intent'); + var ctx = getContext(); + if (!ctx) return false; + var intent = Intent.$new(action); + if (extras) { + var keys = Object.keys(extras); + for (var i = 0; i < keys.length; i++) { + intent.putExtra(keys[i], String(extras[keys[i]])); + } + } + ctx.sendBroadcast(intent); + return true; + } catch (e) { + log('error', 'intent', 'sendBroadcast 失败: ' + e); + return false; + } +} + +function startActivity(componentPkg, componentCls, extras) { + try { + var Intent = Java.use('android.content.Intent'); + var ComponentName = Java.use('android.content.ComponentName'); + var ctx = getContext(); + if (!ctx) return false; + var intent = Intent.$new(); + intent.setComponent(ComponentName.$new(componentPkg, componentCls)); + intent.addFlags(0x10000000); // FLAG_ACTIVITY_NEW_TASK + if (extras) { + var keys = Object.keys(extras); + for (var i = 0; i < keys.length; i++) { + intent.putExtra(keys[i], String(extras[keys[i]])); + } + } + ctx.startActivity(intent); + return true; + } catch (e) { + log('error', 'intent', 'startActivity 失败: ' + e); + return false; + } +} + +// ============================================================ +// § 6 Hook 辅助(安全包装 Java.use / overload) +// ============================================================ + +function tryJavaUse(className) { + try { return Java.use(className); } catch (_) { return null; } +} + +function hookMethod(className, methodName, overloads, callbacks) { + try { + var Cls = Java.use(className); + var method = overloads ? Cls[methodName].overload.apply(Cls[methodName], overloads) : Cls[methodName]; + method.implementation = callbacks.replace || function () { + if (callbacks.before) callbacks.before.apply(this, arguments); + var ret = method.apply(this, arguments); + if (callbacks.after) ret = callbacks.after.call(this, ret, arguments) || ret; + return ret; + }; + log('info', 'hook', '已 Hook: ' + className + '.' + methodName); + return true; + } catch (e) { + log('warn', 'hook', 'Hook 失败: ' + className + '.' + methodName + ' → ' + e); + return false; + } +} + +function hookConstructor(className, overloads, callback) { + try { + var Cls = Java.use(className); + var ctor = overloads ? Cls.$init.overload.apply(Cls.$init, overloads) : Cls.$init; + ctor.implementation = function () { + ctor.apply(this, arguments); + if (callback) callback.apply(this, arguments); + }; + log('info', 'hook', '已 Hook 构造: ' + className); + return true; + } catch (e) { + log('warn', 'hook', 'Hook 构造失败: ' + className + ' → ' + e); + return false; + } +} + +// ============================================================ +// § 7 文件系统辅助 +// ============================================================ + +function readFile(path) { + try { + var File = Java.use('java.io.File'); + var FileInputStream = Java.use('java.io.FileInputStream'); + var BufferedReader = Java.use('java.io.BufferedReader'); + var InputStreamReader = Java.use('java.io.InputStreamReader'); + var f = File.$new(path); + if (!f.exists()) return null; + var reader = BufferedReader.$new(InputStreamReader.$new(FileInputStream.$new(f))); + var sb = []; + var line; + while ((line = reader.readLine()) !== null) sb.push(safeStr(line)); + reader.close(); + return sb.join('\n'); + } catch (_) { return null; } +} + +function fileExists(path) { + try { return Java.use('java.io.File').$new(path).exists(); } catch (_) { return false; } +} + +// ============================================================ +// § 8 缓冲与节流 +// ============================================================ + +function createThrottle(minIntervalMs) { + var lastFire = 0; + return function (fn) { + var now = Date.now(); + if (now - lastFire >= minIntervalMs) { + lastFire = now; + fn(); + } + }; +} + +function createRingBuffer(capacity) { + var buf = []; + return { + push: function (item) { + buf.push(item); + if (buf.length > capacity) buf = buf.slice(-Math.floor(capacity * 0.6)); + }, + slice: function (start, end) { return buf.slice(start, end); }, + get length() { return buf.length; }, + clear: function () { buf = []; }, + }; +} + +// ============================================================ +// § 9 导出(供其它脚本引用) +// ============================================================ + +var CommonUtils = { + log: log, + setLogLevel: setLogLevel, + emitEvent: emitEvent, + safeStr: safeStr, + safeInt: safeInt, + safeJSON: safeJSON, + clamp: clamp, + generateId: generateId, + javaString: javaString, + getContext: getContext, + getPackageVersion: getPackageVersion, + isServiceRunning: isServiceRunning, + execSQL: execSQL, + findDatabase: findDatabase, + sendBroadcast: sendBroadcast, + startActivity: startActivity, + tryJavaUse: tryJavaUse, + hookMethod: hookMethod, + hookConstructor: hookConstructor, + readFile: readFile, + fileExists: fileExists, + createThrottle: createThrottle, + createRingBuffer: createRingBuffer, +}; diff --git a/sdk/app/agent/hook/scripts/syscall_hook.js b/sdk/app/agent/hook/scripts/syscall_hook.js new file mode 100644 index 0000000000..af43cd322f --- /dev/null +++ b/sdk/app/agent/hook/scripts/syscall_hook.js @@ -0,0 +1,312 @@ +/** + * syscall_hook.js — 系统调用层网络拦截(机擎 SDK v3.0 · H23) + * + * 功能: + * 1. 拦截 libc connect/sendto/recvfrom — 监控所有网络连接 + * 2. 拦截 SSL_read/SSL_write — HTTPS 明文抓取 + * 3. DNS 解析拦截 — 域名映射追踪 + * 4. 可选:SSL Pinning Bypass + * + * 使用方式: + * 由 FridaManager 单独加载或与 wechat_hook_v2.js 并行 + * + * @version 1.0.0 + */ +'use strict'; + +var PLATFORM = 'syscall'; + +var SYSCALL_CONFIG = { + HOOK_CONNECT: true, + HOOK_SSL: true, + HOOK_DNS: true, + SSL_BYPASS: false, + LOG_LEVEL: 'info', + CAPTURE_PAYLOAD: false, + MAX_PAYLOAD_SIZE: 4096, + MONITORED_DOMAINS: [ + 'weixin.qq.com', 'wechat.com', 'wx.qq.com', + 'long.weixin.qq.com', 'short.weixin.qq.com', + 'szlong.weixin.qq.com', 'szshort.weixin.qq.com', + ], + MONITORED_PORTS: [80, 443, 8080, 8443], +}; + +// ============================================================ +// 工具 +// ============================================================ + +function log(level, tag, message, extra) { + send({ type: 'log', level: level, tag: 'syscall.' + tag, message: String(message || ''), extra: extra || {}, timestamp: Date.now() }); +} + +function emitEvent(eventType, payload) { + send({ type: 'hook_event', event_type: eventType, platform: PLATFORM, payload: payload || {}, timestamp: new Date().toISOString() }); +} + +function ipFromSockaddr(ptr) { + if (ptr.isNull()) return { ip: '0.0.0.0', port: 0, family: 0 }; + var family = ptr.readU16(); + if (family === 2) { + var port = (ptr.add(2).readU8() << 8) | ptr.add(3).readU8(); + var ip = ptr.add(4).readU8() + '.' + ptr.add(5).readU8() + '.' + ptr.add(6).readU8() + '.' + ptr.add(7).readU8(); + return { ip: ip, port: port, family: 2 }; + } + if (family === 10) { + var port6 = (ptr.add(2).readU8() << 8) | ptr.add(3).readU8(); + var bytes = []; + for (var i = 0; i < 16; i++) bytes.push(ptr.add(8 + i).readU8()); + if (bytes[10] === 0xff && bytes[11] === 0xff && bytes.slice(0, 10).every(function (b) { return b === 0; })) { + return { ip: bytes[12] + '.' + bytes[13] + '.' + bytes[14] + '.' + bytes[15], port: port6, family: 10 }; + } + return { ip: '::ipv6', port: port6, family: 10 }; + } + return { ip: 'unknown', port: 0, family: family }; +} + +// ============================================================ +// 统计 +// ============================================================ + +var _stats = { + connections: 0, + ssl_reads: 0, + ssl_writes: 0, + dns_queries: 0, + bytes_sent: 0, + bytes_received: 0, +}; + +var _connectionMap = {}; + +// ============================================================ +// rpc.exports +// ============================================================ + +rpc.exports = { + ping: function () { return 'pong from syscall_hook'; }, + + getStats: function () { return _stats; }, + + getConnections: function (params) { + var limit = (params && params.limit) || 50; + var keys = Object.keys(_connectionMap).slice(-limit); + return { + success: true, + connections: keys.map(function (k) { return _connectionMap[k]; }), + count: keys.length, + total: Object.keys(_connectionMap).length, + }; + }, + + setConfig: function (params) { + if (params) { + if (params.capture_payload !== undefined) SYSCALL_CONFIG.CAPTURE_PAYLOAD = !!params.capture_payload; + if (params.ssl_bypass !== undefined) SYSCALL_CONFIG.SSL_BYPASS = !!params.ssl_bypass; + if (params.log_level) SYSCALL_CONFIG.LOG_LEVEL = params.log_level; + } + return { success: true, config: SYSCALL_CONFIG }; + }, +}; + +// ============================================================ +// Hook 1: libc connect — 捕获所有 TCP 连接 +// ============================================================ + +if (SYSCALL_CONFIG.HOOK_CONNECT) { + try { + var connectPtr = Module.findExportByName('libc.so', 'connect'); + if (connectPtr) { + Interceptor.attach(connectPtr, { + onEnter: function (args) { + this.fd = args[0].toInt32(); + this.addr = args[1]; + this.addrlen = args[2].toInt32(); + }, + onLeave: function (retval) { + if (retval.toInt32() === 0 || retval.toInt32() === -1) { + try { + var info = ipFromSockaddr(this.addr); + if (info.family === 2 || info.family === 10) { + _stats.connections++; + var connId = 'fd_' + this.fd; + _connectionMap[connId] = { + fd: this.fd, + ip: info.ip, + port: info.port, + timestamp: new Date().toISOString(), + pid: Process.id, + tid: Process.getCurrentThreadId(), + }; + + var isMonitored = SYSCALL_CONFIG.MONITORED_PORTS.indexOf(info.port) !== -1; + if (isMonitored) { + emitEvent('tcp_connect', { fd: this.fd, ip: info.ip, port: info.port }); + } + } + } catch (_) {} + } + }, + }); + log('info', 'connect', 'libc connect 拦截已启用'); + } + } catch (e) { + log('warn', 'connect', 'connect Hook 失败: ' + e); + } +} + +// ============================================================ +// Hook 2: SSL_read / SSL_write — HTTPS 明文读写 +// ============================================================ + +if (SYSCALL_CONFIG.HOOK_SSL) { + var sslLibNames = ['libssl.so', 'libssl.so.1.1', 'libssl.so.3']; + + function hookSSL(libName) { + try { + var SSL_read = Module.findExportByName(libName, 'SSL_read'); + var SSL_write = Module.findExportByName(libName, 'SSL_write'); + + if (SSL_read) { + Interceptor.attach(SSL_read, { + onEnter: function (args) { + this.ssl = args[0]; + this.buf = args[1]; + this.num = args[2].toInt32(); + }, + onLeave: function (retval) { + var len = retval.toInt32(); + if (len > 0) { + _stats.ssl_reads++; + _stats.bytes_received += len; + if (SYSCALL_CONFIG.CAPTURE_PAYLOAD && len <= SYSCALL_CONFIG.MAX_PAYLOAD_SIZE) { + try { + var data = this.buf.readByteArray(Math.min(len, 512)); + emitEvent('ssl_read', { length: len, preview: _bytesToHex(data, 64) }); + } catch (_) {} + } + } + }, + }); + } + + if (SSL_write) { + Interceptor.attach(SSL_write, { + onEnter: function (args) { + this.ssl = args[0]; + this.buf = args[1]; + this.num = args[2].toInt32(); + _stats.ssl_writes++; + _stats.bytes_sent += this.num; + if (SYSCALL_CONFIG.CAPTURE_PAYLOAD && this.num <= SYSCALL_CONFIG.MAX_PAYLOAD_SIZE) { + try { + var data = this.buf.readByteArray(Math.min(this.num, 512)); + emitEvent('ssl_write', { length: this.num, preview: _bytesToHex(data, 64) }); + } catch (_) {} + } + }, + }); + } + + if (SSL_read || SSL_write) { + log('info', 'ssl', 'SSL Hook 已启用: ' + libName); + return true; + } + } catch (_) {} + return false; + } + + var sslHooked = false; + for (var li = 0; li < sslLibNames.length && !sslHooked; li++) { + sslHooked = hookSSL(sslLibNames[li]); + } + if (!sslHooked) { + log('warn', 'ssl', 'SSL 库未找到,跳过 SSL Hook'); + } +} + +// ============================================================ +// Hook 3: DNS 解析拦截 +// ============================================================ + +if (SYSCALL_CONFIG.HOOK_DNS) { + try { + var getaddrinfoPtr = Module.findExportByName('libc.so', 'getaddrinfo'); + if (getaddrinfoPtr) { + Interceptor.attach(getaddrinfoPtr, { + onEnter: function (args) { + this.hostname = args[0].isNull() ? '' : args[0].readCString(); + }, + onLeave: function (retval) { + if (this.hostname && retval.toInt32() === 0) { + _stats.dns_queries++; + var isMonitored = false; + for (var i = 0; i < SYSCALL_CONFIG.MONITORED_DOMAINS.length; i++) { + if (this.hostname.indexOf(SYSCALL_CONFIG.MONITORED_DOMAINS[i]) !== -1) { + isMonitored = true; + break; + } + } + if (isMonitored) { + emitEvent('dns_resolve', { hostname: this.hostname }); + } + } + }, + }); + log('info', 'dns', 'DNS 拦截已启用'); + } + } catch (e) { + log('warn', 'dns', 'DNS Hook 失败: ' + e); + } +} + +// ============================================================ +// 可选: SSL Pinning Bypass +// ============================================================ + +if (SYSCALL_CONFIG.SSL_BYPASS) { + Java.perform(function () { + // TrustManager bypass + try { + var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl'); + TrustManagerImpl.verifyChain.implementation = function () { + return arguments[0]; + }; + log('info', 'bypass', 'TrustManagerImpl bypass 已启用'); + } catch (_) {} + + // OkHttp CertificatePinner bypass + try { + var CertPinner = Java.use('okhttp3.CertificatePinner'); + CertPinner.check.overload('java.lang.String', 'java.util.List').implementation = function () {}; + log('info', 'bypass', 'OkHttp CertificatePinner bypass 已启用'); + } catch (_) {} + + // WebViewClient SSL bypass + try { + var WebViewClient = Java.use('android.webkit.WebViewClient'); + WebViewClient.onReceivedSslError.implementation = function (view, handler, error) { + handler.proceed(); + }; + log('info', 'bypass', 'WebViewClient SSL bypass 已启用'); + } catch (_) {} + }); +} + +// ============================================================ +// 辅助 +// ============================================================ + +function _bytesToHex(arr, maxLen) { + if (!arr) return ''; + var bytes = new Uint8Array(arr); + var hex = []; + var limit = Math.min(bytes.length, maxLen || 64); + for (var i = 0; i < limit; i++) { + hex.push(('0' + bytes[i].toString(16)).slice(-2)); + } + return hex.join(' ') + (bytes.length > limit ? '...' : ''); +} + +log('info', 'init', 'syscall_hook 初始化完成', _stats); +emitEvent('syscall_hook_initialized', { config: SYSCALL_CONFIG, stats: _stats }); diff --git a/sdk/app/agent/hook/wechat_hook_v2.js b/sdk/app/agent/hook/wechat_hook_v2.js index 5441833954..05bd4905d9 100644 --- a/sdk/app/agent/hook/wechat_hook_v2.js +++ b/sdk/app/agent/hook/wechat_hook_v2.js @@ -683,13 +683,10 @@ rpc.exports = { var content = (params && params.content) || ''; var imageUrls = (params && params.image_urls) || []; if (!content && imageUrls.length === 0) return { success: false, error: '朋友圈内容不能为空' }; - try { - return Java.performNow(function () { - return _postMomentsInternal(content, imageUrls); - }); - } catch (e) { - return { success: false, error: String(e) }; - } + return _intentAction('com.workphone.POST_MOMENTS', { + content: content, + image_urls: (imageUrls || []).join(','), + }, 'moments_posted', { content: content }); }, getMoments: function (params) { @@ -902,6 +899,59 @@ rpc.exports = { return _intentAction('com.workphone.GET_TRANSACTIONS', { limit: String(limit) }, null, {}); }, + showPaymentCode: function () { + return _intentAction('com.workphone.SHOW_PAYMENT_CODE', {}, null, {}); + }, + + getSafetyCenter: function () { + return _intentAction('com.workphone.GET_SAFETY_CENTER', {}, null, {}); + }, + + checkRestrictions: function () { + try { + return Java.performNow(function () { + var kws = ['账号暂时无法使用聊天功能', '功能受限', '账号异常', '暂时限制', '封禁', '被限制']; + var sql = 'SELECT content FROM message ORDER BY createTime DESC LIMIT 200'; + var rows = _execSQL(sql); + var hits = []; + for (var i = 0; i < rows.length; i++) { + var c = String(rows[i].content || ''); + for (var j = 0; j < kws.length; j++) { + if (c.indexOf(kws[j]) >= 0 && hits.indexOf(kws[j]) < 0) hits.push(kws[j]); + } + } + return { success: true, restricted: hits.length > 0, restrictions: hits }; + }); + } catch (e) { + return { success: false, error: String(e), restricted: false, restrictions: [] }; + } + }, + + getTopStories: function () { + return _intentAction('com.workphone.GET_TOP_STORIES', {}, null, {}); + }, + + getWechatSteps: function () { + return _intentAction('com.workphone.GET_WECHAT_STEPS', {}, null, {}); + }, + + getStickerList: function () { + return _intentAction('com.workphone.GET_STICKER_LIST', {}, null, {}); + }, + + likeWechatSteps: function (params) { + var userId = (params && params.user_id) || ''; + return _intentAction('com.workphone.LIKE_WECHAT_STEPS', { user_id: userId }, 'steps_liked', { user_id: userId }); + }, + + clearCache: function () { + return _intentAction('com.workphone.CLEAR_CACHE', {}, 'cache_cleared', {}); + }, + + checkForUpdate: function () { + return _intentAction('com.workphone.CHECK_UPDATE', {}, 'update_checked', {}); + }, + // ==================== H26 二维码 ==================== scanQrCode: function (params) { @@ -934,6 +984,9 @@ rpc.exports = { likeChannelVideo: function (params) { var videoId = (params && params.video_id) || ''; + if (!videoId && params && params.index !== undefined) { + videoId = 'index_' + params.index; + } if (!videoId) return { success: false, error: '缺少 video_id' }; return _intentAction('com.workphone.LIKE_CHANNEL', { video_id: videoId }, 'channel_liked', { video_id: videoId }); }, @@ -1041,7 +1094,11 @@ rpc.exports = { addFavorite: function (params) { var msgSvrId = (params && params.msg_svr_id) || ''; var type = (params && params.type) || 'message'; - if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id' }; + var content = (params && params.content) || (params && params.content_desc) || ''; + if (!msgSvrId && content) { + return _intentAction('com.workphone.ADD_FAVORITE_TEXT', { content: content, type: type }, 'favorite_added', { content: content }); + } + if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id 或 content' }; return _intentAction('com.workphone.ADD_FAVORITE', { msg_svr_id: msgSvrId, type: type }, 'favorite_added', { msg_svr_id: msgSvrId }); }, @@ -1081,7 +1138,9 @@ rpc.exports = { setDoNotDisturb: function (params) { var wxid = (params && params.wxid) || ''; var enable = params && params.enable !== undefined ? params.enable : true; - if (!wxid) return { success: false, error: '缺少 wxid' }; + if (!wxid) { + return _intentAction('com.workphone.SET_GLOBAL_DND', { enable: String(enable) }, 'dnd_set', { enable: enable }); + } return _intentAction('com.workphone.SET_DND', { wxid: wxid, enable: String(enable) }, 'dnd_set', { wxid: wxid, enable: enable }); }, @@ -1119,7 +1178,7 @@ rpc.exports = { // ==================== H32 小程序 ==================== openMiniProgram: function (params) { - var appId = (params && params.app_id) || ''; + var appId = (params && params.app_id) || (params && params.name) || ''; var path = (params && params.path) || ''; if (!appId) return { success: false, error: '缺少 app_id' }; return _intentAction('com.workphone.OPEN_MINI_PROGRAM', { app_id: appId, path: path }, 'mini_program_opened', { app_id: appId }); @@ -1161,10 +1220,14 @@ rpc.exports = { }, sendVoice: function (params) { - var toId = (params && params.to_id) || ''; + var toId = (params && params.to_id) || (params && params.user_id) || ''; var voicePath = (params && params.voice_path) || ''; var duration = (params && params.duration) || 0; - if (!toId || !voicePath) return { success: false, error: '缺少 to_id 或 voice_path' }; + if (!toId) return { success: false, error: '缺少 to_id' }; + if (!voicePath && duration) { + return _intentAction('com.workphone.SEND_VOICE_RECORD', { to_id: toId, duration: String(duration) }, 'voice_sent', { to_id: toId, duration: duration }); + } + if (!voicePath) return { success: false, error: '缺少 voice_path' }; return _intentAction('com.workphone.SEND_VOICE', { to_id: toId, voice_path: voicePath, duration: String(duration) }, 'voice_sent', { to_id: toId }); }, @@ -1339,8 +1402,8 @@ rpc.exports = { // ==================== H37 表情管理 ==================== sendEmoji: function (params) { - var toId = (params && params.to_id) || ''; - var emojiMd5 = (params && params.emoji_md5) || ''; + var toId = (params && params.to_id) || (params && params.user_id) || ''; + var emojiMd5 = (params && params.emoji_md5) || (params && params.emoji_name) || ''; if (!toId || !emojiMd5) return { success: false, error: '缺少 to_id 或 emoji_md5' }; return _intentAction('com.workphone.SEND_EMOJI', { to_id: toId, emoji_md5: emojiMd5 }, 'emoji_sent', { to_id: toId }); }, diff --git a/sdk/app/config.py b/sdk/app/config.py index 79b4125862..049545c90c 100644 --- a/sdk/app/config.py +++ b/sdk/app/config.py @@ -2,7 +2,10 @@ 工作手机SDK v3.0 - 配置管理 """ -from pydantic_settings import BaseSettings +try: + from pydantic_settings import BaseSettings +except ImportError: # NAS / pydantic v1 降级 + from pydantic import BaseSettings from typing import Optional diff --git a/sdk/app/data/hook/device_modules.json b/sdk/app/data/hook/device_modules.json index b7a69413e7..4f7e5dc22a 100644 --- a/sdk/app/data/hook/device_modules.json +++ b/sdk/app/data/hook/device_modules.json @@ -172,19 +172,19 @@ "modules": [] }, "7921cf53a297b0d174e09f2f102eced4": { - "supports_hook": false, - "frida_version": "", + "supports_hook": true, + "frida_version": "16.5.6", "root_status": true, "hook_framework": "frida-server", - "updated_at": "2026-04-09T14:08:46.744104+00:00", + "updated_at": "2026-05-24T01:30:32.399950+00:00", "modules": [] }, "xgfe65eimrrofyws": { - "supports_hook": false, - "frida_version": "", + "supports_hook": true, + "frida_version": "16.5.6", "root_status": true, "hook_framework": "frida-server", - "updated_at": "2026-05-05T08:03:16.188260+00:00", + "updated_at": "2026-05-24T02:01:13.193007+00:00", "modules": [] }, "6c08d7b2de2fd67a5e24753a6b6920a5": { diff --git a/sdk/scripts/_hook_smoke.py b/sdk/scripts/_hook_smoke.py new file mode 100644 index 0000000000..ff00deba8c --- /dev/null +++ b/sdk/scripts/_hook_smoke.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# 工作手机 · Frida 主控通道·实机冒烟脚本(一次性跑完 8 项验证) +# 用法:python3 sdk/scripts/_hook_smoke.py +import json, sys, urllib.request, urllib.parse, time + +DEVICE = sys.argv[1] if len(sys.argv) > 1 else 'xgfe65eimrrofyws' +BASE = 'http://127.0.0.1:8899' + +def get(path, params=None): + url = f'{BASE}{path}' + if params: + url += '?' + urllib.parse.urlencode(params) + with urllib.request.urlopen(url, timeout=60) as r: + return json.loads(r.read()) + +def post(path, body): + req = urllib.request.Request( + f'{BASE}{path}', + data=json.dumps(body).encode(), + headers={'Content-Type': 'application/json'}, + method='POST', + ) + with urllib.request.urlopen(req, timeout=120) as r: + return json.loads(r.read()) + +def section(title): + print('\n' + '=' * 60) + print(f' {title}') + print('=' * 60) + +def show(label, value, limit=200): + s = json.dumps(value, ensure_ascii=False) + if len(s) > limit: + s = s[:limit] + ' …(truncated)' + print(f'{label} → {s}') + + +# 1) probe +section('1) /api/v3/hook/probe — Hook 探测') +probe = get(f'/api/v3/hook/probe/{DEVICE}') +show('supports_hook', probe.get('supports_hook')) +show('frida_version', probe.get('frida_version')) +show('wechat_version', probe.get('wechat_version')) +show('profile.nickname', probe.get('profile', {}).get('profile', {}).get('nickname')) + +# 2) actions 清单 +section('2) /api/v3/hook/actions — 全部动作清单') +acts = get('/api/v3/hook/actions') +show('total_actions', acts.get('total_actions')) +show('total_modules', acts.get('total_modules')) + +# 3) 一次性取数 +section('3) /api/v3/hook/data — 取微信资料/联系人/群/消息/标签') +data = get(f'/api/v3/hook/data/{DEVICE}', + {'modules': 'profile,contacts,groups,messages,labels'}) +core = data.get('data', {}) +def _list(node, key1, key2=None): + inner = node.get('data', node) + if key2 and isinstance(inner, dict) and key2 in inner: + return inner[key2] + if isinstance(inner, dict) and key1 in inner: + return inner[key1] + if isinstance(inner, dict) and 'data' in inner and isinstance(inner['data'], list): + return inner['data'] + return inner if isinstance(inner, list) else [] +contacts = _list(core['contacts'], 'contacts') +groups = _list(core['groups'], 'groups') +messages = _list(core['messages'], 'messages') +labels = _list(core['labels'], 'labels') +print(f'contacts : {len(contacts)} 条') +print(f'groups : {len(groups)} 个') +print(f'messages : {len(messages)} 条') +print(f'labels : {len(labels)} 个') +remark = [c for c in contacts if c.get('remark')] +namedgrp = [g for g in groups if g.get('name')] +print(f' - 有备注联系人: {len(remark)} | 例子:', + ', '.join((c.get('remark') or '?')[:24] for c in remark[:5])) +print(f' - 已命名群 : {len(namedgrp)} | 例子:', + ', '.join((g.get('name') or '?')[:24] for g in namedgrp[:5])) +print(f' - 标签 : {len(labels)} | 例子:', + ', '.join((l.get('name') or '?')[:18] for l in labels[:8])) + +# 4) hookStatus / getMoments / getWechatVersion 直调 +section('4) /api/v3/hook/execute — hookStatus + getWechatVersion') +hs = post('/api/v3/hook/execute', { + 'device_id': DEVICE, 'platform': 'wechat', + 'action': 'get_hook_status', 'params': {}, 'hook_only': True +}) +show('hookStatus', hs) +gv = post('/api/v3/hook/execute', { + 'device_id': DEVICE, 'platform': 'wechat', + 'action': 'get_wechat_version', 'params': {}, 'hook_only': True +}) +show('getWechatVersion', gv) + +# 5) 朋友圈 +section('5) /api/v3/hook/execute — getMoments(limit=3)') +mm = post('/api/v3/hook/execute', { + 'device_id': DEVICE, 'platform': 'wechat', + 'action': 'get_moments', 'params': {'limit': 3}, 'hook_only': True +}) +show('getMoments', mm, 400) + +# 6) 真发一条消息到文件传输助手 +section('6) /api/v3/hook/execute — send_message → 文件传输助手') +ts = time.strftime('%H:%M:%S') +send = post('/api/v3/hook/execute', { + 'device_id': DEVICE, 'platform': 'wechat', + 'action': 'send_message', + 'params': { + 'to_id': 'filehelper', + 'content': f'[E2E] Frida 主控通道 实机冒烟 {ts}', + 'msg_type': 'text', + }, + 'hook_only': True +}) +show('send_message', send) + +# 7) 拉一条最近消息证明 read-back +section('7) /api/v3/hook/execute — get_recent_messages(limit=1)') +rm = post('/api/v3/hook/execute', { + 'device_id': DEVICE, 'platform': 'wechat', + 'action': 'get_recent_messages', 'params': {'limit': 1}, 'hook_only': True +}) +show('get_recent_messages', rm, 400) + +# 8) 走标准 unified channel=hook 的 sendMessage(验证存客宝侧路径) +section('8) /api/v3/message/send channel=hook (存客宝标准入口)') +um = post('/api/v3/message/send', { + 'device_id': DEVICE, 'platform': 'wechat', 'to_id': 'filehelper', + 'content': f'[E2E] unified channel=hook {ts}', + 'msg_type': 'text', 'channel': 'hook' +}) +show('unified.sendMessage', um, 400) + +print('\n ✅ Hook 主控冒烟测试完成')