【JS逆向系列】某空气质量监测平台无限 debugger 与 python算法还原

  • 1.前置阅读
  • 2.过反调试
  • 3.js分析
  • 4.代码逻辑改写

1.前置阅读

样品地址:aHR0cHM6Ly93d3cuYXFpc3R1ZHkuY24v
本篇文章可能会省略某些过程,直接使用下面文章的结果,所以建议先阅读下面的文章。本篇文章也有与其他不一样的处理方法,以及单纯使用python来还原数据的加密和解密。
1.某空气质量监测平台无限 debugger 以及数据动态加密分析
2.Python 爬虫进阶必备 - 以 aqistudy 为例的无限 debugger 反调试绕过演示(附视频)

2.过反调试

先打开f12后,打开网页,直接遇到debugger

此时通过调用堆栈查找最上层


点击后跳转到主页源代码


debugger是出现在 txsdefwsw函数里面,如果有办法可以使得这个函数不执行,那是不是就相当于不会出现debugger了。

这里参考了志远哥的fd插件里面,hook注入功能的想法。例如在网页的最前面加入自己的代码,或者修改源代码中的内容,就可以达到某些想要的效果。这里我是用的是mitmproxy这个软件

首先进行安装,直接使用pip

pip install mitmproxy

安装完成后编写一个处理响应的脚本,并命名为main.py

import mitmproxy.http
import reprint('脚本初始化成功')def request(flow: mitmproxy.http.HTTPFlow):passdef response(flow: mitmproxy.http.HTTPFlow):if 'https://www.aqistudy.cn/' == flow.request.url:html = flow.response.texthtml = html.replace('txsdefwsw();', '// txsdefwsw();')flow.response.text = html

然后在命令行启动mitmproxy

mitmdump -q -p 8888 -s main.py

其中 -q 表示静默运行。-p表示监听的端口,-s 表示处理的python脚本

然后打开【网络和internet】设置,去到代理

打开代理服务器开关,地址填写【127.0.0.1】,端口填写刚刚上面的【8888】,然后点击保存。这是再次打开网页

此时可以看到txsdefwsw函数已经被注释了,自然就没有出现debugger了。但是出现了其他的反调试情况。上面的【document.write(‘检测到非法调试, 请关闭调试终端后刷新本页面重试!’);】重写了页面,为了不让它直接,在前面加上return

def response(flow: mitmproxy.http.HTTPFlow):if 'https://www.aqistudy.cn/' == flow.request.url:html = flow.response.texthtml = html.replace('txsdefwsw();', '// txsdefwsw();')html = html.replace("document.write('检测到非法调试, 请关闭调试终端后刷新本页面重试!');","return; document.write('检测到非法调试, 请关闭调试终端后刷新本页面重试!');")flow.response.text = html

再次刷新网页


可以看到最外层的页面没有被重写,说明这里的反调试已经过了。但是这时又出现了之前无限debugger的情况。再次通过调用堆栈的最上层


发现是这个eval出来的,这里有两个eval,只注释第一个有反调试检测的

def response(flow: mitmproxy.http.HTTPFlow):if 'https://www.aqistudy.cn/' == flow.request.url:html = flow.response.texthtml = html.replace('txsdefwsw();', '// txsdefwsw();')html = html.replace("document.write('检测到非法调试, 请关闭调试终端后刷新本页面重试!');","return; document.write('检测到非法调试, 请关闭调试终端后刷新本页面重试!');")flow.response.text = htmlelif 'html/city_realtime.php' in flow.request.url:html = flow.response.textjs = re.findall('eval\(.+', html)[0]html = html.replace(js, '// ' + js)flow.response.text = html

最后再次刷新网页


可以看到注释以后,不会再出现反调试的情况,这时反调试已经过完,可以进行js代码分析

3.js分析

分析过程给出的文章中,均有详细的介绍,所以本篇文章对分析部分跳过一些内容,仅对关键地方介绍。

