ゲームLimboの防衛に関する研究。 Keygen









みなさんこんにちは。 多くの人がこの素晴らしいゲーム-LIMBOを知っています! おそらくSteamで購入するか、トレントからダウンロードしました...

私もそれを一度購入しました( これをお勧めします! )、そして通り抜けました)。 しかし、いつものように、これは私には十分ではありませんでした、そしてスポーツの興味から、私はその保護を研究することにしました。 そのため、ゲームLIMBOにはkeygenがありました。



この記事では、私がそれをどのように行ったかを説明します。



始める前に、覚えておいてください:あなた自身の危険とリスクで実行するすべてのアクション。 ゲーム開発者の仕事を尊重します。




ステージ1:患者の検査


ゲームの完全インストーラーはこちらからダウンロードできます 。 ゲームをインストールした後、まず、いつものように、メインの実行可能ファイルが書き込まれているものを見つけます。 このExeInfo PEに使用します。











Visual Studio 2008が表示されます。 IDA Proは素晴らしい仕事をしているので、そこに送ります。 IDA Pro + HexRaysの束を使用します。 デコンパイラを使用して-作業をスピードアップします。



第二段階:私たちは何を探していますか?


まず、ゲームのメインの実行可能ファイルであるlimbo.exe 分析するためのIdeaを提供しましょう。



次に、実際にここで見つけたいものを正確に判断する必要があります。 ゲームを実行します。











魔法の碑文「 アンロックフルゲーム 」が表示されます。 クリックしてください。 次に、予想外の人が期待しています(少なくとも、このメニュー項目を最初に選択したとき、ゲームのグラフィックエンジンの入力フィールド、またはそのようなものが表示されると予想していましたが、はるかに簡単でした...):











はい、はい! それは普通のウィンドウです! 私たちにとっては簡単です。 何かを入力して、 Unlockを押してみましょう。 このようなもの:











それでは、 IDAのテキストを見て、それを基にして確認する場所を見つけましょう。 そして、残念なことに私を待っていました...

エラーウィンドウのメッセージのテキストによると、 Idaは私にとって何も見つけられませんでした! Total Commanderによるコンテンツ検索でも同じことがわかりました。 メッセージは暗号化されている場合があります。 MessageBoxA / W呼び出しに基づいてウィンドウ呼び出しを見つけようとすることができます。 しかし、私は別の方法で行ったが、それは何らかの理由でいくつかの記事で説明されている。



ステージ3:クリックミー


次のように進みます。 便利なリソースエディタを開き、exeワーカーをそこにドラッグし、キー入力ダイアログのウィンドウを見つけて、その中に[ ロック解除 ]ボタンを追加します。 すぐに言ってやった:











画面 、ボタンのIDを強調表示しました。 それにより、クリックが処理される場所を正確に検索します。 Idaを開き、 Alt + I検索->イミディエート値 ...) 押し、数値203 (10進数なので0xなし)を入力し、何が起こるかを確認します。 そして、これが見つかりました:











Idaがマークした行を参照してください nIDDlgItem ? それらから始めましょう。 これらの結果の最初をダブルクリックします:











緑の矢印を使用して、 Idaが指し示した場所と少し下を指定します( 習慣:目的の場所の上/下にスクロールします )-矢印は、1つの興味深いAPI関数GetDlgItemTextAを呼び出す場所を示します。 MSDNによると、この関数は、バッファ内の指定されたウィンドウ要素のテキストを受け取ります。



入力フィールドIDですぐに検索しなかったのはなぜですか? もちろん、そうすることは可能でした。 しかし、ボタンが押された後、テキストがフィールドから差し引かれる前であっても、あなたは何が起こるかわかりません。


それで、受信したシリアルがどこに行くかを追跡しましょう。 リストをスクロールして、API関数が呼び出された場所全体を確認します。











私の " soapy "の外観は、受信したバッファー( Idaが var_134として指定した)がGetDlgItemTextAの呼び出しに続いて関数に直接渡されることを示しています。 推測を確認しましょう...



ステージ4:逆コンパイル


関数に入ります。 そこに別のアドレスへのジャンプがあります-それに行きます。 通常のコードが表示されるため、そこでF5をクリックしてください( HexRays Decompilerを呼び出します)。



