Rétro-ingénierie de JNI, ou comment Facebook fait planter sa propre application
Facebook ferait apparemment planter ses applications volontairement afin de tester la réaction des utilisateurs et d'évaluer leur attachement au service. Cet article ne porte pas sur l'analyse du comportement des utilisateurs, mais sur les aspects techniques de la méthode employée, ou sur un simple prétexte pour plonger dans la rétro-ingénierie de JNI.
Apparemment, Facebook fait planter ses applications volontairement afin de tester la réaction des utilisateurs et d'évaluer leur attachement au service Facebook. Cet article ne porte toutefois pas sur l'analyse du comportement des utilisateurs, mais sur les aspects techniques de la méthode employée, ou simplement sur un prétexte pour plonger dans la rétro-ingénierie de JNI.
Tout d'abord, qu'est-ce qui nous a amenés à parler de JNI à propos de la fonctionnalité de plantage de l'application Facebook ? En utilisant l'Ostorlab Mobile Application Security Scanner (il est entièrement gratuit et vous pouvez déjà le tester), nous avons découvert les méthodes natives suivantes :

Une méthode native indique qu'elle est implémentée en code natif via JNI (Java Native Interface).
JNI est en gros le moyen d'écrire en C/C++ du code accessible depuis Java. On le fait généralement pour diverses raisons, les performances pouvant en être une. Les avantages du code natif sont principalement les performances, une meilleure obfuscation du code et la possibilité d'accéder à des fonctionnalités de bas niveau. Ses inconvénients sont le manque de portabilité et un risque de sécurité plus élevé.
Les méthodes natives sont donc d'excellentes cibles du point de vue d'un attaquant lorsqu'elles traitent des entrées utilisateur. Dans l'exemple de l'application Instagram, les méthodes natives servent à traiter les images et les vidéos et sont donc potentiellement vulnérables aux erreurs de corruption de mémoire.
La rétro-ingénierie et le fuzzing de JNI semblent un sujet peu étudié, malgré son rôle important pour les applications mobiles Android.
Revenons à notre méthode crashThisProcess de Facebook : comment trouver l'implémentation de cette méthode ?
Si l'on consulte la spécification Java JNI sur la façon dont la JVM résout les méthodes, voici ce qui y est écrit :

Après avoir vérifié les symboles des bibliothèques en recherchant la chaîne Java_, nous n'avons trouvé aucune méthode
nommée crashThisProcess.

L'inspection de la VM OpenJDK confirme la spécification mais ne fournit pas d'autres indices :
Cependant, en recherchant la chaîne crashThisProcess, nous l'avons trouvée dans libbreakpad.so :
Faisons la rétro-ingénierie du binaire pour essayer de comprendre comment cette chaîne est référencée. En ouvrant le binaire dans IDA et en recherchant la chaîne, nous la trouvons dans la section .rodata :
Cette chaîne est référencée par la structure suivante :
Après davantage de recherches, nous avons constaté que la structure correspond à la structure JNINativeMethod :
Et voici un exemple de code qui montre comment elle pourrait être utilisée :
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "simplejni native.cpp"
#include <utils/Log.h>
#include <stdio.h>
#include "jni.h"
static jint
add(JNIEnv *env, jobject thiz, jint a, jint b) {
int result = a + b;
ALOGI("%d + %d = %d", a, b, result);
return result;
}
static const char *classPathName = "com/example/android/simplejni/Native";
static JNINativeMethod methods[] = {
{"add", "(II)I", (void*)add },
};
/*
* Register several native methods for one class.
*/
static int registerNativeMethods(JNIEnv* env, const char* className,
JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
ALOGE("Native registration unable to find class '%s'", className);
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
ALOGE("RegisterNatives failed for '%s'", className);
return JNI_FALSE;
}
return JNI_TRUE;
}
/*
* Register native methods for all classes we know about.
*
* returns JNI_TRUE on success.
*/
static int registerNatives(JNIEnv* env)
{
if (!registerNativeMethods(env, classPathName,
methods, sizeof(methods) / sizeof(methods[0]))) {
return JNI_FALSE;
}
return JNI_TRUE;
}
// ----------------------------------------------------------------------------
/*
* This is called by the VM when the shared library is first loaded.
*/
typedef union {
JNIEnv* env;
void* venv;
} UnionJNIEnvToVoid;
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
UnionJNIEnvToVoid uenv;
uenv.venv = NULL;
jint result = -1;
JNIEnv* env = NULL;
ALOGI("JNI_OnLoad");
if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) {
ALOGE("ERROR: GetEnv failed");
goto bail;
}
env = uenv.env;
if (registerNatives(env) != JNI_TRUE) {
ALOGE("ERROR: registerNatives failed");
goto bail;
}
result = JNI_VERSION_1_4;
bail:
return result;
}
Les méthodes semblent être enregistrées dynamiquement lors du chargement, dans la fonction JNI_OnLoad, à l'aide de la fonction registerNativeMethods. Le dernier élément pointe vers le corps de la méthode JNI. Par exemple, dans le cas de la méthode crashThisProcess, voici le désassemblage du corps de la méthode (le nom de la sous-routine a été ajouté manuellement) :
On voit que la fonction écrit 0x2A à l'adresse mémoire 0x000000, ce qui provoquera un segfault du processus.
En expérimentant l'identification automatisée de ces structures à partir du format de signature unique des méthodes
Java, puis en vérifiant le reste de la structure, nous avons pu écrire un script (disponible sur Github) qui trouve les
méthodes JNI exportées dynamiquement via JNI_OnLoad ou statiquement dans les symboles exportés :
L'Ostorlab Mobile Application Security Scanner listera automatiquement ces méthodes dans les prochaines versions.