1. js 解密,混淆,逆向
-
分析:
- 空气指标的数据是动态加载出来
- 修改了搜索条件后点击搜索按钮会发起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;
- method:
- getServerData(method, param, function(obj),0.5 )
- 分析getServerData函数的实现:
- 基于抓包工具进行全局搜索,定位到了一个指定的数据包,出现了getServerData关键词,这个关键词对应的js代码被加密了
- JS混淆:将js中的核心代码加密
- JS反混淆:
- 暴力破解:
- 分析反混淆后的getServerData函数的实现代码:
- 终于发现了ajax请求对应的代码:
- getParam(method, object)返回动态变化且加密的请求参数d的值。
- method == method
- object == param
- decodeData(data):接受加密的响应数据返回解密后的明文数据
- data:加密的响应数据
- getParam(method, object)返回动态变化且加密的请求参数d的值。
- 终于发现了ajax请求对应的代码:
- js逆向:
- 自动逆向:
- PyExecJS介绍:PyExecJS 是一个可以使用 Python 来模拟运行 JavaScript 的库。
- 一定实现在本机装好nodejs的开发环境
- 我们需要pip install PyExecJS对其进行环境安装。
- PyExecJS介绍:PyExecJS 是一个可以使用 Python 来模拟运行 JavaScript 的库。
- 自动逆向:
- 这两个函数的实现几乎一致,唯一的区别是
- getData(),该函数的实现
- 基于火狐浏览器的开发者工具找到搜索按钮对应点击事件绑定的js函数是哪个
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)
~Anaconda3libsite-packagesexecjs\_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):
~Anaconda3libsite-packagesexecjs\_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):
~Anaconda3libsite-packagesexecjs\_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):
~Anaconda3libsite-packagesexecjs\_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):
~Anaconda3libsite-packagesexecjs\_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:selenumAutomationProfile"
- 9222:端口(任意)
- "C:selenumAutomationProfile":已经事先存在的一个空目录
- chrome.exe --remote-debugging-port=9222 --user-data-dir="C:selenumAutomationProfile"
- 使用如下代码接管目前打开的浏览器:
- 环境配置:
- 浏览器托管
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:UserslaonanhaiAnaconda3libsite-packagesipykernel_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 后要加()