Inside the Android Fortress: Disarming Runtime Permissions through low-level AOSP modifications.
We’ve reached the thirteenth stage of our journey through the deep, hidden, and intricate world of the Android OS. This is the first in a series of two or three articles about "Android's Security Model".
Android's security model is often praised for its layered defense, isolating apps through sandboxes and restricting capabilities via a robust permission system. At its core, every app is confined by the Linux kernel, protected by SELinux, and regulated through both install-time and runtime permission checks. These mechanisms are what make Android a secure platform. But what happens when we look behind the curtain?
In this first article of our "Inside the Android Fortress" series, we’ll demystify two foundational pillars of Android security: sandboxing and permission system. You’ll learn how apps are isolated at the kernel level, how permissions are classified and granted, and how the runtime permission dialog is triggered and enforced by the system.
We won’t stop at the theory. We'll go a level deeper into the Android Open Source Project (AOSP) to show you how the permission enforcement flow works internally. But more importantly, we'll explain how to bypass runtime permission prompts by modifying the system so that permissions are always granted automatically, effectively rewriting Android’s default security behavior.
⚠️ Note: This article is for educational and research purposes only. Disabling Android’s security checks can leave a device exposed and should never be done on production hardware.
But as it has now become a bit of a tradition, let's start with a short story that is a metaphor for this topic and also a way to break the ice.
Here's a fun and simple office-themed story that works as a metaphor for the Android Security Architecture, with characters representing SELinux, Verified Boot, Sandboxes and the Permission Manager.
The Android Office: Security Squad On Duty.
We are in the high-tech skyscraper of Android Corp. and everything runs smoothly. Employees like Will Window, Wendy WindowManager, and even the strict boss Berta WMS (2.) are hard at work making sure everything shows up in the right place and at the right time. But as the company grows, new challenges appear, data leaks, unknown USB devices sneaking in like uninvited guests, and shady apps trying to snoop around private files. It's time to bring in the Security Squad.
Allow me to introduce the Security Team:
🧑✈️ Captain Verified Boot
Captain Verified Boot is the first one in every morning. He stands at the front door of Android Corp. every time the office powers on, making sure that no corrupted or unauthorized software gets into the building. He inspects the system’s shoes (the bootloader), jacket (the kernel), and even the coffee cup (system image), and if anything smells fishy, he stops them right there. “If you're not exactly who you say you are,” he says, “you don't get in. No exceptions.”
Verified Boot makes sure the system hasn't been tampered with before Android even starts. If it detects a problem, it refuses to boot.
👮♂️ Officer SELinux
Next up is Officer SELinux, strict, silent, and impossible to fool. He doesn’t care who you are or how charming your interface looks. Rules are rules. He keeps a giant binder full of policies and isn’t afraid to use them. Want to access a file? “Denied.” Trying to talk to another app’s process? “Denied.” Even the CEO app has to follow the rules. If anyone tries to step out of line, SELinux is on them instantly. Zero margin for error.
SELinux enforces mandatory access control (MAC) policies, isolating apps and system components. It prevents privilege escalation and limits the damage any one process can cause.
📋 The Permission Manager
Then there is the friendly but firm Permission Manager, everyone just calls her PM. She works in HR, and her job is to interview new apps. “Do you need access to the camera?” she asks politely. “And why exactly do you need the user’s location?”. Every time a new app joins the office, she hands them a form. “You may only do what the user agrees to. Nothing more.” Sometimes he walks by the desks: “Hey, the user changed their mind about giving you Contacts access. Pack it up.”
The Permission Model ensures that apps only access sensitive data if the user explicitly allows it. It puts control in the hands of the user.
🧼 Sandboxing Specialist
Dr. Sandbox has a very particular job: making sure that every employee (the apps) works inside their own personal cubicle. “No peeking over the walls,” she says gently. “Everything you need is inside your cubicle. If you want something from another employee, go through the official channels. And no touching the boss’s stuff.” Each cubicle has its own set of office supplies (like files and data), its own private coffee machine (its own runtime), and even its own fridge (private storage). If an app tries to open the neighbor’s fridge? BLOCKED. Dr. Sandbox doesn’t even need to raise her voice. She has the full support of the building’s architecture, and the walls are enforced by the Android kernel itself.
Sandboxing in Android means each app runs in its own isolated environment, with a unique user ID. This prevents one app from accessing another app’s data or code, unless explicitly allowed.
🛡️ The Perfect Team
Together, these four kept Android Corp. safe and sound. Captain Verified Boot makes sure no one bad ever gets in. Officer SELinux ensures everyone inside follows the strict internal policies. The Permission Manager checks that every visitor asks nicely and only touched what they're allowed to. And Dr. Sandbox ensures no one pokes into another app’s private business, even by accident.
They don’t always agree on everything, but they work like clockwork. Thanks to them, Android Corp. remains a safe, organized, and user-respecting place to work.
Alright, after that little story, which I hope you enjoyed, let’s put our software engineering hats back on and take a general look at how Android’s Security Architecture is built.
Android Security Architecture: A Layered Approach to Protecting Devices and Data.
The Android Security Architecture is a multi-layered system designed to protect user data and ensure the integrity of devices (1.). At its foundation, core security features are built directly into the Android operating system. Let’s begin by outlining the critical components that form the backbone of Android’s security model.
First, we have the Application Sandbox, an isolation mechanism that keeps each app in its own separate environment. This prevents apps from directly interacting with each other or accessing system resources without proper permission. The sandbox uses Linux user and group IDs to assign unique process identities to each app, ensuring that every application runs in its own dedicated memory space.
Next comes the Permission Model, a sophisticated framework that controls how apps access sensitive resources and data, based on explicit user consent. This model is built around the principle of least privilege, meaning apps are granted only the permissions strictly necessary to perform their intended functions. Over time, Android’s permission system has evolved to include runtime permissions, giving users more control by allowing them to grant or deny access while the app is running, rather than only at install time. This adds greater transparency and user autonomy.
Another key component is Security-Enhanced Linux (SELinux), a mandatory access control system that operates at the kernel level. SELinux enforces strict policies that govern how processes and applications interact with system resources, providing an extra layer of defense against malicious behavior. It works alongside the application sandbox by adding more granular control over what apps and services are allowed to do.
Then there's Verified Boot, a core security feature that protects the integrity of the Android system during the boot process. It ensures that only trusted, authorized code is executed such as the bootloader and OS kernel and prevents tampered or malicious software from running. Verified Boot uses cryptographic signatures to confirm the authenticity of each component in the boot chain, establishing a root of trust that starts from the hardware and extends up to the operating system.
Together, these foundational components create a strong security framework that protects Android devices from a wide range of threats.
In the next sections, we’ll take a deeper look into each of these elements, exploring how they work, their specific roles, and how they contribute to Android’s overall system security.
Let’s start with the sandbox mechanism.
The Android Sandbox Mechanism.
This mechanism ensures that each application runs in isolation, preventing it from accessing the resources or data of other applications or the system itself without proper permissions.
Android sandboxing happens in two distinct stages:
The first stage takes place when an app is installed, at that moment, the system assigns it a unique UID (also called an app ID). This ID is later used to identify the app’s process when it runs.
The second stage of sandboxing happens at the file system level. Each app gets its own private folder to store data, usually located at a path like "data/data/example.package". Only the app itself can access this folder."
The Android kernel is a modified version of Linux Kernel in order to adopt the specific requirements of a mobile device. The multi-user system used in desktop Linux is adopted to Android as each application as a unique user. Thus the user based isolation is followed as application based isolation with Android . Consequently, each application has got a unique UserID (UID). With the isolation, each application is run within its own Dalivk/ART VM and within its own process.
📌 UID Assignment at Install Time
Each Android app runs as a unique Linux user. When an app is installed, the package manager assigns it a unique UID (PackageManagerService.java):
int uid = UserHandle.getUid(userId, appId);
Here, appId is derived from the app's installation, and userId is the Android multi-user support feature. This UID assignment is enforced by the Linux kernel, which prevents one UID from accessing another’s resources unless explicitly permitted.
This UID also serves as the basis for the app’s user name in the system. Unlike standard Linux systems, Android doesn’t use a /etc/passwd file to map UIDs to names. Instead, the mapping is defined in a system header file called android_filesystem_config.h. In this file UIDs from 1000 to 9999 are reserved for system components (e.g., UID 1000 is the system user), UIDs starting from 10000 are assigned to user-installed apps.
#define AID_ROOT 0 /* traditional unix root user */
#define AID_SYSTEM 1000 /* system server */
#define AID_RADIO 1001 /* telephony subsystem, RIL */
#define AID_BLUETOOTH 1002 /* bluetooth subsystem */
#define AID_GRAPHICS 1003 /* graphics devices */
#define AID_INPUT 1004 /* input devices */
#define AID_AUDIO 1005 /* audio devices */
#define AID_CAMERA 1006 /* camera devices */
...
#define AID_APP 10000 /* TODO: switch users over to AID_APP_START */
#define AID_APP_START 10000 /* first app user */
#define AID_APP_END 19999 /* last app user */
...
Each app gets its own user. The username is generated from the UID using the format:
app_[UID - AID_APP]
For example, if the app's UID is 10055 and AID_APP is 10000, then the user is app_55. On multi-user devices, Android prepends a user ID prefix (u0, u10, etc.). So for UID 10055 on user 0, the app runs as user u0_a55.
🧬Process Creation via Zygote.
As we have seen in one of the previous articles of this series (3.) Zygote is a daemon process started at system boot. Its job is to preload core libraries and fork new app processes efficiently. Forking from Zygote is faster than starting from scratch and ensures all apps share a read-only memory map of common libraries. The Zygote process listens on a UNIX socket for commands to fork: (ZygoteServer.java) :
ZygoteConnection connection = peers.accept();
connection.runOnce();
When a new app is started, the ActivityManagerService (AMS) sends a command to Zygote:
Process.start(...);
This command reaches the Zygote process, which handles it like this (ZygoteConnection.java) :
pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, ...);
This is where the sandbox gets enforced: during the fork, the child process is assigned the app’s unique UID/GID.
🗂️UID/GID and Filesystem Isolation
Since Android apps run under separate Linux UIDs, this provides natural filesystem isolation:
For example:
drwx------ 3 u0_a123 u0_a123 4096 /data/data/com.example.myapp
No other app with a different UID can access this directory unless permissions (e.g., via FileProvider) are explicitly granted. In Android, FileProvider is a special type of ContentProvider that securely facilitates file sharing between applications.
📜 Tracking App UIDs and Permissions.
Android keeps app metadata in this file:
/data/system/packages.xml
Each entry includes: Package name, UID, Data directory path, List of GIDs assigned to the app. Apps that are signed with the same key can be assigned the same UID, allowing them to share data and run in the same process. This is how trusted apps (from the same developer) can cooperate at a deeper level.
Another key file is:
/etc/permissions/platform.xml
This file defines which permissions (which we will analyze in the next section) correspond to which GID, which UIDs are assigned specific high-level permissions.
Example 1: Mapping Permissions to GIDs:
<permission name="android.permission.INTERNET">
<group gid="inet" />
</permission>
<permission name="android.permission.READ_LOGS">
<group gid="log" />
</permission>
Apps granted these permissions will also get the matching GID, which gives them read/write/execute rights for resources tied to that group.
Example 2: Assigning Permissions to System UIDs
<assign-permission name="android.permission.ACCESS_SURFACE_FLINGER" uid="media" />
<assign-permission name="android.permission.WAKE_LOCK" uid="media" />
<assign-permission name="android.permission.ACCESS_SURFACE_FLINGER" uid="graphics" />
These permissions are granted to specific system services, not to third-party apps.
The actual mapping of group names (like "radio" or "system") to GID values starts from the file android_filesystem_config.h. that we have already seen above. This file is consumed by build/tools/fs_config for generating a struct like this:
static const struct android_id_info android_ids[] = {
{ "root", AID_ROOT },
{ "system", AID_SYSTEM },
{ "radio", AID_RADIO },
...
};
Anything #define AID_<name> becomes an item for the mapping. The <name> field is lowercased. For example "#define AID_RADIO 1001" becomes a friendly name of "radio".
Well, what we have seen so far is that because Android applications are sandboxed, they can access only their own files and any world-accessible resources on the device. Such a limited application wouldn’t be very interesting though, and Android can grant additional, fine grained access rights to applications in order to allow for richer functionality. Those access rights are called permissions, and they can control access to hardware devices, Internet connectivity, data, or OS services.
The Permission Model.
Android's permission model is a central component of its security architecture, governing how applications access sensitive resources and data tool. It's a system designed to protect user privacy and device security by requiring applications to explicitly declare the permissions they need.
The foundation of the Android permission model lies in the Principle of Least Privilege. Applications are only granted the permissions necessary to perform their intended functions, minimizing the potential damage if an application is compromised. This helps prevent malicious applications from gaining unauthorized access to sensitive data or system resources.
Prior to Android six (marshmallow) permissions were granted at install time. Users were presented with a list of permissions, an application requested, and had to accept them all to install the app. This approach lacked granularity and transparency, as users often didn't understand the implications of granting certain permissions. With the introduction of runtime permissions in Android 6.0, the permission model had a significant change.
Now, applications request permissions at runtime when they actually need to access a sensitive resource. This gives users more control over what applications can access, and allows them to make more informed decisions about granting permissions.
Runtime permissions are grouped into categories based on the type of resource they protect. For example, the location permission group includes permissions related to accessing the device's location. When an application requests a permission from a group, the user is prompted to grant or deny the permission. If the user grants the permission, the application is allowed to access any resource protected by that permission group.
Custom permissions are defined by individual applications to protect their own resources and data. These permissions are not part of the standard Android permissions set and are specific to the application that defines them. Other applications can request access to these custom permissions in order to interact with the defining application's resources. Custom permissions provide a way for applications to control access to their data and functionality, allowing them to limit access to only trusted applications.
The Android permission model also supports the concept of signature permissions. Signature permissions are granted to applications that are signed with the same certificate. This allows applications from the same developer to share resources and data without requiring explicit user consent. Signature permissions are often used by system applications and other trusted applications to access sensitive system resources.
It's also important to highlight that the permission model is continuously evolving. Google regularly introduces new permissions and updates the existing ones to address emerging security threats and privacy concerns.
Let’s dive a bit deeper into the topic and talk more specifically about the Permission Protection Level.
🛡️Permission Protection Level.
The Permission Protection Level characterizes the potential risk implied in the permission and indicates the procedure that the system should follow when determining whether or not to grant the permission. In practice, this means that whether a permission is granted or not depends on its protection level. The protectionLevel flags for all Android permissions are defined in the following AOSP project file: frameworks/base/core/res/AndroidManifest.xml. Let's look the three protection levels defined in Android :
✅ Normal Level:
This is the default value. It defines a permission with low risk to the system or other applications. Permissions with protection level normal are automatically granted without requiring user confirmation simply declare it in AndroidManifest.xml of the app. Examples are:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
as you can see in the frameworks/base/core/res/AndroidManifest.xml file both these two permissions are declared normal.
<permission android:name="android.permission.INTERNET"
android:protectionLevel="normal" />
<permission android:name="android.permission.BLUETOOTH"
android:protectionLevel="normal" />
<permission android:name="android.permission.ACCESS_NETWORK_STATE"
android:protectionLevel="normal" />
Note that the BLUETOOTH permission is considered normal because it only enables the bluetooth functionality but does not allow you to access personal data unlike BLUETOOTH_CONNECT or BLUETOOTH_SCAN which instead are dangerous.
⚠️ Dangerous Level:
Permissions with the dangerous protection level give access to user data or some form of control over the device. Other examples, besides BLUETOOTH_CONNECT and BLUETOOTH_SCAN which we just mentioned, include READ_SMS, which lets an app read your text messages, and CAMERA , which gives apps access to the device’s camera. Before granting dangerous permissions, Android shows a confirmation dialog that displays information about the requested permissions.
So in the app's manifest.xml file we have:
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.CAMERA"/>
and in the frameworks/base/core/res/AndroidManifest.xml file these permissions are declared dangerous.
<permission android:name="android.permission.READ_SMS"
android:protectionLevel="dangerous" />
<permission android:name="android.permission.CAMERA"
android:protectionLevel="dangerous|instant" />
Notice how the CAMERA permission has a protection level that is not only dangerous but also marked as instant. This means that the instant flag explicitly blocks instant apps from using the permission, even if they declare it in their code. Just as a reminder, Instant Apps are a Google technology that lets users run an Android app without installing it on their device. They work like a lightweight, temporary version of the app that can be launched directly from a link through a browser, a QR code, or other methods.
✍️ Signature Level:
A signature permission is only granted to applications that are signed with the same key as the application that declared the permission. This is the “strongest” permission level because it requires the possession of a cryptographic key, which only the app (or platform) owner controls. Thus, applications using signature permissions are typically controlled by the same author. Built-in signature permissions are typically used by system applications that perform device management tasks. Examples are NET_ADMIN (configure network interfaces, IP, and so on) and INSTALL_PACKAGES (allows an application to install packages).
Manifest declaration:
<uses-permission android:name="android.permission.NET_ADMIN" />
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />
AOSP code (frameworks/base/core/res/AndroidManifest.xml):
<permission android:name="android.permission.NET_ADMIN"
android:protectionLevel="signature" />
<permission android:name="android.permission.INSTALL_PACKAGES"
android:protectionLevel="signature|privileged" />
All packages considered part of the core platform (System UI, Settings, Phone, Bluetooth, and so on) are signed with the platform key.
Now let’s take a look at how Android handles the different types of permissions, focusing on the implementation in AOSP version 12. We’ll explore install-time and runtime permissions using sequence diagrams and relevant code snippets. The code I’ll show is a simplified version of the original, at least in some parts.
Android Permission Management: Install-Time vs. Runtime.
In Android, when a permission is requested depends on two things: the permission’s protection level and the app’s target SDK version.
If the permission has a protection level of "normal" or "signature", it is granted automatically when the app is installed, this is called install-time permission. But if the permission is marked as "dangerous", and the app is running on a device with Android 6.0 (API level 23) or higher, the permission must be requested manually while the app is running, this is known as a runtime permission.
The decision flow happens in PermissionManagerService (PMS) during these three phases:
1. Installation Phase
Actual code path in PackageManagerService.java.
private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
// Parse package
final PackageParser.Package pkg = parsePackage(args);
// Permission handling
mPermissionManager.grantRequestedRuntimePermissions(pkg, userId,
installReason);
}
2. Permission Granting Logic
The actual grant happens in PermissionManagerService.java.
void grantRequestedRuntimePermissions(PackageParser.Package pkg, int userId, int installReason) {
for (String permission : pkg.requestedPermissions) {
BasePermission bp = mSettings.getPermission(permission);
if (bp.isRuntime()) { // Checks if protectionLevel="dangerous"
// Defer to runtime request
continue;
}
if (bp.isNormal() ||
(bp.isSignature() && verifySignatures(bp, pkg)) {
// Grant automatically at install-time
grantPermission(pkg, permission, userId);
}
}
}
3. Runtime Request Flow
When an app calls:
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.CAMERA}, REQUEST_CODE);
The system checks if permission is already granted, if not, shows the permission dialog and stores user's choice persistently.
Let’s now take a closer look at how Android handles the flow of permission requests, whether they’re normal, signature, or dangerous.
📦 Install-Time Permissions - Sequence Diagram.
This diagram illustrates how Android automatically grants Normal Permissions ✅ during app installation.
The process begins when an application triggers installation via PackageInstaller, which forwards the request to PackageManagerService. The PackageManagerService then delegates permission handling to PermissionManagerService, which retrieves the permission details from PermissionSettings and verifies it's a Normal Permission (protectionLevel = "normal"). Once confirmed, PermissionManagerService grants the permission and updates the system state through PermissionSettings. Finally, the granted permission is persisted to disk in permissions.xml for future reference, completing the silent permission grant process without user interaction.
This diagram shows how Android grants signature permissions ✍️ during app installation.
The process starts when an app is installed via PackageInstaller, triggering PackageManagerService to request permission granting. The PermissionManagerService retrieves the declaring package and verifies signature compatibility with the requesting app through verifySignatures(). If the signatures match, the permission is granted and persisted in permissions.xml via PermissionSettings. Unlike normal permissions, signature permissions enforce cryptographic verification before granting, ensuring only trusted apps receive access. The persisted grant allows efficient validation without repeated signature checks.
🔓 Runtime Permissions - Sequence Diagram.
This diagram illustrates Android's runtime permission request flow.
This sequence ensures dangerous permissions require explicit user consent while maintaining persistent permission state across app launches (via runtime-permissions.xml). The flow demonstrates Android's runtime permission model where sensitive accesses must be approved post-installation through direct user interaction.
And here we are, finally at the last section, the most fun part of this article!
💪 Disarming Runtime Permissions. 🛠️ 💣
Modifying the Android source code to bypass runtime permission prompts and silently returns GRANTED isn’t just a clever hacker trick, in some contexts, it's a practical feature. Imagine a custom Android device installed on a treadmill in a gym. The gym manager preloads entertainment apps like YouTube or Netflix to keep users engaged during their workout. Now, picture a user starting YouTube, only to be interrupted by a permission prompt asking to access the microphone. Not only would this disrupt their exercise, but many users wouldn't know how to respond, or might simply walk away. In this case, granting permissions silently makes the experience smoother. So enforcing dynamic permission dialogs creates friction. A silent GRANTED response can offer the best user experience.
And now, let me show you how to completely disable Runtime Permissions on Android 12.
This trick involves just one file: the PermissionManagerService class, the same one we looked at earlier.
After some digging, I found that all the different permission request flows eventually end up in the same deep function, shown in the code snippet below. As you can see, I’ve commented out the entire body of the function and replaced it with a single line that always returns PERMISSION_GRANTED.
private int checkPermissionInternal(@NonNull AndroidPackage pkg, boolean isPackageExplicit, @NonNull String permissionName, @UserIdInt int userId) {
return PackageManager.PERMISSION_GRANTED;
/*
...
...
*/
}
Give it a try, I promise, it works like a charm.
This article ends here.
In the next episode, we’ll continue our deep dive into the Android Security Architecture and get to know the other two characters from our little story: Agent SELinux and Captain Verified Boot.
I remind you my newsletter "Sw Design & Clean Architecture": https://lnkd.in/eUzYBuEX where you can find my previous articles and where you can register, if you have not already done, so you will be notified when I publish new articles.
Thanks for reading my article, and I hope you have found the topic useful,
Feel free to leave any feedback.
Your feedback is very appreciated.
Thanks again.
Stefano
References:
1. Nikolay Elenkov, “Android Security Internals” No Starch Press (October 2014).
2. S.Santilli: Understanding Window Management in Android: A Deep Dive.
3. S.Santilli: Android Boot Process Part 2: From Zygote to SystemServer, SystemUI, and Launcher Initialization.
I would definitely read, if time permits. Thanks for your work.