数据是通过这个接口获取的,请求体和响应体都被加密了



通过调用堆栈,很容易定位到函数调用的地方,这里是通过gHfcltaYLa11POt6函数对参数进行加密,请求成功后,调用ddI2ovOg52ToSyLv1nEa函数进行解密。

但是直接扣代码的话,也没有办法快速解决,因为这个js是会不断改变了,按照前面文章的说法,这个js平均每10分钟变一次,所以并不能简单的把代码扣下来直接用。

经过我向多个大佬询问,这个网站的js会变,但是接口的加密逻辑是不变的。如果把逻辑改成python,那么只有逻辑不变,就可以一直跑了。

4.代码逻辑改写

首先第一步就是获取加密的源代码,也就是gHfcltaYLa11POt6函数的改写

首先从city_realtime.php?v=2.3中提取带有【/js/encrypt_】的js链接,并获取js内容

    requests = requests_html.HTMLSession()url = 'https://www.aqistudy.cn/html/city_realtime.php?v=2.3'headers = {'User-Agent': 'Mozilla/5.0(WindowsNT10.0;WOW64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/69.0.3497.100Safari/537.36',}response = requests.get(url, headers=headers)url = filter(lambda n: '/js/encrypt_' in n.attrs['src'], filter(lambda n: 'src' in n.attrs.keys(), response.html.xpath('//script'))).__next__().attrs['src'].replace('../js/', 'https://www.aqistudy.cn/js/')print(url)response = requests.get(url, headers=headers)data = response.text

然后需要执行代码,获取eval之后的内容,eval后如果存在dswejwehxt,还需要进行base64解码

 while data.startswith("eval("):with open('temp.js', 'w', encoding='utf-8') as f:f.write('console.log(' + data.strip()[5:-1] + ')')nodejs = subprocess.Popen('node temp', stderr=subprocess.PIPE, stdout=subprocess.PIPE)data = nodejs.stdout.read().decode().replace('\n', '')if 'dswejwehxt(dswejwehxt' in data:data = re.findall('(?<=dswejwehxt\(dswejwehxt\().+?(?=\))', data)[0][1:-1]data = base64.b64decode(base64.b64decode(data.encode())).decode()elif 'dswejwehxt(' in data:data = re.findall('(?<=dswejwehxt\().+?(?=\))', data)[0][1:-1]data = base64.b64decode(data.encode()).decode()

这时,data已经是明文的js代码,但是这里的js有可能是压缩的,也有可能是没有压缩的。这样对后面使用正则匹配非常不友好。所以这里使用ast统一格式化代码


const parser = require("@babel/parser");
const generator = require("@babel/generator");
const fs = require("fs");console.log(generator.default(parser.parse(fs.readFileSync('temp.js').toString('utf-8')), {compact: false,comments: false,jsescOption: {minimal: true}
}).code);

保存为【script.js】,并放到py同目录

    # 格式化代码with open('temp.js', 'w', encoding='utf-8') as f:f.write(data)nodejs = subprocess.Popen('node script', stderr=subprocess.PIPE, stdout=subprocess.PIPE)data = nodejs.stdout.read().decode()

这个时候的data就是统一格式化后的js代码,这时可以使用正则提取里面的内容,为加密做准备。

    appId = re.findall("(?<=var appId = ').+?(?=')", data)[0]clienttype = 'WEB'timestamp = int(time.time() * 1000)method = 'GETDATA'objectdata = {'city': '杭州'}param = {'appId': appId,'method': method,'timestamp': timestamp,'clienttype': clienttype,'object': objectdata,'secret': MD5.new((appId + method + str(timestamp) + clienttype + json.dumps(objectdata, ensure_ascii=False, separators=(',', ':'))).encode()).hexdigest()}print(param)param = base64.b64encode(json.dumps(param, ensure_ascii=False, separators=(',', ':')).encode()).decode()print(param)

这时param参数已经组包完成。但是上面文章中有提及。param参数有三种可能,aes加密,des加密和不加密,所以还需要做一个判断


