Android Hacking, Sandboxing, & Security Exploits

Deep Dive: Crafting a Magisk Module for Advanced SELinux Policy Enforcement & Auditing

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction: The Power of Systemless SELinux Control

Android’s security model heavily relies on SELinux (Security-Enhanced Linux), a mandatory access control (MAC) system that defines permissions for every process and file. While robust, default SELinux policies can sometimes be too permissive for specific security requirements or too restrictive for custom system modifications. Traditionally, modifying SELinux policies required system-level changes, often necessitating recompiling the kernel or flashing custom ROMs. This is where Magisk, a popular root solution, offers a game-changing, systemless approach.

This deep dive will guide you through crafting a Magisk module designed to modify and enforce custom SELinux policies, as well as set up auditing rules. This enables advanced security hardening, sandboxing specific applications, or fine-tuning system behavior without touching the system partition, ensuring future OTA update compatibility and easier reversibility.

Understanding SELinux on Android

SELinux operates on the principle of least privilege, defining access based on security contexts (labels) rather than traditional Unix permissions. Every file, process, and IPC mechanism on an Android device has an associated SELinux context, typically represented as user:role:type:sensitivity (e.g., u:object_r:system_file:s0). Policies dictate which types can access which other types using specific permissions.

Key SELinux Concepts:

  • Types: Labels applied to objects (files, processes) that define their role and purpose.
  • Classes: Categories of objects (e.g., file, process, socket).
  • Permissions: Actions that can be performed (e.g., read, write, execute, bind).
  • Rules: Define allowed interactions (e.g., allow domain_type file_type:class permission;).
  • Policy: The collection of all rules.

Android’s SELinux policy is compiled into a binary format (sepolicy or selinux_policy) located in /sys/fs/selinux/policy and often backed up in /sepolicy on the boot image.

Why a Magisk Module for Policy Enforcement?

Modifying the live SELinux policy is fraught with challenges:

  • Persistence: Changes made directly via setenforce 0 or runtime policy loading are lost on reboot.
  • System Integrity: Modifying /sepolicy on the boot image is a system modification, risking bricking and breaking OTAs.
  • Complexity: Manually managing policy rules and ensuring compatibility across Android versions is difficult.

Magisk overcomes these issues by providing a systemless overlay. A Magisk module can execute scripts at boot, allowing us to load custom SELinux rules and enforce them persistently without altering the underlying system partition. This makes the process reversible by simply disabling or uninstalling the module.

Magisk Module Structure for SELinux

A typical Magisk module requires a specific directory structure. For SELinux policy modifications, we’ll focus on the following key files:

my_selinux_module/├── module.prop├── customize.sh├── service.sh├── system/│   └── etc/│       └── selinux/│           └── custom_policies/│               └── my_custom.cil├── util_functions.sh # Optional, for shared functions

module.prop: Module Metadata

This file defines basic information about your module.

id=my_selinux_policy_enforcername=Advanced SELinux Policy Enforcer & Auditdescription=Systemless SELinux policy modifications and auditing for enhanced security.version=v1.0versionCode=1author=Your NameminMagisk=20400# Optional: updateJson, support, etc.

customize.sh: Pre-installation and Device Checks

This script runs during module installation. It’s crucial for performing checks, ensuring compatibility, and preparing necessary files. For SELinux modules, verifying root and detecting the current SELinux status is important.

#!/system/bin/sh# This script will be executed during module installation.# Ensure Magisk is installed and running correctly.OUTFD=$(</dev/magisk/busybox grep -oP "(?<=^OUTFD=)[0-9]+" /proc/cmdline)ui_print() {    if [ "$OUTFD" != "" ]; then        echo "$1" >&$OUTFD    else        echo "$1"    fi}ui_print "- Initializing SELinux Policy Enforcer Module..."if [ ! -f /sys/fs/selinux/policy ]; then    ui_print "! SELinux is not enabled on this device. Aborting."    abort "SELinux is required."fiui_print "- SELinux detected. Preparing for policy injection."

service.sh: The Core of Policy Enforcement

This script runs at every boot after Magisk has mounted its systemless overlay. This is where we’ll inject our custom SELinux rules. We’ll use the sepolicy-inject tool, which Magisk often provides or can be bundled within the module.

