攻擊TrustZone系列(Pt.2) -- 逆向高通TrustZone

在這篇博客中,我們將逆向高通TrustZone的實現(SnapDragon SoC上)。

從哪裡開始?

首先,因為高通TrustZone的實現是閉源的,所以據我所知,市面上沒有任何關於它的架構或是設計的文檔。所以,我們大概得要逆向它的二進位碼來得到TrustZone的代碼,並且分析它。

取得TrustZone的鏡像文件

我們有兩種地方來取得鏡像文件;要麼從設備本身,要麼從設備的原廠鏡像。

我的Nexus 5已經有了root許可權,所以從設備中得到鏡像十分直接。因為鏡像存儲在eMMC晶元中,而eMMC晶元的分區和板塊可以從「/dev/block/platform/msm_sdcc.1 」下取得,我只需要簡單的將相關分區複製到我的電腦里「用dd」。

並且,這些分區的名字都有自己的意義:

就像在圖中,有兩個分區,一個叫做「tz」(TrustZone的簡寫),一個叫做「tzb」(TrustZone backup的簡寫)。他們兩個的內容是一樣的。

然而,通過這個方式取得鏡像文件,我仍然覺得不滿意,兩個原因:

——雖然TrustZone鏡像文件存儲在eMMC晶元中,但高通很簡單就可以讓正常世界沒法得到這個鏡像(要求系統主線上的AxPROT比特位必須被置位),或者這個鏡像可能缺少某些部分。

——從整個分區里提取數據並不能泄露實際(邏輯上)鏡像的界限,所以我們還要做些額外的工作才能確定tz鏡像究竟在何處完結(在這裡,因為tz鏡像是一個elf文件,所以我們可以通過elf頭文件簡單的解決這個問題,但這只是我們運氣好罷了)

所以,用完第一種方法,我們再來看看第二種方法,原廠鏡像。

Nexus 5的原廠鏡像可以從Google中下載。這個原廠鏡像包涵一個zip壓縮文件,這個zip里有全部默認的鏡像,並且還有BootLoader鏡像。

在下載了原廠鏡像後,我們用grep來找和TrustZone有關的字元串,很快我們就能發現在BootLoader鏡像中有我們想要的代碼。

然而,還有一個問題。BootLoader鏡像是一個未知格式(也許google工作人員知道這是啥)。即使這樣,用十六進位編輯器打開這個鏡像文件就能猜到大部分結構,其實這個格式很簡單:

這個BootLoader鏡像文件有以下結構:

——特徵值(「BOOTLDR!」) - 8位元組

——鏡像個數 - 4位元組

——從文件開始到鏡像開始的偏移 - 4位元組

——鏡像中數據的總大小 - 4位元組

——一個數列,這個數列的大小和「鏡像個數」中的值相同,這個數列中每個向量有兩個參數:

——鏡像名字 - 64位元組(用0來填充)

——鏡像長度 - 4位元組

你可以看到上圖中叫tz的鏡像就是我們要找的鏡像。為了解開這個文件,我寫了一個小的python腳本(在這兒),這個腳本會打開BootLoader鏡像然後弄出裡面的鏡像。

將鏡像弄出來以後,我們將它與我們用第一個方法得到的鏡像相比較,結果是一樣的。所以我覺得這意味我們可以接著分析TrustZone鏡像咯。

修補TrustZone鏡像

首先,我們發現這些鏡像文件其實都是ELF文件,好消息呀!這意味著它們的內存分段和映射地址我們都應該能知道。

我們用IDA Pro打開這個文件,並且讓IDA的自動分析跑了一會兒,我想開始逆向這個文件。然而,令人意外的是,好像有一大堆分支都指向沒有映射的地址(甚至於,「tz」二進位文件里不包含的地址)。

我又看了一會兒,似乎所有指向沒有映射的地址的分支都在第一個代碼分段里,而且他們都指向沒有映射的高地址。並且,沒有任何絕對分支指向第一段代碼分段。

這看上去有些可疑。。。所以我們來看看這個ELF文件的文件結構吧?使用readelf可以看到以下信息:

可以發現,有一個NULL類分段映射到高地址,這個分段相對應的映射地址就是那些無效絕對分支指向的地方!

所以,我做了一個安全的猜測,第一個代碼分段其實映射到了錯誤的地址,它應該被映射到更高的地址 -- 0xfe840000. 所以自然的,我想要重定位這個分段的基地址,然而當我使用IDA的重定位基地址時,IDA崩潰了:

我實際上不確定這是高通故意來防止逆向工程的方法,還是說Null分段只是他們內部build過程的結果,但是這可以通過手動修復ELF文件來解決。我們需要做的只是將Null分段移到一個未使用過的地址(反正他也被IDA忽略了),然後再移第一個代碼分段,從0x0fc86000到0xfe840000,像以下這樣:

現在,再次把鏡像文件用IDA讀取,所有的絕對分支都有效了!這意味著我們可以接著分析鏡像咯。

分析TrustZone鏡像

首先,值得注意的是TrustZone鏡像文件是一個相對比較大的二進位文件(285.5KB),其中的字元串相對較少,並且沒有公開的文檔。其次,TrustZone系統由一個完整的內核組成,這個內核具有例如執行應用程序的能力等等。所以。。。我們應該從哪裡開始逆向並不明確,因為如果逆向整個二進位文件會花太多時間。

因為我們想要做的的是從應用處理器攻擊TrustZone內核,所以最大的攻擊面應該是那些是的正常世界可以與安全世界交互的SMC。

值得注意的是,當然,除了SMC以外,我們還有其他與TrustZone交互的方式,比如說共享內存或是中斷處理,但因為這些交互方式提供的攻擊面更窄,所以從分析SMC調用開始應該是個好的選擇。

所以說,我們如何在TrustZone內核中找到處理SMC調用的部分?首先,讓我們先回想一下當執行SMC調用時,類似於處理正常世界的系統調用(SVC調用),安全世界必須先註冊當遇到這個指令時處理器將要跳轉到的向量地址(向量就是指一小段代碼,類似於x86的中斷向量)。

安全世界的對應是MVBAR(監督向量基地址寄存器),它將提供向量地址,這個向量地址包涵安全世界處理器對不同世界的處理函數。

使用MRC/MCR操作碼就可以控制MVBAR,比如以下操作碼:

這意味著我們可以簡單的通過在TrustZone鏡像里搜索MCR操作碼,來找到監督向量。事實上,在IDA里搜索操作碼會得到以下結果:

如圖,start符號的地址(順便說一句,這是唯一的exported符號)被載入到了MVBAR里。

根據ARM文檔,這個監督向量有以下結構:

這意味著如果我們看向之前提到的start符號,我們可以將下面的名字分配給那個表:

現在,我們就能夠分析上圖中的SMC_VECTOR_HANDLER函數(SMC向量處理函數)。事實上,這個函數負責了相當多的任務;首先,它保存所有的狀態寄存器和返回地址到預先定義好的地址(在安全世界裡),然後,它將棧切換到預先定義好的區域(也是安全世界裡)。最後,在進行了必要的準備後,它接著分型用戶要求的操作並且按照要求進行操作。

因為執行SMC的代碼被包含在Linux內核的高通MSM分支,我們可以看看從正常世界向安全世界發送的操作要求的格式。

SMC與SCM

高通選擇將安全世界與正常世界通過SMC操作碼交互的通道叫做SCM(安全通道管理者)。

就像我之前博客里說的,Linux內核里的「qseecom」驅動被用於與安全世界通信,其中使用的方式就是SCM。

高通在Linux內核里提供的文檔延展性很好,而且足夠讓我們知道SCM命令的格式。

簡單的來說,SCM命令有兩類:

正常SCM調用:這些調用將在需要從正常世界向安全世界傳送信息時使用,它們對於處理SCM調用是必須的。內核將傳播一下結構體:

然後TrustZone內核在處理完SMC條用後,將使用「scm_response」結構體來進行回復:

為了要分配和填充這些結構體,內核也許會調用包裹函數「scm_call」,這將得到指向內核空間里想要傳送的數據的位置的指針,以及數據應該返回到的地址。最重要的是服務標識和命令標識。

每一個SCM調用都有一個類別,代表著哪一個TrustZone內核的子系統應該負責處理這個調用。這將由服務標識確定。命令標識確定的是,給定一個服務,哪一個命令被要求執行。

在「scm_call」函數分配和傳播「smc_command」和「smc_response」緩存之後,它將調用內部「__scm_call」函數,這個函數會洗掉所有的cache(內部和外部cache),然後調用「smc」函數。

最後一個函數將實際上執行SMC操作碼,將控制權交給到TrustZone內核手上,如下圖:

值得注意的是R0被設為了1,R1被設為了指向本地內核棧地址,這個調用里用作「context ID」表示。R2被社為了指向分配好的「scm_command」結構體的物理地址。