逆コンパイル結果
bool __cdecl sub_48D410(int a1) { int v1; // esi@7 char *v2; // ebp@7 unsigned int v3; // edi@7 int v4; // ST28_4@9 int v5; // edx@9 int v6; // eax@12 bool result; // al@12 char v8; // [sp+4h] [bp-44h]@8 char v9; // [sp+Ch] [bp-3Ch]@12 char v10; // [sp+1Ch] [bp-2Ch]@7 char v11; // [sp+3Ch] [bp-Ch]@12 if ( strlen((const char *)a1) != 37 || *(_BYTE *)(a1 + 5) != 45 || *(_BYTE *)(a1 + 11) != 45 || *(_BYTE *)(a1 + 17) != 45 || *(_BYTE *)(a1 + 23) != 45 || *(_BYTE *)(a1 + 30) != 45 ) { result = 0; } else { v1 = 0; v2 = &v10; v3 = 0; do { v8 = *(_BYTE *)(v3 + a1); if ( v8 != 45 ) { v4 = (char)sub_412EBD(v8); v1 += v4 << 5 * (3 - v5); if ( v5 == 3 ) { v2 += sprintf(v2, "%05lx", v1); v1 = 0; } } ++v3; } while ( v3 < 0x25 ); v6 = sub_40C48C(&v10, 32); sprintf(&v9, "%08x", v6); result = strcmp(&v9, &v11) == 0; } return result; }
      
      







これで、このコードをより適切なものにすることができます。



まず、入力パラメーターの型がintであることがわかりますが、これは完全に真実ではありません。 それを「 char * 」として指定しましょう。 これを行うには、関数の名前になり、そこでYアイテムタイプの設定 )キーを押します。 入力パラメーターのタイプと名前を修正します( キーと呼びます )。



次...次の行が表示されます。



  if ( strlen(key) != 37 || key[5] != 45 || key[11] != 45 || key[17] != 45 || key[23] != 45 || key[30] != 45 )
      
      





なぜなら 入力パラメータは文字列です。キーの文字が数字と比較される場所で、文字との比較のために修正します。 これを行うには、これらの各番号でRChar )を押します。 すでに良い:



  if ( strlen(key) != 37 || key[5] != '-' || key[11] != '-' || key[17] != '-' || key[23] != '-' || key[30] != '-' )
      
      





ループ:



サイクル番号1
  v1 = 0; v2 = &v10; v3 = 0; do { v8 = key[v3]; if ( v8 != 45 ) { v4 = (char)sub_412EBD(v8); v1 += v4 << 5 * (3 - v5); if ( v5 == 3 ) { v2 += sprintf(v2, "%05lx", v1); v1 = 0; } } ++v3; } while ( v3 < 0x25 );
      
      







明確にするために、 v3に i という名前を付けます。 イテレータとして使用されているようです。 名前キーNName )を押して名前を変更します。



ループでは、各文字がキーから取得され、まだ未知の関数に転送されていることに気付きます。 この関数が何であるかを知ることを提案します。 それをダブルクリックします。 そこに別の関数の呼び出しがあります。 そして、ここにあります-単一のキャラクターを処理します! ( Rキーには多くの作業がありますが、処理結果はすぐに表示するだけです)。



