1. 项目概述:从一次失败的请求说起
做爬虫或者数据采集的朋友,估计都遇到过美团外卖这个“硬骨头”。你可能兴致勃勃地写好了requests代码,模拟了headers,甚至把cookies都处理得妥妥当当,但一发送请求,返回的要么是403 Forbidden,要么就是一堆看不懂的加密数据,核心信息一个都拿不到。问题的根源,往往就出在一个叫做mtgsig的参数上。
这个mtgsig可不是普通的token或者sign,它是美团外卖API在关键请求(尤其是涉及用户、商家、订单等敏感数据时)中使用的一套复杂的签名加密算法。你的请求里如果没有携带一个正确生成的mtgsig,服务器会直接判定为非法请求,拒绝服务。我最初接触时也踩了不少坑,发现网上零散的资料要么过时,要么语焉不详。所以,今天我就把自己逆向分析mtgsig 1.2版本,并用Python完整实现的过程,从头到尾拆解一遍。这不仅仅是一个参数怎么生成的问题,更是一次对现代前端加密和风控策略的深度探索。
本文将详细解析mtgsig 1.2的算法构成,包括其核心的RSA加密、AES加密以及自定义哈希算法的混合应用,并提供可直接运行、用于学习和测试的Python实现代码。无论你是想了解大型互联网公司的风控思路,还是需要解决实际的数据采集难题,这篇文章都能给你提供清晰的路径和可落地的方案。
2. 核心思路与算法架构拆解
逆向分析mtgsig,不能上来就硬抠代码,得先理解它的设计意图。美团外卖作为日活千万级的应用,其API安全的核心目标就两个:防伪造和防重放。mtgsig算法就是为了同时满足这两个目标而设计的复合型签名。
2.1 mtgsig 1.2 的组成与作用
通过抓包分析(使用Fiddler、Charles或浏览器开发者工具),你会发现,在请求美团外卖的某些API接口时,POST的Form Data或者URL参数里,会携带一个名为mtgsig的长字符串。这个字符串看起来像是一段被URLEncode过的、结构化的数据。
经过逆向其前端JavaScript代码(主要是*.js文件中的混淆代码),可以解析出mtgsig 1.2的典型结构。它不是一个单一的加密结果,而是一个包含了多种信息的令牌包。一个解码后的mtgsig示例结构如下:
{ "a": 2, "b": "xxxxxx", "c": "yyyyyy", "d": "zzzzzz", "e": 1234567890123, "f": "1.2", "g": "aaaaaaaa" }每个字段都有其特定的含义:
a(version_algo): 算法标识。2通常代表当前使用的RSA+AES混合算法套件。b(token): 一个关键的令牌,通常由服务端下发或前端生成,具有时效性,是防重放攻击的关键之一。c(encrypted_data):核心加密数据。这是整个签名的“心脏”,它里面加密了本次请求真正的“签名”信息。通常使用AES算法加密。d(aes_key_encrypted): 用于解密c的AES密钥,但这个密钥本身又被RSA公钥加密了。这是典型的“非对称加密保护对称密钥”模式。e(timestamp): 13位毫秒级时间戳,用于校验请求的新鲜度。f(sig_version): 签名版本号,这里是1.2。g(signature): 对上述部分或全部字段(特别是a,b,e,f等)进行某种哈希计算(如自定义的MD5或SHA256变种)得到的签名,用于验证mtgsig令牌包自身的完整性,防止被篡改。
注意: 字段名(
a,b,c...)和具体包含的字段数量可能随版本微调,但核心思路(RSA加密AES密钥,AES加密签名数据)是稳定的。逆向时务必以当前抓包分析为准。
2.2 算法流程与密钥体系
理解了结构,我们再来看mtgsig的生成流程,它清晰地体现了一个分层加密的思想:
- 准备原材料: 首先,需要生成一个本次请求的唯一签名(我们暂称为
inner_sig)。这个inner_sig通常由请求的URL、GET/POST参数、Cookie中的某些字段(如userId)、时间戳等,按照特定规则拼接后,再进行一次哈希(如MD5)计算得到。这是请求内容本身的“指纹”。 - 对称加密核心数据: 将上一步得到的
inner_sig,连同其他一些校验信息(可能包括token、uuid等),组装成一个字符串或JSON对象。然后,使用一个随机生成的AES-128-CBC密钥(aes_key)和随机初始向量(IV)对这个数据包进行加密,得到字段c(encrypted_data)。 - 非对称加密保护密钥: 随机生成的
aes_key和IV本身是明文的,不能直接传输。美团前端会使用一个固定的RSA公钥,对aes_key(有时连同IV)进行加密,得到字段d(aes_key_encrypted)。这个RSA公钥通常硬编码在JavaScript文件中。 - 组装与二次签名: 将算法标识
a、令牌b、加密数据c、加密密钥d、时间戳e、版本号f等字段,以key=value的形式按固定顺序拼接成一个字符串。然后,对这个字符串使用一个自定义的哈希算法(可能是在标准MD5基础上做了盐值混淆或循环处理)进行计算,得到最终的完整性签名g。 - 编码与发送: 将整个
JSON对象({a:..., b:..., ..., g:...})进行JSON序列化,然后进行URL编码(或直接作为字符串),作为mtgsig参数附带到请求中。
服务器端验证过程则相反:先用同样的规则计算g是否匹配,验证令牌包完整;然后用私钥解密d得到aes_key;再用aes_key解密c得到inner_sig;最后用请求内容重新计算inner_sig进行比对。
实操心得: 这里最关键的三个点是:1) 用于计算
inner_sig的参数拼接规则;2) 前端使用的固定RSA公钥;3) 计算最终签名g的自定义哈希算法。这三者都需要通过静态分析JS代码和动态调试来精准定位。
3. 逆向工程关键步骤与工具
理论清楚了,接下来就是实战。逆向mtgsig生成逻辑,主要依赖于对前端JavaScript代码的分析。这个过程需要耐心和一定的技巧。
3.1 环境准备与抓包定位
工欲善其事,必先利其器。你需要准备好以下工具:
- 抓包工具:
Charles或Fiddler。配置好SSL代理,以便捕获HTTPS请求。Chrome/Edge开发者工具的网络面板(Network)也足够用。 - 浏览器: 推荐使用
Chrome或Edge,其开发者工具功能强大。 - 代码调试工具: 浏览器自带的
Sources面板和Console面板是主战场。Firefox的调试器也不错。 - 格式化与搜索工具: 浏览器能自动格式化混淆的
JS,但一个能全局搜索的编辑器(如VSCode)在分析大文件时很有帮助。
首先,打开美团外卖网页版或H5页面,进行登录、搜索商家、查看商品等操作,同时在开发者工具的Network面板中筛选XHR/Fetch请求。找到那些返回数据丰富但请求参数中包含mtgsig的接口(例如商家列表、商品列表、订单详情接口)。记录下完整的请求URL、Headers和Form Data。
3.2 定位加密入口与关键函数
这是逆向中最具技巧性的一步。由于代码被严重混淆(变量名变成a,b,c,逻辑被分割和包裹),直接阅读几乎不可能。我们采用“由外及内,动态跟踪”的方法。
- 搜索关键字: 在开发者工具的
Sources面板中,对所有JS文件进行全局搜索(Ctrl+Shift+F)。搜索关键词可以是mtgsig、encrypt、rsa、aes、sign、1.2等。通常能在某个巨大的vendor.js或app.js中找到相关代码片段。 - 设置XHR断点: 在
Sources面板的XHR/Fetch Breakpoints中,添加一个包含目标接口URL关键词的断点。当浏览器发起对该接口的请求时,执行流会自动暂停。 - 调用栈分析: 断点触发后,查看
Call Stack(调用栈)。调用栈会显示从发起请求到断点处经过的所有函数。从栈顶往下找,寻找那些看起来与业务逻辑相关(函数名可能包含request,ajax,sign等)或者代码结构相对清晰的函数,点击进入。 - 追踪参数生成: 进入疑似函数后,重点观察在发送请求前,
mtgsig参数是如何被赋值的。通常会有一个函数调用,其返回值被赋给了mtgsig。在这个函数上打上断点,刷新页面或重新触发请求,跟进这个函数。 - 逐步执行与观察: 使用
F11(步入)逐步执行代码,同时观察Scope(作用域)中变量值的变化。你的目标是找到:生成inner_sig的拼接逻辑、AES密钥的生成与加密、RSA加密的调用点、以及最终组装和计算签名g的地方。
避坑指南: 混淆代码中会有大量条件判断、循环和异常处理来干扰调试。遇到非常复杂的逻辑块时,可以尝试在
Console中直接执行其中的一小段代码,看输出结果,帮助理解。另外,注意Promise和异步回调,调试时可能需要切换到对应的异步上下文。
3.3 提取核心算法与密钥
一旦跟踪到核心函数,下一步就是将其逻辑“翻译”成Python。
- 提取RSA公钥: 在代码中找到
RSA加密的地方,通常会有一个很长的字符串(Base64编码或16进制表示),这就是公钥。将其完整复制出来。注意观察其格式,可能是PKCS#1或PKCS#8。在Python中,我们需要将其转换为PEM格式的字符串,以便rsa或Crypto库加载。# 示例:从JS中提取的可能是这样的变量 # var rsaPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1W1kH..."; - 分析AES模式与填充: 找到
AES加密的函数调用。确认其使用的模式(通常是CBC)、密钥长度(128位)、填充方式(PKCS7)。同时找到生成随机IV(初始化向量)的代码。 - 还原哈希算法: 计算签名
g的哈希函数是重点。它可能看起来像MD5,但内部可能进行了多次循环、加盐(salt)或字符变换。你需要仔细分析该函数的每一步操作,确保在Python中能完全复现。有时它可能是一个自定义的CRC32或者对SHA256的截断处理。 - 确定参数拼接顺序: 找到组装
mtgsig字典以及计算签名g前拼接字符串的代码。确认各个字段(a,b,c...)的拼接顺序和分隔符(通常是&或|)。
这个过程需要反复对照JS执行结果和你的Python代码输出,使用相同的输入,确保每一步的中间结果都完全一致。
4. Python实现mtgsig 1.2生成器
经过艰苦的逆向分析,我们拿到了所有必要的算法细节。现在,用Python将它们实现出来。这里假设我们已经分析出以下信息(请注意,以下密钥和盐值为示例,实际需自行逆向获取):
RSA公钥: 一个PKCS#8格式的PEM字符串。AES模式:AES-128-CBC,填充为PKCS7。- 哈希算法
g:MD5(拼接字符串 + “固定盐值”)。 inner_sig算法:MD5(参数1=值1&参数2=值2&...&token=xxxx),参数按字母序排序。
我们将使用pycryptodome库来进行加密操作。首先安装依赖:pip install pycryptodome。
4.1 基础工具函数实现
我们先实现一些通用的加密函数。
import json import time import random import base64 import hashlib from urllib.parse import quote_plus from Crypto.Cipher import AES, PKCS1_v1_5 from Crypto.PublicKey import RSA from Crypto.Util.Padding import pad, unpad from Crypto.Hash import MD5 class MTGSigGenerator: def __init__(self, rsa_public_key_pem, hash_salt): """ 初始化生成器 :param rsa_public_key_pem: RSA公钥(PEM格式字符串) :param hash_salt: 计算最终签名g时使用的盐值 """ self.rsa_public_key = RSA.import_key(rsa_public_key_pem) self.hash_salt = hash_salt # 示例固定token,实际中可能需要从网络请求或本地存储获取 self.token = "your_dynamic_token_here" def _random_string(self, length=16): """生成指定长度的随机字符串,用于AES密钥和IV""" chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' return ''.join(random.choice(chars) for _ in range(length)) def _aes_encrypt(self, plaintext, key, iv): """AES-128-CBC PKCS7加密""" cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8')) ciphertext = cipher.encrypt(pad(plaintext.encode('utf-8'), AES.block_size)) return base64.b64encode(ciphertext).decode('utf-8') def _rsa_encrypt(self, plaintext): """RSA加密,使用PKCS#1 v1.5填充""" cipher = PKCS1_v1_5.new(self.rsa_public_key) # RSA加密有长度限制,需要分段加密,但AES密钥较短,一般一次即可 ciphertext = cipher.encrypt(plaintext.encode('utf-8')) return base64.b64encode(ciphertext).decode('utf-8')4.2 核心签名生成流程
接下来是核心的generate方法,它模拟了前端生成mtgsig的全过程。
def generate(self, url_path, query_params, body_params=None): """ 生成mtgsig 1.2 :param url_path: 请求路径,如 /api/v1/poi/filter :param query_params: GET查询参数字典 :param body_params: POST请求体参数字典 :return: mtgsig参数字符串 """ # 1. 生成inner_sig (请求内容签名) inner_sig = self._generate_inner_sig(url_path, query_params, body_params) # 2. 准备AES加密的数据包 (模拟前端结构) # 注意:这里的数据结构需要根据逆向结果调整 data_to_encrypt = { "sig": inner_sig, "token": self.token, "timestamp": int(time.time() * 1000), # 可能还有其他字段,如uuid, deviceId等 } encrypted_data_str = json.dumps(data_to_encrypt, separators=(',', ':'), ensure_ascii=False) # 3. 生成随机的AES密钥和IV,并加密数据 aes_key = self._random_string(16) # AES-128 密钥为16字节 aes_iv = self._random_string(16) # CBC模式需要16字节IV encrypted_data = self._aes_encrypt(encrypted_data_str, aes_key, aes_iv) # 对应字段 c # 4. 用RSA公钥加密AES密钥 (可能同时加密IV,根据逆向结果定) # 假设前端将 key 和 iv 用某个分隔符拼接后一起加密 key_iv_combo = f"{aes_key}|{aes_iv}" encrypted_key = self._rsa_encrypt(key_iv_combo) # 对应字段 d # 5. 组装mtgsig字典 timestamp = int(time.time() * 1000) mtgsig_dict = { "a": 2, # 算法标识 "b": self.token, "c": encrypted_data, "d": encrypted_key, "e": timestamp, "f": "1.2", # 版本号 # g 字段最后计算 } # 6. 计算最终签名 g # 根据逆向结果确定拼接顺序,例如: a=2&b=token&c=...&d=...&e=timestamp&f=1.2 # 注意:字段c和d已经是Base64字符串,可能包含特殊字符,需要确认前端是否做URL编码 sign_string = f"a={mtgsig_dict['a']}&b={mtgsig_dict['b']}&c={mtgsig_dict['c']}&d={mtgsig_dict['d']}&e={mtgsig_dict['e']}&f={mtgsig_dict['f']}" sign_string += self.hash_salt # 加上盐值 # 计算MD5 (可能是自定义哈希,这里用标准MD5示例) signature_g = hashlib.md5(sign_string.encode('utf-8')).hexdigest() mtgsig_dict['g'] = signature_g # 7. 序列化并URL编码 (有时不需要编码,直接发送JSON字符串) mtgsig_json_str = json.dumps(mtgsig_dict, separators=(',', ':'), ensure_ascii=False) # 根据抓包观察决定是否进行URL编码 # mtgsig_final = quote_plus(mtgsig_json_str) mtgsig_final = mtgsig_json_str return mtgsig_final def _generate_inner_sig(self, url_path, query_params, body_params): """生成内部签名,规则需要根据逆向精确还原""" # 这是一个示例规则,实际非常复杂 sign_dict = {} # 合并参数,通常包括URL路径、查询参数、请求体参数、固定token等 sign_dict['url'] = url_path if query_params: sign_dict.update(query_params) if body_params: sign_dict.update(body_params) sign_dict['token'] = self.token # 按Key的字母顺序排序 sorted_items = sorted(sign_dict.items(), key=lambda x: x[0]) # 拼接成 key1=value1&key2=value2 的形式 sign_string = '&'.join([f"{k}={v}" for k, v in sorted_items]) # 计算MD5 inner_sig = hashlib.md5(sign_string.encode('utf-8')).hexdigest() return inner_sig4.3 使用示例与测试
最后,我们写一个简单的示例来使用这个生成器,并与浏览器生成的mtgsig进行对比测试。
# 示例用法 if __name__ == '__main__': # !!! 警告:以下密钥和盐值为虚构示例,不可直接使用 !!! # 你必须通过逆向分析获取真实的公钥和盐值 SAMPLE_RSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx7H1... ... (你的真实RSA公钥) -----END PUBLIC KEY-----""" SAMPLE_HASH_SALT = "&salt=your_salt_value" generator = MTGSigGenerator(SAMPLE_RSA_PUBLIC_KEY, SAMPLE_HASH_SALT) # 模拟一个请求 url = "/api/poi/filter" query = {"lat": "39.90469", "lng": "116.40717", "page": "1"} body = {"category": "美食"} mtgsig = generator.generate(url, query, body) print("生成的mtgsig参数:") print(mtgsig) print("\n解码后的结构:") print(json.loads(mtgsig))关键注意事项: 上面的代码是一个高度简化的框架,展示了核心逻辑。其中最重要的
_generate_inner_sig方法、data_to_encrypt的结构、签名g的拼接规则和哈希算法,都必须根据你逆向分析美团外卖具体接口的实际结果来填充和修改。直接运行上述代码是无法生成有效mtgsig的。
5. 常见问题、调试技巧与对抗升级
即使算法完全正确,在实际使用中也可能遇到各种问题。这里分享一些调试经验和应对策略。
5.1 问题排查清单
当你实现的mtgsig不被服务器接受时,可以按照以下顺序排查:
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
返回403或签名错误 | inner_sig计算错误 | 1. 核对参与签名的所有参数是否齐全(特别是token,uuid,deviceId等)。2. 确认参数拼接顺序、大小写、分隔符。 3. 使用浏览器调试,在计算 inner_sig的函数处断点,记录所有输入,与你Python代码的输入逐字对比。 |
返回mtgsig解析失败 | mtgsig令牌包结构或编码错误 | 1. 检查mtgsig字典的字段名(a,b...)和类型(数字/字符串)是否正确。2. 检查 c和d字段的Base64编码是否正确,是否包含换行符。3. 检查最终输出的 mtgsig是JSON字符串还是URL编码后的字符串,需与抓包请求严格一致。 |
| 返回密钥解密失败 | RSA公钥不正确或加密格式不对 | 1. 确认提取的RSA公钥完整无误,格式是Python的RSA.import_key能识别的PEM。2. 确认加密模式是 PKCS1_v1_5还是OAEP,需要与前端一致。3. 确认加密的内容是 aes_key还是aes_key+iv,以及拼接分隔符。 |
| 返回数据解密失败 | AES密钥/IV错误或加密数据格式不对 | 1. 用RSA私钥(如果你有)解密d字段,看得到的aes_key和iv是否与你生成的一致。2. 确认 AES模式(CBC)、填充(PKCS7)、数据块大小(16)是否正确。3. 确认 c字段解密后的JSON结构是否符合服务器预期。 |
| 请求偶尔成功,多数失败 | token或时间戳失效 | 1.token可能有过期时间,需要定期从某个接口或Cookie中获取。2. 检查服务器时间差, timestamp是否在服务器可接受的时间窗口内(如±5分钟)。 |
5.2 动态参数获取与维护
mtgsig算法中的一些参数是动态的,这增加了自动化难度:
token(b字段): 通常来自登录后的Cookie(如_token)或某个初始化接口的响应。需要维护会话(session)来保持token的有效性。uuid/deviceId: 这些设备标识符可能参与inner_sig的计算。它们通常在页面加载时由前端生成并存储在本地(如localStorage),需要在发起请求前获取。RSA公钥与哈希盐值: 相对固定,但并非永久不变。美团可能会不定期更新前端JS文件,导致公钥和算法微调。需要建立监控机制,当大量请求失败时,重新抓取和分析JS文件。
5.3 对抗风控升级
大型平台的风控是持续对抗的过程。除了mtgsig,美团外卖还可能部署了其他风控措施:
- 请求频率限制: 过于频繁的请求会触发封禁。需要合理设置请求间隔,使用代理IP池。
- 行为指纹: 通过
JavaScript收集浏览器指纹(Canvas,WebGL,Fonts等)。纯Python请求缺乏这些指纹,可能被识别。更高级的模拟需要使用Puppeteer、Selenium等浏览器自动化工具,但效率较低。 - 代码混淆与动态更新: 前端
JS混淆强度会升级,甚至关键逻辑可能通过WebAssembly实现或每次动态加载。这要求逆向者具备更强的代码分析和动态调试能力。 mtgsig版本迭代:1.2之后可能会有1.3、2.0等版本,算法可能完全改变。需要关注请求中f字段(版本号)的变化。
个人经验分享: 对于长期稳定的数据采集需求,最稳妥的方式不是追求完全逆向最新的加密,而是维护一个可自动更新的“算法库”。思路是:将核心的逆向和提取逻辑(如定位公钥、解析哈希函数)也写成脚本,定期自动抓取目标页面最新的主
JS文件,通过特征匹配和简单的静态分析,自动提取出最新的公钥和盐值。这样,当对方更新时,你的系统能在短时间内自动适应,只需人工介入验证即可,极大提升了抗风险能力。当然,这需要更深入的工程化设计。
逆向工程就像一场猫鼠游戏,充满了挑战和乐趣。理解mtgsig这样的加密算法,不仅能解决具体的技术问题,更能让你深刻体会到现代Web安全技术的精妙之处。希望这篇详细的解析和实现指南,能为你打开一扇门。记住,关键在于耐心、细致的分析和不断的实践验证。