
VBScript & VBA ソース間難読化解除ツール(部分評価付き)
vbSparkle は、部分評価に基づくソースコード間のマルチプラットフォーム Visual Basic 難読化解除ツールであり、主に VBScript と VBA(Office マクロ)で書かれた悪意のあるコードの解析を目的としています。
ネイティブ C# で書かれており、.Net Standard ライブラリを提供します。Windows、Linux、MacOS などで動作します。
Visual Basic Script と VBA のパースは、ANTLR の文法&レクサーパーサーを使用して処理されます。
Install-Package vbSparkle
dotnet add package vbSparkle
vbSparkle ライブラリをプロジェクトに追加したら、静的クラス VbPartialEvaluator の関数 PrettifyEncoded を呼び出すだけです:
string obfuscatedVB = "msgbox(chr(65)&chr(66)&(chr(67))"
string deobfuscated = VbPartialEvaluator.PrettifyEncoded(obfuscatedVB);
... は MsgBox("ABC") を返します。
同梱のプロジェクト vbSparkle.CLI は、vbSparkle を CLI として使用する例です。
現在の例では、引数としてパスを取るか、StdIn に完全なバイナリを受け取り、難読化解除された結果を返します。

同梱のプロジェクト vbSparkle.Web は、Web UI 内で vbSparkle を使用する例です。

