おもちゃバコ

中身スカスカ♡

C++: プロセスを取得して色々やってみた

こんにちは

Windowsでプロセスを取得してスナップショットを取ったり,仮想キーコードを送信したりしてみた時の備忘録です。

冗長になりますが,わかりやすさを優先してサンプルのソースコードはエントリポイントから全て記述します。


参考書籍

解析関係の書籍を参考にしました。


開発環境


自己流ですので正確な情報は書籍やMicrosoft公式を確認してください。

プロセスのタイトル名を取得

現在実行されているプロセスのタイトル名を取得してみました。
ここでは,プロセスの取得にEnumWindows関数,タイトル名の取得にGetWindowTextW関数を利用します。

EnumWindows関数

EnumWindows関数はトップレベルのウィンドウハンドルを列挙するために利用されるWin32 APIです。
子ウィンドウはEnumWindows関数では列挙できないので注意してください。

BOOL EnumWindows
(
  [in] WNDENUMPROC lpEnumFunc, // コールバック関数
  [in] LPARAM      lParam      // コールバック関数に渡す任意引数
);

(余談)
EnumWindows関数はマルウェアマルウェア解析のプログラムでよく利用される関数です。
セキュリティソフトによってはEnumWindows関数を使ったプログラムをマルウェア判定することがあります。
https://eset-info.canon-its.jp/malware_info/special/detail/220216_2.html

自分はESETのセキュリティソフトを利用しているのですが,本文中のプログラムをコンパイルする度に生成されるバイナリファイルを自動で削除していました。
Visual StudioからC/C++の最適化設定を色々弄ったらバイナリファイルがマルウェア判定されなくなりましたが,これは大丈夫ですかね?

GetWindowTextW

ウィンドウハンドル(HWND)はEnumWindows関数を利用すると簡単に取得できます。

int GetWindowTextW
(
  [in]  HWND   hWnd,     // ウィンドウハンドル
  [out] LPWSTR lpString, // テキスト用のバッファ
  [in]  int    nMaxCount // コピーする最大文字数(基本的にバッファサイズ)
);

ソースコード

EnumWindows関数はエラーの場合は0を返します。
Enumwindows関数に限らず,大体のWin32 APIのエラー詳細はGetLastError関数で確認できます。

コールバック関数
一部プロセスのタイトル名がワイド文字でないと取得できなかったのでstd::wstringを利用しました。
よって,標準出力にはstd::wcoutを利用しています。
標準出力に何も表示されない場合は,std::setlocale関数を使用すると上手くいくかも。

結構無茶なキャストをしている気がします。
とりあえず動くのでヨシッ。

プロセスのタイトル名からウィンドウハンドルを取得

プロセスのタイトル名が取得できれば簡単です。
今回はVSCodeのウィンドウハンドルを取得してみましょう。

ウィンドウハンドルの取得にはFindWindowW関数を使用します。

FindWindowW関数

FindWindowW関数は指定された文字列と同じタイトル名のトップレベルのウィンドウハンドルを取得します。

HWND FindWindowW
(
  [in, optional] LPCWSTR lpClassName, // クラスのアトム
  [in, optional] LPCWSTR lpWindowName // ウィンドウのタイトル名
);

第一引数のクラスアトムはよくわかりませんが,RegisterClass関数と関係があるような気がしています。
今回は自分でウィンドウを作成していないのでNULLを設定しました。

ソースコード

ウィンドウのタイトル名を正確に記述すればEnumWindows関数による列挙処理は必要ないですが, 今回は対象ウィンドウのタイトル名を部分一致で検索するのに使用しています。

FindWindowW関数が失敗したときのGetLastError関数の戻り値が0でした。
何かが変かも...?

プロセスのスナップショットを取得

プロセスのスナップショットを取得してみます。
プログラム解析の醍醐味ですね。

スレッドIDの取得

初めにウィンドウハンドルからスレッドIDを取得します。
スレッドIDの取得にはGetWindowThreadProcessId関数を使用します。

GetWindowThreadProcessId

ウィンドウハンドルからスレッドIDを取得します。

DWORD GetWindowThreadProcessId
(
  [in]            HWND    hWnd,
  [out, optional] LPDWORD lpdwProcessId
);
ソースコード

ほとんど変更はないですね。

スナップショットの取得

プロセスIDからスナップショットを取得しましょう。
この辺りはよく理解していないので詳細はマクロソフト公式を見てください。
https://learn.microsoft.com/ja-jp/windows/win32/toolhelp/traversing-the-module-list

CreateToolhelp32Snapshot

