ブラックボックス方式を使用した商用保護をバイパスし、リネージュ2のパケットハックを記述します

プロローグ



Tフォーラムの友人の1人が、尊敬されるxkorの著者として、不正行為の世界全体に知られているl2phxプログラムを書き換えることを提案したとき、それはすべて1年前に始まりました。

L2phx自体(l2パケットハック、パケット、スラム)は、系統2クライアント(他のmmorpgのバージョンがあります)の着信および発信パケット(すべてがLSPで実装されている)のスニファーであり、個々のパケットを送信/置換する機能を備えています。 Xkorは次のように試みました:暗号化バイパスメソッドの実装、美しいGUIなど。 しかし、frisheksの悪意のある管理者はそのようなアプリケーションを好まなかった。それは次の1日のイベントの開始時に彼らの収入を著しく殺した。 はい、はい、名前以外の人がどのサーバーにもアクセスして、このツールで完全な乱交を手配することができました。 同時に、あらゆる種類の商用保護があり、パケットの使用を正常にブロックできませんでしたが、それらの中で最も巧妙なトラフィックはさらに暗号化されています。 このような保護の1つは、S。保護に対応しています。今日、S保護はすべてのトップリネージ2サーバーにあります。ところで、xkorはそのような結果を提供し、パケット復号化モジュール(newxor.dll)を独自に作成する機能を実装しました。 はい、それを書いただけでは合理的ではありませんでした:新しいサーバー==新しいnewxor。 初心者はクライアントのメモリ(HxD、チートエンジンなど)を変更してパケットを送信できなかったため、l2での不正行為は次第に死に始めました。



それから、私はこのベンチャーをあまり真剣に受け止めませんでした:私はクライアント->サーバーパケットキャプチャモジュールを書き、それを放棄しました。 なんで? なぜなら。 しかし、ほんの3日前、私はこのプロジェクトの作業を再開し、この記事を公開することにしました。 なんで? 現在、l2詐欺師コミュニティは死んでいます。 すべてのバグと洗濯機は、SkypeとフォーラムTで互いに通信する10人の手にあります。そして、私も去ることにしました。 そして、あなたが去るなら、それは美しいだけです))2年前、私はワーキングバッグを夢見ましたが、今日はそれを必要としません。



免責事項



9月はその年齢を考慮して非常にすぐに燃え上がります。つまり、9月1日にモスクワの学校の1つで昨年に会います。 学校の文学のリストは開かれず、試験の準備をする本はクローゼットの中に散らばっています。 時間がありません。 コードの一部は急いで投げられます。これは、非常に楽しいニュースが作業の完了後に初めて明らかになったためですが、それについては後で詳しく説明します。 また、記事は文学言語で書かれていません。 しかし、それはそうです。



パケット代行受信サーバー->クライアント



クライアントがサーバーから受信するすべてのパケットは、エクスポートされたUNetworkHandler::AddNetworkQueue



内で呼び出すことで最終的にキャッチできます。



画像



これはラッパーであり、その内部には元の関数へのジャンプがあります。



画像



ここで、Sのトリッキーな防御が、サーバー上でさらに暗号化されたパケットを解読することは明らかです。 同じコードがメモリ内でどのように見えるかを見ると、次のことがわかります。



画像



控えめに言って、これは特定のハンドラーの近くで最も一般的なjmpです。 ハンドラー自体は興味深いものではなく、それ自体で機能するようにします。 このフックの後にフックを置き、パケットを復号化された形式で取得します。 その後、最初の問題が発生しました。 科学的な突きの方法により、 VirtualProtect



VirtualAlloc



などの機能がエラーで動作し、それらがないと、レイが保護されたメモリに入ることができないことが明らかになりました。 なぜこれが起こっているのですか? 私は決して見つけませんでした、時間がありませんでした。 しかし、S保護はNtProtectVirtualMemory



