zhi乎接口x-zse-96签名的代码环境补全流程

281 阅读4分钟

https://www.zhihu.com/people/weizhi-xiazhi 开始的请求分析

最终代码补全后的结果

最终目的:获取到关注了该用户的其他用户的知乎个人资料(用户名,性别,教育经历等)

  • 在浏览器打开该页面。点击该用户的关注订阅。点击【关注ta的人】。在控制台中找到https://www.zhihu.com/api/v4/members/weizhi-xiazhi/followers接口,该接口是用来查询谁关注了当前用户的。通过修改用户id发现该接口必须要携带正确的header才行。否则会报错 {"error":{"message":"请求参数异常,请升级客户端后重试。","code":10003}}

将该接口copy成fetch如下

fetch("https://www.zhihu.com/api/v4/members/weizhi-xiazhi/followers?include=data%5B*%5D.answer_count%2Carticles_count%2Cgender%2Cfollower_count%2Cis_followed%2Cis_following%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&offset=0&limit=20", {
  "headers": {
    "x-zse-93": "101_3_3.0",
    "x-zse-96": "2.0_wyIkKa9pm25sNcIeX1mgLDycfir9/zpLRHQM8UrsKCQBbtgy6ken7OG5AuYi8naX"
  },
});

发现里面有两个特殊的请求头,通过在页面切换不同分页后可以简单看出x-zse-93是固定的,x-zse-96是动态的,从上面x-zse-96的内容长度可推测该参数仅仅只是对请求url仅仅加密后生成的一串token。不可能携带得有浏览器环境信息(因为长度实在太短了)。

分析x-zse-96如何生成

在源代码中全局搜“x-zse-96”。下图中标记的标记2.并在该处打一个断点。然后不停使用标记3退出当前函数。 image.png

直到右侧出现te的接口为/api/v4/members/weizhi-xiazhi/followers。 image.png

从这里可以得知这里就是设置请求头的地方。通过观察可明确知道关键逻辑应该在这一段代码内部。打上断点进入该方法

tT = ed(te, tf.body, {
                            zse93: tb,
                            dc0: tO,
                            xZst81: tE
                      }, tA)

多次尝试后可以得出结论。该加密方法是将请求路径经过一个算法生成了一串key。关键逻辑就在下图蓝色框框内。只要扣出对应代码即可

image.png

如何扣代码?

1、先把ty扣出来,要注意这个方法不带任何参数且需要返回另一个函数用来加密路径。

往上搜发现ty如下。像这种带有数字的是webpack打包时模块加载逻辑。而ty应该就是10261这个模块。直接去搜

tg = tr(10261), ty = tr.n(tg)

搜索结果如下。ty本身明显就是这一段蓝框代码。这看样子应该是一段md5算法。不用管他。先走到结尾。

image.png

ty的结尾如下

 void 0 !== (ti = (function() {
                return tT
            }
            ).call(te, tr, te, tt)) && (tt.exports = ti)

从这几行代码可只ty应该就是返回了tT的。将ty处理成下面的形式

function ty(){
     var tu = function(tt, te) {
                var tr = (65535 & tt) + (65535 & te);
                return (tt >> 16) + (te >> 16) + (tr >> 16) << 16 | 65535 & tr
      }
      //中间还有很多代码这里不展开了。具体代码可查看最终成果
      ,tT = function(tt, te, tr) {
                return te ? tr ? tS(te, tt) : tO(te, tt) : tr ? tC(tt) : tE(tt)
       };
      return tT
}

2、tJ代码的提取.

进入到tJ后如下。通过在该方法打几次断点可知道,该方法的tt参数始终为undefined。那么最为关键的就是这个方法内部的tv如何弄了。

  var tJ = function(tt) {
          return tt && tt.version && "function" == typeof tt.encrypt ? tt : {
              encrypt: tv.ZP,
              version: tv.XL
          }
      }

3、tJ内部的tv方法提取
根据tv后发现tv如下,tv是1514这个模块

tp = tr(4667), th = tr.n(tp), tv = tr(1514)

image.png

在1514这个模块底部发现了上面第2步中的ZP和XL。那应该tv方法就是这没错了。

注意该段代码中使用了74185这个模块,这个模块很简单就不详细写了。就是个类似typeof的工具函数。

image.png

如何补全环境

经过上面的步骤关键代码已经扣好了,具体代码可查看

因为代码最后是要在selenium这种无头浏览器驱动中运行的。所以还需要补环境来骗过代码中环境检测逻辑。如果不补环境无法很好与其他脚本集成。补环境的前提是知道代码中使用了哪些全局变量。在js中,fromCharCode用来将UTF-16 码转为字符串。这是任何混淆方法都壁不开的一个api。我们在上面扣好的代码中找到使用了fromCharCode的地方并自己处理下,

类似下面这样。打印下看看都是啥

image.png

结果是这样的

image.png

可以看出有几个比较关键的对象

  • window
  • navigator
  • location
  • history

针对这几个对象使用Proxy看看都访问了什么属性,并把值替换下即可

    let navigator = new Proxy(window.navigator, {
          get(t, k) {
            if (k === 'userAgent') {
              return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
            }
            if (k === 'webdriver') {
              return false
            }
            return t[k]
          },
          set(t, k, v) {
            return (t[k] = v)
          }
        })

image.png

这样就完成了环境补全。实测下来还根据location来判断当前html是否是通过http/s协议打开。所以我们的html文件需要启动一个服务后访问才行。这样就算完成了