第12回  「メモリへの書き込み関数」



メモリ書き換え関数(MemoryEdit)

早速ですが、メモリ書き換えを行う自作関数を見ていきましょう。
この関数は、「プロセス名」と「改造コード」を受け取り、指定のプロセスを書き換える関数です。


MemoryEdit ソースプログラム1

Function MemoryEdit(ByVal ExeName As String, ByVal Code As String) As Long

01>  Dim StartAddress As Long  '書き換えを開始するアドレスを格納
02>  Dim EndAddress As Long   '書き換えを終了するアドレスを格納
03>  Dim Data As String      '書き込むデータを一時的に格納
04>  Dim PutValue As Byte    '書き込えるデータを格納
05>  Dim i As Long, j As Long   'カウンタ
06>  Dim ProcessHandle As Long '書き換えるプロセスのハンドルを格納
07>  Dim Count As Long      '書き換えに成功したかどうかの判断
08>  Dim rc As Long         'ハンドルのクローズ関数の戻り値を格納

09>  '改造コードの種類を判定&処理
10>  If InStr(1, Code, ">") Then
11>    StartAddress = CLng("&h" & Left(Code, InStr(1, Code, ">") - 1))
12>    EndAddress = CLng("&h" & Mid(Code, InStr(1, Code, ">") + 1, _
          InStr(1, Code, "-") - InStr(1, Code, ">") - 1))
13>  Else
14>    StartAddress = CLng("&h" & Left(Code, InStr(1, Code, "-") - 1))
15>    EndAddress = StartAddress
16>  End If
17>  Data = Mid(Code, InStr(1, Code, "-") + 1)

18>  '改造コードの1文字目が「S,M,L,$」のいずれかであれば、対応する16進数の文字列に変換する
19>  Select Case UCase(Left(Data, 1))
20>    Case "S", "M", "L", "$"
21>      Data = DecOrAsciiToHex(Right(Data, Len(Data) - 1), Left(Data, 1))
22>    Case Else
23>  End Select

24>  If Len(Data) Mod 2 <> 0 Then
25>    Data = "0" & Data
26>  End If


おや?何処かで見た覚えがありますね。
そうです、第10回で紹介した、ファイル書き換え関数とそっくりなんです。
違うところと言えば、宣言している変数が少し異なっているぐらいでしょう。

そんなわけで、ここまでの説明は省略します。
では続けて、後半部分を見ていきましょう。


MemoryEdit ソースプログラム2

'プロセスのハンドルを取得
ProcessHandle = GetProcessHandle(ExeName)
If ProcessHandle = 0 Then
  MemoryEdit = 1
  Exit Function
End If

'メモリ書き換え
For i = 0 To EndAddress - StartAddress Step Len(Data) / 2
  For j = 0 To Len(Data) / 2 - 1
    PutValue = CLng("&h" & Mid(Data, j * 2 + 1, 2))
    WriteProcessMemory ProcessHandle, StartAddress + i + j, PutValue, 1, Count
    '書き換えに失敗していないか判断
    If Count <> 1 Then
      rc = CloseHandle(ProcessHandle)
      MemoryEdit = 2
      Exit Function
    End If
  Next j
Next i

'ハンドルのクローズ
rc = CloseHandle(ProcessHandle)

'書き換えに成功した時
MemoryEdit = 0
End Function



これも、ファイル書き換え関数によく似ていますね?
特に、メモリ書き換えのループ部分はほぼ同じです。

というわけで、ループについての詳しい説明も省きます。
以降は、ファイル書き換えの時には出てこなかった部分に特化して解説してきましょう。

まずは、プロセスのハンドルを取得している部分です。
GetProcessHandle」は、プログラムの名前を与えると、そのプロセスのハンドルを返す自作関数です。

