赞
踩
抓取目标:影片相关信息
打开网页后,按F12
打开开发者工具,然后修改年份。
可以看到生成了一个XHR
的数据包,并且还包含其他影片的封面图片。
点击该数据包,查看数据包内容。可以看到该数据包返回的结果为加密数据。
现在还无法确定该数据是否为我们想要的影片信息,为了搞清楚这个问题,我们点击标头查看其请求的过程。
可以看到该数据包通过发送POST
请求到API
接口 https://www.endata.com.cn/API/GetData.ashx
,去获取数据。然后点击载荷,查看POST
请求发送的表单。
表单结构很简单,只有两个值。
year
:顾名思义即为查询的年份
MethodName
:为固定值BoxOffice_GetYearInfoData
然后查看该数据包的请求头。
从请求头和表单数据可以看出,通过POST
请求去获取该数据包的返回值很简单。难点其实是获取到的返回值是经过加密的数据,那么如何将其还原为正常的结果呢?这个就是我们这个案例需要解决的主要问题。
点击启动器,可以看到该数据包请求调用堆栈,不难看出,前两个是jquery
的JS代码。我们从第三个开始分析。
可以发现该JS代码调用的是一块ajax
代码,去发送数据。
通过其success
部分的JS代码不难分析出其逻辑。
1 == (e = "{" == e[0] ? JSON.parse(e) : JSON.parse(webInstace.shell(e))).Status || 200 == e.Code ? r(e.Data) : 200 == e.code ? r(e.data) : a(e.Msg)
该代码的逻辑是,首先判断e
的第一个字符是否是{
,如果是说明e
为JSON
数据,直接调用JSON.parse()
去解析即可;如果不是说明e为加密数据,这个时候调用的是webInstace.shell()
方法对其进行解密。到此我们就直接找到了解密方法为webInstace.shell()
。
在此打上断点,刷新网页,程序就会在断点处截断。
可以看到e
即为我们的加密数据,通过控制台我们也可以直接将其打印出来。
选中webInstace.shell
,进入到该函数内部。
进来之后我们可以看到该方法所在数据包变量名和相关逻辑十分复杂。
这是我们常见的代码混淆模式之一——OB混淆
。
有一种简单做法是直接将整个数据包复制到JS文件中,使用JS代码对其进行解密。
整个JS代码,由于文件内容比较大,在此处省略
直接运行JS代码,发现会报错。navigator is not defined
。
我们可以把该代码复制到控制台中运行查看结果。
该代码的作用是检测请求是否是浏览器发送的,并且是否有userAgent
字段。如果条件成立直接返回空。这里我们是要对结果进行解密,这个条件肯定是永远不成立的,直接删除该部分代码即可。
重新运行代码。可以发现返回结果已经正常。
接下来编写Python代码调用JS对获取的加密数据进行解析即可。
from functools import partial # 这个函数可以锁定一个函数的参数 import subprocess # 固定subprocess.Popen的encoding参数为UTF-8 subprocess.Popen = partial(subprocess.Popen, encoding='utf-8') import requests import execjs url = 'https://www.endata.com.cn/API/GetData.ashx' data = { 'year': 2023, 'MethodName': 'BoxOffice_GetYearInfoData' } headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36', } res = requests.post(url, data=data, headers=headers).text # 解密 with open('04_OB混淆_扣JS.js', 'r', encoding='UTF-8') as f: js_code = f.read() js_compile = execjs.compile(js_code) print(js_compile.call('analyse', res))
可以获取到真实数据
上述做法的特点是不用分析直接抓取整个JS文件,比较粗暴,如果JS代码过多就可能出现效率低下的问题。
接下来我们来使用分析JS代码逻辑,使用Python代码按照其逻辑对其进行改写的方式解密数据。
我们把webDES
类复制到一个JS文件中,对其进行逻辑分析,还原代码。
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; } } } ; }
将其整理一下可以看到该类的结果十分简单,一共就定义了一个变量和两个方法。
先不管上面的变量和方法,分析最后一个方法,将其展开。
现在这个代码无法直接对其进行逻辑分析,因为开发者使用工具对其进行了混淆,变量名和方法名都是一些比较长的字符。现在要做的是将其变量名简化。
第一个要做的是将_0x2246
进行还原改写。
我们通过控制台可以对其进行还原。可以发现其实这些内容就是字符串。
我们一个个对其进行还原即可。过程很简单,但是重复次数多。
var webDES = function () { var _0x4da59e = { 'bUIIa': function _0x2a2af9(_0x779387, _0x4a4fec) { return _0x779387 + _0x4a4fec; } }; var _0x9843d3 = function (_0x29d556, _0xcc6df, _0x3d7020) { if (0x0 == _0xcc6df) return _0x29d556['substr'](_0x3d7020); var _0x48914b; _0x48914b = '' + _0x29d556['substr'](0x0, _0xcc6df); return _0x48914b += _0x29d556['substr'](_0x4da59e['bUIIa'](_0xcc6df, _0x3d7020)); }; 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['pKENi']('tgg', _0x51eedc['wnfPa'])) { this['_append'](a); return this['_process'](); } else { var _0x492a62 = _0x51eedc['VMmle']['split']('|') , _0x356b01 = 0x0; while (!![]) { switch (_0x492a62[_0x356b01++]) { case '0': _0x554c90 = _grsa_JS['DES']['decrypt']({ 'ciphertext': _grsa_JS['enc']['Hex']['parse'](_0xa0c834) }, _0x2cf8ae, { 'iv': _0x554c90, 'mode': _grsa_JS['mode']['ECB'], 'padding': _grsa_JS['pad']['Pkcs7'] })['toString'](_grsa_JS['enc']['Utf8']); continue; case '1': if (_0x51eedc['GKWFf'](null, _0xa0c834) || _0x51eedc['MUPgQ'](0x10, _0xa0c834['length'])) return _0xa0c834; continue; case '2': _0xa0c834 = _0x9843d3(_0xa0c834, _0x2cf8ae, 0x8); continue; case '3': _0x2cf8ae = _grsa_JS['enc']['Utf8']['parse'](_0x554c90); continue; case '4': return _0x554c90['substring'](0x0, _0x51eedc['hLXma'](_0x554c90['lastIndexOf']('}'), 0x1)); case '5': _0x554c90 = _0xa0c834['substr'](_0x2cf8ae, 0x8); continue; case '6': _0x554c90 = _grsa_JS['enc']['Utf8']['parse'](_0x554c90); continue; case '7': if (!navigator || !navigator['userAgent']) return ''; continue; case '8': var _0x554c90 = _0x51eedc['JdOlO'](_0x51eedc['qrTpg'](parseInt, _0xa0c834[_0x51eedc['pdmMk'](_0xa0c834['length'], 0x1)], 0x10), 0x9) , _0x2cf8ae = _0x51eedc['xVKWW'](parseInt, _0xa0c834[_0x554c90], 0x10); continue; case '9': _0xa0c834 = _0x9843d3(_0xa0c834, _0x554c90, 0x1); continue; } break; } } } ; }
其中还有一些16进制的数值,也对其进行还原。
还原后如下:
var webDES = function () { var _0x4da59e = { 'bUIIa': function _0x2a2af9(_0x779387, _0x4a4fec) { return _0x779387 + _0x4a4fec; } }; var _0x9843d3 = function (_0x29d556, _0xcc6df, _0x3d7020) { if (0x0 == _0xcc6df) return _0x29d556['substr'](_0x3d7020); var _0x48914b; _0x48914b = '' + _0x29d556['substr'](0, _0xcc6df); return _0x48914b += _0x29d556['substr'](_0x4da59e['bUIIa'](_0xcc6df, _0x3d7020)); }; 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['pKENi']('tgg', _0x51eedc['wnfPa'])) { this['_append'](a); return this['_process'](); } else { var _0x492a62 = _0x51eedc['VMmle']['split']('|') , _0x356b01 = 0; while (!![]) { switch (_0x492a62[_0x356b01++]) { case '0': _0x554c90 = _grsa_JS['DES']['decrypt']({ 'ciphertext': _grsa_JS['enc']['Hex']['parse'](_0xa0c834) }, _0x2cf8ae, { 'iv': _0x554c90, 'mode': _grsa_JS['mode']['ECB'], 'padding': _grsa_JS['pad']['Pkcs7'] })['toString'](_grsa_JS['enc']['Utf8']); continue; case '1': if (_0x51eedc['GKWFf'](null, _0xa0c834) || _0x51eedc['MUPgQ'](16, _0xa0c834['length'])) return _0xa0c834; continue; case '2': _0xa0c834 = _0x9843d3(_0xa0c834, _0x2cf8ae, 8); continue; case '3': _0x2cf8ae = _grsa_JS['enc']['Utf8']['parse'](_0x554c90); continue; case '4': return _0x554c90['substring'](0, _0x51eedc['hLXma'](_0x554c90['lastIndexOf']('}'), 1)); case '5': _0x554c90 = _0xa0c834['substr'](_0x2cf8ae, 8); continue; case '6': _0x554c90 = _grsa_JS['enc']['Utf8']['parse'](_0x554c90); continue; case '7': if (!navigator || !navigator['userAgent']) return ''; continue; case '8': var _0x554c90 = _0x51eedc['JdOlO'](_0x51eedc['qrTpg'](parseInt, _0xa0c834[_0x51eedc['pdmMk'](_0xa0c834['length'], 1)], 16), 9) , _0x2cf8ae = _0x51eedc['xVKWW'](parseInt, _0xa0c834[_0x554c90], 16); continue; case '9': _0xa0c834 = _0x9843d3(_0xa0c834, _0x554c90, 1); continue; } break; } } } ; }
然后分析shell
函数,该函数首先定义了一个变量_0x51eedc
,该变量为对象类型。定义了很多变量与函数的对应关系,便于后续对其进行调用。
这里的函数功能都比较简单,我们可以直接去分析对象下方的代码逻辑,遇到调用_0x51eedc
的内容再回过来看即可。
if (_0x51eedc['pKENi']('tgg', _0x51eedc['wnfPa'])) {
this['_append'](a);
return this['_process']();
}
该代码调用了_0x51eedc
中的wnfPa
和pKENi
经过查询可以知道wnfPa
为固定值ZGz
,pKENi
为函数(功能是比较两个值是否相等,返回true或者false)
所以这里的这个代码实现的功能是判断tgg
和ZGz
的值是否一样。明显这个条件是永远无法成立的。删除即可。
使用类似这样的方法对其他代码进行分析,将代码进行删除和简化。
var webDES = function () { var _0x4da59e = { 'bUIIa': function _0x2a2af9(_0x779387, _0x4a4fec) { return _0x779387 + _0x4a4fec; } }; var _0x9843d3 = function (_0x29d556, _0xcc6df, _0x3d7020) { if (0 == _0xcc6df) return _0x29d556['substr'](_0x3d7020); var _0x48914b; _0x48914b = '' + _0x29d556['substr'](0, _0xcc6df); _0x48914b += _0x29d556['substr'](_0xcc6df + _0x3d7020); return _0x48914b }; this['shell'] = function (_0xa0c834) { // 死循环 while (true) { var _0x554c90 = parseInt(_0xa0c834[_0xa0c834['length'] - 1], 16) + 9 var _0x2cf8ae = parseInt(_0xa0c834[_0x554c90], 16); _0xa0c834 = _0x9843d3(_0xa0c834, _0x554c90, 1); _0x554c90 = _0xa0c834['substr'](_0x2cf8ae, 8); _0xa0c834 = _0x9843d3(_0xa0c834, _0x2cf8ae, 8); _0x2cf8ae = _grsa_JS['enc']['Utf8']['parse'](_0x554c90); _0x554c90 = _grsa_JS['enc']['Utf8']['parse'](_0x554c90); _0x554c90 = _grsa_JS['DES']['decrypt']({ 'ciphertext': _grsa_JS['enc']['Hex']['parse'](_0xa0c834) }, _0x2cf8ae, { 'iv': _0x554c90, 'mode': _grsa_JS['mode']['ECB'], 'padding': _grsa_JS['pad']['Pkcs7'] })['toString'](_grsa_JS['enc']['Utf8']); continue; return _0x554c90['substring'](0, _0x554c90['lastIndexOf']('}') + 1); } } ; }
然后使用编辑器的替换功能,将复杂变量替换为简单变量。
_0x51eedc--> a
_0x554c90--> b
_0x2cf8ae--> c
_0x9843d3--> func
再把func
内部的局部变量也进行简化。
_0x29d556--> a
_0xcc6df--> b
_0x3d7020--> c
_0x48914b--> result
现在代码就变得简单很多,如下所示
var webDES = function () { var func = function (a, b, c) { if (0 == b) return a['substr'](c); var result; result = '' + a['substr'](0, b); result += a['substr'](b + c); return result }; this['shell'] = function (a) { var b = parseInt(a[a['length'] - 1], 16) + 9 var c = parseInt(a[b], 16); a = func(a, b, 1); b = a['substr'](c, 8); a = func(a, c, 8); c = _grsa_JS['enc']['Utf8']['parse'](b); b = _grsa_JS['enc']['Utf8']['parse'](b); b = _grsa_JS['DES']['decrypt']({ 'ciphertext': _grsa_JS['enc']['Hex']['parse'](a) }, c, { 'iv': b, 'mode': _grsa_JS['mode']['ECB'], 'padding': _grsa_JS['pad']['Pkcs7'] })['toString'](_grsa_JS['enc']['Utf8']); return b['substring'](0, b['lastIndexOf']('}') + 1); }; }
对其进行解读添加注释如下
var webDES = function () { var func = function (a, b, c) { if (0 == b) return a['substr'](c); var result; result = '' + a['substr'](0, b); result += a['substr'](b + c); return result }; this['shell'] = function (a) { // 获取a的最后一个字符将其转化为10进制数值+9赋值为b var b = parseInt(a[a['length'] - 1], 16) + 9 // 将a的b位置的字符转化为10进制赋值为c var c = parseInt(a[b], 16); // 运行函数获得新值赋值给a a = func(a, b, 1); // 对新值进行切片,从c位置开始切片获取8个值赋值给b b = a['substr'](c, 8); // 再次调用函数获取新值赋值给a a = func(a, c, 8); c = _grsa_JS['enc']['Utf8']['parse'](b); // 将b转化使用UTF-8编码方式转化为字节赋值给c b = _grsa_JS['enc']['Utf8']['parse'](b); // 将b转化使用UTF-8编码方式转化为字节赋值给b b = _grsa_JS['DES']['decrypt']({ 'ciphertext': _grsa_JS['enc']['Hex']['parse'](a) // 将a使用16进制编码方式转化为字节 }, c, { 'iv': b, // 解密密钥,由于是ECB编码这里b和c其实是一样的,在Python中不用写iv。 'mode': _grsa_JS['mode']['ECB'], // 解密方法 'padding': _grsa_JS['pad']['Pkcs7'] // 填充 })['toString'](_grsa_JS['enc']['Utf8']); // 转化为UTF-8格式 return b['substring'](0, b['lastIndexOf']('}') + 1); // 删除多余的结果,将}后面的字符都删除 }; }
接下来使用Python语言按照其逻辑进行改写。
# endata,ob混淆 # https://www.endata.com.cn/BoxOffice/BO/Year/index.html from Crypto.Cipher import DES import binascii import json import requests def func(a, b, c): if 0 == b: return a[c:] result = a[:b] result += a[(b+c):] return result def shell(a): b = int(a[-1], 16) + 9 c = int(a[b], 16) a = func(a, b, 1) b = a[c:c+8] a = func(a, c, 8) # 使用UTF-8编码转化为字节 c = b.encode('UTF-8') b = b.encode('UTF-8') a = binascii.a2b_hex(a) # 将16进制数据还原成字节 # DES解密 des = DES.new(key=c, mode=DES.MODE_ECB) result = des.decrypt(a).decode('utf-8') # 删除最右侧}后面的内容 result = result[:result.rindex('}') + 1] return json.loads(result) if __name__ == '__main__': url = 'https://www.endata.com.cn/API/GetData.ashx' data = { 'year': 2023, 'MethodName': 'BoxOffice_GetYearInfoData' } headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36', } res = requests.post(url, data=data, headers=headers).text print(shell(res))
运行结果如下:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。