這個R0里的特徵值代表這是一個正常SCM調用,使用「smc_command」結構。雖然如此,對於有些不需要那麼多數據的命令,不需要浪費這麼多資源來分配這些個數據結構。為了解決這個問題,另一類SCM調用被介紹進來。

原子SCM調用:對於那些參數只有四個以下的命令,還有一種方式來進行SCM調用。

有四個包裹函數「scm_call_atomic_[1-4]」對應著傳遞的參數的個數。這些函數可以被用於使用指定服務標識命令標識以及參數來直接進行SMC調用。

以下是「scm_call_atomic1」函數:

其中SCM_ATOMIC的定義如下:

值得注意的是服務標識和命令標識被編碼進了R0,傳遞的參數個數也在其中(這個函數里是1)。這取代了之前正常SMC調用里值為1的特徵碼。

這個R0里不同的值將告訴TrustZone內核接下來的SCM調用是一個原子調用,這意味著參數將由R2-R5來傳遞(而不是使用一個R2指向的結構體)。

分析SCM調用

既然我們理解了SCM調用是怎麼工作的,並且我們已經發現了TrustZone內核里處理這些SCM函數的函數,我們可以開始反彙編SCM調用來嘗試著找到任意一個SCM的漏洞。

我會略過大部分SMC處理函數的分析,因為這裡面大部分都是標準的用戶輸入處理之類的。雖然如此,在切換到TrustZone的棧並且保存了原來世界調用時的寄存器後,處理函數繼續處理服務標識和命令標識來看看哪個內部處理函數被調用了。

為了簡單的完成服務標識和命令標識與處理函數的映射,一個靜態的列表編譯進了TrustZone鏡像的數據分段,並且被SCM處理函數所引用。以下是列表的一部分:

如圖,這個列表有以下結構:

——指向包含SCM函數名稱的字元串的指針

——調用類型

——指向處理函數的指針

——參數個數

——每個參數的大小(DWORD)

——

服務標識和命令標識,被連接進了同一個DWORD里 - 比如說,「tz_blow_sw_fuse」函數,有0x2002,這表示它的服務標識是0x20,命令標識是0x02.

現在所要做的只剩下反彙編這些函數,並且找到一個可以攻擊的漏洞。

漏洞!

所以在看完之前提到的SMC調用(69個)後,我終於找到了以下函數:

通常,當一個SCM命令使用正常SCM調用機制被調用,R0會含有結果地址,這個地址指向Linux內核分配的「scm_response」緩存,但同時也會被TrustZone內核驗證是否這的確是一個指向允許範圍內的物理地址

- 即是說,是一個指向Linux內核的內存的物理地址,而不是一個指向TrustZone內存的物理地址。

這個驗證是通過一個內部函數來驗證的,我會在下一篇博客中細聊。

但如果我們使用原子SMC調用來執行函數時會怎樣呢?這個情況下,被使用的結果地址將是原子SMC調用的第一個參數。

現在 - 你可以看到上圖函數的問題了吧?

相對於其他的SMC處理函數,這個處理函數無法驗證R0里的值,即結果地址,所以讓我們傳入:

——一個非零數到R1(為了通過第一個分支)

——一個非零書到第四個參數(將會被傳入上圖的var_1C)

——物理地址到R0,包含一個在TrustZone的地址空間的物理地址

這個函數將到達上圖最左邊的分支,並且寫一個0的DWORD到R0里包涵的地址。

責任揭秘

我想說我已經在2014年通過責任揭秘告知了高通這個漏洞,並且這個問題已經被打了補丁。我在下一個博客中將分享具體的時間歷程和解釋,但我想表揚一下高通工作人員,他們回復很快,並且與他們的合作很愉快。

下一步?

在下一個博客中我將會分享詳細(並且複雜!)的基於剛剛那個漏洞的攻擊。它將使我們能夠在TrustZone內核中運行任意代碼。我將分享完整的攻擊碼,保持關注哦!

並且,因為這是我的第二篇博客了,我想有些回復,比如說:

——我應該關注更多(或更少)哪些內容?

——博客設計的問題

——研究想法:)

本文由看雪論壇 wx_rd.cheung 編譯,轉載請註明來自看雪社區


推薦閱讀:

堆溢出研究二
旅行的青蛙Unity遊戲逆向修改Android&iOS

TAG:軟體逆向工程 | 開源鏡像站 | 高通Qualcomm |