def encrypt_data_aes(text, key, iv):secretkey = MD5.new(key.encode()).hexdigest()[16:]secretiv = MD5.new(iv.encode()).hexdigest()[:16]crypto = AES.new(key=secretkey.encode(), mode=AES.MODE_CBC, iv=secretiv.encode())return base64.b64encode(crypto.encrypt(pad(text.encode(), AES.block_size))).decode()def encrypt_data_des(text, key, iv):secretkey = MD5.new(key.encode()).hexdigest()[:8]secretiv = MD5.new(iv.encode()).hexdigest()[24:]crypto = DES.new(key=secretkey.encode(), mode=DES.MODE_CBC, iv=secretiv.encode())return base64.b64encode(crypto.encrypt(pad(text.encode(), DES.block_size))).decode()if 'param = AES.encrypt' in data:keyid = re.findall('(?<=param = AES\.encrypt\(param, ).+?(?=,)', data)[0]key = re.findall('(?<=const )' + keyid + ' = ".+?(?=")', data)[0].split('"')[-1]ivid = re.findall("(?<=, )" + keyid + ', .+?(?=\))', data)[0].split(', ')[-1]iv = re.findall('(?<=const )' + ivid + ' = ".+?(?=")', data)[0].split('"')[-1]param = encrypt_data_aes(param, key, iv)
elif 'param = DES.encrypt' in data:keyid = re.findall('(?<=param = DES\.encrypt\(param, ).+?(?=,)', data)[0]key = re.findall('(?<=const )' + keyid + ' = ".+?(?=")', data)[0].split('"')[-1]ivid = re.findall("(?<=, )" + keyid + ', .+?(?=\))', data)[0].split(', ')[-1]iv = re.findall('(?<=const )' + ivid + ' = ".+?(?=")', data)[0].split('"')[-1]param = encrypt_data_des(param, key, iv)

然后到最关键的请求接口

  dataid = re.findall('(?<=data: \{).+?(?=\})', data, re.S)[0].replace('\n', '').strip().split(':')[0]url = 'https://www.aqistudy.cn/apinew/aqistudyapi.php'postdata = {dataid: param}response = requests.post(url, headers=headers, data=postdata)print(response.text)

这时测试,可以成功获取到加密的响应体,接着就是还原解密的ddI2ovOg52ToSyLv1nEa函数,根据js是固定先进行aes解密,得到的结果再进行des解密,那么可以得到下面的python代码

def decrypt_data(text, data):keyid = re.findall('(?<=data = AES\.decrypt\(data, ).+?(?=,)', data)[0]key = re.findall('(?<=const )' + keyid + ' = ".+?(?=")', data)[0].split('"')[-1]ivid = re.findall('(?<=, )' + keyid + ', .+?(?=\))', data)[0].split(', ')[-1].split(',')[-1]iv = re.findall('(?<=const )' + ivid + ' = ".+?(?=")', data)[0].split('"')[-1]secretkey = MD5.new(key.encode()).hexdigest()[16:]secretiv = MD5.new(iv.encode()).hexdigest()[:16]crypto = AES.new(key=secretkey.encode(), mode=AES.MODE_CBC, iv=secretiv.encode())text = unpad(crypto.decrypt(base64.b64decode(text.encode())), AES.block_size)keyid = re.findall('(?<=data = DES\.decrypt\(data, ).+?(?=,)', data)[0]key = re.findall('(?<=const )' + keyid + ' = ".+?(?=")', data)[0].split('"')[-1]ivid = re.findall('(?<=, )' + keyid + ', .+?(?=\))', data)[0].split(', ')[-1].split(',')[-1]iv = re.findall('(?<=const )' + ivid + ' = ".+?(?=")', data)[0].split('"')[-1]secretkey = MD5.new(key.encode()).hexdigest()[:8]secretiv = MD5.new(iv.encode()).hexdigest()[24:]crypto = DES.new(key=secretkey.encode(), mode=DES.MODE_CBC, iv=secretiv.encode())text = unpad(crypto.decrypt(base64.b64decode(text)), DES.block_size)return base64.b64decode(text).decode()

