プロローグ
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
パッケージのビデオデモ:
これに別れを告げます。