Windows のデスクトップを右クリックしたときに出る コンテキストメニューは、実は Windows レジストリの設定で自由にカスタマイズできる。本記事では C++ + WinAPI で GUI ツール (ボタン・入力欄付きのウィンドウ) を作り、好きなプログラムを「右クリック → 起動」できるメニュー項目として追加・削除する。ウィンドウクラスの登録 / メッセージループ / イベント駆動 / RegCreateKey + RegSetValueEx + RegDeleteKey といった Windows プログラミングの古典的な作法を体感しつつ、レジストリ操作の危険性と マルウェアが永続化に使う仕組みまで通しで学ぶ。
Windows レジストリの操作は OS の動作設定を司る中核データベースへの書き込みで、誤ったキー / 値を編集・削除すると Windows が起動しなくなることがある。実行前に (1) システムの復元ポイント作成 + (2) レジストリのエクスポート (バックアップ) を必ず行う。HKEY_CLASSES_ROOT 配下への書き込みには 管理者権限が必要なため、本ツールは「管理者として実行」で起動する。マルウェア解析の文脈では、レジストリへの書き込み自体が「永続化」のサインとして EDR に検知されることもある — その意味も含めて、学習目的の範囲を超えない実装に留める。
Windows レジストリとコンテキストメニュー #
Windows レジストリは、Windows オペレーティングシステムやインストールされたアプリケーションの設定情報を一元管理する、階層型のデータベース。OS の動作、ハードウェア設定、ユーザープロファイルなど、あらゆる情報が「キー」と「値」の形式で格納されている。
デスクトップの何もないところを右クリックした際に出る「コンテキストメニュー」の内容も、このレジストリによって定義されている。具体的には、HKEY_CLASSES_ROOT\DesktopBackground\Shell というキー配下に、表示したい項目を定義することで、メニューに機能を追加できる。

