一、漏洞背景
2020年3月,谷歌修補了一個存在於聯發科晶片中的安全漏洞(CVE-2020-0069),漏洞影響20餘款聯發科晶片和數百萬Android裝置。該漏洞存在於MediaTek Command Queue驅動(CMDQ命令佇列驅動),允許本地攻擊者實現對實體記憶體地址的任意讀寫,從而導致許可權提升。
二、受影響國產手機型號
Huawei GR3 TAG-L21
Huawei Y5II
Huawei Y6II MT6735 series
Lenovo A5
Lenovo C2 series
Lenovo Tab E7
Lenovo Tab E8
Lenovo Tab2 A10-70F
Meizu M5c
Meizu M6
Meizu Pro 7 Plus
Oppo A59 series
Oppo A5s
Oppo A7x -- up to Android 8.x
Oppo F5 series/A73 -- up to A.39
Oppo F7 series -- Android 8.x only
Oppo F9 series -- Android 8.x only
Oppo R9xm series
Xiaomi Redmi 6/6A series
ZTE Blade A530
ZTE Blade D6/V6
ZTE Quest 5 Z3351S
三、CMDQ驅動簡析
DMA(直接記憶體訪問)是允許專用硬體直接從主儲存器(RAM)傳送或接收資料的一種特性。其目的是透過允許大記憶體訪問而不過多佔用CPU來加速系統。MediaTek Command Queue驅動(CMDQ命令佇列驅動)允許從使用者層與DMA控制器通訊,以實現媒體或顯示相關的任務。
基於Redmi 6/6A 原始碼分析,在cmdq_driver.h標頭檔案中,宣告cmdq驅動的IOCTL呼叫如下:
(1)CMDQ_IOCTL_ALLOC_WRITE_ADDRESS指令為分配一個DMA緩衝區
(2)CMDQ_IOCTL_FREE_WRITE_ADDRESS指令為釋放一個DMA緩衝區
(3)CMDQ_IOCTL_READ_WRITE_ADDRESS指令為讀取一個DMA緩衝區中的資料
(4)CMDQ_IOCTL_EXEC_COMMAND指令執行傳送其他命令
1、 分配過程
透過CMDQ_IOCTL_ALLOC_WRITE_ADDRESS呼叫cmdqCoreAllocWriteAddress ()函式,分配一個DMA緩衝區,該函式關鍵程式碼實現如下:
然後,呼叫cmdq_core_alloc_hw_buffer()函式分配DMA緩衝區,pWriteAddr->va是虛擬地址,pWriteAddr->pa為實體地址,兩者一一對應。並清理緩衝區。
最後,將實體地址賦值到*paStart,並將pWriteAddr結構體新增到gCmdqContext.writeAddrList連結串列中。
2、 執行命令過程
在CMDQ_IOCTL_EXEC_COMMAND呼叫中,採用cmdqCommandStruct結構體作為引數,結構體定義如下:
pVABase指向使用者層存放命令的緩衝區,緩衝區大小放在blockSize中。其中cmdqReadAddressStruct結構體定義如下:
DmaAddresses是要讀取的實體地址,讀取的值存放在values中。在CMDQ_IOCTL_EXEC_COMMAND命令的執行過程,實現程式碼如下:
函式呼叫路徑如下:
Cmdq_core_acquire_task()函式會將command繫結到task中執行。具體實現如下:
呼叫cmdq_core_find_free_task()函式獲取一個空閒task。拿到空閒task並進行一些初始化設定,然後開始呼叫cmdq_core_insert_read_reg_command()函式執行命令。
該函式實現分析,先複製使用者層傳入的命令到DMA緩衝區中。
pCommandDesc->pVABase是存放命令的記憶體起始地址。複製完命令後,後面分幾種方式結尾。
這裡不做深究,最後複製EOC和JUMP指令結尾。這裡也是將使用者層傳入的命令複製過來。
從cmdq_core_acquire_task()函式中返回後,如下:
呼叫cmdq_core_consume_waiting_list()函式執行task。先從等待佇列中獲取task。
然後,獲取空閒核心執行緒。
最後,將task繫結到thread中去執行。
四、讀寫命令分析
以cmdq_test.c測試程式碼為例,分析理解一個完整的讀寫命令構造。cmdq驅動中定義了兩類暫存器,一類是地址暫存器用於存放地址,一類是數值暫存器用於存放讀取或寫入的數值。
regResults是虛擬地址,呼叫cmdq_core_alloc_hw_buffer()函式分配一個dma地址,regResultsMVA與之對應,然後設定regResults中的資料。開始拼接讀取和寫入命令:
將regResults[0]的地址寫入CMDQ_DATA_REG_DEBUG_DST型別的地址暫存器中。
然後,從CMDQ_DATA_REG_DEBUG_DST地址暫存器中讀取資料並寫入到CMDQ_DATA_REG_DEBUG數值暫存器中。這時候,CMDQ_DATA_REG_DEBUG數值暫存器中的值應該為0xdeaddead。
接著,將regResults[1]的地址轉存到CMDQ_DATA_REG_DEBUG_DST地址暫存器中。
最後,將CMDQ_DATA_REG_DEBUG數值暫存器中的0xdeaddead寫入到CMDQ_DATA_REG_DEBUG_DST地址暫存器中儲存的regResults[1]的地址中。即regResults[1]=0xdeaddead。判斷regResults[0]和regResults[1]是否相等。
如果相等,說明讀寫成功。
五、PoC分析與測試
(1)PoC程式碼中,執行寫操作的關鍵程式碼如下:
寫入過程中,先將value[count]移動到CMDQ_DATA_REG_DEBUG數值暫存器中,然後將pa_address+offset地址移動到CMDQ_DATA_REG_DEBUG_DST地址暫存器中,最後將CMDQ_DATA_REG_DEBUG數值暫存器中的value寫入到CMDQ_DATA_REG_DEBUG_DST地址暫存器中儲存的pa_address+offset地址中,即*(pa_address+offset) = value[count]。
(2)PoC程式碼中,執行讀操作的關鍵程式碼如下:
讀取過程中,第一步先將pa_address+offset地址移動到CMDQ_DATA_REG_DEBUG_DST地址暫存器中,然後從CMDQ_DATA__REG_DEBUG_DST地址暫存器中儲存的地址pa_address+offset中讀取資料放到CMDQ_DATA_REG_DEBUG資料暫存器中,再將dma_address+offset地址移動到CMDQ_DATA_REG_DEBUG_DST地址暫存器中,最後將CMDQ_DATA_REG_DEBUG數值暫存器中儲存的資料寫入到CMDQ_DATA_REG_DEBUG_DST地址暫存器中儲存的dma_address+offset地址中,即*(dma_address + offset) = *(pa_address + offset)。
(3)在Reami6測試機中,執行PoC測試,成功將Linux修改成minix。
六、參考連結
1、https://github.com/MiCode/Xiaomi_Kernel_OpenSource/tree/cactus-p-oss/drivers/misc/mediatek/cmdq
2、https://github.com/quarkslab/CVE-2020-0069_poc/blob/master/jni/kernel_rw.c
3、https://blog.quarkslab.com/cve-2020-0069-autopsy-of-the-most-stable-mediatek-rootkit.html
4、https://forum.xda-developers.com/android/development/amazing-temp-root-mediatek-armv8-t3922213
5、https://source.android.com/security/bulletin/2020-03-01