网易云音乐PC客户端加密API逆向解析
1、前言
網(wǎng)上已經(jīng)有大量的web端接口解析的方法了,但是對客戶端的接口解析基本上找不到什么資料,本文主要分析網(wǎng)易云音樂PC客戶端的API接口交互方式。
通過內(nèi)部的代理設(shè)置,使用fiddler作為代理工具,即可查看交互流程:
可以大致看一下交互方式,通過HTTPS POST交互,POST了一串params的內(nèi)容,內(nèi)容加密,返回JSON內(nèi)容,我要做的重點就在于解析params的生成方式,用于模擬這次交互。
(Tan1993:這是后續(xù)編寫的內(nèi)容,截圖很多都是后補的,所以可能會出現(xiàn)使用不同的調(diào)試工具,不同的環(huán)境,不同的時間等,不影響閱讀。另外本人工作主要是linux網(wǎng)絡(luò)方向的,像是這次只是我的一點業(yè)余愛好,也很少會去逆向東西,如果出現(xiàn)一些比較業(yè)余的操作或想法時,還望指出)
2、初步了解
下載最新版PC版網(wǎng)易云安裝(目前是2.3.0.196231版本),分析在程序所在目錄下的文件。
動態(tài)鏈接庫與可執(zhí)行文件:
第一個最讓我注意的時libcurl,這個網(wǎng)絡(luò)庫可以用于HTTP協(xié)議交互,如果通過該庫與服務(wù)器交互, od斷點到curl_easy_perform再往回推就可以判斷轉(zhuǎn)換算法位置了,然而事實比我想象的復(fù)雜多了,這個庫僅在程序剛運行時用于一些無關(guān)的網(wǎng)絡(luò)交互(Tan1993:記不清了,好像是版本還是客戶端信息相關(guān)的請求)。
第二個是libcef,這個是個基于C/C++的Web browser控件,可以簡單理解為就是個瀏覽器的殼子(Tan:為什么說關(guān)鍵API沒用到libcurl庫,因為除了開始時cef框架還沒初始化前網(wǎng)絡(luò)交互用到那個庫而已,一點cef環(huán)境起來了,都是通過JS ajax交互了)。
其他的除了cef依賴的dll外,兩個主程序和cloudmusic.dll都比較值得關(guān)注。
資源文件:
除了在package下的其他都是cef庫依賴的資源文件。
都是未知的格式,一般看到未知格式的文件,我都會用7z嘗試打開看看,是不是某種歸檔格式文件,這個一下就蒙中了,是zip格式的。
除了幾個通過后綴就能看出來的皮膚文件,還有兩個比較可疑的文件,翻一翻比較大的orpheus.ntpk文件,里面可以看到都是網(wǎng)頁相關(guān)的資源文件,看到那個core.js,就讓我聯(lián)想到網(wǎng)頁版API提取時用到的那個core.js文件了,腦海里就想著替換然后對轉(zhuǎn)換流程動態(tài)分析了,事實有點不盡人意,該zip文件加密了。
OK,調(diào)研階段結(jié)束,在不進行逆向解析前,能了解到的也就止步于此了。
3、第一輪嘗試
其實一開始我是把目光放在libcurl上面的,在斷點到curl庫的函數(shù)上時發(fā)現(xiàn)只有程序剛運行時觸發(fā)過幾次,后面所有網(wǎng)絡(luò)交互都不用這個庫了,就轉(zhuǎn)戰(zhàn)到cef上。而cef的重點在于內(nèi)部的JS文件,能提取到該文件才是關(guān)鍵的。
0×2712即CURLOPT_URL宏,eax中存放著url的字符串指針,基本上都是無關(guān)的url。
第一個任務(wù)來了,逆向?qū)ふ姨卣鞔簿褪敲艽a,這里斷點到系統(tǒng)文件操作API上,斷到CreateFileW,一頓的F9后可以看到加載到default.skin文件了(圖中是native.ntpk,同類型的加密ZIP文件),后續(xù)就單步調(diào)試下去。
然后看到一個比較特別的內(nèi)存塊,一看就是PNG格式的文件頭,就可以判斷這一步資源已經(jīng)解壓縮到內(nèi)存了。
往上推幾步,斷點,縮小范圍,再跟下來,看看哪里做了解壓操作,再一步步跟函數(shù)。(Tan1993:可能比較業(yè)余,但我也只能一點點縮小范圍在一點點看流程,憑經(jīng)驗判斷可能會做什么操作,縮短到比較短的范圍,不然一堆匯編碼真的會受不了,感謝世界上程序員的思想都是接近的吧)。
得知密碼后,就可以解壓出core.js文件了(Tan1993:這里僅提供思路,不提供便民服務(wù)哈)
又是這一堆讓人窒息的混淆,卡得懷疑人生,先解壓縮再看吧。
解壓后,搜幾個關(guān)鍵字,比如params,eapi,batch等最上面HTTP交互時的一些特征
關(guān)鍵代碼,像這樣混淆的JS代碼,如果不通過調(diào)試器跟蹤,很難看懂,目前能可以看出也只有channel.serialData應(yīng)該時比較關(guān)鍵的轉(zhuǎn)換函數(shù),但是搜索了整個JS文件都找不到函數(shù)定義,不知道是不是混淆到哪個奇怪的地方了。
雖然cef自帶DevTools,但是已經(jīng)被屏蔽掉了也無法在程序里調(diào)出來,所以我想在JS文件中加上alert調(diào)試關(guān)鍵參數(shù)。然后我修改了core.js文件,按原來的密碼壓縮回去。但程序根本就起不來,為什么呢,看看原版的.ntpk文件,很明顯還有一些奇怪的東西和zip文件一起合成了這個ntpk文件格式。根據(jù)經(jīng)驗判斷很可能時類似于數(shù)字簽名的東西(Tan1993:之前我也會對一些可能被篡改的檔案末尾對整個文件加鹽生成一個hash值用于校驗,但是后續(xù)跟完網(wǎng)易云的數(shù)字簽名方式讓我又學(xué)習(xí)了不少)。
4、第二輪嘗試
為了方便調(diào)試,我需要替換掉資源文件中的core.js文件,但是該資源文件不僅僅加密壓縮了,還有一些其他內(nèi)容存在,所以這次跟代碼就是為了了解除了zip文件本身以外其他部分內(nèi)容的作用。
還是斷到CreateFileW函數(shù)上,其實第一輪跟代碼的時候我就已經(jīng)發(fā)現(xiàn)了部分調(diào)用系統(tǒng)加密服務(wù)提供程序 (CSP)庫的函數(shù)。
一步步跟過來,發(fā)現(xiàn)用的是SHA1數(shù)字簽名算法(Tan1993:不是很了解CSP庫,但這個是為Windows系列操作系統(tǒng)制訂的底層加密接口,和我理解的SHA不太一樣,我姑且將程序內(nèi)部的那部分稱為公鑰,與文件頭部的校驗數(shù)據(jù)進行校驗)。
文件頭NTPK,文件長度0x0D5C5B,校驗串長度0×100
剛好差了0×110長度,除了0×100用于校驗的數(shù)據(jù),還有0×10的頭部。
由于我是無法在不知道私鑰的情況下,再次對該文件進行簽名的,所以我只能把程序內(nèi)部的用于校驗的公鑰一并替換,再生成一個對應(yīng)的檢驗數(shù)據(jù),從而通過系統(tǒng)驗證,或者直接把驗證部分的代碼跳轉(zhuǎn)邏輯修改掉(Tan1993:其實可能改分支流程修改會更簡單也說不定,但我一開始選擇的是替換公鑰重新生成校驗數(shù)據(jù))。
int GenKey(HCRYPTPROV hProv)
{
HCRYPTKEY hKey;
HANDLE hFile = NULL, hOutFile = NULL;
DWORD dwSize = 0, dwRead = 0, dwWrite = 0, dwBlobLen = sizeof(bRsaKey);
BYTE *pbFileData = NULL;
int ret = -1;
// 先讀取原版的dll,加載到內(nèi)存中
hFile = CreateFileW(L"cloudmusic_src.dll",
GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
dwSize = GetFileSize(hFile, NULL);
pbFileData = new BYTE[dwSize];
ReadFile(hFile, pbFileData, dwSize, &dwRead, NULL);
CloseHandle(hFile);
if (!memcmp(pbFileData + 0x7C3438, bRsaKey, sizeof(bRsaKey)))
{
// 重新生成密鑰對
CryptGenKey(hProv, AT_SIGNATURE, CRYPT_EXPORTABLE, &hKey);
memset(bRsaKey, 0, sizeof(bRsaKey));
CryptExportKey(hKey, NULL, PUBLICKEYBLOB, 0, bRsaKey, &dwBlobLen);
// 將新生成的公鑰覆蓋原本dll中的公鑰
memcpy(pbFileData + 0x7C3438, bRsaKey, sizeof(bRsaKey));
// 隨帶把debug端口開了(后續(xù)再解釋)
SetDebugPort(pbFileData);
hOutFile = CreateFileW(L"cloudmusic.dll",
GENERIC_WRITE, FILE_SHARE_READ, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
// 寫回到dll中
WriteFile(hOutFile, pbFileData, dwSize, &dwWrite, NULL);
CloseHandle(hOutFile);
ret = 0;
}
delete[] pbFileData;
CryptDestroyKey(hKey);
return ret;
}
int EncFile(HCRYPTPROV hProv, LPCWCHAR wstrInFile, LPCWCHAR wstrOutFile)
{
HCRYPTHASH hHash;
DWORD dwSize = 0, dwRead = 0, dwWrite = 0, dwOutSignSize = 0;
HANDLE hFile = NULL, hOutFile = NULL;
BYTE *pbFileData = NULL, *pbSignData = NULL;
// 打開帶密碼的壓縮文件
hFile = CreateFileW(wstrInFile,
GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
dwSize = GetFileSize(hFile, NULL);
pbFileData = new BYTE[dwSize];
ReadFile(hFile, pbFileData, dwSize, &dwRead, NULL);
CloseHandle(hFile);
// 打開輸出文件
hOutFile = CreateFileW(wstrOutFile,
GENERIC_WRITE, FILE_SHARE_READ, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
// 寫入文件頭
WriteFile(hOutFile, bHead, sizeof(bHead), &dwWrite, NULL);
// 寫入原壓縮文件長度
WriteFile(hOutFile, &dwSize, sizeof(int), &dwWrite, NULL);
// 創(chuàng)建并計算Hash值
CryptCreateHash(hProv, CALG_SHA, 0, 0, &hHash);
CryptHashData(hHash, pbFileData, dwSize, 0);
CryptSignHash(hHash, AT_SIGNATURE, NULL, 0, NULL, &dwOutSignSize);
pbSignData = new BYTE[dwOutSignSize];
CryptSignHash(hHash, AT_SIGNATURE, NULL, 0, pbSignData, &dwOutSignSize);
// 寫入Hash值大小
WriteFile(hOutFile, &dwOutSignSize, sizeof(int), &dwWrite, NULL);
// 寫入Hash值(校驗數(shù)據(jù))
WriteFile(hOutFile, pbSignData, dwOutSignSize, &dwWrite, NULL);
// 寫入原壓縮文件
WriteFile(hOutFile, pbFileData, dwSize, &dwWrite, NULL);
CloseHandle(hOutFile);
delete[] pbSignData;
delete[] pbFileData;
CryptDestroyHash(hHash);
return 0;
}
截了一部分代碼,用于修改cloudmusic.dll中的二進制數(shù)據(jù),偏移是根據(jù)內(nèi)存加載地址與基址算的,直接固定偏移修改即可。
到這一步其實我已經(jīng)可以替換掉core.js文件并且可以alert彈出對話框,顯示一些JS運行時數(shù)據(jù)了,雖然alert彈框并不是那么好用。
通過alert我可以看到加密前的內(nèi)容,也就是具體發(fā)了哪些數(shù)據(jù),以及加密后是什么樣子的,很可惜的是當(dāng)我嘗試alert(channel.serialData)時發(fā)現(xiàn)是[native code],按我個人理解應(yīng)該是系統(tǒng)二進制函數(shù)才會顯示這個的吧(對JS并不是非常了解),懷疑是庫函數(shù),但查詢無果,后來想了想會不會是JS調(diào)用了C++代碼(憑我對cef粗糙的理解),我嘗試去查了一下,果然是可以的,那么很有可能這部分加密轉(zhuǎn)換的代碼還是在主程序中,這就很頭疼了,剛從主程序逆向脫離出來到JS這個自由的世界,又要回到看匯編碼的環(huán)境了。
5、第三輪嘗試
這一輪主要目的是找到channel.serialData在主程序的位置,根據(jù)我對cef的理解,應(yīng)該是在程序啟動時,注冊了一部分回調(diào)函數(shù),可以從注冊的時候找到回調(diào)函數(shù)入口,然后等觸發(fā)channel.serialData動作時,從回調(diào)函數(shù)跟代碼跟下來。
根據(jù)DLL版本,我找到了對應(yīng)的cef源碼版本,cef注冊回調(diào)時是整個結(jié)構(gòu)體的,必須找到對應(yīng)的版本避免新版本結(jié)構(gòu)體不一樣導(dǎo)致偏移位置有差異。
在看源碼的過程中發(fā)現(xiàn)結(jié)構(gòu)體里有個很有意思的字段,一個debug端口,調(diào)研了一下,這個端口很有用了,可以遠程DevTools,這樣還用什么alert。
如果要在調(diào)用初始化前把結(jié)構(gòu)體改掉,要么API Hook修改,要么靜態(tài)文件修改,文件修改的話只能舍棄一些無用代碼來改這個結(jié)構(gòu)體了,我選了一個不影響的賦值語句,改成給這個地址賦9222。
對照源碼中結(jié)構(gòu)體計算偏移值
原本修改cloudmusic.dll的代碼中增加個代碼段修改的方法
// 修改Debug Port為9222
void SetDebugPort(BYTE *pbFileData)
{
if (!memcmp(pbFileData + 0x14EED, bSettingAsm, sizeof(bSettingAsm)))
{
bSettingAsm[2] = 0x94; // 結(jié)構(gòu)體偏移
bSettingAsm[6] = 0x06; // 0x2406也就是9222端口
bSettingAsm[7] = 0x24;
memcpy(pbFileData + 0x14EED, bSettingAsm, sizeof(bSettingAsm));
}
}
現(xiàn)在我就可以通過http://127.0.0.1:9222遠程訪問DevTools了。可當(dāng)我打開網(wǎng)頁時一片空白,這時候又憑借我對cef粗略的了解,在程序目錄下,并沒有devtools相關(guān)的資源,其實只要把資源文件補上就可以了(官網(wǎng)已經(jīng)沒有這么老的資源文件檔案了,這個還是我網(wǎng)上找的3.1916版本的devtools資源文件)
這時候所有JS調(diào)試命令都可以改成console.log來進行了,方便了好多。
回到正題,從注冊來跟代碼實在是太痛苦了。一個是注冊的內(nèi)容比較多,一層疊一層的,而且程序用的是C++ warp的C語言版本的cef庫,和源碼對照跟的時候還是有點差別的。這時候我想到一個非常好的方法,那就是制造一個死循環(huán)。
6、第四輪嘗試
上面就提到了,我放棄了從注冊一步步跟蹤回調(diào)函數(shù)的麻煩方案,而是在JS中知道一個死循環(huán),不停的調(diào)用channel.serialData函數(shù),等程序單核滿載時,只需要將調(diào)試器附加程序,點一點暫停,基本上就是這個函數(shù)相關(guān)業(yè)務(wù)流程的代碼了(JS到機器碼代碼按我理解應(yīng)該在堆上,而加密的代碼應(yīng)該在程序代碼段上,所以我定位的時候可以忽略掉很多JS的代碼,找到真正相關(guān)的代碼位置)
實際上,channel.serialData的匯編碼也非常多,流程也分了好多部分,這部分工作量實在是降不下來,但是很多可能是為了防止靜態(tài)分析的代碼,部分特征串是運行時生成的,但是因為這部分特征串都是固定的,所以是可以不用去仔細琢磨的(然而我花了一兩天來看那一堆匯編碼來算出特征串,非常郁悶,早知道就逆推就好,但說實話,光逆推也會很難,主要是要有一定理解)
簡單說明一下轉(zhuǎn)換流程
1、 輸入url(請求部分)和data(提交的json數(shù)據(jù))
2、 拼成”nobody” + url + “use” + data + “md5forencrypt”字符串
3、 對字符串計算MD5
4、 二次拼接url + “-36cd479b6b5-” + data + “-36cd479b6b5-” + md5
5、 0×10對齊,缺少的部分會以缺少的位數(shù)來填充
6、 私有轉(zhuǎn)換方法(也許是我不知道的一種加密方式?)
附上一部分分析的圖
待加密數(shù)據(jù),0×10字節(jié)對齊,每次處理0×10字節(jié)的數(shù)據(jù)
輔助加密數(shù)據(jù)(動態(tài)生成,但是是固定的,我還傻傻去復(fù)現(xiàn)了一遍生成流程)
開始對0×10進行轉(zhuǎn)換
一堆異或和位移計算,這個還是很好復(fù)現(xiàn)到C的代碼中的,這個比較長就不全粘貼了。
循環(huán)轉(zhuǎn)換完后再按照”%02X”格式snprintf到字符串即可。我沒有過多去理解這個加密算法究竟是什么原理,只是直譯匯編碼。
后來嘗試反過來解析,看了一早上沒看出來,簡單描述一下為什么難以逆轉(zhuǎn)的問題。
內(nèi)存塊mem
a1b1b1c1a1b1b1c1 a2b2b2c2a2b2b2c2 ………
eax = a1a2a3a4
ebx = b1b2b3b4
ecx = c1c2c3c4
edx = d1d2d3d4
eax = mem[a4 * 8] ^ mem[b3 * 8 + 3] ^ mem[c2 * 8 + 2] ^ mem[d1* 8 + 1]
ebx = mem[a3 * 8] ^ mem[b2 * 8 + 3] ^ mem[c1 * 8 + 2] ^mem[d4 * 8 + 1]
ecx = mem[a2 * 8] ^ mem[b1 * 8 + 3] ^ mem [c4 * 8 + 2] ^ mem[d3* 8 + 1]
edx = mem[a1 * 8] ^ mem[b4 * 8 + 3] ^ mem[c3 * 8 + 2] ^mem[d2 * 8 + 1]
然后在得知后面的eax,ebx,ecx,edx逆推原來的,感覺不太可能,但是mem并不是沒有規(guī)律的一個內(nèi)存塊,而且數(shù)組索引時也做了些巧妙的偏移,事實上內(nèi)存塊確實有不少規(guī)律(比如a1是偶數(shù)時b1是a1的一半,c1是a1 ^ b1),而且和索引時的偏移可能會相得益彰,如果能看出竅門說不定還是能解的,有興趣的小伙伴也可以研究一下(Tan1993:個人沒學(xué)過加密學(xué),只略懂一部分概念)
7、匯總
其實到這一步,我可以通過遠程devtools來看發(fā)送前未加密的內(nèi)容以及結(jié)構(gòu),同時我也可以通過已經(jīng)復(fù)現(xiàn)的加密方法,對不同業(yè)務(wù)數(shù)據(jù)加密發(fā)送出去。我發(fā)現(xiàn)有一部分請求數(shù)據(jù)返回內(nèi)容也是加密的,但這個是可以在客戶端控制e_r的值來控制是否需要返回加密內(nèi)容的。
寫個模擬客戶端下載歌曲的小Demo,本來發(fā)送和接收都是加密的數(shù)據(jù)的下載接口,就可以通過服務(wù)器驗證實現(xiàn)下載了,解析到此告一段落,雖然過程中還有很多內(nèi)容值得研究,如果有機會以后會繼續(xù)挖掘。
8、總結(jié)
由于并沒有找到任何的參考資料,斷斷續(xù)續(xù)也研究了一周時間。除了實現(xiàn)了目標(biāo)以外,還是有不少收獲的,比如比較有趣的加密算法,數(shù)字簽名方法,cef庫,還有一些逆向的思路。
比較遺憾的是沒有把解密的算法也解析出來,同時在客戶端控制e_r的值來控制返回數(shù)據(jù)是否加密顯然不是好方法,官方只需要忽略這個參數(shù)強制對部分API返回加密數(shù)據(jù),正常的客戶端也沒有任何影響(難道有平臺相關(guān)性所以才把這個參數(shù)放到客戶端的嗎?)。
(Tan1993:視情況考慮是否在github提供源碼)
9、彩蛋
將一件有趣的事,當(dāng)時我嘗試在一臺國外IP的服務(wù)器上調(diào)用web的api接口時發(fā)現(xiàn)不能適用,獲取不到數(shù)據(jù),然后我又跟了一便JS代碼發(fā)現(xiàn)邏輯不一樣,其中發(fā)現(xiàn)了一個很有意思的特征串(在你們看不到的地方,總有調(diào)皮的程序員):
總結(jié)
以上是生活随笔為你收集整理的网易云音乐PC客户端加密API逆向解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 借呗就算提前还款当月利息还是要给的么
- 下一篇: 怎样使普通面粉变成低筋粉