最近玩了一下某书,还挺有用的,也发了点搞笑图文,在选择话题时对话题的插入产生了点兴趣,于是分析了一下搜索话题的接口,用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;
}