VBAプロジェクトのパスワードをスキップする

VBAプロジェクトのパスワードをスキップする

~DialogBoxParamAを対象とした32bit/64bit両対応の実装例~

Copyright © 2025 LWP 山中 一弘 本資料は、出典を明記いただければ、商用・非商用を問わず、ご自由に複製・改変・再配布していただけます。なお、著作権表示は改変せず、そのまま記載してご利用くださいますようお願いいたします。

要約

VBAにおいて、プロジェクトのパスワードをスキップするためにWindows API関数 DialogBoxParamA を対象に、その呼び出しを乗っ取るフック処理の実装手法を示す。特に、VBAでは低レベルのメモリ操作が難しいとされる中で、MoveMemory、VirtualProtect、GetProcAddress などを組み合わせ、APIの動的書き換えを実現する。

本稿では、まず32bit版VBAにおける代表的なフック技術をコードレベルで精緻に解説し、次に64bit対応に際して必要となる構造の変更──命令長の拡張、ポインタ型の修正、ジャンプ命令の再構成など──を段階的に導入していく。

結果として、32bit・64bit両対応の完全なVBA実装を提示し、APIフックの構造理解と応用の基礎を体系的に提供する。

第1章 APIフックとは何か

1.1 フックの概念と目的

APIフック(Application Programming Interface Hooking)とは、あるプログラムが本来呼び出すべき関数(API)の処理を、任意の処理に差し替える技術である。WindowsにおけるAPIは、カーネルやユーザーインターフェースを操作するための基本的な入口となっており、これらを動的に書き換えることで、特定の処理を監視・改変・抑止することが可能となる。一般にはセキュリティソフトウェアや監視ツールなどがこの技術を利用しており、対象アプリケーションが内部で何をしているかを知る手段として用いられる。また、ある種の自動化やテストの場面でも、一時的にAPIの振る舞いを変更する目的でフックが用いられる。

フックの実装には大きく分けて、OS全体を対象としたグローバルフックと、プロセス内部だけで完結するローカルフックの二種類がある。前者は主にドライバレベルやインジェクション技術を要するが、後者は自プロセス内でコード書き換えを行うだけで済むため、仕組みは比較的単純であり、VBAでも原理的には実現可能である。ただし、その分対象は限定され、自己のプロセス空間に読み込まれた関数のみに効果が及ぶ。

1.2 VBAにおけるフックの困難性

VBAは本来、Officeアプリケーションの操作を簡便に行うための高水準スクリプト環境として設計されている。したがって、OSやハードウェアを直接制御するための手段はほとんど提供されておらず、CやC++で可能な低レベルな操作は原則として行えない。にもかかわらず、Windows APIのDeclareステートメントを利用すれば、一定範囲でDLL関数を直接呼び出すことは可能である。これにより、VirtualProtectやMoveMemoryといったメモリ制御系APIを駆使することで、VBAであっても限定的なメモリ書き換えや関数パッチングを実現できる。

最大の障壁は、VBAが本来ポインタ演算を提供しておらず、関数ポインタの操作や構造体のバイナリ表現などに極めて弱い点にある。また、64bit Officeでは従来のLong型が機能せず、LongPtrを使った明示的なポインタ制御が必要となるため、コードの移植性にも注意が必要である。さらに、VBAはマルチスレッド環境を想定しておらず、フック処理に伴うタイミングの競合や再入の管理が難しいという制約もある。

1.3 本稿の対象とするAPI:DialogBoxParamA

本稿でフック対象とするのは、Windowsのユーザーインターフェース関連APIであるDialogBoxParamAである。この関数は、リソースで定義されたダイアログボックスを表示し、指定されたダイアログプロシージャに制御を渡すものである。特に、アプリケーション内部でユーザー入力を求める場面などで多用される。

