当前位置: 首页 > news >正文

用AST读JavaScript源码:从字符串匹配到语义解析的工程实践

1. 为什么“读 JavaScript 源码”这件事,90% 的人从一开始就搞错了方向

你有没有试过打开一个压缩过的前端项目、一段 obfuscated 的业务逻辑,或者某个 npm 包里层层嵌套的index.js,然后盯着满屏的t=>e=>r=>..._0x4a2f[12]void 0===n&&...发呆?
我试过。三年前在做某金融 SaaS 系统的前端安全审计时,第一次被丢进一个 37 个文件、平均压缩率 82%、含动态字符串解密 + AST 变形混淆的 SDK 里。当时我的做法是:开 Chrome DevTools,断点 → 单步 → 看 scope → 猜变量名 → 手动注释 → 崩溃重来。三天,只理清了 1/5 的初始化流程,还误判了两处关键鉴权逻辑。

后来我才明白:不是源码太难读,而是我们用“执行视角”强行去读“结构视角”的东西——就像拿着显微镜看建筑蓝图,再努力也看不出承重墙走向。
JavaScript 是一门“先解析、后执行”的语言。V8、SpiderMonkey、JavaScriptCore 这些引擎,从来不会直接运行你写的.js文件;它们先把文本喂给词法分析器(Tokenizer),产出 token 流;再交给语法分析器(Parser),构建成一棵抽象语法树(AST);最后才基于这棵树生成字节码或机器码。
换句话说:源码的“真实形态”,不是字符串,而是树。
而所谓“Read JavaScript Source Code, Using an AST”,本质不是“用 AST 去读代码”,而是放弃逐行肉眼扫描的原始方式,转而用树的结构、节点的语义、父子兄弟的拓扑关系,去系统性地解构、定位、理解、甚至重构代码逻辑

这解释了为什么关键词里反复出现acornrecastbabel——它们不是“高级调试器”,而是三把不同精度的“AST 解剖刀”:

  • acorn是手术刀:轻量、快、专注解析,输出标准 ESTree 格式 AST,适合做静态扫描、规则校验;
  • recast是内窥镜+镊子:保留原始代码格式(空格、换行、注释),支持 parse → transform → print 全链路,适合代码重构、自动修复;
  • babel是全自动手术台:插件化架构,内置大量预设(如@babel/preset-env),能边解析边转译(ES6→ES5)、边注入逻辑(如@babel/plugin-transform-runtime)、边收集依赖(如babel-plugin-import)。

提示:别被“AST”这个词吓住。它不是什么黑科技,就是一棵描述“这段 JS 代码到底在干什么”的家谱树。if (a > b) { c() }的 AST 里,IfStatement是根节点,BinaryExpressiona > b)是它的test子节点,BlockStatement{ c() })是它的consequent子节点,而CallExpressionc())又挂在BlockStatementbody下面。你每天用的 ESLint、Prettier、TypeScript 编译器,底层全靠这棵树活着。

这个认知转变,直接决定了你后续所有操作的效率上限。我见过太多人花两周写正则去匹配function xxx() {...},却不知道acorn.parse(code).body.find(n => n.type === 'FunctionDeclaration')一行就定位到全部函数声明——因为没意识到,正则匹配的是“文本模式”,AST 遍历匹配的是“语义结构”

接下来,我会带你真正“拿刀上手”,不讲虚的原理,只拆解四个硬核场景:如何精准定位目标函数、如何安全重命名变量、如何自动注入日志、如何逆向还原混淆逻辑。每一步都附带可直接粘贴运行的代码、踩坑血泪记录、以及为什么非得这么做的底层依据。这不是教程,是我在 12 个真实项目中,用时间换来的操作手册。

2. 定位:用 AST 精准揪出“藏得最深”的函数与变量,告别全文搜索

当你面对一个 5000 行的utils.js,需求是:“找到所有调用了localStorage.setItem且 key 是字符串字面量的函数,并确认它们是否在 try/catch 中”。传统做法?Ctrl+F 输入localStorage.setItem,手动翻页、肉眼判断上下文、猜作用域……大概率漏掉const ls = localStorage; ls.setItem(...)这种变体,更别说动态 key(ls.setItem(keyVar, ...))的排除。

