黑色信标(BlackBeacon) UnityCN Key的获取

以前从来没考虑过这个key是怎么来的,难得想起来一次。仅供参考。

搜索SetAssetBundleDecryptKey

查看引用发现GameAOT_AOT$$Awake

搜索GameAOT_AOT$$Awake发现Game_GenerateData$$GenerateSetAssetBundleDecryptKey提供了参数。

查看Game_GenerateData$$Generate定义,发现一个可疑的string

得到原始key,后续得知这个其实是RC4的seed

`?dZqfdo|&pM@Js^

但还不是真正的结果,发现还有一层处理


具体实现如下

System_Byte_array *Game_Helper__Encrypt(System_Byte_array *pwd, System_Byte_array *data, const MethodInfo *method)
{
  __int64 v5; // r15
  __int64 v6; // rdx
  __int64 v7; // rax
  __int64 v8; // rdx
  __int64 v9; // r13
  __int64 v10; // rax
  il2cpp_array_size_t max_length; // rcx
  __int64 v12; // r8
  __int64 max_length_low; // rdi
  unsigned __int64 i; // rsi
  __int64 v15; // rdx
  unsigned __int64 v16; // r9
  unsigned __int64 v17; // rax
  int v18; // ebx
  int v19; // ebx
  unsigned int v20; // r9d
  int v21; // r10d
  unsigned __int64 v22; // r11
  int v23; // ecx

  if ( !byte_4E3A8A8 )
  {
    sub_D96BF5(&byte___TypeInfo, data);
    sub_D96BF5(&int___TypeInfo, data);
    byte_4E3A8A8 = 1;
  }
  v5 = sub_D96C5F(int___TypeInfo, 256, method);
  v7 = sub_D96C5F(int___TypeInfo, 256, v6);
  if ( !data || (v9 = v7, v10 = sub_D96C5F(byte___TypeInfo, LODWORD(data->max_length), v8), !pwd) )
LABEL_29:
    sub_D96D1C();
  v12 = v10;
  max_length_low = LODWORD(pwd->max_length);
  for ( i = 0; i != 256; ++i )
  {
    v15 = (unsigned int)((int)i >> 31);
    LODWORD(v15) = (int)i % (int)max_length_low;
    if ( (int)i % (int)max_length_low >= (unsigned int)max_length_low )
      goto LABEL_27;
    if ( !v5 )
      goto LABEL_29;
    v16 = *(unsigned int *)(v5 + 24);
    if ( i >= v16 )
      goto LABEL_27;
    *(_DWORD *)(v5 + 4 * i + 32) = pwd->m_Items[(int)v15];
    if ( !v9 )
      goto LABEL_29;
    v17 = *(unsigned int *)(v9 + 24);
    if ( i >= v17 )
      goto LABEL_27;
    *(_DWORD *)(v9 + 4 * i + 32) = i;
  }
  v15 = 0;
  v18 = 0;
  do
  {
    if ( (unsigned int)v15 >= (unsigned int)v17 )
      goto LABEL_27;
    if ( (unsigned int)v15 >= (unsigned int)v16 )
      goto LABEL_27;
    max_length_low = *(unsigned int *)(v9 + 4 * v15 + 32);
    v19 = *(_DWORD *)(v5 + 4 * v15 + 32) + max_length_low + v18;
    i = (v19 + ((unsigned int)(v19 >> 31) >> 24)) & 0xFFFFFF00;
    v18 = v19 % 256;
    if ( v18 >= (unsigned int)v17 )
      goto LABEL_27;
    i = v18;
    max_length = *(unsigned int *)(v9 + 4LL * v18 + 32);
    *(_DWORD *)(v9 + 4 * v15 + 32) = max_length;
    *(_DWORD *)(v9 + 4LL * v18 + 32) = max_length_low;
    ++v15;
  }
  while ( (_DWORD)v15 != 256 );
  max_length = data->max_length;
  if ( (int)max_length > 0 )
  {
    if ( (unsigned int)v17 >= 2 )
    {
      v20 = data->max_length;
      LODWORD(v15) = 0;
      v21 = 1;
      v22 = 0;
      do
      {
        max_length_low = v21;
        max_length = *(unsigned int *)(v9 + 4LL * v21 + 32);
        v15 = (unsigned int)(((int)max_length + (int)v15) % 256);
        if ( (unsigned int)v15 >= (unsigned int)v17 )
          break;
        *(_DWORD *)(v9 + 4LL * v21 + 32) = *(_DWORD *)(v9 + 4LL * (int)v15 + 32);
        *(_DWORD *)(v9 + 4LL * (int)v15 + 32) = max_length;
        v23 = *(_DWORD *)(v9 + 4LL * v21 + 32) + max_length;
        i = (v23 + ((unsigned int)(v23 >> 31) >> 24)) & 0xFFFFFF00;
        max_length = (unsigned int)(v23 % 256);
        if ( (unsigned int)max_length >= (unsigned int)v17 || v22 >= v20 )
          break;
        if ( !v12 )
          goto LABEL_29;
        if ( v22 >= *(unsigned int *)(v12 + 24) )
          break;
        *(_BYTE *)(v12 + v22 + 32) = data->m_Items[v22] ^ *(_DWORD *)(v9 + 4LL * (int)max_length + 32);
        ++v22;
        v20 = data->max_length;
        if ( (__int64)v22 >= (int)v20 )
          return (System_Byte_array *)v12;
        LODWORD(v17) = *(_DWORD *)(v9 + 24);
        i = (v21 + ((unsigned int)((v21 + 1) >> 31) >> 24) + 1) & 0xFFFFFF00;
        max_length = (unsigned int)(v21 + 1 - i);
        v21 = max_length;
      }
      while ( (unsigned int)max_length < (unsigned int)v17 );
    }
LABEL_27:
    sub_D96D22(max_length_low, i, v15, max_length, v12);
  }
  return (System_Byte_array *)v12;
}

反正就是 RC4 的 KSA + PRGA,有点复杂,可以找现成的python复现或者找相关工具实现。

def rc4(key, data):
    S = list(range(256))
    j = 0
    out = []

    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) & 0xFF
        S[i], S[j] = S[j], S[i]

    i = j = 0
    for b in data:
        i = (i + 1) & 0xFF
        j = (j + S[i]) & 0xFF
        S[i], S[j] = S[j], S[i]
        out.append(b ^ S[(S[i] + S[j]) & 0xFF])

    return bytes(out)

完整流程

public static byte[] Generate()
{
    // 1. ASCII 编码器
    var ascii = new System.Text.ASCIIEncoding();

    // 2. 固定明文字符串
    string plain = "`?dZqfdo|&pM@Js^";

    // 3. 转成 ASCII 字节
    byte[] data = ascii.GetBytes(plain);

    // 4. 使用静态密码进行加密
    // pwd 存在于 Game.GenerateData 的 static fields
    byte[] encrypted = Game.Helper.Encrypt(pwd, data);

    // 5. 再次用 ASCIIEncoding 把 byte[] 转回 byte[]
    return ascii.GetBytes(encrypted);
}

