Cette analyse se concentre sur une application Android nécessitant un abonnement VIP pour accéder à ses principales fonctionnalités. Le coût de l'abonnement à vie est fixé à 168 unités monétaires, ce qui motive l'examen technique de son mécanisme d'autorisation.
L'application utilise une protection de type梆梆 (Bangbang), rendant la décompilation directe complexe en raison du chiffrement du code et des vérifications d'intégrité. Une approche dynamique utilisant Frida a été adoptée pour extraire les fichiers DEX en mémoire.
Le script Frida suivant permet de capturer les fichiers DEX chargés en mémoire en interceptant les appels système appropriés :
'use strict';
function displayLog(message) {
const now = new Date();
const formattedTime = [
now.getHours().toString().padStart(2, '0'),
now.getMinutes().toString().padStart(2, '0'),
now.getSeconds().toString().padStart(2, '0'),
now.getMilliseconds().toString().padStart(3, '0')
].join(':');
console.log(`[${formattedTime}] ${message}`);
}
function getOSVersion() {
if (!Java.available) {
displayLog("Erreur: impossible d'obtenir la version Android");
return 0;
}
const version = parseInt(Java.androidVersion.charAt(0));
displayLog(`Version Android: ${version}`);
return version;
}
function locateTargetFunction() {
const version = getOSVersion();
let targetFunction = "";
if (version >= 5) {
const artExports = Module.enumerateExportsSync("libart.so");
for (const export of artExports) {
if (export.name.includes("OpenMemory") || export.name.includes("OpenCommon")) {
targetFunction = export.name;
displayLog(`Fonction trouvée: ${targetFunction}`);
break;
}
}
} else {
const dvmExports = Module.enumerateExportsSync("libdvm.so");
if (dvmExports.length > 0) {
for (const export of dvmExports) {
if (export.name.includes("dexFileParse")) {
targetFunction = export.name;
displayLog(`Fonction trouvée: ${targetFunction}`);
break;
}
}
} else {
const artExports = Module.enumerateExportsSync("libart.so");
for (const export of artExports) {
if (export.name.includes("OpenMemory")) {
targetFunction = export.name;
displayLog(`Fonction trouvée: ${targetFunction}`);
break;
}
}
}
}
return targetFunction;
}
function extractProcessIdentifier() {
const fopenPtr = Module.findExportByName("libc.so", "fopen");
const fopen = new NativeFunction(fopenPtr, 'pointer', ['pointer', 'pointer']);
const fgetsPtr = Module.findExportByName("libc.so", "fgets");
const fgets = new NativeFunction(fgetsPtr, 'int', ['pointer', 'int', 'pointer']);
const fclosePtr = Module.findExportByName("libc.so", "fclose");
const fclose = new NativeFunction(fclosePtr, 'int', ['pointer']);
const path = Memory.allocUtf8String("/proc/self/cmdline");
const mode = Memory.allocUtf8String("r");
const buffer = Memory.alloc(128);
const fileHandle = fopen(path, mode);
if (!fileHandle.isNull()) {
if (fgets(buffer, 128, fileHandle) !== 0) {
const processName = Memory.readCString(buffer);
displayLog(`Nom du processus: ${processName}`);
fclose(fileHandle);
return processName;
}
fclose(fileHandle);
}
return "";
}
function validateDEXHeader(address) {
const expectedHeader = [0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0x00];
for (let i = 0; i < 8; i++) {
if (Memory.readU8(ptr(address).add(i)) !== expectedHeader[i]) {
return false;
}
}
return true;
}
function captureDEX(targetFunction, processName) {
if (!targetFunction) {
displayLog("Erreur: fonction cible non trouvée");
return;
}
const library = getOSVersion() >= 5 ? "libart.so" : "libdvm.so";
const functionAddress = Module.findExportByName(library, targetFunction) ||
Module.findExportByName("libart.so", targetFunction);
Interceptor.attach(functionAddress, {
onEnter: function(args) {
let dataAddress = null;
if (validateDEXHeader(args[0])) {
dataAddress = args[0];
} else if (validateDEXHeader(args[1])) {
dataAddress = args[1];
}
if (dataAddress) {
const baseAddress = parseInt(dataAddress, 16);
const sizeOffset = baseAddress + 0x20;
const fileSize = Memory.readInt(ptr(sizeOffset.toString()));
displayLog(`Taille DEX: ${fileSize}`);
const outputPath = `/data/data/${processName}/${fileSize}_captured.dex`;
const outputFile = new File(outputPath, "wb");
outputFile.write(Memory.readByteArray(dataAddress, fileSize));
outputFile.flush();
outputFile.close();
displayLog(`DEX sauvegardé: ${outputPath}`);
}
}
});
}
const targetFunc = locateTargetFunction();
const procName = extractProcessIdentifier();
if (targetFunc && procName) {
captureDEX(targetFunc, procName);
}
Après extraction, le fichier DEX est analysé avec JADX. La recherche de termes comme "vip" révèle la logique de vérification dans le code métier. La méthode critique identifiée est getVipStatus() de la classe CommonSetting.
Selon l'analyse du bytecode, lorsque cette méthode retourne la chaîne "1", l'interface utilisateur active les fonctionnalités premium. Un hook Frida peut donc modifier ce comportement :
function overrideVIPCheck() {
const SettingsClass = Java.use("com.shututek.original.utils.CommonSetting");
SettingsClass.getVipStatus.implementation = function() {
displayLog("Interception de la vérification VIP");
const originalResult = this.getVipStatus();
displayLog(`Résultat original: ${originalResult}`);
const forcedResult = "1";
displayLog(`Résultat modifié: ${forcedResult}`);
return forcedResult;
};
}
function initializeHook() {
Java.perform(() => {
overrideVIPCheck();
});
}
setImmediate(initializeHook);
L'application implémente des protections anti-débogage qui terminent le processus lors de la détection de Frida. Cependant, en utilisant le mode d'attachement après le démarrage initial de l'application, il est possible de contourner ces vérifications.
La commande frida -UF -l hook.js permet d'injecter le script dans le processus déjà actif, évitant ainsi les points de contrôle présents au lancement.
Avec cette approche, les fontcionnalités premium deviennent accessibles sans authentification valide, démontrant une vulnérabilité dans la gestion des autorisations côté client.