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