DialogBoxParamAは、引数としてインスタンスハンドル、テンプレート名、親ウィンドウ、ダイアログ関数、初期化パラメータを受け取り、整数値を返す。VBAが直接この関数を呼び出すことは稀であるが、Office製品や外部DLLがこの関数を間接的に使用する場合、その呼び出しを捕捉することで、特定のダイアログの表示を抑止したり、ダイアログ処理をカスタマイズすることができる。

本稿では、このDialogBoxParamAの先頭命令列をフックすることで、特定条件下では本来の動作をキャンセルし、任意の処理に置き換える手法を、32bit/64bitの両対応として実装する。これにより、APIフックの実用的かつ具体的な理解を得ることを目指す。

第2章 32bit環境におけるAPIフックの実装

2.1 Windows APIの宣言

VBAからWindows APIを呼び出すには、Declareステートメントを用いて外部DLL内の関数を明示的に宣言する必要がある。32bit環境では、ポインタも含めたすべてのアドレスやハンドルをLong型(4バイト)で扱うことができるため、宣言は比較的単純である。たとえば、メモリコピーの代表的な関数RtlMoveMemoryは以下のように宣言される。

Private Declare Sub MoveMemory Lib "kernel32" Alias "RtlMoveMemory" _

(Destination As Long, Source As Long, ByVal Length As Long)

この形式は32bit Office/VBA環境では正しく動作するが、64bit環境では動作不良を起こす。現在では、Microsoftが提供する《Win32API_PtrSafe.txt》ファイルにおいて、64bit対応済みの共通API宣言が整備されている。その中では、同じ関数は以下のように宣言される。

Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _

Destination As Any, Source As Any, ByVal Length As LongPtr)

この宣言はPtrSafeを用いることで64bit環境に対応し、ポインタ引数に対してはAs Anyを、サイズ引数に対してはLongPtrを用いることで、32bit・64bitの両環境で安全に利用できるようになっている。本稿では学習上の明瞭性を重視し、以下の章節においては旧来のMoveMemoryによる32bit専用の記述を基準とするが、実運用ではWin32API_PtrSafe形式への置換が推奨される。

2.2 MoveMemoryと関数ポインタの取得

VBAには本来、C言語におけるポインタ操作のような機能は備わっていないが、MoveMemoryを用いることで、任意のアドレス空間の内容を読み書きすることができる。これにより、関数アドレスの取得や、メモリ内容の直接書き換えが可能となる。

API関数のアドレスは、GetModuleHandleAでDLLのベースアドレスを取得し、GetProcAddressで関数のオフセットを得ることで取得できる。たとえば、以下のようにしてDialogBoxParamAのアドレスを取得する。

Dim pFunc As Long
pFunc = GetProcAddress(GetModuleHandleA("user32.dll"), "DialogBoxParamA")

このようにして得られた関数のアドレスを用い、MoveMemoryを使ってその関数の先頭バイト列を読み取り、書き換えることでAPIフックの基盤を構築することができる。

2.3 フック対象アドレスの取得と書き換え

取得した関数アドレスは、通常は読み取り専用または実行専用の保護属性が設定されており、書き換えることはできない。そのため、VirtualProtectを用いて保護属性をPAGE_EXECUTE_READWRITEに変更し、書き込みを可能にする必要がある。

書き換え対象は関数の先頭6バイトである。これは、push アドレス(5バイト)とret(1バイト)で構成されるジャンプ命令列であり、関数の実体を自作の処理にリダイレクトすることができる。書き換え前には、必ず元の6バイトを退避しておく必要がある。これにより、フック解除や復元を安全に行えるようになる。

書き換え操作自体もMoveMemoryで実現する。書き換え用の命令列をバイト配列で定義し、それを対象アドレスにコピーするだけで、関数のエントリポイントが上書きされ、以後の呼び出しは自前の関数へと転送される。

2.4 フックバイト列の構成と意味(push+ret形式)

32bit環境でフック処理を実現するために使用される命令列の典型は、push <address> と ret の組み合わせである。これはスタックにジャンプ先のアドレスを積み、そのままリターンすることでジャンプと同等の効果を得る構造である。具体的には以下のような構成となる。

