シンプルなDIYプロキシDLL

GDS32.DLLの呼び出しをインターセプトするのに時間がかかりました。 プロキシdllを作成することにしました。

リサーチスタンドを作成します


最初に必要なことは、エクスポートされたすべての関数のリストを実際のdllから取得することです。
次のコードでそれをやってみましょう:

1. program GetFuncsDll; 2. {$APPTYPE CONSOLE} 3. uses Windows; 4. var 5. ImageBase: DWORD; //  dll 6. pNtHeaders: PImageNtHeaders; // PE  dll 7. IED: PImageExportDirectory; //    8. ExportAddr: TImageDataDirectory; //   9. I: DWORD; //    10. NamesCursor: PDWORD; //      11. OrdinalCursor: PWORD; //      12. LIB_NAME:AnsiString; //  dll 13. BEGIN 14. LIB_NAME:='MiniLib.dll'; 15. loadlibraryA(PAnsiChar(LIB_NAME)); 16. ImageBase := GetModuleHandleA(PAnsiChar(LIB_NAME)); 17. pNtHeaders := Pointer(ImageBase + DWORD(PImageDosHeader(ImageBase)^._lfanew)); 18. ExportAddr := pNtHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; 19. IED := PImageExportDirectory(ImageBase+ExportAddr.VirtualAddress); 20. NamesCursor := Pointer(ImageBase + DWORD(IED^.AddressOfNames)); 21. OrdinalCursor := Pointer(ImageBase + DWORD(IED^.AddressOfNameOrdinals)); 22. For I:=0 to Integer(IED^.NumberOfNames-1) do begin 23. WriteLn(output,PAnsiChar(ImageBase + PDWORD(NamesCursor)^),'=',OrdinalCursor^ + IED^.Base); 24. Inc(NamesCursor); 25. Inc(OrdinalCursor); 26. end; 27. Readln; 28. end.  1 


問題はないようです。 名前の配列(NamesCursor)と数字の配列(OrdinalCursor)へのポインターのエクスポートテーブル(19行目)に順番にアクセスし、関数、名前、および数字ごとに関数を読み取ります。 関数の数は、NumberOfNamesフィールドにあります。 このコードはインターネット上で入手し、最終的に簡素化されました。

テストDLLを検討してください。

 1. Library MiniLib; 2. function myAdd(a,b:integer): integer; stdcall; 3. begin 4. result:=a+b; 5. end; 6. function mySub(a,b:integer): integer; stdcall; 7. begin 8. result:=ab; 9. end; 10. exports 11. myAdd, 12. mySub; 13. begin 14. end.  2 


難しいこともないようです。 加算と減算の2つの関数をエクスポートします。
エクスポートされる関数と番号のリストは次のとおりです。

myAdd = 2
mySub = 1
リスト3

これらの番号はコンパイラーによって割り当てられます。 なぜまさにこれらなのでしょうか? 知りません
それでは、加算機能に注目しましょう。 呼び出しがコンパイルされたコードを見てみましょう。このため、呼び出してデバッガーを調べます。

 1. program TestCall; 2. {$APPTYPE CONSOLE} 3. uses Windows; 4. var 5. myAdd: function (a,b:integer): integer; stdcall; 6. Handle:HMODULE; 7. N:Integer; 8. begin 9. Handle := loadlibrary('MiniLib.dll'); 10. @myAdd := GetProcAddress(Handle, 'myAdd'); 11. //      12. //@myAdd := GetProcAddress(Handle, PChar(2)); 13. N:=myAdd(1,2); 14. writeLn(N); 15. readln; 16. end.  4 


ここではすべてが簡単です。 関数のアドレスを取得して呼び出します。 2番目のパラメーターGetProcAddressで関数名へのポインターのみを説明しますが、これは$ FFFFより大きい場合、それより小さいか等しい場合、エクスポートテーブル内の関数の番号として解釈されることです。 つまり、関数を番号または名前で呼び出すことができます。

次に、加算の結果が変数に入力される方法、つまり13行目の操作を見てみましょう。

1. TestCall.dpr.13:N:= myAdd(1,2);
2. $ 02を押す
3. $ 01を押す
4. dword ptr [$ 0040cba4]を呼び出します
5. mov [$ 0040cbac]、eax
リスト5

そして、ここではすべてが単純です。スタック、デュース(2)およびユニット(3)を配置し、関数(4)を呼び出します。加算の結果はコンパイラーによってレジスタeaxに配置され、レジスタから結果を変数Nにコピーします(5)。

ここでは、DLLからの一般的な関数呼び出しの前にあります。 引数がスタックにプッシュされ、呼び出しが行われ、結果がレジスタ(またはスタック)から読み取られます。

アイデア


私の考えは、本物の代わりに私の偽のdllがある場合、最初に関数の入力と関数の名前をインターセプトし、次に実際の関数を呼び出して、何もないように呼び出します。

偽のDllを作成します。

