来写几个比较简单的js逆向
GlidedSky JS加密1
开局已经提示了,数据是通过ajax加载的,直接打开开发者工具的 Network
选项:
这里是普通的get请求,其中 page
t
参数都很明显,分别对应页数和秒级时间戳
只有 sign
不太明确,需要逆向找出它的生成逻辑,观察到长度是40,怀疑可以是sha1算法
- md5 的长度是 32
- sha256 的长度是 60
直接全局搜索sign的话,太多结果了,试试 XHR断点:
断住后,一直追栈,直到定位到sign的生成处,如上图所示,抠出主要的逻辑:
1 2
| let t = Math.floor(($('main .container').attr('t') - 99) / 99); let sign = sha1('Xr0Z-javascript-obfuscation-1' + t);
|
这里代码的意思就是,取 main .container
元素的 t
属性的值,做一下简单的数学运算,然后拼接字符串,最后计算SHA1哈希值
注意,请求接口参数的t不能直接取当前的time.time()
,而是要根据页面取出的值做计算得出
所以到这里爬虫逻辑就很清晰了,先访问每一页,取出t值,用来计算sign,然后拿sign去请求ajax接口,拿到全部数字
代码整理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| import hashlib import math import requests
from parsel import Selector
def get_sign(t: int): t = math.floor((t - 99) / 99) text = "Xr0Z-javascript-obfuscation-1" + str(t) sign = hashlib.sha1(text.encode("utf8")).hexdigest() return t, sign
def glidedsky_login(): """ 网站登录,才能看到题目 注意题目域名也必须是 www.glidedsky.com """ EMAIL = "" PASSWORD = "" LOGIN_URL = "http://www.glidedsky.com/login"
session = requests.session() resp = session.get(LOGIN_URL) dom = Selector(resp.text) _token = dom.css("meta[name='csrf-token']::attr(content)").get() form_data = { "_token": _token, "email": EMAIL, "password": PASSWORD, } session.post(LOGIN_URL, data=form_data) return session
def main(): session = glidedsky_login()
for page in range(1, 11): url = ( "http://www.glidedsky.com/level/web/crawler-javascript-obfuscation-1?page=" + str(page) ) resp = session.get(url) t = Selector(resp.text).css("main div.container::attr(t)").get() t, sign = get_sign(int(t))
api = "http://www.glidedsky.com/api/level/web/crawler-javascript-obfuscation-1/items" params = { "page": page, "t": t, "sign": sign, } data = session.get(api, params=params).json() print(f"page {page}: ", data["items"])
if __name__ == "__main__": main()
|
运行结果
彩蛋:
vscode提示缩进和空格混乱的报错:Inconsistent use of tabs and spaces in indentation
解决:查看 - 命令面板(Ctrl + Shift + P)- 输入 Convert Indentation to Spaces
360快讯 INITAL_DATA
还原
和之前分析过的豆瓣读书一样,直接请求的话,是拿不到网页的源码的
初步怀疑是通过 __INITIAL_DATA__
里面的一大串乱码还原出dom树,直接全局搜索关键词__INITIAL_DATA__
(不要加 **windows.**)
定位到 deatilv9.js
文件,这里有70多个matches,一个一个找的话太费事,可以通过打断点方式,浏览器自动帮你跳过同一行的重复matches
最终定位到这里,刷新网页确定断住了,文章内容也还没加载
1
| e.__INITIAL_DATA__ = JSON.parse((0,d["default"])(e.__INITIAL_DATA__, e.uuid.length))
|
这个地方有点可疑,特别是JSON.parse,明显是用来加载json字符串的
console执行确认一下:
解析出真实的数据了,所以可以确定这里就是入口,来分析一下那一行代码:
e.__INITIAL_DATA__
就是那一大串乱码
e.uuid
网页源码里面也有,长度是固定的 32
所以这里 d["default"]
就肯定是解析函数了,进入到内部看看做了什么
1 2 3 4 5 6
| function r(e) { var t = e.slice(0, 1e3).split("").map(function(e, t) { return String.fromCharCode(e.charCodeAt() - t % 2) }).join(""); return Base64.decode(t + e.slice(1e3)) }
|
这一段好像没什么好分析的,不熟悉 map 函数的可能有点疑惑,这里有解释,举两个例子
1 2 3 4 5 6 7 8 9 10 11 12 13
| let array = [1, 2, 3, 4, 5];
let newArray = array.map((item) => { return item * item; }) newArray >>> [1, 4, 9, 16, 25]
let array2 = array.map((item, index) => { return item * index; }) array2 >>> [0, 2, 6, 12, 20]
|
所以这里还原成python代码的话,就是:
1 2 3 4 5 6 7 8 9
| def decode_raw_data(e): t = e[:1000] t1 = "" for i, c in enumerate(t): _c = chr(ord(c) - i % 2) t1 += _c
text = base64.b64decode(t1 + e[1000:]).decode() return text
|
代码整理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import base64 import json import re
import requests
def decode_raw_data(e): t = e[:1000] t1 = "" for i, c in enumerate(t): _c = chr(ord(c) - i % 2) t1 += _c
text = base64.b64decode(t1 + e[1000:]).decode() obj = json.loads(text) return obj
def main(): url = "https://www.360kuai.com/949c28ed30ae1ae77?refer_scene=nh_3&scene=3&sign=look&uid=e29cd854b2b3eaf1b1a73f0cca8bd1c1&tj_url=90dc36726039a3551" resp = requests.get(url) raw_data = ( re.search(r"__INITIAL_DATA__ = '(.+)';", resp.text, flags=re.DOTALL) .group(1) .strip("\\\n") ) obj = decode_raw_data(raw_data) print(obj)
if __name__ == "__main__": main()
|
运行结果
彩蛋
关于 vscode 中文输出乱码的问题
- 在 终端 输入:
chcp 65001
,然后调试
猿人学 第三关:访问逻辑 - 推心置腹
这关就直接明说了,只要注意请求头的顺序就行
代码整理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import requests
def main(): headers = { "Host": "match.yuanrenxue.com", "Content-Length": "0", "User-Agent": "yuanrenxue.project", "Accept": "*/*", "Referer": "https://match.yuanrenxue.com/match/3", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "zh-CN,zh;q=0.9", "Cookie": "sessionid=gb2a3nf1dkvesma9vymfq1zn18q7tz0t", } session = requests.session() session.headers.clear() session.headers.update(headers)
for page in range(1, 6): logo_url = "https://match.yuanrenxue.com/jssm" session.post(logo_url) sqh_url = f"http://match.yuanrenxue.com/api/match/3?page={page}" resp = session.get(sqh_url) print(resp.json()["data"])
if __name__ == "__main__": main()
|
requests.session 默认携带的请求头如下,因此需要调用 clear
方法清除
1
| {'User-Agent': 'python-requests/2.22.0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*
|
headers 需要对照fiddler,一个一个测试修改,注意 Content-Length
的顺序(出题者的恶趣味 🐶)
关于服务器那边的设计逻辑,应该是同一个 sessionid,访问 /api/match/3
前,必须至少访问一次 /jssm
接口,如果没有访问记录就拒绝请求