> 技术文档 > 某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10


【作者主页】:小鱼神1024

【擅长领域】:JS逆向、小程序逆向、AST还原、验证码突防、Python开发、浏览器插件开发、React前端开发、NestJS后端开发等等

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请联系作者立即删除!

【该文章已同步至星球】:https://articles.zsxq.com/id_z0hwwtswwp1n.html

前言

最近听星球小伙伴说,mtgsig 签名算法升级到 1.2 了,出于学习目的,于是乎,我决定重新分析记录一下,希望能帮助到有需要的小伙伴。

前置分析

在请求时,发现请求头中存在 mtgsig 字段,如下所示

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

当请求不携带 mtgsig 字段时,会返回 403 错误,如下所示:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

逆向分析

通过堆栈进入 H5guard.js 文件中,发现有大量的混淆,如下所示:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

为了方便学习观察,我们先使用 AST 还原一下代码,如下所示:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

通过还原后的代码,很明显能看到许多浏览器环境参数,这样即使是补环境,也是清晰明了的。

当然了,咱们出于学习目的,这里就不补环境了,直接分析纯算 mtgsig

那么快速定位到 mtgsig 生产的位置呢?

通过观察发现,mtgsigjson 结构的字符串,很显然它是通过 JSON.stringify 生成的。于是,我们可以通过 hook 它。