したがって、関数と数値のリストがありますが、エクスポートされる各関数には何らかのコードが必要です。 どれ。 それはすべてのためであり、書かれています。 インターネットで見たこれらの例では、インターセプトされた各関数の有用なコードが複製されており、同じパラメーターで実際の関数を呼び出すために、関数のエクスポートパラメーターも知っている必要があります。 今回は(GDS32のすべての機能の説明を見つけて、デルファイで複製するために)そのような骨の折れる作業を実行するのが面倒でした。 それでも、便利なコードを複製することは「私たちの方法ではありません」。 アイデアは次のとおりです。関数を呼び出した後、アプリケーションでコードを実行する必要があります。 コードは同じなので、便利なコードを使用して別のプロシージャを作成しましょう-ProxyProc。 そして、すべての偽のプロシージャはProxyProcを呼び出すだけです。 次に、プロキシプロシージャは、どのプロシージャがそれを引き起こしたかを正確に見つけなければなりません。 考えた後、私は理想的なオプションは関数番号をスタックに置くことであるという結論に達しました。 また、レジスタとフラグの状態を保存する必要があります。これらは、実際のDLLでのプロシージャの実行に影響を与える可能性があるためです。 エクスポートされた関数ごとに、合計で4行のコードが取得されます。 そして、はい、私たちはWindowsの基本的なメカニズムに介入しているため、何がどこで混乱したのかを確認するために、アセンブラーで記述します。

1. pushfd //各関数で同じ
2. pushad //各関数で同じ
3. push 2 //各関数の数が変更されます
4. ProxyProcを呼び出す//各関数で同じ
リスト6

アイデアを実現します


そして、ここにコードがあります。

 1. Library minilib2; 2. 3. Uses Windows; 4. 5. Procedure ProxyProc; assembler; 6. asm 7. end; 8. 9. Procedure FakeProc0001; assembler; 10. asm 11. pushfd 12. pushad 13. push 000000001 14. call ProxyProc 15. end; 16. 17. Procedure FakeProc0002; assembler; 18. asm 19. pushfd 20. pushad 21. push 000000002 22. call ProxyProc 23. end; 24. 25. Exports 26. FakeProc0001 index 1 name 'mySub', 27. FakeProc0002 index 2 name 'myAdd'; 28. Begin 29. End.  7 


ここではすべてが簡単です。 2つの偽のプロシージャをエクスポートし、実際のdllと同じ名前と番号を付けます。
最も扱いにくい部分は、プロキシプロシージャ自体です。 何で構成する必要があります。

1.関数番号と入力パラメーターを使用していくつかの便利な操作を実行します
2.この関数のアドレスを調べる
3.すべてのレジスタを元の状態に戻す
4.何もないように、制御をこの手順のアドレスに移します。

したがって、そのコードは次のようになります。

 1. const LibName:pAnsiChar = 'MiniLib_.DLL'#0; 2. Procedure DeveloperProc; 3. //   4. begin 5. end; 6. Procedure ProxyProc; assembler; 7. asm 8. call DeveloperProc; //  ,      //  ,     9. add esp,4 //       10. push LibName //     dll 11. call LoadLibraryA //  dll  ,   12. push eax //      13. call GetProcAddress //      .    14. mov [esp-4], eax //     , //         15. popad //   16. popfd //   17. jmp [esp-40] //    , //        //     18. end;  8 


このコードをコンパイルすると、「minilib2.dll」が取得されます。 名前を「minilib.dll」に変更して置き換え、「minilib.dll」をそれに応じて「minilib_.dll」に名前変更します

それでは、その仕組みを見てみましょう


TestCall.dpr.13:N:= myAdd(1,2);
1. $ 02を押す
2. $ 01を押す
3. call dword ptr [$ 0040cba4] // myAddを呼び出しますが、偽物になります
4. mov [$ 0040cbac]、eax
リスト9
リスト9は、既に見たコードの一部であり、Dllから関数を呼び出し、偽のプロシージャに入った後、つまり3行目の呼び出しを入力した後のスタックとレジスタの状態の下の表にあります。
EAX 00364434
EBX 7FFDA000
ECX 00000000
EDX 00000003
ESI 16A1F224
EDI 13D84260
EBP 0012FFC0
ESP 0012FFA4
EIP 00364434
EFL 00000246
リスト10
0012FFAC 00000002 // 2番目の引数
0012FFA8 00000001 //最初の引数
-> 0012FFA4 0040811A //実行可能ファイルにアドレスを返す
リスト11


次に、左側に4行の偽のプロシージャのコードを、右側にproxyprocを取得した後、つまり4行目の呼び出しを入力した後のスタックの状態を確認します。

minilib2.myAdd://彼女はfakeProc0002です
1. pushfd
2.プッシュアド
3. $ 02を押す
4. call $ 00364408 // proxyProcを呼び出します
リスト12
0012FFAC 00000002 // 2番目の引数
0012FFA8 00000001 //最初の引数
0012FFA4 0040811A //実行可能ファイルにアドレスを返す
0012FFAO 00000346 //フラグレジスタ
0012FF9C 00364434 // EAXを登録
0012FF98 00000000 // ECXレジスタ
0012FF94 00000003 // EDXを登録する
0012FF90 7FFDA000 // EBXレジスタ
0012FF8C 0012FFAO // ESPレジスタ
0012FF88 0012FFC0 // EBPレジスタ
0012FF84 16A1F224 // ESIレジスタ
0012FF80 13D84260 // EDIを登録する
0012FF7C 00000002 //機能番号(02)
-> 0012FF78 0036443D // fakeProc0002偽プロシージャにアドレスを返す
リスト13