First, ensure sepolicy-inject is available. It’s usually found in Magisk’s internal tools or can be statically compiled and included in your module’s /system/bin directory.

#!/system/bin/shMODPATH=${0%/*}# Path to sepolicy-inject (assuming it's bundled in the module's system/bin)SEPOLICY_INJECT="$MODPATH/system/bin/sepolicy-inject"# Check if sepolicy-inject existsif [ ! -f "$SEPOLICY_INJECT" ]; then    log_print "! sepolicy-inject not found in module. Please ensure it's bundled."    exit 1fi# Set SELinux to permissive temporarily for easier debugging during development# It's recommended to remove this in a production module unless specifically needed.# setenforce 0# In a real scenario, you would compile your .cil policies to .bin# For simplicity, we'll assume direct injection of rules or pre-compiled .bin files.log_print "- Loading custom SELinux policies..."# Example: Load a custom policy file (my_custom.cil) from the module's system/etc/selinux/custom_policiesfor policy_file in "$MODPATH/system/etc/selinux/custom_policies"/*.cil; do    if [ -f "$policy_file" ]; then        log_print "- Injecting policy from: $(basename "$policy_file")"        # Compile .cil to a temporary .bin and then inject        # sepolicy-inject needs the policy in CIL format. If you have pre-compiled .bin, adjust command.        # Here we're assuming sepolicy-inject can parse CIL or we pre-compile.        # For true CIL, you'd usually use 'secilc' to compile to .pol (binary) first.        # Modern sepolicy-inject often takes CIL directly or expects a .pol file.        # Let's assume a simplified direct injection approach for common rules for demonstration.        # A more robust approach would involve compiling CIL to a temporary .bin and loading that.        # Example using audit rules directly:        # $SEPOLICY_INJECT -N -s shell -t adbd_socket -c unix_stream_socket -p connect --auditallow        # More complex scenario: Injecting a full CIL policy requires parsing.        # For Magisk modules, it's often simpler to inject specific rules or replace parts.        # A better approach might be:        # 1. Have your .cil file.        # 2. Use `secilc` tool (if available in AOSP or bundled) to compile it into a .pol (binary) file.        # 3. Use `$SEPOLICY_INJECT -f <your_compiled.pol>` or similar depending on sepolicy-inject capabilities.        # Let's use audit rules as a practical example for direct injection, assuming we can parse CIL.        # Actual injection of CIL files is more complex and depends on sepolicy-inject version.        # A common method is to use audit rules or directly add 'allow' rules if sepolicy-inject supports it.        # Let's write an example of direct rule injection via sepolicy-inject's command line.        # This command is illustrative; real CIL files need compilation or advanced injection.        # The following rule would effectively allow a specific action, but we are focusing on *auditing*        # A common Magisk approach for auditing is to use `auditallow` or `dontaudit` rules.        # First, let's create an example .cil file to demonstrate.        cat <<EOF > "$MODPATH/tmp/temp.cil"        # Allow 'my_app_domain' to read 'my_app_data_file'        # This is a hypothetical example; you'd replace 'my_app_domain' and 'my_app_data_file'        # type my_app_domain;        # type my_app_data_file;        # allow my_app_domain my_app_data_file:file { read getattr };        # Audit all denials for 'init' process accessing 'system_file'        auditallow init system_file:file { read write open getattr execute };        EOF        # Inject the compiled policy (assuming sepolicy-inject takes a .cil or a pre-compiled .pol)        # For simplicity, if sepolicy-inject cannot directly parse CIL, we need to adapt.        # Let's use `auditallow` for our example, which is a common use case for security modules.        # This requires identifying what you want to audit.        # To audit a specific permission for a specific domain-type pair, e.g., 'untrusted_app' trying to 'write' to 'system_file':        # We will add an audit rule to log attempts.        $SEPOLICY_INJECT -N --auditallow -s untrusted_app -t system_file -c file -p write &> /dev/null        if [ $? -eq 0 ]; then            log_print "- Successfully injected audit rule for untrusted_app writing to system_file."        else            log_print "! Failed to inject audit rule."        fi        # Example: Audit connect attempts from 'shell' to 'adbd_socket'        $SEPOLICY_INJECT -N --auditallow -s shell -t adbd_socket -c unix_stream_socket -p connect &> /dev/null        if [ $? -eq 0 ]; then            log_print "- Successfully injected audit rule for shell connecting to adbd_socket."        else            log_print "! Failed to inject audit rule for shell."        fi        # More complex policy injection involves building a full CIL file and compiling it.        # For instance, creating a new type and defining its access:        # type my_restricted_app_data;        # file_type_auto_trans(untrusted_app, my_restricted_app_data, app_data_file_t);        # allow untrusted_app my_restricted_app_data:file { read write create };        # This often requires more advanced sepolicy patching utilities, which might be too complex for service.sh alone.        # For simple 'allow', 'dontaudit', 'auditallow' rules, sepolicy-inject is often sufficient.    fi    # Clean up temporary .cil file if used    rm -f "$MODPATH/tmp/temp.cil"done# Set SELinux back to enforcing after injecting rules# setenforce 1log_print "- Custom SELinux policies loaded and auditing rules applied."log_print "- Remember to check dmesg and audit logs for activity."

