はじめに
この記事では、VBAが、どのようにExcelやWordに格納されているかを考えてみます。
なお、もし、VBAのソースコードをプログラムから取得したりしたいだけならば、この記事は訳に立ちません。そういうことをしたい場合はここでやっている感じでVBProjectプロパティを操作するとできると思います。
xlsm中のVBAはどう格納されているか
まず、拡張子がxlsmというファイルの場合、Excelファイルを7-Zipなどの解凍ソフトで開いてみてください。
そのファイル中の「xl」フォルダに「vbaProject.bin」というバイナリファイルが存在し、それがVBAの中身になります。
このvbaProject.binというバイナリファイルはCompound File Binary Formatというファイル形式で格納されています。
ルートのストレージオブジェクトの中に子ストレージオブジェクトとストリームオブジェクトのツリー構造で構成されており、その仕様は以下に公開されています。
[MS-CFB]: Compound File Binary File Format
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b
7ZIPで確認できるのはストレージまでなのでストリームの内容に関しては後述する方法か、自分でバイナリエディタを開いて解析する必要があります。
VBA固有のストレージとストリームの仕様は以下に定義されています。
[MS-OVBA]: Office VBA File Format Structure
https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ovba/575462ba-bf67-4190-9fac-c275523c75fc
古い形式のXLSの場合
2003以前のExcelでVBAを確認する場合は、xlsファイルがCFBになっているので、7ZIPで直接開くと、_VBA_PROJECT_DIRと言うストレージオブジェクトが確認できます。この中にVBAの情報が格納されています。
バイナリからVBAのソースコードを取得する
[MS-OVBA]: Office VBA File Format Structureによると、ソースコードはModule Stream中のCompressedSourceCodeとして圧縮されたバイト配列で格納されています。
なお、このModule Streamからソースコードを取得するようなPythonのツールが存在しています。
oletools
https://github.com/decalage2/oletools/tree/master/oletools
oletools中のolevbaコマンドを使用することで、xlsmファイル中のモジュールストリームからVBAのコードが抽出可能です。
C:\dev\python3>olevba -c C:\share\vba\test\test.xlsm
olevba 0.54.2 on Python 3.7.4 - http://decalage.info/python/oletools
===============================================================================
FILE: C:\share\vba\test\test.xlsm
Type: OpenXML
-------------------------------------------------------------------------------
VBA MACRO ThisWorkbook.cls
in file: xl/vbaProject.bin - OLE stream: 'VBA/ThisWorkbook'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Option Explicit
-------------------------------------------------------------------------------
VBA MACRO Sheet1.cls
in file: xl/vbaProject.bin - OLE stream: 'VBA/Sheet1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Option Explicit
-------------------------------------------------------------------------------
VBA MACRO Module1.bas
in file: xl/vbaProject.bin - OLE stream: 'VBA/Module1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Option Explicit
Public Sub test()
Dim x As String
Dim y As String
Dim z As String
x = "TEST"
y = "abe"
z = x & " " & z & "neko"
MsgBox z
End Sub
-------------------------------------------------------------------------------
VBA MACRO Class1.cls
in file: xl/vbaProject.bin - OLE stream: 'VBA/Class1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Option Explicit
Public a As Long
Public Sub test()
MsgBox "test"
End Sub
Public Function test2(a, b)
test2 = a + b
End Function
-------------------------------------------------------------------------------
VBA MACRO UserForm1.frm
in file: xl/vbaProject.bin - OLE stream: 'VBA/UserForm1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Option Explicit
Private Sub CommandButton1_Click()
MsgBox "test"
End Sub
-------------------------------------------------------------------------------
VBA FORM STRING IN 'xl/vbaProject.bin' - OLE stream: 'UserForm1/o'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
�Label1
-------------------------------------------------------------------------------
VBA FORM STRING IN 'xl/vbaProject.bin' - OLE stream: 'UserForm1/o'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MS UI Gothic
-------------------------------------------------------------------------------
VBA FORM STRING IN 'xl/vbaProject.bin' - OLE stream: 'UserForm1/o'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
�CommandButton1�
-------------------------------------------------------------------------------
VBA FORM STRING IN 'xl/vbaProject.bin' - OLE stream: 'UserForm1/o'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MS UI Gothic
-------------------------------------------------------------------------------
VBA FORM Variable "b'Label1'" IN 'xl/vbaProject.bin' - OLE stream: 'UserForm1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
None
-------------------------------------------------------------------------------
VBA FORM Variable "b'CommandButton1'" IN 'xl/vbaProject.bin' - OLE stream: 'UserForm1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
None
中間コードを確認する
VBAにはソースコードの他に中間コードが存在しています。
ここ以降の内容は現時点でMicrosoftのホームページから確認できる内容でなく、非公式の話になります。
※2020.05.21追記
下記のVBAの中間コードやキャッシュを圧縮したり削除するツールのドキュメント中にMicrosoftのページを参考にしています。ただ、Link切れを起こしているのでアーカイブなどで当時の公式情報を探すことはできるかもしれませんが、今の情報がどうなっているかは不明です。
http://cpap.com.br/orlando/VBADecompilerMore.asp
Visual Basic Editorでソースコードを編集すると、その時点でP-Codeという中間コードが作成されて、Module Stream中のPerformanceCacheに記録されます。
P-Codeが一度でも実行されると、さらにトークン化された形式でSRP Streamsに格納されます。
P-Codeを取得する
下記のツールを使用することでVBA中のp-codeを取得することが可能です。
pcodedmp.py - A VBA p-code disassembler
https://github.com/bontchev/pcodedmp
たとえば以下のようなVBAのコードがあるとします。
これをpcodemp.pyでダンプを行うと以下のようになります。
C:\dev\python3\pcodedmp\pcodedmp>pcodedmp -d C:\share\vba\test\testdata\test_006.xlsm
Processing file: C:\share\vba\test\testdata\test_006.xlsm
===============================================================================
Module streams:
VBA/ThisWorkbook - 1067 bytes
Line #0:
Option (Explicit)
Line #1:
VBA/Sheet1 - 1059 bytes
Line #0:
Option (Explicit)
Line #1:
VBA/Module1 - 1588 bytes
Line #0:
Option (Explicit)
Line #1:
Line #2:
FuncDefn (Public Sub test())
Line #3:
Dim
VarDefn z (As String)
Line #4:
LitStr 0x000A "abcdあいう"
St z
Line #5:
Ld z
ArgsCall MsgBox 0x0001
Line #6:
EndSub
もし-bオプションを使用した場合は、詳細なアドレスも確認できます。
略
0583: Line #4:
00000000 B9 00 0A 00 61 62 63 64 82 A0 82 A2 82 A4 27 00 ....abcd......'.
00000010 34 02 4.
00B9 LitStr 0x000A "abcdあいう"
0027 St z
略
P-Codeからソースコードを取得する
P-Codeからソースコードに変換するPythonのツールも存在します。
pcode2code.py - A VBA p-code decompiler
https://github.com/Big5-sec/pcode2code
下記のpcodedmpの出力結果からソースコードを取得してみます。
Processing file: C:\share\vba\test\testdata\test_006.xlsm
===============================================================================
Module streams:
VBA/ThisWorkbook - 1067 bytes
Line #0:
Option (Explicit)
Line #1:
VBA/Sheet1 - 1059 bytes
Line #0:
Option (Explicit)
Line #1:
VBA/Module1 - 1588 bytes
Line #0:
Option (Explicit)
Line #1:
Line #2:
FuncDefn (Public Sub test())
Line #3:
Dim
VarDefn z (As String)
Line #4:
LitStr 0x000A "abcdあいう"
St z
Line #5:
Ld z
ArgsCall MsgBox 0x0001
Line #6:
EndSub
以下のようにpcode2codeコマンドの-pオプションにp-codeが記述されたテキストを与えることで、そのVisualBasicのソースコードが確認できます。
C:\dev\python3\pcodedmp\pcodedmp>pcode2code -p code.txt
stream : VBA/ThisWorkbook - 1067 bytes
########################################
Option Explicit
stream : VBA/Sheet1 - 1059 bytes
########################################
Option Explicit
stream : VBA/Module1 - 1588 bytes
########################################
Option Explicit
Public Sub test()
Dim z As String
z = "abcdあいう"
MsgBox z
End Sub
まとめ
OfficeのVBAはCompound File Binary File Formatの形式でファイル内に格納されています。
この内容は、Office VBA File Format Structureに記載されています。ただし、ここには、中間コードについての詳細は記載されていません。
ファイルから圧縮されたVBAのソースコードを抜き出すにはolevbaを使用すること抽出できます。
また、中間コードのp-codeを抜き出すにはpcodedmp.py - A VBA p-code disassemblerが使用できます。
p-codeが一度実行されたあとに作成されるキャッシュの内容を解析できる方法は現時点で確認できませんでした。
VBAはp-codeまたはキャッシュされた内容で動作するので、ソースコードで記載した通りに動作しない場合は、ここで詳細した方法でp-codeの内容を確認することで解決する可能性があるかもしれません。(未検証)
参考
Compound File Binary Format
https://en.wikipedia.org/wiki/Compound_File_Binary_Format
[MS-CFB]: Compound File Binary File Format
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b
[MS-OVBA]: Office VBA File Format Structure
https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ovba/575462ba-bf67-4190-9fac-c275523c75fc
olevba
https://github.com/decalage2/oletools/wiki/olevba
pcodedmp.py - A VBA p-code disassembler
https://github.com/bontchev/pcodedmp
pcode2code.py - A VBA p-code decompiler
https://github.com/Big5-sec/pcode2code