
부분 평가를 지원하는 VBScript 및 VBA 소스 간 디난독화 도구
vbSparkle는 부분 평가(partial-evaluation)에 기반한 소스-투-소스 멀티 플랫폼 Visual Basic 난독화 해제 도구(deobfuscator) 로, 주로 VBScript 및 VBA(Office 매크로)로 작성된 악성 코드 분석에 중점을 둡니다.
네이티브 **C#**으로 작성되었으며 .Net Standard 라이브러리를 제공하고 Windows, Linux, MacOS 등에서 동작합니다.
VBScript 및 VBA의 파싱은 ANTLR 문법(grammar) 및 렉서(lexer) 파서를 통해 처리됩니다.
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(로 교체한 다음 스크립트를 실행하여 출력을 얻는 것입니다.
이 마지막 기법은 간단하지만 몇 가지 단점이 있습니다:
그리고 이 프로젝트가 시작되었을 당시에는 이 두 기법 외에 대안이 없었습니다.
부분 평가는 새로운 개념이 아니며, 주로 컴파일러가 사전 컴파일(pre-compilation) 과정에서 코드 최적화를 위해 사용합니다 (예: https://en.wikipedia.org/wiki/Partial_evaluation).
부분 평가기(partial evaluator)는 프로그램(주제 프로그램(subject program)이라 함)과 해당 프로그램에 대한 일부 입력을 읽고, 제공된 입력에 의존하는 프로그램 부분만 평가합니다. 이러한 모든 부분이 평가되면 나머지 프로그램(잔여 프로그램(residual program)이라 함)이 출력으로 생성됩니다.
예를 들어, 사전 컴파일 과정은 다음 주제 코드를 변환합니다:
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은 문법을 읽고 해당 문법으로 정의된 언어에 대한 인식기(recognizer)를 생성합니다 (즉, 입력 스트림을 읽고 입력 스트림이 문법에 지정된 구문을 따르지 않으면 오류를 생성하는 프로그램).
언어로 유용한 작업을 수행하기 위해 문법의 문법 요소에 액션을 첨부할 수 있습니다. 이러한 액션은 인식기가 생성되는 프로그래밍 언어로 작성됩니다. 인식기가 생성될 때 액션은 인식기 소스 코드의 적절한 지점에 삽입됩니다.
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 메서드가 호출되면 하위 요소도 동일한 부분 평가 수준으로 프리티파이 처리됩니다.