这里还需要RC4的key,然后通过这个算法进一步处理

为了获取RC4的key,还需要查看上一级调用的函数的构造函数,也就是Game_Helper__Encrypt的调用者Game_GenerateData$$Generate的构造函数cctor。

查找Game_GenerateData相关函数,找到Game_GenerateData$$_cctor

可以看到这里创建一个长度为256的数组,明显是RC4相关的数据。接着应该是使用常量Field_eval_p_eval_b来初始化。

跳转到定义

到这里还是有点迷惑,并没有给出实质性的数据,只有System_RuntimeFieldHandle_o <<800002F1h>>这个信息,这个800002F1h是一个token,需要查global-metadata.dat得出结果。

Token → metadata → default value

但是查metadata是需要偏移量的

接下来查找script.json,搜索eval_p.eval_b得到

{
    "Address": 81565432,
    "Name": "Field$eval_p.eval_b",
    "Signature": null
}

这里只给一个RVA地址,没什么用。

接下来查找dump.cs相关定义,成功找到偏移量

[CompilerGenerated]
internal sealed class eval_p // TypeDefIndex: 12761
{
	// Fields
	internal static readonly eval_p.eval_a eval_a /*Metadata offset 0x65D840*/; // 0x0
	internal static readonly eval_p.eval_a eval_b /*Metadata offset 0x65D948*/; // 0x100
}

查metadata导出数据

