[VBA]AvastがVBAから呼び出したCopyMemory(WinAPI)を監視しまくる件

2020年11月14日

事の発端

先日、とあるExcel VBAの速度が、期待したより遅い事象が発生しました。どこで遅くなっているか調査した結果、原因はWindows APIのCopyMemory(RtlMoveMemory)でした。VBAの標準機能ではポインタによるデータ操作ができないVBAにとって、RtlMoveMemoryが遅いのは死活問題です(ポインタが必要なら別言語でやれというツッコミは無しで)。しかし、昔この関数を使っていて、そこまで遅かった記憶が無かったことや、そもそも単なるメモリコピーが何で遅くなるのか疑問に思ったため、根本原因を調査してみました。なお、根本原因はタイトルにある通り、Avast Antivirus(Var.20.4)でした。

調査結果

とあるExcel VBAで負荷テストを実施していた所、ある自作関数を1回実行するのに約10ミリ秒掛かっていました。この関数は連続で100~1000回くらい呼び出されることを想定しており、このままでは実行時間に1~10秒程度掛かってしまいます。対話的な操作を行うソフトウェアにおいて、応答時間が1秒というのは長く、ストレスを感じます。10秒は論外です。可能であれば、私なら別のソフトウェアを探します。

閑話休題。問題の自作関数の中では、いくつかのWindows APIが呼び出されていましたが、極端に遅かったのがCopyMemoryでした。速度を計測したサンプルコードを下記に示します。対照として、同じメモリ操作関数であるZeroMemoryの速度も計測しました。速度の計測にはtimeGetTimeを使用したい所ですが、今回はDeclareする関数を検証する関数だけにしたかったので、敢えてTimer関数を使用しています。

Option Explicit

Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)

Sub MeasureExecTimeOfCopyMemory()
    
    Dim a As Long
    Dim b As Long
    Dim i As Long
    Dim s As Double

    s = Timer

    For i = 1 To 12345
        Call CopyMemory(a, b, 4)
    Next

    Debug.Print Timer - s

End Sub
Option Explicit

Private Declare PtrSafe Sub ZeroMemory Lib "kernel32" Alias "RtlZeroMemory" (dest As Any, ByVal numBytes As LongPtr)

Sub MeasureExecTimeOfZeroMemory()
    
    Dim a As Long
    Dim i As Long
    Dim s As Double

    s = Timer

    For i = 1 To 12345
        Call ZeroMemory(a, 4)
    Next

    Debug.Print Timer - s

End Sub

なお、実行環境は下記の通りです。

項目仕様
OSWindows 10 64bit
Excel2016 32bit
PCThinkPad E595
CPUAMD Ryzen 5 3500U プロセッサー (2.10GHz, 4MB)
メモリDDR4 2400MHz 16GB

3回計測した結果、CopyMemoryは12345回の実行に24秒315、1回の実行に1970ナノ秒掛かっていました。ZeroMemoryは12345回の実行に0.138秒、1回の実行に11ナノ秒掛かっていました。実に176倍の差です。

なお、この事象は新規ワークブックでは発生せず、ワークブックを保存すると発生しました。しかし、アドイン化すると、この問題は発生しませんでした。また、別のパソコンでは、この事象は発生しませんでした。これらの事実から浮かび上がってきた容疑者が、Avastでした。そこで、CopyMemoryを実行している最中にタスクマネージャーでCPUの使用率を確認したところ、「Avast Service」というプロセスが反応していました。

そのため、早速Avastをアンインストールして、再びCopyMemoryの速度計測を行ったところ、12345回の実行に5.523秒、1回の実行が447ナノ秒となりました。少し改善されましたが、体感的にはまだ遅い感じがしたため、タスクマネージャーでCPUの使用率を確認したところ、今度はWindows Defenderが反応していました。

Windows Defenderを安全に無効にするには、別のウィルス対策ソフトをインストールする必要があるため、今度はAviraをインストールしました。その結果、CopyMemoryの速度は12345回の実行で0.065秒、1回の実行が5ナノ秒となりました。Timer関数の精度を考慮し、実行回数を増やした所、123456回で0.385秒、1回の実行が3ナノ秒となりました。Avastと比較して実に657倍の差が発生しました。

また、Avastインストール環境のZeroMemoryがAviraインストール環境のCopyMemoryよりも遅いので、AvastはCopyMemory以外のWindowsAPIも監視しているのだと思います。

ウィルス対策ソフトCopyMemoryの実行時間(1回)
Avast Antivirus1970ナノ秒
Windows Defender447ナノ秒
Avira Antivirus3ナノ秒

調査の過程で、いくつかのWindowsAPIの速度を計測しましたが、顕著の遅くなったのはCopyMemoryのみでした。そのため、CopyMemoryによる速度低下が問題になる場合は、別の関数を使用できないか検討すると良いでしょう。

ウィルス対策ソフトがCopyMemoryを監視する理由

どうやらCopyMemoryはVBAでマルウェアを作る際にも使用される関数のようです(https://www.virusbulletin.com/virusbulletin/2014/04/back-vba)。そのため、ウィルス対策ソフトはCopyMemoryでコピーした内容にマシンコードが含まれているかチェックしているのだと思います。そう考えると、Aviraの速さが逆に気になりますね。ウィルスの検出率は高いようなので、アルゴリズムが優れていると信じたいです。ちなみに、速度計測はしていませんが、McAfeeもAviraと同等か、それ以上の速度でした。

まとめ

  • AvastがインストールされているPCで、VBAからCopyMemory(RtlMoveMemory)を呼び出すと桁違いに遅い
  • 最も確実な対策は、Avastを別のウィルス対策ソフトに変更すること(Avira推奨)
  • 上記対策が困難な場合は、CopyMemory(RtlMoveMemory)を別の関数に置き換えること

VBA

Posted by 黒箱