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 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.Keyhash一遍之后不知道干了什么（设置为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中的SHA256hash就直接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经过SHA256hash的结果。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就分析完成了，接下来按部就班写解密程序即可。
解密明日方舟的数据信息 - 远古版本0.7.8
[scode type="red"]免责声明：本文仅为博主安全研究学习记录，强烈反对参考本文内容进行不正当用途，包括但不限于作弊行为等。请后果自负。[/scode][scode type="yellow"]此文为补写，意为记录，重新分析也是用回远古版本安装包，应该已不适用于目前版本游戏[/scode]0x0 背景2019年5月跟着群友入坑了明日方舟（于2019年11月左右弃坑），顺手拆了CG和立绘（Perfare佬yyds）。后来突发奇想想做个多游戏抽卡模拟器（剧透：咕了）。素材已经到位，接着就是卡池数据了，得想个办法拆出游戏数据，总不能期期手动跟进卡池更新吧？于是说干就干，打开资源文件夹，发现在\AB\Android\gamedata\excel目录下有gacha_table.ab文件，这就是卡池数据了。丢进AssetStudio，点开gacha_table就发现全是看不懂的数据，导出后为gacha_table.bytes。没办法，只能分析下是怎么解密数据的了。注：本文为补写，按照回忆重新开始分析，与文中不同的时当时是直接搜索加密相关关键词直接找到几个解密类Hook的。0x1 寻找解密方法游戏基于UnityEngine引擎且使用了il2cpp。按部就班掏出Il2CppDumper加载libil2cpp.so及assets\bin\Data\Managed\Metadata\global-metadata.dat，然后丢IDA分析并加载dump出的IDA python脚本。（值得一提（？）的是，ARM和ARMv7架构对应lib中libil2cpp.so是完全相同的。仅有少量第三方库存在差异。）与此同时使用dnSpy加载dump出的dummy dll寻找到可疑目标类Torappu.GachaDB，其继承自Torappu.DB.ConstTable -> SingletonAbstractTable -> AbstractTable。跳转到Torappu.DB，好家伙，疑似3个加解密方法：第一个应是Json密文的序列化及反序列化、第二个是很明显的AES加解密，第一次接触RijndaelManaged类还是在崩坏学园2里、而第三个看着像是基于随机种子的加解密。查看DBLoader.LoadTable()方法，伪代码：int __fastcall DBLoader__LoadTable(int a1, _DWORD *abstractTable) { int v3; // r1 int result; // r0 int isProdMode; // r0 int *tableConfig; // r1 int converter; // r5 int v8; // r0 int v9; // r0 if ( !byte_3023ECE ) { sub_26B1D18(15387); byte_3023ECE = 1; } if ( !abstractTable ) goto LABEL_13; v3 = (*(int (__fastcall **)(_DWORD *, _DWORD))(*abstractTable + 212))( abstractTable, *(_DWORD *)(*abstractTable + 216)); result = 1; if ( v3 ) return result; isProdMode = AbstractTable__get_isProdMode(1); tableConfig = abstractTable + 3; if ( isProdMode ) tableConfig = abstractTable + 4; if ( !*tableConfig ) { LABEL_13: v9 = ((int (*)(void))loc_26E73D0)(); return CryptUtils__CalculateMD5FromFileOrEmpty(v9); } converter = ConverterFactory__Create(*tableConfig, *(_DWORD *)(*tableConfig + 20)); v8 = Class_Torappu_DB_DBLoader; if ( (*(_BYTE *)(Class_Torappu_DB_DBLoader + 178) & 1) != 0 && !*(_DWORD *)(Class_Torappu_DB_DBLoader + 96) ) v8 = il2cpp_runtime_class_init_0(); return DBLoader___DoLoadTable(v8, abstractTable, converter, 0); }发现其调用了ConverterFactory.Create()创建了实例并传递给了DBLoader._DoLoadTable()。结合dummy dll来看：using System; using Il2CppDummyDll; namespace Torappu.DB { public static class ConverterFactory { [Address(RVA = "0x219E9B8", Offset = "0x219E9B8", VA = "0x219E9B8")] public static IConverter Create(ConverterFactory.ConverterType converterType) { return null; } public enum ConverterType { JSON_DOT_NET, BSON_DOT_NET, CRYPTIC_A, CRYPTIC_B } } } 可见ConverterFactory.Create()的参数即为使用的加解密方法。而此属性在AbstractTable的_prodConfig中。接着看下ConverterFactory.Create()的伪代码：int *__fastcall ConverterFactory__Create(int a1, unsigned int converterType) { int *v3; // r4 int *result; // r0 int v5; // r4 int v6; // r4 int *v7; // r5 int v8; // r0 unsigned int v9[5]; // [sp+4h] [bp-14h] BYREF if ( !byte_3023EBF ) { sub_26B1D18(14632); byte_3023EBF = 1; } if ( converterType > 3 ) { LABEL_10: v9[0] = converterType; v5 = il2cpp_value_box_0(Class_ConverterFactory_ConverterType, v9); if ( (*(_BYTE *)(Class_string + 178) & 1) != 0 && !*(_DWORD *)(Class_string + 96) ) il2cpp_runtime_class_init_0(Class_string, 0); v6 = String__Concat_25024156(0, StringLiteral_11169, v5, StringLiteral_11170, 0); v7 = sub_26F735C(Class_System_Exception); Exception___ctor_20095288(v7, v6, 0); v8 = ((int (__fastcall *)(int *))il2cpp_raise_exception_0)(v7); result = (int *)CryptUtils__CalculateMD5FromFileOrEmpty(v8); } else { switch ( converterType ) { case 0u: v3 = sub_26F735C(Class_Torappu_DB_JsonNetConverter); JsonNetConverter___ctor(); break; case 1u: v3 = sub_26F735C(Class_Torappu_DB_BsonNetConverter); BsonNetConverter___ctor(); break; case 2u: v3 = sub_26F735C(Class_Torappu_DB_CrypticConverter_A); CrypticConverter_A___ctor((int)v3); break; case 3u: v3 = sub_26F735C(Class_Torappu_DB_CrypticConverter_B); CrypticConverter___ctor((int)v3); v3[4] = 42; break; default: goto LABEL_10; } result = v3; } return result; }可以发现就是通过converterType来创建对应的Converter。那么卡池数据 converterType的值是多少呢？查看GachaDB的初始化方法GachaDB___ctor看到return ConstTable___ctor(a1, Method_ConstTable_GachaData__GachaDB___ctor__);，点进去看到伪代码：int __fastcall ConstTable___ctor(int a1, int a2) { int v4; // r6 int v5; // r6 int v6; // r6 int v7; // r1 int result; // r0 int v9; // r0 *(_DWORD *)(a1 + 28) = (***(int (__fastcall ****)(_DWORD))(*(_DWORD *)(a2 + 12) + 84))(0); if ( a1 ) { v4 = *(_DWORD *)(*(_DWORD *)(*(_DWORD *)(a2 + 12) + 84) + 8); sub_26A3FD8(v4); if ( (*(_BYTE *)(v4 + 178) & 1) != 0 ) { v5 = *(_DWORD *)(*(_DWORD *)(*(_DWORD *)(a2 + 12) + 84) + 8); sub_26A3FD8(v5); if ( !*(_DWORD *)(v5 + 96) ) { v6 = *(_DWORD *)(*(_DWORD *)(*(_DWORD *)(a2 + 12) + 84) + 8); sub_26A3FD8(v6); il2cpp_runtime_class_init_0(v6, v7); } } result = (**(int (__fastcall ***)(int))(*(_DWORD *)(*(_DWORD *)(a2 + 12) + 84) + 4))(a1); } else { v9 = ((int (*)(void))loc_26E73D0)(); result = ActionDict__get_Item_36179992(v9); } return result; }噔噔咚，当场去世，先尝试看看DBLoader._DoLoadTable()吧：int __fastcall DBLoader___DoLoadTable(int a1, int *abstractTable, int converter, int false) { int v7; // r0 int isSuccess; // r7 int isProdMode; // r0 int *v10; // r1 int tableConfig; // r7 int assetPath; // r6 int textAsset; // r6 int v14; // r4 int v15; // r4 int convertSuccess; // r5 int v17; // r4 int v19; // r0 int v20[10]; // [sp+10h] [bp-28h] BYREF v7 = (unsigned __int8)byte_3023ECF; if ( !byte_3023ECF ) { sub_26B1D18(15384); v7 = (int)&unk_3023E20; byte_3023ECF = 1; } if ( false ) { if ( !abstractTable ) goto LABEL_39; goto LABEL_8; } if ( abstractTable ) { v7 = (*(int (__fastcall **)(int *, _DWORD))(*abstractTable + 212))(abstractTable, *(_DWORD *)(*abstractTable + 216)); isSuccess = 1; if ( v7 ) return isSuccess; LABEL_8: isProdMode = AbstractTable__get_isProdMode(v7); v10 = abstractTable + 3; if ( isProdMode ) v10 = abstractTable + 4; tableConfig = *v10; if ( !*v10 ) goto LABEL_39; if ( *(_BYTE *)(tableConfig + 8) ) // if(loadFromResource) { assetPath = *(_DWORD *)(tableConfig + 12); if ( (*(_BYTE *)(Class_Torappu_Resource_ResourceManager + 178) & 1) != 0 && !*(_DWORD *)(Class_Torappu_Resource_ResourceManager + 96) ) { il2cpp_runtime_class_init_0(Class_Torappu_Resource_ResourceManager, 0); } textAsset = ResourceManager__Load(0, assetPath, Method_ResourceManager_Load___TextAsset_); if ( (*(_BYTE *)(Class_UnityEngine_Object + 178) & 1) != 0 && !*(_DWORD *)(Class_UnityEngine_Object + 96) ) il2cpp_runtime_class_init_0(Class_UnityEngine_Object, 0); if ( Object__op_Equality(0, textAsset, 0, 0) == 1 ) { v14 = *(_DWORD *)(tableConfig + 12); if ( (*(_BYTE *)(Class_string + 178) & 1) != 0 && !*(_DWORD *)(Class_string + 96) ) il2cpp_runtime_class_init_0(Class_string, 0); isSuccess = 0; v15 = String__Concat_24998280(0, StringLiteral_11173, v14, 0); if ( (*(_BYTE *)(Class_UnityEngine_DLog + 178) & 1) != 0 && !*(_DWORD *)(Class_UnityEngine_DLog + 96) ) il2cpp_runtime_class_init_0(Class_UnityEngine_DLog, 0); DLog__LogError(0, v15, 0); return isSuccess; } } else { textAsset = *(_DWORD *)(tableConfig + 16); } convertSuccess = (*(int (__fastcall **)(int *, int, int, _DWORD))(*abstractTable + 228))( abstractTable, textAsset, converter, *(_DWORD *)(*abstractTable + 232)); if ( (*(_BYTE *)(Class_Torappu_Resource_ResourceManager + 178) & 1) != 0 && !*(_DWORD *)(Class_Torappu_Resource_ResourceManager + 96) ) { il2cpp_runtime_class_init_0(Class_Torappu_Resource_ResourceManager, 0); } isSuccess = 0; ResourceManager__UnloadAsset(0, textAsset); if ( convertSuccess == 1 ) { if ( (*(_BYTE *)(Class_string + 178) & 1) != 0 && !*(_DWORD *)(Class_string + 96) ) il2cpp_runtime_class_init_0(Class_string, 0); v17 = String__Format(0, StringLiteral_11174, abstractTable, 0); Color__get_blue(v20, 0, 0); if ( (*(_BYTE *)(Class_UnityEngine_DLog + 178) & 1) != 0 && !*(_DWORD *)(Class_UnityEngine_DLog + 96) ) il2cpp_runtime_class_init_0(Class_UnityEngine_DLog, 0); DLog__Log_40639780(0, v17, v20[0], v20[1], v20[2], v20[3], 0); isSuccess = 1; } return isSuccess; } LABEL_39: v19 = ((int (*)(void))loc_26E73D0)(); return CryptUtils__CalculateMD5FromFileOrEmpty(v19); }可以看到核心就在于：convertSuccess = // 此变量名称为猜测 (*(int (__fastcall **)(int *, int, int, _DWORD))(*abstractTable + 228))( abstractTable, textAsset, converter, *(_DWORD *)(*abstractTable + 232));要想找到此方法的地址就需要上动调了，不过在这之前，我们已经可以推断出此方法在AbstractTable的子/父类中，其参数一定包含一个TextAsset和一个IConverter。然后在Torappu.DB.ConstTable搜索到了疑似方法public override bool Init(TextAsset rawData, IConverter converter);，在IDA中为int __fastcall ConstTable__Init(int a1, int a2, _DWORD *a3, int a4)。另外可以看到DBLoader._DoLoadTable()中在成功加载后调用了DLog__Log_40639780()并传进了abstractTable，推测这个日志打印可能会包含有用的信息，因此我决定直接HookConverterFactory.Create()获取传递进来的converterType、Hook log方法来获取日志后根据内容反推converterType。node .\hook.js [*] Script loaded [*] Message: { type: 'send', payload: '0xc7119000' } [*] Message: { type: 'send', payload: 'Attach success, base address is 0xc7119000' } converterType: 0x2 log: B] Inited CharacterDB successfully log: B] Inited TokenDB successfully log: B] Inited SkillDB successfully log: B] Inited BuffDB successfully log: B] Inited stage_db (Torappu.StageDB) successfully log: B] Inited game_data_consts_db (Torappu.GameDataConstsDB) successfully log: B] Inited item_db (Torappu.ItemDB) successfully log: B] Inited StoryDB successfully log: B] Inited gacha_db (Torappu.GachaDB) successfully log: B] Inited audio_db (Torappu.Audio.Middleware.Data.TorappuAudioDB) successfully log: B] Inited enemy_db (Torappu.EnemyDB) successfully log: B] Inited RangeDB successfully converterType: 0x0 log: B] Inited building_db (Torappu.BuildingDB) successfully log: B] Inited handbook_info_db (Torappu.HandbookInfoDB) successfully log: B] Inited HandbookTeamDB successfully log: B] Inited mission_db (Torappu.MissionDB) successfully log: B] Inited zone_db (Torappu.ZoneDB) successfully log: B] Inited CharWordDB successfully log: B] Inited favor_db (Torappu.FavorDB) successfully log: B] Inited checkin_db (Torappu.CheckInDB) successfully log: B] Inited tip_db (Torappu.TipDB) successfully log: B] Inited skin_db (Torappu.SkinDB) successfully log: B] Inited open_server_db (Torappu.OpenServerDB) successfully log: B] Inited EnemyHandBookDB successfully log: B] Inited activity_db (Torappu.ActivityDB) successfully log: B] Inited clue_db (Torappu.ClueDB) successfully log: B] Inited shop_client_db (Torappu.ShopClientDB) successfully converterType: 0x2从log可以看出数据对应的转换方法，这里关心的是Torappu.GachaDB，对应0x2即CRYPTIC_A。0x2 解密CrypticConverter_A为老熟人AES-128-CBC。先看看CrypticConverter_A的构造器伪方法：void *__fastcall CrypticConverter_A___ctor(int a1) { int v2; // r5 void *temp; // r0 void *v4; // r1 int chatMask; // r0 int _chatMask; // r5 int *m_rm; // r6 int m_rm2; // r6 int encoding; // r7 int encoding_1; // r6 void *result; // r0 int v12; // r0 int *v13; // r4 if ( !byte_3023EC7 ) { sub_26B1D18(14805); byte_3023EC7 = 1; } v2 = Class_byte__; sub_26A3FD8(Class_byte__); *(_DWORD *)(a1 + 0x14) = il2cpp_array_new_specific_0(v2, 16);// this.m_iv = new byte[16] CrypticConverter___ctor(a1); temp = (void *)Singleton__get_instance(0, Method_Singleton_PlayerData__get_instance__); if ( temp ) { chatMask = PlayerData__get_chatMask((int)temp); _chatMask = chatMask; if ( !chatMask || String__get_Length(chatMask, 0) != 32 ) { v13 = sub_26F735C(Class_System_Exception); Exception___ctor_20095288(v13, StringLiteral_11171, 0); v12 = ((int (__fastcall *)(int *))il2cpp_raise_exception_0)(v13); return (void *)CryptUtils__CalculateMD5FromFileOrEmpty(v12); } m_rm = sub_26F735C(Class_System_Security_Cryptography_RijndaelManaged); temp = (void *)RijndaelManaged___ctor((int)m_rm); *(_DWORD *)(a1 + 0x10) = m_rm; // this.m_rm = new RijndaelManaged if ( m_rm ) { (*(void (__cdecl **)(size_t))(*m_rm + 0x14C))((size_t)m_rm); temp = *(void **)(a1 + 0x10); // temp = m_rm if ( temp ) { (*(void (__cdecl **)(wint_t))(*(_DWORD *)temp + 348))((wint_t)temp); m_rm2 = *(_DWORD *)(a1 + 0x10); if ( (*(_BYTE *)(Class_System_Text_Encoding + 178) & 1) != 0 && !*(_DWORD *)(Class_System_Text_Encoding + 96) ) il2cpp_runtime_class_init_0(Class_System_Text_Encoding, 0); encoding = Encoding__get_UTF8(); temp = (void *)String__Substring_25026412(_chatMask, 0, 16, 0); v4 = temp; if ( encoding ) { temp = (*(void *(__cdecl **)(size_t))(*(_DWORD *)encoding + 0x104))(encoding);// temp = chatMask[:16] v4 = temp; if ( m_rm2 ) { (*(void (__fastcall **)(int, void *, _DWORD))(*(_DWORD *)m_rm2 + 0x11C))( m_rm2, temp, *(_DWORD *)(*(_DWORD *)m_rm2 + 0x120)); encoding_1 = Encoding__get_UTF8(); temp = (void *)String__Substring_25026412(_chatMask, 16, 16, 0); v4 = temp; if ( encoding_1 ) { result = (*(void *(__cdecl **)(size_t))(*(_DWORD *)encoding_1 + 0x104))(encoding_1);// result = chatMask[16:32] *(_DWORD *)(a1 + 24) = result; // this.m_ivMask = result return result; } } } } } } v12 = ((int (__fastcall *)(void *, void *))loc_26E73D0)(temp, v4); return (void *)CryptUtils__CalculateMD5FromFileOrEmpty(v12); }在这里可以发现chatMask扮演了重要角色，联想到AES-128的key和iv长度为16，很容易怀疑都与chatMask有关。chatMask属于类PlayerData，难道对于不同玩家来说数值不一致？？？（剧透：都是一样的）另外注意这里将m_ivMask设置为了chatMask[16:]，猜测chatMask[:16]就是key。再看看实际进行解密的CrypticConverter_A.DecodeInternal()伪方法：int __fastcall CrypticConverter_A__DecodeInternal(int a1, int src, int dst) { int src_1; // r9 unsigned int v6; // r0 unsigned int int_16; // r7 signed int v8; // r0 unsigned int int_0; // r4 char _src; // r6 int v11; // r0 int m_ivMask; // r8 int m_iv; // r5 int v14; // r0 char ivMask_xor_src; // r6 int v16; // r0 int RijndaelManaged_m_rm; // r0 int RijndaelManaged_m_rm_1; // r0 _DWORD *v19; // r0 _DWORD *v20; // r5 int v21; // r0 int v22; // r4 int v23; // r3 int v24; // r7 int v25; // r0 int v26; // r0 int v27; // r1 int result; // r0 int v29; // r0 int v30; // r3 int v31; // r4 int v32; // r0 if ( !byte_3023EC9 ) { sub_26B1D18(14806); byte_3023EC9 = 1; } src_1 = Misc__ReadAllBytes(0, src, 0); if ( src_1 ) { v6 = 0x10u; // 16 while ( 1 ) { int_16 = v6; // 16 v8 = *(_DWORD *)(src_1 + 12); int_0 = int_16 - 16; // 0 _src = 0; if ( (int)(int_16 - 16) < v8 ) { if ( v8 <= int_0 ) { v11 = sub_26E81BC(); ((void (__fastcall *)(int))il2cpp_raise_exception_0)(v11); } _src = *(_BYTE *)(src_1 + int_16); } m_ivMask = *(_DWORD *)(a1 + 0x18); // *this + 24 = byte[] m_ivMask if ( !m_ivMask ) break; m_iv = *(_DWORD *)(a1 + 0x14); // *this + 20 = byte[] m_iv if ( *(_DWORD *)(m_ivMask + 0xC) <= int_0 )// if(m_ivMask.length <= 0) { v14 = sub_26E81BC(); ((void (__fastcall *)(int))il2cpp_raise_exception_0)(v14); } if ( !m_iv ) break; ivMask_xor_src = *(_BYTE *)(m_ivMask + int_16) ^ _src; if ( *(_DWORD *)(m_iv + 0xC) <= int_0 ) { v16 = sub_26E81BC(); ((void (__fastcall *)(int))il2cpp_raise_exception_0)(v16); } v6 = int_16 + 1; *(_BYTE *)(m_iv + int_16) = ivMask_xor_src;// m_iv = ivMask_xor_src if ( (int)(int_16 - 15) >= 16 ) { RijndaelManaged_m_rm = *(_DWORD *)(a1 + 0x10); if ( RijndaelManaged_m_rm ) { (*(void (__fastcall **)(int, _DWORD, _DWORD))(*(_DWORD *)RijndaelManaged_m_rm + 268))( RijndaelManaged_m_rm, *(_DWORD *)(a1 + 0x14), // m_iv *(_DWORD *)(*(_DWORD *)RijndaelManaged_m_rm + 272)); RijndaelManaged_m_rm_1 = *(_DWORD *)(a1 + 0x10); if ( RijndaelManaged_m_rm_1 ) { v19 = (_DWORD *)(*(int (__fastcall **)(int, _DWORD))(*(_DWORD *)RijndaelManaged_m_rm_1 + 356))( RijndaelManaged_m_rm_1, *(_DWORD *)(*(_DWORD *)RijndaelManaged_m_rm_1 + 360)); v20 = v19; if ( v19 ) { v21 = *v19; v22 = *(_DWORD *)(src_1 + 12); if ( *(_WORD *)(*v20 + 170) ) { v23 = *(_DWORD *)(v21 + 76); v24 = 0; while ( *(_DWORD *)(v23 + 8 * v24) != Class_System_Security_Cryptography_ICryptoTransform ) { if ( ++v24 >= (unsigned int)*(unsigned __int16 *)(*v20 + 170) ) goto LABEL_23; } v25 = v21 + 8 * *(_DWORD *)(v23 + 8 * v24 + 4) + 220; } else { LABEL_23: v25 = sub_26A6A34(v20, Class_System_Security_Cryptography_ICryptoTransform, (const char *)5); } v27 = (*(int (__fastcall **)(_DWORD *, int, int, int, _DWORD))v25)( v20, src_1, 16, v22 - 16, *(_DWORD *)(v25 + 4)); if ( v27 ) { if ( dst ) { result = (*(int (__fastcall **)(int, int, _DWORD, _DWORD, _DWORD))(*(_DWORD *)dst + 340))( dst, v27, 0, *(_DWORD *)(v27 + 12), *(_DWORD *)(*(_DWORD *)dst + 344)); if ( v20 ) { v29 = *v20; if ( *(_WORD *)(*v20 + 170) ) { v30 = *(_DWORD *)(v29 + 76); v31 = 0; while ( *(_DWORD *)(v30 + 8 * v31) != Class_System_IDisposable ) { if ( ++v31 >= (unsigned int)*(unsigned __int16 *)(*v20 + 170) ) goto LABEL_35; } v32 = v29 + 8 * *(_DWORD *)(v30 + 8 * v31 + 4) + 180; } else { LABEL_35: v32 = sub_26A6A34(v20, Class_System_IDisposable, 0); } result = (*(int (__fastcall **)(_DWORD *, _DWORD))v32)(v20, *(_DWORD *)(v32 + 4)); } return result; } } } } } break; } } } v26 = ((int (*)(void))loc_26E73D0)(); return CryptUtils__CalculateMD5FromFileOrEmpty(v26); }可以看到第67行用m_ivMask去异或输入的文件即得到了真正的iv。直接HookPlayerData.get_chatMask()打印返回值、HookRijndaelManagedTransform的构造器打印key和iv：node .\hook.js [*] Script loaded [*] Message: { type: 'send', payload: '0xc6f90000' } [*] Message: { type: 'send', payload: 'Attach success, base address is 0xc6f90000' } #onEnter:get_chatMask 8OjXUSNSi8yXC0u98mNWvh7MRLGhyEuQ #onEnter:RijndaelManagedTransform$$ctor Key: 00000000 38 4f 6a 58 55 53 4e 53 69 38 79 58 43 30 75 39 8OjXUSNSi8yXC0u9 IV: 00000000 7b 0d 0a 20 20 22 63 68 61 72 5f 32 38 35 5f 6d {.. "char_285_m #onEnter:RijndaelManagedTransform$$ctor Key: 00000000 38 4f 6a 58 55 53 4e 53 69 38 79 58 43 30 75 39 8OjXUSNSi8yXC0u9 IV: 00000000 7b 0d 0a 20 20 22 74 72 61 70 5f 30 30 34 5f 69 {.. "trap_004_i #onEnter:RijndaelManagedTransform$$ctor Key: 00000000 38 4f 6a 58 55 53 4e 53 69 38 79 58 43 30 75 39 8OjXUSNSi8yXC0u9 IV: 00000000 7b 0d 0a 20 20 22 73 6b 63 6f 6d 5f 63 68 61 72 {.. "skcom_char #onEnter:RijndaelManagedTransform$$ctor Key: 00000000 38 4f 6a 58 55 53 4e 53 69 38 79 58 43 30 75 39 8OjXUSNSi8yXC0u9 IV: 00000000 7b 0d 0a 20 20 22 73 6c 75 67 67 69 73 68 22 3a {.. "sluggish":可以发现key就是chatMask的前16字节（已更换），异或后的iv其实就是明文件的前16字节。至此，key和iv的算法都拿到了，写个解密程序就万事大吉了。那么chatMask对不同玩家来说不同吗？不：另外咕掉的模拟器（两年前的，插件版本对不上了，连预览都炸了...）：
[scode type="yellow"]此文为补写，意为记录，已不适用于目前版本游戏[/scode]0x0 故事那是2017年... 突然两张GIF刷爆了我加的多个群组。 大伙们都是LSP了，看到这哪还顶得住，纷纷开始寻找原游戏出处。然后发现是TapTap中还未上架的游戏《宝石研物语》，这还不得感觉关注一波？于是游戏关注度飞涨。然后...不太妙的感觉...不过不碍事，游戏关注持续增加。一直到上线后一度挤爆服务器，后续多天也经常维护。至于玩家们最关注的Live2D，则不出意外的惨遭和谐，掀裙子的动画直接消失了。这哪能忍，那么就尝试看看资源文件，是否真的删掉了动画还是仅不播放此动画了。0x1 资源解密解压安装包，assets\res\下有live2d文件夹。好耶，点开模型配置文件model.json！...坏耶，被加密了，再看看动作文件，贴图文件等，发现全部被加密，且文件头为都为gemtrade。结合这游戏是Cocos2d-Lua开发的，盲猜一波是xxtea加密（?）。掏出IDA，加载libcocos2dlua.so，定位到cocos2d::FileUtils::getDataFromFile（），伪代码：cocos2d::Data *__fastcall cocos2d::FileUtils::getDataFromFile(cocos2d::Data *a1, cocos2d::FileUtils *a2, int a3, _BYTE *a4) { int v8; // r2 int v10[8]; // [sp+0h] [bp-20h] BYREF cocos2d::Data::Data(a1); v8 = *(_DWORD *)a2; v10[1] = (int)a1; v10[0] = (int)off_CD734C60; (*(void (__fastcall **)(cocos2d::FileUtils *, int, int *))(v8 + 28))(a2, a3, v10); if ( !cocos2d::Data::isNull(a1) ) *a4 = cocos2d::FileUtils::decodeData(a2, a1); return a1; }发现多了一个FileUtils::decodeData()方法，那么它应该就是目标了，此方法中调用FileUtils::decodeBuffer()紧接着FileUtils::decodeXOR()，伪代码：char *__fastcall cocos2d::FileUtils::decodeXOR(cocos2d::FileUtils *this, unsigned __int8 *a2, int a3, int *a4) { unsigned int v8; // r4 int v9; // r5 int v10; // r8 int v11; // r0 unsigned int v12; // r6 unsigned int v13; // r8 unsigned int v14; // r10 unsigned int v15; // r5 char *v16; // r11 int v17; // r0 __int16 v18; // r12^2 char *v19; // r3 unsigned int v20; // r0 unsigned int v21; // r2 if ( a3 <= 19 ) return 0; v8 = memcmp(&unk_CD6C15E8, a2, 8u); if ( v8 ) return 0; v9 = cocos2d::FileUtils::char2uint(this, a2, 8); v10 = cocos2d::FileUtils::char2uint(this, a2, 12); v11 = cocos2d::FileUtils::char2uint(this, a2, 16); v12 = (unsigned int)(v11 + v10) >> 1; v13 = (unsigned int)(v10 - v11) >> 1; v14 = a3 - 20; *a4 = v14; v15 = v9 - v12 * v13; v16 = (char *)malloc(v14); memcpy(v16, a2 + 20, v14); while ( 1 ) { v17 = cocos2d::FileUtils::char2uint(this, (unsigned __int8 *)v16, v8); v18 = HIWORD(v17) ^ HIWORD(v15); v16[v8] = v17 ^ v15; v19 = &v16[v8]; v8 += 4; v20 = (v17 ^ v15) >> 8; v21 = v14 - v8; --v15; v19[1] = v20; *((_WORD *)v19 + 1) = v18; if ( v14 <= v8 || v21 <= 3 ) break; if ( v12 <= v8 && v12 < v21 ) { v8 += v13; if ( v14 < v8 || v12 > v14 - v8 ) v8 = v14 - v12; } } return v16; }其中unk_CD6C15E8查看为gemtrade。::aru:knife:: 竟然不是xxtea，那么接下来的就是复制粘贴解密了。另外当时求助过浮冰大佬，他认识的大佬也有在研究这个解密，于是直接丢给了我一份成品。不用动手真的太爽了::aru:flower::。解密程序大佬已开源：28598519a/bsydecrypt: a tool can decrypt the file from 寶石研物語 (github.com)解密Live2D后发现没有删除动画，好耶，当时甚至拿Live2D官方模板搓了个APK丢群里玩::aru:cryingface::。 if (window.Hoshi) { window.Hoshi.loadJs("https://cdn.jsdelivr.net/combine/gh/Dreamer-Paul/Pio@master/static/l2d.min.js,gh/Dreamer-Paul/Pio@master/static/pio.min.js", function () { let pio = new Paul_Pio({ "mode": "static", "hidden": false, "content": { "welcome": "欢迎查看本文" }, "model": [ "../../usr/static/articles/gemtrade/magelite/model.json", ] }); }) };
[scode type="yellow"]本文于完成2个月后凭回忆补写并重新配图，且博主并未学过C系语言，难免会有错漏，请见谅[/scode][scode type="lblue"]博主使用设备为iPad Pro 10.5（2017）架构为ARM64，文中所有修改都针对ARM64[/scode][scode type="red"]免责声明：本文禁止转载，仅为博主安全研究学习记录，强烈反对参考本文内容进行不正当用途，包括但不限于作弊行为等。请后果自负。[/scode]0x0 前言由大佬 esterTion 编写的iOS插件 ArcaeaCountdownScore 用于lowiro制作的音游Arcaea。此插件的作用是将游戏分数计算改为倒计分制，即开局满分，产生每个far或者lost时分数递减。这种计分制度对我这种菜鸡即为有用，可以更容易的提前许多预估出本局打歌是否可以达到理想的分数并选择继续还是重试。而616截止至目前（写文时3.6.4）都没有推出具有此功能的搭档。不幸的是，插件在很久前就失效了，表现为什么反应都没有，而鸽王一直都没有更新，我也一直在等待。此时的我并未加入任何音游相关的群组。直至616推出了3.6.0版本更新，此版本中在请求头中新增了X-Random-Challenge字段来验证请求，与此相关的代码逆出来看着简直像一坨shit。于是我想尝试加入一些音游群组向大佬们取经。进入一些群组后才发现原来 esterTion已然弃坑许久（具体原因可参见其B站动态），那么大佬们不再维护ArcaeaCountdownScore，就只能自己撸起袖子上了。。。小知识：616在iOS 3.6.0版本请求头中同时也加入了X-Client-Certificate-Subject-CN以及EnableObfuseCheck，而安卓则在后续版本中加入趣事：616在iOS 3.6.0版本中意外（？）打包进了Algo.zip0x1 方法注入拿到 ArcaeaCountdownScore 源码后首先看看hook的方法：ArcUtil_calculateScore = (unsigned long)MSFindSymbol(NULL, "__ZN7ArcUtil14calculateScoreEiiiii");::aru:blood2:: 怪不得没有任何反应了，iOS二进制文件中已经没有了符号，插件无从找起，方法都没有被替换，当然什么反应都没有了。那么当务之急就是找到新的方法位置进行hook。iOS中没有符号了，那么掏出安卓版3.6.0的二进制想办法对比定位。找到ArcUtil::calculateScore()，然后按x寻找其xref引用，可见一堆调用了calculateScore()的方法。接下来就是在这些方法中找出可以用于在iOS二进制中搜索的信息，由于我之前就有看过OnlineManager::submitScoreInternal()方法，并且此方法中也调用了calculateScore()，所以直接选择从此方法入手。伪代码： v133 = std::string::append((int)&v550, "&score=", 7u); v134 = *(_OWORD *)v133; v553 = *(void **)(v133 + 16); v552 = v134; *(_QWORD *)(v133 + 8) = 0LL; *(_QWORD *)(v133 + 16) = 0LL; *(_QWORD *)v133 = 0LL; v135 = (std::__ndk1 *)ArcUtil::calculateScore( (ArcUtil *)*(unsigned int *)(a4 + 32), *(_DWORD *)(a4 + 28), *(_DWORD *)(a4 + 24), *(_DWORD *)(a4 + 20), *(_DWORD *)(a4 + 28) + *(_DWORD *)(a4 + 32) + *(_DWORD *)(a4 + 24), *(_BYTE *)(a4 + 132)); std::to_string(v135, v136);可以看到调用calculateScore()方法前有个字符串拼接，其中部分字符串为&score=，那么就可以在iOS的二进制中搜索此字符串：找到后查找其引用进入的即为OnlineManager::submitScoreInternal()方法（如果使用其他引用进行查找发现多个调用，则需要对比找到其在iOS中对应方法），伪代码： v72 = std::string::append(&v328, "&score="); v73 = *(_OWORD *)v72; v331 = *(_QWORD *)(v72 + 16); v330 = v73; *(_QWORD *)(v72 + 8) = 0LL; *(_QWORD *)(v72 + 16) = 0LL; *(_QWORD *)v72 = 0LL; v74 = (std::__1 *)sub_10025F358(); std::to_string(v74, v75); v76 = std::string::append();则ArcUtil::calculateScore()在iOS中为位置在base + 0x25F358，查看该方法的汇编对应二进制：CMP W2, W4 B.NE loc_10025F370 CBNZ W5, loc_10025F370 MOV W8, #0x989680 5F 00 04 6B A1 00 00 54 85 00 00 35 08 D0 92 52将原代码中通过符号查找ArcUtil::calculateScore()修改为通过二进制查找。摘取部分代码，对应 ArcaeaCountdownScore中第173-186行：uint8_t calculateScore_inst[] = { 0x5f, 0x00, 0x04, 0x6b, 0xa1, 0x00, 0x00, 0x54, 0x85, 0x00, 0x00, 0x35, 0x08, 0xd0, 0x92, 0x52 }; // ------------摘取部分代码------------- if (instcmp((uint8_t*)ptr, calculateScore_inst, 16)) { ArcUtil_calculateScore = (unsigned long)ptr; found_calculateScore = true; NSLog(@"[arc-score] found calculateScore at %lx", ArcUtil_calculateScore); } 那么对此方法的寻址就修改完成了，值得一提的是，此方法参数列表末尾新增了一个布尔参数，顺便加上：int (*orig_ArcUtil_calculateScore)(int lost, int far, int pure, int shiny_pure, int note_count, bool param_6); int hacked_ArcUtil_calculateScore(int lost, int far, int pure, int shiny_pure, int note_count, bool param_6) { }至此，ArcUtil::calculateScore()的注入修改完成了，接下来就是对ScoreState::init()，ScoreState::hitNote()以及ScoreState::missNote()进行类似的寻找及修改了。ScoreState::init()可通过10ScoreState找到_ZTV10ScoreState DCQ 0，接着查看其引用发现sub_10013DC88()，此方法伪代码： *(_QWORD *)v7 = off_100924018; *(_QWORD *)(v7 + 36) = 0x42C8000000000000LL; *(_WORD *)(v7 + 44) = 256; *(_DWORD *)(v7 + 176) = 0; *(_BYTE *)(v7 + 180) = 0; *(_QWORD *)(v7 + 184) = 0LL; *(_BYTE *)(v7 + 200) = 0; *(_OWORD *)(v7 + 48) = 0u; *(_OWORD *)(v7 + 64) = 0u; *(_QWORD *)(v7 + 92) = 0LL; *(_QWORD *)(v7 + 84) = 0LL; *(_QWORD *)(v7 + 124) = 0LL; *(_QWORD *)(v7 + 116) = 0LL; *(_BYTE *)(v7 + 132) = 0; if ( (unsigned int)sub_10022E8B8(v7, a1, a2, v8, a4) ) <---- 这里就是ScoreState::init() { sub_10052C380(v7); } else { (*(void (__fastcall **)(__int64))(*(_QWORD *)v7 + 8LL))(v7); v7 = 0LL; }ScoreState::hitNote()查找31CharacterAbilityGaugeRateModify -> _ZTI31CharacterAbilityGaugeRateModify DCQ。查看其引用发现sub_10022ED60+A0 ADRL X2, _ZTI31CharacterAbilityGaugeRateModify;，此方法即为ScoreState::hitNote()ScoreState::missNote()查找17LogicLongNoteBase -> _ZTI17LogicLongNoteBase DCQ。调用此类型的方法有点多，3.6.0中发现ScoreState::missNote对应的是sub_1001CBCC0+38 ADRL X1, _ZTI17LogicLongNoteBase;将上面3个方法都找出并将其对应二进制作为特征进行暴力搜索查找注入。另外因为hitNote()和missNote()都调用了ArcUtil::calculateScore()，因此需要补上新增的布尔参数。查看hitNote()的伪代码发现有调用，如下第六个参数即为我们需要的： result = sub_10025F358( *(unsigned int *)(a1 + 96), *(_DWORD *)(a1 + 92), *(_DWORD *)(a1 + 88), *(_DWORD *)(a1 + 84), *(_DWORD *)(*(_QWORD *)(a1 + 160) + 160LL), *(unsigned __int8 *)(a1 + 200));对插件进行修改，拿到此变量传入hacked_ArcUtil_calculateScore()就完成了bool _bool = *(bool*)(((char*)a1) + 200); int score = hacked_ArcUtil_calculateScore(scoreState->lost, scoreState->far, scoreState->pure, scoreState->shiny_pure, scoreState->noteCount, _bool);至此，3.6.0的方法注入就修改完成了。0x2 ScoreState结构体好耶，能正确寻找到方法注入了，可以完事睡大觉了对吧？...进入歌曲，闪退。::aru:knife::...打打log，发现这行导致的闪退：ScoreState *scoreState = (ScoreState*)a1;那么应该就是ScoreState这个结构体有变，嗯..去看看看... 噫？IDA里找不到这个struct，那就看看成员调用猜吧... 完全看不懂.jpg ::aru:dead:: 那就看内存吧。我iPad中已经装有插件DLGMemor-Injected，oslog及frida等常用插件，现在既然已经成功注入方法，就直接使用DLGMemor-Injected查看内存好了。注释掉修改后方法的方法体，在hacked_ScoreState_hitNote()中打印a1，即ScoreState的地址，并调用原方法返回：NSLog(@"[arc-score ScoreState:hitNote] ScoreState *scoreState = (ScoreState*)a1; a1:0x%lx", a1); long returnVal = orig_ScoreState_hitNote(a1, a2, a3, a4, a5); return returnVal;在hacked_ArcUtil_calculateScore()中打印lost,far等参数NSLog(@"[arc-score ArcUtil_calculateScore] lost:%d far:%d pure:%d shiny_pure:%d note_count:%d score:%d param_6:%d", lost, far, pure, shiny_pure, note_count, returnVal, param_6);重新编译安装插件，打一会儿歌，补写本文时打的是Dot to Dot，随意打几个far和lost后暂停，查看log：<Notice>: [arc-score ScoreState:hitNote] ScoreState *scoreState = (ScoreState*)a1; a1:0x283a98b60掏出DLGMemor-Injected，开启对Arcaea的注入，点击下方memory，输入此地址点search即跳转至对应位置了。复制记录下此时的内存。顺便写个Quality of Life脚本处理下地址值：[collapse status="false" title="内存dump地址处理脚本"]base_addr = 0x283A98B60 dump = """283A98B60 40 87 EB 02 01 00 00 00 @....... 283A98B68 01 00 00 00 00 00 00 00 ........ 283A98B70 E3 02 00 00 48 00 00 00 ....H... 283A98B78 75 F2 96 00 D1 50 32 00 u....P2. 283A98B80 49 4D 84 42 00 00 00 00 IM.B.... 283A98B88 00 00 C8 42 00 01 00 00 ...B.... 283A98B90 00 00 00 00 00 00 00 00 ........ 283A98B98 00 00 00 00 00 00 00 00 ........ 283A98BA0 00 00 00 00 00 00 00 00 ........ 283A98BA8 00 00 00 00 00 00 00 00 ........ 283A98BB0 12 00 00 00 6A 00 00 00 ....j... 283A98BB8 6F 00 00 00 04 00 00 00 o....... 283A98BC0 06 00 00 00 11 00 00 00 ........ 283A98BC8 16 00 00 00 08 00 00 00 ........ 283A98BD0 24 00 00 00 03 00 00 00 $....... 283A98BD8 01 00 00 00 03 00 00 00 ........ 283A98BE0 02 00 00 00 00 00 00 00 ........ 283A98BE8 68 F1 56 3E 00 00 00 00 h.V>.... 283A98BF0 00 00 00 00 00 00 00 00 ........ 283A98BF8 02 00 00 00 00 00 00 00 ........ 283A98C00 00 C0 E8 83 02 00 00 00 ........ 283A98C08 00 00 00 00 00 00 00 00 ........ 283A98C10 D0 5C 00 00 00 00 00 00 .\...... 283A98C18 83 00 00 00 24 00 00 00 ....$... 283A98C20 80 CF F2 82 02 00 00 00 ........ 283A98C28 00 00 00 00 00 00 00 00 ........ 283A98C30 CC 00 00 00 00 00 00 00 ........ 283A98C38 00 00 00 00 00 00 80 BF ........ 283A98C40 00 00 00 C0 00 00 80 3F .......? """ qol = "" for line in dump.split("\n"): parts = line.split(" ", maxsplit=1) if len(parts) == 2: addr, content = parts addr = int(addr, 16) - base_addr qol += str(addr).zfill(3) + " " + content + "\n" print(qol) [/collapse][collapse status="false" title="内存dump 1"]000 40 87 EB 02 01 00 00 00 @....... 008 01 00 00 00 00 00 00 00 ........ 016 E3 02 00 00 48 00 00 00 ....H... 024 75 F2 96 00 D1 50 32 00 u....P2. 032 49 4D 84 42 00 00 00 00 IM.B.... 040 00 00 C8 42 00 01 00 00 ...B.... 048 00 00 00 00 00 00 00 00 ........ 056 00 00 00 00 00 00 00 00 ........ 064 00 00 00 00 00 00 00 00 ........ 072 00 00 00 00 00 00 00 00 ........ 080 12 00 00 00 6A 00 00 00 ....j... 088 6F 00 00 00 04 00 00 00 o....... 096 06 00 00 00 11 00 00 00 ........ 104 16 00 00 00 08 00 00 00 ........ 112 24 00 00 00 03 00 00 00 $....... 120 01 00 00 00 03 00 00 00 ........ 128 02 00 00 00 00 00 00 00 ........ 136 68 F1 56 3E 00 00 00 00 h.V>.... 144 00 00 00 00 00 00 00 00 ........ 152 02 00 00 00 00 00 00 00 ........ 160 00 C0 E8 83 02 00 00 00 ........ 168 00 00 00 00 00 00 00 00 ........ 176 D0 5C 00 00 00 00 00 00 .\...... 184 83 00 00 00 24 00 00 00 ....$... 192 80 CF F2 82 02 00 00 00 ........ 200 00 00 00 00 00 00 00 00 ........ 208 CC 00 00 00 00 00 00 00 ........ 216 00 00 00 00 00 00 80 BF ........ 224 00 00 00 C0 00 00 80 3F .......?[/collapse]接着回游戏再打一会儿歌暂停，再次记录内存。[collapse status="false" title="内存dump 2"]000 40 87 EB 02 01 00 00 00 @....... 008 01 00 00 00 00 00 00 00 ........ 016 E3 02 00 00 7A 00 00 00 ....z... 024 B7 88 96 00 92 2D 32 00 .....-2. 032 69 19 A5 42 00 00 00 00 i..B.... 040 00 00 C8 42 00 01 00 00 ...B.... 048 00 00 00 00 00 00 00 00 ........ 056 00 00 00 00 00 00 00 00 ........ 064 00 00 00 00 00 00 00 00 ........ 072 00 00 00 00 00 00 00 00 ........ 080 7A 00 00 00 DE 00 00 00 z....... 088 E7 00 00 00 06 00 00 00 ........ 096 07 00 00 00 25 00 00 00 ....%... 104 2E 00 00 00 0C 00 00 00 ........ 112 2A 00 00 00 04 00 00 00 *....... 120 02 00 00 00 05 00 00 00 ........ 128 04 00 00 00 00 00 00 00 ........ 136 68 F1 56 3E 00 00 00 00 h.V>.... 144 00 00 00 00 00 00 00 00 ........ 152 02 00 00 00 00 00 00 00 ........ 160 00 C0 E8 83 02 00 00 00 ........ 168 00 00 00 00 00 00 00 00 ........ 176 12 B2 00 00 00 00 00 00 ........ 184 58 01 00 00 5B 00 00 00 X...[... 192 80 CF F2 82 02 00 00 00 ........ 200 00 00 00 00 00 00 00 00 ........ 208 CC 00 00 00 00 00 00 00 ........ 216 00 00 00 00 00 00 80 BF ........ 224 00 00 00 C0 00 00 80 3F .......?[/collapse]使用任意diff工具diff一下：结合esterTion已完成的结构变量一起分析：位置016的数值未进行变化，且和原代码中noteCount位置接近，0x2E3即为739，确实为本曲物量。剩下的数值就参照记录下的far，lost等进行猜测。猜不出的则也对ArcaeaCountdownScore来说无用。全部跳过就可以了。其中原代码中的skill_type为血条类型，0为普通血条，1为简单血条，2为困难血条，6为暴风血条。通过切换角色后可以发现152处的数值与此相符。那么修改后的ScoreState如下：typedef struct ScoreState { pointer_t a1; // 0 char a2[8]; // +8 8 int noteCount; // +16 10 int maxCombo; // +20 14 int score; // +24 18 int score_chk; // +28 1c score / 3 float hp; // +32 20 char a3[11 * 4];// 跳过 int combo; // +80 50 int shiny_pure; // +84 54 int pure; // +88 58 int far; // +92 5c int lost; // +96 60 char gap[13 * 4];// 跳过 int skill_type; // +152 98 } ScoreState;至此，插件中ARM64部分修复完毕。我没有ARM设备，未进行修改。另外顺便优化了下游戏启动时的插件弹窗，提示未找到并注入的方法。截止至补写文章时Arcaea版本为3.6.4，此插件依然可用。