Skip to content

上一章说到通过直接调用 wasm 来实现生成 Sign,但发现只针对那一个域名接口的请求有效,换个接口就不行了,看来还是要彻底分析他的生成逻辑。

逆向小程序

在小程序打开前就对它抓包,直到页面加载完毕也没发现相关js的请求,而且api的 referer: 都是指向了 https://servicewechat.com,所以可以基本确定相关的js文件都早打包进小程序下载到了本地。

打开目录 Documents\WeChat Files\Applet 根据修改日期可以确定哪个文件夹对应着小程序包。

接下来需要对 wxapkg 文件进行解密然后解包,由于微信的更新,在5月之前的解包工具都不能正常的解带有分包的小程序,我用的是 unveilr.exe 转收费前的版本,能正常的解大部分的文件,但还是有点小毛病,相关的解包教程可以去网上找。

解包完成后可以通过文件的大小,或者是否存在 app.json 文件来确定是否成功解包,将解包后的相应文件夹导入微信开发者工具,开始动态调试。

因为解包工具比较旧,导入后会出现不少报错,只要根据 微信官方文档 稍微改改就可以正常编译,不需要的页面可以去掉,只保留需要分析的地方。

app.json 文件中的 plugins对象内容可以删除,否则会报错未授权使用插件。其他请求登录接口报错的可以抓包拦截下来,修改为原本小程序的返回包。

如果想要第一屏设置为分包页面,那就将 pages 对象添加对应分包页面,并删除 preloadRule 对象中的分包页面相关信息,否则会报错 pages *** 不应该在分包 subPackages[*] 中,之后重新编译即可进入设置的页面。

分析代码

全局搜索关键字 Sign 可以找到相关代码段,而且代码没有做混淆处理,分析起来非常容易。

比如这一段,一眼就可以看出是 sha1 处理函数,传入字符串,返回sha1字符串

js
function (e, t) {
        var n = global.bc("platform", "cloud-fe-yunsdk-platform");
        e.exports = function (e) {
            var t, n, r = new Uint8Array(function (e) {
                var t, n, r, o = [];
                for (t = 0; t < e.length; t++)(n = e.charCodeAt(t)) < 128 ? o.push(n) : n < 2048 ?
                    o.push(192 + (n >> 6 & 31), 128 + (63 & n)) : ((r = 55296 ^ n) >> 10 == 0 ? (n =
                        (r << 10) + (56320 ^ e.charCodeAt(++t)) + 65536, o.push(240 + (n >> 18 &
                            7), 128 + (n >> 12 & 63))) : o.push(224 + (n >> 12 & 15)), o.push(
                                128 + (n >> 6 & 63), 128 + (63 & n)));
                return o
            }(e)),
                o = 16 + (r.length + 8 >>> 6 << 4);
            for ((e = new Uint8Array(o << 2)).set(new Uint8Array(r.buffer)), e = new Uint32Array(e.buffer),
                n = new DataView(e.buffer), d = 0; d < o; d++) e[d] = n.getUint32(d << 2);
            e[r.length >> 2] |= 128 << 24 - 8 * (3 & r.length), e[o - 1] = r.length << 3;
            var a = [],
                s = [function () {
                    return u[1] & u[2] | ~u[1] & u[3]
                }, function () {
                    return u[1] ^ u[2] ^ u[3]
                }, function () {
                    return u[1] & u[2] | u[1] & u[3] | u[2] & u[3]
                }, function () {
                    return u[1] ^ u[2] ^ u[3]
                }],
                c = function (e, t) {
                    return e << t | e >>> 32 - t
                },
                i = [1518500249, 1859775393, -1894007588, -899497514],
                u = [1732584193, -271733879, null, null, -1009589776];
            for (u[2] = ~u[0], u[3] = ~u[1], d = 0; d < e.length; d += 16) {
                var l = u.slice(0);
                for (t = 0; t < 80; t++) a[t] = t < 16 ? e[d + t] : c(a[t - 3] ^ a[t - 8] ^ a[t - 14] ^ a[t -
                    16], 1), n = c(u[0], 5) + s[t / 20 | 0]() + u[4] + a[t] + i[t / 20 | 0] | 0, u[1] =
                    c(u[1], 30), u.pop(), u.unshift(n);
                for (t = 0; t < 5; t++) u[t] = u[t] + l[t] | 0
            }
            n = new DataView(new Uint32Array(u).buffer);
            for (var d = 0; d < 5; d++) u[d] = n.getUint32(d << 2);
            return Array.prototype.map.call(new Uint8Array(new Uint32Array(u).buffer), (function (e) {
                return (e < 16 ? "0" : "") + e.toString(16)
            })).join("")
        }
    }

调用 sha1 传入字符串的函数是

js
        function l(t, n) {
            var r = arguments.length > 2 && void 0 !== arguments[2] && arguments[2];
            return function (n) {
                if (t.nonce = t.nonce || a()(), t.ts = t.ts || s.default.get(!1, !0), "object" == e(t.body))
                    try {
                        t.body = JSON.stringify(t.body)
                    } catch (n) {
                        t.body = ""
                    }
                n = n.map((function (e) {
                    return t[e]
                })).join(";");
                var o = i()(n);
                return r && console.log("[SAFE]:", o, n), {
                    nonce: t.nonce,
                    ts: t.ts,
                    sign: o
                }
            }(n)
        }

有了上面的这些,也就不难分析出生成 sign的逻辑了。

生成逻辑

传入的参数t包含有nonce,ts和body属性,n通过观察前面的代码可以知道是 ["signKey", "skey", "body", "nonce", "ts"]这样一个数组。

首先,第一个 if 会检查传入的t.nonce,t.ts 是否存在,如不存在则调用相应函数生成,接着判断body的类型,如果是 object 则将其转换为json,转换失败则置空。然后按照n数组的顺序,将数组内容对应值通过 ; 符号连接起来形成这样的字符串 signKey;skey;body;nonce;ts signKey 的值在前面的代码也有(换个域名就不对的原因),skey 为auto-token。再把生成的字符串传给 i 即 sha1 函数,得出 sign 值。

结语

对于这种没有混淆的代码,甚至不用动态调试都可以看出生成的逻辑,便能省掉让其在微信开发者工具中运行的时间。

但是动态调试的话可以看到中间变量,减少分析过程的错误。就比如我,想当然的认为最后的L函数必定会传入body,因为post请求的body就在那里摆着,但sha1函数的输出结果总是和实际值对不起来。直到我在开发者工具里下断点调试了好几遍之后才发现,有的请求虽然有body,但并不会将body传入L函数,sha1的输出也自然没有body的事情,不知到是他代码逻辑就是这样,还是说是开发者写的bug 😓

Released under the MIT License.