當前位置:
首頁 > 最新 > ARM劉永康:淺談Android數字版權管理之視頻保護

ARM劉永康:淺談Android數字版權管理之視頻保護

前言

本文簡略地介紹了如何在Android下實現DRM(Digital Rights Management, 數字版權管理)以及與其適配的Secure Video Path的要點。希望本文能夠省去大家一些閱讀代碼和文檔的時間,幫助接觸Android DRM框架不久的朋友。本人在這次Secure Video Path相關的工作之前並沒有太多的Android經驗;文章中的名詞和概念等都是個人翻譯而來,有不對的地方請指出。

背景

DRM(Digital Rights Management)是一個成熟的操作系統中必須實現的功能。DRM提供的功能正如其字面的意思,可以幫助保護數字版權;目前最直接的一個應用就是對在線播放的媒體流進行保護。在Android下DRM相關的代碼被放置在了多媒體的架構當中。

安卓的DRM架構目前常見的實現有兩種。

經典的Android DRM Framework(https://source.android.com/devices/drm)架構;

現在用的比較多的mediaDRM (https://developer.android.com/reference/android/media/MediaDrm.html)實現。

DRM Framework架構圖,於2018 Apr 18下載自:https://source.android.com/devices/drm

MediaDrm流程及其工作流程圖,於2018 Apr 18下載自:https://developer.android.com/reference/android/media/MediaDrm.html

這兩者的區別是DRM Framework考慮的是通用DRM實現;舉例來說,當播放一個媒體源的時候,會有一些初始的與伺服器交互得到的數據被DRM Manager所解析,來判斷是否含有DRM信息;如果包含相關信息,則對應已註冊的DRM Plugin會被選中用來處理DRM流程;並且在流程完畢以後負責媒體流的解密。

而mediaDRM則在簡化了流程。mediaDRM的API設計主要是為了對接ISO/IEC 23001-7: Common Encryption(縮寫CENC)標準;CENC定義了如何獲得一個媒體流解密所需要的密鑰的流程和數據格式。這個標準相對簡潔,不過這個標準是收費的,筆者也沒有能閱讀詳細的內容,只能從代碼上略知一二。舉例說明,當播放一個媒體流的時候,這個媒體流事先就定義好是哪種符合CENC標準的DRM場景(前面的DRM Framework中有一個嗅探的過程);對於此種DRM場景,Media Framework會直接去查找相應的mediaDRM插件來處理與伺服器的交互,並且流程和信息都遵照CENC標準(DRM Framework中考慮的是通用實現,比如一種全新的DRM場景);最後得到密鑰,來進行媒體流解密。

mediaDRM對於Player應用來說使用起來相對簡單。很多常見的DRM實現基本使用這種方法。比如Widevine; Playready等。而且谷歌的開源播放器Exoplayer可以直接用來測試mediaDRM實現。

Android下實現了一個簡單的開源mediaDRM 插件: ClearKey;讀者可以通過研究這個插件而對mediaDRM的介面有所了解。ClearKey的路徑在:frameworks/av/drm/mediadrm/plugins/clearkey/

由於需要比較好的實現DRM功能;並且現在的操作系統大多為開放式操作系統,被破解或者root的概率是相當的高;所以DRM對設備上從解密到播放的這一條通路都做了要求;要求媒體流數據從解密,被解碼到顯示的過程中一概不能被泄露;WidevineL1之類對此都有嚴格的要求。這種從解密到顯示的通路稱為Video Path;而保證安全的通路則稱為Secure Video Path。

實現過程

對於通用的mediaDRM架構,比如上文提到的ClearKey;或者商用的DRM場景比如Widevine或者Playready;DRM交互協議部分基本已經實現,留下的與設備的密鑰相關的操作一般需要被放置在一個安全的環境里進行。OEM一般需要閱讀DRM場景的文檔,配合DRM場景的要求實現OEM必須要實現的模塊。實現這些模塊是為了達到以下兩個目的:

1. 將安全系統與DRM框架對接,以實現DRM框架所必須的安全功能;比如保護設備私鑰等。常見的做法有使用硬體安全環境;或者運行在可信執行環境(TEE)的安全操作系統(Secure OS)。

保護密鑰是最基本最重要的DRM要求。Widevine L2就是要求保護密鑰;L1則是保護密鑰+Secure Video Path;而L3基本只是為了測試Widevine協議而存在,既不保護密鑰也不保護Video Path;

密鑰的產生和維護過程,又是另一個安全相關的主題;在這篇文章里不做贅述。

2.實現一個安全通路使得從解密開始直到被顯示都是安全的。

為了達到這兩個目的,以下組件需要進行必要的增加或者修改。

安全內存

要點:

實現安全內存分配器(比如ION Heap)

實現安全內存所需的配套設施(Secure Boot, TEE, Bootloader)

為了保存解密後的媒體流,為解碼和顯示做好準備,安全內存必須被提供。安全內存有許多實現方式。使用防火牆或者內存保護單元(MPU – Memory Protection Unit)是比較常見的方法。而對這些安全內存進行分配和使用的操作,Android提供了ION這個組件。

ION是一個安卓下統一的堆(Heap)管理介面。使用ION可以靈活的實現一些特定的內存管理器;正適合作為管理安全內存的介面。ION的實現基於DmaBuf;後者是一套內核API,可以實現在進程間的Dma內存共享;ION在內核API的基礎上提供了介面供應用程序調用(/dev/ion);使得用戶程序也能夠分配在進程間共享的Dma內存。

最簡單的安全內存實現則是在內存中預留一塊區域為安全內存;使用MPU對此地址範圍的內存進行保護,將不合格的存取請求拒絕。這一塊預留的內存可以使用ION Heap管理起來;讓用戶程序可以在這個Heap里分配和釋放內存;然而,僅僅是分配釋放;想Memory Map以後再進行存取,是不可以的(MPU會拒絕非安全存取)。

MPU的規則只能在安全模式下定義;一般可以放在更早的啟動組件里進行(Bootloader);如果具有動態內存許可權設置功能的MPU,對MPU規則的設置可以放在Secure OS里完成。為了保證系統的完整性,安全啟動(Secure Boot)必須被打開,驗證Bootloader和Secure OS的完整性;防止非法篡改。

Linux中預留內存有多種方法。使用顯式的內存預留是一種方法,參見dts代碼:

reserved-memory {

#address-cells = ;

#size-cells = ;

ranges;

/* reserve memory for secure heap */

carveout: carveout@60000000 {

compatible = "ion,heap_secure";

reg = ;

};

}

在上面的例子中,使用了carveout類型;carveout類型總體和安全內存的需求接近;但是Carveout Heap在分配的時候會負責清零;而非安全CPU訪問內存是被MPU禁止的。所以需要一些改動,去除這些直接訪問內存的地方。

經過以上一些列的設置,系統中的安全內存就被管理了起來。

目前常見的Android內核中,都為經典的ION介面API(alloc, free, map),這種方式有一個問題就是所有的Heap ID都是Hard Code。當用戶在ION中添加了一個新Heap,則一個新的Heap ID需要被添加到ion.h中;然後複製到Android的bionic內核頭文件的目錄中;再運行腳本,將這個更新的頭文件被複制到其他的lib頭文件中(比如libion)。這樣帶來一些問題,一是因為在ion.h中,經典的代碼把Heap Id和Heap Type給關聯了起來;實際上這二者是獨立的意義;二是Android使用repo管理很多的git倉庫;假如使用前面修改ion.h的方法,一個簡單的添加Heap Id的改動起碼會影響三個左右的git倉庫。所以在比較新的內核中ION添加了一個方法enumerate;使用這個方法可以得到當前所有的ION Heap的描述,根據描述得到目標Heap的ID,避免了頻繁修改ion.h的問題。條件允許的話,建議大家盡量更新到後面的版本。

安全解密系統

要點:

實現在安全環境里解密並且將結果放入安全內存的操作

嚴格檢查目標地址是否為安全地址

加密的媒體流是放在非安全內存里的。這部分的內容被解密以後結果會被放置到一個安全的環境里;同時這個解密的過程,也需要在一個安全的環境里。這裡就涉及到安全解密系統。安全解密系統往往都是DRM實現的一部分。因為:

DRM流程中需要用到與設備有關的密鑰來進行加解密行為。

解密媒體流所用的密鑰最後也是在安全環境里被算出,並且解密過程需要在安全環境中進行。

目前通用的做法是將安全解密系統實現在安全操作系統中(Secure OS);在支持Arm Trustzone的晶元架構下,Secure OS可以訪問系統的所有資源;在Secure OS中對加密的媒體流進行解密是比較適合的。另外還有其他類似的解決方案,比如硬體的安全加解密環境等。

安全解密系統的職責就是解密,並且把數據放在安全內存中。這裡比較重要的地方是,由於解密系統實際上是第一道檢查安全內存的關卡,它有一個重要的責任就是,確認解密的目的地,必須是安全的。它需要檢查目的地的範圍和屬性。

有一點需要說明的是,在Android中,解密系統是第一個處理媒體流的模塊;但是它所使用的安全內存,是由視頻解碼器調用安全內存的介面(ION Heap)來分配的。

視頻解碼器

要點:

修改Codec組件函數enumerateComponents宣告支持Secure Codec類型

修改Codec組件函數makeComponentInstance支持創建Secure Codec實例

修改media_codecs.xml使得secure codec能夠被Player枚舉

修改內存分配函數,使得為Secure Codec實例分配安全內存成為可能

視頻解碼器需要支持安全解碼;安全解碼器能夠存取安全內存。另一個重要的特點是,安全解碼器,不能夠存取普通內存。這是一個重要的原則,否則安全解碼器就有可能將媒體流泄露到非安全內存中。

在Android播放器一般的初始化流程中,初始化mediaCodec的時候,會為這個mediaCodec對象設置一個輸出Surface:

codec.setOutputSurface(surface);

在上面一小節的介紹中,安全解密系統已經將解密後的媒體流放在了安全內存中等待解碼。這個安全內存是由Codec組件分配,並且在調用解密函數的時候,傳給安全解密系統的。這個存放待解碼的媒體流的Buffer稱為Input Buffer;在這裡,由於需要使用安全內存,這裡的Input Buffer是分配至安全內存的(通過調用ION介面);解碼完成後放置幀數據的內存則來自Surface.

Android下為Secure Video Path所預留的設計是:當一個安全解碼器被需要並且成功載入的時候,Android會激活整個Secure Video Path所需要的flag。安全解碼器是否被需要,一般在mediaDrm Plugin的代碼里會指定:

class CryptoPlugin : public android::CryptoPlugin{

...

virtual bool requiresSecureDecoderComponent(const char* mime) const {

/* TODO: check mime type */

return true;

}

...

}

如果DRM插件返回true的話,Player的一個職責就是需要初始化必要的安全解碼器。安全解碼器的名稱,則是在普通的解碼器名稱後加上了一個後綴」.secure」。系統中所支持的解碼器,都列在了media_codecs.xml中。下面的例子展示了如何添加一個安全解碼器:

其次在Codec的enumerateComponents中,需要在Media Framework中註冊自己所支持的Codec類型。除了通常的decoder和encoder,decoder.secure是需要添加支持的。

Player在根據所需要的解碼器的mimeType,找到可用的Secure Codec以後,會去進行初始化。在初始化函數makeComponentInstance中,需要能夠分配Secure Codec實例。一般來說,這個函數可以和普通的Codec的makeComponentInstance復用;只是發現Codec名稱為」.secure」結尾的時候,在Codec Component內部的數據結構中置上一個Secure標誌;以便後面分配內存的時候,能夠知道當前的Codec Component是不是安全解碼器:

解碼器組件在初始化實例的時候,需要提供實例所支持的介面給Media Framework,這裡使用SoftOMXComponent的代碼作為例子;在硬體解碼器的代碼里也有類似的代碼:

各種必要的函數需要被提供。這裡需要關注的就是AllocateBuffer函數。這個函數在一些情況之下會被調用用來分配Buffer。

Codec Component初始化完成的時候的時候,Media Framework就會發現Player剛初始化了安全解碼器,於是它就會將Secure Video Path上所要用到的組件置上相應的Flag:

在這裡幾個標誌的作用:

kFlagIsSecure標誌決定了Input Buffer需要來自安全內存。由於Media Framework並不知道安全內存的具體實現;在遇到需要分配安全內存的情況下,Framework則會去調用Codec Component提供的AllocateBuffer函數。

所有的Surface內存都是由Gralloc來進行分配。kFlagIsGrallocUsageProtected標誌決定了當使用Gralloc來分配Surface內存的時候,Gralloc需要支持從安全內存分配器分配內存。使用安全內存的Surface一般稱呼為Protected Surface.

kFlagPushBlankBuffersToNativeWindowOnShutdown表示在Surface無效的時候,顯示空白的畫面;而不是之前尚存在於Surface中的數據。

最終在AllocateBufferWrapper中,Component通過檢查secure標誌來決定是否要從安全內存中分配一塊區域並返回:

安全內存被分配以後,其handle將被在安全解密系統(DRM進程)和多媒體(Media進程)之間傳遞。安全解密系統通過ION的API可以獲得安全內存的地址,來進行解密操作。而Codec的驅動也可以獲得安全內存的地址,將其作為DMA地址來進行解碼。

圖形和顯示系統和Gralloc

要點:

實現支持安全複合的硬體顯示設備(HwComposor)

在Gralloc()分配安全內存給具有GRALLOC_USAGE_PROTECTED標誌的分配請求

如果不能實現安全的GPU,則將GPU隔離在Secure Video Path之外

解碼後用於顯示的Surface由SurfaceFlinger進程創建而來。在解碼器組件被實例化以後,所需要分配的Surface被放置上了保護flag:

這個保護flag最後在分配Surface所需要使用的內存的時候,會被傳遞到Gralloc模塊里。Gralloc模塊負責分配所有與顯示相關的內存。在Gralloc模塊的代碼里,會根據傳入的flag選擇適當的內存分配器。檢查到 GRALLOC_USAGE_PROTECTED標誌,在本文的例子中,則會去使用ION申請一塊安全內存。

硬體複合器負責對硬體的Layer進行複合,並且顯示最終結果;其組件名稱為HwCompsor;一般存在於系統分區(/vendor/lib/hw/hwcomposer.xxxx.so).GPU則是負責圖形繪製和渲染的引擎。使用硬體複合器可以減輕GPU負擔。

含有解碼後內容的Surface一般直接就會被複合後輸出。在以下情況下,GPU會操作這個Surface:

Player對輸出的Surface進行了特效或者貼圖等後期處理;

Surface所在的Layer (這裡為Protected Layer)的特性不符合硬體複合器的要求;複合操作被Reject,GPU將負責這個Layer的複合操作。

在Secure Video Path中硬體複合最好能夠被滿足;因為軟體複合意味著CPU將可以存取Protected Surface的內容。MPU也會拒絕CPU對保護內存的訪問。如果不能夠被滿足,那麼使用Secure state CPU來進行複合操作,則會導致整個多媒體框架實現的複雜度。

在Android的Surfaceflinger中,不會對Protected Layer進行複合操作;遇到Protected Layer就會顯示黑屏。這也是Surfaceflinger知道自己可能無法訪問安全內存而做出的一個保險的行為。

所以想要改動最少的實現Secure Video Path,則這點需要被滿足:

確保Protected Layer的特性不會被硬體複合器拒絕。可以使用dumpsys SurfaceFlinger查看原因;如果複合器在dump函數中記錄了Reject Reason的話。通常被拒絕的原因是顏色格式不支持;或者要做Downscale。Upscale一般沒限制。所以播放的媒體流的解析度,最好不要超過屏幕的解析度。

安全內存File Descriptor在進程間的傳遞

要點:

使用native_handle作為安全內存的Handle類型

除了Codec,DRM安全解密系統也需要在用戶端操作安全內存句柄。在Android 7.0 (Android N)開始,DRM Server (mediaDRM所在的進程)和Media Service不在一個進程里;Codec組件無論是自己調用ION介面分配的函數;還是調用一個管理安全內存的動態庫分配的函數,安全內存所對應的File Descriptor(以下簡稱FD)都只在被分配的進程里有效;同樣的FD數值被傳遞到另一個進程會導致得不到安全內存的信息而不能操作。

在Android中,Binder服務可以幫助傳遞FD去別的進程;它可以在目標進程里映射一個新的FD。在新建一個Parcel的時候,如果類型是BINDER_TYPE_FD,則Binder驅動會映射一個目標FD。

在Codec的內存分配函數AllocateBufferWrapper中,由於它可接受的句柄類型,並不接受FD,只有如下所示的三種類型;所以無法直接返回一個FD給AllocateBufferWrapper的調用者(Media Framework)。

其中Secure Codec所使用的安全內存句柄只能為後面兩種。其中,kSecureBufferTypeNativeHandle就是為FD的傳遞而包裹的一個類型。這個類型可以幫進程傳遞一個或者多個FD去另一個進程。當Media Framework檢測到安全內存類型為kSecureBufferTypeNativeHandle的時候,它會調用相應的處理函數來處理。分配內存的偽代碼請參考上方Pseudo AllocateBufferWrapper的代碼段部分。在DRM進程里請參考:system/core/include/cutils/native_handle.h裡面的函數;基本上只要取出native_handle_t裡面FD數組裡的成員,就是在當前進程里可以訪問的安全內存FD.

硬體所要具備的條件

安全內存的實現,離不開硬體。硬體需要做到以下幾點:

每個硬體需要有不同的ID來表示自己。

具有防火牆功能,能夠鑒別訪問內存的硬體ID,並且根據ID和防火牆規則來處理訪問許可權。

需要訪問普通內存和安全內存的硬體,需要有多種ID,適時切換ID。

能訪問安全內存的ID,不能夠去訪問普通內存;反之亦然。

硬體複合器這樣的硬體,不能對兩種內存有寫許可權。


問答

假如一個非安全的解碼器假裝是安全的解碼器,它是否能夠偷取信息?

只有真正安全的解碼器,才能夠訪問安全內存,這是由MPU所保證的。假如非安全的解碼器任意分配了一塊內存冒充安全的解碼器,安全解密器會檢查內存的屬性進而發現這種冒用;假如它真的分配了安全內存(安全內存誰都可以分配)但是最終只有HwComposor能夠讀取內容並且顯示;其他的非安全模塊均不能存取這塊內存。

為何大多使用靜態預留的方式實現安全內存?

因為預留的方式簡單;MPU僅僅使用範圍檢查就能知道內存的屬性;而動態分配安全內存的方法,經常需要修改內存的屬性,稍有疏漏就會留下安全漏洞。

後續

DRM本身的意義,越來越薄弱。因為版權保護意識的增強,防範越來越不重要。但是針對DRM保護的技術,繼續會產生巨大的用途,比如在隱私保護等領域。舉例,人臉識別演算法中的視頻和中間數據,是有相當的意義來保護它的。Secure Video Path的存在是相當有必要的。

參考資料

Android Hardware Composor:

https://source.android.com/devices/graphics/arch-sf-hwc

Arm TZMP1 Slides and Video:

https://es.slideshare.net/linaroorg/hkg18408-a-drm-solution-using-tzmp

http://connect.linaro.org/resource/hkg18/hkg18-408/

NXP: Secure Data Path work with i.MX8M

http://connect.linaro.org/resource/hkg18/hkg18-113/


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Linuxer 的精彩文章:

一瓶酒後的雄文:快速理解比特幣/區塊鏈的原理

TAG:Linuxer |