多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从零拆解快手Web端__NS_hxfalcon:JS逆向签名参数定位与复现

从零拆解快手Web端__NS_hxfalcon:JS逆向签名参数定位与复现 做逆向分析这些年接触过的签名参数数不过来但快手Web端的__NS_hxfalcon算是我认真研究过、也跟它反复纠缠过的参数之一。这个参数几乎出现在快手网页版大部分核心接口的请求里评论、点赞、分享、关注、作品列表都要带它。你要是抓包看一段请求就会发现它通常藏在请求体的某个位置形态是一个固定长度的字符串。很多刚接触JS逆向的朋友第一次搜到__NS_hxfalcon会以为它是一个固定的算法结果照着网上一些老文章去搜代码、找入口结果发现快手前端代码已经经过了多层混淆和拆包直接搜索往往搜不到东西。这篇文章我就把我分析这个参数的完整思路、拆解过程、以及踩过的坑从头到尾写一遍重点不是给你一个能直接用的成品而是告诉你怎么从零开始定位它、拆解它、复现它以及在整个过程中要注意哪些边界问题。1. 先搞清楚__NS_hxfalcon到底是什么1.1 这个参数长什么样出现在哪里在开始动手之前先说你会在哪里看到这个参数。打开快手网页版随便触发一个需要登录态的接口比如打开某个用户的作品列表或者给视频点个赞打开开发者工具的Network面板找一个XHR请求比如https://www.kuaishou.com/graphql这样的接口地址。在请求的Payload中你会看到类似这种结构{ operationName: visionProfilePhotoList, variables: {}, query: ..., __NS_hxfalcon: a3f2c9d8e7b6a5f4... }注意这里__NS_hxfalcon就是那个签名参数它的值通常是一串看起来毫无规律的十六进制或自定义字符集组成的字符串。我第一次拿到这个参数的时候也想过直接去搜索这个字符串生成的地方但很快发现这条路基本走不通。这个参数的特点是不同接口、不同用户、不同时间请求它的值都不一样。哪怕你间隔几秒钟连续发两次同样的请求参数值也在变说明生成逻辑里一定包含了动态因子最常见的就是时间戳、随机数、设备指纹等。1.2 为什么值得分析它很多人会问快手那么多参数sig3也好、__NStokensig也好为什么非要跟__NS_hxfalcon较劲我的判断是这个参数在快手的Web端架构里属于基础签名之一它承担的不只是某一个接口的校验而是整个GraphQL请求体系的公共参数。从学习角度讲分析这个参数能让你对现代Web前端反爬体系有个完整的认知。它不是一个孤立的算法而是一个由时间戳、环境采集、字符映射、哈希计算组成的综合性签名。当你把它拆开你会发现它和很多大厂的签名参数抖音的X-Bogus、某宝的_m_h5_tk在思路上有共通之处学会一个后续再遇到类似参数你至少知道该从哪几个方向去找。另外快手Web端的JS代码使用的是Webpack打包包含大量模块且不同环境下的代码版本还不一样分析这个参数的过程会逼着你学会在混淆代码里定位入口、跟调用栈、处理和补环境相关的一系列问题。技术含量是够的。1.3 分析前需要准备哪些工具工欲善其事必先利其器。分析__NS_hxfalcon我实际用到的工具并不复杂但每一件都有它的用途Chrome浏览器 DevTools这是主战场。抓包、断点、调用栈、全局搜索都靠它。建议用Chrome无痕模式排除插件干扰。Fiddler或Charles备选某些场景需要在非浏览器环境抓包或者捕获WebSocket流量用第三方代理工具会更稳。Node.js环境本地复现算法、运行扣下来的JS代码基本必备。Python可选如果你需要批量验证签名结果用Python发请求对比最方便。VS Code或类似编辑器用来保存代码片段、查看JS源码、格式化混淆后的代码。准备工作其实花不了多少时间但有一点值得提醒浏览器版本、Node版本不同某些环境相关的代码行为会有差异。我遇到过在Chrome里能正常生成签名、换到Node里就报错的情况最后发现是某个环境变量差异导致的。建议你分析期间固定一套环境至少固定同一个Chrome版本减少变量。2. 从发起请求到定位生成入口2.1 第一步永远是抓包定位而不是直接搜代码很多新手做逆向上来就用CtrlShiftF全局搜参数名结果搜了个寂寞然后开始怀疑人生。这里我想先说一个方法论任何参数的逆向第一步永远是搞清楚它在哪个请求里、以什么形式存在、前后有哪些关联参数。我当时的操作路径是这样的首先打开Chrome DevTools的Network面板勾选Preserve log保留请求日志。然后手动在快手网页版上操作比如点击一个用户的头像进入主页触发作品列表加载。找到带有__NS_hxfalcon的那个请求后右键复制为cURL格式先把它保存下来。这时候你能拿到几样关键信息请求的URL和Method请求体的完整结构包括__NS_hxfalcon的准确位置请求头里带了哪些Cookie、哪些自定义Header参数的值是什么格式、长度多少这一步不需要动脑子但非常关键。如果你连参数在哪个接口里都不确定后面所有工作都是空中楼阁。2.2 全局搜索法用参数名反查JS代码定位好请求之后接下来就是去前端代码里找这个参数了。正常情况下参数名__NS_hxfalcon会作为字符串出现在某一段JS代码里你直接全局搜索它就能命中。但快手这里有个坑JS代码经过Webpack打包和变量混淆__NS_hxfalcon这个字符串很可能不是直接以明文形式出现在源码里而是经过拼接或者被存在某个常量文件里。我第一次搜索的时候Sources面板里命中的结果很少而且点进去看周围全是压缩过的、完全不可读的代码。解决办法可以分几步在DevTools的Sources面板里按CtrlShiftF输入__NS_hxfalcon在所有加载的JS文件中搜索。如果搜索不到搜索它的部分子串比如NS_hxfalcon甚至hxfalcon。仍然找不到就搜索请求体中跟它邻近的参数名比如operationName先定位到构造请求体的那一段代码再往上找签名参数是怎么塞进去的。我当时是通过第3种方式找到的。定位到请求构造处之后在那一行打上断点刷新页面触发请求就能看到代码在组装请求体时__NS_hxfalcon这个字段对应的值是从哪个变量、哪个函数返回来的。2.3 调用栈跟踪从请求发起处逆推到加密源头找到请求构造处之后下一步就是跟调用栈。这是逆向里技术含量最高的一步也是最考验耐心的。在断点命中的位置打开DevTools右侧的Call Stack面板。你能看到一层层的调用关系。重点不是全部看而是找跟签名生成相关的那些帧。怎么判断哪些帧跟签名有关看函数名、看作用域里的变量。举个例子如果调用栈里出现了e.getSign、i.sign、generateFalcon这类名字基本就是签名生成函数。点进去你会发现这个函数内部会做一些事情读取当前时间戳读取一些环境信息比如canvas指纹、userAgent、屏幕分辨率对字符串做某种编码或哈希返回一个固定格式的结果这个过程中我建议你多用几个断点不要只在一个位置看。把断点打在请求参数构造完成之后的那一行在Console里手动执行生成函数看看每次执行返回的值是不是可复现的。如果函数执行结果每次都在变说明里面引入了随机数或时间戳如果结果不变那说明它依赖的状态少后面本地复现会更容易。3. 核心逻辑拆解与算法还原3.1 找到生成函数后的第一件事验证并缩小范围当你通过调用栈找到了疑似生成__NS_hxfalcon的函数先别急着读代码。你的第一步是在Console里手动调用它几次确认它是不是就是唯一的生成入口。我当时的做法是在断点处执行生成函数然后把返回值跟请求体里的__NS_hxfalcon比对完全一致就说明找对了。这一步虽然简单但能避免你跟着错误的调用链跑偏。确认之后就要开始读生成函数的代码了。但在压缩且混淆的代码里直接读基本跟看天书差不多。我的习惯是先把相关代码片段复制出来放到编辑器里格式化然后手动重命名一些关键变量把可读性拉高一点再分析。格式化这一步可以用DevTools自带的Pretty-print就是Sources面板左下角的{}按钮也可以把代码贴到本地编辑器里用Prettier这类工具格式化。格式化之后你会发现虽然变量名还是a、c、d这种短命名但至少函数边界清晰了找关键逻辑会方便很多。3.2 从算法结构上拆解分成“输入”和“计算”两部分算法阅读是逆向的核心但没必要一开始就逐行读先做结构拆解会高效得多。任何签名算法从宏观上都可以分成输入和计算两部分。输入部分也就是生成这个签名需要哪些前置数据。以__NS_hxfalcon为例我拆下来发现它至少依赖以下几类当前时间戳通常是毫秒级可能是Date.now()或new Date().getTime()一个随机字符串可能是Math.random()的结果或者其他伪随机源设备或环境的指纹信息比如canvas指纹、屏幕尺寸、时区、语言某些固定的常量字符串通常隐藏在JS模块里计算部分拿到输入之后代码会对它们做一系列处理。从我分析的版本看这种处理往往不是一个标准算法比如简单的MD5或者SHA256而是把多个输入值拼接成一个字符串对字符串做自定义的替换、反转、字符映射再进行某种哈希运算或者对称加密最后对结果做一次base64或hex编码我建议你在阅读过程中把函数里出现的所有字符串常量都记下来。这些常量很有价值——它们可能是字符映射表、盐值、或者某个固定标识。我遇到过一次还算有意思的情况代码里藏着一段经过编码的固定字符串把它解出来之后才发现它就是一个写死的盐值跟当前环境完全无关。3.3 常见判断点toString检测、环境检测、时间戳校验在分析__NS_hxfalcon的过程中有几个点值得单独拿出来说因为它们是国内主流签名参数里常见的“标配”而且对后续是否要补环境、是否要hook原生函数有决定性影响。第一个是toString检测。很多JS加密库会判断原生函数有没有被hook过。比如代码里会执行Function.prototype.toString.call(fn)然后检查返回的字符串里有没有native code字样。如果你在Node里跑这段代码很多原生方法不会被识别为native就会直接走异常分支。这个点后来在补环境的时候坑了我不少时间。第二个是环境变量检测。比如检测navigator.userAgent、navigator.language、screen.width、screen.height、document.visibilityState这些值。浏览器里有但Node里没有或者值不一样。这类检测不一定写在同一个函数里它可能散布在多个模块中你单纯补一个window可能不够。第三个是时间戳校验。有些版本的签名算法会把时间戳直接拼进明文有些则是生成后单独校验时间窗口。如果你复现出来的签名比服务器时间晚了哪怕几秒钟服务器也可能直接拒绝。所以拿到生成逻辑后一定要确认它内部用的是什么时间源本地跑的时候要不要同步。从分析角度说这些判断点的存在本身并不复杂复杂的是它们交织在一起而且代码里可能做了一层包裹导致你单纯搜navigator、window是搜不到的。你需要靠断点一步步去触发看它到底读取了哪些环境字段。4. 扣代码与补环境实战要点4.1 扣代码还是纯算法重写先想清楚再动手分析到这一步你基本已经掌握了算法的输入和计算逻辑接下来的选择会决定后面工作量的多少是把整个加密模块扣出来在本地跑还是用Python等语言照着算法逻辑重写一遍两种方案各有利弊我实际体验下来是这样扣代码优点是速度快不容易改错只要环境补齐就行缺点是遇到大量环境依赖时非常痛苦可能要补几百行环境代码而且浏览器和Node的proto链差异还会导致各种诡异报错。纯算法重写优点是最终产物干净、性能好、没有环境依赖缺点是需要你完全读懂算法逻辑一旦某个字符映射错误结果就对不上调试成本高。对于__NS_hxfalcon我的建议是先用扣代码的方式在Node里跑通拿到正确的签名输出再考虑要不要重写。理由是先验证“判断逻辑理解得对不对”比“能不能优雅实现”重要得多。代码能在Node里跑出正确结果说明你的分析是完整的然后再谈优化、谈重写。4.2 补环境的典型项目window、document、navigator选择扣代码这条路之后你很快会遇到一个坎——补环境。所谓补环境就是在一个没有DOM和BOM的Node环境里手动模拟出浏览器环境里那些全局对象让被扣下来的JS代码以为自己还在浏览器里跑。补环境到底补什么这完全取决于代码读到了什么。我建议的做法是不要盲补而是让报错告诉你答案。把扣下来的代码放到Node里跑每报一个错就去查是哪个对象的哪个属性没定义然后在全局作用域里补上。比如报window is not defined就在文件开头补global.window global;报navigator is not defined就补global.navigator { userAgent: Mozilla/5.0 ..., language: zh-CN, platform: Win32 };在这个反复“报错-补全”的过程里我要提醒你一点不要只补值不补行为。有些代码不是简单读取一个属性而是会调用方法。比如它可能调用canvas.toDataURL()来生成指纹你在Node里没有canvas环境那就得考虑用其他方式模拟一个返回值或者干脆跳过这段采集。我踩过的最大的坑是某一个全局对象已经被补上了但代码检测到它的原型链跟浏览器不一致直接走了异常分支导致签名结果错误。这种问题用报错是查不出来的只能靠你用断点去跟踪。后来我的习惯是每补一个环境变量就用Object.getOwnPropertyDescriptor检查一下它跟浏览器原生实现的差异。4.3 环境检测对象的绕过与取舍边界聊到环境检测我得先把话说明白。在快手的实际场景中从浏览器环境切到Node环境本质上就是一个“环境不匹配”的问题研究它、理解它是技术学习的一部分。但如果你试图把识别出来的某类检测点用于规避平台安全机制、伪造环境去批量攻击接口那就越线了。这篇文章里我只讨论“在本地复现一个签名逻辑”时需要处理的正常适配问题比如浏览器和Node的API差异。这些是纯技术问题任何一个前端工程师在写同构JS代码时都会遇到属于正常的工程问题不涉及绕过安全防御。理解了这层边界你在后续操作中就会站在一个合理的位置上。具体到实践我自己在适配环境差异时会优先采用“按需补齐”而不是“全部伪造”。比如代码需要navigator.userAgent我就给它一个正常的UA字符串代码需要screen.width我就给它一个常见分辨率。这种补齐的目的只是让代码在非浏览器环境里能按照原逻辑执行而不是去骗取任何系统的信任。我的经验是代码里真正跟平台安全强相关的检测点通常不会直接告诉你它检查了什么而是通过一些间接行为比如某个方法返回的值不同影响最终结果。遇到这种情况你在本地只能“记录差异”不要为了强行通过而去劫持这些方法。记录差异、理解差异本身就是逆向学习最有价值的部分。5. 常见问题与排查经验实录5.1 报错xxx is not defined怎么办这是补环境路上遇到最多的报错。解决办法看起来简单定义一个全局变量就行。但“定义”本身是有讲究的。如果你只是浅显地补一个全局对象比如global.window {}很多代码还是会报错因为它会访问window.screen、window.localStorage这些深层属性。这时候你需要根据报错提示一层层把对象结构补出来。我的建议是准备一个小工具函数专门用来在全局注入带层级结构的对象能省不少事。还有一种情况比较隐蔽代码里用了window.A但实际检测的是globalThis.A。在Node里这两个指向并不完全一致你补了global.window.A却忘了global.A就会出现“明明补了还是undefined”的诡异问题。排查方法很简单在报错行上下断点打印当前作用域里所有全局变量的名称对比你补的那些一眼就能看出来。5.2 签名总是对不上问题大概率在时间或随机数如果你确认生成逻辑完全正确、本地也能跑出字符串但放到实际请求里服务器就是不认我第一个建议是检查时间同步。签名里如果带了时间戳而你的本地时间和服务器时间差太多基本必挂。我当时的处理方式是在请求前直接用接口返回的服务器时间校准一次。比较通用做法是访问一个不带签名的接口拿到响应头里的时间字段然后用它来替代本地Date.now()。另外要留意随机数。很多签名算法里会拼接一个随机字符串如果你本地复现时没处理它服务端验证签名时发现随机串跟签名内容对不上也会拒绝。这个排查起来比较费时间我的经验是在浏览器里多采样几组“输入参数 最终签名”的对应关系用这些样本来反推随机串在签名里的位置和格式。5.3 请求频繁被拒绝先别急着怀疑算法还有一种很常见的情况签名明明正确但请求发多了之后开始返回异常页面或风控提示。这时候问题基本不在签名本身而在频率和Cookie状态。我自己试过在无痕窗口里第一次请求往往很顺利但第二次、第三次就会遇到验证码或直接失败。这背后是完整的风险控制系统签名只是其中一环。它还会看Cookie的完整性、用户行为轨迹、请求频率等。也就是说如果你只在签名这一层下功夫很容易碰壁。正确思路是先控制请求频次模拟真实用户的浏览节奏间隔拉长其次保证Cookie的完整性和新鲜度特别是登录态的Cookie过期了签名再对也没用。最后一旦触发了风控不要硬刚停一停再继续比疯狂换IP、换参数要有效得多。6. 合规使用与边界问题我必须多说几句6.1 哪些场景可以用哪些碰都不要碰写技术文章我不能只讲技术不讲边界。JS逆向这个东西工具属性很强它可以用来做好事也很容易被人拿去干坏事。我把自己的判断标准分享给你你心里有数就行。合理的场景包括学习API的交互逻辑、完善第三方API文档、对自己账号下的数据做备份和导出、研究前端加密算法的通用思路、做网站安全防御的反向验证。这些方向里逆向是手段目的不是破坏而是补全认知。不合理的场景包括批量抓取他人数据用于商业用途、绕过平台认证机制、恶意攻击或干扰平台服务、伪造请求刷量刷券等等。这些行为不但违反平台规则还可能触碰法律红线任何一个有底线的从业者都不应该碰。6.2 我的建议永远把“搞懂原理”放在“拿到结果”前面如果你问我分析一个签名参数最大的收获是什么我的答案不是“成功跑通了它”而是理解了前端安全设计的逻辑、理解了浏览器环境和Node环境的异同、理解了现代Web应用是怎么在速度和安全性之间做权衡的。这些知识是通用的今天你能分析快手明天换个平台你也能上手因为它们底层思路是相通的。所以我的建议是哪怕你的最终目标只是防爬、写脚本也请把重心放在“搞懂原理”上。遇到一个签名参数多想一想它为什么要拼这个字符串为什么选择这种编码方式服务端会怎么校验它。这些问题想通了比收集十个现成脚本的价值大得多。我在分析__NS_hxfalcon的过程中最深的一点体会是任何一个看起来复杂的签名系统拆到最底层你会发现组成它的都是很基础的知识点——字符串拼接、位运算、哈希函数、环境对象。逆向考验的不是你会不会某种高深算法而是能不能在混乱的代码里组织出一条清晰的逻辑线来。这种组织能力我们通常叫它“代码嗅觉”它是靠一次次断点、一次次报错、一次次比对喂出来的。最后分享一个小经验遇到特别诡异的签名结果对不上的情况先别急着怀疑算法读错了。把浏览器里生成的签名和本地生成的签名放在一起一个字符一个字符地比对——长度是否一致、字符集是否一致、前几位和后几位有没有固定规律。很多时候答案就藏在这些细枝末节的差异里。搞逆向耐心永远是第一位的其次是好奇心和一点强迫症。
返回列表