0x0 背景
最近一个新二游Nikke
正式开服了,由于某种原因在各群各站传的沸沸扬扬。玩这个游戏的冲什么来懂得都懂,实际玩了一下后发现也就只有模型和剧情值得一览。这不得解解包,四舍五入通下关。主要文章是分析完之后补写,实际分析过程是动调静态一起上的。
0x1 寻找解密方法
查看AssetBundle
可以发现Header
被修改。正常应该为UnityFS
+ 1 byte版本号 + 5.x.x.2021.3.9f1
+ 2 bytes AB大小等。而Nikke
修改后的Header
为NKAB
+ 不明 + GoddessofVictory
等。且观察发现并非简单的将信息移动,那就需要分析看看了。
起手il2cppdumper
后直接在dump.cs
中搜索关键词NKAB
就能够定位到相关方法,这个字符串在类名中包含,并且很贴心的只出现了一次:
// Namespace: UnityEngine.ResourceManagement.ResourceProviders
internal enum NKBundleEncryptionMode // TypeDefIndex: 25907
{
// Fields
public ushort value__; // 0x0
public const NKBundleEncryptionMode Default = 0;
}
// Namespace: UnityEngine.ResourceManagement.ResourceProviders
public class NKABStreamProcessorWithSeek : IDataConverter // TypeDefIndex: 25908
{
public Stream CreateReadStream(Stream input, string id) { }
public Stream CreateWriteStream(Stream input, string id) { }
public void .ctor() { }
}
// Namespace: UnityEngine.ResourceManagement.ResourceProviders
internal class NKAssetBundleStream : Stream // TypeDefIndex: 25909
{
// Fields
[CompilerGenerated]
private readonly Stream <baseStream>k__BackingField; // 0x28
[CompilerGenerated]
private readonly NKBundleHeader <Header>k__BackingField; // 0x30
[CompilerGenerated]
private readonly Aes <AES>k__BackingField; // 0x38
[CompilerGenerated]
private ICryptoTransform <transform>k__BackingField; // 0x40
[CompilerGenerated]
private byte[] <encryptedBuffer>k__BackingField; // 0x48
[CompilerGenerated]
private byte[] <plainBuffer>k__BackingField; // 0x50
[CompilerGenerated]
private readonly string <Name>k__BackingField; // 0x58
[CompilerGenerated]
private bool <Disposed>k__BackingField; // 0x60
// Properties
protected Stream baseStream { get; }
public NKBundleHeader Header { get; }
protected Aes AES { get; }
protected ICryptoTransform transform { get; set; }
protected byte[] encryptedBuffer { get; set; }
protected byte[] plainBuffer { get; set; }
protected string Name { get; }
protected bool Disposed { get; set; }
public override bool CanRead { get; }
public override bool CanSeek { get; }
public override bool CanWrite { get; }
public override long Length { get; }
public override long Position { get; set; }
[CompilerGenerated]
protected Stream get_baseStream() { }
[CompilerGenerated]
public NKBundleHeader get_Header() { }
[CompilerGenerated]
protected Aes get_AES() { }
[CompilerGenerated]
protected ICryptoTransform get_transform() { }
[CompilerGenerated]
protected void set_transform(ICryptoTransform value) { }
[CompilerGenerated]
protected byte[] get_encryptedBuffer() { }
[CompilerGenerated]
protected void set_encryptedBuffer(byte[] value) { }
[CompilerGenerated]
protected byte[] get_plainBuffer() { }
[CompilerGenerated]
protected void set_plainBuffer(byte[] value) { }
[CompilerGenerated]
protected string get_Name() { }
[CompilerGenerated]
protected bool get_Disposed() { }
[CompilerGenerated]
private void set_Disposed(bool value) { }
protected void .ctor(Stream stream) { }
public override bool get_CanRead() { }
public override bool get_CanSeek() { }
public override bool get_CanWrite() { }
public override long get_Length() { }
public override long get_Position() { }
public override void set_Position(long value) { }
public override void Flush() { }
public override void SetLength(long value) { }
public override long Seek(long offset, SeekOrigin origin) { }
public override int Read(byte[] buffer, int offset, int count) { }
public override void Write(byte[] buffer, int offset, int count) { }
protected void RentBuffers(int length) { }
protected void ReturnBuffers() { }
protected void TransformBlock(byte[] from, byte[] to) { }
protected override void Dispose(bool disposing) { }
}
// Namespace: UnityEngine.ResourceManagement.ResourceProviders
internal class NKAssetBundleReadStream : NKAssetBundleStream // TypeDefIndex: 25910
{
// Fields
private bool _isDecrypted; // 0x61
// Properties
public override bool CanRead { get; }
// Methods
internal void .ctor(Stream stream) { }
public override bool get_CanRead() { }
private void CheckAndDecryptOnce() { }
public override int Read(byte[] buffer, int offset, int count) { }
}
// Namespace: UnityEngine.ResourceManagement.ResourceProviders
internal class NKBundleHeader // TypeDefIndex: 25911
{
// Fields
private static readonly byte[] HEADER_ID; // 0x0
private const uint LATEST_VERSION = 1;
private const ushort HEADER_SIZE_BF_KEY = 16;
private const ushort IV_SIZE = 16;
private const ushort HEADER_SIZE_MIN = 32;
private const ushort OBFUSCATE_VALUE = 100;
internal uint Version; // 0x10
internal NKBundleEncryptionMode EncryptionMode; // 0x14
internal ushort KeyLength; // 0x16
internal ushort EncryptedLength; // 0x18
internal byte[] Key; // 0x20
internal byte[] IV; // 0x28
// Properties
internal ushort HeaderSize { get; }
// Methods
internal ushort get_HeaderSize() { }
private ushort ObfuscateUInt16(ushort value) { }
private ushort DeobfuscateUInt16(ushort value) { }
public bool ReadFromStream(Stream stream) { }
public void .ctor() { }
private static void .cctor() { }
}
看到类、方法名很容易就能确定这些就是我们需要分析的目标:NKAssetBundleStream
中有老熟人关键词AES
,而NKBundleHeader
的字段中有Key
、IV
等关键词。
上IDA
一通翻找后确定有用的关键方法为.ctor(Stream stream)
以及NKBundleHeader.ReadFromStream(Stream stream)
。
0x2 NKAssetBundleReadStream
先看NKAssetBundleReadStream
:
void __cdecl UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleReadStream___ctor(UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleReadStream_o *this, System_IO_Stream_o *stream, const MethodInfo *method)
{
__int64 v5; // x21
const MethodInfo *v6; // x2
UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_o *Header; // x0
struct System_Security_Cryptography_Aes_o *aes; // x0
System_Security_Cryptography_HashAlgorithm_o *sha256; // x0
System_Security_Cryptography_HashAlgorithm_o *v10; // x20
struct UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_o *Header_; // x8
System_Byte_array *key_sha256_hash; // x1
__int64 v13; // x21
System_Security_Cryptography_HashAlgorithm_c *v14; // x8
__int64 v15; // x9
int *v16; // x10
__int64 v17; // x0
__int64 v18; // x21
struct UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_o *v19; // x8
struct System_Security_Cryptography_Aes_o *v20; // x0
const MethodInfo *v21; // x2
struct UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_o *v22; // x8
struct System_Security_Cryptography_Aes_o *v23; // x0
struct System_Security_Cryptography_ICryptoTransform_o *v24; // x1
if ( (byte_A29949B & 1) == 0 )
{
sub_71C6D98(&System_IDisposable_TypeInfo, stream, method);
*(_BYTE *)(v5 + 1179) = 1;
}
UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleStream___ctor(
(UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleStream_o *)this,
stream,
method);
Header = this->fields._Header_k__BackingField;
if ( !Header )
goto LABEL_20;
UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader__ReadFromStream(Header, stream, v6);
aes = this->fields._AES_k__BackingField;
if ( !aes )
goto LABEL_20;
((void (__fastcall *)(struct System_Security_Cryptography_Aes_o *, __int64, const MethodInfo *))aes->klass->vtable._15_set_KeySize.methodPtr)(
aes,
256LL,
aes->klass->vtable._15_set_KeySize.method);
sha256 = (System_Security_Cryptography_HashAlgorithm_o *)System_Security_Cryptography_SHA256__Create(0LL);
v10 = sha256;
Header_ = this->fields._Header_k__BackingField;
if ( !Header_ )
sub_71C6EC8();
if ( !sha256 )
sub_71C6EC8();
key_sha256_hash = System_Security_Cryptography_HashAlgorithm__ComputeHash(sha256, Header_->fields.Key, 0LL);
if ( !v13 )
sub_71C6EC8();
(*(void (__fastcall **)(__int64, System_Byte_array *, _QWORD))(*(_QWORD *)v13 + 504LL))(
v13,
key_sha256_hash,
*(_QWORD *)(*(_QWORD *)v13 + 512LL));
v14 = v10->klass;
v15 = v10->klass->_2.interface_offsets_count;
if ( v10->klass->_2.interface_offsets_count )
{
v16 = &v14->_1.interfaceOffsets->offset;
while ( *((System_IDisposable_c **)v16 - 1) != System_IDisposable_TypeInfo )
{
v16 += 4;
if ( !--v15 )
goto LABEL_12;
}
v17 = (__int64)&v14->vtable + 16 * *v16;
}
else
{
LABEL_12:
v17 = sub_71FA5E8(v10, System_IDisposable_TypeInfo, 0LL);
}
(*(void (__fastcall **)(System_Security_Cryptography_HashAlgorithm_o *, _QWORD))v17)(v10, *(_QWORD *)(v17 + 8));
if ( v18 )
sub_724180C(v18);
v19 = this->fields._Header_k__BackingField;
if ( !v19
|| (v20 = this->fields._AES_k__BackingField) == 0LL
|| (((void (__fastcall *)(struct System_Security_Cryptography_Aes_o *, struct System_Byte_array *, const MethodInfo *))v20->klass->vtable._10_set_IV.methodPtr)(
v20,
v19->fields.IV,
v20->klass->vtable._10_set_IV.method),
(v22 = this->fields._Header_k__BackingField) == 0LL)
|| (UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleStream__RentBuffers(
(UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleStream_o *)this,
v22->fields.EncryptedLength,
v21),
(v23 = this->fields._AES_k__BackingField) == 0LL) )
{
LABEL_20:
sub_71C6EC8();
}
v24 = (struct System_Security_Cryptography_ICryptoTransform_o *)((__int64 (__fastcall *)(struct System_Security_Cryptography_Aes_o *, const MethodInfo *))v23->klass->vtable._22_CreateDecryptor.methodPtr)(
v23,
v23->klass->vtable._22_CreateDecryptor.method);
this->fields._transform_k__BackingField = v24;
sub_71C6D34((__int64)&this->fields._transform_k__BackingField, (__int64)v24);
}
不难看出首先读了文件头加载至NKBundleHeader
:
Header = this->fields._Header_k__BackingField;
if ( !Header )
goto LABEL_20;
UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader__ReadFromStream(Header, stream, v6);
NKBundleHeader
逻辑后续再看,接着拿到AES
实例,设置KeySize
为256:
aes = this->fields._AES_k__BackingField;
if ( !aes )
goto LABEL_20;
((void (__fastcall *)(struct System_Security_Cryptography_Aes_o *, __int64, const MethodInfo *))aes->klass->vtable._15_set_KeySize.methodPtr)(
aes,
256LL,
aes->klass->vtable._15_set_KeySize.method);
然后把NKBundleHeader.Key
hash一遍之后不知道干了什么(设置为AES
的Key
):
sha256 = (System_Security_Cryptography_HashAlgorithm_o *)System_Security_Cryptography_SHA256__Create(0LL);
v10 = sha256;
Header_ = this->fields._Header_k__BackingField;
if ( !Header_ )
sub_71C6EC8();
if ( !sha256 )
sub_71C6EC8();
key_sha256_hash = System_Security_Cryptography_HashAlgorithm__ComputeHash(sha256, Header_->fields.Key, 0LL);
if ( !v13 )
sub_71C6EC8();
(*(void (__fastcall **)(__int64, System_Byte_array *, _QWORD))(*(_QWORD *)v13 + 504LL))(
v13,
key_sha256_hash,
*(_QWORD *)(*(_QWORD *)v13 + 512LL));
再往下就是设置IV
以及调用NKAssetBundleStream.RentBuffers(int length)
并传入NKBundleHeader.EncryptedLength
干了什么:
_NKHeader = this->fields._Header_k__BackingField;
if ( !_NKHeader
|| (v20 = this->fields._AES_k__BackingField) == 0LL
|| (((void (__fastcall *)(struct System_Security_Cryptography_Aes_o *, struct System_Byte_array *, const MethodInfo *))v20->klass->vtable._10_set_IV.methodPtr)(
v20,
_NKHeader->fields.IV,
v20->klass->vtable._10_set_IV.method),
(v22 = this->fields._Header_k__BackingField) == 0LL)
|| (UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleStream__RentBuffers(
(UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleStream_o *)this,
v22->fields.EncryptedLength,
v21),
(v23 = this->fields._AES_k__BackingField) == 0LL) )
最后初始化Decryptor
:
v24 = (struct System_Security_Cryptography_ICryptoTransform_o *)((__int64 (__fastcall *)(struct System_Security_Cryptography_Aes_o *, const MethodInfo *))v23->klass->vtable._22_CreateDecryptor.methodPtr)(
v23,
v23->klass->vtable._22_CreateDecryptor.method);
this->fields._transform_k__BackingField = v24;
sub_71C6D34((__int64)&this->fields._transform_k__BackingField, (__int64)v24);
0x3 动态Hook + 文件样本
开头提到其实是动调+静态一同分析的。事实上看到NKAssetBundleStream
中的SHA256
hash就直接Hook
对应方法拿到了hash的内容以及返回值。Hook
也帮助理解了接下来的NKBundleHeader
分析。其实如何Hook
走了挺多弯路,花了周末一晚上+一个白天才搞定,那就是另一篇博文了。
多次尝试后Hook
的方法有这几个:
HashAlgorithm.ComputeHash(byte[] buffer)
AesCryptoServiceProvider.set_Key(byte[] value)
AesCryptoServiceProvider.set_IV(byte[] value)
NKAssetBundleStream.TransformBlock(byte[] from, byte[] to)
样本文件头,避免乱码,看不出的内容全用.
代替:
0000h 4E 4B 41 42 01 00 00 00 CC FF 9C FF AC FF 0C 00 NKAB............
0010h 47 6F 64 64 65 73 73 6F 66 56 69 63 74 6F 72 79 GoddessofVictory
0020h 11 56 19 17 89 16 EE FD 6C 55 E8 D4 4B 75 D5 A1 ................
0030h 5C A3 77 70 44 51 CD B3 52 2E 08 1B CA 03 03 14 ................
0040h 37 D2 33 C7 87 40 D9 C9 52 15 19 87 11 62 24 4A ................
0050h 1C DF 5C 0F 24 69 9C B1 CA 26 C8 CF 2D B9 CF C0 ................
0060h FB 58 19 73 C6 55 31 2D A1 6E 80 51 D9 A1 F1 C5 ................
0070h 50 BC 24 20 BC 8F 31 DB ED 79 8D 1E B9 24 EE C4 ................
0080h BE 86 8A 06 EA 1A F3 7E D8 4D 8B 83 6B C9 05 07 ................
0090h 9E 54 F9 04 31 44 10 8F E4 1E D5 27 6B B4 49 6C ................
00B0h F9 03 89 FA 00 03 00 01 F7 7C 00 00 6C BD 00 03 ................
Hook
方法HashAlgorithm.ComputeHash(byte[] buffer)
打印结果:
On enter HashAlgorithm byte[] ComputeHash(byte[] buffer) !
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 47 6f 64 64 65 73 73 6f 66 56 69 63 74 6f 72 79 GoddessofVictory
On leave HashAlgorithm byte[] ComputeHash(byte[] buffer) { }!
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 af 02 5f 29 32 be cd 01 f2 2f 86 c5 93 c7 65 f8 .._)2..../....e.
00000010 96 a3 b7 5d 19 14 e5 46 a3 e5 29 1d 55 32 94 fa ...]...F..).U2..
Hook
方法AesCryptoServiceProvider.set_Key(byte[] value)
打印结果:
On enter AesCryptoServiceProvider void set_Key(byte[] value) { } !
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 af 02 5f 29 32 be cd 01 f2 2f 86 c5 93 c7 65 f8 .._)2..../....e.
00000010 96 a3 b7 5d 19 14 e5 46 a3 e5 29 1d 55 32 94 fa ...]...F..).U2..
On leave AesCryptoServiceProvider void set_Key(byte[] value) { }!
Hook
方法AesCryptoServiceProvider.set_IV(byte[] value)
打印结果:
On enter AesCryptoServiceProvider void set_IV(byte[] value) { } !
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 11 56 19 17 89 16 ee fd 6c 55 e8 d4 4b 75 d5 a1 .V......lU..Ku..
On leave AesCryptoServiceProvider void set_IV(byte[] value) { }!
Hook
方法NKAssetBundleStream.TransformBlock(byte[] from, byte[] to)
打印结果:
On enter NKAssetBundleStream TransformBlock(byte[] from, byte[] to)!
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 5c a3 77 70 44 51 cd b3 52 2e 08 1b ca 03 03 14 ................
00000010 37 d2 33 c7 87 40 d9 c9 52 15 19 87 11 62 24 4a ................
00000020 1c df 5c 0f 24 69 9c b1 ca 26 c8 cf 2d b9 cf c0 ................
00000030 fb 58 19 73 c6 55 31 2d a1 6e 80 51 d9 a1 f1 c5 ................
00000040 50 bc 24 20 bc 8f 31 db ed 79 8d 1e b9 24 ee c4 ................
00000050 be 86 8a 06 ea 1a f3 7e d8 4d 8b 83 6b c9 05 07 ................
00000060 9e 54 f9 04 31 44 10 8f e4 1e d5 27 6b b4 49 6c ................
00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
On leave NKAssetBundleStream TransformBlock(byte[] from, byte[] to)!
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 55 6e 69 74 79 46 53 00 00 00 00 08 35 2e 78 2e UnityFS.....5.x.
00000010 78 00 32 30 32 31 2e 33 2e 39 66 31 00 00 00 00 x.2021.3.9f1....
00000020 00 00 0b ba 99 00 00 00 88 00 00 00 e9 00 00 02 ................
00000030 43 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000040 1e 00 01 00 30 09 00 02 07 00 42 3a ef 00 03 0a ................
00000050 00 23 8c 37 0a 00 34 01 dc 34 0a 00 24 d0 d7 0a ................
00000060 00 24 d1 8d 0a 00 24 cb 9f 0a 00 24 b1 b5 0a 00 ................
00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
这里对比一下就会发现AES
的Key
实际上就是文件头的0x10 - 0x19
经过SHA256
hash的结果。IV
是文件头的0x20 - 0x29
。
再结合多个样本发现密文是文件头0x30 - ??
不定长内容(在NKBundleHeader
中有读取到)。
0x3 NKBundleHeader
伪代码(看得眼花):
bool __cdecl UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader__ReadFromStream(UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_o *this, System_IO_Stream_o *stream, const MethodInfo *method)
{
UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_c *v32; // x0
if ( (byte_A29949C & 1) == 0 )
{
sub_71C6D98(&Method_System_Buffers_ArrayPool_byte__get_Shared___165997888, stream);
sub_71C6D98(&System_Buffers_ArrayPool_byte__TypeInfo, v5);
sub_71C6D98(&System_BitConverter_TypeInfo, v6);
sub_71C6D98(&byte___TypeInfo, v7);
sub_71C6D98(&System_Enum_TypeInfo, v8);
sub_71C6D98(&Method_System_MemoryExtensions_SequenceEqual_byte____166505312, v9);
sub_71C6D98(&UnityEngine_ResourceManagement_ResourceProviders_NKBundleEncryptionMode_var, v10);
sub_71C6D98(&UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_TypeInfo, v11);
sub_71C6D98(&Method_System_ReadOnlySpan_byte__op_Implicit___166240120, v12);
sub_71C6D98(&Method_System_Span_byte__Slice___166253488, v13);
sub_71C6D98(&Method_System_Span_byte___ctor___166253408, v14);
sub_71C6D98(&Method_System_Span_byte__op_Implicit___166253512, v15);
sub_71C6D98(&System_Type_TypeInfo, v16);
sub_71C6D98(&ushort_TypeInfo, v17);
*(_BYTE *)(v18 + 1180) = 1;
}
v68 = 0LL;
if ( !stream )
goto LABEL_74;
if ( ((__int64 (__fastcall *)(System_IO_Stream_o *, const MethodInfo *, const MethodInfo *))stream->klass->vtable._11_unknown.methodPtr)(
stream,
stream->klass->vtable._11_unknown.method,
method) )
{
v19 = ((__int64 (__fastcall *)(System_IO_Stream_o *, const MethodInfo *))stream->klass->vtable._7_unknown.methodPtr)(
stream,
stream->klass->vtable._7_unknown.method);
if ( !v19 )
return v19;
}
v19 = ((__int64 (__fastcall *)(System_IO_Stream_o *, const MethodInfo *))stream->klass->vtable._6_unknown.methodPtr)(
stream,
stream->klass->vtable._6_unknown.method);
if ( !v19 )
return v19;
if ( !System_Buffers_ArrayPool_byte__TypeInfo->_2.cctor_finished )
j__il2cpp_runtime_class_init_0();
v20 = Method_System_Buffers_ArrayPool_byte__get_Shared___165997888;
v21 = *((_QWORD *)Method_System_Buffers_ArrayPool_byte__get_Shared___165997888 + 4);
if ( (*(_WORD *)(v21 + 306) & 1) == 0 )
v21 = sub_71FA3E0(v21);
v22 = **(_QWORD **)(v21 + 192);
if ( (*(_WORD *)(v22 + 306) & 1) == 0 )
v22 = sub_71FA3E0(v22);
if ( !*(_DWORD *)(v22 + 224) )
j__il2cpp_runtime_class_init_0();
v23 = v20[4];
if ( (*(_WORD *)(v23 + 306) & 1) == 0 )
v23 = sub_71FA3E0(v23);
v24 = **(_QWORD **)(v23 + 192);
if ( (*(_WORD *)(v24 + 306) & 1) == 0 )
v24 = sub_71FA3E0(v24);
v25 = **(_QWORD **)(v24 + 184);
if ( !v25 )
LABEL_74:
sub_71C6EC8();
v26 = (*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)v25 + 376LL))(
v25,
16LL,
*(_QWORD *)(*(_QWORD *)v25 + 384LL));
v28 = v26;
v68 = v26;
if ( byte_9D0853A )
{
if ( v26 )
{
LABEL_22:
v29 = *(_DWORD *)(v28 + 24);
goto LABEL_25;
}
}
else
{
sub_71C6D98(&System_Type_TypeInfo, v27);
*(_BYTE *)(v30 + 1338) = 1;
if ( v28 )
goto LABEL_22;
}
v29 = 0;
LABEL_25:
v67[0] = 0LL;
v67[1] = (__int64)&v68;
if ( ((unsigned int (__fastcall *)(System_IO_Stream_o *, __int64, _QWORD, __int64, const MethodInfo *))stream->klass->vtable._31_unknown.methodPtr)(
stream,
v28,
0LL,
16LL,
stream->klass->vtable._31_unknown.method) != 16 )
goto LABEL_72;
if ( v29 <= 3 )
System_ThrowHelper__ThrowArgumentOutOfRangeException(0LL);
v31 = *((_QWORD *)Method_System_Span_byte__Slice___166253488 + 4);
if ( (*(_WORD *)(v31 + 306) & 1) == 0 )
sub_71FA3E0(v31);
v32 = UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_TypeInfo;
if ( !UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_TypeInfo->_2.cctor_finished )
{
j__il2cpp_runtime_class_init_0();
v32 = UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_TypeInfo;
}
v33 = System_ReadOnlySpan_byte___op_Implicit(
v32->static_fields->HEADER_ID,
(const MethodInfo_22EBDD4 *)Method_System_ReadOnlySpan_byte__op_Implicit___166240120);
v34 = Method_System_MemoryExtensions_SequenceEqual_byte____166505312;
v69 = 0LL;
if ( !(*(unsigned int (__fastcall **)(__int64 *))(*(_QWORD *)(*((_QWORD *)Method_System_MemoryExtensions_SequenceEqual_byte____166505312
+ 7)
+ 8LL)
+ 8LL))(&v69) )
{
if ( v33.fields._length == 4 )
{
v40 = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)(v34[7] + 16LL) + 8LL))(v35, 4LL);
v41 = (*(__int64 (__fastcall **)(intptr_t, _QWORD))(*(_QWORD *)(v34[7] + 24LL) + 8LL))(
v33.fields._pointer.fields._value,
*(_QWORD *)&v33.fields._length);
v38 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)(v34[7] + 32LL) + 8LL))(v40, v41, 4LL);
goto LABEL_37;
}
LABEL_72:
sub_5F03DF8(v67);
LOBYTE(v19) = 0;
return v19;
}
if ( v33.fields._length != 4 )
goto LABEL_72;
v36 = (uint8_t *)(*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)(v34[7] + 16LL) + 8LL))(v35, 4LL);
v37 = (uint8_t *)(*(__int64 (__fastcall **)(intptr_t, _QWORD))(*(_QWORD *)(v34[7] + 24LL) + 8LL))(
v33.fields._pointer.fields._value,
*(_QWORD *)&v33.fields._length);
v38 = System_SpanHelpers__SequenceEqual(v36, v37, 4 * v69, 0LL);
LABEL_37:
if ( (v38 & 1) == 0 )
goto LABEL_72;
if ( (v29 & 0xFFFFFFFC) == 4 )
System_ThrowHelper__ThrowArgumentOutOfRangeException(0LL);
v42 = *((_QWORD *)Method_System_Span_byte__Slice___166253488 + 4);
if ( (*(_WORD *)(v42 + 306) & 1) == 0 )
sub_71FA3E0(v42);
v71.fields._pointer.fields._value = v39 + 4;
*(_QWORD *)&v71.fields._length = 4LL;
v43 = System_Span_byte___op_Implicit(
v71,
(const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512);
if ( !System_BitConverter_TypeInfo->_2.cctor_finished )
j__il2cpp_runtime_class_init_0();
this->fields.Version = System_BitConverter__ToUInt32_103767940((System_ReadOnlySpan_byte__o)v43, 0LL);
if ( v29 <= 9 )
System_ThrowHelper__ThrowArgumentOutOfRangeException(0LL);
v45 = *((_QWORD *)Method_System_Span_byte__Slice___166253488 + 4);
if ( (*(_WORD *)(v45 + 306) & 1) == 0 )
sub_71FA3E0(v45);
v72.fields._pointer.fields._value = v44 + 8;
*(_QWORD *)&v72.fields._length = 2LL;
v73 = System_Span_byte___op_Implicit(
v72,
(const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512);
v66 = System_BitConverter__ToUInt16_103767716((System_ReadOnlySpan_byte__o)v73, 0LL) + 100;
v47 = v29 & 0xFFFFFFFE;
if ( (v29 & 0xFFFFFFFE) == 10 )
System_ThrowHelper__ThrowArgumentOutOfRangeException(0LL);
v48 = *((_QWORD *)Method_System_Span_byte__Slice___166253488 + 4);
if ( (*(_WORD *)(v48 + 306) & 1) == 0 )
sub_71FA3E0(v48);
v74.fields._pointer.fields._value = v46 + 10;
*(_QWORD *)&v74.fields._length = 2LL;
v75 = System_Span_byte___op_Implicit(
v74,
(const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512);
v49 = System_BitConverter__ToUInt16_103767716((System_ReadOnlySpan_byte__o)v75, 0LL);
v50 = UnityEngine_ResourceManagement_ResourceProviders_NKBundleEncryptionMode_var;
if ( !System_Type_TypeInfo->_2.cctor_finished )
j__il2cpp_runtime_class_init_0();
v70.fields.value = (intptr_t)v50;
v51 = System_Type__GetTypeFromHandle(v70, 0LL);
v52 = v49 + 100;
LOWORD(v69) = v49 + 100;
v53 = (Il2CppObject *)j__il2cpp_value_box_0(ushort_TypeInfo, &v69);
if ( !System_Enum_TypeInfo->_2.cctor_finished )
j__il2cpp_runtime_class_init_0();
if ( !System_Enum__IsDefined(v51, v53, 0LL) )
goto LABEL_72;
this->fields.EncryptionMode = v52;
if ( v29 < 0xC || v47 == 12 )
System_ThrowHelper__ThrowArgumentOutOfRangeException(0LL);
v55 = *((_QWORD *)Method_System_Span_byte__Slice___166253488 + 4);
if ( (*(_WORD *)(v55 + 306) & 1) == 0 )
sub_71FA3E0(v55);
v76.fields._pointer.fields._value = v54 + 12;
*(_QWORD *)&v76.fields._length = 2LL;
v56 = System_Span_byte___op_Implicit(
v76,
(const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512);
if ( !System_BitConverter_TypeInfo->_2.cctor_finished )
j__il2cpp_runtime_class_init_0();
this->fields.KeyLength = System_BitConverter__ToUInt16_103767716((System_ReadOnlySpan_byte__o)v56, 0LL) + 100;
if ( v29 < 0xE || v47 == 14 )
System_ThrowHelper__ThrowArgumentOutOfRangeException(0LL);
v58 = *((_QWORD *)Method_System_Span_byte__Slice___166253488 + 4);
if ( (*(_WORD *)(v58 + 306) & 1) == 0 )
sub_71FA3E0(v58);
v77.fields._pointer.fields._value = v57 + 14;
*(_QWORD *)&v77.fields._length = 2LL;
v78 = System_Span_byte___op_Implicit(
v77,
(const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512);
this->fields.EncryptedLength = System_BitConverter__ToUInt16_103767716((System_ReadOnlySpan_byte__o)v78, 0LL) + 100;
sub_5F03DF8(v67);
v59 = (struct System_Byte_array *)sub_71C6DFC(byte___TypeInfo, this->fields.KeyLength);
this->fields.Key = v59;
sub_71C6D34((__int64)&this->fields.Key, (__int64)v59);
v61 = ((__int64 (__fastcall *)(System_IO_Stream_o *, _QWORD, _QWORD, _QWORD, const MethodInfo *))stream->klass->vtable._31_unknown.methodPtr)(
stream,
*v60,
0LL,
this->fields.KeyLength,
stream->klass->vtable._31_unknown.method);
LOBYTE(v19) = 0;
if ( v61 == this->fields.KeyLength )
{
v62 = (struct System_Byte_array *)sub_71C6DFC(byte___TypeInfo, 16LL);
this->fields.IV = v62;
v63 = &this->fields.IV;
sub_71C6D34((__int64)v63, (__int64)v62);
v64 = ((__int64 (__fastcall *)(System_IO_Stream_o *, struct System_Byte_array *, _QWORD, __int64, const MethodInfo *))stream->klass->vtable._31_unknown.methodPtr)(
stream,
*v63,
0LL,
16LL,
stream->klass->vtable._31_unknown.method);
LOBYTE(v19) = 0;
if ( v64 == 16 )
LOBYTE(v19) = ((__int64 (__fastcall *)(System_IO_Stream_o *, const MethodInfo *))stream->klass->vtable._11_unknown.methodPtr)(
stream,
stream->klass->vtable._11_unknown.method) == v66;
}
return v19;
}
一上来就看的眼花缭乱,特别是IDA
蓝汪汪的一片,经常看串行,第一次看愣是没看明白。后来结合实际文件头以及Hook
打印一起看其实逻辑很清晰。
以下只摘取关键代码:
v33 = System_ReadOnlySpan_byte___op_Implicit(
v32->static_fields->HEADER_ID,
(const MethodInfo_22EBDD4 *)Method_System_ReadOnlySpan_byte__op_Implicit___166240120);
v34 = Method_System_MemoryExtensions_SequenceEqual_byte____166505312;
if ( !(*(unsigned int (__fastcall **)(__int64 *))(*(_QWORD *)(*((_QWORD *)Method_System_MemoryExtensions_SequenceEqual_byte____166505312
+ 7)
+ 8LL)
+ 8LL))(&v69) )
{
if ( v33.fields._length == 4 )
{
v40 = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)(v34[7] + 16LL) + 8LL))(v35, 4LL);
v41 = (*(__int64 (__fastcall **)(intptr_t, _QWORD))(*(_QWORD *)(v34[7] + 24LL) + 8LL))(
v33.fields._pointer.fields._value,
*(_QWORD *)&v33.fields._length);
v38 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)(v34[7] + 32LL) + 8LL))(v40, v41, 4LL);
goto LABEL_37;
}
LABEL_72:
sub_5F03DF8(v67);
LOBYTE(v19) = 0;
return v19;
}
if ( v33.fields._length != 4 )
goto LABEL_72;
v36 = (uint8_t *)(*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)(v34[7] + 16LL) + 8LL))(v35, 4LL);
v37 = (uint8_t *)(*(__int64 (__fastcall **)(intptr_t, _QWORD))(*(_QWORD *)(v34[7] + 24LL) + 8LL))(
v33.fields._pointer.fields._value,
*(_QWORD *)&v33.fields._length);
v38 = System_SpanHelpers__SequenceEqual(v36, v37, 4 * v69, 0LL);
条件后执行的一堆看不懂,但可以看出v33
是创建的ReadOnlySpan
,数据是HEADER_ID
,v34
是Equal
方法中的一环。
结合实际文件头4E 4B 41 42
(NKAB) 可以猜测是先判断HEADER_ID
的长度是不是4,之后与文件头的前4个字节对比看是否吻合。
接下来:
v71.fields._pointer.fields._value = v39 + 4;
*(_QWORD *)&v71.fields._length = 4LL;
v43 = System_Span_byte___op_Implicit(
v71,
(const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512);
this->fields.Version = System_BitConverter__ToUInt32_103767940((System_ReadOnlySpan_byte__o)v43, 0LL);
结合文件头01 00 00 00
可知首先把指针指向了文件头+4的位置,然后设长度为4,读取之后转成UInt32
存为NKBundleHeader.Version
。接下来都是同样的逻辑。v66
没看懂是啥,先跳过。
v74.fields._pointer.fields._value = v46 + 10;
*(_QWORD *)&v74.fields._length = 2LL;
v75 = System_Span_byte___op_Implicit(
v74,
(const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512);
v49 = System_BitConverter__ToUInt16_103767716((System_ReadOnlySpan_byte__o)v75, 0LL);
v52 = v49 + 100;
this->fields.EncryptionMode = v52;
结合文件头9C FF
可知首先把指针指向了文件头+10的位置,然后设长度为2,读取之后转成UInt16
再+100之后存为NKBundleHeader.EncryptionMode
。每个文件都是一致,算出EncryptionMode = 0
,后续以此类推得知KeyLength = ACFF.ToUInt16 + 0x64 = 16
。
重点是接下来的EncryptedLength
:
v76.fields._pointer.fields._value = v54 + 12;
*(_QWORD *)&v76.fields._length = 2LL;
v56 = System_Span_byte___op_Implicit(
v76,
(const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512);
this->fields.KeyLength = System_BitConverter__ToUInt16_103767716((System_ReadOnlySpan_byte__o)v56, 0LL) + 100;
可知EncryptedLength
是文件头+12读取2字节并转为UInt16
后再+100。Length小于100的就溢出后的数值实际长度,可还行
0x4 结尾
至此如何解密NKAB
就分析完成了,接下来按部就班写解密程序即可。
8 条评论
大佬,是对下的加密bundle包用i2ldumper逆向吗?现版本已经il2cpp file not supported,这个有排查思路吗?
樓主
請教一下
我解密完畢後,已經可以透過AssetStudioGUI抓取資訊
C410_cover_00xxxxxx
想請教如何透過hex檔抓取這個C410。
不然每次更新要暴力法找有點累
可能需要自己写程序或者脚本来实现了。
C#来写可以引用AssetStudio.dll,AssetStudio.PInvoke.dll,AssetStudioUtility.dll,Texture2DDecoderWrapper.dll。
Python来写可以用 https://github.com/K0lb3/UnityPy
这边还有打算后续更新吗,上次更新日停在11/11
没有打算后续更新了。加密是AES,Key和IV的读取以及哪部分为密文均已提到,没考虑放出具体解密代码。 本来打算做一个展示站,不过发现有前辈做过但由于不可抗力被迫关掉,也鸽了。
github上现在有个nikke的wiki站,有做角色的spine动画展示,但没做角色释放技能的展示。我猜你说的那位前辈是Pelom777,他做的在github上还有fork,角色的和技能的展示齐全,但我下下来试了下不能加载现版本解包出的角色spine文件,搞不清楚是哪里有差异,明明两个项目用的pixijs-spine没什么差别
LOL了解了,
不過我也已經成功照著大佬的提示成功實做出 python版的解密部分OTz
太强了,大佬教教