普通视图

发现新文章,点击刷新页面。
昨天以前残页的小博客

Android 2026 年每月安全补丁分析索引

作者 残页
2026年3月18日 23:00

2024 年度补丁分析:点我
2025 年度补丁分析:点我

我是 2025 年 Android VRP 冠军!点这里看我的名字: https://bughunters.google.com/blog/google-vrps-in-review-2025#android-devices

最后更新时间:2026/03/18 更新内容:哼哼啊啊啊啊啊啊啊啊

2026-03-01

在野利用漏洞:CVE-2026-21385

Framework

CVE-2026-0047 EoP Critical
只影响 16-qpr2 的漏洞。截至发稿,漏洞补丁未公开。漏洞描述:

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.

根据已经公开的 16 QPR2 源码,可以很明显地看出来提交 https://cs.android.com/android/_/android/platform/frameworks/base/+/0ebd869c069fb58671b947955be1e67241783d73 在 activity manager 引入了 dumpBitmapsProto 这个 aidl 调用,可以获得系统里面正在运行的所有 java 进程里的所有 bitmap 数据而没有任何权限保护,很明显的漏洞。

CVE-2025-32313 EoP High
又放了个无法访问的链接。。。
手动大法,获得这两个:
https://android.googlesource.com/platform/frameworks/base/+/fd4045126ff01cec3d65c053a0c2c01dc231a0f5
https://android.googlesource.com/platform/frameworks/native/+/611b730bade54a0a79dbcc3087d9393086e6dbdf
也就是 Parcel.setDataPosition() 设置的 position 大于 Parcel.dataSize() 时不会正确增长 buffer,造成越界写。通过反序列化 NotificationHistory 对象时使用畸形数据触发。
漏洞描述又是 UsageEvents 里的 OOB write,不知道发什么神经

CVE-2025-48544 EoP High
去年 9 月放过,又放了一遍,懒得重新分析了,看之前的吧

CVE-2025-48567 EoP High
之前 CVE-2024-43093 的后续,MediaProvider 里使用正则过滤敏感路径,需要去除掉路径中的可忽略代码点

CVE-2025-48568 EoP High
https://android.googlesource.com/platform/frameworks/base/+/d8c3d450f77f77232a89ac37c9b9b266e28c0202
切换用户过程中的 race condition,导致锁屏绕过。这个问题我有时候会碰到,不知道修没修好。

CVE-2025-48574 EoP High
WindowManagerService 内对 PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP 的权限检查损坏,在 Binder.clearCallingIdentity() 之后调用了 ActivityTaskManagerService.enforceTaskPermission() 检查权限,里面用的是 checkCallingPermission(),而它依赖 Binder.getCallingUid() 的结果,实际上不会起到任何作用,因此可以拦截用户 drag and drop 传递的数据。
这个漏洞我在 2025 年 7 月 29 日报过,duplicate,看 issue id 可能也就差几天的时间,可惜了

CVE-2025-48578 EoP High
MediaProvider 收到畸形 URI (authority 不正确,或者无法把 id 解析成数字)的时候恢复 binder calling identity 再继续执行。

CVE-2025-48579 EoP High
补丁链接跟 CVE-2025-48578 是一样的。

CVE-2025-48582 EoP High
此漏洞由我发现并报告。
MediaProvider 请求权限的 PermissionActivity 内使用了不安全的 getCallingPackage() 获取调用者身份,可以被伪造导致权限绕过。补丁改成在 createRequest() 的时候记录 calling uid。

CVE-2025-48605 EoP High
SystemUI 显示锁屏的时候移除队列里已有的隐藏锁屏消息,防止残留的请求意外 dismiss 掉锁屏

CVE-2025-48619 EoP High
ContentProvider 被要求打开文件的时候,如果 mode 里没有 w,这个时候 framework 只会检查读权限,过滤掉 truncate bit 和 append bit 避免只有读权限的调用者裁剪文件。

CVE-2025-48634 EoP High
WindowManagerService relayoutWindow 的过程中没有对 private flags 做权限检查,任何 app 都能使用 PRIVATE_FLAG_TRUSTED_OVERLAY PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP 等敏感 flag。

CVE-2025-48635 EoP High
此漏洞由我发现并报告。
补丁:https://android.googlesource.com/platform/frameworks/base/+/36e65fce2d5119ad1d62b4696c003f93df649e52
问题跟 CVE-2025-0098 基本是一样的,当时只修了 android 15,但是有问题的代码其实在 android 14 就存在了,然后我又报了一个。

CVE-2025-48645 EoP High
加载 device admin info 的 description 时 catch OOM,避免超大字符串导致崩溃

CVE-2025-48646 EoP High
https://konata.github.io/posts/identity-squashing/

CVE-2025-48654 EoP High
系统开机的时候移除所有已被 revoke 的 companion device associations

CVE-2026-0007 EoP High
往 parcel 写入 WindowInfo.name 的时候截断超长字符串,这个涉及到我之前提出的一种新攻击手段,后续会发文章介绍

CVE-2026-0010 EoP High
drmserver 中的越界写/栈上缓冲区溢出

CVE-2026-0011 EoP High
修复 shared user id 的系统 app 卸载更新然后重新启用时没有重用现有的状态会被重新分配一个新的 uid 的 bug。

CVE-2026-0013 EoP High
DocumentsUI PickActivity 重用来源 intent 启动 activity 之前先清掉 selector,避免用 DocumentsUI 的权限启动任意 activity

CVE-2026-0020 EoP High
解析权限的时候去除名字里的头尾空格,看描述是能绕过授权弹窗

CVE-2026-0023 EoP High
安装 app 时忽略外部传来的 INSTALL_FROM_MANAGED_USER_OR_PROFILE 标志

CVE-2026-0026 EoP High
补丁:
https://android.googlesource.com/platform/frameworks/base/+/0ead58f69f5de82b00406316b333366d556239f1
https://android.googlesource.com/platform/frameworks/base/+/528a87e90ff9354581d54fd37fbe9f95cccbcdb1
之前的 CVE-2023-20971,不知道为啥重新给了一个 CVE,终于发现之前给的 CVE 不对了吗?

CVE-2026-0034 EoP High
限制最多只能激活 100 个 notification listener service / condition provider service,避免序列化太长的字符串导致异常

CVE-2025-48630 ID High
绘制模糊区域时使用边界图层进行裁剪,防止超出图层边界。看起来是之前 CVE-2025-48561 (Pixnapping) 的后续,https://www.pixnapping.com/

CVE-2026-0012 ID High
官方公告放少了一个补丁所以看起来怪怪的,应该需要以下两个补丁才对:
https://android.googlesource.com/platform/frameworks/base/+/f275f865d49559a6bb3ef9cecf0ab1dd7a7a0bc3
https://android.googlesource.com/platform/frameworks/base/+/e93d4b015c283e55cb08d68a499aab41f03e1272
所以是动画导致的 contact name leak?

CVE-2026-0025 ID High
Notification 从 extras 里还原 EXTRA_MESSAGESEXTRA_HISTORIC_MESSAGES 没有类型限制,可以往里添加一个 ParceledListSlice,因为 ParceledListSlice 反序列化时需要调用回对端提供的 binder,在 system server 去 visit uri 的时候 app 可以返回畸形数据,让 system server 反序列化 messages 时 ParceledListSlice 抛出异常导致整项都反序列化失败,然后 defuse 返回 null,因此被 visit uri 跳过;在 SystemUI 去读的时候就返回可被解析的数据,让 messages 可以被 SystemUI 正常读取和渲染。补丁就是限制了 EXTRA_MESSAGESEXTRA_HISTORIC_MESSAGES 数组的每一项都必须是 bundle。

CVE-2025-48644 DoS High
解析输入法 metadata 的时候限制里面 string 的大小,防止超过 binder 传输大小。

CVE-2026-0014 DoS High
此漏洞由我发现并报告。
AppOpsService 内信任一切安装在 system image 上的 app 提供的 AttributionTag,这个检查可以被 SDK Sandbox 绕过,跟 CVE-2025-48524 CVE-2025-48545 是类似的。

CVE-2026-0015 DoS High
AppOpsService 内忽略不可信 proxy 提供的无效 proxied attribution tag,之前的代码里如果提供的 proxied attribution tag 在被代理的 package 里找不到但是 proxy 里能找到还是会认为它有效,现在即使 proxy 里能找到,如果请求来自不可信 proxy 也会忽略掉它。

System

CVE-2026-0006 RCE Critical
libopenapv 中的越界读写/堆上缓冲区溢出,把它更新到 v0.2.0.0。只影响 16

CVE-2025-48631 DoS Critical
去年 12 月的 CVE-2025-48631 重新放一遍,当时 16 QPR2 没修好

CVE-2025-48577 EoP High
SystemUI 切换用户时如果收到生物认证或者解除锁屏请求,可能会因为 race condition 导致发生在错误的用户上。添加用户 id 验证以避免这些情况。

CVE-2025-48602 EoP High
用户切换的时候取消还在队列里的播放锁屏退出动画的请求。

CVE-2025-48641 EoP High
nfc 中多线程访问造成的 UAF,需要加锁

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 的记录。

CVE-2026-0017 EoP High
高版本(16+)的“使用生物认证解锁”使用的 Settings.Secure 项和低版本的不同,旧版本更新到 16+ 之后系统只会去读新的 Settings.Secure 项的值,只能获得默认的 ON,即使更新系统之前把这项功能关掉了,更新之后还是可以生物认证解锁手机。添加迁移逻辑避免这个问题。

CVE-2026-0021 EoP High
Settings 如果是 2 pane (大屏设备),AppInfoBase 检查 calling package 的跨用户权限而非 calling uid 的权限,因为 multi pane 的实现需要 Settings 用自己的权限去重新启动 activity,这个过程会导致 calling uid 变成 Settings 自己的,具体见 https://cs.android.com/android/platform/superproject/+/android-16.0.0_r1:packages/apps/Settings/src/com/android/settings/homepage/SettingsHomepageActivity.java;l=678
这个问题我之前看出来了,本来打算有空的时候实验然后提交,结果就忘了。。。这里经过最开始我报的 CVE-2024-43088 后面 CVE-2025-22428,兜兜转转最后又回到原点。不知道以后还有没有

CVE-2026-0035 EoP High
MediaProvider 不允许获得不存在的文件的权限,避免悬空攻击

CVE-2024-43766 ID High
蓝牙里的未加密通信?

CVE-2025-48642 ID High
使用 dc cvac 指令代替 dc cvau,保证数据写入到主存。https://zhuanlan.zhihu.com/p/718112749

CVE-2025-64783 ID High
更新 DNG SDK 到 1.7.1 2410

CVE-2025-64784 ID High
和上面的一样

CVE-2025-64893 ID High
和上面的一样

CVE-2026-0005 ID High
SystemUI crash 重启后重新进入 app pinning 模式。类型给错了吧?应该给 EoP

CVE-2026-0024 ID High
使用 file picker 选择媒体文件的时候检查请求 app 有没有权限从里面读取位置,如果没权限应该去除掉位置信息(比如 EXIF 里可能包含位置)

CVE-2025-48585 DoS High
ProfilingService 里保证传入的包名属于 calling uid,只影响 16

CVE-2025-48587 DoS High
ProfilingService 添加 trigger 的时候校验 trigger 的类型确保是合法的,只影响 16

CVE-2025-48609 DoS High
MmsProvider 里的路径穿越,以 phone 的权限删除任意文件造成电话/短信/彩信功能异常

2026-02-01

木大木大,全部木大

2026-01-05

Dolby

CVE-2025-54957 RCE Critical
https://project-zero.issues.chromium.org/issues/428075495
https://projectzero.google/2026/01/pixel-0-click-part-1.html

频道内容备份

作者 残页
2026年3月18日 22:00

