0x0 背景

最近一个新二游Nikke正式开服了,由于某种原因在各群各站传的沸沸扬扬。玩这个游戏的冲什么来懂得都懂,实际玩了一下后发现也就只有模型和剧情值得一览。这不得解解包,四舍五入通下关。主要文章是分析完之后补写,实际分析过程是动调静态一起上的。

0x1 寻找解密方法

查看AssetBundle可以发现Header被修改。正常应该为UnityFS + 1 byte版本号 + 5.x.x.2021.3.9f1 + 2 bytes AB大小等。而Nikke修改后的HeaderNKAB + 不明 + 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的字段中有KeyIV等关键词。

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一遍之后不知道干了什么(设置为AESKey):

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  ................

这里对比一下就会发现AESKey实际上就是文件头的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_IDv34Equal方法中的一环。

结合实际文件头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就分析完成了,接下来按部就班写解密程序即可。

最后修改:2022 年 11 月 11 日
如果觉得我的文章对你有用,请随意赞赏