某书的X-S、X-T算法分析【2023.11.24】

最近玩了一下某书,还挺有用的,也发了点搞笑图文,在选择话题时对话题的插入产生了点兴趣,于是分析了一下搜索话题的接口,用C#写了接口中的X-S和X-T的生成方法。下面把完整分析过程记录一下。

1、定位JS位置

1、首先打开创作服务平台,发布笔记,上传图片,到可以插入话题的地方,打开开发者工具。如下图:

2、在开发者工具中点击Network,切换到网络请求监听页,再点击话题按钮,这时会产生查询话题的网络请求,找到topic请求,如下图

点击选中这个topic后,界面如下:

这里我们能看到发送请求时需要的两个参数X-S和X-T。

3、点击上图中的Headers后面的Initiator,可以看到发送这个网络请求调用的所有js方法和方法所在的js文件。如下图:

X-S和X-T参数的生成肯定在这些js文件中,我们从上往下查找。我们先点开第一个js,publish?source=official:1,点开后搜索关键字X-S,如下图:

这里可以看到搜索结果为0,表示不在这个文件。接着回到Network中,点击第二个js文件vendor-main.5b5b100.js:2,同样,点开后搜索关键字X-S,如下图:

这里基本确定了,参数生成就在这个JS中,我们查看所有的查询结果,找到第5处和第6处,如下图:

这里很容易看出来,第5处就是具体的参数生成方法,第6处是调用了这个方法。方法为:sign(e,t),返回一个{"X-s":"","X-t":""}的json对象。

4、我们先看这个方法的入参是什么,在上面找到的第6处,在左边单击一下鼠标,就可以设置一个断点,如下图:

设置好断点后,回到发布笔记的界面,尝试插入话题,再次触发查询话题的网络请求,程序运行到刚才的断点处则暂停下来,如下图:

程序停在这里,我们就能看到很多参数了,如下图所示:

我们可以在Console中输入想看的参数,或者鼠标悬停在某个参数上,这里可以看出X-T虽然他也是sign返回的,但其实就是13位的时间戳,这里就可以不用分析了。我们重点分析X-S,可以看到sign的参数有两个,第一个getRealUrl,我们输出一下,发现就是接口地址,去掉前面的主域名,而第二个参数i,就是请求参数,下图可以看到:

这里顺便提一下开发者工具右边有一个很有用的东西,Call Stack,这里可以追踪js的调用,当你不知道当前js方法是哪里调用的,就可以查看这个Call Stack,点击其中的一个步骤,就会调转到对应的js代码地方。

2、分析JS

下面我们的主要任务就是分析这个sign方法了。我们定位到sign的定义地方,如下图,

看到这里,可能有很多人就不淡定了,这都是些什么,完全看不懂,其实不用慌,这些都是迷惑大家的,我们用逆推法来解决问题,根本不用去看明白这些经过加密混淆的东西。

先定位到方法的return地方,再一步一步往回推,就能轻松解决问题了。我们看到如图的地方:

可以看到X-S就是 

ur[cr(re, ne)](mr, ur[cr(oe, ie)](MD5, [br, fr, e, vr ? JSON[cr(ae, se) + "fy"](t) : ""][cr(ue, le)]("")))

,我们来分析一下这一行代码。ur是什么,往上面翻,可以看到如图定义的地方:

这个ur是一个对象,它里面像map一样存了很多的方法,那么 ur[cr(re, ne)] 其实就是一个方法名,这个方法名就是定义在ur中的,而后面的就是这个方法需要的参数,几个参数呢,两个参数,一个参数是mr,另一个参数是 ur[cr(oe, ie)](MD5, [br, fr, e, vr ? JSON[cr(ae, se) + "fy"](t) : ""][cr(ue, le)]("")),可以看到,另一个参数又是一个方法计算出来的,什么方法呢,同样,方法名是 ur[cr(oe, ie)],参数是 MD5和 [br, fr, e, vr ? JSON[cr(ae, se) + "fy"](t) : ""][cr(ue, le)]("")。这样是不是就看懂了很多了。

那么下面我们来断点看看这些参数,如下图,断点后,分别在Console中计算各自的值:

那么我们的关键代码 ur[cr(re, ne)](mr, ur[cr(oe, ie)](MD5, [br, fr, e, vr ? JSON[cr(ae, se) + "fy"](t) : ""][cr(ue, le)]("")))  就得到了简化,如下图:

也就是 

ur[cr(re, ne)](mr, ur[cr(oe, ie)](MD5, '1700813207675test/web_api/sns/v1/search/topic{"keyword":"","suggest_topic_request":{"title":"","desc":"#"},"page":{"page_size":20,"page":1}}'))

,这里看到,这个方法的入参两个,分别是MD5和时间戳+test+请求接口地址路径+参数的字符串。

让我们继续往下分析,cr(oe, ie)  在Console中的值是'UyzSw',那我们找到ur中的'UyzSw',如图:

可以看到,ur[cr(oe, ie)] 就是一个方法,这个方法接收两个参数,e和t,其中e也是一个方法,返回e(t),那么上面的 

ur[cr(oe, ie)](MD5, '1700813207675test/web_api/sns/v1/search/topic{"keyword":"","suggest_topic_request":{"title":"","desc":"#"},"page":{"page_size":20,"page":1}}')

  其实就是

MD5( '1700813207675test/web_api/sns/v1/search/topic{"keyword":"","suggest_topic_request":{"title":"","desc":"#"},"page":{"page_size":20,"page":1}}')

