JavaScript逆向之七麦数据实战
知识点
Promise对象
Promise对象是ES6版本中提供的,主要是为了解决死亡回调的问题。
先看一段代码:
点击查看代码
function fn() {
let username = "alex";
let password = "123456";
// 发送请求给服务器要求登录
console.log("发送请求出去,尝试登录");
setTimeout(function () {
console.log("服务器返回了一个结果");
let result_1 = true;
if(result_1===true){ //登录成功
// 加载菜单信息
console.log("准备加载菜单信息");
setTimeout(function () {
console.log("显示菜单的信息");
// 加载用户信息
console.log("准备加载用户信息");
setTimeout(function () {
console.log("显示用户信息");
},1000)
}, 1000);
}
}, 1000);
}
该代码是登录网站后网站一步步显示信息的一个demo,可以看到里面存在很多的嵌套,如果想要解决多层嵌套的问题,就可以采用Promise对象,看如下demo:
点击查看代码
function send(url) {
// promise:确保,保证
// reslove:解决了
// reject:拒绝
return new Promise(function (resolve, reject) {
console.log("帮你发送一个请求到", url); // 答应你的一件事
let result = 123;
if (result) {
// 这件事我办成了
// 接下来你要做的事应该是调用这个函数的那个人去写
resolve(i); //这里代表当前任务被解决
} else {
// 这件事没办成
reject(i); // 这里代表当前任务没解决
}
});
}
function fn() {
let username = "";
let password = "";
//发送请求到登录
send("xxxxx").then(function (data) {
console.log("登录的结果");
console.log("登录返回的结果是",data);
return send("加载菜单");
}).then(function (data) {
// 加载菜单
console.log("加载菜单得到的信息",data);
return send("加载个人信息")
}).then(function (data) {
// 加载个人信息
console.log("加载个人信息得到的信息", data);
})
}
send
函数中会返回一个Promise对象,如果成功了就会执行resolve
对应的函数,失败了就执行reject
对应的函数。在fn
函数中省略了reject
对应的函数,因为一般会在Promise对象的最后加一个catch,只要失败了,就直接走catch中的函数,把整个代码抽象一下如下:
点击查看代码
//链式逻辑
new Promise(function (a, b) {}).then(function () {
return new Promise();
},function () {
}).then(function () {
return new Promise();
}).then(function () {
}).catch(function () {
console.log("程序出错,请联系管理员....")
})
axios拦截器
axios是一个基于Promise的网络请求库,网站如果采用的是axios方法,那么加密和解密的逻辑大概率存在于axios拦截器中。
axios拦截器分为请求拦截器和响应拦截器,加密逻辑大概率在请求拦截器中,解密逻辑大概率在响应拦截器中,下面看axios拦截器使用的代码:
点击查看代码
// 请求的拦截器
axios.interceptors.request.use(function (config) {
console.log(config, "你好啊");
// 尝试修改请求参数
config.data['hehe'] = "i love you";
return config;
}, function (err) {
console.log(err);
});
// 响应的拦截器
axios.interceptors.response.use(function (response) {
console.log(response);
// 这里一般会有什么??? 解密操作
return response.data; // 拦截器返回的东西直接给到then中的函数
}, function (err) {
console.log(err);
});
上面两个知识点讲完,就该进入正篇了。
七麦数据实战
url:https://www.qimai.cn/rank
滑动页面,抓包,老样子还是看Fetch/XHR
类型的。
有三个数据包,样式都一样,看下它们的请求参数和响应数据。
这样子就知道0
对应的是付费榜,1
对应的是免费榜,2
对应的是畅销榜。既然三个请求参数都一样,那就以其中一个为例即可。
请求头中就一个analysis
参数的值是加密的,那目标就是知道该参数的值如何加密的。
按照惯例,搜索url。
总共三处地方,但这三处全是赋值操作,没有其他的代码,那么搜索url就失效了,接下来搜索analysis
关键词。
三处地方,但analysis
都位于url地址中,根本不可能是给analysis
参数赋值的,所以这也失效了,最后只能通过Initiator来找了。
明显的看到了Promise对象,就可以联想到axios拦截器了,搜索interceptors
。
也是三处,第一处是个赋值,不可能是加密逻辑,看下第二处和第三处整个的逻辑。
点击查看代码
l.prototype.request = function(e) {
"string" == typeof e ? (e = arguments[1] || {}).url = arguments[0] : e = e || {},
(e = s(this.defaults, e)).method ? e.method = e.method.toLowerCase() : this.defaults.method ? e.method = this.defaults.method.toLowerCase() : e.method = "get";
var t = [o, void 0]
, n = Promise.resolve(e);
for (this.interceptors.request.forEach((function(e) {
t.unshift(e.fulfilled, e.rejected)
}
)),
this.interceptors.response.forEach((function(e) {
t.push(e.fulfilled, e.rejected)
}
)); t.length; )
n = n.then(t.shift(), t.shift());
return n
}
先对e
进行类型判断和值的重新赋值,然后声明t
为数组和n
为Promise对象,接着两个for循环,请求拦截器中遍历往t
数组的头部插入元素,响应拦截器遍历往t
数组的尾部插入元素,可以看到遍历完成后,t
数组中总共有6个对象,最后从t
数组的头部弹出两个元素交给Promise对象的then函数执行。
根据Promise对象的then函数可以知道,会给其传两个参数,成功了执行第一个参数,失败了执行第二个参数。所以如果这里存在加密逻辑的话,那么一定在t
数组的第一个参数处,定位。
从以下三个变量的值也可以看出没找错地方。
这段代码中存在非常多的花指令,得先将其还原,打断点进行调试,还原出来的代码如下。(catch中函数就不用管了)
点击查看代码
function fn(t) {
var n;
n = i["ej"]("synct"),
s = c["default"]["prototype"]["difftime"] = -i["ej"]("syncd") || +new z["Date"] - 1000 * n;
var e, r = +new z["Date"] - (s || 0) - 1661224081041, a = [];
return void 0 === t["params"] && (t["params"] = {}),
z["Object"]["keys"](t["params"])["forEach"](function (n) {
if (n == "analysis")
return !1;
t["params"]["hasOwnProperty"](n) && a["push"](t["params"][n])
}),
a = a["sort"]()["join"](""),
a = i["cv"](a),
a = (a += "@#" + t["url"]["replace"](t["baseURL"], "")) + ("@#" + r) + ("@#" + 3),
e = i["cv"](i["oZ"](a, "xyz517cda96efgh")),
-B == t["url"]["indexOf"]("analysis") && (t["url"] += (-B != t["url"]["indexOf"]("?") ? "&" : "?") + "analysis" + "=" + z["encodeURIComponent"](e)),
t
}
接下来分析这段代码。
n = i["ej"]("synct")
用于获取cookie中synct
的值。
s = c["default"]["prototype"]["difftime"] = -i["ej"]("syncd") || +new z["Date"] - 1000 * n;
用于获取cookie中syncd
中的值,如果cookie中没有syncd
,则s=new Date()-1000*n
。
r = +new z["Date"] - (s || 0) - 1661224081041
就是计算一个时间差,这个值不是固定的,所以我们可以直接把s
的值固定,上面两行代码就没用了。
void 0 === t[Zt] && (t[Zt] = {})
就是false。
z["Object"]["keys"](t["params"])["forEach"](function (n) { if (n == "analysis") return !1; t["params"]["hasOwnProperty"](n) && a["push"](t["params"][n]) })
遍历t["params"]
中的所有键,将对应的值全部存放到a
数组中,z
是window对象,故z["Object"]["keys"]
等同于Object["keys"]
。
a = a["sort"]()["join"]("")
对a数组进行排序,并用空字符串连接。
a = i["cv"](a)
需要知道i["cv"]
是什么,等下直接把源代码复制进来即可。
a = (a += "@#" + t["url"]["replace"](t["baseURL"], "")) + ("@#" + r) + ("@#" + 3)
对a
的值进行拼接。
e = i["cv"](i["oZ"](a, "xyz517cda96efgh"))
同理,直接复制源代码。
-B == t["url"]["indexOf"]("analysis") && (t["url"] += (-B != t["url"]["indexOf"]("?") ? "&" : "?") + "analysis" + "=" + z["encodeURIComponent"](e))
判断url背后的参数是用&
还是?
连接。这里最主要的是要得到e
的值,直接返回e即可。
到目前为止,化简后的代码为:
点击查看代码
function fn(t) {
var e, r = new Date() + 226 - 1661224081041, a = [];
return false,
Object["keys"](t["params"])["forEach"](function (n) {
if (n == "analysis")
return !1;
t["params"]["hasOwnProperty"](n) && a["push"](t["params"][n])
}),
a = a["sort"]()["join"](""),
a = i["cv"](a),
a = (a += "@#" + t["url"]["replace"](t["baseURL"], "")) + ("@#" + r) + ("@#" + 3),
e = i["cv"](i["oZ"](a, "xyz517cda96efgh")),
e;
}
下面就是要去补全i["cv"]
和i["oZ"]
和这两个函数中用到的其他变量,花指令该还原就还原。
补全和还原后的代码如下:
点击查看代码
function o(n) {
t = "",
['66', '72', '6f', '6d', '43', '68', '61', '72', '43', '6f', '64', '65']["forEach"](function(n) {
t += unescape("%u00" + n)
});
var t, e = t;
return String[e](n)
}
function u() {
return unescape("861831832863830866861836861862839831831839862863839830865834861863837837830830837839836861835833"["replace"](/8/g, "%u00"))
}
var i = {
cv:function v(t) {
t = encodeURIComponent(t)["replace"](/%([0-9A-F]{2})/g, function(n, t) {
return o("0x" + t)
});
try {
return btoa(t)
} catch (n) {
return Buffer["from"](t)["toString"]("base64")
}
},
oZ:function h(n, t) {
t = t || u();
for (var e = (n = n["split"](""))["length"], r = t["length"], a = "charCodeAt", i = 0; i < e; i++)
n[i] = o(n[i][a](0) ^ t[(i + 10) % r][a](0));
return n["join"]("")
}
};
function fn(t) {
var e, r = new Date() + 226 - 1661224081041, a = [];
return false,
Object["keys"](t["params"])["forEach"](function (n) {
if (n == "analysis")
return !1;
t["params"]["hasOwnProperty"](n) && a["push"](t["params"][n])
}),
a = a["sort"]()["join"](""),
a = i["cv"](a),
a = (a += "@#" + t["url"]["replace"](t["baseURL"], "")) + ("@#" + r) + ("@#" + 3),
e = i["cv"](i["oZ"](a, "xyz517cda96efgh")),
e;
}
测试一下。
点击查看代码
var t = {
"url": "/rank/indexPlus/brand_id/1",
"method": "get",
"headers": {
"common": {
"Accept": "application/json, text/plain, */*"
},
"delete": {},
"get": {},
"head": {},
"post": {
"Content-Type": "application/x-www-form-urlencoded"
},
"put": {
"Content-Type": "application/x-www-form-urlencoded"
},
"patch": {
"Content-Type": "application/x-www-form-urlencoded"
}
},
"params": {},
"baseURL": "https://api.qimai.cn",
"transformRequest": [
null
],
"transformResponse": [
null
],
"timeout": 15000,
"withCredentials": true,
"xsrfCookieName": "XSRF-TOKEN",
"xsrfHeaderName": "X-XSRF-TOKEN",
"maxContentLength": -1,
"maxBodyLength": -1
}
console.log(fn(t));
运行结果如下:
得到了跟analysis
参数值相似的字符串,说明我们找到了加密的逻辑,接下来就可以写python代码爬取数据了,完整的python代码和JavaScript代码如下:
JavaScript代码:
点击查看代码
function o(n) {
t = "",
['66', '72', '6f', '6d', '43', '68', '61', '72', '43', '6f', '64', '65']["forEach"](function(n) {
t += unescape("%u00" + n)
});
var t, e = t;
return String[e](n)
}
function u() {
return unescape("861831832863830866861836861862839831831839862863839830865834861863837837830830837839836861835833"["replace"](/8/g, "%u00"))
}
var i = {
cv:function v(t) {
t = encodeURIComponent(t)["replace"](/%([0-9A-F]{2})/g, function(n, t) {
return o("0x" + t)
});
try {
return btoa(t)
} catch (n) {
return Buffer["from"](t)["toString"]("base64")
}
},
oZ:function h(n, t) {
t = t || u();
for (var e = (n = n["split"](""))["length"], r = t["length"], a = "charCodeAt", i = 0; i < e; i++)
n[i] = o(n[i][a](0) ^ t[(i + 10) % r][a](0));
return n["join"]("")
}
};
function fn(t) {
var e, r = new Date() + 226 - 1661224081041, a = [];
return false,
Object["keys"](t["params"])["forEach"](function (n) {
if (n == "analysis")
return !1;
t["params"]["hasOwnProperty"](n) && a["push"](t["params"][n])
}),
a = a["sort"]()["join"](""),
a = i["cv"](a),
a = (a += "@#" + t["url"]["replace"](t["baseURL"], "")) + ("@#" + r) + ("@#" + 3),
e = i["cv"](i["oZ"](a, "xyz517cda96efgh")),
e;
}
function final(url, pm) {
var params = {
"url": url,
"baseURL": "https://api.qimai.cn",
"params":pm,
};
return fn(params);
}
python代码:
点击查看代码
import subprocess
from functools import partial
subprocess.Popen = partial(subprocess.Popen, encoding="utf-8")
import execjs
import json
import requests
f = open("拦截器逻辑二.js", mode="r", encoding="utf-8")
js = execjs.compile(f.read())
f.close()
data = {
"brand": "all",
"country": "cn",
"date": "2024-03-18",
"device": "iphone",
"genre": "36",
"page": 2,
}
host = "https://api.qimai.cn"
url = "/rank/indexPlus/brand_id/1"
analysis = js.call("final", url, data)
final_url = host+url+"?analysis=" + analysis
# print(final_url)
session = requests.session()
session.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 "
"Safari/537.36",
}
# 加载最开始的cookie
session.get("https://www.qimai.cn/rank")
# 经过测试,这玩意没什么用
session.cookies["qm_check"] = "A1sdRUIQChtxen8pI0dAMRcOUFseEHBeQF0JTjVBWCwycRd1QlhAXFEGFUdeS0laHQdKAAkABAsgXyVBWD0TR1JRRAp0BQlFEBQ3TSZKFUdBbwxvBBRFIlQsSUhTFxsQU1FVV1NHXEVYVElWBRsCHAkSSQ%3D%3D"
# 开干
resp = session.get(final_url)
decoded_text = bytes(resp.text, 'utf-8').decode('unicode_escape')
print(decoded_text)
运行python代码结果如下:
成功拿到数据。
补充
在python代码中可以看到添加了cookie,虽然测试后得知这个参数没有用,但是出于学习的目的,也可以来看一下这个参数的加密逻辑。
全局搜索qm_check
。
一个都没搜到,这时候就要用到上节讲过的webhook工具了,选择Hook Setcookie
后,刷新界面。
看到val
中还没有出现qm_check
,一直放,直到看到qm_check
。
这里的qm_check
已经被加密了,要想找到加密逻辑,就得往上看,通过Call Stack往上找。
v(p(z[Y3][V3](n), s))
这段代码执行得到的结果就是加密后的字符串,并且n
和s
都是明文,所以加密逻辑肯定跟v
、p
、z
这几个有关。
找到这几个函数的实现代码。
z[Y3][V3]
相当于JSON.stringify
p
函数的实现如下。
花指令处理过后如下:(u()函数生成的是一个固定的字符串)
点击查看代码
function p(n, t) {
t = t || 'a12c0fa6ab9119bc90e4ac7700796a53';
for (var e = (n = n["split"](""))["length"], r = t["length"], a = "charCodeAt", i = 0; i < e; i++)
n[i] = o(n[i][a](0) ^ t[(i + 10) % r][a](0));
return n["join"]("")
}
v
函数的实现如下
花指令处理过后如下:
点击查看代码
function v(t) {
t = encodeURIComponent(t)["replace"](/%([0-9A-F]{2})/g, function(n, t) {
return o("0x" + t)
});
try {
return btoa(t)
} catch (n) {
return Buffer["from"](t)["toString"]("base64")
}
}
把这两个函数用到的其他函数找到,补充完整,整体代码如下:
点击查看代码
function p(n, t) {
t = t || 'a12c0fa6ab9119bc90e4ac7700796a53';
for (var e = (n = n["split"](""))["length"], r = t["length"], a = "charCodeAt", i = 0; i < e; i++)
n[i] = o(n[i][a](0) ^ t[(i + r) % r][a](0));
return n["join"]("")
}
function v(t) {
t = encodeURIComponent(t)["replace"](/%([0-9A-F]{2})/g, function(n, t) {
return o("0x" + t)
});
try {
return btoa(t)
} catch (n) {
return Buffer["from"](t)["toString"]("base64")
}
}
function o(n) {
t = "",
['66', '72', '6f', '6d', '43', '68', '61', '72', '43', '6f', '64', '65']["forEach"](function(n) {
t += unescape("%u00" + n)
});
var t, e = t;
return String[e](n)
}
var n = {
"gpu": "ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E9B) Direct3D11 vs_5_0 ps_5_0, D3D11)",
"check": "0,0,0,0,0"
};
var s = "xyz57209048efgh";
console.log(v(p(JSON.stringify(n),s)));
运行得到的结果如下:
页面上存储的值如下:
两个值相同,说明加密逻辑没有找错。