[转载] BH3解密三件套

文章来源:https://blog.palug.cn/2460.html

https://blog.palug.cn/2659.html

https://blog.palug.cn/2728.html

[Honkai 3rd]v3.6.1资源加密分析(前)

又是崩坏3ᖗ( ᐛ )ᖘ

中篇: [Honkai 3rd]v3.8.0资源加密分析(中)

3.5.0版本miHoYo开始加密资源文件,当时正处于考试周加上被ban了一个月所以暂时弃坑,现在重新捡起来。
直接说重点了,首先定位到关键函数CreateDecompressor可以发现传入参数a1即为flag,位于文件0x2E-0x31的四字节整数

switch ( a1 )
  {
    case 0:
      break;
    case 1:
      v2 = operator new(4, a2, 16, nullptr, 55);
      *v2 = &`vtable for'Lz4Decompressor;
      break;
    case 2:
    case 3:
      v2 = operator new(4, a2, 16, nullptr, 52);
      *v2 = &`vtable for'LzmaDecompressor;
      break;
    case 5:
      v2 = operator new(4, a2, 16, nullptr, 59);
      *v2 = &`vtable for'Lz4DecompressorWithDecryption;
      break;

具体代码可以看这里BundleFile.cs#L139
不过这里和AssetStudio有点不一样,mhy给自定义加密设置的flag是0x45,所以解密时会走分支5,区别如下

原版

int Lz4Decompressor::DecompressMemory(Lz4Decompressor *this, void* buffer, int* compressSizePtr, void *decodeBuffer, int* decompressSizePtr)
{
  compressSize = *compressSizePtr;
  decompressSize = *decompressSizePtr;
  result = 0;
  if ( compressSize <= 0x7FFFFFFE && decompressSize <= 0x7FFFFFFE )
  {
    return_decompressSize = UNITY_LZ4_decompress_safe(buffer, decodeBuffer, compressSize, decompressSize);
    result = 0;
    *decompressSizePtr = return_decompressSize;
    if ( return_decompressSize > 0 )
      result = 1;
  }
  return result;
}

自定义版

int Lz4Decompressor::DecompressMemoryWithDecrypt(Lz4Decompressor *this, void* buffer, int* compressSizePtr, void *decodeBuffer, int* decompressSizePtr)
{
  compressSize = *compressSizePtr;
  decompressSize = *decompressSizePtr;
  if ( compressSize > 0xFF )
  {
    buffer = DecryptFunc(buffer, &compressSize);
    result = 0;
    if ( !buffer || compressSize > 0x7FFFFFFE )
      return result;
    
  }
  else
  {
    result = 0;
  }
  if ( decompressSize <= 0x7FFFFFFE )
  {
    return_decompressSize = UNITY_LZ4_decompress_safe(buffer, decodeBuffer, compressSize, decompressSize);
    result = 0;
    *decompressSizePtr = return_decompressSize;
    if ( return_decompressSize > 0 )
      result = 1;
  }
  return result;
}

可以看到唯一区别就是在自定义版本中数据还要经过DecryptFunc这个函数,当然已经被混淆了,伪代码大概1691行
资源文件分只加密header还会加密chunk两种情况。后者在数据头部前还有0x14字节mr0k为开头的数据。最终会调用UNITY_LZ4_decompress_safe解压

只加密header

在恢复文件头部几个关键数据就可以直接用AssetStudio打开
2020-04-03 14:59:25
补图虽然不是同一个文件

还会加密chunk

分3中情况

  • 所有chunk全加密 (像excel_output)
  • 只加密第一个chunk
  • 只加密第二个chunk

反正对着AssetStudio的源代码抄就行,判断一下前4个字节是不是0x6D72306B,是的话走解密,不是就跳过。

[Honkai 3rd]v3.8.0资源加密分析(中)

前篇: [Honkai 3rd]v3.6.1资源加密分析(前)
后篇: [Honkai 3rd]v4.0.0资源加密算法还原(后)

这次miHoYo把所有资源文件打包到wmv文件中,很简单就不多说了,直接对着class BlockFileInfo{}抄一遍。
不过我倒是找到了Header部分的加密算法,位于ArchiveStorageReader::ReadHeader函数下

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int DecryptHeader(unsigned char* EncryptdNum, uint64_t key, unsigned int* keyArray) {
  uint64_t v12 = 4 * key - 0x61C8864E7A143579;
  for (int i = 0; i < 4; ++i) {
    uint64_t tmp = keyArray[0] ^ (keyArray[0] << 11) ^ ((keyArray[0] ^ (keyArray[0] << 11)) >> 8) ^ keyArray[3] ^ (keyArray[3] >> 19);
    keyArray[0] = keyArray[1];
    keyArray[1] = keyArray[2];
    keyArray[2] = keyArray[3];
    keyArray[3] = tmp;
    *(EncryptdNum + i) ^= *(unsigned char*)((uint64_t)&v12 | (tmp & 7));
  }
  return 1;
}

int ReadHeader_CUSTOM(/*void* buffer, int CompressSize*/) {
  /*
  Flags: 45
  BundleSize: 3AC76
  DecompressSize: 97
  CompressSize: 68
  */
  
  unsigned int Key = 0x16B114C;
  unsigned int Flag = 0xB7989872;
  unsigned int DecompressSize = 0xB1B1B720;
  unsigned int CompressSize = 0xFB10FE3;
  int64_t bundleSize = 0x447937B137E4F841;

  unsigned int KeyArray[4];
  KeyArray[0] = Key;
  KeyArray[1] = 0x6C078965 * KeyArray[0] + 1;
  KeyArray[2] = 0x6C078965 * KeyArray[1] + 1;
  KeyArray[3] = 0x6C078965 * KeyArray[2] + 1;

  if (DecryptHeader((unsigned char*)&Flag, Key, &KeyArray[0]) == 1) {
    printf("Flags: %X\n", Flag);
    if (Key) {
      int64_t DecryptBundleSizeKey = 8 * Key - 0x61C8864E7A143579;
      unsigned int tmp = 0;
      for (int i = 0; i < 8; ++i) {
        tmp = KeyArray[0] ^ (KeyArray[0] << 11);
        KeyArray[0] = KeyArray[1];
        KeyArray[1] = KeyArray[2];
        KeyArray[2] = KeyArray[3];
        KeyArray[3] ^= tmp ^ (tmp >> 8) ^ (KeyArray[3] >> 19);
        *(((unsigned char*)&bundleSize) + i) ^= *(unsigned char*)((int64_t)&DecryptBundleSizeKey | (KeyArray[3] & 7));
      }
      printf("BundleSize: %X\n", (unsigned int)bundleSize);
    }
    DecryptHeader((unsigned char*)&DecompressSize, Key, &KeyArray[0]);
    DecryptHeader((unsigned char*)&CompressSize, Key, &KeyArray[0]);
    printf("DecompressSize: %X\nCompressSize: %X\n", DecompressSize, CompressSize);
  }
  return 1;
}

int main(void) {
  printf("Hello World\n");
  ReadHeader_CUSTOM();
  return 0;
}


随便找一个只加密了header的文件验证一下

扔进去跑出结果

修复文件头


正常打开,完成
buffer部分的解密函数有ollvm混淆,本来准备写ollvm反混淆从入门到跑路来着,但是这次我下载了桌面版,所以事情就变得有趣起来
直接复制目录下UnityPlayer.dll文件路径,调用LoadLibrary加载它,初始化函数后就可以正常调用,觉了
还是拿上一篇文章的例子,可以看到和dump出来的数据一样


就这,最复杂的部分反而是最简单的,不知道为什么鹅厂的虚拟机保护怎么没有把这部分也虚拟化。

[Honkai 3rd]v4.0.0资源加密算法还原(后)

应该是最后一篇了

前篇: [Honkai 3rd]v3.6.1资源加密分析(前)
中篇: [Honkai 3rd]v3.8.0资源加密分析(中)

重新学习清除混淆需要的时间太长,所以最后是直接手撕了。因为是标准的ollvm混淆,所以在调试过程中还算简单,就是很精污。

指令替换

加法混淆

这个被ida pro的反编译器直接优化了,但还是举个例子

rax + rbx = rax - rbp + rbx + rbp
mov     rbp, 0E4F1996E191D8FB0h
sub     rbx, rbp
add     rbx, rax
mov     rax, [rsp+28h]
mov     rsi, [rsp+28h]
shrd    rsi, rax, 33h
add     rbx, rbp

似乎还有另一种情况,在某个每次i+4的循环中

v60 + 1
v60 & 1 | v60 ^ 1

减法混淆也是一样,不举例了。xor混淆

直接就是异或公式(~A & B) | (A & ~B)

v214 = v213 ^ v212
v214 = (~v213 & 0xDB | v213 & 0x24) ^ (~v212 & 0xDB | v212 & 0x24)

and混淆

需要先把xor混淆去掉

v41 = ptr[i] & 0x80000000;
v41 = ptr[i] & (ptr[i] ^ 0x7FFFFFFF);

or混淆

也是要先清除xor混淆

v44 = (B | A) >> 1 ^ ptr[i + 0x9C];
v44 = ((B & A | B ^ A) >> 1) ^ ptr[i + 0x9C];

这几个。不过我觉得还原算法时就算照抄混淆后的伪代码,编译器也会优化回去。
控制流平坦化伪造控制流没办法了,硬怼( ゚∀。)
总之经过快一周的分析之后,得出解密流程如下:

// #####################################################################################
// 计算HashOutput
// #####################################################################################
// 第一步,初始化TheLastOfKeyArray
InitializationTheLastOfKeyArray(&TheLastOfKeyArray);
// 第二步,初始化InitVector
for(int i = 0; i < 0x80; ++i) {
    InitVector[i] = TheLastOfKeyArrayCalc(&TheLastOfKeyArray);
}
// 第三步,初始化HashInput
for(int i = 0; i < 4; ++i) {
    HashInput[i] = TheLastOfKeyArrayCalc(&TheLastOfKeyArray);
}
// 第四步,产生5个hash
HashCalc(&HashOutput, &HashInput);
// #####################################################################################
// 结束
// #####################################################################################
// 第五步,第一次初始化
padding_key(HashOutput, mr0k_key, DecryptionKeyInitVector1, buffer);
// 第六步,循环0x10次
for(int i = 0; i < 0x10; ++i) {
    mr0k_key[i] = DecryptionKeyInitVector1[i] ^ DecryptionKeyInitVector2[i];
}
// 第七步,第二次初始化
padding_key(HashOutput, DecryptionKey, DecryptionKeyInitVector1, buffer);
// 第八步,循环0x10次
for(int i = 0; i < 0x10; ++i) {
    DecryptionKey[i] = DecryptionKeyInitVector1[i] ^ DecryptionKeyInitVector2[i] ^ mr0k_key[i];
}
// 第九步
uint64_t DecryptionKeyInitVector2Int = *DataLenPtr - 0x14 + *(uint64_t*)DecryptionKeyInitVector2;
// 第十步,赋值
DecryptionKeyInt = *(uint64_t*)DecryptionKey;
mr0k_key = *(uint64_t *)mr0k_key;
// 第十一步,最终解密阶段
for(int i = 0; i < 0x80 /*0x400 / 8*/; ++i) {
    DecryptDataPtr[i] ^= 
        InitVector[i] ^ 
        DecryptionKeyInt ^ 
        DecryptionKeyInitVector2Int ^ 
        mr0k_key;
}
// 最后返回
*DataLenPtr -= 0x14;
return DecryptBufferStart;

可以看出每次解密只与mr0k_keyDecryptionKey有关,其它部分直接在初始化之后dump出来就行。
所以需要还原的算法只有padding_key这一个,👴像个憨批一样一个个还原人都傻了。


2021-05-18 20:03:42
米桑真不能处,偷偷把算法换了,所以工具已经失效了,也没有更新的打算ᕕ( ᐛ )ᕗ
因为比起还原算法,运行时主动调用解密函数显然更简单,十分钟搞定


以上ᖗ( ᐛ )ᖘ完结撒花~