update of the japscan extension - the complete reverse of the JS (#17181)

* Update Japscan.kt

I completely changed the way to find the keys allowing the decryption of the base64 which contains the information of the images to be recovered. Before there was a recovery by regex, but this was not reliable because the location of these strings changes places and the order is important because it allows to reconstitute the two decryption keys. To succeed in recovering them correctly and in the right order, I debugged the javascript, which allowed me to understand how obfuscation works.

 I understood that the base64 encryption file, the zjs, was not the same file when we had a mobile useragent and a PC useragent, it is however the same key, just that they are placed at different other place
at the end of the file we have the JS which assembles the keys directly as parameters of a function:

a0_0x39cacb('0x13d') + a0_0x39cacb('0x130') + a0_0x39cacb('0x118') + '6N', a0_0x39cacb('0x145') + a0_0x39cacb('0x116') + a0_0x39cacb('0x10f') + 'oe'


we have another function here:

return a0_0x46c1 = function(_0x46c1ab, _0x5a3fac) {
        _0x46c1ab = _0x46c1ab - 0x105;
        let _0x5c76b5 = _0xd80153[_0x46c1ab];
        return _0x5c76b5;
    }


which call string array which contains the key chunks

0 : "laretiLesrap"
1 : "gnp.600/sgmi/"
2 : "atad"
3 : "daol"
4 : "8ceABpX"
..


_0x46c1ab = _0x46c1ab - 0x105;


this operation allows to know the shift in the table, for example
By subtracting 0x105 from '0x13d', we get:

317 - 261 = 56

at location 56, so at index 55 of the array we have: "ohcVEMDTYO7FpBA20zRg"

so here: a0_0x39cacb('0x13d') + a0_0x39cacb('0x130') + a0_0x39cacb('0x118') + '6N'
we replace a0_0x39cacb('0x13d')

by this chain

"ohcVEMDTYO7FpBA20zRg" + a0_0x39cacb('0x130') + a0_0x39cacb('0x118') + '6N'

and that gives us the beginning of the key
and then reverse it
and that gives us the right key

* Update build.gradle

Update build version

* Update Japscan.kt - Completely reverse JavaScript

My modification was missing part of the javascript code to reverse, this modification brings greater stability.
In the JS is decrypts the data, there is a table of character string, this table is the most important element of the decryption because it is used in almost all the functions.

What the algorithm does in a nutshell:

- he shuffles the board until he arrives at a precise position, he pushes all the elements backwards until he finds the right combination. The correct combination is determined by the sum of the digits in the character strings with a more or less complex calculation.

- It calls the table in a "hidden" way with a function that takes a hexadecimal as a parameter (which corresponds to the index in the table)

- once all the keys have been retrieved from the character string table, it assembles them, then reverses them to give the complete key


How I proceeded:

- For the correct position in the table I perform almost the same calculation as the JS but I simplify, this to a tiny margin of error. In the calculation in the JS it parses the INTs in the chain and makes calculations with it, to simplify my task I only check if ALL the elements used contain INTs, I did several debug tests and they are obliged to contain them when the position is good. So starting from this principle I would almost certainly fall on the right position.

- Once my array is correctly sorted, I can with a simple calculation managed to recover the requested element with the hexadecimal

- I reverse the keys and I test the two positions of the keys to decipher the base64

* Update Japscan.kt

Added the possibility to have the parseInt in negative

* Update Japscan.kt

Modification needed to take into account the wider possibility of script changes

* Update Japscan.kt - Add comments

Ajout des commentaires
This commit is contained in:
zormy111 2023-07-22 00:57:53 +02:00 committed by GitHub
parent 5b1d56d15a
commit a3543bf2b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 222 additions and 20 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'Japscan'
pkgNameSuffix = 'fr.japscan'
extClass = '.Japscan'
extVersionCode = 41
extVersionCode = 42
}
apply from: "$rootDir/common.gradle"

View File

@ -261,8 +261,26 @@ class Japscan : ConfigurableSource, ParsedHttpSource() {
}
}
private val shortDecodingStringsRe: Regex = Regex("""'([\dA-Z]{2})'""", RegexOption.IGNORE_CASE)
private val longDecodingStringsRe: Regex = Regex("""'([\dA-Z]{20})'""", RegexOption.IGNORE_CASE)
private fun extractQuotedContent(input: String): List<String> {
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> {
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> {
/*
@ -271,39 +289,223 @@ class Japscan : ConfigurableSource, ParsedHttpSource() {
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.
Since figuring out how to properly map characters would be more effort than I want to
put in, this just flips around the charsets if the first attempt didn't succeed.
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()
// Japscan split its lookup table into 1 string of 2 chars + 2 * 3 strings of 20 chars
// Try to find the 2 set of 3 strings of 20 chars first
val rawLongStringLookupTables = longDecodingStringsRe.findAll(zjs).map {
it.groupValues[1]
}.toList()
/*
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()
if (rawLongStringLookupTables.size != 6) {
throw Exception("Attendait 6 chaînes de recherche dans ZJS, a trouvé ${rawLongStringLookupTables.size}")
/*
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é")
}
// Then try to find the 2 strings of 2 chars (will output 3 values the first is something else)
val rawShortStringLookupTables = shortDecodingStringsRe.findAll(zjs).map {
it.groupValues[1]
}.toList()
/*
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.
if (rawShortStringLookupTables.size != 3) {
throw Exception("Attendait 3 chaînes de recherche dans ZJS, a trouvé ${rawShortStringLookupTables.size}")
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(
rawShortStringLookupTables[1].reversed() + rawLongStringLookupTables[5].reversed() + rawLongStringLookupTables[2].reversed() + rawLongStringLookupTables[0].reversed(),
rawShortStringLookupTables[2].reversed() + rawLongStringLookupTables[3].reversed() + rawLongStringLookupTables[4].reversed() + rawLongStringLookupTables[1].reversed(),
key1.reversed(),
key2.reversed(),
)
val scrambledData = document.getElementById("data")!!.attr("data-data")