次に、行6の後に真のプロシージャのアドレスを受け取った後、左側にプロキシプロシージャコードを、右側にスタック状態を確認します。fakeProc0002偽プロシージャへの戻りアドレスがスタックから削除され、関数番号がスタックから削除されましたが、実際の関数のアドレスはスタックに表示されました

minilib2.ProxyProc:
1. espを追加、$ 04
2. dword ptrをプッシュ[$ 0036782c]
3. call $ 00364394 //これはLoadLibraryです
4. eaxを押す
5. call $ 00364384 //これはGetProcAdressです
6. mov [esp- $ 04]、eax
7. popad
8. popfd
9. jmp dword ptr [esp- $ 28]
リスト14
0012FFAC 00000002 // 2番目の引数
0012FFA8 00000001 //最初の引数
0012FFA4 0040811A //実行可能ファイルにアドレスを返す
0012FFAO 00000346 //フラグレジスタ
0012FF9C 00364434 // EAXを登録
0012FF98 00000000 // ECXレジスタ
0012FF94 00000003 // EDXを登録する
0012FF90 7FFDA000 // EBXレジスタ
0012FF8C 0012FFAO // ESPレジスタ
0012FF88 0012FFC0 // EBPレジスタ
0012FF84 16A1F224 // ESIレジスタ
-> 0012FF80 13D84260 // EDIを登録する
0012FF7C 0037437C //このdllのこのプロシージャのアドレス
リスト15


次に、左側の表のレジスターの状態と、真のプロシージャーのjmpの前、つまりリスト14の行9を実行する前のスタックの状態を確認します。ご覧のとおり、スタックとレジスターの状態は、偽のプロシージャーに入った直後の状態と同じです(リスト10および11)。真のDLLプロシージャが違いを感じないことを願っています。 (16進数の28は10進数の40です。つまり、各4バイトの10倍は、真のプロシージャーのアドレスがあるスタック上の正確な場所です(リスト17))。

EAX 00364434
EBX 7FFDA000
ECX 00000000
EDX 00000003
ESI 16A1F224
EDI 13D84260
EBP 0012FFC0
ESP 0012FFA4
EIP 00364422
EFL 00000246
リスト16
0012FFAC 00000002 // 2番目の引数
0012FFA8 00000001 //最初の引数
-> 0012FFA4 0040811A //実行可能ファイルにアドレスを返す
1. 0012FFAO 00000346 //フラグレジスタ
2. 0012FF9C 00364434 // EAXを登録する
3. 0012FF98 00000000 // ECXレジスタ
4.0012FF94 00000003 // EDXを登録する
5.0012FF90 7FFDA000 // EBXレジスタ
6. 0012FF8C 0012FFAO // ESPレジスタ
7.0012FF88 0012FFC0 // EBPレジスタ
8. 0012FF84 16A1F224 // ESIレジスタ
9.0012FF80 13D84260 // EDIを登録する
10. 0012FF7C 0037437C //このdllのこのプロシージャのアドレス
リスト17


そして最後に、開発者の手順。

この手順では、アセンブラで記述する必要はありません。 ここで、レジスタとスタックの内容を損なうことなく、実際に傍受することができます。

たとえば、呼び出された関数のすべての数をファイルに出力する単純なコードは次のようになります。

 1. Procedure DeveloperProc; 2. var 3. F:text; 4. _ebp:PAnsiChar; //   5.begin 6. asm 7. mov _ebp,ebp; 8. end; 9. assignfile(F,'G:\Projects\dllproxy\logdll.txt'); 10. append(F); 11. writeln(F,DateTimeToStr(now),': ',PDWORD(_ebp+3*4)^); 12. closefile(F); 13.end;  18 


7行目で、ベースポインターが_ebp変数に入力されました
9行目で、変数Fをファイルに関連付けます
10行目で追加するファイルを開きました
11行目では、現在の日付と時刻、および呼び出された関数の番号を記録しました
関数番号の後のスタックには3つのポインターがあるため、ベースポインターに4バイトを3回追加する必要があります。1。偽のプロシージャに戻るポインター、2。プロキシプロシージャに戻るポインター、3。コンパイラーによって配置されたスタックへのポインター( ebpを押します)。 ポインタータイプPAnsiCharが選択されたのは、数値による加算と減算の演算が可能なためです。
行12はファイルを閉じました。

こちらからサンプルをダウンロードしてください

PS Proxy-GDS32.Dllは正常にコンパイルされ、それを使用するプログラムは動作中にエラーを生成せず、すべての呼び出しはログファイルにインターセプトされ、失敗したSQLクエリはキャッチされ、最適化されました。
PPSこの記事の著者は、この記事の情報および資料の使用について責任を負いません。 すべての情報は教育目的のみです。

Source: https://habr.com/ru/post/J215389/


All Articles