Note on sepolicy-inject: The exact capabilities of sepolicy-inject can vary. Some versions might directly accept CIL rules, others might expect a pre-compiled binary policy (.pol or .bin). For comprehensive policy changes (like adding new types), you might need to bundle secilc to compile CIL files to binary policies on the device, or pre-compile them and include the .pol files in your module.

Example: Restricting a Specific App’s Network Access

Let’s create a hypothetical scenario where we want to audit if a specific untrusted application tries to connect to a sensitive network port or service that it shouldn’t access.

1. Identify the Target

Suppose you have an app that, for some reason, attempts to connect to adbd‘s debugging socket.

2. Determine Current SELinux Contexts

Use ps -Z to find the app’s process context and ls -Z for file contexts. For network sockets, identify the target service’s context (e.g., adbd_socket).

adb shellps -Z | grep <app_package_name>ls -Z /dev/socket/adbd

3. Write a Custom Policy Rule (CIL)

We’ll primarily use auditallow for auditing purposes. This rule logs attempts without denying them, allowing you to observe behavior first.

# system/etc/selinux/custom_policies/audit_adbd_connect.cil# Audit attempts by 'untrusted_app' domain to connect to 'adbd_socket' auditallow untrusted_app adbd_socket:unix_stream_socket { connect };

4. Integrate into service.sh

As shown in the service.sh example, you would iterate through your .cil files and use sepolicy-inject. If your sepolicy-inject tool can directly take CIL, or if you pre-compile, the process is straightforward.

For the auditallow rule, you would execute:

# Inside service.sh$SEPOLICY_INJECT -N --auditallow -s untrusted_app -t adbd_socket -c unix_stream_socket -p connect &> /dev/null

Auditing and Verification

After your module is flashed and rebooted, you need to verify that your policies are applied and check for audit messages.

  • Check Current Enforcing Status:
    adb shellcat /sys/fs/selinux/enforce

    It should output 1 for enforcing mode.

  • Review Policy Rules: Unfortunately, there’s no easy way to dump active policy and see your specific injected rules directly in a human-readable format on the device. However, you can check dmesg for auditallow messages.
  • Monitor Audit Logs: SELinux audit messages are sent to the kernel log (dmesg) and often to logcat, tagged with auditd or SELinux. Look for AVC (Access Vector Cache) messages.
    adb shell dmesg | grep 'audit'adb logcat | grep 'SELinux'

    You should see messages when an audited action occurs, indicating your auditallow rules are working.

Building and Flashing the Module

1. Create a Zip File: Zip the contents of your my_selinux_module/ directory. Ensure module.prop, customize.sh, and service.sh are in the root of the zip. The system/ directory should be nested correctly.

zip -r my_selinux_policy.zip my_selinux_module/

2. Flash via Magisk Manager: Open Magisk Manager, go to ‘Modules’, tap ‘Install from storage’, and select your .zip file.

3. Reboot: Reboot your device for the changes to take effect.

Conclusion

By leveraging Magisk, you can implement sophisticated SELinux policy modifications and auditing rules in a systemless manner. This not only enhances the security posture of your Android device but also provides unparalleled flexibility for researchers and power users to experiment with sandboxing, fine-tuning permissions, and observing system behavior without the risks associated with permanent system modifications. This approach opens up new avenues for custom security solutions and deeper understanding of Android’s security mechanisms.

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner