トップ回答者
他プロセス・メモリのRead、Writeについて

質問
-
他プロセス・メモリのRead、Writeがしたく、Visual Studio 2008 VC++にて、
下【ソース・コード・イメージ】に示すようなコードを作成して試してみましたが、
他プロセス・メモリのRead、Write(ReadProcessMemory、WriteProcessMemory)が
期待通りにできません。他プロセス・メモリのReadについては、ReadPcocessMemory APIによるものでは
期待通りの結果にはなりませんでしたが、
直接の代入等によるネイティブ・コード(例: v = *p : mov命令等によるメモリ読み取り)
ではできるようです。他プロセス・メモリの書き換えについては、直接の代入等によるネイティブ・コード
(例: *p = v : mov命令等によるメモリ書き込み)では、
アクセス保護例外が起きてしまいます。
他プロセス・メモリの書き換えについては、WriteProcessMemory APIによる方法しか
ないものと思います。Visual Studio 2008のデバッグ・ツールでは、他プロセス・メモリの書き換えが
できています。 ですので、何かを見落としているに違いありません。一体何を見落としているのかご指摘頂ければ、あるいは参考になるページを
ご紹介いただければ幸いに思います。実際にやろうとしているのは、他プロセスIMPORTテーブル 特定APIの
ジャンプ・ベクタの変更です。【本件について参考にした資料、URLなど】
・ http://codezine.jp/article/detail/426?p=2・ http://www7a.biglobe.ne.jp/~tsuneoka/win32tech/18.html
・ Advanced Windows 第5版 ・・・ 日経BPソフトプレス ISBN978-4-89100-593-1
第22章 DLLインジェクションとAPIフック・ セキュア ソフトウェア ・・・ 日経BP社 ISBN4-8222-8210-4
第3章 リバースエンジニアリングとプログラムの読解 / シンプルなx86デバッガ【他プロセスのメモリRead、Writeをするために使用するAPI】
他プロセス・メモリを読み書きするためのチェック・ポイントは、
そのために使用するAPIの呼び出しパラメータの設定内容になるかと思います。下に使用したAPIと設定概要を示します。
(1) CreateProcess
ターゲット・プロセスは、dwCreationFlagsをDEBUG_PROCESSで
生成。
プロセス・アクセス権の設定変更は、特に行っていません。(2) VirtualQueryEx
ターゲット・プロセスのRead、Writeしたいメモリ・アドレスは、
取得済みで、デバッガからそれが誤っていないことを確認すみです。
ちなみに、メモリはコード・ページです。ターゲット・メモリのアクセス権を知るため、本APIを使用。
ターゲット・プロセスのプロセス・ハンドルを第1引数にして、本APIを
呼び出した。 APIが返すMEMORY_BSIC_INFORMATIONのProtectメンバは、
0x80(=PAGE_EXECUTE_WRITECOPY)となっていた。(3) VirtualProtectEx
第1引数のhProcessをターゲットのプロセス・ハンドルに、
flNewProtectは次の値にしてみてそれぞれ確認。
・ PAGE_EXECUTE_READWRITE
・ PAGE_READWRITE
・ PAGE_WRITECOPY(4) ReadProcessMemory、またはWriteProcessMemory
第1引数のhProcessをターゲットのプロセス・ハンドルに、
ReadProcessMemory、またWriteProcessMemoryを呼び出した。
APIの戻り値はTRUEであったが、期待とは異なり次のような結果となった。・ ReadProcessMemory
0x00ではない値が入っているはずなのに、0x00と読まれた。・ WriteProcessMemory
書き込みしようとした値が書かれず、元の値のままであった。Read、Writeしたいメモリは、デバッガのデバッグ/メモリ・ツールで
読めています。
書き込みができていないことは、その後の処理が書き込み前と
同じとなってしまうことから判断しました。【ソース・コード・イメージ】
/*
* ターゲット・プロセスのメモリ書き換え
*
* 以下の処理は、デバッグ・メイン・ループにおいて、
* CREATE_PROCESS_DEBUG_EVENT, LOAD_DLL_DEBUG_EVENT等の後の
* 最初のBREAKPOINTイベントの処理として行っています。
*/
DWORD ReplaceMemory(
PVOID AddrRW,
PVOID* pWdata,
・・・ )
{
void* rdata ;
MEMORY_BASIC_INFORMATION mbi ;
DWORD dwOldProtect ;if ( !::VirtualQueryEx( m_hTarget, AddrRW, &mbi, sizeof(
MEMORY_BASIC_INFORMATION ) ) ) {
return( ::GetLastError() ) ;
}if ( !::VirtualProtectEx(
m_hTarget, mbi.BaseAddress, mbi.RegionSize,
PAGE_EXECUTE_READWRITE, &dwOldProtect ) ) {
return( ::GetLastError() ) ;
}if ( !::ReadProcessMemory(
m_hTarget, AddrRW, &rdata, sizeof( rdata ), NULL ) ) {
return( ::GetLastError() ) ;
}if ( !::WriteProcessMemory(
m_hTarget, AddrRW, pWdata, sizeof( *pWdata ), NULL ) ) {
return( ::GetLastError() ) ;
}・・・・・・・
}/*
* メイン・ループ
*/
void DebuggerMain()
{
PVOID AddrRW ;
PVOID wdata = 0x12345678 ;
HANDLE m_hTarget ; // ターゲット・プロセス・ハンドル
DEBUG_EVENT de ; // デバッグ・イベント
BOOL bFirstBreak = TRUE ; // 初めてのブレーク::CreateProcess( ..., DEBUG_PROCESS, ... ) ;
while ( ::WaitForDebugEvent( &de, INFINITE ) ) {
switch ( de.dwDebugEventCode ) { // イベント・コード ?
case LOAD_DLL_DEBUG_EVENT:
・・・・・case CREATE_PROCESS_DEBUG_EVENT: // プロセス生成イベント ?
・・・・・case CREATE_THREAD_DEBUG_EVENT: // スレッド生成イベント ?
・・・・・case EXIT_PROCESS_DEBUG_EVENT: // プロセス終了イベント ?
・・・・・case EXCEPTION_DEBUG_EVENT: // 例外キャッチ ?
// ブレークポイント例外 ?
if ( de.u.Exception.ExceptionRecord.ExceptionCode
== EXCEPTION_BREAKPOINT ) {
if ( bFirstBreak ) { // 初めてのブレーク ?
bFirstBreak = FALSE ;
ReplaceMemory( AddrRW, &wdata ) ; // ターゲット・プロセスのメモリ書き換え
・・・・・
}
・・・・・- 編集済み ななち 2010年3月29日 4:31
回答
-
みなさん、申し訳ありません。
他プロセスへのWriteProcessMemory、ReadProcessMemoryは、期待通りに動作していました。
Visual Studio 2008のデバッグ・ツール/メモリでは、あたかも他プロセス・メモリが
読めているよう表示されますが、この表示は他プロセス・メモリ内容の実態を反映していない
ことがわかりました。早い話が、デタラメを表示しているということです。 期待とは異なるメモリの内容を
表示しているようです。
おそらくWriteProcessMemory、ReadProcessMemoryで扱うべき線型仮想アドレス
(LDTに設定されているアドレスをベースにしたアドレス体系?)のメモリ内容を読んでいる
わけではないのでしょう。確認においてメモリ書き換えの効果がなかったように見えたのは、他の問題、つまりバグのようでした。
作業を進めていくうちに、他プロセスの実行状況が掴めるようになり、問題箇所を特定できるように
なったおかげです。お騒がせして申し訳ありませんでした。
Y. Peabrain- 回答としてマーク ななち 2010年4月26日 6:35
すべての返信
-
Y. Peabrainです。
補足情報です。
試験環境は、Windows XP with SP3です。
同じソース・コードでは試していませんが、Windows 7でも同じ結果になるものと
思っています。 初期は、Windows 7で動作確認を行っていました。また、試したアカウントの権限は、一般Userです。
一時期異なるソース・コードで、Administrators権限でも試してみましたが、
User権限で試した時と異なる現象は見られませんでした。そもそも、デバッガ・プロセスのデバッグ対象プロセスの制御は、
User権限でも可能なはずです。なお、ウィルス対策ソフトに、Norton Internet Security 2010を使用しています。
これに悪影響が与えられていることってことも、考えられるのでしょうか ?現在検証中のデバッガ・プロセスを裸で起動した場合、その中でターゲット・プロセスを
起動しようとすると、Nortonに警告されてしまいます。他プロセス・メモリをRead、Writeしようとしている時、Nortonの警告は
表示されていませんが、何か干渉されているのかもしれません。# NNTP Bridgeからの投稿だと、半角カナや特殊文字を使わなくても
# 文字化けが発生しました。 しかも、重い!
Y. Peabrain
- 編集済み ななち 2010年3月28日 10:58 検証した事柄情報を追加
-
Y. Peabrainです。
「Advanced Windows 第5版/第13章 Windowsメモリアーキテクチャ」によると、
コード・ページの書き換えを行う際のメモリ・ページ保護属性が
PAGE_EXECUTE_WRITECOPY になっていれば、
Read、Writeを行ってもアクセス違反にはならないとのことです。確かにアクセス違反は、発生していませんでした。
先にお知らせした私が試験しているケースでは
VirtualProtectEx によるメモリ・ページ保護属性の変更は不要でしたね。> (2) VirtualQueryEx
> ターゲット・プロセスのRead、Writeしたいメモリ・アドレスは、
> 取得済みで、デバッガからそれが誤っていないことを確認すみです。
> ちなみに、メモリはコード・ページです。
>
> ターゲット・メモリのアクセス権を知るため、本APIを使用。
>
> ターゲット・プロセスのプロセス・ハンドルを第1引数にして、本APIを
> 呼び出した。 APIが返すMEMORY_BSIC_INFORMATIONのProtectメンバは、
> 0x80(=PAGE_EXECUTE_WRITECOPY)となっていた。ただし、なんで読み書きができていないのかっていう件は、
今なお見当がついていません。何か、お気づきのことがありましたら、よろしくお願いいたします。
Y. Peabrain -
Y. Peabrainです。
私がやろうとしていることとほぼ同じようなソース・コードを
見つけました。
皆さんへの参考として、示したいと思います。http://nienie.com/~masapico/doc_ApiSpy.html
先に示した例と違っていることといったら、FlushInstructionCache APIを
呼んでいることだけだと思いますが、これは既に試しています。個人的には、セキュリティ対策でReadProcessMemory、WriteProcessMemoryの仕様が
変更されたのではと疑っています。別の方法として、ターゲット・プロセスを先に起動、
問題のプロセスで次に示すようにターゲット・プロセスをアタッチ
しようしてみましたが、これは「アクセス拒否」で成功しません。・ OpenProcess( PROCESS_ALL_ACCESS | PROCESS_VM_WRITE, ... )
これも気に入りません。
CreateProcessでのプロセス起動は、プロセス・アクセス権はPROCESS_ALL_ACCESSで
生成されるはずです。
なぜ、アクセス拒否されるのか、理由がさっぱり見当つきません。" | PROCESS_VM_WRITE"は、POCESS_ALL_ACCESSに含まれているはずで、
ただ単にターゲット・プロセスにこういうアクセスをしますよと明示している
だけのものです。とりあえず、OS最新の状態ではなく、ちょっと古いXP + SP2で同じ問題を
検証しようと思っています。
Y. Peabrain -
参考として投稿します。
作ろうとしているものはデバッガではありませんが、デバッガ実装の技術はかなり
利用します。WriteProcessMemoryによる、ターゲット・プロセスのコード・ページ直接書き換えは
あきらめ、代わりに次の方法をとることにしました。(1) コード・ページを書き換えるDLLをターゲット・プロセスに挿入。
CreateRemoteThreadにより、DLLのコードを実行。 ターゲットのプロセス空間で、
コード・ページを書き換える。
自プロセスのコード・ページを書き換えられることは、確認すみ。(2) 仮称デバッガ・プロセスからターゲット・プロセスへ、パイプによって
コード・ページ書き換え依頼を行う。(3) ターゲット・プロセスは、いつでもコード・ページ書き換え依頼が受け付けられるよう、
仮称デバッガ・プロセスからのコード・ページ依頼受信と、実際の書き換え処理を1つの
スレッドで行うようにする。これにも、1つ問題があります。
ContinueDebugEvent APIを呼び出すまでのデバッグ・イベント処理期間中は、イベントを上げた
スレッドのみならず、ターゲット・プロセス全体が停止状態になってしまいます。デバッガ・プロセスからのコード・ページ書き換え依頼はデバッグ・イベント中に行います。
この期間中は、ターゲットのコード書き換えスレッドが動作しないので、
デバッガ・プロセス依頼と同時に、ターゲット・プロセスのコード・ページ書き換えができない
ということになります。ContinueDebugEvent APIを呼び出すと、コード書き換えスレッドは動きますが、
どのタイミングでコード書き換えを行うのかが不定となってしまいます。どう任意のタイミングでコード書き換えを行わせるのかが大きな問題です。
命令コードの書き換えですから、書き換えタイミングをきちんと制御しなければ
なりません。当面は以上の方法で逃げますが、最終的には、やはり他プロセスのメモリを書き換えるAPIが
欲しいところです。なお、ReadProcessMemoryについては、他プロセス・メモリであっても、
読めるケースがありました。
読めないケースと読めるケースの違いについては、詳しくは調べていません。- 編集済み ななち 2010年4月18日 15:12 デバッグ・イベント処理期間中のターゲット状態説明が足らなかった
-
みなさん、申し訳ありません。
他プロセスへのWriteProcessMemory、ReadProcessMemoryは、期待通りに動作していました。
Visual Studio 2008のデバッグ・ツール/メモリでは、あたかも他プロセス・メモリが
読めているよう表示されますが、この表示は他プロセス・メモリ内容の実態を反映していない
ことがわかりました。早い話が、デタラメを表示しているということです。 期待とは異なるメモリの内容を
表示しているようです。
おそらくWriteProcessMemory、ReadProcessMemoryで扱うべき線型仮想アドレス
(LDTに設定されているアドレスをベースにしたアドレス体系?)のメモリ内容を読んでいる
わけではないのでしょう。確認においてメモリ書き換えの効果がなかったように見えたのは、他の問題、つまりバグのようでした。
作業を進めていくうちに、他プロセスの実行状況が掴めるようになり、問題箇所を特定できるように
なったおかげです。お騒がせして申し訳ありませんでした。
Y. Peabrain- 回答としてマーク ななち 2010年4月26日 6:35
-
皆さんへの参考として、下のコードを提示したいと思います。
このままコンパイルが通って実際に実行できるようなコードではありませんが、
他プロセス・メモリの読み書きについて、イメージが掴めるのではないかと思います。このコードは、他プロセスのある関数のエントリにブレーク・ポイントが設定されていて、
ブレークが発生した時、スタックから関数の戻りアドレスと3つの引数を読むものです。--------------------------------------------------------------------------------
void* RetnAddr ; // 関数戻りアドレス
DWORD Args[3] ; // 関数の引数DWORD dwStackBase ; // スタック・ベース・アドレス
CONTEXT context ; // スレッド・コンテキスト
LDT_ENTRY Ldt ; // スタック・セグメントLDT
HANDLE hProcess ; // ターゲット・プロセスのハンドル
HANDLE hThread ; // 〃 スレッドのハンドル::memset( Args, 0x00, sizeof( Args ) ) ;
//
// レジスタ読み取りとスタック・ベースの算出
//
context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS ;
if ( !::GetThreadContext( hThread, &context )
|| !::GetThreadSelectorEntry( hThread, context.SegSs, &Ldt ) ) {
return( エラー ) ;
}dwStackBase = ( Ldt.HighWord.Bytes.BaseHi << 24 ) + ( Ldt.HighWord.Bytes.BaseMid << 16 ) + Ldt.BaseLow ;
//
// スタックからリターン・アドレスと引数を読む
//
// スタックよりAPI戻りアドレスを読む
if ( ReadProcessMemory( hTarget, dwStackBase + context.Esp, &RetnAddr, sizeof( RetnAddr ) ) <= 0
// 関数の引数を読み取る
|| ReadProcessMemory( // Read calling argument
hTarget, // target process
context.Esp + 4 + dwStackBase ), // 1st arg address
Args, // 引数値読み取り領域
sizeof( Args ) ) <= 0 ) { // 読み取りサイズ
return( エラー ) ;
}//
// 関数エントリのコードを元に戻す
//
--context.Eip ; // インストラクション・ポインタを1つ戻す
context.EFlags |= EFLAGS_TF ; // シングル・ステップ設定
if ( !WriteProcessMemory( hTarget, // 関数エントリに原コードを戻す
pApiEntry->m_dwEntryAddr,
&pApiEntry->m_byOrigCode,
sizeof( pApiEntry->m_byOrigCode ) )
|| !::SetThreadContext( hThread, &context ) ) {
return( エラー ) ;
}/*
* シングル・ステップのデバッグ・イベントが上がった時、
* 再び関数のエントリにブレーク・ポイントを設定する。
*/--------------------------------------------------------------------------------
/*
* 他プロセス・メモリへの書き込み
*/
int WriteProcessMemory(
__in HANDLE hProcess,
__out DWORD dwAddr,
__in const void* pbuf,
__in int size )
{
SIZE_T dwWritten = 0 ;
MEMORY_BASIC_INFORMATION mbi ;
DWORD dwOldProtect ;if ( ::WriteProcessMemory( hProcess, (LPVOID)dwAddr, pbuf, size, &dwWritten ) ) {
::FlushInstructionCache( hProcess, (LPCVOID)dwAddr, size ) ;
return( dwWritten ) ;
}
if ( ::GetLastError() != ERROR_ACCESS_DENIED ) {
return( 0 ) ;
}if ( !::VirtualQueryEx( hProcess, (LPCVOID)dwAddr,
&mbi, sizeof( MEMORY_BASIC_INFORMATION ) ) ) {
return( 0 ) ;
}
/*
* リージョンまたがりがないか注意すること
*/
if ( !::VirtualProtectEx( hProcess,
mbi.BaseAddress, mbi.RegionSize, PAGE_WRITECOPY, &dwOldProtect )
|| !::WriteProcessMemory( hProcess, (LPVOID)dwAddr, pbuf, size, &dwWritten ) ) {
dwWritten = 0 ;
}
else {
::FlushInstructionCache( hProcess, (LPCVOID)dwAddr, size ) ;
}
::VirtualProtectEx( hProcess, mbi.BaseAddress, mbi.RegionSize, mbi.Protect, &dwOldProtect ) ;return( dwWritten ) ;
}/*
* 他プロセス・メモリの読み込み
*/
int ReadProcessMemory(
...........
とほほん