ところで、「ハンドル」というのは一体何なんでしょうか?
実のところはっきりとした説明は私にもできません。(^^;

あえて述べるならば、「Windowsがオブジェクトに与える番号」という所でしょうか?
プロセス等のオブジェクトを区別・指定する為にOSが管理している番号・・・としてもよいでしょう。

プログラムの名前が分かっているからといって、そのプログラムを書き換えられるとは限りません。
なぜなら、同じプログラムを同時に複数起動している可能性もあるからです。

そんなわけで、名前よりも確実にプロセスを指定するためにハンドルを使用しています。
だから、まずはハンドルを取得する必要があるんですね。

GetProcessHandleは、プロセスのハンドルの取得に失敗すると「0」を返すので、
この時は、関数が失敗した事を示す目印として、戻り値「1」をセットし関数を抜けます。

一方、プロセスハンドルの取得に成功したならばメモリを書き換えます。
ループの内容の詳しい説明は省きますが、1バイトずつメモリを書き換えているのです。

WriteProcessMemory」とは、メモリを書き換えるためのAPI関数です。
引数は左から順番に以下のようになります。

「書き換えるプロセスのハンドル」「書き換えるアドレス値」「書き込む値」「書き込むバイト数」「実際に何バイト書き換えたか」

最後の引数は、関数を実行したときに値が得られるもので、いわば戻り値の役目を果たしています。
引数を戻り値のように使う方法については、VBちょっといい話を参照して下さい。

そして、この引数を利用して、書き換えが成功しているかどうかを調べています。
1バイト書き換えるという指示を4番目の引数で与えているので、
Count が1以外の値だと、書き換えに失敗したという事が分かります。
もし失敗していたら、使用していたハンドルをクローズし、書き換え失敗の目印を戻り値に指定し、関数を抜けます。

無事に最後まで書き換えが終わったら、ハンドルをクローズします。
ファイルと同じく、使い終わったハンドルは必ずクローズする必要があります。
(ただし、VBの「hWnd」プロパティ、「hDC」プロパティは、特殊なハンドルなので 解放する必要はありません)

そうしないと、メモリーリーク(使用していないのにメモリを確保し続ける事)を引き起こしてしまいます。
これは長時間ほっておくと、リソース不足になったり、動作が不安定になったりしますので注意しましょう。

特に、分岐処理が入ると、ハンドルのクローズをうっかり忘れてしまう事が多いです。
(クローズ処理の記述があっても、実行時にその部分を飛ばしてしまう等)

そして、書き換えに成功した事を示す戻り値「0」をセットして関数を終了します。


プロセスのID・ハンドルを取得する

先ほど、プロセスのハンドルを取得するために、GetProcessHandleという関数を利用しましたね。
今度はこの関数を見ていきましょう。


GetProcessHandle ソースプログラム

Private Function GetProcessHandle(ByVal ExeName As String) As Long

Dim ProcessID As Long

'まずはプロセスIDを取得する
ProcessID = GetProcessID(ExeName)
If ProcessID = 0 Then
  Exit Function
End If

'読み取り、書き込み、操作権限
GetProcessHandle = OpenProcess(&H38, 0, ProcessID)

End Function



なかなかシンプルですね。
では、簡単に説明していきましょう。

プロセスのハンドルを得るためには、事前にプロセスIDを取得する必要があります。
では、「プロセスID」とは何なんでしょうか?

実はこれもうまく説明する自信はありません。(汗)
まあ、とにかくプロセスを識別する為の番号だと考えていただければ良いでしょう。

プロセスIDとは、その名の通りプロセスに付けられたIDの事ですが、
ハンドルは、プロセスに限らず、オブジェクトに付けられた番号の事です。
って、正確な説明にはなってないんですが、あまり気にしないで下さいませ。

で、そのプロセスIDを取得する為には「GetProcessID」という自作関数を使用しています。
プロセスIDの取得に失敗したなら、実行失敗の目印である「0」を戻り値にセットして関数を抜けます。

成功したならば、そのプロセスIDを使ってプロセスハンドルを取得します。
プロセスハンドルの取得には「OpenProcess」というAPI関数を使用します。

引数は左から順番に以下のようになります。
「アクセス権」「カレントプロセスを継承するかどうか」「プロセスID」

アクセス権とは、オープンしたプロセスに対して実行できる権利の事です。
今回は「読み込み」「書き込み」「操作」が可能になるように指定しています。

尚、「読み込み」の権限は無くても、MemoryEdit関数の動作に支障はありませんが、
他の関数で利用する為に、「読み込み」の権限も付加してあります。

カレントプロセスを継承するかどうかは、「0・・・継承しない」「1・・・継承する」という具合になります。
そもそもカレントプロセスの継承とは何かって所は気にしないで下さい。


では続いて、GetProcessIDについて見ていきましょう。


GetProcessID ソースプログラム

Private Function GetProcessID(ByVal ExeName As String) As Long
Dim hSnap As Long
Dim rc As Long
Dim PE As tagPROCESSENTRY32
Dim PExeName As String

hSnap = CreateToolhelp32Snapshot(2, 0)
PE.dwSize = 296
rc = Process32First(hSnap, PE)

Do While rc
  PExeName = StrConv(PE.szExeFile, vbUnicode)
  PExeName = CutNullChar(Mid(PExeName, InStrRev(PExeName, "\") + 1))
  If UCase(PExeName) = UCase(ExeName) Then
    GetProcessID = PE.th32ProcessID
    rc = CloseHandle(hSnap)
    Exit Function
  End If
  rc = Process32Next(hSnap, PE)
Loop

rc = CloseHandle(hSnap)
GetProcessID = 0
End Function



今度はかなり複雑ですね。
これを1つ1つ解説するのは非常にややこしいので概要のみ説明します。

まず、引数としてプログラムの名前を受け取ります。
それから、現在実行中のプログラムをリストの形にします。
そして、ループの中で以下のような処理を行っています。

1.先ほどのリストの先頭から得たプログラムの名前を、適切な形に変換する
2.その名前が、引数で指定されたプログラムの名前と一致するかを調べる
3.一致したらそのプロセスのIDを戻り値に格納する。

これを、プログラムのリストと、指定されたプログラムの名前とが一致するか、
プログラムのリストの最後まで調べるまで繰り返します。

最後まで見つからなかった時は失敗となり、関数を抜けています。

要は、しらみつぶしに指定したプログラムを探し出しているという事ですね。


今度の解説はいかがだったでしょうか?
最後はちょっと手抜きになりましたが、これでメモリ書き換え関数の説明はお終いです。

APIを利用していたりするので、少し難しかったかもしれませんが、
一気に理解できなくても全く問題はありません。
このソースを流用したりしてプログラムを作っていくうちに、段々と理解できるようになるでしょう。

では、今回はここまでです。
次回は、レジストリの書き換えを行う部分を解説します。



前に戻る     次に進む

講座の初めに戻る