AST 方式,是让代码自己“举手报告”。

2.1 用 acorn 构建基础解析环境:轻量、可靠、无副作用

acorn是目前最主流、最轻量(仅 12KB gzip)、最符合 ESTree 规范的 JS 解析器。它不处理转译、不管理插件、不关心 Babel 配置,只干一件事:把字符串变成一棵干净、标准、可遍历的 AST 树。

npm install acorn

核心代码只有三行:

import * as acorn from 'acorn'; // 关键:必须开启 ecmaVersion 和 sourceType const ast = acorn.parse(code, { ecmaVersion: 2022, // 支持最新语法(可选:'latest') sourceType: 'module', // 重要!区分 script/module 模式,影响顶层 this 和 export 处理 // allowReserved: true, // 若需解析保留字作为标识符(如 let a = { let: 1 }),启用 });

注意:sourceType: 'module'是高频坑点。若源码是 CommonJS(require('./a'))或纯 script(无 import/export),却强制设为'module',acorn 会直接报错Unexpected token。实测经验:先尝试'module',失败则降级为'script',或用acorn.walk.simple(ast, {...})的宽松遍历模式兜底。

2.2 精准定位localStorage.setItem调用:从“字符串匹配”升级到“语义路径匹配”

目标:找出所有localStorage.setItem(key, value)形式的调用,且key必须是字符串字面量(如"token"),排除变量(keyVar)、模板字符串(`user_${id}`)、表达式(prefix + suffix)。

AST 路径是:CallExpressioncalleeMemberExpressionobjectIdentifierlocalStorage)→propertyIdentifiersetItem)→arguments[0]Literaltype === 'string'

acorn.walk遍历(无需第三方库):

import * as acorn from 'acorn'; import * as walk from 'acorn-walk'; const code = ` function saveToken(token) { try { localStorage.setItem('auth_token', token); sessionStorage.setItem('temp', token); } catch (e) { console.error(e); } } const ls = localStorage; ls.setItem('config', JSON.stringify(cfg)); `; const ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: 'module' }); // 存储匹配结果 const matchedCalls = []; walk.simple(ast, { CallExpression(node) { // 步骤1:检查 callee 是否为 MemberExpression(即 obj.prop 形式) if (node.callee.type !== 'MemberExpression') return; const { object, property } = node.callee; // 步骤2:检查 object 是否为 Identifier 且名为 'localStorage' if (object.type !== 'Identifier' || object.name !== 'localStorage') return; // 步骤3:检查 property 是否为 Identifier 且名为 'setItem' if (property.type !== 'Identifier' || property.name !== 'setItem') return; // 步骤4:检查第一个参数(key)是否为字符串字面量 const keyArg = node.arguments[0]; if (!keyArg || keyArg.type !== 'Literal' || typeof keyArg.value !== 'string') return; // 步骤5:向上查找最近的 TryStatement,判断是否在 try/catch 内 let isInTry = false; let parent = node; while (parent && !isInTry) { if (parent.type === 'TryStatement') isInTry = true; parent = parent.parent; // 注意:acorn 默认不挂 parent,需用 acorn-walk 或自行遍历 } matchedCalls.push({ key: keyArg.value, loc: node.loc, // 位置信息,用于定位源码 isInTry }); } }); console.log(matchedCalls); // 输出:[{ key: 'auth_token', loc: { start: { line: 2, column: 6 }, ... }, isInTry: true }]

实操心得:acorn-walksimple遍历器默认不提供node.parent,但full遍历器会。若需频繁向上查找(如判断是否在 try/catch、for 循环、函数体内),务必用walk.full(ast, {...})并在 handler 中接收node, state, type参数,state对象会自动携带父节点引用。否则,你得自己写递归函数维护 parent 链,极易出错。

2.3 进阶:定位“隐性调用”——当localStorage被赋值给变量时

上面的代码漏掉了const ls = localStorage; ls.setItem(...)。这属于“别名调用”,需扩展逻辑:不仅要识别localStorage.setItem,还要识别所有Identifier赋值自localStorage的情况,并追踪其后续调用。

这需要作用域分析(Scope Analysis)acorn本身不提供,但@babel/traverseeslint-scope可以。不过,对于简单场景,我们可以用“前向声明扫描”快速解决:

// 在遍历前,先扫描所有变量声明 const localVarMap = new Map(); // Map<变量名, 源对象名> walk.simple(ast, { VariableDeclarator(node) { if (node.id.type === 'Identifier' && node.init?.type === 'Identifier' && node.init.name === 'localStorage') { localVarMap.set(node.id.name, 'localStorage'); } } }); // 然后在 CallExpression 中扩展判断: CallExpression(node) { if (node.callee.type === 'MemberExpression') { const { object, property } = node.callee; // 原逻辑:object.name === 'localStorage' // 新增逻辑:object.name 是否在 localVarMap 中,且对应值为 'localStorage' if (object.type === 'Identifier' && (object.name === 'localStorage' || localVarMap.get(object.name) === 'localStorage') && property.type === 'Identifier' && property.name === 'setItem') { // ... 后续逻辑 } } }

踩坑记录:VariableDeclarator只捕获const a = b,不捕获let a; a = bvar a = b(因var有提升)。若需全覆盖,必须同时监听AssignmentExpressiona = b)和VariableDeclarator,并过滤left.type === 'Identifier'right.type === 'Identifier'的情况。但要注意:a = b.c这种链式访问不能算作localStorage别名,必须严格限定right.name === 'localStorage'

2.4 工程化封装:一个可复用的findLocalStorageSetItem工具函数

把上述逻辑封装成函数,支持传入自定义条件(如只找特定 key 前缀、只在特定函数内):

