“分析今日头条内小视频和西瓜视频分享后浏览器打开所用的signature签名算法。”
上月写的一篇关于使用微信的wxid加好友的文章,竟然无意间碰到了一个微信搜索黑洞,成就了本号有史以来搜索量、阅读量、关注度最大的文章,看样子纯粹的干货受众面有限,还是关注度高的信息才能带来更大的收获。
为更好地满足大家的需求,可以发送“wxid”获取微信使用wxid加好友的具体方法截图,毕竟我没法一条条请求都回复,也没法手把手地教,各位解决问题还是以自助为主。
下面是这篇文章,有需要的朋友可以点进去学习:
今天这篇文章,对今日头条的签名算法进行分析,相当干货,里面的知识在今日头条相关的技术应用中使用非常广泛,希望大家能够学习愉快。
今日头条作为一大当红炸子鸡,很多人都盯着,当然,爬取头条里各种类型的数据必不可少,网上很多分析头条signature签名算法的文章,但都不尽兴,有些问题没有明确,导致实践起来却有些问题,这里将给出signature签名更有价值的说明。
另外,不得不说,js是世界上最垃圾的语言,希望早日退出历史的舞台。
01
—
signature是什么?
在头条里,对一些关键节点的数据,服务器会进行校验,校验的一部分,就是在客户端对数据进行签名,在服务器对数据进行合法性检查,这样,就能够过滤掉很大一部分不合法的请求。
例如,在访问一个头条分享的视频过程中,浏览器会自动请求下面这个URL:
https://m.365yg.com/i6714101379172027656/info/?_signature=lyZkrxARynO8Pdz-QfDCYJcmZL&i=6714101379172027656
这个里面,_signature在它前后的报文中是没有的,是头条服务器下发下来的js根据一些参数计算出来的,也就是签名。
这个_signature如果错误,服务器就会发现,就不会返回我们真正需要的报文。
上面的这个URL,是用来请求视频的一些关键信息的,如果_signature错误,则会返回一个头条自己的默认视频。
因此,signature就是一个由客户端根据头条下发的js算法生成的校验值,即签名,服务器将会对它进行校验,并根据校验情况作出不同的反馈。
02
—
签名算法
知道了头条的signature签名是怎么回事,大家一定会想到,既然signature值是在客户端由js生成的,而js我们能够轻松拿到,并且js代码是没法真正加密的,那这个签名值不就很容易算出来吗?
我一开始也是这么想的,当看到了生成signature签名的js算法的时候,我气馁了,这到底是什么玩意?!经过了混淆,里面到处是乱码,没有格式,一堆莫名其妙的字符串。
那就正儿八经地一点点的分析吧。
先说下,这个签名算法很早前就有人将它逆向出来了,网上搜“头条signature”就能找到一大堆,经过确认,在头条小视频和西瓜视频中使用的就是这个算法。
这里介绍下符合头条小视频和西瓜视频的分析过程,以便更好的使用。
使用fiddler抓包,很容易就能抓到头条的各类js文件,其中,签名算法在xigua_video.38p5Q8hF.js这个文件中,下面是对文件格式化后的签名部分:
看着很崩溃吧,里面一堆乱码,这是js所谓的特性,除了制造不必要的阻碍,没有任何作用,我们要有信心,只要有能运行js的环境,js必然是裸奔,最终都将还原成浏览器可以识别的代码,事实也确实是这样。
一步步来把它解开,先将前面那部分来尝试解开,下面是代码,直接就输出结果了:
var x =function(l){return'e(e,a,r){(b[e]||(b[e]=t("x,y","x "+e+" y")(r,a)}a(e,a,r){(k[r]||(k[r]=t("x,y","new x[y]("+Array(r+1).join(",x[y]")(1)+")")(e,a)}r(e,a,r){n,t,s={},b=s.d=r?r.d+1:0;for(s["$"+b]=s,t=0;t>>065:h=,y=,[y]=h66:u(e(t[b],,67:y=,d=,u((g=).x===c?r(g.y,y,k):g.apply(d,y68:u(e((g=t[b])<"<"?(b--,f):g+g,,70:u(!1)71:n72:+f73:u(parseInt(f,3675:if(){bcase 74:g=<<16>>16g76:u(k[])77:y=,u([y])78:g=,u(a(v,x-=g+1,g79:g=,u(k["$"+g])81:h=,[f]=h82:u([f])83:h=,k[]=h84:!085:void 086:u(v[x-1])88:h=,y=,h,y89:u({e{r(e.y,arguments,k)}e.y=f,e.x=c,e})90:null91:h93:h=0:;default:u((g<<16>>16)-16)}}n=this,t=n.Function,s=Object.keys||(e){a={},r=0;for(c in e)a[r]=c;a=r,a},b={},k={};r'.replace(/[-]/g,function(e){return l[15&e.charCodeAt(0)]})}("v[x++]=v[--x]t.charCodeAt(b++)-32function return ))++.substrvar .length(),b+=;break;case ;break}".split(""));
console.log(x);
结果如下:
function e(e,a,r){return (b[e]||(b[e]=t("x,y","return x "+e+" y")))(r,a)}function a(e,a,r){return (k[r]||(k[r]=t("x,y","return new x[y]("+Array(r+1).join(",x[++y]").substr(1)+")")))(e,a)}function r(e,a,r){var n,t,s={},b=s.d=r?r.d+1:0;for(s["$"+b]=s,t=0;t>>0;break;case 65:h=v[--x],y=v[--x],v[--x][y]=h;break;case 66:u(e(t[b++],v[--x],v[--x]));break;case 67:y=v[--x],d=v[--x],u((g=v[--x]).x===c?r(g.y,y,k):g.apply(d,y));break;case 68:u(e((g=t[b++])<"<"?(b--,f()):g+g,v[--x],v[--x]));break;case 70:u(!1);break;case 71:v[x++]=n;break;case 72:v[x++]=+f();break;case 73:u(parseInt(f(),36));break;case 75:if(v[--x]){b++;break}case 74:g=t.charCodeAt(b++)-32<<16>>16,b+=g;break;case 76:u(k[t.charCodeAt(b++)-32]);break;case 77:y=v[--x],u(v[--x][y]);break;case 78:g=t.charCodeAt(b++)-32,u(a(v,x-=g+1,g));break;case 79:g=t.charCodeAt(b++)-32,u(k["$"+g]);break;case 81:h=v[--x],v[--x][f()]=h;break;case 82:u(v[--x][f()]);break;case 83:h=v[--x],k[t.charCodeAt(b++)-32]=h;break;case 84:v[x++]=!0;break;case 85:v[x++]=void 0;break;case 86:u(v[x-1]);break;case 88:h=v[--x],y=v[--x],v[x++]=h,v[x++]=y;break;case 89:u(function (){function e(){return r(e.y,arguments,k)}return e.y=f(),e.x=c,e}());break;case 90:v[x++]=null;break;case 91:v[x++]=h;break;case 93:h=v[--x];break;case 0:return v[--x];default:u((g<<16>>16)-16)}}var n=this,t=n.Function,s=Object.keys||function (e){var a={},r=0;for(var c in e)a[r++]=c;return a.length=r,a},b={},k={};return r
到网上找个网站格式化下,会发现和网上公开的被破解出来的头条signature代码的get_as_cp_signature()其中的一部分基本一致。
在get_as_cp_signature这个函数中,我们只需要TAC.sign这一部分,所以as,cp这两个值的生成相关的可以直接删掉,简化代码。
删除生产as,cp这两个值的之后的部分就是用来生成签名的加密算法,而生成加密算法的素材就是紧跟在后面的内容了:
("v[x++]=v[--x]t.charCodeAt(b++)-32function return ))++.substrvar .length(),b+=;break;case ;break}".split("")))()('gr$Daten 袠b/s!l y蛼y墓g,(lfi~ah`{mv,-n|jqewVxp{rvmmx,&effkx[!cs"l".Pq%widthl"@q&heightl"vr*getContextx$"2d[!cs#l#,*;?|u.|uc{uq$fontl#vr(fillTextx$$榫樴笐喔犼步2<[#c}l#2q*shadowBlurl#1q-shadowOffsetXl#$$limeq+shadowColorl#vr#arcx88802[%c}l#vr&strokex[ c}l"v,)}eOmyoZB]mx[ cs!0s$l$Pb>>s!0s%yA0s"l"l!r&lengthb&l!l Bd>&+l!l &+l!l 6d>&+l!l &+ s,y=o!o!]/q"13o!l q"10o!],l 2d>& s.{s-yMo!o!]0q"13o!]*Ld>>b|s!o!l q"10o!],l!& s/yIo!o!].q"13o!],o!]*Jd>>b|&o!]+l &+ s0l-l!&l-l!i\'1z141z4b/@d
又是乱码,网上公开的get_as_cp_signature() 算法将它转成url编码了:
r(decodeURIComponent("gr%24Daten%20%D0%98b%2Fs!l%20y%CD%92y%C4%B9g%2C(lfi~ah%60%7Bmv%2C-n%7CjqewVxp%7Brvmmx%2C%26eff%7Fkx%5B!cs%22l%22.Pq%25widthl%22%40q%26heightl%22vr*getContextx%24%222d%5B!cs%23l%23%2C*%3B%3F%7Cu.%7Cuc%7Buq%24fontl%23vr(fillTextx%24%24%E9%BE%98%E0%B8%91%E0%B8%A0%EA%B2%BD2%3C%5B%23c%7Dl%232q*shadowBlurl%231q-shadowOffsetXl%23%24%24limeq%2BshadowColorl%23vr%23arcx88802%5B%25c%7Dl%23vr%26strokex%5B%20c%7Dl%22v%2C)%7DeOmyoZB%5Dmx%5B%20cs!0s%24l%24Pb%3Ck7l%20l!r%26lengthb%25%5El%241%2Bs%24j%02l%20%20s%23i%241ek1s%24gr%23tack4)zgr%23tac%24!%20%2B0o!%5B%23cj%3Fo%20%5D!l%24b%25s%22o%20%5D!l%22l%24b*b%5E0d%23%3E%3E%3Es!0s%25yA0s%22l%22l!r%26lengthb%3Ck%2Bl%22%5El%221%2Bs%22j%05l%20%20s%26l%26z0l!%24%20%2B%5B%22cs'(0l%23i'1ps9wxb%26s()%20%26%7Bs)%2Fs(gr%26Stringr%2CfromCharCodes)0s*yWl%20._b%26s%20o!%5D)l%20l%20Jb%3Ck%24.aj%3Bl%20.Tb%3Ck%24.gj%2Fl%20.%5Eb%3Ck%26i%22-4j!%1F%2B%26%20s%2ByPo!%5D%2Bs!l!l%20Hd%3E%26l!l%20Bd%3E%26%2Bl!l%20%3Cd%3E%26%2Bl!l%206d%3E%26%2Bl!l%20%26%2B%20s%2Cy%3Do!o!%5D%2Fq%2213o!l%20q%2210o!%5D%2Cl%202d%3E%26%20s.%7Bs-yMo!o!%5D0q%2213o!%5D*Ld%3Cl%204d%23%3E%3E%3Eb%7Cs!o!l%20q%2210o!%5D%2Cl!%26%20s%2FyIo!o!%5D.q%2213o!%5D%2Co!%5D*Jd%3Cl%206d%23%3E%3E%3Eb%7C%26o!%5D%2Bl%20%26%2B%20s0l-l!%26l-l!i'1z141z4b%2F%40d%3Cl%22b%7C%26%2Bl-l(l!b%5E%26%2Bl-l%26zl'g%2C)gk%7Dejo%7B%7Fcm%2C)%7Cyn~Lij~em%5B%22cl%24b%25%40d%3Cl%26zl'l%20%24%20%2B%5B%22cl%24b%25b%7C%26%2Bl-l%258d%3C%40b%7Cl!b%5E%26%2B%20q%24sign%20"), [TAC = {}]);
简单点说,签名算法一直保持不变,名字就是TAC.sign。
再继续找,找到签名,知道被签名值即入参是前面出现的url中的i,即视频id值:
var _signature = (0, TAC.sign)("6714101379172027656");
但是,遗憾的是,这样生成的签名signature值,在构造请求时,却无法有效使用,访问本文的朋友估计都是出现了这一问题,前面的是干货,但没有新意,后面的才是独家的,可以解决构造请求时,signature值却无效的问题。
signature无效,是因为签名算法TAC.sign与很多个变量有关。
03
—
签名算法解析
整个签名算法,就是一个给各种数据算sdbmhash哈希的过程。算哈希的内容不同,当然结果不同。
首先,毫无疑问,signature值与入参有关,在访问头条小视频和西瓜视频的请求中,这个入参是url中的i值,即视频id值。
其次,在访问头条小视频和西瓜视频的请求中,signature签名与一个tac值有关,在发起前文中那个url请求的页面中存在:
当然也是乱码,不用管它,直接用就行了。
再次,signature签名与还与浏览器userAgent有关,即发起请求的浏览器UA,当然,伪造手机请求得用手机上能出现的UA,这个在网上公开的那个破解出的代码头部也有出现:
navigator1 = {
userAgent: "Mozilla/5.0 (Linux; Android 8.8.2; xxxxx Build/g) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.109 Mobile Safari/537.36"
}, window = this, window.navigator = navigator1;
最后,signature签名还与第二段乱码中的一个乱码字造出的图像值有关,根据调试趟过的坑,在PC和手机上大概率这个图像是不一样的,在PC上,乱码生成的图片是这样子:
这个玩意的sdbmhash哈希,和手机上产生的报文里signature值中的哈希是不等的,我只好用蛮办法,爆破出我用来调试的手机上的哈希是:723236945,而PC上的算法里这个图像产生的哈希是:3311753357。
控制好这四个内容,就能够产生正确的signature值了,最终生成的一个26字节的signature值,根据调试过程,可以按下面分成5个部分:
j5qI5 xAY0r xK.9Bq 6f.DU 4-aiP
5a |5b |6c |5d |5e
6c与构造的乱码字图像有关
5d与浏览器UA有关
tac与所有的内容有关
视频id值与5d有关
04
—
福利
前面提到,要控制四个值,才能生成可用的signature值,其中,视频id是入参,tac值,可以直接在代码中赋值,这两个都好控制,但UA值,确是js从当前运行环境自动获取的,乱码字图像,也是通过一些js系统函数生成的,没法控制产生过程,不容易替换。
这里提供替换代码,首先是UA的替换,在get_as_cp_signature的case 77中:
case 77:
y = v[--x];
p00 = v[--x];
if("navigator"==y)
{
p = navigator1;
}
else
{
p = p00[y];
}
u(p);
// y = v[--x],
// u(v[--x][y]);
break;
其次是乱码字图像哈希值的替换,在get_as_cp_signature的case 62中:
case 62:
g = v[--x],
k[0] = 65599 * k[0] + k[1].charCodeAt(g) >>> 0;
if(k[0]==3311753357)
k[0]=723236945;
break;
这都是一步步调出来的血汗代码呀。
祝大家工作愉快。
要记得多多关注我,你给我动力,我给你想要的干货。
另外,重要的是,如果文章有价值,点击右下方“在看”,与朋友们一起分享吧↘