From 29fec2e0def3948f0d0af1aa3376f75886fef50e Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 1 Dec 2014 22:26:35 -0800 Subject: [PATCH] Add initial support for rooted devices running Lollipop with SELinux set to enforcing. This should really be improved in the future since we're modifying policies for untrusted_app. --- .../binding/input/evdev/EvdevReader.java | 52 ++++---- .../binding/input/evdev/EvdevShell.java | 118 ++++++++++++++++++ .../binding/input/evdev/EvdevWatcher.java | 16 +++ 3 files changed, 161 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/com/limelight/binding/input/evdev/EvdevShell.java diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java index 102c17f9..d5c7c6de 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java @@ -1,7 +1,7 @@ package com.limelight.binding.input.evdev; -import java.io.IOException; -import java.io.OutputStream; +import android.os.Build; + import java.nio.ByteBuffer; import java.util.Locale; @@ -11,31 +11,33 @@ public class EvdevReader { static { System.loadLibrary("evdev_reader"); } + + public static void patchSeLinuxPolicies() { + // + // FIXME: We REALLY shouldn't being changing permissions on the input devices like this. + // We should probably do something clever with a separate daemon and talk via a localhost + // socket. We don't return the SELinux policies back to default after we're done which I feel + // bad about, but we do chmod the input devices back so I don't think any additional attack surface + // remains opened after streaming other than listing the /dev/input directory which you wouldn't + // normally be able to do with SELinux enforcing on Lollipop. + // + // We need to modify SELinux policies to allow us to capture input devices on Lollipop and possibly other + // more restrictive ROMs. Per Chainfire's SuperSU documentation, the supolicy binary is provided on + // 4.4 and later to do live SELinux policy changes. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + EvdevShell shell = EvdevShell.getInstance(); + shell.runCommand("supolicy --live \"allow untrusted_app input_device dir getattr\" " + + "\"allow untrusted_app input_device chr_file { open read write ioctl }\""); + } + } // Requires root to chmod /dev/input/eventX - public static boolean setPermissions(String[] files, int octalPermissions) { - ProcessBuilder builder = new ProcessBuilder("su"); - - try { - Process p = builder.start(); - - OutputStream stdin = p.getOutputStream(); - for (String file : files) { - stdin.write(String.format((Locale)null, "chmod %o %s\n", octalPermissions, file).getBytes("UTF-8")); - } - stdin.write("exit\n".getBytes("UTF-8")); - stdin.flush(); - - p.waitFor(); - p.destroy(); - return true; - } catch (IOException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - return false; + public static void setPermissions(String[] files, int octalPermissions) { + EvdevShell shell = EvdevShell.getInstance(); + + for (String file : files) { + shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file)); + } } // Returns the fd to be passed to other function or -1 on error diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevShell.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevShell.java new file mode 100644 index 00000000..0684010d --- /dev/null +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevShell.java @@ -0,0 +1,118 @@ +package com.limelight.binding.input.evdev; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Scanner; +import java.util.UUID; + +public class EvdevShell { + private OutputStream stdin; + private InputStream stdout; + private Process shell; + private final String uuidString = UUID.randomUUID().toString(); + + private static final EvdevShell globalShell = new EvdevShell(); + + public static EvdevShell getInstance() { + return globalShell; + } + + public void startShell() { + ProcessBuilder builder = new ProcessBuilder("su"); + + try { + // Redirect stderr to stdout + builder.redirectErrorStream(true); + shell = builder.start(); + + stdin = shell.getOutputStream(); + stdout = shell.getInputStream(); + } catch (IOException e) { + // This is unexpected + e.printStackTrace(); + + + + // Kill the shell if it spawned + if (stdin != null) { + try { + stdin.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } finally { + stdin = null; + } + } + if (stdout != null) { + try { + stdout.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } finally { + stdout = null; + } + } + if (shell != null) { + shell.destroy(); + shell = null; + } + } + } + + public void runCommand(String command) { + if (shell == null) { + // Shell never started + return; + } + + try { + // Write the command followed by an echo with our UUID + stdin.write((command+'\n').getBytes("UTF-8")); + stdin.write(("echo "+uuidString+'\n').getBytes("UTF-8")); + stdin.flush(); + + // This is the only command in flight so we can use a scanner + // without worrying about it eating too many characters + Scanner scanner = new Scanner(stdout); + while (scanner.hasNext()) { + if (scanner.next().contains(uuidString)) { + // Our command ran + return; + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void stopShell() throws InterruptedException { + boolean exitWritten = false; + + if (shell == null) { + // Shell never started + return; + } + + try { + stdin.write("exit\n".getBytes("UTF-8")); + exitWritten = true; + } catch (IOException e) { + // We'll destroy the process without + // waiting for it to terminate since + // we don't know whether our exit command made it + e.printStackTrace(); + } + + if (exitWritten) { + try { + shell.waitFor(); + } finally { + shell.destroy(); + } + } + else { + shell.destroy(); + } + } +} diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevWatcher.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevWatcher.java index 6584dcc3..41080178 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevWatcher.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevWatcher.java @@ -19,6 +19,8 @@ public class EvdevWatcher { private boolean ungrabbed = false; private EvdevListener listener; private Thread startThread; + + private static boolean patchedSeLinuxPolicies = false; private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) { @Override @@ -117,6 +119,15 @@ public class EvdevWatcher { startThread = new Thread() { @Override public void run() { + // Initialize the root shell + EvdevShell.getInstance().startShell(); + + // Patch SELinux policies (if needed) + if (!patchedSeLinuxPolicies) { + EvdevReader.patchSeLinuxPolicies(); + patchedSeLinuxPolicies = true; + } + // List all files and allow us access File[] files = rundownWithPermissionsChange(0666); @@ -140,6 +151,11 @@ public class EvdevWatcher { // Giveup eventX permissions rundownWithPermissionsChange(066); + + // Kill the root shell + try { + EvdevShell.getInstance().stopShell(); + } catch (InterruptedException e) {} } }; startThread.start();