由于频道被封,整理一下还算有用的内容发到这里来。偏技术向闲聊,发布一些随谈、碎碎念或短篇/价值密度不高等我觉得不适合专门写一篇博客但仍然有留下来的价值的内容,包括但不限于编程想法、系统内部解构、人生感悟等。
有挺多人好奇我的人生经历(见 #303),比如我在我的领域是怎么起步的,还有我作为一个厌学、走职业教育路的差生的故事(#88 #303),这里一并留下来。中考五五分流让一半的学生去读了职校,但现在主流叙事却只把职校学生当成审丑对象来满足自己的优越感,把上普高上本科考研读博考公成为人生赢家当成唯一的定死的路,职校生的评论区充斥着“保送富士康”“和我的准时宝说去吧”等高高在上的刻板印象凝视,他们失去话语权失去主体性成为别人应对学历焦虑、确认自己的努力是值得并感叹“还好我还没有跟他一样”的工具,我只感觉到溢出的社达气息。这里想提供一个不一样的视角。我承认我也在反思职校和职校生自己的缺点,比如学校里所谓的早上七点必须离开宿舍、早读必须多大声等形式主义,还有一方面后悔没考上高中另一方面又不尝试通过高职高考等方式提升学历的同学,但我的反思是为了修正错误,而不是建构所谓的等级体系。很多年轻人受到的教育都是“好好学习改变命运”,“不学习以后就要干又苦又低薪的活”,这种焦虑逼着所有人往上爬。但是,难道所谓底层工作就真的天生低人一等吗?做到尊重与平等,承认工程师和外卖员流水线普工一样光荣,同时保障所有劳动者的权益,才是学历通胀时代的正确解法。
部分我觉得现在没什么用的内容比如纯情感发泄和线下贴贴就不再留着了。另外考虑到这里偏正式,部分条目有修改。
本频道任何消息均不构成医学建议。

2021

3月6日 #1 一切的开始

频道已创建。

5月15日 #3

意外发现magisk中某个关键组件存在问题,影响magisk hide的隐蔽性,可以导致riru被轻易检测,各类xposed实现的某个特征无法被用户通过命令隐藏
再次总结:magisk能运行到现在是一个奇迹

7月21日 #10 #11 解决 magisk hide 随机失效

给 magisk 提交了一个 pr ,出现 magiskhide 完全或随机失效的可以尝试
https://github.com/topjohnwu/Magisk/pull/4507

7月22日 #12 #13 magiskhide bug

https://github.com/topjohnwu/Magisk/blob/v23.0/native/jni/magiskhide/proc_monitor.cpp#L387-L403
妙呀,之前我遇到的那个 raise(SIGSTOP) 停不下来的问题是另一个bug(应该算bug吧)

9月25日 #19 #20 #21 #22 #23 #24 busybox bug

1
2
3
4
5
6
7
8
9
/* 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)

https://github.com/topjohnwu/Magisk/pull/4731
busybox 内置一个神奇优化,把一些变量声明成 const 然后用魔法去改,但是改变 const 在 c 里是 ub,编译器不知道数据依赖关系,会把代码优化成会崩溃的形式…

11月7日 #25 androidx bug

https://cs.android.com/androidx/platform/frameworks/support/+/c50410874c4f5c64e0089a161a24347e0f39d711
androidx 的兼容方法 ProcessCompat.isApplicationUid(int) 在 API 17-23 上看起来漏了一个 return ,会丢掉已经获取到的结果,固定返回 true;API 16 工作正常。

2022

2月21日 #27 art bug

https://segmentfault.com/a/1190000041412586

4月23日 #35 Android Design

Android 4.x 时代的设计规范
http://adchs.github.io/index.html

附带一篇当时对 Holo 和 Android Design 的评价
http://www.geekpark.net/news/179488

5月6日 #37 visualsource

https://www.visualsource.net/
生成 git 仓库可视化的开发(施法)过程视频

5月23日 #42 #43 质疑

https://juejin.cn/post/7100079518756552734
血压高了,就这也敢叫 《android 进阶宝典》

简单看了一下,一些明显错误:

其实方法的本质就是arm指令,然后JVM的执行引擎会执行arm指令

arm 的 cpu 直接就能执行 arm 指令,jvm 也不是用来执行 arm 指令的,jvm 只接受字节码。android 上的 dalvik/art 稍有不同,接受 dalvik 字节码指令集(通常称为 smali)。至于所谓的 arm 指令,那是 runtime 在用户手机上生成的,即使没有,程序也可以被解释执行。

arm指令是存在于dex文件中的,也就是说,我们可以从dex文件中取出arm指令,查看一个方式是如何被执行的

dex 里没有 arm 指令。如果要查看被编译后的机器指令,请使用 dex2oat 将其编译为 oat 后再 oatdump 。

JVM的执行引擎会将arm指令从方法区中拿出来,放到虚拟机栈中执行(栈帧的概念,每一个方法对应的dx指令集就是一个栈帧,每一次方法调用都有栈帧入栈和出栈)

这里假设你的”arm 指令“是字节码,它在执行的时候也不会被放到虚拟机栈中再执行。栈是程序执行时的临时数据区,程序通过各种指令操作,放/取数据,而非指令。另外,”dx 指令集“又是什么玩意儿?

同是执行 10 + 20 ,JVM是先创建一个10变量,然后再创建20 ,最后将两个相加然后返回;但是dx指令是直接计算好了,然后创建v0 = 30,直接返回,所以:Android编译器在编译的过程中会做优化,提高执行的效率

首先,你给的 java 字节码里面没有加法指令,是个除法指令,也就是说这不是你给的加法函数编译出来的结果。这种优化叫常量折叠,从理论上来说 javac 也会进行这种优化。

之前我们介绍过阿里的AndFix或者Sophix是通过hook native层修改字节码指令完成,之前我们介绍的arm指令集,就是实现热修复的基础。所以AndFix热修复,就是将正确的arm指令替换调异常的arm指令,等到再次加载这个类执行方法时,执行引擎拿到的是正确的arm指令交由虚拟机栈。

函数入口与指令本身有着本质区别。替换函数入口也不需要了解 arm 指令集。

记录当前方法被调用的次数,如果超过某个限制,那么该方法就被标记为是热方法,热方法是被缓存到一块内存,下次执行到这个方法,不需要压栈,直接返回结果

hotness_count_ 与 JIT 编译相关,当方法未被编译而调用次数达到阈值后,会有专门的 JIT 线程把它编译为机器指令加速执行。“热方法是被缓存到一块内存,下次执行到这个方法,不需要压栈,直接返回结果”,首先非热方法也在内存中,其次压栈与方法调用有本质区别。我觉得你应该是想说 inline 优化,但那与 hotness_count_ 无关。

找到ArtMethod,在JNI层是能够实现的,通过JNIEnv的FromReflectedMethod函数
从 android 11 开始,FromReflectedMethod 返回的可能不是 ArtMethod*。

尤其是通过hook native底层修改arm指令集
你是在说 inline hook?

6月28日 #44 Android名字规则

转发自 南宫雪珊 https://t.me/vvb2060Channel/441

以大小写英文或点(.)开头,后续可以用大小写英文、数字、点(.)和下划线(_)

package(包名)和sharedUserId有额外规则:不是是非法文件名。即不能为 . 或 .. ,以utf8方式转成比特数组后,长度不能大于255。(虽然aapt2连250都不允许……)

需要至少一个点:package,sharedUserId,不以:开头的process。
不要求至少一个点:splitName,以:开头的process。

不以:开头的process如果是system,例外允许。
以:开头的process需要至少两个字符,即不能只有一个:。

7月26日 #45 使用 Linux signal handler 劫持内存访问

https://harrychen.xyz/2022/03/23/hijack-memory-access-using-linux-signal-handler/

8月7日 #46 #47 android_dlopen_ext 坑

https://developer.android.com/ndk/reference/group/libdl
这是 android_dlopen_ext 还有那一堆 flags 的文档。里面只说了 android_dlopen_ext is available since API level 21,没说那些 flags 是什么时候添加的。
以 Android 5.1 为例,有效的 flag 只有这么几个:https://cs.android.com/android/platform/superproject/+/android-5.0.0_r1.0.1:bionic/libc/include/android/dlext.h;l=57-62

1
2
3
4
5
6
/* Mask of valid bits */
ANDROID_DLEXT_VALID_FLAG_BITS = ANDROID_DLEXT_RESERVED_ADDRESS |
ANDROID_DLEXT_RESERVED_ADDRESS_HINT |
ANDROID_DLEXT_WRITE_RELRO |
ANDROID_DLEXT_USE_RELRO |
ANDROID_DLEXT_USE_LIBRARY_FD,

使用不支持的其他 flag 如 ANDROID_DLEXT_FORCE_LOAD 会直接被 linker 报错拒绝加载。坑人呢

10月14日 #48 Android NDK 移除 x86 支持

https://github.com/android/ndk/issues/1772

2023

1月2日 #49 #50 android selinux bug

https://stackoverflow.com/questions/28590831/android-permission-denied-when-reading-proc-self-exe-from-non-main-thread

Release 模式的 APP 在非主线程上对 /proc/self/exe 进行 readlink 在旧版本的 Android/(Kernel?)上会触发 Permission denied 错误。把 debuggable 设置为 true 后正常,在主线程进行也正常,在没有 SELinux 的 Android 5.0 AVD 上也正常。
猜测可能是旧版本 Android 的 SELinux 有问题?

这个问题导致了大量旧 Android (5.x/6.x)用户更新 Magisk 25.0 后“卡在开屏页”

应该是 kernel 问题,在非 Android 平台上也有出现
https://github.com/moby/moby/issues/18883

1月13日 #51 #52 Pixel 6 系列 ramdisk 位置移动

Pixel 6 系列(只看了 6a 但是应该是整个系列都有这个问题)的 Android 13 QPR2 B1/2
boot.img 里的 ramdisk 用的 gzip 压缩,vendor_boot.img 里的 ramdisk 用的 lz4_legacy。
bootloader 解包的时候是把两个 ramdisk 拼起来扔给 kernel 然后 kernel 直接整包解压。这需要两个 ramdisk 起码使用相同的压缩格式。所以 boot.img 里的 ramdisk 从来没有被成功解压过,自然 Magisk 怎么修补都无效。这就是 修补原厂 boot.img 没有反应但是修补 Pixel 7 的 boot 刷入就好了的原因 (https://github.com/topjohnwu/Magisk/issues/6441)

Pixel 6 系列机型 QPR2 用户临时的解决方案:

  1. 下载 Pixel 7 系列机型的 boot.img,修补,给自己的手机刷入。
  2. 将 boot.img 里的 ramdisk 手动压缩成 lz4_legacy 格式再扔给 Magisk 修补。
  3. 手动修补 vendor_boot.img (无法使用 Magisk app 进行)。

更新:QPR2 Beta 3 已修复。Pixel 6 系列手机的用户可以正常修补 boot.img 获取 root。

Google:自己写的 文档 ,当然要由自己亲手破坏

1月18日 #55 #56 #57 #58 #59 zygote fd 检查

Android 7.0.0_r1 SDK24 没有 fd 检查,没有 frameworks/base/core/jni/fd_utils-inl.h
Android 7.1.0_r1 SDK25 没有 fd 检查,没有 frameworks/base/core/jni/fd_utils-inl.h
Android 7.0.0_r29 SDK24 有 fd 检查:https://cs.android.com/android/platform/superproject/+/android-7.0.0_r29:frameworks/base/core/jni/fd_utils-inl.h
Android 7.1.0_r2 SDK25 有 fd 检查:https://cs.android.com/android/platform/superproject/+/android-7.1.0_r2:frameworks/base/core/jni/fd_utils-inl.h

如果你的应用(无论 root 还是 非 root)需要假定 Android 的内部行为,最好不要直接检查 SDK_INT 不然莫名其妙的崩溃就是下场。

Magisk 假定 fds_to_ignore 这个参数不存在就没有 fd 检查,然后在 华为/魅族的 7.0 上崩了。
Pine 根据系统版本假定 kAccCompilerDontBother 的值,然后在官方的 Pixel 2 8.0 ROM 上崩了。

2月3日 #60 乌云

今天意外发现乌云网 www.wooyun.org 已经没有 DNS 解析记录了(以前是访问显示正在升级),分享一个备份 https://wooyun.js.org/
致敬

2月10日 #61 ndk 移除 4.4 支持

https://github.com/android/ndk/issues/1751

2月12日 #62 #63 #64 #65 AutoCloseable

Android 文档:TypedArray implements AutoCloseable added in API level 1
实际上:直到 Android 11.0.0_r1 这个类都没有实现 AutoCloseable 这个 interface。
所以,如果你很自然的写出了

1
try (var typedArray = obtainAttributes()) {}

或者

1
obtainAttributes().use {}

之类的代码,在旧版本上会崩溃。没有警告。

同样情况的还有 LocalServerSocket 这个类。直到 8.0 都没有实现 Closeable。
这个类 比 TypedArray 更离谱,TypedArray 大家都知道是用 recycle,而 LocalServerSocket 一直都有 close,但是偏偏一直没去 implements Closeable。

3月12日 #66 讨论

https://juejin.cn/post/7208345469658415159

时隔多年又看见了这种『一个 app 有多少个 XXX』这种问题。备份一下我的评论,以免又被掘金删掉。

首先,hashCode 跟对象地址没有半毛钱关系。确实有部分虚拟机选择将 hashCode 直接实现为内存地址,但是 ART 不是。
ART 的实现在这里:http://aospxref.com/android-13.0.0_r3/xref/art/runtime/mirror/object.cc?fi=GenerateIdentityHashCode#170
很明显可以看出 1103515245 是一个线性同余法生成伪随机数选择的常用魔数(不过整个算法看起来不是典型线性同余)。
至于为什么生成的 Application 对象 hashCode 值相同,可能是因为这些进程都从 Zygote 继承了相同的随机数种子,而 framework 内代码对象创建次数往往相同,经过相同的随机次数后产生一致结果就很正常了。
你可以在 Application 的静态代码块内创建随机数量的对象扰乱结果,应该就能得到不一致的 hashCode。
然后是『获取物理内存地址』,Unsafe 这种方法获取到的依然是虚拟的逻辑地址,且用户空间没有办法获取真实的物理地址(除非利用内核漏洞)。
全部给逻辑地址的好处很多:
1.可以在内存紧张时将未使用的内存空间临时换到硬盘上存储,应用实际访问时再触发缺页中断从硬盘读数据,整个过程对处在用户空间的应用完全没有影响
2.逻辑地址无法直接跨进程使用,增强安全性
3.完全阻止恶意应用读写其他进程的私有内存空间
等等等等。

然后我觉得『一个app到底有多少个 Application』『一个 app 有多少个 Context』这种问题一点意义都没有。如果是想知道『一个 app 的进程有多少个 Context』,可以调用 VMDebug.getInstancesOfClasses 这个方法。
文中忽略了一种情况,就是多个应用跑在同一个进程。当多个应用具有相同的签名,配置的 sharedUserId 相同且 android:process 为相同的共享进程时,会发生这种情况。此时一个进程会创建多个 Application 对象(每个应用一次)。

3月23日 #67 #68 bionic bug

之前发现在自己的 Android 5.1 kernel 3.10 arm32 设备上调用 mremap 移动内存空间时会奇怪地报出 EINVAL (invalid argument) 错误,怀疑是低版本 kernel 的问题。后面西大师发现 6.0 x86 的 AVD 上也可以复现出这个错误,x86_64 却不会,而且主动使用 linux-syscall-support 这个库调用 mremap 是没问题的,所以猜测是旧版本 bionic 的实现问题。
果然,旧版本 mremap 参数列表里第四个 int 写成了 unsigned long,缺少变长参数 ( http://aospxref.com/android-6.0.1_r9/xref/bionic/libc/include/sys/mman.h#62 ),同时 x86 发起 syscall 的时候也只处理了四个参数( http://aospxref.com/android-6.0.1_r9/xref/bionic/libc/arch-x86/syscalls/mremap.S#18
然后找到了这个修复提交 https://android-review.googlesource.com/c/platform/bionic/+/180130
所以如果你的程序用了 mremap 又想兼容 7.0 以前的设备,请不要使用系统自带的 libc 里面的 mremap,自己 syscall 或者使用 linux-syscall-support 代替。

(离谱,好特么坑啊

提醒:想要兼容旧设备,还有其他坑……
android 6.0 以前,系统 libc 的 getmntent 和 getmntent_r 是空实现。
https://cs.android.com/android/_/android/platform/bionic/+/e3c4acf1e3ef36c2ab1f48b1261dec9a1d8330a4

还有,init.rc 的 exec 也是空实现……
http://aospxref.com/android-5.0.2_r3/xref/system/core/init/builtins.c#257

clock_nanosleep 这个函数的行为也不对,在 https://android-review.googlesource.com/c/platform/bionic/+/110652 这个提交之前它在出错时会返回 -1 然后把错误码设置到 errno 里,正确行为应该是直接返回正数的 errno

4月2日 #69 魅族系统 bug

魅蓝 M3 Note,系统版本 Android 5.1,内核版本 Linux localhost 3.10.72+ #1 SMP PREEMPT Tue Sep 22 18:07:30 CST 2020 aarch64 Android
在这台设备上,syscall getrandom 会返回 0 同时 errno 变为 2 (No such file or directory)
似乎只在 64 位运行时出现,32 位正常。
而 getrandom 这个 syscall 从 Linux 3.17 才开始有,正常情况下它应该失败且返回 -1,errno 设置为 ENOSYS。
手上的其他 3.10 设备都没有这种怪行为。
而很不幸,较新一点的系统 libc 里面有个函数,叫 getentropy,它会尝试循环 syscall getrandom,只有当它返回 -1 的时候才会认为内核不支持这个 syscall 而进行回退。这个函数会在 libc 初始化时被调用。
所以如果你的程序静态链接了新的 libc,在这台设备上运行的时候会在 libc 初始化时调用 getentropy 而陷入死循环,连 main 都没有机会运行。
修复手段也不是没有,在编译时添加选项使用 wrap functions 功能覆盖掉这几个函数,然后处理一下 getrandom 返回 0 的情况。不过我觉得把当时弄出这种东西的魅族工程师拖出来打一顿比较好。

顺带一提,在这台设备上你想运行 lldb-server 也会因为同样原因卡死,strace 也会不明原因卡死,我是去找了一份 gdb 发现能用才发现这个问题的,不然连 backtrace 都拿不到

4月20日 #72 #73 #74 #75 #76 好心人变冤种

摘要:在长沙街头碰上人借车费,说晚上就还,然后就被删好友了
图片懒得补了,碎碎念可以补一下:

啊睡不着,瞎扯几句
我 2020 年刚进中职的时候,有个同班同学总是找我借饭卡刷,还有微信借钱,后面还带了另一个人找我借钱。我当时想都没想就借给他们了,到后面滚雪球越滚越多,他们借了我 642 块。后面我找他们要钱的时候他们老是说没钱,再后面这两个人, 一个退了学,一个街头持刀抢劫别人被抓了。就只还了150,他们到现在还欠我差不多500块,而我甚至连他们其中一个人的名字都不知道。
性格缺陷是这样的,害怕又渴望社交,害怕好不容易建立起来的社交关系维持不下去。

这回也是,我不认识这个人,这个人路上见到我说微信限额了帮忙付个 79 块的车费。

我这是善良还是纯圣母呢,我觉得是圣母。
我甚至觉得哪天我挂了去见上帝的时候上帝会说哟豁,好久没见过这么纯净的灵魂了。

大家别学我当这种冤种哦

睡了,明天还有 第 81 届中国教育装备展示会

4月22日 #78 权限文档

https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/permission/Permissions.md
给系统开发者的权限文档,竟然塞在这种地方,想不到
不过我这里用 Chrome 打开这个页面,Using role for permission protection 这个链接是 blocked,不懂 Google 在搞什么。应该是指向这个链接 https://cs.android.com/android/platform/superproject/+/master:packages/modules/Permission/PermissionController/src/com/android/permissioncontroller/role/RolePermissionProtection.md

App Ops 文档 https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/app/AppOps.md

顺便带上一份 Android 13 的权限列表,http://aospxref.com/android-13.0.0_r3/xref/frameworks/base/core/res/AndroidManifest.xml
需要其他版本的,选好版本直接搜索 /core/res/AndroidManifest.xml 就好了

4月24日 #79 畸形二维码导致扫码模块崩溃

转发自 不存在的世界 https://t.me/illusory_world/4359
https://nano.ac/posts/86257129/

4月30日 #82 华为旧机型全盘加密

拿到了一台华为 Nova 1 (Android 7.0,EMUI 5.0.4),getprop 显示是全盘加密,然而输密码解锁时这台设备的表现完全不像全盘加密。
在没有输密码解锁时连上 adb,mounts 显示 /data 已经挂载上 f2fs,vold.decrypt = trigger_restart_framework,vold.post_fs_data_done = 1,表明它已经解密完成。

而正常的已经设置密码的 Android 全盘加密设备的解密流程应该是:设备开机 -> 给 /data 挂载上 tmpfs -> 启动 framework -> framework 显示输锁屏密码页面 -> 用户输密码后,解密密钥 -> 关闭 framework,解密 data,挂载真正的东西到 /data -> 再次触发 post-fs-data 正常开机。

查看 dmesg 后发现这台设备在解锁之前用默认密码解密就成功了,锁屏密码似乎就只是锁屏密码。华为对 FDE 设备开机时间过长所做的“优化”?

编写了一个 PoC https://github.com/canyie/BypassKeyguard ,经测试能在设备开机后第一次输入密码前绕过锁屏进入桌面并访问用户数据,侧面印证了上面的猜想。

5月18日 #88 中职/职高/技校就读指南

(为自己读了三年的感想总结,仅作备份)

0.快跑!如果你是 2023 届中考生,不是特殊情况,快去复习!想啥中专!
1.就算你真打算进中专,也一定要去中考考一下,别签那啥自愿放弃中考同意书!有很多好的中专也是要看中考成绩的
2.好好选学校,好的中专跟坏的中专差别很大。中考成绩出来发现没希望上普高的话,没打算复读的话,建议早点选好学校。有很多好的中专是有高考班的,但是学位不多,想报趁早。
亲身经历,咱中考没考,报学校又是等快开学了才去问,结果都没啥学校还有学位,最后进了间民办中职。
3.如果你成功进入中专,在课室里上着课,那么无论如何,恭喜你进入人生新阶段!不过不要觉得进了中专就舒服不用学习了,越垃圾的学校规矩越多还特喜欢整活。以笔者亲身经历来说,这学校能停任何课让你去搞卫生,甚至在离考试还有两三天的时候对我们上一届高考班发出“你们还是高考班,你们连卫生都搞不好你们高个屁的考”这种离谱言论并让他们停课搞卫生。
4.在学校的时候,尽量完善自己的课后生活。有升学的打算的话可以复习文化课和专业课,特别是英语单词,如果你们地区要考英语而你比较差的话建议一开始就狠抓英语。
5.强烈建议报技能竞赛,无论是专业竞赛还是计算机竞赛英语竞赛或者什么禁毒知识竞赛法治知识竞赛,学校没有的话就和学校申请自己代表学校去,无论如何让校领导对你形成两个印象,一是“这是个优秀的好学生”,二是“这个学生能给我学校争到荣誉”
6.强烈建议高一就去考技能证书,尤其是广东学生。个人建议计算机证书和英语证书最好拿到一张。
7.别觉得你的同学有多么善良,尤其是在中职学校。你的很多同学会在一年内选择退学或者被开除。高一的时候,我有个同学借了我六百多到现在都没还,后面这个学生大街上持刀抢劫路人被抓了。我们班还发生过晚自习一群人起哄上台用课室多媒体放黄色视频的事情,这种情况保证自己别被影响到就好了。在一间不怎么样的学校,很难看见希望,但是无论如何相信自己。
8.无论第一年多么苦多么不适应,都请珍惜,第二年随着你们班上大部分学生满了16岁,有些学校会开始以“实习”的名义送学生进厂打螺丝。这个时候竞赛的好处就体现出来了,大部分学校都不会送竞赛生进厂。笔者曾经有幸体验过三个月,那三个月里每天基本上都是因为手指套破损来不及换搞到一手的血,三个月下来拿笔都拿不稳,还高个毛线考。
9.如果你不太好运,已经进厂了,牢记一件事:实习进厂再苦至少拿到毕业证就能离开,没专业技能到社会上不走运的话这就是你以后一辈子的生活。
10.如果打算升学,建议最晚考试前一年开始准备文化课,除非您是技能竞赛保送生。当然任何时候开始学都不晚,各省考纲不同,以笔者广东考生的经验来说学一个学期都能考得很不错了(当然那样绝对累死)。
11.语文的话我其实觉得背那些古诗文没什么必要了,有这时间不如多记几个数学公式。时间充裕的话可以专门去记,不充裕的话那早读的时候读一下就好了,时间紧迫的话直接忽略都没问题。数学最后一道题做不出来就别纠结了,还不如去检查前面的小题,本人就是这样在考场上丢了十分😭

祝所有 2023 届中考生旗开得胜

5月23日 #91 盗号防范提醒

提醒:近日盗号骗子盛行,请大家注意账号安全,注意以下行为:

  1. 觉得很正常的账号突然发一条“在吗”然后以“防止双向发消息限制”等理由让你加好友,大概率就是被盗号了。
  2. 以各种名义让你搜索某些文字然后截图发给对方。原理:telegram 给你发的验证码里含有这些文字,搜索的时候被列了出来,然后截图截到了。
  3. 莫名其妙的人给你发送文件,不要顺手点击运行了

强烈建议添加任何人为联系人时取消勾选 “Share my phone number” 选项,同时不要随意截图,需要发送截图时裁剪掉敏感信息。

5月24日 #92 拼多多 target sdk 观察

https://twitter.com/oasisfeng/status/1661046351151665153?s=19

网上随便找了几个包看了下,play 版 6.48.0 target 31 即 Android 12,国内版 6.49.0 target 27 即 Android 8.1,国内版 6.60.0 target 29 即 Android 10。

5月25日 #96 器官捐献


拿到了实体卡 开心

5月30日 #98 转发新闻:刷机黑客团伙落网

发个新闻
https://j.eastday.com/m/1685165054031431

存档链接
https://web.archive.org/web/20230530003013/https://j.eastday.com/p/1685165054031431

疑似相关的央视网视频报道
http://m.app.cctv.com/video/detail/5a400b4969334dea87d7b05b0d1fd6a8/index.shtml#0

央视网报道被转发至哔哩哔哩
https://b23.tv/YNmoZR7

6月21日 #106 #107 vivo 手机需要加 qq 才能看 logcat

https://t.me/TooruchanNews/26896
https://t.me/LetITFlyW/9983

留一个文章链接避免上面频道也被封:https://juejin.cn/post/7338239261194010674

6月22日 #108 Android 静默升级

冷知识:从 Android 12 开始,应用可以声明 UPDATE_PACKAGES_WITHOUT_USER_ACTION 权限(是普通权限,用户无法禁止),然后调用新的 PackageInstaller.SessionParams#setRequireUserAction(int) API,来静默更新自己。无需用户确认。
看起来这么流氓的功能为什么没被国内一众流氓软件跟进呢🤔
猜测是要么是不知道,要么是因为 target sdk 限制:此 API 要求被安装的应用至少 target 30 (在 Android 13 及之前)或 31 (Android 14 及之后),并且未来的 Android 版本里还会继续提升这个限制。

文档:https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int)

PS: MIUI 用户应该是不受影响,在开启 MIUI 优化的情况下,整个 PackageInstaller API 都被完全破坏,无法使用。应该已经修好了

6月25日 #112 Signed Condig

https://source.android.com/docs/core/runtime/signed-config
竟然还有这种东西,我可以理解为官方后门吗?

6月30日 #114 分享几个 Android 的安全漏洞

(注:RCE=远程代码执行 EoP=权限提升 ID=信息泄露 DoS=拒绝服务)

CVE-2021-0693 ID 高危
漏洞成因:Shell 应用错误把一个 provider 设置成其他 app 能访问,导致应用可以访问不应该能访问的数据。

CVE-2021-0314 EoP 高危
漏洞成因:卸载应用的确认弹窗没有设置不能被覆盖,导致恶意应用可以自行弹出悬浮窗遮挡住关键的确认信息,让用户在不知情的情况下卸载应用。

CVE-2017-13242 ID 中危
漏洞成因:蓝牙配对的时候,会弹出一个对话框,里面有一个选项是要不要允许配对设备访问通讯录。这个选项框包含了对端设备的名字,而这是攻击者可控的,然后攻击者可以通过在设备名字里插入换行符把关键的提示信息顶出屏幕。

CVE-2018-9432 EoP 高危
漏洞成因:基本和上面那个一样,攻击者改设备名把关键的确认信息顶出屏幕。

CVE-2021-0691 EoP 中危
漏洞成因:sepolicy 里多加了几行,授予了 system app 写入已安装 apk 的权限。尽管只是个中危漏洞,但它是『魔形女』漏洞链的重要一环,攻击者利用此漏洞可成功将原本数个越权任意写入文件的漏洞转化为任意代码执行。

CVE-2021-39631 ID 高危
漏洞成因:这个漏洞就更离谱了,一个提示消息写得不够清晰,被认定为高危漏洞…… 对应补丁在这里 https://android.googlesource.com/platform/packages/apps/Settings/+/a36d55e8f83e8bf6e50254cda04632e233598f42

CVE-2021-0434 EoP 高危
漏洞成因:警告信息写的不够详细。

CVE-2023-21090 DoS 高危
漏洞成因:没有限制 app 的 uses-permission 里值的最大长度,导致可以写得非常非常长把系统给搞崩溃。

千里之堤,溃于蚁穴。

7月3日 #115 好孩子规训

“我想躺在一张很大很软的床上 想做个好孩子”

好孩子是什么呢

我小学时候奖状能挂满一面墙 我是好孩子吗
我五年级开始发病逃学 我是好孩子吗
我大概五六年级开始自学编程 每天抱着手机就为了学编程 我是好孩子吗
我小学拿缝衣针捅进插座里电自己 拿刀划自己手腕 我是好孩子吗
我小学有两年+初中四年(休过一年学)都几乎没去上学 连我们班任课老师都不认识我 我是好孩子吗
我的同学在发奋图强学习文化课的时候我在惠爱看病 浪费了家里不知道多少钱 我是好孩子吗
我连中考都不敢去参加 最后随便上了所中职 我是好孩子吗
我害怕社交害怕到想吃烤肠拿着钱在店门口徘徊不敢进去 我是好孩子吗
我头发前过眉侧过耳后过颈 我是好孩子吗

好孩子是什么呢
“乖”的、“听话”的,就是好孩子吗

7月5日 #117 AssetManager.addAssetPath

AssetManager.addAssetPath 这个 hidden api 应该很多人都在用,提醒一下这玩意的两个坑:

  1. android 6 及之前,加载的文件夹/zip 里必须包含 AndroidManifest.xml (这个可能很多人都知道)
  2. Android 9-10,这个 api 只能加载 zip,加载文件夹的功能在一次重构中被破坏掉了。

这个故事告诉我们没事最好别用 hidden api,用这种玩意就是给自己找麻烦。

7月11日 #121 我的一天

(以下均为大概时间):

04:30 突然醒了 不知道过了多久才睡着
06:40 醒了 在床上发呆 玩手机
08:30 到了公司开始写代码
09:30 开始发躯体化症状
10:30 持续不能缓解 请假回来休息 然后躺在床上不知道什么时候睡着了
13:40 醒了 然后在床上躺着玩手机
14:30 努力从床上下来去吃饭
15:00 吃完饭回来在床上躺着发呆和玩手机 知道自己要干正经事就是没力气去干
18:00 傍晚了 感觉心理状态明显好转 甚至有点想涩涩 努力逼自己干了急需的正经事情(其实也就只需要十分多钟就能干完 orz)
19:00 从床上下来吃饭 玩手机 洗澡 玩手机
22:30 想着应该尽量拉住自己 然后给自己下单了个智能手环 orz
23:10 睡觉
(然后三点半又醒了在这里打字 emmm)

7月11日 #122 Math.abs()

冷知识测验:在 Java 中,调用 java.lang.Math 类的 abs(int) 方法一定会返回非负数吗?
答:Math.abs(Integer.MIN_VALUE) 会因为 int 不够放它的绝对值而溢出,返回 Integer.MIN_VALUE,负数。

7月16日 #125 补充 #88

有人问“你会后悔初中没去读吗?你会后悔上一间中职学校吗?”
其实实际上,我妈到现在都也还在不断重复“要是你初中的时候忍忍就好了”之类的观点。

我其实不后悔。我甚至觉得当时做的决定非常正确。可能说唯一有点后悔的是没选到一间比较好的中职学校,进厂打了几个月螺丝。

一方面,我厌学,另一方面,初中阶段很多孩子开始进入青春期,一堆小孩完全不知道自己在干什么,把霸凌当玩笑。再后面大家都为了升学努力,待在这种环境里只会让我心理状况更加恶化。
我自知我的人格是缺损的。所以我甚至很感激 gap 的那几年,我能尝试去把残破的人格补全。那几年里我脱离让我恐惧的学校环境,不用再为“又在学校发病又要麻烦老师把我送回家”这种事情内疚,我可以一点一点学习如何正确和别人相处,还能静心研究自己喜欢的事情。后面到了中职,虽然还是非常非常害怕,但是它至少是个比较轻松的低压环境,我可以尝试去融入它,我可以犯错。
到现在三年中职读完,从第一次住宿舍到一个人跑几百公里去陌生城市实习,感觉真的是个奇迹。虽然现在情况还是不太好,但至少有独立活着的机会了。
所以,如果你刚刚参加完中考,打算读中职的话,不用失望,好好读下去就好了。至于学历,中职学校也是可以升学的,况且如果活不下去,在意学历有啥用?

顺便放首歌:https://www.bilibili.com/video/BV1N64y1B7NZ

7月22日 #130 记录第一次向 Google 报告安全漏洞的过程

  1. 打开 https://bughunters.google.com 注册账号,可以阅读里面的 Rules 了解哪些问题会被认定为安全漏洞。还有就是,提交安全问题的报告或者补丁,满足漏洞奖励计划(VRP)的话,是有可能会奖钱的!!一般来说,漏洞本身的影响越大、提交的报告质量越高,获得的赏金也就会越多。所以初次提交建议仔细阅读 Rules 里的内容。

  2. 确定发现的问题是安全漏洞之后,就可以进入 Report 页面选择要报告漏洞的影响范围(原文是 “report types”,翻译为 “报告类型” 感觉并不对,不知道怎么翻译才好)。我发现的漏洞位于 AOSP 内,影响 Android 操作系统,所以我选择了 Report a security vulnerability in a Google-owned product 然后点击 Report。

  3. 然后它会让你填写你的名字和联系方式,然后会让你写要报告的问题的简要描述(会被当作 Issue 的标题),该漏洞位于哪个地方(我这里是 Android 直接在下拉框里选择 Android 就好了),然后是详细描述你的问题,越详细越好,最好把详细漏洞点和成因都写出来,可以上传一个不超过 50MB 的文件(大家一般都是上传 PoC 之类的吧,我是传了个演示视频)来辅助说明。然后点击确认,它就会在 Google 的内部 Issue Tracker 里为你创建一个对应的 Issue。这个 issue 默认是被保护的,只有你和被指定的 Google 员工可以查看。对了,一个很好的点是,你不一定要用英文!Google 有来自全球不同地区的员工,用你的母语沟通基本上是没问题的!

  4. 我是 6.28 提交的漏洞报告,6.29 这个报告就被 triaged 了,给我定的 Priority P2 + Severity S2,然后会有人让你确认有没有签 Google 的 CLA ,还会问你你打算怎么被感谢(我的理解就是怎么在 Android Security Acknowledgements 那个页面的 Researchers 一栏上写你的信息吧,至少我是这样理解然后按这样回复的),然后也会提醒你上传 PoC 之类的。

  5. 6.30 我回复了之后,7.13 获得了这样的回复:

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.

得,连 CVE ID 都没了,再也不相信信息安全了

8月30日 #143 Magisk v26.2 regression

不建议 更新 Magisk v26.2 (准确来说是 26106 及之后所有的版本一直到最新的 26202),建议等待已知问题都被修复后再尝鲜
花式爆炸

Edit: v26.3 应该把所有 bug 修好了,吧?

想知道详情的可以点击下面几个链接
https://github.com/topjohnwu/Magisk/commit/de00f1d5a94b83ad7bf49b0bdffab310e58db471
https://github.com/topjohnwu/Magisk/issues/7214
https://github.com/topjohnwu/Magisk/issues/7264
https://github.com/topjohnwu/Magisk/issues/7274

所以为什么这种问题只到发版的时候才能发现啊,根本没人用 canary 是吧

9月12日 #145 #146 中职发的书



翻到了这本神书 是我读中职的时候学校发的
中职教育到底搞的咋样 看看应该就懂了(

10月21日 #171 期望

我妹是 2017 年出生的,我比她大 13 岁,那个时候已经是初中生,我们这边的儿童游乐园大人陪小孩进去大人是不收钱的,然后你就能看见我打着照顾我妹的旗号在海洋球池里不停扔球而我妹安安静静坐在旁边的景象。
很多人看见大孩子玩这种东西应该都会奇怪,因为他们觉得大孩子应该坐在学校里上课或者就算玩游戏也不该玩这种婴幼儿玩的东西。这其实是人对他人的一种“期望”。
人从小到大都活在“期望”里。我妹出生之后我们期望着她三个月会翻身六个月会坐八个月会爬一岁会站,我们每天和她说话希望她早点学会说话,她不会做幼儿园的算术题的时候我家里人会叹气说这么简单的题都不会以后怎么办。就算到爷爷奶奶这个退休的年纪我们一家也期望着他们身体健康,晚上烧一桌合胃口的饭菜。
但是我觉得人不该只活在期望里。人生的意义不该是为了满足别人对自己的期望。
人生的意义是啥?我觉得该是享受。
享受啥?享受快乐。享受炎炎夏日电视机播着高糖饮料伤身而你拧开一瓶冰镇快乐水灌进嘴里,享受去吃了心心念念好久的龙虾自助发现也不是那么贵,享受你心爱的人说宝宝我爱你然后亲你一口,享受蒸鸡蛋你非要往里试试加可乐会是啥味道的瞬间,享受看见一个笑话你朋友说有他妈这么好笑吗而你笑到肚子疼,享受自己花时间写的东西有人读有人点赞,享受在网上看见有精神病在试图教你人生的意义是什么你觉得很奇特顺手转发给你的朋友。
我们现在的教育搞的快乐像原罪一样,但是我觉得享受快乐是人与生俱来的权力。

10月19日 #173 碎碎念

碎碎念:突然发现一直都在提出奇奇怪怪的想法 但是大部分最后都是被别人做出来
例子:
native bridge 注入:最后被 Riru ZygiskOnKernelSU 采用,Magisk Zygisk 也计划改成这样实现
MomoHider :从 Shamiko 开始我就没怎么动过了
resetprop 检测:一直知道有这个方法 懒得写 最后被 nullptr 先写出来加进牛头检测器了
重写 zygisk 注入,改成监控 preload-class 并 ptrace zygote:写到一半突然就不想写然后 git reset 了 然后被 ZygiskNext 写出来用上了

该说不愧是 ISFP 吗,绝对不做没有 deadline 的事情(

11月12日 #174 magiskinit

写了一下 Android 启动过程中 init 这个进程的一些细节,以及 magiskinit 是怎么处理这些不同设备的
可能是你在网上能找到的关于 2SI、 magiskinit 等鬼东西的最详细的文章

https://blog.canyie.top/2023/11/12/android-booting-shenanigans-and-magiskinit-analysing/

11月20日 #177 #178 奇怪的补丁

Android 14 有一项行为变更,ActivityManager.killBackgroudProcesses() 这个 API 不能结束其他应用的进程了,只能终止自己的后台进程
但是查看 2023-10 安全公告会发现这个补丁实际上被下发到了 Android 11-13,还被授予了 CVE-2023-21266 这个编号,类型是 EoP 权限绕过,定级为高危
之前一直想不明白为什么 Google 会把行为变更当成安全漏洞处理,直到今天突然想到,可以结束其他应用后台进程,会对 RAM 使用量造成影响,那可以判断 kill 前后的 RAM 使用量,如果出现大幅度下降说明 kill 成功,说明 kill 之前这个应用有进程在运行,相当于绕过了权限检查指定软件包是否在运行,是侧信道

信息安全,真他娘的奇妙

其实这样解释也和 Google 其他行为自相矛盾
之前有个漏洞(具体编号忘了 CVE-2023-21377),是任意第三方 app 可以 inotify 其他应用的 apk 路径,对应应用启动的时候一定会 open apk 然后就会收到通知,相当于绕过了“使用记录访问权限”监听对应应用启动
Google 给出的补丁是用 sepolicy 封堵对应权限,然而“为了破坏现有 app,只对 target >= 34 的应用启用此限制”

只能理解为 Google 内部管理混乱

更新:查看了 CVE 描述,发现是能用这个玩意“绕过 play 保护”,也就是能一直杀掉 play protect 的进程。嗯,也挺离谱的……

12月8日 #181 小米答题


试了一下小米社区的解锁答题(2023.12.07 更新的)
嗯,非常好,题目非常有质量
成功把我给挡在外面了

2024

1月6日 #188 resetprop 检测

公开两个检测 resetprop 的方法:

通过 setprop/getprop 等手段操作property 实际上是操作了位于 /dev/__property__ 下的文件。按 property 各自的 SELinux Context 存储,相同的 context 分为一组,存储在一个文件内。每个文件大小为固定的 128KB。需要使用的 prop_bt prop_info 等对象由一个线性内存分配器实现,每次分配内存时只是简单地 bump 地址,不支持释放内存。
更多细节可以查看 https://blog.canyie.top/2022/04/09/property-implementation-and-isolation/

Magisk 的 resetprop 在删除 property 时,会将 prop_bt 中指向 prop_info 的引用置空,然后清空整个 prop_info 的内存。问题就发生在这里,prop_info 的内存被清空但并没有被释放,这就导致对应内存位置出现一片很大的空区域。通过检测这里,应用可以检测到对应属性区域有属性被删除了,进而推断出设备已经 root(因为正常情况 property 是不支持删除的)。

什么时候会发生删除操作?通过 resetprop 重置 ro 也就是只读属性时。而保存着 bootloader 锁定状态的多个 property 和保存着 native bridge 的 ro.dalvik.vm.native.bridge 还有经常被修改的设备指纹都是只读属性。也就是说,除非避免修改这些属性,否则就会被检测到,而不修改 bootloader 状态的属性又会导致里面直接存着“已解锁”,应用一样可以读到。

https://github.com/topjohnwu/Magisk/commit/f41994cb52ca08856216a8da0a28ed148c833f4e 过后,作为副作用,上面的问题刚好被缓解,但还有另一个问题:

prop_info 上有一个字段叫 serial,当这个 prop_info 被更新时,serial 会自增 2。也就是说,prop 被更新的次数 = serial / 2。而对于 ro 属性,它们是只读的,正常情况根本无法被更新,因此 serial 应该始终保持为 0。也就是说,如果发现 serial 不为 0 的只读属性,就代表它被 resetprop 过。

我们调查发现已经有应用在使用这些方法检测 root,Shamiko 1.0+ 已经尽全力隐藏了相关痕迹。我知道有很多人并不愿意使用 Shamiko,而其他隐藏方案的开发者并不知道这两个检测点,也就没有办法处理它,所以我公开了。

1月16日 #189 三星系统漏洞

阅读以下三篇博客,可以感受三星的代码质量

https://blog.oversecured.com/Two-weeks-of-securing-Samsung-devices-Part-1/

https://blog.oversecured.com/Two-weeks-of-securing-Samsung-devices-Part-2/

https://blog.oversecured.com/Discovering-vendor-specific-vulnerabilities-in-Android/

2月2日 #192 Financed Device

Android 不为人知的新功能:Financed Device 的原生支持

这个功能做了很久,但是非常低调,我完全找不到文档,甚至没法确定这个功能到底做完没有,唯一能找到的只有这句话:
A financed device is a device purchased through a creditor and typically paid back under an installment plan. The creditor has the ability to lock a financed device in case of payment default.
(Ref: https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:packages/modules/Permission/framework-s/java/android/app/role/RoleManager.java;l=175

简单来说,financed device 指通过分期付款购买的设备,这个功能借用了现有 Android for Work 中设备管理(device owner)的功能,允许债权人自定义的 app 对设备施加使用限制或在逾期不付钱的情况下远程锁定设备。现有 DevicePolicyManagerService 中添加了特定的 FINANCED_DEVICE_OWNER 类型,与传统的完全受管设备(fully-managed devices)区别,限制了 financed device owner 所能执行的操作,更加安全(?)。

从设计上来看,设备第一次开机,设置向导时 DeviceLockController 向自定义的 app 授予 android.app.role.FINANCED_DEVICE_KIOSK 这个 role(Android 14+),app 通过调用新增的 DeviceLockManager API 调用 DeviceLockController 提供的服务,DeviceLockController 验证权限后利用现有设备管理的 lock task 机制锁定设备。而 financed device owner 可以直接调用 DevicePolicyManager API 管理设备。不知道为什么要分两个,可能是允许 OEM 自定义。
以后就可以享受和 iphone 一样的,分期付款逾期不付直接被锁定手机的体验了。

注:搜索可以发现 Google 早在 2020 年就已经在 DeviceLockController 中加入了相关功能,但是并没有在系统底层添加原生支持。添加支持应该是为了方便 OEM 自定义?
https://www.xda-developers.com/google-device-lock-controller-banks-payments

2月21日 #199 软件包可见性


Android 11 中引入的软件包可见性(package visibility, https://developer.android.com/training/package-visibility )可以过滤应用能够访问的应用列表,但实际上是鸡肋功能

一方面,该功能虽然对 PM API 及其他一些 API (如 getDefaultSmsPackage)返回的结果进行了过滤,但系统中仍然存在大量 API 允许应用违反可见性获得未知包,如 AccessibilityManager.getEnabledAccessibilityServiceList() 允许应用获得正在运行的无障碍服务列表,还有 Settings API 允许应用通过读取系统设置的方式获得无障碍服务列表及通知监听器列表等
另一方面,系统中存在大量漏洞允许应用绕过软件包可见性查询指定软件包是否存在,以 CVE-2021-0975 为例,该漏洞允许攻击者通过系统抛出的异常信息中的微小差异判断软件是否已被安装。对应补丁在 https://cs.android.com/android/_/android/platform/frameworks/base/+/bf0d59726a0d9973f6867faedac0fe476c81fe8b
类似的漏洞 Google 给予的评级一直都是 Moderate severity 因此只在 Android 大版本迭代时对其进行修复。这种漏洞还有多少,我简单检索了一下,仅仅只是 Android 14 中修复的有 CVE 编号的漏洞就至少有图上这些。同时,2023 年 5 月起大部分 moderate 漏洞不会再被授予 CVE 编号,被默默修复而不为人所知的漏洞又会有多少呢?

注:即使使用 HideMyApplist 这样的 Xposed 模块,也无法阻止软件通过上述方式违规获得信息。不排除未来会有软件利用这种方式绕过用户安装的 HideMyApplist 等模块获取敏感应用(如 Magisk/KernelSU manager)信息。如果想治住这些软件,稍微靠谱一点的方法是利用多用户,把它们扔一个单独的用户里。注意必须是完全用户,使用 work profile 是不行的。

3月16日 #202 #203 #204 #205 #206 Android 开发文档离谱错误一览

1、2:把 service 打成了 ervice,少了一个 s,而且是两个 API 的示例代码一起打错。我 debug 了三个多小时才发现!!
https://developer.android.com/reference/android/nfc/cardemulation/HostApduService
https://developer.android.com/reference/android/nfc/cardemulation/OffHostApduService

3:文档里给出的示例代码引用的 API 根本不存在,尝试编译直接报错
https://developer.android.com/reference/android/content/om/OverlayManagerTransaction

4:一个 API,注释里写了可能返回 null,却被标记成 NonNull
https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/PackageManager.java;l=7390?q=PackageManager.java

5:已经被修复的一个错误,用 void 表示状态

4月3日 #211 作用于“系统框架”的模块可以打破作用域的限制

所谓“系统框架”其实指的就是 system server 进程,其中运行着大量的关键系统服务。虽然应用的进程是由 zygote fork 出来的,但实际上每个应用进程都会先向 system server 注册自己,system server 发回应用的信息,如 apk 路径,然后应用进程继续执行。只要作用于 system server 的模块都有机会篡改这个 apk 路径,打破作用域的限制,向任意应用进程注入代码。

这就完了吗?没有。

如果被注入的应用被授予了 root 权限,那恶意模块就能以该应用作为跳板,进一步拿到 root 权限。
那如果我一个应用都不给 root 呢?很遗憾,你的 root 管理器应用(比如 magisk 的 app)本身就有 root 权限,直接把恶意代码注入进去,就又能拿到 root 了。

强烈建议各位激活任意模块时都小心一点,尤其是当它推荐你选择“系统框架”的时候。

(PS:前几天就想发了,但是刚好碰上愚人节,容易被觉得是愚人节玩笑,所以延后几天)

4月12日 #214 Telegram RCE

https://t.me/Rosmontis_Daily/4631
(虽然从一开始的 0-click 变成了 1-click)

从作者引用的 PR 来看,我更倾向于认为这其实是两个 bug:

  1. telegram 会在用户尝试运行文件时发出警告,但是代码里面把识别 pyzw 的字符串打错成了 pywz,使得警告没有被正确弹出,这是第一个 bug
  2. 发送文件时允许发送者任意指定 mimeType 造成钓鱼攻击,这是第二个 bug

第二个 bug 可能最后被认为是预期行为,但是我个人倾向于认为是 bug。如果能找到不在白名单中的可执行文件后缀,加上这个 bug 又能再次实现 RCE。
事实上这种『缺乏对可执行文件的正确处理』的 bug 已经不是第一次出现了,详细可以查看:
https://evilpan.com/2021/09/23/macos-rce/#:~:text=%E7%9C%9F%E6%AD%A3%E7%9A%84%20RCE%E3%80%82-,%E7%9C%9F%20%C2%B7%20macOS%20RCE,-%E7%BD%91%E4%B8%8A%E4%B8%8B%E8%BD%BD%E7%9A%84

4月24日 #215 Telegram 加群人机验证踩坑实录

大部分加群验证机器人都是在有人进群的时候在群里发一条消息要求完成人机验证,而不是使用 telegram 的加群审批机制,打扰正常聊天,所以之前一直使用 Telegram Watchdog。

坑点:
1.官方实例经常掉线
2.部分使用 telegram 内置代理的用户尝试验证时打不开验证网页。可能是 iOS 上 webview 不走 telegram 内置代理?
3.部分用户尝试验证显示 Access denied。可能是使用的客户端版本过旧被服务器拦截,但是没有人配合测试,无法确认原因
4.在群组内有两只 watchdog 的情况下,部分用户反馈只收到一条验证消息(而且还过不去),不清楚原因

可能还有其他坑点,但是一直没有人配合调查,结果就是每天都有几十个人申请加群却过不了验证

后来 Rose @MissRose_bot 支持了新的加群批准,大喜,以为终于能摆脱坑了,然后踩进更大的坑
1.rose 的加群审批要开欢迎消息才能用,用户被批准进群后还会在群里再看见一条人机验证,完全失去优势
2.没有跟 rose 说过话的用户尝试验证会提示 bot 无法主动向用户发起私聊

实在不行了,Watchdog 和 Rose 一起开吧,能用哪个用哪个,总可以了吧?
大坑:
1.这种情况下,被 watchdog 批准的用户会被 rose 自动禁言,还要完成 rose 在群里发的验证(然后还设置了几分钟后自动删除)。经过测试,发现即使是被管理员手动批准的人也会被禁言,6…
2.如果之前已经被禁言,即使退群再通过 Rose 加群,进群之后仍然是禁言状态,必须再完成群里那个验证才能解除

6月4日 #217 CVE-2024-31318

跨频道联动:https://t.me/real5ec1cff/194

Android 2024 年 6 月安全补丁修复了一个由我发现并报告的漏洞 CVE-2024-31318 (漏洞补丁: https://android.googlesource.com/platform/frameworks/base/+/b68b257d56a8600d53b4d2d06fb82aa44086a4a5

回答思考题第 0 题:显然还是以 app uid 身份调用。但如果系统开发者忽略了 app 也可以调用 shell command 这一点,就可能造成安全漏洞。此漏洞本质上还是缺权限检查,不赘述。关于这一类漏洞的技术细节我会在本月博客上的安全补丁分析里说明。

那么为什么技术高超的 Google 程序员也能犯这种错误?
通过分析提交历史,我发现这个函数以前其实是有一个权限检查的,但是在 Android 14 的一个提交 https://cs.android.com/android/_/android/platform/frameworks/base/+/7a8f22792e45630bff14eb8b276c7649b0d79097 里被人删掉了。所以这其实是一个只影响 Android 14 的漏洞(这下新版本魔人大失败了)。
由于我不是 Google 员工,没法直接知道这个人这么做的原因,我这里只能猜测一下:可能是眼花了把这里看成已经在用 handleShellCommand 了,可能是觉得权限检查重复(Android 12 的时候这块代码在 ShellCmd 构造函数里),或者干脆就是 git 出了 bug 把这行给意外移除了,类似那个 gotofail 漏洞。

继续追踪提交历史,我发现了一个更搞笑的:2020 年,同样的系统服务,同样的 onShellCommand,曾经出现过一个一模一样的漏洞 CVE-2020-0227 https://android.googlesource.com/platform/frameworks/base/+/84cccfe6cdbc57ee372ee1a0fea64c7a11c53766^!/#F1
嗯,让你不写回归测试,白给赏金了这下。

时间线:
2023.10.4 Android 14 正式发布
2023.11.25 我注意到此漏洞
2023.11.26 构建了 PoC 并向 Android 安全团队反馈
2024.2.10 Google 确认此漏洞并将严重程度评级为高。对,你没看错,两个半月
2024.5.21 Google 通知我补丁将在下个月发布,并分配 CVE-2024-31318
2024.6.3 已经过去了 190 天,补丁终于发布

P.S. 从 Android 14 QPR2 开始,ActivityManagerService onShellCommand 中添加了检查,只允许 root/shell uid 调用,所以 app uid 无法再使用 am 或 cmd activity 系列命令。具体提交可见 https://android.googlesource.com/platform/frameworks/base/+/9e0c05e36afe0109b1df0d1bc375ade722138c81^!/#F0 ~

6月10日 #218 系统开启 R8

https://android.googlesource.com/platform/frameworks/base/+/1149dbb04b5a367c46bcfa5fcc0083dc2767a8eb

继对 system server 及部分系统应用开启 R8、Android 15 strip libart.so 后,Google 计划对 system server 开启完全 R8 优化。预计系统模块将可能会大规模失效。为啥以前机子存储空间小的时候不在意,反而现在开始扣大小?搞不懂搞不懂
(我自己在 Android 14 就被影响过一次了,虽然当时是 SystemUI)

给逝去的日子简单点首歌:
https://www.bilibili.com/video/BV13k4y1n7bd

6月14日 #221 爱哭鬼

转发自 易然个人频道

爱哭鬼 || 银教授

人活着这么辛苦,为什么还要活着?

是为了比一比谁更不辛苦,如果你的辛苦程度比别人低,嘻嘻,瞬间就开心了有没有?

就拿李建军来说,一场车祸,李建军失去了双腿,很辛苦了对吧?但是他的同学王褶容,失去了双亲。 每次想到这里李建军都忍不住笑出声。

如果你的人生都忍不住笑出声了,试问你还想死吗?一般都不会。

但也有人因为太辛苦而想自杀的,有一次我在天台抽烟,看到一个女孩站在天台上打算跳楼,我问:“你为什么跳楼?是感情不顺吗?”

女孩说:“女孩的心思男孩你别猜。”

如果一个女孩动不动就说“女孩的心思男孩你别猜”,那么肯定有感情问题。

我问:“你可以告诉是什么事情吗,说不定我可以帮你。”

女孩叙述了事情的经过:

就在刚才,她和男友吵架,

女孩对男孩说: “你根本不知道我在想什么!”

男孩:“你不说我怎么知道?”

女孩:“你不会猜吗?”

男孩:“那我猜猜。”

女孩:“女孩的心思男孩你别猜!”

我:然后呢?

女孩:然后他沉默。于是我就想死了,因为我最怕空气突然安静。

我:我觉得想死的应该是你男友。 他人呢?

女孩:在楼下。

我:为什么不上来?

女孩:刚跳下去。

我:那你为什么要跳呢?

女孩:我想和他双宿双飞。

我:人都死了,还想着双飞。

女孩:嘻嘻,真幽默。你有对象吗?

我:没有。

女孩:那你怎么还不去死?

我:死了也没有对象,那我为什么要去死。

女孩:也不是没有道理。

我:不过有时候的确想死。一个人久了,什么事都是自己做,就算自焚连个递打火机的人都没有,太苦了。

女孩:是的,我现在想跳楼,连个推我的人都没有。

我:要不我推你一把?

女孩:好呀!

于是我把手放在女孩腰间。

女孩咯咯咯笑了,她说:不能放这里,太痒了,我会笑的。

我:笑不好吗?可以含笑九泉。

女孩:我笑起来不好看。

我:不,你笑起来很美。

女孩:谢谢你。你真好。我男朋友从来没夸过我。

我:开心吗?

女孩:开心。现在我又不想死了。我觉得只要还能开心,就没必要死。

我叹了口气。

女孩:叹气干什么。

我:你已经死了。

女孩:什么意思?

我:三年前我在这里跳楼,跳完我就变成了鬼。

人在刚变成鬼时,并不知道自己是鬼,所以我以为自己还没死。

当时我站在天台上,看着太阳升起,突然不想死了,于是我就下楼回家。

回家后,我发现大家都看不到我,无论我跟谁说话,都没人理我。

我对着我的亲人大喊大叫,声嘶力竭,但我好像是一团空气。

那种无力感你知道吗?就是用尽所有的力气却没有任何作用。

后来我才意识到,我已经死了。

听完我的话,这个女孩继续哭。

哭了很久很久都没有停下来,也许她就是传说中的爱哭鬼吧。

这三年来,我见过很多自杀而死的鬼魂,大多数都很后悔。

活得不开心,可以换一种活法。但是死得不开心,就没法重新死了。

7月11日 #225 magisk.log

经常看见有人上传一份 magisk 日志就来反馈问题,然后被禁言又在那里抱怨说“我提供了日志啊”
那就来看一下,magisk app 底栏上那个“日志”里面保存下来的到底是什么东西
它大概包含这些玩意:
第一部分:设备基础信息,内核版本
第二部分:系统属性,注意这里不是 root 获取的,所以只包含 untrusted_app 有权限访问的
第三部分:环境变量
第四部分:app 的 mountinfo
第五部分:magisk 自己产生的 log,即 /data/cache/magisk.log 或 /cache/magisk.log。注意,它只包含 magisk 自己主动打出来的日志,其他日志,比如崩溃日志,不会被记录下来
第六部分:app 自己运行 logcat 读到的日志,只能读到自己 uid 产生的

接下来设想几个情况
场景一:开不了机。很明显根本进不去 app,即使运气好有 adb,如果没有提前授权 root,也拿不到 magisk.log。就算拿到了,它只包含 magiskd 自己主动打出来的日志,不包含崩溃日志,里面几乎不会有什么有用的东西。不如一句 adb logcat 有用。
场景二:magisk app 显示“无法获取”,即所谓的“掉 root”。这种情况有太多种原因,然而无论是哪种,由于 magisk app 自己没有 root,它能读到的东西基本上不会有什么用。不如一份 logcat 或者 bugreport 有用。
(题外话,如果是因为 magiskd 崩溃引起的,由于它会 block 所有信号,崩溃的时候不会有任何记录,静悄悄的退出)
场景三:zygote 崩溃太多次,触发 zygisk 自动关闭。magisk.log 不会记录崩溃日志,自然也记录不到 zygote 崩溃日志。不如 logcat 有用。
场景四:模块工作不正常。magisk.log 只会记录自己主动产生的日志,模块自己出的问题当然和它没关系。不如一份 logcat。

很多人说日志太多了,我觉得恰恰相反,是太少了以至于它自己的问题都难以调试。感兴趣的可以看一下我 debug 这个问题的艰难过程:https://github.com/topjohnwu/Magisk/issues/4174

8月22日 #229 MagiskEoP

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.

演示视频,利用 Magisk 中的一个漏洞静默获取 root 权限并以 root 特权执行任意代码而无需用户确认。我们同时展示了如何利用此漏洞获取 root 后向任意 app 静默授权 root。

8月24日 #231 补充 #229

Exploit source code and writeup is now available at https://github.com/canyie/MagiskEoP
本来还想着要多久才能有人发现这是 app 问题的,结果当天晚上就被吴自己指出来了,没意思

9月3日 #239 不会开发也能挖漏洞

不会 Android 开发的人,能不能挖到 Android 系统的漏洞?

先说结论,可以,但是能挖到的漏洞类型肯定有局限

其实我觉得关键的反而不是技术实力,是能不能判断出漏洞
很多人都觉得,系统安全漏洞这种东西,肯定很复杂,肯定都是内存破坏什么的,都是什么缓冲区溢出、释放后使用这种一听就很高大上的东西,包括我自己以前也这么以为
其实 Android 系统很多漏洞都不是内存问题,反而有很多的 UI 层漏洞,有些问题甚至不用写代码就能触发,看你对系统的熟悉程度
举个例子,2022 年那个价值 7 万美元的锁屏密码绕过漏洞 CVE-2022-20465,这就是纯粹的 UI 层漏洞,不用写一行代码就能触发,能在没有锁屏密码的情况下解锁设备,很明显这是漏洞,对吧?那其他看着没那么明显的东西,有没有可能也是漏洞?

今年八月有个漏洞 CVE-2024-34734,触发方式是在设备锁屏的时候下拉状态栏,里面有个“活跃的应用”,旁边有个“停止”按钮,用这种方式可以在没有密码的情况下杀掉 VPN 应用。可能看起来它不像漏洞,但是它能让没有密码的人停止掉 VPN,假如机主没有注意到 VPN 已经停止,继续使用网络,这个时候是不是就在用非预期的不安全网络?

Android 系统还有一个功能叫多用户,效果很明显,可以让多个人共用一台手机。NFC 设置里有个选项,只允许锁屏解锁之后使用 NFC,术语叫 secure nfc。假如机主打开了 secure nfc,切到访客用户然后给另一个人借用手机,这个人进设置里关掉了 secure nfc,然后切到机主用户,不输密码,是不是也能用机主的 NFC 支付了(CVE-2021-39807)?类似的还有访客用户帮你降级系统应用、添加 WIFI 网络等等。

Android 12 开始有个功能,有应用在录音或者录像的时候,状态栏右边会显示一个绿色的小点来提醒你,正式的名称叫“隐私指示器”。假如你平常用手机的时候,发现在某个应用里录音或者拍照的时候,这个指示器没出来,那你也可以去报漏洞。更进一步,这个指示器是 SystemUI 显示的,假如录音到一半 SystemUI 进程死掉了,那重启之后这个绿点还会不会显示(CVE-2024-0019)?

还有个功能叫屏幕固定,英语叫 app pinning,作用是借给别人手机的时候把手机固定在某个应用,比如借给别人打电话的时候你肯定不想让他进你支付宝里转钱。还可以设置退出这个状态时必须输入密码。那如果有办法在无密码的时候退出这个状态,也会被认为是漏洞。那假如说在这个状态下,被固定的应用被卸载了会发生什么(CVE-2020-0024)?

上面这种情况还有很多,比如如果在锁屏页面能查看隐藏起来的通知,那也算漏洞,看你的想象力够不够丰富。还可以玩排列组合,比如 Secure NFC 那个漏洞,访客用户不允许去改这个设置,那普通的第二用户应不应该去改(CVE-2023-21086)?锁屏的时候不能断开 VPN 了,那访客用户能不能断开主用户的 VPN?用屏幕固定这个功能固定了一个应用,那能不能下拉状态栏,在“活跃的应用”里停止当前应用,从而退出屏幕固定?

所以其实我认为关键点在于:1. 能不能想象到威胁场景 2. 看见一个东西能不能判断出它有没有安全风险。Android 给的漏洞赏金还是挺多的,高质量的高危漏洞报告基本上就是 7000 美刀。打个五折,不算税费,还有 3500 刀,按今天的汇率换算成人民币大概是 25000,一个月花 1000 也够花两年多了。如果真的有人对这方面感兴趣,可以关注我的博客跟频道,后续也会推送相关内容。

9月7日 #242 爆照

这里有一张照片但是我懒得放到博客里 自己想象吧(
配文:
洗完头没梳头 懒得梳
感觉不如把头发全拨到前面扮贞子
反正刚好也在床底下 还可以喊我床底下怎么有人

9月25日 #247 回复 #88

一年之后:当我在放屁
读大学有啥用啊 还不如直接出来进厂

9月26日 #248 getprop hole

引用 https://t.me/newqianzhuang/24

从 Android 14 开始,普通应用将无法读取 ro.debuggable 和 ro.secure 两个系统属性[2][3]。这对使用第三方 ROM 的隐藏魔人来说,无疑是个好消息:许多应用通过这两个属性来判断系统是否为 userdebug 或 eng 构建。

续前文,其实我们当时还发现了另一种方法在 AOSP 上绕过此限制:使用 app zygote。
AOSP 中这个限制是使用的 SELinux 规则来做:

1
get_prop({domain -untrusted_app_all -isolated_app_all -ephemeral_app },  userdebug_or_eng_prop)

对 domain 授予权限,但排除 untrusted_app_all、isolated_app_all、ephemeral_app 这三个域。也就是说,这条规则会对除不可信应用外的其他所有域授予读取权限。但是实际上,app zygote 进程运行在 app_zygote 域下,而 app_zygote 并不属于上面提到的任何一个域,所以其实用 app zygote 就能直接绕过限制。
我们给 AOSP 提了修复,很简单,直接在上面的规则里加上 -app_zygote 即可。预计应该是 Android 16 才会包含这个修复。

https://android-review.googlesource.com/c/platform/system/sepolicy/+/3266573/5#message-be806bdae1d35ef80b7cbf5e41a0b7ea94a8aeda

9月30日 #250 SDK Sandbox 分析

Android 15 默认开启了 Sdk Sandbox ~
勘误:被文档和其他人误导了,没有默认开启,到目前 Android 16 DP1 仍然处于默认禁用状态
官方文档:https://developers.google.com/privacy-sandbox/private-advertising/sdk-runtime

研究记录:

  1. 代码在 packages/modules/AdServices/sdksandbox/ 下,入口点在 https://cs.android.com/android/platform/superproject/+/android-15.0.0_r1:packages/modules/AdServices/sdksandbox/service/java/com/android/server/sdksandbox/SdkSandboxManagerService.java
  2. Sdk Sandbox 的 UID 取决于调用者的 uid,而不是大家以为的像 isolated service 那样每次都随机,具体为 uid - 10000 + 20000 1 2
  3. 启动 Sandbox 进程使用的是 AMS 中新增的 startSdkSandboxService/bindSdkSandboxService 方法,所以内部其实还是走的 startService/bindService 那套 3
  4. 启动 Sandbox 时会使用调用者在 <application> 下设置的 processName 加上后缀 _sdk_sandbox 作为 service 的 instance name,由于 retrieveServiceLocked 会使用这个 instance name 查找 ServiceRecord,如果多个应用(这里不考虑多个应用共享进程的情况)同时调用同一个 sdk,由于进程名不同,instance name 也不同,即使该 sdk 已经有进程在运行也不会被复用 3 4
  5. 系统服务认为所有 Sdk Sandbox 的 UID 即 20000~29999 都属于系统中提供 com.android.sdksandbox.SdkSandboxService 这个 service 的 app,即 com.android.sdksandbox 5 6

细节还是挺有意思的,当然也有未考虑到的地方,如有错误不吝赐教。目前 Magisk 的 denylist 还没处理这种情况。

10月8日 #251 CVE-2024-0044

复活一个半年前就被修复的漏洞?我把三月份就应该被修复的 CVE-2024-0044 续命了半年
PoC & writeup:
https://github.com/canyie/CVE-2024-0044
这个漏洞已知在被取证公司积极利用,不想被取证的用户建议尽快升级。看热闹不嫌事大的可以关注各取证厂商,估计很快就会说自己突破了提权技术难关之类的了。

10月24日 #253 无障碍出行

陪同残疾人士 乘坐东方航空航班 广州白云->上海虹桥 体验

  1. 一定一定一定一定一定要让人早点到机场!!!16:30起飞的飞机,我14点到机场,然后一直没值机,就是为了等人,结果人15:35才来,给我气炸了

  2. 虽然来得比较晚,但是提前预约了东航的无障碍服务,各项照顾还是很到位的,比如下机用无障碍登机车,机场有专人帮忙推轮椅到机场出口等

  3. 虽然预约了东航无障碍 但是没约白云机场的无障碍…然后到现场之后临时约,无障碍登机车是没了只能爬登机梯,然后因为登机即将截止,机场小姐姐帮忙全速推轮椅终于赶上飞机,在这里给白云机场工作人员点个好评(

  4. 提前预约东航无障碍服务会导致他们给你提前订好一个座位 然后我值机的时候想坐一起 尝试选旁边座位没成功 所以有相关需求的旅客建议提前预选好座位 虽然这次航班机组直接给免费升了超级经济舱(

总结:1.早点到机场 2.需要特殊服务早点预约 3.座位早点选

附1:上海地铁好几个站的无障碍电梯是在付费区外然后直接进入站台的 导致两个问题 1.进站之后想用无障碍电梯需要先找工作人员出站然后按求助按钮让工作人员给你开电梯 2.从站台坐电梯上去会直接出付费区,需要找工作人员帮忙刷卡出闸,否则系统里没有出闸记录

附2:预订酒店时应该提前确认该酒店是否无障碍友好,否则就可能像我们一样,到了酒店发现房间是复式的,分为多层,需要走楼梯才能上床,然后只能默默退房找了间汉庭住

10月24日 #254 #255 #256 #257 GEEKCON 2024 上海站

总体挺好的 就是这个会场有点伤眼睛
省流:

  1. 『某第三方应用市场』利用多个漏洞进行后台保活、后台弹窗、恶意发送通知、绕过权限安装应用等恶意行为,对多个手机厂商均有覆盖,甚至利用了自家 sdk 中的漏洞
  2. 『某手机厂商』的系统存在应用劫持,用户点击目标应用后实际会先启动广告页面,利用用户进行广告推广获利
  3. 『远程让手机爆炸』这一环节没有队伍报名,也就没有挑战
  4. 『万事俱备,只欠一台车』环节由于找不到该厂商的车无法演示而放弃。我到现场才发现这标题原来是这个意思,6

10月27日 #258 补充 #67

https://android.googlesource.com/platform/bionic/+/master/android-changes-for-ndk-developers.md

才发现竟然有这种文档,不知道是不是我火星了。里面 Correct soname/path handling 这个问题是当年 Magisk 还支持 Android 5.0 的时候就遇到过的。

11月2日 #261 每天都被撑死

去食堂打饭 阿姨打的饭嫌多 平均只能吃掉一半
去外面吃饭 吃啥都嫌多 一碗啥都不加的螺蛳粉吃完还剩一半粉
在家天天被念叨让多吃一点
水喝多了都嫌撑
碰上聚餐/花钱比较多的情况更是不得了 只能本着钱都花了不能浪费的原则往嘴里塞
然后代价就是胃疼/反胃/反酸 不敢平躺 睡不着
到底哪里才有适合我这种饭量的人的饭啊 难道我只配一直啃包子/馒头/面包吗

11月5日 #277 android-platform-common-vulnerabilities

https://blog.canyie.top/2024/11/05/android-platform-common-vulnerabilities/

新博客,介绍一下挖 AOSP 漏洞的时候可以挖到哪些。
这篇博客本身有没有用我还不知道,但是编写它让我发现了一个高危 CVE-2024-43088。谷歌在 11 月补丁中终于修复了它,也让这篇文章终于能够被大家看见。
另:11 月补丁中修复了我的另外三个漏洞 CVE-2024-43080 CVE-2024-43081 CVE-2024-43090 ,同时修复了 LSPosed 团队其他成员报告的 CVE-2024-43093 。感兴趣的可以看看。

11月5日 #278 补充 #229

这个问题被分配了 CVE-2024-48336。
这里给出一个额外提醒,就是很多 app 使用了类似的代码,但没有正确修复这个问题,比如 FoxMagiskModuleManager 1 和 KeyAttestation 2。用同样的方法可以注入任意代码进受害 app,然后利用该 app 被授权的额外权限比如 root。能做的事情就很多了。
当然你还可以搞一个什么 “密钥认证一键修复 app”然后发去小绿书(

11月6日 #279 Telegram 数据取证要点

  1. 有效法律证据形式
    根据 GA/T 1564-2019《法庭科学 现场勘查电子物证提取技术规范》,应对现场、屏幕进行拍照或录像,记录物证的系统时间和北京时间;对有密码保护的数据提取并保存在有唯一性编号的专用存储介质中。
    根据《公安机关办理刑事案件电子数据取证规则》第八条,需要及时固定相关证据时,可采用打印、拍照或录像等方式。第二十三条,对公开发布的电子数据、境内远程计算机信息系统上的电子数据,可以通过网络在线提取。

  2. 取证要点难点
    数据解密(Android 上应用数据被 FDE/FBE 保护,开机第一次输入密码后才会解密数据),数据保持,数据防销毁

  3. 分点解析
    由于 Telegram 功能特性,聊天的一方可以为另一方删除聊天记录,扣押到检材后需要立刻断开网络,不要轻易重新连接网络,以免证据被远程删除;可重点关注被设置了定时删除或加密的聊天;可以通过截图、录屏等方式保存证据,对获取到的数据计算哈希值校验并留存。通过 Telegram Desktop 也可以直接导出嫌疑账号数据,但是需要注意账号新登录官方版 telegram desktop 后尝试直接导出数据会提示需要等待24小时,并且该操作会向其他登录会话发送提醒。也有其他第三方 telegram 专用取证软件可用,但一定注意提取范围不要设置过大,避免数据量过大触发服务端安全保护。
    对于群聊数据遭到批量损毁的情况,若被控制账号有群聊管理员权限,可使用脚本导出群聊近期操作记录,如 https://gist.github.com/avivace/4eb547067e364d416c074b68502e0136
    根据设备类型不同,也有第三方数据提取软件可用,如针对 Android 平台的多个取证软件,这些取证工具一般使用漏洞提升权限然后读取目标应用的私有数据,常见的被利用漏洞有 CVE-2024-0044(AOSP 框架漏洞,由 Meta Red Team X 及我报告)、CVE-2024-31317(AOSP 框架漏洞,由 Meta Red Team X 报告)、CVE-2024-4610(Arm GPU 驱动漏洞,影响除高通平台外大部分平台)及部分厂商如三星自己的漏洞等。取证公司仍在持续挖掘漏洞并转化为利用,如奇安信旗下盘古石取证团队近日宣布再次突破 Android 11 - Android 14 最新安全补丁提权:https://mp.weixin.qq.com/s/MorsW-Vx_Eb_WnqFtj_T5Q
    部分设备可能存在设备管理 app 等,可能导致检材上的数据被擦除,可以使用漏洞对相关管理 app 进行禁用或无效化处理。除了传统的垂直提权漏洞,其他类型的漏洞也有可能被利用。本月 Android 安全补丁就修复了我发现的一个可使设备管理机制完全失效的漏洞 CVE-2024-43081,可能会被积极利用。
    部分第三方系统存在额外安全保护机制,如设备闲置过指定时间后启动额外的数据保护机制如重启,应阻止设备休眠。

仅做备份处理,希望大家都用不到。

11月7日 #281 CVE-2024-40676

https://blog.canyie.top/2024/11/07/self-changing-data-type/

之前十月补丁分析没给出 CVE-2024-40676 这个漏洞的细节,原本以为可以安安静静当谜语人的,结果还挺多人对它挺感兴趣的,那顺便把之前分析的结果公开到博客上。准备以后抽空把之前挖的坑全填上。
另:总感觉我的博客浏览起来比其他博客更卡,有人有同样感觉的吗?似乎该花点时间再优化下体验了

11月9日 #282 回忆

最近认识了很多人 感谢各位大佬的引荐 才能让我认识这么多大佬
很多人认识我的第一句话是 “你好年轻” “还有这么年轻的研究员” 之类的
类似的话我已经听了六七年了 但是每过一年听 都有不一样的感觉
今年20岁了 现在更像一种怀念 怀念我真正年轻的时候
作为一名研究员 我发现过很多漏洞 用一年时间打进了全球前二十
但是我觉得论这些洞的巧妙程度 元反射是当之无愧的王
提出它的那一年我14岁 初二
没有现在这么多的知识积累 纯靠脑力 从最基本的逻辑出发 以子之矛攻子之盾 用最简单的方法击碎谷歌精心布局设下的防御
即使它根本就不是个安全漏洞 我也觉得它是我提出过的最好最有创造力的 exp
跟它相比 我后面写的东西挖的洞都一文不值
“天才”描述的是一个人有极强的“跳跃式”的理解能力和创造力
而“工匠精神”的“工匠”强调的是一个人追求技能的极致完美并付出行动去打磨它
两者的区别是 工匠的成长过程是重复的 线性的 而天才是非线性的
天才不需要多少知识储备就能抓住事物的本质并打破传统思维路径提出创新
这是天赋 是上天给的 后天补不了的 也是我永远都不可能再有的
所以“工匠精神”被传颂 被称赞 被学习 却没有人宣传“天才精神”让大家学习当天才的
自那以后 我再没有提出过如此精巧令人惊叹的创新
我怀念我的14岁 我怀念我没被禁锢的 真正年轻真正有活力的头脑
14岁 2018年
那一年 还没有疫情
那一年 《学猫叫》火遍大街小巷
那一年 我初二 现在大二
那一年 没有成就 没有作品 甚至连电脑都没有 只有一颗活跃的大脑
那一年 像是被灌输了无穷的青春活力的一年
那一年 只能留在回忆里 永远也回不去了

11月13日 #283 虚伪的艺术

人类的有些反应是刻在大脑里的。

但是人们都不愿意说出来。因为说出来显得自己很不理智,不符合社会对一个成年人的期望。
理由,或者说借口,就是这样衍生出来的。说白了就是掩饰自己的原始欲望而已。

人们嘲笑胖猫是因为胖猫抢了自己的饭碗,或者胖猫投河污染了自己的水源吗?
人们嘲笑胖猫是因为想。
这里胖猫只是个符号而已,什么符号?被嘲笑的符号。至于胖猫自己到底是谁干了啥已经不重要了。有瓜不吃不嘲笑就失去了这两个字的意义。
中文互联网流行给别人名字冠上牢字辈。不知道胖猫有没有获此殊荣。

你小时候哭,家长有的时候会哄你,等你长大了再哭,家长就会教育你“大丈夫流血不流泪”。
他们是希望你用流血代替流泪吗?他们是不耐烦你不想看你哭。
要是小孩真的去用流血代替流泪,用自残发泄情绪,他们就会求你别这样干了让你想想你的父母。
别问我怎么知道的。

把人类历史往前推百万年也一样。
生活在四百万年前的南方古猿,或者现在流行叫吗喽,能繁殖延续这么久,难道是为了保护生态圈防止种族灭绝吗?
吗喽繁殖是因为想。繁殖了它就爽。

想体验一把吗喽的原始快乐怎么办?
有一个网上流传的方法,找个甜的橙子洗干净,去浴室,关门,开热水淋浴,直到把全身都打湿,直到浴室里充满蒸腾的水汽。
这个时候咬一口橙子。
品就好了,不需要全吃下去,让汁水从嘴边混着热水流下去也行的。
没条件试的可以上 b 站搜“洗澡吃橙子”,赛博体验一下。

你看,事情就这么简单。刻在基因里的想。
其他什么理由什么借口都只是人类文明给生物本能加上的华丽修辞。
说好听点叫华丽,说难听点叫虚伪。

当然我也并不是什么好人。从吗喽继承过来的本能我也有呢。
人类的行为没有原因。也不是万事都需要原因。
生物学上的平等才是真正的平等。
感谢生物学,感谢进化论。

不过你要追究其原因,当然也是可以的。
从弗洛伊德的精神分析理论去看待,它可以被解释为“合理化”或者“压抑”的“防御机制”。
再更进一层,它可能是什么神经元什么神经递质层面的反应。
这样探究下去就无止境了。在有原因和没有原因之间反复横跳。
可惜我不懂这么高深的知识。我也没义务去给所有人做心理治疗。

11月14日 #285 Pwn2Own 小米

偶然看见去年 Pwn2Own 上攻破小米 13 pro 的一个团队公开相关信息了,刚把 ppt 看完,觉得挺有意思的,不过最有意思的部分不是漏洞细节

演讲视频:https://www.youtube.com/watch?v=B0A8F_Izmj0&ab_channel=DEFCONConference
相关材料:https://github.com/Yogehi/cve-2024-4406-xiaomi13pro-exploit-files

省流:小米在 Pwn2Own 期间故意进行针对性干扰以阻止其产品被选手攻破,相关干扰措施仅在 Pwn2Own 期间生效,比赛结束后即被移除
其描述的相关细节让我觉得似曾相识。扩展阅读:https://xuanxuanblingbling.github.io/iot/2022/09/16/mi/

怎么说呢,鉴于小米产品在“各类黑客比赛上/期间”表现出“特别”突出的安全性,建议研究人员不要将小米产品作为目标

11月16日 #286 忆三年前进电子厂经历

(长文预警)

2021年9月至12月5日,我刚满17岁,因学校强制要求实习,我和班上大部分刚满16岁的同学一起先后在两家电子厂当普工。第一家电子厂在河源,我只待了一个多星期,后跟随学校转场到另外一间位于东莞大朗的电子厂。

这间厂是造手机的代工厂,我在流水线上干了三个月,换了三间车间,每个车间代工不同品牌手机。

可能有人不知道流水线的工作是咋样的,大概就是每个工位只负责做自己固定的任务,比如装摄像头等,装完传给下一个。所有操作都被标准化规定好,工位上还有标准作业程序(简称 SOP)。我进的厂入职还有考试,内容就是防静电操作等。我上班的厂给每个工位都配备了凳子,但是我基本上都是站着干活,因为坐着根本干不过来。然后还有 IPQC 和 FQC 负责监督你干活还有干活的质量。

流水线在工厂里叫“拉”,相应的“线长”就叫“拉长”。两班倒(除部分车间),七点四十五之前要到车间,有几率要站那里听车间主任或者拉长开会。然后八点正式开始干活。每天的任务不同,一般一条线一天要下 1000 至 2000 台手机。(这里不用“生产”是因为厂里除了组装线还有测试线、包装线以及测包线)

下班时间看你干活的速度,早上八点上班的话基本上都是晚上九点左右下班。中午和下午各有一个小时吃饭时间,但实际上还有一个叫做“连班”的东西,即下午的班跟晚上的班连着上,中间不给你休息时间也就不用想吃饭。我三个车间的工作都在拉尾所以基本上是最晚下班的,工作时间最长的一次是早上七点四十五上班,晚上十一点二十下班,而且还是连班,晚饭都没吃。休假更是不用想,宣称是单休但实际上连单休都难。中秋节放了一天假就把当周周日的休假取消补回来。其他大部分时候周日也放不了假。国庆放假三天应该是最长的假期。不过也是放过一次双休的,是因为工厂没货干不了活。

至于工资,作为一名光荣的学生工,我们光荣地在东莞这种被广州和深圳夹着的地方获得了高达 9.5 元人民币的时薪。问车间其他员工也大部分表示是 11 至 13 元左右。我知道我该得的工资肯定有一部分进了别人手上。第一个月发了2800,第二个月发了3800,第三个月3500,12月在厂里干了三个晚上给我发了58块钱(不过忘记是不是因为其他因素扣款了)。

我待过的三个车间里绝大部分都是像我这样的未成年学生工。全国各地的都有,最远的见过四川过来的。厂门口有个篮球场,接学生工过来的大巴就停在旁边的路上。我离开的前几天路过那里,都还能看见大巴车跟新来的学生。至于为什么,可能是因为学生好骗好管还不用开太多工资吧。

在厂里工作的三个月,我的感觉就是麻木。每天的生活就是宿舍-食堂-车间,没有时间也没有力气再去干别的什么事。三个月出厂的次数估计不超过十次,唯一的一次双休还因为我头晕而在床上度过。偶尔出去一次,看着厂门口的广告牌写着“诚招普工,单休,月工资6000-8000”就觉得好笑。厂的位置在东莞松山湖,离华为东莞总部只有三四公里,很接近深圳,在天上每天都能看见飞机。可是与我无关。

我到现在还记得工厂里的防静电要求,静电服、静电环、手指套、离子风机样样俱全,酒精上还印着 RoHS 标,虽然上班一个小时之后手指套基本上就破完了,然后就是手指肉去生碰机子,每天下班手指头上都是血。

厂里让我印象深刻的有两件事:
1.12月3号我干活的时候对面工位的同事在纸上给我写了一个 qq 号,但是我在干活暂时没理他,他就把纸条扔进了垃圾桶。我胆小,没敢说话,原本想着4号还能再问问的,结果因为我们学校的学生全部5号走,整个车间4号直接停了。我没能看见他给我留的 qq 号,没能知道他的名字,也再不会有机会见他一面或者跟他聊天了。我其实还记得当年几个拉长还有几个同事的名字,但我这辈子应该也没有机会再见一次他们了。
2.某次我跟其他几个人一起干 MMI 测试,我旁边是个看起来比我还小的小姑娘,她问我为什么有台机子重测了几遍都测不过,我看了一眼上面显示 fingerprint sensor fail 之类的英文,跟她说打个不良标放着,报表上记个不良品,指纹传感器有问题。然后她特别惊奇地跟我说,你看得懂英文?你看得懂英文为什么会在这里?那一瞬间我的心里五味杂陈。

那三个月我甚至不知道我是怎么活下来的。说不定是每天干活能流血感受疼痛还能让我保持点清醒吧,毕竟我现在也经常自残看血流出来,哈哈哈。可能得归功于 lsp 群群友愿意陪我聊天吧。不过也物是人非。当年和我聊天的人里很多都销号了,我还记得的人里有个被某些自命清高的高雅人士逼退网了,有个因为癌症离世了。

三年过去,我也换身份了,从电子厂员工变回学生再变成 framework 开发者再变成安全研究员。我收到第一笔漏洞奖金的时候第一感觉是惊讶,然后就是在想,卧槽钱原来这么好赚,那我之前的日子,尤其是在电子厂那三个月,是不是都活到狗身上了。我是个小县城长大的孩子,不开玩笑地说,那笔钱直接改变了我对钱的认知。到后面我以世界顶级研究员的身份受邀跟其他大佬见面,见到了之前我代工过的某手机品牌的公司的技术大佬。我特别戏谑地跟他们说,你知道吗,我给你们造过手机,然后解释原因。我们聊天的时候都是笑着的。
三年前,17岁的我是你们品牌代工厂流水线上的一个月工资3000的学生工。三年后,20岁的我用自己的努力和实力以受邀者的身份出现在这里,至少在这张桌子上,我和你们平起平坐。
我怎么活过来的,只有我知道。

11月16日 #287 #288 #289 补充 #286

哦对了,忘了提一点,当年我是拒了阿里的内推选择跟着学校去电子厂实习的。至于原因,家里人觉得杭州太远,不如跟着学校还安全点。
为电子厂拒阿里,这事我特么能吹一辈子。

我的感想就是人应该多去看看世界的另一部分人是怎么样的,增长下自己的见识。别像我一样在上海世贸大厦底下看着电梯要刷卡才能进去选择在一楼转圈消磨时间,转了几分钟才知道是要找人帮忙刷卡。

然后我去高德上搜了一下那个厂的评价,看起来没变多少。我当时住的是六人间改的八人间,据说厂里在建新宿舍,到时候住宿环境会更好。但是看今年的评论,似乎变成十二人间了。

11月18日 #290 OnePlus 可用性警告

据报告部分一加设备升级到 Android 15 后出现无法使用 Termux 的情况,相关进程被不明原因杀死。Termux 开发者表示禁用 phantom process killer 后问题仍然存在。有用户报告此问题影响 Magisk 导致无法在 Magisk app 内修补镜像。部分受影响设备安装最新的 ColorOS 更新后问题消失,但似乎仍有部分受影响设备尚未收到更新。
建议一加用户若收到 Android 15 更新且构建日期早于 2024 年 11 月 9 日,谨慎选择安装。若您已受该问题影响,请等待厂商发布更新。

参考:
https://old.reddit.com/r/termux/comments/1gks9mf/announcement_termux_broken_on_android_15_for/
https://github.com/termux/termux-app/issues/4219
https://github.com/topjohnwu/Magisk/issues/8553

11月20日 #291 #292 #293 #294 #295 Android 16 第一个开发者预览版已发布

版本代号为 Baklava 而非 W 开头,比往常提前了几个月。预计 2025 年将有两个大版本发布 1
这一次版本代号重新回归到了 B 开始计数,可能是因为 Android 内部开发流程发生较大变更(project trunk stable?)所致 (来源:Mishaal Rahman 2)。作为 project trunk stable 的重要部分,aconfig / feature launch flags 的文档已经上传至 source.android.com 3

简单刷上去试了一下,感觉 UI 更丑了,有一种没事强行整强调背景色的感觉…… 放了亮色和暗色模式的截图,感兴趣的可以看一下。
系统设置项被重新排列,把 Google 账户放到了第一位,About phone 被放到了中间,Accessibility 跟 Tips & support 放到了最底下…… 感觉让残疾人更难操作了。
Pixel 6 系内核从 5.10 升到了 6.1 好评,其他没感觉到什么大的变更,简单跑了一下梦境,一行代码不用改直接跑通,估计没啥大更改(也有可能是都没开?不过不太可能)

11月27日 #301 #302 为什么不拜阎王

中国很多家庭会在家里供奉神明或者仙人,拜佛烧香。拜文曲星或者观音菩萨的多见,但是好像很少看见有人拜阎王的。可能是觉得跟死亡沾边不吉利,但是清明节去上坟烧香的又不少。另一种解释是拜佛上香火是为了向神明表明自己的虔诚以祈求庇护,而阎王的形象让人感觉拜了也不会得到庇护。民间流传的阎罗王殿贴着的对联有多个不同版本,在这里挑几副供大家欣赏:

“上联:阳世人间,为非作歹任凭你;
下联:阴曹地府,古往今来放过谁?​
横批:正要抓你”

“上联:能耐再大,到这里休得施展;
下联:冤深似海,非此处哪能分明?​
横批:你可来了”

传说十殿阎罗的第五位是大名鼎鼎的铁面无私包青天,从这里也可以看出人们认为阎王的形象是处罚坏人的,即“惩恶”,而像“扬善”这样的活就交给慈悲为怀的菩萨。朴素的贫苦大众相信恶人无论在阳世间多有钱有权,在另一个世界都会被用油锅烙铁案板屠刀各种刑罚好好对待。但是拜铁面无私的执法者并不能直接给自己带来好处,有限的香火不如供奉其他神明。大家心里是希望好人得到好报而坏人得到坏报的,但在两者之间权衡,似乎让自己得到好处更优先。如果负责“扬善”的神明只需要人用香火去供奉就会给那人以好处,那这“扬”的,还是“善”吗?这样看来,负责“惩恶”的神明又显得有点可怜了。

以上文字写于某位友人离世一周年之际。终于赶在凌晨零点之前发出来了。其实我还想设定时消息,但是 telegram 定时消息有很大延迟。

写到一半才想起来,刚好昨天也是另一位熟人离世一周年。之所以说是熟人,是因为我和她直接接触很少。但她关注了我的某个频道,所以我记着。我也经常偷偷的去看她的频道,但是没关注。

传说中的阎罗王是绝对公平绝对正义的吧,我希望是这样。

我常幻想没有我的日子里,或者说我还不存在和我不会再存在的日子里,的故事。我作为看客,静静地看着斗转星移春去秋来。但我恐怕很害怕看见幸福结尾。我仍想抓住世间事物的一点点痕迹,但我只能看着没续费的博客随着时间的推移不再可以访问,看着太久没登录的账号被注销。看着人来,目送人远去。只是目送。

12月27日 #303 成长

一直有人想知道我的“成长路径”是啥样的 这里随便写点东西

我出生在一个广东的小县城,简单来说就是穷地方,gdp 在全省是倒数的。小时候就对计算机很感兴趣,四年级的时候学校开了电脑兴趣班,本来因为是教编程就报了,去了之后才发现是教用 flash 画画。四年级的时候身体经常不舒服,后来才知道这个叫躯体化症状,五年级的时候更严重了,从那个时候开始就很少去上学了,初中休学一年但是复学之后也没怎么上学,一直到我上中专。所以我在义务教育阶段有五六年的时间都是没正常上学的。这就是我为什么没参加中考。

除了去看病(包括住院)之外,剩下的时间我一般就是在家待着。我本来就对游戏没什么兴趣(事实上我到现在都没玩过什么流行游戏,什么王者荣耀什么吃鸡我都没玩过),在家待着也挺无聊的,干脆学一下自己一直很感兴趣的做软件(后面才知道有个术语叫编程)。

那个时候我只有一台手机,我还记得型号,是酷派的 Y80d,安卓4.4,家里人只有每天下午到晚上会给我手机,没有电脑。在网上闲逛的时候发现了一个叫做 iapp 的软件,那个时候还是 1.x 的版本,能自己在手机编个小软件出来,然后就喜欢上了,开始学。它支持可视化设计 app 的界面,可以直接点点点然后就可以拖一个按钮出来,用的是自己发明的一个脚本语言,难度不大,语法非常奇怪,但是有判断循环这种简单结构,然后我就这样打包出了第一个 app,然后我发出去别人就能直接安装使用了。我第一次发现原来做个软件这么简单,说真的它很大程度上激发了我的热情,然后我就开始玩了起来。那个时候我最不缺的就是时间。你问我为什么不去学易语言?因为我没电脑。

后面想学点正经的,学了点 js 跟 lua,现在已经完全忘光了,后面觉得还是学 java 好。正好手机上也有个能用 java 编 android app 的软件,叫 aide,然后就学起来了。说实话我不是个聪明的孩子,我当年学 java 耗费了很长的时间,难以理解类跟对象的概念以及它到底有什么用。后面我选择不看什么入门教程了,直接去贴吧找了个简单的项目打开硬生生啃,我也忘记了多久,突然就像开窍了一样,然后算是终于过了 java 这关,然后进入真正的 android 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,把我也拉进去了。

后面就是无聊的日常。中专二年级学校强制要求送人进厂实习,在电子厂打工真的是个折磨,我想做点自己喜欢的东西,然后我开始给 magisk 提交 pr。然后折腾折腾就变成了 collaborator。

从电子厂回来之后就是上学,考完高职高考之后学校又想送我进厂,我直接玩消失。去了个小公司搞 android framework,也是变成自己小时候梦寐以求的系统开发工程师了。到开学的时候了,踏进了大学的校门,我变成了一个本科在读生。

在学校里就没经济来源了,我想经济独立。想起来搞之前上班的时候发现过漏洞,也开始无聊翻源码希望有收获。一开始我只是翻着无聊玩的,结果后面发现他们给的实在是太多了,就开始投入更多时间精力搞这块。一年下来也算有点收获,在全球的排行榜上排个前十几,不算亏。

这就是故事的全部。你可以发现我对未来没什么计划。我不是一个聪明的孩子,我要是聪明的话就学 OI 打比赛去了,感谢上天愿意赐我一条生路。我也不是一个擅长预测风口的人,电商网购直播短视频等等风口我一个都没预料到,我做事纯凭兴趣,小时候看着 kingroot 觉得很厉害也想搞的小孩也不会想到长大之后是这样。现在回过头来看,我可能应该开个公众号,把人引流过去,然后接广告或者开知识星球来变现,再或者去视频平台投个《20岁谷歌认证世界顶级研究员的一天是什么样的》给自己出道。不过我最后还是没干就是了。

然后我随便总结了点经验:
0. 有些东西没想象中那么难 不试试怎么知道

  1. 很多技术或者知识不迎合市场或者太过高深很难赚到钱,该放弃就放弃
  2. 我很认同“知识改变命运”但是反对把它曲解成“读书改变命运”或者“接受义务教育就能改变命运”
  3. 我知道这个频道有很多无法取得足够学历的人关注 我想说 人生不只有一种活法 虽然我知道这种人生经历难以被复刻
  4. 不用尝试预测未来 谁都不知道未来会怎么样 如果我当年有电脑的话很可能根本就不会碰 android
  5. 我个人觉得“想”要比“做”重要 早期在缺乏调试能力的环境里硬啃源码的经历让我更习惯在脑子里思考 导致我看的代码比自己写的要多得多 一定程度上也算给后来搞安全打下了点基础
  6. 累了就休息 不用强迫自己累了还干不喜欢的事 可能人只有放下了别人对自己的期待才能找到属于自己的平静
  7. 身体老是不舒服记得想想精神科的原因

2025

1月1日 #307 #308 #309 #310 #311 #312 #313 去年干了什么

2025 了,首先祝大家新年快乐哈
今年发生了挺多事情的,感觉脑袋快烧了,感谢大家一年的陪伴
上学是真没意思,要干的活还多
今年抽了点课外时间花在 android 安全上,做了一点点微薄的贡献,现在勉强在 Android Program 排行榜上排了个第 10 名,整个 Google BugHunters 平台第 55 名。今年 3 月之前排名还是 N/A,感谢各位大佬的谦让哈,让我这种无名小卒也能参与这种大项目
本来今天想把 2024 年度排名一起发出来的,结果到现在都还没刷新下来,等它出来了再发吧
我心里知道我跟真正的大佬还是没法比,Chrome VRP 一给就是三万刀五万刀,我个只玩 Android VRP 的怎么比,没办法
今年十二月的 swag 也没拿到
我不会忘记我的出身,也感谢大家这一年的支持,学到很多,感谢给我这个机会去给一个操作系统安全添砖加瓦
郭德纲有句话很有名,“江山父老能容我,不使人间造孽钱”,我想在后面加一句,“心中自有青天在,愿行千山不染尘”
再次感谢大家支持哈,要滚回去复习了,考试快挂了

P.S. 这段文字是提前写好的,本来想留时间出来复习的,真的到元旦了发现自己一点都不想碰学校课程。新的一年第一天请大家吃预制菜寓意一年遇见智慧的人(

1月20日 #316 一个新鲜的 parcel mismatch

https://android.googlesource.com/platform/frameworks/base/+/2ce74e2d84777657f11b5cbabc501e6d79c86337
翻了一下连 15 都没进
估计最多活了一个测试版
没啥用 发出来给大家乐呵乐呵
昔日 Mismatch 今犹在,不见当年 PDD

1月28日 #318 对今天是除夕这件事完全没有实感

看着换对联,贴福字,摆灶神,搞卫生,心里没有一点波澜,手机上 app 的红色新年活动页面也懒得点开,还记得很久以前花一堆时间刷 qq 红包跟支付宝集五福,现在只觉得闹心,还不如去领红包封面
下午出去街上逛了一圈,虽然有点心理预期(县城人过年一般晚上才出来散步),冷清程度让我怀疑是不是回到了2020年疫情封城那个时候,紧闭的商铺大门,偶尔能看见贴着福字,旁边一个满满的垃圾桶,最顶上的是刚撕下来的福字对联,偶尔从遥远处传来一两声烟花爆竹爆炸的声音,对比出一种荒谬感,跟小时候记忆里的景象没有一点关联
可能长大了感觉不同吧,小时候期盼家人团聚,现在只觉得团聚了也是听吵架没意义。看我妹也只是拿着手机刷了一天抖音,跟其他不用上学的日子没有任何不同。
今年这节日不如改名叫 DeepSeek 节,给我的触动还不如我妹在我面前外放奶龙带来的感觉大

2月3日 #322 积极地浪费生命

我浪费生命 我好
有没有人教一下怎么高效、稳定、节能、可持续地浪费生命
指开学了还能继续躺着啥也不干

2月5日 #323 生活小技巧

如果你有一些欠你钱不还的青少年朋友 可以这个时候试试催
一般来说这个时候他们该收的压岁钱都收完了 而该奢侈花的钱还没来得及花出去

2月9日 #326 有些东西错过就没了

比如 CVE-2024-31318 这个漏洞
刚开始知道评了 high severity 的时候还挺开心的 那个时候是纯小白
结果后面才知道类似洞可以评 critical
(CVE-2024-31320 给了 critical)
然后过了一年之后才又有勉强能摸着 critical 边的洞 CVE-2025-0100
影响是可以用户无授权录制设备屏幕
应该也是最开始评的 critical 满足降级规则被降成 high
下一个不知道是什么时候了
我还想拿 CVE-2025-0001 这种数字的 比较有纪念意义 又得等一年才能碰运气了
(你不如拿个 114514 更有纪念意义)

2月11日 #328 支付宝蚂蚁森林侧信道信息泄露

原理很简单,用户通过支付宝做了特定操作之后,第二天蚂蚁森林会产生特定数值的能量球
比如通过支付宝网购火车票会产生 136g 能量,购买电影票会产生 180g 能量,每天运动则会根据步数产生 0~296g 不等的能量
所以可以根据好友的蚂蚁森林能量球上的数值大概猜测前一天干了什么 不过实际利用效果受各种因素限制 比如如果对方提前把能量全收了你就看不见了
(以前用过这个方法猜测了一个没给我开放步数权限的朋友的一天运动量 别问我为啥)
如果对隐私实在在意的 可以在支付宝蚂蚁森林设置里关掉“向好友展示能量球数值”

2月14日 #329 补充 #279

如果有人想知道 取证人员都能提取出什么数据的话 可以看一下这个 虽然不是真实环境
https://mp.weixin.qq.com/s/Mjuv81xJ1SVc0Pte-0eHQA

搜了一下发现这家搞的有意思的东西还挺多的:

推销自己的工具,可以实现“三星、华为、OPPO、VIVO、小米、魅族、锤子、美图、360、努比亚、金立、乐视、海信、朵唯等国内外300余个品牌、数千款机型的锁屏密码破解与镜像获取。针对安卓2.0至10.0均有专业的锁屏密码绕过,权限获取(ROOT)方案、手机全盘加密及文件加密的密码绕过方案”,且不会导致“数据丢失或触发恢复出厂”
https://mp.weixin.qq.com/s/l2ciZrOBc8HMGu1hSgZ6DQ
(这里有提到华为 FDE 机型提到权限之后可以直接导出全部数据,这个我之前也做过一点分析,可以看 https://t.me/CanyieChannel/83

OPPO 手机恢复出厂后残留数据取证
https://mp.weixin.qq.com/s/hJVbCmsFkWlLLtmbJGbJBw

爆破华为隐私空间密码
https://mp.weixin.qq.com/s/bs7nMsiWw-F753kPUxOfWg

其他厂商活也挺多的:

取证厂商五五安科表示自己支持“OPPO、VIVO FBE 机型绕过屏幕锁定密码提取文件系统”,“搭载部分芯片的 FBE 设备计算锁屏密码”(虽然这个在群里面发过但不妨再发一次)
https://mp.weixin.qq.com/s/XUpeMM7cJO7GVN0uEhNCIA

2月16日 #331 什么时候知道自己老了

上网冲浪的时候无意间刷到了初中时候的自己原创的笑话

2月21日 #332 回复 #331

老个毛线 医生让住院都让住儿少科

2月23日 #333 在 ART 上根据 Class 对象获取所属文件路径

1
2
3
4
5
6
7
8
9
10
11
12
13
public static String getFileLocation(Class<?> cls) {
        try {
            Class<?> DexCache = Class.forName("java.lang.DexCache");
            Field dexCache = Class.class.getDeclaredField("dexCache");
            dexCache.setAccessible(true);
            @SuppressLint("BlockedPrivateApi")
            Field location = DexCache.getDeclaredField("location");
            location.setAccessible(true);
            return (String) location.get(dexCache.get(cls));
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException(e);
        }
    }

需要关闭 hidden api 限制
只在 Android 12 上测试过

3月15日 #337 或许在未来会有这么一天

清晨八点多,你从睡梦中自然醒来。
这一夜你睡得很好,没有在三四点的时候睁眼望着天花板,也没有往常早晨醒来时的疲惫感。
昨晚刚下了一场雨,植物的叶片还挂着水滴,清晨的阳光从窗外透进来,照得你浑身暖洋洋的。你知道,冬天已经过去了。
身体传来的不是躯体化带来的疼痛或疲惫,而是少见的饥饿感。
洗漱完出门,早餐店的叔叔阿姨早已经开始了一天的忙碌。你点了往常最爱吃的肉包,鲜嫩的肉汁在你嘴里绽放。你好久好久没有细细地品味过你最爱吃的东西了。
眼前的世界是真实的,笼罩在眼前似有似无的黑幕已经消散,灵魂重新匹配身体,而身体又实实在在地触摸着世界,路边的红绿灯再也不会在视野里突然消失。
你看见一片还带着露珠的叶子,翠绿的颜色,水珠反射着太阳的光芒。你感觉它焕发着生命的气息。你喜欢它。你可以没来由地喜欢一件事物。
你突然觉得其实有些事情也没有那么难做。好像有好多事情一直放在 TODO list 里,打扫房间、跟许久不见的朋友一起逛街,或是去听曾经很喜欢的歌手的演唱会。有空可以清理一下 TODO 了,你想。
从这一天开始,你内心的刑期悄然结束了。夜晚,你不再辗转反侧,也不会凌晨三点醒来。你开始对事物产生兴趣,重新有了自己喜欢的东西。你慢慢停掉了药物。不知道持续了多少时间的冬天自己过去了,春天自己会来。

3月25日 #338 爬虫

转发自 https://t.me/cxplayworld/4481
如果你用 Telegram, 以防你不知道有人已经索引了所有人在公开群组和频道的发言记录了.
打开机器人选择语言后发送 /me 命令查询, 选择菜单里的 Groups 或者 Channels 就能看到喽.

4月28日 #339 #340 Clash Verge Rev 本地提权漏洞

转发自 https://t.me/qingmingjian123/295

https://github.com/bron1e/clash-verge-rev-privilege-escalation-poc

Clash Verge Rev默认强制安装系统服务(Clash Verge Service),并通过未授权 HTTP API /start_clash 暴露关键功能,允许本地用户提交任意 bin_path 参数,直接传递给服务进程执行,导致本地提权

影响范围:Clash Verge Rev<=v2.2.3(Windows/Linux/MacOS)

FlClash 似乎也受影响

https://github.com/chen08209/FlClash/issues/1131

6月6日 #343 假冒手机

据 Securelist 报道,部分电商平台上存在假冒手机,其固件内置木马,可执行各种针对性的复杂攻击,包括但不限于盗取各大平台(Telegram、Instagram、Line、Skype、Tiktok、Facebook 等)账号、使用受害者 WhatsApp 账号发送任意消息、发送任意短信、将剪贴板内加密货币钱包地址替换为攻击者的地址等。

原文链接:https://securelist.com/triada-trojan-modules-analysis/116380/
中文翻译:https://mp.weixin.qq.com/s/itrYPTDKwoGBQPFF4_idHA

那就有个很好玩的事情了,这些假机子能过 play integrity 认证吗?

6月7日 #344 全自动取证

https://mp.weixin.qq.com/s/gxTkcmDtyFeZ42XkcpTeJg

AI 技术驱动的智能取证机器人,宣传称其可自动分析聊天记录并可自动标记敏感信息、智能统计涉案金额、串联证据链条等。
已于 6 月 5 日在 BCS2025 (北京网络安全大会)网络犯罪治理论坛上首发,预计 7 月底全面上线。

6月12日 #345 USB 漏洞保护

为什么每次这个频道发取证相关的东西都会有一堆我看不见的转发…?关注这频道的都什么成分(

既然大家喜欢的话那就再来一个:现在的取证技术很多是利用 USB 相关的漏洞解锁设备。Android 12 开始其实支持从软件层面禁用 USB 数据信号传输(需要 HAL 支持),但是系统里并没有开关给你操作,只有手动进入 lockdown mode 或在 Advanced Protection mode 启用时锁定设备 1 时会阻断 USB 连接。
相关 API 其实是已经暴露出来了,可供设备管理应用使用 2。可以下载安装 TestDPC 然后使用 adb 将其配置为 device owner 或 organization-owned profile owner,之后就可以在 TestDPC 里找到 Enable USB data signaling 来手动开关它。
警告:在部分机型上激活设备管理员可能存在风险 3,禁用 USB 后无法使用基于 USB 的 adb,若设备后续变砖可能严重影响救砖能力,请谨慎考虑。

6月17日 #348 补充 #250

疑似 Google 大手子开始云控发力,通过 Google Play 服务远程开关设备配置,手上的 Pixel 6a Android 16 已经是默认开启状态,另一台跑着 Android 14 的一加机子也是已经打开但不知道为何 global_kill_switch 是 true,实测 SDK 相关功能是能正常用的,不知道为啥

命令:

1
2
3
adb shell device_config get adservices disable_sdk_sandbox
adb shell device_config get adservices global_kill_switch
adb shell dumpsys sdk_sandbox

欢迎大家把自己设备的结果晒在评论区

6月22日 #350 #351 #352 #353 #354 #355 #356 #357 #358 #359

时隔四年再次来到东莞,感谢华为邀请,身份从流水线工人变成受邀嘉宾,终于能进来这个​曾经没机会来的地方看看
华为园区真的很漂亮,所有照片都是原图直出,既饱眼福又饱口福
这两天东莞本来下雨的,我们一到松山湖雨就停了,连续两天都是这样,感谢天公作美让我有机会看看美景
​就是可惜华为小火车没坐到,一查才知道在溪流背坡村,三丫坡没有,看看明年能不能补上这个遗憾
哦对了 还被人线下认出来了
坐在椅子上旁边突然有人问“哎你是残页吧 我关注了你频道看见过你照片”
下次再也不敢发照片了

7月10日 #360 #361 TapTrap

https://taptrap.click/
利用 activity 跳转动画劫持,CVE-2021-0339 再现,只不过这次是用透明度

https://nvd.nist.gov/vuln/detail/cve-2021-0339
https://android.googlesource.com/platform/frameworks/base/+/36bcc77337814d4d36e2b10eb062ac417d91611e

7月13日 #362 补充 #362

其实我们之前也报过一个手法可以让用户误触某些按钮,之前搞了个直接恢复出厂设置清除用户所有数据的 poc 当漏洞报上去,经过了半年的审核给我 wontfix 了。
如果有人感兴趣的话我可以发出来给大家看看(

7月24日 #363 没事干开始共情塑料

需要几百年才能降解,做的厚一点能重复利用很多次,但是大部分时候被使用一次就被当成垃圾扔掉了
古代人用竹子、梅花啥的象征坚韧不屈的精神,我觉得是因为古代没有塑料,否则应该会出现一堆《咏塑料》之类的诗歌
这么坚韧的东西却只需要几分钱几毛钱就能买到,大部分是作为一次性容器,用完就扔掉,说不定这些人类自己都不吃的东西还会进海洋里被某个幸运海洋生物吞进肚子里,然后还要因为人类的行为背上一个“污染环境”“毒害海洋生物”的骂名,真的好冤好可怜
所以我都倾向于使用能重复利用的东西,比如选择到店吃而不是点外卖,签器官捐献+遗体捐献志愿,用过的塑料袋留着拿来套垃圾桶之类的,真的完全用不上的快递箱之类的也完全舍不得扔

7月31日 #366 补充 #239

CVE-2025-31229:密码可能会被大声读出
https://support.apple.com/en-us/124147

8月3日 #370 #371 Android 诊断模式

这个功能附带一个 UI ,可以用

1
adb shell am start -n com.android.devicediagnostics/.MainActivity

拉起来,然后使用另一台设备通过二维码完成验证。有密钥认证担保,这才是它应该有的使用场景嘛
文档 https://source.android.com/docs/core/perf/trade-in-mode?hl=zh-cn#gather

另外这个页面还支持检测屏幕坏点和触控,把工厂流水线的 MMI app 功能复制过来了?这一套组合拳是为了方便转转上门收手机吗

“搭载 Android 16(或更高版本)的设备在启动时会进入以旧换新模式。在这种模式下,您可以使用 adb 连接到设备,并可以使用命令获取设备相关信息。”

很有意思的功能,换句话说,Android 16 设备恢复出厂设置后,在没有网络连接且未完成设置向导时,会允许 adb 连接并运行受限命令。注意这种情况不需要打开开发者选项,也不需要手动授权调试。

内部实现文档:https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/adb/docs/dev/adb_tradeinmode.md

两层机制保证安全:

  1. adbd 会降权至受限的 adbd_tradeinmode 域而非 adbd 域
  2. 使用正则表达式确保用户只能运行 adb shell tradeinmode 命令。源码: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/adb/daemon/tradeinmode.cpp;l=86

在该模式下,运行 adb shell tradeinmode evaluate 可以进入评估模式,正常使用所有 adb 命令。进入评估模式会创建 /metadata/tradeinmode/wipe 文件,下次开机时 init 看见有这个文件会将设备恢复出厂设置以避免任何东西残留。

9月4日 #375 补充 #250

Android 终于开始修复我之前发现的一批和 SDK Sandbox 这个有趣的新功能相关的漏洞,9 月补丁里修复了两个,在这里放补丁链接抛砖引玉,各位大佬有什么新的想法欢迎和我交流下,不吝赐教

CVE-2025-48524 https://android.googlesource.com/platform/packages/modules/Wifi/+/298745e0cb23cbef631aff1977b284155384bbf0

CVE-2025-48545 https://android.googlesource.com/platform/frameworks/base/+/66ac17909252c80b0edf7f4ae282bce4579410ad

10月8日 #384 ACE

CVE-2024-34740,本地 app 提权至 system_server
https://github.com/michalbednarski/AbxOverflow

CVE-2024-49746,本地 app 提权至 system uid
https://github.com/michalbednarski/ThisSeemsWrong

CVE-2025-22441,本地 app 提权至 SystemUI(由于 PoC 使用了 WebView 因此无法注入进 Settings 等 system uid app)
https://github.com/michalbednarski/ResourcePoison

10月12日 #387 补充 #362 一个打地鼠游戏如何让你被自愿丢失所有数据

误触能造成的危害远不止授权设备管理员,如果误触授权无障碍,恶意应用可以立即显示悬浮窗遮盖界面、读取显示的内容并代替用户操作设备;授权录屏权限则可以立即开始录屏并启动各种聊天软件,首页显示的对话就会被包含进录屏里。
用户当然可以立即关机并且重启到安全模式来卸载恶意应用,但是已经造成的危害无法清除。人是不可能比机器快的。我给 Google 提过这个问题,Android ID 389091354,似乎 ECM 已经开始限制 device admin 授权,但不清楚他们是否会继续采取措施(我觉得合理的解决方案是打开页面时禁用激活按钮 1 秒确保用户看见了警告不会手抖再允许激活),有权限查看的人可以点进去看看

10月14日 #388 安卓像素劫持攻击威胁双重认证安全

转发自 https://t.me/zaihuapd/36486

安全研究人员发现一种名为“像素劫持(Pixnapping)”的新型安卓攻击手段。该攻击通过恶意应用调用系统API,无需特殊权限即可读取其他应用显示的像素数据。攻击利用GPU侧信道技术,通过测量像素渲染时间推断屏幕内容,可窃取双重认证码、聊天消息等隐私信息。研究团队在谷歌Pixel 6至9和三星Galaxy S25设备上演示成功,部分设备攻击成功率超过50%。

谷歌已在9月安全公告中发布初步补丁,但研究人员称攻击改良版仍有效,预计12月将有额外修复。攻击每秒仅泄露0.6至2.1个像素,但足以威胁短期有效的2FA认证码。目前未发现实际利用案例,但突显了安卓安全机制的局限性。该技术基于2013年浏览器攻击,显示侧信道风险的持续性。

https://www.pixnapping.com/

10月18日 #393 补充 #250

SDK Sandbox 被弃用。解决了“提权沙盒”这个问题本身

10月24日 #395 补充 #387


一直知道误触是黑产百试不爽的利用手段,但是事实证明各种我们没想到的攻击手段已经被他们玩出花了。
伪装成屏幕上的头发丝骗用户去碰,我着实没想到。

10月26日 #406 GEEKCON

我比较关心的议题 1:快应用
快应用只需要点一下就能用,通过恶意 deeplink 可以让灰黑产点一下就把用户的手机变成广告蛊场,用户手忙脚乱之下只会打开更多一连串的难以关闭的快应用
议题 2:透明桌面小组件(app widget)
恶意应用诱骗用户添加一开始看似正常的小组件,随后将其设置成透明以防止被用户发现,并利用小组件的特性实现保活、后台弹广告等一系列灰黑产行为
我之前也注意到过小组件的滥用潜力,也报过一些相关漏洞(CVE-2024-43762),但对于 Google 认为没有“安全影响”的问题无可奈何。只能希望未来的 Android 版本能做出进一步限制。
现场视频: https://www.bilibili.com/video/BV19DxFzBE4L
议题 3:多个厂商的 TA 实现不当,存在多种漏洞将 authtoken 暴露给不可信方,导致可被暴力破解手机 PIN 码
同时演讲者演示了在一台刚开机还未输入过密码解锁(即处于所谓的 Before-First-Unlock,BFU 状态,data 分区还未解密)的设备上成功爆破 PIN 码
演讲者也提到这种攻击技术可能已在被取证公司利用

10月30日 #434 #438 一个或许从 2014 年隐藏到现在的 bug

2025年了,activity 生命周期 api 还能出问题,离谱,难道这个功能到现在都没有任何一个人用过吗?
https://android-review.googlesource.com/c/platform/frameworks/base/+/3826768

奖池还在累计
https://android-review.googlesource.com/c/platform/frameworks/base/+/3563923

11月16日 #440 被规则忘记的你

前段时间跑了三次某国际银行想要开一个内地账户,跑了三次被拒了三次。在我已经有该银行香港地区需要资产门槛才能开贵宾账户的情况下。理由是我是学生,建议等我毕业工作之后再尝试。我说我现在确实是学生,但我有自己的收入来源还纳税,现在已经能养活自己了,为什么非得等到有工作了才能开?如果我毕业之后是自由职业者怎么办?银行的回复把我气笑了:“那不是相当于您这个大学白读了吗,相信自己肯定能找到工作的”。

我只感觉这个世界是如此的荒唐,死的规则把我和所有规则考虑不到的活着的人拦在门外。

被规则忘记的不仅是我。回想起陪同残疾人出行时遇到的种种不便。很多城市进行基础建设的时候就完全没有考虑到残障人士的出行需求,比如早期修建的很多地铁的无障碍电梯是直接连通站厅非付费区和站台(付费区)的,为了防止逃票这些电梯平时是关闭的,有人需要的时候由同行人拿着行李过安检,拿着交通卡去闸机刷卡但不入闸,然后呼叫工作人员打开电梯,然后下到站台乘车。如果目的地也是采用这种设计就需要反着再来一遍。这算是历史遗留问题没办法解决可以理解,但是还有很多城市道路也难以让残障人士通过,比如盲道上的石墩子,只有楼梯可通行还没有扶手的道路,还有坡度大到只能给电瓶车通过的坡。我觉得这就是纯粹的态度问题了。以我带着一个需要使用助行器的人的体验而言,遇到楼梯时我需要拿着他的助行器,然后把我的手或者肩给他当扶手,让他上去之后再把助行器放到地上让他扶。对我们而言再平常不过的道路对他们而言可能是足以致命的陷阱。可能没有经历过这些的人很难理解残障人士的出行困境,高德地图在设置里可以开启无障碍模式(可能很少人知道这个功能,其实不止高德地图,像小红书也是专门适配过无障碍浏览的,为所有付出行动优化体验的软件点赞),然后随便点个附近的目标点尝试导航,看看会收获多少次“无法避开台阶路段”的提示。所以很多时候我们宁愿选择打车。中国有约 8500 万残疾人,除去外表上看不出来的残疾类型,为什么我们日常很少见到残疾人?我想这就是一大原因。媒体报道的残疾人一般都被冠以『坚强』『乐观』这些形容词,但抛开这些与性格相关的形容词,他们不过是受限的普通人。『坚强』是我的优点,但是『不坚强』也不该成为被排除的理由。

这种困境不仅仅局限于残障人士。年老,意外受伤,推婴儿车,甚至只是拉着大行李箱,都会让一个『普通人』对无障碍设施的需求瞬间变得迫切。

多数人的幸福支撑社会的繁荣,少数人的境遇映照社会的温度。虽然我很讨厌『少数群体』这几个字,但是我拙劣的语文水平确实想不出其他的话了。很多人讨厌『少数群体』这个表述是持着“别人都行为什么就你不行”这种想法想让所有人都强行融进自己的框架里,我认为这种忽视个体差异和客观需求的想法无异于把需要时间破茧成蝶才能飞的毛毛虫放进本来就会飞的鸟群里。

最后附上去年缴税的证明,证明下我有经济能力。

11月18日 #443 #444

转发自 https://t.me/androidMalware/2734
One of top-selling digital picture frames from Amazon’s between March and April 2025 comes:

原来世界上还有数码相框这种设备

11月20日 #447 不要删掉 CompanionDeviceManager

经常在各种地方看见精简系统软件的教程或者“去除了系统中不必要组件的精简版 ROM”,这边提醒一下,我个人是不建议随意动系统组件的,即使它看起来完全没用
举个例子,com.android.companiondevicemanager 这个 app 用来在配对配套设备(如手表)的时候弹对话框,似乎对没有外设的用户没用,但在安卓 12 及以上移除这个 app 会造成可以导致电话、联系人、短信、日历、读取通知、控制通话等多个敏感权限被自动授权给恶意软件的严重安全漏洞
有些“教程”的理由是移除一些非必须的 app 可以减少攻击面,让其中可能存在的安全漏洞变得无法被利用。我是觉得不如买手机的时候选个代码质量好然后安全更新频繁的厂商
话说有没有人知道是否真的有什么精简版 ROM 把 com.android.companiondevicemanager 删了?
用户想自测的可以跑一下

1
pm path com.android.companiondevicemanager

看看有没有输出

12月22日 #463 The worst programming language of all time

https://www.youtube.com/watch?v=7fGB-hjc2Gc

bilibili: https://www.bilibili.com/video/BV1xYUtB5Evr

2026

1月13日 #465 来点 blackhat

感谢群友让我蹭名字
https://blackhat.com/asia-26/briefings/schedule/speakers.html#songzhou-shi-51645

1月17日 #466 打假

关于最近流传的 Android 16 0 click RCE + 反弹 root shell 的视频
乍一看很唬人 仔细看越看越假 严重怀疑打印出来的内容是 AI 生成的
我把几个我看见的不合理的点列出来

  1. 从视频里的崩溃日志看,崩溃进程名字叫 com.android.mms,而 Android 上多媒体相关的代码一般被隔离在另一个进程如 mediaextractor, com.android.mms 的 maps 里也不可能有 libmediaextractor
  2. 虽然部分崩溃栈被挡住,依稀能辨认出来被挡住的 ****ediaextractor.so 应为 libmediaextractor.so,但不存在名为 libmediaextractor.so 的文件,也不存在任何名字结尾跟视频中崩溃内容相符的库,正确的名字应该是 libmediaextractorservice.so
  3. 崩溃栈里有 MediaExtractor.setDataSource 说明是请求解析媒体文件的进程崩溃,崩溃点在一个什么什么 parseChunk 里,不合逻辑,这种函数就算崩了也应该崩在 mediaextractor 进程里
  4. 我尝试根据崩溃调用栈还原原本符号,但大多数都无法在 AOSP 里找到符合的
  5. 打出来的崩溃栈 pc 一堆 1234 5678 abcd 这种顺得像乱写的东西,出现一个两个还有可能,怎么可能同时出现这么多
  6. 里面还有一个什么疑似 MediaPlayerService::Client::decode 的东西,先不说这个符号根本找不到
    就算是三星自己加的,MediaPlayerService::Client 里面也应该只有跟远端服务交互的逻辑,不应该能被不可信数据弄崩
  7. 视频里面拿到 root 之后 ls / 没有 data_mirror debug_ramdisk 和 init.environ.rc

1月18日 #467 真正的 exp!


如果一个视频就能卖四百万刀,那我也来发一个演示,拿 root shell + 禁用 selinux,免费公开 exp 源码,保证真实,可以自己跑一遍确认真的拿到了 root shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/system/bin/sh

echo "*******************"
echo "*  by @canyie  *"
echo "*******************"

echo "[+] Exploiting ...."
sleep 1
echo "[+] Disable addr_limit ...."
sleep 1
echo "[+] Override task_struct ...."
sleep 1
echo "[+] Success!"
echo "[+] Disable SELinux..."
su -c setenforce 0
sleep 2
echo "[+] Spawning root shell..."
exec /debug_ramdisk/su -Z u:r:untrusted_app_30:s0:c81,c257,c512,c768

需要 magisk

1月20日 #469 #470 补充 #447

效果:无用户交互自动授权读写联系人、短信、日程、通话记录和语音信箱,拨打电话或接听来电,操纵通话设置,发送通知,读取并操作其他应用发送的通知,控制附近设备,录制声音,读取设备唯一标识符,和绕过后台执行限制。
录屏:https://github.com/canyie/CVE-2024-23700/blob/main/screen-20260120-233400-1768923180588.mp4
漏洞评级(Severity):Critical 最高/严重

PoC 代码:https://github.com/canyie/CVE-2024-23700

下载编译好的 apk:https://github.com/canyie/CVE-2024-23700/releases

和视频里的版本相比优化了几个细节:
视频里尝试提升权限时会有一闪而过的白屏,比较容易被注意到,进行了优化,让利用更隐蔽
支持了静默自动获取录音权限,需要 Android 14+
添加了一个拨号按钮,提权之后可以无用户交互拨打电话,确保提到的权限真的能用

提供的测试 app 在绝大部分设备上都无法安装。如果在你的设备上安装失败,是正常的,说明它目前不受影响(但是你后面又刷了其他 ROM 或者开了什么模块就不一定了)。

1月30日 #473 补充 #261

后续:去医院治了胃病之后好了 现在变成吃多少都吃不饱了
跟我有一样体验(吃什么都嫌多)的人可以去医院测个幽门螺旋杆菌

1月30日 #474 Exploiting MediaTek’s Download Agent

https://blog.r0rt1z2.com/posts/exploiting-mediatek-datwo/

1月31日 #475 kotlin 是一种折磨

分析/审计 kotlin 代码是一种折磨 一种灾难
就算我哪怕知道把它分析完就能拿 $7000 USD 的漏洞奖金 我也完全提不起一丝看它的兴趣
一个语言能设计成这种毫无设计哲学全是语法糖推崇写以后不用 review 的一次性代码的样子也是一般人做不到的
我很想知道让写出 kotlin 一次性代码的人过几年让他自己读能不能读懂
我丝毫不怀疑把 kotlin 代码编译完再反编译成 java 都比读原代码有可读性
我还记得 Android 7.0 的时候 Google 宣布引进 Kotlin 作为正统 Android 开发语言时我的激动之情 那时我还在用手机写代码 期望它能解决 java 的一系列问题
现在我拒绝 kotlin 出现在我项目里的任何地方 包括依赖和编译脚本 当年期望有多大现在失望就有多大

2月2日 The End

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)

Android Runtime Resources Overlay 加载时序分析

作者 残页
2025年8月24日 22:30

Fabricated Runtime Resources Overlay (FRRO) 是 Android 12 引入的一项新功能,它让开发者可以用代码或 shell 命令的方式动态操作 Runtime Resources Overlay(RRO) 而不需要像以前一样必须创建一个单独的 overlay app。然而四年后,网络上关于 FRRO 的文章依然少得可怜,所以在这里记录一次我调试 FRRO 问题的过程。

推荐阅读:

起因:FRRO 失效?

在 PackageManagerService 启动的时候会调用如下伪代码获取某个 string:

1
2
3
public static String getConfig(Context systemContext) {
return systemContext.getResources().getString(R.string.config_xxxxxx);
}

frameworks/base/core/res/res/values/config.xml 内有如下配置:

1
<string name="config_xxxxxx"></string>

系统内部有一个包名为 android.auto_generated_rro_product__ 的 RRO app 对该 string 进行了 overlay:

1
<string name="config_xxxxxx">From RRO</string>

现在我想进行动态调试,快速修改这个 string 的值并观察系统反应。以往我们需要修改 RRO 中的值并重新编译系统,现在让我们试试 FRRO,root 下使用命令 cmd overlay fabricate --target android --name test android:string/config_xxxxxx test 创建一个 FRRO 然后 cmd overlay enable 一下,然后重启系统,看看反应!
嗯,一点反应都没有……在预期里,应该是哪里没做对,俗话说遇事不决就重启,换个重启方式试试?事实证明,无论是杀掉 zygote 触发软重启,还是使用 rebootsvc power reboot 发起真正的重启都没有任何改变。
那跑命令看一下 overlay 有没有生效?还好命令行也是能查询资源值的:cmd overlay lookup android android:string/config_xxxxxx。看输出结果确实已经被替换了。
脑袋要烧了,写代码验证一下是不是真的被替换了吧:

1
2
Resources res = Resouces.getSystem();
print(res.getString(res.getIdentifier("config_xxxxxx", "string", "android")));

神奇的事情发生了,代码输出的结果是来自 RRO 的值,FRRO 没有替换成功!为什么呢?
会不会是出现某种权限问题,导致 app 进程无法打开 FRRO 所需的文件,所以 FRRO 没有生效呢?验证这个结论很简单,cat /proc/$(pidof 进程名)/maps | grep frro 看一下有没有我们自己的 FRRO 路径就好了。经过确认,是有的,这个结论不攻自破。
那会不会是系统原本自带的 RRO 比我们的 FRRO 优先级更高所以被优先使用了呢?cmd overlay dump 看一下就能知道新建的 FRRO 是启用状态,且优先级已经是最高的 2147483647。反之如果真的是这样,那么 cmd overlay lookup 也不会返回我们的值。又是一条死路。
这个时候我发现一个更神奇的现象:把上面代码的 Resouces.getSystem() 换成 context.getResources(),结果就正常了。
是什么导致了这个差异?它是 FRRO 对 PackageManagerService 内代码不生效的原因吗?想搞清楚这个问题,只能钻一遍 RRO 的加载流程了……

不可变 RRO 在 Zygote 中的预加载

想搞清楚 Resources.getSystem()context.getResources() 的差别,首先让我们搞清楚它们都是从哪来的。点开 Resources.getSystem() 看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 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.
*/
public static 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
private Resources() {
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());
}

注意 AssetManager.getSystem()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* Return a global shared asset manager that provides access to only
* system assets (no application assets).
* @hide
*/
@UnsupportedAppUsage
public static 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
public static void createSystemAssetsInZygoteLocked(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) {
throw new IllegalStateException("Failed to create system AssetManager", e);
}
}

我们在这里首次看见了 overlay 字眼,OverlayConfig 这个类看名字就是解析 overlay 相关配置文件的,看一下它怎么做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
@VisibleForTesting
public OverlayConfig(@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);

ArrayMap<Integer, List<String>> activeApexesPerPartition = getActiveApexes(partitions);

final Map<String, ParsedOverlayInfo> packageManagerOverlayInfos =
packageProvider == null ? null : getOverlayPackageInfos(packageProvider);

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));
}
}

partitionConfigs.sort(sStaticOverlayComparator);
overlays.addAll(partitionConfigs);
}

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).
final boolean 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);
}

invocation.overlayPaths.add(overlay.parsedConfig.parsedInfo.path.getAbsolutePath());
}
return idmapInvocations;
}

首先在构造函数里扫描了 /apex 及其他系统分区内所有的 apk 文件,记录下所有的 overlay app 然后将所有在启用状态、不可变且目标是 android 的 overlay app 返回给 zygote 预加载。具体会扫描的分区如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 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).
*/
private static final 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 */)));

