【作者主页】:吴秋霖
【作者介绍】:Python领域优质创作者、阿里云博客专家、华为云享专家。长期致力于Python与爬虫领域研究与开发工作!
【作者推荐】:对JS逆向感兴趣的朋友可以关注《爬虫JS逆向实战》,对分布式爬虫平台感兴趣的朋友可以关注《分布式爬虫平台搭建与开发实战》
还有未来会持续更新的验证码突防、APP逆向、Python领域等一系列文章
??打开网站,随意点击票房板块抓包查看接口请求,响应数据是加密的,也是本次需要分析解决的
这里直接根据请求的堆栈去分析,或者也可以通过搜索加密接口的方式找到JS文件,如下两种方式均可:
通过请求堆栈跟踪来到PostAPI代码处,在此处挂上断点再次刷新页面,如下所示:
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的第一个字符是{就调用JSON.parse解析,显然这个地方逻辑有点多余,数据本身就是加密的!可能为了适配不同场景的可能
另外一个分支就是调用webInstace.shell去解密加密之后的数据e
我们能够直接通过数据接口拿到加密后的数据,接口请求的参数也均没有加密,那么要做的就是直接分析webInstace.shell方法实现对数据的解密
鼠标移动至webInstace.shell方法处,点击对应JS调用跳转找到加密函数的具体实现代码,如下所示:
解密方法经过了OB混淆,第一种咱们可直接复制webDES方法,执行这个方法,根据提示补齐所有缺失的属性即可
这里直接将webDES.min.js?v=1.0.0的整个JS代码全部扣下来
JS解密代码扣下来后,编写Python请求接口测试代码调用扣下来的JS解密方法进行测试,代码实现如下:
import execjs
import requests
def fetch_data_from_api():
url = "https://www.endata.com.cn/API/GetData.ashx"
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"
}
data = {
"year": 2023,
"MethodName": "BoxOffice_GetYearInfoData"
}
response = requests.post(url, headers=headers, data=data)
response.raise_for_status()
return response.text
def execute_js_code(ori_data):
with open("endata.js", 'r') as f:
js_code = f.read()
return execjs.compile(js_code).call("webInstace.shell", ori_data)
def main():
try:
encipher_data = fetch_data_from_api()
result = execute_js_code(encipher_data)
print(result)
except requests.RequestException as e:
print(f"请求异常:{e}")
except Exception as e:
print(f"执行 JavaScript 代码时发生错误:{e}")
if __name__ == "__main__":
main()
运行上面Python代码出现如下异常错误:
execjs._exceptions.ProgramError: ReferenceError: navigator is not defined
异常提示JS代码中存在一个navigator属性,是环境属性,作用是用来检测请求是否为浏览器发送!这个在控制台根据混淆的JS代码调试一下,再手动补上
如上图显示可以看到navigator检测有UserAgent字段,一行代码,放到先前扣下来的JS代码最开始位置即可,如下所示:
global.navigator = {'userAgent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0. 0 Safari/537.36"}
这个navigator也可以选择不补它,直接把JS混淆内代码删除即可,删除如下代码即可:
if (!navigator || !navigator[_0x2246('0x36', 'OMJW')])
return '';
最后我们再次运行上面Python代码,测试结果如下:
??JS代码文件目前有2000+行,且都是混淆的,其中有大部分无效可以弃用的操作逻辑,这里我们可以尝试调试分析JS混淆代码的逻辑,再使用Python重新实现!真正的核心解密算法在下面这部分代码:
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方法,控制台依次查看几个关键的调用都是啥,还原混淆后的内容,梳理清晰逻辑,过程很简单,就是很枯燥~没办法,做爬虫它就是这样,你挑的嘛!偶像!
包括混淆代码中的16进制都在控制台打印查看一下,进行还原,如下所示:
接下来对函数this[_0x2246(‘0x257’, ‘nArV’)]进行还原,控制台可以看到它就是shell方法,同时可以看到_0x51eedc定义了很多变量与函数的对应关系,方便调用
整体逻辑总的来说就是一些字符转进制操作、切片赋值、编码转字节,最终Python解密算法代码实现如下:
from Crypto.Cipher import DES
import binascii
import json
def extract_data(data, start_index, length):
"""从数据中提取特定部分并返回"""
if start_index == 0:
return data[length:]
return data[:start_index] + data[(start_index + length):]
def decrypt_data(encrypted_data):
"""解密数据并返回 JSON 对象"""
# 计算解密时使用的索引和长度
last_char_index = int(encrypted_data[-1], 16) + 9
char_at_last_index = int(encrypted_data[last_char_index], 16)
# 提取数据和密钥
intermediate_data = extract_data(encrypted_data, last_char_index, 1)
key = encrypted_data[char_at_last_index:char_at_last_index + 8]
intermediate_data = extract_data(encrypted_data, char_at_last_index, 8)
# 将密钥和数据转换为字节
key_bytes = key.encode('UTF-8')
intermediate_data_bytes = binascii.a2b_hex(intermediate_data)
# 使用 DES 解密
des_cipher = DES.new(key=key_bytes, mode=DES.MODE_ECB)
decrypted_result = des_cipher.decrypt(intermediate_data_bytes).decode('utf-8')
# 从解密结果中提取有效的 JSON 字符串并返回
json_end_index = decrypted_result.rindex('}') + 1
json_string = decrypted_result[:json_end_index]
return json.loads(json_string)
最后测试了一下两种解密方案调用速度,Python调用的速度确实要远高于扣下来的JS臃肿代码,所以说当JS代码过多的时候,能够实现加解密的逻辑可以尝试实现算法,不然直接调用JS的话效率会比较低下,附运行图如下所示:
Python解密算法
JS混淆原生算法
??好了,到这里又到了跟大家说再见的时候了。创作不易,帮忙点个赞再走吧。你的支持是我创作的动力,希望能带给大家更多优质的文章