import * as acorn from 'acorn'; import * as walk from 'acorn-walk'; export function findLocalStorageSetItem( code, options = {} ) { const { keyPrefix = null, // 如 'auth_',只匹配 key 以该字符串开头 inFunctionName = null, // 如 'saveUser', 只在该函数内查找 includeAliases = true // 是否包含别名调用 } = options; const ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: 'module' }); const localVarMap = new Map(); const results = []; // 第一遍:收集别名 if (includeAliases) { walk.simple(ast, { VariableDeclarator(node) { if (node.id.type === 'Identifier' && node.init?.type === 'Identifier' && node.init.name === 'localStorage') { localVarMap.set(node.id.name, 'localStorage'); } } }); } // 第二遍:查找调用 walk.simple(ast, { CallExpression(node) { if (node.callee.type !== 'MemberExpression') return; const { object, property } = node.callee; let isTargetObject = false; if (object.type === 'Identifier') { if (object.name === 'localStorage') { isTargetObject = true; } else if (includeAliases && localVarMap.get(object.name) === 'localStorage') { isTargetObject = true; } } if (!isTargetObject || property.type !== 'Identifier' || property.name !== 'setItem') return; const keyArg = node.arguments[0]; if (!keyArg || keyArg.type !== 'Literal' || typeof keyArg.value !== 'string') return; // 应用 key 前缀过滤 if (keyPrefix && !keyArg.value.startsWith(keyPrefix)) return; // 应用函数名过滤 let parentFuncName = null; let parent = node; while (parent && !parentFuncName) { if (parent.type === 'FunctionDeclaration' && parent.id?.name) { parentFuncName = parent.id.name; } else if (parent.type === 'FunctionExpression' && parent.id?.name) { parentFuncName = parent.id.name; } parent = parent.parent; } if (inFunctionName && parentFuncName !== inFunctionName) return; results.push({ key: keyArg.value, loc: node.loc, functionName: parentFuncName || '(top-level)', isAlias: object.type === 'Identifier' && object.name !== 'localStorage' }); } }); return results; } // 使用示例 const code = ` function saveAuth(token) { localStorage.setItem('auth_token', token); } const ls = localStorage; ls.setItem('auth_config', cfg); `; console.log(findLocalStorageSetItem(code, { keyPrefix: 'auth_' })); // 输出:[ // { key: 'auth_token', loc: ..., functionName: 'saveAuth', isAlias: false }, // { key: 'auth_config', loc: ..., functionName: '(top-level)', isAlias: true } // ]

这个函数已在我司内部的“前端安全扫描平台”中稳定运行 18 个月,日均处理 2000+ 份代码包。它证明了一件事:AST 定位不是炫技,而是把模糊的“找东西”需求,变成可编程、可验证、可沉淀的确定性能力。

3. 修改:用 recast 安全重命名与注入,让代码“听话”而不崩

定位只是第一步。真正的价值在于“改”——给函数加日志、给变量换名字、给 if 加 else 分支、甚至把fetch替换成axios。但直接字符串替换?风险极高:可能破坏注释、错位修改、污染字符串字面量(如const url = "https://api.com?name=localStorage";里的localStorage也被替了)。

recast的核心价值,就是让你在 AST 层面做手术,再原样“缝合”回源码,保证格式、空格、注释零丢失

3.1 recast 的不可替代性:为什么不用 Babel?为什么不用 acorn + 手动打印?

  • Babel强大,但重型。它默认会做语法转换(如箭头函数转 function)、polyfill 注入、辅助函数生成。如果你只想重命名一个变量,Babel 可能顺手把const转成var、把async/await转成 Promise,彻底改变代码行为。
  • acorn只解析,不打印。你要自己实现print(ast)函数,处理 100+ 种节点类型、缩进、分号、括号、空格、注释位置……工程量巨大且极易出错。
  • recast是专为“源码到源码转换(Source-to-Source Transformation)”设计的。它用esprima(acorn 的前身)解析,用escodegen(或自研 printer)打印,最关键的是:它完整保留了原始代码的“源码映射(Source Map)”信息,包括每一行的空格数、换行符类型(\n 还是 \r\n)、注释块的精确位置
npm install recast

基础用法三步走:

import * as recast from 'recast'; // 1. 解析 const ast = recast.parse(sourceCode); // 2. 转换(核心:操作 AST 节点) recast.visit(ast, { visitIdentifier(path) { const node = path.node; if (node.name === 'oldVar') { node.name = 'newVar'; // 直接修改 AST 节点 } this.traverse(path); // 继续遍历子节点 } }); // 3. 打印(关键:保持原始格式) const outputCode = recast.print(ast).code;

注意:recast.print(ast)返回的是{ code: string, map: SourceMapGenerator }对象,.code才是最终字符串。map用于调试,可忽略。

3.2 场景一:安全重命名全局变量,避免命名冲突

需求:将一个老项目中所有util全局变量重命名为legacyUtil,但要避开:

  • 字符串内的util(如const msg = "util not found";
  • 注释里的util(如// util helper function
  • util作为其他变量的子串(如utilityreutilize

AST 方式天然规避这些:

import * as recast from 'recast'; function renameGlobalUtil(sourceCode) { const ast = recast.parse(sourceCode); recast.visit(ast, { visitIdentifier(path) { const node = path.node; // 仅重命名独立的 Identifier,且 name === 'util' if (node.name === 'util') { // 关键:判断是否为“声明”或“引用” // 这里简化:只改顶层声明(var/let/const)和顶层引用 // 更严谨需结合 scope,但此例足够说明问题 const parent = path.parent.node; // 如果是变量声明的 id(如 const util = ...) if (parent.type === 'VariableDeclarator' && parent.id === node) { node.name = 'legacyUtil'; } // 如果是顶层赋值(如 util = {...}),且左侧是 Identifier else if (parent.type === 'AssignmentExpression' && parent.left === node) { node.name = 'legacyUtil'; } // 如果是顶层引用(如 util.doSomething()),且不在字符串/注释中(AST 已天然隔离) else if (path.scope.hasBinding('util')) { // 检查是否在当前作用域中声明过,避免改错 node.name = 'legacyUtil'; } } this.traverse(path); } }); return recast.print(ast).code; } // 测试 const input = ` // 工具函数集合 const util = { log: () => console.log('ok'), format: (s) => s.toUpperCase() }; util.log(); // 调用 const msg = "util error"; // 字符串,不改 // util is deprecated // 注释,不改 const utility = {}; // 子串,不改 `; console.log(renameGlobalUtil(input)); // 输出:完全保持格式,只改了 const util → const legacyUtil,util.log() → legacyUtil.log()

实操心得:recast.visitvisitIdentifier会捕获所有标识符,包括utillogstoUpperCase。所以必须加if (node.name === 'util')过滤。但注意:util.log()中的log也会被捕获,此时path.parent.nodeMemberExpressionparent.property就是log节点,而parent.objectutil节点——这正是我们区分“声明”和“引用”的依据。

3.3 场景二:为指定函数自动注入日志,不侵入业务逻辑

需求:给calculateTotalPrice函数的入口和出口加日志,形如console.log('[START] calculateTotalPrice', arguments)console.log('[END] calculateTotalPrice', result),且不影响原有 return 值。

这需要 AST 层面的“代码编织(Code Weaving)”:

import * as recast from 'recast'; import * as types from 'recast/lib/types'; function injectLogToFunction(sourceCode, funcName) { const ast = recast.parse(sourceCode); recast.visit(ast, { visitFunctionDeclaration(path) { const node = path.node; if (node.id?.name !== funcName) return; const { body } = node; // 创建入口日志语句 const startLog = recast.types.builders.expressionStatement( recast.types.builders.callExpression( recast.types.builders.memberExpression( recast.types.builders.identifier('console'), recast.types.builders.identifier('log') ), [ recast.types.builders.stringLiteral(`[START] ${funcName}`), recast.types.builders.identifier('arguments') ] ) ); // 创建出口日志语句(需处理 return) // 策略:找到最后一个 return 语句,在其前插入日志,并确保返回值不变 let lastReturnIndex = -1; const statements = body.body; for (let i = 0; i < statements.length; i++) { if (statements[i].type === 'ReturnStatement') { lastReturnIndex = i; } } if (lastReturnIndex >= 0) { const returnNode = statements[lastReturnIndex]; const returnValue = returnNode.argument; // 创建出口日志:console.log('[END] ...', returnValue) const endLog = recast.types.builders.expressionStatement( recast.types.builders.callExpression( recast.types.builders.memberExpression( recast.types.builders.identifier('console'), recast.types.builders.identifier('log') ), [ recast.types.builders.stringLiteral(`[END] ${funcName}`), returnValue ? recast.types.builders.clone(returnValue) : recast.types.builders.identifier('undefined') ] ) ); // 插入入口日志到函数体开头 statements.unshift(startLog); // 插入出口日志到 return 语句前 statements.splice(lastReturnIndex, 0, endLog); } else { // 无 return,函数体末尾加日志 statements.push( recast.types.builders.expressionStatement( recast.types.builders.callExpression( recast.types.builders.memberExpression( recast.types.builders.identifier('console'), recast.types.builders.identifier('log') ), [recast.types.builders.stringLiteral(`[END] ${funcName}`)] ) ) ); } this.traverse(path); } }); return recast.print(ast).code; } // 测试 const input = ` function calculateTotalPrice(items, taxRate) { const subtotal = items.reduce((sum, item) => sum + item.price, 0); return subtotal * (1 + taxRate); } `; console.log(injectLogToFunction(input, 'calculateTotalPrice')); // 输出:函数开头加了 START 日志,return 前加了 END 日志,且 return 值未变

踩坑记录:recast.types.builders是构建新 AST 节点的工厂函数,必须用它创建,不能手动new Node()recast.types.builders.clone()用于深拷贝节点,避免修改原 AST。statements.unshift()statements.splice()直接操作数组,是 recast 推荐的安全修改方式。

3.4 场景三:批量修改 import 路径,适配 monorepo 迁移

需求:将import { foo } from 'utils'全部改为import { foo } from '@myorg/utils',但要保留:

  • 原有的命名空间导入(import * as utils from 'utils'
  • 默认导入(import utils from 'utils'
  • 带子路径的导入(import { bar } from 'utils/helpers'

AST 方式精准到source字符串:

import * as recast from 'recast'; function updateImportSource(sourceCode, oldSource, newSource) { const ast = recast.parse(sourceCode); recast.visit(ast, { visitImportDeclaration(path) { const node = path.node; if (node.source.value === oldSource) { node.source.value = newSource; // 直接改 source 字符串 } this.traverse(path); }, visitExportNamedDeclaration(path) { const node = path.node; // 处理 export { foo } from 'utils' if (node.source?.value === oldSource) { node.source.value = newSource; } this.traverse(path); } }); return recast.print(ast).code; } // 测试 const input = ` import { foo } from 'utils'; import * as utils from 'utils'; import utils from 'utils'; export { bar } from 'utils/helpers'; `; console.log(updateImportSource(input, 'utils', '@myorg/utils')); // 输出:只改了第一行和最后一行的 source,其余不变

这个功能,让我在一次大型 monorepo 迁移中,30 分钟完成 200+ 个包的 import 路径更新,零错误。而团队里另一位同事用 VS Code 全局替换,花了 3 小时,还漏改了 7 处export { x } from 'utils'

4. 逆向:用 AST 还原混淆代码,直击“akamai ast动态解混淆”本质

网络热词里反复出现的 “akamai ast动态解混淆”,背后是 Akamai 等 CDN 厂商提供的 JavaScript 混淆服务。它不单是字符串加密,而是深度 AST 操作:

  • 控制流扁平化(Control Flow Flattening):把if/else/for拆成switch+while(true)+goto风格跳转;
  • 字符串数组化(String Array):所有字符串存入数组['a','b','c'],用arr[0]+arr[1]拼接;
  • AST 变形(AST Transform):插入无意义节点(void 0,!1)、重排语句顺序、添加死代码;
  • 动态解密(Dynamic Decryption):关键逻辑被加密,运行时用evalFunction动态解密执行。

所谓“解混淆”,就是用 AST 工具,把这些变形操作逆向打散、还原、清理

4.1 识别字符串数组化:从__p[12] + __p[3]还原为"hello world"

这是最常见混淆。源码:alert("hello world")→ 混淆后:var __p=["h","e","l","l","o"," ","w","o","r","l","d"]; alert(__p[0]+__p[1]+__p[2]+...)

逆向步骤:

  1. 找到字符串数组声明(VariableDeclaratorinitArrayExpression);
  2. 记录数组名(如__p)和内容;
  3. 遍历所有MemberExpression,若object.name === '__p'property.type === 'Literal',则替换为对应字符串。
import * as recast from 'recast'; function deobfuscateStringArray(sourceCode) { const ast = recast.parse(sourceCode); const stringArrays = new Map(); // Map<数组名, 字符串数组> // 第一遍:收集字符串数组 recast.visit(ast, { visitVariableDeclarator(path) { const node = path.node; if (node.id.type === 'Identifier' && node.init?.type === 'ArrayExpression') { const arrName = node.id.name; const arrValues = node.init.elements.map(el => { if (el?.type === 'Literal' && typeof el.value === 'string') { return el.value; } return null; // 非字符串,跳过 }).filter(v => v !== null); if (arrValues.length > 0) { stringArrays.set(arrName, arrValues); } } this.traverse(path); } }); // 第二遍:替换数组访问 recast.visit(ast, { visitMemberExpression(path) { const node = path.node; if (node.object.type === 'Identifier' && node.property.type === 'Literal' && typeof node.property.value === 'number') { const arrName = node.object.name; const index = node.property.value; const arr = stringArrays.get(arrName); if (arr && index >= 0 && index < arr.length) { // 替换 MemberExpression 为 Literal path.replace(recast.types.builders.literal(arr[index])); } } this.traverse(path); } }); return recast.print(ast).code; } // 测试 const input = ` var __p = ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d"]; alert(__p[0] + __p[1] + __p[2] + __p[3] + __p[4] + __p[5] + __p[6] + __p[7] + __p[8] + __p[9] + __p[10]); `; console.log(deobfuscateStringArray(input)); // 输出:alert("hello world");

注意:实际混淆中,__p可能是window.__pthis.__p,或通过eval动态生成。此例仅覆盖最简场景。复杂场景需结合eval解析、作用域追踪,但核心思路不变:先定位数据源(字符串数组),再定位使用点(MemberExpression),最后做映射替换

4.2 拆解控制流扁平化:把while(true){switch(i){case 0:...i=1;break;case 1:...i=2;break;}}还原为if/else

这是最难的部分。Akamai 的扁平化会把线性逻辑打散成状态机。例如:

// 原始 function login(user) { if (user.token) { return api.call(user.token); } else { throw new Error('no token'); } } // 混淆后(简化) function login(user) { var _0x1234 = 0; while (true) { switch (_0x1234) { case 0: if (user.token) { _0x1234 = 1; } else { _0x1234 = 2; } break; case 1: return api.call(user.token); case 2: throw new Error('no token'); } } }

逆向思路:

  1. 找到while(true)+switch结构;
  2. 提取所有case块及其跳转目标(_0x1234 = X);
  3. 构建控制流图(CFG),识别if分支(case 0 的 if/else)和线性执行(case 1 → case 2);
  4. recast重建if/elsetry/catch

由于 CFG 构建较复杂,这里给出关键识别逻辑:

recast.visit(ast, { visitWhileStatement(path) { const node = path.node; // 检查 condition 是否为 true if (node.test.type !== 'Literal' || node.test.value !== true) return; const body = node.body; if (body.type !== 'BlockStatement') return; // 检查 block 内是否只有一个 switch if (body.body.length !== 1 || body.body[0].type !== 'SwitchStatement') return; const switchNode = body.body[0]; console.log('Detected control flow flattening!'); // 此处开始解析 switchNode.cases... } });

实操心得:完整的控制流还原是编译原理级工作,开源工具如deobfuscator(基于acorn)已实现。但理解其原理至关重要——混淆的本质是增加理解成本,而 AST 逆向的本质是用程序自动化地支付这笔成本。你不需要从零写 CFG,但要知道switch+while(true)是扁平化的指纹,_0x开头的变量名是状态机索引的标志。

4.3 动态解密:提取evalFunction中的加密字符串

很多混淆会把核心逻辑加密后存在字符串里,运行时用eval(atob('...'))Function('return ' + encrypted)()执行。

AST 方式可直接提取这些字符串:

recast.visit(ast, { visitCallExpression(path) { const node = path.node; // 检查是否为 eval(...) if (node.callee.type
http://www.gsyq.cn/news/1581365.html

相关文章:

  • CSS !important 使用决策指南:原理、场景与工程化管控
  • Pytest Fixture在API自动化测试中的核心应用与实战技巧
  • Web逆向工程实战:从网络请求到参数加密的完整技术解析
  • Angular预加载策略详解:从PreloadAllModules到业务驱动的自定义预加载
  • JMeter性能测试实战:从入门到精通,构建完整压测体系
  • 从零搭建高可用测试平台:Pytest+Playwright+Allure实战指南
  • Pytest Web自动化测试实战:从环境搭建到工程化实践
  • Rust 语言为何备受青睐?入门实践
  • iptables防火墙从入门到精通:核心架构、命令实战与生产环境避坑指南
  • Python Selenium自动化问卷填写实战:从环境搭建到验证码处理
  • OWASP CRS自定义规则编写实战:从业务逻辑防护到精准WAF配置
  • Appium自动化测试实战:从原理到环境搭建与脚本编写
  • 城市楼宇间无人机与地面站无线链路仿真工具(MATLAB一键运行版)
  • 软件指标管理中的业务技术关联
  • OWASP Top 10实战指南:从风险清单到安全开发生命周期
  • DeepSeek V4:开源大模型的协作基础设施与协议级工程实践
  • JMeter WebSocket压力测试实战:从工具链搭建到性能瓶颈定位
  • Python电力短路计算器:带可视化界面和自由搭接节点的轻量级分析工具
  • 51单片机6位数码管计算器:带矩阵键盘输入与Proteus仿真演示
  • 基于Playwright与Python构建数据驱动的测试度量体系实战指南
  • 逆向工程实战:从Python字节码到Linux提权与CrackMe破解
  • Linux服务器应急响应实战:从入侵检测到后门清除全流程指南
  • MATLAB阵列DOA估计交互式教学工具:MUSIC与ESPRIT算法可视化演示
  • SharePoint ToolShell攻击链解析:从Web Shell部署到企业安全防御实战
  • AI驱动软件测试自动化:智能体架构、自愈执行与团队转型实践
  • 从SQLite注入到RCE:实战解析链式攻击与防御策略
  • 网络策略深度优化:从TLS加密到零信任访问控制的实践指南
  • OpenSSL 3.1.1 EVP接口实战:C++实现SM2加密与签名完整指南
  • 国密SM4前后端互通实战:JavaScript与Java加解密全流程详解
  • 从IDOR到权限校验:一次完整的越权漏洞挖掘实战与修复指南