In dumpBitmapsProto of ActivityManagerService.java, there is a possible way for an app to access private information due to a missing permission check. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is not needed for exploitation.
CVE-2025-48650 EoP High MmsProvider/SmsProvider/MmsSmsProvider 中的 SQL 注入,加了个括号平衡的检查
CVE-2025-48653 EoP High 合并同一个 shared user id 中所有 package 请求的所有权限。如果 Package A 属于 UID U,A 没有请求某个权限 P 但是它的 UID U 有这个权限(比如 U 里的其他 app 请求了它),因为权限检查是基于 uid 的,A 实际上也可以使用这个权限,所以 PermissionController 在查看 A 的权限使用记录的时候也必须包含它使用 P 的记录。
/* We use a trick to have more optimized code (fewer pointer reloads): * ash.c: extern struct globals *const ash_ptr_to_globals; * ash_ptr_hack.c: struct globals *ash_ptr_to_globals; * This way, compiler in ash.c knows the pointer can not change. * * However, this may break on weird arches or toolchains. In this case, * set "-DBB_GLOBAL_CONST=''" in CONFIG_EXTRA_CFLAGS to disable * this optimization. */
1 2 3 4
#define INIT_S() do { \ (*(struct lineedit_statics**)not_const_pp(&lineedit_ptr_to_statics)) = xzalloc(sizeof(S)); \ barrier(); \ } while (0)
arm 的 cpu 直接就能执行 arm 指令,jvm 也不是用来执行 arm 指令的,jvm 只接受字节码。android 上的 dalvik/art 稍有不同,接受 dalvik 字节码指令集(通常称为 smali)。至于所谓的 arm 指令,那是 runtime 在用户手机上生成的,即使没有,程序也可以被解释执行。
Thank you for your submission. This vulnerability has been rated as Moderate severity with Medium Quality and unfortunately does not meet our bar for reward. We will pass this report to a feature team for remediation and will be closing this report and not providing further updates. Thank you for working with the Android Vulnerability Reward Program!
中等质量和没有赏金是意料之中的,毕竟没花时间去详细研究导致报告里的信息不够详细;至于漏洞严重性我一开始评估的是 High 高危,可能是满足了分级调节方式给我降到了 Moderate 中危,其实有点失望,不过也在意料之中。 我当时想着中危应该也会给 CVE 编号吧,看往年的页面说不定也有致谢信息,然后发现了这条:
Additionally, starting May 15th, 2023, Android will no longer assign Common Vulnerabilities and Exposures (CVEs) to most moderate severity issues. CVEs will continue to be assigned to critical and high severity vulnerabilities.
所谓“系统框架”其实指的就是 system server 进程,其中运行着大量的关键系统服务。虽然应用的进程是由 zygote fork 出来的,但实际上每个应用进程都会先向 system server 注册自己,system server 发回应用的信息,如 apk 路径,然后应用进程继续执行。只要作用于 system server 的模块都有机会篡改这个 apk 路径,打破作用域的限制,向任意应用进程注入代码。
后来 Rose @MissRose_bot 支持了新的加群批准,大喜,以为终于能摆脱坑了,然后踩进更大的坑 1.rose 的加群审批要开欢迎消息才能用,用户被批准进群后还会在群里再看见一条人机验证,完全失去优势 2.没有跟 rose 说过话的用户尝试验证会提示 bot 无法主动向用户发起私聊
实在不行了,Watchdog 和 Rose 一起开吧,能用哪个用哪个,总可以了吧? 大坑: 1.这种情况下,被 watchdog 批准的用户会被 rose 自动禁言,还要完成 rose 在群里发的验证(然后还设置了几分钟后自动删除)。经过测试,发现即使是被管理员手动批准的人也会被禁言,6… 2.如果之前已经被禁言,即使退群再通过 Rose 加群,进群之后仍然是禁言状态,必须再完成群里那个验证才能解除
继对 system server 及部分系统应用开启 R8、Android 15 strip libart.so 后,Google 计划对 system server 开启完全 R8 优化。预计系统模块将可能会大规模失效。为啥以前机子存储空间小的时候不在意,反而现在开始扣大小?搞不懂搞不懂 (我自己在 Android 14 就被影响过一次了,虽然当时是 SystemUI)
https://github.com/canyie/MagiskEoP/blob/main/screen-20220302-093745.mp4 Demo video for exploiting a vulnerability in Magisk that allows local app to gain root access without any confirmation or acceptance and execute arbitrary code with root privileges. It demonstrates silently obtaining root privileges and silently granting root to any app.
然后后面我又写腻了,我不知道该写什么项目玩,看着各位大神博客都是分析系统源码,看起来好像也不难的样子,那就开始看这块吧,想到什么地方看什么地方。我英语不好,但是官方的 api 文档都是英文的,很多时候去看文档不开翻译器看不懂,干脆直接去看系统源码,有些时候反而还容易理解一些。那个时候 android 很火的技术就是插件化,热修复,kotlin 什么的,kotlin 我在手机上玩不了,就从其他两项入手自己玩玩。
然后我还是不知道干啥,我觉得自己基础的都会了,但是自己搓的东西跟别人开源的库根本比不了。客户端技术实在是变化太快了,后面流行什么 databinding,还有 ReactNative 跨平台这些,我只有个手机根本就没法玩。(这里加一句,当年写跨平台的公众号现在都开始发鸿蒙开发了,可见当年跨平台搞得到底怎么样。)我那个时候很浮躁,很想搞点大名堂,像 weishu 的 VirtualXposed 那样,但是又不知道干啥。一直到后面 android 9.0 测试版,引入了 hidden api 限制,对我个玩插件化的人来说简直是晴天霹雳,然后赶快去网上搜相关文章。刚好 weishu 也发了个博客,分析 art 源码去绕过,看完之后觉得其实也没那么难,然后自己也提成了两个绕过方法,但是只有一台安卓5.1的手机没法测试,后面还是去找了个群友才验证成功。
上面的经历让我敢于尝试去搞搞 art 相关的,看 weishu 的 VirtualXposed 搞得那么厉害,我也想搞一个玩玩。非常感谢 weishu 以及 YAHFA、SandHook、FastHook 的作者,你们的文章带领我进入了 art 的世界。后面我终于有了一台能跑 android studio 的电脑,终于可以写 c++ 了,然后就边写边在模拟器上测试,终于整出来一个勉强能用的东西,我管这个项目叫梦境。
后面在某个群里碰到了一位老板找人付费写代码,我接了下来,几十分钟写完拿了1000块的报酬。这是我自认为的真正意义上的第一桶金,我拿着这笔钱找我妈说我想买个二手手机(我钱在 qq 上,没银行卡转不去其他地方),然后在淘宝买了个华强北 pixel 3,终于把那部装不了微信的手机换了下来。这是我真正的靠自己买到的第一个想要的东西。我都还记得我第一次摸到 Google 亲儿子,第一次看见运行原生安卓的手机,第一次见到 type c 充电口,用卖家送的充电线充电的兴奋感。
到了中考,班主任问我去不去,我选择直接放弃报名。我的义务教育阶段结束了。但是我家很想让我继续读书,上个电大也行,去找县里的中职被拒了,最后联系到一家市里的民办中职。那个暑假我在网上查什么是中等职业学校,搜到了中职生也是有自己的升学渠道的,突然我就又想升上来大学看看究竟是什么样的了。我第一次去市里上学,第一次吃食堂,第一次住集体宿舍。可能因为课程比较简单压力很小,我竟然能坚持上这个学,竟然真的为了升学准备文化课。我的上学生活又开始了。那个时候还在开发 EdXposed 的西大师和 ksm 找到我讨论 art 相关的问题,后面他们创立了 LSPosed,把我也拉进去了。
这就是故事的全部。你可以发现我对未来没什么计划。我不是一个聪明的孩子,我要是聪明的话就学 OI 打比赛去了,感谢上天愿意赐我一条生路。我也不是一个擅长预测风口的人,电商网购直播短视频等等风口我一个都没预料到,我做事纯凭兴趣,小时候看着 kingroot 觉得很厉害也想搞的小孩也不会想到长大之后是这样。现在回过头来看,我可能应该开个公众号,把人引流过去,然后接广告或者开知识星球来变现,再或者去视频平台投个《20岁谷歌认证世界顶级研究员的一天是什么样的》给自己出道。不过我最后还是没干就是了。
清晨八点多,你从睡梦中自然醒来。 这一夜你睡得很好,没有在三四点的时候睁眼望着天花板,也没有往常早晨醒来时的疲惫感。 昨晚刚下了一场雨,植物的叶片还挂着水滴,清晨的阳光从窗外透进来,照得你浑身暖洋洋的。你知道,冬天已经过去了。 身体传来的不是躯体化带来的疼痛或疲惫,而是少见的饥饿感。 洗漱完出门,早餐店的叔叔阿姨早已经开始了一天的忙碌。你点了往常最爱吃的肉包,鲜嫩的肉汁在你嘴里绽放。你好久好久没有细细地品味过你最爱吃的东西了。 眼前的世界是真实的,笼罩在眼前似有似无的黑幕已经消散,灵魂重新匹配身体,而身体又实实在在地触摸着世界,路边的红绿灯再也不会在视野里突然消失。 你看见一片还带着露珠的叶子,翠绿的颜色,水珠反射着太阳的光芒。你感觉它焕发着生命的气息。你喜欢它。你可以没来由地喜欢一件事物。 你突然觉得其实有些事情也没有那么难做。好像有好多事情一直放在 TODO list 里,打扫房间、跟许久不见的朋友一起逛街,或是去听曾经很喜欢的歌手的演唱会。有空可以清理一下 TODO 了,你想。 从这一天开始,你内心的刑期悄然结束了。夜晚,你不再辗转反侧,也不会凌晨三点醒来。你开始对事物产生兴趣,重新有了自己喜欢的东西。你慢慢停掉了药物。不知道持续了多少时间的冬天自己过去了,春天自己会来。
Your community 小页页的胡言乱语 was blocked for violations of the Telegram Terms of Service (https://telegram.org/tos) based on user reports confirmed by our moderators.
Thank you! Your appeal has been successfully submitted. Our team’s supervisors will check it as soon as possible. (No more response)
/** * Return a global shared Resources object that provides access to only * system resources (no application resources), is not configured for the * current screen (can not use dimension units, does not change based on * orientation, etc), and is not affected by Runtime Resource Overlay. */ publicstatic Resources getSystem(){ synchronized (sSync) { Resources ret = mSystem; if (ret == null) { ret = new Resources(); mSystem = ret; } return ret; } }
它的注释里明确表示 is not affected by Runtime Resource Overlay,似乎我们的问题就这么简单地解决了,只是简单的 API 用错了而已……? 如果 Resources.getSystem() 真的完全不受 RRO 影响,那测试代码应该输出来自 frameworks/base/core/res/res/values/config.xml 的空值而不是来自 RRO 的值。所以问题并没有这么简单,我们继续。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/** * Only for creating the System resources. This is the only constructor that doesn't add * Resources itself to the ResourcesManager list of all Resources references. */ @UnsupportedAppUsage privateResources(){ mClassLoader = ClassLoader.getSystemClassLoader(); sResourcesHistory.add(this);
final DisplayMetrics metrics = new DisplayMetrics(); metrics.setToDefaults();
final Configuration config = new Configuration(); config.setToDefaults();
mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config, new DisplayAdjustments()); }
/** * Return a global shared asset manager that provides access to only * system assets (no application assets). * @hide */ @UnsupportedAppUsage publicstatic AssetManager getSystem(){ synchronized (sSync) { createSystemAssetsInZygoteLocked(false, FRAMEWORK_APK_PATH); return sSystem; } }
/** * This must be called from Zygote so that system assets are shared by all applications. * @hide */ @GuardedBy("sSync") @VisibleForTesting publicstaticvoidcreateSystemAssetsInZygoteLocked(boolean reinitialize, String frameworkPath){ if (sSystem != null && !reinitialize) { return; }
try { final ArrayList<ApkAssets> apkAssets = new ArrayList<>(); apkAssets.add(ApkAssets.loadFromPath(frameworkPath, ApkAssets.PROPERTY_SYSTEM));
// TODO(Ravenwood): overlay support? final String[] systemIdmapPaths = RavenwoodEnvironment.getInstance().isRunningOnRavenwood() ? new String[0] : OverlayConfig.getZygoteInstance().createImmutableFrameworkIdmapsInZygote(); for (String idmapPath : systemIdmapPaths) { apkAssets.add(ApkAssets.loadOverlayFromPath(idmapPath, ApkAssets.PROPERTY_SYSTEM)); }
sSystemApkAssetsSet = new ArraySet<>(apkAssets); sSystemApkAssets = apkAssets.toArray(new ApkAssets[0]); if (sSystem == null) { sSystem = new AssetManager(true/*sentinel*/); } sSystem.setApkAssets(sSystemApkAssets, false/*invalidateCaches*/); } catch (IOException e) { thrownew IllegalStateException("Failed to create system AssetManager", e); } }
@VisibleForTesting publicOverlayConfig(@Nullable File rootDirectory, @Nullable Supplier<OverlayScanner> scannerFactory, @Nullable PackageProvider packageProvider){ Preconditions.checkArgument((scannerFactory == null) != (packageProvider == null), "scannerFactory and packageProvider cannot be both null or both non-null");
final ArrayList<OverlayPartition> partitions; if (rootDirectory == null) { partitions = new ArrayList<>( PackagePartitions.getOrderedPartitions(OverlayPartition::new)); } else { // Rebase the system partitions and settings file on the specified root directory. partitions = new ArrayList<>(PackagePartitions.getOrderedPartitions( p -> new OverlayPartition( new File(rootDirectory, p.getNonConicalFolder().getPath()), p))); } mIsDefaultPartitionOrder = !sortPartitions(PARTITION_ORDER_FILE_PATH, partitions); mPartitionOrder = generatePartitionOrderString(partitions);
final ArrayList<ParsedConfiguration> overlays = new ArrayList<>(); for (int i = 0, n = partitions.size(); i < n; i++) { final OverlayPartition partition = partitions.get(i); final OverlayScanner scanner = (scannerFactory == null) ? null : scannerFactory.get(); final ArrayList<ParsedConfiguration> partitionOverlays = OverlayConfigParser.getConfigurations(partition, scanner, packageManagerOverlayInfos, activeApexesPerPartition.getOrDefault(partition.type, Collections.emptyList())); if (partitionOverlays != null) { overlays.addAll(partitionOverlays); continue; }
// If the configuration file is not present, then use android:isStatic and // android:priority to configure the overlays in the partition. // TODO(147840005): Remove converting static overlays to immutable, default-enabled // overlays when android:siStatic and android:priority are fully deprecated. final ArrayList<ParsedOverlayInfo> partitionOverlayInfos; if (scannerFactory != null) { partitionOverlayInfos = new ArrayList<>(scanner.getAllParsedInfos()); } else { // Filter out overlays not present in the partition. partitionOverlayInfos = new ArrayList<>(packageManagerOverlayInfos.values()); for (int j = partitionOverlayInfos.size() - 1; j >= 0; j--) { if (!partition.containsFile(partitionOverlayInfos.get(j) .getOriginalPartitionPath())) { partitionOverlayInfos.remove(j); } } }
// Static overlays are configured as immutable, default-enabled overlays. final ArrayList<ParsedConfiguration> partitionConfigs = new ArrayList<>(); for (int j = 0, m = partitionOverlayInfos.size(); j < m; j++) { final ParsedOverlayInfo p = partitionOverlayInfos.get(j); if (p.isStatic) { partitionConfigs.add(new ParsedConfiguration(p.packageName, true/* enabled */, false/* mutable */, partition.policy, p, null)); } }
for (int i = 0, n = overlays.size(); i < n; i++) { // Add the configurations to a map so definitions of an overlay in an earlier // partition can be replaced by an overlay with the same package name in a later // partition. final ParsedConfiguration config = overlays.get(i); mConfigurations.put(config.packageName, new Configuration(config, i)); } }
/** * Retrieves a list of immutable framework overlays in order of least precedence to greatest * precedence. */ @VisibleForTesting public ArrayList<IdmapInvocation> getImmutableFrameworkOverlayIdmapInvocations(){ final ArrayList<IdmapInvocation> idmapInvocations = new ArrayList<>(); final ArrayList<Configuration> sortedConfigs = getSortedOverlays(); for (int i = 0, n = sortedConfigs.size(); i < n; i++) { final Configuration overlay = sortedConfigs.get(i); if (overlay.parsedConfig.mutable || !overlay.parsedConfig.enabled || !"android".equals(overlay.parsedConfig.parsedInfo.targetPackageName)) { continue; }
// Only enforce that overlays targeting packages with overlayable declarations abide by // those declarations if the target sdk of the overlay is at least Q (when overlayable // was introduced). finalboolean enforceOverlayable = overlay.parsedConfig.parsedInfo.targetSdkVersion >= Build.VERSION_CODES.Q;
// Determine if the idmap for the current overlay can be generated in the last idmap // create-multiple invocation. IdmapInvocation invocation = null; if (!idmapInvocations.isEmpty()) { final IdmapInvocation last = idmapInvocations.get(idmapInvocations.size() - 1); if (last.enforceOverlayable == enforceOverlayable && last.policy.equals(overlay.parsedConfig.policy)) { invocation = last; } }
if (invocation == null) { invocation = new IdmapInvocation(enforceOverlayable, overlay.parsedConfig.policy); idmapInvocations.add(invocation); }
/** * The list of all system partitions that may contain packages in ascending order of * specificity (the more generic, the earlier in the list a partition appears). */ privatestaticfinal ArrayList<SystemPartition> SYSTEM_PARTITIONS = new ArrayList<>(Arrays.asList( new SystemPartition(Environment.getRootDirectory(), PARTITION_SYSTEM, Partition.PARTITION_NAME_SYSTEM, true/* containsPrivApp */, false/* containsOverlay */), // new SystemPartition(Environment.getVendorDirectory(), PARTITION_VENDOR, Partition.PARTITION_NAME_VENDOR, true/* containsPrivApp */, true/* containsOverlay */), new SystemPartition(Environment.getOdmDirectory(), PARTITION_ODM, Partition.PARTITION_NAME_ODM, true/* containsPrivApp */, true/* containsOverlay */), new SystemPartition(Environment.getOemDirectory(), PARTITION_OEM, Partition.PARTITION_NAME_OEM, false/* containsPrivApp */, true/* containsOverlay */), new SystemPartition(Environment.getProductDirectory(), PARTITION_PRODUCT, Partition.PARTITION_NAME_PRODUCT, true/* containsPrivApp */, true/* containsOverlay */), new SystemPartition(Environment.getSystemExtDirectory(), PARTITION_SYSTEM_EXT, Partition.PARTITION_NAME_SYSTEM_EXT, true/* containsPrivApp */, true/* containsOverlay */)));
/** * Recursively searches the directory for overlay APKs. If an overlay is found with the same * package name as a previously scanned overlay, the info of the new overlay will replace the * info of the previously scanned overlay. */ publicvoidscanDir(File partitionOverlayDir){ if (!partitionOverlayDir.exists() || !partitionOverlayDir.isDirectory()) { return; }
if (!partitionOverlayDir.canRead()) { Log.w(TAG, "Directory " + partitionOverlayDir + " cannot be read"); return; }
final File[] files = partitionOverlayDir.listFiles(); if (files == null) { return; }
for (int i = 0; i < files.length; i++) { final File f = files[i]; if (f.isDirectory()) { scanDir(f); }
if (!f.isFile() || !f.getPath().endsWith(".apk")) { continue; }
final ParsedOverlayInfo info = parseOverlayManifest(f, mExcludedOverlayPackages); if (info == null) { continue; }
/** * Load in commonly used resources, so they can be shared across processes. * * These tend to be a few Kbytes, but are frequently in the 20-40K range, and occasionally even * larger. * @hide */ @UnsupportedAppUsage publicstaticvoidpreloadResources(){ try { final Resources sysRes = Resources.getSystem(); sysRes.startPreloading(); if (PRELOAD_RESOURCES) { Log.i(TAG, "Preloading resources...");
long startTime = SystemClock.uptimeMillis(); TypedArray ar = sysRes.obtainTypedArray( com.android.internal.R.array.preloaded_drawables); int numberOfEntries = preloadDrawables(sysRes, ar); ar.recycle(); Log.i(TAG, "...preloaded " + numberOfEntries + " resources in " + (SystemClock.uptimeMillis() - startTime) + "ms.");
@NonNull Set<UserPackage> registerFabricatedOverlay( @NonNull final FabricatedOverlayInternal overlay) throws OperationFailedException { if (FrameworkParsingPackageUtils.validateName(overlay.overlayName, false/* requireSeparator */, true/* requireFilename */) != null) { thrownew OperationFailedException( "overlay name can only consist of alphanumeric characters, '_', and '.'"); }
final FabricatedOverlayInfo info = mIdmapManager.createFabricatedOverlay(overlay); if (info == null) { thrownew OperationFailedException("failed to create fabricated overlay"); }
final Set<UserPackage> updatedTargets = new ArraySet<>(); for (int userId : mSettings.getUsers()) { updatedTargets.addAll(registerFabricatedOverlay(info, userId)); } return updatedTargets; }
// Generate the file path of the fabricated overlay and ensure it does not collide with an // existing path. Re-registering a fabricated overlay will always result in an updated path. std::string path; std::string file_name; do { constexprsize_t kSuffixLength = 4; conststd::string random_suffix = RandomStringForPath(kSuffixLength); file_name = StringPrintf("%s-%s-%s.frro", overlay.packageName.c_str(), overlay.overlayName.c_str(), random_suffix.c_str()); path = StringPrintf("%s/%s", kIdmapCacheDir.data(), file_name.c_str());
// Invoking std::filesystem::exists with a file name greater than 255 characters will cause this // process to abort since the name exceeds the maximum file name size. constsize_t kMaxFileNameLength = 255; if (file_name.size() > kMaxFileNameLength) { return error( base::StringPrintf("fabricated overlay file name '%s' longer than %zu characters", file_name.c_str(), kMaxFileNameLength)); } } while (std::filesystem::exists(path)); builder.setFrroPath(path);
constuid_t uid = IPCThreadState::self()->getCallingUid(); if (!UidHasWriteAccessToPath(uid, path)) { return error(base::StringPrintf("will not write to %s: calling uid %d lacks write access", path.c_str(), uid)); }
constauto frro = builder.Build(); if (!frro) { return error(StringPrintf("failed to serialize '%s:%s': %s", overlay.packageName.c_str(), overlay.overlayName.c_str(), frro.GetErrorMessage().c_str())); } // Persist the fabricated overlay. umask(kIdmapFilePermissionMask); std::ofstream fout(path); if (fout.fail()) { return error("failed to open frro path " + path); } auto result = frro->ToBinaryStream(fout); if (!result) { unlink(path.c_str()); return error("failed to write to frro path " + path + ": " + result.GetErrorMessage()); } if (fout.fail()) { unlink(path.c_str()); return error("failed to write to frro path " + path); }
/** * Call this to synchronize the Settings for a user with what PackageManager knows about a user. * Returns a list of target packages that must refresh their overlays. This list is the union * of two sets: the set of targets with currently active overlays, and the * set of targets that had, but no longer have, active overlays. */ @NonNull ArraySet<UserPackage> updateOverlaysForUser(finalint newUserId){ if (DEBUG) { Slog.d(TAG, "updateOverlaysForUser newUserId=" + newUserId); }
// Remove the settings of all overlays that are no longer installed for this user. final ArraySet<UserPackage> updatedTargets = new ArraySet<>(); final ArrayMap<String, PackageState> userPackages = mPackageManager.initializeForUser( newUserId); CollectionUtils.addAll(updatedTargets, removeOverlaysForUser( (info) -> !userPackages.containsKey(info.packageName), newUserId));
final ArraySet<String> overlaidByOthers = new ArraySet<>(); for (PackageState packageState : userPackages.values()) { var pkg = packageState.getAndroidPackage(); final String overlayTarget = pkg == null ? null : pkg.getOverlayTarget(); if (!TextUtils.isEmpty(overlayTarget)) { overlaidByOthers.add(overlayTarget); } }
// Update the state of all installed packages containing overlays, and initialize new // overlays that are not currently in the settings. for (int i = 0, n = userPackages.size(); i < n; i++) { final PackageState packageState = userPackages.valueAt(i); var pkg = packageState.getAndroidPackage(); if (pkg == null) { continue; }
// When a new user is switched to for the first time, package manager must be // informed of the overlay paths for all overlaid packages installed in the user. if (overlaidByOthers.contains(packageName)) { updatedTargets.add(UserPackage.of(newUserId, packageName)); } } catch (OperationFailedException e) { Slog.e(TAG, "failed to initialize overlays of '" + packageName + "' for user " + newUserId + "", e); } }
// Update the state of all fabricated overlays, and initialize fabricated overlays in the // new user. for (final FabricatedOverlayInfo info : getFabricatedOverlayInfos()) { try { CollectionUtils.addAll(updatedTargets, registerFabricatedOverlay( info, newUserId)); } catch (OperationFailedException e) { Slog.e(TAG, "failed to initialize fabricated overlay of '" + info.path + "' for user " + newUserId + "", e); } }
// Collect all of the categories in which we have at least one overlay enabled. final ArraySet<String> enabledCategories = new ArraySet<>(); final ArrayMap<String, List<OverlayInfo>> userOverlays = mSettings.getOverlaysForUser(newUserId); finalint userOverlayTargetCount = userOverlays.size(); for (int i = 0; i < userOverlayTargetCount; i++) { final List<OverlayInfo> overlayList = userOverlays.valueAt(i); finalint overlayCount = overlayList != null ? overlayList.size() : 0; for (int j = 0; j < overlayCount; j++) { final OverlayInfo oi = overlayList.get(j); if (oi.isEnabled()) { enabledCategories.add(oi.category); } } }
// Enable the default overlay if its category does not have a single overlay enabled. for (final String defaultOverlay : mDefaultOverlays) { try { // OverlayConfig is the new preferred way to enable overlays by default. This legacy // default enabled method was created before overlays could have a name specified. // Only allow enabling overlays without a name using this mechanism. final OverlayIdentifier overlay = new OverlayIdentifier(defaultOverlay);
final OverlayInfo oi = mSettings.getOverlayInfo(overlay, newUserId); if (!enabledCategories.contains(oi.category)) { Slog.w(TAG, "Enabling default overlay '" + defaultOverlay + "' for target '" + oi.targetPackageName + "' in category '" + oi.category + "' for user " + newUserId); mSettings.setEnabled(overlay, newUserId, true); if (updateState(oi, newUserId, 0)) { CollectionUtils.add(updatedTargets, UserPackage.of(oi.userId, oi.targetPackageName)); } } } catch (OverlayManagerSettings.BadKeyException e) { Slog.e(TAG, "Failed to set default overlay '" + defaultOverlay + "' for user " + newUserId, e); } }
Status Idmap2Service::acquireFabricatedOverlayIterator(int32_t* _aidl_return) { std::lock_guard l(frro_iter_mutex_); if (frro_iter_.has_value()) { LOG(WARNING) << "active ffro iterator was not previously released"; } frro_iter_ = std::filesystem::directory_iterator(kIdmapCacheDir); if (frro_iter_id_ == std::numeric_limits<int32_t>::max()) { frro_iter_id_ = 0; } else { ++frro_iter_id_; } *_aidl_return = frro_iter_id_; return ok(); }
Status Idmap2Service::nextFabricatedOverlayInfos(int32_t iteratorId, std::vector<os::FabricatedOverlayInfo>* _aidl_return) { std::lock_guard l(frro_iter_mutex_);
constexprsize_t kMaxEntryCount = 100; if (!frro_iter_.has_value()) { return error("no active frro iterator"); } elseif (frro_iter_id_ != iteratorId) { return error("incorrect iterator id in a call to next"); }
/** * Full paths to the locations of extra resource packages (runtime overlays) * this application uses. This field is only used if there are extra resource * packages, otherwise it is null. * * {@hide} */ @UnsupportedAppUsage public String[] resourceDirs;
/** * Contains the contents of {@link #resourceDirs} and along with paths for overlays that may or * may not be APK packages. * * {@hide} */ public String[] overlayPaths;
@UnsupportedAppUsage public Resources getResources(){ if (mResources == null) { final String[] splitPaths; try { splitPaths = getSplitPaths(null); } catch (NameNotFoundException e) { // This should never fail. thrownew AssertionError("null split not found"); }
if (Process.myUid() == mApplicationInfo.uid) { ResourcesManager.getInstance().initializeApplicationPaths(mResDir, splitPaths); }
if (userId == UserHandle.USER_SYSTEM) { // Keep the overlays in the system application info (and anything special cased as well) // up to date to make sure system ui is themed correctly. for (int i = 0; i < numberOfPendingChanges; i++) { final String targetPackageName = pendingChanges.keyAt(i); final OverlayPaths newOverlayPaths = pendingChanges.valueAt(i); maybeUpdateSystemOverlays(targetPackageName, newOverlayPaths); } }
if (updateFrameworkRes) { // Update system server components that need to know about changed overlays. Because the // overlay is applied in ActivityThread, we need to serialize through its thread too. final Executor executor = ActivityThread.currentActivityThread().getExecutor(); final DisplayManagerInternal display = LocalServices.getService(DisplayManagerInternal.class); if (display != null) { executor.execute(display::onOverlayChanged); } if (mWindowManager != null) { executor.execute(mWindowManager::onOverlayChanged); } } }
@GuardedBy(anyOf = {"mService", "mProcLock"}) voidupdateApplicationInfoLOSP(List<String> packagesToUpdate, int userId, boolean updateFrameworkRes){ final ArrayMap<String, ApplicationInfo> applicationInfoByPackage = new ArrayMap<>(); for (int i = packagesToUpdate.size() - 1; i >= 0; i--) { final String packageName = packagesToUpdate.get(i); final ApplicationInfo ai = mService.getPackageManagerInternal().getApplicationInfo( packageName, STOCK_PM_FLAGS, Process.SYSTEM_UID, userId); if (ai != null) { applicationInfoByPackage.put(packageName, ai); } } mService.mActivityTaskManager.updateActivityApplicationInfo(userId, applicationInfoByPackage);
final ArrayList<WindowProcessController> targetProcesses = new ArrayList<>(); for (int i = mLruProcesses.size() - 1; i >= 0; i--) { final ProcessRecord app = mLruProcesses.get(i); if (app.getThread() == null) { continue; }
@VisibleForTesting(visibility = PACKAGE) publicvoidhandleApplicationInfoChanged(@NonNull final ApplicationInfo ai){ // Updates triggered by package installation go through a package update // receiver. Here we try to capture ApplicationInfo changes that are // caused by other sources, such as overlays. That means we want to be as conservative // about code changes as possible. Take the diff of the old ApplicationInfo and the new // to see if anything needs to change. LoadedApk apk; LoadedApk resApk; // Update all affected loaded packages with new package information synchronized (mResourcesManager) { WeakReference<LoadedApk> ref = mPackages.get(ai.packageName); apk = ref != null ? ref.get() : null; ref = mResourcePackages.get(ai.packageName); resApk = ref != null ? ref.get() : null; for (ActivityClientRecord ar : mActivities.values()) { if (ar.activityInfo.applicationInfo.packageName.equals(ai.packageName)) { ar.activityInfo.applicationInfo = ai; if (apk != null || resApk != null) { ar.packageInfo = apk != null ? apk : resApk; } else { apk = ar.packageInfo; } } } }
if (apk != null) { final ArrayList<String> oldPaths = new ArrayList<>(); LoadedApk.makePaths(this, apk.getApplicationInfo(), oldPaths); apk.updateApplicationInfo(ai, oldPaths); } if (resApk != null) { final ArrayList<String> oldPaths = new ArrayList<>(); LoadedApk.makePaths(this, resApk.getApplicationInfo(), oldPaths); resApk.updateApplicationInfo(ai, oldPaths); } if (android.content.res.Flags.systemContextHandleAppInfoChanged() && mSystemThread) { finalvar systemContext = getSystemContext(); if (systemContext.getPackageName().equals(ai.packageName)) { // The system package is not tracked directly, but still needs to receive updates to // its application info. final ArrayList<String> oldPaths = new ArrayList<>(); LoadedApk.makePaths(this, systemContext.getApplicationInfo(), oldPaths); systemContext.mPackageInfo.updateApplicationInfo(ai, oldPaths); } }
synchronized (mResourcesManager) { // Update all affected Resources objects to use new ResourcesImpl mResourcesManager.applyAllPendingAppInfoUpdates(); }
/** * Update the ApplicationInfo for an app. If oldPaths is null, all the paths are considered * new. * @param aInfo The new ApplicationInfo to use for this LoadedApk * @param oldPaths The code paths for the old ApplicationInfo object. null means no paths can * be reused. */ publicvoidupdateApplicationInfo(@NonNull ApplicationInfo aInfo, @Nullable List<String> oldPaths){ if (!setApplicationInfo(aInfo)) { return; }
final List<String> newPaths = new ArrayList<>(); makePaths(mActivityThread, aInfo, newPaths); final List<String> addedPaths = new ArrayList<>(newPaths.size());
if (oldPaths != null) { for (String path : newPaths) { final String apkName = path.substring(path.lastIndexOf(File.separator)); boolean match = false; for (String oldPath : oldPaths) { final String oldApkName = oldPath.substring(oldPath.lastIndexOf(File.separator)); if (apkName.equals(oldApkName)) { match = true; break; } } if (!match) { addedPaths.add(path); } } } else { addedPaths.addAll(newPaths); } synchronized (mLock) { createOrUpdateClassLoaderLocked(addedPaths); if (mResources != null) { final String[] splitPaths; try { splitPaths = getSplitPaths(null); } catch (NameNotFoundException e) { // This should NEVER fail. thrownew AssertionError("null split not found"); }
/** * Starts the small tangle of critical services that are needed to get the system off the * ground. These services have complex mutual dependencies which is why we initialize them all * in one place here. Unless your service is also entwined in these dependencies, it should be * initialized in one of the other functions. */ privatevoidstartBootstrapServices(@NonNull TimingsTraceAndSlog t){ t.traceBegin("startBootstrapServices"); // ... t.traceBegin("StartPackageManagerService"); try { Watchdog.getInstance().pauseWatchingCurrentThread("packagemanagermain"); mPackageManagerService = PackageManagerService.main( mSystemContext, installer, domainVerificationService, mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF); } finally { Watchdog.getInstance().resumeWatchingCurrentThread("packagemanagermain"); } // ... // Set up the Application instance for the system process and get started. t.traceBegin("SetSystemProcess"); mActivityManagerService.setSystemProcess(); t.traceEnd(); // Manages Overlay packages t.traceBegin("StartOverlayManagerService"); mSystemServiceManager.startService(new OverlayManagerService(mSystemContext)); t.traceEnd(); t.traceEnd(); // startBootstrapServices }
CVE-2025-48565 EoP High 补丁链接和 CVE-2025-48564 是一样的,推测这个 CVE 是分配给第三个问题的
CVE-2025-48572 EoP High CVE-2023-40111 和 CVE-2025-22437 的变种。MediaSessionService 启动应用通过 setMediaButtonReceiver() 传递的 PendingIntent 时不要通过 setBackgroundActivityStartsAllowed(true) 的方式去允许启动前台服务,因为这样可能会不小心允许 app 绕过 BAL 限制启动前台 activity,改成 tempAllowlistTargetPkgIfPossible()。
CVE-2025-48573 EoP High 此漏洞由我发现并报告。 MediaController sendCommand() 使用 tempAllowlistTargetPkgIfPossible() 尝试将被调用的 app 加进白名单允许其启动前台服务并获得 while in use 权限,即使其在后台。补丁认为这个调用只是用来交换信息的,直接把 tempAllowlistTargetPkgIfPossible() 删掉。
CVE-2025-48580 EoP High 此漏洞由我发现并报告。 框架的 MediaBrowser API 连接到应用提供的 MediaBrowserService 时支持不使用 BIND_INCLUDE_CAPABILITIES 标志,避免意外授予 while in use 权限。
CVE-2025-48591 ID High MMS Service 读取 URI (readPduFromContentUri())的时候校验 user id 相同。也是我很早就发现过的漏洞,没来得及报就被修了。
CVE-2025-48592 ID High media codec2 C2SoftDav1dDec.cpp 里禁用 layer buffers,看起来是处理不足导致的越界读
CVE-2025-48628 ID High PrintManagerService 里使用 ContentProvider.getUserIdFromAuthority() 而非 Uri.getEncodedUserInfo() 检查跨用户,防止被 encoded 的 @ 字符绕过检查,跟之前的 CVE-2025-0082/CVE-2025-0083/CVE-2025-26453 类似。也是我很早之前发现过报过但是 duplicate 的洞。
CVE-2025-48633 ID High 添加 device owner 的时候保证拿到所有的 account 而不会受 visibility 限制。又乱给类型,明明应该是 EoP
CVE-2025-48576 DoS High 删除从未被使用过的 updateNotificationChannelGroupFromPrivilegedListener API,可被滥用创建大量 NotificationChannelGroup 耗尽系统内存。又是我报了结果 duplicate 的洞。
CVE-2025-48584 DoS High 限制通过 Android 16 新 API createConversationNotificationChannelForPackage() 可创建的 NotificationChannel 的数量。嗯,没错,又是报了然后 duplicate。
CVE-2025-48590 DoS High AppOpsService 对于 target API <= Q 的 app 会豁免 attribution tag 必须有效的检查,可被滥用绕过先前大量与 attribution tag 相关的 DoS 补丁。 感觉对于这种需要低 target 才能触发的漏洞,认不认完全看审核团队的心情,我很早就发现了这个地方有问题但是觉得不会认就没报,这块规则也不是很清楚,头大
CVE-2025-48607 DoS High CVE-2025-26429 的变体,AppOpsService getPackagesForOpsForDevice() 的返回值使用 ParceledListSlice 切片传输,避免传输过大数据造成 TransactionTooLargeException。
CVE-2025-48614 DoS High CVE-2024-49736 的变体,DSU 模式下禁止通过编程方式触发恢复出厂设置。
If a content: URI is passed, the intent should also have the flag Intent.FLAG_GRANT_READ_URI_PERMISSION and the uri should be added to the android.content.ClipData of the intent.
但是说真的,我觉得限制必须手动发出 URI grant 不是很合理。目前我还没找到有其他地方能让 URI 权限被授予给 ManagedProvisioning 的地方,有人找到的话可以跟我交流一下。
CVE-2025-26464 EoP High 此漏洞由我发现并报告。 AppSearchManagerService.executeAppFunction() 内会使用 BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS 标志绑定到 app 提供的服务,造成无限制的 BAL 绕过。只影响 15,因为这块代码是意外留在 android 15 里的,16 上这块功能已经被移动到了 AppFunctionManagerService 里。补丁把遗留代码给移除了。
In onActivityResult of VoicemailSettingsActivity.java, there is a possible work profile contact number leak due to a confused deputy. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is not needed for exploitation.
更新:补丁出来了,把 VoicemailSettingsActivity onActivityResult 中原本的比较 user id 的跨用户检查改成了使用 android 16 新 api 去检查来源 app 有没有权限读返回的 uri。没看懂咋绕过原先的检查,需要后续分析。 再更新:应该是使用 content://com.android.contacts/data_enterprise/phones 从 work profile 里泄露出联系人号码
In markMediaAsFavorite of MediaProvider.java, there is a possible way to bypass the WRITE_EXTERNAL_STORAGE permission due to a confused deputy. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is needed for exploitation.
CVE-2025-48527 ID High 如果一个用户已经锁定并且设置了锁定时不显示敏感通知信息,那么在设置的 NotificationHistoryActivity 里不要显示这个用户的通知内容。
CVE-2025-48551 ID High ChooserActivity 使用启动 chooser 的 user 而非 personal profile 去打开图片编辑器
CVE-2025-48560 ID High AbstractAccessibilityServiceConnection 里检查 ACCESSIBILITY_MOTION_EVENT_OBSERVING 权限的代码放在了 Binder.clearCallingIdentity() 后面,造成权限绕过。
CVE-2025-48524 DoS High 此漏洞由我发现并报告。 和上面的 CVE-2025-48545 类似,wifi 模块内也存在着不安全的权限检查,如果调用者是系统 app 就放行,因此可被 SDK Sandbox 绕过。
CVE-2025-48534 DoS High 小区广播服务(CellBroadcastService)查找默认的 CellBroadcastReceiver app 时需要指定 MATCH_SYSTEM_ONLY flag,仅匹配系统应用,防止被第三方恶意应用抢占影响可用性。
In getContextForResourcesEnsuringCorrectCachedApkPaths of RemoteViews.java, there is a possible way to load arbitrary java code in a privileged context due to a confused deputy. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is needed for exploitation.
CVE-2025-26445 ID High ConnectivityService offerNetwork 添加权限检查。
CVE-2025-26453 ID High 蓝牙 BluetoothOppSendFileInfo 内读取 decode 过的 Authority 再手动解析 user id 而非使用 getUserInfo,防止被 encoded 的 @ 字符绕过检查,跟之前的 CVE-2025-0082/CVE-2025-0083 类似。
CVE-2025-26436 EoP High 清楚一个 PendingIntentRecord 的 BAL token 的时候也要顺带清掉它带的 allowlist duration。
CVE-2025-26440 EoP High CameraService 优化 app 前后台状态改变时的处理代码,避免后台 app 仍然能使用摄像头的问题
CVE-2025-26444 EoP High VoiceInteractionManagerService 会在用户强行停止第三方语音助手时将目前选择的语音助手设置成系统默认的,重设 ROLE_ASSISTANT 会导致额外权限被授权给默认的语音助手,这不是合理的行为所以删除相关处理。
CVE-2025-26424 ID High VpnManagerService 新增的 getFromVpnProfileStore、putIntoVpnProfileStore、removeFromVpnProfileStore、listFromVpnProfileStore 添加权限检查。只影响 15
CVE-2025-26442 ID High 之前 CVE-2024-49742 的补丁有问题,错误的检查了 resolve 出来的组件的包名而非整个组件名,所以可以通过声明两个组件的方式来绕过补丁。
CVE-2025-26429 DoS High AppOpsService collectOps 内限制返回的数据大小,避免过大超过 binder 数据传输大小限制造成异常。
In multiple functions of GrantPermissionsActivity.java , there is a possible way to trick the user into granting the incorrect permission due to permission overload. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is not needed for exploitation.
CVE-2025-26421 EoP High 添加了一个可以自定义的 config_biometric_protected_package_names,当用户在设置里尝试强行停止、禁用或卸载这些被保护的 app 的更新时要求用户验证身份再继续。没太看懂,漏洞描述:
In multiple locations, there is a possible lock screen bypass due to a logic error in the code. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is not needed for exploitation.
CVE-2025-26423 EoP High 添加 Wi-Fi 网络时检查附带的 IpConfiguration 和 ProxyInfo 的大小。
CVE-2025-26425 EoP High RoleService 的 getDefaultApplicationAsUser/setDefaultApplicationAsUser 两个 API 限制只能在 Android 14+ 上被调用,因为里面是通过检查 MANAGE_DEFAULT_APPLICATIONS 这个权限来确保这些 API 不被滥用的,在 Android 13 或更低版本的系统上 MANAGE_DEFAULT_APPLICATIONS 权限并不存在,所以第三方 app 可以自己定义这个权限然后给自己授权从而绕过 RoleService 里面的检查。
CVE-2025-26430 EoP High Settings SpaAppBridgeActivity 里检查 package name 只能包含合法字符串,猜测是注入 / 字符伪造后面的 user id
CVE-2025-22416 EoP High ChooserActivity 预览 ChooserTarget 的 icon 时先检查是否有权限读取 uri。
CVE-2025-22417 EoP High WindowManager transaction 过程中的 race condition,看的不是很明白。漏洞描述:
In finishTransition of Transition.java, there is a possible way to bypass touch filtering restrictions due to a tapjacking/overlay attack. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is needed for exploitation.
CVE-2025-22422 EoP High System UI 处理 authentication prompt 时的逻辑 bug,当 top app 和 client app 不一样时说明可能真正请求验证的 app 已经被恶意 app 顶到了后台,用户看见的可能是恶意 app 显示的内容(类似 task hijacking),这个时候需要告知原来的 client 验证失败。但是在使用了 createConfirmDeviceCredentialIntent (会 resolve 到 settings 的 ConfirmDeviceCredentialActivity)而非手动调用 BiometricPrompt 进行验证的情况,此时直接发起验证流程的 client package 是 settings,即使后续 top activity 变成了 settings 的其他 activitiy,这个判断也会认为 top app 并没有改变从而不会取消验证请求。利用这个 bug 应该需要能找到足够具有迷惑性的 activity 所以应该还挺麻烦的?补丁加了一个 top activity 的 class name 检查。 相关引用:CVE-2020-27059
CVE-2025-22424 EoP High CVE-2025-22426 EoP High pm ComputerEngine resolveContentProvider 同时接收 uri 的 authority 和 user id,但调用者可能会把 user id 存储在 authority 内,要把这个 user id 提取出来。不太清楚具体是哪里可能会发生这种情况,感觉挺有意思的
CVE-2025-22434 EoP High 用户点击键盘上打开系统设置的快捷键时没有检查是否有锁屏就直接打开了设置 app,造成锁屏绕过。
CVE-2025-0080 EoP High PackageInstaller unarchive app 的对话框添加 flag 防止被 overlay。
CVE-2024-43090 ID High 此漏洞由我发现并报告。 去年11月放过的,systemui 里面的 icon 跨用户泄露,当时在 Android 15 上忘了启用相关 flag 导致补丁没生效,所以重新放一遍
CVE-2025-0083 ID High telecom 内使用 getAuthority() 然后手动解析 @ 字符前面的内容去获取 uri 内的 user id 而非使用 getEncodedUserInfo。从 commit message 内大概可以猜测是把 @ 字符使用 uri encode 的格式来绕过原来的补丁。
CVE-2025-0086 ID High AccountManagerService getAuthToken 校验 Authenticator 返回的 account type,避免读到其他 account 数据。
CVE-2024-49740 DoS High telephony 限制 VisualVoicemailSmsFilterSettings 必须由默认拨号 app 设置,同时限制其内容的大小
CVE-2025-26417 ID High 将已经下载好的文件插入进 DownloadProvider (DownloadManager.addCompletedDownload)时,确保路径对应文件的所有者与调用者匹配,否则恶意应用可以通过插入操作产生的 DownloadProvider 的 id 通过 DownloadProvider 的权限访问该文件
In multiple locations, there is a possible way to obtain any system permission due to a logic error in the code. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is needed for exploitation.
1 2 3 4 5
Fix Dynamic Permission group auto grant behaivor Fix the Dynamic Permission group auto grant behaivor so that a permission group is only considered granted when (1) all permissions were auto-granted or (2) a platform permission in the same group is granted.
CVE-2024-43765 EoP High 1 悬浮窗覆盖漏洞再次限时回归。Document UI 这个 app 里隐藏遮盖窗体,防止点击劫持。
Magisk App before Canary version 27007 contains a vulnerability CVE-2024-48336, which allows a local untrusted app with no additional privileges to silently execute arbitrary code in the Magisk app and escalate privileges to root via a crafted package without user interaction. The following is copied from my repo https://github.com/canyie/MagiskEoP for backup purposes. For more info such as PoC code, please check the original repo.
Introduction
This is an exploit for a vulnerability CVE-2024-48336 in Magisk app that allows a local app to silently gain root access without user consent.
Vulnerability was initially reported by @vvb2060 and PoC-ed by @canyie. It has been fixed in Canary 27007.
The install() function of ProviderInstaller.java in Magisk App before canary version 27007 does not verify the GMS app before loading it, which allows a local untrusted app with no additional privileges to silently execute arbitrary code in the Magisk app and escalate privileges to root via a crafted package, aka Bug #8279. User interaction is not needed for exploitation.
Details
Old Android versions do not support some algorithms. To make Magisk work properly on these platforms, it tries to load conscrypt from GMS by calling createCallingContext(). Check this link for more details: https://t.me/vvb2060Channel/692
However, GMS is not always preinstalled on all devices. Magisk assumes that loading code from GMS is always safe, however attackers can create a fake malicious app with the same package name. When Magisk app is launched, malicious code will get executed in Magisk app. Since Magisk app is always granted root access, this allows attackers to silently gain root access and execute arbitrary code with root privileges without user acceptance.
Vulnerable Devices
Devices with no GMS preinstalled
Devices with broken signature verification implementation (e.g. Disabled by CorePatch)
Note: This issue is fixed in Canary 27007 by ensuring GMS is a system app before loading it. However, it’s still possible to exploit this issue on devices with pre-installed GMS but have broken signature verification implementations (e.g. CorePatch).
Note 2: Although a fix for this issue is present in the official Magisk app, there are many other instances of similar code exist in other apps without a proper fix such as this and this. This potentially allows an arbitrary code execution in vulnerable apps and potentially allows attackers to gain root access again if it is granted to victim apps.
privatevoidcheckCallerIsSystemOr(String pkg, int userId)throws RemoteException { if (isCallerSystem()) { return; } checkArgument(getCallingUserId() == userId, "Must be called by either same user or system"); mAppOpsManager.checkPackage(Binder.getCallingUid(), pkg); }
URI 在 Android 中非常常用,如常见的分享文件操作,就是创建一个 ACTION_SEND 的 intent,把要分享的文件的 URI 放到里面,然后 startActvity。常见的有安全影响的 URI 有 content uri 和 file uri(Android 7.0+ 已弃用)等。
作者写的文章也可以看一下。这里提一个点就是,禁止向外传出 file:// URI 这个功能只是 Strict mode 的限制,只需要把 strict mode 关闭就好。
file:// URI 从 Android 7.0 开始被弃用,像这样直接构造指向私有文件的 file URI 的这种利用手法近几年应该是几乎绝迹,但像这样缺失 URI 检查的漏洞其实还有很多,最近比较多的是高权限应用程序接收 URI 时没有检查该 URI 是否指向其他用户,从而允许一个用户越权读取到另一个用户的媒体。content URI 正常格式为 content://host:port/path/id,跨用户的 content URI 会在 host 前面加上用户 ID,形如 content://10@host:port/path/id。
另外一点,系统内部也大量应用了广播机制传递信息,广播这玩意和 Activity Service Provider 都不同,一次广播可能被多个个体接收,如果说系统内部发送广播传递内部信息的时候没有指定接收者,那就有可能被第三方应用拿到敏感信息。但是,我们不可能把所有已知会接收这个广播的可信应用硬编码在代码里,由此就引入了另一个安全机制,不仅广播接收者可以指定要有某某权限才能给我发广播,发送者也可以指定接收者必须有某某权限才能收到广播,利用现有的权限机制解决了问题。这里可能出现的漏洞就是写代码的人粗心大意忘记指定权限,导致信息被其他不相关应用接收。
BG-FGS:Background-Foreground Service start,绕过上面的第二个限制
WIU:while-in-use 权限,简称前台权限,指用户仅允许应用“在使用中才能获得”的权限
恶意应用利用 BAL 漏洞可以在后台随意弹出广告,严重影响用户对手机的正常使用。能够绕过 BAL 限制的漏洞一般至少都会被授予 High 等级,而虽然绕过 BG-FGS 限制并不被直接视为是安全漏洞,但如果能从后台获取 WIU 权限,仍然会被认为是高危。部分此类漏洞具有相似的模式,从近几个月的安全广告来看也确实每隔一两个月都有类似漏洞出现。本节我们主要介绍利用 PendingIntent 实现 BAL。
// Legacy behavior allows to use caller foreground state to bypass BAL restriction. // The options here are the options passed by the sender and not those on the intent. final BackgroundStartPrivileges balAllowedByPiSender = PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller( checkedOptions, realCallingUid, realCallingPackage);
@BalCodeint resultIfPiSenderAllowsBal = BAL_BLOCK; if (realCallingUid != callingUid && considerPiRules) { resultIfPiSenderAllowsBal = checkPiBackgroundActivityStart(callingUid, realCallingUid, backgroundStartPrivileges, intent, checkedOptions, realCallingUidHasAnyVisibleWindow, isRealCallingUidPersistentSystemProcess, verdictLogForPiSender); } if (resultIfPiSenderAllowsBal != BAL_BLOCK && balAllowedByPiSender.allowsBackgroundActivityStarts() && !logVerdictChangeByPiDefaultChange) { // The result is to allow (because the sender allows BAL) and we are not interested in // logging differences, so just return. return resultIfPiSenderAllowsBal; }
private@BalCodeintcheckPiBackgroundActivityStart(int callingUid, int realCallingUid, BackgroundStartPrivileges backgroundStartPrivileges, Intent intent, ActivityOptions checkedOptions, boolean realCallingUidHasAnyVisibleWindow, boolean isRealCallingUidPersistentSystemProcess, String verdictLog){ finalboolean useCallerPermission = PendingIntentRecord.isPendingIntentBalAllowedByPermission(checkedOptions); if (useCallerPermission && ActivityManager.checkComponentPermission( android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, realCallingUid, -1, true) == PackageManager.PERMISSION_GRANTED) { return logStartAllowedAndReturnCode(BAL_ALLOW_PENDING_INTENT, /*background*/false, callingUid, realCallingUid, intent, "realCallingUid has BAL permission. realCallingUid: " + realCallingUid, verdictLog); }
// don't abort if the realCallingUid has a visible window // TODO(b/171459802): We should check appSwitchAllowed also if (realCallingUidHasAnyVisibleWindow) { return logStartAllowedAndReturnCode(BAL_ALLOW_PENDING_INTENT, /*background*/false, callingUid, realCallingUid, intent, "realCallingUid has visible (non-toast) window. realCallingUid: " + realCallingUid, verdictLog); } // if the realCallingUid is a persistent system process, abort if the IntentSender // wasn't allowed to start an activity if (isRealCallingUidPersistentSystemProcess && backgroundStartPrivileges.allowsBackgroundActivityStarts()) { return logStartAllowedAndReturnCode(BAL_ALLOW_PENDING_INTENT, /*background*/false, callingUid, realCallingUid, intent, "realCallingUid is persistent system process AND intent " + "sender allowed (allowBackgroundActivityStart = true). " + "realCallingUid: " + realCallingUid, verdictLog); } // don't abort if the realCallingUid is an associated companion app if (mService.isAssociatedCompanionApp( UserHandle.getUserId(realCallingUid), realCallingUid)) { return logStartAllowedAndReturnCode(BAL_ALLOW_PENDING_INTENT, /*background*/false, callingUid, realCallingUid, intent, "realCallingUid is a companion app. " + "realCallingUid: " + realCallingUid, verdictLog); } return BAL_BLOCK; }
如果 realCallingUid 即 PendingIntent/IntentSender 的发送者拥有可见窗体,或者是需要持续运行的系统重要进程,那么就有机会被允许。常见的是很多 OEM 在 system uid 的进程中实现手势导航等自定义功能,导致系统认为 system uid 具有可见窗口,暴露攻击面。而系统中有很多接收 PendingIntent/IntentSender 的接口,一般用于异步操作完成后向调用者发送返回值,如果忘记在 options 内指定参数禁止 BAL,那就有可能被我们利用。
这里有一点,Android 14 中对 BAL 限制做出了一些增强,其中 “应用会收到来自其他可见应用发送的 PendingIntent” 一项中加了一个对我们很重要的限制:
Note: Starting from Android 14, apps targeting Android 14 or higher must opt in to allow background activity launch when sending a PendingIntent. To opt in, the app should pass an ActivityOptions bundle with setPendingIntentBackgroundActivityStartMode(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
publicstatic BackgroundStartPrivileges getDefaultBackgroundStartPrivileges( int callingUid, @Nullable String callingPackage){ if (UserHandle.getAppId(callingUid) == Process.SYSTEM_UID) { // We temporarily allow BAL for system processes, while we verify that all valid use // cases are opted in explicitly to grant their BAL permission. // Background: In many cases devices are running additional apps that share UID with // the system. If one of these apps targets a lower SDK the change is not active, but // as soon as that app is upgraded (or removed) BAL would be blocked. (b/283138430) return BackgroundStartPrivileges.ALLOW_BAL; } boolean isChangeEnabledForApp = callingPackage != null ? CompatChanges.isChangeEnabled( DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingPackage, UserHandle.getUserHandleForUid(callingUid)) : CompatChanges.isChangeEnabled( DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingUid); if (isChangeEnabledForApp) { return BackgroundStartPrivileges.ALLOW_FGS; } else { return BackgroundStartPrivileges.ALLOW_BAL; } }
Hello, Thanks for reaching out. Rewards are processed at 90 days after submission and once it has been processed, you will receive an email with details on the next steps to collect the reward. Best Regards, Android Security Team
嗯,90 天,Only Google can do。而对于一个被确认的安全漏洞的生命周期是这样的:
Initial severity rating assessment (subject to change after review by component owners) (3)
Development of an update
Assignment of CVE
Shared under NDA, as part of coordinated disclosure, to Android partners for remediation
Release in a public Android security bulletin
Android Security Rewards payment (if applicable)
而 Google 的慢会体现在每一步上,第一步的评级就有可能花几个月。即使内部已经写出来了修复,Google 也要首先向所有合作伙伴共享该漏洞,然后至少等一个月才会在每月安全公告中发布。不过值得高兴的一点是,大部分时候漏洞赏金都会在漏洞被修复之前就给你。很明显,如果你是一个独立的安全研究员,指望靠挖洞吃饭,先不论你能找到多少洞获得多少钱,就这个速度,估计你在钱到手上之前就饿死了。当然,如果你背后有着专业的安全公司或者团队,或者就是对安全感兴趣想来试试,那可以当我没说。我相信人的兴趣来了没有人能挡住,就像以前的我一样 (你先把你脑子治好再说话才能让人信服)。如果你仍然愿意投入时间精力研究,那我相信你不会被亏待。
// Detach but the process should still remain stopped // The hide daemon will resume the process after hiding it LOGI("proc_monitor: [%s] PID=[%d] UID=[%d]\n", cmdline, pid, uid); detach_pid(pid, SIGSTOP); hide_daemon(pid); returntrue;
not_target: PTRACE_LOG("[%s] is not our target\n", cmdline); detach_pid(pid); returntrue; }