CRACKL @ B Contest2010。最初のタスクの分析



2010年は終了し、リソースの世界的な改革が進行中でした。 これらは問題を抱えた時代でした。 そして、この厳しい時期に、ローカルトーナメントを作成するというアイデアが浮上しています。 このアイデアは、地域コミュニティによって非常に喜んで受け入れられました。 しばらくして、3つのタスクが作成され(5つが計画されていました)、審査員と評価システムが選択されました。 そして、それが始まった。



まえがき


最初の割り当ては、 PE_KillによるKeyGenMeでした。 タスクは、1つの名前に対して多くのキーを生成するKeyGenを作成することでした。 著者の元の説明は次のとおりです。

これがKeyGenMeです。 名前はそれ自体を物語っています-キージェネレータを書く必要があります、

KeyGenMeで正常にテストされます。 どんな名前でも、ジェネレータは

多くの一意のシリアル番号を提供します。 KeyGenMeには、簡単な自家製が詰め込まれています

パッカー。



警告したいです。 KeyGenMeは見かけほど複雑ではありませんが、簡単ではありません

また、非標準的なアプローチが必要です。 小さなプラスは開梱されて提供されます

KeyGenMeオプション。



頑張って




分析


説明からわかるように、keygenmyは自家製のパッカーで覆われているため、一部のウイルス対策ソフトウェアは強制的に誤検知を発生させます。 まあ、私はそれを気にせず、 QUの助けを借りてそれを削除しました。 次に、解凍​​したファイルをOllyDbgで開き、EP(エントリポイント)のコードで、keygenmyがDelphiで記述されていることを確認しました。 これを知って、私はkeygenをIDR( Interactive Delphi Reconstructor )に固執することにしました。これにより、VCLおよびDelphiランタイム関数が認識されます。 これは、次の2つの場合にのみ必要です。



ファイルの完全な分析が完了したら、.mapファイルを作成します。





そして、ボタンクリックハンドラーのアドレスを見つけます。





OllyDbgで.mapファイルを使用するには、 mapimpプラグインをインストールする必要があります。 デフォルトでは、.mapファイルはプログラムが開かれたのと同じフォルダーに、プログラムと同じ名前で保存されます。すべてが正しく行われ、プラグインがインストールされている場合、Olkでkeygensを開くと、.mapファイルをダウンロードするように求めるメッセージが表示されます「はい」とすべてが順調です。 何か問題がある場合は、メインメニューから手動でダウンロードできます。

すべての準備が完了しています。 OllyDbgに移動して、チェックボタンをクリックするための処理関数のアドレスに移動します。このボタンは、IDRで記憶/書き込み/コピーする必要がありました。 そして、最初に名前とシリアルがどのように取得されるかを確認します(それほど重要ではありません)が、最も興味深い部分が続きます。



つまり、TKyeEngineクラスのインスタンスであるTKeyEngine.Createは、物事の論理に従って作成されます。このクラスは、回路全体の中核です。 2つの関数がそれに続きます; 2番目の結果は、呼び出しの直後に続く条件の1つに関係します。 ここでそれらはすべて結論付けられています。 そのため、それぞれについてもう少し詳しく説明します。



最初の機能では、さらに別の取るに足りないものに遭遇します。これは、分析を複雑にします。これは小さな難読化であり、スクリプトのマスクで簡単に削除できます。



また、子関数でさらに深くなります。 したがって、マスクを検索して最初の会議の住所を見つけたところです。この住所が出発点になりました。 スクリプトでは、ここから最後の夜明けまで、これらすべてのセクションがあふれるまで検索します(実際には長い時間、数分、まあ、単一の操作を最適化するための計算はありませんでした)。

MOV addr, 0046BED9 @loop: FIND addr, #EB033BC?7?# CMP $RESULT, 0 JE @exit MOV addr, $RESULT FILL addr, 5, 90 ADD addr, 5 JMP @loop @exit: RET
      
      





さて、この難読化を満たす前に、いくつかのアクションがありました。これは、後続の「単純な」操作の名前で16バイトの配列が作成され、分析する必要のあるコードがあまりない心の弱い人がいたことですホラー。 しかし、「悪魔は彼が描かれているほどひどいものではありません。」 より詳細な分析により、セマンティックの深い負荷がかからないことが明らかになりました。 私はこの大きなループについて、10001 * 16回の繰り返しでスイッチを使用しています。



