部分日本漫画站点图像混淆算法逆向与解析
标准流程
- 抓包,检查API通信格式、图像采用怎样的加密/混淆方式,有时候可以直接从抓到的响应中找到其加密/混淆方式的指示信息。
- 检查图像对应的元素是以怎样的形式呈现图像的,是
canvas?还是img但是用的blob:?- 如果图像请求有栈跟踪,就在浏览器开发者工具中查看栈跟踪,找到对应的解密流程即可。
- 如果没有,就给图像元素的父元素打断点,找到其渲染逻辑。
- 如果都难以找到其解密流程,考虑从API请求开始梳理
如果提供了非网页套壳的手机客户端,也可以尝试抓包,有的公司的手机客户端是无打乱的。
コミチ+ (Comici+)
- 固定分块数 图像打乱
- JPEG格式
使用Comici+ SaaS搭建的平台均采用统一的加密方式,只是由于部署的版本不同,在API上可能有些出入。
抓包观察
可以看到在/api/book/contentsInfo有下面这样的响应
{
"totalPages": 23,
"scrollDirection": "横",
"spreadDesignation": 2,
"result": [{
"imageUrl": "",
"scramble": "[4, 5, 1, 10, 14, 3, 2, 7, 0, 6, 15, 9, 11, 8, 12, 13]",
"sort": 0,
"width": 1260,
"height": 1780,
"expiresOn": 1770183109000
}]
}
这个scramble属于是一看就知道肯定是图像块的打乱顺序了,所以接下来只要找到这个顺序到底是怎么对应的就行了。
检查图像元素,发现是canvas形式,且图像请求没有栈跟踪,所以选择对canvas的父节点打断点。
追踪解密流程
一触发断点,马上就看到了断点位置下面的代码段
for (
var u = Math.floor(r / 4),
d = Math.floor(i / 4),
f = 0,
v = function (e, t) {
for (
var n = e.length,
r = [],
i = t.replace(/\s+/g, '').slice(1).slice(0, - 1).split(','),
o = 0;
o < n;
o++
) r.push(e[i[o]]);
return r
}(Ir, n.scramble),
p = 0;
p < 4;
p++
) for (var h = 0; h < 4; h++) {
var g = v[f][0],
m = v[f][1];
l.drawImage(e, u * g, d * m, u, d, u * p, d * h, u, d),
f++
}
梳理一下就是下面这样
pos = list()
for col in range(4):
for row in range(4):
pos.append((row, col))
scrambled_pos = [pos[i] for i in scramble]
SpeedBinB
- ヤンマガWeb (Young Manga)
- きら星ポータル (Kirapo)
- がうがうモンスター+ (GauGau Monster+)
- 动态分块数 非精确切片 图像打乱
- 复杂分块(可选)
- JPEG格式
SpeedBinb也是被多个网站采用的常用加密方式,以 ヤンマガWeb (Young Manga) 和 きら星ポータル (Kirapo) 为其两大类加密方式的代表
- 解密
ptbl/ctbl,对ptbl和ctbl密钥表进行派生,得到混淆类型和位置信息,按类型解混淆 ヤンマガWeb (Young Manga) - 直接通过API下发
ptimg.json,提供一个甚至人类可读的分块映射表 きら星ポータル (Kirapo)
只要在资源列表中看到带有speedbinb字样的JS文件,即可判断该网站采用SpeedBinb加密。
特色的API接口为bibGetCntnt...,非常有标志性
因而我们应当直接对SpeedBinb本体JS下手,从中逆向出其解密流程即可。
这玩意真的很烦人,直接丢给AI给你解混淆比我们在这分析半天快得多。
GigaViewer
- 一迅プラス (Ichijin Plus / IchiComi)
- コミック アース・スター (COMIC EARTH STAR)
- くらげバンチ (Kurage Bunch)
- HERO'S Web(コミプレ)
- 固定分块数 图像打乱
- JPEG格式
下面以 一迅プラス (Ichijin Plus / IchiComi) 为例,介绍一下我的逆向流程。
抓包观察
没有找到明显的数组和哈希值等信息,考虑固定顺序或内嵌顺序。
追踪解密流程
其图像请求有栈跟踪,直接从栈跟踪入手。
首先注意到一个名称十分引人注目:skipDrm,在脚本里搜索一下:
i.skipDrm
? this.appendPageImage(e, t, t.src)
: requestIdleCallback(
() => {
const i = (0, me.ZE)(t);
i &&
(i.classList.add("page-image", "js-page-image"),
this.appendPageImage(e, i, t.src));
},
{
timeout: 1000,
},
);
再回头检查一下,哪里有choJuGiga?
结果是内嵌在HTML的头部。观察抓包结果,HTML下发时就带有该JSON。
这也是GigaViewer的一大特征。
<script id="episode-json" type="text/json" data-value=''>
{
"readableProduct": {
"showSquareThumbnailInRecommendation": false,
"imageUrisDigest": "",
"hasPurchased": false,
"publishedAt": "",
"prevReadableProductUri": null,
"isPublic": true,
"permalink": "4",
"pageStructure": {
"pages": [
{ "type": "other" },
{
"src": "",
"width": 1688,
"contentStart": "",
"height": 2400,
"type": "main"
}
],
"choJuGiga": "baku",
"startPosition": "latter",
"readingDirection": "rtl"
},
"number": 1,
"typeName": "episode",
"toc": null,
}
}
现在我们知道,choJuGiga就表明该是否有加密,那么到底怎么解密呢?
回头仔细看看,发现const i = (0, me.ZE)(t),那我们就搜索ZE,就找到了这里:
55324: (e, t, i) => {
i.d(t, {
ZE: () => d
});
对着混淆过的代码摸不着头脑的时候,往下稍稍一看:
class a extends r.$ {
constructor(e, t) {
(super(e),
o(this, "DIVIDE_NUM", 4),
o(this, "MULTIPLE", 8),
o(this, "width", void 0),
o(this, "height", void 0),
o(this, "cell_width", void 0),
o(this, "cell_height", void 0),
(this.width = t.width),
(this.height = t.height),
(this.cell_width =
Math.floor(this.width / (this.DIVIDE_NUM * this.MULTIPLE)) *
this.MULTIPLE),
(this.cell_height =
Math.floor(this.height / (this.DIVIDE_NUM * this.MULTIPLE)) *
this.MULTIPLE));
}
solve() {
this.view.drawImage(0, 0, this.width, this.height, 0, 0);
for (let e = 0; e < this.DIVIDE_NUM * this.DIVIDE_NUM; e++) {
const t = Math.floor(e / this.DIVIDE_NUM) * this.cell_height,
i = (e % this.DIVIDE_NUM) * this.cell_width,
r = Math.floor(e / this.DIVIDE_NUM),
n = (e % this.DIVIDE_NUM) * this.DIVIDE_NUM + r,
s = (n % this.DIVIDE_NUM) * this.cell_width,
o = Math.floor(n / this.DIVIDE_NUM) * this.cell_height;
this.view.drawImage(i, t, this.cell_width, this.cell_height, s, o);
}
}
}
这里的逻辑已经很清晰了,对着重写一份即可。
マガポケ (Manga Pocket)
- Web端固定分块数 图像打乱
客户端无打乱- JPEG格式
发现其有手机客户端就下载了抓包试试,发现其客户端API提供的图像直接就是无打乱的。
3月份开始,MangaPocket对手机客户端也使用和网页端一致的固定分块数打乱图像,并且质量仅有Q75
(先前发送原图时可以获得高达Q87的图像)
因此我们高度不推荐该网站…
但来都来了,让我们看看其WEB端是如何解混淆的吧
抓包观察
在https://se-api.pocket.shonenmagazine.com/web/episode/viewer下可以看到scramble_seed字段。
追踪解密流程
打开资源就能看到一个WASM,众所周知这玩意一般都是拿来协助加密用的。打开一看果不其然。
由于都是伪汇编,这里就不展示了…
这部分直接丢给AI给你解汇编即可,人确实不太擅长处理这种内容。
总之MangaPocket使用Xorshift32,利用scramble_seed计算出映射表
但是问题来了,这个scramble_seed它到底是怎么从字符串,算出来一个Xorshift32用的数值的?
这块有点太难了,WASM实在是解析不动,于是打算回头去客户端碰碰:
为什么客户端直接就是数值啊!
就…这样解决了?
所以解密流程就是:
- 以
scramble_seed作为Xorshift32哈希的Seed - 迭代16次,获取16个值,并同时保存生成的顺序,存放为
[(0, <value_1>), (1, <value_2>),...] - 对哈希值进行升序排序,排序后的生成顺序就是原始分块位置
サイコミ (CyComi)
- 二进制流加密(RC4)
- JPEG格式
写到这里我们第一次遇到了直接加密图像二进制流的站点,这种形式好就好在不需要对原图像做二次编码,坏就坏在本地要对整个图片解一次。
这种加密方式给提供高质量JPEG提供了机会,只是CyComi似乎不想这么做。
抓包观察
没有明显的加密信息。
追踪解密流程
一打开栈跟踪就看见那个大大的loadEncryptImageData,真是好人啊。
W = (0, l.useMemo)(
() => ({
loadEncryptImageData: (e) => o(e),
content: C,
setContent: (e) => {
n.current = e;
},
hasNextContent: T,
hasPrevContent: F,
nextContent: N,
prevContent: L,
nextPage: V,
prevPage: _,
}),
[C, n, T, F, N, L, _, V],
);
这个loadEncryptImageData指向了o函数,往上查找一下这个函数。
s = n(36364);
async function o(e) {
var t;
if (!e) return;
let n = null == (t = e.match(/\/([0-9a-zA-Z]{32})\//)) ? void 0 : t[1];
if (!n) return e;
let r = await a.A.get(e, {
responseType: "arraybuffer",
}),
i = (0, s.M)(new Uint8Array(r.data), n),
l = (
await new Promise((e, t) => {
let n = new FileReader();
(n.addEventListener("error", t),
n.addEventListener("load", () => e(n.result)),
n.readAsDataURL(new Blob([i])));
})
).split(",")[1];
if (l) return ["data:null;base64", l].join(",");
}
这里读取了一段32字符的东西,再看看图像的URL构成,我们可以发现其解密用的密钥就在图像URL上。
然后我们追踪这里的s.M进一步查找解密的具体过程,可以看到s指向了36364,那我们就搜这个数字。
36364: (e, t, r) => {
"use strict";
r.d(t, {
j: () => l,
M: () => n,
});
let n = (e, t) => {
let r = ((e) => {
let t = new Uint8Array(256);
t.forEach((e, r) => {
t[r] = r;
});
let r = 0;
return (
t.forEach((n, i) => {
r = (r + t[i] + e.charCodeAt(i % e.length)) % 256;
let o = t[i];
((t[i] = t[r]), (t[r] = o));
}),
t
);
})(t),
n = 0,
i = 0,
o = new Uint8Array(e.length);
for (let t = 0, a = e.length; t < a; t++) {
let a = 256;
i = (i + r[(n = (n + 1) % 256)]) % a;
let s = r[n % 256];
((r[n % 256] = r[i]), (r[i] = s));
let l = r[(r[n] + r[i]) % a];
o[t] = l ^ e[t];
}
return o;
};
};
从这里可以看到,CyComi 使用的是非常标准的 RC4算法。
小学馆旗下站点
- マンガワン (MangaOne)
- 週刊コロコロコミック (CoroCoro)
- 二进制流加密(AES-256)
- Protobuf数据传输
- WebP格式
欢迎来到新时代!
写到这里我们第一次遇上了采用WebP格式的新世代漫画站点。并且我测试过该网站似乎没有JPEG Fallback。
小学馆系统最突出的特色是其采用Protobuf传输数据,而非传统的JSON,这也导致了我们要获知每个字段的准确含义变得更麻烦了。
下面以 マンガワン (MangaOne) 为例,展示如何逆向这套方案
抓包观察
发现传输的数据是二进制流,不是JSON,并且在HEX视图可以看到数据中有URL等信息。
追踪解密流程
发现图像栈跟踪和断点均无法找到对应的解密流程,所以我们只能换换思路了。
在抓包观察中,发现其图像URL是通过/api/client?rq=viewer_v2下发的,那我们就搜索viewer_v2追踪图像下发全流程。
let a = (e) => ({
path: "viewer_v2",
parameters: (0, n.G)(e),
method: "post",
decode: (e) => s.u.v4.WebViewerResponse.decode(new Uint8Array(e)),
});
可以看到其有个消息解码的函数,指向WebViewerResponse.decode,那么我们就找WebViewerResponse
e.WebViewerResponse = function () {
let e;
function t(e) {
if (((this.pages = []), (this.recommendedTitles = []), e))
for (let t = Object.keys(e), n = 0; n < t.length; ++n)
null != e[t[n]] && (this[t[n]] = e[t[n]]);
}
return (
(t.prototype.pages = l.emptyArray),
(t.prototype.aesKey = ""),
(t.prototype.aesIv = ""),
(t.prototype.currentTitle = null),
(t.prototype.author = null),
(t.prototype.currentChapter = null),
...
);
...
};
这里的WebViewerResponse是一个Protobuf Message,我们可以发现其数据中有两个参数很引人注目:aesKey和aesIv
那么图像就大概率是AES加密了。
接下来的事就简单了:直接搜索aesKey,看看是哪个地方用了它:
if (e.image) {
let s = e.image,
r = j(null !== (i = s.pageSide) && void 0 !== i ? i : void 0);
return {
...g,
src: null !== (n = s.imageUrl) && void 0 !== n ? n : "",
sizeRatio: (null == s ? void 0 : s.width)
? (null !== (a = null == s ? void 0 : s.height) && void 0 !== a ? a : 0) /
(null == s ? void 0 : s.width)
: 0,
crypto:
(null == t ? void 0 : t.aesIv) && t.aesKey
? {
method: "aes-cbc",
key: t.aesKey,
iv: t.aesIv,
}
: void 0,
side:
null !== (l = null == r ? void 0 : r.side) && void 0 !== l ? l : void 0,
};
}
所以使用AES CBC解密即可。
Protobuf的具体定义可以在前面我们提到的WebViewerResponse中找到,下面有一大堆Switch...Case...,根据那个自己编写一份Protobuf即可。
カドコミ (Kadokawa Comic / ComicWalker)
- 二进制流加密(XOR)
- WebP格式
抓包观察
{
"promotionsEnd": [],
"labelLogo": "",
"scrollDirection": "rtl",
"expiresAt": "",
"startPosition": "left",
"displayAds": true,
"manuscripts": [
{
"drmMode": "xor",
"drmHash": "c0f2759b27ca8baf",
"drmImageUrl": "",
"page": 1,
"width": 768,
"height": 1085
}
]
}
已经把加密方式给我直接标出来了,真的很好心啊。
那要做的就很简单了:直接搜xor,或者都知道是XOR,那直接套基础的XOR算法也不是不行。
switch (e) {
case "raw":
return new rq(r);
case "xor":
return rV.create({
sourceUrl: r,
hash: t,
});
}
可以看到其只有raw和xor两种形式。没有别的加密形式了。
class rV {
constructor(e, t) {
((this.sourceUrl = e), (this.xorKey = t));
}
#e = !1;
static create(e) {
return new rV(e.sourceUrl, this.populateKey(e.hash));
}
static populateKey(e) {
let t = e.slice(0, 16).match(/[\da-f]{2}/gi);
if (eH(t)) throw Error("error");
return new Uint8Array(t.map((e) => parseInt(e, 16)));
}
static xor(e) {
return (t) => {
let { length: r } = t,
{ length: n } = e,
i = new Uint8Array(r);
for (let o = 0; o < r; o += 1) i[o] = t[o] ^ e[o % n];
return i;
};
}
}
就是非常标准的XOR算法,直接用drmHash跑这个就行了。
ヤンジャン+ (Young Jump+)
- 固定分块数 图像打乱
- WebP格式
抓包观察
无明显密钥数据。
追踪解密流程
图片可以栈跟踪,没几下就会看到
((c = P(() => a.mangaPage.pageImageUrl)),
(g = P(() => a.mangaPage.imageHorizontalSize)),
(r = P(() => a.mangaPage.imageVerticalSize)),
(p = Math.floor(g.value / ne)),
(s = Math.floor(r.value / ne)),
(n = q(!1)),
(h = q(!1)),
(V = q(!1)));
let E = null;
const m = async () => {
if (!(n.value || h.value)) {
((h.value = !0), (i.value.width = g.value), (i.value.height = r.value));
try {
E.clearRect(0, 0, g.value, r.value);
const k = await Nt(c.value);
if ((E.drawImage(k, 0, 0, g.value, r.value), !a.isBenefitViewer))
for (let C = 0; C < ne * ne; C++) {
const b = Math.floor(C / ne) * s,
M = (C % ne) * p,
z = Math.floor(C / ne),
G = (C % ne) * ne + z,
j = (G % ne) * p,
U = Math.floor(G / ne) * s;
E.drawImage(k, M, b, p, s, j, U, p, s);
}
((n.value = !0), (h.value = !1));
} catch {
...
}
}
};
居然是传统的固定分块位置。搜索一下这个ne变量,可以发现其是定值4。
照着转写即可。
マンガPark (Manga Park)
- 二进制流加密(XOR)
- JPEG格式
十分传统派的网站,JS没有压缩过,也没有分块,功能职责非常清晰。
抓包观察
{
"csrf_token": "",
"is_logged_in": false,
"data": {
"chapter": [
{
"images": [
{
"path": "",
"width": 0,
"height": 0,
"key": "QfX+Z3qY3I64c6sIuVV4cQ=="
}
],
"width": 0,
"height": 0
}
],
"consume_type": "free",
"page_start": "LEFT"
}
}
这有个看着就像Base64编码的KEY。
追踪解密流程
发现图像可以栈跟踪,点两下就看到这个地方:
{
key: 'loadMinobiImage',
value: function (e, n) {
var o = this;
return new t(
function (t, r) {
var i = o.decodeB64(n),
a = new XMLHttpRequest;
a.onreadystatechange = function () {
if (4 === a.readyState) if (200 === a.status) {
var n = new Uint8Array(a.response);
if (i) for (var o = i.length, s = 0; s < n.length; s++) n[s] ^= i[s % o];
var u = URL.createObjectURL(new Blob([n], {
type: 'image/jpeg'
}));
t(u)
} else console.error('We can\'t load file: ', e, a),
r()
},
a.onerror = r,
a.open('GET', e, !0),
a.responseType = 'arraybuffer',
a.send(null)
}
)
}
}
就是Base64解码Key之后,对图像二进制流进行标准XOR解密操作即可。
ニコニコ漫画 (NicoNico Manga)
- 二进制流加密(XOR)
- WebP格式
追踪解密流程
因为这个比较简单,我们就跳过抓包观察这一步了。
直接看加载的图像的栈跟踪,可以看见_ImageLoader</this.getFromCORSRequest这一步调用
this.getFromCORSRequest = function (e, n, r) {
if (/^data:/.test(n)) return r(n);
var i = this.getKeyFromUrl(n);
e.open('GET', n, !0),
e.responseType = 'arraybuffer',
e.onload = function (e) {
var t = new Uint8Array(this.response);
'' !== i &&
(t = o.decrypt(t, i));
t = 'data:image/' + o.getDataType(t) + ';base64,' + o.toBase64String(t);
a[n] = t,
r(t)
},
e.send()
}
进来就发现其调用了getKeyFromUrl,我们就要敏感地意识到加密使用的密钥可能就是从这里获得的。
观察代码发现密钥就在URL里。
this.getKeyFromUrl = function (e) {
e = e.match('/image/([a-z0-9_]+)/');
return null === e ? '' : e[1].split('_') [0]
}
然后继续向下,眼尖的你肯定立刻看到了o.decrypt(t, i)。
而这一函数块的头部正赫然写着var o = this。
this.decrypt = function (e, t) {
for (var n = [], r = 0; r < 8; r++) n.push(parseInt(t.substr(2 * r, 2), 16));
for (r = 0; r < e.length; r++) e[r] = e[r] ^ n[r % 8];
return e
}
看看代码,正是我们熟悉的XOR。
抓包观察
我们回过头来,有一个问题还没解决:这图的URL是哪来的?
使用Charles抓包,然后直接搜索图片URL后发现,这部分信息全部塞在漫画页面的<script "type="text/javascript">中
pixivコミック (pixiv Comic)
- 固定分块尺寸 图像打乱
- JPEG格式
抓包观察
分块数非常巨大,看着非常恐怖。
在/read_v4下可以找到各图像的URL,同时还有一个key。
仔细观察URL还会发现路径上有写JPEG Quality和具体的分块尺寸。
{
"data": {
"reading_episode": {
...
"pages": [
{
"url": "/c/q90_gridshuffle32:32/images/page/",
"height": 1024,
"width": 720,
"gridsize": 32,
"key": "7e9135b1-06b9-4e1c-96da-184488675102"
}
],
}
}
}
追踪解密流程
由于图像没法栈跟踪,选择直接搜索key,看看这个key用到哪里去了,结果很快就发现:
a.onload = async () => {
if (i) {
i.drawImage(a, 0, 0);
let n = i.getImageData(0, 0, r.width, r.height),
o = new ImageData(
await (0, s.T)(
n.data,
4,
r.width,
r.height,
r.gridsize,
r.gridsize,
"(此处略去)",
r.key,
!0,
),
r.width,
r.height,
);
(i.putImageData(o, 0, 0),
l.toBlob((r) => {
r ? e(URL.createObjectURL(r)) : t();
}));
} else t();
};
直接给这里下断点,看看这个s.T指向什么地方?
let i = globalThis.crypto;
async function o(e, t, a, r, o, s, l, d, c) {
let u = Math.ceil(r / s),
g = Math.floor(a / o),
p = Array(u)
.fill(null)
.map(() => Array.from(Array(g).keys()));
{
let e = new TextEncoder().encode(l + d),
t = new n(new Uint32Array(await i.subtle.digest("SHA-256", e), 0, 4));
for (let e = 0; e < 100; e++) t.next();
for (let e = 0; e < u; e++) {
let a = p[e];
for (let e = g - 1; e >= 1; e--) {
let r = t.next() % (e + 1),
n = a[e];
((a[e] = a[r]), (a[r] = n));
}
}
}
if (c)
for (let e = 0; e < u; e++) {
let t = p[e],
a = t.map((e, a) => t.indexOf(a));
if (a.some((e) => e < 0)) throw Error("Failed to reverse shuffle table");
p[e] = a;
}
let h = new Uint8ClampedArray(e.length);
for (let n = 0; n < r; n++) {
let r = p[Math.floor(n / s)];
for (let i = 0; i < g; i++) {
let s = r[i],
l = (n * a + i * o) * t,
d = (n * a + s * o) * t,
c = o * t;
for (let t = 0; t < c; t++) h[l + t] = e[d + t];
}
{
let r = (n * a + g * o) * t,
i = (n * a + a) * t;
for (let t = r; t < i; t++) h[t] = e[t];
}
}
return h;
}
这里还要用到一个n,往上翻一下或者打个断点就能追踪到了:
class n {
next() {
let e = (9 * r((5 * this.s[1]) >>> 0, 7)) >>> 0,
t = (this.s[1] << 9) >>> 0;
return (
(this.s[2] = (this.s[2] ^ this.s[0]) >>> 0),
(this.s[3] = (this.s[3] ^ this.s[1]) >>> 0),
(this.s[1] = (this.s[1] ^ this.s[2]) >>> 0),
(this.s[0] = (this.s[0] ^ this.s[3]) >>> 0),
(this.s[2] = (this.s[2] ^ t) >>> 0),
(this.s[3] = r(this.s[3], 11)),
e
);
}
constructor(e) {
if (4 !== e.length)
throw Error("seed.length !== 4 (seed.length: ".concat(e.length, ")"));
((this.s = new Uint32Array(e)),
0 === this.s[0] &&
0 === this.s[1] &&
0 === this.s[2] &&
0 === this.s[3] &&
(this.s[0] = 1));
}
}
pixiv Comic 的解混淆过程非常健壮而复杂,我没这个水平读通,所以就交给AI吧。
我梳理一下输出,大概是这样的:
- 准备阶段
- 将固定前缀与
Key拼接,用SHA-256哈希处理拼接后的字符串 - 取哈希值的前16字节,构造包含4个
UInt32的数组 - 这4个32位整数作为种子,进行
xoshiro128**伪随机,并连续调用100次预热
- 将固定前缀与
- 构建洗牌表
- 根据图像宽高和网格大小算出切分的列数
u和行数g - 对图像每一行初始化一个索引数组
- 用伪随机数执行
Fisher-Yates / Knuth洗牌算法,洗牌索引数组 c参数为true,计算洗牌表的逆映射
- 根据图像宽高和网格大小算出切分的列数
- 重组
- 使用逆向洗牌表
r获取切片对应的原始位置,将其搬回 - 对于无法整除
gridsize的剩余像素,直接原样拷贝
- 使用逆向洗牌表
Renta! (レンタ)
- 固定分块数 图像打乱
- JPEG格式
Renta! 在线阅读使用的JPEG切片仅有Quality 70,非常惨不忍睹,高度不推荐该站点
但是来都来了,我们就来逆向一下它的算法吧
追踪解密流程
在XHR一眼看到了图像二进制流的传输,我原本以为会是二进制流加密,还高兴了一下。
发现有栈跟踪,第一个就是getImageData,马上跟进去:
var a = t.target.response;
++now_load_pos >= max_data_len && (now_load_pos = 0);
for (var n = new Uint8Array(a), r = '', i = 0; i < 9; i++) {
r += String.fromCharCode(n[i]);
}
上来先读了9个字节,并转换为字符。让我们看看对应的内容(已decode):000000596
是ASCII编码的头部长度。
if (isNaN(r)) cdata1 = '';
else {
for (i = 9; i < r + 9; i++) o += String.fromCharCode(n[i]);
var l = o.split('|');
data1_w = l[0],
data1_h = l[1]
}
然后读取了整个头部。
844|1200|0,2749|2749,2238|4987,6104|11091,...
头部以|号切分,第一和第二部分是图像的长和高,后面的部分又以,二次切分,是<去掉头部后的偏移>,<数据长度>
通过十六进制编辑器观察,可以看到对应的位置有0xFFD8标头,还有JFIF标志,就是JPEG分片。
然后我们来看打乱算法
// 读取头数据
var c = (l = o.split("|"))[0],
m = l[1],
g = l[2], // 后面还原出来之后观察可以发现,第一张图是宽度%7的部分。即宽度平均切分7份后遗留下来的部分。
p = l[3]; // 第二张图是长度%7的部分。
l = l.splice(4); // 剩余的图像
(parseInt(c / 7), parseInt(m / 7)); // 固定分块数:长宽各7,总计49块
var f = Math.ceil(c % 7),
h = Math.ceil(m % 7),
u = "",
y = ((s = 0), new Array(7));
// 生成 7*7 矩阵
for (i = 0; i < 7; i++) {
y[i] = new Array(7);
for (var w = 0; w < 7; w++) ((y[i][w] = s), s++);
}
// 第一轮打乱:行循环移位
for (i = 0; i < 7; i++) {
var v = new Array(7),
_ = 7 - (i % 7);
for (w = 0; w < 7; w++) (_ >= 7 && (_ = 0), (v[w] = y[i][_]), (_ += 1));
for (w = 0; w < 7; w++) y[i][w] = v[w];
}
// 第二轮打乱:列循环移位
for (i = 0; i < 7; i++) {
for (v = new Array(7), _ = 7 - (i % 7), w = 0; w < 7; w++)
(_ >= 7 && (_ = 0), (v[w] = y[_][i]), (_ += 1));
for (w = 0; w < 7; w++) y[w][i] = v[w];
}
// 第三轮打乱:按种子洗牌
for (i = 0; i < 7; i++) {
var C = i + 1,
I = parseInt(e) + parseInt(prd_ser); // 首先计算因子I,这里的传入参数e是页码,prd_ser在图像URL中。
I % 20 == 0 && (I = Math.abs(e - prd_ser) + 21); // 如果I可以整除20,则使用特殊算法计算I
for (w = parseInt((C * I + e / 20) % 20) - 1; w >= 0; w--) // 计算出这一环节的打乱次数
y = fShuffleR(y, w, i, 7);
}
行循环和列循环移位都非常基础,不再进一步解释了。
让我们重点看看fShuffleR的实现:
function fShuffleR(e, t, a, n) {
var r = kn = parseInt(n / 2);
n % 2 != 0 &&
(r += 1);
var i = new Array(r),
o = new Array(kn),
l = k_cnt = 0,
s = new Array(n),
d = 0;
if (t % 2 == 0) { // 根据传入参数t(即上文中的w)是奇还是偶进入不同的处理
for (var c = 0; c < n; c++) c % 2 == 0 ? (i[l] = e[c][a], l++) : (o[k_cnt] = e[c][a], k_cnt++); // 对本段数据按下标奇偶切分
for (c = 0; c < r; c++) void 0 != o[c] && // 交叉插入奇偶索引,一奇一偶
(s[d] = o[c], d++),
void 0 != i[c] &&
(s[d] = i[c], d++)
} else {
for (c = 0; c < n; c++) c < r ? (i[l] = e[c][a], l++) : (o[k_cnt] = e[c][a], k_cnt++); // 对本段数据按前半后半切分
for (c = 0; c < r; c++) void 0 != i[c] && // 交叉插入前后索引,一前一后
(s[d] = i[c], d++),
void 0 != o[c] &&
(s[d] = o[c], d++)
}
for (c = 0; c < n; c++) e[c][a] = s[c];
return s = null,
i = null,
o = null,
e
}
整体来说不难,但是图像质量太烂了,非常不推荐。
无加密站点
不是哥们,原图直出吗。
ガンガンONLINE (GanGanOnline)
- WebP格式
ゼロサムオンライン (ZeroSum Online)
- WebP格式
アルファポリス (Alphapolis)
- JPEG格式
Manga-Website 漫画街
- JPEG格式
这网站真的有点复古了
Renta! TW(亂搭)
- JPEG格式
(你这站点是日本站点吗?)

浙公网安备 33010602011771号