普通のアプリの設定は ~/.config/ や 設定ファイル.ini に書かれているが、Windows は OS とアプリ全部の設定を 1 つの巨大なツリー型データベース (レジストリ) にまとめている。デスクトップの背景色からインストールしたソフトのライセンス情報、コンテキストメニューの項目まで、すべてここに入っている。右クリックで出るメニューも、結局は「HKEY_CLASSES_ROOT\DesktopBackground\Shell の下に何が登録されているか」で決まっているだけ。だから、ここに自分でキーを追加すれば、自由にメニューを増やせる。
実装の仕組み #
このツールは、ユーザーが GUI で入力した「表示名」と「プログラムのパス」を受け取り、WinAPI が提供するレジストリ操作関数 (RegCreateKey, RegSetValueEx, RegDeleteKey) を直接呼び出すことで機能する。
GUI 部分は、WinAPI の伝統的なウィンドウプログラミング (ウィンドウクラスの登録、ウィンドウ作成、メッセージループ処理) によって構築されている。
コンテキストメニューへの追加処理 #
「追加」ボタンを押すと、AddRegistry 関数が以下の 2 段階の処理を行う。
HKEY_CLASSES_ROOT\DesktopBackground\Shell\[表示名] というキーを作成。(既定値) に表示名を設定 (これがメニューに表示されるテキスト)。Icon という値にプログラムのパスを設定すると、メニュー項目に実行ファイルのアイコンを表示できる。...\[表示名]\Command というサブキーを作成。(既定値) に実行したいプログラムのフルパスを設定。Windows はこのキーに登録されたパスを実行する。コンテキストメニューからの削除処理 #
「削除」ボタンを押すと、RemoveRegistry 関数が追加時とは逆の手順で、作成したキーを削除する。レジストリキーはサブキーを持つと削除できないため、必ず階層の深い方 (Command キー) から先に削除する必要がある。
レジストリの RegDeleteKey は 「サブキーを持つキーは削除できない」仕様。だから親キーから先に削除しようとすると失敗する。木の枝を切る順番と同じで、葉から先に切らないと根元が落とせない。深いキー (Command) → 浅いキー (表示名) の順で削除する、というのが Windows プログラミングの定石。
C++ によるコード解説 #
WinMain と ウィンドウ初期化 (InitApp, InitInstance) #
WinMain は、WinAPI アプリケーションのエントリーポイント (開始地点)。
| 関数 | 役割 |
|---|---|
| InitApp | ウィンドウの基本的な外観や動作 (ウィンドウプロシージャ WndProc の指定など) を「ウィンドウクラス」として OS に登録 |
| InitInstance | クラス設計図を基に実際のウィンドウを CreateWindow で生成・表示 |
| メッセージループ | GetMessage で、ユーザー操作などのイベントを待ち受ける |
// InitApp: ウィンドウクラスをOSに登録
ATOM InitApp(HINSTANCE hInst) {
WNDCLASSEX wc;
// (ウィンドウのスタイル、プロシージャ、アイコンなどを設定)
return (RegisterClassEx(&wc));
}
// InitInstance: 実際にウィンドウを作成
BOOL InitInstance(HINSTANCE hInst, int nCmdShow) {
HWND hWnd;
hWnd = CreateWindow(
szClassName, // クラス名
TEXT("Registry Controller"), // タイトル
WS_OVERLAPPED | WS_SYSMENU, // スタイル
// (座標、サイズなどの設定)
);
// (ウィンドウの表示・更新)
return TRUE;
}
ウィンドウプロシージャ (WndProc) — WM_CREATE #
ウィンドウが作成された直後に一度だけ WM_CREATE メッセージが送信される。このタイミングで、ウィンドウ内に配置する GUI 部品 (コントロール) を作成する。CreateWindow や CreateWindowEx を使い、「STATIC」(静的テキスト)、「EDIT」(テキスト入力ボックス)、「BUTTON」(押しボタン) を適切な座標に配置する。
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
// (static 変数の定義)
switch (msg) {
case WM_CREATE: {
// "RegsName" の静的テキスト
HWND hRegsNameText = CreateWindowEx( ... TEXT("STATIC"), TEXT(" RegsName "), ... );
// "ProgPath" の静的テキスト
HWND hProgPathText = CreateWindowEx( ... TEXT("STATIC"), TEXT(" ProgPath "), ... );
// レジストリ名入力用エディットボックス
hRegsNameEdit = CreateWindowEx( ... TEXT("EDIT"), ... );
// プログラムパス入力用エディットボックス
hProgPathEdit = CreateWindowEx( ... TEXT("EDIT"), ... );
// 「追加」ボタン
CreateWindow( TEXT("button"), TEXT("追加"), ... (HMENU)add_BUTTON, ... );
// 「削除」ボタン
CreateWindow( TEXT("button"), TEXT("削除"), ... (HMENU)rm_BUTTON, ... );
break;
}
// ( ... 以下略 ... )
ウィンドウプロシージャ (WndProc) — WM_COMMAND #
WM_COMMAND メッセージは、ボタンがクリックされた時などに送信される。LOWORD(wp) でどの子ウィンドウ (ボタン) が操作されたかを識別する。
add_BUTTON または rm_BUTTON の場合、GetWindowTextA を使ってエディットボックス (hRegsNameEdit, hProgPathEdit) に入力されている現在のテキストを取得し、それぞれ AddRegistry または RemoveRegistry 関数に渡して実行する。
case WM_COMMAND: {
switch (LOWORD(wp)) {
case add_BUTTON: {
// エディットボックスからテキストを取得
GetWindowTextA(hRegsNameEdit, regsName, sizeof(regsName));
GetWindowTextA(hProgPathEdit, progPath, sizeof(progPath));
AddRegistry(regsName, progPath); // 追加関数を呼び出し
MessageBox(hWnd, TEXT("...追加されました。"), ...);
break;
}
case rm_BUTTON: {
GetWindowTextA(hRegsNameEdit, regsName, sizeof(regsName));
RemoveRegistry(regsName); // 削除関数を呼び出し
MessageBox(hWnd, TEXT("...削除されました。"), ...);
break;
}
}
break;
}
// ( ... 以下略 ... )
レジストリ追加関数 (AddRegistry) #
レジストリ操作の核心部。_stprintf_s で、ベースとなるキーパス (DesktopBackground\Shell\) とユーザーが入力した名前 (regsName) を連結し、フルパスを作成する。
| API | 役割 |
|---|---|
RegCreateKey |
指定したパスのキーを作成。既に存在する場合はそのキーを開く |
RegSetValueEx |
キーに対して「値」を設定。(LPBYTE)regsName や (LPBYTE)progPath で、実際のデータを書き込む |
RegCloseKey |
操作が完了したら、キーのハンドルを必ず閉じる |
これを、...\[regsName] キーと ...\[regsName]\Command キーの 2 回に分けて実行している。
void AddRegistry(TCHAR* regsName, TCHAR* progPath) {
HKEY hKey;
// パス文字列を構築 (Shellキー)
TCHAR szKey[] = TEXT("DesktopBackground\\Shell\\");
TCHAR fullszKey[256];
_stprintf_s(fullszKey, ...);
// パス文字列を構築 (Commandキー)
TCHAR szKey2[] = TEXT("\\Command");
TCHAR fullszKey2[256];
_stprintf_s(fullszKey2, ...);
// 1. Shellキーの作成と値の設定
if (RegCreateKey(HKEY_CLASSES_ROOT, fullszKey, &hKey) == ERROR_SUCCESS) {
RegSetValueEx(hKey, NULL, 0, REG_SZ, (LPBYTE)regsName, ...); // (既定値) = 表示名
RegSetValueEx(hKey, TEXT("Icon"), 0, REG_SZ, (LPBYTE)progPath, ...); // Icon = プログラムパス
RegCloseKey(hKey);
}
// 2. Commandキーの作成と値の設定
if (RegCreateKey(HKEY_CLASSES_ROOT, fullszKey2, &hKey) == ERROR_SUCCESS) {
RegSetValueEx(hKey, NULL, 0, REG_SZ, (LPBYTE)progPath, ...); // (既定値) = プログラムパス
RegCloseKey(hKey);
}
}
レジストリ削除関数 (RemoveRegistry) #
削除は RegDeleteKey 関数を使う。この関数は、サブキーが存在するキーを削除できないため、必ず階層の深いキー (fullszKey2 が指す Command キー) から先に削除し、その次に親キー (fullszKey が指す Shell キー) を削除する。
void RemoveRegistry(TCHAR* regsName) {
// (パス文字列の構築 ... AddRegistry と同様)
TCHAR fullszKey[256];
TCHAR fullszKey2[256];
// 下の階層から順にキーを削除
LONG result2 = RegDeleteKey(HKEY_CLASSES_ROOT, fullszKey2); // 1. Command キーを削除
LONG result1 = RegDeleteKey(HKEY_CLASSES_ROOT, fullszKey); // 2. Shell キーを削除
}
実際の動作 #
起動 #
ビルドが完了したら、実行ファイルを **「管理者として実行」**で起動する。
起動させると、プログラム名やパスを入力する欄やボタンが配置されたウィンドウが開く。

追加 #
今回は筆者がよくお世話になっている Bandicam をコンテキストメニューに追加してみる。
RegsName にプログラム (任意) の名前を入力し、ProgPath に起動させたいプログラムのパスを入力する。
追加ボタンをクリックすると、ウィンドウが表示されレジストリに入力した情報が追加される。

レジストリエディタで確認してみると、入力した情報がちゃんと追加できていることが確認できる。

コンテキストメニューを確認してみると、Bandicam という項目が追加されており、クリックすると Bandicam が起動した。

削除 #
これらの追加した項目が必要なくなったら、削除したいプログラム名を入力し、削除ボタンをクリックする。すると、ウィンドウが表示されレジストリから削除される。

再びレジストリエディタとコンテキストメニューを確認してみると、追加した項目が消えていることが分かる。


感想と考察 — レジストリ操作とセキュリティ #
今回はレジストリに変更を加え、コンテキストメニューに起動させたいプログラムを追加できるソフトを作った。
WinAPI を使い、ウィンドウを用いることによって直感的に操作できるプログラムの開発は、CUI とは異なる 「イベント駆動型」 のコーディング (ボタンが押されたら処理する、など) の難しさと面白さを実感する良い経験となった。
マルウェアの永続化との関係 #
特に、レジストリは悪意を持った攻撃者が攻撃に利用することも多いので、プログラムからレジストリを操作する手法を学ぶことは、セキュリティの分野においても有意義な学習だった。
例えば、マルウェアが自身の起動設定をレジストリに書き込んで **「永続化 (Persistence)」**する手口は有名:
| キー | 役割 (マルウェアが悪用する場面) |
|---|---|
HKCU\Software\Microsoft\Windows\CurrentVersion\Run |
ログイン時に自動実行されるプログラム一覧 |
HKLM\System\CurrentControlSet\Services |
サービスとして登録 (SYSTEM 権限で常駐) |
HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\Shell |
ログオン時のシェル置き換え |
HKCR\*\shellex\ContextMenuHandlers |
コンテキストメニュー拡張 (= 今回触った領域) |
今回 RegCreateKey や RegSetValueEx といった API を直接触ったことで、その仕組みをコードレベルで具体的に理解できた。
マルウェア解析の文脈では、レジストリの Run キーや Services への書き込み = 高確度で永続化マルウェアのサインと見なされる。CrowdStrike / Microsoft Defender for Endpoint / SentinelOne などの EDR は 「実行ファイルが Run キーに自分自身のパスを書き込んだ」挙動を検知し、即座にアラートを上げる。本記事のような 正規のレジストリ拡張アプリであれば、署名 + 管理者承認のもとで動くため検知されないが、署名なし / 非対話的 / 通信機能ありの組合せだと一発で疑われる。
結論 #
システムの根幹に触れるレジストリ操作は危険も伴うが、それ故に防御側としても知っておくべき必須知識だと改めて感じた。
レジストリは 「Windows の全部の設定が詰まった魔法の箱」。便利なカスタマイズの入口でもあり、マルウェア永続化の常套手段でもある。今回の 「コンテキストメニューに項目を追加する」という小さな実装で、RegCreateKey / RegSetValueEx / RegDeleteKey の使い方 + キーの階層構造 + 削除時の順序 + 管理者権限の要否という Windows プログラミングの基礎を一気に体感できた。「攻撃者の手口を、防御側の視点で実装してみる」のは、セキュリティ実務の理解を深めるうえで最も効率の良い学習方法の 1 つ。次は 「Run キーを監視する EDR ライクな小ツール」を書いて、攻撃と防御の両側を実装で結びつけたい。
COMMENTS 1