リッピングするのに十分です。 確かに、このリッピングされたコード部分は12,000行を超えますが、難読化解除後にノードからコードをクレンジングすると、6k行弱が残ります。 しかし、それをリッピングする前に、より正確な形にすることができます。 なぜなら 回路全体がクラスのインスタンスで機能するため、この配列は、このインスタンスへのポインターを基準にして5バイトのシフトがある内部のどこかにあります。 したがって、フリルなしで配列のみをリッピングしたコードに転送するには、すべてのオフセットを修正する必要があります。 このために、私はこのスクリプトを書きました。

 MOV addr, 0046BED9 @loop: OPCODE addr CMP $RESULT_2, 3 JB @next MOV temp, addr ADD temp, $RESULT_2 SUB temp, 2 MOV val, [temp], 1 CMP val, 43, 1 JE @ok CMP val, 53, 1 JNE @next @ok: INC temp MOV val, [temp], 1 SUB val, 5 MOV [temp], val, 1 @next: ADD addr, $RESULT_2 CMP addr, 004707A1 JA @exit JMP @loop @exit: RET
      
      





その後、すべてをリッピングできます。 asm insertの助けを借りてすべてを詰め込むことは可能でしたが、許容できる時間内に解決できない問題がいくつかあったため、これをすべて別の関数としてasmファイルに入れてプロジェクトに接続し、すぐにすべての問題を解決しました。 リッピングされたコードにはさらに2つのつまずきがあります。サイズの1バイトのローカル変数を使用し、スタックの先頭ではなく、間違った場所にあり、ここではebp-5、これをすべて置き換えることができますが、再び最小のパスに沿って行きましたレジスタンスは、ローカルのニーズ(アライメントなど)に8バイトのスタックを割り当てました。 2つ目のつまずきは、別の関数への呼び出しがまだあるということですが、それは小さな2つのチームで、手動でリッピング/リライトし(どちらが速いかさえわかりません)、呼び出し名を自分のものに置き換えます。 それがすべて、1/4遅れています。



そこで、名前だけで機能する最初の関数の分析を終了しました。2番目の関数は、シリアルとそのシリアルから、そして最初の関数からのデータで既に動作しています。 2番目の機能を入力するとすぐに、入力したシリアルとその変換の有効性のチェックが行われます。



1つ目は、その中のハイフンの長さと位置のチェックです。これにより、シリアルが次の形式であることがわかります。

XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXX-XXXXXX





次に、ハイフンが切り取られ、結果の文字列がデコードされます。 デコード機能は標準アルゴリズムであり、すぐにはわかりませんでした。 重要な手がかりは、シリアルの文字がアルファベットと照合されることです。



このアルファベットをグーグルで調べてみると 、それがデコード関数のBase32であることがわかりました。 その後、サイズが25バイトの配列があり、3つの部分に分割されます。最初の配列は16バイトの配列、2番目は1バイトの値、3番目は8バイトの配列です。



 typedef struct _KEY { unsigned char SomeBytes[16]; unsigned char SomeCount; unsigned int CipherText[2]; } KEY, *LPKEY;
      
      





構造の3番目の要素を見るとわかるように、意味のある名前があります。先を見据えて、最新のテストで参照文字列と比較される暗号化された文字列があるはずです。 これで、シリアルの検証とコンポーネントへの分解が完了しました。 この関数を終了するとき、KEY.SomeCountが32より大きいかどうかのチェックがあり、そうであれば、次に進みます。 次に、スワップがWORD'S KEY.SomeBytesと名前から受け取った配列をたどる小さなサイクルです。コードを拡張するのに役立ちます。 論理的な意味はありません 次に、同じ配列とそれに対応する要素を使用します。最初の要素は最初の要素、2番目は2番目の要素などです。

そして、別のスイッチが来ます。 最初の人がすべての欲求を撃退しなかった人のために、2番目の人はさらに多くの人を除草しました。 それ自体は簡単です。各バイト値に対して、同じプロトタイプに従って関数が呼び出されますが、ここでは関数はすべての人にとって異なります。



この場所からより詳細に。 そのため、名前から取得した配列がゲームに戻ります(別の機能にさらに参加すると確信しています)。 バイトの値とアクションが実行されることに応じて、一種のpcodeとして機能します。 KEY.SomeBytes配列の要素は、これらの関数への入力値として機能します。 そして、それが送信されるそれらの関数は同様の単​​純なロジックを持ち、それに供給されるバイトはビットごとに解析され、このビットまたはそのビットが設定されているかどうかに応じて、一連の小さなアクションが単一バイトで実行されます。



そのような各関数には各ビットに独自のアクションがありますが、最も重要なことは、特定のビットが1つある場合、これらすべての操作の結果がKEY.CipherTextの特定の位置に配置され、8(t .e。8つの置換がKEY.CipherTextで行われた)、エラーで関数を終了します。 操作ごとに増加する別のカウンターもあります。 最初のビットを除いてビットが設定されている場合、バイトを取得すると、これらのジェスチャーがすべて発生します。 これらすべてを分析すると、これらすべての操作を完全に無視できる脆弱性が明らかになりました。 KEY.CipherTextへの書き込みを担当するビットが設定されていない場合、KEY.CipherTextは変更されないという事実にあります。これは非常にクールです。これにより、設定した方法でKEY.CipherTextを元の形式のままにすることができます。



