
工具
Ghidra 11.0 Public Version(逆向分析)
https://hexed.it(查看二进制文件)
Python(实现解包算法)
初步调查
相比于上次针对《白色相簿2》的逆向,这次的工业级软件结构明显复杂起来,最直观的就是多出了许多dll(应用程序扩展)。与音效相关的算法在哪里?
第一反应是根据文件名分析audioeffects.dll,但始终找不到读取配置文件的入口和“NCAE”的文件头,而且所见之处皆为零碎的算法模块,分析不出完整的调用链,故转向自顶向下分析。cloudmusic.exe调用了kernel32、user32、@mscore、dbghelp、rpcrt4等dll,但最关键的是调用了cloudmusic.dll,而cloudmusic_util.exe只调用了advapi32、ole32、libeay32、libcurl、avcodec-58、avformat-58、avutil-56、swresample-3等提供基础服务(网络请求、音视频解码等)的dll。

起点
cloudmusic.dll调用的许多次级dll中,有一个neaudioeffects.dll。原来一直找错了入口!次级调用的入口在0x10868F40的audiofx::AudioEffects::Create()函数指针,由此reference到FUN_10045380,再到FUN_100455d0,最终在FUN_10207050发现“audio effects”的字符串。将此作为逆向工程的起点:
void __cdecl FUN_10207050(int param_1,int param_2,int **param_3,char param_4)
{
bool bVar1;
char cVar2;
uint uVar3;
int **ppiVar4;
int iVar5;
undefined4 *puVar6;
int *piVar7;
void **ppvVar8;
int ****ppppiVar9;
undefined4 ***pppuVar10;
bool bVar11;
undefined auStack_16c [4];
void *local_168 [6];
void *local_150 [6];
void *local_138 [6];
int local_120 [44];
uint local_70;
int ****local_6c [5];
undefined4 ***local_58;
undefined local_54;
undefined4 uStack_48;
uint local_44;
undefined4 local_40;
undefined4 local_3c;
undefined4 local_38;
undefined4 ***local_34 [4];
uint local_24;
uint local_20;
uint local_1c;
void *local_14;
undefined *puStack_10;
undefined4 local_c;
local_c = 0xffffffff;
puStack_10 = &LAB_10832245;
local_14 = ExceptionList;
local_1c = DAT_10a76ad0 ^ (uint)auStack_16c;
uVar3 = DAT_10a76ad0 ^ (uint)&stack0xfffffe88;
ExceptionList = &local_14;
bVar11 = false;
local_70 = 0;
if (param_4 == '\0') {
ppiVar4 = (int **)FUN_100455d0();
(**(code **)(**ppiVar4 + 0x18))(1,uVar3);
iVar5 = FUN_103a6f00();
if (iVar5 < 1) {
puVar6 = FUN_103a6090(local_120,"audio_effect.cpp",0xf2,0);
local_c = 0;
local_70 = 1;
FUN_100124b0(puVar6 + 2,"Switch Off AudioEffect.");
}
bVar11 = 0 < iVar5;
}
else if (param_3[4] == (int *)0x0) {
iVar5 = FUN_103a6f00();
if (iVar5 < 1) {
puVar6 = FUN_103a6090(local_120,"audio_effect.cpp",0xf6,0);
local_c = 1;
local_70 = 2;
FUN_100124b0(puVar6 + 2,"Set AudioEffect Failed. Path Empty.");
}
bVar11 = iVar5 >= 1;
}
else {
if (param_1 - 1U < 2) {
FUN_100161e0((undefined2 *)local_6c);
local_c = 3;
if (param_2 == 1) {
cVar2 = FUN_103a94b0(2,local_6c);
if (cVar2 != '\0') {
ppiVar4 = (int **)FUN_103a40d0((undefined2 *)local_150,param_3);
local_c = CONCAT31(local_c._1_3_,4);
FUN_103a3ed0(local_6c,ppiVar4);
ppvVar8 = local_150;
goto LAB_10207229;
}
}
else if (param_2 == 2) {
cVar2 = FUN_1006a790(0,(int **)local_6c);
if (cVar2 != '\0') {
ppiVar4 = (int **)FUN_103a40d0((undefined2 *)local_138,param_3);
local_c = CONCAT31(local_c._1_3_,5);
FUN_103a3ed0(local_6c,ppiVar4);
ppvVar8 = local_138;
goto LAB_10207229;
}
}
else if (param_2 == 3) {
ppiVar4 = (int **)FUN_103a3e10(local_168,param_3);
local_c = CONCAT31(local_c._1_3_,6);
FUN_103a3ed0(local_6c,ppiVar4);
ppvVar8 = local_168;
LAB_10207229:
local_c = CONCAT31(local_c._1_3_,3);
FUN_10016280(ppvVar8);
}
bVar1 = FUN_103ab6a0(local_6c);
if (bVar1) {
local_20 = 0xf;
local_24 = 0;
local_34[0] = (undefined4 ***)((uint)local_34[0] & 0xffffff00);
local_c._0_1_ = 8;
cVar2 = FUN_103ac270(local_6c,local_34);
if (cVar2 == '\0') {
iVar5 = FUN_103a6f00();
if (iVar5 < 1) {
puVar6 = FUN_103a6090(local_120,"audio_effect.cpp",0x115,0);
local_c = CONCAT31(local_c._1_3_,9);
local_70 = 0x10;
FUN_100124b0(puVar6 + 2,"SetAudioEffectParam failed. read effect file error.");
}
local_c = 8;
if (iVar5 < 1) {
FUN_103a63d0(local_120);
}
}
else {
local_40 = 0xf;
local_44 = 0;
local_54 = 0;
local_c = CONCAT31(local_c._1_3_,10);
pppuVar10 = local_34;
if (0xf < local_20) {
pppuVar10 = local_34[0];
}
local_70 = 1;
iVar5 = FUN_1015be70((char *)pppuVar10,local_24,(int **)&local_54,&local_70);
if (iVar5 == 0) {
if (local_70 == 1) {
pppuVar10 = (undefined4 ***)&local_54;
goto LAB_10207511;
}
if (local_70 != 2) goto LAB_1020750a;
local_70 = 0;
local_38 = 0;
local_3c = 0;
FUN_107c3e80(&local_70,&local_38,&local_3c);
if ((param_1 == 1) && (5 < (int)local_70)) {
ppiVar4 = (int **)FUN_100455d0();
(**(code **)(**ppiVar4 + 0x84))();
ppiVar4 = (int **)FUN_100455d0();
(**(code **)(**ppiVar4 + 0x74))(1);
ppiVar4 = (int **)FUN_100455d0();
pppuVar10 = &local_58;
if (0xf < local_44) {
pppuVar10 = local_58;
}
(**(code **)(**ppiVar4 + 0x78))(pppuVar10,uStack_48);
ppiVar4 = (int **)FUN_100455d0();
(**(code **)(**ppiVar4 + 0x88))();
}
else {
ppiVar4 = (int **)FUN_100455d0();
(**(code **)(**ppiVar4 + 0x84))();
ppiVar4 = (int **)FUN_100455d0();
(**(code **)(**ppiVar4 + 0x6c))(1);
ppiVar4 = (int **)FUN_100455d0();
pppuVar10 = &local_58;
if (0xf < local_44) {
pppuVar10 = local_58;
}
(**(code **)(**ppiVar4 + 0x70))(pppuVar10,uStack_48);
ppiVar4 = (int **)FUN_100455d0();
(**(code **)(**ppiVar4 + 0x88))();
}
}
else {
LAB_1020750a:
pppuVar10 = local_34;
LAB_10207511:
FUN_10206e00(pppuVar10);
}
FUN_1000a6d0((void **)&local_54);
}
FUN_1000a6d0(local_34);
}
else {
iVar5 = FUN_103a6f00();
if (iVar5 < 1) {
puVar6 = FUN_103a6090(local_120,"audio_effect.cpp",0x110,0);
local_c = CONCAT31(local_c._1_3_,7);
bVar11 = true;
local_70 = 8;
ppppiVar9 = (int ****)local_6c;
if ((undefined4 ***)0x7 < local_58) {
ppppiVar9 = local_6c[0];
}
piVar7 = FUN_100124b0(puVar6 + 2,"SetAudioEffectParam failed. path not exist:");
FUN_103a6800(piVar7,(int **)ppppiVar9);
}
local_c = 3;
if (bVar11) {
FUN_103a63d0(local_120);
}
}
local_c = 0xffffffff;
FUN_10016280(local_6c);
goto LAB_102075a6;
}
iVar5 = FUN_103a6f00();
if (iVar5 < 1) {
puVar6 = FUN_103a6090(local_120,"audio_effect.cpp",0xfa,0);
local_c = 2;
local_70 = 4;
FUN_100124b0(puVar6 + 2,"Set AudioEffect Failed. Error Type.");
}
bVar11 = iVar5 >= 1;
}
local_c = 0xffffffff;
if (!bVar11) {
FUN_103a63d0(local_120);
}
LAB_102075a6:
ExceptionList = local_14;
FUN_10704e34(local_1c ^ (uint)auStack_16c);
return;
} Sanity check带来大量的if-else嵌套,具体表现为“iVar5, puVar6, local_c, local_70, FUN_100124b0, bVar11”参数链。将sanity check简化掉,提取核心代码:
local_c = 0xffffffff;
puStack_10 = &LAB_10832245;
local_14 = ExceptionList;
local_1c = DAT_10a76ad0 ^ (uint)auStack_16c;
uVar3 = DAT_10a76ad0 ^ (uint)&stack0xfffffe88;
ExceptionList = &local_14;
bVar11 = false;
local_70 = 0;
FUN_100161e0((undefined2 *)local_6c);
local_c = 3;
if (param_2 == 1) {
cVar2 = FUN_103a94b0(2,local_6c);
if (cVar2 != '\0') {
ppiVar4 = (int **)FUN_103a40d0((undefined2 *)local_150,param_3);
local_c = CONCAT31(local_c._1_3_,4);
FUN_103a3ed0(local_6c,ppiVar4);
ppvVar8 = local_150;
local_c = CONCAT31(local_c._1_3_,3);
FUN_10016280(ppvVar8);
}
}
else if (param_2 == 2) {
cVar2 = FUN_1006a790(0,(int **)local_6c);
if (cVar2 != '\0') {
ppiVar4 = (int **)FUN_103a40d0((undefined2 *)local_138,param_3);
local_c = CONCAT31(local_c._1_3_,5);
FUN_103a3ed0(local_6c,ppiVar4);
ppvVar8 = local_138;
local_c = CONCAT31(local_c._1_3_,3);
FUN_10016280(ppvVar8);
}
}
else if (param_2 == 3) {
ppiVar4 = (int **)FUN_103a3e10(local_168,param_3);
local_c = CONCAT31(local_c._1_3_,6);
FUN_103a3ed0(local_6c,ppiVar4);
ppvVar8 = local_168;
local_c = CONCAT31(local_c._1_3_,3);
FUN_10016280(ppvVar8);
}
bVar1 = FUN_103ab6a0(local_6c);
local_20 = 0xf;
local_24 = 0;
local_34[0] = (undefined4 ***)((uint)local_34[0] & 0xffffff00);
local_c._0_1_ = 8;
cVar2 = FUN_103ac270(local_6c,local_34);
local_40 = 0xf;
local_44 = 0;
local_54 = 0;
local_c = CONCAT31(local_c._1_3_,10);
pppuVar10 = local_34;
if (0xf < local_20) {
pppuVar10 = local_34[0];
}
local_70 = 1;
iVar5 = FUN_1015be70((char *)pppuVar10,local_24,(int **)&local_54,&local_70);
if (local_70 == 1) {
pppuVar10 = (undefined4 ***)&local_54;
goto LAB_10207511;
}
if (local_70 != 2) goto LAB_1020750a;
local_70 = 0;
local_38 = 0;
local_3c = 0;
FUN_107c3e80(&local_70,&local_38,&local_3c);
if ((param_1 == 1) && (5 < (int)local_70)) {
ppiVar4 = (int **)FUN_100455d0();
(**(code **)(**ppiVar4 + 0x84))();
ppiVar4 = (int **)FUN_100455d0();
(**(code **)(**ppiVar4 + 0x74))(1);
ppiVar4 = (int **)FUN_100455d0();
pppuVar10 = &local_58;
if (0xf < local_44) {
pppuVar10 = local_58;
}
(**(code **)(**ppiVar4 + 0x78))(pppuVar10,uStack_48);
ppiVar4 = (int **)FUN_100455d0();
(**(code **)(**ppiVar4 + 0x88))();
}
else {
ppiVar4 = (int **)FUN_100455d0();
(**(code **)(**ppiVar4 + 0x84))();
ppiVar4 = (int **)FUN_100455d0();
(**(code **)(**ppiVar4 + 0x6c))(1);
ppiVar4 = (int **)FUN_100455d0();
pppuVar10 = &local_58;
if (0xf < local_44) {
pppuVar10 = local_58;
}
(**(code **)(**ppiVar4 + 0x70))(pppuVar10,uStack_48);
ppiVar4 = (int **)FUN_100455d0();
(**(code **)(**ppiVar4 + 0x88))();
} 逆向讲究抓大放小,读代码要读“走势”。文件名放入local_6c后,若FUN_103ac270返回NULL,则报配置文件读取错误:
/* WARNING: Function: __alloca_probe replaced with injection: alloca_probe */
void __cdecl FUN_103ac270(undefined4 *param_1,void *param_2)
{
char cVar1;
uint uVar2;
FILE *_File;
uint uVar3;
int *apiStack_10008 [16385];
uVar2 = DAT_10a76ad0 ^ (uint)&stack0xfffffffc;
cVar1 = FUN_103a5a60(param_1);
if ((cVar1 == '\0') &&
(_File = (FILE *)FUN_103ab5d0(param_1,(int **)&DAT_109d7320), _File != (FILE *)0x0)) {
uVar3 = _fread(apiStack_10008,1,0x10000,_File);
while (uVar3 != 0) {
if (param_2 != (void *)0x0) {
FUN_10015950(param_2,apiStack_10008,uVar3);
}
uVar3 = _fread(apiStack_10008,1,0x10000,_File);
}
_fclose(_File);
FUN_10704e34(uVar2 ^ (uint)&stack0xfffffffc);
return;
}
FUN_10704e34(uVar2 ^ (uint)&stack0xfffffffc);
return;
} 果然是文件IO,其中FUN_103ab5d0大概负责读取,FUN_10015950负责复制内容进栈(未被识别出的kernel API call似乎都被编译进100开头的低位地址块)。

