【作者主页】:小鱼神1024
【擅长领域】:JS逆向、小程序逆向、AST还原、验证码突防、Python开发、浏览器插件开发、React前端开发、NestJS后端开发等等
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请联系作者立即删除!
【该文章已同步至星球】:https://articles.zsxq.com/id_z0hwwtswwp1n.html
前言
最近听星球小伙伴说,mtgsig
签名算法升级到 1.2
了,出于学习目的,于是乎,我决定重新分析记录一下,希望能帮助到有需要的小伙伴。
前置分析
在请求时,发现请求头中存在 mtgsig
字段,如下所示:
当请求不携带 mtgsig
字段时,会返回 403
错误,如下所示:
逆向分析
通过堆栈进入 H5guard.js
文件中,发现有大量的混淆,如下所示:
为了方便学习观察,我们先使用 AST
还原一下代码,如下所示:
通过还原后的代码,很明显能看到许多浏览器环境参数,这样即使是补环境,也是清晰明了的。
当然了,咱们出于学习目的,这里就不补环境了,直接分析纯算 mtgsig
。
那么快速定位到 mtgsig
生产的位置呢?
通过观察发现,mtgsig
是 json
结构的字符串,很显然它是通过 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
生成的位置,如下所示:
很显然 mtgsig
是通过 new gO()
生成的,那找到这个 gO
类,如下所示:
简单分析后,发现它是一个 JSVMP
。那插桩呗!
说到插桩,可以先了解一下 日志插桩框架
,让你插桩分析更高效。传送门:终极逆向插桩日志框架,让浏览器崩溃成为历史!
将常见的运算符,比如:+、-、*、/、&、>>
等等,都插入日志,如下所示:
当日志执行完成后,发现 mtgsig
就生成了。如下所示:
a1
这个就不用多说了。就是 1.2
。
a2
a2
就是一个时间戳,可以用 Date.now()
生成。
a3
搜索 a3
的值:wz71wxx81z4v5x001wyyy43161z01uz6805500982y397958uwyxux11
, 如下所示:
它其实就是 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)// 1736411131947AOMEOAGfd79fef3d01d5e9aadc18ccd4d0c9507return `${dfp_timestamp}${constant_str}${gA}`
}
Ok,到这里,a3
的值就搞定了。
a5
搜索日志,找到第一个 a5
值生成的地方,如下所示:
通过日志发现,a5
是通过 kV
函数生成的。其中函数的参数数组又是通过函数 kX
生成的。
一步一步往上翻日志,它是通过截取 a6
值的前 10
位,再拼接上 a2
时间戳得到的。如图:
根据箭头指向,就能找到 a6
生成所有需要的参数。
其中,所涉及到的函数,直接缺啥补啥就行。这个过程比较简单,就不赘述了。
a6
gU(kz, !1, kt)
找到这个位置,它就是 a6
的值。
进入这个函数,看看它做了什么事情。
看到这里就很清楚了。将环境参数进行 ase
加密后,再 base64
加密。
有一个坑点要注意,它的秘钥 key
是动态的。原因是生成的 66
位大数组是随机的。
剩下的就是扣代码了。这个不难了,直接扣就行。
a8
搜索日志,找到第一个 a8
值生成的地方,如下所示:
通过分析可以发现,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_arr16
和 a6_index_arr16
是动态生成的。需要花点时间分析一下。
OK,到这里,a8
的值就搞定了。
a9
搜索日志,找到第一个 a9
值生成的地方,如下所示:
3.0.0
是 sdkVersion
版本号,7
是固定的。45
是随机生成的,可以用 Math.floor(Math.random() * 256)
生成即可。
那 a9
也没啥难度了,直接生成即可。
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
值生成的地方,如下所示:
首先它是根据 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;
}
那么这个数组又是怎么来的呢?继续往下看。
通过日志逐步分析得知,它是 a1
、a3
、a5
、a6
、a8
、a10
的值拼接起来的。
将拼接后的字符串:
41.21736410129324wz71wxx81z4v5x001wyyy43161z01uz6805500982y397958uwyxux11qIOev3SBPmKv0Imwif8vMvw1FM2mO8lW44ueWHC41uNU+CoUqc==h1.7Y4FJL8ZKH1YV87TLKT20o9eVBh+MFKyzDoQeZdPnueUWkLVh1aKXvwFwF/fohFW54blQoiRhD9Msea0fsBrQ+96IhtC7Duuf0jk6KG+j4Jpj89hygFdmSJC69KR/YkvtQsUi+iCbsNRLfjSJnvs2UA==34d26105456cd9b2494448073da64a1b3.0.0,7,45d76c3213382629f09445
通过 kK
函数,生成 16
位 Uint8Array
数组。
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
就搞定了。
参数验证
写个小例子,验证下生成的参数是否正确,如下:
搞定!!
如果还有什么疑问,请在星球里留言。