TypeScript
deno
Ghidra
frida
Il2CppInspector
0
どのような問題がありますか?

投稿日

更新日

カラオケ配信アプリトピアの非公開APIを分析してみよう(1):app token

はじめに

カラオケ配信アプリ「トピア」のことは知っていますか?いろんな人と歌ったり話したりバーチャルアバターで遊んだりできる楽しいアプリです!

トピア紹介イメージ

アプリはUnityとFirebaseで作られてて、下の記事にもトピアのことが紹介されています。

このサービスは(当たり前なんですけど)アカウントを作ったりするAPIがドキュメンテーションされていません。でも好奇心が強い開発者なら多分どうなっているのか気になると思います。サーバーの暗号化はしっかりされているのか、どのようで情報を送受信するのか。

では、早速始めてみましょう。

このポストはreverse engineeringの経験の共有のため書かれたものであります。違法的または悪いことに使わないようにしてください。🥰

Packet Capture

アプリに関して何もわからない状態なので、いったんどんなパケットを送るのかキャプチャーしてみましょう。そのため、Android 10以前のスマホにHttpCanaryって言うアプリをインストールしましょう。

Android 11からはSSLの証明書をインストールするためには設定のアプリを通じないといけないようになりました。

App Token V2

トピアv4.14.3からX-App-TokenっていうHTTP Requestのヘッダーが必要になりました。このapp tokenの役割はサーバーにリクエストを送るクライアントがトピアのアプリなのかそれとも違う何かなのかを判断することです。このapp token (v1)にはユーザーの情報が入ってなかったため、そしてトークンが無効になるまでの時間が長すぎたため(1分)、別のクライアントからリクエストを送ってもらったトークンを別のサーバーに送ってそのまま使うことが可能でした。

トピアv4.20.0からはこれを改善し、ユーザー情報が入るように変わりました。また、トークンが無効になるまでの時間が何秒くらいに非常に短くなりました。ですから公式アプリからトークンをもらってそのまま使えなくなりました。

image.png
優しいエラーメッセージのトピア

では方法が全くないのか?!当然、違います。全てのコードはクライアントの中にあるため、それをわかってゼロからトークンを作るとリクエストを送るのができます。

Apktoolでapkファイルをdecode

トピアv4.20.0のapkファイルをダウンロードして、apktoolでdecodeしてみましょう。

apktool d トピア_4_20_0.apk

トピアはUnityアプリですから、JavaまたはKotlinコードは少ないです。その代わりにC#のコードがcompileされたbinaryが入っています。

image.png

libil2cpp.soがbinaryファイルで、global-metadata.datがC#コードからの情報を持っているファイルになります。この二つのファイルを一つのフォルダーに集めてIl2CppInspectorにあげると素敵なことが起きます!

git clone https://github.com/djkaty/Il2CppInspector
cd Il2CppInspector
zsh get-plugins.sh

cd Il2CppInspector.CLI
dotnet publish -r <arch> -c Release # <arch>はアーキテクチャ、例えばosx-x64

mv ../plugins ./Il2CppInspector.CLI/bin/Release/netcoreapp3.1/<arch>

cd <libil2cpp.soとglobal-metadata.datがあるフォルダー>
<Il2CppInspectorレポジトリ>/Il2CppInspector.CLI/bin/Release/netcoreapp3.1/<arch>/Il2CppInspector -p il2cpp.py -t Ghidra

image.png

