Bypassing the Android Linker Namespace

The Android's linker (bionic) disallows loading most of the shared libraries from /system when a request is going from the executable code belonging to "classloader-namespace". (Source code is updated for Android 11).

Bypassing the Android Linker Namespace

Quick Guide

JNI -> android_create_namespace -> android_dlopen_ext

Full solution: https://github.com/fadeevab/android-custom-jni-namespace/
Detailed explanation: Android Linker Namespace: Security Flaws

Goal

Recently, I investigated Android Linker Namespace: Security Flaws, and here I would like to show you how to bypass linker namespaces access control.

Starting from Android 8.0 (Project Treble) while trying to access the private system API from a Java application, you may see the following error:

java.lang.UnsatisfiedLinkError: dlopen failed: library "libwifi-system.so" ("/system/lib64/libwifi-system.so") needed or dlopened by "/system/lib64/libnativeloader.so" is not accessible for the namespace "classloader-namespace"

It is a consequence of doing things like:

// From Java code
System.loadLibrary("wifi-system");

or

// From JNI
dlopen("libwifi-system.so", RTLD_NOW);

The Android's linker (bionic) disallows to load the most of the shared libraries from /system when the request is carried out from the executable code belonging to "classloader-namespace".

Namespace Inheritance
Zygote process assigns the "classloader-namespace" to a Java application with the help of the libnativeloader.so. System.loadLibrary loads a shared library into the same "classloader-namespace", preserving the namespace of parent process.

An ordinary application from the market should not use the private system libraries (not included into NDK) in order to be portable among many devices. And the linker namespace mechanism is a nice thing to control undesirable dependencies. But if you are really sure about to use a private system API, see the method below.

Steps

The current example shows how to load libwifi-system.so: any uncommon system library to which access is most likely denied. The nuance of the linker namespace API is going to be used.

Full example is here: https://github.com/fadeevab/android-custom-jni-namespace/.

  1. The first step is to get access to native code from Java application. Create a Java Native Interface (JNI) and declare a native method (e.g. linkSystemLib):

    public final class RoadToSystem {
        public native int linkSystemLib();
    
        static {
            System.loadLibrary("roadtosystem");
        }
    }
    
  2. Build a shared library, implementing linkSystemLib method:

    #include "dlext_namespaces.h"
    
    JNIEXPORT jint JNICALL Java_com_example_android_jniapi_RoadToSystem_linkSystemLib(
            JNIEnv *env, jobject obj) {
        ...
    }
    

    "dlext_namespaces.h" is copied from Android 8.0 sources, the header provides prototypes of the linker namespace functions, which are being the exported symbols of libdl.so linked into the process.

  3. JNI shared library resides in "classloader-namespace" after loading by System.loadLibrary call. dlopen() returns NULL for the most of the system libraries.
    ANDROID 8: Need to create a namespace with "no rules" (permitted paths will contain /system/lib[64]) and load a system shared library into the one:

    struct android_namespace_t *ns =
        android_create_namespace(
          "trustme",
          "/system/lib64/",
          "/system/lib64/",
          ANDROID_NAMESPACE_TYPE_SHARED |
          ANDROID_NAMESPACE_TYPE_ISOLATED,
          "/system/:/data/:/vendor/",
          NULL);
    

    ANDROID 11: android_create_namespace is removed from libdl.so, also android_get_exported_namespace is moved to libc.so and it becomes private. The workaround is to find android_get_exported_namespace by an offset from any public symbol and retrieve the vndk namespace.

    struct android_namespace_t* (*android_get_exported_namespace)(const char*) =
        dlsym(RTLD_DEFAULT, "android_get_device_api_level");
    if (!android_get_exported_namespace) {
      LOGV("failed to get android_get_device_api_level\n");
      return NULL;
    }
    
    android_get_exported_namespace += 0x650e0;
    
    ns = android_get_exported_namespace("vndk");
    

    And the rest of the code looks like the following:

    const android_dlextinfo dlextinfo = {
      .flags = ANDROID_DLEXT_USE_NAMESPACE,
      .library_namespace = ns,
    };
    
    // Access granted
    void *so = android_dlopen_ext("libwifi-system-iface.so", RTLD_LOCAL | RTLD_NOW, &dlextinfo);
    

    android_dlopen_call returns a valid non-zero pointer in a case of success, while dlopen returns NULL trying to load a system library from JNI.