前言上个月帮朋友爬某头部电商的商品数据遇到了变态的JS加密每个请求都要带一个_sign参数有效期只有5分钟换IP换UA都没用只要_sign不对直接返回403。一开始我用Selenium模拟登录结果Selenium的自动化特征被识别还是被封。后来想直接复现_sign的生成逻辑打开JS文件一看全是混淆后的代码变量名都是a、b、c控制流绕来绕去字符串全是加密的数组根本看不懂。折腾了整整两周我用AST解混淆Chrome断点调试Python复现的全流程终于搞定了这个_sign参数。现在这套流程已经帮我搞定了10有JS加密的网站成功率90%以上平均每个网站从折腾一周到1天就能搞定。今天把完整的流程分享出来从AST解混淆的具体代码、算法还原的断点技巧到Python复现的注意事项全是经过实战验证的干货复制粘贴就能用再也不用怕JS加密。一、JS加密常见混淆手段与痛点现在的反爬JS加密90%都会用以下几种混淆手段每一种都能让你看得头疼1.1 常见混淆手段变量名/函数名混淆把有意义的变量名改成a、b、_0x1234这种无意义的名字控制流平坦化把正常的if-else、for循环拆成switch-case用状态机控制流程绕来绕去根本看不懂字符串数组加密把所有字符串放到一个数组里用索引访问数组本身还可能加密死代码注入插入大量永远不会执行的代码干扰你的分析虚拟机保护把JS代码编译成自定义的字节码用虚拟机执行完全看不到原始逻辑1.2 传统分析方法的痛点瞎猜逻辑看不懂混淆代码只能靠猜试错成本极高调试困难控制流平坦化后断点调试根本不知道下一步跳到哪复现困难即使大概知道逻辑也很难保证Python复现的结果和JS完全一致效率极低一个简单的加密可能要折腾好几天而AST解混淆算法还原Python复现的全流程完美解决了这些问题先把混淆代码还原成可读代码再精准定位加密函数最后100%复现到Python。二、整体流程框架样本采集抓包获取加密参数AST解混淆还原可读代码算法定位Chrome断点调试算法还原分析加密逻辑Python复现确保100%一致验证测试对比JS与Python输出三、AST解混淆从混淆代码到可读代码AST抽象语法树是JS代码的结构化表示通过操作AST我们可以自动化地还原混淆代码。3.1 工具选择推荐用Node.js acorn estraverse escodegen这套组合acorn把JS代码解析成ASTestraverse遍历和修改AST节点escodegen把修改后的AST重新生成JS代码npminstallacorn estraverse escodegen3.2 常见混淆的解混淆实现1. 字符串数组还原这是最常见的混淆先看一个例子// 混淆后的代码var_0x4e2a[\x68\x65\x6C\x6C\x6F,\x77\x6F\x72\x6C\x64];function_0x1234(_0x5678){return_0x4e2a[_0x5678];}console.log(_0x1234(0) _0x1234(1));解混淆思路找到字符串数组_0x4e2a找到访问数组的函数_0x1234遍历AST把所有_0x1234(0)替换成对应的字符串hello具体代码constacornrequire(acorn);constestraverserequire(estraverse);constescodegenrequire(escodegen);constfsrequire(fs);// 读取混淆后的代码constobfuscatedCodefs.readFileSync(obfuscated.js,utf-8);// 解析成ASTconstastacorn.parse(obfuscatedCode);// 1. 提取字符串数组和访问函数letstringArray[];letaccessFunctionName;estraverse.traverse(ast,{enter:function(node){// 找到字符串数组声明if(node.typeVariableDeclaratornode.init?.typeArrayExpressionnode.init.elements.every(ee.typeLiteral)){stringArraynode.init.elements.map(ee.value);}// 找到访问数组的函数if(node.typeFunctionDeclarationnode.body.body.length1node.body.body[0].typeReturnStatementnode.body.body[0].argument?.typeMemberExpressionnode.body.body[0].argument.object?.typeIdentifier){accessFunctionNamenode.id.name;}}});// 2. 替换所有函数调用为对应的字符串estraverse.replace(ast,{enter:function(node){if(node.typeCallExpressionnode.callee.typeIdentifiernode.callee.nameaccessFunctionNamenode.arguments.length1node.arguments[0].typeLiteraltypeofnode.arguments[0].valuenumber){constindexnode.arguments[0].value;return{type:Literal,value:stringArray[index],raw:JSON.stringify(stringArray[index])};}}});// 3. 生成解混淆后的代码constdeobfuscatedCodeescodegen.generate(ast);fs.writeFileSync(deobfuscated.js,deobfuscatedCode);console.log(解混淆完成);运行后混淆代码会被还原成console.log(hello world);2. 变量名/函数名还原思路遍历AST给所有无意义的变量名比如_0x1234重新命名成有意义的名字比如var_0、func_0虽然不能还原成原始的有意义名字但至少能让代码更易读。3. 控制流平坦化还原这是最难的一种混淆思路是找到状态机变量比如state分析每个case对应的代码块根据跳转条件把switch-case还原成正常的if-else、for循环控制流平坦化还原比较复杂这里就不贴完整代码了核心思想是通过AST分析状态机的跳转逻辑重新构建控制流。四、算法还原定位加密函数与逻辑分析解混淆后代码已经可读了接下来就是定位加密函数分析加密逻辑。4.1 样本采集首先抓包看哪些参数是加密的打开Chrome DevTools切换到Network面板刷新页面触发请求看请求参数比如sign、token、_xsrf这些值看起来是随机的、有固定长度的大概率是加密的比如某招聘网站的请求GET /api/job/list?page1sign8A2F3D4E5B6C7A8B9C0D1E2F3A4B5C6Dt1713456789000这里的sign就是32位的MD5值t是时间戳。4.2 断点调试定位加密函数在DevTools的Sources面板按CtrlShiftF搜索sign或者加密参数名找到设置sign的地方下断点刷新页面触发断点一步步跟代码找到生成sign的函数比如跟到最后找到这样的代码functiongenerateSign(params,timestamp){// 按字母顺序排序参数constsortedKeysObject.keys(params).sort();letstr;for(constkeyofsortedKeys){strkeyparams[key];}// 拼接时间戳和盐值strttimestampsaltabc123;// MD5加密转大写returnmd5(str).toUpperCase();}4.3 逻辑分析找到加密函数后一步步分析逻辑参数排序按字母顺序排序请求参数拼接字符串keyvaluekeyvaluettimestampsaltabc123MD5加密对拼接后的字符串做MD5转大写把MD5结果转成大写这里的关键是找到盐值salt很多加密都会加一个固定的盐值藏在混淆代码里解混淆后很容易找到。五、Python复现确保100%一致性分析完加密逻辑后就要用Python复现这里有几个关键注意事项否则很容易出现结果不一致的情况。5.1 关键注意事项编码一致JS默认用UTF-16Python默认用UTF-8要确保编码一致字节序一致如果涉及到字节操作要确保大端小端一致时间戳格式一致JS的Date.now()是毫秒Python的time.time()是秒要注意转换随机数种子一致如果加密用了随机数要确保种子一致字符串处理一致比如字符串拼接、大小写转换、特殊字符处理5.2 代码复现把上面的JS加密函数翻译成Pythonimporthashlibimporttimedefgenerate_sign(params,timestamp):# 1. 按字母顺序排序参数sorted_keyssorted(params.keys())str_list[]forkeyinsorted_keys:str_list.append(f{key}{params[key]})# 2. 拼接时间戳和盐值str_list.append(ft{timestamp})str_list.append(saltabc123)sign_str.join(str_list)# 3. MD5加密转大写md5hashlib.md5()md5.update(sign_str.encode(utf-8))returnmd5.hexdigest().upper()# 测试if__name____main__:params{page:1,size:10}timestampint(time.time()*1000)# 转成毫秒和JS一致signgenerate_sign(params,timestamp)print(fsign:{sign})print(ftimestamp:{timestamp})5.3 验证测试用同样的输入对比JS和Python的输出在Chrome DevTools的Console里调用JS的generateSign函数传入同样的参数和时间戳运行Python代码传入同样的参数和时间戳对比两个sign必须完全一致如果不一致就一步步排查是编码问题时间戳问题还是字符串拼接问题六、踩坑实录JS加密的那些坑6.1 坑1字符串数组的动态解密最开始我解混淆某网站的代码字符串数组是加密的需要调用一个解密函数才能拿到真实字符串我一开始没调用这个函数结果解混淆后的字符串全是乱码。解决方法在解混淆前先在Node.js里运行混淆代码调用字符串数组的解密函数拿到解密后的数组再进行解混淆。6.2 坑2控制流平坦化的死循环还原某网站的控制流平坦化时我没处理好跳转条件结果解混淆后的代码出现了死循环。解决方法在还原控制流时加入循环检测如果发现某个代码块被重复访问超过100次就停止还原手动分析这部分逻辑。6.3 坑3JS和Python的MD5结果不一致有一次复现MD5加密同样的输入JS和Python的结果就是不一样排查了很久才发现JS的字符串里有一个\u200b零宽空格Python里没有。解决方法在Python里先对输入字符串做清洗去掉不可见字符或者在JS里打断点看原始字符串的每个字符的Unicode编码。6.4 坑4加密函数的动态生成某网站的加密函数不是一开始就有的而是通过eval动态生成的我在Sources面板里根本找不到这个函数。解决方法用Chrome DevTools的XHR/fetch Breakpoints在发送请求前下断点然后在Call Stack里找到生成加密函数的地方或者用Proxy拦截eval函数拿到动态生成的代码。七、效果总结这套AST解混淆算法还原Python复现的全流程我已经在10有JS加密的网站上验证过指标传统瞎猜方法本方案平均搞定一个网站的时间7天1天成功率30%90%结果一致性经常不一致100%一致可维护性差出问题不知道怎么改好逻辑清晰总结JS加密并不可怕只要掌握了AST解混淆断点调试Python复现的全流程90%的JS加密都能搞定。核心思路就是先把混淆代码还原成可读代码再精准定位加密函数最后100%复现到Python。这套流程不仅适用于爬虫反爬还适用于任何需要分析JS代码的场景比如前端逆向、安全分析等。如果你正在被JS加密折磨强烈建议你试试这套流程绝对会给你带来惊喜。 点击我的头像进入主页关注专栏第一时间收到更新提醒有问题评论区交流看到都会回。