扫描逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 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.
*/
public void scanDir(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;
}

mParsedOverlayInfos.put(info.packageName, info);
}
}

以上逻辑都在 zygote 里完成,存在一个专门的 Resources.preloadResources() 函数用来预加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 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
public static void preloadResources() {
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.");

startTime = SystemClock.uptimeMillis();
ar = sysRes.obtainTypedArray(
com.android.internal.R.array.preloaded_color_state_lists);
numberOfEntries = preloadColorStateLists(sysRes, ar);
ar.recycle();
Log.i(TAG, "...preloaded " + numberOfEntries + " resources in "
+ (SystemClock.uptimeMillis() - startTime) + "ms.");
}
sysRes.finishPreloading();
} catch (RuntimeException e) {
Log.w(TAG, "Failure preloading resources", e);
}
}

而 FRRO 位于 /data/resource/cache 下,以 .frro 为后缀,显然扫描结果不可能包含任何一个 FRRO,所以不会被 zygote 预加载也不会影响 Resources.getSystem()。而虽然 android.auto_generated_rro_product__ 所属的 /product/overlay 下没有 config.xml 文件,但它被配置为静态 RRO:

1
2
3
4
<overlay
android:priority="1"
android:targetPackage="android"
android:isStatic="true"/>

静态 RRO 默认就是启用且不可变的,所以被 zygote 预加载了。通过 cat /proc/$(pidof zygote)/maps | grep idmap 也可以确认输出结果含有 android.auto_generated_rro_product__ 的 idmap 但没有 FRRO 的。

