资讯详情

美团mtgsig 1.2签名逆向:补环境与原型链实战

📅 2026/9/19 17:50:34 | 华诺云谱 👁 阅读
美团mtgsig 1.2签名逆向:补环境与原型链实战
1. 从一次接口调试说起mtgsig到底卡在哪做数据采集或者接口联调的朋友大概率都遇到过这样一个场景请求参数、Cookie、Header 全都对得上返回结果却始终是请求异常或者干脆给你一个空壳。抓包一看请求体里多了一个叫mtgsig的字段值是一长串看起来毫无规律的字符串而且每次刷新页面它都在变。这就是美团系接口里那个让人又爱又恨的签名参数。mtgsig是美团在客户端与服务端之间做请求合法性校验的一套签名机制目前流传较广的版本是 1.2。它的核心作用很简单证明这个请求是由真实的前端环境发出来的而不是脚本直接拼出来的。服务端拿到请求后会用同样的算法重新算一遍签名对不上就拒绝。所以你想稳定地调用这类接口就必须搞清楚这个签名是怎么生成的。这篇文章面向的是有一定 JavaScript 基础、做过接口分析、听说过补环境但还没完整跑通过一套流程的开发者。我会把 mtgsig 1.2 从环境检测、原型链补齐到最终签名串生成的完整链路拆开讲重点放在为什么这么补、补的时候容易在哪翻车而不是只丢一段代码让你抄。需要提前说明的是本文讨论的是前端签名算法的技术原理与工程实现思路用于学习 JavaScript 逆向与浏览器环境模拟的相关知识请在实际使用中遵守目标平台的服务条款与相关规范。先说结论mtgsig 1.2 的难点从来不是那个哈希函数本身而是它依赖的那一整套浏览器环境。你把算法逻辑扒出来只是第一步真正耗时间的是让这段逻辑在一个非浏览器环境里觉得自己还在浏览器里。这就是补环境这三个字的全部含义。2. mtgsig 1.2 的签名构成与校验逻辑2.1 签名串里到底装了什么先别急着补环境得先弄明白这个签名串是由什么拼出来的。把 mtgsig 的值做一次拆分你会发现它大致是这样一个结构mtgsig1.2_加密后的数据段_时间戳相关段不同业务线下细节会有差异但核心组成逃不出这几块版本标识开头的1.2服务端据此选择对应的校验算法。环境指纹从navigator、screen、window等对象上采集的一堆属性经过处理后的摘要。请求上下文当前请求的 URL、方法、部分参数参与签名防止篡改。时间因子一个随时间变化的量保证签名一次性有效防止重放。这四块里环境指纹是最难搞的因为它采集的字段又多又杂而且很多字段之间存在关联校验。你随便填一个navigator.userAgent是过不了的因为代码会拿它跟navigator.appVersion、navigator.platform做交叉比对。2.2 服务端是怎么验的理解校验逻辑才能理解为什么补环境要补得那么细。服务端拿到 mtgsig 后大致做三件事解出签名里的环境指纹和上下文跟自己这边记录的会话信息比对。用同样的算法重算一遍看结果是否一致。检查时间因子超出有效窗口直接判失效。关键在于第 1 步。服务端并不是孤立地看某个字段而是看字段之间的逻辑自洽性。比如你的userAgent声称是 Chrome 120但navigator.plugins是空的、window.chrome不存在那这个环境就是假的。所以补环境的目标不是把字段填满而是让字段之间互相说得通。提示很多人补环境失败不是漏了某个属性而是属性之间自相矛盾。补之前先想清楚你要伪装成什么浏览器、什么版本然后所有字段都往那个目标上靠。2.3 为什么直接抠算法跑不通有人会想我把签名函数整个抠出来在 Node 里直接调用不就行了实测下来十有八九会报错。原因有两个一是函数内部大量引用了全局对象。签名逻辑里会直接写window.xxx、document.xxx、navigator.xxxNode 环境里这些压根不存在一执行就ReferenceError。二是存在主动的环境探测。代码里会故意去访问一些只有真实浏览器才有的东西比如document.createElement(canvas).getContext(webgl)或者检测某些原型链上的方法是否被篡改过。你补得不对它要么报错要么返回一个污染标记让签名结果悄悄变错——这种最坑因为不报错你根本不知道哪里出了问题。3. 补环境的核心思路让代码相信自己在浏览器里3.1 补环境不是造一个浏览器新手最容易走进的误区是试图用 Node 完整模拟一个浏览器。这是条死路工作量巨大且永远补不全。正确的思路是按需补代码用到什么我就补什么代码怎么检测我就怎么伪装。具体做法是先让签名逻辑跑起来看它第一个报错是什么补上再跑看下一个报错再补。这个报错驱动的迭代过程比一次性把window对象写全要高效得多。但这里有个前提——你得能看到它访问了哪些属性。直接跑只能看到报错的那一个看不到它悄悄访问但没报错的那些。所以更专业的做法是给环境对象加一层Proxy 代理把所有属性访问都打印出来。3.2 用 Proxy 把属性访问全打出来Proxy 是补环境阶段最好用的工具没有之一。它的作用是拦截对目标对象的所有操作你可以在拦截器里记录日志也可以动态返回伪造的值。function createProxy(target, name) { return new Proxy(target, { get(obj, prop) { // 过滤掉一些噪音属性避免日志刷屏 if (typeof prop symbol) { return obj[prop]; } console.log([GET] ${name}.${String(prop)}); const value obj[prop]; // 如果取到的是对象继续套一层代理实现递归监控 if (value typeof value object) { return createProxy(value, ${name}.${String(prop)}); } return value; }, set(obj, prop, value) { console.log([SET] ${name}.${String(prop)} ${value}); obj[prop] value; return true; } }); }把window、document、navigator、location这些顶层对象都套上代理然后跑一遍签名逻辑。控制台会刷出几百行访问记录这就是你的补环境清单。哪些属性被读了、读的顺序是什么、有没有被写一目了然。注意Proxy 会拖慢执行速度而且某些原生方法比如Function.prototype.toString被代理后行为会变。所以监控阶段用代理正式跑的时候要去掉或者只对关键对象保留。3.3 原型链补环境比属性补更隐蔽的坑光补属性还不够因为检测代码会去查原型链。举个例子代码可能这样判断if (navigator.plugins instanceof PluginArray) { // 认为是真实环境 }你光给navigator.plugins赋一个数组instanceof检查就过不了。这时候就得把PluginArray这个构造函数也补上并且让navigator.plugins的原型指向它。这就是原型链补环境的含义。常见的需要补原型链的对象包括对象需要补的原型检测目的navigator.pluginsPluginArray判断插件列表真实性navigator.mimeTypesMimeTypeArray判断 MIME 类型真实性document.allHTMLAllCollection判断 document 完整性各种 DOM 元素HTMLElement 及其子类判断元素是否由浏览器创建补原型链的通用写法是function setProto(obj, protoName) { // 动态创建一个同名构造函数让 instanceof 能通过 const FakeConstructor function () {}; Object.defineProperty(FakeConstructor, name, { value: protoName }); Object.setPrototypeOf(obj, FakeConstructor.prototype); return obj; }这样obj instanceof FakeConstructor就成立了而FakeConstructor.name又是正确的名字能骗过大部分基于constructor.name的检测。3.4 iv8 补环境另一种思路除了在 Node 里手动补还有一种思路是用iv8这类工具。iv8 的本质是把 V8 引擎单独抽出来配合一套预置的浏览器环境桩代码让你能直接执行前端 JS 而不用自己从零补。它的优势是开箱即用很多常见的浏览器对象已经内置了省去大量重复劳动。但劣势也很明显一是环境桩是固定的遇到定制化检测还是得自己改二是版本更新可能滞后新出现的检测点覆盖不到。我的建议是新手可以先用 iv8 跑通流程理解整体链路等遇到它搞不定的检测点再回头学手动补环境。两者不是对立的iv8 内部其实也是在做补环境只是帮你封装好了。4. 手把手从零跑通一次签名生成4.1 定位签名入口第一步永远是找到签名函数在哪。常规手段是关键字搜索在 Sources 面板里全局搜mtgsig找到给它赋值的地方。通常你会看到类似这样的代码const sig window._sign(params); request.headers[mtgsig] sig;顺着_sign往上追就能找到真正的签名实现。如果代码被混淆了函数名是一堆乱码那就靠调用栈在赋值那行下断点刷新页面看调用栈里哪一层是签名计算的核心。定位到之后把整个签名函数及其依赖的模块抠出来。抠的时候注意不要只抠一个函数它依赖的工具函数、常量表、加密库都要一起带走否则跑起来就是一堆undefined。4.2 搭建最小可运行环境抠出来的代码先别急着跑先搭一个最小的环境骨架// env.js —— 环境骨架 const window {}; const document {}; const navigator {}; const location { href: https://example.com/, protocol: https:, host: example.com }; // 把常用全局对象挂到 global 上让抠出来的代码能直接访问 global.window window; global.document document; global.navigator navigator; global.location location;然后加载签名代码跑一次看第一个报错。大概率是window.xxx is not defined或者Cannot read property xxx of undefined。按报错逐个补这就是迭代的开始。4.3 关键字段的填充策略补到一定程度报错会消失但签名结果还是不对。这时候就要靠前面说的 Proxy 日志去比对真实浏览器里这些字段的值和你补的值是否一致。几个高频出问题的字段我列一下实测经验userAgent必须和你要伪装的目标浏览器版本严格对应不能随便写。建议直接从真实浏览器复制。screen 相关width、height、availWidth、colorDepth这几个要互相自洽别出现availWidth width这种矛盾。navigator.pluginsChrome 现在默认返回固定的几个 PDF 插件长度和内容都要对。window.chromeChrome 特有对象检测很频繁必须补且chrome.runtime等子对象也要有。performance.now()很多签名会用它做时间因子补的时候要保证单调递增。提示判断字段补得对不对最靠谱的方法是在真实浏览器里把这些值打印出来存成 JSON然后在你的环境里按这份 JSON 填充。别凭记忆瞎写。4.4 时间因子与随机数的处理签名里通常有一个随时间变化的量。补环境时这个量不能写死否则签名会永远一样服务端一眼就看出问题。正确做法是让它跟随真实时间// 用真实时间戳但注意单位要和原逻辑一致秒还是毫秒 const timestamp Date.now();如果原逻辑用的是performance.now()那就要补一个基于process.hrtime的单调时钟const startTime process.hrtime.bigint(); global.performance { now() { const diff process.hrtime.bigint() - startTime; return Number(diff) / 1e6; // 转成毫秒 } };随机数同理Math.random在 Node 里是有的但如果代码用的是crypto.getRandomValues那就得补上crypto对象。4.5 验证签名是否真的可用补完之后怎么确认签名是对的唯一标准是拿真实接口去验。构造一个请求带上你生成的 mtgsig看服务端返回是否正常。如果返回异常别急着怀疑算法先按这个顺序排查签名串格式对不对版本号、分隔符、长度是否和真实的一致。时间因子是否在有效窗口内本地时间和服务器时间差太多也会失败。环境字段是否自洽用 Proxy 日志逐条比对。请求上下文是否参与签名有些签名把 URL 和参数也算进去了你换接口就得重算。5. 那些年我踩过的补环境深坑5.1 属性补了但 getter 没补有些属性在真实浏览器里是getter不是普通值。比如navigator.userAgent在某些实现里是动态计算的。你直接navigator.userAgent xxx赋值检测代码用Object.getOwnPropertyDescriptor一看发现是个普通数据属性直接判定为伪造。正确做法是用Object.defineProperty定义 getterObject.defineProperty(navigator, userAgent, { get() { return Mozilla/5.0 ...; }, configurable: true });5.2 toString 检测这是最阴的一招。检测代码会调用Function.prototype.toString.call(someFunction)看返回的是不是function xxx() { [native code] }。你补的函数如果 toString 出来是源码就露馅了。应对方法是重写 toStringconst originalToString Function.prototype.toString; Function.prototype.toString function () { if (this someFakeFunction) { return function someFakeFunction() { [native code] }; } return originalToString.call(this); };但要注意重写 toString 本身也可能被检测。所以更稳妥的做法是只对特定函数做处理别全局改。5.3 原型链被污染导致检测失败有时候你补着补着把Object.prototype或者Array.prototype给改了导致后续所有对象的检测都出问题。这种 bug 特别难查因为报错点离污染点很远。我的经验是补环境时尽量用Object.defineProperty而不是直接赋值并且补完一个对象就检查一下它的原型链是否干净。可以用Object.getPrototypeOf逐层打印确认。5.4 环境补太全反而暴露新手容易犯的另一个错是把环境补得比真实浏览器还全。比如真实 Chrome 里某些属性是undefined的你给补上了值检测代码一比对这个属性不该有值反而判定异常。所以补环境的原则是以真实浏览器为唯一标准多一个少一个都不行。这也是为什么我一直强调要先把真实环境的值 dump 下来。6. 让签名方案更稳的几个工程化建议6.1 把环境配置外置成 JSON别把环境字段硬编码在代码里。把它们抽成一个 JSON 配置文件代码启动时加载。这样换目标浏览器版本时只改配置不改逻辑维护成本低很多。const envConfig require(./env-config.json); Object.keys(envConfig.navigator).forEach(key { Object.defineProperty(navigator, key, { get: () envConfig.navigator[key], configurable: true }); });6.2 加一层签名结果自检签名生成后可以在本地做一次格式校验长度对不对、分隔符数量对不对、版本号对不对。格式不对就别发请求了省得白白触发风控。6.3 关注版本变化mtgsig 的版本号会变1.2 之后可能还有新版本。每次目标平台前端更新签名逻辑都可能调整。建议定期用 Proxy 重新 dump 一遍属性访问日志跟之前的对比看有没有新增的检测点。这是保持方案长期可用的关键。6.4 控制请求节奏再完美的签名如果请求频率异常照样会被识别。签名解决的是请求合法性解决不了行为合理性。这两件事要分开对待别指望补环境能搞定一切。7. 关于这套流程的一点个人体会补环境这件事说到底是一场信息对称的博弈。检测方想尽办法找你不是浏览器的证据你想尽办法把这些证据抹掉。mtgsig 1.2 的检测点不算特别刁钻但它胜在字段多、关联性强逼着你把整个环境补得足够自洽。我自己踩下来最大的感受是别急着写代码先花时间把真实环境的值 dump 清楚。很多人一上来就闷头补补了半天发现方向错了因为压根不知道真实环境长什么样。Proxy 日志加真实浏览器 dump这两样东西能帮你省掉至少一半的调试时间。另外原型链补环境这块建议单独花时间理解instanceof、constructor、Object.getPrototypeOf这几个机制的运作方式。理解了原理遇到新的检测点你就能举一反三而不是每次都得去搜现成的答案。最后说一句这类技术的学习价值在于理解前端安全的检测思路和 JavaScript 的运行机制把它用在正道上比如做自己系统的安全测试、理解浏览器环境模拟原理才是长久之计。技术本身没有对错怎么用才是关键。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。