我的工作領域和視頻相關,保護視頻內容非常重要,主流的瀏覽器和移動設備都支持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非常的安全,有時候真的是不堪一擊,某個環節出現漏洞,同樣面臨極大的安全問題。
如果此篇文章不適合公布,請通知我,謝謝。
|