可变 RRO 的加载

上面这么一大串生效的前提是 1. RRO 不是 FRRO;2. RRO 不可变且处于启用状态。我们还是没有找到 FRRO 的处理逻辑,不知道这段代码藏在哪处。我们用代码或者 shell 命令创建 FRRO 的时候实际上是在和 OverlayManagerService 交互,看一下它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@NonNull
Set<UserPackage> registerFabricatedOverlay(
@NonNull final FabricatedOverlayInternal overlay)
throws OperationFailedException {
if (FrameworkParsingPackageUtils.validateName(overlay.overlayName,
false /* requireSeparator */, true /* requireFilename */) != null) {
throw new OperationFailedException(
"overlay name can only consist of alphanumeric characters, '_', and '.'");
}

final FabricatedOverlayInfo info = mIdmapManager.createFabricatedOverlay(overlay);
if (info == null) {
throw new OperationFailedException("failed to create fabricated overlay");
}

final Set<UserPackage> updatedTargets = new ArraySet<>();
for (int userId : mSettings.getUsers()) {
updatedTargets.addAll(registerFabricatedOverlay(info, userId));
}
return updatedTargets;
}

调用了 Idmap2Service 来实际创建 FRRO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
constexpr std::string_view kIdmapCacheDir = "/data/resource-cache";

