In many ways, mobile devices lead the security industry when it comes to defense-in-depth and mitigation. Over the years, it has been proven time and again that the kernel cannot be trusted to be secure. As such, there has been effort put into moving secrets (ie. encryption keys) and other sensitive data out of the kernel and gate it behind an API at higher levels in the chain of trust, whether it be the hypervisor or secure enclaves. In any case, the kernel must have a lot of control over the system.
To strike a balance, some companies have tasked the more trusted hypervisor to provide some level of integrity protection to the guest kernel. In the Android space, Huawei and Samsung in particular have refined virtualization-based security over the years into a pretty solid protection. These solutions are used as a selling point of the device, and the fact they exist is fairly common knowledge, however, how they work at a deep level is not. The code is all closed-source, and public documentation only talks about the architecture at a high-level overview.
In this series of articles, we're going to focus on Samsung's exynos-implementation of their mobile hypervisor security platform. This platform is commonly referred to as Real-time Kernel Protection or RKP, which itself is a part of Knox. While some past blog posts exist that have talked about RKP, they are 4-5 years old at this point, and much has changed in recent years.
Note: Snippets and screenshots of reversed code are from the Galaxy S23 Exynos version.
Permission Model Overview
Mobile has a slightly complex permission model, so it's worth covering briefly for those who may not be familiar with it. We're going to focus on modern devices, which means ARMv8 architecture (aka AARCH64). ARMv8 provides hardware support for four Exception Levels (EL) and two secure states. Unlike x86, the privilege of each exception level increases the higher it is. Where user-mode is ring3 in x86, it is EL0 in ARMv8. Similarly, EL1 would be the equivalent of x86's ring0.
Two security states allow for switching between a "non-secure" and a "secure" world, which is facilitated by TrustZone. TrustZone allows developers to isolate security-related code and data from an untrusted environment (the main OS). Non-secure world is typically called the Rich Execution Environment (REE), where secure world is referred to as the Trusted Execution Environment (TEE). Communication between these environments is facilitated by the secure monitor, which runs in EL3.
When user-mode in EL0 needs to call into EL1, it will use the SuperVisor Call (SVC) instruction. Similarly, EL1 to EL2 will use HyperVisor Call (HVC). Both EL1 and EL2 will use Secure Monitor Call (SMC) to call into EL3.
Everything we'll be looking at resides in non-secure world, and most of the code we'll look at will be running at EL2.
A Brief History
Before we dive into H-Arx, let's first look at how Samsung's hypervisor has changed over the years.
Pre-2018 - vmm.elf
In the early days, RKP was a monolithic hypervisor contained in the vmm.elf
file that was embedded in the kernel and shipped in source tarballs (with symbols too!). Embedding the hypervisor has some benefits from a developer's perspective, such as being able to reuse existing kernel code. From a security perspective though, this was a poor design if you consider the kernel to be untrusted. In circa 2018, the hypervisor was refactored into a separate binary in the Bootloader (BL) partition as uh.bin
.
2018-2020 - uh.bin
uh.bin
now contained the hypervisor ELF with a proprietary "GREENTEA" header and several obfuscations were added. Symbols were stripped and string formatting information was replaced wholesale with hashed versions in some part of the build process. It's been speculated that "uH" likely means "micro-hypervisor", though it seems that this name was a bit of a place-holder at this point, as functionality hadn't been split up yet. This iteration of the hypervisor is the most recent one covered by previous public writeups [1].
Side-note: We toyed with the idea of bruteforcing these hashes at one point in time, as they're truncated (upper 32-bits) SHA256 hashes of the format string contents. Unfortunately, we quickly discovered that when truncating SHA256 hashes to the most significant bits especially, you quickly run into multiple collisions due to the pigeonhole principle. We were able to recover some format strings this way, but not all of them.
Post-2020 - H-Arx and Rust
Around the release of the Samsung Galaxy S20, uh.bin
started to look a lot weirder. The structure of the binary changed pretty significantly, and we see the presence of a new binary in the BL partition, harx.bin
. On the Galaxy S22 and later, one can quickly see that Samsung has transitioned to a rust code-base to some degree. This is tipped-off by looking at strings, notice the rust filenames and non-null terminated strings.
Another small yet notable detail is that uh.bin
no longer contains the hypervisor as an ELF file but instead is a completely proprietary format. Data at 0x1000 is particularly interesting, as it denotes the file as a "plug-in", also referencing H-Arx. This "plug-in" header has also gone through some changes and reiteration, which we'll get into a little later. The current header (v2) looks something like this:
It's clear that later versions of the hypervisor had its functionality split into a more modular design, which likely enabled rewriting some of the hypervisor in Rust. How this latest iteration is architected is not obvious, and of course, not publicly documented either. Seeing as to this day there is still nothing we could find that detailed how this architecture works, we decided to pull our prior research together and document it as a blog post.
Loading H-Arx
In the interest of brevity, we're not going to go into a detailed analysis of Samsung's boot process. We do however have to look at SBOOT a bit to get a picture of how H-Arx gets loaded. SBOOT is Samsung's Exynos bootloader, and one of its responsibilities is to initialize the hypervisor. If you want to read more on SBOOT and Samsung's chain of trust, QuarksLab has some excellent blog posts [2] [3]. While the posts are fairly old and the architecture has undergone some changes, many details are still relevant. Our look into SBOOT will be strictly for the loading and initialization of the hypervisor (EL2).
As always, a good place to start is by looking at strings. Luckily SBOOT has quite a few of them that lead us to the right place.
Many of these strings are referenced in the same function, which we can see is ultimately responsible for loading and initializing H-Arx. Below is a snippet of the reversed function.
int load_init_harx()
{
uintptr_t unk_var_38h;
// ...
if (load_el2_module("HARX", 0xc0000000, unk_var_38h) != 0) {
printf("[H-Arx] ERROR: Fail to load H-Arx binary\n");
return -1;
}
printf("[H-Arx] Loading done\n");
if (__smc_call(0x82000480, 0xc0000000) != 0) {
printf("[H-Arx] ERROR: Fail to initialize H-Arx [ret = %lx]\n", ret);
return -1;
}
printf("[H-Arx] Complete to initialization\n");
// ...
}
We can guess with pretty high confidence that 0xc0000000
is the load address of `harx.bin`, and we can see that it's initialized and jumped to via a Secure Monitor Call (SMC) into EL3.
Unfortunately, as mentioned by QuarksLab, the TrustZone firmware that runs in EL3 and handles these calls appears to be encrypted. You can confirm this by looking at the entropy of partitions such as TZSW.img
and seeing that it's extremely high, which almost always indicates encryption.
All things told, while it would be nice to see what's happening on the TrustZone-side, we don't really need to in order to understand how it's loaded. We know it's loaded to 0xc0000000
and runs in EL2, which is a good start. We can use this information to load harx.bin
into a disassembler.
H-Arx Plugin Loading
We've referred to "plug-ins" a few times now. H-Arx consists of a common core (harx.bin
), and a series of loadable plug-ins. You can think of plug-ins similar to .ko kernel modules from the Linux kernel. As you may have guessed from the snippet of SBOOT strings, SBOOT is also what kickstarts the loading of these plug-ins. The following reversed functions are relevant for loading uh.bin
.
int get_plugin_info(uint64_t p_addr, uint64_t *p_size)
{
struct uh_header *uh = (struct uh_header *) *p_addr;
printf("uh size : %x\n", uh->field_08h);
printf("uh header size : %x\n", uh->field_48h);
if (memcmp(addr, "GREENTEA", 8) != 0) {
printf("Invalid UH magic\n");
return -1;
}
*p_addr += uh->field_48h; // header size
*p_size = uh->field_48h + uh->field_08h + 0x310;
printf("new plugin_entry : %lx, new plugin_size : %llx\n", *p_addr, *p_size);
return 0;
}
int load_plugin(char *name, uintptr_t base_addr)
{
int ret;
uint64_t size;
uint64_t plugin_addr = base_addr;
if (g_harx_init == 0) {
printf("[H-Arx Plug-in] H-Arx is not initialized\n");
return -1;
}
if (load_el2_module(name, base_addr, &size)) {
printf("[H-Arx Plug-in] ERROR: Fail to load %s binary\n", name);
return -1;
}
if (memcmp(name, "UH", 2) == 0) {
if (ret = get_plugin_info(&plugin_addr, &size); ret != 0) {
printf("[H-Arx Plug-in] %s Fail to get plug-in info (%lx)\n", name, ret);
return -1;
}
} else {
// ...
}
printf("[H-Arx Plug-in] plugin_addr: %lx size: %llx, entry: %lx\n",
base_addr, size, base_addr);
__hvc_call(0xc6000010, base_addr, size, plugin_addr, 0);
// ...
}
int sub_245988()
{
// ...
if (load_plugin("UH", 0xc1400000) != 0) {
printf("There is no H-Arx plug-in\n");
}
// ...
}
In some ways this looks very similar to the function that loads harx.bin
, one notable difference though is that the initialization happens via a Hypervisor Call (HVC) rather than an SMC. We'll circle back to this shortly. This bit of code gives some good insight into the proprietary uH format. Again, this header has undergone some various revisions, but the latest should look like this:
We can see how the data matches up to the parsing logic.
struct uh_header
{
char magic[0x8]; // 0x00 - should be "GREENTEA"
uint64_t plugin_size; // 0x08 - doesn't include header+footer
char unk_10h[0x38]; // 0x10
uint64_t header_size; // 0x48 - should be 0x1000
char unk_50h[0xFB0]; // 0x50
} // size: 0x1000
We now know that the uH plugin is loaded to 0xc1400000
, and also runs in EL2.
It's finally time we look at harx.bin
, and we'll start by looking at the 0xc6000010
hypercall used in the plugin load process. While we can go through the process of looking at the vector table to find the exception handlers, a quick search of the High Level IL (HLIL) in Binary Ninja quickly gets us to where we want to be.
exynos_plugin_init()
contains a ton of useful strings for giving us a complete picture of the parsing logic. Cleaned up, the part of the function we care about for figuring out the format looks like this:
int exynos_plugin_init(uint64_t addr, uint64_t size, void *entry)
{
if (ret = exynos_check_dram_address(addr, size); ret != 0) {
printf("\n%s: Invalid Plug-in address[0x%llx]/size[0x%llx] - (%x)\n",
"exynos_plugin_init", addr, size, ret);
return 0xb001;
}
if ((entry & 3) != 0) {
printf("\n%s: Not aligned plugin_entry[0x%llx]\n", "exynos_plugin_init", entry);
return 0xb002;
}
if (entry < addr || entry >= (addr + size)) {
printf("\n%s: Invalid plugin_entry[0x%llx]\n", "exynos_plugin_init", entry);
return 0xb003;
}
// ...
memcpy(&plugin_v2_magic, entry + 0x10, 0x10);
if (strncmp(&plugin_v2_magic, "PLUG-IN_VER2", 0xC) == 0) {
header_ver = 2;
memcpy(&header.v2, entry, 0x78);
plugin_text_start = header.v2.field_20h;
plugin_text_end = header.v2.field_28h;
plugin_rodata_start = header.v2.field_30h;
plugin_rodata_end = header.v2.field_38h;
plugin_rwdata_start = header.v2.field_40h;
plugin_rwdata_end = header.v2.field_48h;
plugin_rela_start = header.v2.field_60h;
plugin_rela_end = header.v2.field_68h;
plugin_entrypoint = (void *) entry + 0x78;
} else {
header_ver = 1;
memcpy(&header.v1, entry, 0x40);
plugin_text_start = header.v1.field_10h;
plugin_text_end = header.v1.field_18h;
plugin_rodata_start = header.v1.field_20h;
plugin_rodata_end = header.v1.field_28h;
plugin_rwdata_start = header.v1.field_30h;
plugin_rwdata_end = header.v1.field_38h;
plugin_entrypoint = (void *) entry + 0x40;
}
printf("... Plug-in header version %d\n", "exynos_plugin_init", header_ver);
plugin_text_size = plugin_text_end - plugin_text_start;
plugin_rodata_size = plugin_rodata_end - plugin_rodata_start;
plugin_rwdata_size = plugin_rwdata_end - plugin_rodata_end;
plugin_total_size = plugin_rodata_size + plugin_text_size + plugin_rwdata_size;
// ... (plugin signature verification via EL3)
printf("... Plug-in verification success\n", "plugin_verification");
printf("... [Plug-in] TEXT start : 0x%llx\n", plugin_text_start);
printf("... [Plug-in] TEXT size : 0x%llx\n", plugin_text_size);
printf("... [Plug-in] RODATA start : 0x%llx\n", plugin_rodata_start);
printf("... [Plug-in] RODATA size : 0x%llx\n", plugin_rodata_size);
printf("... [Plug-in] RWDATA start : 0x%llx\n", plugin_rwdata_start);
printf("... [Plug-in] RWDATA size : 0x%llx\n", plugin_rwdata_size);
printf("... [Plug-in] Total size : 0x%llx\n", plugin_total_size);
if (header_ver == 2) {
// ...
printf("... [Plug-in] RELA start : 0x%llx\n", plugin_rela_start);
printf("... [Plug-in] RELA end : 0x%llx\n", plugin_rela_end);
printf("... [Plug-in] Plugin relocation done\n");
}
// ...
ret = plugin_entrypoint(0xc01c0000, 0x80);
}
While plugins may no longer be ELF files, they have a header with information that's just as useful for loading and analyzing plugins!
struct harx_plugin_info_v1
{
uint64_t plugin_text_start; // 0x10
uint64_t plugin_text_end; // 0x18
uint64_t plugin_rodata_start; // 0x20
uint64_t plugin_rodata_end; // 0x28
uint64_t plugin_rwdata_start; // 0x30
uint64_t plugin_rwdata_end; // 0x38
}; // size: 0x40 (including common header)
struct harx_plugin_info_v2
{
char version_two[0x10]; // 0x10 - should be "PLUG-IN_VER2"
uint64_t plugin_text_start; // 0x20
uint64_t plugin_text_end; // 0x28
uint64_t plugin_rodata_start; // 0x30
uint64_t plugin_rodata_end; // 0x38
uint64_t plugin_rwdata_start; // 0x40
uint64_t plugin_rwdata_end; // 0x48
char unk_50h[0x10]; // 0x50
uint64_t plugin_rela_start; // 0x60
uint64_t plugin_rela_end; // 0x68
uint64_t unk_70h; // 0x70
}; // size: 0x78 (including common header)
struct harx_plugin_header
{
char magic[0x10]; // 0x00
union
{
struct harx_plugin_info_v1 v1; // 0x10
struct harx_plugin_info_v2 v2; // 0x10
};
}; // size: 0x40 (v1) or 0x78 (v2)
If we make this into a pattern for ImHex and look at our uh.bin file again:
This also confirms that the uh.bin
plugin will be loaded to 0xc1400000
. Further, we have the segment layout. We used this to write a nice binary ninja loader. The segment specified by 0x1050 and 0x1058 appears to be mostly unused / annotation section. It's worth noting that the first DWORD after the header is the first instruction, which will jump to the plugin's entrypoint routine.
All in all, the H-Arx's load process looks like this:
Plugin and Core Communication
At this point, we've reversed the file formats for the H-Arx core (harx.bin
) and plugins (uh.bin
), and how they're both loaded by SBOOT. But how do they interact with each other? We can see from plugin_init()
detailed earlier that after parsing the header, mapping the plugin into EL2 memory, and verifying the signature, it'll jump to the plugin_entrypoint()
.
int exynos_plugin_init(uint64_t addr, uint64_t size, void *entry)
{
// ...
ret = plugin_entrypoint(0xc01c0000, 0x80);
// ...
}
The first argument to the entrypoint is interesting where it is a pointer in H-Arx memory. If we look at this area in the binary though, it's filled with zeroes. Clearly this area is initialized at runtime rather than statically, and discovering where this area is initialized is actually somewhat challenging if taking a completely static reversing approach. It's also difficult to approach it by looking at how uh.bin
uses this argument, because uh.bin
is stripped and has very few useful strings to aid our reversing efforts. The fact that it's Rust also means a lot of "bloat" for lack of a better word is emitted in the compilation process, further complicating reversing.
Instead, we decided to go the route of emulating the H-Arx core to gain introspection that way. We'll dedicate the next blog post on emulating H-Arx, for now though, let's just jump to the end results of the emulation. We wrote a hook that will log all the writes to this area when it gets initialized, the results are as follows (zero writes are omitted for brevity):
[EMULATOR] write 0x0 -> 0xc01c0000
...
[EMULATOR] write 0xc0003920 -> 0xc01c0020
...
[EMULATOR] write 0xc0003c40 -> 0xc01c0080
[EMULATOR] write 0xc0003ba8 -> 0xc01c0088
...
[EMULATOR] write 0xc00031f8 -> 0xc01c0100
[EMULATOR] write 0xc0003258 -> 0xc01c0108
[EMULATOR] write 0xc00032b0 -> 0xc01c0110
[EMULATOR] write 0xc0003530 -> 0xc01c0118
[EMULATOR] write 0xc00035b8 -> 0xc01c0120
[EMULATOR] write 0xc0003614 -> 0xc01c0128
[EMULATOR] write 0xc0003654 -> 0xc01c0130
[EMULATOR] write 0xc0003658 -> 0xc01c0138
[EMULATOR] write 0xc0003660 -> 0xc01c0140
[EMULATOR] write 0xc0003668 -> 0xc01c0148
[EMULATOR] write 0xc0003670 -> 0xc01c0150
...
[EMULATOR] write 0xc0003678 -> 0xc01c0180
[EMULATOR] write 0xc00036cc -> 0xc01c0188
[EMULATOR] write 0xc0003720 -> 0xc01c0190
[EMULATOR] write 0xc0003730 -> 0xc01c0198
[EMULATOR] write 0xc000373c -> 0xc01c01a0
...
[EMULATOR] write 0xc0000230 -> 0xc01c01c0
[EMULATOR] write 0xc0005028 -> 0xc01c01c8
...
[EMULATOR] write 0xc00037f0 -> 0xc01c0240
...
[EMULATOR] write 0xc0011be8 -> 0xc01c0280
[EMULATOR] write 0xc0011c20 -> 0xc01c0288
[EMULATOR] write 0xc0011c58 -> 0xc01c0290
...
[EMULATOR] write 0xc00118f0 -> 0xc01c02c0
[EMULATOR] write 0xc0011910 -> 0xc01c02c8
...
[EMULATOR] write 0xc0003888 -> 0xc01c0300
...
[EMULATOR] write 0xc0004ba4 -> 0xc01c0340
[EMULATOR] write 0xc00039f8 -> 0xc01c0348
[EMULATOR] write 0xc0003a18 -> 0xc01c0350
...
[EMULATOR] write 0xc000c56c -> 0xc01c0380
[EMULATOR] write 0xc000c7c0 -> 0xc01c0388
[EMULATOR] write 0xc000c884 -> 0xc01c0390
[EMULATOR] write 0xc000c8c4 -> 0xc01c0398
[EMULATOR] write 0xc000c904 -> 0xc01c03a0
NOTICE: H-Arx Start
That is a lot of pointers! What's more is, these pointers are all in H-Arx's .text
segment, and a quick look in the disassembler shows that these are all function pointers. In other words, the array passed to the plugin entry point is an array of callbacks for the plugin to use to call back into the H-Arx core. These functions are referred to as "plugin API", and many of them can be easily identified via strings. One example is the function 0xc0003920
which is written to the callback array at offset `+0x20`, and is used for the plugin to register itself.
Below is a listing of known API functions and their offsets in the callback array.
+0x020 - exynos_plugin_api_register
+0x080 - exynos_plugin_api_register_hvc_handler
+0x088 - exynos_plugin_api_register_sync_exception_handler
+0x110 - exynos_plugin_api_stage1_entire_dram_map
+0x118 - exynos_plugin_api_hyp_map_va
+0x130 - exynos_plugin_common_hyp_fixmap_unmap
+0x190 - exynos_plugin_api_memory_host_donate_hyp
+0x198 - exynos_plugin_api_memory_hyp_donate_host
+0x1a0 - exynos_plugin_api_hyp_private_and_stage2_map
+0x1c0 - exynos_plugin_set_hcr_el2
+0x1c8 - exynos_plugin_read_vcntr
+0x240 - exynos_plugin_api_register_sec_os_handler
+0x300 - exynos_plugin_api_register_pm_handler
+0x348 - exynos_plugin_api_invoke_smc
+0x350 - exynos_plugin_api_sw_reset
+0x380 - exynos_plugin_api_tzmp2_drm_buffer_protect
+0x388 - exynos_plugin_api_tzmp2_drm_buffer_unprotect
+0x390 - exynos_plugin_api_tzmp2_vgen_configure
+0x398 - exynos_plugin_api_tzmp2_vgen_configure_2
On the plugin side, we can see these callbacks in action in the uH plugin.
Here's a diagram that illustrates the interactions between H-Arx core and the plug-ins.
Delivery of Hypercalls
One may wonder, if the H-Arx core has a handler for HVCs for the bootloader to use, how exactly does RKP get notified and get the chance to handle its own set of hypercalls issued from the kernel? Those who looked closely at the _start()
routine in uh.bin
may have spotted the answer, the plugin_api_register_hvc_handler()
callback. Let's take a look at the top of H-Arx's HVC handler to look more closely.
All of the hypercalls that are relevant to the bootloader begin with a 0xc6......
prefix. If the command doesn't fall into this range, it is dispatched to the plug-ins via exynos_plugin_hvc_handler()
. We can also see that there's support for up to 4 plug-ins.
There are three fields from the plug-in info object that are used here. The field at offset +0x10
denotes the start of the command range, and +0x14
the end of the range. Finally, the function pointer at offset +0x18
is the handler for that plugin. These fields are set when the plug-in invokes the plugin_api_register_hvc_handler()
callback.
If we take a look at uh.bin's entrypoint again, we can see this callback being used and determine the uH plug-in's HVC handler function.
void _start(void **api_callbacks, int arg2)
{
// ...
int handle = plugin_register(1, "rkp", 3);
g_plugin_handle = handle;
plugin_api_register_hvc_handler_1(g_plugin_handle, 0x83000000, 0x8300ffff, sub_c140bf04);
// ...
}
This command range matches up to the linux/uh.h
header you can find in the Samsung kernel source repositories [4].
/* For uH Command */
#define PLATFORM 0
#define APP_RKP 1
#define APP_KDP 2
#define APP_HDM 4
#define APP_HARSH 5
#define UH_PREFIX UL(0x83000000)
#define UH_APPID(APP_ID) (((UL(APP_ID) << 8) & UL(0xFF00)) | UH_PREFIX)
// ...
The header hints that there are multiple "apps" inside of uH, and bits 16:8 encode the "app ID". These acronyms can be found in Samsung docs and headers. Commands for these apps can be found in different headers as well.
uH's HVC handler stores EL2 system registers along with the command and arguments in a per-CPU structure stored in the read-write data region.
A function later in the handler is invoked that will do a table lookup to invoke the app-specific handler. These tables are initialized via calls to each app's init routine from the entrypoint.
void _start(void **api_callbacks, int arg2)
{
// ...
init_rkp_app(0)
init_kdp_app(0)
init_hdm_app(0)
// ...
}
The location of these functions are given away by the fact that they reference the plaintext command names when initializing their command tables. For example, below is a screenshot of init_rkp_app()
's HLIL.
Real-time Kernel Protection (RKP)
Real-time Kernel Protection (RKP) is the headlining hypervisor-based security feature and is mainly used to manage and set pages as read-only, protecting them from being corrupted easily by a compromised kernel. Commands and input/output structures are found in linux/rkp.h
[5].
enum __RKP_CMD_ID {
RKP_START = 0x00,
RKP_DEFERRED_START = 0x01,
/* RKP robuffer cmds*/
RKP_GET_RO_INFO = 0x2,
RKP_CHG_RO = 0x03,
RKP_CHG_RW = 0x04,
RKP_PGD_RO = 0x05,
RKP_PGD_RW = 0x06,
RKP_ROBUFFER_ALLOC = 0x07,
RKP_ROBUFFER_FREE = 0x08,
/* module, binary load */
RKP_DYNAMIC_LOAD = 0x09,
RKP_MODULE_LOAD = 0x0A,
RKP_BPF_LOAD = 0x0B,
/* Log */
RKP_LOG = 0x0C,
#ifdef CONFIG_RKP_TEST
RKP_TEST_INIT = 0x0D,
RKP_TEST_GET_PAR = 0x0E,
RKP_TEST_EXIT = 0x0F,
RKP_TEST_TEXT_VALID = 0x12,
#endif
RKP_KPROBE_PAGE = 0x11,
};
Kernel Data Protection (KDP)
Kernel Data Protection (KDP) is used to protect sensitive data in the kernel and primarily exists to mitigate Data-Oriented Programming (DOP) attacks. Some of the things it's used to protect include process credentials, variables for controlling SELinux policy enforcement, and page tables. Commands can be found in `linux/kdp.h` [6].
enum __KDP_CMD_ID {
KDP_INIT = 0x00,
JARRO_TSEC_SIZE = 0x02,
SET_SLAB_RO = 0x03,
SET_FREEPTR = 0x04,
PREPARE_RO_CRED = 0x05,
SET_CRED_PGD = 0x06,
SELINUX_CRED_FREE = 0x07,
PGD_RWX = 0x08,
MARK_PPT = 0x09,
PROTECT_SELINUX_VAR = 0x0A,
NS_INIT = 0x10,
SET_NS_BP = 0x11,
SET_NS_DATA = 0x12,
SET_NS_ROOT_SB = 0x13,
SET_NS_SB_VFSMOUNT = 0x14,
SET_NS_FLAGS = 0x15,
#ifdef CONFIG_KDP_TEST
TEST_INIT = 0x16,
TEST_GET_PAR = 0x17,
TEST_EXIT = 0x18,
#endif
};
Hypervisor Device Manager (HDM)
A relatively newer protection, the Hypervisor Device Manager (HDM) can control access to hardware and peripherals, such as the camera and microphone [7]. This is specifically designed to mitigate the capability of malware to spy on victims after compromising the kernel. Policies can be set in place to disable access to certain hardware if the device is determined to be "compromised". As all of this is configured and set in motion before the kernel runs, the kernel's header doesn't contain HDM commands. However, we can see them in uh.bin
as plaintext strings.
enum HDM_CMD_ID
{
HDM_INIT = 0x00,
HDM_CONTROL = 0x01,
HDM_BL_CONTROL = 0x02,
HDM_QC_SEC_BUF_INTERFACE = 0x09,
};
Conclusion and Takeaways
Samsung has moved away from a monolithic design for their Exynos hypervisor implementation, opting for a more modular design. Migrating to Rust for the more accessible components of the hypervisor is a smart decision, though calling it a "micro-hypervisor" is a bit of a misnomer. While the uH plugin is technically separately loaded and written in a safer language than the H-Arx core, they both run at the same privilege level with no privilege boundary between them. Still, one can only imagine their design will improve going forward, and perfect shouldn't be the enemy of good.
In future blog posts, we plan to go into more detail on how H-Arx can be emulated for reverse engineering purposes. We'd also like to get into more detail on how some of the RKP/KDP specific functionality is implemented. There may also be additional posts we add on to the series at a later point, so stay tuned!
This research was pulled together while preparing material for our training focused on virtualization and security hypervisors, which we are giving this summer at Hardwear.IO USA and REcon Montreal. See our training page for dates and links if you or someone you know is interested!
References
- [1] A Samsung RKP Compendium: https://blog.impalabs.com/2101_samsung-rkp-compendium.html
- [2] Reverse Engineering Samsung S6 SBOOT - Part 1: https://blog.quarkslab.com/reverse-engineering-samsung-s6-sboot-part-i.html
- [3] A Deep Dive Into Samsung's TrustZone (Part 1): https://blog.quarkslab.com/a-deep-dive-into-samsungs-trustzone-part-1.html
- [4] Galaxy S23FE Kernel Mirror (linux/uh.h): https://github.com/Elchanz3/android_kernel_samsung_sdk-s5e9925_r11s/blob/DXI3-rebase/include/linux/uh.h
- [5] Galaxy S23FE Kernel Mirror (linux/rkp.h): https://github.com/Elchanz3/android_kernel_samsung_sdk-s5e9925_r11s/blob/DXI3-rebase/include/linux/rkp.h
- [6] Galaxy S23FE Kernel Mirror (linux/kdp.h): https://github.com/Elchanz3/android_kernel_samsung_sdk-s5e9925_r11s/blob/DXI3-rebase/include/linux/kdp.h
- [7] Knox HDM: High-Assurance Control of Peripheral Devices: https://www.samsungknox.com/en/blog/knox-hdm
- Our Binary Ninja Loader: https://github.com/dayzerosec/Samsung-HARX-Loader