ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JS逆向实战:破解快手Web端__NS_hxfalcon签名参数

JS逆向实战:破解快手Web端__NS_hxfalcon签名参数 1. 事情的开端为什么我会盯上这个参数前阵子接了个前端数据采集的需求目标平台大家基本都知道——短视频领域头部应用之一快手Web端。业务方要的是公开页面的部分数据不是什么隐私信息数据本身也是页面公开展示给大家看的东西。按理说这种量级的数据写个脚本循环请求接口就行了结果我刚把请求包构造好跑出去第一条就给我弹了个错误回来。响应体不是正常JSON而是一段风控提示大意是请求异常请稍后再试。一开始我以为是IP被限了换了代理、清了Cookie重来还是不行。直到我把浏览器里正常发出的请求和脚本发出的请求放在一起对比才发现端倪浏览器发出的请求头里多了一个参数名字叫__NS_hxfalcon往往还带着一长串看似乱码的值。我把这个参数带上去再请求瞬间就通了返回200数据正常。那一刻我明白了这个平台在Web端的关键接口上基本都套了一层自定义签名校验__NS_hxfalcon就是那个通行证。这个参数就是典型的JS逆向分析对象。所谓JS逆向本质上是把前端JavaScript代码里对请求参数、请求头、签名Cookie的生成逻辑给扒出来在非浏览器环境或者自动化脚本里复现。放在浏览器里一切正常是因为前端代码替你算好了放到脚本里没人替你算就必须自己动手去搞明白它怎么来的。这篇文章我不会贴一套能直接跑起来的生产级代码因为平台的签名算法会频繁更新贴了也大概率过几天就废。我更想把这一路分析下来的完整思路、调试工具、定位手段和坑原原本本写清楚。你如果遇到的是别的平台、别的参数名这套方法论一样能套。做JS逆向思路永远比代码值钱。2. 抓包对比先确认签名出现在哪个环节很多人一上来就F12打开控制台开始翻JS文件找到个加密函数就一脸兴奋这是典型的走弯路。正确顺序是先抓包把请求的全貌看清楚搞清楚这个参数是加在哪个环节的。是请求头是请求体是Query ?还是Cookie?拿快手Web端来说我这边观察到的现象是__NS_hxfalcon出现在几个核心接口的请求头里尤其是一些需要登录态的、和数据列表相关的接口。它的值是一串看起来很像Base64的字符结尾还经常带号。“很像Base64”是个很重要的线索但这不代表它就是用Base64直接编码的有可能只是最后一步套了一层。抓包工具方面浏览器开发者工具就够用了。这里我建议你直接看Network面板里的Request Headers按接口名字去搜别肉眼翻。我的操作习惯是在Network面板里过滤XHR或者Fetch类型然后输入接口URL里的关键字比如graphql或者接口的特征路径。找到目标请求之后右键点击选择Copy - Copy as cURL就能拿到完整的请求信息包括所有请求头。接下来做一件事把cURL命令导入到Postman或者直接放到一个临时脚本里把浏览器环境里除Cookie之外的东西尽量带上。这里有一个关键点——如果你直接去掉__NS_hxfalcon去请求大概率会被拒如果你带上浏览器里抓到的那个值去请求短时间内也是有效的。这说明两件事一是服务端确实校验这个字段二是这个字段存在一定有效期。有效期这个特性很重要决定了后面我们必须在本地生成它而不是硬编码一个抓回来的值。为了确认这个参数的值到底有多少种变化建议你连续刷新几次页面、多抓几条请求把不同请求里的__NS_hxfalcon值拉出来对比一下。我那次对比之后发现不同请求、不同时间值完全不同。这就基本排除了写死值的可能确定了它是每次动态生成、请求级别的签名。到这里抓包第一步的目标就完成了定位到签名参数的位置确认它的形态确认它动态生成。3. 断点与调用栈分析从看到值到找到生成函数3.1 两种找函数入口的办法接下来是JS逆向的正戏找到__NS_hxfalcon这个值到底是在哪个JS函数里被算出来的。我一般会尝试两种手段交叉验证互相兜底。第一种叫XHR/fetch断点。Chrome DevTools的Sources面板里右侧有XHR/fetch breakpoints你可以添加一个包含URL片段的断点比如__NS_hxfalcon所在的完整接口名。这样每次请求发出去之前浏览器都会在发请求的那一行JS代码处停下来。这个断点停下的位置往往就是签名参数的最终调用点也就是fetch或者XMLHttpRequest.send被调用的地方。停在发请求的那一行之后重点不是看这一行而是看右侧的Call Stack调用栈。调用栈是从上往下由近到远的函数调用链我们关心的签名生成逻辑通常就藏在这条链的某几层。我那次顺着调用栈往下点很快就看到了几个函数名和sign、falcon相关的命名。方法名带hxfalcon、sign、token、encrypt这类关键词的基本就是目标了。第二种办法是全局搜索。在Sources面板里按CtrlShiftF全局搜索文件内容直接搜hxfalcon或者NS_hxfalcon。因为参数名作为字符串大概率出现在JS文件的代码里要么是作为HTTP请求头的key要么是作为对象的属性名。搜索出来之后点击搜索结果跳到对应位置然后往上找这个值是在哪里被赋值的。通常你会看到类似这样的代码结构headers: { __NS_hxfalcon: generateFalcon(param1, param2) }看到目标函数名之后下一步就是给这个函数打上普通断点刷新页面等它触发。这一步就能进入真正的代码分析阶段了。3.2 进入函数后的第一件事别急着读逻辑很多新手看到一堆加密函数就懵了追着一个变量看半天最后绕晕了。我的建议是进入生成函数之后先别急着逐行读代码而是观察输入和输出。在函数入口处DevTools会显示当前函数的参数列表。你可以在Console里手动执行这个函数传不同的参数观察返回值变化。我通常会在Console里做几组实验// 假设目标函数是 window.__genFalcon window.__genFalcon(abc, def) // 得到一个值A window.__genFalcon(abc, xyz) // 得到一个值B和A不一样说明第二个参数参与计算 window.__genFalcon(, def) // 得到一个值C和A不一样说明第一个参数也参与计算 window.__genFalcon(abc, def) // 再次执行看结果是否等于A用于判断是否存在时间因子整个分析过程其实就是控制变量法的反复应用。哪个参数变了输出跟着变说明它参与签名哪个参数变了输出纹丝不动说明它可能只是辅助信息或者压根没被用到。输出值是不是每次都一样则能帮你判断签名里是否包含时间戳这类的动态因子。快手这类平台接口的签名基本都会绑定时间戳或一个不固定数值否则签名就失去意义了。3.3 断点处的三个关键观察点在真正调试那些算签名的代码时有几个位置特别值得留意第一个是字符串拼接处。签名算法通常在最后一步会把所有参与签名的字段按某种顺序拼接起来然后塞进一个加密函数里。如果你能在调用栈里找到一处特别长的字符串拼接代码那基本就摸到源头了。观察这些拼接的字段名往往就能知道签名用了哪几个请求参数、顺序是什么。第二个是加密函数调用处。JS里的加密函数通常来自一些通用库比如CryptoJS、jsencrypt、forge或者是平台自己实现的一个纯JS加密函数。如果是通用库特征很清晰——代码里会出现CryptoJS.HmacSHA256、CryptoJS.SHA256这类调用。如果是自研函数名称一般会很奇怪比如是一串无意义字母或者带有falcon、xss这类业务词。无论哪种用断点跟进去观察它的入参和返回值判断它做的是哈希、HMAC、还是AES/RSA这类可逆加密就能大致界定算法类型。第三个是补环境前的关键hook点。所谓补环境就是在Node.js或者Python里模拟浏览器环境让原本依赖window、document、navigator这些浏览器对象的JS代码能在非浏览器里跑起来。在断点分析阶段我习惯顺手在Console里执行几个评估比如typeof window、typeof document、navigator.userAgent这些信息后面补环境时都要用到。提前记下来省得回头再翻一遍源码。4. 算法还原与本地复现从看明白到算得出4.1 判断算法类型哈希、HMAC还是别的前面在函数入口做控制变量实验时其实已经能看出一点算法痕迹。如果输入稍微变一点输出的整串字符就面目全非那很可能是哈希类算法如果输出的字符串长度固定比如64位那大概率是SHA25632位则可能是MD5也大概率是哈希类算法。这里有一个非常实用的判断技巧看输出长度和编码表。如果输出是32位十六进制小写多半是MD564位十六进制多半是SHA256如果是包含大写小写数字和/符号的变长字符串那可能是Base64或Base64变种如果打开一看还有中文字符那就很可能是某种自定义编码。在实际分析快手这个案例时我最终看到的是一串Base64美化过的、带填充符的值。但这里要提醒一句不要把表象当成算法的全部。Base64可能只是最外层包装真正的核心可能是HMAC-SHA256或者其他摘要算法的结果。所以我的判断路径始终是先假设通用算法用已有的值去试试出匹配的就直接用试不出来再进函数内部看源码逻辑。验证匹配的过程其实就是自己手动调一下常用的算法库传入猜测的参数组合看看生成结果和浏览器里抓到的__NS_hxfalcon值是否一致。这个步骤必须做它能帮你确认所有参与算法的字段、字段顺序、分隔符、编码方式任何一环错了结果都无法匹配。4.2 用Node.js补环境跑原包JS如果签名算法本身不复杂直接在目标平台源码里找到对应的加密函数复制出来在Node.js里补齐依赖就能算出一样的值。这个方案我称之为搬运原包JS。具体操作流程大概是在Sources面板里找到包含签名函数的JS文件手动把整个文件内容复制下来保存为本地falcon.js。在Node.js里require它或者直接node falcon.js运行。如果直接报错比如window is not defined、document is not defined那就说明原包JS依赖浏览器环境。此时需要补环境常见做法是在JS文件顶部预先定义一批全局变量// env.js var window { navigator: { userAgent: Mozilla/5.0 ... }, location: { href: https://www.kuaishou.com/, origin: https://www.kuaishou.com } }; var document { cookie: , referrer: }; var navigator { userAgent: Mozilla/5.0 ..., platform: Win32, language: zh-CN };把env.js在falcon.js之前引入再尝试调用目标函数看能不能跑通。补环境是一个典型的报错驱动的过程。一开始补全window、document、navigator然后可能会报canvas相关找不到那就再补一个document.createElement(canvas)的假实现。这个过程比较烦人但每报一个错就说明我们又离跑通近了一步。对付这类JS耐心比技术重要。还有一个技巧值得分享如果目标函数没有被挂载到window上而是在某个模块作用域内部直接复制整个文件也找不到调用入口。这时可以在源码里找到目标函数定义在函数外层手动包一层window.__for_test 目标函数名;利用DevTools的本地覆盖Overrides功能或者直接在Console里执行这段赋值语句把函数暴露到全局。这样后面在Node.js里就能直接调它来测试。4.3 补环境跑不通时的降级方案原包JS跑不通的情况也很常见。可能是代码太大了、依赖的浏览器特性太多也可能是代码做了重度混淆难以直接调用。这时候我通常会走另一条路纯Python复现算法。纯Python复现的前提是看得懂JS代码里每一段操作的意图。如果是简单的字符串拼接、排列、替换那太容易转了。如果是标准加密更简单用hashlib、hmac、PyCryptodome这些库替换就行。真正麻烦的是遇到一些自研的位运算、自研的表替换、或者依赖某个中间态结构的情况。这种就得硬着头皮把JS代码从头到尾读明白再逐行翻译。在快手这个案例里我发现签名值的最外层确实是Base64但里头不是一个单纯的哈希结果而是对一段JSON格式的字符串做了编码。那段JSON里包含了一些请求元数据、时间戳、随机数。虽然被处理过但整体上都是标准操作所以我最后选择了纯Python复现没有依赖浏览器环境。这一点的好处是脚本运行速度快、部署简单——一台普通服务器、一个Python环境就能跑不用装一堆浏览器依赖。也是从这时候起本地脚本才真正具备了替代浏览器请求的能力。5. 细节补全时间戳、随机数、参数顺序一个都不能错5.1 梳理参与签名的字段前面做了那么多分析最终要落地的是一张签名原材料清单。我在分析每个签名参数的时候都会顺手整理出下面这张表你也可以照着做参与字段名字/来源是否动态示例请求路径接口路径本身固定/rest/wd/photo/likeCookie字段如did、kpf等动态长串字符时间戳从Date.now()或者performance.now()获取动态1736845200000随机数可能叫nonce或直接用Math.random()动态0.123456789请求参数拼接好的query string或body视请求而定?photoId...记录这张表的意义在于你后面写代码时必须保证这每个字段的取值逻辑和原JS完全一致包括获取顺序、拼接顺序。很多时候签名对不上不是你算法没看对而是有个字段你少传了或者多传了一个空字符串。5.2 时间戳的粒度问题签名相关的时间戳特别容易踩坑。原JS代码里可能用Date.now()获取毫秒时间戳也可能用Math.floor(Date.now() / 1000)获取秒级时间戳。如果你在复现时搞错了粒度生成的签名必然不匹配。判断方法很简单下一行跟前端同时跑的调试日志打印出签名生成那一刻的原始时间戳值然后再看算法里用的是13位还是10位的。只要有一次对不上就用这个方式重新校准。还有一种更隐蔽的情况原代码里拿到时间戳后可能会先toString()然后截取前几位或者后几位再进行运算。这种微小的字符串操作不用断点跟踪到具体行基本发现不了。5.3 随机数或nonce的生成方式有些签名算法会掺入一个随机字符串防止同一时间戳下多次请求生成的签名完全一样也增加一些不可预测性。这个随机值可能在JS里就是Math.random().toString()也可能是自定义函数生成的包含时间戳用户agent摘要的字符串。复现这类动态值时有个原则必须保证随机值每次都是重新生成的并且上传到服务端的随机值要与签名生成时一致。你可以理解为服务端会重新计算一次签名并比对如果你脚本里生成随机值和签名时用的是同一个变量那就正确如果你一个请求重发多次签名值全都一样服务端校验可能就会怀疑你在重放请求。这一点在快手场景里尤其重要因为我测下来它的校验机制有重放防护的痕迹你连续用同一个签名打几次接口大概率就会被风控拦掉。5.4 参数顺序和拼接方式的隐藏陷阱签名算法的核心往往不是加密本身而是把参数字段拼成什么样再加密。这个什么样的规则各家平台各不相同。有的是把参数按字典序排列再用连接有的是按固定顺序拼接再在前面加一个固定salt还有的是把参数放进一个JSON对象里再序列化。我在分析中发现这类平台签名经常会对参数做一层排序或者过滤只保留参与签名的白名单字段其他字段一概不参与。这就意味着就算你翻到加密那一步也得回头确认参与加密的字段列表千万不能想当然认为所有请求字段都参与了。正确的做法还是回到断点找到拼接字符串形成的位置把那次拼接前后的字符串完整打出来看一眼你就知道规则了。6. 验证与实测新签名能不能真正打通接口签名代码写完能不能用最后还是要靠实测说话。我一般分三步走第一步离线验证。在Node.js或Python里生成一个签名然后和浏览器里抓到的同一次请求签名做对比。注意浏览器里那次请求的请求时间、参数、Cookie和你脚本里的必须尽量一致否则签名本来就不一样对比没有意义。实际操作时我把抓包拿到的那组值——URL、请求头、Cookie、时间戳——原封不动喂给签名函数看能否产出和抓包一致的签名。通常这一步就对不齐的没必要继续往下走。第二步在线验证。用脚本重新发起一次完整请求带着新生成的签名去访问目标接口。判断标准很简单HTTP状态码200、返回正常JSON、没有风控提示。这步通过了说明签名基本打通了。第三步稳定性验证。多跑几次请求把时间间隔拉长到几分钟甚至几小时再测确认签名不会因为时间变化而失效也不会因为请求频率升高被风控。我实测下来高并发场景下如果每个请求都重新算一次签名基本没问题但如果你图省事同一个签名复用多次很快就会被识别出来。还要提醒一点Cookie和这个签名参数往往是绑定的。快手Web端的Cookie里有些基础标识比如did、kpf、clientid签名生成时可能会用到其中一部分。所以你在脚本里模拟请求时不要把Cookie固定死最好用一个稳定的Cookie池做轮换这样能明显降低风控触发概率。当然如果你的目的是采集公开数据那对Cookie质量的要求不会特别高你要是涉及登录态接口那Cookie的获取和保鲜就又是一个大话题了。7. 风控对抗与边界逆向分析的能与不能聊到这里我必须把视角从怎么实现拉回到该不该做上。快手这类平台的JS签名机制核心目的是风控和反作弊。作为开发者去分析它是为了什么决定这件事的性质。如果你是做授权接口调试、App合规测试、前端数据安全评估、爬虫学习验证这些方向那JS逆向是必备技能合理合法。我建议你在任何实际项目里都明确自己是否有权限调用这些接口、是否遵守了平台的服务条款和Robots协议。尤其是涉及登录态和用户数据时边界感要非常强。我个人的底线是只采集公开可见数据不碰用户隐私信息。请求频率控制在正常用户行为范围内不做暴力拉取。不把签名破解能力用于任何攻击、刷量、薅羊毛等黑灰产场景。分析过程中发现高危漏洞时走平台SRC安全应急响应中心渠道上报而不是私下利用。这套边界我在这些年踩过一些坑之后才慢慢建立起来。说句实在话技术的双刃剑特性在这个领域体现得比大多数方向都明显。能写签名校验的人很多但能把能力用在对的方向上、并且持续做合规测试的人才是真正有价值的技术人。平台的风控策略也在不断升级。你用旧版签名核心跑通的代码过几周可能就收到签名无效的报错。这不一定是你分析错了而是平台侧的算法或校验逻辑变了。这种情况不用慌回到第一步重新抓包、重新打断点、看看新的代码里改了什么通常半天时间就能跟上更新节奏。最后再分享一个我实际分析过程中的体会JS逆向这件事最有成就感的瞬间不是参数跑通了的那一刻而是你在调试中断断续续拼出全貌、意识到整个签名体系原来是这样设计的时候。那是一种和代码作者隔空对话的感觉。签名参数看起来很神秘拆开来看也无非是一个开发者为了风控、为了服务稳定性做的工程手段。理解了这一点你对前端的安全和体验之间的关系也会有更深一层的认识。如果你也正在被某个平台的签名参数折磨希望这份思路能帮你少走点弯路。别急着找现成代码先塌下心来把抓包、断点、调用栈这几个基本功练熟。工具都是死的思路是活的这套方法论你用熟了以后遇到任何平台的签名分析都只是时间问题。
返回列表