某条反爬JS分析(3):签名算法分析


本文仅用于技术学习、交流,切莫用于非法用途,读者的一切行为后果自行承担。

签名整体分析

去除监听事件干扰

做完插桩处理后,刷新页面。

然后首先看下 trance_I,会发现调用了几次 addEventListener,设置了鼠标和键盘相关的几个事件,回调函数是 _YL

事件设置

为了搞清楚 _YL 是干啥的,首先调用 cleanTrance 清除所有记录,然后触发鼠标移动事件,再看一下几个记录数组里面多出了什么。

首先是 trance_fun

鼠标移动后的trance_fun

可以看出,我的鼠标移动操作一共触发了 12 次 mousemove 事件。

再看 trance_Itrance_S

鼠标移动后的trance_I

鼠标移动后的trance_S

显然,是把鼠标的移动轨迹存放到了某个数组中。

我只是稍微移动了一下鼠标,就收集了这么多的轨迹,加上这些轨迹并没有在请求时被带上,那么它们自然也没有参与到请求签名的生成之中。由此可以猜测,这些监听事件去掉了应该也不影响网页的正常使用。而若留着它们,则会对逆向分析造成干扰。

所以,再改一下 js 代码把监听事件去掉(注意这样的代码有两处):

if((I = S[A--]).x === _TM) {
  S[++A] = _jt(r, I.pc, I.len, C, I.z, x);
} else if(I.name !== 'addEventListener'){
  S[++A] = I.apply(x, C);
  tranceI(I,[x, C]);
  tranceS(S, 'apply ' + I.name + ' (' + x + ' ' + C + ') = ' + S[A]);
}

修改后再次刷新页面,果然还可以正常拿到数据,并且现在不会再有监听事件的干扰了。

请求签名的组成

查看 trance_fun,直接拉到最后:

数组的最后发现了请求签名

可以看到,调用 _TI,传入 URL,生成了一个字符串。看一眼抓到的包,可以发现它正好就是请求签名。

再往上翻,不难发现请求签名是由多个字符串拼接而成的。其中有一个字符串特别长,并且可以发现它是由 Cookie 中的 tt_scid 处理得到的。

`_Xx(tt_scid)`可得请求签名的最长分段

byted_acrawler的初始化

容易知道,acrawler.js 实际上定义了一个对象 byted_acrawler

使用它之前会先通过 byted_acrawler.init 初始化,然后才调用 byted_acrawler.sign 生成签名。初始化的过程需要传入参数,这可能会影响签名的计算。

byted_acrawler.init Hook 一下:

var _init = byted_acrawler.init;
byted_acrawler.init = function() {
    console.log('------------init: ' + JSON.stringify(arguments[0]) + '-----------------');
    _init(arguments[0]);
}

这样就可以拿到初始化参数 {"aid":24,"dfp":true}

此外,通过抓包易知,页面会访问 xxbg.snssdk.com,并得到一行执行回调的代码。像这样:

GET https://xxbg.snssdk.com/websdk/v1/p?callback=_9076_1592919432466
-> _9076_1592919432466("J4GudCwFIH55mpDQ8zo=");

这里也有一个字符串,它也可能会影响请求签名的计算结果。

为了观察初始化以及访问 xxbg.snssdk.com 对签名结果的影响,在本地写一个空白 HTML 并引入 acrawler.js,打开。

打开本地页面后,几个记录数组都为空。通过抓包软件可以看到,此时也并未访问 xxbg.snssdk.com

控制台执行 byted_acrawler.init({"aid":24,"dfp":true}),容易发现对 xxbg.snssdk.com 的访问是在初始化的过程中执行的。

初始化前后

通过请求调用堆栈,可以发现这个请求是在 S[++A] = I.apply(x, C) 处执行的。

网络请求的调用堆栈

直接查看 trance_I 来定位请求的发起并不方便。再改一下代码:

if((I = S[A--]).x === _TM){
    S[++A] = _jt(r, I.pc, I.len, C, I.z, x)
} else if(I.name !== 'addEventListener') {
    console.log(I.name);
    S[++A] = I.apply(x, C);
    tranceI(I,[x, C]);
    tranceS(S, 'apply ' + I.name + ' (' + x + ' ' + C + ') = ' + S[A]);
}

