Samsung's lkmauth feature suffers from a kernel module verification bypass vulnerability.
94c50ebfbad9ceb87ec411b72014c425
TIMA Arbitrary Kernel Module Verification Bypass
In order to ensure the integrity of the Linux Kernel on Android devices, Samsung has introduced a feature called "lkmauth". This feature is meant to make sure that only Samsung-approved kernel modules may be loaded into the kernel.
The "lkmauth" feature is used whenever the kernel attempts to load a kernel module. Before loading the kernel module, the kernel will load the "lkmauth" trustlet and send it a request in order to verify the integrity of the module.
Since Samsung devices use two different TEEs, the "lkmauth" feature is implemented separately, once for each TEE.
On devices with the QSEE TEE (where the kernel configuration TIMA_ON_QSEE is used), the "tima_lkmauth" trustlet is used in order to verify the integrity of the loaded kernel modules. The trustlet itself is quite straightforward - it contains a hard-coded list of SHA1 hashes for all the "allowed" kernel modules. If the SHA1 of the kernel module being loaded is not in the hard-coded list, it is rejected.
On devices with the MobiCore TEE (where the kernel configuration TIMA_ON_MC20 is used), the "ffffffff00000000000000000000000b.tlbin" trustlet is used in order to verify the integrity of the loaded kernel modules. However, the flow in this case is slightly more complex. Here's a short run down of the steps taken when loading a module:
1. [If the trustlet has not been loaded before]: The trustlet is loaded.
2. [If the approved hash list has not already been loaded]: A request is sent to the trustlet in order to load the signed list of approved SHA1 hashes.
3. The buffer containing the kernel module is passed to the trustlet for verification. If the SHA1 hash of the kernel module is not within the previously loaded list of approved hashes, it is rejected.
The list of approved hashes is stored as part of the device's firmware, within the file "/system/lkm_sec_info". This file has the following structure:
<LIST_OF_APPROVED_SHA1_HASHES> || <RSA-SHA1(LIST_OF_APPROVED_HASHES)>
The RSA signature itself is done with PKCS #1 v1.5 padding, where BT=1 and the PS is a constant string of 0xFF bytes.
The public key with which the signature is verified can be statically retrieved from the trustlet. The 2048-bit modulus (N) is hard-coded in reverse byte order in the trustlet itself. I have verified that the same constant modulus is used across many different devices and versions (i.e., GT-I9300, SM-P600, SM-G925V, etc.). The modulus itself is 23115949866714941391353337177289175219285878274139282906616665210063884406381659531323213685988661310147714551519208211866717752764819593136041821730036424774768373518089158559738346399417711215445691103520271683108620470478217421253901045241463596145712323679479119182170178158376677146612087823704797563128645982031650495998390419939015769566125776929249878666421780560391442439477189264423758971325406632562977618217815844688082799802924540355522191958147326121713251815752299744182840538928330568160188518794896256711464745438125835732128172016078553039694575936536720388879378619731459541542508235684590815108447. The public exponent is 3.
The request buffer sent to the trustlet has the following structure:
/* Message types for the lkmauth command */
typedef struct lkmauth_hash_s {
uint32_t cmd_id;
uint32_t hash_buf_start; /* starting address of buf for ko hashes */
uint32_t hash_buf_len; /* length of hash buf, should be multiples of 20 bytes */
uint8_t ko_num; /* total number ko */
} __attribute__ ((packed)) lkmauth_hash_t;
I've reverse-engineered the handling code for this command in the trustlet. Here is the high-level logic of the handling function:
int load_hash_list(char* hash_buf_start, uint32_t hash_buf_len, uint8_t ko_num) {
//Checking the signature of the hash buffer, without the length of the
//public modulus (256 bytes = 2048 bits)
uint32_t hash_list_length = hash_buf_len - 256;
char* rsa_signature_blob = hash_buf_start + hash_list_length;
if (verify_rsa_signature(hash_buf_start, hash_list_length, rsa_signature_blob))
return SIGNATURE_VERIFICATION_FAILURE;
//Copying in the verified hashes into the trustlet
//SHA1 hashes are 20 bytes long (160 bits) each
//The maximal number of copied hashes is 0x23
//g_hash_list is a list in the BSS section of the trustlet
//g_num_hashes is also in the BSS section of the trustlet
uint8_t i;
for (i=0; i<ko_num && i<0x23; i++) {
memcpy(g_hash_list + i*20, hash_buf_start + i*20, 20);
}
g_num_hashes = i;
return SUCCESS;
}
The code described above contains a logical flaw: the "ko_num" field is not verified in order to make sure it matches the actual length of the hash list. This means that an attacker may trick the trustlet into loading additional "allowed hashes", although they were not a part of the signed blob. This can be done by supplying a "ko_num" field that is larger than the actual number of hashes, while supplying "hash_buf_len" which matches the original length of the signed hash list. Then, an attacker may supply any SHA1 hash after the signature blob in the buffer. Causing these additional hashes to be copied into the trustlet's approved hash list.
Here's an example for such an attack:
hash_buf_start = <ORIGINAL_SIGNED_HASH_LIST> ||
<RSA-SHA1(ORIGINAL_SIGNED_HASH_LIST)> ||
<4 GARBAGE BYTES> ||
<ATTACKER_CONTROLLED_SHA1_HASH>
hash_buf_len = len(<ORIGINAL_SIGNED_HASH_LIST>) +
len(<RSA-SHA1(ORIGINAL_SIGNED_HASH_LIST)>)
ko_num = (<ORIGINAL_SIGNED_HASH_LIST>/20) + ceil(256/20) + 1
Since the original hash list in "/system/lkm_sec_info" is always rather short (i.e., never more than 8), the value of ((<ORIGINAL_SIGNED_HASH_LIST>/20) + ceil(256/20) + 1) is never larger than 22. This is still below the hard-coded limit of 0x23 (35) hashes, which means the code above will happily comply with the provided command. This, in turn, will cause the approved hash list to take on the following structure:
original_approved_hash_1
original_approved_hash_2
...
original_approved_hash_n
bytes_00_to_20_of_rsa_signature
bytes_20_to_40_of_rsa_signature
...
bytes_240_to_256_of_rsa_signature || 4_garbage_bytes
attacker_controlled_sha1_hash
Essentially inserting the attacker controlled SHA1 hash into the approved hash list, bypassing the signature verification.
One way to exploit this vulnerability is by taking control of a process which is capable of loading kernel modules (capable(CAP_SYS_MODULE)) and sending the poisoned hash list request to the trustlet. For example, the "system_server" process has this capability, and is also capable of loading and communicating with trustlets (as verified on an SM-G925V's default SELinux policy):
allow system_server mobicore-user_device : chr_file { ioctl read write getattr lock append open } ;
allow system_server mobicoredaemon : unix_stream_socket connectto ;
allow system_server mobicore_device : chr_file { ioctl read write getattr lock append open } ;
After loading the poisoned hash list into the trustlet, the attacker may then attempt to load a kernel module with the matching SHA1 hash which has just been inserted into the list. Note that the first attempt to load the module will fail, since the kernel will attempt to load the approved hash list itself, but the trustlet will detect this state and return an error code of RET_TL_TIMA_LKMAUTH_HASH_LOADED. This causes the kernel to mark a flag indicating that the list has already been loaded - meaning the next attempt to load a module will not attempt to reload the list:
...
else if (krsp->ret == RET_TL_TIMA_LKMAUTH_HASH_LOADED) {
pr_info("TIMA: lkmauth--lkm_sec_info already loaded\n");
ret = RET_LKMAUTH_FAIL;
lkm_sec_info_loaded = 1;
}
...
Finally, the second attempt to load the poisoned module will succeed, as its hash is within the approved list.
This bug is subject to a 90 day disclosure deadline. If 90 days elapse
without a broadly available patch, then the bug report will automatically
become visible to the public.
Found by: laginimaineb