Showing Posts From
逆向
TrickyStore 替代方案:2026 年 Android Root 通过 Play Integrity 的主流组合
背景 TrickyStore 的核心功能是:劫持 Android KeyStore,注入 Keybox,伪造 TEE 硬件证明(Hardware Attestation),让 Play Integrity 拿到 MEETS_STRONG_INTEGRITY。 2026 年 Google 大规模推进 RKP(Remote Key Provisioning),设备 Keybox 越来越多通过远程下发,公开泄露的 Keybox 被大量封禁,TrickyStore 的有效性明显下降。 主流替代方案对比方案 用途 推荐度Play Integrity Fork Device / Basic Integrity 修复 ⭐⭐⭐⭐⭐ReZygisk 替代原生 Zygisk ⭐⭐⭐⭐Shamiko Root 痕迹隐藏 ⭐⭐⭐⭐HMA-OSS(Hide My Applist) 隐藏 Root、LSPosed、模块列表 ⭐⭐⭐⭐KernelSU Next + SUSFS Kernel 级隐藏,最难检测 ⭐⭐⭐⭐⭐TrickyStore Strong Integrity / TEE 硬件证明 ⭐⭐⭐(RKP 影响下效果变弱)各场景推荐组合 银行 App(不要求 Strong Integrity) Play Integrity Fork + HMA-OSS + ReZygisk很多银行只要求 DEVICE_INTEGRITY,更多是检测:Root 文件、Bootloader 状态、Magisk/LSPosed 痕迹、可疑 App 列表。HMA-OSS 可以对指定 App 隐藏已安装列表,避免被扫描到 Root 工具。 Google Wallet Google Wallet 目前仍需要 STRONG_INTEGRITY,TrickyStore 仍然是成功率最高的方案,没有更好的替代: TrickyStore + Play Integrity ForkKeybox 失效后需要更换有效的私有 Keybox,公开 Keybox 已不可靠。 游戏(Pokemon GO、吃鸡等) 游戏越来越少单纯依赖 Play Integrity,更多依赖自己的反作弊检测: KernelSU Next + SUSFS + HMA-OSSSUSFS 在内核层隐藏 /proc、/sys 特征,比用户空间隐藏更彻底,是目前社区反作弊对抗的主流选择。 完全放弃 TrickyStore 的通用方案 KernelSU Next + SUSFS + ReZygisk + Play Integrity Fork对绝大部分银行 App 和普通应用可以通过 BASIC + DEVICE Integrity,已经够用。 Magisk 体系 vs KernelSU 体系维度 Magisk 体系 KernelSU 体系方式 修改 boot.img 内核模块隐藏难度 中(用户空间检测较容易发现) 高(内核层操作更底层)兼容性 广(绝大多数设备) 需要设备支持 KMI 或自编译内核Zygisk 原生支持 需 ReZygiskSUSFS 支持 有 susfs4magisk,但效果弱于 KernelSU 原生支持,效果最好重要说明 银行 App 实际检测逻辑通常不只看 Play Integrity,还包括:/proc/mounts 是否有可疑挂载 系统分区是否有异常文件 可疑进程名(magiskd、zygote64d 等) 已安装 App 列表(通过 PackageManager)即使 Play Integrity 全部通过,Root 痕迹未隐藏干净仍可能被识别。HMA-OSS 对目标 App 可以精确控制可见的应用列表,是隐藏 Root 工具的关键一步。
为什么 Hook eval 会让 webpack 报 __webpack_require__ is not defined
问题复现 油猴脚本 hook eval 后报错: ReferenceError: __webpack_require__ is not defined at eval (service-mark.js:1:1) at eval (<anonymous>) at unsafeWindow.eval (my-script.user.js:28:24) at ./javascript/common/service-mark.js (task-common.js?61:240:1)hook 写法: const oldEval = window.eval;window.eval = function (code) { console.log(code); return oldEval.apply(this, args); // ← 问题在这里 };根本原因:直接调用 vs 间接调用 JS 中只有一种 direct eval(直接调用): eval(code)字面上写 eval(...) 才是直接调用,执行时能访问当前词法作用域。 其他所有形式都是 indirect eval(间接调用),在全局作用域执行: const e = eval; e(code); // indirecteval.call(null, code); // indirect eval.apply(this, [code]); // indirect (0, eval)(code); // indirect(常见技巧)webpack 模块代码是在模块工厂函数的作用域里 eval 执行的: function(module, exports, __webpack_require__) { eval("代码内容..."); // direct eval,能访问 __webpack_require__ }当你 hook 了 window.eval 并用 oldEval.apply(...) 转发时: window.eval = function(code) { return oldEval.apply(this, args); // indirect eval // ↑ 变成了间接调用,在全局作用域执行 };__webpack_require__ 是工厂函数的局部变量,全局作用域里不存在,所以报错。 为什么 return eval(code) 也不行 即使改成字面调用: unsafeWindow.eval = function(code) { return eval(code); // direct eval };这里的 eval(code) 是在你的 hook 函数作用域里执行的,不是在 webpack 工厂函数里。作用域仍然不对。 结论:无法通过 hook eval 的方式同时拦截代码和保持 webpack 的模块作用域。 这是 JS 引擎层面的限制,不是 hook 写法问题。 正确方案:hook webpackChunk.push 如果你需要修改 webpack 模块的源码(比如删掉 debugger),正确方式是在模块执行前 hook 模块定义,而不是 hook eval: const chunkKey = Object.keys(window).find(k => k.startsWith('webpackChunk'));const oldPush = window[chunkKey].push;window[chunkKey].push = function(chunk) { const modules = chunk[1]; for (const id in modules) { const fn = modules[id]; modules[id] = function(module, exports, __webpack_require__) { // 在这里可以拿到模块源码字符串 let code = fn.toString(); // 修改源码(例如移除 debugger) code = code.replace(/\bdebugger\b/g, ''); // 重建函数并在正确的参数上下文里执行 const newFn = eval('(' + code + ')'); return newFn(module, exports, __webpack_require__); }; } return oldPush.call(this, chunk); };这个方法的优势:在模块函数还没执行之前修改,不破坏作用域 __webpack_require__ 仍然由 webpack 正常传入 可以针对特定模块 ID 修改,不影响其他模块只是想看 eval 执行了什么代码 如果目的只是监听而不是转发执行,可以用 Object.defineProperty 劫持赋值时机: let _eval = window.eval;Object.defineProperty(window, 'eval', { get() { return _eval; }, set(v) { console.log('eval 被替换', v); _eval = v; }, configurable: true });或者用 Proxy 记录参数但不干预执行: const rawEval = window.eval;// 只记录,不修改执行上下文 const logFn = function(code) { console.log('[eval]', typeof code === 'string' ? code.slice(0, 200) : code); };// 在外层包一层,直接调用原始 eval window.__logEval = function(code) { logFn(code); return rawEval.call(this, code); };但真正的"无损 hook eval"做不到,只能选择:记录参数(丢失 webpack 作用域)或不 hook(不记录)。
读懂 webpack 混淆代码:(0, fn)() 模式与 RxJS 链解析
(0, fn)(...) 是什么 这是 webpack / Babel 生成代码中常见的间接调用写法: (0, s.p)(args)等价于: const _fn = s.p; _fn(args); // 普通函数调用,this === undefined(严格模式)为什么不直接 s.p(args)? s.p(args) 会将 this 绑定为 s;逗号运算符写法 (0, s.p) 会把方法"脱离"对象,变成普通函数调用,this 不再指向 s。 这在以下场景有实际意义:s.p 是从模块导入的函数(如 rxjs.from),不应绑 this 打包工具为了兼容 ES Modules 的 strict mode 语义,确保 this === undefined Tree-shaking 友好:逗号写法对某些打包器更易分析常见形式: (0, rxjs.of)(1, 2, 3) // 等价 rxjs.of(1, 2, 3) (0, lodash.map)(arr, fn) // 等价 lodash.map(arr, fn) (0, react.createElement)(...) // 等价 react.createElement(...) (0, o.Z)(list) // 混淆后的模块导出函数读到这种代码,直接把 (0, expr) 去掉,剩下的 expr(...) 就是实际调用。 真实打包代码示例 下面是一段典型的混淆打包代码(来自实际页面逆向): (0, s.p)( (t = window.$).when.apply(t, (0, o.Z)(window.beforeSubmitQueue.map(function(e) { return e(); })) ) ).switchMap( window.MarkLib.submitMarkAnswer.bind(window.MarkLib, !0, e) ).filter(function(t) { var n = t.fire_status, r = void 0 === n || n; var a = t.reject, o = void 0 !== a && a; return !e && w(!1), r && !o; }).map(JSON.stringify.bind(JSON)).do(function(t) { return y({ type: "markpage/saveAnswer", payload: { isTemporary: 1, packetId: Number(p), ... } }); }).subscribe();逐步还原 第一步:去掉 (0, expr) 包装 s.p(...) // s.p 可能是 rxjs.from / rxjs.fromPromise o.Z(list) // o.Z 是某个导出函数,根据上下文判断第二步:提取 window 上的对象引用 const { $, beforeSubmitQueue, MarkLib } = window;第三步:还原 .apply 和 .bind // (t = window.$).when.apply(t, args) → $.when(...args) // fn.bind(obj, true, e) → () => obj.fn(true, e)第四步:改写旧版 RxJS API旧版 新版.do(fn) .tap(fn)fromPromise(p) from(p).map(fn) .map(fn) 不变还原结果: const { $, beforeSubmitQueue, MarkLib } = window;from($.when(...beforeSubmitQueue.map(fn => fn()))) .pipe( switchMap(() => MarkLib.submitMarkAnswer(true, e)), filter(({ fire_status = true, reject = false }) => { if (!e) w(false); return fire_status && !reject; }), map(JSON.stringify), tap(answer => { y({ type: "markpage/saveAnswer", payload: { isTemporary: 1, packetId: Number(p), pageId: Number(a), round: f, stage: k, answer: j(answer, c, u) }, callback() { if (!e) w(false); } }); }) ) .subscribe();逻辑说明 还原后的业务逻辑只有一句话:执行提交前钩子(beforeSubmitQueue)→ 提交答案(submitMarkAnswer)→ 过滤无效结果 → 序列化 → 保存到 Redux store关键点:$.when(...tasks):jQuery Deferred,等待所有 before-hooks 完成 switchMap:前一个 Observable 完成后切换到新的,丢弃之前未完成的 filter:fire_status 不为 false 且 reject 不为 true 才继续 .subscribe():没有 subscribe,RxJS 链不会执行(冷 Observable)其他常见混淆模式 // void 0 === undefined void 0 // → undefined// !! 转布尔 !!value // → Boolean(value)// 逗号运算符返回最后一个值 (a(), b(), c()) // 执行 a、b、c,返回 c 的值// 短路赋值 x = void 0 === x ? defaultVal : x // → x ?? defaultVal(现代写法)读打包代码时,把这些模式认出来后,理解代码逻辑会快很多。
Electron asar 解包:资源为什么只有一部分以及各类资源的位置
解包 app.asar 后只看到"部分资源",通常不是工具有问题,而是资源本来就不全在 asar 里。 app.asar.unpacked 目录 很多 Electron 应用把资源拆成两部分: resources/ ├─ app.asar ← 打包进归档的前端代码 └─ app.asar.unpacked ← 不打包,直接放文件系统不能放进 asar 的资源通常在 unpacked:.node 原生模块 dll、exe 二进制 ffmpeg 大模型权重 WASM 文件 Chromium 扩展解包命令: npx asar extract app.asar out查看归档内容: npx asar list app.asar运行时动态下载 很多 AI 软件、IDE 首次启动后会在线下载:JS chunk 模型权重 WASM 模块 加密资源这些完全不在 asar 里。常见存放位置: # Windows %AppData%\Roaming\应用名\ ← 配置、数据库、历史记录 %AppData%\Local\应用名\ ← 缓存、大文件、模型webpack 打包合并 现代 Electron 应用几乎不会保留 src/pages/ 这样的目录结构,而是打成 bundle: main.js renderer.js chunk.abc123.js你以为"缺文件",其实文件已经被合并进 bundle 了。搜索以下关键词确认: webpackChunk __webpack_require__加密和混淆 部分商业应用会:存 .jsc / .bin V8 字节码(bytenode),无法直接还原源码 webpack chunk 内字符串 XOR 混淆 运行时解密:decrypt(buffer); eval(...) 拿到真正的代码各类资源的位置速查目标 位置前端源码(React/Vue) app.asar 解包后的 bundle配置、token、sqlite %AppData%\Roaming\应用名缓存、模型、大文件 %AppData%\Local\应用名原生模块、二进制 app.asar.unpacked内联图片 bundle 里 base64 编码关键 IPC 桥接逻辑 preload.jsElectron 默认缓存目录 Cache/ Code Cache/ ← 可能有 V8 字节码缓存 GPUCache/ IndexedDB/ ← 配置、文档、AI 数据 Local Storage/ blob_storage/完整逆向流程 # 1. 解包 npx asar extract app.asar out# 2. 找 webpack 入口 grep -r "__webpack_require__" out/# 3. 重点看 preload # out/main/preload.js 或 dist-electron/preload.js# 4. 如果有加密,搜解密关键词 grep -r "decrypt\|Buffer\|crypto\|atob" out/# 5. 找用户数据 ls %AppData%\Roaming\应用名常见真实目录结构 MyApp/ ├─ resources/ │ ├─ app.asar │ ├─ app.asar.unpacked/ │ │ ├─ native.node │ │ └─ ffmpeg.dll │ └─ ... └─ locales/AppData/Roaming/MyApp/ ├─ IndexedDB/ ├─ Local Storage/ ├─ settings.json └─ user.db
解包 Electron 的 app.asar 只有部分文件?六个原因
Electron 应用的 resources/app.asar 解开看,只有一部分源码 / 资源——很多明明代码里 require 过的模块或者用到的图片、模型完全找不到。不是解包工具坏,是 Electron 应用故意这么组织的。 1. 文件在 app.asar.unpacked 最常见。resources/ 目录下有两个: resources/ ├── app.asar # 归档 └── app.asar.unpacked/ # 不归档,直接文件系统electron-builder / electron-packager 有 asarUnpack 配置,指定哪些文件不打进 asar。原因:原生模块 .node:Node 加载 native addon 只认真实文件路径 可执行文件(ffmpeg.exe、外部工具) 大文件(模型、字体、图片)——放 asar 会导致启动时全部读到内存 需要写入的资源(缓存、配置模板)解决:同时解 asar 和 unpacked 目录,才能拿全资源。 npx asar extract app.asar out/ # 然后手动合并 app.asar.unpacked 里的内容2. 运行时动态下载 很多 AI / IDE / 客户端首次启动会拉模型或 SDK:Cursor / VSCode 下载语言服务 Notion / Figma 缓存 web 资源 Whisper.cpp 下运行 GGUF 模型 客户端更新组件到 %AppData%常见落地目录: Windows: %APPDATA%\<AppName>\ %LOCALAPPDATA%\<AppName>\macOS: ~/Library/Application Support/<AppName>/ ~/Library/Caches/<AppName>/Linux: ~/.config/<AppName>/ ~/.cache/<AppName>/这些目录不属于 asar,别在 asar 里找。 3. webpack chunk 打包 / 混淆 代码里看到: require("./modules/wallet.js")在 asar 里 modules/ 目录空空如也。原因是打包时被 webpack 摊平: (() => { var __webpack_modules__ = { 123: (e, t, r) => { /* wallet.js 的内容 */ }, 456: (e, t, r) => { /* something else */ }, }; // ... })();wallet.js 变成模块 ID 123,塞进主 bundle。名字都没了,找源文件是找不到的。 恢复思路:用 webcrack 或 react-native-decompiler 尝试还原模块结构 source map 有的话直接 .map 文件恢复 主 bundle 里搜函数名 / 字符串手工定位4. asar 完整性校验 Electron 的 --asar-integrity 特性(v22+)——启动时校验 asar 的 SHA-256,被改过就直接崩: Integrity check failed: 0x...有的应用还会自校验: const hash = crypto.createHash("sha256").update(fs.readFileSync(asarPath)).digest("hex"); if (hash !== EXPECTED_HASH) app.exit();改过 asar 塞回去,应用启动就退。想跑起来得同时改校验逻辑。 5. 资源加密 商业 Electron 应用常见做法: 方式 A:启动时解密 const encryptedBuffer = fs.readFileSync("./data.bin"); const source = decrypt(encryptedBuffer, KEY); new Function(source)(); // 或 eval, vm.runInNewContextasar 里只能看到加密后的 blob。想读得先拿到 KEY——通常在主 bundle 里硬编码或经过混淆。 方式 B:V8 Bytecode(bytenode) xxx.jsc xxx.bin这些是 bytenode 编译的 V8 字节码,普通反编译工具还原不了源码。只能在运行时逆向。 6. 解包工具版本太老 老的第三方解包工具不支持新版 asar 格式:新版 asar header 有 offset 大文件跨块 unicode 文件名编码变化永远优先用官方: # 官方 CLI npx @electron/asar extract app.asar out/ npx @electron/asar list app.asar或者装全局: npm i -g @electron/asar asar extract app.asar out/一张排查表 看到"资源缺",按这个顺序查:现象 大概率原因.node / ffmpeg 找不到 在 app.asar.unpacked模型 / 大图找不到 unpacked 或运行时下载require 的模块找不到源文件 webpack 打包混淆.jsc / .bin 打不开 V8 bytecode 或加密改了 asar 应用启动就退 完整性校验一部分目录空 / 文件小 解包工具太老,换 @electron/asar一句话总结 Electron 解包"少东西"是常态。先看 app.asar.unpacked、再看 %AppData% 用户目录、再考虑 webpack chunk / 加密 / bytecode。用 @electron/asar 官方工具,别用老的第三方版本。
JSC 文件分析:V8 字节码、bytenode 产物与字符串提取
JSC 文件的三种类型 .jsc 不是一种统一格式,常见有三类:类型 本质 能否反编译bytenode 生成 V8 字节码 基本不能还原源码JavaScriptCore(iOS/WebKit) JSCore 缓存 无稳定通用工具微信小程序 V8 字节码或混淆加密体 几乎不可能还原核心结论:JSC 是编译产物,不是源码压缩。变量名、结构、注释已丢失,不存在完美反编译。 View8 报错原因 RuntimeError: Failed to detect version for file index.jscView8 要先检测 V8 版本,失败通常是这几个原因:文件不是 V8 cache——bytenode 生成的 .jsc 结构与 V8 code cache 不同 .jsc 被加了 header、base64 或 zip 包装 V8 版本超出 View8 支持范围(通常只支持 8.x~11.x)用 Python 快速判断: with open("index.jsc", "rb") as f: print(f.read(32).hex())V8 code cache 开头有固定的 magic bytes。如果头部是乱码或压缩特征,View8 直接放弃。 Python 提取可读字符串 不管是哪种 JSC,都可以先提取二进制文件中的可读字符串,判断是否有逆向价值: import rewith open("index.jsc", "rb") as f: data = f.read()strings = re.findall(rb"[ -~]{6,}", data)seen = set() for s in strings: text = s.decode("utf-8", errors="ignore") if text not in seen: seen.add(text) print(text)正则 [ -~]{6,} 匹配长度 >= 6 的 ASCII 可读字符串。 结果判断:出现 /api/、token、userInfo 等 → 文件未加密,有分析价值 全是乱码 → 已加密或压缩,需换思路bytenode JSC 的运行时分析 bytenode 产物无法静态反编译,但可以在 Node.js 中加载后 hook: require("bytenode");// hook 所有函数调用 const originalCall = Function.prototype.call; Function.prototype.call = function(...args) { if (this.name) { console.log("[call]", this.name, args.slice(0, 2)); } return originalCall.apply(this, args); };require("./index.jsc");或者用 Node.js inspect 调试: node --inspect-brk -e "require('bytenode'); require('./index.jsc')"然后用 Chrome DevTools 连接 chrome://inspect,可以打断点、查看调用栈。 三种方案对比方案 成功率 适用场景Python strings 提取 高(未加密) 快速判断有无价值运行时 hook 高 bytenode 产物Node inspect 调试 高 有 require 入口静态 V8 字节码分析 低 仅作辅助对于加密 JSC(AES/RC4 加密,或微信小游戏自定义 VM),以上方法均不适用,需要先逆向解密逻辑。