指定したプロセスIDのプロセスのヒープ・モジュール・スレッドのスナップショットを取得します。

HANDLE CreateToolhelp32Snapshot
(
  [in] DWORD dwFlags,      // スナップショットに含める要素
  [in] DWORD th32ProcessID // プロセススレッドID
);

今回はモジュールリストのスナップショットを取得したいのでTH32CS_SNAPMODULEを指定しました。

ソースコード

スナップショットを取得したら読み込みたいプロセスモジュールを取得しましょう。
プロセスのモジュールはModule32First関数とModule32Next関数で走査しています。

今回取得するバイナリファイル(exe)のモジュールのベースアドレスは次のメモリ読み取りで使用します。

出力結果
「モジュールって具体的になんだ」って思っていたのですが,実行してみたらバイナリファイルの集合っぽいなって思いました。
バイナリファイルを構成するためのexeやdllがまとまったものをモジュールと呼んでいるんですかね?

Target: sample.txt - Visual Studio Code
Binary=Code.exe,"VSCodeのバイナリへのパスが書かれている"
        ntdll.dll,C:\WINDOWS\SYSTEM32\ntdll.dll
        KERNEL32.DLL,C:\WINDOWS\System32\KERNEL32.DLL
        KERNELBASE.dll,C:\WINDOWS\System32\KERNELBASE.dll
        apphelp.dll,C:\WINDOWS\SYSTEM32\apphelp.dll
        OLEAUT32.dll,C:\WINDOWS\System32\OLEAUT32.dll
    ... 以下略

今回のプログラムはバイナリファイルに使われているdllファイルの列挙などにも使えますね。

プロセスのメモリ読み取り

次はプロセスのメモリを覗いてみます。
メモリ読み取りにはOpenProcess関数とReadProcessMemory関数を利用します。

OpenProcess

プロセスオブジェクトを取得します。
GetWindowThreadProcessId関数から取得したPIDを利用しましょう。

HANDLE OpenProcess
(
  [in] DWORD dwDesiredAccess, // プロセスのアクセス権
  [in] BOOL  bInheritHandle,  // ハンドルを継承するか
  [in] DWORD dwProcessId      // プロセスID
);

プロセスによっては解析対策でOpenProcess関数の使用やアクセス権を監視している場合があります。
はじめはプロセスのアクセス権をPROCESS_VM_READで使用するのがよいと思います。

ReadProcessMemory

指定したプロセスのメモリ領域を読み取ります。
OpenProcess関数が読み取りモードで成功していないと動作しません。

BOOL ReadProcessMemory
(
  [in]  HANDLE  hProcess,      // プロセスオブジェクト
  [in]  LPCVOID lpBaseAddress, // 読み取り開始のベースアドレス
  [out] LPVOID  lpBuffer,      // 読み取り先
  [in]  SIZE_T  nSize,         // 読み取るサイズ
  [out] SIZE_T  *lpNumberOfBytesRead // 転送されたバイト数
);

ソースコード

ReadProcessMemory関数のベースアドレスには,スナップショットを取得したバイナリファイルのベースアドレスを使用しました。
正直,APIの利用方法が正しいかはよくわかっていない。

出力結果
VSCodeのバイナリをベースアドレスから1024バイト分見てみました。
MZで始まることからおそらくプログラム領域が指定できているはず。

Target: sample.txt - Visual Studio Code
MZx@xo´     I!,LI!This program cannot be run in DOS mode.$PEd?UIcd"
?N?Do+÷?" Z?`AUORX§*h   o??o;o?'@    XY?^?Y( °N8?@??`.text??N?N `.rdataAdC?NfC?N@@.data<DF
d@A.pdatao;?;6@@.00cfg(?     <×@@.gxfg@°  B>×@@.retplneA ×.rodataA   ?× `.tls?0   ?×@A.voltblR@ ?×CPADinfo8P  ?×@A_RDATAo`  ?×@@malloc_hep        ?× `.rsrco?  A?×@@.relocXY@        Z`@B

応用

対象のプロセスをフォアグラウンドに配置して,仮想キーコードを送信してみました。
簡単なBOTぐらいは作れそうですね。

一見,char型で指定した文字を入力しているように見えますが,対応する仮想キーコードを入力しないと想定した動作にならないので注意してください。
https://learn.microsoft.com/ja-jp/windows/win32/inputdev/virtual-key-codes

(応用と書いていますが,スナップショットの項目で説明した部分は使用していません。)


感想

正しいソースコードなのかは分かりませんが,Windows APIでプロセス情報を取得する方法が少し理解できたのでヨシッ!

使い方によっては悪用できてしまうので注意したいですね。