测试解密正常,完整代码如下


import requests_html
import time
import json
import base64
import re
import subprocess
from Crypto.Util.Padding import pad, unpad
from Crypto.Cipher import AES, DES
from Crypto.Hash import MD5def encrypt_data_aes(text, key, iv):secretkey = MD5.new(key.encode()).hexdigest()[16:]secretiv = MD5.new(iv.encode()).hexdigest()[:16]crypto = AES.new(key=secretkey.encode(), mode=AES.MODE_CBC, iv=secretiv.encode())return base64.b64encode(crypto.encrypt(pad(text.encode(), AES.block_size))).decode()def encrypt_data_des(text, key, iv):secretkey = MD5.new(key.encode()).hexdigest()[:8]secretiv = MD5.new(iv.encode()).hexdigest()[24:]crypto = DES.new(key=secretkey.encode(), mode=DES.MODE_CBC, iv=secretiv.encode())return base64.b64encode(crypto.encrypt(pad(text.encode(), DES.block_size))).decode()def decrypt_data(text, data):keyid = re.findall('(?<=data = AES\.decrypt\(data, ).+?(?=,)', data)[0]key = re.findall('(?<=const )' + keyid + ' = ".+?(?=")', data)[0].split('"')[-1]ivid = re.findall('(?<=, )' + keyid + ', .+?(?=\))', data)[0].split(', ')[-1].split(',')[-1]iv = re.findall('(?<=const )' + ivid + ' = ".+?(?=")', data)[0].split('"')[-1]secretkey = MD5.new(key.encode()).hexdigest()[16:]secretiv = MD5.new(iv.encode()).hexdigest()[:16]crypto = AES.new(key=secretkey.encode(), mode=AES.MODE_CBC, iv=secretiv.encode())text = unpad(crypto.decrypt(base64.b64decode(text.encode())), AES.block_size)keyid = re.findall('(?<=data = DES\.decrypt\(data, ).+?(?=,)', data)[0]key = re.findall('(?<=const )' + keyid + ' = ".+?(?=")', data)[0].split('"')[-1]ivid = re.findall('(?<=, )' + keyid + ', .+?(?=\))', data)[0].split(', ')[-1].split(',')[-1]iv = re.findall('(?<=const )' + ivid + ' = ".+?(?=")', data)[0].split('"')[-1]secretkey = MD5.new(key.encode()).hexdigest()[:8]secretiv = MD5.new(iv.encode()).hexdigest()[24:]crypto = DES.new(key=secretkey.encode(), mode=DES.MODE_CBC, iv=secretiv.encode())text = unpad(crypto.decrypt(base64.b64decode(text)), DES.block_size)return base64.b64decode(text).decode()def main():requests = requests_html.HTMLSession()url = 'https://www.aqistudy.cn/html/city_realtime.php?v=2.3'headers = {'User-Agent': 'Mozilla/5.0(WindowsNT10.0;WOW64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/69.0.3497.100Safari/537.36',}response = requests.get(url, headers=headers)url = filter(lambda n: '/js/encrypt_' in n.attrs['src'], filter(lambda n: 'src' in n.attrs.keys(), response.html.xpath('//script'))).__next__().attrs['src'].replace('../js/', 'https://www.aqistudy.cn/js/')print(url)response = requests.get(url, headers=headers)data = response.textwhile data.startswith("eval("):with open('temp.js', 'w', encoding='utf-8') as f:f.write('console.log(' + data.strip()[5:-1] + ')')nodejs = subprocess.Popen('node temp', stderr=subprocess.PIPE, stdout=subprocess.PIPE)data = nodejs.stdout.read().decode().replace('\n', '')if 'dswejwehxt(dswejwehxt' in data:data = re.findall('(?<=dswejwehxt\(dswejwehxt\().+?(?=\))', data)[0][1:-1]data = base64.b64decode(base64.b64decode(data.encode())).decode()elif 'dswejwehxt(' in data:data = re.findall('(?<=dswejwehxt\().+?(?=\))', data)[0][1:-1]data = base64.b64decode(data.encode()).decode()# 格式化代码with open('temp.js', 'w', encoding='utf-8') as f:f.write(data)nodejs = subprocess.Popen('node script', stderr=subprocess.PIPE, stdout=subprocess.PIPE)data = nodejs.stdout.read().decode()appId = re.findall("(?<=var appId = ').+?(?=')", data)[0]clienttype = 'WEB'timestamp = int(time.time() * 1000)method = 'GETDATA'objectdata = {'city': '杭州'}param = {'appId': appId,'method': method,'timestamp': timestamp,'clienttype': clienttype,'object': objectdata,'secret': MD5.new((appId + method + str(timestamp) + clienttype + json.dumps(objectdata, ensure_ascii=False, separators=(',', ':'))).encode()).hexdigest()}print(param)param = base64.b64encode(json.dumps(param, ensure_ascii=False, separators=(',', ':')).encode()).decode()print(param)if 'param = AES.encrypt' in data:keyid = re.findall('(?<=param = AES\.encrypt\(param, ).+?(?=,)', data)[0]key = re.findall('(?<=const )' + keyid + ' = ".+?(?=")', data)[0].split('"')[-1]ivid = re.findall("(?<=, )" + keyid + ', .+?(?=\))', data)[0].split(', ')[-1]iv = re.findall('(?<=const )' + ivid + ' = ".+?(?=")', data)[0].split('"')[-1]param = encrypt_data_aes(param, key, iv)elif 'param = DES.encrypt' in data:keyid = re.findall('(?<=param = DES\.encrypt\(param, ).+?(?=,)', data)[0]key = re.findall('(?<=const )' + keyid + ' = ".+?(?=")', data)[0].split('"')[-1]ivid = re.findall("(?<=, )" + keyid + ', .+?(?=\))', data)[0].split(', ')[-1]iv = re.findall('(?<=const )' + ivid + ' = ".+?(?=")', data)[0].split('"')[-1]param = encrypt_data_des(param, key, iv)dataid = re.findall('(?<=data: \{).+?(?=\})', data, re.S)[0].replace('\n', '').strip().split(':')[0]url = 'https://www.aqistudy.cn/apinew/aqistudyapi.php'postdata = {dataid: param}response = requests.post(url, headers=headers, data=postdata)print(response.text)data = decrypt_data(response.text, data)print(data)if __name__ == '__main__':main()

