JS逆向之电影数据

95 阅读8分钟

逆向目标网站 www.endata.com.cn/BoxOffice/B…

数据加密问题

我们先看一下返回的加密数据长啥样:

image.png 我们正常对这个接口发请求也能很轻松的拿到这一坨加密数据:

import requests

headers = {
    'Connection': 'keep-alive',
    'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="98", "Google Chrome";v="98"',
    'Accept': 'text/plain, */*; q=0.01',
    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
    'X-Requested-With': 'XMLHttpRequest',
    'sec-ch-ua-mobile': '?0',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36',
    'sec-ch-ua-platform': '"Windows"',
    'Origin': 'https://www.endata.com.cn',
    'Sec-Fetch-Site': 'same-origin',
    'Sec-Fetch-Mode': 'cors',
    'Sec-Fetch-Dest': 'empty',
    'Accept-Language': 'zh-CN,zh;q=0.9',
}

data = {
    'year': '2022',
    'MethodName': 'BoxOffice_GetYearInfoData',
}

response = requests.post('https://www.endata.com.cn/API/GetData.ashx', headers=headers, data=data)
print(response.text)
#这边打印出来是一长串加密符号

分析接口,断点调试

问题的关键就是咱们怎么解密,我们直接跟栈看一下,它必定会发ajax请求 在这里插入图片描述
点进去
在这里插入图片描述
打个断点调试一下看看,不出意外应该就是else里面对数据解密
在这里插入图片描述
继续往下走一步
在这里插入图片描述
果然数据过了else,已经解密出来了,在此我们就可以断定webInstace.shell(data)是对数据进行解密的。 我们直接进入这个方法里面:
在这里插入图片描述
到这里可能看着一脸懵逼,觉得无从下手,不要懵,继续分析逻辑,哪个变量不认识直接在控制台输出一下看看,就像这样
在这里插入图片描述 _0x2246(‘0x257’, ‘nArV’)这个东西运算出来的最终结果也就是一个shell字符串,我们知道在js里面, this[‘shell’] 和 this.shell是一样的,所以第2173行就是this.shell = function(_0xa0c834),

所有这个this.shell就是我们之前见到的webInstace.shell,我们可以在这个js文件里面搜一下webInstace,

image.png 可以看到在最后一行创建的这个对象,我们再搜一下webDES

image.png

简化还原 js 所以我们可以在这里直接把webDES全部抠出来,慢慢分析

