前言

之所以有这篇文章,还是因为在写别的内容时一不小心写多了,那就作为独立的文章发出来罢。
这篇文章主要展示的是一种调试方法——如何以某个应用的身份执行操作。这在一些权限和功能测试中也许会比较有用。
本文中存在一些未经验证的直觉与推断,如有不准确的地方还望包容和指正。

权限控制概述

在 Android 平台上,个人认为进程的权限控制主要由两大块组成。

uid、gid、groups

第一块是对 Linux 内核 uid、gid、groups 机制的复用。

Android 会为应用程序(和系统服务)分配(1) 对应的 uid、gid,并根据权限和功能增加对应的 groups(2) 从而:

  • 配合 Binder.getCallingUid() 在 system server 侧实现鉴权。
  • 配合文件系统权限实现数据隔离与保护。
  • 配合 kernel 自身对 syscall 的鉴权(4) 。

注释:

  • (1) 关键系统服务往往拥有预先设置好的固定 uid / gid ,能够绕过框架中的一些鉴权。用户应用的 uid / gid 是在安装时被分配的,不同应用的 uid / gid 一般是不同的(除非设置了 sharedUserId )。
  • (2) 比如拥有网络权限的应用会自动被加入 INET group ,从而拿到打开 socket 的权限;属于同一个 用户(3) 的应用会被加入那个用户对应的 everybody group,从而获得如 /sdcard 目录的文件系统权限(仅针对 sdcardfs )。还有很多,不再举例。
  • (3) 这里的“用户”是指 Android 的多用户机制,而非上面的 uid 。
  • (4) Android 的最关键进程都运行在 uid = 0 (root) 下,比如 vold、init、zygote、ueventd 等,于是它们可以绕过 kernel 的鉴权,自由的使用如 mount 之类的 syscall。

SELinux

另一块则是 SELinux,这个东西其实比想象的要重要得多,它是 Android 安全模型中不可缺少的一部分
SELinux 更像是一种白名单机制,用 policy 明确写明了某种类型的进程只能对某种类型的目标进行某种类型操作,policy 中没有提及的一律 denied 。
也许你会觉得 SELinux 仅仅只是一种“增强保护”,没有它,单靠上一块的实现,一切仍然能工作的很好,但这其实是大错特错了。

你可以试试,在 SELinux Permissive 的情况下:

  • 任何应用都可以执行 /system/bin/restart 来直接重启你的设备。
  • 任何应用都可以随意的修改你的 prop 从而影响系统服务的行为,因为自从 这个提交 之后传统的鉴权已经被弃用,SELinux 是 prop 的唯一保护。

杂谈

当然,还有一些其它的权限控制机制,比如 capabilities 下放(1) 与 Seccomp filter(2) ,它们与正常的用户应用还是有一段距离的。

  • (1) Capabilities 会在 zygote fork() 后根据新进程的类型下放给部分系统服务,比如蓝牙服务和网络栈。某些进程如 child zygote 会被下放部分高级 capabilities (比如 CAP_SETUID)(3)。
  • (2) Seccomp 主要根据进程类型,屏蔽非白名单内的 syscall,用来阻止直接瞄准内核的攻击。
  • (3) (离题) 这些高级 capabilities 本应在 child zygote 再 fork() 后被 drop 掉,但谷歌却留了个接口使用户可以在 child zygote 进程本体通过 ZygotePreload 执行自定义代码。在进程本体也就意味着拥有这些高级 capabilities !这可是典型的用户代码在特权进程执行,顺理成章的就有了利用这个缺陷提权的尝试,比如 Magica 。这个项目使用 native hook 的方式干掉了 child zygote 的 capset() 方法,使得 fork() 出用户服务进程后 drop capabilities 的过程无法进行,于是这些 capabilities 就能够以 permitted set 的形式被保留到用户服务启动后。但遗憾的是,在 Seccomp 正常运行时,用户程序无法正常的使用 setuid 相关 syscall ,因此提权无法进行。但只要禁用掉 SELinux 和 Seccomp,它就能够顺利的拿到 root 权限。虽说从结果看是防住了,但,这也还是太危险了吧,不优雅的设计全靠安全机制挡刀。

身份模拟

本文主要聚焦对上述两大块的权限控制方式的模拟方法,第三块用的太少而且与用户程序几乎没什么关系,于是此处就不再讨论了。
这里的“模拟”是指尝试以指定的身份执行操作,是高权限(root)向低权限(比如应用)的模拟。是用来方便调试而不是用来提权的。

su? switch user!

首先,是对 uid、gid、groups 也就是上面的第一块内容的模拟。

这里针对的是 AOSP 在 userdebug 和 eng 构建上 自带的 su binary ,如果采用的是第三方 root 方案,那可能有一些不同的实现。

su 命令,通过 SUID bit 来工作。在一般玩家眼中也许它只是用来提升权限到 root 的,但实际上,AOSP 的 su 集成了用户切换的功能:

usage: su [WHO [COMMAND...]]

Switch to WHO (default 'root') and run the given COMMAND (default sh).

WHO is a comma-separated list of user, group, and supplementary groups
in that order.

你可以通过传入一组逗号分隔的参数来实现用户和用户组的切换。

比如,你想同时切换用户和用户组到 1000:
su 1000 sh
于是你就会以 uid=1000、gid=1000 来执行命令 sh

又或者,你想分别指定不同的 uid 与 gid :
su 1000,2000 sh
于是你就会以 uid=1000、gid=2000 来执行命令 sh