0x68:push命令のオペコード(直後に32bitの即値)

[アドレスの4バイト]:ジャンプ先関数のアドレス

0xC3:ret命令

この6バイトは、VBAの配列操作で容易に構築できる。たとえば、以下のように設定する。

HookBytes(0) = &H68

MoveMemory ByVal VarPtr(HookBytes(1)), VarPtr(p), 4

HookBytes(5) = &HC3

この命令列を、対象関数の先頭に上書きすることで、任意の処理へと関数の流れを強制的に移すことができる。

2.5 フック関数の設計と再帰的復元

ジャンプ先となる関数(本稿ではMyDialogBoxParam)は、オリジナルのAPIと完全に同じ引数構造と戻り値型を持たなければならない。これはAddressOfによって得られるアドレスを使用するためであり、型が一致しないと実行時エラーやクラッシュの原因となる。

この関数内では、特定条件のときに処理を上書きして返すか、それ以外はオリジナルの処理に復帰させるという判定を行う。たとえば、テンプレート名が特定のID(たとえば4070)であれば処理をスキップし、それ以外は本来のダイアログを表示させる。その際、一時的にフックを解除し、処理後に再度フックをかけ直す。

このように再帰的にフックの解除と再設定を行う構造により、ダイアログの抑制と通常表示を柔軟に制御できる。また、復元処理では、事前に保存しておいたオリジナルの6バイトを元の関数アドレスに書き戻すことで、正確な復元を保証する。これにより、フックによる影響を局所化し、安定した挙動を保ちながらフック処理を実装することが可能となる。

第3章 64bit対応のための設計変更

3.1 32bitと64bitの違い:ポインタ・命令長・保護属性

32bit環境と64bit環境では、ポインタのサイズ、命令形式、メモリ保護属性の扱いに本質的な違いがある。まず、ポインタのサイズは32bitでは4バイト、64bitでは8バイトであり、構造体や関数アドレス、戻りアドレスなどの扱いがすべて変化する。これにより、関数アドレスを直接バイナリで書き込む際には、4バイトから8バイトへの拡張に対応した命令構成を用いなければならない。

また、32bitでは比較的簡潔なジャンプ命令が利用可能だったが、64bit環境では絶対アドレスへのジャンプが制限されており、mov命令でレジスタにアドレスを格納してからjmpする形式が必要となる。さらに、Windowsの64bitアーキテクチャでは、実行中コードのメモリ領域に対して書き込み制限が強化されており、保護属性の変更も厳格になっている。これに対処するために、VirtualProtectを用いてPAGE_EXECUTE_READWRITEへの変更処理を必ず伴う必要がある。

64bit対応とは単なる型変更にとどまらず、アーキテクチャに即した命令レベルの再設計と、安全なメモリ操作の保証を伴う総合的な再構成である。

3.2 64bit API宣言と PtrSafe の使い方

64bit VBA環境に対応するためには、すべてのAPI宣言にPtrSafeキーワードを付与する必要がある。これは、VBA7以降に導入された仕様であり、これがないと64bitモジュールではコンパイルエラーとなる。また、ポインタを表現する型としては、32bit/64bitでサイズが自動調整されるLongPtr型を用いる。これにより、共通のコードを一つの宣言で扱うことができる。

たとえば、以下は64bitに対応した典型的なAPI宣言である。

Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _

Destination As Any, Source As Any, ByVal Length As LongPtr)

また、関数ポインタの取得やコールバック関数の設定においても、AddressOfで得られたアドレスを受け渡す変数はLongPtrで受ける必要がある。64bit対応を行う際は、既存コードのすべてのLongを無条件にLongPtrに置き換えるのではなく、ポインタに該当する変数のみを適切に型変換することが重要である。

Microsoftから提供されている《Win32API_PtrSafe.txt》には、代表的なAPIのPtrSafe対応宣言が網羅されており、これを参考に実装することで安全性と保守性を高めることができる。

