文章目录
- [1. 写在前面](#1. 写在前面)
- [1. 接口分析](#1. 接口分析)
- [2. 反混淆分析](#2. 反混淆分析)
【🏠作者主页】:吴秋霖
【💼作者介绍】:擅长爬虫与JS加密逆向分析!Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Python与爬虫领域研究与开发工作!
【🌟作者推荐】:对爬虫领域以及JS逆向分析感兴趣的朋友可以关注《爬虫JS逆向实战》《深耕爬虫领域》
未来作者会持续更新所用到、学到、看到的技术知识!包括但不限于:各类验证码突防、爬虫APP与JS逆向分析、RPA自动化、分布式爬虫、Python领域等相关文章
作者声明:文章仅供学习交流与参考!严禁用于任何商业与非法用途!否则由此产生的一切后果均与作者无关!如有侵权,请联系作者本人进行删除!
1. 写在前面
前段时间有几个小伙伴咨询过关于某漫画网站的图片数据如何下载获取,看了一下觉得这个网站蛮适合初学者或者逆向分析爱好者练手的!它涉及到反调试、数据解密、JS反混淆、Cookie反爬虫、TLS指纹的检测
分析目标:
aHR0cHM6Ly93d3cuY29sYW1hbmdhLmNvbS9tYW5nYS1tZjg3NDEyNy8xLzU2Lmh0bWw=
初看时有小伙伴也提出过使用自动化的方式来获取图片链接再下载,但是这个链接是临时的。自动化是可以的,但只能是等待所有服务端下发的图片内容加载完毕渲染呈现到页面后使用截图的方式来获取,如下所示:
1. 接口分析
打开网站准备调试分析之前是有一个反调试的,一般这种大多通过动态生成的函数或代码片段触发!然后过这种反调试的方案是很多的(还有一些大佬开源分享的绝大场景下通杀的方案)如下所示:
这里我们也是可以通过重写构造函数与其原型方法拦截且移除动态生成代码中反调试语句,代码如下所示:
javascript
(function () {
'use strict';
const OriginalFunction = Function;
Function = function (...args) {
handleDebuggerRemoval(args);
logStackTrace("Function");
return OriginalFunction(...args);
};
Function.prototype = OriginalFunction.prototype;
Function.prototype.constructor = function (...args) {
handleDebuggerRemoval(args);
logStackTrace("Function.constructor");
return OriginalFunction(...args);
};
/**
* 移除字符串参数中的 "debugger" 语句
* @param {Array} args - 参数数组
*/
function handleDebuggerRemoval(args) {
for (let i = 0; i < args.length; i++) {
if (typeof args[i] === "string") {
args[i] = args[i].replace(/debugger/g, "");
}
}
}
function logStackTrace(context) {
const stackTrace = new Error().stack;
log(`[${context}] Call Stack:`, stackTrace);
if (DEBUG?.deb === 0) {
debugger;
}
log(`[${context}] =============== End ===============`);
}
})();
过了反调试之后,我们首先去看一下发包的情况。其实初次看的话没有明确的特征告诉我们从哪里下手,只能花点时间来各方面来分析一下,如下所示:
点击可发现这个接口貌似就是图片请求加载的发包(不过注意请求的是.enc.webp)大概率是经过处理的,而且在Cookies中也是添加了某些关键的字段,如下所示:
这里猜测在后续的请求中可能是需要携带这个Cookie参数请求的
这种场景下通过经验来梳理一下流程分析我们可以从网页加载的源码中来开始,它这种实时章节的加载大概率是不断的拼接后续的漫画图来获取资源的!然后在首次请求页面资源的时候肯定有基础的数据或者一些特征可以挖掘的
这里我们过掉反调试之后重方一下页面请求(请求记得过一下TLS检测)并保证Cookie请求的时候携带了**__cf__bkm**参数,如下所示:
可以看到请求的HTML内容中有一串密文(C_DATA)这个就是需要去解密的,解密后会拿到当前漫画章节中的详情信息JSON数据
2. 反混淆分析
它这个JS代码都是经过混淆的!不要硬看,浪费时间。核心逻辑基本都在custom.js、read.js文件中,先把JS拿下来反混淆静态分析一下!找到解密C_DATA的地方,混淆代码如下所示:
整个这块拿下来先解一下混淆,静态分析就很清晰了。处理解密C_DATA的混淆源码还原之后的JS代码如下所示:
javascript
if (__cad.isInReadPage()) {
let decryptedData;
__cad.useCodeIndex = 1;
try {
decryptedData = window.devtools.jsd(
"USJZOHqNw84GoMA9",
window.devtools.jsc.enc.Base64.parse(window.C_DATA).toString(window.devtools.jsc.enc.Utf8)
);
if (decryptedData === '') {
__cad.useCodeIndex = 2;
decryptedData = window.devtools.jsd(
"c9UPIOaql84fJIoz",
window.devtools.jsc.enc.Base64.parse(window.C_DATA)
.toString(window.devtools.jsc.enc.Utf8)
);
}
window.devtools.jse(decryptedData);
} catch (error) {
__cad.useCodeIndex = 2;
decryptedData = window.devtools.jsd(
"c9UPIOaql84fJIoz",
window.devtools.jsc.enc.Base64.parse(window.C_DATA)
.toString(window.devtools.jsc.enc.Utf8)
);
}
window.devtools.jse(decryptedData);
const decodedUrls = window.devtools.jsc.enc.Base64.parse(window.image_info.urls__direct).toString(window.devtools.jsc.enc.Utf8);
window.__images_yy = decodedUrls.split("|SEPARATER|");
window.__specialDisplay = 1;
if (!window.image_info.img_type) {
window.__specialDisplay = 0;
}
}
直接在控制台把进行解密的JS代码执行可以看到明文的C_DATA数据,如下所示:
来!接下来分析一下上面还原之后的JS代码到底做了些什么。首先可以看到入口则是检测是否处于阅读页面,开始对C_DATA密文数据进行解密操作,它这个解密的逻辑基本都是一样的,先尝试使用默认的第一个密钥加B64的解码,数据钥匙解出来没有继续尝试切换使用第二个密钥!最后解密图片的URL信息并分割URL列表,最后的话是设置显示的操作
下面作者根据反混淆之后的JS代码使用Python算法来实现对C_DATA的解密操作,代码实现所示:
python
import base64
from loguru import logger
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Protocol.KDF import scrypt
def base64Decode(base64Str):
return base64.b64decode(base64Str).decode('utf-8')
def aesDecrypt(encData, key):
key_bytes = key.encode('utf-8')
cipher = AES.new(key_bytes, AES.MODE_ECB)
decrypted = unpad(cipher.decrypt(encData), AES.block_size)
return decrypted.decode('utf-8')
def jsd(key, encryptedData):
decodedData = base64Decode(encryptedData)
encData = base64.b64decode(decodedData)
return aesDecrypt(encData, key)
def decryptCData(c_data):
key1 = 'USJZOHqNw84GoMA9'
decryptedData = jsd(key1, c_data)
logger.info(f"解密数据:{decryptedData}")
if __name__ == '__main__':
c_data = '' # 密文数据
decryptCData(c_data)
这里直接到浏览扣一个加密数据丢进去测试,得到运行如下所示:
通过下面混淆代码调试标记出来的的几处不难发现大致的流程
对混淆的JS代码简单做一下还原可以更加直观有效的帮助分析。__cad[_0x3b6833(0x591)]实则就是一个setCookieValue的操作,通过获取上面JSON数据中的enc_code2跟enc_code1的值来对下面Cookies中的值进行一个解密操作,如下所示:
接下来,针对还原后的JS代码来进行分析,代码如下所示:
javascript
let decryptedValue = window.devtools.jsd(
_0x447fdd,
window.devtools.jsc.enc.Base64.parse(window.mh_info.enc_code2).toString(window.devtools.jsc.enc.Utf8)
);
if (decryptedValue === '') {
decryptedValue = window.devtools.jsd("RMjidK1Dgv0Ojuhm", window.devtools.jsc.enc.Base64.parse(window.mh_info.enc_code2).toString(window.devtools.jsc.enc.Utf8));
}
if (!decryptedValue.startsWith(mh_info.mhid + '/')) {
decryptedValue = window.devtools.jsd("RMjidK1Dgv0Ojuhm", window.devtools.jsc.enc.Base64.parse(window.mh_info.enc_code2).toString(window.devtools.jsc.enc.Utf8));
}
let cookieOptions = { "expires": 0.005 };
__cad.cookie(_0x29107e, decryptedValue, cookieOptions);
let decryptedValue2 = window.devtools.jsd(
_0x447fdd,
window.devtools.jsc.enc.Base64.parse(window.mh_info.enc_code1).toString(window.devtools.jsc.enc.Utf8)
);
if (decryptedValue2 === '') {
decryptedValue2 = window.devtools.jsd("HNoYX7fJXcM1PWAK", window.devtools.jsc.enc.Base64.parse(window.mh_info.enc_code1).toString(window.devtools.jsc.enc.Utf8));
}
// 转换解密后的值为整数
let valueAsInt = parseInt(decryptedValue2);
// 如果转换失败(NaN),再次尝试解密
if (String(valueAsInt) === "NaN") {
decryptedValue2 = window.devtools.jsd("HNoYX7fJXcM1PWAK", window.devtools.jsc.enc.Base64.parse(window.mh_info.enc_code1).toString(window.devtools.jsc.enc.Utf8));
}
// 存储第二个cookie
let cookieOptions2 = { "expires": 0.005 };
__cad.cookie(_0x3ee2e4, decryptedValue2, cookieOptions2);
通过对上面还原后的JS代码进行静态分析可以发现,初始化的时候是给了一个密钥,然后假设解密是空的,就会使用默认的密钥进行解密!如果解密值不符合预期(不以mh_info.mhid/
开头),则重试解密,enc_code1的流程差不多
接下来我们看一下devtools.jsd的解密算法调用,用的什么
这里我们根据调试以及反混淆后的JS代码还原一下对mh_info参数中的字段解密,加密算法如下所示:
javascript
const CryptoJS = require('crypto-js');
function aesDecrypt(encData, key) {
const parsedKey = CryptoJS.enc.Utf8.parse(key);
const decrypted = CryptoJS.AES.decrypt(encData, parsedKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return CryptoJS.enc.Utf8.stringify(decrypted);
}
function parseBase64(encodedStr) {
return CryptoJS.enc.Base64.parse(encodedStr);
}
function decryptProcess(encCode1, encCode2, pageId, mhId) {
const key1 = "ZsfOA40m7kWjodMH";
const parsedEncCode2 = parseBase64(encCode2).toString(CryptoJS.enc.Utf8);
const parsedEncCode1 = parseBase64(encCode1).toString(CryptoJS.enc.Utf8);
let decryptedEncCode2;
try {
decryptedEncCode2 = aesDecrypt(parsedEncCode2, key1);
if (!decryptedEncCode2 || !decryptedEncCode2.startsWith(`${mhId}/`)) {
decryptedEncCode2 = aesDecrypt(parsedEncCode2, key2);
}
} catch (e) {
decryptedEncCode2 = aesDecrypt(parsedEncCode2, key2);
}
return {
cookie: { key: `_tkb_${pageId}`, value: decryptedEncCode2 },
};
}
// 测试数据
const mh_info = {
"startimg": 1,
"enc_code1": "cDJSdkkyUFUzbVZrUXZ1S213TFBuQT09",
"mhid": "873947",
"enc_code2": "Q1FrNTVrRGZHZjhQM3dEdkg0cU4vYnVmTU9RWjBWdzMzYmhYSlpyKzM0QjN3cmxFSTdYV1VVWUlXRkNMVHhhNw==",
"mhname": "捉刀人",
"pageid": 7557687,
"pagename": "56",
"pageurl": "1/57.html",
"readmode": 3,
"maxpreload": 10,
"defaultminline": 1,
"domain": "img.colamanga.com",
"manga_size": "",
"default_price": 0,
"price": 0,
"use_server": "",
"webPath": "/manga-mf874127/"
};
const result = decryptProcess(mh_info.enc_code1, mh_info.enc_code2, mh_info.pageid, mh_info.mhid);
console.log(result);
注意一下上面算法解密所使用到的AES密钥是每天都在更新的哈
解决完Cookie生成解密后我们来看最终的图片如何才能去下载的!从前往后分析的话已经拿到了C_DATA数据并解密,通过对解密数据中的Key成功解密获取到Cookie参数,下面就需要知道完整的图片地址,携带Cookie去请求即可,如下继续分析:
图片地址生存获取的JS混淆代码同样需要还原,还原如下所示:
javascript
window.getpice = function (pageIndex) {
let imageUrl = '';
if (!window.image_info.img_type) {
let currentLine = window.lines[chapter_id].use_line;
let imageIndex = parseInt(window.mh_info.startimg) + pageIndex - 1;
let fileName = __cr.PrefixInteger(imageIndex, 4) + ".jpg";
if (window.image_info.imgKey != undefined && window.image_info.imgKey !== '') {
fileName = __cr.PrefixInteger(imageIndex, 4) + ".enc.webp";
}
let baseDomain;
let sanitizedDomain = currentLine.replace("img.", '');
sanitizedDomain = document.domain.replace("www.", '');
let cookieValue = __cad.getCookieValue();
let pageId = mh_info.pageid;
let cookieKey = cookieValue[0] + pageId.toString();
let encodedPath = __cad.cookie(cookieKey);
if (encodedPath == null) {
__cad.setCookieValue();
encodedPath = __cad.cookie(cookieKey);
}
if (mh_info.use_server === '') {
baseDomain = `//img.${sanitizedDomain}/comic/${encodeURI(encodedPath)}${fileName}`;
} else {
baseDomain = `//img${mh_info.use_server}.${sanitizedDomain}/comic/${encodeURI(encodedPath)}${fileName}`;
}
imageUrl = baseDomain;
} else {
let imagePath = window.__images_yy[pageIndex - 1];
if (window.image_info.img_type === '1') {
imageUrl = __cr.switchWebp(imagePath, window.mh_info.manga_size);
} else {
imageUrl = imagePath;
}
}
return imageUrl;
};
先获取当前章节的线路信息再计算图片序号,根据序号生成图片文件名JPG然后替换它的主域名。其中也进行了一些Cookie的设置操作最终拿到完整图片路径
最后的图片数据则是通过AES解密二进制图片数据,然后就可以直接下载了!_0x1d85d5是密文对象,包含了加密的图片数据,解密的结果**_0x5183f2**则是图片的二进制数据(WordArray类型)
javascript
var key = "KZTC0WwWqyeStZD2";
var _0x5183f2 = window.CryptoJS.AES.decrypt(_0x1d85d5, key, {
'iv': window.CryptoJS.enc.Utf8.parse("0000000000000000"),
'mode': window.CryptoJS.mode.CBC,
'padding': window.CryptoJS.pad.Pkcs7
});
貌似不携带Cookie里面的参数也是可以的,感兴趣的自己尝试