【逆向工程】网易云音乐音效模块解析(上).ncae文件解密
AllenHeartcore
编辑于 2024年02月12日 23:46
收录于文集
共4篇

工具

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。

cut-off

起点

cloudmusic.dll调用的许多次级dll中,有一个neaudioeffects.dll。原来一直找错了入口!次级调用的入口在0x10868F40的audiofx::AudioEffects::Create()函数指针,由此reference到FUN_10045380,再到FUN_100455d0,最终在FUN_10207050发现“audio effects”的字符串。将此作为逆向工程的起点:

代码块
clike
自动换行
复制代码
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简化掉,提取核心代码:

代码块
clike
自动换行
复制代码
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,则报配置文件读取错误:

代码块
clike
自动换行
复制代码
/* 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开头的低位地址块)。

cut-off

解密算法结构

  • FUN_1015bb40 (15bad0, 00a8e0)

  • FUN_1015bde0 (159ae0, 159bf0)

  • FUN_1015bbe0 (6e1370, 6dfc00, 015a80, 00abe0, 6e1320)

local_34的文件内容随后被传入pppuVar10,然后是FUN_1015be70,推测为顶层的解密函数:

代码块
clike
自动换行
复制代码
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三个子函数:

代码块
clike
自动换行
复制代码
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——这是什么?

代码块
clike
自动换行
复制代码
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的输出有点绕,其实主体部分的算法与以下等效:

代码块
clike
自动换行
复制代码
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字节取异或,才得到真正的密码区。

cut-off

解密算法(续)及其简化

密码区传回15bb40的param_3,然后是15be70的local_44和pppuVar3,最后传入FUN_1015bde0的param_1:

代码块
clike
自动换行
复制代码
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,与结构体无关)

代码块
clike
自动换行
复制代码
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也是等效的。故主体循环可以压缩为:

代码块
clike
自动换行
复制代码
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,指针重写为更清晰的数组索引形式:

代码块
clike
自动换行
复制代码
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的完整算法:

代码块
clike
自动换行
复制代码
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:

代码块
clike
自动换行
复制代码
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;
}
复制成功

呜哇!指针套指针密密麻麻。来段整理过格式的表达式体会下——

代码块
clike
自动换行
复制代码
*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循环。首先去掉所有的强制格式转换:

代码块
clike
自动换行
复制代码
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]:

代码块
clike
自动换行
复制代码
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,统统简化掉:

代码块
clike
自动换行
复制代码
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);
复制成功

至此表达式结构已统一,打包为循环:

代码块
clike
自动换行
复制代码
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为数据区长度:

代码块
clike
自动换行
复制代码
for (uint i = 0; i < len; i++) {
    bVar5 = this[i + 1];
    data[i] ^= this[this[bVar5 + i + 1] + bVar5 & 0xff];
}
复制成功

这就是这个长达九十余行的解密函数FUN_10159bf0,其中的完整算法。

cut-off

解密算法示例

网易云的本地音质配置文件存储于以下地址:

代码块
YAML
自动换行
复制代码
C:\Users\%USERNAME%\AppData\Local\Netease\CloudMusic\audioeffect
复制成功

以“360°环绕.ncae”为例,首先读取文件头:

代码块
YAML
自动换行
复制代码
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:

代码块
YAML
自动换行
复制代码
6E AE 1A 67 38 9B 1D 66 5E 9D 79 38 04
复制成功

以及数据区data:

代码块
YAML
自动换行
复制代码
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:

代码块
YAML
自动换行
复制代码
56 96 22 5F A3 25 5E 66 A5 41 00 3C
复制成功

将key按照以下算法,作用于数组arr:

代码块
Python
自动换行
复制代码
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为:

代码块
YAML
自动换行
复制代码
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:

代码块
Python
自动换行
复制代码
for j in range(len(data)):
    byte = arr[(j + 1) & 0xFF]
    data[j] ^= arr[arr[(byte + j + 1) & 0xFF] + byte & 0xFF]
复制成功

进行异或解密后的data为:

代码块
YAML
自动换行
复制代码
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
复制成功

cut-off

结论与下期预告

完整的Python解密代码如下:

代码块
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()
复制成功

解密出的配置文件,有些共享文件头:

代码块
YAML
自动换行
复制代码
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
复制成功

另一些有自己的文件头:

代码块
YAML
自动换行
复制代码
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),算法大概会相当复杂。逆向还在进行中,这部分放到下期解密。