ヤッタ~! イエ━━٩(*´ᗜ`)ㅅ(ˊᗜˋ*)و━━イ 
il2cpp.py, types.cs, metadata.jsonが生成されました。

types.csから暗号化コードを探す

types.csTokenなんとかで検索すると、なんかぽいクラスを見つかることができます。

namespace Enlil
{
	public sealed class BackendTokenCalculator // TypeDefIndex: 11655
	{
		// Fields
		private const string COMMON_KEY = "ひ🌟み🌟つ😉"; // Metadata: 0x0073D3DB
		private byte[] _sessionKey; // 0x10
		private byte[] _version; // 0x18
		private long _userId; // 0x20
		private byte[][] _methodCache; // 0x28
		private byte[] _cnonceCache; // 0x30
		private byte[] _pathCache; // 0x38
		private byte[] _hashDataCache; // 0x40
		private char[] _base64Cache; // 0x48
		private string[] _excludeEndpointNames; // 0x50
		[CompilerGenerated] // 0x0201E850-0x0201E860
		private static BackendTokenCalculator _Instance_k__BackingField; // 0x00
	
        // ...

		// Nested types
		[MessagePackObject] // 0x0201E82C-0x0201E840
		public struct TokenInfo // TypeDefIndex: 11656
		{
			// Fields
			public long ts; // 0x00
			public byte[] cnonce; // 0x08
			public byte[] hash; // 0x10
		}
	
        // ...

		// Methods
        // ...
		public void ResetSessionKey(string nonce); // 0x04F58DA4-0x04F58F34
		public void AddTokenHeader(Type clientType, HTTPRequest request, long timestamp); // 0x04F58F34-0x04F5913C
		public TokenInfo CalcTokenHash(long timestamp, HTTPMethods method, string path, byte[] body, byte[] cnonce); // 0x04F5942C-0x04F59664
		private TokenInfo CalcTokenHashImpl(long timestamp, byte[] version, long userId, byte[] method, CacheWithCleaner<byte> path, byte[] body, byte[] cnonce); // 0x04F598A4-0x04F59D98
        // ...
	}
}

φ(・ω・。)フムフム
CalcTokenHashImplが一番パラメータが多そうな感じがするので、これを中心にみてみましょう。もちろん他のメソードもチェック!

Ghidra

Ghidraでlibil2cpp.soファイルを開いてみましょう。Low levelコード恐怖症を持っている方はご注意ください。

  1. brew install openjdk@11でOpenJDK 11をインストールしてPATHに入れます。
  2. brew install ghidraでGhidraをインストールしてghidraRunで開きます。
  3. プロジェクトを生成してlibil2cpp.soをインポートします。
  4. File -> Parse C Sourceからcpp/appdata/il2cpp-types.hをインポートしてオプションを-D_GHIDRA_に設定します。
  5. Parse to Programボタンを押します.
  6. Script managerを開いてil2cpp.pyファイルを読み込みます。
  7. il2cpp.pyを実行して待ちます。
  8. Analysis -> Auto Analyze libil2cpp.soを押して、我慢して待ちます。

JDKのバージョンは大事です。今の時点でJDK 11が必要です。

Script managerのボタンはこれです。

image.png

左のSymbol Treeから関数BackendTokenCalculator_CalcTokenHashImplを探します。
image.png

そうすると…

/* WARNING: Removing unreachable block (ram,0x04f59ca4) */
/* WARNING: Removing unreachable block (ram,0x04f59b78) */
/* WARNING: Removing unreachable block (ram,0x04f59c88) */
/* WARNING: Removing unreachable block (ram,0x04f59c98) */
/* WARNING: Could not reconcile some variable overlaps */
/* BackendTokenCalculator+TokenInfo CalcTokenHashImpl(Int64, Byte[], Int64, Byte[],
   BackendTokenCalculator+CacheWithCleaner`1[System.Byte], Byte[], Byte[]) */

BackendTokenCalculator_TokenInfo *
BackendTokenCalculator_CalcTokenHashImpl
          (BackendTokenCalculator_TokenInfo *__return_storage_ptr__,BackendTokenCalculator *this,
          int64_t timestamp,Byte__Array *version,int64_t userId,Byte__Array *method_1,
          BackendTokenCalculator_CacheWithCleaner_1_System_Byte_ path,Byte__Array *body,
          Byte__Array *cnonce,MethodInfo *method)

