1.前言
最近在写qq机器人,然后爬某抑云music的api,虽然网页端千年不变,但是抱着学的态度,想试试爬客户端,所以有就记录下踩坑记录,然后潜水好几年了就来记录记录。
2.逆向环境介绍
| 工具 |
名称 |
| 系统 |
win11 |
| 逆向工具 |
frida,ida |
| 软件版本 |
pc端 3.1.28 |
3.逆向记录
哎没啥准备环境的 frida和ida 论坛一大堆教程
3.1逆向js代码
查看软件目录知道是类electron的程序所以先看看js代码,好消息是可以轻松的打开远程调试功能
cloudmusic.exe --remote-debugging-port=9222
然后使用edge附加,好消息js代码混淆不是很严重直接在搜索栏点击事件断点,追踪到下面代码
serialData(t) {
return u(this, void 0, void 0, (function*() {
const n = (...t) => u(this, void 0, void 0, (function*() {
return yield(t => u(this, void 0, void 0, (function*() {
return window.channel.serialData(t)
})))(...t)
}));
return yield n(t)
}))
}
大概是这样子的 window.channel.serialData(t)函数是一个c++本地函数,需要找c++实现,先记录传入的参数t=["/api/search/pc/result/tab","jsonXX"]第一个是接口名称,第二个是jsondata字符串
3.2寻找serialData对应的c++核心函数
寻找资源库没有*.node,然后查资料发现cloudmusic.dll为核心库, 先踩一个坑,我使用frida hook 一直触发不了frida "C:\Program Files\NetEase\CloudMusic\cloudmusic.exe" -l 163.js原因是frida没有附加对线程,这里有4个相关的进程,我花费了很久,找到内存占用最大的那个dll附加就好了
cloudmusic.dll导出函数没有serialData相关的函数,然后搜索字符串发现以下代码
{
sub_180063FB0(v63, "encodeAnonymousId2");
v62 = sub_18033E000(a2, v63);
sub_180063E30(v63);
if ( v62 )
{
v11 = sub_180DB73A0(&unk_181DDE678);
sub_180E0AD00(v11, a4, a5);
v64 = 1;
sub_18006F2E0(v53);
sub_1804DB440(a3);
return v64;
}
else
{
sub_180063FB0(v66, "encryptId");
v65 = sub_18033E000(a2, v66);
sub_180063E30(v66);
if ( v65 )
{
v12 = sub_180DB73A0(&unk_181DDE678);
sub_180E0AB10(v12, a4, a5);
v67 = 1;
sub_18006F2E0(v53);
sub_1804DB440(a3);
return v67;
}
else
{
sub_180063FB0(v69, "serialData");
v68 = sub_18033E000(a2, v69);
sub_180063E30(v69);
if ( v68 )
{
v13 = sub_180DB73A0(&unk_181DDE678);
sub_180E0D3A0(v13, a4, a5);
v70 = 1;
sub_18006F2E0(v53);
sub_1804DB440(a3);
return v70;
}
else
{
sub_180063FB0(v72, "serialData2");
v71 = sub_18033E000(a2, v72);
sub_180063E30(v72);
if ( v71 )
{
v14 = sub_180DB73A0(&unk_181DDE678);
sub_180E0D0C0(v14, a4, a5);
v73 = 1;
sub_18006F2E0(v53);
sub_1804DB440(a3);
return v73;
}
else
{
sub_180063FB0(v75, "deSerialData");
v74 = sub_18033E000(a2, v75);
sub_180063E30(v75);
if ( v74 )
{
hook180E0D3A0函数发送网络请求会触发就进一步追踪
__int64 __fastcall sub_180E0D3A0(__int64 a1, __int64 a2, __int64 a3)
v18 = sub_180CAD730((int)v14, (int)v15, (__int64)v13);
if ( v18 != 1 )
{
if ( (int)sub_1805D35A0() <= 2 )
{
v30 = sub_1805D1BD0(
(__int64)v25,
(__int64)"D:\\jenkins\\workspace\\18_11_IOS_PACKER_CI_PC3fabu\\music_pc\\setup\\build\\na\\orpheus\\src\\"
"framework\\framework\\client\\render_process_center.cpp",
dword_181D7549C + 16,
2);
v31 = v30;
v29 |= 1u;
LocaleT = _LocaleUpdate::GetLocaleT(v30);
v7 = sub_180067D20(LocaleT, "Serial:");
v8 = unknown_libname_3022(v7, v14);
v9 = sub_180067D20(v8, " error:");
v32 = sub_1800642E0(v9, v18);
v10 = sub_180060B00((__int64)v24);
nullsub_1(v10, v32);
}
if ( (v29 & 1) != 0 )
{
v29 &= ~1u;
sub_1805D2020(v25);
}
}
如果sub_180CAD730返回为!1就会报错那说明180CAD730在处理加密逻辑继续追踪180CAD730发现下面代码
__int64 __fastcall sub_180CB26A0(int a1, int a2, int a3, int a4, __int64 a5, __int64 a6)
{
size_t v7; // [rsp+20h] [rbp-28h]
LODWORD(v7) = a4;
return sub_180CB2910((int)L"y1LN8qzeNzxTWX6dVeyshvKmXJRQRfkZy9Y7e7fao6g=", a1, a2, a3, v7, a5, a6);
}
发现很像是加密函数点进去
else
{
if ( v34 == 1 )
{
if ( (int)AES_set_encrypt_key() < 0 )
{
v34 = 6;
}
else if ( Size_4 )
{
v7 = ((Size_4 - 1) >> 4) + 1;
do
{
AES_encrypt();
v25 += 16;
--v7;
}
while ( v7 );
}
}
v35 = Block;
if ( !Block )
goto LABEL_55;
}
free(v35);
然后hook180CB2910发现结果 a1 a2 a3 v7 是之前类似于t=["/api/search/pc/result/tab","jsonXX"]的字符串已经其大小
好好看见AES_encrypt()就是加密代码了
LODWORD(Block) = sub_180CB3D00(L"qGWDhNWDRGvh421GZVutvg==", 0i64, &v46);
if ( (_DWORD)Block == 2 )
{
v11 = j__malloc_base((unsigned int)v46);
LODWORD(Block) = sub_180CB3D00(L"qGWDhNWDRGvh421GZVutvg==", v11, &v46);
if ( (_DWORD)Block != 1 )
{
if ( v11 )
free(v11);
}
}
LODWORD(v49) = sub_180CB3D00(L"ErCUMN/gBpmtg+wmLZrDCA==", 0i64, &v46);
if ( (_DWORD)v49 == 2 )
{
v8 = j__malloc_base((unsigned int)v46);
LODWORD(v49) = sub_180CB3D00(L"ErCUMN/gBpmtg+wmLZrDCA==", v8, &v46);
if ( (_DWORD)v49 != 1 )
{
if ( v8 )
free(v8);
}
}
v14 = sub_180CB3D00(L"xKlkMXZUU8J2uUH2ZfmYmQ==", 0i64, &v46);
if ( v14 == 2 )
{
v9 = j__malloc_base((unsigned int)v46);
v14 = sub_180CB3D00(L"xKlkMXZUU8J2uUH2ZfmYmQ==", v9, &v46);
if ( v14 != 1 )
{
if ( v9 )
free(v9);
}
}
v15 = sub_180CB3D00(L"eOYLRn09GyKgAzkfn3pLFA==", 0i64, &v46);
if ( v15 == 2 )
{
v10 = j__malloc_base((unsigned int)v46);
v15 = sub_180CB3D00(L"eOYLRn09GyKgAzkfn3pLFA==", v10, &v46);
if ( v15 != 1 )
{
if ( v10 )
free(v10);
}
}
这里180CB3D00点进去是有解密函数那么,肯定是将数据提取出来变成 v8 v9 v10 v11
if ( (_DWORD)Block == 1 && (_DWORD)v49 == 1 && v14 == 1 && v15 == 1 )
{
v16 = Size;
v49 = Size + (_DWORD)a5 + 22;
v57 = 0i64;
v17 = (char *)j__malloc_base(v49);
v18 = v17;
if ( !v17 )
{
if ( v11 )
free(v11);
if ( v8 )
free(v8);
if ( v9 )
free(v9);
if ( v10 )
free(v10);
return 4i64;
}
v19 = Src;
*(_DWORD *)v17 = *(_DWORD *)v8;
*((_WORD *)v17 + 2) = v8[2];
v54 = v16;
memmove(v17 + 6, v19, v16);
v20 = (unsigned int)(v16 + 6);
v21 = v52;
*(_WORD *)&v18[v20] = *(_WORD *)v9;
v18[v20 + 2] = v9[2];
LODWORD(v20) = v20 + 3;
memmove(&v18[(unsigned int)v20], v21, (unsigned int)a5);
v22 = (unsigned int)(v20 + a5);
*(_QWORD *)&v18[v22] = *v10;
*(_DWORD *)&v18[v22 + 8] = *((_DWORD *)v10 + 2);
v18[v22 + 12] = *((_BYTE *)v10 + 12);
sub_180CB6E50(v56);
sub_180CB6E80(v56, v18, v49);
sub_180CB6B40(&v57, v56);
free(v18);
sub_180CB2060(&v57, &v58);
*(_QWORD *)&v57 = Size_4;
v23 = j__malloc_base(Size_4);
v46 = (size_t)v23;
v24 = v23;
if ( !v23 )
return 8i64;
v25 = v23;
Block = 0i64;
LODWORD(v49) = Size_4 - (Size + a5 + 58);
memmove(v23, Src, Size);
v26 = v54;
v27 = v52;
*(_QWORD *)&v24[v54] = *v11;
*(_DWORD *)&v24[v26 + 8] = *((_DWORD *)v11 + 2);
v24[v26 + 12] = *((_BYTE *)v11 + 12);
memmove(&v24[Size + 13], v27, (unsigned int)a5);
v28 = Size;
v29 = v59;
v30 = (unsigned int)a5 + Size + 13;
*(_QWORD *)&v24[v30] = *v11;
v31 = v58;
*(_DWORD *)&v24[v30 + 8] = *((_DWORD *)v11 + 2);
v24[v30 + 12] = *((_BYTE *)v11 + 12);
v32 = (unsigned int)(v30 + 13);
*(_OWORD *)&v24[v32] = v31;
*(_OWORD *)&v24[v32 + 16] = v29;
v33 = v32 + 32;
if ( v33 != v28 + (_DWORD)a5 + 58 )
{
free(v11);
free(v8);
free(v9);
free(v10);
return 7i64;
}
memset(&v24[v33], v49, (int)v49);
v34 = sub_180CB3D00(v53, 0i64, &v49);
if ( v34 == 2
&& (Block = j__malloc_base((unsigned int)v49), v34 = sub_180CB3D00(v53, Block, &v49), v34 != 1)
&& Block )
{
free(Block);
v35 = Block;
}
else
{
if ( v34 == 1 )
{
if ( (int)AES_set_encrypt_key() < 0 )
{
v34 = 6;
}
else if ( Size_4 )
{
v7 = ((Size_4 - 1) >> 4) + 1;
do
{
AES_encrypt();
v25 += 16;
--v7;
}
while ( v7 );
}
}
v35 = Block;
if ( !Block )
goto LABEL_55;
}
这里sub_180CB6E50(v56);sub_180CB6E80(v56, v18, v49);sub_180CB6B40(&v57, v56);点击去发现是Md5计算然后转化为消息16进制,因为我不知道sub_180CB3D00函数返回的数据所以在计算md5前 hook
v34 = sub_180CB3D00(v53, Block, &v49)这里是取出真正的key hook直接提取出来,这里似乎是ECB加密,PaddingMode.PKCS7 填充(因为之前很多逆向发现很多是PaddingMode.PKCS7 ,因为我需要逐步计算 就hook了AES_encrypt()函数发现的逻辑整理用c#在下面表示
private static string Md5String(string input)
{
var data = Encoding.UTF8.GetBytes(input);
var hash = MD5.HashData(data);
var sb = new StringBuilder();
Console.WriteLine();
foreach (var b in hash) sb.Append(b.ToString("x2")); // 小写 32 位
return sb.ToString();
}
private class AesEncryptionHelper
{
private static readonly byte[] key = new byte[]
{
0x65, 0x38, 0x32, 0x63, 0x6b, 0x65, 0x6e, 0x68,
0x38, 0x64, 0x69, 0x63, 0x68, 0x65, 0x6e, 0x38
};
private static readonly string hea = "nobody";
private static readonly string en = "use";
private static readonly string end = "md5forencrypt";
public static string EncryptAes(string a1, string a2)
{
var s = hea + a1 + en + a2 + end;
var ddd = a1 + "-36cd479b6b5-" + a2 + "-36cd479b6b5-" + Md5String(s);
var ecb = AesEncryptEcb(Encoding.UTF8.GetBytes(ddd));
return BytesToHex(ecb);
}
private static byte[] AesEncryptEcb(byte[] raw)
{
using var aes = Aes.Create();
aes.Key = key; //
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
aes.BlockSize = 128;
using var encryptor = aes.CreateEncryptor();
return encryptor.TransformFinalBlock(raw, 0, raw.Length);
}
public static byte[] AesDecryptEcb(byte[] cipher)
{
using var aes = Aes.Create();
aes.Key = key;
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
aes.BlockSize = 128;
using var decryptor = aes.CreateDecryptor();
return decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
}
private static string BytesToHex(byte[] bytes)
{
var hex = new StringBuilder(bytes.Length * 2);
foreach (var b in bytes) hex.Append($"{b:X2}");
return hex.ToString();
}
public static byte[] HexToBytes(string hex)
{
var len = hex.Length;
var bytes = new byte[len / 2];
for (var i = 0; i < len; i += 2) bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
}
然后通过控制台调用传入参数发现一模一样加密逻辑解决,后面解密第一步尝试用相同key解密看看,结果就是同一个key就直接不逆向了