背景
最近公司在做竞品数据分析服务,简单来说就是取各个应用市场的数据,然后抓到自己的数据库保存,最后提供接口给前端,做成竞品服务。就有点类似 蝉大师 。安卓的像小米华为并没有进行数据加密,还比较简单,vivo、oppo的商店数据是进行了加密,就需要反编译,进行安卓逆向(安卓逆向本篇文章暂时不涉及),从而得到解密代码。苹果数据由于本身技术太菜以及设备限制,无法直接从苹果商店抓取。所以我干脆就不自己抓了,抓蝉大师、七麦他们的吧😄。但是在这过程中发现很多网站都做了签名加密,导致我们不能直接拼接查询字符串进行查询,为了能通用所以需要对其进行JS逆向分析。本篇文章以七麦数据为例子,帮助大家进行js逆向分析,便于后续的人员维护竞品数据抓取接口正常运行。
数据接口分析
现如今的web项目基本都是前后端分离,而前段一般是通过http请求,进行数据交互。所以打开谷歌浏览器F12(开发者工具),切换到NetWork(网络),查看请求包
如上图所示,我们要的数据就在这个请求中。把请求地址贴出来:
七麦数据接口:https://api.qimai.cn/app/commentRate?analysis=dR51TCxkW0h9SnUEYVYDBSQXGQBAQB9TX11dXQpDagVAUyETAQECAQQDDVMGCFQAdkIB&appid=303191318&country=cn
可以发现请求中有几个参数:
appid: 就是我们的竞品数据appid。
country: 因为苹果数据包含了全球各国的数据,而这个的cn代表中国区的数据
analysis:一大串,完全不知道是啥。但是从这个字符串,可以大概猜测,这是base64加密的数据。那接下来就对analysis进行分析。
入参简单分析
appid和country是明文,比较简单
而analysis我们先猜测它是base64加密过后的。那现在我们就去解密看看。说不定解密出来是明文,那我们处理就简单了,直接拼接然后base64加密就可以了。
解密完发现是乱码,果然没那么简单。那我们就只能跟进js里进行逆向分析
JS逆向分析
为啥要逆向,在我们这目的就是找到加密的代码,看看他是怎么加密的,然后去把代码抠出来放到java或者python或者其他语言去执行,在我们抓取数据的时候可以直接带上analysis,绕过七麦后端验证。
先调试下吧,现在可能有的人要问了。怎么调试呢,我们都不知道入口方法,总不能一个个去试吧。其实谷歌浏览器有针对指定url的在发送请求前进行断点调试方法,我们来看看怎么用吧
如上图所示,我们可以添加一个url断点。我之前已经添加过我需要的了(commentRate),就不添加了。
断点完,刷新
可以看到断点进到了f.send()这一步。往前找找看看入参,发现入参是e,控制台输入e,查看具体的入参信息:
可以发现,此时url已经被加上了analysis参数了。那只能往前找。找到加analysis参数的方法。所以需要查看这个方法的调用栈。查看调用栈有下面两种方法
1、右边面板
2、控制台输入console.trace()
根据调用栈(先进的在底部),一步步断点,查看analysis参数是在什么时候加密的。
发现此时的url还没有带上analysis参数,并且发现拦截器。此时我们进入拦截器看看
会发现analysis参数就是在这个方法被加上去的。那这时候我们就对这个方法单独分析。分析下这个方法的作用
主要为n.cv, n.oZ这两个方法。跟进去发现对应下图h方法,o.oZ对应下图g方法,n.cv为base64编码。出现了两次,第一次是上图6187行,作用是对排序后的params字符串进行base64编码。第二次对n.oZ返回的结果进行base64编码,所以我们主要看看g这个方法
其中入参t为固定值(是固定字符串位运算得来, 可不管, 我们程序可以直接写死)。e为拼接后的字符串 MzAzMTkxMzE4Y24=@#/app/commentRate@#112156797867@#1
所以g方法的主要作用是将入参e中的每一位和固定字符串t的第(r+10)%n位的ascii值进行异或运算,再转为对应的字符串。其中r为e的下标, n为固定字符串t的长度。
最后再通过base64将返回的结果加密,设置到入参analysis中
代码实现
# -*- coding: utf-8 -*-
# @Time : 2021/07/26 11:57
# @Author : lee
import base64
import requests
import time
import json
class QiMai(object):
def __init__(self):
self.string = '00000008d78d46a'
def params_b64(self, path, params=None):
"""
b64编码
:params = {
'appid': "303191318",
'country': "cn"
}
:param path: '/app/commentRate'
:return:
"""
if not params:
t = int(time.time() * 1000) - 276 - 1515125653845
return '@#' + path + '@#' + str(t) + '@#1'
params_list = []
for key in params:
params_list.append(params[key])
params_list.sort()
params = ''.join(params_list)
params_b64 = base64.b64encode(params.encode()).decode()
t = int(time.time() * 1000) - 276 - 1515125653845
params = params_b64 + '@#' + path + '@#' + str(t) + '@#1'
return params
def data_encrypt(self, data):
"""
加密函数 异或运算
:param data: '@#/app/commentRate@#1'
:return:
"""
data_list = list(data)
for i in range(0, len(data_list)):
data_list[i] = chr(ord(data_list[i]) ^ ord(self.string[(i + 10) % len(self.string)]))
return base64.b64encode(''.join(data_list).encode()).decode()
def data_decrypt(self, data):
"""
解密函数
:param data: 'dR51TCxkW0h9SnUEYVYDBSQXGQBAQB9TX11dXQpDagVAUyETAQECAQUHDFwGC1MFdkIB'
:return:
"""
data_list = list(base64.b64decode(data.encode()).decode())
string = ''
for i in range(len(data_list)):
data_ord = ord(data_list[i])
str_ord = data_ord ^ ord(self.string[(i + 10) % len(self.string)])
string += chr(str_ord)
return string
def str_replace(self, data):
chr(data)
if __name__ == '__main__':
params = {
'appid': "303191318",
'country': "cn"
}
path = '/app/commentRate'
Q = QiMai()
# 入参排序并进行base64加密
params_b64 = Q.params_b64(path, params)
# 获取analysis入参
analysis = Q.data_encrypt(params_b64)
print(analysis)
# 反解密
# string = Q.data_decrypt(analysis)
headers = {
"Accept": "application/json, text/plain, */*",
"Referer": "https://www.qimai.cn/",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/59.0"
}
url = 'https://api.qimai.cn' + path + '?analysis=' + analysis
r = requests.get(url, headers=headers, params=params)
print(json.loads(r.text))
运行结果
{
"code": 10000,
"msg": "成功",
"rateInfo": {
"current": {
"ratingAverage": 4.9,
"total": 848299,
"list": [
{
"name": "5星",
"num": 792916,
"percent": "93.471287836011%"
},
{
"name": "4星",
"num": 30649,
"percent": "3.6129949463574%"
},
{
"name": "3星",
"num": 8904,
"percent": "1.0496299064363%"
},
{
"name": "2星",
"num": 4487,
"percent": "0.52894085693841%"
},
{
"name": "1星",
"num": 11343,
"percent": "1.3371464542573%"
}
]
},
"all": {
"ratingAverage": 4.9,
"total": 918462,
"list": [
{
"name": "5星",
"num": 851200,
"percent": "92.676670346732%"
},
{
"name": "4星",
"num": 35332,
"percent": "3.8468657385934%"
},
{
"name": "3星",
"num": 10797,
"percent": "1.1755521730894%"
},
{
"name": "2星",
"num": 5444,
"percent": "0.59273002040367%"
},
{
"name": "1星",
"num": 15689,
"percent": "1.7081817211817%"
}
]
}
},
"is_logout": 0
}
评论