{
  bool bVar1;
  undefined8 uVar2;
  long *plVar3;
  long *plVar4;
  code **ppcVar5;
  BackendTokenCalculator *pBVar6;
  BackendTokenCalculator_TokenInfo *pBVar7;
  MethodInfo *method_00;
  BackendTokenCalculator **in_x8;
  long lVar8;
  ulong uVar9;
  uint *puVar10;
  Byte__Array *pBVar11;
  int aiStack160 [6];
  BackendTokenCalculator **ppBStack136;
  BackendTokenCalculator *pBStack128;
  BackendTokenCalculator *pBStack120;
  undefined auStack112 [16];
  
  method_00 = (MethodInfo *)timestamp;
  pBStack120 = this;
  if ((bRam00000000072561c7 & 1) == 0) {
    thunk_FUN_01f299f8(0x3958);
    bRam00000000072561c7 = 1;
  }
  auStack112._0_8_ = 0;
  auStack112._8_8_ = 0;
  if ((((timestamp == 0) || (userId == 0)) || (cnonce == (Byte__Array *)0x0)) ||
     (pBStack128 = (BackendTokenCalculator *)path._Cache_k__BackingField,
     path._Cache_k__BackingField == (Byte__Array *)0x0)) {
                    /* WARNING: Subroutine does not return */
    FUN_01f6f5e8();
  }
  ppBStack136 = in_x8;
  auStack112 = BackendTokenCalculator_GetHashDataCache
                         ((BackendTokenCalculator *)__return_storage_ptr__,
                          (int)body + *(int *)(timestamp + 0x18) + *(int *)(userId + 0x18) +
                          *(int *)&cnonce->max_length +
                          *(int *)&(path._Cache_k__BackingField)->max_length + 0x10,method_00);
  uVar2 = SUB168((undefined  [16])auStack112,0);
                    /* try { // try from 04f59974 to 04f59987 has its CatchHandler @ 04f59d50 */
  plVar3 = (long *)thunk_FUN_01f2ac94(MemoryStream__TypeInfo);
  MemoryStream__ctor_2(plVar3,uVar2,0);
                    /* try { // try from 04f59994 to 04f599a7 has its CatchHandler @ 04f59d30 */
  plVar4 = (long *)thunk_FUN_01f2ac94(BinaryWriter__TypeInfo);
  BinaryWriter__ctor_1(plVar4,plVar3,0);
  if (plVar4 == (long *)0x0) {
                    /* WARNING: Subroutine does not return */
                    /* try { // try from 04f59c84 to 04f59c87 has its CatchHandler @ 04f59cfc */
    FUN_01f6f5e8();
  }
                    /* try { // try from 04f599b8 to 04f599c3 has its CatchHandler @ 04f59cfc */
  (**(code **)(*plVar4 + 0x2a0))(plVar4,pBStack120,*(undefined8 *)(*plVar4 + 0x2a8));
                    /* try { // try from 04f599d0 to 04f599db has its CatchHandler @ 04f59cf8 */
  (**(code **)(*plVar4 + 0x200))(plVar4,timestamp,*(undefined8 *)(*plVar4 + 0x208));
                    /* try { // try from 04f599e8 to 04f599f3 has its CatchHandler @ 04f59cf4 */
  (**(code **)(*plVar4 + 0x2a0))(plVar4,version,*(undefined8 *)(*plVar4 + 0x2a8));
                    /* try { // try from 04f59a00 to 04f59a0b has its CatchHandler @ 04f59cf0 */
  (**(code **)(*plVar4 + 0x200))(plVar4,userId,*(undefined8 *)(*plVar4 + 0x208));
                    /* try { // try from 04f59a18 to 04f59a2b has its CatchHandler @ 04f59cec */
  (**(code **)(*plVar4 + 0x210))
            (plVar4,method_1,0,(ulong)body & 0xffffffff,*(undefined8 *)(*plVar4 + 0x218));
                    /* try { // try from 04f59a38 to 04f59a43 has its CatchHandler @ 04f59cc4 */
  (**(code **)(*plVar4 + 0x200))(plVar4,cnonce,*(undefined8 *)(*plVar4 + 0x208));
                    /* try { // try from 04f59a50 to 04f59a5b has its CatchHandler @ 04f59cc0 */
  (**(code **)(*plVar4 + 0x200))(plVar4,pBStack128,*(undefined8 *)(*plVar4 + 0x208));
  aiStack160[0] = 0x90;
  lVar8 = *plVar4;
  if ((ulong)*(ushort *)(lVar8 + 0x126) != 0) {
    uVar9 = 0;
    puVar10 = (uint *)(*(long *)(lVar8 + 0xb0) + 8);
    do {
      if (*(IDisposable__Class **)(puVar10 + -2) == IDisposable__TypeInfo) {
        ppcVar5 = (code **)(lVar8 + (ulong)*puVar10 * 0x10 + 0x130);
        goto LAB_04f59ac8;
      }
      uVar9 = uVar9 + 1;
      puVar10 = puVar10 + 4;
    } while (uVar9 < *(ushort *)(lVar8 + 0x126));
  }
                    /* try { // try from 04f59aac to 04f59ad3 has its CatchHandler @ 04f59d28 */
  ppcVar5 = (code **)FUN_01f20fbc(plVar4,IDisposable__TypeInfo,0);
LAB_04f59ac8:
  (**ppcVar5)(plVar4,ppcVar5[1]);
  if (plVar3 != (long *)0x0) {
    lVar8 = *plVar3;
    if ((ulong)*(ushort *)(lVar8 + 0x126) != 0) {
      uVar9 = 0;
      puVar10 = (uint *)(*(long *)(lVar8 + 0xb0) + 8);
      do {
        if (*(IDisposable__Class **)(puVar10 + -2) == IDisposable__TypeInfo) {
          ppcVar5 = (code **)(lVar8 + (ulong)*puVar10 * 0x10 + 0x130);
          goto LAB_04f59b38;
        }
        uVar9 = uVar9 + 1;
        puVar10 = puVar10 + 4;
      } while (uVar9 < *(ushort *)(lVar8 + 0x126));
    }
                    /* try { // try from 04f59b1c to 04f59b43 has its CatchHandler @ 04f59d20 */
    ppcVar5 = (code **)FUN_01f20fbc(plVar3,IDisposable__TypeInfo,0);
LAB_04f59b38:
    (**ppcVar5)(plVar3,ppcVar5[1]);
  }
  bVar1 = aiStack160[0] != 0x90;
  pBVar11 = __return_storage_ptr__->hash;
                    /* try { // try from 04f59b8c to 04f59b9f has its CatchHandler @ 04f59d2c */
  plVar3 = (long *)thunk_FUN_01f2ac94(HMACSHA512__TypeInfo);
  HMACSHA512__ctor_1(plVar3,pBVar11,0);
  if (plVar3 == (long *)0x0) {
                    /* WARNING: Subroutine does not return */
                    /* try { // try from 04f59c94 to 04f59c97 has its CatchHandler @ 04f59cb8 */
    FUN_01f6f5e8();
  }
                    /* try { // try from 04f59bac to 04f59bbb has its CatchHandler @ 04f59cc8 */
  pBVar6 = (BackendTokenCalculator *)
           HashAlgorithm_ComputeHash_2(plVar3,auStack112._0_8_,0,auStack112._8_8_ & 0xffffffff,0);
  aiStack160[(int)(uint)bVar1] = 0xef;
  lVar8 = *plVar3;
  if ((ulong)*(ushort *)(lVar8 + 0x126) != 0) {
    uVar9 = 0;
    puVar10 = (uint *)(*(long *)(lVar8 + 0xb0) + 8);
    do {
      if (*(IDisposable__Class **)(puVar10 + -2) == IDisposable__TypeInfo) {
        ppcVar5 = (code **)(lVar8 + (ulong)*puVar10 * 0x10 + 0x130);
        goto LAB_04f59c28;
      }
      uVar9 = uVar9 + 1;
      puVar10 = puVar10 + 4;
    } while (uVar9 < *(ushort *)(lVar8 + 0x126));
  }
                    /* try { // try from 04f59c0c to 04f59c33 has its CatchHandler @ 04f59d24 */
  ppcVar5 = (code **)FUN_01f20fbc(plVar3,IDisposable__TypeInfo,0);
LAB_04f59c28:
  (**ppcVar5)(plVar3,ppcVar5[1]);
  pBVar7 = (BackendTokenCalculator_TokenInfo *)
           func_0x02687e60(auStack112,
                           BackendTokenCalculator_CacheWithCleaner_1_System_Byte__Dispose__MethodInf o
                          );
  *ppBStack136 = pBStack120;
  ppBStack136[1] = pBStack128;
  ppBStack136[2] = pBVar6;
  return pBVar7;
}

