背景

最近公司在做竞品数据分析服务,简单来说就是取各个应用市场的数据,然后抓到自己的数据库保存,最后提供接口给前端,做成竞品服务。就有点类似 蝉大师 。安卓的像小米华为并没有进行数据加密,还比较简单,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

}