JSON.stringify_ = JSON.stringify;JSON.stringify = function () { if (arguments[0] && arguments[0][\"a1\"]) { debugger; } let result = JSON.stringify_.apply(this, arguments); return result;};

如果想了解更多实用 hook,传送门:JS 逆向定位神器:史上最实用的 Hook 脚本

通过 hook,快速定位到 mtgsig 生成的位置,如下所示:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

很显然 mtgsig 是通过 new gO() 生成的,那找到这个 gO 类,如下所示:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

简单分析后,发现它是一个 JSVMP。那插桩呗!

说到插桩,可以先了解一下 日志插桩框架,让你插桩分析更高效。传送门:终极逆向插桩日志框架,让浏览器崩溃成为历史!

将常见的运算符,比如:+、-、*、/、&、>> 等等,都插入日志,如下所示:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

当日志执行完成后,发现 mtgsig 就生成了。如下所示:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

a1

这个就不用多说了。就是 1.2

a2

a2 就是一个时间戳,可以用 Date.now() 生成。

a3

搜索 a3 的值:wz71wxx81z4v5x001wyyy43161z01uz6805500982y397958uwyxux11, 如下所示:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

它其实就是 localStorage 中的 dfpId 的值。

如果喜欢刨根问底,可能会问,localStorage 中的 dfpId 的值又是哪里来的呢?

它可以本地生成,也可以通过请求 webdfpid 接口获取。

不管用那种方式,都需要生成临时的 dfpId 值,代码如下:

/** * 获取a3 * @returns */function get_dfpId() { const dfp_timestamp = Date.now() const constant_str = \"AOMEOAG\" const env = { \"platform\": \"Win32\", \"vendor\": \"Google Inc.\" } const envUint8Array = new TextEncoder().encode(JSON.stringify(env)) // md5.md5算法可以在星球获取 const gA = md5.md5(envUint8Array) // 1736411131947AOMEOAGfd79fef3d01d5e9aadc18ccd4d0c9507 return `${dfp_timestamp}${constant_str}${gA}`}

Ok,到这里,a3 的值就搞定了。

a5

搜索日志,找到第一个 a5 值生成的地方,如下所示:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

通过日志发现,a5 是通过 kV 函数生成的。其中函数的参数数组又是通过函数 kX 生成的。

一步一步往上翻日志,它是通过截取 a6 值的前 10 位,再拼接上 a2 时间戳得到的。如图:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

根据箭头指向,就能找到 a6 生成所有需要的参数。

其中,所涉及到的函数,直接缺啥补啥就行。这个过程比较简单,就不赘述了。

a6

gU(kz, !1, kt) 找到这个位置,它就是 a6 的值。

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

进入这个函数,看看它做了什么事情。

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

看到这里就很清楚了。将环境参数进行 ase 加密后,再 base64 加密。

有一个坑点要注意,它的秘钥 key 是动态的。原因是生成的 66 位大数组是随机的。

剩下的就是扣代码了。这个不难了,直接扣就行。

a8

搜索日志,找到第一个 a8 值生成的地方,如下所示:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

通过分析可以发现,a8 是通过三个数组异或操作之后,再通过 toString(16) 转换为 16 进制字符串得到的。

const a5_index_arr16 = [202, 0, 115, 219, 118, 30, 116, 201, 100, 35, 92, 162, 207, 176, 73, 182] // 自行动态替换const a6_index_arr16 = [51, 152, 137, 25, 242, 234, 154, 33, 133, 11, 152, 70, 200, 246, 61, 173] // 自行动态替换const a8_index_arr16 = [115, 77, 208, 7, 220, 219, 190, 23, 10, 174, 113, 15, 83, 31, 108, 51]function get_a8() { let a8 = \"\" for (let i = 0; i < 16; i++) { const v1 = a5_index_arr16[i] ^ a6_index_arr16[i] const v2 = v1 ^ a8_index_arr16[i] const v3 = v2.toString(16) if (v3.length === 1) { a8 = a8 + (\"0\" + v3) } else { a8 = a8 + v3 } } return a8}

其中,a8_index_arr16 是固定的数组。这个生成 a8 的算法不难还原,主要 a5_index_arr16a6_index_arr16 是动态生成的。需要花点时间分析一下。

OK,到这里,a8 的值就搞定了。

a9

搜索日志,找到第一个 a9 值生成的地方,如下所示:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

3.0.0sdkVersion 版本号,7 是固定的。45 是随机生成的,可以用 Math.floor(Math.random() * 256) 生成即可。

a9 也没啥难度了,直接生成即可。

a10

这个更简单了,直接去代码里扣就行。如图:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

function get_a10() { for (var m2 = [], m3 = \"0123456789abcdef\", m4 = 0; m4 < 2; m4++) m2[m4] = m3[\"substr\"](Math[\"floor\"](16 * Math[\"random\"]()), 1); return m2[\"join\"](\"\");}

其实它就是在 0123456789abcdef 中随机区两个字符拼接起来。

x0

这个就不说了,就是固定 4

d1

这个才是重头戏。一起来看看吧。

找到第一个 d1 值生成的地方,如下所示:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

首先它是根据 16位数组 结合 toString(16) 生成的,生成代码如下:

function get_d1() { const result_arr = []; // 自行动态替换 let d1 = \"\"; for (let i = 0; i < result_arr.length; i++) { const v = result_arr[i].toString(16); if (v.length === 1) { d1 = d1 + (\"0\" + v); } else { d1 = d1 + v; } } return d1;}

那么这个数组又是怎么来的呢?继续往下看。

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

通过日志逐步分析得知,它是 a1a3a5a6a8a10 的值拼接起来的。

将拼接后的字符串:

41.21736410129324wz71wxx81z4v5x001wyyy43161z01uz6805500982y397958uwyxux11qIOev3SBPmKv0Imwif8vMvw1FM2mO8lW44ueWHC41uNU+CoUqc==h1.7Y4FJL8ZKH1YV87TLKT20o9eVBh+MFKyzDoQeZdPnueUWkLVh1aKXvwFwF/fohFW54blQoiRhD9Msea0fsBrQ+96IhtC7Duuf0jk6KG+j4Jpj89hygFdmSJC69KR/YkvtQsUi+iCbsNRLfjSJnvs2UA==34d26105456cd9b2494448073da64a1b3.0.0,7,45d76c3213382629f09445

通过 kK 函数,生成 16Uint8Array 数组。

function kK(lI) { for ( var lJ = encodeURIComponent(lI), lK = [], lL = 0; lL < lJ[\"length\"]; lL++ ) { var lM = lJ[\"charAt\"](lL); if (\"%\" === lM) { var lN = lJ[\"charAt\"](lL + 1) + lJ[\"charAt\"](lL + 2), lO = parseInt(lN, 16); lK[\"push\"](lO), (lL += 2); } else lK[\"push\"](lM[\"charCodeAt\"](0)); } return lK;}function get_table_string({ a1, a3, a5, a6, a8, a10, randomNum, params, api, method, data,}) { const string = `41.21736410129324wz71wxx81z4v5x001wyyy43161z01uz6805500982y397958uwyxux11qIOev3SBPmKv0Imwif8vMvw1FM2mO8lW44ueWHC41uNU+CoUqc==h1.7Y4FJL8ZKH1YV87TLKT20o9eVBh+MFKyzDoQeZdPnueUWkLVh1aKXvwFwF/fohFW54blQoiRhD9Msea0fsBrQ+96IhtC7Duuf0jk6KG+j4Jpj89hygFdmSJC69KR/YkvtQsUi+iCbsNRLfjSJnvs2UA==34d26105456cd9b2494448073da64a1b3.0.0,7,45d76c3213382629f09445`; const array = new Uint8Array(kK(string)); return md5.md5(array);}

那到索引表之后,再通过异或操作,生成 16 位数组。代码如下:

function get_arr16() { // const table = \"8a1ba972861df8c6f1d9462890b29a32\"; const table = get_table_string(); const temp_arr = [ 55, 63, 160, 244, 222, 253, 77, 56, 156, 75, 165, 121, 198, 117, 170, 115, ]; const result_arr = []; for (let i = 0; i < table.length; i += 2) { const v1 = \"0x\" + table.charAt(i); const v2 = v1 + table.charAt(i + 1); const v3 = temp_arr[i / 2] ^ parseInt(v2); result_arr.push(v3); } return result_arr;}

这就是 16 位数组生成过程。

当然了,别看我写的简单,其实这里有很多细节需要注意的。自己可以尝试还原一下。

此时 mgtsig 就搞定了。

参数验证

写个小例子,验证下生成的参数是否正确,如下:

某团 mtgsig1.2 | sdkVersion: 3.0.0 签名算法分析记录(2025/1/9)_mtgsig 1.2 a10

搞定!!

如果还有什么疑问,请在星球里留言。