な、、なんやこれ。人間が読むには無理なのか、、

まず落ち着いていったん読んでみましょう。

Decompileされたコードの形式

UnityコードはC#で作成されてて、それをコンパイルしてまたデコンパイルした結果が上のコードでございます。まず、典型的なコードがあります。

まず真下pBStack120 = this;の真下にあるコードをご覧ください。

if ((bRam00000000072561c7 & 1) == 0) {
    thunk_FUN_01f299f8(0x3958);
    bRam00000000072561c7 = 1;
}

このコードはIl2Cppがメタデータを生成するコードで、すべて無視することができます。

次はconstructorです。

/* try { // try from 04f59974 to 04f59987 has its CatchHandler @ 04f59d50 */
plVar3 = (long *)thunk_FUN_01f2ac94(MemoryStream__TypeInfo);
MemoryStream__ctor_2(plVar3,uVar2,0);

ctorはconstructorを略したので、上のコードはハイレベル言語ではplVar3 = new MemoryStream(uVar2);的な感じです。

Method callはこんな感じです。

result = Class_Method((Class *)this, ...parameters);

エラーが発生した時はreturnまで行かずに他のとこにジャンプするので、/* WARNING: Subroutine does not return */っていうコメントが付いています。例えば、

if (plVar3 == (long *)0x0) {
                /* WARNING: Subroutine does not return */
                /* try { // try from 04f59c94 to 04f59c97 has its CatchHandler @ 04f59cb8 */
    FUN_01f6f5e8();
}