,分析到这里就差不多要大功告成了,入参可以说是把 时间戳+test+请求接口地址路径+请求参数 进行MD5计算。

目前我们要分析的代码串简化为:

ur[cr(re, ne)](mr, MD5('1700813207675test/web_api/sns/v1/search/topic{"keyword":"","suggest_topic_request":{"title":"","desc":"#"},"page":{"page_size":20,"page":1}}'))

我们继续简化,看到还是一样的方法ur[cr(re, ne)],又是'UyzSw',再次简化为:mr(MD5('参数')),所以最后让我们把精力集中在mr这个方法,也就是一堆参数,经过mr方法计算,返回了最终的需要的X-S。

那这个mr是什么呢,我们在sign中找到它的定义,如图所示:

我们简单看一下这个方法,首先,它定义了两个变量,一个t,一个r,然后再定义了一个方法n,最后是方法的主体,一个for循环。我们看到的都是混淆加密后的样子,所以我们不必去仔细理解,大致看懂流程就行,这里看到最终for循环中case "0"时return了一个变量i,那么这里就是我们的突破口,这个i在case "5"中被计算出来,同样,我们把断点设置在case "5"中i的赋值语句,如下图:

从上图中,可以看出来很多东西,比如,入参是MD5(参数),for循环中步骤都是固定的,2,3,4,1,5,0,这个顺序就是写死的,case "5"中,for循环的次数是32次,因为e[n(1188, ve)]其实就是计算入参的长度,MD5后长度必然是32位,,那我们重点关注到case "5"的for循环中。

case "5"是一个循环,循环次数是入参的长度,这个算法应该就是从入参中取一些值经过计算拼接位字符串返回。

先看前三行, 

a = e[n(me, ye) + n(be, _e)](d++), 
s = e[n(we, 1227) + n(Se, 999)](d++), 
u = e[n(1359, ye) + n(Te, _e)](d++)

,  我们在Console中计算一下,如图所示:,这一下就清楚了,

a=charCodeAt(0),
s=charCodeAt(1),
u=charCodeAt(2)

,接着看 l = gr[n(1137, Ee)](a, 2),Console中计算n(1137, Ee)='lPLEl',那么找到gr的定义,

再找到其中'lPLEl'的定义,

同样,断点到这里,lPLEl 可以简化为

function(e,t){
    return ur['GJYEy'](e,t);
}

,  再找到ur中的'GJYEy'定义,如图:

,这里就看懂了,那么上面的代码  l = gr[n(1137, Ee)](a, 2)  简化为:

l = a >> 2 

是不是很简单,只要耐心去一步一步跟踪,代码都能转换为如此简单的表达式。

下面的c,p,f同理,可以简化为

c = ((a & 3) << 4) | (s >> 4); 
p = ((15 & s) << 2) | (u >> 6); 
f = u & 63; 

那最后就剩下两步了

先看  gr[n(Pe, Le)](isNaN, s) ? p = f = 64 : gr[n(De, 1297)](isNaN, u) && (f = 64),根据之前的经验,这里其实就是一个判断 gr[n(Pe, Le)](isNaN, s)  就是isNaN(s),所以这里的大概意思就是判断s是不是空,如果是空,那么p和f就直接赋值为64,如果不是空,后面就是一个表达式,没有任何赋值的语句,也就没有什么意义了,可以不用看。

现在就看最后一条语句了,这也是整个问题的最后一个点了,其实有了前面的经验,分析这个语句,也是得心应手,原来的代码

i = gr[n(Fe, Ue)](gr[n(He, 1157)](gr[n(ze, We)](i, hr[n(Ve, Ze)](l)), hr[n(Ge, 1043)](c)) + hr[n(qe, 1043)](p), hr[n(874, Ze)](f));

; 同样我们在Console中逐步计算,发现

n(Ve, Ze)、n(Ge, 1043)、n(qe, 1043)、n(874, Ze)都是一个方法,charAt,实际意思就是从hr中取对应索引的字符,hr我们Console一下就是一个写死的字符串

hr='A4NjFqYu5wPHsO0XTdDgMa2r1ZQocVte9UJBvk6/7=yRnhISGKblCWi+LpfE8xzm3'

,继续分析一下前面的方法,我们很轻松的知道了这个i就是每次循环都在hr中取4个字符,然后组成字符串,到此整个分析就结束了。

3、翻译为C#

代码如下:

public static string GetXHS_XS(string realUrl, string postData, string timeStamp) 
        {
            string result = "";
            try
            {
                string paramstr = timeStamp + "test" + realUrl + postData;
                paramstr = MD5Str(paramstr);
                if (!string.IsNullOrEmpty(paramstr)) 
                {
                    int a, s, u, l, c, p, f;
                    int d = 0;
                    const string hr = "A4NjFqYu5wPHsO0XTdDgMa2r1ZQocVte9UJBvk6/7=yRnhISGKblCWi+LpfE8xzm3";

                    while (d < 32)
                    {
                        a = (int)paramstr[d];
                        d++;
                        s =(int)paramstr[d];
                        d++;
                        if (d > 31)
                        {
                            u = 0;
                        }
                        else
                        {
                            u = (int)paramstr[d];
                        }
                        d++;
                        l = a >> 2;
                        c = ((a & 3) << 4) | (s >> 4);
                        p = ((15 & s) << 2) | (u >> 6);
                        f = u & 63;

                        if (d > 31) 
                        {
                            f = 64;
                        }

                        result = result + hr[l] + hr[c] + hr[p] + hr[f];
                    }
                }
            }
            catch (Exception ex) 
            { 
                
            }
            return result;
        }

  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值