任意访问 Android data 的研究
一条帖子
前些天 int 给我发了一条消息,里面有个简短的poc,可以直接进入受限的路径。
我那小米13是Android 13,因此正常访问过去的话应该是受限的。
然而使用cd /storage/emulated/0/Android/$'\u200d'data后可以直接进入受限的目录,
并列出目录,甚至能进入别的应用的目录并查看文件,这就很危险了。
int 在使用Pixel 9,能接收到最新的Android安全更新,然而并没有修复这个漏洞,
这令我非常好奇,决定探索一番。
于是我跟着给的帖子看了一眼,是一个24年的漏洞,但是国内厂商并不一定会修复。 帖子里提到的CVE-2024-43093我找到google修复的代码了,但是 int 那里仍然能复现, 这是怎么回事?
漏洞严重性与修复
如果你并不想知道漏洞原理的话,只看这个章节就够了,毕竟整个攻击的原理还是非常复杂的...
严重性
攻击者可以访问任意/storage/emulated/0/Android/data下的目录,这里包括了各种应用的临时缓存,
可以用来探测安装了哪些应用,以及提取其他应用的缓存数据,如微信等。
漏洞利用条件是安装一个应用,不需要给予任何权限。
官方修复方案
被评为 Won't fix (infeasible),不会修复。
修复
所有的修复方案都需要root权限! 需要按照以上帖子里给出的方案,选择一个。 最优方案是用Xposed模块打一个补丁,影响最小。需要从Telegram下载这个应用。
详细分析
不完整的修复
要想知道这个漏洞是怎么发生的,可以先看上次的修复:
1 |
|
可以看到之前修复这个漏洞时,原先是使用regex判断路径是否匹配/storage/emulated/0/Android/{data,obb,sandbox},
后面换成了成功打开目录后调用底层api,检查打开的文件是否匹配三个中的任意一个,不再使用语义化判断。
底层一般使用inode匹配的方式做检查,所以这一步基本绕不过。
但是这个漏洞理论上应该已经在 int 的手机上修复了,怎么还不行?我用pm从他的手机上提取出来,然后放到jadx 里看了一眼,没问题。难道说,漏洞并不在这里?
FUSE
我尝试用ps匹配ExternalStorageProvider的进程,结果并没有,或许,根本就不是这个组件?
使用mount查看挂载信息,发现了这几条密切相关的信息:
1 | /dev/fuse on /storage/emulated type fuse (rw,nosuid,nodev,noexec,noatime,lazytime,user_id=0,group_id=0,allow_other) |
可以看到要访问的“根”是挂载到一个fuse上的,而我们正在hack的关键路径是挂载到真实的块设备上的。
FUSE 是什么
FUSE: Filesystem in Userspace,用户态文件系统,当一个目录以FUSE的方式挂载时, 对其任何操作都会由内核打包后送往FUSE daemon,有daemon负责处理,并返回结果。 daemon就像中间人,负责决定实际访问的目录、允不允许访问等。
由于termux做的都是底层syscall,而不是弹出文件选择器询问文件,因此完全绕过了
ExternalStorageProvider部分的检查。
不同应用,不同用户
如果我们查看/storage/emulated/0下的目录,可以发现它们的用户和组为u0_a220和media_rw,
但是.../Android/data就不一样,它下面的用户和组都是每个用户对应的uid,如u0_a351和media_rw。
u0_a220到底是何方神圣?稍后揭晓。在我的系统上,termux的用户id是u0_a311,可以看到
.../Android/data/com.termux的持有者也确实是u0_a311,说明在这个app私有的目录下,
每个目录的持有者都是app对应的用户。
在Android这套体系中,为了分割每个app的权限,每个app都有一个独立的用户,用u${USERID}_a${APPID}
来标识。例如termux是我手机上的第311个应用,同时我没有启用工作空间,那么此时
USERID=0,APPID=311。
如果我们cd到.../Android/data/com.termux,那当然一点问题都没有,但是我们又不能cd到
.../Android/data/org.tasks这种别的应用目录里。但是当我们利用这个漏洞的时候,
又确实能进入任意的目录中,这怎么可能?!权限管理怎么又失效了?
事已至此,先研究一下FUSE吧。既然有root权限,直接用lsof看一下/dev/fuse是谁持有的就可以,
于是找到了com.android.providers.media.module这个包,查看这个进程的status文件,
可以发现它的uid正是u0_a220。当然,这还没完。它的附加组里还有1023,即media_rw组。
也就是说,这个进程就是FUSE daemon,所有打到/storage/emulated/0上的请求,
都会被转发到它这来处理。
文件系统fallback
有root权限,我们直接用strace attach到这个进程上看系统调用,这块我测试了一下,
监控newfstatat看得比较清楚。
正常情况下,我们在列出/storage/emulated/0/Pictures等目录时,是能看到目录下的文件被stat
的:
如果我们尝试列出/storage/emulated/0/Android/data或访问其中的目录时,是看不到相应的请求的,
真的,就是一条请求都没刷新。但是如果我们利用一下漏洞的话,会发现又有大量的请求:
由此我们不难得出,termux可以访问外部存储(/storage/emulated/0 or /sdcard)的情况下,
当它在访问app私有目录.../Android/data/...时,这一步请求时在实际的f2fs文件系统上处理的,
因此会检查目录的持有者和当前用户是否匹配。然而,如果利用漏洞,加上ZWC字符后,
所有请求会过一遍fuse,fuse层面并不做用户检查(因为本来就是共享目录,检查无意义),
fuse确认完这些目录是安全的后,会“帮我们完成我们的操作”。而且fuse daemon有media_rw组,
因此也能任意访问所有.../Android/data下面的目录,换言之,我们也有了对于他们的访问权限。
在查找目录过程中,VFS会从根目录不断walk。挂载点都有缓存,在尝试解析一个目录的时候,
会先lookup_fast,从缓存里找,挂载点就在里面。如果没匹配到,就会走lookup_slow,
从parent出发lookup。不过我也不是很清楚是不是这样的,有讲错的话欢迎指正。
fuse daemon 的检查
等等,fuse怎么没检查出来这是危险目录?检查代码,发现它的错误和之前修复漏洞那一块的错误是一样的。 daemon跳到java中,尝试匹配目录是否是一个app-private的路径,代码是这样的:
1 | public static final Pattern PATTERN_OWNED_PATH = Pattern.compile( |
这是一条处理open的链子(可以从jni/FuseDaemon.cpp::pf_open开始跟),
可以由于零宽字符,路径脱离了regex的匹配,被认为不是app-private的路径,
于是就会尝试打开底层文件系统中对应的文件。
utf8 casefold
说了这么多,似乎有一个事被刻意地忽视了:为什么路径里包含零宽字符,
但是还能从底层文件系统中找到?这其实来源于底层文件系统——f2fs的特性。
终于,我们需要研究帖子中提到的[内核commmit]了。这个commit修复了一个漏洞,
当文件名是CASEDFOLDED并且没找到时,会尝试禁用hash再找一次。原先的逻辑是,
假定所有文件名都有哈希,因此只在哈希匹配成功时,才继续匹配完整文件名。
继续查看这个提到的另一个commit,里面是有关 Ignorable code points 的处理。
由这些信息,可以推断出f2fs的哈希其实是casefold哈希,是跟utf8强相关的,
并不是粗暴地根据字节流直接哈希。跟踪前一个commit的文件名判断函数f2fs_match_name,
其实是可以找到里面也是判断utf8 casefold后文件名的比较结果。
UTF8 casefold 是内核的utf8字符归一化处理,会将所有英文字符处理成小写, 还会处理一些别的逻辑。最特殊的是它会移除unicode零宽字符。
f2fs原先引入casefold的设计逻辑是大小写不敏感匹配文件名, 但是没想到这个特性还顺便把零宽字符也去除了。终于,我们抓住了这个漏洞的本质:
- 加入零宽字符的路径在内核层面没有改变挂载点,将lookup请求发送到了fuse
- fuse在判断的时候没有考虑零宽字符,认为其实安全的,向底层f2fs请求文件
- f2fs对路径名做casefold归一化处理,丢弃了零宽字符,打开了不含零宽字符的文件
由此实现了一个unicode零宽字符,允许攻击者直接访问任何app-private目录。
疑问
正当我以为我已经对底层掌握的一清二楚的时候,我想到改变大小写对内核切换挂载点的影响。
访问/STORAGE/EMULATED/0/Android,会返回ENOENT的错误,但是如果访问
/storage/emulated/0/ANDROID/DATA,又能正常匹配到挂载点导致无法列目录。
我对着内核VFS路径lookup的代码看了又看,始终想不明白。
正常情况下,内核是逐字节哈希、匹配的,并且f2fs是casefold的, 因此应该使用简单的大小写就能绕过限制;然而真实情况却是内核仍然匹配到了挂载点。 如果内核使用casefold的方式匹配挂载点,那就能解释为什么改变路径的大小写也能匹配到挂载点, 但是这样的话零宽字符在匹配挂载点的时候就应该被移除掉了,因此漏洞使用的路径, 应该同样能匹配到挂载点。
如果有内核✌️在看的话,希望在评论区解答一下🙏
如果你安装termux后,发现漏洞打不通的话,是因为termux的target api很低,
需要手动允许外部文件访问,才能正常cd过去。对于最近的应用来说,是不需要设置权限,
就能直接访问/storage/emulate/0的。
上游 issue tracker
目前这个issue应该已经公开,可以直接访问
踩的一些坑
Google 也是相当有钱啊,复现一下别人的发现就发了 250 USD,发到 bugcrowd 账号。 如果后续有别的师傅要想从 bugcrowd 提取金额,注意 税表尽量填真实的, 否则就他们的处理速度,如果没填对就得等好几天;如果税表没填对,会发邮件通知你, 在重写填写后记得要回复邮件更新消息。支付信息尽量填银行转账,事比 paypal 少很多, 如果只能选 paypal 的话,可以用 Xoom 转账,可以直接转到国内的支付宝账号里, 手续费也比较少。
时间线
- 1 月 9 日: 初次见到漏洞,根据帖子复现问题
- 1 月 13 日: writeup 编写完毕,并报送 Google
- 2 月 21 日: Google 判定完毕,认为bug不需要修复
- 3 月 25 日: writeup 公开
参考
- 标题: 任意访问 Android data 的研究
- 作者: RocketDev
- 创建于 : 2026-03-25 17:42:00
- 更新于 : 2026-01-13 21:02:00
- 链接: https://rocketma.dev/2026/03/25/AndroidDataAccess/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。