VBScript と VBA のコード難読化は攻撃者の間で人気があり、検知対策、アンチウイルス、ファイアウォール、EDR を回避し、マルウェア解析をより困難にすることができます。
そのため、マシンが侵害される最初の段階で難読化された VBScript が使われるのはよくあることです。
典型的な VBScript のコード難読化手法は、スクリプトを難読化された文字列にカプセル化し、デコード/評価された後に Execute("...code..." function) を介して実行するものです。
例を見てみましょう:
Execute(chr( 456127/3833 ) & chr( 817650/7110 ) & chr( 759429/7671 ) & chr( 834138/7317 ) & chr( 57960/552 ) & chr( 2549-2437 ) & chr( 1078568/9298 ) & chr( 5143-5097 ) & chr( -8682+8783 ) & chr( 529-430 ) & chr( 673088/6472 ) & chr( 761682/6862 ) & chr( 302336/9448 ) & chr( 9427-9393 ) & chr( 410176/3944 ) & chr( -5271+5372 ) & chr( 796608/7376 ) & chr( 2896-2788 ) & chr( 318792/2872 ) & chr( 1076-1042 ) & vbcrlf )
ここで、攻撃者は以下を使用しています:
chr(CHAR_CODE) 関数(例:chr(65) = "A")456127/3833)&Execute(..) 関数の呼び出しスクリプトの実行に沿って見てみましょう:
456127/3833 => 119chr(119) => "w"817650/7110 => 115chr(115) => "s""w" & "s" => "ws"Execute("ws.....元のスクリプトは次のとおりです:
wscript.echo "hello"
手動での難読化解除は、大きなスクリプトでは非常に困難で時間がかかります。
VBScript での一般的な難読化解除手法は、単純に Execute() 関数を WScript.Echo( に置き換え、スクリプトを実行して出力を得るというものです。
この後者の手法は単純ですが、いくつかの欠点があります:
そして、このプロジェクトが開始された時点では、これらの 2 つの手法に代わるものはありませんでした。
部分評価は新しい概念ではなく、主にコンパイラが事前コンパイルプロセスでコード最適化に使用します(例:https://en.wikipedia.org/wiki/Partial_evaluation)。
部分評価器は、プログラム(対象プログラムと呼ばれます)とそのプログラムへの一部の入力を読み取り、提供された入力に依存するプログラムの部分だけを評価します。 これらの部分がすべて評価されると、残りのプログラム(残留プログラムと呼ばれます)が出力として生成されます。
例えば、事前コンパイルプロセスは次の対象コードを変換します:
var1 = 3*3
var2 = sin(arg1)
var3 = var1 + func_xx(2 * var1 + var2)
次の残留コードに変換します:
var2 = sin(arg1)
var3 = 9 + func_xx(18 + var2)
このコードは、パフォーマンスを最適化するために、コンパイル前に部分評価されています。 このプロセスによりコードが簡素化され、不要な操作が取り除かれることがわかります。 これは興味深い点です。難読化は、コードの実行に不要なコードや操作を追加することでコードを隠すからです。
最初のサンプルをステップバイステップで調べてみましょう:
456127/3833 のような算術演算は部分評価できます:副作用なしchr(119) は部分評価できます:119 は定数であり、これはネイティブ関数のため副作用なし"w" & "s" は部分評価できます:定数の文字列連結のため副作用なしExecute( は既知の副作用(スクリプト実行)があるため、関数の副作用を管理/エミュレートできる場合を除き、評価されませんこのサンプルを部分評価すると、次の結果になります:
Execute("wscript.echo ""hello""" & vbCrLf)
そして、どうでしょう? サンプルの難読化を解除できました。
実際には、はるかに複雑です:
幸いなことに、ANTLR は、構造化テキストやバイナリファイルを読み取り、処理、実行、または変換するための強力なパーサージェネレーターです。言語、ツール、フレームワークの構築に広く使用されていることで知られています。ANTLR は文法から、構文木を構築できるパーサーを生成し、関心のあるフレーズの認識に応答しやすくするリスナーインターフェース(またはビジター)も生成します。
ANTLR は、レクサー、パーサー、ツリーパーサー、および複合レクサー/パーサーを生成できます。パーサーは、構文木または抽象構文木を自動的に生成でき、それらをツリーパーサーでさらに処理できます。ANTLR は、レクサー、パーサー、ツリーパーサーを指定するための一貫した単一の表記法を提供します。
デフォルトでは、ANTLR は文法を読み取り、その文法で定義された言語の認識器を生成します(つまり、入力ストリームを読み取り、入力ストリームが文法で指定された構文に準拠していない場合にエラーを生成するプログラム)。
その言語で何か有用なことを行うために、文法内の文法要素にアクションを添付できます。これらのアクションは、認識器が生成されるプログラミング言語で記述されます。認識器の生成時、アクションは認識器のソースコードの適切な位置に埋め込まれます。
ANTLR と、Ulrich Wolffgang によって書かれた既存の文法のおかげで、AST、レクサー、パーサーの最初のバージョンを迅速かつ簡単に生成できました。
VB 文法からのレールロードダイアグラムのサンプル:

この文法は、一部の特定の Visual Basic 構文(インラインコード、いくつかの演算子の問題)では完全には機能しなかったため、完全な VBS & VBA 互換の文法を提供するにはいくつかの変更が必要でした。変更された文法はこちらにあります:Visual Basic Grammar
以下は、次のコードのビジュアルな構文木の例です:
Msgbox("Hello world " & Chr(33), vbCritical, "The Title")

パーサーと AST が生成されたら、それを使い始めてコードを探索できます:
using Antlr4.Runtime;
using System.Linq;
namespace vbSparkle
{
public class PartialEvaluator
{
public static string Prettify(string script)
{
var inputStream = new AntlrInputStream(script);
// Create the lexer
var lexer = new VBScriptLexer(inputStream);
// Instanciate of common token from lexer
var commonTokenStream = new CommonTokenStream(lexer);
// Create the parser
var parser = new VBScriptParser(commonTokenStream);
// Get the start rule (root element of our AST)
VBScriptParser.StartRuleContext stContext = parser.startRule();
// Instanciate the visitor
VbAnalyser analyser = new VbAnalyser();
// Visit the AST
analyser.Visit(stContext);
// Root AST element of a vbScript being a "Module", we get it
var module = analyser.Modules.FirstOrDefault();
// Then we partially evaluate our module
string result = module.Prettify(partialEvaluation: true);
return result;
}
}
}
ビジターは AST を横断し、各ノードの部分評価器/プリティファイアを作成します。
これらは再帰的に評価する役割を担います:

例えば、以下は VbModule 評価器をインスタンス化するビジターです:
public class VbAnalyser
{
public List<VbModule> Modules { get; set; } = new List<VbModule>();
internal void Visit(VBScriptParser.StartRuleContext stContext)
{
var moduleContext = stContext.module();
VbModule module = new VbModule(this, moduleContext);
Modules.Add(module);
}
}
以下は、Prettify 関数を持つノードの例です:
public class VbOnGotoStatement : VbStatement<VBScriptParser.OnGoToStmtContext>
{
public VBValueStatement OnValue { get; set; }
public VBValueStatement[] GotoValues { get; set; }
public VbOnGotoStatement(IVBScopeObject context, VBScriptParser.OnGoToStmtContext bloc)
: base(context, bloc)
{
OnValue = VBValueStatement.Get(context, bloc.valueStmt().First());
GotoValues = bloc.valueStmt().Skip(1).Select(v => VBValueStatement.Get(context, v)).ToArray();
}
public override DExpression Prettify(bool partialEvaluation = false)
{
return new DCodeBlock($"On {OnValue.Prettify(partialEvaluation)} Goto {string.Join(", ", GotoValues.Select(v=> v.Prettify(partialEvaluation)))}");
}
}
コンストラクタ VbOnGotoStatement( によってノードが訪問されると、サブノードを再帰的に訪問し、それぞれのビジターのインスタンスを作成します。
その後、Prettify メソッドが呼び出されると、サブ要素は同じ部分評価レベルで整形されます。