Android Linker Namespace: Security Flaws

Linker namespaces are the feature of Android's dynamic linker "bionic". I'm going to show the linker namespace engine, security issues, security flaws, in detail from a security perspective. (Updated in 2021).

Android Linker Namespace: Security Flaws

Part 2 ("how to"): Bypassing the Android Linker Namespace.

Namespace Formula

namespace = groups and rules used by linker while linking

  1. groups: symbols are being resolved according to their belonging to a named group.
  2. rules: search paths, permitted paths, relations between groups of the namespace.
  3. linker: /system/bin/linker[64].
  4. linking: dlopen(), android_dlopen_ext(), DT_NEEDED.

Overview

The linker namespace mechanism is introduced as a part of Project Treble starting from Android O (8.0). The linker namespace is implemented as a part of the Android linker (bionic linker) and as a set of patches in Android system framework.

Despite the linker namespace is described in detail in the official documentation, the explanation seems to be quite confusing.

Strictly speaking, the most confusing part is that the linker namespace is neither security countermeasure nor the protection mechanism for system API. It's all about to resolve symbols safely.

real-world-of-android-namespace-linking

Two main goals are declared at the top of the main article:

  1. to prevent symbol conflicts,
  2. to detect hidden runtime dependencies.

You could see about the Linker namespace isolation: notice, that isolation is not equal to protection. Yet prevention and detection.

Namespace Function
Prevent symbol conflicts +
Prevent unpredictable resolutions +
Restrict System.loadLibrary in Java +
Protect system native API -
Securely control JNI libraries -
Securely isolate groups of libraries -

I'm going to show the linker namespace mechanism in details from security perspective.

Namespace Interface

bionic-linker-namespace-api

  1. /system/bin/ld.config.txt: the linker loads a "namespace config" initializing the namespace according to it. The linker loads a "namespace config" (ld.config.txt) when the process starts. There are a couple of rules for /system and /vendor paths:
    dir.system = /system/bin/
    dir.vendor = /vendor/bin/
    
    [system]
    additional.namespaces = sphal,vndk,rs
    
    namespace.default.permitted.paths = /system/${LIB}:/vendor/${LIB}
    namespace.sphal.permitted.paths = /vendor/${LIB}
    namespace.rs.permitted.paths = /vendor/${LIB}:/data
    namespace.vndk.permitted.paths = /vendor/${LIB}/hw:/vendor/${LIB}/egl
    
    [vendor]
    namespace.default.search.paths = /vendor/${LIB}:/vendor/${LIB}/vndk-sp:/system/${LIB}/vndk-sp:/system/${LIB}
    
  2. The linker assigns a default namespace to each library while resolving dependencies according to DT_NEEDED entry.
  3. libdl.so in Android 8, and libc.soin Android 11, exports a few symbols (implementation resides in the linker):
    3.1. android_dlopen_ext(): assigns a custom namespace to newly loaded shared library; file must comply to the rules of the new namespace to be loaded.
    3.2. dlopen(): loads dynamic library according to rules of caller's namespace; assigns the caller's namespace.
    3.3. android_create_namespace() (removed in Android 11).
    3.4. android_init_anonymous_namespace() (removed in Android 11).
    3.5. android_get_exported_namespace() (private function in libc.so in Android 11).
Interface / Event Effect Notes
load ld.config.txt create ns
resolve DT_NEEDED assign ns
android_create_namespace create ns
android_get_exported_namespace get ns "sphal" is only case: "namespace.sphal.visible = true"
android_init_anonymous_namespace create ns for JIT executable code
android_dlopen_ext assign ns from any place
dlopen check ns load library according to the namespace rules

Namespace Scenarios

Native Application

📌 Namespaces: default, sphal, rs, vndk.

  1. Executable starts, e.g. /system/bin/ls.
  2. The linker is loaded into virtual memory of newly created task.
  3. The linker loads /system/bin/ld.config.txt.
  4. The linker initializes a list of namespaces: default, sphal, rs, vndk.
  5. libhardware.so calls libvndksupport.so loading libraries into the sphal namespace.

android-linker-namespace-scenario-native

Java Application

📌 Namespaces: default, sphal, rs, vndk, classloader-namespace, anonymous.

  1. Native process zygote (/system/bin/app_process) forks.
  2. All native namespaces are inherited.
  3. libnativeloader.so creates a classloader-namespace (request is starting from ZygoteInit.java). The purpose is to restrict System.loadLibrary() to load library (JNI) from path "/data/<app>/lib/".
  4. Anonymous namespace is created by the means of android_init_anonymous_namespace for JIT compiled code.

Security

1. Missing Threat Model

I would draw a trust boundary only between processes, not inside of a single process.

linker-namespace-threat-model

In the current design, consumers of the linker namespace (shared libraries, main code, all executable code) and the namespace controller (linker) reside in a single virtual memory of a process, which makes possible to trick with the namespaces and with execution flow.

2. Lack of Access Control

Current design of the Android Linker Namespace API has a lack of control, let's see:

  1. Only dlopen() respects the namespace configuration.
  2. android_create_namespace() allows to create any namespace (in Android 8).
  3. android_dlopen_ext() can specify any namespace to a desired library.

libnativeloader.so creates "classpath-namespace" from zygote process, libvndksupport.so loads libraries to a specific namespace ("sphal"), anonymous namespace is being created on-the-fly in Java application. Perhaps developers faced with a high complexity to properly isolate dependencies in preconfigured namespace, e.g. to restrict a process to just a /system path, or /vendor path.

3. A Caller Can Be Forged

A caller is an executable in a memory of a process identified by a return address (/bionic/libdl/libdl.c:101):

void* dlopen(const char* filename, int flag) {
  const void* caller_addr = __builtin_return_address(0);
  ...
}

ldload-namespace

  1. Option 1: Forging a return address from inside of a process.
  2. Option 2: Forging a return address from a tracer attached to a process.

Conclusion

Speaking about security functions, the linker namespace carries out the following ones:

Security Function Status
Detection +
Prevention +
Protection -

Undesirable linking is prevented. Inappropriate usage of system and vendor libraries is prevented. Runtime errors during symbol resolution can be detected as well as unintentional dependencies on the system framework (libraries in /system/lib).

That being said, the linker namespace does not provide the reasonable level of protection against the deliberate exposure to namespace mechanism.