これはnull pointer exceptionです。

Decompileされたコードの分析

オオォ(☆・Д・★)ォオオ
これで全てを分かれるのか!!上のコードを再度見てみましょう。

method_00 = (MethodInfo *)timestamp;

え?

// BackendTokenCalculator_TokenInfo *__return_storage_ptr__
auStack112 = BackendTokenCalculator_GetHashDataCache
                         ((BackendTokenCalculator *)__return_storage_ptr__, // <--------- ?
                          (int)body + *(int *)(timestamp + 0x18) + *(int *)(userId + 0x18) +
                          *(int *)&cnonce->max_length +
                          *(int *)&(path._Cache_k__BackingField)->max_length + 0x10,method_00);

???

  (**(code **)(*plVar4 + 0x2a0))(plVar4,pBStack120,*(undefined8 *)(*plVar4 + 0x2a8));
                    /* try { // try from 04f599d0 to 04f599db has its CatchHandler @ 04f59cf8 */
  (**(code **)(*plVar4 + 0x200))(plVar4,timestamp,*(undefined8 *)(*plVar4 + 0x208));
                    /* try { // try from 04f599e8 to 04f599f3 has its CatchHandler @ 04f59cf4 */
  (**(code **)(*plVar4 + 0x2a0))(plVar4,version,*(undefined8 *)(*plVar4 + 0x2a8));
                    /* try { // try from 04f59a00 to 04f59a0b has its CatchHandler @ 04f59cf0 */
  (**(code **)(*plVar4 + 0x200))(plVar4,userId,*(undefined8 *)(*plVar4 + 0x208));
                    /* try { // try from 04f59a18 to 04f59a2b has its CatchHandler @ 04f59cec */
  (**(code **)(*plVar4 + 0x210))
            (plVar4,method_1,0,(ulong)body & 0xffffffff,*(undefined8 *)(*plVar4 + 0x218));
                    /* try { // try from 04f59a38 to 04f59a43 has its CatchHandler @ 04f59cc4 */
  (**(code **)(*plVar4 + 0x200))(plVar4,cnonce,*(undefined8 *)(*plVar4 + 0x208));
                    /* try { // try from 04f59a50 to 04f59a5b has its CatchHandler @ 04f59cc0 */
  (**(code **)(*plVar4 + 0x200))(plVar4,pBStack128,*(undefined8 *)(*plVar4 + 0x208));

