Diving down the Magisk rabbit hole

Sihan Goi
CSG @ GovTech
Published in
11 min readJul 29, 2020
Magisk logo

[Disclaimer: The goal of this article is to bring academic insights to Magisk’s functionalities and how Magisk evades root detection. The insights shared are purely for learning purposes. The author and CSG does not condone, encourage, nor intend for the lessons described below to be used for any purposes other than cybersecurity research.]

Introduction

While rooting and root detection methods for Android devices have been around for more than a decade, Magisk software continues to be extremely resistant to root detection methods. Today, there are no known methods on how to detect rooting by Magisk. While testers are presented a convenient and reliable “cheat code” when conducting penetration tests, the downside: we have no means of mitigating against attacks that use Magisk.

Recent discussions with my colleagues, however, have got me thinking about a new Magisk detection method. This blog post summarises a joint attempt by my colleagues and I at a Magisk detection method. I will first introduce the concept of rooting and explain how Magisk evades root detection. I will then outline some methods for Magisk root detection.

What is rooting?

Before we dive down the Magisk rabbit hole, it may be helpful to understand what rooting is, and why people, particularly security testers, want to root their mobile devices. We will be looking specifically at the Magisk software, which only works on Android. A “rooted” Android device is conceptually similar to a “jailbroken” iOS device, but iOS jailbreaking will not be discussed here.

Android has its roots (pun intended) in the Linux kernel, which stems from UNIX. In UNIX/Linux terminology, the root user is a special user with administrative privileges. In fact, the command in Android/Linux to elevate a normal user’s privileges to root is called su, which is short for “super user”. One can only imagine the kind of privileges accorded to the root user.

By default, Android devices do not have root users. The security model of Android is such that each application exists as a separate non-privileged user, denoted by its own User ID, or UID. Like how users in Linux only have access to their own data and not data of others, this ensures that Android applications only have access to their own data. This is the foundation of the Android application sandboxing concept. Yet, with the introduction of a root user, all bets are off, since a root user has access to pretty much the entire device, thereby violating the principle of application sandboxing.

Application sandboxing in Android

Attackers, security testers, and power users prefer full control of the devices they own (or pwn). With normal access, a user’s or application’s actions are limited by the Android framework; such restrictions have also become increasingly tighter with each Android iteration. Root access reduces the number of OS level restrictions, thereby expanding the possibilities of what one can do with the device. For attackers and security testers, this means that they can now run tools and use techniques to reverse engineer applications, find vulnerabilities, and exploit them.

What is Magisk?

Magisk is a popular “systemless” Android root method and has been the de facto Android root method since 2016. Traditional root methods, such as the now defunct Chainfire SuperSu, worked by modifying the system partition which contains the system files. Magisk’s “systemless” root means that it roots an Android device without modifying the system partition. It does this by storing the modifications in the boot partition instead of modifying the actual system files. Since the original system files remain unchanged, Magisk can avoid detection by popular root detection methods such as Google SafetyNet.

The root detection arms race

Root methods and root detection is a constant arms race: software developers are constantly looking out for new ways of detecting root methods, and root developers are constantly looking for new ways to evade detection. Despite not modifying the system partition, Magisk nevertheless leaves behind hints of its presence. Software developers leverage these trails to detect Magisk’s presence, and Magisk has since been updated to counter such methods to evade detection. One such trail can be found at a process’ mount points.

Linux Procfs

Before elaborating further on the root detection and evasion methods, let’s briefly refresh our knowledge of the Linux Process File Systems, or “procfs”. Procfs is a special filesystem maintained by the kernel that contains information about processes and other system resources, and resides in /proc. Every process that is currently running will have a corresponding directory in /proc referenced by its process id (PID). Hence, a running process with the PID of “1234” will have a directory in /proc/1234 that presents information about itself, such as status, memory, and thread information. One such piece of information available here is mount information, which resides in /proc/<PID>/mounts. This shows the mount points visible to the process.

In Linux, different processes may see different mount points due to OS restrictions. For example, a process may be restricted to a subtree of the filesystem tree because of the chroot command. In other words, a “chrooted” process will not see mount points outside its root. The mount points in procfs, therefore, show the list of paths that are visible to the running process.

