はじめに
カラオケ配信アプリ「トピア」のことは知っていますか?いろんな人と歌ったり話したりバーチャルアバターで遊んだりできる楽しいアプリです!
アプリは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からはこれを改善し、ユーザー情報が入るように変わりました。また、トークンが無効になるまでの時間が何秒くらいに非常に短くなりました。ですから公式アプリからトークンをもらってそのまま使えなくなりました。
では方法が全くないのか?!当然、違います。全てのコードはクライアントの中にあるため、それをわかってゼロからトークンを作るとリクエストを送るのができます。
Apktoolでapkファイルをdecode
トピアv4.20.0のapkファイルをダウンロードして、apktoolでdecodeしてみましょう。
apktool d トピア_4_20_0.apk
トピアはUnityアプリですから、JavaまたはKotlinコードは少ないです。その代わりにC#のコードがcompileされたbinaryが入っています。
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
ヤッタ~! イエ━━٩(*´ᗜ`)ㅅ(ˊᗜˋ*)و━━イ
il2cpp.py
, types.cs
, metadata.json
が生成されました。
types.cs
から暗号化コードを探す
types.cs
でToken
なんとかで検索すると、なんかぽいクラスを見つかることができます。
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コード恐怖症を持っている方はご注意ください。
-
brew install openjdk@11
でOpenJDK 11をインストールしてPATHに入れます。 -
brew install ghidra
でGhidraをインストールしてghidraRun
で開きます。 - プロジェクトを生成して
libil2cpp.so
をインポートします。 -
File -> Parse C Source
からcpp/appdata/il2cpp-types.h
をインポートしてオプションを-D_GHIDRA_
に設定します。 -
Parse to Program
ボタンを押します. - Script managerを開いて
il2cpp.py
ファイルを読み込みます。 -
il2cpp.py
を実行して待ちます。 -
Analysis -> Auto Analyze libil2cpp.so
を押して、我慢して待ちます。
JDKのバージョンは大事です。今の時点でJDK 11が必要です。
左のSymbol Treeから関数BackendTokenCalculator_CalcTokenHashImpl
を探します。
そうすると…
/* 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);
こんな感じですよね。BinaryWriter
でMemoryStream
にタイムスタンプとかバージョンとかを書いて
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することができます。
- クライアントから生成できるトークンはセキュリティとは言えません。
コメント