3.3 mov rax → jmp rax 形式のジャンプ命令構成

64bit環境では、従来のpush address → retという6バイト構成のジャンプ命令は使用できない。これは、32bit即値を使ったpush命令がアドレス全体を表現できないためである。代わりに使用されるのが、次の13バイト構成による間接ジャンプ命令である。

48 B8 <8バイトアドレス> ; mov rax, <ジャンプ先アドレス>

FF E0 ; jmp rax

この命令列は、64bitレジスタであるraxに対象アドレスをロードし、そのアドレスにジャンプするという手順を実現する。これはVBAでは以下のように構築される。

HookBytes(0) = &H48

HookBytes(1) = &HB8

MoveMemory VarPtr(HookBytes(2)), VarPtr(p), 8

HookBytes(10) = &HFF

HookBytes(11) = &HE0

HookBytes(12) = &HC3 ' 保険としてretを追加

このように、64bitではジャンプ命令が長くなり、また命令構成も複雑であるため、書き換え対象バイト数も6バイトから13バイトへと拡張される。これに伴い、復元用の元バイト列も13バイトを退避しておく必要がある。

3.4 実装上の留意点と確認手順

64bit対応コードを実装する際には、以下の点に注意する必要がある。

  • ポインタ型の適切な型指定:すべてのアドレス、ハンドル、関数ポインタにはLongPtrを使用する。

  • 命令列の正確な構築:64bitジャンプは13バイト固定であり、途中に誤った命令や不要なバイトが混入しないよう、配列への書き込み順を厳密に管理する。

  • 保護属性の変更と復元:VirtualProtectによる書き込み許可と、処理後の保護属性の復旧を確実に行う。

  • 関数の完全一致性:ジャンプ先の関数は、呼び出し元の関数と同じ引数構造でなければならない。

  • 互換性の確認:64bit Officeで実行する場合、参照設定やVBE環境が32bitと異なるため、コンパイルエラーや挙動不審に注意する。

確認の際は、アドレスの読み取りとジャンプ先の挙動をステップ実行またはログ出力により検証し、正しくフックが機能していることを逐一確認することが望ましい。また、64bit環境でのデバッグは難易度が高いため、最初は仮想環境などで安全に動作確認を行うのが推奨される。

第4章 64bit対応コードの全文と注釈

4.1 コード全体の掲載

以下に示すのは、64bit版VBAに対応したAPIフックの完全な実装コードである。本コードは、Windows API DialogBoxParamA を対象に、関数先頭を動的に書き換えてフックを行い、条件に応じて独自処理または元の関数を実行する構造を採っている。ジャンプ命令は64bit特有の mov rax → jmp rax による13バイト構成を用いており、すべてのアドレス・ポインタ操作には LongPtr を使用することで、32bit/64bitの差異を吸収している。

Option Explicit
#If VBA7 Then
    Private Declare PtrSafe Sub MoveMemory Lib "kernel32" Alias "RtlMoveMemory" ( _

ByVal Destination As LongPtr, _

ByVal Source As LongPtr, _

ByVal Length As LongPtr)

    Private Declare PtrSafe Function VirtualProtect Lib "kernel32" ( _

ByVal lpAddress As LongPtr, _

ByVal dwSize As LongPtr, _

ByVal flNewProtect As Long, _

ByRef lpflOldProtect As Long) As Long

    Private Declare PtrSafe Function GetModuleHandleA Lib "kernel32" ( _

ByVal lpModuleName As String) As LongPtr

    Private Declare PtrSafe Function GetProcAddress Lib "kernel32" ( _

ByVal hModule As LongPtr, _

ByVal lpProcName As String) As LongPtr

    Private Declare PtrSafe Function DialogBoxParam Lib "user32" Alias "DialogBoxParamA" ( _

ByVal hInstance As LongPtr, _

ByVal pTemplateName As LongPtr, _

ByVal hWndParent As LongPtr, _

ByVal lpDialogFunc As LongPtr, _

ByVal dwInitParam As LongPtr) As Long