Enter Magisk Hide

Magisk Hide is a built-in module within Magisk that attempts to evade root detection for selected apps. It allows fine-grained control of which apps to hide Magisk from, and is configured through the Magisk Manager app, which is installed as part of the Magisk rooting process.

Magisk Hide in Magisk Manager

Once enabled, root status should theoretically not be visible to the selected apps. In other words, the apps will continue to function as if they are installed in a non-rooted device.

To understand how root detection works, we need to delve into how Magisk Hide works behinds the scenes.

As mentioned earlier, the Linux procfs tracks the state of all running processes, including the mount information. Since Android uses the Linux kernel, this feature exists here as well.

Let’s look at the differences between an app’s mount information when Magisk Hide is disabled as compared to when it is enabled. In this case, we are looking at the “Damn Insecure and Vulnerable” App, or DIVA.

Here is DIVA’s mount information when Magisk Hide is disabled:

DIVA’s mount information when Magisk Hide is disabled

Now, here it is when Magisk Hide is enabled:

DIVA’s mount information when Magisk Hide is enabled

As you can see, Magisk Hide unmounts the Magisk specific paths and /sbin (boxed up in red), from the app process. This prevents apps scanning for the presence of these paths from detecting Magisk.

The /proc/<PID> directory is dynamic, which is to say that the directory will only exist for the duration that the process is running. As such, Magisk will need to know when the app process starts in order to unmount the paths right at this point. Any later and the root detection may kick in and defeat Magisk Hide.

Now, the question is, how does Magisk know when an app process starts?

There are several ways of detecting when a process starts in Android. Here are some possibilities.

  • Monitoring logcat

Logcat is a command line tool that dumps a log of system messages. It is similar to the Linux dmesg, except it displays both system and application logs. When an app process starts, Android dumps a series of log messages. For example, the following log message appears in logcat when the DIVA is started using the Android launcher:

System log when DIVA app is launched

Additionally, we can filter out process start system log messages by using am_proc_startfilter, which then yields the following output when the DIVA is started:

Logcat with am_proc_start filter when DIVA app is launched

By monitoring the logs, one can detect am_proc_start events in order to deduce when an app process starts. Due to the overheads required to constantly monitor the system logs, however, this method is very slow.

  • inotify

Linux has a feature called inode notify (inotify) that allows filesystems to notice changes to themselves, and report those changes to applications. Whenever an app is started, its corresponding APK file will be accessed from the filesystem. We can, therefore, use inotify to detect open() syscalls to the target APKs to detect app process starts.

  • Ptrace

Since all Android app processes are forked from zygote, we can also monitor zygote by attaching to it with ptrace and stepping through all fork events in order to detect forks of app processes.

So, which method does Magisk use to detect app process starts?

The answer is “All of them”, depending on which Magisk version you’re using.

Magisk started by using the Logcat monitoring method, presumably because it was a well-known and easy way of accomplishing the goal. As mentioned earlier, Logcat has its downsides: it is slow due to the overheads required for constant log monitoring. Also, its success depends on Magisk reacting fast enough to hijack the process before root detection is performed — essentially creating a race condition — which could in turn lead to inconsistent results.

In early 2019, Magisk Hide was completely reworked to use the inotify method. The advantage gained is that there is now much fewer overheads: instead of polling logcat, it just sits in the background and is notified by inotify whenever an app starts. The race condition problem, however, still applies as Magisk is still just reacting to app process starts.

Shortly after experimenting with inotify, Magisk Hide moved to the zygote ptrace method. This method is more reliable because of 2 reasons. Firstly, all zygote forks will be notified, meaning no polling overhead. Secondly, subsequent thread spawning from child processes are also closely monitored to find the earliest point to identify what the process will eventually be. Essentially, this can target a process before it even starts to run, eliminating the race condition problem.

Game over for root detection?

So far, it seems like zygote ptrace solves the problem of the previous methods, so is it “ggkthx” for root detection? Not so fast…