解密算法结构
FUN_1015bb40 (15bad0, 00a8e0)
FUN_1015bde0 (159ae0, 159bf0)
FUN_1015bbe0 (6e1370, 6dfc00, 015a80, 00abe0, 6e1320)
local_34的文件内容随后被传入pppuVar10,然后是FUN_1015be70,推测为顶层的解密函数:
void __fastcall FUN_1015be70(char *param_1,uint param_2,int **param_3,uint *param_4)
{
uint uVar1;
int iVar2;
undefined4 ****ppppuVar3;
uint ****ppppuVar4;
undefined4 ****local_44 [4];
int local_34;
uint local_30;
uint ****local_2c [4];
uint local_1c;
uint local_18;
uint local_14;
void *local_10;
undefined *puStack_c;
undefined4 local_8;
puStack_c = &LAB_1081b5d0;
local_10 = ExceptionList;
local_14 = DAT_10a76ad0 ^ (uint)&stack0xfffffffc;
ExceptionList = &local_10;
local_30 = 0xf;
local_34 = 0;
local_44[0] = (undefined4 ****)((uint)local_44[0] & 0xffffff00);
local_18 = 0xf;
local_1c = 0;
local_2c[0] = (uint ****)((uint)local_2c[0] & 0xffffff00);
local_8 = 1;
if ((((param_1 != (char *)0x0) && (param_2 != 0)) &&
(iVar2 = FUN_1015bb40(param_1,param_2,local_44,local_2c,param_4), uVar1 = local_1c, iVar2 == 0
)) && (param_3 != (int **)0x0)) {
ppppuVar4 = (uint ****)local_2c;
if (0xf < local_18) {
ppppuVar4 = local_2c[0];
}
ppppuVar3 = local_44;
if (0xf < local_30) {
ppppuVar3 = local_44[0];
}
iVar2 = FUN_1015bde0((int)ppppuVar3,local_34,(byte *)ppppuVar4,local_1c);
if (iVar2 == 0) {
FUN_1015bbe0((uint *)ppppuVar4,uVar1,param_3);
}
}
if (0xf < local_18) {
FID_conflict__free(local_2c[0]);
}
local_18 = 0xf;
local_1c = 0;
local_2c[0] = (uint ****)((uint)local_2c[0] & 0xffffff00);
if (0xf < local_30) {
FID_conflict__free(local_44[0]);
}
ExceptionList = local_10;
FUN_10704e34(local_14 ^ (uint)&stack0xfffffffc);
return;
} 其中param_1为加密文件内容,param_2为长度,另两个参数暂时未知。(之后得知pppuVar3、pppuVar4分别为密码区和数据区的指针。)进一步探索15bb40、15bde0、15bbe0三个子函数:
undefined4 __fastcall
FUN_1015bb40(char *param_1,uint param_2,void *param_3,void *param_4,uint *param_5)
{
void **ppvVar1;
int iVar2;
uint uVar3;
if (param_2 < 0x12) {
return 0xffffffff;
}
if ((((*param_1 == 'N') && (param_1[1] == 'C')) && (param_1[2] == 'A')) && (param_1[3] == 'E')) {
ppvVar1 = *(void ***)(param_1 + 4);
uVar3 = (uint)(byte)param_1[0x10];
if (param_2 != uVar3 + 0x11 + (int)ppvVar1) {
return 0xfffffffd;
}
if (param_3 != (void *)0x0) {
iVar2 = FUN_1015bad0((int)(param_1 + 0x11),uVar3,param_3);
if (iVar2 != 0) {
return 0xfffffffc;
}
}
if (param_5 != (uint *)0x0) {
*param_5 = (uint)*(ushort *)(param_1 + 0xe);
}
if (param_4 != (void *)0x0) {
FUN_1000a8e0(param_4,(int **)(param_1 + uVar3 + 0x11),ppvVar1);
}
return 0;
}
return 0xfffffffe;
} 很好,发现文件头!每个参数分别为param_2文件总长度、ppuVar1数据区长度、uVar3密码区长度(因为param_2 = uVar3 + 0x11 + ppuVar1)。返回错误码-1、-2、-3、-4分别表示文件过短、文件头不匹配、文件长度不匹配、以及FUN_1015bad0返回非0——这是什么?
undefined4 __fastcall FUN_1015bad0(int param_1,uint param_2,void *param_3)
{
byte bVar1;
uint uVar2;
byte local_5;
if ((param_2 - 1 & 3) == 0) {
bVar1 = *(byte *)(param_1 + 4);
uVar2 = 0;
if (param_2 != 0) {
do {
if ((uVar2 != 4) && (local_5 = *(byte *)(uVar2 + param_1) ^ bVar1, param_3 != (void *)0x0))
{
FUN_10015950(param_3,(int **)&local_5,1);
}
uVar2 = uVar2 + 1;
} while (uVar2 < param_2);
}
return 0;
}
return 0xffffffff;
} 原来错误码-4表示密码区长度param_2(也即父函数中的uVar3)不合法,必须被4除余1。Ghidra的输出有点绕,其实主体部分的算法与以下等效:
for (uint i = 0; i < len; i++) {
if (i == 4) continue;
byte b = src[i] ^ src[4];
memcpy(dest, (int **)&b, 1);
}
return 0; 原来密码区要去掉第4字节后,与第4字节取异或,才得到真正的密码区。

