1. JS 解密, 混淆, 逆向
url:https://www.aqistudy.cn/html/city_detail.html
分析:
空气指标的数据是动态加载出来
修改了搜索条件后点击搜索按钮会发起 Ajax 请求, 请求到我们想要的指标数据.
从上一步定位到的数据包中提取出 url, 请求方式, 请求参数
url 和请求方式可以拿来直接用
请求参数是动态变化且加密
响应数据也是加密的密文数据
找到点击搜索按钮发起的 Ajax 请求对应的代码
基于火狐浏览器的开发者工具找到搜索按钮对应点击事件绑定的 JS 函数是哪个
getData(), 该函数的实现
type=HOUR: 以小时为单位进行数据的查询
调用了另两个函数: getAQIData(), getWeatherData()
并没有找到 Ajax 请求对应的的代码
分析 getAQIData&getWeatherData:
这两个函数的实现几乎一致, 唯一的区别是
- var method = 'GETDETAIL';
- var method = 'GETCITYWEATHER';
也没有找到 Ajax 请求对应的代码, 但是发现了另一个函数的调用:
- getServerData(method, param, function(obj),0.5 )
- method:
- 'GETDETAIL'
- 'GETCITYWEATHER'
param 是一个字典, 有四组键值对:
- city;
- type;
- startTime;
- endTime;
分析 getServerData 函数的实现:
基于抓包工具进行全局搜索, 定位到了一个指定的数据包, 出现了 getServerData 关键词, 这个关键词对应的 JS 代码被加密了
JS 混淆: 将 JS 中的核心代码加密
JS 反混淆:
暴力破解:
url:https://www.bm8.com.cn/jsConfusion/
分析反混淆后的 getServerData 函数的实现代码:
终于发现了 Ajax 请求对应的代码:
getParam(method, object) 返回动态变化且加密的请求参数 d 的值.
- method == method
- object == param
decodeData(data): 接受加密的响应数据返回解密后的明文数据
data: 加密的响应数据
JS 逆向:
自动逆向:
PyExecJS 介绍: PyExecJS 是一个可以使用 Python 来模拟运行 JavaScript 的库.
一定实现在本机装好 Node.JS 的开发环境
我们需要 pip install PyExecJS 对其进行环境安装.
- In [3]:
- # 模拟执行 JS 函数获取动态变化且解密的请求参数 d 的值
- import ExecJS
- node = ExecJS.get()
- # Params
- method = 'GETCITYWEATHER'
- city = '北京'
- type = 'HOUR'
- start_time = '2018-01-25 00:00:00'
- end_time = '2018-01-25 23:00:00'
- # Compile JavaScript
- file = 'test.js'
- ctx = node.compile(open(file,encoding='utf-8').read())
- # Get params
- JS = 'getPostParamCode("{0}","{1}","{2}","{3}","{4}")'.format(method, city, type, start_time, end_time)
- params = ctx.eval(JS)# 模拟执行指定的 JS 函数
- print(params)
- tdgHOYxwKdDSgYXe+RLPzYCgLvrddahasI5XXklB4gVLYqab+XRPpMD/oSqnJ/aEmFwzVEUhLnPzRy03+X1BI4qc9EYeRPqiKrT+f1JQExGQ4ii8kKvZhGH+nPffaX/xq5iLB6vblcvBC/L8e6UxdnHlajfkXrLQf1qv5Hcg3c++RoGxPAMOgNc6HbCbQG2sE6yemJ7l8HI9CyNktTP7AwQC04bTbY+s+o7lljhqUvsyMZq88MU1VV46TFExCP7vxfmEl6YFeV892bU27lPedTCtSnYbCEfFCJDP0DfEBHe0XFOcgXs+Yl5h58efciX69k9IEvGCKenhokOJQ2tS178anRoT37sEBV5cZeLY8Uzh8UUWgxg2sH+JJsg8ARclHhK0AN/SA4wFy8XmwdBun1zHxV8LoPfn3cxqzXnNKOp/nowpNnbyuMSZtftbf41HB1dEdkm07a2LzCaJgUEpPmLZUuA7+lDlCKqTsEZVh9w=
对 Ajax 的 url 携带 d 请求参数进行 post 请求的发送可以获取加密的响应数据
- In [5]:
- import ExecJS
- import requests
- node = ExecJS.get()
- # Params
- method = 'GETCITYWEATHER'
- city = '北京'
- type = 'HOUR'
- start_time = '2018-01-25 00:00:00'
- end_time = '2018-01-25 23:00:00'
- # Compile JavaScript
- file = 'test.js'
- ctx = node.compile(open(file,encoding='utf-8').read())
- # Get params
- JS = 'getPostParamCode("{0}","{1}","{2}","{3}","{4}")'.format(method, city, type, start_time, end_time)
- params = ctx.eval(JS)
- # 发起 post 请求
- url = 'https://www.aqistudy.cn/apinew/aqistudyapi.php'
- response_text = requests.post(url, data={'d': params}).text
- print(response_text)
- eAkzHZvdWqslCrP29e8XgEP22qdvyxus1TrEFB8uvsD0ChwbOTBCJErsCqVJyLQJ9wdhdK9lk3nl/SEeVqoXSY48w11ODT7v6rhQkkXuZ3Vv+VOQ7C7zXtLvbJJDIq9Nu3RRA+8rS/R0lnyMUk98IQ==
- In [12]:
- # 将密文的响应数据进行解密: 模拟调用 decodeData(data)
- import ExecJS
- import requests
- node = ExecJS.get()
- # Params
- method = 'GETCITYWEATHER'
- city = '北京'
- type = 'HOUR'
- start_time = '2018-01-25 00:00:00'
- end_time = '2018-01-25 23:00:00'
- # Compile JavaScript
- file = 'test.js'
- ctx = node.compile(open(file,encoding='utf-8').read())
- # Get params
- JS = 'getPostParamCode("{0}","{1}","{2}","{3}","{4}")'.format(method, city, type, start_time, end_time)
- params = ctx.eval(JS)
- # 发起 post 请求
- url = 'https://www.aqistudy.cn/apinew/aqistudyapi.php'
- response_text = requests.post(url, data={'d': params}).text
- # #对加密的响应数据进行解密
- jss = 'decodeData("{0}")'.format(response_text)
- print(jss)
- decrypted_data = ctx.eval(jss)
- print(decrypted_data)
- decodeData("S+PG+bQmwr20q9LEnYxZb6d9kwGuj+GKqm/YqBW0N9VTTsfFzS6mR86ne1uxqNuepTIfI+opvFV/np093XWIf2IXLkXoN7yUEFNnINrJBIN9MFj2Y9rWgCXZXe4k0PtMub9YKalryHwuO7IlNJN3OA==")
- ---------------------------------------------------------------------------
- ProgramError Traceback (most recent call last)
- <ipython-input-12-775eb2217305> in <module>()
- 26 jss = 'decodeData("{0}")'.format(response_text)
- 27 print(jss)
- ---> 28 decrypted_data = ctx.eval(jss)
- 29 print(decrypted_data)
- ~\Anaconda3\lib\site-packages\ExecJS\_abstract_runtime_context.py in eval(self, source)
- 25 if not self.is_available():
- 26 raise ExecJS.RuntimeUnavailableError
- ---> 27 return self._eval(source)
- 28
- 29 def call(self, name, *args):
- ~\Anaconda3\lib\site-packages\ExecJS\_external_runtime.py in _eval(self, source)
- 76
- 77 code = 'return eval({data})'.format(data=data)
- ---> 78 return self.exec_(code)
- 79
- 80 def _exec_(self, source):
- ~\Anaconda3\lib\site-packages\ExecJS\_abstract_runtime_context.py in exec_(self, source)
- 16 if not self.is_available():
- 17 raise ExecJS.RuntimeUnavailableError
- ---> 18 return self._exec_(source)
- 19
- 20 def eval(self, source):
- ~\Anaconda3\lib\site-packages\ExecJS\_external_runtime.py in _exec_(self, source)
- 86 else:
- 87 output = self._exec_with_pipe(source)
- ---> 88 return self._extract_result(output)
- 89
- 90 def _call(self, identifier, *args):
- ~\Anaconda3\lib\site-packages\ExecJS\_external_runtime.py in _extract_result(self, output)
- 165 return value
- 166 else:
- --> 167 raise ProgramError(value)
- 168
- 169
- ProgramError: Error: Malformed UTF-8 data
- selenium
回顾
单线程 + 多任务的异步协程
特殊的函数
调用后实现内部的程序语句不会被立即执行
调用后返回一个协程对象
协程对象
协程 == 特殊函数 == 一组指定的操作
任务对象
高级的协程对象
绑定回调函数
- task.add_done_callback(func)
- func:
必须要有一个参数 (当前的任务对象)
参数. result() 表示的就是特殊函数的返回值
任务对象 == 一组指定的操作
事件循环对象
创建一个 eventloop 对象
作用:
必须要装载一个或者多个任务对象 (任务对象是需要注册到 eventloop)
启动事件循环对象
可以异步的执行其内部注册的每一个任务对象对应的指定操作
等待 await: 确保 eventloop 一定会执行阻塞操作
挂起: 让当前发生阻塞的任务对象交出 CPU 的使用权
asyncio.wait(tasks)
重点:
特殊函数内部不可以出现不支持异步模块的代码
aiohttp: 支持异步的网络请求模块
使用使用上下文机制 (with...as)
实例化一个请求对象 (ClientSession())
get/post() 进行请求发送.(阻塞操作)
proxy 参数:'http://ip:port/'
获取响应数据 (阻塞操作)
response.text(): 字符串
- response.read():byte
- selenium
动作链
无头浏览器
规避检测
浏览器托管
12306 的模拟登陆
动作链
from selenium.webdriver import ActionChains
NoSuchElementException: 没有定位到指定的标签
定位的标签是存在于一张嵌套的子页面中, 如果想定位子页面中的指定标签的话需要:
bro.switch_to.frame('iframe 标签 id 的属性值'): 将当前浏览器页面切换到指定的子页面的范围中
针对指定的浏览器实例化一个动作链对象
- action = ActionChains(bro)
- action.click_and_hold(tagName)
- move_by_offset(10,15)
perform() 是的动作链立即执行
- In [6]:
- from selenium import webdriver
- from selenium.webdriver import ActionChains
- from time import sleep
- # 后面是你的浏览器驱动位置, 记得前面加 r'','r'是防止字符转义的
- bro = webdriver.Chrome('./chromedriver.exe')
- bro.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')
- # 标签定位
- bro.switch_to.frame('iframeResult')
- div_tag = bro.find_element_by_id('draggable')
- # 需要使用 ActionChains 定制好的行为动作
- action = ActionChains(bro)# 针对当前浏览器页面实例化了一个动作链对象
- action.click_and_hold(div_tag)# 点击且长按指定的标签
- for i in range(1,7):
- action.move_by_offset(10,15).perform()#perform() 是的动作链立即执行
- sleep(0.5)
- bro.quit()
无头浏览器
没有可视化界面的浏览器
phantomJS: 无头浏览器
谷歌无头浏览器:
就是你本机安装的谷歌浏览器, 只是需要通过代码进行相关配置就可以变成无头浏览器
- In [8]:
- from selenium.webdriver.Chrome.options import Options
- chrome_options = Options()
- chrome_options.add_argument('--headless')
- chrome_options.add_argument('--disable-gpu')
- bro = webdriver.Chrome(executable_path='./chromedriver.exe',chrome_options=chrome_options)
- bro.get('https://www.baidu.com/')
- sleep(1)
- bro.save_screenshot('./1.png')# 截屏
- print(bro.page_source)
selenium 规避检测
浏览器托管
环境配置:
1. 本机谷歌浏览器驱动程序所在的目录的路径添加到环境变量中
2. 使用本机谷歌的驱动程序开启一个浏览器
Chrome.exe --remote-debugging-port=9222 --user-data-dir="C:\selenum\AutomationProfile"
9222: 端口 (任意)
"C:\selenum\AutomationProfile": 已经事先存在的一个空目录
使用如下代码接管目前打开的浏览器:
- In [30]:
- from selenium import webdriver
- from selenium.webdriver.Chrome.options import Options
- chrome_options = Options()
- chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
- bro = webdriver.Chrome(executable_path='./chromedriver.exe',chrome_options=chrome_options)# 代码托管代开的浏览器, 不会实例化一个新的浏览器.
- bro.get('https://kyfw.12306.cn/otn/login/init')
- C:\Users\laonanhai\Anaconda3\lib\site-packages\ipykernel_launcher.py:8: DeprecationWarning: use options instead of chrome_options
12306 模拟登陆
url:https://kyfw.12306.cn/otn/login/init
分析:
识别的验证码图片必须通过截图获取然后存储到本地
登陆操作和唯一的验证码图片一一对应
- In [25]:
- #pip install Pillow
- from PIL import Image
- from selenium.webdriver import ActionChains
- from selenium import webdriver
- # 识别验证码的函数
- def transformCode(imgPath,imgType):
- chaojiying = Chaojiying_Client('13614167787', '13614167787', '903126')
- im = open(imgPath, 'rb').read()
- return chaojiying.PostPic(im,imgType)['pic_str']
- In [26]:
- bro = webdriver.Chrome(executable_path='./chromedriver.exe')
- bro.get('https://kyfw.12306.cn/otn/login/init')
- sleep(2)
- bro.find_element_by_id('username').send_keys('xxxxxxx')
- bro.find_element_by_id('password').send_keys('12345465')
- # 验证码的点击操作
- bro.save_screenshot('main.png')# 将页面当做图片保存到本地
- # 将单独的验证码图片从 main.PNG 中裁剪下载
- img_tag = bro.find_element_by_xpath('//*[@id="loginForm"]/div/ul[2]/li[4]/div/div/div[3]/img')# 将验证码图片的标签定位到了
- location = img_tag.location
- size = img_tag.size
- # print(location,size)
- # 裁剪的范围 (验证码图片左下角和右上角两点坐标)
- rangle = (int(location['x']),int(location['y']),int(location['x']+size['width']),int(location['y']+size['height']))
- # 使用 Image 类根据 rangle 裁剪范围进行验证码图片的裁剪
- i = Image.open('./main.png')
- frame = i.crop(rangle)# 验证码对应的二进制数据
- frame.save('./code.png')
- result = transformCode('./code.png',9004)#99,71|120,140
- #99,71|120,140 == [[99,71],[120,140]]
- all_list = []#[[99,71],[120,140]]
- if '|' in result:
- list_1 = result.split('|')
- count_1 = len(list_1)
- for i in range(count_1):
- xy_list = []
- x = int(list_1[i].split(',')[0])
- y = int(list_1[i].split(',')[1])
- xy_list.append(x)
- xy_list.append(y)
- all_list.append(xy_list)
- else:
- x = int(result.split(',')[0])
- y = int(result.split(',')[1])
- xy_list = []
- xy_list.append(x)
- xy_list.append(y)
- all_list.append(xy_list)
- for data in all_list:
- x = data[0]#11
- y = data[1]#22
- ActionChains(bro).move_to_element_with_offset(img_tag,x,y).click().perform()
- sleep(1)
- sleep(2)
- bro.find_element_by_id('loginSub').click()
- bro.quit()
- In [21]:
- # 下载好的示例代码
- #!/usr/bin/env python
- # coding:utf-8
- import requests
- from hashlib import md5
- class Chaojiying_Client(object):
- def __init__(self, username, password, soft_id):
- self.username = username
- password = password.encode('utf8')
- self.password = md5(password).hexdigest()
- self.soft_id = soft_id
- self.base_params = {
- 'user': self.username,
- 'pass2': self.password,
- 'softid': self.soft_id,
- }
- self.headers = {
- 'Connection': 'Keep-Alive',
- 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
- }
- def PostPic(self, im, codetype):
- """
- im: 图片字节
- codetype: 题目类型 参考 http://www.chaojiying.com/price.html
- """
- params = {
- 'codetype': codetype,
- }
- params.update(self.base_params)
- files = {'userfile': ('ccc.jpg', im)}
- r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files, headers=self.headers)
- return r.JSON()
- def ReportError(self, im_id):
- """
- im_id: 报错题目的图片 ID
- """
- params = {
- 'id': im_id,
- }
- params.update(self.base_params)
- r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)
- return r.JSON()
- # if __name__ == '__main__':
- # chaojiying = Chaojiying_Client('超级鹰用户名', '超级鹰用户名的密码', '96001') #用户中心 >> 软件 ID 生成一个替换 96001
- # im = open('a.jpg', 'rb').read() #本地图片文件路径 来替换 a.jpg 有时 WIN 系统须要 //
- # print chaojiying.PostPic(im, 1902) #1902 验证码类型 官方网站 >> 价格体系 3.4 + 版 print 后要加 ()
来源: http://www.bubuko.com/infodetail-3371884.html