前言

想写这篇主要是七麦网的加密方式比较有趣,有挺多值得深究的地方,在文章中我会一步步用python还原,并介绍一些加密的知识,那么话不多说,let the hacking begin !

调试

七麦网基本每个数据接口都需要带上 analysis参数,不然就会返回 Access Error

直接全局搜analysis基本没有线索,说明七麦把这个键值做了隐藏,直接xhr下断点

断住了,接下来就是观察变量和追踪 Call Stack

在 promise 异步请求的 interceptors 中发现可疑点,打个断点试试

promise 是 ES6 新增的一个用于处理异步任务的javascript类,因为js代码是单线程执行的,很多浏览器事件都必须异步执行(传送门)

1
2
// promise 执行并打印结果
window.PSign.sign(cxs).then(function(e) {console.log(e.h5st)});

interceptors 即Axios(一个基于 promise 的 HTTP 库)的拦截器模式,分成两种,分别是request请求拦截器response响应拦截器,其中,请求拦截器可以修改body参数(fulfilled操作),也可以直接拒绝非法请求(rejected操作)(传送门)

这里主要是修改body,所以我们进入 fulfilled 的 FunctionLocation 查看

打断点调试,Resume一下,这里我们看到a就是analysis的值,但在函数入口的时候还没有,所以必定是在上面几行代码中生产的

代码分析

在函数内部我们看到 a的生成代码是

1
2
3
4
5
a = (0,
n.cv)((0,
n.oZ)(r, l))

// 也就是 a = (0,n.cv)((0,n.oZ)(r, l))

l参数

先分析参数,首先是 l,往上找到生成步骤,即 (0,n.nF)("qimai|Technologyx", 1)

传入的是两个常数,进入n.nF内部查看,也基本是字符串转换、位计算的一些常规操作,所以这里l直接取 0000000c735d856就行

r参数

经过调试发现,r的初始值是几个查询值的列表

先经过排序join 操作,接下来的n.cv其实就是 base64 编码

后面的几步操作无非是字符串的拼接,其中涉及到了三个变量

  • e.url.replace(e.baseURL, "") 即去掉主站域名后的路径,比如 https://api.qimai.cn/rank/index 生成 /rank/index
  • d 的值是固定的 “@#”
  • o 的值是 o = +new Date - (f || 0) - 1515125653845,new Date 是当前的13位毫秒时间戳
  • f 的值好像是个和 cookies 有关的整数,这里我直接取随机整数也能通过,精力有限就不深究了

两个函数

最后就是 a = (0,n.cv)((0,n.oZ)(r, l))了,这里明显是经过了两个函数的处理

  1. n.oZ

都是一些常规的计算,直接用 python 还原

1
2
3
4
5
6
7
8
def oz(a):
t = "0000000c735d856"
n = len(t)
e = []

for i in range(len(a)):
e.append(chr(ord(a[i]) ^ ord(t[(i + 10) % n])))
return "".join(e)

PS:
这里涉及到字符和ASCII码的相互转换

  • ord 得到 ASCII码,对应 js 的 CharCodeAt,用法是 "cxs".CharCodeAt(0),即转换第一个字符c
  • chr 得到 字符,对应 js 的 fromCharCode,用法是 String.fromCharCode(77),得到字符”M”

fromCharCode 的方法在代码中没有直接体现,而是

其实直接拿 a变量那一整句去 console执行,就得出fromCharCode了(对面的前端好无聊 😑😑😑)

  1. n.cv

进入函数内部,多次调试发现,只进行了 base64 加密,encodeURIComponent 那一部分最先对原参数进行处理,但是跟没处理一样 ,可以忽略

replace(/%([0-9A-F]{2})/g, ... 的意思就是 遍历字符串,寻找能匹配 %([0-9A-F]{2})的子字符串,做替换/g 的意思是尽可能匹配多的结果,没有的话只会进行一次匹配,还有其他表示: /i 忽略大小写,/m 多行匹配

代码整理

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 time
import random
import requests

from base64 import b64encode


default_headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
"Accept-Encoding": "gzip, deflate, br",
}


def oZ(a):
t = "0000000c735d856"
n = len(t)
e = []

for i in range(len(a)):
e.append(chr(ord(a[i]) ^ ord(t[(i + 10) % n])))
return "".join(e)

def base64_encode(s: str):
return b64encode(s.encode()).decode()

def gen_token(url, params):
baseURL = "https://api.qimai.cn"
f = random.randint(100, 4000)
o = int(time.time() * 1000) - f - 1515125653845

r = params.values()
r = "".join(sorted(r))
r = base64_encode(r)
r += "@#" + url.replace(baseURL, "")
r += "@#" + str(o)
r += "@#1"
a = base64_encode(oZ(r))
return a


def main():
url = "https://api.qimai.cn/rank/indexSnapshot"
params = {
"brand": "paid",
"device": "iphone",
"country": "cn",
"genre": "6014",
"date": "2022-05-13",
"page": "1",
"is_rank_index": "1"
}
params['analysis'] = gen_token(url, params)
resp = requests.get(url, params=params, headers=default_headers)
print(resp.json())


if __name__ == "__main__":
main()

运行结果: