文章来源:https://blog.palug.cn/2460.html
https://blog.palug.cn/2659.html
https://blog.palug.cn/2728.html
[Honkai 3rd]v3.6.1资源加密分析(前)
又是崩坏3ᖗ( ᐛ )ᖘ
在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_key
和DecryptionKey
有关,其它部分直接在初始化之后dump出来就行。
所以需要还原的算法只有padding_key
这一个,👴像个憨批一样一个个还原人都傻了。
2021-05-18 20:03:42
米桑真不能处,偷偷把算法换了,所以工具已经失效了
,也没有更新的打算ᕕ( ᐛ )ᕗ
因为比起还原算法,运行时主动调用解密函数显然更简单,十分钟搞定
以上ᖗ( ᐛ )ᖘ完结撒花~