Convert_char関数
 char __cdecl convert_char(char C) { char _C; // al@1 _C = C; if ( C < '0' ) return -1; if ( C <= '9' ) return C - '0'; if ( C < 'A' ) return -1; if ( C <= 'Z' ) { if ( C != 'I' && C != 'L' && C != 'O' && C != 'U' ) { if ( C >= 'U' ) _C = C - 1; if ( _C >= 'O' ) --_C; if ( _C >= 'L' ) --_C; if ( _C >= 'I' ) --_C; return _C - '7'; } return -1; } if ( C < 'a' || C > 'z' || C == 'i' || C == 'l' || C == 'o' || C == 'u' ) return -1; if ( C >= 'u' ) _C = C - 1; if ( _C >= 'o' ) --_C; if ( _C >= 'l' ) --_C; if ( _C >= 'i' ) --_C; return _C - 'W'; }
      
      







いいね! ここで、 Escキーを使用してメイン関数に戻ります。 IDA自体が、シンボル処理関数によって返される結果のタイプを再定義したことに気付きました。 さらに名前を付け、型を指定して、次のループコードを取得します。



サイクル番号2
  sum = 0; x5buf = v10; i = 0; do { C = key[i]; if ( C != '-' ) { new_c = j_convert_char(C); sum += new_c << 5 * (3 - itr); if ( itr == 3 ) { x5buf += sprintf(x5buf, "%05lx", sum); sum = 0; } } ++i; } while ( i < 0x25 );
      
      







気づいたら、興味深い逆コンパイラのバグが1つあります。 itrとして指定された変数は、まったくインクリメントされないことがわかります。 実際に何が起こるかを調べるには、 RMB- > Copy to assemblyをクリックし、 itr使用されている場所を確認します。 私たちが見つけたのは、このサイクルで直接増分され(予想されていた)、サイクルの前にリセットされるということです。 keygenを作成するとき、これを考慮します。



ここで、キー検証関数の2番目の部分...まだCRC32カウント関数に非常によく似ている未探索の関数が1つあります。 処理の結果(急いでいますが、読みやすい):



crc32
 int __cdecl calc_crc32(char *my_key, int len) { int i; // ebp@1 unsigned int _xor; // ecx@1 char *_my_key; // edi@2 char C; // al@3 signed int mask; // edx@3 int B; // esi@3 bool bit; // al@4 int crc32; // eax@10 signed int _len; // edx@10 i = len; _xor = 0xFFFFFFFF; if ( len ) { _my_key = my_key; do { C = *_my_key; --i; ++_my_key; mask = 1; B = (unsigned __int8)C; do { bit = (_xor & 0x80000000) == 0x80000000; _xor *= 2; if ( B & mask ) bit = bit == 0; if ( bit ) _xor ^= 0x4C11DB7u; mask *= 2; } while ( (_BYTE)mask ); } while ( i ); } crc32 = _xor & 1; _len = 0x1F; do { _xor >>= 1; crc32 = _xor & 1 | 2 * crc32; --_len; } while ( _len ); return ~crc32; }
      
      







残りの部分(変換後):

  crc32 = j_calc_crc32(my_key, 32); sprintf(crc32_, "%08x", crc32); result = strcmp(crc32_, &my_key[32]) == 0;
      
      







ステージ5:keygenの作成


目的:逆関数を記述するために、キーで正確に何が起こったかを判断する。 私はDelphiで常識に反してHexRaysを発行しますが、より簡単な言語で書くことができます。



デバッグすることにより、何が起こったのかがわかります。

  1. ゲームにはハイフンのない32文字のキーが必要です(ハイフンのある37 文字 )。
  2. キーから4文字が取得されます(ハイフンは考慮されません)。 それぞれがconvert_char関数を介して渡され、次の公式によって合計されます 。sum + = new_c << 5 *(3-itr) ;
  3. そのような量はそれぞれ小文字の 16進文字列( 5文字 )に変換され、既存の文字列(合計40文字 )に接着されます。
  4. CRC32は 、結果の文字列の最初の32文字から取得され、前の段落で受信した文字列の残りの8文字と比較されます。
  5. 行が一致しない場合、キーは正しくありません。




逆思考:

  1. 40文字のハッシュをキーに戻す関数を作成します。
  2. 32文字のハッシュを生成します。
  3. 8からカウント- 文字 CRC32;
  4. ステップ( 2 )および( 3 )で取得した線を接着します。
  5. 関数に渡す( 1 )-目的のキーを取得します。




変換機能に関する考え:

  1. なぜなら 入力ハッシュは8つの 5文字のハッシュチャンクから取得されました。「 ファイブ 」に従って同じ方法で処理します。
  2. 各「 5 」は、 4つのキーキャラクターから取得されました。
  3. なぜなら 「 5 」の各計算中に、 5ビットを左にシフトしキーの各文字に 5ビットがあることがわかります
  4. convert_char関数コードを注意深く調べると、キーの文字セットは「 0123456789ABCDEFGHJKMNPQRSTVWXYZ 」の文字のみに制限されるという考えに至ります
  5. 合計: 32個のハッシュ文字がファイブ 」から生成されます。 32%5 = 24の整数文字2の残り -つまり 再生成する必要がある2つの文字。


ハッシュ生成関数の最終バージョン(Delphi)
 function GenHash(len: byte): string; var i, k: byte; sum: integer; begin Randomize; Result := ''; sum := 0; k := 0; for i := 1 to len do begin sum := sum + (Random(Length(alphabet)) shl ((3 - k) * 5)); Inc(k); if k = 4 then begin Result := Result + AnsiLowerCase(Int2Hex(sum, 5)); sum := 0; k := 0; end; end; Result := Result + 'a0'; //   ,       end;
      
      







次に、ハッシュからCRC32を検討します。

 var key, hash, crc32: string; begin hash := GenHash(24); crc32 := Crc32b(hash); //    ,   -
      
      





トランス機能コード:

ハッシュからコードへのコンバーター関数
 function Hash2Code(const Hash: string): string; var s: string; five: integer; begin Result := ''; s := Hash; while Length(s) > 0 do begin five := Hex2Int(Copy(s, 1, 5)); Delete(s, 1, 5); Result := Result + alphabet[(five and $F8000 shr 15) + 1] + alphabet[(five and $07C00 shr 10) + 1] + alphabet[(five and $003E0 shr 05) + 1] + alphabet[(five and $0001F shr 00) + 1]; end; end;
      
      









そして最後に、結果のライセンスキーの取得:



  key := Hash2Code(hash + crc32); lic_code := Format('%s-%s-%s-%s-%s-%s', [Copy(key, 1, 5), Copy(key, 6, 5), Copy(key, 11, 5), Copy(key, 16, 5), Copy(key, 21, 6), Copy(key, 27, 6) ]);
      
      





確認し、... 生成されたキーを入力するゲームがアクティブになり、アクティブ化ポイントが消えました!



まとめ


keygenを作成する際の主なことは、考え直すことができるようにすることです。 つまり あなたが持っているものの逆になるようなアルゴリズムを書くことができます。 これは簡単な作業ではありませんが、ほとんどの場合解決できます。



PS


記事が乱雑すぎたのかもしれませんが、わかりません。 私が伝えたかった主なアイデア:keygenは、あなたが頭脳を持ち、忍耐とともに欲望を持っているなら、それほど難しいことではありません。



PPS


次の記事では、別のゲームであるUnepicに keygenをどのように作成したかを説明します。



All Articles