While Zygote ptrace is supposedly much faster and more reliable, it relies on the assumption that zygote is the one forking a new app process.

As it turns out, this assumption does not always hold.

Magisk Hide and isolated processes

When an app spawns a service through Android APIs, the new service can run in the same process context, or a different one, depending on the android:process attribute in the Android Manifest. For the latter, the service’s parent process is usually zygote, and the process runs with the same process name as the main app process, but with a suffix indicated in the Android Manifest. For a long time, Magisk was unable to hide the mount points from remoted services, leading to root detection methods leveraging on this fact to detect Magisk. Eventually, Magisk Hide started to hide the paths from this remote service.

Android also introduced the concept of Isolated Process in version 4.1, where such services run under a special process that is isolated from the rest of the system and has no permissions of its own. This can be configured in the Android Manifest with the android:isolatedProcess attribute. The only way to communicate with it is through service APIs such as binding and starting. In this case, the zygote ptrace method apparently cannot reliably detect isolated processes that are spawned by a parent app. This is shown in the two mount information screenshots below where Magisk Hide was enabled for the app. The following figure shows the mount information of the main app process.

Mount information of the main process

The next figure shows the mount information of the isolated process that was started by the main process.

Mount information of isolated process

As you can see, the familiar Magisk specific paths and /sbin paths (boxed up in red) appear once again in the isolated process mount information. Leveraging on this fact, root detection can be performed by spawning an isolated process from the main app and then checking that process’s mount information to detect the tell-tale paths.

Ironically, the older logcat monitoring method catches this, as shown by the 2 am_proc_start entries in logcat.

am_proc_start logcat entries for isolated process

Because of this, Magisk v18 and below are more resistant to certain forms of root detection than versions 19 and newer.

What about SafetyNet?

You may have heard of Google’s SafetyNet attestation. In principle, SafetyNet was not designed to perform root detection, but rather to ensure that devices pass Google’s Android Compatibility Test Suite (CTS). This excludes cheap tablets from a sweatshop factory that were never pre-installed with Google’s apps, as well as rooted devices. Therefore, many Android developers, such as Niantic (the makers of Pokémon Go) use it to detect rooted devices. Despite it coming straight from Google, SafetyNet has not been foolproof. Magisk was able to successfully evade SafetyNet attestation by creating an isolated “safe environment” for the SafetyNet detection process and create a tampered SafetyNet result to satisfy the software attestation.

Google has since taken off their kid gloves and made it explicitly harder for rooted devices, by implementing hardware attestation. What this means is that some hardware-backed verification will be performed; such verification is not easily spoofed with software.

SafetyNet hardware attestation has not been officially rolled out yet, though several devices have apparently already been affected over the past few months. Once fully deployed, this will present a major challenge to root evasion tools such as Magisk Hide, which will have to figure out novel ways of defeating it.

Conclusion

As reverse engineers and penetration testers, we typically prefer the path of least resistance. Rather than reverse engineering the app binary, figuring out where root detection is happening, and then tampering with the binary or runtime instance to bypass it, we use tools that can do all that for us automatically, thereby freeing up our time for more interesting attacks. In this case, despite the slower performance, Magisk v18 seems to be more reliable in evading root detection. More importantly, because Magisk v18 does not involve ptracing an app process, it allows us to perform runtime tampering using debuggers or instrumentation frameworks such as Frida, which also require ptracing the app process.

Unfortunately, this is a known and acknowledged problem of Magisk, and its sole developer is not keen on fixing this problem. However, as this is ultimately a cat and mouse game, and Magisk is an open source project, it may not be too long before another Magisk module is released to fix this problem. Then, the ball will once again be returned the root detection developers’ court.

Google SafetyNet hardware attestation is also a major hurdle for root evasion, and it remains to be seen if this can be overcome.

For mobile app developers, understand that root detection, while important, cannot be the only mitigating control that is protecting your apps. Defence in depth is especially important in mobile apps since the app binary is installed on uncontrolled devices, of which users — including attackers — have full access and control. It is only a matter of time before client-side protections are defeated. Evading root detection is just the first step.

--

--