GPUAF - Two ways of Rooting
All Qualcomm based Android
phones
PAN ZHENPENG & JHENG BING JHONG
About us
● Pan Zhenpeng(@peterpan980927), Senior Mobile Security Researcher at
STAR Labs
● Jheng Bing Jhong(@st424204), Senior Security Researcher at STAR Labs
Agenda
● Backgrounds
● Bug analysis
● GPUAF exploit
● Conclusion
Android exploits
● Universal exploit
● Chipset specific exploit
● Vendor specific exploit
● Model specific exploit
Android exploits
● Universal exploit
○ Linux kernel bugs: net, binder, etc…
● Chipset specific exploit
● Vendor specific exploit
● Model specific exploit
Android exploits
● Universal exploit
○ Linux kernel bugs: net, binder, etc…
● Chipset specific exploit
○ Mali GPU, Qualcomm GPU, etc…
● Vendor specific exploit
● Model specific exploit
Android exploits
● Universal exploit
○ Linux kernel bugs: net, binder, etc…
● Chipset specific exploit
○ Mali GPU, Qualcomm GPU, etc…
● Vendor specific exploit
○ Samsung NPU, Xclipse GPU, etc…
● Model specific exploit
Android exploits
● Universal exploit
○ Linux kernel bugs: net, binder, etc…
● Chipset specific exploit
○ Mali GPU, Qualcomm GPU, etc…
● Vendor specific exploit
○ Samsung NPU, Xclipse GPU, etc…
● Model specific exploit
○ Pixel X driver A, Samsung [A/S/Z] XX driver B, etc…
Why Qualcomm GPU?
● Universal exploit
○ Linux kernel bugs: net, binder, etc…
● Chipset specific exploit
○ Mali GPU, Qualcomm GPU, etc…
● Vendor specific exploit
○ Samsung NPU, Xclipse GPU, etc…
● Model specific exploit
○ Pixel X driver A, Samsung [A/S/Z] XX driver B, etc…
Why Qualcomm GPU?
● Samsung Galaxy S series (non Exynos chips)
● Honor phones (x9b, 90…)
● Xiaomi phones (14, 14 Pro, Redmi Note 13 Pro…)
● Vivo phones (iQOO Z9s Pro, T3 Pro…)
● …
Qualcomm GPU Introduction
● kgsl_mem_entry represents for a userspace memory allocation. Just like
every GPU Driver, it can be allocated from GPU Driver, or import from CPU
memory.
Qualcomm GPU Introduction
● Vbo is also a kgsl_mem_entry struct but with different flags and ops in
memdesc field
Qualcomm GPU Introduction
● Instead of allocate memory from GPU or import from CPU, VBO map zero
page initially and can bind other kgsl_mem_entry to its region.
Agenda
● Backgrounds
● Bug analysis
● GPUAF exploit
● Conclusion
Bug#0 CVE-2024-23380
● Race condition in Kgsl VBO map buffer
Bug#0 CVE-2024-23380
● In BlackHat USA 2024, Google researchers also used this bug to create page
UaF with the path below
Bug#0 CVE-2024-23380
● After page UaF, they reuse the pages as kgsl_memdesc objects to do
physical AARW
Bug#0 CVE-2024-23380
● But here, we decided to chain another 2 bugs together to build an exploit to
help understand more about the Qualcomm GPU Driver internals.
● In our case, we won’t race with add and remove, but race with two bind
operations to create side effect
● After gaining GPUAF primitive, we will also use two different post exploit
techniques to gain AARW and root shell
Bug#1 CVE-2024-23373
● Page UaF if unmap failed
Bug#2
● Ptep destroy when tblcnt reach to zero
Chain bugs to page UAF
We first shape the memory to make it looks like below, vbo maps from 0x0 to 0x10000, and
then there’s hole (first mapped and unmapped), then is entry C import from CPU, start from
0x1ff000 to somewhere
Chain bugs to page UAF
Thread A got the mutex lock and un-map VBO at [0x0000,0x2000], insert the range into
VBO’s interval tree
Chain bugs to page UAF
Thread A release the lock and not yet map range [0x0000,0x2000], thread B got the mutex
lock and unmap VBO at [0x1000,0x3000], and since thread A and B got a overlap region, so
the range will split into 2.
Chain bugs to page UAF
Thread A release the lock and not yet map range [0x0000,0x2000], thread B got the mutex
lock and unmap VBO at [0x1000,0x3000], and since thread A and B got a overlap region, so
the range will split into 2.
Chain bugs to page UAF
Thread A finish the map at [0x0000,0x2000] on IOMMU.
Chain bugs to page UAF
Thread B try map at [0x1000,0x3000] but fail because [0x1000,0x2000] had been mapped
by thread A so [0x2000,0x3000] left unmapped, but the range insert into the interval tree of
vbo. this create the inconsistency of tlbcnt and IOMMU.
Chain bugs to page UAF
Delete VBO will unmap whole VBO which is 0x10 pages, and it will decrease more pages
than tblcnt, and finally free the ptep by qcom_io_pgtable_log_remove_table, but entry C is
half at this ptep, cause entry C halfly unmapped.
Chain bugs to page UAF
When delete entry C, it fail to unmap [0x1ff000,0x200000] because first page’s ptep has
been destroyed and leave IOMMU still mapped at [0x200000,0x20x000]
Chain bugs to page UAF
When delete entry C, it fail to unmap [0x1ff000,0x200000] because first page’s ptep has
been destroyed and leave IOMMU still mapped at [0x200000,0x20x000]
Chain bugs to page UAF
Free entry C memory on CPU side to give entry C’s physical pages back to kernel, then
access entry C’s second half from GPU to cause page UaF access.
Agenda
● Backgrounds
● Bug analysis
● GPUAF exploit
● Conclusion
What is user import pages?
● When import memory from CPU for GPU, we got multiple choices
● anon mappings -> MAP_ANON
● dma-buf -> /dev/dma_heap/system (ion -> /dev/ion)
● Aio pages -> io_setup syscall
● …
Linux Physical Memory Management 101
Linux Physical Memory Management 101
Linux Physical Memory Management 101
Choose aio pages as victim
● We need find the proper candidates that can be reused for important kernel
objects such as task_struct or page tables
Choose aio pages as victim
● Aio pages has the GFP_HIGHUSER flag thus can be reused by kernel later.
● Aio pages are order 0, if we wanna reuse the page as page table and directly
build physical AARW primitive, this will be a good option
Way 1 - Exploit PageTables
● We can easily reclaimed our UaF pages as page table by spraying anon
mappings
Way 1 - Exploit PageTables
● ARM64 Pagetable 101
Way 1 - Exploit PageTables
● ARM64 Pagetable 101
Way 1 - Exploit PageTables
● ARM64 Pagetable 101
Way 1 - Exploit PageTables
● ARM64 Pagetable 101
Way 1 - Exploit PageTables
● To summarize, by manipulating the PTE at CPU side, we can:
○ Modify the read only page’s AP bits of PTE and make it as RW.
○ Build Physical memory AARW primitive by modify page frame
○ Mark a region of memory as rwx and execute arbitrary kernel shell code
Find kernel code physical address
● Except for Samsung phones, physical address of kernel image was fixed
● Since the kernel image is linearly mapped, we can calculate other global
symbols such as selinux_state based on the fixed kernel code address
● For Samsung phones, we will introduce two bypass ways later
Disable selinux
● Extract those offsets from kernel image and overwrite selinux_state to bypass
selinux.
Gain root privileges
● mmap libc++.so into userspace , make it partially RW and inject with our
shellcode to hijack init process and spawn our reverse shell to get root
● Init process will enter a while loop wait for event trigger after
SecondStageMain
● We can use setprop to make init process the event and get hijacked
Way 2 - Exploit pipe_buffer
● pipe_buffer is a core struct implemented for pipe relative function to do page
read/write
Way 2 - Exploit pipe_buffer
● pipe_read is used to read page from pipe_buffer
● By forging pipe_buffer, we can achieve arbitrary address read
Way 2 - Exploit pipe_buffer
● pipe_write is used to write data into the page of pipe_buffer
● When PIPE_BUF_FLAG_CAN_MERGE is included, pipe directly write data
instead of allocate a new page
● By forging pipe_buffer, we can also achieve arbitrary address write
Way 2 - Exploit pipe_buffer
1. Spray pipe_buffer to reclaim our UAF page.
2. Bypass physical ASLR and locate kernel code physical address
3. overwrite selinux_state to bypass selinux.
4. splice libc++.so into pipe_buffer, overwrite pipe_buffer’s flag to
PIPE_BUF_FLAG_CAN_MERGE, so we can overwrite libc++.so with our
shellcode to hijack init process and spawn our reverse shell to get root
Modern Mitigations Bypass on Samsung
● Enhanced Selinux
● KNOX in EL2
● DEFEX
● Physical address ASLR
Enhanced Selinux
● we find CONFIG_SECURITY_SELINUX_DEVELOP was not set on S24 Ultra
when checking config.gz
Enhanced Selinux
● Without CONFIG_SECURITY_SELINUX_DEVELOP, We can not overwrite
selinux_state to disable it
Enhanced Selinux
● Function security_compute_av will add AVD_FLAGS_PERMISSIVE flag if
ebitmap_get_bit return non 0 value
Enhanced Selinux
● Function avc_denied can always pass with AVD_FLAGS_PERMISSIVE
Enhanced Selinux
● Originally the e->node will be null, we need to forge a node by AARW in
kernel space, since this will be checked every time, we better find a space
that is global and unused by kernel.
Enhanced Selinux
● We got many options and luckily the bit is not quite huge, or we need to find
multiple unused place in kernel to chain this node link list together. Now 5 or 6
nodes will be enough, with each node fullfill the requirements below.
KNOX
● KNOX is a security hypervisor on Samsung mobile device is to ensure kernel
integrity at runtime, In order to do that, the hypervisor is executing at a higher
privilege level (EL2) than the kernel (EL1)
● Some mitigation security boundary is backed by KNOX, such as DEFEX
● We don’t have to “directly” bypass it to achieve our goals
DEFEX
● This protection prevents process not in the whitelist to run as root, introduced
since Android 8
● The whitelist is protected by EL2, can’t modify even with AARW in kernel
● Once detected, it will kill the malicious process by SIGKILL
DEFEX Bypass
● DEFEX tried so hard to prevent attackers bypassing it from Kernel level
● In fact, we might can easily bypass in directly in userspace
● After selinux disabled, we can inject dynamic library into any process
● So we just launch a new process in whitelist with our payload library
Phys ASLR Way 1
● Samsung’s PhyASLR is aligned with 0x10000 and quite weak, by checking
the instructions of _stext, can bypass it within 20 times.
Phys ASLR Way 2
● Remap vdso page to the pagetable we control that reclaimed our UaF pages
● So we can find a new PTE that contains vdso_data_store’s physical address,
and calculate selinux_state and other variables address.
Mitigation bypass on Vendor A/B/C
● To be honest, what else “Big” mitigations left to tell…?
Mitigation bypass on Vendor A/B/C
● To be honest, what else mitigations left to tell…?
● No, unless you manually made one xD
Mitigation bypass on Vendor A/B/C
● Where’s my firmware ???
Mitigation bypass on Vendor A/B/C
● Where’s my firmware ???
Obtain kernel offsets without firmwares - Way 1
● -rw-r--r-- 1 root root u:object_r:proc_security:s0 0 2024-09-09 14:12
/proc/sys/kernel/kptr_restrict
● allow init proc_security:file { append getattr ioctl lock map open read watch
watch_reads write };
● -r--r--r-- 1 root root u:object_r:proc_kallsyms:s0 0 2024-09-09 14:17
/proc/kallsyms
● type_transition init traced_perf_exec:process traced_perf;
● allow traced_perf proc_kallsyms:file { getattr ioctl lock map open read watch
watch_reads };
Obtain kernel offsets without firmwares - Way 2
● allow init dev_type:blk_file { getattr ioctl lock map open read watch watch_reads }; ->
dev/block/by-name/boot*
● allow init shell_test_data_file:file { create getattr map open read relabelfrom relabelto setattr
unlink write }; -> /data/local/tests
● allow shell shell_test_data_file:file { append create execute execute_no_trans getattr ioctl
lock map open read rename setattr unlink watch watch_reads write }; -> /data/local/tests
● allow shell shell_data_file:file { append create execute execute_no_trans getattr ioctl lock
map open read rename setattr unlink watch watch_reads write }; -> /data/local/tmp/
● allow init shell:process { rlimitinh siginh transition };
● allow shell shell_exec:file { entrypoint execute execute_no_trans getattr ioctl lock map open
read watch watch_reads };
● drwxrwx--x 2 shell shell 3.3K 2024-09-06 17:08 tmp (need bypass DAC to access by root
user)
Obtain kernel offsets without firmwares - Combine together
1. Hijack init to chmod 0777 for dir /data/local/tmp, thus make root user can
create/write the file under this dir (or setuid later to skip this)
2. Read target offsets info and copy contents into /data/local/tests dir
3. Transition our context to u:r:shell:s0 and cp boot.img from /data/local/tests to
/data/local/tmp
4. Untrusted_app can get the offsets from /data/local/tmp
5. [Optional] get offsets from boot.img
Addons: Root shell 1
● Get remote fd by pid and dup to init’s stdin/out/err
Addons: Root shell 2
● Let init process launch a bindshell that can be connected at anytime
Demo Samsung
Demo Other Vendor
Demo Other Vendor
Agenda
● Backgrounds
● Bug analysis
● GPUAF exploit
● Conclusion
Conclusions
● More details of using GPUAF has been discussed
● Creating side effects with page table are quite interesting
● Mitigation design should follow the attacker’s step
● Human made “mitigations” are not good mitigations
References
● The way to android root exploiting your gpu on smartphone
● The code that wasnt there, reading memory on an android device by accident
● Linux Kernel Physical Memory Management
● Armv8 Reference Manual for A-profile architecture
● How we use Dirtypipe to get reverse shell on Android Emulator and Pixel6
Q&A
Thanks for listening