Brief
对于Android 10上关闭selinux会导致root提权的事情一直都是略有耳闻,偶然的机会终于在github上找到了demo工程. 配合demo可以很好的理解整个root提权的过程,体会到Android/Linux常见的安全机制的作用效果,以及它们是如何一个一个的实效的。
这也是我第一次认真的学习Zygote的运作流程以及Linux Capabilities和Seccomp的运作机制,通过本文也记录下。
Root Cause
在关闭SELinux的环境下启动Zygote,Zygote会关闭所孵化子进程的Seccomp规则。而AppZygote需要重新设置Isolated Service
子进程的uid,因此会拥有CAP_SETUID/CAP_SETGID
的权限。利用该能力可以将当前进程或是子进程设置为root uid(0). 从而实现提权;
AppZygote 机制
AppZygote 是Android 10引入的机制,它允许应用自定义控制Isolated进程能够加载和访问的资源,使用AppZygote的应用将会被分配一个专门的AppZygote用于孵化其他的Isolated 进程。
使用该功能需要应用在Manifest文件中做一些简单的配置:
1 | <application |
所有设置了android:useAppZygote="true"
的Isolated进程全部都会通过应用的AppZygote来加载。在<application>
标签下配置android:zygotePreloadName
来指定用于管理Isolated进程能够加载和访问的资源;
1 | public final class AppZygote implements ZygotePreload { |
与Zygote的加载流程相似,doPreload
方法也会在应用加载之前被调用;
AppZygote Server 启动
首先来分析下负责孵化Isolated Service进程的AppZygote Service是如何被启动的.
1 | // android.os.AppZygote |
AppZygote Service 本质上是一个类似于Zygote进程,包含进程的pid以及一个LocalSocketAddress。通过向这个本地socket发送信息来创建所属的Isolated Service. 而AppZygote Service进程的启动需要向Zygote进程的发送请求来创建;
1 | // android.p008os.ZygoteProcess |
经过一系列的参数合并操作后,通过LocalSocket 发送给Zygote进程,让其去fork AppZygote 子进程。
略过Zygote进程接收socket传输内容的部分,这部分代码在大多介绍zygote的文章中都有详细的介绍且和本文关系并不大。直接看下对AppZygote启动命令的解析部分:
1 | // com.android.internal.p027os.ZygoteConnection |
通过调用Zygote.nativeForkAndSpecialize的native方法来实现进程的创建,实际上App的启动也同样是通过该native方法来创建的,但是启动参数包含了一个startChildZygote
用于标记创建的进程是否为AppZygote进程。带有此标记的进程会有一些特殊的配置。
1 | static jlong CalculateCapabilities(JNIEnv* env, jint uid, jint gid, jintArray gids, |
上面这段代码展示了Zygote进程在孵化AppZygote进程会赋予其CAP_SETUID, CAP_SETGID, CAP_SETPCAP,这是为了AppZygote能够修改Isolated Service进程uid。但是这并不意味着AppZygote可以任意修改进程的uid,Zygote为其设置了seccomp filter 限制其只能将uid修改为指定范围内
1 | static void com_android_internal_os_Zygote_nativeInstallSeccompUidGidFilter( |
至此AppZygoteProcess进程创建完毕,后续标记了android:useAppZygote的Isolated Service就全部会通过这个AppZygote来孵化。
Isolated Service 的启动
带有标记android:useAppZygote的Isolated Service会在其ServiceInfo中带有一个flag
1 | // android/content/pm/ServiceInfo.java |
相应的这些Service启动的时候就会通过AppZygote来创建子进程
1 | // com.android.server.p028am.ProcessList |
AppZygote与Zygote不同的是,其会反射APP中的方法来配置子进程可以访问的目录或是lib
1 | // com.android.internal.os.AppZygoteInit$AppZygoteConnection |
关闭Selinux 获取root权限
AppZygote Service 提权
从上文的代码中可以了解到AppZygote是可以修改进程的uid/gid的,并通过一个seccomp filter来避免AppZygote任意修改uid来做root提权或是占用其他进程的uid。但是在此回顾seccomp的代码发现,当setenforce 0
的时候,seccomp filter不会被设置
1 | static void com_android_internal_os_Zygote_nativeInstallSeccompUidGidFilter( |
而且并不仅仅是用于限制uid修改范围的seccomp filter不会被加载,是所有Zygote子进程的seccomp filter都不会被记载。
在没有seccomp filter的限制,AppZygote即可犹如脱缰的野马,任意设置uid/gid。比如将自己的uid设置为root
1 | public final class AppZygote implements ZygotePreload { |
Isolated Service 提权
原本是受限的进程,但是在selinux & seccomp机制全部被关闭的情况下,也是有机会将自己提权到root的。
想要提权就需要先解决一个问题,AppZygote进程是具有CAP_SETUID等能力的,但是其fork的Isolated Service并不会被赋予这些能力,那么如何继承到这些Capabilities呢?
方法很简单,那就是inline hook。在梳理Isolated Service的启动流程的时候提到AppZygote在fork子进程之前会先调用App的preload中定义的方法来加载一些lib库。通过加载的lib代码,可以实现对libandroid_runtime.so
的hook,将Capabilities继承到Isolated Service 进程中
1 | jint JNI_OnLoad(JavaVM *jvm, void *v __unused) { |
后续通过注册和调用一个native方法就可以实现修改uid/gid来提权
1 | static void root(JNIEnv *env __unused, jclass clazz __unused) { |
一些其他的限制条件
上面提到的两个修改uid/gid的例子都是由于在通过setenforce 0
关闭了selinux之后,Zygote又关闭了seccomp filter而导致的。这就需要在Zygote进程启动的时候,selinux就已经被关闭了。由于gIsSecurityEnforced变量在Zygote启动的时候就被设定了而不会改变
1 | static void com_android_internal_os_Zygote_nativeSpecializeAppProcess(..) |
因此在测试提权之前,需要想办法重启一下Zygote进程。可以通过toolbox中的start/stop命令来完成framework的重启
1 | // system/core/toolbox/start.cpp |
该命令需要root权限才可以调用但可以方便快捷的重启Zygote.
检查下Zygote fork出来的子进程也可发现没有设置任何的seccomp filter.
总结一下
当下,Android早就强制必须开启selinux,如果selinux被关闭则Android 系统可能认为处于调试/测试环境下,一些类似的安全机制如seccomp也会被一起关闭。整个设备将处于更低的安全环境中,会不自觉的扩展出更多的攻击面。
对于selinux正常运行的环境来讲,本文描述的内容很难被认为是一个安全漏洞。需要在Zygote启动前,通过setenforce 0来关闭selinux. 显然在为获取root权限之前这几乎是不可能办到的。可能是基于此原因这部分功能虽然已经广受讨论但是在最新的Android代码中仍然没有修改。
一句话,safe.
本文的最后,特别感谢@炜唯师父在我分析过程中提供的帮助;如需复现本文中的方案使用Reference中的Magica项目在Android 10之后的系统测试即可。