#End If

Private Const PAGE_EXECUTE_READWRITE = &H40
Dim HookBytes(0 To 12) As Byte
Dim OriginBytes(0 To 12) As Byte
Dim pFunc As LongPtr
Dim Flag As Boolean
Public Sub RecoverBytes()
    If Flag Then

MoveMemory pFunc, VarPtr(OriginBytes(0)), 13

    End If
End Sub
Public Function Hook() As Boolean
    Dim TmpBytes(0 To 12) As Byte
    Dim p As LongPtr
    Dim OriginProtect As Long

Hook = False

    pFunc = GetProcAddress(GetModuleHandleA("user32.dll"), "DialogBoxParamA")

If pFunc = 0 Then Exit Function

    If VirtualProtect(pFunc, 13, PAGE_EXECUTE_READWRITE, OriginProtect) <> 0 Then

MoveMemory VarPtr(TmpBytes(0)), pFunc, 13

        If TmpBytes(0) <> &H48 Then

MoveMemory VarPtr(OriginBytes(0)), pFunc, 13

p = AddressOf MyDialogBoxParam

HookBytes(0) = &H48

HookBytes(1) = &HB8

MoveMemory VarPtr(HookBytes(2)), VarPtr(p), 8

HookBytes(10) = &HFF

HookBytes(11) = &HE0

HookBytes(12) = &HC3

MoveMemory pFunc, VarPtr(HookBytes(0)), 13

Flag = True

Hook = True

        End If
    End If
End Function
Private Function MyDialogBoxParam( _

ByVal hInstance As LongPtr, _

ByVal pTemplateName As LongPtr, _

ByVal hWndParent As LongPtr, _

ByVal lpDialogFunc As LongPtr, _

ByVal dwInitParam As LongPtr) As Long

    If pTemplateName = 4070 Then

MyDialogBoxParam = 1

    Else

RecoverBytes

        MyDialogBoxParam = DialogBoxParam(hInstance, pTemplateName, hWndParent, lpDialogFunc, dwInitParam)

Hook

    End If
End Function

4.2 各セクションの説明(宣言・フック構成・回復処理)

本コードは大きく3つの構造で構成されている。

1. API宣言部

PtrSafe付きのDeclare文により、64bit環境でも安全にWindows APIを利用できるようにしている。ポインタやアドレスを扱うすべての引数・戻り値はLongPtrで定義されており、VBA7以降の仕様に完全に準拠している。

2. フックの構成(Hook関数)

GetProcAddressで得た関数アドレスをpFuncに格納し、その先頭13バイトを一時保存のうえ、ジャンプ命令に書き換える。ジャンプ命令は mov rax, address → jmp rax の形式で構成されており、フック対象の関数呼び出しをMyDialogBoxParamへと転送する。

3. 回復処理とフック関数(RecoverBytes / MyDialogBoxParam)

フックによって書き換えられた先頭13バイトを元の命令に戻すためにRecoverBytesを用意している。MyDialogBoxParam関数では、特定の条件(ここではpTemplateName = 4070)に該当する場合に処理をスキップし、そうでない場合は一時的にフックを解除して元のDialogBoxParamAを呼び出す。呼び出し後には再度フックを設定し、以後の呼び出しに備える。

4.3 64bit環境でのテスト観点と落とし穴

64bit環境での実装は、32bitと比べて以下のような特有の難しさを伴う。

  • ジャンプ命令の長さ:64bitでは13バイトの命令が必要となるため、対象関数の先頭がそれだけのバイト長で分離可能であることが前提となる。これが満たされない関数ではフックが困難または不可能となる。

  • 保護属性変更の成否:Windowsの保護機構により、VirtualProtectでコードセクションの保護を緩和できない場合がある。セキュリティソフトの干渉やDEPの設定によって動作が制限される可能性がある。

  • 関数の互換性保持:ジャンプ先となる関数の引数・戻り値が元のAPIと完全に一致していない場合、クラッシュや予期せぬ動作を招く。

  • アドレス誤差の検出が難しい:64bitでは即値の範囲が広く、ジャンプ先の不整合が致命的エラーとして表面化しにくい。そのため、デバッグには詳細なステップ実行やログ出力が必要となる。

