An Interesting Feature in the Samsung DSP Driver

Written by David Berard - 02/03/2021 - in Exploit - Download
In February 2021 Samsung made some changes in one of its low level drivers : the Digital Signal Processor (DSP) Linux driver. They removed one interesting feature : the ability for untrusted apps to load a custom DSP firmware of their choice.

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 :

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.

The queue is filled by dsp_util_queue_enqueue :

This function is called with mbox->to_fw as queue argument. This pointer is set in dsp_mailbox_start :

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 :

The 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 :

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 :

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 a text pointer outside the vmalloc'ed buffer.
  • dest pointer is pointing inside the DSP_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).