百度登录的 sig, shaOne 算法

登录百度的时候,需要的参数有点多,其余参数都千篇一律,不过前几个月多出来(我也不知道什么时候多出来的)一个 sig 跟 shaOne 参数,其实也不太清楚这两个参数是不是必须参数,不过还是研究了一下。

sig

这个 sig 参数是这样的:

SHpuaVowZG1wbENyNVlZN2Q3bzJUNHVpNUtxMWJPNHFJSDZvbEpUa1dSSXl6UTF5ajd1dmRXMnI5TldDN3FSSA==

很明显,Base64 编码,于是解码看看:

HzniZ0dmplCr5YY7d7o2T4ui5Kq1bO4qIH6olJTkWRIyzQ1yj7uvdW2r9NWC7qRH

还是 Base64 编码,那再解码看看,这次就变成“乱码”了。

差不多可以理清这样的编码顺序,密文被编码成 Base64,再编码一次 Base64。

打断点

首先打开登录页面,全局搜索 sig 关键字,最终定位在:https://wappass.baidu.com/static/waplib/moonshad.js,这个 js 里面。

再次搜索 sig 关键字,定位在这个定义 json object 的地方:

t = {
    time: r.time,
    alg: r[_0x19c3("0x92")],
    sig: o[_0x19c3("0x9b")](r, c, _),
    elapsed: (new Date)[_0x19c3("0x95")]() - n || "",
    shaOne: i
}

好家伙,原来 shaOne 也在这,舒服了。

代码分析

代码是经过混淆的,不过打断点之后可以知道具体使用的方法名。

我们通过监事表达式 Watch,知道了 _0x19c3(“0x9b”) 其实是 encryption 方法。

参数

里面传入的参数分别是 r,c,_ 三个参数,c 参数不重要。

r 参数

其中,r 可以直接看到是传入了一个 json object,为:

{
    "staticpage":"https://tieba.baidu.com/tb/static-common/html/pass/v3Jump.html",
    "charset":"UTF-8",
    "token":"da4e6e6f5420c0bf8416b3ed89bd3d8c",
    "tpl":"tb",
    "subpro":"",
    "apiver":"v3",
    "tt":1589191798699,
    "codestring":"",
    "safeflg":"0",
    "u":"https://tieba.baidu.com/index.html",
    "isPhone":"",
    "detect":"1",
    "gid":"C818A74-69B5-4F10-AE1C-40BF7135BD57",
    "quick_user":"0",
    "logintype":"dialogLogin",
    "logLoginType":"pc_loginDialog",
    "idc":"",
    "loginmerge":"true",
    "mkey":"",
    "splogin":"rate",
    "username":"",
    "password":"",
    "mem_pass":"on",
    "rsakey":"",
    "crypttype":12,
    "ppui_logintime":23819177,
    "countrycode":"",
    "fp_uid":"",
    "fp_info":"",
    "loginversion":"v4",
    "supportdv":"1",
    "ds":"",
    "dv":"",
    "fuid":"",
    "alg":"v3",
    "time":1589191799
}
token 参数:有一个 get 的 API,可以自己抓,访问的时候需要带上 Cookie,才会返回正确的 token。
tt 和 time 参数:时间戳,其中 tt 为 13 位,time 为 10 位。
上面这个 json object 中,token 值非常重要,gid 可带可不带,其余除时间戳外都是固定的字符串。

_ 参数

经过几次断点,发现这个 _ 参数,是一个固定值:moonshad0moonsh1,16位,应该是 AES 密钥,经过前段时间逆向极验的代码,一下就觉得可能是用这个密钥 AES 加密上面的 r 参数。

加密过程

继续跟进到 encryption 方法:

encryption: function(x, c, _) {
    var t = r(x, c);
    return i(n(t, _))
}

发现上面的 r 参数被当做 x 传进来了,然后通过 r 方法处理,返回了一个 t。

跟进一下 r 方法:

x.exports = function(x, c) {
    var _ = [];
    for (var t in x)
        x.hasOwnProperty(t) && _.push(t);
    _[_0x19c3("0x9c")]();
    for (var r = [], n = 0, i = _.length; n < i; n++) {
        var e = _[n];
        r[_0x19c3("0x21")](e + "=" + x[e])
    }
    var o = u(r.join("&"))
      , a = ""
      , s = (window[_0x19c3("0x8f")][_0x19c3("0x90")] + "" + window[_0x19c3("0x8f")].height)[_0x19c3("0x9d")]("");
    for (n = 0; n < s[_0x19c3("0x18")]; n++) a += h[s[n]]; return function f(x, c) { var _, t = "", r = x[_0x19c3("0x9d")](""), n = c[_0x19c3("0x9d")](""); if (r[_0x19c3("0x18")] >= n[_0x19c3("0x18")]) {
            for (_ = 0; _ < n[_0x19c3("0x18")]; _++)
                t += r[_] + n[_];
            t += x[_0x19c3("0x9e")](_)
        } else {
            for (_ = 0; _ < r.length; _++)
                t += r[_] + n[_];
            t += c[_0x19c3("0x9e")](_)
        }
        return t
    }(o, a)
}

这个方法没有再调用别的方法,很好翻译,整个流程就是:

先把 json object 里的 key 拿出来,放入一个列表,之后对这个列表进行字典序排列,再重新按照 key=value 这样排列放入一个列表,最后用 & 符号连接每一个值。

里面有个 u 方法,其实就是 md5 加密,所以不算外部自定义方法,将上述用 & 串好的值做 md5 加密得到一个字符串。

再通过 for 循环进行字符串重新排序组合。

使用 Python 重写如下:

def r(x):
    o = md5("&".join([f"{n}={x[n]}" for n in sorted([t for t in x])]))
    h = {"0":"s","1":"t","2":"r","3":"h","4":"i","5":"j","6":"k","7":"l","8":"m","9":"n","a":"3","b":"4","c":"5","d":"9","e":"8","f":"7","g":"1","h":"2","i":"6","j":"0","k":"a","l":"b","m":"c","n":"d","o":"e","p":"f","q":"g","r":"z","s":"y","t":"x","u":"w","v":"v","w":"u","x":"o","y":"p","z":"q"}
    a = ""
    s = ["1", "9", "2", "0", "1", "0", "8", "0"]
    for n in s:
        a += h[n]
    return f(o, a)

def f(x, c):
    t, r, n = "", list(x), list(c)
    if len(r) >= len(n):
        for _ in range(len(n)):
            t += r[_] + n[_]
        t += x[_+1:]
    else:
        for _ in range(len(r)):
            t += r[_] + n[_]
        t += c[_+1:]
    return t

现在我们得到了 t 值,最终的 sig 值是 i(n(t, _)) 得到的

其中 _ 值是我们上面提到的 AES 密钥,这样一看,好像确实是 AES 密钥,跟进一下 n 方法:

x[_0x19c3("0x0")] = function(x, c) {
    var _ = c
      , t = (c = r[_0x19c3("0x1d")].Utf8[_0x19c3("0x26")](_),
    r[_0x19c3("0x1d")][_0x19c3("0x24")][_0x19c3("0x26")](x));
    return r.AES[_0x19c3("0x43")](t, c, {
        mode: r[_0x19c3("0x46")][_0x19c3("0x9f")],
        padding: r[_0x19c3("0x4f")][_0x19c3("0x50")]
    })[_0x19c3("0x14")]()
}

嗯,是 AES 加密,用的是 ECB 模式。

通过网页在线 AES 加密,把 t 参数作为需要加密的内容,_ 参数为密钥,密文用 Base64 编码,得到的结果与 n(t, _) 是一样的。

OK,再跟进一下 n 方法:

x[_0x19c3("0x0")] = function(x) {
    var c, _, t, r, n, i, e = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    for (t = x[_0x19c3("0x18")],
    _ = 0,
    c = ""; _ < t; ) { if (r = 255 & x.charCodeAt(_++), _ === t) { c += e.charAt(r >> 2),
            c += e[_0x19c3("0x6c")]((3 & r) << 4), c += "=="; break } if (n = x[_0x19c3("0x23")](_++), _ === t) { c += e[_0x19c3("0x6c")](r >> 2),
            c += e[_0x19c3("0x6c")]((3 & r) << 4 | (240 & n) >> 4),
            c += e[_0x19c3("0x6c")]((15 & n) << 2), c += "="; break } i = x[_0x19c3("0x23")](_++), c += e[_0x19c3("0x6c")](r >> 2),
        c += e.charAt((3 & r) << 4 | (240 & n) >> 4),
        c += e.charAt((15 & n) << 2 | (192 & i) >> 6),
        c += e[_0x19c3("0x6c")](63 & i)
    }
    return c
}

一看到这个 e 参数,那下面不用看了,盲猜 Base64 编码。

经过测试,这个方法确实是用来做 Base64 编码的,那就跟我们上面想的一样了,AES 加密后的密文,通过两次 Base64 编码,得到了 sig。

shaOne

对,就是 SHA-1,既然知道这个值是 sha1 值,那么我们只需要不断往上层走,看看是把什么内容加密成了 sha1。

最上面可以看到,shaOne 给的是 i 参数,通过 js 再往上看,i 是通过 一个 for 循环来得到的:

var n, i = "";
for (n = i = (new Date)[_0x19c3("0x95")](); "00" !== (i = f(u(i)))[_0x19c3("0x14")]()[_0x19c3("0x20")](0, 2); )

首先给 n 和 i 赋值为 13 位时间戳,再 for 循环重新给 i 赋值并且判断如果 i 的前两位字符串等于 “00” 就跳出循环。

首先我们看看将 i 通过 u 方法执行后会得出什么结果:

>>> u(i)
<<< "e4ff899a14532fc40acc5bb651fb4190"

好像是个 md5 加密,毕竟位数都一样,先测试一波,在线将 1589191804791 用 md5 加密一下,得到了 e4ff899a14532fc40acc5bb651fb4190,简单,就是 md5 加密。省去了看代码的功夫。

那我们再看看 f(u(i)) 执行后会是什么结果。

>>> f(u(i))
<<< "e7afd4765c748dcd83ba337290feec549c248d7f"

哦~ 说不定是 sha1 加密,故技重施,将 e4ff899a14532fc40acc5bb651fb4190 用 sha1 再加密一下,果然跟结果是一样的。

那就简单了,过程应该是,n 和 i 都赋值为 13 位时间戳,然后进入循环体,i 被 md5 加密 n 后再进行 sha1 加密后赋值,判断此时的 i 前两位是不是 “00”,如果是,就跳出循环,不是则继续。

按照这个逻辑写一下 Python:

import hashlib

def md5(s):
    cm = hashlib.md5(s.encode('utf-8')).hexdigest()
    return cm
    
def sha1(s):
    cm = hashlib.sha1(s.encode('utf-8')).hexdigest()
    return cm

def shaOne():
    n = "1589191804791"
    i = sha1(md5(n))
    while "00" != i[:2]:
        i = sha1(md5(i))
    return i

运行的时候将 n 赋值为 1589191804791,运行最后得出的结果跟上面 js 运行得到的结果是一样的。

总结

sig 参数是通过将一个 json object 通过排列组合得到一个字符串,再将这个字符串 AES 加密,经过两次 Base64 编码得到。

shaOne 参数是通过将时间戳进行 md5 加密,再 sha1 加密,之后再将 sha1 做 md5 加密后再 sha1 加密,如此套娃几百次之后,得到一个 00 开头的 sha1,就是 shaOne 参数了。

发表评论

发表评论

*

沙发空缺中,还不快抢~