これらの点を踏まえ、64bit環境でのAPIフックは高い技術的要件を伴うが、十分に制御された環境であれば動的な関数フックの強力な手段となる。検証は仮想環境や専用のテストモジュール内で行うことが望ましく、業務利用や一般配布においては厳重な検証と制限のもとで実施されるべきである。

4.4 オリジナルコード

Option Explicit
Private Const PAGE_EXECUTE_READWRITE = &H40
Private Declare PtrSafe Sub MoveMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As LongPtr, Source As LongPtr, ByVal Length As LongPtr)
Private Declare PtrSafe Function VirtualProtect Lib "kernel32" (lpAddress As LongPtr, ByVal dwSize As LongPtr, ByVal flNewProtect As LongPtr, lpflOldProtect As LongPtr) As LongPtr
Private Declare PtrSafe Function GetModuleHandleA Lib "kernel32" (ByVal lpModuleName As String) As LongPtr
Private Declare PtrSafe Function GetProcAddress Lib "kernel32" (ByVal hModule As LongPtr, ByVal lpProcName As String) As LongPtr

Private Declare PtrSafe Function DialogBoxParam Lib "user32" Alias "DialogBoxParamA" (ByVal hInstance As LongPtr, ByVal pTemplateName As LongPtr, ByVal hWndParent As LongPtr, ByVal lpDialogFunc As LongPtr, ByVal dwInitParam As LongPtr) As Integer

Dim HookBytes(0 To 5) As Byte
Dim OriginBytes(0 To 5) As Byte
Dim pFunc As LongPtr
Dim Flag As Boolean
Private Function GetPtr(ByVal Value As LongPtr) As LongPtr

GetPtr = Value

End Function
Private Sub RecoverBytes()

If Flag Then MoveMemory ByVal pFunc, ByVal VarPtr(OriginBytes(0)), 6

End Sub
Public Function Hook() As Boolean
  Dim TmpBytes(0 To 5) As Byte
  Dim p As LongPtr
  Dim OriginProtect As LongPtr

Hook = False

  pFunc = GetProcAddress(GetModuleHandleA("user32.dll"), "DialogBoxParamA")
  If VirtualProtect(ByVal pFunc, 6, PAGE_EXECUTE_READWRITE, OriginProtect) <> 0 Then

MoveMemory ByVal VarPtr(TmpBytes(0)), ByVal pFunc, 6

    If TmpBytes(0) <> &H68 Then

MoveMemory ByVal VarPtr(OriginBytes(0)), ByVal pFunc, 6

      p = GetPtr(AddressOf MyDialogBoxParam)

HookBytes(0) = &H68

MoveMemory ByVal VarPtr(HookBytes(1)), ByVal VarPtr(p), 4

HookBytes(5) = &HC3

MoveMemory ByVal pFunc, ByVal VarPtr(HookBytes(0)), 6

Flag = True

Hook = True

    End If
  End If
End Function
Private Function MyDialogBoxParam(ByVal hInstance As LongPtr, ByVal pTemplateName As LongPtr, ByVal hWndParent As LongPtr, ByVal lpDialogFunc As LongPtr, ByVal dwInitParam As LongPtr) As Integer
  If pTemplateName = 4070 Then

MyDialogBoxParam = 1

  Else

RecoverBytes

    MyDialogBoxParam = DialogBoxParam(hInstance, pTemplateName, hWndParent, lpDialogFunc, dwInitParam)

Hook

  End If
End Function
Public Sub unprotected()
  If Hook Then

MsgBox "VBA Project is unprotected!", vbInformation, "*****"

  End If
End Sub