Hiding root with suhide

Update: This post was written after he release of suhide v0.01 and documents that version. Scroll further down for some notes on the newer suhide v0.12.

ChainFire recently released suhide, a new “root hiding” mod for SuperSU. It is claimed to beat SafetyNet - and it does, for now - no configuration necessary. Here is some proof, using our SafetyNet Playground app: Alt Text

So how does it do it?

suhide.zip is flashed to the device through Android recovery. The installer script just edits a few files inside initramfs (boot image ramdisk): init.zygote.rc, init.zygote32.rc, init.zygote64.rc, init.zygote64_32.rc.

These files contain the initialization commands for zygote and look like this:

service zygote /system/bin/app_process32 -Xzygote /system/bin --zygote --start-system-server
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks /dev/stune/foreground/tasks

The script just adds a startup command in this file, similar to this:

    setenv LD_PRELOAD /data/adb/suhide32.so

After this is done, the script repackages the ramdisk/bootimg, ready for use in the next boot.

The result is that zygote will start using an LD_PRELOAD environment variable. Zygote is the initial android app process; all other apps are forked from it.

LD_PRELOAD is a special environment variable that points to a shared library. Be design, the runtime linker is going to link this library into the process before anything else, even before libc.so; the details are not in scope for this post. Using this old trick, the “preloaded” library can replace functions that are implemented elsewhere, e.g. it can provide custom implementations of bionic functions.

suhide library

The suhide.so shared library contains a custom implementation - a wrapper really - of the system’s setresgid() function. setresgid() is normally called by zygote during every Android app’s fork and specialization process in order to set the correct UID, EUID and SUID of the new forked app. This is how android processes are restricted to their unique identifier as opposed to using zygote’s privileged UID.

Here is the new implementation, with comments:

int setresgid(int ruid, int euid, int suid)
{
  char *pathEnv;
  const char *newPathEnv;

  if ( !real_setresgid )
    real_setresgid = dlsym(0xFFFFFFFE, "setresgid");// find setresgid symbol address
  if ( ruid != getgid() || euid != getegid() )// run this when we're in zygote specialization
  {
    uidNotBlacklisted = isUidNotBlacklisted(ruid);// check if zygote attempts to start a blacklisted app
    if ( uidNotBlacklisted )                   // if the UID is NOT blacklisted... add /su/bin and /su/xbin to PATH
    {
      pathEnv = getenv("PATH");                 // get PATH
      if ( pathEnv )
      {
        newPathEnv = replaceInPathEnv(pathEnv, "/", "/su/bin:/");// add "/su/bin" to PATH
        if ( newPathEnv )
        {
          newPathEnv = replaceInPathEnv(newPathEnv, ":/system/xbin", ":/su/xbin:/system/xbin");// add "/su/xbin" to PATH
          if ( newPathEnv )
          {
            *_errno() = 0;
            setenv("PATH", newPathEnv_1, 1);    // set the new PATH variable for the process
            free(newPathEnv);
          }
        }
      }
    }
    else                                        // if UID is blacklisted
    {
      unmountBlockDevice();                     // umount loop device if mounted
    }
  }
  return real_setresgid(ruid, euid, suid);// call real setresgid()
}

In short: When a new non-blacklisted app is started, suhide will add /su/xbin and /su/bin to the PATH. However, if the app is ‘blacklisted’ (uid present in /data/adb/suhide.uid), instead of modifying PATH, suhide will unmount /dev/block/loop for that app. This loop device contains the su executable in the /su mount point - this is how “systemless root” works. This whole system is pretty smart; it works because each forked process has its own private mount namespace (unshare(CLONE_NEWNS) is executed during zygoteInit()) and rootfs is a slave subtree; changes in one app’s mount namespace do not propagate to the parent (zygote) or siblings (other apps).

suhide detection

As ChainFire himself explains, this is a cat and mouse game, but ultimately ‘detectors’ have more chances to win. Detections such as SafetyNet can only get stronger in time; the more advanced the bypasses out there, the more advanced detections will become. Ultimately, things like SafetyNet will leverage their privileged position and utilize hardware attestation, making it really hard for bypasses to follow - TrustZone rootkits excluded, I suppose.

suhide does take some steps to protect itself (for example, it unsets the LD_PRELOAD env variable after it loads up). However it can still be easily detected - by blacklisted apps - through artifacts it leaves behind. Some easy picks: The suhide shared library is left mapped into the process space of all apps, blacklisted or not, and setresgid() remains hooked.

Update - suhide v0.12

ChainFire released a new version of suhide, fixing some compatibility issues. The most interesting bit is the addition of a new self-protection technique, as ChainFire explains: >Last but definitely not least, I have added some basic GUI hiding. In other words, other apps can less easily detect the SuperSU GUI being installed. It’s not perfect, but it’s something. I learned a few new tricks there, as this hack employs methods I have never used before (fun!).

So what is different?

Most things remain the same. The shared library still gets injected using the same method and hooks setresguid(); however it now also hooks fork(), unshare() and ioctl().

fork() and unshare() are hooked so that the library can easily understand if it is running within the parent or the forked child process and count the forked apps.

The ioctl() wrapper is used to hide the SuperSU GUI app. Through the ioctl hook, suhide alters the default behavior of BINDER_WRITE_READ if it’s not running inside system_server. The technical details are really fun but the gist is this: suhide hooks certain Android Binder transactions and removes references to eu.chainfire.supersu from replies to blacklisted apps.

mobile security, static & dynamic analysis, automation, payments

London, UK