【JS逆向系列】某空气质量监测平台无限 debugger 与 python算法还原相关推荐

  1. 【JS 逆向百例】某空气质量监测平台无限 debugger 以及数据动态加密分析

    关注微信公众号:K哥爬虫,持续分享爬虫进阶.JS/安卓逆向等技术干货! 文章目录 声明 逆向目标 写在前面 绕过无限 debugger 方法一 方法二 方法三 抓包分析 加密入口 动态 JS 本地改写 ...

  2. 【JS 逆向百例】某空气质量监测平台无限 debugger 以及数据动态加密

    关注微信公众号:K哥爬虫,持续分享爬虫进阶.JS/安卓逆向等技术干货! 文章目录 声明 逆向目标 写在前面 绕过无限 debugger 方法一 方法二 方法三 抓包分析 加密入口 动态 JS 本地改写 ...

  3. 点击时候确定某个元素 js_某空气质量监测平台 JS反爬

    目标:中国空气质量在线监测分析平台|城市分析 参考CSDN中文章,记录一下学习过程 通过切换城市,页面数据是通过 Ajax 加载的,数据接口:https://www.aqistudy.cn/apine ...

  4. js逆向 空气质量检测平台

    js逆向 空气质量检测平台 郑重声明 郑重声明:本项目的所有代码和相关文章, 仅用于经验技术交流分享,禁止将相关技术应用到不正当途径,因为滥用技术产生的风险与本人无关. 反调试绕过 url:https ...

  5. 基于 Python 的全国空气质量监测与可视化分析平台

    温馨提示:文末有 CSDN 平台官方提供的学长 Wechat / QQ 名片 :) 1. 项目背景 空气质量优劣程度与一个城市的综合竞争力密切相关,它直接影响到投资环境和居民健康,因此越来越受到政府和 ...

  6. 空气质量监测系统的组成和应用

    空气质量监测系统产品简介 网格化微型空气质量站是一种集数据采集.存储.传输和管理于一体的无人值守的环境监测系统,能全天候.连续.自动地监测环境,在提供PM10.PM2.5. SO2. NO2.CO.O ...

  7. js逆向爬虫实战之快手第三方平台之获取登录cookies!

    爬虫js逆向系列 我会把做爬虫过程中,遇到的所有js逆向的问题分类展示出来,以现象,解决思路,以及代码实现.我觉得做技术分享,不仅仅是要记录问题,解决办法,更重要的是要提供解决问题的思路.怎么突破的, ...

  8. arduino读取水位传感器的数据显示在基于i2c的1602a上_构建Arduino的LoRa远程智能空气质量监测系统...

    背景知识视频教程 Arduino分步指南:完整指南 - 国外课栈​viadean.com Arduino微控制器:学习Arduino制作项目 - 国外课栈​viadean.com 通过构建实际应用程序 ...

  9. 实时空气质量监测解决方案

    一.行业背景 近年来空气质量一直都是大家关注的重点,PM2.5.重工业污染.沙尘暴等无时无刻都在影响着我们的健康.伴随着人们生活水平的提升,对于自身生活环境数据的事实了解意向也越来越强烈.空气质量有没 ...