var webDES = function() {
    var _0x4da59e = {
        'bUIIa': function _0x2a2af9(_0x779387, _0x4a4fec) {
            return _0x779387 + _0x4a4fec;
        }
    };
    var _0x9843d3 = function(_0x29d556, _0xcc6df, _0x3d7020) {
        if (0x0 == _0xcc6df)
            return _0x29d556[_0x2246('0x254', '4VZ$')](_0x3d7020);
        var _0x48914b;
        _0x48914b = '' + _0x29d556[_0x2246('0x255', 'GL3Q')](0x0, _0xcc6df);
        return _0x48914b += _0x29d556['substr'](_0x4da59e[_0x2246('0x256', 'DK[&')](_0xcc6df, _0x3d7020));
    };
    this[_0x2246('0x257', 'nArV')] = function(_0xa0c834) {
        var _0x51eedc = {
            'pKENi': function _0x2f627(_0x5b6f5a, _0x440924) {
                return _0x5b6f5a === _0x440924;
            },
            'wnfPa': 'ZGz',
            'VMmle': '7|1|8|9|5|2|3|6|0|4',
            'GKWFf': function _0x1a4e13(_0x40cfde, _0x16f3c2) {
                return _0x40cfde == _0x16f3c2;
            },
            'MUPgQ': function _0x342f0d(_0x19038b, _0x4004d6) {
                return _0x19038b >= _0x4004d6;
            },
            'hLXma': function _0x55adaf(_0x45a871, _0x161bdf) {
                return _0x45a871 + _0x161bdf;
            },
            'JdOlO': function _0x13e00a(_0x5899a9, _0x4bb34d) {
                return _0x5899a9 + _0x4bb34d;
            },
            'qrTpg': function _0x1198fb(_0x55b317, _0x22e1db, _0x1b091a) {
                return _0x55b317(_0x22e1db, _0x1b091a);
            },
            'pdmMk': function _0xe2b022(_0x4af286, _0x4c2fd4) {
                return _0x4af286 - _0x4c2fd4;
            },
            'xVKWW': function _0x1094a3(_0x5f3627, _0x2a0ac5, _0x3ad2e5) {
                return _0x5f3627(_0x2a0ac5, _0x3ad2e5);
            }
        };
        if (_0x51eedc[_0x2246('0x258', '@1Ws')](_0x2246('0x259', 'E&PI'), _0x51eedc['wnfPa'])) {
            this['_append'](a);
            return this[_0x2246('0x25a', 'GL3Q')]();
        } else {
            var _0x492a62 = _0x51eedc[_0x2246('0x25b', '&59Q')][_0x2246('0x25c', ')q#9')]('|')
              , _0x356b01 = 0x0;
            while (!![]) {
                switch (_0x492a62[_0x356b01++]) {
                case '0':
                    _0x554c90 = _grsa_JS[_0x2246('0x25d', 'E&PI')]['decrypt']({
                        'ciphertext': _grsa_JS['enc'][_0x2246('0x25e', 'sy^o')]['parse'](_0xa0c834)
                    }, _0x2cf8ae, {
                        'iv': _0x554c90,
                        'mode': _grsa_JS[_0x2246('0x16c', 'O^50')][_0x2246('0x25f', 'Who^')],
                        'padding': _grsa_JS[_0x2246('0x260', '7IfV')][_0x2246('0x261', 'E&PI')]
                    })[_0x2246('0x1c', 'yY#5')](_grsa_JS['enc'][_0x2246('0x262', ']2BX')]);
                    continue;
                case '1':
                    if (_0x51eedc[_0x2246('0x263', 'Jsmq')](null, _0xa0c834) || _0x51eedc[_0x2246('0x264', '!2eC')](0x10, _0xa0c834['length']))
                        return _0xa0c834;
                    continue;
                case '2':
                    _0xa0c834 = _0x9843d3(_0xa0c834, _0x2cf8ae, 0x8);
                    continue;
                case '3':
                    _0x2cf8ae = _grsa_JS[_0x2246('0x265', 'RQ2o')][_0x2246('0x266', '3j7z')][_0x2246('0x267', 'RQ2o')](_0x554c90);
                    continue;
                case '4':
                    return _0x554c90[_0x2246('0x268', 'cs*4')](0x0, _0x51eedc[_0x2246('0x269', 'MVsm')](_0x554c90[_0x2246('0x26a', '0J6f')]('}'), 0x1));
                case '5':
                    _0x554c90 = _0xa0c834[_0x2246('0x26b', 'UwHa')](_0x2cf8ae, 0x8);
                    continue;
                case '6':
                    _0x554c90 = _grsa_JS[_0x2246('0x26c', '4VZ$')]['Utf8']['parse'](_0x554c90);
                    continue;
                case '7':
                    if (!navigator || !navigator[_0x2246('0x26d', '0I#o')])
                        return '';
                    continue;
                case '8':
                    var _0x554c90 = _0x51eedc[_0x2246('0x26e', 'Yb4P')](_0x51eedc[_0x2246('0x26f', 'BQ5p')](parseInt, _0xa0c834[_0x51eedc[_0x2246('0x270', 'Z2VK')](_0xa0c834['length'], 0x1)], 0x10), 0x9)
                      , _0x2cf8ae = _0x51eedc[_0x2246('0x271', 'yY#5')](parseInt, _0xa0c834[_0x554c90], 0x10);
                    continue;
                case '9':
                    _0xa0c834 = _0x9843d3(_0xa0c834, _0x554c90, 0x1);
                    continue;
                }
                break;
            }
        }
    }
    ;
}

可以把第一行和最后两行干掉,我们主要分析那个为this.shell即这个js里的this[_0x2246(‘0x257’, ‘nArV’)]