解密算法(续)及其简化
密码区传回15bb40的param_3,然后是15be70的local_44和pppuVar3,最后传入FUN_1015bde0的param_1:
undefined4 __fastcall FUN_1015bde0(int param_1,int param_2,byte *param_3,uint param_4)
{
uint *this;
int iVar1;
this = (uint *)FUN_10705677(0x100);
if (this == (uint *)0x0) {
return 0xffffffff;
}
FUN_10709e90(this,0,0x100);
iVar1 = FUN_10159ae0(this,param_1,param_2);
if (iVar1 != 0) {
FID_conflict__free(this);
return 0xfffffffe;
}
FUN_10159bf0(this,param_3,param_3,0,0,param_4);
FID_conflict__free(this);
return 0;
} 不错,短小精悍。密码区连同其长度param_2传入FUN_10159ae0,似乎是在填充刚刚malloc(256)的this数组。(Ghidra把复用ecx寄存器而不推入调用栈的参数自动命名为this,与结构体无关)
undefined4 __thiscall FUN_10159ae0(void *this,int param_1,int param_2)
{
byte bVar1;
uint uVar2;
byte *pbVar3;
int iVar4;
int iVar5;
int local_10;
uint local_c;
local_c = 0;
uVar2 = 0;
do {
*(char *)(uVar2 + (int)this) = (char)uVar2;
uVar2 = uVar2 + 1;
} while (uVar2 < 0x100);
pbVar3 = (byte *)((int)this + 1);
local_10 = 0x40;
iVar4 = 0;
do {
bVar1 = pbVar3[-1];
iVar5 = iVar4 + 1;
uVar2 = (uint)bVar1 + *(byte *)(iVar4 + param_1) + local_c & 0xff;
if (iVar5 == param_2) {
iVar5 = 0;
}
pbVar3[-1] = *(byte *)(uVar2 + (int)this);
*(byte *)(uVar2 + (int)this) = bVar1;
bVar1 = *pbVar3;
iVar4 = iVar5 + 1;
uVar2 = (uint)bVar1 + *(byte *)(iVar5 + param_1) + uVar2 & 0xff;
if (iVar4 == param_2) {
iVar4 = 0;
}
*pbVar3 = *(byte *)(uVar2 + (int)this);
*(byte *)(uVar2 + (int)this) = bVar1;
bVar1 = pbVar3[1];
iVar5 = iVar4 + 1;
uVar2 = (uint)bVar1 + *(byte *)(iVar4 + param_1) + uVar2 & 0xff;
if (iVar5 == param_2) {
iVar5 = 0;
}
pbVar3[1] = *(byte *)(uVar2 + (int)this);
*(byte *)(uVar2 + (int)this) = bVar1;
bVar1 = pbVar3[2];
iVar4 = iVar5 + 1;
local_c = (uint)bVar1 + *(byte *)(iVar5 + param_1) + uVar2 & 0xff;
if (iVar4 == param_2) {
iVar4 = 0;
}
pbVar3[2] = *(byte *)(local_c + (int)this);
*(byte *)(local_c + (int)this) = bVar1;
pbVar3 = pbVar3 + 4;
local_10 = local_10 + -1;
} while (local_10 != 0);
return 0;
} this首先在第一个do-while中被填充0~255,然后在第二个循环中在bVar1、iVar4/5、uVar2/local_c、pbVar3等参数间反复传递64次。但不难发现iVar4和iVar5是等效的,每次都互相赋值为对方+1;uVar2和local_c也是等效的。故主体循环可以压缩为:
pbVar3 = (byte *)(int)this;
local_10 = 0x100;
iVar = 0;
do {
bVar1 = *pbVar3;
iVar = iVar + 1;
uVar2 = (uint)bVar1 + *(byte *)(iVar + param_1) + uVar2 & 0xff;
if (iVar == param_2) {
iVar = 0;
}
*pbVar3 = *(byte *)(uVar2 + (int)this);
*(byte *)(uVar2 + (int)this) = bVar1;
pbVar3 = pbVar3 + 1;
local_10 = local_10 + -1;
} while (local_10 != 0); 重写为for loop,指针重写为更清晰的数组索引形式:
iVar = 0;
for (uint j = 0; j < 0x100; j++) {
bVar1 = this[j];
uVar2 = bVar1 + param_1[iVar] + uVar2 & 0xff;
iVar++;
if (iVar == param_2) iVar = 0;
this[iVar] = this[uVar2];
this[uVar2] = bVar1;
} 最后注意到iVar的变换规则其实是在0~param_2-1之间循环,重写为余除形式,同时加入this的初始化循环,得到FUN_10159ae0的完整算法:
for (uint j = 0; j < 0x100; j++) {
this[j] = j;
}
for (uint j = 0; j < 0x100; j++) {
bVar1 = this[j];
uVar2 = bVar1 + param_1[j % param_2] + uVar2 & 0xff;
this[j] = this[uVar2];
this[uVar2] = bVar1;
} 代码量一下少了80%!所以把逻辑分析清楚是非常实用的。在从密码区获得this后,这个数组连同param_3和param_4一起被传入FUN_10159bf0:
void __thiscall
FUN_10159bf0(void *this,byte *param_1,byte *param_2,uint param_3,byte *param_4,uint param_5)
{
byte *pbVar1;
uint uVar2;
byte bVar3;
char cVar4;
byte bVar5;
byte *pbVar6;
int iVar7;
pbVar6 = param_2;
bVar5 = (byte)param_3;
if (param_5 >> 3 == 0) {
param_4 = param_2;
param_2 = param_1;
}
else {
param_2 = param_1;
uVar2 = param_5 >> 3;
do {
param_4 = (byte *)uVar2;
cVar4 = (char)param_3;
bVar5 = *(byte *)((uint)(byte)(cVar4 + 1U) + (int)this);
*pbVar6 = *(byte *)(((uint)*(byte *)((uint)(byte)(cVar4 + 1U + bVar5) + (int)this) +
(uint)bVar5 & 0xff) + (int)this) ^ *param_2;
bVar5 = *(byte *)((uint)(byte)(cVar4 + 2U) + (int)this);
pbVar6[1] = *(byte *)(((uint)*(byte *)((uint)(byte)(cVar4 + 2U + bVar5) + (int)this) +
(uint)bVar5 & 0xff) + (int)this) ^ param_2[1];
bVar5 = *(byte *)((uint)(byte)(cVar4 + 3U) + (int)this);
pbVar6[2] = *(byte *)(((uint)*(byte *)((uint)(byte)(cVar4 + 3U + bVar5) + (int)this) +
(uint)bVar5 & 0xff) + (int)this) ^ param_2[2];
bVar5 = *(byte *)((uint)(byte)(cVar4 + 4U) + (int)this);
pbVar6[3] = *(byte *)(((uint)*(byte *)((uint)(byte)(cVar4 + 4U + bVar5) + (int)this) +
(uint)bVar5 & 0xff) + (int)this) ^ param_2[3];
bVar5 = *(byte *)((uint)(byte)(cVar4 + 5U) + (int)this);
pbVar6[4] = *(byte *)(((uint)*(byte *)((uint)(byte)(cVar4 + 5U + bVar5) + (int)this) +
(uint)bVar5 & 0xff) + (int)this) ^ param_2[4];
bVar5 = *(byte *)((uint)(byte)(cVar4 + 6U) + (int)this);
pbVar6[5] = *(byte *)(((uint)*(byte *)((uint)(byte)(cVar4 + 6U + bVar5) + (int)this) +
(uint)bVar5 & 0xff) + (int)this) ^ param_2[5];
bVar3 = *(byte *)((uint)(byte)(cVar4 + 7U) + (int)this);
bVar5 = cVar4 + 8;
param_3 = (uint)bVar5;
pbVar6[6] = *(byte *)(((uint)*(byte *)((uint)(byte)(cVar4 + 7U + bVar3) + (int)this) +
(uint)bVar3 & 0xff) + (int)this) ^ param_2[6];
pbVar1 = param_2 + 7;
param_2 = param_2 + 8;
pbVar6[7] = *(byte *)(((uint)*(byte *)((param_3 + *(byte *)(param_3 + (int)this) & 0xff) +
(int)this) + (uint)*(byte *)(param_3 + (int)this) & 0xff
) + (int)this) ^ *pbVar1;
pbVar6 = pbVar6 + 8;
uVar2 = (int)param_4 - 1;
param_4 = pbVar6;
} while (uVar2 != 0);
}
if ((param_5 & 7) != 0) {
bVar5 = bVar5 + 1;
*param_4 = *(byte *)(((uint)*(byte *)((uint)(byte)(bVar5 + *(byte *)((uint)bVar5 + (int)this)) +
(int)this) + (uint)*(byte *)((uint)bVar5 + (int)this) &
0xff) + (int)this) ^ *param_2;
for (iVar7 = (param_5 & 7) - 1;
(((iVar7 != 0 &&
(bVar3 = *(byte *)((uint)(byte)(bVar5 + 1) + (int)this),
param_4[1] = *(byte *)(((uint)*(byte *)((uint)(byte)(bVar5 + 1 + bVar3) + (int)this) +
(uint)bVar3 & 0xff) + (int)this) ^ param_2[1], iVar7 != 1)) &&
(bVar3 = *(byte *)((uint)(byte)(bVar5 + 2) + (int)this),
param_4[2] = *(byte *)(((uint)*(byte *)((uint)(byte)(bVar5 + 2 + bVar3) + (int)this) +
(uint)bVar3 & 0xff) + (int)this) ^ param_2[2], iVar7 != 2)) &&
(((bVar3 = *(byte *)((uint)(byte)(bVar5 + 3) + (int)this),
param_4[3] = *(byte *)(((uint)*(byte *)((uint)(byte)(bVar5 + 3 + bVar3) + (int)this) +
(uint)bVar3 & 0xff) + (int)this) ^ param_2[3], iVar7 != 3 &&
(bVar3 = *(byte *)((uint)(byte)(bVar5 + 4) + (int)this),
param_4[4] = *(byte *)(((uint)*(byte *)((uint)(byte)(bVar5 + 4 + bVar3) + (int)this) +
(uint)bVar3 & 0xff) + (int)this) ^ param_2[4], iVar7 != 4)) &&
((bVar3 = *(byte *)((uint)(byte)(bVar5 + 5) + (int)this),
param_4[5] = *(byte *)(((uint)*(byte *)((uint)(byte)(bVar5 + 5 + bVar3) + (int)this) +
(uint)bVar3 & 0xff) + (int)this) ^ param_2[5], iVar7 != 5 &&
(bVar3 = *(byte *)((uint)(byte)(bVar5 + 6) + (int)this),
param_4[6] = *(byte *)(((uint)*(byte *)((uint)(byte)(bVar5 + 6 + bVar3) + (int)this) +
(uint)bVar3 & 0xff) + (int)this) ^ param_2[6], iVar7 != 6))))));
iVar7 = iVar7 + -7) {
bVar5 = bVar5 + 7;
*param_4 = *(byte *)(((uint)*(byte *)((uint)(byte)(bVar5 + *(byte *)((uint)bVar5 + (int)this))
+ (int)this) + (uint)*(byte *)((uint)bVar5 + (int)this) &
0xff) + (int)this) ^ *param_2;
}
}
return;
} 呜哇!指针套指针密密麻麻。来段整理过格式的表达式体会下——
*pbVar6 = *(byte *)(
(
(uint)*(byte *)(
(uint)(byte)(cVar4 + 1U + bVar5) + (int)this
) + (uint)bVar5 & 0xff
) + (int)this
) ^ *param_2; 莫慌,一步步简化。注意到if ((param_5 & 7) != 0)之后大概率是对edge case的收尾工作,故关注主体的do-while循环。首先去掉所有的强制格式转换:
do {
param_4 = uVar2;
cVar4 = param_3;
bVar5 = *((cVar4 + 1U) + this);
*pbVar6 = *((*((cVar4 + 1U + bVar5) + this) + bVar5 & 0xff) + this) ^ *param_2;
bVar5 = *((cVar4 + 2U) + this);
pbVar6[1] = *((*((cVar4 + 2U + bVar5) + this) + bVar5 & 0xff) + this) ^ param_2[1];
bVar5 = *((cVar4 + 3U) + this);
pbVar6[2] = *((*((cVar4 + 3U + bVar5) + this) + bVar5 & 0xff) + this) ^ param_2[2];
bVar5 = *((cVar4 + 4U) + this);
pbVar6[3] = *((*((cVar4 + 4U + bVar5) + this) + bVar5 & 0xff) + this) ^ param_2[3];
bVar5 = *((cVar4 + 5U) + this);
pbVar6[4] = *((*((cVar4 + 5U + bVar5) + this) + bVar5 & 0xff) + this) ^ param_2[4];
bVar5 = *((cVar4 + 6U) + this);
pbVar6[5] = *((*((cVar4 + 6U + bVar5) + this) + bVar5 & 0xff) + this) ^ param_2[5];
bVar3 = *((cVar4 + 7U) + this);
bVar5 = cVar4 + 8;
param_3 = bVar5;
pbVar6[6] = *((*((cVar4 + 7U + bVar3) + this) + bVar3 & 0xff) + this) ^ param_2[6];
pbVar1 = param_2 + 7;
param_2 = param_2 + 8;
pbVar6[7] = *((*((param_3 + *(param_3 + this) & 0xff) + this) + *(param_3 + this) & 0xff) + this) ^ *pbVar1;
pbVar6 = pbVar6 + 8;
uVar2 = param_4 - 1;
param_4 = pbVar6;
} while (uVar2 != 0); 然后是把指针重写为数组索引形式,所有的*(idx + this)等效于this[idx]:
do {
param_4 = uVar2;
cVar4 = param_3;
bVar5 = this[cVar4 + 1U];
pbVar6[0] = this[this[cVar4 + 1U + bVar5] + bVar5 & 0xff] ^ param_2[0];
bVar5 = this[cVar4 + 2U];
pbVar6[1] = this[this[cVar4 + 2U + bVar5] + bVar5 & 0xff] ^ param_2[1];
bVar5 = this[cVar4 + 3U];
pbVar6[2] = this[this[cVar4 + 3U + bVar5] + bVar5 & 0xff] ^ param_2[2];
bVar5 = this[cVar4 + 4U];
pbVar6[3] = this[this[cVar4 + 4U + bVar5] + bVar5 & 0xff] ^ param_2[3];
bVar5 = this[cVar4 + 5U];
pbVar6[4] = this[this[cVar4 + 5U + bVar5] + bVar5 & 0xff] ^ param_2[4];
bVar5 = this[cVar4 + 6U];
pbVar6[5] = this[this[cVar4 + 6U + bVar5] + bVar5 & 0xff] ^ param_2[5];
bVar3 = this[cVar4 + 7U];
bVar5 = cVar4 + 8;
param_3 = bVar5;
pbVar6[6] = this[this[cVar4 + 7U + bVar3] + bVar3 & 0xff] ^ param_2[6];
pbVar1 = param_2 + 7;
param_2 = param_2 + 8;
pbVar6[7] = this[this[param_3 + this[param_3] & 0xff] + this[param_3] & 0xff] ^ *pbVar1;
pbVar6 = pbVar6 + 8;
uVar2 = param_4 - 1;
param_4 = pbVar6;
} while (uVar2 != 0); 这两把斧下来,代码工整了不是一星半点。注意到pbVar6等效于param_2,*pbVar1等效于param_2[7],bVar3等效于bVar5,cVar4等效于param_3,param_4等效于uVar2,统统简化掉:
do {
bVar5 = this[param_3 + 1U];
param_2[0] = this[this[param_3 + 1U + bVar5] + bVar5 & 0xff] ^ param_2[0];
bVar5 = this[param_3 + 2U];
param_2[1] = this[this[param_3 + 2U + bVar5] + bVar5 & 0xff] ^ param_2[1];
bVar5 = this[param_3 + 3U];
param_2[2] = this[this[param_3 + 3U + bVar5] + bVar5 & 0xff] ^ param_2[2];
bVar5 = this[param_3 + 4U];
param_2[3] = this[this[param_3 + 4U + bVar5] + bVar5 & 0xff] ^ param_2[3];
bVar5 = this[param_3 + 5U];
param_2[4] = this[this[param_3 + 5U + bVar5] + bVar5 & 0xff] ^ param_2[4];
bVar5 = this[param_3 + 6U];
param_2[5] = this[this[param_3 + 6U + bVar5] + bVar5 & 0xff] ^ param_2[5];
bVar5 = this[param_3 + 7U];
param_2[6] = this[this[param_3 + 7U + bVar5] + bVar5 & 0xff] ^ param_2[6];
bVar5 = this[param_3 + 8U];
param_2[7] = this[this[param_3 + 8U + bVar5] + bVar5 & 0xff] ^ param_2[7];
param_3 = param_3 + 8;
param_2 = param_2 + 8;
uVar2 = uVar2 - 1;
} while (uVar2 != 0); 至此表达式结构已统一,打包为循环:
while (uVar2-- != 0) {
for (uint i = 1; i <= 8; i++) {
bVar5 = this[param_3 + i];
param_2[i - 1] ^= this[this[param_3 + i + bVar5] + bVar5 & 0xff];
}
param_3 += 8;
param_2 += 8;
} 8字节为一单元,大概率是针对处理器pipeline的优化。重写循环,同时注意到父函数调用时设定了param_2为数据区指针,param_3为0,param_4为数据区长度:
for (uint i = 0; i < len; i++) {
bVar5 = this[i + 1];
data[i] ^= this[this[bVar5 + i + 1] + bVar5 & 0xff];
} 这就是这个长达九十余行的解密函数FUN_10159bf0,其中的完整算法。

解密算法示例
网易云的本地音质配置文件存储于以下地址:
C:\Users\%USERNAME%\AppData\Local\Netease\CloudMusic\audioeffect 以“360°环绕.ncae”为例,首先读取文件头:
4E 43 41 45 3D 00 00 00 00 00 00 00 01 00 01 00 0D 能看到‘NCAE’的魔数,0x3D=61的数据区长度,和0x0D=13的密码区长度,13 % 4 = 1,故密码区长度合法。紧跟着的是密码区key0:
6E AE 1A 67 38 9B 1D 66 5E 9D 79 38 04 以及数据区data:
82 52 97 D1 EC 37 8E B1 F7 27 7D B6 8F F5 9C 8D
D2 1D 31 02 56 F5 A1 0E D8 CF BE B6 B8 16 F9 84
95 BD E9 89 0B 14 D5 89 A1 66 4A 71 6D C8 47 E2
9D 8C 88 51 90 A1 9D BA 4B 4E 09 31 6B 去掉key0的第4个字节0x38,再将其余各字节与0x38取异或,得到真正的密码区key:
56 96 22 5F A3 25 5E 66 A5 41 00 3C 将key按照以下算法,作用于数组arr:
byte = 0
arr = np.arange(0x100, dtype=np.uint8)
for i in range(0x100):
byte = arr[i] + key[i % len(key)] + byte & 0xFF
arr[i], arr[byte] = arr[byte], arr[i] 进行256次元素交换后的arr为:
56 E2 11 73 78 35 A6 82 4F 1F 16 0E BC 70 8F FD
63 2C E7 72 79 BA 3C 64 8C 29 14 18 F3 DC 51 89
E3 0F 2A 7E F8 3E FB 81 4C 6A 4E 45 32 90 9B 8B
60 5E 67 5A 95 91 BB 2E 96 77 B1 A8 49 94 0A 00
28 B2 52 46 74 25 4B 02 0B 5B AA 75 EF DE 62 8D
2B FC A0 D6 58 E0 41 C3 99 D0 9E 98 06 03 AD 7A
05 CA 53 24 EC 7F A1 13 69 12 1C 3D 1E 84 F7 C5
31 26 F5 7C 93 7B 07 8E DA 36 DD F1 D9 FF A9 09
F2 85 D5 04 43 4A 3F 39 BE E1 C9 D8 AC E9 83 76
4D CE 15 34 EE A5 21 FA CB F0 6E 42 6D F9 88 D4
71 57 A2 D1 61 D7 01 D2 68 80 5C 6B DB 2F 65 E5
9D C1 A3 BD C8 47 EA C4 22 50 3A 87 E4 86 AE FE
9C 5D 97 59 AF 0C F6 BF C7 66 2D B9 E6 92 08 B0
1D 17 C6 EB 54 B6 6F B4 5F D3 1A CC DF 30 A7 A4
27 48 ED 37 8A 20 B8 B5 3B C2 CD 1B 6C 19 AB 44
F4 B7 33 9A 9F C0 23 10 CF E8 40 7D 55 B3 38 0D 再将arr按照以下算法,作用于data:
for j in range(len(data)):
byte = arr[(j + 1) & 0xFF]
data[j] ^= arr[arr[(byte + j + 1) & 0xFF] + byte & 0xFF] 进行异或解密后的data为:
AB 56 4A 2D 54 B2 AA 56 CA CF 53 B2 4A 4B 9C 8D
D2 1D 31 02 56 F5 A1 0E D8 CF BE B6 B8 16 F9 84
95 BD E9 89 0B 14 D5 89 A1 66 4A 71 6D C8 47 E2
9D 8C 88 51 90 A1 9D BA 4B 4E 09 31 6B

