js逆向学习记录某真气网

项目场景:

第一次写不知道能否表达清楚

在B站上学习JS逆向,结果学到最后一个练习项目时。因为之前学习得人把人家网站搞崩了。到了我来爬取得时候,这个网站反爬已经大幅度升级了,爬取难度急据升高。

声明:JS逆向成功后我就没有继续爬了,没有攻击对方服务器,如果有什么违规地方,立马删除。(网站url:私聊吧,不然又崩了可不太好)


相关工具

语言:

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 结果不一样的解决方法_白御空的博客-CSDN博客_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))

结果

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值