From b9eeebc6fbdd34fe79c7140a0458e6c06d753adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=85=8E=E9=A5=BC=E6=9E=9C=E5=AD=90=E5=8D=B7=E9=B2=A8?= =?UTF-8?q?=E9=B1=BC=E8=BE=A3=E6=A4=92?= Date: Tue, 3 Feb 2026 19:53:10 +0800 Subject: [PATCH] =?UTF-8?q?```=20feat(cursor=5Fhook):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?fs=E6=A8=A1=E5=9D=97Hook=E4=BF=9D=E6=8A=A4storage.json=E4=B8=AD?= =?UTF-8?q?=E7=9A=84telemetry=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增fs.writeFileSync/writeFile拦截功能,防止storage.json中 telemetry相关字段被覆盖 - 实现storage.json写入保护机制,维护telemetry.machineId、 telemetry.macMachineId、telemetry.devDeviceId、telemetry.sqmId等 关键字段的完整性 - 支持同步和异步两种写入方式的拦截处理 - 添加动态导入fs模块的Hook支持 - 在文档注释中添加第6项功能说明 fix(cursor_linux_id_modifier): 修复Python脚本缩进问题 - 将Python代码从混合缩进改为标准4空格缩进,避免IndentationError - 修正字符串转义处理逻辑,提升函数特征匹配准确性 ``` --- scripts/hook/cursor_hook.js | 162 ++++++++++++ scripts/run/cursor_linux_id_modifier.sh | 338 ++++++++++++------------ 2 files changed, 331 insertions(+), 169 deletions(-) diff --git a/scripts/hook/cursor_hook.js b/scripts/hook/cursor_hook.js index 30191c3..c81c2e2 100644 --- a/scripts/hook/cursor_hook.js +++ b/scripts/hook/cursor_hook.js @@ -9,6 +9,7 @@ * 3. @vscode/deviceid - 拦截 devDeviceId 获取 * 4. @vscode/windows-registry - 拦截注册表读取 * 5. os.networkInterfaces - 拦截 MAC 地址获取 + * 6. fs.writeFileSync/writeFile - 拦截 storage.json 写入,保护 telemetry 字段 * * 📦 使用方式: * 将此代码注入到 main.js 文件顶部(Sentry 初始化之后) @@ -208,6 +209,10 @@ var __cursor_hook_config__ = { else if (normalizedId === 'os') { hooked = hookOs(result); } + // Hook fs 模块 (新增:保护 storage.json) + else if (normalizedId === 'fs') { + hooked = hookFs(result); + } // Hook crypto 模块 else if (normalizedId === 'crypto') { hooked = hookCrypto(result); @@ -310,6 +315,121 @@ var __cursor_hook_config__ = { return os; } + // ==================== fs Hook (新增) ==================== + // 🔧 拦截 storage.json 写入操作,保护 telemetry 字段不被覆盖 + + // 需要保护的 telemetry 字段列表 + const PROTECTED_TELEMETRY_KEYS = [ + 'telemetry.machineId', + 'telemetry.macMachineId', + 'telemetry.devDeviceId', + 'telemetry.sqmId' + ]; + + // 检查路径是否为 storage.json + function isStorageJsonPath(filePath) { + if (!filePath || typeof filePath !== 'string') return false; + const normalized = filePath.replace(/\\/g, '/').toLowerCase(); + return normalized.includes('globalStorage/storage.json') || + normalized.includes('globalstorage/storage.json'); + } + + // 保护 storage.json 中的 telemetry 字段 + function protectStorageJson(content, filePath) { + if (!isStorageJsonPath(filePath)) return content; + + try { + const fs = require('fs'); + let newData = typeof content === 'string' ? JSON.parse(content) : content; + + // 如果写入的内容不是有效的 JSON 对象,直接返回 + if (typeof newData !== 'object' || newData === null) { + return content; + } + + // 读取当前文件中的受保护字段 + let existingProtected = {}; + try { + if (fs.existsSync(filePath)) { + const existing = JSON.parse(fs.readFileSync(filePath, 'utf8')); + for (const key of PROTECTED_TELEMETRY_KEYS) { + if (existing[key] !== undefined) { + existingProtected[key] = existing[key]; + } + } + } + } catch (e) { + // 文件不存在或解析失败,使用 Hook 配置的 ID + } + + // 强制使用 Hook 配置的 ID(优先级最高) + const protectedValues = { + 'telemetry.machineId': __cursor_ids__.machineId, + 'telemetry.macMachineId': __cursor_ids__.macMachineId, + 'telemetry.devDeviceId': __cursor_ids__.devDeviceId, + 'telemetry.sqmId': __cursor_ids__.sqmId + }; + + // 检测并修正被覆盖的字段 + let modified = false; + for (const key of PROTECTED_TELEMETRY_KEYS) { + const protectedValue = protectedValues[key]; + if (newData[key] !== undefined && newData[key] !== protectedValue) { + log(`[fs Hook] 拦截 ${key} 覆盖: ${newData[key].substring(0, 16)}... -> ${protectedValue.substring(0, 16)}...`); + newData[key] = protectedValue; + modified = true; + } else if (newData[key] === undefined && existingProtected[key]) { + // 如果新数据没有该字段,但旧文件有,保留旧值 + newData[key] = existingProtected[key]; + modified = true; + } + } + + if (modified) { + log('[fs Hook] storage.json telemetry 字段已保护'); + return typeof content === 'string' ? JSON.stringify(newData, null, '\t') : newData; + } + } catch (e) { + log('[fs Hook] 处理 storage.json 失败:', e.message); + } + + return content; + } + + function hookFs(fsModule) { + const originalWriteFileSync = fsModule.writeFileSync; + const originalWriteFile = fsModule.writeFile; + + // Hook writeFileSync + fsModule.writeFileSync = function(filePath, data, options) { + const protectedData = protectStorageJson(data, filePath); + return originalWriteFileSync.call(this, filePath, protectedData, options); + }; + + // Hook writeFile (异步版本) + fsModule.writeFile = function(filePath, data, options, callback) { + // 处理参数重载: writeFile(path, data, callback) 或 writeFile(path, data, options, callback) + if (typeof options === 'function') { + callback = options; + options = undefined; + } + const protectedData = protectStorageJson(data, filePath); + return originalWriteFile.call(this, filePath, protectedData, options, callback); + }; + + // Hook promises API (fs.promises.writeFile) + if (fsModule.promises) { + const originalPromisesWriteFile = fsModule.promises.writeFile; + fsModule.promises.writeFile = async function(filePath, data, options) { + const protectedData = protectStorageJson(data, filePath); + return originalPromisesWriteFile.call(this, filePath, protectedData, options); + }; + } + + log('[fs Hook] 已启用 storage.json 写入保护'); + return fsModule; + } + // ==================== crypto Hook ==================== function hookCrypto(crypto) { @@ -501,11 +621,53 @@ var __cursor_hook_config__ = { return hooked; }; + // Hook fs 模块的动态导入 (新增:保护 storage.json) + const hookDynamicFs = (fsModule) => { + if (hookedDynamicModules.has('fs')) { + return hookedDynamicModules.get('fs'); + } + + const hooked = { ...fsModule }; + const originalWriteFileSync = fsModule.writeFileSync; + const originalWriteFile = fsModule.writeFile; + + hooked.writeFileSync = function(filePath, data, options) { + const protectedData = protectStorageJson(data, filePath); + return originalWriteFileSync.call(this, filePath, protectedData, options); + }; + + hooked.writeFile = function(filePath, data, options, callback) { + if (typeof options === 'function') { + callback = options; + options = undefined; + } + const protectedData = protectStorageJson(data, filePath); + return originalWriteFile.call(this, filePath, protectedData, options, callback); + }; + + // Hook promises API + if (fsModule.promises) { + const originalPromisesWriteFile = fsModule.promises.writeFile; + hooked.promises = { + ...fsModule.promises, + writeFile: async function(filePath, data, options) { + const protectedData = protectStorageJson(data, filePath); + return originalPromisesWriteFile.call(this, filePath, protectedData, options); + } + }; + } + + log('动态导入: 已 Hook fs 模块'); + hookedDynamicModules.set('fs', hooked); + return hooked; + }; + // 将 Hook 函数暴露到全局,供后续使用 globalThis.__cursor_hook_dynamic__ = { crypto: hookDynamicCrypto, deviceId: hookDynamicDeviceId, windowsRegistry: hookDynamicWindowsRegistry, + fs: hookDynamicFs, // 新增 fs Hook ids: __cursor_ids__ }; diff --git a/scripts/run/cursor_linux_id_modifier.sh b/scripts/run/cursor_linux_id_modifier.sh index 402cea4..cbf4311 100755 --- a/scripts/run/cursor_linux_id_modifier.sh +++ b/scripts/run/cursor_linux_id_modifier.sh @@ -1505,175 +1505,175 @@ EOF if [ "$(basename "$file")" = "main.js" ]; then if command -v python3 >/dev/null 2>&1; then local b6_result - b6_result=$(python3 - "$file" "$machine_guid" "$machine_id" <<'PY' -if True: - import re, sys - - def diag(msg): - print(f"[方案B][诊断] {msg}", file=sys.stderr) - - path, machine_guid, machine_id = sys.argv[1], sys.argv[2], sys.argv[3] - - with open(path, "r", encoding="utf-8") as f: - data = f.read() - - # ✅ 1+3 融合:限定 out-build/vs/base/node/id.js 模块内做特征匹配 + 花括号配对定位函数边界 - marker = "out-build/vs/base/node/id.js" - marker_index = data.find(marker) - if marker_index < 0: - print("NOT_FOUND") - diag(f"未找到模块标记: {marker}") - raise SystemExit(0) - - window_end = min(len(data), marker_index + 200000) - window = data[marker_index:window_end] - - def find_matching_brace(text, open_index, max_scan=20000): - limit = min(len(text), open_index + max_scan) - depth = 1 - in_single = in_double = in_template = False - in_line_comment = in_block_comment = False - escape = False - i = open_index + 1 - while i < limit: - ch = text[i] - nxt = text[i + 1] if i + 1 < limit else "" - - if in_line_comment: - if ch == "\n": - in_line_comment = False - i += 1 - continue - if in_block_comment: - if ch == "*" and nxt == "/": - in_block_comment = False - i += 2 - continue - i += 1 - continue - - if in_single: - if escape: - escape = False - elif ch == "\\\\": - escape = True - elif ch == "'": - in_single = False - i += 1 - continue - if in_double: - if escape: - escape = False - elif ch == "\\\\": - escape = True - elif ch == '"': - in_double = False - i += 1 - continue - if in_template: - if escape: - escape = False - elif ch == "\\\\": - escape = True - elif ch == "`": - in_template = False - i += 1 - continue - - if ch == "/" and nxt == "/": - in_line_comment = True - i += 2 - continue - if ch == "/" and nxt == "*": - in_block_comment = True - i += 2 - continue - - if ch == "'": - in_single = True - i += 1 - continue - if ch == '"': - in_double = True - i += 1 - continue - if ch == "`": - in_template = True - i += 1 - continue - - if ch == "{": - depth += 1 - elif ch == "}": - depth -= 1 - if depth == 0: - return i - - i += 1 - return None - - # 🔧 修复:避免 raw string + 单引号 + ['"] 字符组导致的语法错误;同时修正正则转义,提升 b6 特征匹配命中率 - hash_re = re.compile(r"""createHash\(["']sha256["']\)""") - sig_re = re.compile(r'^async function (\w+)\((\w+)\)') - - hash_matches = list(hash_re.finditer(window)) - diag(f"marker_index={marker_index} window_len={len(window)} sha256_createHash={len(hash_matches)}") - - for idx, hm in enumerate(hash_matches, start=1): - hash_pos = hm.start() - func_start = window.rfind("async function", 0, hash_pos) - if func_start < 0: - if idx <= 3: - diag(f"候选#{idx}: 未找到 async function 起点") - continue - - open_brace = window.find("{", func_start) - if open_brace < 0: - if idx <= 3: - diag(f"候选#{idx}: 未找到函数起始花括号") - continue - - end_brace = find_matching_brace(window, open_brace, max_scan=20000) - if end_brace is None: - if idx <= 3: - diag(f"候选#{idx}: 花括号配对失败(扫描上限内未闭合)") - continue - - func_text = window[func_start:end_brace + 1] - if len(func_text) > 8000: - if idx <= 3: - diag(f"候选#{idx}: 函数体过长 len={len(func_text)},已跳过") - continue - - sm = sig_re.match(func_text) - if not sm: - if idx <= 3: - diag(f"候选#{idx}: 未解析到函数签名(async function name(param))") - continue - name, param = sm.group(1), sm.group(2) - - # 特征校验:sha256 + hex digest + return param ? raw : hash - has_digest = re.search(r"""\.digest\(["']hex["']\)""", func_text) is not None - has_return = re.search(r'return\s+' + re.escape(param) + r'\?\w+:\w+\}', func_text) is not None - if idx <= 3: - diag(f"候选#{idx}: {name}({param}) len={len(func_text)} digest={has_digest} return={has_return}") - if not has_digest: - continue - if not has_return: - continue - - replacement = f'async function {name}({param}){{return {param}?"{machine_guid}":"{machine_id}";}}' - abs_start = marker_index + func_start - abs_end = marker_index + end_brace - new_data = data[:abs_start] + replacement + data[abs_end + 1:] - with open(path, "w", encoding="utf-8") as f: - f.write(new_data) - diag(f"命中并重写: {name}({param}) len={len(func_text)}") - print("PATCHED") - break - else: - diag("未找到满足特征的候选函数") - print("NOT_FOUND") + b6_result=$(python3 - "$file" "$machine_guid" "$machine_id" <<'PY' +# 🔧 修复:使用标准 4 空格缩进,避免 IndentationError +import re, sys + +def diag(msg): + print(f"[方案B][诊断] {msg}", file=sys.stderr) + +path, machine_guid, machine_id = sys.argv[1], sys.argv[2], sys.argv[3] + +with open(path, "r", encoding="utf-8") as f: + data = f.read() + +# ✅ 1+3 融合:限定 out-build/vs/base/node/id.js 模块内做特征匹配 + 花括号配对定位函数边界 +marker = "out-build/vs/base/node/id.js" +marker_index = data.find(marker) +if marker_index < 0: + print("NOT_FOUND") + diag(f"未找到模块标记: {marker}") + raise SystemExit(0) + +window_end = min(len(data), marker_index + 200000) +window = data[marker_index:window_end] + +def find_matching_brace(text, open_index, max_scan=20000): + limit = min(len(text), open_index + max_scan) + depth = 1 + in_single = in_double = in_template = False + in_line_comment = in_block_comment = False + escape = False + i = open_index + 1 + while i < limit: + ch = text[i] + nxt = text[i + 1] if i + 1 < limit else "" + + if in_line_comment: + if ch == "\n": + in_line_comment = False + i += 1 + continue + if in_block_comment: + if ch == "*" and nxt == "/": + in_block_comment = False + i += 2 + continue + i += 1 + continue + + if in_single: + if escape: + escape = False + elif ch == "\\": + escape = True + elif ch == "'": + in_single = False + i += 1 + continue + if in_double: + if escape: + escape = False + elif ch == "\\": + escape = True + elif ch == '"': + in_double = False + i += 1 + continue + if in_template: + if escape: + escape = False + elif ch == "\\": + escape = True + elif ch == "`": + in_template = False + i += 1 + continue + + if ch == "/" and nxt == "/": + in_line_comment = True + i += 2 + continue + if ch == "/" and nxt == "*": + in_block_comment = True + i += 2 + continue + + if ch == "'": + in_single = True + i += 1 + continue + if ch == '"': + in_double = True + i += 1 + continue + if ch == "`": + in_template = True + i += 1 + continue + + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + return i + + i += 1 + return None + +# 🔧 修复:避免 raw string + 单引号 + ['"] 字符组导致的语法错误;同时修正正则转义,提升 b6 特征匹配命中率 +hash_re = re.compile(r"""createHash\(["']sha256["']\)""") +sig_re = re.compile(r'^async function (\w+)\((\w+)\)') + +hash_matches = list(hash_re.finditer(window)) +diag(f"marker_index={marker_index} window_len={len(window)} sha256_createHash={len(hash_matches)}") + +for idx, hm in enumerate(hash_matches, start=1): + hash_pos = hm.start() + func_start = window.rfind("async function", 0, hash_pos) + if func_start < 0: + if idx <= 3: + diag(f"候选#{idx}: 未找到 async function 起点") + continue + + open_brace = window.find("{", func_start) + if open_brace < 0: + if idx <= 3: + diag(f"候选#{idx}: 未找到函数起始花括号") + continue + + end_brace = find_matching_brace(window, open_brace, max_scan=20000) + if end_brace is None: + if idx <= 3: + diag(f"候选#{idx}: 花括号配对失败(扫描上限内未闭合)") + continue + + func_text = window[func_start:end_brace + 1] + if len(func_text) > 8000: + if idx <= 3: + diag(f"候选#{idx}: 函数体过长 len={len(func_text)},已跳过") + continue + + sm = sig_re.match(func_text) + if not sm: + if idx <= 3: + diag(f"候选#{idx}: 未解析到函数签名(async function name(param))") + continue + name, param = sm.group(1), sm.group(2) + + # 特征校验:sha256 + hex digest + return param ? raw : hash + has_digest = re.search(r"""\.digest\(["']hex["']\)""", func_text) is not None + has_return = re.search(r'return\s+' + re.escape(param) + r'\?\w+:\w+\}', func_text) is not None + if idx <= 3: + diag(f"候选#{idx}: {name}({param}) len={len(func_text)} digest={has_digest} return={has_return}") + if not has_digest: + continue + if not has_return: + continue + + replacement = f'async function {name}({param}){{return {param}?"{machine_guid}":"{machine_id}";}}' + abs_start = marker_index + func_start + abs_end = marker_index + end_brace + new_data = data[:abs_start] + replacement + data[abs_end + 1:] + with open(path, "w", encoding="utf-8") as f: + f.write(new_data) + diag(f"命中并重写: {name}({param}) len={len(func_text)}") + print("PATCHED") + break +else: + diag("未找到满足特征的候选函数") + print("NOT_FOUND") PY ) if [ "$b6_result" = "PATCHED" ]; then