我的工作領(lǐng)域和視頻相關(guān),保護(hù)視頻內(nèi)容非常重要,主流的瀏覽器和移動(dòng)設(shè)備都支持DRM。近日我偶然發(fā)現(xiàn)Google Chrome瀏覽器的CDM(Content Decryption Module-內(nèi)容解密模塊)框架存在重大的設(shè)計(jì)缺陷,通過(guò)一些手段就可以輕松繞過(guò)DRM保護(hù)機(jī)制,非常容易的獲取解密后的數(shù)據(jù),從而把視頻重新封裝為未壓縮的MP4等格式文件,還可以做到在觀看的過(guò)程中直接進(jìn)行無(wú)加密的視頻直播。
發(fā)現(xiàn)問(wèn)題后,我花了一點(diǎn)時(shí)間,寫(xiě)了一個(gè)測(cè)試程序,驗(yàn)證了我的想法。由于DRM的保護(hù)機(jī)制非常的重要,如果可以輕松被攻破,將對(duì)整個(gè)視頻領(lǐng)域是一個(gè)威脅,所以我當(dāng)時(shí)就向Google Chromium提交了bug,說(shuō)明了CDM的框架的重大缺陷,并描述了實(shí)現(xiàn)細(xì)節(jié)。
這是的issue url(一般人沒(méi)有權(quán)限查看,僅內(nèi)部可見(jiàn)):
https://bugs.chromium.org/p/chromium/issues/detail?id=721639
Google有漏洞獎(jiǎng)勵(lì)規(guī)則,這是規(guī)則描述地址:
https://www.google.com/about/app ... -rewards/index.html
其實(shí)我當(dāng)時(shí)的想法是,如果能獲取來(lái)自Google的獎(jiǎng)勵(lì),不論獎(jiǎng)金多少,都將是一個(gè)至高無(wú)上的榮譽(yù)。
我提交后,Chromium團(tuán)隊(duì)很快的做出了回復(fù),他們確認(rèn)這是一個(gè)重大的安全問(wèn)題,而且影響所有運(yùn)行Chrome瀏覽器的操作系統(tǒng),包括: Linux, Windows, Chrome, Mac等等. 但另一個(gè)員工說(shuō)這是一個(gè)已知的問(wèn)題,并提供了一個(gè)issue號(hào): 658022,但我無(wú)限查看漏洞內(nèi)容是否與我提交的一致。之后我向google 團(tuán)隊(duì)的幾個(gè)成員發(fā)了郵件,說(shuō)既然是已知的問(wèn)題,那也就是不符合獎(jiǎng)勵(lì)規(guī)則,因此我也就可以公布細(xì)節(jié),讓視頻內(nèi)容公司重視這個(gè)問(wèn)題,以便盡早的商討更加安全的解決方案。
說(shuō)了這么多,只想說(shuō)明一下事件的背景,下面我就說(shuō)明實(shí)現(xiàn)細(xì)節(jié),很多東西可能有些專業(yè),主要講述一個(gè)過(guò)程。
1.安裝Google Chrome 32bit版本(32版本容易使用工具進(jìn)行調(diào)試)
2.Chrome內(nèi)置的CDM是Widevine(幾年前收購(gòu)來(lái)的),目錄在Google\Chrome\Application\58.0.3029.110\WidevineCdm,在子目錄_platform_specific\win_x86有下2個(gè)dll:
widevinecdm.dll - widevine核心模塊,導(dǎo)出的函數(shù)有:InitializeCdmModule_4,DeinitializeCdmModule,CreateCdmInstance,GetCdmVersion,GetHandleVerifier
widevinecdmadapter.dll - PPAPI插件標(biāo)準(zhǔn)適配庫(kù),導(dǎo)出函數(shù)有:PPP_GetInterface,PPP_InitializeModule,PPP_ShutdownModule
3.正常情況下播放DRM視頻的流程:
Chrome -> Widevine CDM Adapter(widevinecdmadapter.dll) -> Widevine CDM Module (widevinecdm.dll)
widevinecdmadapter.dll調(diào)用widevinecdm.dll的導(dǎo)出函數(shù)CreateCdmInstance來(lái)創(chuàng)建CDM實(shí)例.
4.CDM框架是Google Chrome的標(biāo)準(zhǔn),所以API參數(shù)和接口都有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實(shí)例要繼承于class ContentDecryptionModule_8,查看class ContentDecryptionModule_8,發(fā)現(xiàn)了一個(gè)非常重要的函數(shù):Decrypt,主要是將加密的數(shù)據(jù)傳入,解密后的數(shù)據(jù)傳出,我的天吶,只要截獲了這個(gè)函數(shù)不就搞定了嗎!!!
[C++] 純文本查看 復(fù)制代碼
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參數(shù)和class定義后,就設(shè)想如果在widevinecdmadapter.dll 和 widevinecdm.dll之間在互相調(diào)用時(shí)截獲到解密后的數(shù)據(jù)就可以繞過(guò)DRM保護(hù)機(jī)制.
6.開(kāi)始寫(xiě)一個(gè)DLL, 名為CdmProxy.dll, 我自己的CreateCdmInstance函數(shù),這部分不解釋了:
[C++] 純文本查看 復(fù)制代碼
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;
}
|
我的代{過(guò)}{濾}理class MyContentDecryptionModuleProxy,代碼不解釋:
// class MyContentDecryptionModuleProxy
[C++] 純文本查看 復(fù)制代碼
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:
// 最重要的解密函數(shù),保存原數(shù)據(jù)和解密后的數(shù)據(jù)
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.核心代碼寫(xiě)完了,就要解決DLL加載的問(wèn)題,嘗試了幾種簡(jiǎn)單的方式,分別把widevinecdm.dll和widevinecdmadapter.dll改名為widevinecdm_org.dll和widevinecdmadapter_org.dll,然后自己寫(xiě)一個(gè)DLL,導(dǎo)出和widevinecdm.dll或者widevinecdmadapter.dll相同的API,然后再調(diào)用原來(lái)DLL的方式,但這種方式發(fā)現(xiàn)不可行,原因在于Chrome的安全沙盒,所有的插件都是加載的沙盒進(jìn)程空間,敏感的API都無(wú)法使用,比如:ReadProcessMemory,CeateFile,OutputDebugString等等. 逃脫沙盒(Sandbox Escape)是Google獎(jiǎng)金數(shù)額非常高的,最高可達(dá)$15,000.
8.既然進(jìn)程都是Chrome創(chuàng)建的,所以就直接對(duì)chrome.exe下手,直接patch chrome.exe,讓其加載我的CdmProxy.dll,結(jié)果成功了,畢竟chrome啟動(dòng)時(shí)還未啟用沙盒機(jī)制,所以沒(méi)花費(fèi)太多技術(shù)就解決了DLL加載問(wèn)題.
9.改變API截獲方式,對(duì)widevinecdmadapter.dll進(jìn)行動(dòng)態(tài)的補(bǔ)丁,將調(diào)用API CreateCdmInstance的地址改為我自己的API my_CreateCdmInstance,然后在DLL加載時(shí)進(jìn)行以下處理:
[C++] 純文本查看 復(fù)制代碼
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.然后進(jìn)行測(cè)試,播放一個(gè)有DRM保護(hù)的DASH視頻:
https://shaka-player-demo.appspo ... gleKey/Manifest.mpd
發(fā)現(xiàn)文件沒(méi)保存下來(lái),LOG也沒(méi)輸出,想必是安全沙盒起作用了。
11.又要逃脫沙盒,經(jīng)過(guò)研究,發(fā)現(xiàn)根本不需要,只要在chrome啟動(dòng)參數(shù)增加 --no-sandbox 即可。我的天吶,為啥要提供這樣一個(gè)后門啊!!!
12.再次播放加密的視頻,文件順利保存下來(lái),LOG也輸出成功,經(jīng)驗(yàn)證,解密后的數(shù)據(jù)與之前未加密的數(shù)據(jù)是一致的。
13.Google Chrome的CDM就這樣被破解了,非常的簡(jiǎn)單的就繞過(guò)了Widevine DRM的算法,這應(yīng)該是Chrome CDM的框架設(shè)計(jì)的嚴(yán)重問(wèn)題,估計(jì)要改變也不是非常容易的。
這是LOG數(shù)據(jù):
( "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這么多年了,很多保護(hù)機(jī)制和算法都做的非常嚴(yán)密,但還是被Zhu一樣的隊(duì)友給出賣了。
公開(kāi)這個(gè)研究,是為了讓廣大的視頻公司不要以為DRM非常的安全,有時(shí)候真的是不堪一擊,某個(gè)環(huán)節(jié)出現(xiàn)漏洞,同樣面臨極大的安全問(wèn)題。
如果此篇文章不適合公布,請(qǐng)通知我,謝謝。
|