プロローグ
Tフォーラムの友人の1人が、尊敬されるxkorの著者として、不正行為の世界全体に知られているl2phxプログラムを書き換えることを提案したとき、それはすべて1年前に始まりました。
L2phx自体(l2パケットハック、パケット、スラム)は、系統2クライアント(他のmmorpgのバージョンがあります)の着信および発信パケット(すべてがLSPで実装されている)のスニファーであり、個々のパケットを送信/置換する機能を備えています。 Xkorは次のように試みました:暗号化バイパスメソッドの実装、美しいGUIなど。 しかし、frisheksの悪意のある管理者はそのようなアプリケーションを好まなかった。それは次の1日のイベントの開始時に彼らの収入を著しく殺した。 はい、はい、名前以外の人がどのサーバーにもアクセスして、このツールで完全な乱交を手配することができました。 同時に、あらゆる種類の商用保護があり、パケットの使用を
それから、私はこのベンチャーをあまり真剣に受け止めませんでした:私はクライアント->サーバーパケットキャプチャモジュールを書き、それを放棄しました。 なんで? なぜなら。 しかし、ほんの3日前、私はこのプロジェクトの作業を再開し、この記事を公開することにしました。 なんで? 現在、l2詐欺師コミュニティは死んでいます。 すべてのバグと洗濯機は、SkypeとフォーラムTで互いに通信する10人の手にあります。そして、私も去ることにしました。 そして、あなたが去るなら、それは美しいだけです))2年前、私はワーキングバッグを夢見ましたが、今日はそれを必要としません。
免責事項
パケット代行受信サーバー->クライアント
クライアントがサーバーから受信するすべてのパケットは、エクスポートされた
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を生成する最も一般的なフックです。 私の言葉では、これを理解することは非常に簡単ではありませんが、スキームは次のとおりです。
- 保護ハンドラーSのガベージ命令の代わりにフックを設定します。最後に、これらのガベージ命令をメモリに復元してジャンプします。 このようにして、フックを消去します。
-  保護ハンドラーSは、制御を処理して元のSendPacket
 
 
 
 関数にSendPacket
 
 
 
 ます。
- 最後に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%)。 上記のおを要約します。
- メモリページの属性を変更し、例外ハンドラを設定します
-  その中で、元の属性を復元し、 SendPacket
 
 
 
 のアドレスで例外が発生した場合、独自のハンドラーを呼び出すことができます
-  最終的に、制御は元のSendPacket
 
 
 
 関数に戻り、その最後に2番目のフックがあります
-  次に、メモリページの属性を再設定し、 SendPacket
 
 
 
 を呼び出したコードに制御を移します。
-  この時点で、 Render
 
 
 
 プロシージャでは、同じメモリスポーンへの同じ属性のインストールがスポーンされます。
サーバーへのパケットの送信
最もおいしいが、パケットキャプチャを中心としたタンバリンとのダンスの後、クライアント->サーバーは非常にシンプルです。 上記でSendPacketアドレスを取得する方法を学びました。そこで、この関数に引数を渡す例をスパイしました。 どうする 電話してみてください! そしてかなりたくさん。
engine.dll
      
      のアドレス空間からではなく引数をスリップしようとしています-額に入れます。
engine.dll
      
      のアドレス空間からではなく、リターンアドレスをスリップしようとしています。耳で取得します。 メインストリームからではなく、dll`kiから直線に関数を呼び出そうとしています-肝臓を通過します。 最終的に、レシピは次のとおりです。
-  保護されたSは、エクスポートされたengine.dll
 
 
 
 関数の1engine.dll
 
 
 
 かを気にしません。これはSendPacket
 
 
 
 を呼び出しSendPacket
 
 
 
 (しかし無駄です!)
-   Protect SはSendPacket
 
 
 
 についてengine.dll
 
 
 
 しません(戻りアドレスはengine.dll
 
 
 
 内にある必要があり、呼び出しはメインスレッドから行われる必要があります
-   Protect Sは、 SendPacket
 
 
 
 関数の引数がどのアドレス空間にあるかを気にしません
そして、ここに治療法があります:
-   SendPacket
 
 
 
 関数を呼び出すときの偽の返信アドレス
- 渡された引数の偽のアドレス空間
- メインスレッドから呼び出します
どうやってやるの? とても簡単です!
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
      
      関数はこの目的のために行います。
クライアントへのパッケージの送信
同様に実装されます。
着信パケットと発信パケットの置換
上記のインターセプト
完全に忘れてしまった
- すべてがテストされたサーバー-Pirta
- 申請書は間奏記録に基づいて書かれました
- エクスポートテーブルの交換時に保護Sが誓う
エピローグ
少なくとも一度は私を助けてくれたフォーラムTの各メンバーに感謝します。 バグを探し、サーバーを複製していました。 また、セキュリティ開発者Sにも、このvraytapを作成する理由を教えてくれてありがとう。 そしてもちろん、記事を最後まで読んだhabrのユーザーに。
完全なソース添付: klats
パッケージのビデオデモ:
これに別れを告げます。