をインターセプトし、そこで何かを行うと言えます。 それから私は防御を欺くためにcな計画を立て始めましたが、私の怠inessが優勢であり、私はこれを愚かにしました:



 HANDLE hMain = OpenProcess(PROCESS_VM_OPERATION, FALSE, GetCurrentProcessId()); VirtualProtectEx(hMain, ... );
      
      





もちろん、プロセス内にいることを考えると、美しくありません(言及するのを忘れていましたが、dllを記述しています)。 しかし、それは動作します。 フックに戻ると、2番目の問題が発生します。保護は、この関数の最初の10〜20バイトをチェックします。 すぐに判明し、呪われた場所に窓が出てくるかもしれません。 どうする そうです、フックをつけてください。 オフセット0x14を選択しました(上の図を参照)。 jmp nearは5バイトかかります。それらを書き換えます



 add esi, 0x3c push 0x1
      
      









 jmp ...
      
      





これを忘れないでください。おそらくハンドラの最後にそれらを復元する必要があります。 ところで。 フックは、インポートしたEnterCriticalSection



内または他の場所に配置できます。 さらに進みます。 2010年にAddNetworkQueue



関数に渡されるパッケージの構造は、尊敬されるGoldFinchによって公開されました。



 struct NetworkPacket { unsigned char id, _padding1, exid, _padding2; unsigned short size, _padding3; unsigned char* data; }
      
      





id



data



フィールドに興味があります。 ecx



内容と同様に。 なぜecx



か? 簡単です: __thiscall



を扱っており、 UNetworkHandler



クラスの関数を呼び出すには、オブジェクトへのポインターが必要です。 ecx



で送信されます。 なんで電話する必要があるの? さらに、あなたはすべてを理解しますが、今のところ完成したコードを提供します:



 BYTE *AddNetworkQueue = (BYTE *)GetProcAddress(hEngine, "?AddNetworkQueue@UNetworkHandler@@UAEHPAUNetworkPacket@@@Z"); AddNetworkQueue += *(DWORD *)(AddNetworkQueue + 1) + 5; retAddr_AddNetworkQueue = (DWORD)AddNetworkQueue + 0x19; trmpAddr = (DWORD)wrapper_AddNetworkQueue - ((DWORD)AddNetworkQueue + 0x14 + 5); VirtualProtectEx(hMain, AddNetworkQueue + 0x14, 1, PAGE_EXECUTE_READWRITE, &tmpProtect); *(AddNetworkQueue + 0x14) = 0xE9; *(DWORD *)(AddNetworkQueue + +0x14 + 1) = trmpAddr; VirtualProtectEx(hMain, AddNetworkQueue + 0x14, 1, PAGE_EXECUTE, &tmpProtect); while (!unh) Sleep(100);
      
      





準備ができていない人は、この瞬間に死にたいと思うでしょう。 実際、すべてがシンプルです。 AddNetworkQueue += *(DWORD *)(AddNetworkQueue + 1) + 5;



jmpを使用したラッパーから実際のAddNetworkQueue関数に移動するだけです。 unh



とは? これは、ハンドラーで変数に押し込んだものと同じecx