dat = r"global-metadata.dat"
with open(dat, "rb") as f:
    f.seek(0x65D948)
    eval_b = f.read(0x100)
print(eval_b.hex())

得到RC4的key为eval_b

eval_b = bytes.fromhex( "ca2c93b9b371dc2b9d91303bd6fc9836c246a8f4970481f2c6abd4635f996fb324eb4f62a450d693237315468e3fbc9e4f80c41a412b422690db81fac7c5dd24e3ac74989a1823d3b76ceccfc8033c52e0fc2066c2e6f37634de8cebc2686ab6c184f9b2d253673aacc519ee7da24702a378c7f40ca48e39f824ed91783e1aa15ed3018df351f11f3c1226440b42b566abf6f066769ab4c70c5ec2d07fd097204143d290082db6e0882bc5f838cb0740f4c2f6e5c2c3af86838e5317cf6d3678d9c1dea986c554e69b33d2bc35f0625b63715f261be571e05ef80d56ce2bf2f88187bd4f4cc310a167924ec7982e9786c3ddd5610f8d9249812b860d46e86ff5"
)

再次回到此前的流程

public static byte[] Generate()
{
    // 固定明文string
    string plain = "`?dZqfdo|&pM@Js^";

    // ASCII Encode: string(UTF16) -> Bytes
    byte[] data = ascii.GetBytes(plain);

    // 使用静态密码进行RC4加密
    byte[] encrypted = Game.Helper.Encrypt(pwd, data);

    // 再次用 ASCIIEncoding 把 byte[] 转回 byte[]
    return ascii.GetBytes(encrypted);
}

所以最终结果

seed = b"`?dZqfdo|&pM@Js^" #string -> bytes
encrypted = rc4(eval_b, seed)
final_key = encrypted.decode("ascii", errors="replace")
print(encrypted.hex())
print(final_key)
print(final_key.encode("ascii", errors="replace").hex())

输出得到

encrypted = 5f6c4efe3ae2238d9ab28666a81acbe8
final_key = _lN�:�#����f���
UnityCN key = 5f6c4e3f3a3f233f3f3f3f663f1a3f3f //跟表格里的数据一致。

但是为什么先解码再编码后结果出现了一些差异?

5f
6c
4e
fe #3f
3a
e2 #3f
23
8d #3f
9a #3f
b2 #3f
86 #3f
66
a8 #3f
1a
cb #3f
e8 #3f

发现有很多个字节应该是3f,但是却不对。其实final_key = _lN�:�#����f��� 这里出现的问号已经解释了原因:ASCII解码会进行如下处理

  1. 0x00–0x7F:合法,原样保留
  2. ≥ 0x80:无法表示 → 替换为 ?(0x3F)

然后再次ASCII编码的时候,这些原先的非法字节已经都被替换了,原先合法的字节就原封不动了。

这也就解释了最后一步到底在干嘛。

return ascii.GetBytes(encrypted);

先看下这个方法的原型
ASCIIEncoding.GetBytes 方法 (System.Text) | Microsoft Learn

ASCIIEncoding.GetBytes接受的参数只能是字符数组/字符串,这里输入的encryptedbyte[],应该是不合法的操作,但是为什么会发生呢?应该需要先进行数据类型转换。

其实这段是gpt反编译的,IDA反编译这一段失败了。

但是大致可以推测是因为il2cpp编译的时候优化了这个部分。从汇编的角度来看就只是操作寄存器了,也就是直接移动内存数据进行计算。

实际上应该是这样

string tmp = ascii.GetString(encrypted);
return ascii.GetBytes(tmp);

也就是byte[] -> string -> byte[]的一个过程。这里的两个映射规则遵从ASCII的解码/编码。

通俗地讲就是把目标数据进行了一次ASCII洗筛,把不能够通过ASCII解码的数据清洗为合法数据,然后复原。

总结

总结一下,大概就是

  1. 明文string 作为RC4的 seed
  2. metadata中取出一个约定的256b数据eval_b作为key
  3. 计算密文encrypted = rc4(eval_b, seed)
  4. 对密文进行ASCII洗筛得到 UnityCN的key

若内容对你有所帮助,欢迎点击右下角:heart:表示支持,你的鼓励将成为我持续更新的动力(maybe)。

7 个赞