Status Idmap2Service::createFabricatedOverlay(
const os::FabricatedOverlayInternal& overlay,
std::optional<os::FabricatedOverlayInfo>* _aidl_return) {
idmap2::FabricatedOverlay::Builder builder(overlay.packageName, overlay.overlayName,
overlay.targetPackageName);
if (!overlay.targetOverlayable.empty()) {
builder.SetOverlayable(overlay.targetOverlayable);
}

for (const auto& res : overlay.entries) {
if (res.dataType == Res_value::TYPE_STRING) {
builder.SetResourceValue(res.resourceName, res.dataType, res.stringData.value(),
res.configuration.value_or(std::string()));
} else if (res.binaryData.has_value()) {
builder.SetResourceValue(res.resourceName, res.binaryData->get(),
res.binaryDataOffset, res.binaryDataSize,
res.configuration.value_or(std::string()),
res.isNinePatch);
} else {
builder.SetResourceValue(res.resourceName, res.dataType, res.data,
res.configuration.value_or(std::string()));
}
}

// 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 {
constexpr size_t kSuffixLength = 4;
const std::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.
const size_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);

const uid_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));
}

const auto 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);
}

os::FabricatedOverlayInfo out_info;
out_info.packageName = overlay.packageName;
out_info.overlayName = overlay.overlayName;
out_info.targetPackageName = overlay.targetPackageName;
out_info.targetOverlayable = overlay.targetOverlayable;
out_info.path = path;
*_aidl_return = out_info;
return ok();
}

根据调用者的包名和 overlay 的名字,在 /data/resource-cache 下随机生成 FRRO 文件。猜测应该会有个地方列出这个文件夹里所有的 FRRO 然后逐个加载,搜索发现 OverlayManagerService 启动时会调用 OverlayManagerServiceImpl.updateOverlaysForUser()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/**
* 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(final int 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;
}

var packageName = packageState.getPackageName();
try {
CollectionUtils.addAll(updatedTargets,
updatePackageOverlays(pkg, newUserId, 0 /* flags */));

// 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);
final int userOverlayTargetCount = userOverlays.size();
for (int i = 0; i < userOverlayTargetCount; i++) {
final List<OverlayInfo> overlayList = userOverlays.valueAt(i);
final int 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);
}
}

cleanStaleResourceCache();
return updatedTargets;
}

它收集了所有 overlay app 然后调用 Idmap2Service 遍历 /data/resource-cache 收集所有 FRRO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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_);

constexpr size_t kMaxEntryCount = 100;
if (!frro_iter_.has_value()) {
return error("no active frro iterator");
} else if (frro_iter_id_ != iteratorId) {
return error("incorrect iterator id in a call to next");
}

size_t count = 0;
auto& entry_iter = *frro_iter_;
auto entry_iter_end = end(*frro_iter_);
for (; entry_iter != entry_iter_end && count < kMaxEntryCount; ++entry_iter) {
auto& entry = *entry_iter;
if (!entry.is_regular_file() || !android::IsFabricatedOverlay(entry.path().native())) {
continue;
}

const auto overlay = FabricatedOverlayContainer::FromPath(entry.path().native());
if (!overlay) {
LOG(WARNING) << "Failed to open '" << entry.path() << "': " << overlay.GetErrorMessage();
continue;
}

auto info = (*overlay)->GetManifestInfo();
os::FabricatedOverlayInfo out_info;
out_info.packageName = std::move(info.package_name);
out_info.overlayName = std::move(info.name);
out_info.targetPackageName = std::move(info.target_package);
out_info.targetOverlayable = std::move(info.target_name);
out_info.path = entry.path();
_aidl_return->emplace_back(std::move(out_info));
count++;
}
return ok();
}

计算完所有 RRO 的状态后,通知 PackageManagerService 更新信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* Updates the target packages' set of enabled overlays in PackageManager.
* @return the package names of affected targets (a superset of
* targetPackageNames: the target themselves and shared libraries)
*/
@NonNull
private List<String> updatePackageManagerLocked(@NonNull Collection<String> targetPackageNames,
final int userId) {
try {
traceBegin(TRACE_TAG_RRO, "OMS#updatePackageManagerLocked " + targetPackageNames);
if (DEBUG) {
Slog.d(TAG, "Update package manager about changed overlays");
}
final PackageManagerInternal pm =
LocalServices.getService(PackageManagerInternal.class);
final boolean updateFrameworkRes = targetPackageNames.contains("android");
if (updateFrameworkRes) {
targetPackageNames = pm.getTargetPackageNames(userId);
}

final ArrayMap<String, OverlayPaths> pendingChanges =
new ArrayMap<>(targetPackageNames.size());
synchronized (mLock) {
final OverlayPaths frameworkOverlays =
mImpl.getEnabledOverlayPaths("android", userId, false);
for (final String targetPackageName : targetPackageNames) {
final var list = new OverlayPaths.Builder(frameworkOverlays);
if (!"android".equals(targetPackageName)) {
list.addAll(mImpl.getEnabledOverlayPaths(targetPackageName, userId, true));
}
pendingChanges.put(targetPackageName, list.build());
}
}

final HashSet<String> updatedPackages = new HashSet<>();
final HashSet<String> invalidPackages = new HashSet<>();
pm.setEnabledOverlayPackages(userId, pendingChanges, updatedPackages, invalidPackages);
return new ArrayList<>(updatedPackages);
} finally {
traceEnd(TRACE_TAG_RRO);
}
}

PackageManagerService 更新受影响包的状态,最重要的是 ApplicationInfo 内的两个字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 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;

对应进程启动时会读取它们,创建 Resources 对象的时候会用到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@UnsupportedAppUsage
public Resources getResources() {
if (mResources == null) {
final String[] splitPaths;
try {
splitPaths = getSplitPaths(null);
} catch (NameNotFoundException e) {
// This should never fail.
throw new AssertionError("null split not found");
}

if (Process.myUid() == mApplicationInfo.uid) {
ResourcesManager.getInstance().initializeApplicationPaths(mResDir, splitPaths);
}

mResources = ResourcesManager.getInstance().getResources(null, mResDir,
splitPaths, mLegacyOverlayDirs, mOverlayPaths,
mApplicationInfo.sharedLibraryFiles, null, null, getCompatibilityInfo(),
getClassLoader(), null);
}
return mResources;
}

那如果受影响的进程已经启动了(比如是 system_server 自己)又会是什么情况呢?回到 PackageManagerService#setEnabledOverlayPackages(),里面有一段特殊处理,修改了 android 包对应的 ApplicationInfo:

1
2
3
4
5
6
7
8
9
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);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private void maybeUpdateSystemOverlays(String targetPackageName, OverlayPaths newOverlayPaths) {
if (!mResolverReplaced) {
if (targetPackageName.equals("android")) {
if (newOverlayPaths == null) {
mPlatformPackageOverlayPaths = null;
mPlatformPackageOverlayResourceDirs = null;
} else {
mPlatformPackageOverlayPaths = newOverlayPaths.getOverlayPaths().toArray(
new String[0]);
mPlatformPackageOverlayResourceDirs = newOverlayPaths.getResourceDirs().toArray(
new String[0]);
}
applyUpdatedSystemOverlayPaths();
}
} else {
if (targetPackageName.equals(mResolveActivity.applicationInfo.packageName)) {
if (newOverlayPaths == null) {
mReplacedResolverPackageOverlayPaths = null;
mReplacedResolverPackageOverlayResourceDirs = null;
} else {
mReplacedResolverPackageOverlayPaths =
newOverlayPaths.getOverlayPaths().toArray(new String[0]);
mReplacedResolverPackageOverlayResourceDirs =
newOverlayPaths.getResourceDirs().toArray(new String[0]);
}
applyUpdatedSystemOverlayPaths();
}
}
}

private void applyUpdatedSystemOverlayPaths() {
if (mAndroidApplication == null) {
Slog.i(TAG, "Skipped the AndroidApplication overlay paths update - no app yet");
} else {
mAndroidApplication.overlayPaths = mPlatformPackageOverlayPaths;
mAndroidApplication.resourceDirs = mPlatformPackageOverlayResourceDirs;
}
if (mResolverReplaced) {
mResolveActivity.applicationInfo.overlayPaths = mReplacedResolverPackageOverlayPaths;
mResolveActivity.applicationInfo.resourceDirs =
mReplacedResolverPackageOverlayResourceDirs;
}
}

通知完 PackageManagerService 后,OverlayManagerService 会通知 ActivityManagerService,由 ActivityManagerService 通知相关进程刷新 ApplicationInfo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@GuardedBy(anyOf = {"this", "mProcLock"})
private void updateApplicationInfoLOSP(@NonNull List<String> packagesToUpdate,
boolean updateFrameworkRes, int userId) {
if (updateFrameworkRes) {
ParsingPackageUtils.readConfigUseRoundIcon(null);
}

mProcessList.updateApplicationInfoLOSP(packagesToUpdate, userId, updateFrameworkRes);

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);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@GuardedBy(anyOf = {"mService", "mProcLock"})
void updateApplicationInfoLOSP(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;
}

if (userId != UserHandle.USER_ALL && app.userId != userId) {
continue;
}

app.getPkgList().forEachPackage(packageName -> {
if (updateFrameworkRes || packagesToUpdate.contains(packageName)) {
try {
final ApplicationInfo ai = applicationInfoByPackage.get(packageName);
if (ai != null) {
if (ai.packageName.equals(app.info.packageName)) {
app.info = ai;
app.getWindowProcessController().updateApplicationInfo(ai);
PlatformCompatCache.getInstance()
.onApplicationInfoChanged(ai);
}
app.getThread().scheduleApplicationInfoChanged(ai);
targetProcesses.add(app.getWindowProcessController());
}
} catch (RemoteException e) {
Slog.w(TAG, String.format("Failed to update %s ApplicationInfo for %s",
packageName, app));
}
}
});
}

mService.mActivityTaskManager.updateAssetConfiguration(targetProcesses, updateFrameworkRes);
}

进程收到消息后,刷新 ApplicationInfo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@VisibleForTesting(visibility = PACKAGE)
public void handleApplicationInfoChanged(@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) {
final var 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);
}
}

ResourcesImpl beforeImpl = getApplication().getResources().getImpl();

synchronized (mResourcesManager) {
// Update all affected Resources objects to use new ResourcesImpl
mResourcesManager.applyAllPendingAppInfoUpdates();
}

ResourcesImpl afterImpl = getApplication().getResources().getImpl();

if ((beforeImpl != afterImpl) && !Arrays.equals(beforeImpl.getAssets().getApkAssets(),
afterImpl.getAssets().getApkAssets())) {
List<String> beforeAssets = Arrays.asList(beforeImpl.getAssets().getApkPaths());
List<String> afterAssets = Arrays.asList(afterImpl.getAssets().getApkPaths());

List<String> onlyBefore = new ArrayList<>(beforeAssets);
onlyBefore.removeAll(afterAssets);
List<String> onlyAfter = new ArrayList<>(afterAssets);
onlyAfter.removeAll(beforeAssets);

Slog.i(TAG, "ApplicationInfo updating for " + ai.packageName + ", new timestamp: "
+ ai.createTimestamp + "\nassets removed: " + onlyBefore + "\nassets added: "
+ onlyAfter);

if (DEBUG_APP_INFO) {
Slog.v(TAG, "ApplicationInfo updating for " + ai.packageName
+ ", assets before change: " + beforeAssets + "\n assets after change: "
+ afterAssets);
}
}
}

看见了熟悉的 updateApplicationInfo(),里面就会重新创建出 Resources 对象了,完成资源的刷新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 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.
*/
public void updateApplicationInfo(@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.
throw new AssertionError("null split not found");
}

mResources = ResourcesManager.getInstance().getResources(null, mResDir,
splitPaths, mLegacyOverlayDirs, mOverlayPaths,
mApplicationInfo.sharedLibraryFiles, null, null, getCompatibilityInfo(),
getClassLoader(), mApplication == null ? null
: mApplication.getResources().getLoaders());
}
}
mAppComponentFactory = createAppFactory(aInfo, mDefaultClassLoader);
}

至此整个加载流程完成。

问题解答

回到我们开头抛出的两个问题:

  1. 为什么 FRRO 对 Resources.getSystem() 不生效?答:Resources.getSystem() 反映的是 zygote 中预加载的系统资源,只有不可变且启用的传统 app 格式的 RRO 会对其生效。
  2. 为什么 PackageManagerService 内没有读取到 FRRO 替换的数据?答:首先肯定跟 Resources.getSystem() 没有关系,看示意代码就已经能知道是用 context 拿的 Resources 了……那根据我们之前的分析,FRRO 应该会生效,所以肯定还存在什么我们还没有发现的东西。其实答案很简单,点开 SystemServer.java 看一下系统服务的启动顺序:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    /**
    * 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.
    */
    private void startBootstrapServices(@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
    }
    我们前面提到过,所有可变 RRO 及所有 FRRO 都是在 OverlayManagerService 启动的时候被处理然后加入到 Resources 中的,而 PackageManagerService 刚好在 OverlayManagerService 之前启动(毕竟 OverlayManagerService 的初始化还要依赖 PackageManagerService 呢),在 PackageManagerService 去 getString() 的时候 FRRO 根本还没被加载呢,自然不可能读到我们预期的值,只能读到已经在 zygote 里被预加载的另一个 RRO 设定的值……

总结

FRRO 是一个很好的新东西,但是仍然不能完全替代传统的 app 格式的 RRO,除了能替换的资源类型受限之外,如果需要替换在系统启动非常早期就需要获取的值,遇到的加载时序问题可以说是根本无法解决的。另一方面,Google 的文档真的可用性堪忧,FRRO 这个特性可以说是完全没有任何文档,只能硬啃代码。这次可以说是踩了大部分人都碰不到的坑,以后还是老老实实用有文档的东西吧……

Android 2025 年每月安全补丁分析索引

作者 残页
2025年1月29日 14:00

2024 年度补丁分析:点我
2026 年度补丁分析:点我

给大家分享一个好消息,经过一年的研究,我现在在 Google BugHunters 平台上总排名 24,2024 年度第 6 名,Android Program 第 4 名!感谢各位一年陪伴!

最后更新时间:2026/03/18 更新内容:更新 2026

状态更新:由于本人健康问题,此博客跟我的频道可能会更新缓慢,建议关注我的频道获取最新状态

2025-12-01

又一年,我们 2026 再见!
妈的,这个月公告里一堆我的 duplicate 😭
Android 16 QPR2 也开始发布安全公告了,有人想看大/小版本的安全公告分析的吗?

在野利用漏洞:CVE-2025-48633 CVE-2025-48572

Framework

CVE-2025-48631 DoS Critical
限制展示的图片大小,最大 4096x4096,防止渲染图片时内存溢出

CVE-2025-22420 EoP High
创建 notification channel 时就检查调用者是否有权限访问它的 sound uri。之前放过一遍,当时的修复是检查失败直接抛异常让 app crash,后面因为兼容性问题被撤回了,改成重置回默认通知声音重新放一遍。

CVE-2025-32319 EoP High
系统绑定 PrintService 时不要使用 BIND_INCLUDE_CAPABILITIES,这个 flag 会让后台 app 也可以使用 while in use 权限

CVE-2025-32328 EoP High
system_server 填充 autofill 资源的时候用了自己的默认 context,其 user id 是 0,补丁改成了使用当前的前台 user id 重新创建一个 context 以避免误用。

CVE-2025-32329 EoP High
补丁链接和 CVE-2025-32328 是一样的。

CVE-2025-48525 EoP High
配套设备配对的时候可能会自动授权读取通知,所以当所有配套设备 associations 被撤销的时候需要移除通知读取权限。

CVE-2025-48564 EoP High
IntentForwarderActivity 中的权限检查问题导致不安全的 intent 可能被转发进其他 profile,具体来说修复了这些问题:

  • 添加并使用可以显式指定 type 的 resolveActivity() API,防止被 Self-changing Data Type 绕过检查
  • 将不安全的、可被伪造的 getCallingPackage() 换成 getLaunchedFromPackage()
  • 如果传来的 intent 带有 selector,也需要 sanitize 它

根据提交信息带的 bug id,推测这个 CVE ID 是属于第一个问题的

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-48583 EoP High
LazyValue 实现中的 Parcel UAF 问题。补丁和漏洞描述都没给触发方法,我这里给出来的方法是在 LazyValue 里放一个 ParceledListSlice,它被反序列化的时候会 binder 调用回 app 进程,app 进程再递归调用 system server 导致这个 LazyValue 被再次反序列化,由于 binder 驱动的特性(见 Binder calls and mutexes reentrancy)这个调用一定会由同一个 binder 线程执行,这样同一个 LazyValue 被 getValue() 两次,导致引用计数被减小两次,而实际上只有一个 LazyValue 被替换成了真实的 Parcelable,所以可以造成引用计数等于 0 触发 Parcel 回收后 map 内依然存在 LazyValue 引用着被回收的 parcel 的情况。这个漏洞我和 Michal Bednarski 在 bugSWAT Mexico 2025 现场一起合作发现过,但是还没来得及上报就被告知是 duplicate,挺可惜的。修复是把 LazyValue 用过的 Parcel 直接 destroy() 掉而非 recycle(),这样它再也不会被 reuse,也就不再有可能被利用。

CVE-2025-48588 EoP High
避免在安全模式下意外清空 always-on VPN。

CVE-2025-48589 EoP High
点击隐私指示器的时候过滤掉非前台用户,漏洞描述是可以跨用户更改权限

CVE-2025-48594 EoP High
CDM 使用 PackageManager#getNameForUid() 去拿包名,对于使用了 sharedUserId 的应用来说这个方法不会返回包名,导致无法移除 association。

CVE-2025-48596 EoP High
Parcel::appendFrom() 里保证 mDataPos 不大于 mDataSize

CVE-2025-48597 EoP High
进入 PiP (画中画模式)的时候只有 WindowManager Shell 成功收到了 transaction 才把 PiP 窗口标记为 TRUSTED_OVERLAY,退出 PiP 时在 WindowManagerService 内立刻将其标为非 trusted。

CVE-2025-48601 EoP High
该漏洞未公开补丁链接,但 bug id 与我报告的两个 bug 重复,因此知道是什么问题。手动还原得到: https://android.googlesource.com/platform/frameworks/base/+/236cff6879b255d0b1b3faa40dff8badb39cbf8b
NotificationManagerService#updateNotificationChannelGroupFromPrivilegedListener()NotificationListenerService#updateNotificationChannelFromPrivilegedListener() 两个方法可以任意指定包名,这个包名会被传到 getOrCreatePackagePreferencesLocked() 里随后会导致一个 PackagePreferences 被创建并存到 map 里,后续 writePackageXml() 会尝试把包名写进 xml 里,通过指定超长字符串可以阻止未来的 notification 相关的变化(比如撤销读取通知权限)被写进 xml 里。

CVE-2025-48618 EoP High
telephony 只在屏幕解锁后才尝试启动浏览器。

CVE-2025-48620 EoP High
VoiceInteractionManagerService 中的逻辑 bug 导致已经被设置上的 voice recognition service 即使在 app 被卸载后也不会清除,后续如果有同名恶意 app 被安装就能直接拿到权限。

CVE-2025-48621 EoP High
不透明度小于 0.5 的窗口不接收点击事件,同时把最大动画时长从 3 秒改成 1.5 秒。CVE-2021-0339 再现。
演示视频及论文见 https://taptrap.click/

CVE-2025-48627 EoP High
ActivityTaskManagerService#startNextMatchingActivity() 中的 BAL,CVE-2024-0036 的变体,原先的修复是给 ActivityOptions 设置避免移动到前台的 flag,这个修复有问题,改成修复根因(realCallingUid 不正确)。

CVE-2025-48629 EoP High
该漏洞未公开补丁链接,但是其实就是 https://android.googlesource.com/platform/frameworks/base/+/0bc6ced90e2ee1e6140a4d87ee62923bdfd2ca93
VoiceInteractionManagerService 在没有 voice recognition service 的情况下会自动尝试找符合要求的 app 并把它设置成默认的,这个过程没有过滤第三方 app,所以第三方 app 有可能被系统自动设置成 voice recognizer。修复就是过滤掉没有 FLAG_SYSTEM 标志的 app。这个问题我也报过,当时以为他们会在大版本里修,结果居然 backport 了,勤奋程度超出我想象。

CVE-2025-48632 EoP High
AssociationRequest 的 display name 最长不能超过 1024,其实就是 9 月份的 CVE-2025-48522,当时修得不够彻底