値です。



 void __declspec(naked) wrapper_AddNetworkQueue() { __asm { pushad pushfd sub [unh], 0 jnz L1 mov [unh], ecx L1: lea eax, [esp + 44] //32 (pushad) + 4 (pushfd) + 4 (push 4) + 4 (ret addr) push eax call [handler_AddNetworkQueue] popfd popad add esi, 0x3c //see disasm push 0x1 jmp [retAddr_AddNetworkQueue] } } void __stdcall handler_AddNetworkQueue(DWORD *stack) { NetworkPacket_t *pck = (NetworkPacket_t *)*stack; if (ShowServerPck) { printf("s -> c | %02hhX ", pck->id); for (int i = 0; i < pck->size; i++) printf("%02hhX ", pck->data[i]); printf("\n"); } }
      
      





ここで、naked wrapper_AddNetworkQueue



関数はすべてのレジスタの値をunh



unh



取得して、ハンドラーを呼び出します。 その中で、スタックを恐れることなくパッケージを便利に処理し、制御をラッパーに戻します。 彼は、詰まった指示を復元し、元のコードを壊した場所にジャンプします。 Noos、1つ少ない問題。



クライアント→サーバーパケットキャプチャ



正直なところ、これらは最もおいしいパッケージです。 すべてのデュープの70%がベースになっているのはそれらです。 一般にSendPacket



と呼ばれる非エクスポート関数は、これらのパケットの送信を担当します。



 UNetworkHandler::SendPacket(char* msk, ...)
      
      





この関数には、最初の引数(マスク)に基づいてスタックから取得する可変数のパラメーターがあります。 このCDEの住所や輸出された女性の住所を取得する方法は? それは簡単です、それがどのように呼ばれるかを見てください。 この記事はapi lineage 2のチュートリアルではないと主張しているため、具体的な呼び出し例を示します。



画像



これで、レジスタ値が必要な理由が明確になります。



ecx







 SendPacket = (BYTE *)*(DWORD *)(**(DWORD **)(unh + 0x48) + 0x68); SendPacket += *(DWORD *)(SendPacket + 1) + 5;
      
      





SendPacketはラッパーでもあり、その中にはmain関数の通常のjmpがあります。 その始まりは次のようになります。



画像



そして、次のように、 AddNetworkQueue



との類推によるメモリ内で:



画像



繰り返しますが、特定のハンドラーへの平凡なジャンプですが、この場合は無視できません-パケットの暗号化を実行します。 どうする ジャンプで上書きしようとすると、ディフェンスSが誓います。 そして、あなたがこのジャンプに沿って行けば?



画像



彼女、もう一回ジャンプ。 私はそれを台無しにします:それらのうちさらに5つがあります(jmpの代替/近くの呼び出し)。 難読化を扱っています、クールです。 制御フローを復元するのが面倒だとしたらどうでしょうか?



前額法



これらの5つのjmpの1つを自分の近くに書き直してみませんか? 最初はそれだけでしたが、それは致命的な間違いでした。 判明したように、Sプロテクションはこれらの場所のコードの整合性をチェックし、元のコードと一致しない場合は誓います。 しかし! すぐではありません、カール! 15分後にのみ。 もちろん、開発段階では、そのような時間のパフォーマンスをテストする余裕はありませんでした。 プロジェクト全体の作業の終わりに、私はうれしい驚きを覚えました。 しかし、私はしおれませんが、... 2番目の致命的なミスを犯しました。 別名難読化コードの上にインラインパッチ手法を試してみたところ、自己クリアフック(残念ながら、gitに関する質問に対するそのオプションのソースコードはありません)。 仕組み:ガベージ命令を探し、ハンドラーの近くでjmpで上書きします。 その中で、すぐに元のバイトを復元し(jmpが置かれたメモリに書き込み、ハンドラー内の詰まったバイトを実行するだけではありません)、トリックを行い、制御を元の関数に戻します。 しかし、このオプションは一度だけ機能しますか? つまり、フックを再度設定するまでです。 保護Sは関数の最初のバイトのみをチェックし、最後にフックを置いても単語を言っていないことを思い出してください。 SendPacket



の最後に2番目のフックを配置します。これは、難読化されたコードのガベージ命令のアドレスのエントリの近くでjmpを生成する最も一般的なフックです。 私の言葉では、これを理解することは非常に簡単ではありませんが、スキームは次のとおりです。



  1. 保護ハンドラーSのガベージ命令の代わりにフックを設定します。最後に、これらのガベージ命令をメモリに復元してジャンプします。 このようにして、フックを消去します。
  2. 保護ハンドラーSは、制御を処理して元のSendPacket



    関数にSendPacket



    ます。
  3. 最後に2番目のフックを配置し、最初のフックを再インストールします。


なぜこの小さなスキームを致命的なミスと呼ぶのですか? 実際、このアプローチは、保護が現在のスレッドからのコードの整合性をチェックする場合にのみ機能するということです。 2番目のスレッドの重さがあり、最初のスレッドのバイト数をチェックしている場合、このトリックは機能しません。 そしてそれは起こった、私はちょうど時間をかけた。 どうする メモリ内のバイトを変更することはできません! 生き方は? 窓を出る?



バック方式



実際、この状況では、フックをインストールするためのオプションがいくつかあります。 そのうちの1つを選択しました。メモリページの権限を変更することによるフックです。 はい、これは最良の選択肢ではありませんが、締め切りは燃えていました(この記事が書かれた直前に最後に行われたことを思い出します)。 ここでは、Broken Swordの「保護されたモードのIntelプロセッサ」のすばらしいシリーズの記事を参照する価値があります。 読んで、怠けないでください。 また、Matt Pietrekによる一連の記事への参照として、「a」Win32 SEHの内部からの引用もあります。 グーグルはとても簡単です。 さて、私はあなたがすべての塩が何であるかを理解することを望みます。 SendPacket



プロシージャSendPacket



れているページの属性を変更します(実際、S保護ハンドラーが配置されているメモリページの属性を変更することにしました。詳細は後ほど説明します)。 複雑に聞こえますが、実際には、次のコードを実行する必要があります。



 VirtualProtectEx(hMain, SendPacket, 1, PAGE_EXECUTE | PAGE_GUARD, &tmpProtect);
      
      





クライアントがSendPacket



関数を呼び出した後、例外がスローされます。これは処理する必要があります。 私は本当にtibについて書きたくないので、私たちはすべてを非常に単純に行い、審美的にはしません



 AddVectoredExceptionHandler(1, wrapper_SendPacket);
      
      





では、 SendPacket



を呼び出すと、 wrapper_SendPacket



ます。



 long __stdcall wrapper_SendPacket(PEXCEPTION_POINTERS exInfo) { if (exInfo->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) { VirtualProtectEx(hMain, SendPacket, 1, PAGE_EXECUTE, &tmpProtect); if (exInfo->ContextRecord->Eip == (DWORD)SendPacket) { handler_SendPacket((DWORD *)exInfo->ContextRecord->Esp + 3); //4 (ret addr) + 4 (ret addr) + 4 (1 arg) } return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_CONTINUE_SEARCH; }
      
      





お気づきかもしれませんが、 wrapper_SendPacket



関数はVirtualProtectEx



呼び出します。これにより、ページ属性が正規化され、制御が返されます。 しかし、ページ属性を正規化するには==フックを削除します。 「額の方法」で説明した上記の2番目の方法を使用し、 SendPacket



関数の終わりをインターセプトして再度設定します(関数には2つのレットがあるため、2つのフックを設定します)。



 trmpAddr = (DWORD)wrapper_SendPacketEnd - ((DWORD)SendPacket + 0xb5 + 5); //first ret inside SendPacket VirtualProtectEx(hMain, SendPacket + 0xb5, 1, PAGE_EXECUTE_READWRITE, &tmpProtect); *(SendPacket + 0xb5) = 0xE9; *(DWORD *)(SendPacket + 0xb5 + 1) = trmpAddr; trmpAddr = (DWORD)wrapper_SendPacketEnd - ((DWORD)SendPacket + 0xc5 + 5); //second ret inside SendPacket *(SendPacket + 0xc5) = 0xE9; *(DWORD *)(SendPacket + 0xc5 + 1) = trmpAddr; VirtualProtectEx(hMain, SendPacket + 0xc5, 1, PAGE_EXECUTE, &tmpProtect);
      
      





wrapper_SendPacketEnd



自体:



 void __declspec(naked) wrapper_SendPacketEnd() { __asm { pushad pushfd call [handler_SendPacketEnd] popfd popad add esp, 0x2000 //see disasm ret } } void __stdcall handler_SendPacketEnd() { if (ShowClientPck) VirtualProtectEx(hMain, SendPacket, 1, PAGE_EXECUTE | PAGE_GUARD, &tmpProtect); }
      
      





PAGE_GUARD



属性を設定してPAGE_GUARD



だけで、 PAGE_GUARD



の最後ではなく、それを呼び出す関数に戻るだけで、複雑なことはありません。



wrapper_SendPacket



に戻りましょう。 忘れないでください? チェックに注意してください



 if (exInfo->ContextRecord->Eip == (DWORD)SendPacket)) { ... }
      
      





それ以外の場合はありますか? 幸いなことに、私たちの場合、残念ながらそうです。 VirtualProtectEx



を実行すると、少なくともメモリのページ全体の属性が変更されます。 これらの最小4キロバイトのコードは使用できません。 また、他の手順が存在する場合があります。 これらの例外は、SendPacketが呼び出されたときに必ずしもスローされるとは限りません。 これはこのメソッドの主な欠点です(最後にフックが復元されないプロシージャを呼び出すと、ハンドラーはフックを削除します)が、解決されます。 それを修正するためのいくつかのオプションがあります。 最高品質ではなく、最速を使用します。 引数PAGE_GUARD



してVirtualProtectEx



を愚かに生成します。 この目的のために(スポイラー:それだけではありません)、エクスポートされた関数FPlayerSceneNode::Render(FRenderInterface *)



が選択されました。これはループ内のメインスレッドによって呼び出されます



画像



防御Sは、最初にそれを傍受しても誓いません。 VirtualProtectEx



をインターセプトして生成します。 これは、フックがトリガーされることを100%保証しますか? もちろん違います。 95%のみ。 それで十分でした。 私は松葉杖を煩わせず、転がしませんでした。 フックはengine.dll



のアドレススペースではなく、保護ハンドラーSのアドレスにインストールされると上記で書きました。なぜですか? 回答のほんの一部です



 if (exInfo->ContextRecord->Eip == (DWORD)SendPacket)) { ... }
      
      





はるかに(経験的に検証済み)。 SendPacket



の最後にインストールしたフックに、パケットの送信後に100%表示される特定のインジケーターラインの出力を追加すると、次の図が表示されます。



画像



連続する#pck



行は、フックが機能しなかったことを示しています(同じ5%)。 上記のおを要約します。



  1. メモリページの属性を変更し、例外ハンドラを設定します
  2. その中で、元の属性を復元し、 SendPacket



    のアドレスで例外が発生した場合、独自のハンドラーを呼び出すことができます
  3. 最終的に、制御は元のSendPacket



    関数に戻り、その最後に2番目のフックがあります
  4. 次に、メモリページの属性を再設定し、 SendPacket



    を呼び出したコードに制御を移します。
  5. この時点で、 Render



    プロシージャでは、同じメモリスポーンへの同じ属性のインストールがスポーンされます。


サーバーへのパケットの送信



最もおいしいが、パケットキャプチャを中心としたタンバリンとのダンスの後、クライアント->サーバーは非常にシンプルです。 上記でSendPacketアドレスを取得する方法を学びました。そこで、この関数に引数を渡す例をスパイしました。 どうする 電話してみてください! そしてかなりたくさん。 engine.dll



のアドレス空間からではなく引数をスリップしようとしています-額に入れます。 engine.dll



のアドレス空間からではなく、リターンアドレスをスリップしようとしています。耳で取得します。 メインストリームからではなく、dll`kiから直線に関数を呼び出そうとしています-肝臓を通過します。 最終的に、レシピは次のとおりです。



  1. 保護されたSは、エクスポートされたengine.dll



    関数の1 engine.dll



    かを気にしません。これはSendPacket



    を呼び出しSendPacket



    (しかし無駄です!)
  2. Protect SはSendPacket



    についてengine.dll



    しません(戻りアドレスはengine.dll



    内にある必要があり、呼び出しはメインスレッドから行われる必要があります
  3. Protect Sは、 SendPacket



    関数の引数がどのアドレス空間にあるかを気にしません


そして、ここに治療法があります:



  1. SendPacket



    関数を呼び出すときの偽の返信アドレス
  2. 渡された引数の偽のアドレス空間
  3. メインスレッドから呼び出します


どうやってやるの? とても簡単です! engine.dll



内の空きスペースを見つけて(アライメントから完全に適合する)、そこに1つのスプリングボードと小さなバッファーを配置するだけで十分です。 言葉から行動に移りましょう:



 BYTE *Remove = (BYTE *)GetProcAddress(hEngine, "?Remove@?$TArray@E@@QAEXHH@Z"); Remove += *(DWORD *)(Remove + 1) + 5; pckMsk = (char *)Remove + 0x74; //max 44 chars with zero (43 without). You can find more. VirtualProtectEx(hMain, pckMsk, 1, PAGE_EXECUTE_READWRITE, &tmpProtect); //
      
      





44バイトの長さで最初に使用可能なスペースが見つかりました(さらに検索できます)。 バッファがそこに置かれ、そこに文字列が書き込まれ、最初の(実際には2番目の)引数でSendPacket



に渡されます。



返信先をどうしますか? engine.dll



内のスプリングボードをハンドラーにengine.dll



するだけで十分です( engine.dll



を呼び出した後SendPacket



コントロールはスプリングボードに移動し、そこからハンドラーに移動します)。 どのように見えますか? このように:



 BYTE* RequestRestart = (BYTE *)GetProcAddress(hEngine, "?RequestRestart@UNetworkHandler@@UAEXAAVL2ParamStack@@@Z"); RequestRestart += *(DWORD *)(RequestRestart + 1) + 5; retAddr_handler_Render = RequestRestart + 0x2b; trmpAddr = (DWORD)fixupStack_Render - ((DWORD)retAddr_handler_Render + 5); VirtualProtectEx(hMain, retAddr_handler_Render, 1, PAGE_READWRITE, &tmpProtect); *retAddr_handler_Render = 0xE9; *(DWORD *)(retAddr_handler_Render + 1) = trmpAddr; VirtualProtectEx(hMain, retAddr_handler_Render, 1, PAGE_EXECUTE, &tmpProtect);
      
      





fixupStack_Render



自体:



 void __declspec(naked) fixupStack_Render() { __asm { add esp, [fixupSize] //SendPacket has cdecl convention mov esp, ebp //prolog of pop ebp ///////handler_Render ret //ret to the end of wrapper_Render } }
      
      





fixupSizeとは何ですか? SendPacket



呼び出すSendPacket







 fixupSize = 12; //4 (push eax) + 4 (push [pckMsk]) + 4 (push 0x46) __asm { mov ecx, [unh] mov eax, [ecx + 0x48] mov ecx, [eax] mov edx, [ecx + 0x68] //SendPacket push 0x46 push [pckMsk] push eax push [retAddr_handler_Render] //trampoline to fixupStack_Render jmp edx }
      
      





可変数のパラメーターを渡すため、スタックをクリアする必要があります。 fixupStack_Render



プロシージャのコードfixupStack_Render



これを実行fixupStack_Render



ます。 もちろん、 SendPacket



自体はメインスレッドからSendPacket



必要があり、前述のエクスポートされたRender



関数はこの目的のために行います。



クライアントへのパッケージの送信



同様に実装されます。



着信パケットと発信パケットの置換



上記のインターセプトうまく学習しなかった関数の引数を変更するだけで十分です。



完全に忘れてしまった



  1. すべてがテストされたサーバー-Pirta
  2. 申請書は間奏記録に基づいて書かれました
  3. エクスポートテーブルの交換時に保護Sが誓う


エピローグ



少なくとも一度は私を助けてくれたフォーラムTの各メンバーに感謝します。 バグを探し、サーバーを複製していました。 また、セキュリティ開発者Sにも、このvraytapを作成する理由を教えてくれてありがとう。 そしてもちろん、記事を最後まで読んだhabrのユーザーに。



完全なソース添付: klats



パッケージのビデオデモ:





これに別れを告げます。



All Articles