this.shell = function(_0xa0c834) {
        var _0x51eedc = {
            'pKENi': function _0x2f627(_0x5b6f5a, _0x440924) {
                return _0x5b6f5a === _0x440924;
            },
            'wnfPa': 'ZGz',
            'VMmle': '7|1|8|9|5|2|3|6|0|4',
            'GKWFf': function _0x1a4e13(_0x40cfde, _0x16f3c2) {
                return _0x40cfde == _0x16f3c2;
            },
            'MUPgQ': function _0x342f0d(_0x19038b, _0x4004d6) {
                return _0x19038b >= _0x4004d6;
            },
            'hLXma': function _0x55adaf(_0x45a871, _0x161bdf) {
                return _0x45a871 + _0x161bdf;
            },
            'JdOlO': function _0x13e00a(_0x5899a9, _0x4bb34d) {
                return _0x5899a9 + _0x4bb34d;
            },
            'qrTpg': function _0x1198fb(_0x55b317, _0x22e1db, _0x1b091a) {
                return _0x55b317(_0x22e1db, _0x1b091a);
            },
            'pdmMk': function _0xe2b022(_0x4af286, _0x4c2fd4) {
                return _0x4af286 - _0x4c2fd4;
            },
            'xVKWW': function _0x1094a3(_0x5f3627, _0x2a0ac5, _0x3ad2e5) {
                return _0x5f3627(_0x2a0ac5, _0x3ad2e5);
            }
        };
        if (_0x51eedc[_0x2246('0x258', '@1Ws')](_0x2246('0x259', 'E&PI'), _0x51eedc['wnfPa'])) {
            this['_append'](a);
            return this[_0x2246('0x25a', 'GL3Q')]();
        } else {
            var _0x492a62 = _0x51eedc[_0x2246('0x25b', '&59Q')][_0x2246('0x25c', ')q#9')]('|')
              , _0x356b01 = 0x0;
            while (!![]) {
                switch (_0x492a62[_0x356b01++]) {
                case '0':
                    _0x554c90 = _grsa_JS[_0x2246('0x25d', 'E&PI')]['decrypt']({
                        'ciphertext': _grsa_JS['enc'][_0x2246('0x25e', 'sy^o')]['parse'](_0xa0c834)
                    }, _0x2cf8ae, {
                        'iv': _0x554c90,
                        'mode': _grsa_JS[_0x2246('0x16c', 'O^50')][_0x2246('0x25f', 'Who^')],
                        'padding': _grsa_JS[_0x2246('0x260', '7IfV')][_0x2246('0x261', 'E&PI')]
                    })[_0x2246('0x1c', 'yY#5')](_grsa_JS['enc'][_0x2246('0x262', ']2BX')]);
                    continue;
                case '1':
                    if (_0x51eedc[_0x2246('0x263', 'Jsmq')](null, _0xa0c834) || _0x51eedc[_0x2246('0x264', '!2eC')](0x10, _0xa0c834['length']))
                        return _0xa0c834;
                    continue;
                case '2':
                    _0xa0c834 = _0x9843d3(_0xa0c834, _0x2cf8ae, 0x8);
                    continue;
                case '3':
                    _0x2cf8ae = _grsa_JS[_0x2246('0x265', 'RQ2o')][_0x2246('0x266', '3j7z')][_0x2246('0x267', 'RQ2o')](_0x554c90);
                    continue;
                case '4':
                    return _0x554c90[_0x2246('0x268', 'cs*4')](0x0, _0x51eedc[_0x2246('0x269', 'MVsm')](_0x554c90[_0x2246('0x26a', '0J6f')]('}'), 0x1));
                case '5':
                    _0x554c90 = _0xa0c834[_0x2246('0x26b', 'UwHa')](_0x2cf8ae, 0x8);
                    continue;
                case '6':
                    _0x554c90 = _grsa_JS[_0x2246('0x26c', '4VZ$')]['Utf8']['parse'](_0x554c90);
                    continue;
                case '7':
                    if (!navigator || !navigator[_0x2246('0x26d', '0I#o')])
                        return '';
                    continue;
                case '8':
                    var _0x554c90 = _0x51eedc[_0x2246('0x26e', 'Yb4P')](_0x51eedc[_0x2246('0x26f', 'BQ5p')](parseInt, _0xa0c834[_0x51eedc[_0x2246('0x270', 'Z2VK')](_0xa0c834['length'], 0x1)], 0x10), 0x9)
                      , _0x2cf8ae = _0x51eedc[_0x2246('0x271', 'yY#5')](parseInt, _0xa0c834[_0x554c90], 0x10);
                    continue;
                case '9':
                    _0xa0c834 = _0x9843d3(_0xa0c834, _0x554c90, 0x1);
                    continue;
                }
                break;
            }
        }
    }

_0x51eedc这个先不看,就正常声明一个对象。

接下来我们的工作就是重复性的还原变量了,比如在下面if里面有_0x2246(‘0x258’, ‘@1Ws’)这种,先在浏览器控制台输出看一下

image.png

就这样在js里替换下来,所以第一个if还原:

image.png 我们看一下_0x51eedc[‘pKENi’]是干嘛的,在上面

image.png 就是传入两个参数判断是否相等。

我们就需要比较一下字符串’tgg ‘和 _0x51eedc[‘wnfPa’]是否相等就行了。而_0x51eedc[‘wnfPa’]还在上面,就是’ZGz’,明显不相等,返回false。既然这个if 条件恒为false,那我们也不用看里面的逻辑了,直接看else里的,把if条件和else花括号都可以干掉了,接着往下走,接着还原;

image.png

'VMmle’就是 ‘7|1|8|9|5|2|3|6|0|4’,那这句就可以替换为:

var _0x492a62 = '7|1|8|9|5|2|3|6|0|4'.split('|')

就是以|分割开成一个数组

image.png

var _0x492a62 = ['7', '1', '8', '9', '5', '2', '3', '6', '0', '4']

image.png _0x356b01 = 0x0就是_0x356b01 = 0,那个switch就是从数组7(case7)到最后的4顺序执行。 并且可以看到程序执行到最后的4的时候直接return,程序终止,所有各个case只执行了一遍,我们先看7,

image.png 它就判断一下有没有userAgent,其实想想程序不可能第一步到这就直接return终止,不然不是白费劲了吗?所以7啥也没干,对我们的数据没有任何影响,可以干掉了,接下来我们直接按顺序把各个case里的代码提出来就可以干掉while了

 if (_0x51eedc[_0x2246('0x263', 'Jsmq')](null, _0xa0c834) || _0x51eedc[_0x2246('0x264', '!2eC')](0x10, _0xa0c834['length']))
                        return _0xa0c834;
    
    var _0x554c90 = _0x51eedc[_0x2246('0x26e', 'Yb4P')](_0x51eedc[_0x2246('0x26f', 'BQ5p')](parseInt, _0xa0c834[_0x51eedc[_0x2246('0x270', 'Z2VK')](_0xa0c834['length'], 0x1)], 0x10), 0x9)
                      , _0x2cf8ae = _0x51eedc[_0x2246('0x271', 'yY#5')](parseInt, _0xa0c834[_0x554c90], 0x10);
    
    

     _0xa0c834 = _0x9843d3(_0xa0c834, _0x554c90, 0x1);
     
     _0x554c90 = _0xa0c834[_0x2246('0x26b', 'UwHa')](_0x2cf8ae, 0x8);
     
      _0xa0c834 = _0x9843d3(_0xa0c834, _0x2cf8ae, 0x8);
      
      _0x2cf8ae = _grsa_JS[_0x2246('0x265', 'RQ2o')][_0x2246('0x266', '3j7z')][_0x2246('0x267', 'RQ2o')](_0x554c90);
      
      _0x554c90 = _grsa_JS[_0x2246('0x26c', '4VZ$')]['Utf8']['parse'](_0x554c90);
      
      _0x554c90 = _grsa_JS[_0x2246('0x25d', 'E&PI')]['decrypt']({
                        'ciphertext': _grsa_JS['enc'][_0x2246('0x25e', 'sy^o')]['parse'](_0xa0c834)
                    }, _0x2cf8ae, {
                        'iv': _0x554c90,
                        'mode': _grsa_JS[_0x2246('0x16c', 'O^50')][_0x2246('0x25f', 'Who^')],
                        'padding': _grsa_JS[_0x2246('0x260', '7IfV')][_0x2246('0x261', 'E&PI')]
                    })[_0x2246('0x1c', 'yY#5')](_grsa_JS['enc'][_0x2246('0x262', ']2BX')]);
      
      return _0x554c90[_0x2246('0x268', 'cs*4')](0x0, _0x51eedc[_0x2246('0x269', 'MVsm')](_0x554c90[_0x2246('0x26a', '0J6f')]('}'), 0x1));

接着还原,先看第一个

image.png 看一下_0x51eedc[‘GKWFf’]是个啥,

image.png

就判断是否相等。_0xa0c834明显就是我们传过来的data加密数据呀,怎么可能等于null,所以不成立,再看后面的_0x51eedc[‘MUPgQ’]是个啥:

image.png 就判断第一个参数数是否大于等于第二个参数,这里是判断10受否大于等于我们加密数据的长度,从前面我们知道我们的加密data远大于10,所以很明显也不成立。所以这个if可以直接干掉了。

接着还原往下看:

image.png

那个加密的数据,_0xa0c834看着太难受了,我们直接全局给他替换为data,

image.png

这样看着就好受多了,接着看

image.png

替换为

image.png image.png 替换为

image.png

这一行的意思就是data的最后一位数据由16进制转化成十进制再加9就完事了。

第二行也是这样的逻辑还原

image.png

继续往下还原,data = 那一行先不管,继续往下把所有的能还原的还原出来

image.png

到这里就可以看出是要用des解密了。最后return一行就是去掉}以外的内容保留json数据。 现在可以把和data = 相关的_0x9843d3 还原一下

image.png

image.png 至此所有的逆向工作已经基本完成了。

最后一步,python还原js算法 接下来我们就用python直接还原,

代码示例:

import requests
import binascii
import json
from Crypto.Cipher import DES

headers = {
    'Connection': 'keep-alive',
    'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="98", "Google Chrome";v="98"',
    'Accept': 'text/plain, */*; q=0.01',
    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
    'X-Requested-With': 'XMLHttpRequest',
    'sec-ch-ua-mobile': '?0',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36',
    'sec-ch-ua-platform': '"Windows"',
    'Origin': 'https://www.endata.com.cn',
    'Sec-Fetch-Site': 'same-origin',
    'Sec-Fetch-Mode': 'cors',
    'Sec-Fetch-Dest': 'empty',
    'Accept-Language': 'zh-CN,zh;q=0.9',
}

data = {
    'year': '2023',
    'MethodName': 'BoxOffice_GetYearInfoData',
}

response = requests.post('https://www.endata.com.cn/API/GetData.ashx', headers=headers, data=data)
data = response.text
# print(data)

def _0x9843d3(aa,bb,cc):
    if 0 == bb:
        return aa[cc:]
    _0x48914b = '' + aa[:bb]

    _0x48914b += aa[(bb+cc):]
    return _0x48914b

a = int(data[-1],16) +9
b = int(data[a],16)
data = _0x9843d3(data, a, 1)
a = data[b:b+8]
data = _0x9843d3(data, b, 8)
b = a.encode('utf-8')
a = a.encode('utf-8')
# 16 转二进制
dt = binascii.a2b_hex(data)
des = DES.new(b,mode=DES.MODE_ECB)
res = des.decrypt(dt).decode('utf-8')
abc = res[:-26]
result = json.loads(abc)['Data']['Table']
# print(result)
for index,item in enumerate(result):
    MovieName = item['MovieName']
    Genre_Main = item['Genre_Main']
    BoxOffice = item['BoxOffice']
    AvgPrice = item['AvgPrice']
    AvgPeoPle = item['AvgPeoPle']
    Area = item['Area']
    ReleaseTime = item['ReleaseTime']

    print(f"电影排名: {index+1}")
    print(f"影片名称: {MovieName}")
    print(f"类型: {Genre_Main}")
    print(f"总票房: {BoxOffice}")
    print(f"平均票价: {AvgPrice}")
    print(f"场次人数: {AvgPeoPle}")
    print(f"Month: {Area}")
    print(f"ReleaseTime:{ReleaseTime}")
    print('-' * 20)

可以看到看着很复杂的js代码,一旦捋清逻辑就变得很简单。

这个案例难度倒是不大,就是有点绕,需要我们耐心找寻规律,做逆向切忌心浮气躁,一步一个脚印,终会柳暗花明。