update 2003.11.05 第3回

YS6 EXP Viewerの制作過程


第3回 プロセスメモリの読み書き
前回はプロセスオープンの方法を紹介しましたが、今回はその続きのプロセスメモリの読み書きを行います。
ただ、いきなり実践でステータスモニタを作っても理解しきれない可能性が高いので、
とりあえずサンプルを作って色々実験してみましょう。

Win32コンソールプログラム
今さらコンソールというのも何ですが、さりげにMFC(Microsoft Foundation Class)まで使えてしまったりと
実はサンプルプログラムを作る程度であれば今でも十分実用レベルになったりします。

とりあえずVisual C++6.0を用いた場合で解説しますが、
新規作成→プロジェクト→Win32 Console Application→MFCをサポートするアプリケーション
の順で任意のWin32コンソールアプリケーション開発画面に進みます。
ソースファイルのメインCPPソースを開いて、
	// MFC の初期化および初期化失敗時のエラーの出力
	if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0))
	{
		// TODO: 必要に応じてエラー コードを変更してください。
		cerr << _T("Fatal Error: MFC initialization failed") << endl;
		nRetCode = 1;
	}
	else
	{
		// TODO: この位置にアプリケーションの動作を記述してください。
		CString strHello;
		strHello.LoadString(IDS_HELLO);
		cout << (LPCTSTR)strHello << endl;
	}
この記述のところからプロセス取得→オープン→メモリ編集のプログラムを組みます。
とりあえずラストのCString→coutのHello,world!出力部分は要らないので消しましょう。
そして、この中にプログラムを組んでゆきます…が、
読み込みサンプルプログラムのC++ソースコードのみ ダウンロード

読み込みサンプルプログラム一式 ダウンロード

というわけで今回は既に組んだサンプルを見ながら解説します。
(上記の方法でWin32 Console Applicationのプロジェクトを開いた後に、
メインのCPPソースコードに上書きすることでビルドできます)。

説明
処理の流れを説明すると、
引数にプロセス名を指定→アドレスを指定→読み込みサイズを指定→読み込んでデータを表示
という極めてシンプルなものです。

では初めに49行目のプロセス名指定の処理を。
		if(argc==1){
			printf("プロセスメモリ読み込みサンプル\n");
			printf(" usage:pro_read.exe {対象プロセス名}\n");
			return 0;
		}
		strupr(argv[1]); // 引数指定されたプロセス名を大文字に変換
これは、これまでの改造講座でも頻繁に出てきた「引数指定」です。
例えば C:\GAME>DISWIN -s START.EXE
という逆アセンブルのコマンドであればこのうち -s と START.EXE は引数になります。
これと同じようにこのリードサンプルプログラムでは引数にプロセス名を指定します。
(例)>PRO_READ.EXE EXPLORER → エクスプローラのプロセスを指定

そのまま前回までに説明したプロセス名取得→プロセスオープンの過程を経て、
// アドレスと読み込むバイト数を入力してそのデータを出力
		printf("アドレス(16進)="); scanf("%lx",&ulReadAddress);
		printf("サイズ(10進)="); scanf("%d",&iReadSize);
		// 読み込み
		ReadProcessMemory( hnd,(LPVOID)(DWORD)ulReadAddress,bMemoryData,iReadSize, NULL);
		// 表示
		for(i=1;i<=iReadSize;i++){
			printf("%02X ",bMemoryData[i]);
			if(i%16==0) puts(""); // 16バイトごとに改行
		}
今回のメインであるプロセスメモリを読み込む処理に入ります。
説明しなくてもわかると思いますが、ReadProcessMemory()が名前の通りメモリの参照用関数です。
引数の内容は以下のとおりです。
hndOpenProcess()で開いたハンドルを指定
(LPVOID)(DWORD)ulReadAddress(LPVOID)(DWORD)型でアドレスを指定します。
(LPVOID)(DWORD)0x401000のようにアドレス値を直接指定も出来ます。
bMemoryData読み込んだデータを格納するバッファのアドレスを指定します。
配列以外に読み込む場合は当然 &bMemoryData のように指定します。
iReadSize読み込むサイズをバイトで指定します。
NULL本来は関数実行後に読み取り成功したバイト数が返ります。
狭い範囲を簡単に読むサンプル程度であれば不要です。

例えばDWORD(4バイト)型変数dwDataにアドレス0x00567890の情報を格納するなら、
ReadProcessMemory(hnd ,(LPVOID)(DWORD)0x00567890 ,&dwData ,4 ,NULL);
こんな感じになります。

なお、サンプルとして適当に行うと危険なので組みませんが、
プロセスメモリに書き込む場合は
WriteProcessMemory(hnd ,(LPVOID)(DWORD)0x00567890 ,&dwData ,4 ,NULL);
このように記述します。
この例ではアドレス0x00567890にdwDataの4バイトを書き込みますが、
書き換えても平気なアドレスを予めサーチしてからでなければ危険です。
こちらは特に注意してからご使用ください。

これまでの講座でわかっていること
第1回講座で経験値の格納されているアドレスがわかっています。
今回の講座で任意のアドレスからデータを読み込む方法がわかりました。

…実際にはもうモニタツールを作れるだけの情報は十分揃ってたりしますね(^^;;;
ダイアログベースでSetTimer()を用いて一定周期ごとに情報を収集して、
EditBoxに表示する方法を使っても、それは十分モニタツールになります。

次回は少し番外でウインドウのタイトルバーの書き換えです。
ま、YS6 EXP Viewerがそういう方法をとっているのでそのついでに、ですね。

>>第4回に続く