结论与下期预告
完整的Python解密代码如下:
import os
import numpy as np
translator = {
"迷幻电音" : "electronic",
"动感电音" : "electronic_plus",
"摇滚经典" : "rock",
"嘻哈音效" : "hiphop",
"纯净ACG" : "acg",
"民谣音效" : "folk",
"婉约古风" : "classical",
"音乐厅" : "concert",
"教堂混响" : "church",
"复古收音机": "radio",
"怀旧卡带机": "tape",
"HiFi电子管": "vacuumtube",
"极重低音" : "bass",
"超重低音" : "bass_plus",
"清澈人声" : "vocal",
"高解析人声": "vocal_plus",
"演唱会现场": "live",
"HiFi现场" : "live_plus",
"狂嗨LIVE" : "live_surr",
"LiveHouse现场": "livehouse",
"震撼全景" : "panorama",
"3D环绕" : "surr",
"360°环绕" : "surr_rotate",
"独享立体声": "stereo",
"环绕立体声": "stereo_surr",
"水晶立体声": "stereo_crystal",
"录音棚立体声": "stereo_studio",
"NINEONE#专属音效": "nineone",
"毛不易《小王》专属音效": "xiaowang",
"《不完美人生指南》专属音效": "bwmrszn"
}
toint = lambda x: int.from_bytes(x, byteorder='big')
if not os.path.exists('processed'):
os.makedirs('processed')
for file in os.listdir('raw'):
fin = open('raw/%s' % file, 'rb')
magic = fin.read(4)
assert magic == b'NCAE'
ldata = toint(fin.read(4))
fin.read(6) # unused
short = toint(fin.read(2))
lkey = toint(fin.read(1)) - 1
assert lkey % 4 == 0
key0 = bytearray(fin.read(lkey + 1))
key = np.array(key0[:4] + key0[5:]) ^ key0[4]
data = bytearray(fin.read(ldata))
fin.close()
byte = 0
arr = np.arange(0x100, dtype=np.uint8)
for i in range(0x100):
byte = arr[i] + key[i % lkey] + byte & 0xFF
arr[i], arr[byte] = arr[byte], arr[i]
for j in range(len(data)):
byte = arr[(j + 1) & 0xFF]
data[j] ^= arr[arr[(byte + j + 1) & 0xFF] + byte & 0xFF]
file = translator[file[:-5]] + '.bin'
fout = open('processed/%s' % file, 'wb')
fout.write(data)
fout.close() 解密出的配置文件,有些共享文件头:
AB56 4A2? 5?B2 AA56 CACF 53B2: classical, concert, bass, vocal, stereo, stereo_crystal, stereo_surr, surr, surr_rotate, bwmrszn
558B 310? 80?0 1004 FF72 F5?A: electronic, hiphop, live
558B 410A 8020 14?? EFF2 D7??: acg, folk
6D51 DB?? 8320 10FD 977? ????: electronic_plus, livehouse
6D91 ?B6E 8??0 10?? ?F65 ????: church, live_surr 另一些有自己的文件头:
0D57 7B58 8DDB 13DE 2ADD 5352: vacuumtube
0D94 693C 555D 03C5 A592 A141: radio
0D97 6740 4E5F 1CC7 DBA9 B487: live_plus
1598 6938 964F 1B87 4524 3B59: stereo_studio
1D57 0758 5447 D79E 72B7 2F5D: tape
1D93 096C 5545 1486 BF29 C596: vocal_plus
3D56 7D4C D655 147E 052D 8950: bass_plus
5DCC DD0A C230 0C05 E077 C975: rock
6559 0974 95C5 15FE 0730 8988: panorama
7D92 DD6E 8330 0C85 DFC5 D7A6: nineone
8592 C16E C230 0C86 DFC5 E704: xiaowang 这其中的格式,在被称作SetAudioEffectParam的FUN_1015bbe0中有所体现。这个函数有5个子函数,而且用到了函数指针表(vftable),算法大概会相当复杂。逆向还在进行中,这部分放到下期解密。