刷新页面,可以发现是在 appendChild 函数之后发起了请求。

借助打印日志定位请求发起位置

现在才来看 trance_I

对应的appendChild的记录

显然,这里是通过动态增加 script 标签的方式来访问 API 并执行回调。

此时并不能通过控制台访问到这个回调函数。

刷新页面,打上条件断点 I.name == 'appendChild',然后再次执行初始化。此时会断在 S[++A] = I.apply(x, C) 处,单步执行一次,此时请求已经发出,并且在控制台中可以访问到回调函数。

抓住你了

它在 JS 文件中对应下面这一段代码内的函数 e

oprand = _ju(r, o), I = oprand[1], S[A] = function (a, b) {
  var d = function e() {
    let res = _jt(tacStr, arguments.callee.pc, arguments.callee.len, arguments, _Sj, this);
    tranceFun(arguments.callee.name, arguments, res);
    return res;
  };
  return d.pc = a, d.len = b, d.x = _TM, d.z = t, d;
}(o + 6, I - 4), o += 2 * I - 2;

let res = _jt(tacStr, arguments.callee.pc, arguments.callee.len, arguments, _Sj, this); 这一行打上断点,然后 F8。

清空记录数组,然后在 return res; 这一行打上断点,再次 F8。

此时在控制台中已经无法访问回调函数了,但是我们可以通过记录数组查看 _1849_1592922338354("TZP68aNkQMIdQ1DhdRM=") 到底做了些什么。

回调函数的trance_S

回调函数的trance_fun

稍作分析即可发现,回调函数大概做了这么些事情:

  • TZP68aNkQMIdQ1DhdRM= 中截取后半段 QMIdQ1DhdRM=,将其作为函数 _VV 的参数,得到 @Â\u001dCPáu\u0013
  • 利用 TZP68aNkQMIdQ1DhdRM= 的前半段 TZP68aNk 和后半段处理后的字符串 @Â\u001dCPáu\u0013 生成一个特征值
  • 若干次执行函数 _Uf,将上面得到的特征值转换成另一个数
  • 使用 canvas 构造一张以文本 \u0098\u0011 ½ 为内容的图片,并获取图片的 Base64 编码字符串
  • 使用图片 Base64 和若干次执行 _Uf 得到的数,再生成另一个数

其中,\u0098\u0011 ½ 是之前将密文数组解密后得到的固定字符串之一。

继续执行代码,直到初始化完成,trance_S 中将得到两万多条记录,trance_fun 中也有一千多条。

查看 trance_fun 可知,除了访问 xxbg.snssdk.com 并执行回调函数以外,整个初始化过程似乎只是计算了一次 _Xx(tt_scid)

那么,如果不初始化,是否能正常地生成请求签名,并通过校验呢?

byted_acrawler.init 赋值为一个空函数,然后刷新某条的页面,我惊讶地发现,居然真的不需要初始化!emmm……也不排除后续版本又会改来改去啦。

虽然似乎白费了力气,但也有收获不是吗?这是逆向的魅力之一

请求签名逐段分析

为简单起见,先清空 cookie,排除 tt_scid 等值的影响。对它们的分析不妨放到之后再做。

刷新页面后,先执行一次 cleanTrance,并把 dcp 设置为 true,然后往下滚,触发一次列表查询。

这一次,得到了非常简单的结果:

_TI({url:"https://www.toutiao.com/toutiao/c/user/article/?page_type=1&…me=1592921783&count=20&as=A1C50E4FB2D246B&cp=5EF2F254060BDE1"})
== "_02B4Z6wo00f01UeM5mQAAIBDWTr108VwoFFHjeLAAA8S38"

往后就是分析 trance_S 等几个数组,过程简单但冗长,实在是懒得为了写博客重新做一遍了……哪天闲着蛋疼了再把整个过程写一下吧。

分析插桩日志,可以很容易地搞懂整个签名生成过程。

最终也会发现,__ac_signature 的生成用的是同一个算法。


文章作者: yuanbug
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yuanbug !
评论
  目录