别忘了我们上面提及的 groups,我们可以继续往后加逗号和组号,除去前两个参数外,剩下的都会被作为新进程的 groups:
su 10113,10113,1077,3003,9997,20113,50113 sh
那么,这些组号是哪里来的呢?

比如我想以“电话”应用的 uid、gid、groups 身份运行一个 shell,那么首先找出应用的 pid:

OnePlus8T:/ # ps -ef | grep com.android.dialer
u0_a79        5245   841 0 18:40:43 ?     00:00:00 com.android.dialer
root          6531  6399 1 19:31:12 pts/0 00:00:00 grep com.android.dialer

嗯,是 5245 ,然后去 /proc/<pid>/status 查看对应的组号:

OnePlus8T:/ # cat /proc/5245/status
Name:   .android.dialer
......
Uid:    10079   10079   10079   10079
Gid:    10079   10079   10079   10079
FDSize: 256
Groups: 3001 3002 3003 9997 20079 50079
......

uid 和 gid 既可以用上面出现的 u0_a79 也可以用 10079 它们是等价的。
接下来,开始 switch user :

OnePlus8T:/ # su 10079,10079,3001,3002,3003,9997,20079,50079 sh
OnePlus8T:/ $ ls /sdcard
ls: /sdcard: Permission denied

由于它默认并没有存储权限,所以 ls /sdcard 会直接给出 Permission denied,就像应用本身访问 /sdcard 一样。这对于调试权限有很大的帮助。
哦对了,我们可以直接 id 一下来看看当前的用户、用户组和 groups 信息:

OnePlus8T:/ $ id
uid=10079(u0_a79) gid=10079(u0_a79) groups=10079(u0_a79),3001(net_bt_admin),3002(net_bt),3003(inet),9997(everybody),20079(u0_a79_cache),50079(all_a79) context=u:r:su:s0

和上面我们指定的吻合。诶,最后那个 context 是个啥玩意儿?别急,那是接下来要讲的。

runcon? setcon!

然后将目光聚焦于 SELinux,也就是对上面第二块内容的模拟。

进程在运行时会具有一个 SELinux 下的 “身份”(1) 。不同的“身份”能够解锁不同的 SELinux 策略,从而获得不同的权限。上面所看到的 context 便是目前的 SELinux “身份”。

  • (1) 准确点,应该叫 domain 或者 source context ?不过这里用“身份”应该更好理解吧。

很显然,刚刚的那一番切换 uid、gid、groups 的操作并没有能够切换这个“身份”,它仍然是 su binary 所具有的 u:r:su:s0

那么,怎么切换它呢?

AOSP 在 toybox 中有一个 runcon 的实现,专门用来进行这个“身份”的切换。但遗憾的是,它并不能在 enforcing 的情况下正常工作,它自己会被 SELinux 策略干掉。。。

于是,小小的 rewrite 了一个 setcon 来取代它 。使用方法不在这里赘述,有兴趣请参考链接里的 README 。
那么,接下来就全部用 setcon 了。

在切换之前,我们得先知道一个应用的“身份”吧?
和上面的类似,我们得先获取一个应用的 pid (不演示了),然后我们可以从 /proc/<pid>/attr/current 处得到这个“身份”的信息。

还是以上面的 dialer 为例:

OnePlus8T:/ # cat /proc/5245/attr/current
u:r:priv_app:s0:c512,c768OnePlus8T:/ #

由于返回值的行末没有 \n ,所以我们得凭肉眼把它区分出来:u:r:priv_app:s0:c512,c768

然后:

OnePlus8T:/ # /data/setcon u:r:priv_app:s0:c512,c768 sh
OnePlus8T:/ # id
uid=0(root) gid=0(root) groups=0(root),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) context=u:r:priv_app:s0:c512,c768

(这里我只 setcon 了,没有 su,所以 uid 之类的还是默认的)
可以看到,“身份”发生了改变。

此时我们如果 ls /data 会发现竟然权限不足?

OnePlus8T:/ # ls /data
ls: /data: Permission denied

如果此时你有兴趣看看 avc 的日志,是可以发现 denied 信息的,详细参见 setcon 的 README,这里就不再演示了。

很有趣吧,明明 uid 还是 root ,居然还是被 SELinux 约束住了。这说明了 SELinux 也完全能约束住上面所提及的像 vold、init、zygote、ueventd 这样的 root 进程,假如这些程序中有漏洞,那有了 SELinux 的保护,攻击者能施展的攻击面就会小非常多。

另外,最重要的,这还说明了所有的 priv_app 都不能 ls /data,毕竟我们在以它们的“身份”执行操作嘛。

结合一下

上面是单独介绍,接下来结合两者,很简单,一条命令搞定:

OnePlus8T:/ # su 10079,10079,3001,3002,3003,9997,20079,50079 /data/setcon u:r:priv_app:s0:c512,c768 sh
OnePlus8T:/ $ id
uid=10079(u0_a79) gid=10079(u0_a79) groups=10079(u0_a79),3001(net_bt_admin),3002(net_bt),3003(inet),9997(everybody),20079(u0_a79_cache),50079(all_a79) context=u:r:priv_app:s0:c512,c768

Easy。
需要注意的是,不要交换切用户和切 SELinux “身份” 的顺序,要不然执行 su 会直接被 SELinux 拦截了。。

好了,两者一结合,我们几乎就能够完美的模拟应用身份,以应用的权限执行操作了。
如果你还想进一步的模拟应用所处的环境,可以再看看 这篇文章 ,相信它也会对你有所帮助。