项目场景:
第一次写不知道能否表达清楚
在B站上学习JS逆向,结果学到最后一个练习项目时。因为之前学习得人把人家网站搞崩了。到了我来爬取得时候,这个网站反爬已经大幅度升级了,爬取难度急据升高。
声明:JS逆向成功后我就没有继续爬了,没有攻击对方服务器,如果有什么违规地方,立马删除。
相关工具
语言:
Python 3.6.1 :: Anaconda 4.4.0 (64-bit)
node.js v14.16.0
第三方库:
requests
execjs
json
subprocess
partial
开发工具:pycharm,chrome
找到可疑点:
提示:JS逆向主要是在开发者模式中,断点调试找到加密得地方
进入开发者模式模式:
理论上这段是不用说的,但这个网站反爬技术很完善
如图该网站禁用了右键和F12
所以可以通过浏览器右上角三个点---->更多工具---->开发者工具,进入
无限debugger
进入之后就会发现自动进入到了断点调试的状态,是因为下面这据代码
(function anonymous(
) {
debugger
})
破解方法:选择"debugger"前的行号,右键弹出下图窗口。可以选择1、或者2、
选择1、要在以后输入一个false。选择2、直接就可。两者都要再点击“跳到下一个断点”
具体参照:web反调试之无限debugger饶过方案汇总 - 996station
到这里还不行,你会看到下面的情况
它的的原理是,监视页面的窗口大小,小于某个值就会显示以上内容。破解方法很简单,如下。
两种找到可疑点的方法:
第一种全局搜索fromData参数名:
chrome抓包找到对应的请求
第二种,通过按钮查看绑定的事件,一层层解析
接下来可以看Python爬虫从入门到大神到JS逆向再到APP逆向 持续更新中~_哔哩哔哩_bilibili
最后还是到这
逆向分析
从这里可以看到FromData来源与data:{hT5i4Eu2x:pwG2gyr}。pwG2gyr是由“
var pwG2gyr =pIywvl1oiy(mvpDrF875,ooISoR2Gom);”得到的。所以关键函数就是pIywvl1oiy()如下
return function(method, obj){
var appId = '2b3d1754aac69d4e84b3bb5620ba578e';
var clienttype = 'WEB';
var timestamp = new Date().getTime();
// console.log(method, obj,ObjectSort(obj),appId + method + timestamp + 'WEIXIN' + JSON.stringify(ObjectSort(obj)));
var param = {
appId: appId,
method: method,
timestamp: timestamp,
clienttype: clienttype,
object: obj,
secret: hex_md5(appId + method + timestamp + clienttype + JSON.stringify(ObjectSort(obj)))//加密处
};
param = BASE64.encrypt(JSON.stringify(param));//加密处
param = AES.encrypt(param, ackvMDije6Ku, aciiBrTMXFqu);//加密处
return param;
};
})();
可以看出影响结果地方是“hex_md5(appId + method + timestamp + clienttype + JSON.stringify(ObjectSort(obj)))“和 ”param = AES.encrypt(param, ackvMDije6Ku, aciiBrTMXFqu);“
而timestamp、clienttype是不变的,method,obj是我们自己输入的。appId,ackvMDije6Ku,aciiBrTMXFqu乍一看是写死的,其实不是。如果你多次抓包就可以发现它们三在每个文件中的值是变化的,所以必须实时获取。这处很坑,如果不注意就会落入陷阱,觉得一切都是对的但结果就是出不来。appId来自当前函数,ackvMDije6Ku,aciiBrTMXFqu出现在文件开头,如下。
你有没有发现有几个参数的名字特别怪,例如ackvMDije6Ku,pIywvl1oiy,mvpDrF875等。这是因为这些关键字,甚至包括这个文件都是被动态加密的。在这其中有一个很重要的关键字FromData的键,如下图。这个值不能写死,他又时间限制,一段时间后会失效。必须实时获取。见前文这个关键字在Data里。
总上如果我们想要成功加密,就必须动态获取以上几个参数。而在写参数都来自于那个JS文件。这个文件的url,通过左侧的全局搜索得到https://www.xxxxx.cn/js/encrypt_e6Q87gvKk99t.min.js?v=1658658361
而这个URL也是实时生成的如这e6Q87gvKk99t和v=1658658361。后面的是时间戳,而前面的是对方后台生成的,我们无法自己生成。但是学过WEB前端的都知道,js文件一般放在html文件的<script>标签中。可以通过HTML文件来找到JS文件。想到这点就可以通过全局搜索js/encrypt这就会出来几个HTML文件,随便选择一个都行。例如我选择"https://www.xxxx.cn/html/city_detail.php?v=1.10"。接下来就是通过requests发起get请求等到HTML文件,在通过xpath来获取JS文件URL,然后获取URL文件并解析想要的参数。
s = requests.session()
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36",
"Referer": "https://www.aqistudy.cn/html/city_detail.php?v=1.10",
}
get_js_value_page=s.get(url="https://www.aqistudy.cn/html/city_realtime.php?v=2.3",headers=headers).text
tree=etree.HTML(get_js_value_page)
get_js_value=tree.xpath("//script/@src")[1]
js_url="https://www.aqistudy.cn"+get_js_value.replace("..","")
response_funEnData=s.get(url=js_url,headers=headers,allow_redirects=False).text
但是得到的JS文件长下面这样,跟想要的文件大相径庭。通过网上翻阅资料,发现他是一个JS混淆的一种方式。通过eval(),是利用了eval()函数的特性,它跟Python里的eval函数差不多,能把字符串解析成代码来执行,具体见:JS逆向 | 面向小白之eval混淆 - 知乎
可以通过console.log方式破解。把"eval"替换成console.log()就可以了。如下
虽然可以但console.log()输出到控制台上。不能直接复制给变量。这里我想到了C语言的printf,他本质就是将一个字符串用sys_write()写道显示器那块显存里。log()也一定是这样,但如果直接写入到显存,那应该和之前看到的一样是eval(.......)。所以他一定在写道显存中调用了其它方法。网上搜了下它的源码,果然它调用了toString()方法。接下来就有点饶了。如下
eval("var data="+enData.replace("eval","")+".toString();");
enData就是那个字符串eval(.......),先是让"eval"消失,然后调用toString()方法。在这里会发现我是拼接了".toString();"。因为enData原本就是字符串,在调用toString()是无效的。但是如果是拼接了".toString();他们就在一个层次了。然后这里有人就要问了,字符串有什么用,又不是语句。对字符串是常量,但不代表不能执行,eval不就是把字符串解析成代码来执行吗。eval消失了又回来了。又发现我在前面拼接了"var data="提前把他赋值了。因为里面这个东西有点奇怪,如果你不赋值给其它变量,他会先执行自己然后就报错,不会执行toString()。我也不知道为啥。然后就可以得到源码了吗???????
当然不是,之后多次请求发现toString()后并没有得到源码,而是下面两种情况。
然后全局搜索dswejwehxt()发现是
//将加密函数进行解密,此处DeFun是我自己起的,原来叫做dswejwehxt
function DeFun(enData) {
var b = new Base64();
return b.decode(enData);
}
这个函数就是把加密的函数进行解密。也就是要提取(我是用正则提取的)中间引号里的字符串,传给DeFun()之后会有概率得到源码,但大多数情况是
乍一看和之前的一样,好像是被toString()解密了,然后又被DeFun()加密了。到这里我也很崩溃,以为错了。不想放弃,因为明明有成功得到源码的情况。之后又重复了上述行为,就真的得到源码了。因为文件是对方动态生成的,所以得到源码的路径又多条,如下
function getDeData(enData){
//原始的--》toString-->dswejwehxtdswejwehxt-->DeFun(DeFun-->fun-->toString-->dswejwehxt--DeFun-->finalDefun
eval("var data="+enData.replace("eval","")+".toString();");
var myRe=/'(.*?)'/g;
//dswejwehxtdswejwehxt
var count=data.split("(").length;
switch (count){
case 3: //dswejwehxt
data=data.match(myRe)[0];
//DeFun
data=DeFun(data);
if(!data.includes("eval"))
break;
eval(" data="+data.replace("eval","")+".toString();");
break;
case 4:
//只有eval中才用要解密
if(data.includes("eval")) {
data = data.match(myRe)[0];
//DeFun(DeFun
data = DeFun(DeFun(data));
if(!data.includes("eval"))
break;
//fun
eval(" data=" + data.replace("eval", "") + ".toString();");
//dswejwehxt
//只有eval中才用要解密的字符串
if (data.includes("eval")) {
data = data.match(myRe)[0];
//DeFun
data = DeFun(data);
}
}
break;
}
return data;
由此就得到了/js/encrypt文件源码,之后在通过正则匹配到那几个动态加载的数据,如下
function getEncryptData(data){
data=getDeData(data);
var encryptData={
param:"",
appId:"",
AESList:[]
};
//获得AES
var myRe=/"(.*?)"/g;
encryptData.AESList=data.match(myRe);
//获得fromdata
myRe=/data:\s?\{(.*?):/g;
data.match(myRe);
encryptData.param=RegExp.$1;
myRe=/appId\s?=\s?'(.*?)'/g;
data.match(myRe);
encryptData.appId=RegExp.$1;
encryptData=JSON.stringify(encryptData)
return encryptData;
}
然后在python文件中,调用js。但不可以通过execjs这个库。因为会莫名其妙的报错,而且错误还不唯一,错误原因也很空,但在node里可以正常执行。我是用以下subprocess 调用node执行JS文件
具体参照:execjs 运行结果和 nodejs 结果不一样的解决方法_白御空的博客
如何将 python3 中 os.popen()的默认编码由 ascii 修改为 ‘utf-8’ ? - 知乎
var arguments = process.argv; //JS
var args=arguments.splice(2);
if(args.length!=0){
if(args[0]=="getEncryptData")
console.log(getEncryptData(args[1]));
}
#python
process=Popen(["node","./jiemi.js","getEncryptData",response_funEnData],stdout=PIPE,stderr=PIPE)
stdout,stderr=process.communicate()
encryptData=stdout.decode("utf-8")
#encryptData时js传来的数据,json格式
encryptData_json=json.loads(encryptData)
param=encryptData_json["param"]
appId=encryptData_json["appId"]
aesList=[]
for i in encryptData_json["AESList"]:
#的到的数据是""data"",需要去除一对引号
aesList.append(eval(i))
总上,动态实时变化的参数已经全部获取了。接下来就是fromdata进行加密了,就是复刻对方代码,如下
function getFromData(city,method,startTime,endTime,type,id,ack,aci) {
ackvMDije6Ku =ack;//AESkey,可自定义
aciiBrTMXFqu =aci;
//我想要修改全局变量但把全局变量和形参得名称相同了,所以并没有修改appId全局
appId=id;
var param = {};
param.city = city;
param.type = type;
//type :HOUR
param.startTime = startTime;//2022-07-20 15:00:00
param.endTime = endTime;//2022-07-21 16:00:00
var fromData = pz0QXkl8bZn(method, param);
return fromData;
}
var pz0QXkl8bZn = (function(){
return function(method, obj){
// var appId = '2b3d1754aac69d4e84b3bb5620ba578e';
var clienttype = 'WEB';
var timestamp = new Date().getTime();
// console.log(method, obj,ObjectSort(obj),appId + method + timestamp + 'WEIXIN' + JSON.stringify(ObjectSort(obj)));
var param = {
appId: appId,
method: method,
timestamp: timestamp,
clienttype: clienttype,
object: obj,
secret: hex_md5(appId + method + timestamp + clienttype + JSON.stringify(ObjectSort(obj)))
};
param = BASE64.encrypt(JSON.stringify(param));
// param = AES.encrypt(param, ackvMDije6Ku, aciiBrTMXFqu); //之句话好像有和没有都可以
return param;
};
})();
之后就是请求服务器,获取响应数据了,如下
ctx=execjs.compile(open("jiemi.js",encoding="utf-8").read())
funName='getFromData("{}","{}","{}","{}","{}","{}","{}","{}")'.format("南京","GETCITYWEATHER","2022-07-13 15:00:00","2022-07-23 16:00:00","HOUR",appId,aesList[6],aesList[7])
fromData=ctx.eval(funName)
#调用js,获得fromdata相关参数
url="https://www.aqistudy.cn/apinew/aqistudyapi.php"
data={
param: fromData}
page_text=requests.post(url=url,headers=headers,data=data).text
这样就得到了加密后的响应数据,通过抓包知道是
再跟进,发现函数也在/js/encrypt文件。但还是跟之前一样有动态变化的参数,不过好在之前都获取了。重新赋值以下就可了,如下。
function getFinalData(enData,a,b,d,f,h,x,o,p) {
ask7U5FgBhMS=a;
asioJThvmqnh=b;
ackvMDije6Ku=d;
aciiBrTMXFqu=f;
dskcLK6StPtw=h;
dsi6hLAy1Fd4=x;
dckCXFfq0HiS=o;
dciK2RcdgCIs=p;
return dhGrEIFMzqtu7te9N1pka9(enData);//这里的函数其实就是上面那个图片的dbcvv.....函数,只是名字不一样,毕竟是实时变化的吗。
}
最后python里调用以下这个函数就成功的道解密后的数据了,如下。
funName="getFinalData('{}','{}','{}','{}','{}','{}','{}','{}','{}')".format(page_text,aesList[0],aesList[1],aesList[2],aesList[3],aesList[4],aesList[5],aesList[6],aesList[7])
print(ctx.eval(funName))