深入分析TIMA任意內核模塊認證繞過漏洞
前言
為了確保Android設備中Linux內核的完整性,三星推出了一個名為“lkmauth”的功能。該功能的最初目的是,確保只有三星核準的那些內核模塊才可以加載到Linux內核中。
TIMA任意內核模塊認證繞過漏洞分析
每當內核嘗試載入內核模塊時,系統就會用到“lkmauth”功能。在加載內核模塊之前,內核首先會加載“lkmauth”的trustlet,并發送一個請求,來驗證模塊的完整性。
由于三星設備使用了兩個不同的TEE,所以對于每個TEE都單獨實現了相應的“lkmauth”功能。
在使用QSEE TEE(它使用了內核配置TIMA_ON_QSEE)的設備上,使用“tima_lkmauth”的trustlet來驗證待加載的內核模塊的完整性。當然,這個trustlet本身是相當簡單的——它提供了一個硬編碼的列表,來保存所有“可允許”的內核模塊的SHA1哈希值。如果當前待加載的內核模塊的SHA1沒有出現在硬編碼的列表中,那么它就會被拒絕。
對于使用MobiCore TEE(使用內核配置TIMA_ON_MC20)的設備而言,它們會通過“ffffffff00000000000000000000000b.tlbin”trustlet來驗證待加載內核模塊的完整性。然而,在這種情況下,其流程會稍微有點復雜,下面簡單介紹加載模塊的具體步驟:
- [如果trustlet尚未加載]:加載trustlet。
- [如果已批準的哈希值列表尚未加載]:向trustlet發送請求,以便加載已批準的SHA1哈希簽名列表。
- 將存放內核模塊的緩沖區傳遞給trustlet進行驗證。如果該內核模塊的SHA1哈希值不在先前加載的已批準哈希值列表中,則會被拒絕。
已經批準的模塊的哈希值組成的列表,將作為設備固件的一部分,存儲在文件“/system/lkm_sec_info”中。該文件的結構如下所示:
- <LIST_OF_APPROVED_SHA1_HASHES> || <RSA-SHA1(LIST_OF_APPROVED_HASHES)>
RSA簽名本身會使用PKCS#1 v1.5進行填充,其中BT = 1,PS是0xFF字節的常量字符串。
用于驗證簽名的公鑰,我們可以通過靜態分析方法從trustlet中找到。在trustlet的自身代碼中,2048位的模數(N)是以反向字節順序硬編碼的形式存在的。經驗證,在許多不同的設備和版本(如GT-I9300、SM-P600、SM-G925V等)中,都使用了相同的常量模量。這個模數本身是
- 23115949866714941391353337177289175219285878274139282906616665210063884406381659531323213685988661310147714551519208211866717752764819593136041821730036424774768373518089158559738346399417711215445691103520271683108620470478217421253901045241463596145712323679479119182170178158376677146612087823704797563128645982031650495998390419939015769566125776929249878666421780560391442439477189264423758971325406632562977618217815844688082799802924540355522191958147326121713251815752299744182840538928330568160188518794896256711464745438125835732128172016078553039694575936536720388879378619731459541542508235684590815108447
這里使用的公鑰指數為3。
發送到trustlet的請求緩沖區具有以下結構:
- /* 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;
通過對trustlet中處理這個命令的代碼進行逆向工程,得到了處理函數高級邏輯代碼,具體如下所示:
- 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;
- }
問題在于,上述代碼包含了一個邏輯缺陷:沒有對“ko_num”字段進行相應的驗證,以確保其匹配哈希值列表的實際長度。這意味著攻擊者能夠欺騙trustlet來加載額外的“允許哈希值”,即使它們不是已經簽名的blob的一部分。為此,可以在提供與哈希值列表的原始長度匹配的"hash_buf_len"的時候,通過提供一個大于實際哈希值數量的“ko_num”字段來達到這一目的。然后,攻擊者可以在緩沖器中的簽名blob之后提供任意的SHA1哈希值,從而導致這些額外的哈希值也會被復制到已經批準的可信哈希值列表中。
下面給出此類攻擊的一個具體例子:
- 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
由于“/system/lkm_sec_info”中的原始哈希值列表長度總是很短(例如從來不超過8)的,因此表達式((
- 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
實際上,這就將攻擊者控制的SHA1哈希值插入到了已批準的哈希值列表中,從而成功繞過了簽名驗證。
該漏洞的一種利用方法是,控制一個可以加載內核模塊的進程,然后將感染的哈希值列表請求發送給trustlet。例如,“system_server”進程就具有這種能力,同時還能夠加載trustlet,并與之進行通信(我們已經在SM-G925V的默認SELinux策略中進行了相應的驗證):
- 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 } ;
將受感染的哈希值列表加載到trustlet之后,攻擊者就可以嘗試加載與剛才插入到列表中的SHA1哈希值相匹配的內核模塊了。需要注意的是,加載模塊的第一次嘗試將會失敗,因為內核將嘗試加載已批準的哈希值列表本身,但是trustlet將檢測到此情況并返回錯誤代碼RET_TL_TIMA_LKMAUTH_HASH_LOADED。這樣的話,內核會做一個標記,指出列表已經加載好了——也就是說,下一次加載模塊的時候,就不會重新加載這個列表了:
- ...
- 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;
- }
- ...
之后,第二次嘗試加載已經感染的模塊的時候,就會成功了,因為它的哈希值已經位于已批準的哈希值列表中了。