来写几个比较简单的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 中文输出乱码的问题

  1. 终端 输入: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': '*/*', 'Connection': 'keep-alive'}
  • headers 需要对照fiddler,一个一个测试修改,注意 Content-Length 的顺序(出题者的恶趣味 🐶)

  • 关于服务器那边的设计逻辑,应该是同一个 sessionid,访问 /api/match/3 前,必须至少访问一次 /jssm 接口,如果没有访问记录就拒绝请求