初めに、私が伝えたように、 Mhookについてのいくつかの言葉。 Mhookは、外部アプリケーションのコードストリームにコードを埋め込むためのライブラリです。 コードインジェクションの問題はすでにHabréで提起されているため、問題の詳細な議論に興味がある人は、サイトでそれを探すことができます。 要するに、コードをインターセプトするために、インターセプトされるアドレスで5バイトが削除され、代わりに無条件ジャンプコード(jmp #addr)がインターセプト関数に書き込まれます。 削除された5バイトは、特別に割り当てられた場所に転送されます。これは、スプリングボードとも呼ばれます。 インターセプト関数が動作すると、スプリングボードへの無条件の遷移を行うことができ、それらの5つの保存されたバイトがそこで実行され、インターセプトされたコードへの遷移が発生します。 だから、それは図式的に見えるでしょう:
見た目は素晴らしいですが、特にコードの実行をどこかでインターセプトする必要がある場合、たとえば手順の途中で、何か面白いことを行います(一部のメモリセルの状態をファイルに書き込み、レジスタを変更するなど)。インターセプトハンドラーとインターセプトハンドラーの間に別の中間関数を挿入する必要があります。 この関数は_nakedとして宣言されており、レジスタを損なわないために必要です。 以前は次のようにしていました(これを表示するのは恥ずかしいので、非表示のテキストを使用します)。
恐ろしい恐怖
//傍受したデータのハンドラー関数
void onProc ( DWORD backTrace、DWORD arg1 ) {
printf ( "%d%d" 、backTrace、arg1 ) ;
}
//中間傍受関数
__declspec (ネイキッド) void hookProc ( ) {
//レジスタを保存します
プッシャッド
//必要なデータを取得します
__asm {
プッシュ[ esp + 0x20 ]
ポップバック
プッシュ[ esp + 0x24 ]
arg1をポップ
} ;
//ハンドラーを呼び出します
onProc ( backTrace、player ) ;
__asm {
//レジスタを復元し、インターセプトされた関数のスプリングボードに移動します
ポパド
jmp realProc
} ;
}
//インターセプトを設定します
Mhook_SetHook ( & ( PVOID & ) realProc、hookProc ) ;
どうやら、それはあまり見えません。 また、使用するのもまあまあです。すべてのバインディングを記述する必要があるため、常に新しいインターセプトを実行するのは面倒です。 この非常に中間的な関数(hookProc)をスローすることにしました。 ハンドラー関数では、レジスターの値をC ++クラスの形式で渡します。 このクラスをコンテキストと呼びます。 すべての汎用レジスタが含まれており、それらにアクセスできます。 完全に幸せにするには、次の機能が必要です。
コンテキストcontext ;
//レジスタから値を読み取ることができます
DWORD var =コンテキスト。 EAX ;
//ただし、レジスタに書き込むことができます
コンテキスト。 EAX = var
//メモリから値を読み取ることができます。
//インデックスは定数のようにすることができます
var = context [ 0xBEDABEDA ]
//レジスタも同様です
var = context [ EAX ]
//また、メモリに書き込むことができる必要があります
コンテキスト[ 0xBEDABEDA ] = var
コンテキスト[ EAX ] = var
//そして、レジスタの算術演算を実行できるように
コンテキスト。 EAX =コンテキスト。 EAX +コンテキスト。 EBX * 4
//そして、この算術式がインデックスになるように
コンテキスト[コンテキスト。 EAX +コンテキスト。 EBX * 4 ] = var
//あまり明白ではありませんが、何らかの理由でしたかったのです。
//レジスタからのオフセットを角括弧で示すことができるように
var =コンテキスト。 ESI [ 0x20 ]
前の例の関数は次の形式を取ります。
void onProc (コンテキストコンテキスト) {
DWORD backTrace =コンテキスト[ esp ]
DWORD arg1 =コンテキスト[ esp + 4 ]
printf ( "%d%d" 、backTrace、arg1 ) ;
}
RegisterHook ( realProc、hookProc ) ;
この喜びをすべて実現するために、Registerクラスを作成し、そのクラスで型変換、インデックス付け、および算術演算の演算子をオーバーロードする必要がありました。 私はこれに関する優れた専門家ではありませんが、誰かが本当に望んでいる場合は、記事の最後にあるコードへのリンクをご覧ください)。
これらはすべてプログラムにどのように組み込まれますか? mhookの仕組みについてはすでに説明しました。 もう1つ手順を追加しました。
; レジスタを保存する
プッシャッド
; ハンドラー関数でそれらへのポインターを渡します
ESPを プッシュ
; ハンドラー関数を呼び出す
コールフック
; クリアスタック
espを 追加 、 4
; レジスタを復元する
ポパド
; 実際の機能のための踏み台に行く
jmpトランポリン
このコードは、行"x60x54xE8x00x00x00x00x83xC4x04x61xE9x00x00x00x00"に保存されます。 フックを設定する必要がある場合、メモリが割り当てられ、そこに行がコピーされ、ハンドラー関数のオフセットがcallステートメントに置き換えられ、スプリングボードオフセットがjmp演算子に配置されます。 pushadは、レジスターの保存に加えて、いわばハンドラー関数のデータを準備します。 ハンドラー関数がこのデータを変更する場合、変更はpopadコマンドによって適用されます。 よくわかりません。 写真で見せてみます。
繰り返しますが、espコマンドが実行された後、スタックポインターは保存されているすべてのレジスタを指すため、実行コンテキストへのポインターとしてハンドラー関数に渡されます。
そして今-デザート! ヒキガエルとボールに関するゲームで、これをすべて実際の条件で使用します。 タスクを簡単にします。 たとえば、ボールの報酬を10倍にします。
まず、アカウントが保存されている場所を見つけようとします。 さらに興味深いことに、私はデバッガーやArt Moneyなどのユーティリティを使用していません。
ズマでは、多くのカジュアルゲームのように、スコアは10または100の倍数です。 私の知る限り、これは、何らかの理由で最後にゼロのスコアがプレーヤーによってより「楽しい」と認識され、ゲームの喜びが増すために行われます。 そして、Vanka-Kosoyについての冗談のように、私は奇妙な結論を下します。 スコアが10の倍数である場合、どこかで分割されます。 また、理論的には、アカウントは整数であり、常に10で割り切れるため、整数の除算を使用する必要があります。 これらすべての除算操作をインターセプトし、この除算が発生する配当と住所を記録してから、スコアと比較できます。 これは、コンパイラーが整数除算を10で最適化し、定数0x66666667による乗算とそれに続く右へのシフトで置き換えることにより簡素化されます(興味がある場合は、本「プログラマー向けアルゴリズムトリック」、10.3章を参照してください)。 このようなもの:
mov eax 、 66666667h ; 2.5で除算するマジックナンバー
imul ecx ; EDX:EAX←EAX(edxで-2.5による除算の結果)
sar edx 、 2 ; 結果は4で除算されます。edx= ecx / 10であることがわかります。
もう1つの成功は、mov eax、66666667が正確に5バイトを使用し、jmpまたはcallに簡単に置き換えられることです。 ループ内のTEXTセクションを調べて、オペレーションコード(0xB8、0x67、0x66、0x66、0x66)を探し、トラップの呼び出しに置き換えます。 10で割ることはかなり一般的な操作であるため、トラップに陥るすべての操作を表示することは絶対に有益ではなく、ゲームの速度に悪影響を及ぼします。 オペランドの1つが現在のアカウントと等しくなるようにフィルタリングする必要があります。 そして、もしそれを探しているだけなら、どうやってアカウントを知るのですか? 特定の時点でスコアがどうなるかを予測できるように、テストを実施するためのいくつかのルールを作成する必要があります。
1)ゲームはゼロに等しいスコアで始まります
2)各マウスクリック-ショット
3)各ショットは効果的です
4)各ショットは30ポイントをもたらします
5)2番目のルールの結果:ゲームの前にマウスを数回クリックする必要があるため、インターセプトプログラムによるスコアリングは、ゲームが開始された直後から開始する必要があり、すでにボールのシーケンスを収集することが可能です。
ルールが定式化されると、ゲームのウィンドウプロシージャにトラップをかける必要があることが明らかになります。 その後、マウスボタンのすべてのクリックについて認識され、キーボードのボタンを押すことでゲームの開始を知らせることができます。
ウィンドウプロシージャのアドレスを見つけるのは難しくありません。このため、IDAのRegisterClass関数の引数を調べる必要があります。 すべての要件で、ウィンドウプロシージャフックは次のようになります。
void onMainProc (コンテキスト*コンテキスト) {
//テンキーのボタン0は状態を切り替えます:スコアがあるかどうか
if ( (コンテキスト- > ESP [ 0x08 ] == WM_KEYUP ) && (コンテキスト- > ESP [ 0x0C ] == VK_NUMPAD0 ) ) {
if ( state == kCounting ) {
状態= kNotCounting ;
} else {
状態= kCounting ;
}
}
//カウントが進行中で、マウスの左ボタンが押されると、予想されるカウントが増加します
if ( ( state == kCounting ) && ( context- > ESP [ 0x08 ] == WM_LBUTTONUP ) ) {
スコア+ = 30 ;
}
}
//そして、除算のトラップはこのようになります
void onDivision (コンテキスト*コンテキスト) {
char buf [ 512 ] ;
if ( state == kCounting ) {
//可能な配当の1つが予想されるアカウントと等しい場合、除算が発生するアドレスとレジスタ値がデバッグ出力に送信されます
if ( (コンテキスト- > EBX ==スコア) || (コンテキスト- > ECX ==スコア) || (コンテキスト- > EDX ==スコア) ) {
sprintf ( buf、 "%x:EBX =%d、ECX =%d、EDX =%d" 、コンテキスト- > ESP [ 0 ] 、コンテキスト- > EBX、コンテキスト- > ECX、コンテキスト- > EDX ) ;
OutputDebugStringA ( buf ) ;
}
}
//このコードの代わりにjmpが挿入されたため、実行する必要があります
コンテキスト- > EAX = 0x66666667 ;
}
ビデオは、住所を見つける方法を示しています(ルールのかなり興味深い変更が判明しました。「ズマのプレゼント」)。
住所が見つかったら、それが属する機能を調べます。
分割が発生する部分は、プレイヤーがレベルの最初から獲得したポイントの量の桁の計算を実行します。 一般的に、この関数は一連のボールを破壊した後にスコアカウンターを更新する役割を果たしているようです。 2つの引数が渡されます。 最初の引数は、いくつかの構造(おそらくカウンター自体)へのポインターです。 オフセット0x18Cは現在のポイント数であり、オフセット0x190はレベルの先頭のポイント数です。 2番目の引数は現在のスコアです。 それがどこから来たのかを知るために、私はスタックを上っていきます。 すぐに、現在のアカウントは、オフセット0x104にある構造から関数に転送されることがわかります。 少し高い操作で、[esi + 0x104]、ediを追加します。 明らかに、これは私が探しているものであり、ここでトラップを設定すると、その組み合わせで受け取るポイントの数にある程度の影響を与えることができます。 トラップコードは非常に単純です。 EDI登録が10倍に増加し、成功した組み合わせのポイントが増えました
void onAddScore (コンテキスト*コンテキスト) {
コンテキスト- > EDI =コンテキスト- > EDI * 10 ;
}
すべてが好きです。 投稿と馬のイラストの間隔が長いことをおtoびします(もっと良い配置方法を教えてくれた方、ありがとうございました)。
ああ、そしてもちろん、 コード 。 しかし、これは大ファンであり、親しみと結果のために、私は責任を負いません。