我的工作領域和視頻相關,保護視頻內容非常重要,主流的瀏覽器和移動設備都支持DRM。近日我偶然發現Google Chrome瀏覽器的CDM(Content Decryption Module-內容解密模塊)框架存在重大的設計缺陷,通過一些手段就可以輕松繞過DRM保護機制,非常容易的獲取解密后的數據,從而把視頻重新封裝為未壓縮的MP4等格式文件,還可以做到在觀看的過程中直接進行無加密的視頻直播。
發現問題后,我花了一點時間,寫了一個測試程序,驗證了我的想法。由于DRM的保護機制非常的重要,如果可以輕松被攻破,將對整個視頻領域是一個威脅,所以我當時就向Google Chromium提交了bug,說明了CDM的框架的重大缺陷,并描述了實現細節。
這是的issue url(一般人沒有權限查看,僅內部可見):
https://bugs.chromium.org/p/chromium/issues/detail?id=721639
Google有漏洞獎勵規則,這是規則描述地址:
https://www.google.com/about/app ... -rewards/index.html
其實我當時的想法是,如果能獲取來自Google的獎勵,不論獎金多少,都將是一個至高無上的榮譽。
我提交后,Chromium團隊很快的做出了回復,他們確認這是一個重大的安全問題,而且影響所有運行Chrome瀏覽器的操作系統,包括: Linux, Windows, Chrome, Mac等等. 但另一個員工說這是一個已知的問題,并提供了一個issue號: 658022,但我無限查看漏洞內容是否與我提交的一致。之后我向google 團隊的幾個成員發了郵件,說既然是已知的問題,那也就是不符合獎勵規則,因此我也就可以公布細節,讓視頻內容公司重視這個問題,以便盡早的商討更加安全的解決方案。
說了這么多,只想說明一下事件的背景,下面我就說明實現細節,很多東西可能有些專業,主要講述一個過程。
1.安裝Google Chrome 32bit版本(32版本容易使用工具進行調試)
2.Chrome內置的CDM是Widevine(幾年前收購來的),目錄在Google\Chrome\Application\58.0.3029.110\WidevineCdm,在子目錄_platform_specific\win_x86有下2個dll:
widevinecdm.dll - widevine核心模塊,導出的函數有:InitializeCdmModule_4,DeinitializeCdmModule,CreateCdmInstance,GetCdmVersion,GetHandleVerifier
widevinecdmadapter.dll - PPAPI插件標準適配庫,導出函數有:PPP_GetInterface,PPP_InitializeModule,PPP_ShutdownModule
3.正常情況下播放DRM視頻的流程:
Chrome -> Widevine CDM Adapter(widevinecdmadapter.dll) -> Widevine CDM Module (widevinecdm.dll)
widevinecdmadapter.dll調用widevinecdm.dll的導出函數CreateCdmInstance來創建CDM實例.
4.CDM框架是Google Chrome的標準,所以API參數和接口都有C++ include文件,比如API CreateCdmInstance:
CDM_API void* CreateCdmInstance(int cdm_interface_version, const char* key_system, uint32_t key_system_size, GetCdmHostFunc get_cdm_host_func, void* user_data);
CDM實例要繼承于class ContentDecryptionModule_8,查看class ContentDecryptionModule_8,發現了一個非常重要的函數:Decrypt,主要是將加密的數據傳入,解密后的數據傳出,我的天吶,只要截獲了這個函數不就搞定了嗎!!!
[C++] 純文本查看 復制代碼
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class CDM_CLASS_API ContentDecryptionModule_8 { // Decrypts the |encrypted_buffer|.
//
// Returns kSuccess if decryption succeeded, in which case the callee
// should have filled the |decrypted_buffer| and passed the ownership of
// |data| in |decrypted_buffer| to the caller.
// Returns kNoKey if the CDM did not have the necessary decryption key
// to decrypt.
// Returns kDecryptError if any other error happened.
// If the return value is not kSuccess, |decrypted_buffer| should be ignored
// by the caller.
virtual Status Decrypt(const InputBuffer& encrypted_buffer,
DecryptedBlock* decrypted_buffer) = 0;
};
|
5.了解了API參數和class定義后,就設想如果在widevinecdmadapter.dll 和 widevinecdm.dll之間在互相調用時截獲到解密后的數據就可以繞過DRM保護機制.
6.開始寫一個DLL, 名為CdmProxy.dll, 我自己的CreateCdmInstance函數,這部分不解釋了:
[C++] 純文本查看 復制代碼
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
extern "C" __declspec(dllexport) void * CDMAPI_DEFINE my_CreateCdmInstance(int cdm_interface_version, const char* key_system,
uint32_t key_system_size, GetCdmHostFunc get_cdm_host_func, void* user_data)
{
gHostUserData = user_data;
wsprintf(wchLog, L"CdmProxy - call CreateCdmInstance(%d, %S, %d, 0x%08X, 0x%08X)",
cdm_interface_version, key_system, key_system_size, get_cdm_host_func, user_data);
OutputDebugStringW(wchLog);
void *p = pCreateCdmInstance(cdm_interface_version, key_system, key_system_size, get_cdm_host_func, user_data);
cdm::ContentDecryptionModule_8 *pCdmModule = (cdm::ContentDecryptionModule_8 *)(p);
MyContentDecryptionModuleProxy *pMyCdmModule = new MyContentDecryptionModuleProxy(pCdmModule);
return pMyCdmModule;
}
|
我的代{過}{濾}理class MyContentDecryptionModuleProxy,代碼不解釋:
// class MyContentDecryptionModuleProxy
[C++] 純文本查看 復制代碼
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
class MyContentDecryptionModuleProxy : public cdm::ContentDecryptionModule_8
{
public:
MyContentDecryptionModuleProxy(cdm::ContentDecryptionModule_8 *pCdm)
{
mCdm = pCdm;
}
private:
cdm::ContentDecryptionModule_8 *mCdm;
public:
// 最重要的解密函數,保存原數據和解密后的數據
virtual cdm::Status Decrypt(const cdm::InputBuffer& encrypted_buffer, cdm::DecryptedBlock* decrypted_buffer)
{
cdm::Status status = cdm::kSuccess;
codelive();
DebugDecryptBreak(encrypted_buffer.iv, encrypted_buffer.key_id, encrypted_buffer.data);
status = mCdm->Decrypt(encrypted_buffer, decrypted_buffer);
string strIV = data2HexString((const char *)encrypted_buffer.iv, encrypted_buffer.iv_size);
string strEncData = data2HexString((const char *)encrypted_buffer.data, min(encrypted_buffer.data_size, 32));
string strDecData = data2HexString((const char *)decrypted_buffer->DecryptedBuffer()->Data(),
min(decrypted_buffer->DecryptedBuffer()->Size(), 32));
wsprintf(wchLog, L"CdmProxy - call Decrypt(IV:%S, encData(%d):%S, decData(%d):%S)",
strIV.c_str(), encrypted_buffer.data_size, strEncData.c_str(),
decrypted_buffer->DecryptedBuffer()->Size(), strDecData.c_str());
OutputDebugStringW(wchLog);
if(mEncFile == NULL)
{
mEncFile = fopen("d:\\cdm_enc.bin", "wb");
}
if(mEncFile != NULL)
{
fwrite(encrypted_buffer.data, 1, encrypted_buffer.data_size, mEncFile);
}
if(mDecFile == NULL)
{
mDecFile = fopen("d:\\cdm_dec.bin", "wb");
}
if(mDecFile != NULL)
{
fwrite(decrypted_buffer->DecryptedBuffer()->Data(), 1, decrypted_buffer->DecryptedBuffer()->Size(), mDecFile);
}
return status;
}
};
|
7.核心代碼寫完了,就要解決DLL加載的問題,嘗試了幾種簡單的方式,分別把widevinecdm.dll和widevinecdmadapter.dll改名為widevinecdm_org.dll和widevinecdmadapter_org.dll,然后自己寫一個DLL,導出和widevinecdm.dll或者widevinecdmadapter.dll相同的API,然后再調用原來DLL的方式,但這種方式發現不可行,原因在于Chrome的安全沙盒,所有的插件都是加載的沙盒進程空間,敏感的API都無法使用,比如:ReadProcessMemory,CeateFile,OutputDebugString等等. 逃脫沙盒(Sandbox Escape)是Google獎金數額非常高的,最高可達$15,000.
8.既然進程都是Chrome創建的,所以就直接對chrome.exe下手,直接patch chrome.exe,讓其加載我的CdmProxy.dll,結果成功了,畢竟chrome啟動時還未啟用沙盒機制,所以沒花費太多技術就解決了DLL加載問題.
9.改變API截獲方式,對widevinecdmadapter.dll進行動態的補丁,將調用API CreateCdmInstance的地址改為我自己的API my_CreateCdmInstance,然后在DLL加載時進行以下處理:
[C++] 純文本查看 復制代碼
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch(ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
hWideVineCdm = LoadLibraryW(L"{PATH}\\widevinecdm.dll");
pInitializeCdmModule_4 = (InitializeCdmModule_4Func)GetProcAddress(hWideVineCdm, "InitializeCdmModule_4");
pDeinitializeCdmModule = (DeinitializeCdmModuleFunc)GetProcAddress(hWideVineCdm, "DeinitializeCdmModule");
pCreateCdmInstance = (CreateCdmInstanceFunc)GetProcAddress(hWideVineCdm, "CreateCdmInstance");
pGetCdmVersion = (GetCdmVersionFunc)GetProcAddress(hWideVineCdm, "GetCdmVersion");
hWideVineCdmAdapter = LoadLibraryW(L"{PATH}\\widevinecdmadapter.dll");
if(hWideVineCdmAdapter != NULL)
{
DWORD dwSrcAddr = (DWORD)hWideVineCdmAdapter + 0x0000446D;
const BYTE chVerify[] = { 0xFF, 0x15 };
BOOL isOK = patch_DsCallFunction(dwSrcAddr, (DWORD)my_CreateCdmInstance, chVerify, sizeof(chVerify));
wsprintf(wchLog, L"CdmProxy - patch CreateCdmInstance, Address:0x%08X-0x%08X, %s.",
dwSrcAddr, (uint32_t)my_CreateCdmInstance,
isOK ? L"OK" : L"FAILED");
OutputDebugStringW(wchLog);
}
}
break ;
}
}
|
10.然后進行測試,播放一個有DRM保護的DASH視頻:
https://shaka-player-demo.appspo ... gleKey/Manifest.mpd
發現文件沒保存下來,LOG也沒輸出,想必是安全沙盒起作用了。
11.又要逃脫沙盒,經過研究,發現根本不需要,只要在chrome啟動參數增加 --no-sandbox 即可。我的天吶,為啥要提供這樣一個后門啊!!!
12.再次播放加密的視頻,文件順利保存下來,LOG也輸出成功,經驗證,解密后的數據與之前未加密的數據是一致的。
13.Google Chrome的CDM就這樣被破解了,非常的簡單的就繞過了Widevine DRM的算法,這應該是Chrome CDM的框架設計的嚴重問題,估計要改變也不是非常容易的。
這是LOG數據:
( "CdmProxy - call CreateCdmInstance(8, com.widevine.alpha, 18, 0x70F86310, 0x00D75A88)" ) 0.0001399
( "CdmProxy - call CreateCdmInstance(8, com.widevine.alpha, 18, 0x70F86310, 0x00D757C8)" ) 0.0000937
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818F00000000000000000, encData(348):FFF158402B9FFC2FF05300F2BF83E9A0, decData(0):)" ) 0.0001350
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818F00000000000000000, encData(348):FFF158402B9FFC2FF05300F2BF83E9A0, decData(348):FFF158402B9FFC00D03403E95B8639BD)" ) 0.0001335
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818F00000000000000000, encData(348):FFF158402B9FFC2FF05300F2BF83E9A0, decData(348):FFF158402B9FFC00D03403E95B8639BD)" ) 0.0001032
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818F10000000000000000, encData(348):FFF158402B9FFC487380B8930FFFAB41, decData(348):FFF158402B9FFC00F43420C24620902C)" ) 0.0001392
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818F20000000000000000, encData(349):FFF158402BBFFC1175E15FE4B6154B30, decData(349):FFF158402BBFFC00FA342D90762A3188)" ) 0.0001032
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818F30000000000000000, encData(348):FFF158402B9FFCC45D5715E87235E5CF, decData(348):FFF158402B9FFC00F8342CEC825A2D85)" ) 0.0000994
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818F40000000000000000, encData(348):FFF158402B9FFC6749FBAF64926471DE, decData(348):FFF158402B9FFC00F83421884529290A)" ) 0.0000880
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818F50000000000000000, encData(349):FFF158402BBFFCF8132EFC31C186DDE1, decData(349):FFF158402BBFFC00F2342D9049124988)" ) 0.0001088
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818F60000000000000000, encData(348):FFF158402B9FFC82EDA0BD4AB7158938, decData(348):FFF158402B9FFC00EE342D7475223D85)" ) 0.0001035
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818F70000000000000000, encData(348):FFF158402B9FFC4B2C585CC10F74036E, decData(348):FFF158402B9FFC00F4342D74662A2088)" ) 0.0001555
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818F80000000000000000, encData(349):FFF158402BBFFCCF33665AC4E219EC92, decData(349):FFF158402BBFFC00FA342E30547B0604)" ) 0.0001494
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818F90000000000000000, encData(348):FFF158402B9FFC2C9A7362594261CE23, decData(348):FFF158402B9FFC00F4342E305429150A)" ) 0.0004035
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818FA0000000000000000, encData(348):FFF158402B9FFC1905A086AE3CEF0AEC, decData(348):FFF158402B9FFC00EE342E3456391906)" ) 0.0005913
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818FB0000000000000000, encData(349):FFF158402BBFFC8D0EB865013262FB6E, decData(349):FFF158402BBFFC00F6342E34563A2186)" ) 0.0001479
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818FC0000000000000000, encData(348):FFF158402B9FFC484211C612F22283FB, decData(348):FFF158402B9FFC0102342E74482A2E02)" ) 0.0002507
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818FD0000000000000000, encData(348):FFF158402B9FFC283122B1DDE740DAC2, decData(348):FFF158402B9FFC0104342EB464190D08)" ) 0.0003011
( "CdmProxy - call Decrypt(IV:6CD1F4FBCE5818FE0000000000000000, encData(349):FFF158402BBFFCAC8759D48FF1A258A3, decData(349):FFF158402BBFFC010E342ED04A1A2982)" ) 0.0003095
DRM這么多年了,很多保護機制和算法都做的非常嚴密,但還是被Zhu一樣的隊友給出賣了。
公開這個研究,是為了讓廣大的視頻公司不要以為DRM非常的安全,有時候真的是不堪一擊,某個環節出現漏洞,同樣面臨極大的安全問題。
如果此篇文章不適合公布,請通知我,謝謝。
|