0(:3 )〜 _('、3」 ∠ )_

なんかIl2CppInspectorとGhidraがすごく間違いました。タイプも全然違うし、無数のstackとかの訳わからない変数が分析を難しくします。

でも、情報を少しでも得ることはできます。

                    /* try { // try from 04f59974 to 04f59987 has its CatchHandler @ 04f59d50 */
  plVar3 = (long *)thunk_FUN_01f2ac94(MemoryStream__TypeInfo);
  MemoryStream__ctor_2(plVar3,uVar2,0);
                    /* try { // try from 04f59994 to 04f599a7 has its CatchHandler @ 04f59d30 */
  plVar4 = (long *)thunk_FUN_01f2ac94(BinaryWriter__TypeInfo);
  BinaryWriter__ctor_1(plVar4,plVar3,0);
  if (plVar4 == (long *)0x0) {
                    /* WARNING: Subroutine does not return */
                    /* try { // try from 04f59c84 to 04f59c87 has its CatchHandler @ 04f59cfc */
    FUN_01f6f5e8();
  }

これはplVar3 = new MemoryStream(uVar2); plVar4 = new BinaryWriter(plVar3);ですね。ですから

  (**(code **)(*plVar4 + 0x2a0))(plVar4,pBStack120,*(undefined8 *)(*plVar4 + 0x2a8));
                    /* try { // try from 04f599d0 to 04f599db has its CatchHandler @ 04f59cf8 */
  (**(code **)(*plVar4 + 0x200))(plVar4,timestamp,*(undefined8 *)(*plVar4 + 0x208));
                    /* try { // try from 04f599e8 to 04f599f3 has its CatchHandler @ 04f59cf4 */
  (**(code **)(*plVar4 + 0x2a0))(plVar4,version,*(undefined8 *)(*plVar4 + 0x2a8));
                    /* try { // try from 04f59a00 to 04f59a0b has its CatchHandler @ 04f59cf0 */
  (**(code **)(*plVar4 + 0x200))(plVar4,userId,*(undefined8 *)(*plVar4 + 0x208));
                    /* try { // try from 04f59a18 to 04f59a2b has its CatchHandler @ 04f59cec */
  (**(code **)(*plVar4 + 0x210))
            (plVar4,method_1,0,(ulong)body & 0xffffffff,*(undefined8 *)(*plVar4 + 0x218));
                    /* try { // try from 04f59a38 to 04f59a43 has its CatchHandler @ 04f59cc4 */
  (**(code **)(*plVar4 + 0x200))(plVar4,cnonce,*(undefined8 *)(*plVar4 + 0x208));
                    /* try { // try from 04f59a50 to 04f59a5b has its CatchHandler @ 04f59cc0 */
  (**(code **)(*plVar4 + 0x200))(plVar4,pBStack128,*(undefined8 *)(*plVar4 + 0x208));

このコードは

// pBStack120とpBStack128はわからないけど、、
// BinaryWriter plVar4;
plVar4.Write(timestamp);
plVar4.Write(version);
plVar4.Write(userId);
// ???
// (**(code **)(*plVar4 + 0x210))
//           (plVar4,method_1,0,(ulong)body & 0xffffffff,*(undefined8 *)(*plVar4 + 0x218));
plVar4.Write(cnonce);

こんな感じですよね。BinaryWriterMemoryStreamにタイムスタンプとかバージョンとかを書いて

  pBVar11 = __return_storage_ptr__->hash;
                    /* try { // try from 04f59b8c to 04f59b9f has its CatchHandler @ 04f59d2c */
  plVar3 = (long *)thunk_FUN_01f2ac94(HMACSHA512__TypeInfo);
  HMACSHA512__ctor_1(plVar3,pBVar11,0);
  if (plVar3 == (long *)0x0) {
                    /* WARNING: Subroutine does not return */
                    /* try { // try from 04f59c94 to 04f59c97 has its CatchHandler @ 04f59cb8 */
    FUN_01f6f5e8();
  }
                    /* try { // try from 04f59bac to 04f59bbb has its CatchHandler @ 04f59cc8 */
  pBVar6 = (BackendTokenCalculator *)
           HashAlgorithm_ComputeHash_2(plVar3,auStack112._0_8_,0,auStack112._8_8_ & 0xffffffff,0);

HMACSHA512でハッシュ化することですね。じゃ__return_storage_ptr__->hashはなんだ?

上のこのコード覚えていますか?

// BackendTokenCalculator_TokenInfo *__return_storage_ptr__
auStack112 = BackendTokenCalculator_GetHashDataCache
                         ((BackendTokenCalculator *)__return_storage_ptr__, // <--------- ?                                       // ...

実はBackendTokenCalculator_CalcTokenHashImplの一つ目のパラメータはBackendTokenCalculator_TokenInfo *__return_storage_ptr__じゃなくてBackendTokenCalculator *thisで、__return_storage_ptr__->hash*(this + 0x10)、つまりthis->_sessionKeyでした。

// types.cs

namespace Enlil
{
	public sealed class BackendTokenCalculator // TypeDefIndex: 11655
	{
		// Fields
		private const string COMMON_KEY = "..."; // Metadata: 0x0073D3DB
		private byte[] _sessionKey; // 0x10 <--------------------------- ここ
        // ...

		// Nested types
		[MessagePackObject] // 0x0201E82C-0x0201E840
		public struct TokenInfo // TypeDefIndex: 11656
		{
			// Fields
			public long ts; // 0x00
			public byte[] cnonce; // 0x08
			public byte[] hash; // 0x10 <------------------------------- ここ
		}
    }
}

でもこれをわかっていないことにして、分析を続きましょう。

Fridaでログを出力

わからないから、直接数値を見てみましょう。Fridaを使うと簡単にこの作業ができます。

pip install frida-tools
frida --version # 15.2.2

そしてFridaのリリーズページからfrida-server-15.2.2-android-arm64をダウンロードして、adbで入れてサーバーを開きます。(Root必要)

adb shell
device:/ $ su
device:/ # chmod +x /data/local/tmp/frida-server
device:/ # /data/local/tmp/frida-server

FridaはJavaScriptでスクリプトを作成することができます。frida_script.jsがそのスクリプトファイルだとすると、

frida -Uf jp.co.unbereal.enlil -l ./frida_script.js --no-pause

で実行できます。

みたいメソードは下の五つです。

  • ResetSessionKey: Session keyを計算するメソードです。Session keyがなんなのかわからないから、一旦ロギングします。
  • CalcTokenHashImpl
  • ComputeHash1: Session keyのハッシュ化をする関数です。
  • ComputeHash2: App tokenのハッシュ化をする関数です。
  • HMACSHA512__ctor_1: HMAC-SHA512のキーを見ることができます。
function awaitForIL2CPPLoad(callback) {
  const i = setInterval(function () {
    const addr = Module.findBaseAddress("libil2cpp.so");
    if (addr) {
      clearInterval(i);
      callback(+addr)
    }
  }, 0);
}

let il2cpp = null;

Java.perform(function () {
  awaitForIL2CPPLoad(function (base) {
    il2cpp = ptr(base);
    attachResetSessionKeyInterceptor();
    attachCalcTokenHashImplInterceptor();
    attachComputeHash1Interceptor();
    attachComputeHash2Interceptor();
    attachHashConstructorInterceptor();
  })
});

function attachResetSessionKeyInterceptor() {
  Interceptor.attach(il2cpp.add(0x04F58DA4), {
    onEnter: function (args) {
      this.instance = args[0];
    },
    onLeave: function () {
      const sessionKey = this.instance.add(0x10).readPointer().add(0x20);
      console.log("attachResetSessionKeyInterceptor, SessionKey");
      console.log(sessionKey.readByteArray(64));
    }
  })
}

function attachComputeHash1Interceptor() {
  Interceptor.attach(il2cpp.add(0x03E209D8), {
    onEnter: function (args) {
      this.instance = args[0];
      this.arg1 = args[1];
    },
    onLeave: function () {
      console.log("ComputeHash1HashUpdate @", this.arg1);
      console.log(this.arg1.add(0x20).readByteArray(64));
    }
  })
}

function attachComputeHash2Interceptor() {
  Interceptor.attach(il2cpp.add(0x03E20B8C), {
    onEnter: function (args) {
      this.instance = args[0];
      this.arg1 = args[1];
      this.arg2 = args[2];
      this.arg3 = args[3];
    },
    onLeave: function () {
      console.log("ComputeHash2HashUpdate @", this.arg1);
      console.log(this.arg1.add(0x20).readByteArray(128));

      console.log("offset", Number(this.arg2))
      console.log("count", Number(this.arg3))
    }
  })
}

function attachHashConstructorInterceptor() {
  Interceptor.attach(il2cpp.add(0x03e2439c), {
    onEnter: function (args) {
      this.instance = args[0];
      this.arg1 = args[1];
    },
    onLeave: function () {
      console.log("Hash key", this.arg1);
      console.log(this.arg1.add(0x20).readByteArray(64));
    }
  })
}

function attachCalcTokenHashImplInterceptor() {
  Interceptor.attach(il2cpp.add(0x04F598A4), {
    onEnter: function (args) {
      this.instance = args[0];
      this.timestamp = args[1];
      this.version = args[2];
      this.userId = args[3];
      this.method = args[4];
      this.path = args[5];
      this.body = args[7];
      this.cnonce = args[8];
    },
    onLeave: function () {
      console.log("Real timestamp", Math.floor(Date.now() / 1000));
      console.log("timestamp", Number(this.timestamp), "0x" + Number(this.timestamp).toString(16));

      function readString(pointer) {
        return pointer.add(0x20).readCString().slice(0, pointer.add(0x18).readInt());
      }

      console.log("VERSION @", this.version);
      console.log(readString(this.version));

      console.log("USER ID")
      console.log(Number(this.userId), "0x" + Number(this.userId).toString(16));

      console.log("METHOD @", this.method);
      console.log(readString(this.method));

      console.log("PATH @", this.path);
      console.log(readString(this.path));

      console.log("BODY @", this.body);
      console.log(readString(this.body));

      console.log("CNONCE");
      console.log(this.cnonce.add(0x20).readByteArray(8));

      const sessionKey = this.instance.add(0x10).readPointer().add(0x20);
      console.log("SessionKey");
      console.log(sessionKey.readByteArray(64));
    }
  })
}

ログを見てわかった結果が下の擬似コードです。

  • Session keyの計算:new HMACSHA512(COMMON_KEY).update(userSignatureNonce).digest(), userSignatureNonceはアカウントを作る時とアクセストークンをリフレッシュする時サーバーからもらう文字列。
  • App tokenのHashの計算:new HMACSHA512(sessionKey).update(timestamp as int64).update(version).update(userId as int64).update(path).update(body).update(cnonce), timestampはunix epochからの秒数
  • App tokenの計算:ts(timestamp), cnonce, hashをMessagePackでパッキング

最終トークン生成コードはDenoで作成しました。

import { Base64, Base64Url, Sha512 } from "../deps.ts";
import { CommonKey } from "./rest/topia.ts";
import { Hex } from "./util.ts";

const BYTE_SEQUENCE_0 = [0x83, 0xa2, 0x74, 0x73, 0xce];
const BYTE_SEQUENCE_1 = [0xa6, 0x63, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0xc4, 0x08];
const BYTE_SEQUENCE_2 = [0xa4, 0x68, 0x61, 0x73, 0x68, 0xc4, 0x40];

export interface DecodedToken {
  timestamp: number; // in seconds
  cnonce: Uint8Array;
  hash: Uint8Array;
}

export interface TokenInfo {
  userId: number;
  userSignatureNonce: string;
  sessionKey?: Uint8Array;
  timestamp: number;
  version: string;
  method: string;
  path: string;
  body?: string;
  cnonce?: Uint8Array;
}

export function decodeToken(token: string): DecodedToken {
  const bytes = Base64Url.decode(token);

  if (bytes.length !== 97) throw Error("invalid token length");

  const timestampBytes = bytes.slice(5, 9);
  const timestamp = Number(Hex.toBigInt(timestampBytes));

  const cnonce = bytes.slice(18, 26);
  const hash = bytes.slice(-64);

  return {
    timestamp,
    cnonce,
    hash,
  };
}

export function encodeToken(decodedToken: DecodedToken) {
  const bytes = new Uint8Array(97);

  const timestampSeconds = BigInt(decodedToken.timestamp);

  bytes.set(BYTE_SEQUENCE_0, 0);
  bytes.set(Hex.fromBigInt(timestampSeconds), 5);
  bytes.set(BYTE_SEQUENCE_1, 9);
  bytes.set(decodedToken.cnonce, 18);
  bytes.set(BYTE_SEQUENCE_2, 26);
  bytes.set(decodedToken.hash, 33);

  return Base64Url.encode(bytes);
}

export function computeToken(
  tokenInfo: TokenInfo,
  stage: "production" | "edge" | "staging" | "dev" = "production",
) {
  const COMMON_KEY = Base64.decode(
    CommonKey[stage],
  );

  const cnonce = tokenInfo.cnonce ?? crypto.getRandomValues(new Uint8Array(8));
  const { userId, userSignatureNonce, timestamp, version, method, path, body } =
    tokenInfo;

  const sessionKey = tokenInfo.sessionKey ?? new Uint8Array(
    new Sha512.HmacSha512(COMMON_KEY).update(userSignatureNonce).digest(),
  );

  const hash = new Uint8Array(
    new Sha512.HmacSha512(sessionKey)
      .update(new Uint8Array(new Uint32Array([timestamp, 0]).buffer))
      .update(version)
      .update(new Uint8Array(new Uint32Array([userId, 0]).buffer))
      .update(method)
      .update(path)
      .update(body ?? "")
      .update(cnonce)
      .digest(),
  );

  const decodedToken: DecodedToken = {
    timestamp,
    cnonce,
    hash,
  };

  return encodeToken(decodedToken);
}

ヽ(。ゝω·。)ノ

結論

  • apktool, Il2CppInspector, Ghidra, Fridaを使いました。
  • Unity Androidアプリはこうreverse engineeringすることができます。
  • クライアントから生成できるトークンはセキュリティとは言えません。

参考資料

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
ユーザー登録ログイン

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
0
どのような問題がありますか?
ユーザー登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
ユーザー登録ログイン
ストックするカテゴリー