最新文章

  1. locate,find
  2. java 实体 text字段,如何在Java中修剪对象的某些字段?
  3. MVC3----筛选数据(BeginForm:输出form表单)
  4. RTSP协议-中文定义
  5. 程序员修炼之道-笔记
  6. 电子设计竞赛电源题(4)-Buck与Boost电路
  7. Struts Gossip: 模組化程式
  8. 征集.NET中国峰会议题
  9. 进程的创建-Process子类(python 版)
  10. vue2.0-脚手架-todolist案例
  11. 8个球放入3个盒子方式_盒子这样做皮薄如纸,不露馅超好吃,孩子一口气吃好几个...
  12. 哈希冲突常用解决方法
  13. PS滤镜插件工具箱Mac版:Nik Collection 4
  14. K-th Number Poj - 2104 主席树
  15. PlayReady 和WideVine
  16. 基于Multisim的220v转12v典型开关电源电路仿真
  17. SVN update拒绝访问,clean up失败
  18. python做乘法运算定律_乘法运算定律(四年级数学下册乘法运算定律)
  19. Python -- 堆数据结构 heapq - I love this game! - 博客频道 - CSDN.NET
  20. 水晶报表打印出错,未能加载文件或程序集“CrystalDecisions.CrystalReports.Engine, Version=10.5.3700.0

热门文章

  1. 【Camera专题】Qcom-你应该掌握的Camera调试技巧2
  2. 豆豆的软件生活——创刊号
  3. HISI3516 MMP VB调试汇总
  4. 成为更好程序员的7个方法
  5. 2007年10月26日 星期五
  6. 2017中国(上海)国际城市地下综合管廊产业展览会暨主题论坛会刊(参展商名录)
  7. 469A - I Wanna Be the Guy
  8. Question 1(解决输入Scanner一系列的问题)
  9. 【hit说话人确认实验】基于GMM-UBM的MSR Identity Toolkit工具使用
  10. tb6612电机驱动软件开发(代码pid实现,调试,控制实现)