March 22, 2016

Bypass SELinux on Android

Background

As I wrote in the previous post "Shared Library Injection in Android", I've made an injection util on Android. The injection util works perfectly on Android 4.0 device, but it failed on my Nexus 5, an Android 4.4.4 device. I looked through the log and found that the PTRACE_ATTACH failed at the very beginning.
It's really weird. To my knowledge, only the root user is allowed to call ptrace. I have root access to both devices. After checking every single line of my code, I'm sure the failure on Android 4.4.4 is irrelevant to the code itself. I checked the ring buffer by dmesg, and got the following line:
type=1400 audit(1458651173.360:3282): avc: denied { ptrace } for pid=16977 comm="injector"
Obviously, something prevent our injector to make the ptrace call. I checked Android official website, knowing that starting with Android 4.3, SELinux is enabled by default. Now the problem is clear, we need to bypass the SELinux to perform injection.

SELinux on Android

SELinux is abbreviation for Security-Enhanced Linux. It is a Linux kernel security module that provides a mechanism for supporting access control security policies. Briefly speaking, with SELinux enabled, root access can't do everything anymore. There're two mode of SELinux, enforcing and permissive. SELinux has made some rules. Under the enforcing mode, any operation that violates the rules will be denied, while under the permissive mode, illegal operation will only by recorded.

On Android 4.3, SELinux is set to permissive mode by default. On partial Android 4.4 devices and all devices above Android 5.0, SELinux runs under enforcing mode by default. Thus root access is not the only thing we need to perform injection on Android 4.4.4 devices.

Solution

Basically, there're two ways to bypass the SELinux. The first is the moderate one, adding rules for our injector. The second is the rough one, changing the running mode of SELinux to permissive.
I choose the later one. Adding rules is complicated and may behave differently between devices. The later one is simpler and more reliable.
I did the following things:

  • Open the /proc/filesystems file, search for the string "selinuxfs", if the string exists, SELinux is running on this device.
  • If SELinux is running, open the /proc/mounts file, read the line that contains "selinuxfs". In this line, we can extract the absolute path of SELinux directory.
  • Find a file named "enforce" in the SELinux directory, overwrite this file with a single "0".

The code is listed below:

bool IsSelinuxEnabled() {
  FILE* fp = fopen("/proc/filesystems", "r");
  char* line = (char*) calloc(50, sizeof(char));
  bool result = false;
  while(fgets(line, 50, fp)) {
    if (strstr(line, "selinuxfs")) {
      result = true;
    }
  }
  if (line) {
    free(line);
  }
  fclose(fp);
  return result;
}

void DisableSelinux() {
  FILE* fp = fopen("/proc/mounts", "r");
  char* line = (char*) calloc(1024, sizeof(char));
  while(fgets(line, 1024, fp)) {
    if (strstr(line, "selinuxfs")) {
      strtok(line, " ");
      char* selinux_dir = strtok(NULL, " ");
      char* selinux_path = strcat(selinux_dir, "/enforce");
      FILE* fp_selinux = fopen(selinux_path, "w");
      char* buf = "0"; // set selinux to permissive
      fwrite(buf, strlen(buf), 1, fp_selinux);
      fclose(fp_selinux);
      break;
    }
  }
  fclose(fp);
  if (line) {
    free(line);
  }
}

The code works on my Nexus 5, Android 4.4.4. We have successfully bypass the SELinux and our injector is able to work now.
Please note that setting SELinux to permissive mode may take some security risks, don't do it unless you know what you're doing.