逆向目标网站 www.endata.com.cn/BoxOffice/B…
数据加密问题
我们先看一下返回的加密数据长啥样:
我们正常对这个接口发请求也能很轻松的拿到这一坨加密数据:
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,
可以看到在最后一行创建的这个对象,我们再搜一下webDES
简化还原 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’)这种,先在浏览器控制台输出看一下
就这样在js里替换下来,所以第一个if还原:
我们看一下_0x51eedc[‘pKENi’]是干嘛的,在上面
就是传入两个参数判断是否相等。
我们就需要比较一下字符串’tgg ‘和 _0x51eedc[‘wnfPa’]是否相等就行了。而_0x51eedc[‘wnfPa’]还在上面,就是’ZGz’,明显不相等,返回false。既然这个if 条件恒为false,那我们也不用看里面的逻辑了,直接看else里的,把if条件和else花括号都可以干掉了,接着往下走,接着还原;
'VMmle’就是 ‘7|1|8|9|5|2|3|6|0|4’,那这句就可以替换为:
var _0x492a62 = '7|1|8|9|5|2|3|6|0|4'.split('|')
就是以|分割开成一个数组
即
var _0x492a62 = ['7', '1', '8', '9', '5', '2', '3', '6', '0', '4']
_0x356b01 = 0x0就是_0x356b01 = 0,那个switch就是从数组7(case7)到最后的4顺序执行。 并且可以看到程序执行到最后的4的时候直接return,程序终止,所有各个case只执行了一遍,我们先看7,
它就判断一下有没有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));
接着还原,先看第一个
看一下_0x51eedc[‘GKWFf’]是个啥,
就判断是否相等。_0xa0c834明显就是我们传过来的data加密数据呀,怎么可能等于null,所以不成立,再看后面的_0x51eedc[‘MUPgQ’]是个啥:
就判断第一个参数数是否大于等于第二个参数,这里是判断10受否大于等于我们加密数据的长度,从前面我们知道我们的加密data远大于10,所以很明显也不成立。所以这个if可以直接干掉了。
接着还原往下看:
那个加密的数据,_0xa0c834看着太难受了,我们直接全局给他替换为data,
这样看着就好受多了,接着看
替换为
替换为
这一行的意思就是data的最后一位数据由16进制转化成十进制再加9就完事了。
第二行也是这样的逻辑还原
继续往下还原,data = 那一行先不管,继续往下把所有的能还原的还原出来
到这里就可以看出是要用des解密了。最后return一行就是去掉}以外的内容保留json数据。 现在可以把和data = 相关的_0x9843d3 还原一下
至此所有的逆向工作已经基本完成了。
最后一步,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代码,一旦捋清逻辑就变得很简单。
这个案例难度倒是不大,就是有点绕,需要我们耐心找寻规律,做逆向切忌心浮气躁,一步一个脚印,终会柳暗花明。