つまり、このビットが考慮されないバイト値ごとにマスクテーブルを作成する必要があります。 また、最初にバイトが取得されるビットが設定されないようにする必要があります。これは、2番目のカウンターのビットを簡単にカウントするために必要です(1回のチェックに必要です)。 各バイト値のビットを見つけるこのルーチンを回避するために、必要なビットを見つけ、それらから必要なマスクを作成してテーブルに書き込む小さなスクリプトを作成しました。

 MOV addr, 004707FC ALLOC 100 CMP $RESULT, 0 JE @error MOV table, $RESULT PUSHA MOV c, 0 @loop: FIND addr, #751190909090900FB643260FB6440326884304# CMP $RESULT, 0 JE @exit MOV cl, [$RESULT - 1], 1 MOV addr, $RESULT ADD addr, 13 FIND addr, #751E90909090900FB643260FB65304885403269090909090FE4326# CMP $RESULT, 0 JE @exit MOV bl, [$RESULT - 1], 1 OR bl, cl NOT bl MOV [table + c], bl, 1 MOV addr, $RESULT ADD addr, 20 INC c CMP c, 100 JE @exit JMP @loop @exit: POPA DM table, 100, "table.bin" FREE table, 100 @error: RET
      
      





このサイクルの後、ビットカウンターは値KEY.SomeCountと比較され、それらが等しい場合、すでにフィニッシュラインにいます。 次に、KEY.CipherTextがデータとして転送され、キーとして名前を持つ配列への関数呼び出しが行われます。内部には、目的の関数に到達するまでさらにいくつかのエントリがあります。デルタが使用される場所まで、 XTEAのあるものであると判断されました。 判明したように、元のアルゴリズムはわずかに変更されています。



画面からわかるように、各ステップを分解して、変更が行われた場所を見つけました。 それは、ブラケットの位置が2つの場所で変更されたという事実から成りました。 優先アクション。

オリジナル:

 void xtea_decipher(unsigned int num_rounds, uint32_t *v, uint32_t const *k) { unsigned int i; uint32_t v0=v[0], v1=v[1], delta=0x9E3779B9, sum=delta*num_rounds; for (i=0; i < num_rounds; i++) { v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + k[(sum>>11) & 3]); sum -= delta; v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + k[sum & 3]); } v[0]=v0; v[1]=v1; }
      
      





変更済み:

 void xtea_decipher(unsigned int num_rounds, uint32_t *v, uint32_t const *k) { unsigned int i; uint32_t v0=v[0], v1=v[1], delta=0x9E3779B9, sum=delta*num_rounds; for (i=0; i < num_rounds; i++) { v1 -= ((((v0 << 4) ^ (v0 >> 5)) + v0) ^ sum) + k[(sum>>11) & 3]; sum -= delta; v0 -= ((((v1 << 4) ^ (v1 >> 5)) + v1) ^ sum) + k[sum & 3]; } v[0]=v0; v[1]=v1; }
      
      







だから、すべてが渡され、フィニッシュラインを越えるために残っています。 その代わりに、解読後のデータとCRACKLABという言葉が比較されます。これは、この言葉を暗号化する必要があることを意味します。

それだけです、keygenを書きます。



まとめ


keygenを作成するには、名前付き配列を作成するコード、Base32Encodingの実装、変更されたXTEAの実装、マスクテーブルを作成し、コードにコードを追加する必要があります。 まず、テーブルのダンプをコードに挿入できる配列に変換します。誰でも適切と思うように実行できます。 シリアル化生成アルゴリズムの形式は次のとおりです。

  1. 名前=> name_array [16]
  2. モーフ(name_array)
  3. KEY.SomeBytes [i] =ランダム()&テーブル[name_array [i]]
  4. KEY.SomeCount = bitscount(KEY.SomeBytes)
  5. KEY.CipherText = xtea_encode(「CRACKLAB」)
  6. SerialBase = Base32Encode(キー)
  7. シリアル= InsertDefis(SerialBase)




私のkeygen: github.com/reu-res/CRACKLAB-Contest-2010



次の記事では、2番目のタスクを分析します。これはおそらくこれよりも興味深いでしょう。 ネタバレ注意! そこでは、VMの議論があります(著者はそう言っています)。その実装は、構造が互いに類似している通常の実装とは異なります。 じゃあね



All Articles