近日出于学习的目的对某Boss网站的反爬机制进行了分析和逆向,终于完全搞定了,记录总结下方便日后学习!
本代码请仅用于 纯技术研究的 用途,请勿用于商业用途或 非法用途,如果因使用者非法使用造成的法律问题与本作者无关!
核心js加密文件进行了全逆向,爬取方案不需要浏览器参与,纯 python+js 即可!
本爬取方案最终通过Python实现了完全自动化爬取职位信息列表页,并存储到一个txt文件中(需要的可自行扩展数据库实现),为了简单起见未使用多线程,使用的技术有:
某Boss网站在访问页面时都需要携带一个 __zp_stoken__
=xxxxxx 这样的cookie
每次访问页面都会更新这个__zp_stoken__
,(另外,每次__zp_stoken__
其实可以使用数次(貌似<5次),具体次数没有试验)
__zp_sseed__
、 __zp_sname__
、__zp_snts__
三个cookie (通过查看response 的 set-cookie部分)__zp_stoken__
存入cookie中如果不携带cookie __zp_stoken__
或者__zp_stoken__
无效,就会返回一个302跳转
302跳转Location 为 https://www.zhipin.com/web/common/security-check.html?seed=r9L%2BjsS5vloxpthkin%2F2Yskecz1G198Nk%2B4RCkA2YiA%3D&name=d05572cd&ts=1587092342543&callbackUrl=%2Fc101280600%2F%3Fquery%3Dpython%26page%3D2%26ka%3Dpage-2&srcReferer=
Location 中seed、name、ts为关键参数,用于计算 __zp_stoken__
这个页面中的js所做的工作就是为
__zp_stoken__
var getQueryString = function(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
};
var seed = decodeURIComponent(getQueryString("seed")) || "";
var ts = getQueryString("ts");
var fileName = getQueryString("name");
var expiredate = new Date().getTime() + 32 * 60 * 60 * 1000 * 2;
var code = "";
var ABC = window.ABC || frame.contentWindow.ABC;
try {
//code = new ABC().z(seed, parseInt(ts)+(480+new Date().getTimezoneOffset())*60*1000);
code = new ABC().z(seed, parseInt(ts));//+(480+new Date().getTimezoneOffset())*60*1000 时区相对于北京的偏移
} catch (e) {}
if (code ) Cookie.set("__zp_stoken__", code, expiredate, COOKIE_DOMAIN, "/"); //该方法会做一次 encodeURIComponent
对于2或3 拿到seed、name、ts后的流程基本一样,都是调用new ABC().prototype.z(seed,ts)函数,生成
__zp_stoken__
并做一次 encodeURIComponent 后存放到cookie中。
__zp_stoken__
通过name指定的加密js 计算出来 cookie值,由客户端基于服务器提供seed和ts来生成,每次发送请求需携带有效的 __zp_stoken__
__zp_stoken__
,是难得一见的具备可读性的变量__zp_stoken__
的入口爬取方案的思路为
__zp_stoken__
__zp_stoken__
爬取指定列表页数据# 对指定的页面进行遍历翻页,将结果写入指定文件
from XBossSpider import bossSpider,extractJobInfo
#import XBossSpider
import importlib
importlib.reload(XBossSpider)
provider={}
path="C:/Users/jam/Desktop/temp/tempdata/" # 需要确保jsbeautify.js在该目录下,中间过程文件、最终文件都在该目录下
for i in range(1):
url="https://www.zhipin.com/c101280600/?query=python&page="+str(i)+"&ka=page-"+str(i)
text=bossSpider(url,path,provider)
result=extractJobInfo(text)
if(not result or len(result)<10 or result.isspace()):
print("请检查IP是否已被封,需进行滑块验证!")
break
resultFileName=path+"result.txt"
with open(resultFileName,'a',encoding='UTF-8') as f:
f.write(result)
以下说明过程都以temp.js为例,temp.js 是 对3d0a2109.js进行最简化处理后的核心代码
在调试时会频繁出现debugger断点,这是js中进行断点投毒,其中有定时任务频繁执行,无敌循环debugger,严重影响本地调试。
需要对temp.js 中全局变量 _0x1c69[‘0x3af’-0x0]的值进行替换 (全局变量名称会根据js的不同而不同)
只需要将js中的固定字符串 YkZGNWFtTjJhWHhrWTFGU0lIQnJkR2NnZENBeUlHTjRiQ0E1ZEdsNFVWSXdJSGQ0ZFdwNmVuaG5NQ0JuZUdscVoyTWdZM2hzSURsMGFYaFJVaUJXSUhRZ015QmFXVmt3Y2xGU1VnPT0=
替换为YkZGNWFtTjJhWHhrWTFGU0lIQnJkR2NnZENBeUlHTjRiQ0E1ZEdsNFVWSXdJQ0FnSUNBZ0lDQWdNQ0JuZUdscVoyTWdZM2hzSURsMGFYaFJVaUJXSUhRZ015QmFXVmt3Y2xGU1VnPT0=
常量池中原始字符串
_0x1c69['0x3af'-0x0]="YkZGNWFtTjJhWHhrWTFGU0lIQnJkR2NnZENBeUlHTjRiQ0E1ZEdsNFVWSXdJQ0FnSUNBZ0lDQWdNQ0JuZUdscVoyTWdZM2hzSURsMGFYaFJVaUJXSUhRZ015QmFXVmt3Y2xGU1VnPT0=";
经过一次base64接码后
_0x25ec['mdGvrp']['0x3af'-0x0]="bFF5amN2aXxkY1FSIHBrdGcgdCAyIGN4bCA5dGl4UVIwICAgICAgICAgMCBneGlqZ2MgY3hsIDl0aXhRUiBWIHQgMyBaWVkwclFSUg==";
这个比较特殊还需要再经过一次解密才能得到明文,也就是在调试时看到那个拼命无敌debugger
(function() {var a = new Date(); debugger; return new Date() - a > 100;}())
为了优雅的搞掉他,我对解密函数进行了你想,写了加密函数encry,然后将明文中的debugger替换成空格,则给base64编码得到无毒的字符串常量用于替换
btoa(btoa(encry("(function() {var a = new Date(); ; return new Date() - a > 100;}())")));//去掉debugger 注意要两次base64加密
//YkZGNWFtTjJhWHhrWTFGU0lIQnJkR2NnZENBeUlHTjRiQ0E1ZEdsNFVWSXdJQ0FnSUNBZ0lDQWdNQ0JuZUdscVoyTWdZM2hzSURsMGFYaFJVaUJXSUhRZ015QmFXVmt3Y2xGU1VnPT0=
本地调试时只需进行这一步替换就能解决到debugger问题了。
域名投毒处理
temp.js的 1572行,涉及域名投毒,本地调试或执行js时获取域名是不对的,而这个域名是需要参与最终cookie计算的,所以需要处理,处理方式比较暴力简单,强制写成正确的域名即可,把其余代码注释掉。
case '4': _0x476e10 ="https://www.zhipin.com/";
//_0x476e10 = ((typeof top) == ( _0x261f6f[_0x40b4('0x345')])) && ((typeof top[_0x40b4('0x199')]) == ( _0x261f6f[_0x40b4('0x345')])) && ((/\w+:\/\/(.*?)\//[_0x40b4('0x37c')](top[_0x40b4('0x199')][_0x40b4('0x8b')])) != ( null)) ? /\w+:\/\/(.*?)\//[_0x40b4('0x37c')](top[_0x40b4('0x199')][_0x40b4('0x8b')])[0x0] : _0x261f6f[_0x40b4('0x3ab')];
在js中会对系统环境进行多种判断,如果识别为非正常浏览就会投毒!
这些判断包括windows、document、global、title、setInterval等,为了可读性我进行了操作符函数替换后再做的如下处理(其实可以不用进行函数替换的),主要投毒位置记录如下,具体代码处理时的策略和下方标注略有差异。
74行判断条件需要保证为true 或者 干掉投毒函数setInterval_ =false,ss 第9位置1
注释79-84行
Date[ptt][gm] = function() { //投毒
修改Data.prototype.getMonth()函数固定返回12
return ((((0x1) << ( 0x3))) | ( 0x4));
}
;
setInterval_ = ![];
ss = ((ss) | ( ((0x1) << ( 0x8))));
以上代码全部注释掉
214行判断global process 等对象,不存在则投毒 ss 0xc+1置1
注释215行
原始代码
if (((typeof global) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof process) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof child_process) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof Buffer) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof sessionStorage) == ( _0x2629ad[_0x2b8b('0x173')]))) {//干掉或注释下面的内容
ss = ((ss) | ( ((0x1) << ( 0xc))));
}
解析后
if (((typeof global) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof process) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof child_process) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof Buffer) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof sessionStorage) == ( _0x2629ad[_0x2b8b('0x173')]))) {
ss = ((ss) | ( ((0x1) << ( 0xc))));
}
284-287注释掉 ss = ss | 31;//1-5位置1
284行判断浏览器类型 确保执行 ss = ss | (0x1 << 0x1) 倒数第2位置1,否则0xf+1位投毒置1
原始代码
((typeof window) == ( _0x2c63cc[_0x2b8b('0x395')])) && ((typeof window[_0x2b8b('0xf4')]) == ( _0x2c63cc[_0x2b8b('0x395')])) ? ((typeof window[_0x2b8b('0xf4')][_0x2b8b('0xac')]) == ( _0x2c63cc[_0x2b8b('0x11c')])) ? /phantomjs/[_0x2b8b('0x32d')](window[_0x2b8b('0xf4')][_0x2b8b('0xac')][_0x2b8b('0x407')]()) ? ss = ((ss) | ( ((0x1) << ( 0xf)))) : window[_0x2b8b('0xf4')][_0x2b8b('0x1a8')] ? ss = ((ss) | ( ((0x1) << ( 0xf)))) : ss = ((ss) | ( ((0x1) << ( 0x1)))) : ((typeof window[_0x2b8b('0xf4')][_0x2b8b('0x8')]) == ( _0x2c63cc[_0x2b8b('0x116')])) ? ss = ((ss) | ( ((0x1) << ( 0xf)))) : !window[_0x2b8b('0xf4')][_0x2b8b('0x8')] ? ss = ((ss) | ( ((0x1) << ( 0xf)))) : ss = ((ss) | ( ((0x1) << ( 0xf)))) : ss = ((ss) | ( ((0x1) << ( 0xf))));
解析后代码
((typeof window) == ( "object")) && ((typeof window["navigator"]) == ( "object")) ?
((typeof window["navigator"]["userAgent"]) == ( "string")) ?
/phantomjs/["test"](window["navigator"]["userAgent"]["toLowerCase"]()) ?
ss = ((ss) | ( ((0x1) << ( 0xf))))
: window["navigator"]["webdriver"] ? ss = ((ss) | ( ((0x1) << ( 0xf)))) : ss = ((ss) | ( ((0x1) << ( 0x1))))
:((typeof window["navigator"]["appVersion"]) == ( "undefined")) ?
ss = ((ss) | ( ((0x1) << ( 0xf))))
: !window["navigator"]["appVersion"] ? ss = ((ss) | ( ((0x1) << ( 0xf)))) : ss = ((ss) | ( ((0x1) << ( 0xf))))
: ss = ((ss) | ( ((0x1) << ( 0xf))));
285行判断屏幕 确保执行 ss = ((ss) | ( ((0x1) << ( 0x2)))) 第3位置1,否则0x10+1位投毒置1
原始代码
((typeof window) == ( _0x2c63cc[_0x2b8b('0x395')])) && ((typeof window[_0x2b8b('0x30c')]) == ( _0x2c63cc[_0x2b8b('0x395')])) ? ((((window[_0x2b8b('0x30c')][_0x2b8b('0x426')]) + ( window[_0x2b8b('0x30c')][_0x2b8b('0x124')]))) > ( 0x0)) ? ss = ((ss) | ( ((0x1) << ( 0x2)))) : ss = ((ss) | ( ((0x1) << ( 0x10)))) : ss = ((ss) | ( ((0x1) << ( 0x10))));
解析后代码
((typeof window) == ( "object")) && ((typeof window["screen"]) == ( "object")) ?
((((window["screen"]["availHeight"]) + ( window["screen"]["availWidth"]))) > ( 0x0)) ?
ss = ((ss) | ( ((0x1) << ( 0x2))))
: ss = ((ss) | ( ((0x1) << ( 0x10))))
: ss = ((ss) | ( ((0x1) << ( 0x10))));
286行判断getElementById 确保执行 ss = ((ss) | ( ((0x1) << ( 0x3)))) 第4位置1,否则0x11+1位投毒置1
原始代码
((typeof document) == ( _0x2c63cc[_0x2b8b('0x395')])) && ((typeof document[_0x2b8b('0x3dd')]) == ( fff)) ? !document[_0x2b8b('0x3dd')](_0x2c63cc[_0x2b8b('0x367')]) ? ss = ((ss) | ( ((0x1) << ( 0x3)))) : ss = ((ss) | ( ((0x1) << ( 0x11)))) : ss = ((ss) | ( ((0x1) << ( 0x11))));
解析后代码
((typeof document) == ( "object")) && ((typeof document["getElementById"]) == ( fff)) ?
!document["getElementById"]("glcanvaxs") ?
ss = ((ss) | ( ((0x1) << ( 0x3))))
: ss = ((ss) | ( ((0x1) << ( 0x11))))
: ss = ((ss) | ( ((0x1) << ( 0x11))));
287行判断phantom 确保执行ss = ((ss) | ( ((0x1) << ( 0x4)))) 第5位置1,否则0x12+1位投毒置1
原始代码
((typeof window) == ( _0x2c63cc[_0x2b8b('0x395')])) ? ((typeof window[_0x2b8b('0x55')]) == ( _0x2c63cc[_0x2b8b('0x116')])) ? ss = ((ss) | ( ((0x1) << ( 0x4)))) : ss = ((ss) | ( ((0x1) << ( 0x12)))) : ((typeof window) == ( _0x2c63cc[_0x2b8b('0x395')])) && ((typeof window[_0x2b8b('0x34b')]) == ( _0x2c63cc[_0x2b8b('0x116')])) ? ss = ((ss) | ( ((0x1) << ( 0x12)))) : ss = ((ss) | ( ((0x1) << ( 0x12))));
解析后代码
((typeof window) == ( "object")) ?
((typeof window["callPhantom"]) == ( "undefined")) ?
ss = ((ss) | ( ((0x1) << ( 0x4))))
: ss = ((ss) | ( ((0x1) << ( 0x12))))
: ((typeof window) == ( "object")) && ((typeof window["_phantom"]) == ( "undefined")) ?
ss = ((ss) | ( ((0x1) << ( 0x12))))
: ss = ((ss) | ( ((0x1) << ( 0x12))));
206 207注释掉,前面插入 ss = ss|0x1;
原始代码
((typeof window) != ( _0x1e3f0e[_0x2b8b('0x241')])) && ((this) == ( window)) ? ss = ((ss) | ( 0x1)) : (ss = ((ss) | ( ((0x1) << ( 0xb)))),
_0x327c14 = _0x2ce1dc());
解析后代码
((typeof window) != "undefined") && ((this) == ( window)) ?
ss = ((ss) | ( 0x1))
: (ss = ((ss) | ( ((0x1) << ( 0xb)))),_0x327c14 = _0x2ce1dc());
297 298 300 275行代码判断横屏竖屏及调试 outer-inner如果<=160 为false ,否则为true window不存在也是true 好像没啥影响
定时触发
为避免麻烦,两个都确保为true,进入300行if判断的else分支 var _0x5c72b8 =true;//
var _0x6c7431 =true;//
if分支会将ss的0x13+1位置1 100010000000000000011111
var _0x5c72b8 = ((typeof window) == ( _0x5ecff7[_0x2b8b('0x1e9')])) ? ((((window[_0x2b8b('0x392')]) - ( window[_0x2b8b('0x1cb')]))) > ( _0x59c9ca)) : ![];
var _0x6c7431 = ((typeof window) == ( _0x5ecff7[_0x2b8b('0x1e9')])) ? ((((window[_0x2b8b('0x306')]) - ( window[_0x2b8b('0x19e')]))) > ( _0x59c9ca)) : ![];
var _0x4f3ce8 = _0x5c72b8 ? _0x5ecff7[_0x2b8b('0x21f')] : _0x5ecff7[_0x2b8b('0x21d')]; //width差值大于160为vertical,否则horizontal
if (!((_0x6c7431) && ( _0x5c72b8)) && (((typeof window) == ( _0x5ecff7[_0x2b8b('0x1e9')])) && ((typeof window[_0x2b8b('0x157')]) == ( _0x5ecff7[_0x2b8b('0x1e9')])) && ((typeof window[_0x2b8b('0x157')][_0x2b8b('0x2c8')]) == ( _0x5ecff7[_0x2b8b('0x1e9')])) && window[_0x2b8b('0x157')][_0x2b8b('0x2c8')][_0x2b8b('0x1dd')] || _0x5c72b8 || _0x6c7431))
解析后为
var _0x5c72b8 = ((typeof window) == ( "object")) ? ((((window["outerWidth"]) - ( window['innerWidth'))) > ( _0x59c9ca)) : ![];
var _0x6c7431 = ((typeof window) == ( "object")) ? ((((window["outerHeight"]) - ( window['innerHeight']))) > ( _0x59c9ca)) : ![]; if (! var _0x4f3ce8 = _0x5c72b8 ? _0x5ecff7[_0x2b8b('0x21f')] : _0x5ecff7[_0x2b8b('0x21d')]; //width差值大于160为vertical,否则horizontal
if (!((_0x6c7431) && ( _0x5c72b8))
&& (((typeof window) == ("object"))
&& ((typeof window["Firebug"]) == ("object"))
&& ((typeof window["Firebug"]["chrome"]) == ("object"))
&& window["Firebug"]["chrome"]["isInitialized"] || _0x5c72b8 || _0x6c7431))
如果是调试状态会调用事件发布,执行275处的相关window判断, 确保不执行 0x1) << ( 0xd)
((typeof window) != ( _0x5b9200[_0x2b8b('0x361')])) && ((typeof window[_0x2b8b('0x327')]) == ( fff)) && ((typeof CustomEvent) == ( fff)) ? window[_0x2b8b('0x327')](new CustomEvent(_0x5b9200[_0x2b8b('0xe0')],_0x4fa1b3)) : ss = ((ss) | ( ((0x1) << ( 0xd))));
解析后
((typeof window) != "undefined") && ((typeof window["dispatchEvent"]) == ("function")) && ((typeof CustomEvent) == ( "function")) ?
window["dispatchEvent"](new CustomEvent("bcr_change",_0x4fa1b3))
: ss = ((ss) | ( ((0x1) << ( 0xd))));
接下来的方法都是调用main()之后触发
349行 给ts,seed赋值 确保三元运算符为false,或者截取: 前的代码
(_0x327c14 = _0x33b55a,_0x46dced = _0x3220f9); 其他注释掉
((typeof document) == ( _0x15044d[_0x2b8b('0x1f5')])) && ((typeof document[_0x2b8b('0x1b3')]) != ( _0x15044d[_0x2b8b('0x1fc')])) ? _0x327c14 = _0x2ce1dc() : (_0x327c14 = _0x33b55a,
_0x46dced = _0x3220f9);
解析后为
((typeof document) == "object") && ((typeof document["title"]) != ( "string")) ?
_0x327c14 = _0x2ce1dc() : //生成一个随机字符串,44位,最后一位为=,下毒
(_0x327c14 = _0x33b55a, _0x46dced = _0x3220f9);//第一个赋值seed,第二个赋值ts
1739行 判断moveBy 确保执行((ss) | ( ((0x1) << ( 0x5)))) 第6位置1,否则0x14+1位投毒置1,并debugger随机生成字符串
ss = ((ss) | ( ((0x1) << ( 0x5)))); 其余注释掉
((typeof window) == ( _0x578d43[_0x2b8b('0x3f3')])) && /function|object/[_0x2b8b('0x32d')](typeof window[_0x2b8b('0x152')]) ? ss = ((ss) | ( ((0x1) << ( 0x5)))) : (ss = ((ss) | ( ((0x1) << ( 0x14)))),
_0x327c14 = _0x2ce1dc());
解析后为
((typeof window) == "object") && /function|object/["test"](typeof window["moveBy"]) ?
ss = ((ss) | ( ((0x1) << ( 0x5))))
: (ss = ((ss) | ( ((0x1) << ( 0x14)))),_0x327c14 = _0x2ce1dc());
1407行 判断window.open是函数 确保执行 ss| (0x1<< 0x6) 第7位置1,否则0x15+1投毒置1,并将seed置为随机字符串
ss=ss| (0x1<< 0x6); 其余注释掉 两行
下面的特殊字符影响markdown显示 xxxx 删掉即可
!!(((typeof window) == ( _0x578d43[_0x2b8b('0x3f3')])) && (((window[_0x2b8b('0x412')]) + ( ''))[_0x2b8b('0x70')]()[_0x2b8b('0x386')](/function\s*([^(]*)\xxxx(/)!=null)) ? ss = ((ss) | ( ((0x1) << ( 0x6)))) : (_0x327c14 = _0x2ce1dc(),
ss = ((ss) | ( ((0x1) << ( 0x15)))));
解析后为
!!((typeof window == "object") && ((window["open"]+ '')["toString"]()["match"](/function\s*([^(]*)\xxxx(/)!=null)) ?
ss = (ss| ( (0x1<< 0x6)))
: (_0x327c14 = _0x2ce1dc(),ss = (ss| ( 0x1 << 0x15)));
1699行 判断document.createElement可用 确保执行ss = ((ss) | ( ((0x1) << ( 0x7)))) 第8位置1,否则0x17投毒置1
ss = ((ss) | ( ((0x1) << ( 0x7)))) ; 注释掉其余的
((typeof document) == ( _0x578d43[_0x2b8b('0x3f3')])) && ((typeof document[_0x2b8b('0x391')]) == ( fff)) && ((typeof (caption = document[_0x2b8b('0x391')](_0x578d43[_0x2b8b('0x2e8')]))) == ( _0x578d43[_0x2b8b('0x3f3')])) && ((caption[_0x2b8b('0x24f')]) == ( _0x578d43[_0x2b8b('0x302')])) ? ss = ((ss) | ( ((0x1) << ( 0x7)))) : ss = ((ss) | ( ((0x1) << ( 0x16))));
解析后为
((typeof document) == ("object")) && ((typeof document["createElement"]) == ( fff)) && ((typeof (caption = document["createElement"]("caption"))) == ( "object")) && ((caption["tagName"]) == ( "CAPTION")) ?
ss = ((ss) | ( ((0x1) << ( 0x7))))
: ss = ((ss) | ( ((0x1) << ( 0x16))));
1543行 判断window[“moveTo”] 确保执行 ss = ((ss) | ( ((0x1) << ( 0x0)))),第0位置1,否则投毒0xe+1置1
ss = ((ss) | ( ((0x1) << ( 0x0))))
!!(((typeof window) == ( _0x578d43[_0x2b8b('0x3f3')])) && window[_0x2b8b('0x137')]) ? ss = ((ss) | ( ((0x1) << ( 0x0)))) : (_0x327c14 = _0x2ce1dc(),
ss = ((ss) | ( ((0x1) << ( 0xe)))));
解析后为
!!(((typeof window) == "object") && window["moveTo"]) ?
ss = ((ss) | ( ((0x1) << ( 0x0))))
: (_0x327c14 = _0x2ce1dc(), ss = ((ss) | ( ((0x1) << ( 0xe)))));
88行判断setInterval需要处理,否则官方定义会投毒 Date.getMonth
强制进入if分支,定义setIntervalnew为空函数
if(true){//if (typeof setInterval == _0x2b8b('0x2ed') && setInterval[_0x2b8b('0x70')]()[_0x2b8b('0x1db')](/\s+/g, '') == _0x2b8b('0x2d3') || typeof setInterval == _0x2b8b('0x28')) {//setInterval未被替换
setIntervalnew=function(a,b){};//setIntervalnew = setInterval;
}
第20行有一处 window 不用处理
处理windows.atob()
函数,如果系统不存在该函数 官方代码自定义了算法,所以无需处理
到这里投毒类就都处理完成了
通过逆向,发现js的核心流程就是一个AES加密算法。
加密模式ECB,填充方式pkcs5padding,数据块128位,密码4e227197bf508bab,偏移量4e227197bf508bab,输出base64,字符集gb2312
生成秘钥
No. 1.1 将 zz5IntArray_0x4c34d0 的前17位赋值,初始化后对应字符串 ‘4e227197bf508bab’ 第17位对应整数0,对应原始代码5890行左右,关键字[0xf],下断点,即可得到秘钥
生成要加密的原文
No. 1.2 plainText_0x5bd912 为seed+’@’+“https://www.zhipin.com/@1587351053@8388833”+’@9a04’转成的整型数组追加 4个4 和 1个0 凑够97位
“hQGTnOz8GDIwnLC+cm6m7eUC65EOVoF3GtqnngpvfN8=”+’@’+“https://www.zhipin.com/@1587351053@8388833”+’@9a04’
生成每次轮函数要是用的轮密钥
No. 1.3 调用函数根据 zz5IntArray_0x4c34d0 生成复杂对象 complex2Array_0x1a6b6f 包含二维数组
complex2Array_0x1a6b6f 中a开头的属性为数组结构,i开头的元素为整数
将明文分组为6组,每组128位,循环对每一组进行AES加密
No. 1.4 循环调用函数生成 zz97IntArray_0x14daee, 值来源于plainText_0x5bd912每次取16个进行处理,循环6次,complex2Array_0x1a6b6f 也参与计算
最后一轮的加密结果进行扩展生成128位密文整数数组
No. 1.5 调用函数生成 zz5IntArray_0x4c34d0,值来源于 zz97IntArray_0x14daee 97位数组用于扩展生成128位zz整数数组 3变4
将密文按照ASCII码转成字符,并增加四个字符的前缀,也就最终cookie前身(再做一次encodeURIComponent就是 __zp_stoken__
了)
No. 1.6 生成zz也就是token值 前四位固定,后面由zz5IntArray_0x4c34d0按照ASCII码转变而来
前四位获取位置,原始代码约789行 关键词zz = zz = _0x1ddb83[_0x2241(‘0x2d0’)];; 756行_0x5c82f4[_0x2241(‘0x2d0’)] = _0x2241(‘0xf’);
其实完全可以通过 js 获取固定的秘钥 和 固定的四字符前缀(js不同 秘钥、前缀也不同),然后在Python中使用标准AES算法直接计算__zp_stoken__
值。有兴趣的同学可去实现!
通过官方js解析出秘钥和前缀 秘钥关键词[0xf] 大部分是常量赋值,但有一部分是通过date计算,前缀关键词zz = 通过一次函数替换由常量池获取,注意常量池有偏移量
自己通过AES加密获取
__zp_stoken__
明文 seed+’@’+“https://www.zhipin.com/@+ts/1000+@8388833”+@+四位前缀
token=encodeURICompent(四位前缀+生成的密文)
在逆向中存在以下几点疑问,知道的同学还望不吝赐教,我感激不尽!
标准AES算法中在进行字节代换时使用的是一个S盒,通过8位二进制的高、低四位形成行、列坐标,然后在S盒中获取替代值,只有一个S盒而且S盒中的值都是小于<256的,但在temp.js中存在四个S盒,而且S盒中存放的是32位数(有正有负数),为什么会有四个S盒?
在标准AES算法中在进行列混淆时,是通过一个常量矩阵相乘进行混淆的,而在temp.js中没看到常量矩阵的影子或者它使用的是全1的常量矩阵,这与标准的AES算法也对应不上,不知道这个常量矩阵和上面的四个S盒做了何种融合?(因为最终的计算结果和标准AES加密一致)
根据加密的调用次数不同,最终生成的__zp_stoken__
也不完全相同,但都可以使用
__zp_stoken__
值,后续调用基本都是这个 __zp_stoken__
__zp_stoken__
值__zp_stoken__
都可以正常使用