CVE-2025-48639 EoP High
补丁链接和 CVE-2025-48621 是一样的。

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-48603 DoS High
拒绝大小超出 200KB 的输入法 metadata。

CVE-2025-48607 DoS High
CVE-2025-26429 的变体,AppOpsService getPackagesForOpsForDevice() 的返回值使用 ParceledListSlice 切片传输,避免传输过大数据造成 TransactionTooLargeException。

CVE-2025-48614 DoS High
CVE-2024-49736 的变体,DSU 模式下禁止通过编程方式触发恢复出厂设置。

System

CVE-2023-40130 EoP High
2023 年的老漏洞,当时的修复可能会造成崩溃,加上一个 catch 重新发。作者的博客:https://wrlus.com/android-security/bindservice-error-handle/

CVE-2025-22432 EoP High
四月发过的洞,和 CVE-2023-40130 类似,因为同一个原因被撤回重发。详见 4 月分析。

CVE-2025-48536 EoP High
Settings app 里添加一个 string array 指定只在 debuggable build 里允许访问 settings slice 的 app。是因为之前把所有包名都在 slice_allowlist_package_names 白名单里列出,而部分包名在 release build 上没有,导致包名伪造攻击?

CVE-2025-48566 EoP High
和 CVE-2025-48564 的补丁链接是一样的,我报过 getCallingPackage() 的问题,与这个 bug id 重复了,所以猜测这个 CVE 是分配给第二个问题即 getCallingPackage() 的 misuse。

CVE-2025-48575 EoP High
此漏洞由我发现并报告。
CertInstaller 内校验调用者的包名是否是 com.android.settings 来决定是否允许安装证书,但是在手表/电视/车机上,设置的包名并不是 com.android.settings,这些系统上默认没有叫 com.android.settings 的应用,但是 CertInstaller 内的校验逻辑并没有修改,所以任意应用都可以抢占 com.android.settings 这个包名然后绕过校验直接安装证书。

CVE-2025-48586 EoP High
和 CVE-2025-32346 类似,把 EditFdnContactScreen onActivityResult 中原本的比较 user id 的跨用户检查改成了使用 android 15 新 api 去检查来源 app 有没有权限读返回的 uri。依然需要分析。
更新:应该是使用 content://com.android.contacts/data_enterprise/phones 从 work profile 里泄露出联系人号码

CVE-2025-48598 EoP High
类似 CVE-2025-48541,添加生物验证的时候忽略不可信调用者传递的 user id

CVE-2025-48599 EoP High
用户受到 DISALLOW_CONFIG_LOCATION 限制时禁止打开 WifiScanModeActivity

CVE-2025-48612 EoP High
设置付款应用时存在字符串注入,先前的逻辑把 app 组件名 + 一个空格 + user id 拼接成一个字符串,然后再以空格为分隔符还原出原来的值,如果 app 组件名里包含空格就能操控后面的 user id

CVE-2025-48626 EoP High
Launcher3 导航到 home 时原本是直接 startActivity(),补丁改成了通过 binder 向 SystemUI 发送 KEYCODE_HOME。看不懂,漏洞描述似乎是 BAL?

CVE-2025-48555 ID High
从设置里隐藏锁定用户的通知

CVE-2025-48604 ID High
MMS Service 从 content uri 下载 pdu (writePduToContentUri())的时候校验 user id。和 CVE-2025-48591 类似

CVE-2025-48622 ID High
把旧版的 dng sdk 1.4 更新到 1.7.1,见 https://project-zero.issues.chromium.org/issues/412422252

2025-11-01

System

CVE-2025-48593 RCE Critical
蓝牙 BTA 层中 HFP 协议实现中的 Classic UAF,和 CVE-2025-0084 很像
详细分析及 PoC: https://worthdoingbadly.com/bluetooth/

CVE-2025-48581 EoP High
apexd 中的问题,会导致安全更新无法安装,不清楚具体是什么原因

2025-10-01

不出意料地在给了我一堆 CVE 后再次放鸽子,休息一个月

2025-09-01

在野大哥整的新活:CVE-2025-38352 CVE-2025-48543
这个月更新了五十多个漏洞,累死我了

Android Runtime

CVE-2025-48543 EoP High
jni 调用 NewObject() 和 AllocObject() 的时候检查要 new 的类不能是抽象类,否则会发生内存损坏。
这个问题第一次被讨论应该是这里:
https://www.jianshu.com/p/2304405947c7

Framework

CVE-2025-0089 EoP High
对于启动 launcher 的格式不正确的 intent 对其内容进行修正,如移除多余的 category、重置 intent type、重置显式设置的 component 及添加 FLAG_ACTIVITY_NEW_TASK 标志。看不太懂,漏洞描述是 hijack launcher app。

CVE-2025-32324 EoP High
Android 15 新添加的 am start-in-vsync 命令把实际 startActivity 动作放在了 UI 线程处理,导致里面读到的调用者权限是 system_server 自己的(不在 binder 调用过程中的线程调用 Binder.getCallingUid() 等方法会返回当前进程自己的信息),导致可以忽略权限启动任意 activity。和去年的 CVE-2023-21114 有点像。

CVE-2025-32325 EoP High
Parcel::appendFrom() 里的越界写,看测试代码是 parcel offset 在一个 FileDescriptor 中部时调用 appendFrom() 会产生问题

CVE-2025-32331 EoP High
在 app pinning 模式下不显示可被 dismiss 的 keyguard,否则在折叠屏设备上会造成问题,导致绕过 app pinning。

CVE-2025-32349 EoP High
隐藏各种不可信窗口时使用系统默认的动画,同时禁止 toast 窗口自定义动画,应该是能指定一个很长时间的动画绕过 tapjacking 保护。

CVE-2025-32350 EoP High
SystemUI 不再导出 ControlsActivity,同时为 ControlsSettingsDialog 添加标志隐藏悬浮窗。

CVE-2025-48522 EoP High
ComponaionDeviceManagerService AssociationRequest 的 display name 限制最长为 1024。

CVE-2025-48528 EoP High
生物认证弹窗的窗口类型从 TYPE_APPLICATION_OVERLAY 修正为 TYPE_KEYGUARD_DIALOG,防止被其他 app 悬浮窗覆盖

CVE-2025-48540 EoP High
binder RPC 过程中读到错误的 object table size 及内存耗尽时手动 shutdown session,看描述是越界写

CVE-2025-48546 EoP High
启动 Activity 时禁止指定 launch window mode 为 pinned,阻止 BAL 绕过,应该使用公开的 ActivityOptions#makeLaunchIntoPip() API,这个 API 做了 BAL 检查

CVE-2025-48548 EoP High
AppOpsService 内的管理问题,会导致后台 app 也能录音及录音时不显示隐私指示器

CVE-2025-48549 EoP High
补丁同 CVE-2025-48548

CVE-2025-48552 EoP High
DevicePolicyManagerService 里重置代理时忽略写入 SettingsProvider 时可能发生的异常,防止干扰到后续操作(如移除设备管理员)

CVE-2025-48553 EoP High
Device Admin app 被更新时如果附带的 device admin info 不再有效就移除掉这个 admin

CVE-2025-48556 EoP High
NotificationChannel 截短过长的 id、parentChannelId 和 conversationId,防止序列化时发生错误。具体触发点是在 Android 16 新增的 NotificationListenerService#createConversationNotificationChannelForPackage() API 中。
为什么我这么清楚,因为这个洞我7月自己发现过一次,喜迎 duplicate 😭

CVE-2025-48558 EoP High
BatteryService 只启动系统 app 声明的 ShutdownActivity,防止隐式 intent 劫持

CVE-2025-48563 EoP High
绑定 AutofillService 如果收到 null 就解绑,避免 BAL 绕过

CVE-2025-0076 ID High
在“快速访问钱包”这个功能里只允许 bitmap 类型的 card image 和 icon。

CVE-2025-32330 ID High
蓝牙音频分享这个功能使用 SecureRandom 生成随机密码,提升安全性。

CVE-2025-48529 ID High
telephony 内遗留代码产生的跨用户音频泄露,修复就是把遗留代码删掉

CVE-2025-48537 ID High
修复系统代码里认为“condition id 相同的则一定指向同一个 ConditionProviderService“的错误假设,同时禁止非系统调用者创建指向系统 authority 的 rule。没看懂有啥用。

CVE-2025-48545 ID High
此漏洞由我发现并报告。
AccountManagerService 存在不安全的权限检查,当调用者是系统预装 app 时会放行调用某些特权 API,而 Android 13 引入的一个新功能 SDK Sandbox 允许不可信开发者发布的 sdk 在单独的进程中运行,这些进程会被认为属于 com.google.android.sdksandbox 这个 app,其是一个系统预装应用因此可以绕过上述检查调用特权 API。

CVE-2025-48561 ID High
应用可以请求模糊多个窗口,通过测量模糊各个窗口所需的时间判断出上面显示出的内容。
太牛逼了,本年度最牛逼侧信道

CVE-2025-48562 ID High
写入 pdf 文档的时候指定 truncate flag。

CVE-2025-48538 DoS High
禁止 pm hide com.android.systemui,即使在 profile 里进行该操作也会导致整个设备无法使用。

CVE-2025-48542 DoS High
AccountManagerService 里把账号的 password、auth token 等也纳入内存限制。

CVE-2025-48550 DoS High
授权 slice 权限时保证包名合法,防止路径穿越覆盖其他文件。

CVE-2025-48554 DoS High
解析 device admin service info 时 catch 可能发生的 OutOfMemoryError

CVE-2025-48559 DoS High
和之前的 CVE-2025-22431 类似,有 proxy app 给 root 提供 attribution tag 时不能直接无条件信任

System

CVE-2025-48539 RCE Critical
之前那一堆蓝牙里 log UAF 的问题又来了,只影响 15 16

CVE-2021-39810 EoP High
官方未放出补丁链接,手动还原结果:
https://android.googlesource.com/platform/packages/apps/Nfc/+/d6515512436fe0976220d3919668b8ad2705144a
逻辑很清晰,选取默认支付服务的时候过滤掉非系统预装应用。
很久之前的老洞,本来在 Android 14 上修了,现在 backport 给 13。

CVE-2023-24023 EoP High
蓝牙 Legacy Secure Connection (LSC) 协议设计问题,又名 BLUFFS。没仔细看,看起来是 key 较短时会发生的中间人攻击

CVE-2024-49714 EoP High
蓝牙 AVRC 里的越界写

CVE-2025-26454 EoP High
ManagedProvisioning app 里读取其他 app 提供的 disclaimer 中的 URI 时需要检查权限,以免发生跨用户读取。
我觉得这个修复是错的,代码里使用 Binder.getCallingUid() 去拿调用者信息,而 app 传递 disclaimer 是启动 ManagedProvisioning 的一个 activity 在 intent extras 里放 URI,这里根本就在主线程而非 binder 调用线程(启动 Activity 也不是直接 app -> ManagedProvisioning 这样的直接 binder 调用),返回的只会是 ManagedProvisioning 自己的 uid。要从 Activity 拿调用者应该使用 Activity.getLaunchedFromPackage() 或者 Activity.getLaunchedFromUid()
但是补丁中的另一个问题又刚好阻止了利用:它调用 Context.checkUriPermission() 去检查权限,这个函数只在有人通过 Intent.FLAG_GRANT_READ_URI_PERMISSION 显式授权 URI 访问权限的时候会返回 true,所以只在有人显示给 ManagedProvisioning 授权的时候才能再次利用成功。我也不确定这个用错的 checkUriPermission() 是不是 bug,注释里有这样一句,所以可能是预期的:

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 里。补丁把遗留代码给移除了。

CVE-2025-32321 EoP High
AccountTypePreferenceLoader 内检查 intent 确认安全之后显式设置 component 让它变成一个显式 intent,如果是隐式 intent 的话,从 resolve intent 到 startActivity 中间存在一个时间窗口,可能会存在系统里面有什么东西改变了导致两边解析到不同的 activity。这种漏洞的典型例子就是 Self-changing Data Type - CVE-2024-40676
同时建议阅读 DEFCON 的这篇演讲:Dead Made Alive Again: Bypassing Intent Destination Checks and Reintroducing LaunchAnyWhere Privilege Escalation (a.k.a BadResolve)

CVE-2025-32323 EoP High
此漏洞由我发现并报告。
DocumentUI 里显示权限请求对话框的时候包含应用的名字,应用可以在名字里放入误导性的文字和很多个换行把原本真正要显示的警告信息顶出屏幕。很经典的字符串 UI 欺骗漏洞,我之前的文章 Android 平台常见安全漏洞类型 有详细讲解。修复就是把应用名称截断再显示。

CVE-2025-32326 EoP High
和上面的 CVE-2025-32321 类似,Settings AppRestrictionsFragment 内检查 intent 确认安全之后显式设置 component 让它变成一个显式 intent,防止经典的 TOCTOU race condition。无论是用 Self-changing Data Type 还是 BadResolve 都能很简单地利用。
这个洞我去年 10 月报过,可惜跟 CVE-2025-0091 一样是 duplicate,然后等了差不多一年才修好,挺好的

CVE-2025-32327 EoP High
MediaProvider 中的 SQL 注入,补丁在 queryMediaIdForAppsLocked() 过滤掉了意外出现的值,保证不会被返回给调用者。

CVE-2025-32333 EoP High
把 CVE-2025-26430 的补丁向后移植给 android 14

CVE-2025-32345 EoP High
CVE-2025-26435 的变体,之前补丁是只禁止访客用户关闭 deceptive app scanning,现在改成禁止所有非管理员用户

CVE-2025-32346 EoP High
截至发稿,此漏洞补丁未公开。仅给 16 更新了。漏洞描述:

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 里泄露出联系人号码

CVE-2025-32347 EoP High
Settings 添加生物认证信息时忽略不可信调用者传递的 PendingIntent。这个地方就是我之前利用 CVE-2025-0100 使用的 trampoline,能够以 Settings 权限 startIntentSenderForResult() 我指定的一个 PendingIntent,然后再把收到的 result 通过 setResult() 返回给调用者。当时的补丁只修了 MediaProjectPermissionActivity,没有修复这个 trampoline,这次看漏洞描述是能确定设备的位置,怀疑是读的 WifiDialogActivity 的 result,但是我当时也测试过 WifiDialogActivity,并没有成功收到 result,不知道少了什么。这次把 trampoline 一块修了,不知道其他地方还有没有这么好用的东西了。

CVE-2025-48523 EoP High
Contacts app SelectAccountActivity 之前在刚好只有一个 account 的时候会跳过弹窗自动加载 vcard 文件添加新联系人,补丁把这段特殊处理删了,无论如何都需要用户手动确认

CVE-2025-48526 EoP High
IntentResolver ChooserActivity 跨 profile 启动 intent 的时候也需要清掉 package 和 component。

CVE-2025-48531 EoP High
Settings CredentialStorage 内将不安全的 getCallingPackage 换成 getLaunchedFromPackage。

CVE-2025-48532 EoP High
截至发稿,此漏洞补丁未公开。仅给 16 更新了。漏洞描述:

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.

更新:补丁出来了,原本 MediaStore markMediaAsFavorite 这个调用接受一个 ContentValues,app 可以往里面注入其他内容,补丁修改了 MediaProvider 改成不再接受外部 ContentValues,原本通过 ContentValues 传递的 boolean 改成通过 extras 单独传递。

CVE-2025-48535 EoP High
去年我发现的 CVE-2024-43080 的变体,当时的补丁只在 startActivity() 的时候进行了 new Intent(),但是其实 resolveActivity() 本身也是一个跨进程的 aidl 调用,intent 后面的参数也可能被污染,攻击者可以污染 flags 这个参数让 resolveActivity 解析到攻击者自己的 activity 但是在实际 start 的时候解析到别的 activity,造成 launch anywhere。补丁就是把 new Intent() 给放到了 resolve 前面。
Parcel Mismatch 不死!Parcel Mismatch 万岁!

CVE-2025-48541 EoP High
不可信调用者启动 FaceSettings 时忽略其提供的 token、sensor id、challenge、user id 等数据。

CVE-2025-48544 EoP High
外部攻击者访问 MediaProvider 时可以在提供的 extras 中设置 INCLUDED_DEFAULT_DIRECTORIES 越权访问文件,这个 extra 只应该被 MediaProvider 内部使用,补丁把它移出了 bundle 改成作为单独参数提供。

CVE-2025-48547 EoP High
当且仅当没有任何一次性权限授予给一个 app 的时候才停止这个 app 的 one-time permission session

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,仅匹配系统应用,防止被第三方恶意应用抢占影响可用性。

2025-08-01

连补丁链接都不给了,还要我手动去翻,差评

Framework

CVE-2025-22441 EoP High
补丁:
https://android.googlesource.com/platform/frameworks/base/+/60335b2eae7311fe6e10e140b64489008a38a5a8
https://android.googlesource.com/platform/frameworks/base/+/37bf5823504f2a256f128123393cd149721b87fc
漏洞描述:

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.

最大的变化就是,补丁去掉了两个地方的 LoadedApk.checkAndUpdateApkPaths() 调用。点进去可以发现,这个方法会更新当前进程的 ApplicationInfosourceDir 等字段,把 librarySearchPath 加入到 ClassLoader 内,但由于 makePaths() 被调用了两次,如果当前 LoadedApk 已经有一个 ClassLoader 并不会将新的 sourceDir 直接添加进 ClassLoader。librarySearchPath 需要进程调用 System.loadLibrary() 才会被用到,考虑到新的 librarySearchPath 会位于数组末尾,且漏洞描述是加载 java 代码,目前还不清楚具体如何做到代码执行。如果有人复现成功的话可以和我交流一下。

CVE-2025-48533 EoP High
补丁:
https://android.googlesource.com/platform/frameworks/base/+/1ac00dac4a161ca6bef252ed36646c4b125132d0
窗口管理模块的 race condition,可以在锁屏上通过一个可以显示在锁屏上的 app 的 context menu 启动另一个 showWhenLocked=false 的 activity。

System

CVE-2025-48530 RCE Critical
补丁:
https://android.googlesource.com/platform/external/rust/crabbyavif/+/87124e11e14f2f6fed75d57f5723ddab37cd4bff
crabbyavif (AVIF 解析模块)里 Rust 写出来的越界访问,看补丁加了一堆 round up,应该又是 size 计算的问题。只影响 16

2025-07-01

谷歌放鸽子,啥也没有,没想到吧
又可以偷懒一个月了

2025-06-01

Android Runtime

CVE-2025-26456 DoS High
DexUseManager 内限制 dex 使用记录的数据的大小。

Framework

CVE-2025-26450 EoP High
KeyEvent 发送给输入法之前先验证其确实来自系统,且其不是太早之前产生的(防止重放攻击)

CVE-2025-26452 EoP High
ResourcesImpl 加载 FRRO 的时候保证其确实是一个 FRRO (在 /data/resource-cache/ 下并且以 .frro 结尾)。看漏洞描述是可以读其他 task 的快照

CVE-2025-26455 EoP High
AMediaCodec_getInputBufferAMediaCodec_getOutputBuffer 两个函数的返回值相关的越界写入问题

CVE-2025-26458 EoP High
LocationProviderManager 内发送 PendingIntent 的时候加上 setPendingIntentBackgroundActivityLaunchAllowed(false) 阻止 BAL

CVE-2025-26462 EoP High
AccessibilityService onBind() 返回 null 时需要 unbind,防止 BAL

CVE-2025-32312 EoP High
PackageParser 校验输入类名必须是一个 IntentInfo 的子类,防止调用任意 class 的构造函数。这个补丁打断了 https://github.com/michalbednarski/TheLastBundleMismatch 里用到的 trampoline,防止绕过 lazy value 保护创造出 self-changing bundle。

CVE-2025-26437 ID High
CredentialManagerService getCandidateCredentials() 校验调用者必须是默认的 Credential Autofill service,只影响 15

CVE-2025-26448 ID High
CursorWindow 内使用 calloc 代替 malloc 避免未初始化内存

CVE-2025-26432 DoS High
CompanionDeviceManagerService setAssociationTag 限制 tag 最大长度为 1024。只影响 15

CVE-2025-26449 DoS High
传递 AutomaticZenRule 和 ZenModeConfig 时使用 ParceledListSlice ,防止单次数据量过大超过 binder 传输大小上限

CVE-2025-26463 DoS High
BlobStoreManager allowPackageAccess 内限制传入包名和证书的大小。

System

CVE-2025-26443 EoP High
ManagedProvisioning HtmlToSpannedParser 内限制传入的 url 必须是 http 或 https 的,看漏洞描述是可以安装 app,猜测是使用 file 或者 content 的 uri。其实没看懂咋做到的,明明 createIntent 内部有一个一模一样的检查,为什么要在外面再加一个?

更新:经提醒,URLSpan 内有一个默认的 onClick 实现,会发送 Intent.ACTION_VIEW 尝试打开 url。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void onClick(View widget) {
Uri uri = Uri.parse(getURL());
Context context = widget.getContext();
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.w("URLSpan", "Activity was not found for intent, " + intent.toString());
}
}

CVE-2025-26441 ID High
蓝牙协议栈 sdp_discovery.cc 中的越界读取

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 类似。

2025-05-01

在野利用漏洞:CVE-2025-27363,终于不是 USB 了

Framework

CVE-2023-21342 EoP High
绑定到应用提供的 speech RecognitionService 时不指定 BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS 标记,避免 BAL bypass
Android 14 就修了的洞,现在 backport 给 13

CVE-2024-34739 EoP High
去年八月就放过一遍,不知道为什么又放了一遍。设置向导未完成时,插入 USB 设备不弹出 activity,避免 FRP 绕过。

CVE-2025-0077 EoP High
切换用户过程中的 race condition,有概率导致切换到其他用户后不显示目标用户设置的锁屏。只影响 15。
这个问题我很早也注意到了,可惜一直没能稳定复现出来。

CVE-2025-0087 EoP High
PackageInstaller 内的逻辑 bug,请求为其他用户卸载 app 时只在当前用户是目标用户的父用户的情况下才允许。

CVE-2025-22425 EoP High
PackageInstaller 内使用了不安全的 getCallingPackage 函数获取调用者,可能被伪造结果,改成使用 getLaunchedFromPackage。

CVE-2025-26422 EoP High
WindowManagerService dump 函数内的权限检查放错了位置,调用者传递 --high-priority 参数的时候不会检查权限,造成敏感信息泄露。只影响 15。

CVE-2025-26426 EoP High
registerReceiver 在调用者给定 caller package = "android" 时需要保证 calling uid 真的是 core uid。

CVE-2025-26427 EoP High
跟 CVE-2024-0032 一样,限制不让访问 /sdcard/Android/data。不知道同一个漏洞还能放多少遍。。。

CVE-2025-26428 EoP High
如果当前 task 已经进入 lock task mode,忽略后续的 app pinning 请求。

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 数据传输大小限制造成异常。

System

CVE-2025-27363 RCE High
上游 freetype 中的整数溢出 bug

CVE-2025-26420 EoP High
官方未放出此漏洞补丁链接,手动查找得到:
https://android.googlesource.com/platform/packages/modules/Permission/+/5c07da90f7ae911b9e64d4e34871b7d2115a0a7f
看起来是显示多个权限请求对话框时的内容遮盖问题。官方给出的漏洞描述:

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-26435 EoP High
禁止访客用户禁用 deceptive app scanning

CVE-2025-26438 EoP High
蓝牙 SMP 协议实现中的验证绕过 bug

CVE-2023-35657 ID High
蓝牙 BTA 层的类型混淆,会导致越界读

2025-04-01

Android 12 & 12L 的支持已于本月结束,呜呜呜
在野利用漏洞:CVE-2024-53150CVE-2024-53197,都是内核 USB 模块

Framework

CVE-2025-22429 ID Critical
bundle 解析过程中的逻辑 bug,try 块中调用 readArrayMap 解析其值,finally 块中判断若 lazy value 数量为 0 则说明不需要 lazy unparcel,直接释放 parcel。但这个 lazy value 数量是使用 readArrayMap 的返回值来描述的,如果 readArrayMap 已经成功创建了一些 LazyValue 但在后续解析过程中抛出异常,由于 readArrayMap 并没有正常返回自然也就没有返回值,此时上层记录的 lazy value 数量还是 0,parcel 在 finally 中被释放,后续 LazyValue 再使用 parcel 就会产生 UAF。注意,若进程开启了 Defuse (在 system_server 内满足此条件),对 BadParcelableException 的 catch 块内清空了 map,而从代码上来看失去引用的 LazyValue 并不会操作 parcel,所以我猜测想实际触发这个漏洞需要一个并非 BadParcelableException 但在上层被 catch 的异常才能让 LazyValue 驻留在 map 里?
类似之前的 leak value,但在没有另一个能倒退 parcel position 的漏洞的情况下是否能像之前那样利用?从漏洞描述上来看应该是做到了代码执行的

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-22437 EoP High
注意官方公告放错了补丁!!官方公告里放的补丁和 CVE-2024-49728 一致,但是正确的补丁其实是 https://android.googlesource.com/platform/frameworks/base/+/25d6165d462ad8fe83ea8dd254214a420bc1a526
其实这个漏洞和 2023 年 11 月公告 CVE-2023-40111 是一样的,当时可能由于兼容性考虑只给 android 14 上了这个 patch,现在可能是觉得不妥当又给 backport 到了 13。

CVE-2025-22438 EoP High
input dispatcher 里的加锁问题,会导致 dispatchEntry 这个变量 UAF

CVE-2025-22442 EoP High
work profile 默认禁用开发者选项,看漏洞描述是能往 profile 里装 app

CVE-2024-49722 ID High
在系统设置里更换用户头像的时候使用显式 intent 进行图片选择,确保只启动系统自己的头像选择器。
(这个问题反反复复修复几遍怎么又来了?之前测试过这里没能测出什么问题,这次只给 15 更新了,可能是新版本又加了什么东西)

CVE-2025-22421 ID High
通知的 content description (用于无障碍)里不要包含通知内容,否则可能会被读屏软件在没解锁屏幕的时候就读出来

CVE-2025-22430 ID High
TrustManager 新增的 isInSignificantPlace 方法需要 ACCESS_FINE_LOCATION 权限。只影响 15

CVE-2025-22431 DoS High
AppOpsService 内即使直接调用者可信也不应该直接信任由 proxy app 提供的 proxied attribution tag,改为只在没有 proxy app 或其是系统 app 时才信任

System

CVE-2025-26416 EoP Critical
skia 解码 bmp 图片时的未初始化变量
更新:似乎只是逻辑错误造成的堆溢出

CVE-2025-22423 DoS Critical
dng_sdk 里如果 tag count 非法则提前返回,避免后续崩溃

CVE-2024-40653 EoP High
Telecomm 里如果绑定到 ConnectionService 后 15 秒还没有响应就解绑

CVE-2024-49720 EoP High
通过 role 授予权限时把用户之前对该权限选择的“每次都询问”视为有效的用户决定,不要去覆盖它

CVE-2024-49730 EoP High
fuse daemon 内增大 MAX_READ_SIZE,看描述是 oob write

CVE-2025-22418 EoP High
系统设置内选择铃声的 intent 指定包名,怀疑跟之前那堆铃声漏洞(CVE-2023-40108、CVE-2023-40132)是一样的

CVE-2025-22419 EoP High
telephony app 多个 activity 添加 flag 防止被悬浮窗覆盖。

CVE-2025-22427 EoP High
settings NotificationAccessConfirmationActivity 错误地将 SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS 传递给 addFlags (作为一个 system flag 应该使用 addSystemFlags/addPrivateFlags),导致其被错误解释为了 FLAG_SHOW_WHEN_LOCKED 从而显示在了锁屏之上,不解锁就可以授权。

CVE-2025-22428 EoP High
在我之前那个 CVE-2024-43088 的补丁基础之上把检查 calling package 的权限更换为了检查 calling uid 的权限。不是很懂,猜测是可能有一个 app 在主用户上有跨用户权限但在次用户上没权限的情况?
更新:https://konata.github.io/posts/identity-squashing/

CVE-2025-22432 EoP High
此漏洞由我发现并报告。
telecomm 内使用了 BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS 来 bind CallRedirectionSevice(其是 BAL 豁免名单之一),所以响应超时了也需要 unbind,否则会导致 BAL bypass。更多内容可见于 https://wrlus.com/android-security/bindservice-error-handle/

CVE-2025-22433 EoP High
IntentForwarderActivity 内检查 intent 是否可以被转发到其他 profile 时的逻辑 bug,如果 intent 有 selector 那这个 intent 本身和 selector 都要被检查。

CVE-2025-22435 EoP High
蓝牙 AVDT 协议实现里增加了一个消息类型检查,防止可能的类型混淆

CVE-2025-22439 EoP High
DocumentUI 加载 last accessed stack 记录的 uri 时忘记校验 uri,可以绕过存储限制授权

CVE-2024-49728 ID High
通过蓝牙发送文件时检查传递的 uri 是否为指向其他用户的 uri。印象中很久以前我是测试过这里的,没利用成功,不知道咋复现的

2025-03-01

在野利用漏洞:CVE-2024-43093CVE-2024-50302
懒得放补丁链接了,自己去官方公告里面找吧

Framework

CVE-2024-0032 EoP High
去年2月补丁里放过的,限制通过 SAF 访问 /sdcard/Android/data。不知道为啥重新放了一遍。

CVE-2024-43093 EoP High
跟去年11月补丁的一样,自己去看去年的分析吧,懒得再写一遍了。去年忘了给 12L 更新,重新放一遍。

CVE-2025-0078 EoP High
service manager 注册自己的时候需要打开 requesting sid 从而可以正确检索 caller 的 selinux context。关于 requesting sid 的更多信息见 https://project-zero.issues.chromium.org/issues/42450815 (CVE-2019-2023 及相关的一系列漏洞)

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 设置,同时限制其内容的大小

System

下面有很多一个补丁链接对应多个漏洞的情况,不一一复制粘贴了,看得明白就行

CVE-2025-0074 RCE Critical
CVE-2025-22403 RCE Critical
bluetooth sdp sdp_discovery.cc 内两处出错 log 里使用了可能已经被释放掉的指针,造成 UAF。真的能造成 RCE?我持怀疑态度。只影响 15。

CVE-2025-0075 RCE Critical
sdp_server.cc log 语句内的 UAF,跟上面一样。只影响 15。

CVE-2025-0084 RCE Critical
蓝牙 SDP 协议实现中,同一个客户端发起多个连接可能导致 UAF

CVE-2025-22408 RCE Critical
CVE-2025-22410 RCE Critical
CVE-2025-22411 RCE Critical
CVE-2025-22412 RCE Critical
CVE-2025-22409 EoP Critical
CVE-2025-22404 EoP High
CVE-2025-22405 EoP High
CVE-2025-22406 EoP High
CVE-2025-22407 ID High
跟上面一堆是类似问题,蓝牙 log 内可能使用已经被释放的变量导致 UAF。真的有必要搞这么多 CVE?只影响 15。

CVE-2025-0081 DoS Critical
解析 jpeg 时如果哈夫曼表为空直接 abort

CVE-2023-21125 EoP High
蓝牙 btif/src/btif_hh.cc 内的 UAF 问题

CVE-2025-0079 EoP High
蓝牙 AVCTP/AVDTP 指定 BTA_SEC_ENCRYPT 保证加密传输

CVE-2025-0082 ID High
补丁跟上面的 CVE-2025-0083 是一样的。

CVE-2025-0092 ID High
CVE-2025-0093 ID High
蓝牙设备解除绑定时重置短信、通讯录、SIM 权限授权状态

CVE-2025-26417 ID High
将已经下载好的文件插入进 DownloadProvider (DownloadManager.addCompletedDownload)时,确保路径对应文件的所有者与调用者匹配,否则恶意应用可以通过插入操作产生的 DownloadProvider 的 id 通过 DownloadProvider 的权限访问该文件

2025-02-01

可能的被在野利用漏洞:CVE-2024-53104,内核中 USB 模块的越界写入

Framework

CVE-2024-49721 EoP High 1
CVE-2024-49721 InputMethodSubtypeArray 反序列化漏洞分析

CVE-2024-49743 EoP High 1 2 3
这个漏洞比较复杂,后续会单独发文章解析,敬请期待

CVE-2024-49746 EoP High 1 2
Parcel::continueWrite 中的 fd 处理问题,看起来是会错误 close 掉不该被关闭的 fd?还加了很多溢出的检查

CVE-2025-0097 EoP High 1
WindowManagerService transferTouchGesture 中除了校验 token 还需要校验 calling uid 跟 owner uid 相等。只影响 15

CVE-2025-0098 EoP High 1
TaskFragmentOrganizerController 内会检查 activity 的 pid 与注册 organizer 的时候的 calling pid 是否相等,如果不相等说明不是同一个进程,这个时候不发送 activity token 以避免它泄露。但是这个检查存在缺陷,因为如果 app 进行 binder transact 的时候指定了 FLAG_ONEWAY,说明这是一个异步调用,这个时候被调用者拿到的 calling pid 会是 0,以此可以泄露出 pid 是 0 的 ActivityRecord 的 token。什么时候 ActivityRecord 的 pid 是 0?怀疑是 activity 所属的进程已经退出了的时候。只影响 15。

CVE-2025-0099 EoP High 1
CompanionDeviceManagerService 新增的 getBackupPayload 和 applyRestoredPayload 这两个函数没有权限检查,应该加上 uid 校验,只允许 system 身份调用。

