JapScan: Use lib-synchrony to deobfuscate code (#17329)

This commit is contained in:
beerpsi 2023-08-01 00:37:56 +07:00 committed by GitHub
parent d3e6595dc3
commit 37a4b20ef2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 16 additions and 235 deletions

View File

@ -6,7 +6,11 @@ ext {
extName = 'Japscan' extName = 'Japscan'
pkgNameSuffix = 'fr.japscan' pkgNameSuffix = 'fr.japscan'
extClass = '.Japscan' extClass = '.Japscan'
extVersionCode = 42 extVersionCode = 43
}
dependencies {
implementation(project(":lib-synchrony"))
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -5,6 +5,7 @@ import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
@ -261,252 +262,28 @@ class Japscan : ConfigurableSource, ParsedHttpSource() {
} }
} }
private fun extractQuotedContent(input: String): List<String> { private val decodingStringsRe: Regex = Regex("""'([\dA-Z]{62})'""", RegexOption.IGNORE_CASE)
val regex = Regex("'(.*?)'")
return regex.findAll(input).map { it.groupValues[1] }.toList()
}
private fun listJSToKey(jsList: MutableList<String>, offsettab: Int, listKey: List<String>): MutableList<String> { private val sortedLookupString: List<Char> = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray().toList()
for (i in 0 until jsList.size) {
if (jsList[i].contains("0x")) {
var decoupeHexa = jsList[i].split("('")[1]
decoupeHexa = decoupeHexa.split("')")[0]
var indexkey = Integer.decode(decoupeHexa) - offsettab - 1
if (indexkey < 0) {
indexkey = listKey.size - 1
}
jsList[i] = listKey[indexkey]
}
}
return jsList
}
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
/*
JapScan stores chapter metadata in a `#data` element, and in the `data-data` attribute.
This data is scrambled base64, and to unscramble it this code searches in the ZJS for
two strings of length 62 (base64 minus `+` and `/`), creating a character map.
Comment le script fonctionne globalement :
Il garde dans un tableau les morceaux diviser en 3 partie des deux clés string.
Une partie de ces clé peuvent aussi être en partie en direct dans le concaténage final de la clé, exemple :
a0_0x1dc175('0x16b') + 'H1M9pwuXgyKmTLJqekNd' + 'P85arn0hFDA2RBOQUZvI' + 'Wi', 'fOu6QZGtyepSkzRsEdba' + a0_0x1dc175('0x132') + a0_0x1dc175('0x112') + 'IX'
On peut voir que les autres parties des clés comme par exemple : a0_0x1dc175('0x16b') est appelé par une fonction.
Pour expliquer simplement, cette fonction permet de GET un élément du tableau en fonction du paramètre, le paramètre est une chaine en hexadecimal qui est par la suite
converti en int.
//================= Une partie du code JS pour mieux comprendre
//il enregistre la fonction dans une variable;
const a0_0x1dc175 = a0_0x4ed0;
//le premier paramètre de la fonction est l'hexadecimal qu'on lui passe en paramètre, le deuxième est à null tout le temps
function a0_0x4ed0(_0x39b48a, _0x134ab9) {
//cette fonction récupère l'entièrté du tableau de chaine
const _0x472ace = a0_0x472a();
(le tableau à cette forme : const _0x1ac112 = ['kcolb', 'retpahc-txen#', 'yalpsid', '}\x20gnitcepxE', 'tArahc', 'dedaoLtnetnoCMOD', 'tfeLworrA', 'egnahc', .... etc )
return a0_0x4ed0 = function(_0x4ed09a, _0x4045cf) {
//ce calcul consiste à utiliser l'hexadecimal prit en paramètre qui a cette valeur pour exemple : '0x16b'
//Cela donne : 0x16b - 0x10b => 363 - 267 = 96
//A noter que le 0x10b est en dur dans le code, mais quand le script se reload (environ toutes les 48h actuellement) cette valeur peut changer !
_0x4ed09a = _0x4ed09a - 0x10b;
//_0x472ace étant le tableau de chaine, ici on utilise maintenant l'index qu'on a généré (96) pour aller chercher l'élément en question
let _0x913c35 = _0x472ace[_0x4ed09a];
//Donc ici la fonction retourne enfin le bon morceau de clé! En résumé il faut faire un calcul d'offset avec la valeur prit en paramètre et récupérer le résultat dans le tableau
return _0x913c35;
}
,
//appel la fonction juste en haut, c'est juste pour brouiller le suivi visuel
a0_0x4ed0(_0x39b48a, _0x134ab9);
}
Avec tout ces éléments il est possible de récupérer les informations dans le tableau, mais ça n'est pas tout!
Le script a mit en place une autre sécurité qu'il faut contourner, les emplacements des chaines dans le tableau ne sont pas bonne à la génération de celui-ci, il y a
toute une partie du script qui replace correctement les élément dans le tableau avant de faire le calcul que j'ai expliqué précédemment.
Sans le remettre en ordre le tableau va juste nous renvoyer des valeurs qui ne correspondent pas aux morceaux de clé !
Comment le script fonctionne pour remplacer les éléments :
ici des valeurs sont déclaré, c'est le même format que pour get les éléments du tableau, et ces effectivements aussi leurs utilités.
Pour remettre au bon emplacement les chaines il va falloir faire des tests sur les emplacements.
const a0_0x14d6c4 = {
_0x24dcbe: '0x172', //ici ce sont des valeurs utilisé par la suite, c'est seulement pour brouiller la lecture, exemple : 0x1 * (-parseInt(_0x3d53a2(a0_0x14d6c4._0x14050e)) devient => 0x1 * (-parseInt(_0x3d53a2('0x172'))
_0x14050e: '0x15b',
_0x3ece33: '0x147',
_0x2cd65f: '0x129',
_0x15deac: '0x151'
}
, _0x3d53a2 = a0_0x4ed0
, _0x20d103 = _0x5d612f();
//boucle jusqu'à ce que le tableau soit dans l'ordre
while (!![]) {
try {
//on récupère à des index fixe les éléments dans le tableau, ces index sont défini de la même façon que les clé, c'est à dire avec
//un index en hexadecimal et un petit calcul derrière
//ensuite chaque élément à un parseInt, c'est à dire que tout les éléments sélectionné dans ces index doivent avoir des chiffres dans leurs chaines pour que le calcul fonctionne correctement
//la plupart du temps ce calcul renvoie "NaN", car il y a peu de int dans le calcul (c'est important car je m'en sers dans mon algo par la suite)
const _0x474f86 = -parseInt(_0x3d53a2(a0_0x14d6c4._0x24dcbe)) / 0x1 * (-parseInt(_0x3d53a2(a0_0x14d6c4._0x14050e)) / 0x2) + -parseInt(_0x3d53a2('0x12b')) / 0x3 + parseInt(_0x3d53a2('0x160')) / 0x4 * (-parseInt(_0x3d53a2('0x14a')) / 0x5) + parseInt(_0x3d53a2('0x14c')) / 0x6 + -parseInt(_0x3d53a2('0x14f')) / 0x7 + -parseInt(_0x3d53a2(a0_0x14d6c4._0x3ece33)) / 0x8 + -parseInt(_0x3d53a2(a0_0x14d6c4._0x2cd65f)) / 0x9 * (-parseInt(_0x3d53a2(a0_0x14d6c4._0x15deac)) / 0xa);
//ici se trouve le check permettant de savoir si l'emplacement du tableau est bon.
//_0x1fbb37 est une valeur fixe déclaré précemment dans le script, par exemple il peut avoir la valeur : 99005
//_0x474f86 est la valeur qui va changer selon les emplacements des éléments dans le tableau
//quand le calcul est bon, _0x474f86 a forcément la valeur de _0x1fbb37, cela signifie que le tableau est dans le bon ordre et que le script peut continuer
if (_0x474f86 === _0x1fbb37)
break;
else
//Ici est une partie TRES IMPORTANTE du code, lorsque la valeur n'est pas bonne, donc quand le tableau
//n'est pas trié correctement, les éléments dans le tableau bascule vers l'arrière, c'est à dire que par exemple l'élement en 3eme position passe en 2eme position
//l'élement en 4eme position passe en 3 etc
//
//Tout ceci provoque un décalage constant jusqu'à ce que le tableau tombe sur la bonne combinaison
_0x20d103['push'](_0x20d103['shift']());
} catch (_0x383e51) {
//lui non plus
_0x20d103['push'](_0x20d103['shift']());
}
}
Voilà ! Une fois qu'on a comprit tout ça, on peu aisément comprendre la suite du code.
*/
val zjsurl = document.getElementsByTag("script").first { val zjsurl = document.getElementsByTag("script").first {
it.attr("src").contains("zjs", ignoreCase = true) it.attr("src").contains("zjs", ignoreCase = true)
}.attr("src") }.attr("src")
Log.d("japscan", "ZJS at $zjsurl") Log.d("japscan", "ZJS at $zjsurl")
//on récupère le script JS permettant de déchiffrer le base64 val obfuscatedZjs = client.newCall(GET(baseUrl + zjsurl, headers)).execute().body.string()
val zjs = client.newCall(GET(baseUrl + zjsurl, headers)).execute().body.string() val zjs = Deobfuscator.deobfuscateScript(obfuscatedZjs) ?: throw Exception("Impossible à désobfusquer ZJS")
/* val stringLookupTables = decodingStringsRe.findAll(zjs).mapNotNull {
On récupère le tableau qui contient toute les chaines de caractère it.groupValues[1].takeIf {
(Pour rappel : le tableau à cette forme : const _0x1ac112 = ['kcolb', 'retpahc-txen#', 'yalpsid', '}\x20gnitcepxE', 'tArahc', 'dedaoLtnetnoCMOD', 'tfeLworrA', 'egnahc', .... etc ) it.toCharArray().sorted() == sortedLookupString
*/
var tabKey = "'" + zjs.split("=['")[1]
tabKey = tabKey.split("];")[0]
val listKey = tabKey.split("','").toMutableList()
/*
On récupère l'offset permettant le calcul de l'index du tableau quand on appel la fonction qui récupère un élément dans le tableau :
Exemple si dans le script on avait :
_0x4ed09a = _0x4ed09a - 0x10b;
L'objectif est de récupérer 0x10b pour pouvoir effectuer ce calcul manuellement
*/
var decoupeOffset = zjs.split("-0x")[1]
decoupeOffset = "0x" + decoupeOffset.split(";")[0]
// on converti directement en int pour le calcul
val offsettab = Integer.decode(decoupeOffset)
/*
Ici le but est de récupérer toute cette partie de js :
while (!![]) {
try {
const _0x474f86 = -parseInt(_0x3d53a2(a0_0x14d6c4._0x24dcbe)) / 0x1 * (-parseInt(_0x3d53a2(a0_0x14d6c4._0x14050e)) / 0x2) + -parseInt(_0x3d53a2('0x12b')) / 0x3 + parseInt(_0x3d53a2('0x160')) / 0x4 * (-parseInt(_0x3d53a2('0x14a')) / 0x5) + parseInt(_0x3d53a2('0x14c')) / 0x6 + -parseInt(_0x3d53a2('0x14f')) / 0x7 + -parseInt(_0x3d53a2(a0_0x14d6c4._0x3ece33)) / 0x8 + -parseInt(_0x3d53a2(a0_0x14d6c4._0x2cd65f)) / 0x9 * (-parseInt(_0x3d53a2(a0_0x14d6c4._0x15deac)) / 0xa);
if (_0x474f86 === _0x1fbb37)
break;
else
*/
var decoupeFuncOrder = zjs.split("while(!![])")[1]
decoupeFuncOrder = decoupeFuncOrder.split("if")[0]
/*
Au lieu de parse les int comme dans le script JS, ici je récupère seulement les offsets entre quote, donc dans listKeyOrder il y a ces valeurs si je reprend l'exemple juste au dessus : 0x12b, 0x160, 0x14a, 0x14c
*/
val listKeyOrder = extractQuotedContent(decoupeFuncOrder).toMutableList()
if (listKeyOrder.size < 3) {
throw Exception("L'ordre des clés n'a pas pu être déterminé")
} }
}.toList()
/* if (stringLookupTables.size != 2) {
Ici je boucle sur les offsets que j'ai récupéré et je regarde si ils contiennent des chiffres, si ils contiennent tous des chiffres aux même moment. throw Exception("Attendait 2 chaînes de recherche dans ZJS, a trouvé ${stringLookupTables.size}")
Si ils ne contiennent pas tous des chiffres alors je fais un décalage dans le tableau de la même manière que le fais le JS.
Si ils ont tous des chiffres, alors je considère que le tableau est correctement trié et je passe à la suite.
Pour faire bien à 100% il faudrait aussi récupérer la valeur qui valide le bon trie et simulé l'entièrté du calcul des ParseInt, cependant vu que les informations
proviennent de scraping il y a une marge d'erreur et ça serait beaucoup trop imprécis pour être plus fiable que cette méthode.
Elle n'est donc pas infaillible, mais 95% du temps cela va fonctionner.
*/
var goodorder = false
//je boucle sur toute les positions possible du tableau, si aucune ne fonctionne c'est qu'il y a un problème
for (i in 0 until listKey.size) {
//je vérifie sur chacun des offsets récupéré la valeur dans le tableau
for (z in 0 until listKeyOrder.size) {
if (listKey[Integer.decode(listKeyOrder[z]) - offsettab - 1].contains("[0-9]".toRegex())) {
goodorder = true
} else {
goodorder = false
break
} }
}
//Si tout les éléments contiennent au moins un chiffre alors c'est bon on sort de la boucle
if (goodorder) {
break
}
//je bascule les éléments du tableau vers l'arrière
val firstElement = listKey.removeAt(0)
listKey.add(firstElement)
}
//pas trouver de bonne position pour le tableau
if (!goodorder) {
throw Exception("L'ordre des clés n'a pas pu être déterminé")
}
/*
Ici l'objectif est de récupéré ceci : a0_0x1dc175('0x16b') + 'H1M9pwuXgyKmTLJqekNd' + 'P85arn0hFDA2RBOQUZvI' + 'Wi', 'fOu6QZGtyepSkzRsEdba' + a0_0x1dc175('0x132') + a0_0x1dc175('0x112') + 'IX'
C'est l'assemblage des morceaux de clé.
Pour se faire je me base sur un principe du script qui est que cette information se trouve dans les paramètres d'une fonction, généralement tout à la fin du script, et qu'elle se trouve dans avant ceci /[A-Z0-9]/gi :
, 'ecalper', /[A-Z0-9]/gi, a0_0x1dc175('0x16b') + 'H1M9pwuXgyKmTLJqekNd' + 'P85arn0hFDA2RBOQUZvI' + 'Wi', 'fOu6QZGtyepSkzRsEdba' + a0_0x1dc175('0x13
Après ce paramètre il y a toujours l'assemblage des deux clés, puisque notre tableau est dans le bon ordre maintenant, on peut récupérer ses morceaux de clé
et les assembler en faisant en sorte de bien parse séparemment toute les parties.
*/
val zjscalc = zjs.split("/[A-Z0-9]/gi,")[1]
//je découpe la première clé et la met dans un tableau
//ensuite avec la fonction listJSToKey je remplace les clé qui se trouve dans le tableau de chaine par les vrai chaine récupéré aux bon offset
val calc1 = zjscalc.split(",")[0]
var calc1tab = calc1.split("+").toMutableList()
calc1tab = listJSToKey(calc1tab, offsettab, listKey)
//pareil ici
val calc2 = zjscalc.split(",")[1]
var calc2tab = calc2.split("+").toMutableList()
calc2tab = listJSToKey(calc2tab, offsettab, listKey)
//j'assemble les tableaux pour reformer la chaine
var key1 = calc1tab.joinToString("")
var key2 = calc2tab.joinToString("")
//on clean les chaines
key1 = key1.replace("'", "")
key2 = key2.replace("'", "")
key1 = key1.replace(" ", "")
key2 = key2.replace(" ", "")
// Once we found the 8 strings, assuming they are always in the same order
// Since Japscan reverse the char order, reverse the strings
val stringLookupTables = listOf(
key1.reversed(),
key2.reversed(),
)
val scrambledData = document.getElementById("data")!!.attr("data-data") val scrambledData = document.getElementById("data")!!.attr("data-data")