From 37a4b20ef29ceefccc72ef72ae9d69e38f5f6118 Mon Sep 17 00:00:00 2001 From: beerpsi <92439990+beerpiss@users.noreply.github.com> Date: Tue, 1 Aug 2023 00:37:56 +0700 Subject: [PATCH] JapScan: Use lib-synchrony to deobfuscate code (#17329) --- src/fr/japscan/build.gradle | 6 +- .../tachiyomi/extension/fr/japscan/Japscan.kt | 245 +----------------- 2 files changed, 16 insertions(+), 235 deletions(-) diff --git a/src/fr/japscan/build.gradle b/src/fr/japscan/build.gradle index a4207ceb8..57fdc6215 100644 --- a/src/fr/japscan/build.gradle +++ b/src/fr/japscan/build.gradle @@ -6,7 +6,11 @@ ext { extName = 'Japscan' pkgNameSuffix = 'fr.japscan' extClass = '.Japscan' - extVersionCode = 42 + extVersionCode = 43 +} + +dependencies { + implementation(project(":lib-synchrony")) } apply from: "$rootDir/common.gradle" diff --git a/src/fr/japscan/src/eu/kanade/tachiyomi/extension/fr/japscan/Japscan.kt b/src/fr/japscan/src/eu/kanade/tachiyomi/extension/fr/japscan/Japscan.kt index 65b4533ba..50ab54f2d 100644 --- a/src/fr/japscan/src/eu/kanade/tachiyomi/extension/fr/japscan/Japscan.kt +++ b/src/fr/japscan/src/eu/kanade/tachiyomi/extension/fr/japscan/Japscan.kt @@ -5,6 +5,7 @@ import android.content.SharedPreferences import android.net.Uri import android.util.Base64 import android.util.Log +import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.interceptor.rateLimit @@ -261,253 +262,29 @@ class Japscan : ConfigurableSource, ParsedHttpSource() { } } - private fun extractQuotedContent(input: String): List { - val regex = Regex("'(.*?)'") - return regex.findAll(input).map { it.groupValues[1] }.toList() - } + private val decodingStringsRe: Regex = Regex("""'([\dA-Z]{62})'""", RegexOption.IGNORE_CASE) - private fun listJSToKey(jsList: MutableList, offsettab: Int, listKey: List): MutableList { - 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 - } + private val sortedLookupString: List = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray().toList() override fun pageListParse(document: Document): List { - /* - 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 { it.attr("src").contains("zjs", ignoreCase = true) }.attr("src") Log.d("japscan", "ZJS at $zjsurl") - //on récupère le script JS permettant de déchiffrer le base64 - val zjs = client.newCall(GET(baseUrl + zjsurl, headers)).execute().body.string() + val obfuscatedZjs = client.newCall(GET(baseUrl + zjsurl, headers)).execute().body.string() + val zjs = Deobfuscator.deobfuscateScript(obfuscatedZjs) ?: throw Exception("Impossible à désobfusquer ZJS") - /* - On récupère le tableau qui contient toute les chaines de caractère - (Pour rappel : le tableau à cette forme : const _0x1ac112 = ['kcolb', 'retpahc-txen#', 'yalpsid', '}\x20gnitcepxE', 'tArahc', 'dedaoLtnetnoCMOD', 'tfeLworrA', 'egnahc', .... etc ) - */ - 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é") - } - - /* - 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. - 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 - } + val stringLookupTables = decodingStringsRe.findAll(zjs).mapNotNull { + it.groupValues[1].takeIf { + it.toCharArray().sorted() == sortedLookupString } + }.toList() - //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) + if (stringLookupTables.size != 2) { + throw Exception("Attendait 2 chaînes de recherche dans ZJS, a trouvé ${stringLookupTables.size}") } - //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") for (i in 0..1) {