CVE-2023-40133 ID High
CVE-2024-0037 ID High
1
检查 Autofill 相关的 Slice 中的 Icon URI 是否属于当前用户,避免跨用户泄漏。这个应该是去年就放过一次,不知道为什么又放了一遍

CVE-2023-40134 ID High
CVE-2023-40135 ID High
CVE-2023-40136 ID High
CVE-2023-40137 ID High
CVE-2023-40138 ID High
CVE-2023-40139 ID High
1
检查 Autofill 相关 RemoteViews 中 URI 是否全部属于当前用户,避免跨用户泄漏。这个去年也放过一次,同样搞不懂为什么又放

CVE-2025-0100 ID High 1
此漏洞由我发现并报告。
SystemUI MediaProjectionPermissionActivity 使用 getCallingPackage 获取调用者然后校验其权限,如果是特权应用就跳过确认环节。但是 getCallingPackage 的返回结果实际上可以被伪造,补丁改成了更准确的 getLaunchedFromPackage。
这里有一个好玩的点,就是 getCallingPackage 返回的是哪个 app 会收到当前 Activity 返回的结果,所以可以被伪造,但是 SystemUI 需要把 IMediaProjection 返回给调用者然后调用者才能使用它开始录屏,伪造 getCallingPackage 实际上会导致应用收不到返回的 binder 自然也就无法完成后续利用。那么这个看起来触发条件和可用性相悖的漏洞是怎么被利用的呢?这里先卖个关子,读者可以先自己思考思考。在本页面搜索 CVE-2025-32347 可查看答案。

CVE-2024-49741 DoS High 1
此漏洞由我发现并报告。AppWidgetServiceImpl 中限制一个应用最多可以创建 20 个 host,每个 host 可以拥有 200 个 widget,这个补丁之前是完全没有限制的,所以可能发生超大数据量导致 DoS。

Platform

CVE-2025-0094 EoP High 1
禁止 work profile 打开多用户管理页面,看漏洞描述是可以移除 work profile?

System

CVE-2025-0091 EoP High 1
在 Settings 应用中的账号管理页面里,禁止启动带有 content URI 的 intent。跟 Self-changing Data Type - CVE-2024-40676 漏洞分析 是同一类漏洞,去年那篇文章一直没发就是为了等这个漏洞审核,可惜 duplicate 了 😭

CVE-2025-0095 EoP High 1
Settings AppTimeSpentPreference 中使用一个只有 Action 的隐式 Intent 启动 Activity,可能会被恶意应用劫持。补丁给这个 intent 加上了 setPackage。

CVE-2025-0096 EoP High 1
nfc 模块里面有个 malloc 的 size 写小了,只影响 15

CVE-2024-49723 ID High 1 2
移除 Conscrypt 的 3DES 支持,只给 15 更新了

CVE-2024-49729 ID High 1
fs_mgr/libdm/dm.cpp 中 redact 掉可能的敏感信息,否则会导致其在 bug report 过程中被 dump 出来,泄漏原始加密密钥。

2025-01-01

这个月补丁好多,算是解答了上个月的“为什么这么少”的疑问
大家新年快乐!一直在等补丁链接,等了一个月还没放,只能先发一个不完整版的了
注:本月补丁链接未放出 以下部分补丁来源为手动还原
2025/02/24 更新:官方公告里已经放出补丁链接 建议去看官方公告里的;官方公告里移除了 CVE-2023-40108 CVE-2023-40132,不知道为什么

Framework

CVE-2024-49724 EoP High 1
AccountManagerService checkKeyIntent 加了个 setComponent 让 intent 变成显式 intent,防止 resolve 到 startActivity 期间某些影响 intent resolve 流程的因素发生改变,导致 TOCTTOU race condition。具体实例:Self-changing Data Type

CVE-2024-49732 EoP High 1
CompanionDeviceManagerService 里新增的 enablePermissionsSync disablePermissionsSync getPermissionSyncRequest 三个 API 缺少权限检查,补丁限制只有 system uid 可以调用。只影响 15。

CVE-2024-49735 EoP High 1
存储 NotificationChannel 的时候,限制附带的 VibrationEffect 的大小。

CVE-2024-49737 EoP High 1
调用 startActivityInTaskFragment 的时候已经 clearCallingUid 了,之后在 AMS 里构造 SafeActivityOptions 的时候会拿到自己的 uid,改成调用的时候就主动按照已经记录的 calling uid/pid 构造 SafeActivityOptions 再传过去。只影响 13+。看了眼致谢信息,果然是 Michał Bednarski。

CVE-2024-49738 EoP High 1
Parcel::writeInplace() 在写入数据之前先校验数据。没太看懂咋触发,看描述是越界写。

CVE-2024-49744 EoP High 1
此漏洞由我发现并报告。
AccountManagerService checkKeyIntentParceledCorrectly 里只检查了 intent 的类型,没有检查再次反序列化后 intent 的类型,可以通过 bundle mismatch 的方式绕过。关于绕过了有什么用,可以查看这一篇: https://konata.github.io/posts/creator-mismatch/
注:这个漏洞看起来很明显,但实际上并不好利用,ChooseTypeAndAccountActivity 或者 AddAccountSettings 等常见利用点都已经在之前的 CVE-2023-20944 跟 CVE-2023-21124 的补丁里加上了 new Intent 使得我们伪造的 intent 并不会直接传递给 startActivity。那么如何触发这个漏洞呢?这里先留一个悬念,读者可以自己先思考思考。

CVE-2024-49745 EoP High 1
Parcel::growData 中如果现在的 data position 已经大于 data size 就直接返回。看漏洞描述是越界写。

CVE-2023-40108 ID High 1
SettingsProvider 中设置铃声的时候校验这个 URI 确实是一个音频文件。追代码可以看见后面会读取这个 URI 把它写进一个 cache dir 里,这里是以 SettingsProvider 的权限去读取,SettingsProvider 跑在 system_server 里所以是 system 权限,然后这个 cache file 可以通过调用 SettingsProvider 的 openFile() 打开,返回一个 InputStream 而不需要任何权限,所以这里是一个以系统权限读取任意 URI。
很早就知道的老问题了,属于有生之年系列,最早应该是 CVE-2022-20353 在 Settings 调用铃声选择器的时候加了校验,看代码可以发现 com.android.dialer.app.settings.DefaultRingtonePreference 也有类似问题(这个月的 CVE-2023-40132),而且铃声属于 Settings.System 组,可以被拿到读写系统设置权限的 app 手动触发。这次在根源上修复了,应该不会再有问题了。

CVE-2024-49733 ID High 1
此漏洞由我发现并报告。
Settings 的 ServiceListing 始终显示被启用了的服务,保证用户也能看见有些校验不通过但是已经被启用的服务。
感觉类型给错了,应该给 EoP 的

Media Framework

CVE-2023-40132 EoP High 1 2
跟上面的 CVE-2023-40108 是一个问题,不再重复解释了。这个补丁比上面那个补丁更早,是在运行在客户端的 RingtoneManager 里也加上文件类型检查,应该是给 com.android.dialer 或者其他 app 的情况用的。上面那个补丁应该也能覆盖这个漏洞的情况。

System

蓝牙开会了属于是
待分析漏洞:CVE-2024-49742 CVE-2024-49734

CVE-2024-43096 RCE Critical 1
蓝牙 GATT 协议栈 build_read_multi_rsp 函数中 MTU 等于 0 时的越界写入,补丁判断了这种情况直接返回。

CVE-2024-43770 RCE Critical
CVE-2024-43771 RCE Critical
CVE-2024-49747 RCE Critical
CVE-2024-49748 RCE Critical
1
四个漏洞都在 GATT 里,在四个不同的地方都没有检查 gatt_tcb_get_payload_size 的返回值是不是 0,而这个函数在 channel 已经被关闭的时候会返回 0,在后续造成越界写。
影响函数:
CVE-2024-43770 gatts_process_find_info
CVE-2024-43771 gatts_process_read_req
CVE-2024-49747 gatts_process_read_by_type_req
CVE-2024-49748 gatts_process_primary_service_req

CVE-2024-49749 RCE High 1
giflib 解析 gif 的过程中,判断 width 和 height 是否非法的 if 语句存在逻辑错误,变成了只有三项条件全部成立才认为文件非法,应该是三项条件里只要有一项成立就为非法。导致后续计算 size 的时候发生整数溢出进而越界写。

CVE-2024-34722 EoP High 1
蓝牙配对过程中的授权绕过问题,去年补丁就放过一次,看起来是没修复完

CVE-2024-34730 EoP High 1
蓝牙 HID 链接过程中的权限绕过问题

CVE-2024-43095 EoP High 1
改的地方很多,看起来是动态添加的权限相关的自动授权问题

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 里隐藏遮盖窗体,防止点击劫持。

CVE-2024-43763 DoS High 1
一个蓝牙的逻辑 bug,应该只会造成功能异常?

CVE-2024-49736 DoS High 1
禁止在 DSU 模式下恢复出厂设置。可能是因为 DSU 用的 data 不一样,所以不会再要求输入一遍锁屏密码?

MagiskEoP (CVE-2024-48336): Magisk App Arbitrary Code Execution Vulnerability

作者 残页
2024年8月24日 12:00

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.

Demo video for exploit this vulnerability to silently obtaining root privileges and granting root to any app: https://github.com/canyie/MagiskEoP/blob/main/screen-20220302-093745.mp4

Steps to reproduce this vulnerability:

  1. Install vulnerable Magisk app builds on a device that has no GMS preinstalled
  2. Install this exploit app
  3. Force stop Magisk app and this exploit app
  4. Open Magisk app
  5. Open this exploit app, type your commands and press Execute to execute them with root privileges

Vulnerability Info

Name: Magisk App Arbitrary Code Execution Vulnerability

Alias: Magisk Privilege Escalation Vulnerability

The Basics

Product: Magisk

CVE: CVE-2024-48336

Reporter: @vvb2060

Initial Report Date: 2024-08-01

Patch Date: 2024-08-21

Disclosure Date: 2024-08-24

Affected Versions: Manager v7.0.0 ~ Canary 27006

First Patched Versions: Canary 27007

Issue/Bug report: https://github.com/topjohnwu/Magisk/issues/8279

Patch CL: https://github.com/topjohnwu/Magisk/commit/c2eb6039579b8a2fb1e11a753cea7662c07bec02

Bug-introducing CL: https://github.com/topjohnwu/Magisk/commit/920b60da19212dd8d54d27ada77a30067ce50de6

Bug Class: Unsafe Dynamic External Code Loading

Weakness Enumerations:

Summary

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.

Android 平台常见安全漏洞类型

作者 残页
2024年11月5日 06:00

本文适用于已对 Android 开发有基础了解,希望了解 Android 系统层常见安全漏洞的人。祝大家写代码无 bug,挖洞天天挖到 Critical RCE 漏洞链。
本文开始创作时间:2024-02-19 完成时间:2024-02-29 发布时间:2024-11-05

过年了,不要再讨论什么 CVE、CNVD、CNNVD 之类的了。你的漏洞们不能给你带来任何实质性作用,朋友们兜里掏出一大把钱吃喝玩乐,你默默的在家里打开你的 Test PLMN 。亲威朋友吃饭问你收获了什么,你说我的漏洞被谷歌评级高危了,Android 14 最新安全补丁都能用,亲戚们懵逼了,你还在心里默默嘲笑他们,笑他们不懂你的 BAL,不懂什么是 BG-FGS,不懂怎么利用 confused deputy 类型漏洞进行跨用户读取,不懂 LaunchAnywhere,不懂 PendingIntent 要 FLAG_IMMUTABLE,笑他们手机上的拼多多。你父母的同事都在说自己的子女一年的收获,儿子买了个房,女儿买了个车,姑娘升职加薪了,你的父母默默无言,说我的女儿天天在家里对着电脑上的一堆英文发呆,嘴里念叨谷歌怎么还不回我,有时候还给我们发一堆乱码的文件。

权限,没有你我怎么活啊权限

Android 权限机制概述

权限肯定是每个 Android 开发者都用过的东西,比如应用要访问网络,就要像这样加上网络权限:

1
<uses-permission android:name="android.permission.INTERNET" />

每个应用在被安装时都会被赋予一个 UID,包括系统应用。一般情况下,每个应用都会有一个独一无二的 UID,除非应用使用 sharedUserId 与其他应用共享 UID(这里忽略配置了 isolatedProcess=true 的服务进程,它们使用随机 UID 且几乎没有任何权限)。Android 系统内部也使用 UID 区分调用者。

Android 中通过 getSystemService() 可以拿到一堆的 XxxManager,而几乎所有的这些 XxxManager 都会和 system_server 暴露出来的一个 XxxManagerService 进行交互。应用调用这些系统服务提供的接口时,如果需要权限,系统会首先校验权限。以接口 ConnectivityManager.getActiveNetwork() 为例,它需要 android.permission.ACCESS_NETWORK_STATE 这个权限,所以 ConnectivityService 就会主动校验这个权限:

1
2
3
4
5
6
7
8
9
10
11
12
private void enforceAccessPermission() {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.ACCESS_NETWORK_STATE,
"ConnectivityService");
}

@Override
@Nullable
public Network getActiveNetwork() {
enforceAccessPermission();
return getActiveNetworkForUidInternal(mDeps.getCallingUid(), false);
}

这里的 enforceCallingOrSelfPermission 内部就会调用 Binder.getCallingUid() 拿到调用者的 UID 然后检查这个 UID 是否具有对应权限。所以实际上,鉴权用的是调用者 UID 而不是包名。

权限还有一种特殊类型,叫做特殊权限,定义时在 protectionLevel 中加入 appop 的就是。特殊权限描述一组对系统有特殊意义的权限,如悬浮窗权限和修改系统设置权限。设置中有一个“特殊应用权限”的页面专门用来控制这些特殊权限。要检查这些特殊权限的授权状态需要用到 AppOpsManager

系统中还有很多别的安全机制,如 SELinux、capability 和 seccomp。这里略过不讲,感兴趣的可以自行搜索。

想要以一个系统开发者的身份了解更多关于权限的知识,可以查看谷歌官方说明文档:Android permissions for system developers

常见漏洞类型:主动鉴权不当

CVE-2023-40094:鉴权缺失

补丁链接:Require permission to unlock keyguard

此种类型的漏洞可谓是最简单最经典的漏洞,对 Android 权限模型足够熟悉的人可能看一眼就能知道有问题,且漏洞危害性往往较高,如此例的 CVE-2023-40094,允许任意 app 调用特权 API 无密码解除锁屏,Google 也给出了高危评级。这种漏洞简单但可遇不可求,要在 AOSP 上百 GB 的源码仓库中发现未被适当保护的特权 API,可谓是大海捞针,捞到一个白嫖一个 CVE,偷着乐去。近几个月安全补丁中大概 1~3 个月就会有这种漏洞被公开,说明这种漏洞可能还存在不少,可能集中在新加的 API 和被重构过的服务中,还有各个 OEM 添加的自定义 API。

CVE-2021-0554:鉴权在客户端

补丁链接:Enforce BACKUP permission on Service end.

Android 很多系统服务向应用暴露了自己的接口,允许应用调用。一般来说,应用这边使用的 API 叫 XxxManager,system_server 里会有一个对应的 XxxManagerService。应用这边的 XxxManager 基本上什么都不干,就只是调用 system_server 里的 XxxManagerService 而已,真正干活的是 XxxManagerService。

发现了什么吗?以 ActivityManager 为例,虽然 ActivityManager 和 ActivityManagerService 都是系统的类,但是应用调用 API 的时候,ActivityManager 这个类里的代码是运行在调用者进程的,只有 ActivityManagerService 是在系统进程的!在调用者进程意味着调用者可以随便干扰,所以所有鉴权操作都应该放在系统进程里的 ActivityManagerService 以避免被干扰。而在这个漏洞中,代码刚好写反了,app 调用 BackupManager 的时候,BackupManager 在当前进程检查一遍权限,而BackupManagerService 中的接口却没有被保护。无论是利用反射、hook,或者直接调用 IBackupManager 都可以轻易绕过权限检查。

这个其实和上一个没什么区别了,没有人这么写代码,近几年也没再看见过类似漏洞,参考价值不大,纯当乐子看就好。

CVE-2015-6624 & CVE-2015-6625:特殊接口 dump

CVE-2015-6624 补丁:Add DUMP permission check to ResourceManagerService.

CVE-2015-6625 补丁:Add DUMP permission check to WifiScanner service.

这两个漏洞本质上都还是接口缺权限校验,但漏洞点位于特殊接口 dump 内。dump 是 binder 内置的接口,很少被开发者注意到,仅用于在调试时(如 dumpsys 或 cmd xxx dump)输出信息。为了保护这些敏感数据,调用者应该具有 android.permission.DUMP 权限,而大部分接口也确实检查了权限,但仍然不排除有开发者失误的情况。识别这种漏洞可以尝试使用自动化工具,多设备批量对所有系统服务进行测试。不过现在 CTS 测试会确保没权限的时候调用 dump 任何服务都不会返回正确信息,所以应该也没什么挖掘价值。

CVE-2020-0109:特殊接口 shellCommand

补丁链接:Fix notification shell commands

和上一条类似,不过这里是特殊接口 shellCommand,该接口用于 adb shell 命令行调试时调用 cmd xxx yyy。和上一条的 dump 相比,这一项更为复杂一点:

  1. Binder.java 中默认的 onShellCommand 实现会保证调用者 UID 为 root 或 shell,不满足直接拒绝,然后调用 handleShellCommand 实现具体操作,但 AOSP 中存在大量服务类直接重写了 onShellCommand 而非 handleShellCommand,使得这个校验被跳过;
  2. 即使服务没有使用 handleShellCommand,如果 onShellCommand 内正确检查了调用者权限或调用者 UID,那也是安全的;
  3. 即使未直接检查调用者权限,如果 ShellCommand 内调用的都是会检查权限的公开接口(且没有 clear calling id),那也可以认为安全。

在这个漏洞中,部分操作未正确检查调用者权限,使得攻击者可以无权限调用系统内部私有接口造成漏洞。我发现的 CVE-2024-31318 就属于这种漏洞。这种漏洞近几年也不多见,了解一下就行。

CVE-2021-0683:不正确的 ShellCommand 实现

补丁链接:Fix side effects of trace-ipc and dumpheap commands

本例中,ActivityManagerShellCommand 会在 ActivityManagerService.onShellCommand 中被调用,所以这里的代码运行在 system_server 进程,有着系统权限,直接调用 file.delete() 是以系统特权执行的,而很明显原本调用者不应该有删除系统文件的权限,造成漏洞。可能你会说,就算不删除文件,底下 system_server 一样要打开这个文件才能往里写入内容啊,这不是一样会覆盖原文件?请注意这里使用的是 openFileForSystem,这个函数实际上不会自己打开文件,而是使用调用者传递的 ShellCallback,让调用者进程打开文件然后返回文件描述符,所以实际上还是用调用者本身的权限。其他系统服务中,直接接受文件路径的接口(如 pm install /path/to/apk)也都应该这样实现,避免越权。

CVE-2021-0327 & CVE-2021-0398:一行 clearCallingIdentity 引发的惨案

CVE-2021-0327 补丁:Ensure caller identity is restored in CP quick-path.

CVE-2021-0398 补丁:Set mAllowWhileInUsePermissionInFgs correctly when bindService() from background.

还记得上面提到的 Binder IPC 鉴权过程吗?enforceCallingOrSelfPermission 等 API 实际都是依赖 Binder.getCallingUid() 返回的调用者 UID 进行鉴权的,而另一个 API clearCallingIdentity 允许让 getCallingUid 返回自己的 UID,以此绕过部分权限检查。有时这种行为是有意且安全的,而有时则能造成安全漏洞。Android 发展十几年,API 之间的调用关系错综复杂,如果有一点没考虑到,就有可能造成漏洞。要发现这种漏洞,可以使用静态代码分析工具,但仍然需要人工一个一个筛选可用项,还是体力活。

CVE-2015-6623:鉴权,但是手滑写错了

补丁:Ask for system permission to enable ePNO

太戏剧了一看就懂,应该不用多说……

除了纯粹手滑,这种漏洞比较多的出现在大版本迭代行为变更时,改了一个但是没改完的情况,如 CVE-2015-3833,Android 5.0 废弃 getRecentsTask 方法,只在调用者拥有 android.permission.REAL_GET_TASKS 特权时才返回其他进程信息,但是 getRunningAppProcesses() 没任何权限检查,调用这个接口可以变相达成 getRecentsTask 的效果。

CVE-2020-0107 & CVE-2020-0246 & CVE-2023-21092:调用者伪造 package name

CVE-2020-0107 补丁:Check UID in getUiccCardsInfoSecurity

CVE-2020-0246 补丁:Add package checking with Uid in EuiccController#getEid

CVE-2023-21092 补丁:Checking if package belongs to UID before registering broadcast receiver

虽然大部分情况下鉴权只需要一条 enforceCallingOrSelfPermission,但有些时候仍然需要调用者的包名,而 Binder 默认只支持返回调用者的 UID 和 PID(PID 不可靠,调用者调用到一半被杀了,或者调用者指定 FLAG_ONEWAY 表示此次调用是异步调用时会是 0),因此遇上这种情况时,通常都是在参数中加一个 packageName 让调用者传递自己的包名。注意这里所谓的 packageName 是调用者主动传递所以完全受控于调用者,服务端必须检查传递的包名属于调用者 UID,而万一有一个地方粗心大意漏了,就有可能造成漏洞。实际上不只是 AOSP,OEM 代码中也出现过类似漏洞,如三星的 SVE-2021-23076 (CVE-2021-25510, CVE-2021-25511)

CVE-2021-0319:验证调用者包名,但是没完全验

补丁:Fix CDM package check

上回书说到,服务端必须保证调用者传入的包名属于调用者 UID,这种验证一般有三种方式,第一张是调用 AppOpsManager.checkPackage(),它在不匹配时会抛出 SecurityException;第二种是主动调用 PackageManager.getPackageUid() 与 calling uid 相比对,第三种是通过 isSameApp

初看漏洞代码:

1
2
3
4
5
6
7
8
private void checkCallerIsSystemOr(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);
}

用了上面提到的第一种方式,看起来好像并没有什么问题,对吧?我个人觉得即使是随便找一个安全研究员,跟他明确说这个文件里有校验包名不正确的漏洞,大概率也不会有人留意这里。

其实关键点在这里:

1
private IAppOpsService mAppOpsManager;

别被 mAppOpsManager 这个名字骗啦,它的类型不是 android.app.AppOpsManager,而是内部的 com.android.internal.app.IAppOpsService!虽然它们都有 checkPackage() 方法,但是前者在检查失败时会抛出异常,而后者只是返回错误而已!当初这段代码的作者应该也是觉得自己在用前者,结果实际上是后者,才造成漏洞。这种漏洞很少见,可以通过静态代码搜索找到所有对 IAppOpsService.checkPackage() 的调用然后一个个检查。

鉴权虽好,可不要漏信息哦

此类漏洞通常形式:攻击者传入其他应用的包名,然后通过微小的行为差异绕过 Android 11 中的“软件包可见性”(package visibility) 判断指定应用是否已经安装。

例子1:CVE-2021-0321getPackageUid 在对应包名不存在时会返回 Process.INVALID_UID,因此满足下面的 if 直接返回,调用者拿到空列表;而对应包名存在时,会调用 enforceCallingPermission(android.Manifest.permission.DUMP, function),调用者收到 SecurityException。这个微小的行为差异造成了信息泄漏。

例子2:CVE-2021-0975,包名存在时抛出的异常信息为 "package " + packageName + " does not match caller's uid " + uid 而不存在时是 "package " + packageName + " not found",微小的信息差异造成信息泄漏。

这种漏洞非常非常多,我简单搜索了一下,仅仅 Android 14 一个大版本中就修复了至少 37 个类似的漏洞,已被修复的类似漏洞预计已达上百个,可以想象 AOSP 中还有多少。此种漏洞谷歌一般评级仅为 Moderate,获得的赏金上限仅为 $250 且没有 CVE 编号,不值得专门去找,适合 code review 时顺手提交上去赚赏金。

图:NVD 搜索结果

设备管理与多用户

Android 从 4.2 开始加入了多用户功能,允许多个用户公用一台设备,每个用户可以安装各自的 app,数据互相隔离,一个用户通常情况下无法跨越用户边界读取或操作其他完全用户的数据(但管理员可以操作设备部分功能是否对其他用户开放)。跨用户操作通常需要 INTERACT_ACROSS_USERS INTERACT_ACROSS_USERS_FULL 等系统权限,如果有方法能绕过跨用户限制,就会被视为安全漏洞。部分关键系统应用可以只在主用户运行,其他用户访问到的只是主用户的实例。

Android 同时支持设备管理。系统层提供 DevicePolicyManager 用于设备管理员控制设备,常用的有 lock task mode 可用于锁定设备或限制设备只能运行某几个应用(专用设备等用途,如自动售货机)和 User Restrictions 用于阻止用户操作特定设备功能,如禁用 WiFi、禁用蓝牙等。如果有方法绕过这些限制也会被视为安全漏洞。

CVE-2019-2098 & CVE-2021-0686:API 缺失跨用户检查

上文提到,为了支持多用户,很多 API 的参数里都加上了一个 userId,而跨用户操作需要系统权限,这需要服务端主动鉴权,API 这么多总会有一两个漏掉的。另一种形式是要求调用者传入 UID,但调用者可以传入属于其他用户的 UID。下面两个漏洞就是很经典的漏跨用户检查。

CVE-2019-2098 补丁:Add cross user permission check - areNotificationsEnabledForPackage

CVE-2021-0686 补丁:Add cross-user check for getDefaultSmsPackage().

CVE-2023-21107:组件缺失跨用户

漏洞补丁:Enforce INTERACT_ACROSS_USERS_FULL permission for NotificationAccessDetails

很多时候我们关注跨用户只关注系统提供的带有 userId 参数的 API,而忽略了系统应用。作为一个关键系统应用,系统设置拥有跨用户权限,此漏洞中的一个 Fragment 接收外部传递的 user handle 而没有任何输入校验和权限检查,然后直接开始使用这个 user handle,这样就使得攻击者可以利用一个低权限的恶意应用,打开 Settings 并传入其他用户的 handle,让高权限的 Settings 错误访问和操作其他用户的数据。这种“低权限个体欺骗高权限个体执行特权操作”的攻击模式被称为 confused deputy,虽然此例也可以说 missing permissions check 或者 missing input validation。
事实上在编写这篇文章的过程中,我顺手搜了一下该漏洞中用到的 Intent.EXTRA_USER_HANDLE,然后就发现 Settings 中的另一个被称为 AppInfoBase 的 fragment 也存在一模一样的漏洞,喜提天上掉下来的高危漏洞 CVE-2024-43088。

CVE-2023-21123:缺失 User Restrictions 检查

漏洞补丁:Add DISALLOW_DEBUGGING_FEATURES check

DISALLOW_DEBUGGING_FEATURES 是一个 user restriction,可以由设备管理员设置以关闭调试相关的功能。Tracur 是一个系统内置的应用,和 trace 相关,而 trace 是调试功能。此例中,Tracur 没有检查 DISALLOW_DEBUGGING_FEATURES,虽然设置中开发者选项打不开,但是利用一个应用可以直接调起 Tracur 从而操作 trace。

个人观察到的是 Android 12-13 刚发布的时候出现了很多这种 restriction bypass 的漏洞,可能是 Android 12 中将系统设置、SystemUI 等应用程序重构为新的 Material You 设计风格时进行了较大的代码改动所导致。笔者也曾经提交过类似漏洞,只获得了 Moderate 评级,摸不清 Google 心情。

CVE-2021-0691:SELinux 权限配置不当

上文说,Android 除了使用 UID 等 Linux 传统的 DAC 机制,还使用 SELinux 这一 MAC 机制进一步缩减进程权限,确保系统安全。而本节中提到的 CVE-2021-0691 就是 SELinux 权限配置过大导致。补丁链接:system_app: remove adb data loader permissions

可以看见,原本的策略允许 system_app 写入 apk_data_file 即已安装应用的 apk 文件,可以覆盖其他应用的 dex/so 等文件进而向其他应用持久注入代码。虽然攻击只能由系统的 system_app 发起,这满足了 分级调节规则 中【需要作为特权上下文运行才能执行攻击】一条,严重程度被降为 moderate,但如果把这个漏洞和 system_app 中的其他漏洞如路径穿越写漏洞结合起来,就能组成一条非常有威力的漏洞链。事实上,此漏洞本身就是“魔形女”漏洞链的重要一环。想了解这一漏洞链可以参考这篇文章。历史上也出现过权限配置错误造成的严重程度更高的漏洞,通杀联发科设备的 MediaTek-SU 漏洞(CVE-2020-0069)就属于此类。

虽然为了安全,Android 的 SELinux 中存在大量 neverallow 规则保证 OEM 不会添加太过离谱的规则,修改 neverallow 会导致无法通过 CTS,但我们确实发现有一些 OEM 确实允许了被 neverallow 的条目。只能对 OEM 质量一声叹息。

组件与意图

每个 Android 程序员都知道的 Android 四大组件:Activity Service BroadcastReceiver ContentProvider。Intent 则是与组件交互的桥梁。有时,不经意间的不当使用,也许就会造成漏洞。

CVE-2021-0693:组件权限配置不当

漏洞补丁:Don’t export HeapDumpProvider.

组件的 exported 属性表示该组件是否可被外部应用访问,若没有设置则有 intent-filter 的组件默认导出(target Android 12+ 的 app 如果出现这种情况会被直接拒绝安装)。即使导出了组件,也可以设置 android:permission 等属性来限制只有拥有对应权限的应用才能访问此组件。本例中受害 provider 设置 exported=true 即导出,同时没有设置访问权限,任何应用都能直接访问,而访问 HeapDumpProvider 能获取到调试用的其他 app 的 heap dump,显然是敏感信息,从而造成非常明显的安全漏洞。

这种漏洞前几年比较多,早期甚至出现过把 android:exported="true" 误写成 exported="true" 这种漏洞(CVE-2013-6272),随着系统的日益完善成熟,近几年观察到的少了。后续挖掘重点可以集中在 OEM 自定义的组件上。

LaunchAnywhere:危险的 Intent Redirection

大名鼎鼎的 LaunchAnywhere 应该是最早也最经典的 Intent Redirection 类型漏洞,可惜的是年代久远没有找到 CVE 编号,只有一个 Bug ID A-7699048。

这个洞网上的详细分析已经有很多了,这里不赘述,只简单介绍一下它的基本原理:应用 A 通过 AccountManager API 添加账号时,AccountManagerService 会请求 account 的 authenticator(也是一个应用),而这个 authenticator 可能需要向用户显示一个界面,所以可能返回一个 intent,此时 AccountManagerService 把 intent 返回给 A,在 A 中运行的系统代码会直接调用 startActivity 启动这个 intent,这里用的是 A 的身份,从而可以无限制访问应用内部 Activity 组件。如果 A 是 Settings,它拥有系统权限,此时就可以访问所有应用的所有 Activity,无视它是否导出或是否被权限保护。

Intent Redirection 是 Android 平台最经典的漏洞类型,已在 Android 系统、定制系统、三方应用程序等中多次出现过。其基本特征为收到别人发送的 intent,随后将其转发出去,常见 API 有 startActivitysendBroadcastsetResult 等。前两个很明显,可以访问内部组件,而 setResult 虽然不能直接访问内部组件,但攻击者可以通过指定 intent 的 data 为应用内部受保护的 URI,并在 intent flags 中指定 FLAG_GRANT_READ/WRITE_URI_PERMISSION,当受害应用使用恶意 intent 调用 setResult() 时,就会不知不觉授予恶意应用读写自身内部 content provider 的权限。

在 Android 12 中,为了规避这类问题,Strict mode 引入了新的功能,可以检测到应用收到(getParcelable)别人发来的 intent 并将其立即转发的情况,一定程度上帮助了开发者识别此类漏洞。但是,这个工具并不能扫描到所有危险,而且直接转发 intent 并不是 intent redirection 的唯一一种类型,CVE-2022-20550 & CVE-2024-0015 就是一个例子,特权应用接收不可信的 ComponentName 然后直接创建指向该 ComponentName 的 intent 并 startActivity,也能够越权访问组件。补丁在这里:Fix vulnerability that allowed attackers to start arbitary activities

CVE-2014-8609(BroadcastAnywhere):危险的 Pending Intent

上文提到的 Intent 其实还漏了一种特殊形式,即 PendingIntent。简单来说,PendingIntent 由应用主动创建,代表某项特殊的操作,可以传送给别的应用,别的应用发送这个 PendingIntent 时就以 PendingIntent 创建者的身份发送。创建时可以指定 PendingIntent 是否可变,如果可变则允许发送者修改未被显式指定的 intent 字段,如 action、data、flags 等。为了安全起见,默认 selector 和 ComponentName 是不允许被修改的。

回过头来看漏洞补丁:SECURITY: Don’t pass a usable Pending Intent to 3rd parties.

很明显,这里的 mPendingIntent 创建时使用了一个空的、啥都没有的 intent,同时未指定 FLAG_IMMUTABLE(虽然实际原因是这玩意在 Android 6.0 才加,那个年代还没有这玩意),这使得攻击者拿到这个 PendingIntent 之后可以任意改写里面的 intent 再发送,同时由于这个 PendingIntent 是 Settings 创建的,具有系统权限,攻击者发送 PendingIntent 时会以创建者身份发送,也就同样是以系统权限。这里 PendingIntent 是用的 getBroadcast,最后 send 的时候也会以广播方式发送,比如攻击者改写 action 字段为 android.intent.action.MASTER_CLEAR,广播发送出去后就会触发 MasterClearReceiver 进行恢复出厂设置的操作。

