vbSparkle 是一个基于部分求值(partial-evaluation)的跨平台 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 用作命令行工具的一个示例。
当前示例接受一个路径作为参数,或从 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(,然后运行脚本以获取输出。
最后这种技术虽然简单,但也有一些缺点:
而且,在本项目启动时,除了这两种技术之外没有其他替代方案。
部分求值并不是一个新概念,它主要用于编译器在预编译过程中的代码优化(例如 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 是一个常量,并且这是一个原生函数,没有副作用对该示例进行部分求值将得到:
Execute("wscript.echo ""hello""" & vbCrLf)
你猜怎么着?哦,我们刚刚对示例进行了反混淆。
嗯,在实践中,情况要复杂得多:
幸运的是,ANTLR 是一个强大的解析器生成器,用于读取、处理、执行或翻译结构化文本或二进制文件。它以广泛用于构建语言、工具和框架而闻名。ANTLR 从语法生成解析器,该解析器可以构建解析树,并生成监听器接口(或访问者),从而易于响应感兴趣短语的识别。
ANTLR 可以生成词法分析器、解析器、树解析器以及组合的词法分析器-解析器。解析器可以自动生成解析树或抽象语法树,这些树可以进一步用树解析器处理。ANTLR 为指定词法分析器、解析器和树解析器提供了一种一致统一的表示法。
默认情况下,ANTLR 读取语法并为该语法定义的语言生成识别器(即,一个读取输入流并在输入流不符合语法指定的语法时生成错误的程序)。
为了对该语言进行一些有用的操作,可以在语法中为语法元素附加动作。这些动作以正在生成识别器的编程语言编写。生成识别器时,这些动作会在适当位置嵌入识别器的源代码中。
得益于 ANTLR,利用 Ulrich Wolffgang 编写的现有语法,快速轻松地生成了第一版 AST、词法分析器和解析器。
来自 VB 语法的 Railroad 示例:

这个语法在部分特定的 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 方法时,子元素会以相同的部分求值级别进行美化。