事实上,就算是开发者记得填充 action,有时候也不能避免漏洞的出现。恶意应用可以在自己的 AndroidManifest.xml 中注册相同 action 的 intent-filter,改写 package 指向恶意应用自己,flags 添加授权 flags,data/clipdata uri 指向受害应用私有的或者可访问的 ContentProvider 并发送,此时恶意应用就会被授予权限。更多可以查看 OPPO 的这篇文章:PendingIntent重定向:一种针对安卓系统和流行App的通用提权方法——BlackHat EU 2021议题详解 (下)

(注:为了安全考虑,如果受害者是 root/system UID,授权时要求 URI 必须是几个特定 authority 否则会被拒绝,但对于系统中及应用市场中的海量应用,它们仍可能被攻击)

为了保证安全性,Android 6.0 添加了 FLAG_IMMUTABLE 用于指定 PendingIntent 不可变,而 Android 12 添加了 FLAG_MUTABLE,target Android 12+ 的应用创建 PendingIntent 时必须显式指定可变性,不再让应用开发者随手写下的 0 变成安全漏洞,自此这一类漏洞销声匿迹。但有部分场景 PendingIntent 仍然是可变的,此时就要万分小心。

CVE-2017-0639:危险的 URI

URI 在 Android 中非常常用,如常见的分享文件操作,就是创建一个 ACTION_SEND 的 intent,把要分享的文件的 URI 放到里面,然后 startActvity。常见的有安全影响的 URI 有 content uri 和 file uri(Android 7.0+ 已弃用)等。

这里其实有一点要注意的,就是接收到的 URI 可能指向原本发送者不应该有权限访问的数据,如果受害应用不加检查直接使用,就有可能造成信息泄漏。本例的 CVE-2017-0639 就是这样一个漏洞,漏洞补丁:OPP: Restrict file based URI access to external storage

作者写的文章也可以看一下。这里提一个点就是,禁止向外传出 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

广播权限与保护广播

上文中说到四大组件都可以定义权限,限制只能有对应权限的个体访问,但其实对于广播来说,它还有额外的保护机制。

我们常用的 BOOT_COMPLETED 等广播都是只有系统能发送,如果应用发送了这种广播,会发生什么?答案是会被系统拒绝发送。这一系列的广播对系统极为重要,如果第三方应用可以随意发送,比如发送 android.intent.action.REBOOT,系统就重启了;发送 android.intent.action.MASTER_CLEAR,系统就会开始恢复出厂设置。当时为了解决这个问题,一种方法是限制所有的系统广播接收器必须加上权限,只有拥有系统权限才能访问,但是难搞的是,这些广播中有一些是粘性广播(sticky broadcasts),它在系统中会长时间存留,直到后一个覆盖前一个,这样应用仍然有机会覆盖粘性广播的数据;另一个选择是,给这些广播都定义上权限,只有有权限的个体才能发送,但是给这些广播一个一个定义专用权限显然不现实,于是保护广播便诞生了,还是限制只有系统能发送,但是没有像传统一样定义特别的权限。很显然,这么多危险的广播,如果有一个被漏掉忘记被保护,或者定义保护广播没有生效,就会出现严重的安全漏洞。事实上,历史上确实发生过类似的漏洞:CVE-2017-0601CVE-2020-0391

另外一点,系统内部也大量应用了广播机制传递信息,广播这玩意和 Activity Service Provider 都不同,一次广播可能被多个个体接收,如果说系统内部发送广播传递内部信息的时候没有指定接收者,那就有可能被第三方应用拿到敏感信息。但是,我们不可能把所有已知会接收这个广播的可信应用硬编码在代码里,由此就引入了另一个安全机制,不仅广播接收者可以指定要有某某权限才能给我发广播,发送者也可以指定接收者必须有某某权限才能收到广播,利用现有的权限机制解决了问题。这里可能出现的漏洞就是写代码的人粗心大意忘记指定权限,导致信息被其他不相关应用接收。

(题外话,其实隐式 intent 启动 Activity 也有可能出现类似问题,三方应用定义一个相同的 intent-filter 就有机会被系统启动,不过这种漏洞偏少,启动 activity 只会启动一个,可以通过在 intent-filter 配置大于 0 的优先级来解决。不过也不是没有,CVE-2020-0396)。

Parcel Mismatch

其实这一段不应该放在组件与意图的,但是真的不知道该放哪。

除了传统的 Java 序列化方法,android 还为跨进程通信专门设计了一套轻量级的序列化方案:Parcelable。和 Serializable 不同,使用 Parcelable 要求开发者手动往 parcel 写入或读取数据。以下展示了一个典型的 Parcelable 实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Person implements Parcelable {
private String name;
private int age;
private int money;
public static final Creator<Person> CREATOR = new Creator<Person>() {
@Override public Person createFromParcel(Parcel in) {
Person obj = new Person();
obj.name = in.readString();
obj.age = in.readInt();
obj.money = in.readInt();
return obj;
}

@Override public Person[] newArray(int size) {
return new Person[size];
}
};

@Override public int describeContents() {
return 0;
}

@Override public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(age);
dest.writeInt(money);
}
}

假如这里程序员手滑了,write 写入的数据和 read 读取的数据不匹配,比如说一边写了 long 另一半读了 int 或者忘记读某个字段,会发生什么问题吗?

查看以上代码,两次 readInt 调用同一个方法能读取到不同的数据,而且读取顺序和写入顺序完全相同,这说明 Parcel 内部必定存在着一个偏移值,每次 read 读取当前偏移的数据然后自增偏移。假如 read 没有完全消费它写入的所有值,那么 parcel 中残留的值可能会影响后续值的解析。

举个例子,假如有如下 aidl 函数定义:

1
void registerPerson(Person person, int flags);

假如 Person 中最后一行 obj.money = in.readInt() 缺失了,那么 Person 对象反序列化完之后,还会有一个 int 残留在 parcel 内,接下来尝试读取 flags 调用 readInt 时实际上读取到的是 Person 残留的 money 而不是正确的 flags。

再回过头来看一下之前 LaunchAnywhere 漏洞,当时的修复补丁是,authenticator 返回 intent 后 AccountManagerService 检查 intent 要调起的 activity 的包的签名是否与 authenticator 自身匹配,匹配才发送给调用者让它打开。这里有一个点,authenticator 返回的数据类型实际上是 Bundle,intent 也是放进 bundle 里存储的。Bundle 这里可以简单理解为一个 map,存储着键值对,里面值的类型除了可以是基本类型和 String,还可以是任意 Parcelable 对象。

这两点结合起来,会碰撞出怎样的火花呢?注意上面检查 bundle 是在 AccountManagerService 中,它在 system_server 中运行,而实际调起 activity 的操作在调用者进程中,所以这里 bundle 从 system_server 到 app 还要经过一次序列化和反序列化。我们已经知道 bundle 是一个 map 且可以存储任意 Parcelable 对象,那假如我们在 bundle 中任意放一个反序列化错位的对象,就有机会污染它后面的键值对,通过精心构造内存布局,我们甚至能让 AccountManagerService 检查时找不到 intent,而经过一次序列化和反序列化传输到 app 后却能找到 intent,绕过 LaunchAnywhere 漏洞的修复!由于 Bundle 接受任意 Parcelable,所以实际上任何有问题的 Parcelable 都能拿来这样利用!这种技术有一个特别的名字,叫 Self-changing Bundle。2023 年国内某电商软件大肆在野利用的其中一个漏洞 CVE-2023-20963 就属于此类漏洞。获得了以系统权限启动任意 Activity 的能力后,可以利用系统内部一些 activity 的 intent redirection,设置 data 和 flags 拿到内部 content provider 如一些 file provider 的读写权限,可以越权读写系统关键文件,改写系统配置;另一种利用方案是攻击其他应用未导出的组件,读写应用内部数据甚至向其他应用注入恶意代码。

由于 Google 已经意识到这类漏洞的强大破坏力,在 Android 13 中引入了多个机制缓解此类漏洞,如 Lazy BundleParcel.enforceNoDataAvail()。另外 Google 在 AccountManagerService 引入了一个新的 checkKeyIntentParceledCorrectly 函数,校验 bundle 中的 intent 反序列化前后是否一致,并把该补丁一路向下 backport 到了 Android 11,算是基本封死了这类利用 bundle 的方法。

更多关于这类漏洞的细节,可以参考以下文章:

同时强烈建议查看 Michał Bednarski 的 GitHub 主页,大部分都是 parcel mismatch 类漏洞且每一个都有很详细的 writeup。

组件启动的后台限制

Android 系统对在后台运行的应用施加严格限制,如果应用有办法绕过这些限制,就可能被视为安全漏洞。本节我们主要关注以下限制:

同时我们定义以下缩写:

  • BAL:Background activity launch,绕过上面的第一个限制
  • BG-FGS:Background-Foreground Service start,绕过上面的第二个限制
  • WIU:while-in-use 权限,简称前台权限,指用户仅允许应用“在使用中才能获得”的权限

恶意应用利用 BAL 漏洞可以在后台随意弹出广告,严重影响用户对手机的正常使用。能够绕过 BAL 限制的漏洞一般至少都会被授予 High 等级,而虽然绕过 BG-FGS 限制并不被直接视为是安全漏洞,但如果能从后台获取 WIU 权限,仍然会被认为是高危。部分此类漏洞具有相似的模式,从近几个月的安全广告来看也确实每隔一两个月都有类似漏洞出现。本节我们主要介绍利用 PendingIntent 实现 BAL。

以 Android 14 为例,系统判断 BAL 是否被允许的逻辑在 BackgroundActivityStartController.checkBackgroundActivityStart() 里,判断是否能启动前台服务的逻辑则在 ActiveServices.canStartForegroundServiceLocked()

以 BAL 为例,我们这里就不分析完整判断逻辑了,实在是太长了,详细分析绝对不是这里能写完的。我们主要关注这一小段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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);

final boolean logVerdictChangeByPiDefaultChange = checkedOptions == null
|| checkedOptions.getPendingIntentBackgroundActivityStartMode()
== ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
final boolean considerPiRules = logVerdictChangeByPiDefaultChange
|| balAllowedByPiSender.allowsBackgroundActivityStarts();
final String verdictLogForPiSender =
balAllowedByPiSender.allowsBackgroundActivityStarts() ? VERDICT_ALLOWED
: VERDICT_WOULD_BE_ALLOWED_IF_SENDER_GRANTS_BAL;

@BalCode int 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;
}

如果调用者实际上不是自己 startActivity,而是发送了由其他 UID 创建的 PendingIntent (或 IntentSender,实际上是 PendingIntent 内部实现),则 realCallingUid != callingUid 会成立,然后会调用 checkPiBackgroundActivityStart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private @BalCode int checkPiBackgroundActivityStart(int callingUid, int realCallingUid,
BackgroundStartPrivileges backgroundStartPrivileges, Intent intent,
ActivityOptions checkedOptions, boolean realCallingUidHasAnyVisibleWindow,
boolean isRealCallingUidPersistentSystemProcess, String verdictLog) {
final boolean 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,那就有可能被我们利用。

CVE-2023-21081 & CVE-2023-21099 为例,系统中 PackageManager 多个 API 接受一个 IntentSender 作为回调接口,而发送该 IntentSender 时没有指定 options 导致其为默认的 null。解决方法就是加一个 options 并且 setPendingIntentBackgroundActivityLaunchAllowed(false)

这里有一点,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)

getDefaultBackgroundStartPrivileges 在 options 为空时返回的默认值也有更改,可能不允许 BAL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static 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;
}
}

再加上大部分这种漏洞都已经被挖完了,这种漏洞在 2024 年之后基本在 AOSP 中绝迹。

想要了解更多的话,可以查看 OPPO 的这篇文章:恶意 App 后台弹窗技术手法分析

骗!偷袭!不讲武德!

上面介绍了很多技术漏洞,这一节从人出发,介绍几个原理很简单朴素的漏洞。

CVE-2018-9432:一个小小的字符串能有什么坏心思呢?

假如说,你是系统开发者,你自定义了一个特权操作,比如从用户绑定的钱包里划走一百块,同时允许应用调用你定义的接口请求这个操作,那肯定得需要得到用户明确允许才行。一般的做法是设计一个对话框,如下所示,其中 <appname> 代表发起请求的应用名称:

1
2
3
<appname> 正在请求消费 100 元人民币,此笔款项将会从您的钱包中扣除。您确认要支付吗?

取消 确认

而如果我们将应用名设置的特别长,会怎么样呢?我们精心设置一个超长的应用名,它会这样显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<XX 应用需要您同意隐私协议。
XX 应用隐私协议:
1. 本应用由 xxx 公司开发。
2. 本应用在运行过程中,需要以下权限运行:
(1). 网络权限。本应用需要此权限以连接服务器、向服务器发送数据并拉取数据。您必须授予此权限。
(2). 存储卡权限。本应用部分功能需要读写您的照片、笔记等内容,因此需要存储卡权限。我们不会滥用通过此权限获得的数据,也不会将其持久化存储或上传到网络。我们只在您使用特定功能时请求此权限,您也可以随时拒绝或撤销此权限。不授予此权限不会对其他功能产生影响。
.......


如您同意此协议,请点击“确认”继续运行。如您拒绝此协议,本应用无法运行并将自动退出。




> 正在请求消费 100 元人民币,此笔款项将会从您的钱包中扣除。您确认要支付吗?

取消 确认

这里用 <> 括起来的一大段其实都是应用名。而受限于屏幕大小限制,对话框显示的时候只能显示前面的应用名,真正的“正在请求消费”信息被挤下去了。如果用户没有注意到对话框文本可以滑动(事实上就算注意到了,估计也会认为剩下的全是又臭又长的协议),直接点了确认,就会不知不觉间损失财产。

实际生活中的支付对话框当然不可能设计的这么简单,但历史上确实出现过此类漏洞,如 CVE-2015-3878 屏幕录制授权欺骗漏洞、CVE-2017-13242 蓝牙配对欺骗漏洞、CVE-2018-9432: 蓝牙通讯录访问欺骗漏洞等。感兴趣的可以查看这篇文章:Android 中的特殊攻击面(一)——邪恶的对话框

高版本 Android 系统对话框做了一些更改,如权限请求对话框的“允许”“拒绝”选项现在和对话框的内容放在一起,用户必须完全滚动到最下方才能点到按钮,算是基本杜绝了此类漏洞。

CVE-2021-0314:UI 覆盖与点按劫持

Android 有一个功能,允许应用在其他应用之上显示内容,通常简称为“悬浮窗”。早在 Android 4.x 时代,恶意开发者就已经滥用这个权限制作恶意软件,如在设备开机时显示全屏悬浮窗阻止用户使用手机,从而勒索钱财,即所谓的“锁机软件”。通过这个功能,还可以实现很多有意义的攻击。还是以上面的对话框为例,这次我们不使用超长应用名称,而是利用悬浮窗功能,在对话框显示的同时覆盖我们自己的内容到对话框的文本上面,用户很容易就会被误导点击确认。

这种类型的攻击叫做点按劫持攻击(Tapjacking Attack)。对大部分情况来说,能够被其他应用覆盖并不是安全问题,而对于敏感对话框,可以通过申请 HIDE_NON_SYSTEM_OVERLAY_WINDOWS 权限并对当前 window 添加 SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS 这个属性或调用 setHideOverlayWindows 来隐藏所有的非系统悬浮窗,还可以通过 filterTouchesWhenObscured onFilterTouchEventForSecurity 等 API 过滤可能被劫持的输入事件。但实际上,Android 系统自身也出现过多个忘记对敏感对话框使用防御措施的漏洞,如此例的 CVE-2021-0314 便是卸载应用的确认对话框。

想要了解更多,可以查看以下文章:

宁为玉碎 —— 拒绝服务类攻击

Android 的漏洞分类中,还有一种特殊的漏洞,既不能像影视剧里的黑客一样敲敲键盘入侵敌国核弹系统,也不能泄漏别人的银行卡密码,它能做的只有使手机工作出现异常。它就是拒绝服务类漏洞。虽然不像 RCE、EoP、ID 那么亮眼,但也不能小看这类一不小心就让你手机变砖头的漏洞。在 Android 的漏洞严重程度定义中,有如下内容:

严重(Critical):设备遭到远程发起的持久性拒绝服务攻击(永久性损坏、需要重新刷写整个操作系统或恢复出厂设置)
高(High):设备遭到本地发起的持久性拒绝服务攻击(永久性损坏、需要重新刷写整个操作系统或恢复出厂设置);攻击者可以在没有用户互动的情况下远程阻止对移动网络或 Wi-Fi 服务的访问(例如,用格式不正确的数据包使移动网络无线装置服务崩溃)
中(Moderate):设备遭到远程发起的设备暂时性拒绝服务攻击(远程挂起或重新启动设备)

能达到“严重”程度的 DoS 漏洞很少见,看见过的几个都是 TextView 文字渲染的崩溃或者死循环。我们主要瞄准严重程度“高”的漏洞。

想要“持久性拒绝服务攻击”,比如让手机系统崩溃开不了机无限重启,除了利用系统本身的缺陷,很容易能想到的还有传统 DoS 中的“资源耗尽”,简单来说,占用系统大量资源使其停止工作。但是,让 system_server 崩溃一次最多只会造成系统软重启一次,并不算持久。怎么样才能持久呢?

“持久”,这两个字能描述的东西,还有数据。如果能够将恶意的数据保存下来,系统每次启动尝试去读取它的时候就都会崩溃,陷入死局。而所谓“能让系统崩溃的恶意数据”除了精心构造的、利用代码本身问题的数据,很容易能想到的还有超大量的数据,在系统处理的过程中耗尽系统内存资源触发崩溃。以 CVE-2021-0934 为例,Account 就是要被持久化存储的数据,虽然已经考虑到系统资源负担,对 Account 内字符串的大小及 Account 数量做出限制,然而字符串大小限制在客户端,能被绕过。这里注意,binder 一次能传输数据的大小也是有限制的,大概在 1mb 左右,所以还不能一次传太大,只能一个一个传。像这样的漏洞还有很多,系统开发者设计接口时一不小心忘记加上限制就有可能变成一个 CVE,如 CVE-2022-20494。这种一点一点增大系统资源负载的攻击很像成语“压死骆驼的最后一根稻草”的故事,因此也被称为稻草攻击(Straw Attack)。复旦大学有一篇论文《Exploit the Last Straw That Breaks Android Systems》,发表在 IEEE Symposium on Security and Privacy. 2022 上,专门介绍这类攻击手法,感兴趣的可以看一下。英文不太好的同学也可以看看这篇译文《稻草攻击:压死安卓系统的最后一根稻草》(这算是把中文翻译成英文又翻译成中文吗?)

扩展阅读

上面的文章里放了很多文章,这里再放一些同样很优秀的扩展类文章。强烈建议阅读 OPPO 几个微信公众号发布的系列文章。

结语

本文介绍了大量 Android 系统中的漏洞,并尝试总结出一些常见类型,希望能给对 Android 系统安全感兴趣的人一点帮助。当然,如果你想成为一名专业的 Android 安全研究员,传统二进制漏洞如 buffer overflow、use-after-free 等也肯定是需要了解的。我个人觉得,入门学习 Android 安全最好的时间是 2023 年之前。在 2023 年之前,只要附上一份高质量的漏洞描述,单个 moderate 级别漏洞就能获得 $2000 奖励,High/Critical 更高,而且只要是有效漏洞,Google 给予的漏洞分级一般都不会低于 moderate。如果提交了补丁且被接受,就有机会额外再加 $1000。然而,从 2023 年 5 月开始,moderate 级别漏洞奖金大幅缩水,最高只有 $250,且大部分此级别漏洞不再被分配 CVE。笔者曾经提交过 Moderate Severity + Medium Quality 的 bug report,得到的回复是不符合奖励标准。即使获得奖金,Google 的动作也实在是有些慢。笔者曾经询问过奖金进度,得到的回复是这样的:

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。而对于一个被确认的安全漏洞的生命周期是这样的:

  1. Initial severity rating assessment (subject to change after review by component owners) (3)
  2. Development of an update
  3. Assignment of CVE
  4. Shared under NDA, as part of coordinated disclosure, to Android partners for remediation
  5. Release in a public Android security bulletin
  6. Android Security Rewards payment (if applicable)

而 Google 的慢会体现在每一步上,第一步的评级就有可能花几个月。即使内部已经写出来了修复,Google 也要首先向所有合作伙伴共享该漏洞,然后至少等一个月才会在每月安全公告中发布。不过值得高兴的一点是,大部分时候漏洞赏金都会在漏洞被修复之前就给你。很明显,如果你是一个独立的安全研究员,指望靠挖洞吃饭,先不论你能找到多少洞获得多少钱,就这个速度,估计你在钱到手上之前就饿死了。当然,如果你背后有着专业的安全公司或者团队,或者就是对安全感兴趣想来试试,那可以当我没说。我相信人的兴趣来了没有人能挡住,就像以前的我一样 (你先把你脑子治好再说话才能让人信服)。如果你仍然愿意投入时间精力研究,那我相信你不会被亏待。

残页 于 2025/01/29:
原本在这里写了个天道酬勤的,最后还是删了。祝大家都能找到自己的幸福

检测Magisk与Xposed

作者 残页
2021年5月1日 13:23

不久前,开发者Rikka & vvb2060上架了一款环境检测应用Momo,把大家一直以来信任的各种反检测手段击得粉碎。下面我会通过部分已公开的源码,分析这个可能是史上最强的环境检测应用。

检测 Magisk

这一部分只分析有趣的东西,关于MagiskDetector的其他一些具体实现细节请直接查看 https://github.com/vvb2060/MagiskDetector/blob/master/README_ZH.md

反Magisk Hide

首先分析Magisk Hide的原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
static void new_zygote(int pid) {
struct stat st;
if (read_ns(pid, &st))
return;

auto it = zygote_map.find(pid);
if (it != zygote_map.end()) {
// Update namespace info
it->second = st;
return;
}

LOGD("proc_monitor: ptrace zygote PID=[%d]\n", pid);
zygote_map[pid] = st;

xptrace(PTRACE_ATTACH, pid);

waitpid(pid, nullptr, __WALL | __WNOTHREAD);
xptrace(PTRACE_SETOPTIONS, pid, nullptr,
PTRACE_O_TRACEFORK | PTRACE_O_TRACEVFORK | PTRACE_O_TRACEEXIT);
xptrace(PTRACE_CONT, pid);
}

void proc_monitor() {
// 省略...

// First try find existing zygotes
check_zygote();

for (int status;;) {
const int pid = waitpid(-1, &status, __WALL | __WNOTHREAD);
if (pid < 0) {
// 省略...
}

if (!WIFSTOPPED(status) /* Ignore if not ptrace-stop */)
DETACH_AND_CONT;

int event = WEVENT(status);
int signal = WSTOPSIG(status);

if (signal == SIGTRAP && event) {
unsigned long msg;
xptrace(PTRACE_GETEVENTMSG, pid, nullptr, &msg);
if (zygote_map.count(pid)) {
// Zygote event
switch (event) {
case PTRACE_EVENT_FORK:
case PTRACE_EVENT_VFORK:
PTRACE_LOG("zygote forked: [%lu]\n", msg);
attaches[msg] = true;
break;
// ...
}
} else {
switch (event) {
case PTRACE_EVENT_CLONE:
PTRACE_LOG("create new threads: [%lu]\n", msg);
if (attaches[pid] && check_pid(pid)) // 这里就会实际hide magisk
continue;
break;
// ...
}
}
xptrace(PTRACE_CONT, pid);
} else if (signal == SIGSTOP) {
if (!attaches[pid]) {
// Double check if this is actually a process
attaches[pid] = is_process(pid);
}
if (attaches[pid]) {
// This is a process, continue monitoring
PTRACE_LOG("SIGSTOP from child\n");
xptrace(PTRACE_SETOPTIONS, pid, nullptr,
PTRACE_O_TRACECLONE | PTRACE_O_TRACEEXEC | PTRACE_O_TRACEEXIT);
xptrace(PTRACE_CONT, pid);
} // ...
} // ...
}
}

可以看见,magisk hide通过ptrace机制跟踪所有zygote,通过cat /proc/<pid>/status看见TracerPid也可以证实我们的发现。子进程的第一个线程创建时,对其实际进行hide。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static bool check_pid(int pid) {
char path[128];
char cmdline[1024];
struct stat st;

sprintf(path, "/proc/%d/cmdline", pid);
if (auto f = open_file(path, "re")) {
fgets(cmdline, sizeof(cmdline), f.get());
} else {
// Process died unexpectedly, ignore
detach_pid(pid);
return true;
}

if (cmdline == "zygote"sv || cmdline == "zygote32"sv || cmdline == "zygote64"sv ||
cmdline == "usap32"sv || cmdline == "usap64"sv)
return false;

// 通过uid和进程名判断是否需要hide
if (!is_hide_target(uid, cmdline))
goto not_target;

// 如果命名空间未分离,进行unmount会影响到zygote从而影响到而后启动的所有进程,跳过
read_ns(pid, &st);
for (auto &zit : zygote_map) {
if (zit.second.st_ino == st.st_ino &&
zit.second.st_dev == st.st_dev) {
// ns not separated, abort
LOGW("proc_monitor: skip [%s] PID=[%d] UID=[%d]\n", cmdline, pid, uid);
goto not_target;
}
}

// 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);
return true;

not_target:
PTRACE_LOG("[%s] is not our target\n", cmdline);
detach_pid(pid);
return true;
}

hide_daemon里会fork一个新的进程,setns到目标进程的命名空间,然后卸载所有被magisk修改过的东西。注意,里面有个if判断,如果命名空间未分离,进行unmount会影响到zygote从而影响到而后启动的所有进程,那么直接跳过。这就是Magisk Hide的第一个问题。

在MagiskDetector的实现细节介绍里说明了有两种情况符合:

一个是应用appops的读取存储空间op为忽略,一个是该进程为隔离进程。

此处的隔离进程指的就是配置了 android:isolatedProcess="true"的service。而且,Android 10上还有一种有(私)趣(货)的东西,叫做App Zygote,这玩意几乎找不到说明,唯一的文档就是ZygotePreload,感觉更像谷歌给Chrome开的后门。咳咳,偏题了,这玩意运行在一个单独的进程,也不会分离命名空间。

目前已知解决此问题的方案有两种,第一种就是Magisk Lite,直接对zygote卸载而非应用,但这种方式会破坏很多现有模块;另一种就是利用进程注入,强行分离命名空间,典型的解决方案是Riru-Unshare

好的,这个问题说完了,下一个~~

在上面的判断代码里,读取进程名部分,是通过读取/proc/<pid>/cmdline进行判断的;而实际上,这个文件内容的长度是有限制的!这表示,当配置的进程名过长时,Magisk读取到的进程名会不匹配,从而跳过这个进程!这也就是Issue #3997的原理。Magisk对此做了临时修复:如果前缀匹配就直接认为是目标进程进行hide。

完了吗?没有。下一个问题在把进程添加到数据库的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int add_list(const char *pkg, const char *proc) {
if (proc[0] == '\0')
proc = pkg;

if (!validate(pkg) || !validate(proc))
return HIDE_INVALID_PKG;

// ...
}

static bool validate(const char *s) {
if (strcmp(s, ISOLATED_MAGIC) == 0)
return true;
bool dot = false;
for (char c; (c = *s); ++s) {
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '_' || c == ':') {
continue;
}
if (c == '.') {
dot = true;
continue;
}
return false;
}
return dot;
}

这里会对包名和进程名进行检查,如果含有非法字符或者没有点,那么认为是无效进程。Android对包名有严格规定,通过android:process配置的进程名也有规定,似乎无法作妖?然而问题确实发生了:Issue #4176

经过检查,该应用程序使用了隔离进程来检查Magisk,但不同的是,其服务类名含有非法字符(Java并没有限制类名),且Android 10+,系统会给隔离进程的名字追加类名(https://t.me/vvb2060Channel/441),导致检查不通过。解决方法也很简单,修改一下这个validate就好。

检测init.rc的修改:

随机只有在无法遍历的情况下才有效。如果可以遍历,使用统计方法即可准确找出每次都不一样的东西。

这句话看着有点迷惑,看看Magisk源码就知道了。https://github.com/topjohnwu/Magisk/blob/master/native/jni/init/rootdir.cpp#L45:

1
2
3
4
5
6
7
// Inject Magisk rc scripts
char pfd_svc[16], ls_svc[16], bc_svc[16];
gen_rand_str(pfd_svc, sizeof(pfd_svc));
gen_rand_str(ls_svc, sizeof(ls_svc));
gen_rand_str(bc_svc, sizeof(bc_svc));
LOGD("Inject magisk services: [%s] [%s] [%s]\n", pfd_svc, ls_svc, bc_svc);
fprintf(rc, MAGISK_RC, tmp_dir, pfd_svc, ls_svc, bc_svc);

Magisk在启动时会往init.rc中注入三个自己的服务,用来接收post-fs-data等事件;这三个服务的名称是做了随机化处理,而init实际上会往系统属性里添加像init.svc.<service name>这样子的属性,值是running或者stopped,以告诉其他进程该服务的状态。MagiskDetector就利用了这个机制,遍历系统属性记录所有服务名,然后在用户重启之后就能知道是否有服务的名称发生了变化。

检测SELinux规则

https://github.com/topjohnwu/Magisk/blob/master/native/jni/magiskpolicy/rules.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Allow these processes to access MagiskSU
const char *clients[] { "init", "shell", "appdomain", "zygote" };
for (auto type : clients) {
if (!exists(type))
continue;
allow(type, SEPOL_PROC_DOMAIN, "unix_stream_socket", "connectto");
allow(type, SEPOL_PROC_DOMAIN, "unix_stream_socket", "getopt");

// Allow termios ioctl
const char *pts[] { "devpts", "untrusted_app_devpts" };
for (auto pts_type : pts) {
allow(type, pts_type, "chr_file", "ioctl");
if (db->policyvers >= POLICYDB_VERSION_XPERMS_IOCTL)
allowxperm(type, pts_type, "chr_file", "0x5400-0x54FF");
}
}

由于Magisk允许了一些ioctl,所以会被检测到。解决方法是,更新到Android 8+ & Magisk 21+,Magisk会自动使用新的规则。

同时,不仅仅是Magisk自身的锅,错误使用SELinux也可能会造成Magisk被轻易检测到。举例:

1
2
type(SEPOL_PROC_DOMAIN, "domain");
type(SEPOL_FILE_TYPE, "file_type");

加了两个magisk自己的domain,看起来没问题,但是,如果用户把selinux设置为宽容模式(permissive),那么app可以进行selinux_check_access()(java层对应的接口为SELinux.checkSELinuxAccess()),如果获得允许,那么代表这个domain存在 => 安装了Magisk。

不只是宽容模式,如果添加了allow appdomain xxx relabelfrom之类的规则,又没有deny appdomain magisk_file relabelto,则app可能把某个文件的context给chcon成magisk_file,然后通过尝试操作这个文件判断有没有被拒绝就可以测试出系统中有没有这个domain。

SELinux是Android安全机制中的重要组成部分,强烈反对将其设置为宽容模式或忽略neverallow随意添加规则。

题外话:检测 magiskd

虽然 MagiskDetector 没有使用这个方法,但觉得有点意思,可以拿出来讲一讲。
Android 7之前,/proc没有限制,任何人都能遍历获得进程列表;在7的时候,加了hidepid=2,但并不是所有厂商都跟上了;对于这些设备,扫一下看看有没有个叫magiskd的进程就能确定有没有magisk。

Xposed

检测 Xposed

原版 Xposed 框架将自己的类加入到bootclasspath中,这导致任何人都能轻易找到。之后,大家都选择把 classloader 隔离开来,让检测没有那么容易;但是,只要它存在于内存中,那就可以被找到。XposedDetector的原理很简单,通过art的一个内部接口(VisitRoots),找到堆里的所有ClassLoader,然后一个一个尝试。目前lsp、edxp、梦境等都选择只在目标应用加载,以阻止误伤。把这个函数hook了当然可以,但我们并不想玩这种猫鼠游戏,只能保证非目标应用的环境不被修改。

反 Xposed Hook

XposedDetector的做法是,通过上面的方法,可以找到当前进程里的所有类,依据此法找到 XposedBridge 把 disableHookssHookedMethodCallbacks 改掉就可以。

实际上,还有很多其他方法:
除了原版xposed和0.5之前的edxposed,其他框架基本都直接忽略隔离进程,可以把重要的东西放在隔离进程。

通过 Xposed hook 一个方法,最终都会走到这个方法:

1
2
3
4
5
6
public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) {
// ...
else if (hookMethod.getDeclaringClass().isInterface()) {
throw new IllegalArgumentException("Cannot hook interfaces: " + hookMethod.toString());
}
}

这段检查在Android 7之后是有问题的,因为Android 7支持了一个Java 8特性,叫做interface default method。interface不再只能“说空话”,而也能有自己的方法体,而实现类只要不重写,该方法的declaring class就是interface,Xposed进行hook时就会抛出异常。

Xposed和各路实现基本原理都是对entry_point_from_quick_compiled_code_这个成员动手脚,可以直接修改这个成员也可以inline hook;而在art中有一个“万能入口”:解释执行入口,通过设置方法入口为解释执行可以使得 Xposed hook 失效,但对 Frida 这种修改了解释器的无效。

❌
❌