├── LICENSE ├── README.adoc ├── class.vba ├── parser.py ├── starter.py ├── unprotect.py ├── vhook.bat └── vhook.vbs /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 ESET spol. s r.o. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = VBA Dynamic Hook - vhook 2 | 3 | Copyright (C) 2016 ESET 4 | 5 | == References 6 | 7 | * Conference talk: http://www.computerworld.pl/konferencja/semafor2016/ 8 | 9 | == Description 10 | 11 | This is our approach to dynamic VBA analysis. 12 | 13 | We use idea similar to Windows API Hooking techniques. 14 | 15 | Basically, we are trying to find 16 | 17 | * the most popular internal VBA functions used inside malicious files (like `Shell`), 18 | * user defined functions which return string, 19 | * external function declarations (like `URLDownloadToFileA`), 20 | * method calls (like `http.Open`) 21 | 22 | and log their usage. 23 | 24 | This information can be used to decide if macro behaves in a suspicious way. 25 | 26 | == Content of this repository 27 | 28 | `vhook.bat`:: Start `vhook.vbs` using `cscript` so `Echo` is printed to the console 29 | `vhook.vbs`:: Main script which runs `unprotect.py`, `parser.py` and `starter.py`, add `class.vba` content to file as another macro 30 | `unprotect.py`:: Try to remove VBA password protection from `.doc` file 31 | `parser.py`:: Parse macro content, extract function usage and add logging code to them 32 | `starter.py`:: Open malicious `.doc` document and close it after timeout 33 | `class.vba` :: Contain function wrappers and helpers 34 | 35 | == Usage 36 | 37 | [WARNING] 38 | Only use VBA Dynamic Hook inside a sandboxed virtual machine! 39 | 40 | Before using VBA Dynamic Hook, enable macro support inside Word: 41 | 42 | ---- 43 | File -> Options -> Trust Center -> Trust Center Settings -> Enable all macros 44 | ---- 45 | 46 | Start script using: 47 | 48 | ---- 49 | vhook.bat word_document.doc 50 | ---- 51 | 52 | Three files will be created after a successful execution: 53 | 54 | ---- 55 | word_document_without.doc <-- file without VBA macro password protection 56 | word_document_output.doc <-- file with added hooks 57 | vhook_%date%.txt <-- script output 58 | ---- 59 | 60 | == Example 61 | 62 | Here is example VBA module. 63 | 64 | ---- 65 | #If Win32 Then 66 | Public Declare Sub MessageBeep Lib "User32" (ByVal N As Long) 67 | #Else 68 | Public Declare Sub MessageBeep Lib "User" (ByVal N As Integer) 69 | #End If 70 | 71 | Public Function hex2ascii(ByVal hextext As String) As String 72 | For y = 1 To Len(hextext) 73 | Num = Mid(hextext, y, 2) 74 | Value = Value & Chr(Val("&h" & Num)) 75 | y = y + 1 76 | Next y 77 | hex2ascii = Value 78 | End Function 79 | 80 | Sub test_function() 81 | a = StrReverse("gnitset") 82 | b = Mid("abcexampledef", 4, 7) 83 | c = Environ("Temp") 84 | d = hex2ascii("656e636f6465645f6865785f737472696e67") 85 | MsgBox (d) 86 | Shell (Chr(99) & Chr(97) & Chr(108) & Chr(99) & Chr(46) & Chr(101) & Chr(120) & Chr(101)) 87 | 88 | Set http = CreateObject("Microsoft.XmlHttp") 89 | http.Open "GET", "http://example.com", False 90 | http.Send 91 | E = http.responseText 92 | 93 | MessageBeep (100) 94 | End Sub 95 | ---- 96 | 97 | Output of VBA Dynamic Hook will look like this: 98 | 99 | ---- 100 | StrReverse testing 101 | MID example 102 | Environ Temp 103 | MID 65 104 | MID 6e 105 | MID 63 106 | MID 6f 107 | MID 64 108 | MID 65 109 | MID 64 110 | MID 5f 111 | MID 68 112 | MID 65 113 | MID 78 114 | MID 5f 115 | MID 73 116 | MID 74 117 | MID 72 118 | MID 69 119 | MID 6e 120 | MID 67 121 | hex2ascii : encoded_hex_string 122 | Messagebox encoded_hex_string 123 | Shell calc.exe 124 | CreateObject Microsoft.XmlHttp 125 | http.Open, GET, http://example.com, False 126 | ---- 127 | -------------------------------------------------------------------------------- /class.vba: -------------------------------------------------------------------------------- 1 | ' Code related to ESET's VBA Dynamic Hook research 2 | ' For feedback or questions contact us at: github@eset.com 3 | ' https://github.com/eset/vba-dynamic-hook/ 4 | ' 5 | ' This code is provided to the community under the two-clause BSD license as 6 | ' follows: 7 | ' 8 | ' Copyright (C) 2016 ESET 9 | ' All rights reserved. 10 | ' 11 | ' Redistribution and use in source and binary forms, with or without 12 | ' modification, are permitted provided that the following conditions are met: 13 | ' 14 | ' 1. Redistributions of source code must retain the above copyright notice, this 15 | ' list of conditions and the following disclaimer. 16 | ' 17 | ' 2. Redistributions in binary form must reproduce the above copyright notice, 18 | ' this list of conditions and the following disclaimer in the documentation 19 | ' and/or other materials provided with the distribution. 20 | ' 21 | ' THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | ' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | ' IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | ' DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | ' FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | ' DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | ' SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | ' CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | ' OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | ' OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | ' 32 | ' Kacper Szurek 33 | ' 34 | ' Contain function wrappers and helpers 35 | 36 | Public vhook_fso As FileSystemObject 37 | Public vhook_log_object As TextStream 38 | Public vhook_word_object As Object 39 | Public vhook_word_document As Object 40 | Public vhook_class_module As Object 41 | 42 | Function vhook_timestamp() 43 | Dim iNow 44 | Dim d(1 To 6) 45 | Dim i As Integer 46 | 47 | 48 | iNow = Now 49 | d(1) = Year(iNow) 50 | d(2) = Month(iNow) 51 | d(3) = Day(iNow) 52 | d(4) = Hour(iNow) 53 | d(5) = Minute(iNow) 54 | d(6) = Second(iNow) 55 | 56 | For i = 1 To 6 57 | If d(i) < 10 Then vhook_timestamp = vhook_timestamp & "0" 58 | vhook_timestamp = vhook_timestamp & d(i) 59 | If i = 3 Then vhook_timestamp = vhook_timestamp & " " 60 | Next i 61 | End Function 62 | 63 | Public Function vhook_log(content As Variant) 64 | vhook_log_object.WriteLine content 65 | End Function 66 | 67 | Public Sub log_return_from_string_function(name As Variant, content As Variant) 68 | vhook_log_object.WriteLine name & " : " & content 69 | End Sub 70 | 71 | Public Sub log_call_to_function(ParamArray a() As Variant) 72 | vhook_log("External call: " & a(0) ) 73 | 74 | Dim counter 75 | counter = 0 76 | For Each b In a 77 | if counter > 0 Then 78 | vhook_log(vbTab & "Param " & counter) 79 | If TypeOf b Is Object Then 80 | vhook_log (vbTab & vbTab & "object") 81 | Else: 82 | vhook_log (vbTab & vbTab & b) 83 | End If 84 | End if 85 | counter = counter + 1 86 | Next 87 | End Sub 88 | 89 | Function log_call_to_method(ParamArray d() As Variant) 90 | Dim result As String 91 | result = "" 92 | For i = 0 To UBound(d) 93 | If TypeOf d(i) Is Object Then 94 | 95 | Else: 96 | If i > 0 Then 97 | result = result & ", " & d(i) 98 | Else: 99 | result = d(i) 100 | End If 101 | End If 102 | Next i 103 | vhook_log result 104 | End Function 105 | 106 | Public Function Shell(PathName As Variant, Optional a As Variant) as Variant 107 | vhook_log("Shell " & PathName) 108 | Shell = vhook_word_object.Run("Shell_Builtin", PathName, a) 109 | End Function 110 | 111 | Public Function Mid(content As Variant, Start As Variant, Optional Length As Variant) As Variant 112 | Dim temp 113 | temp = vhook_word_object.Run("Mid_Builtin", content, Start, Length) 114 | vhook_log ("MID " & temp) 115 | Mid = temp 116 | End Function 117 | 118 | Public Function CreateObject(ObjectName As Variant) As Object 119 | vhook_log("CreateObject " & ObjectName) 120 | Set CreateObject = vhook_word_object.Run("CreateObject_Builtin", ObjectName) 121 | End Function 122 | 123 | Public Function GetObject(Optional a As Variant, Optional b as Variant) As Object 124 | If IsMissing(b) Then 125 | vhook_log ("GetObject " & a) 126 | Set GetObject = vhook_word_object.Run("GetObject_Builtin", a) 127 | Else 128 | If IsMissing(a) Then 129 | vhook_log ("GetObject " & b) 130 | Set GetObject = vhook_word_object.Run("GetObject_Builtin_2", b) 131 | Else 132 | vhook_log ("GetObject " & a & " " & b) 133 | Set GetObject = vhook_word_object.Run("GetObject_Builtin", a, b) 134 | End If 135 | End If 136 | End Function 137 | 138 | Public Function StrReverse(content as Variant) as Variant 139 | Dim temp 140 | temp = vhook_word_object.Run("StrReverse_Builtin", content) 141 | vhook_log("StrReverse " & temp) 142 | StrReverse = temp 143 | End Function 144 | 145 | Public Function Left(content As Variant, number_of_characters as Variant) as Variant 146 | Dim temp 147 | temp = vhook_word_object.Run("Left_Builtin", content, number_of_characters) 148 | vhook_log("Left " & temp) 149 | Left = temp 150 | End Function 151 | 152 | Public Function Environ(a as Variant) as Variant 153 | vhook_log("Environ " & a) 154 | Environ = vhook_word_object.Run("Environ_Builtin", a) 155 | End Function 156 | 157 | Public Function MsgBox(Prompt As Variant, Optional a As Variant, Optional b As Variant, Optional c As Variant, Optional d As Variant) as Variant 158 | vhook_log("Messagebox " & Prompt) 159 | MsgBox = vhook_word_object.Run("MsgBox_Builtin", Prompt) 160 | End Function 161 | 162 | Public Sub vhook_init() 163 | wrapper = "Public Function Mid_Builtin(content as Variant, Start As Variant, Optional Length As Variant) as Variant" 164 | wrapper = wrapper & vbLf & " Mid_Builtin = Mid(content, Start, Length)" 165 | wrapper = wrapper & vbLf & "End Function" 166 | wrapper = wrapper & vbLf & "Public Function CreateObject_Builtin(ObjectName As Variant) As Object" 167 | wrapper = wrapper & vbLf & " Set CreateObject_Builtin = CreateObject(ObjectName)" 168 | wrapper = wrapper & vbLf & "End Function" 169 | wrapper = wrapper & vbLf & "Public Function GetObject_Builtin(a As Variant, Optional b as Variant) As Object" 170 | wrapper = wrapper & vbLf & " Set GetObject_Builtin = GetObject(a, b)" 171 | wrapper = wrapper & vbLf & "End Function" 172 | wrapper = wrapper & vbLf & "Public Function GetObject_Builtin_2(b as Variant) As Object" 173 | wrapper = wrapper & vbLf & " Set GetObject_Builtin_2 = GetObject(, b)" 174 | wrapper = wrapper & vbLf & "End Function" 175 | wrapper = wrapper & vbLf & "Public Function Shell_Builtin(PathName As Variant, Optional a As Variant) as Variant" 176 | wrapper = wrapper & vbLf & " Shell_Builtin = Shell(PathName)" 177 | wrapper = wrapper & vbLf & "End Function" 178 | wrapper = wrapper & vbLf & "Public Function StrReverse_Builtin(content as Variant) as Variant" 179 | wrapper = wrapper & vbLf & " StrReverse_Builtin = StrReverse(content)" 180 | wrapper = wrapper & vbLf & "End Function" 181 | wrapper = wrapper & vbLf & "Public Function Left_Builtin(content As Variant, number_of_characters as Variant) as Variant" 182 | wrapper = wrapper & vbLf & " Left_Builtin = Left(content, number_of_characters)" 183 | wrapper = wrapper & vbLf & "End Function" 184 | wrapper = wrapper & vbLf & "Public Function Environ_Builtin(a as Variant) as Variant" 185 | wrapper = wrapper & vbLf & " Environ_Builtin = Environ(a)" 186 | wrapper = wrapper & vbLf & "End Function" 187 | wrapper = wrapper & vbLf & "Public Function MsgBox_Builtin(Prompt As Variant) as Variant" 188 | wrapper = wrapper & vbLf & " MsgBox_Builtin = MsgBox(Prompt)" 189 | wrapper = wrapper & vbLf & "End Function" 190 | 191 | Set vhook_fso = New FileSystemObject 192 | Set vhook_log_object = vhook_fso.CreateTextFile(ThisDocument.Path & "\vhook_" & vhook_timestamp() & ".txt", True) 193 | Set vhook_word_object = VBA.CreateObject("Word.Application") 194 | Set vhook_word_document = vhook_word_object.Documents.Add 195 | Set vhook_class_module = vhook_word_document.VBProject.VBComponents.Add(1) 196 | vhook_class_module.Name = "vhook" 197 | vhook_class_module.CodeModule.AddFromString wrapper 198 | End Sub 199 | -------------------------------------------------------------------------------- /parser.py: -------------------------------------------------------------------------------- 1 | # Code related to ESET's VBA Dynamic Hook research 2 | # For feedback or questions contact us at: github@eset.com 3 | # https://github.com/eset/vba-dynamic-hook/ 4 | # 5 | # This code is provided to the community under the two-clause BSD license as 6 | # follows: 7 | # 8 | # Copyright (C) 2016 ESET 9 | # All rights reserved. 10 | # 11 | # Redistribution and use in source and binary forms, with or without 12 | # modification, are permitted provided that the following conditions are met: 13 | # 14 | # 1. Redistributions of source code must retain the above copyright notice, this 15 | # list of conditions and the following disclaimer. 16 | # 17 | # 2. Redistributions in binary form must reproduce the above copyright notice, 18 | # this list of conditions and the following disclaimer in the documentation 19 | # and/or other materials provided with the distribution. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | # 32 | # Kacper Szurek 33 | # 34 | # Parse macro content, extract function usage and add logging code to them 35 | 36 | import sys 37 | import re 38 | 39 | class vhook: 40 | IMPORTANT_FUNCTIONS_LIST = ["CallByName"] 41 | EXTERNAL_FUNCTION_DECLARATION_REGEXP = re.compile(r'Declare *(?:PtrSafe)? *(?:Sub|Function) *(.*?) *Lib *"[^"]+" *(?:Alias *"([^"]+)")?') 42 | EXTERNAL_FUNCTION_REGEXP = None 43 | EXTERNAL_FUNCTION_REGEXP_2 = None 44 | BEGIN_FUNCTION_REGEXP = re.compile(r"\s*(?:function|sub) (.*?)\(", re.IGNORECASE) 45 | END_FUNCTION_REGEXP = re.compile(r"^\s*end\s*(?:function|sub)", re.IGNORECASE) 46 | METHOD_CALL_REGEXP = re.compile(r"^\s*([a-z_\.0-9]+\.[a-z_0-9]+)\s*\((.*)\)", re.IGNORECASE) 47 | METHOD_CALL_REGEXP_2 = re.compile(r"^\s*([a-z_\.0-9]+\.[a-z_0-9]+) +(.*)", re.IGNORECASE) 48 | IMPORTANT_FUNCTION_REGEXP = re.compile(r"^(Set\s+)?([a-z0-9]+\s*=)?\s*("+"|".join(IMPORTANT_FUNCTIONS_LIST)+")\s*\((.*)\)", re.IGNORECASE) 49 | IMPORTANT_FUNCTION_REGEXP_2 = re.compile(r"^(Set\s+)?([a-z0-9]+\s*=)?\s*("+"|".join(IMPORTANT_FUNCTIONS_LIST)+")(.*)", re.IGNORECASE) 50 | 51 | is_auto_open_function = False 52 | current_function_name = "" 53 | declared_function_original_names = {} 54 | 55 | lines = []; 56 | i = 0 57 | counter = 0 58 | line = "" 59 | 60 | output = [] 61 | 62 | 63 | def print_info(self, message): 64 | print message 65 | 66 | def __init__(self): 67 | self.lines = sys.stdin.read() 68 | 69 | if len(self.lines) == 0: 70 | self.print_info("[-] Missing input") 71 | sys.exit() 72 | 73 | # For external functions we can ignore long lines errors 74 | self.prepare_external_function_calls(self.lines.replace(" _\n", " ")) 75 | self.lines = self.lines.split("\n") 76 | self.dispatch() 77 | 78 | def add_content_to_output(self, content): 79 | self.output.append(content) 80 | 81 | def add_line_to_output(self, i): 82 | self.add_content_to_output(self.get_line(i)) 83 | 84 | def add_current_line_to_output(self): 85 | self.add_content_to_output(self.get_current_line()) 86 | 87 | 88 | """ 89 | See: https://msdn.microsoft.com/en-us/library/ba9sxbw4.aspx 90 | We simply skip those kind of lines 91 | """ 92 | def is_long_line(self): 93 | counter = 0 94 | while True: 95 | if self.get_line(self.i+counter)[-2:] == " _": 96 | counter += 1 97 | else: 98 | return counter 99 | 100 | """ 101 | Those functions starts automatically when a document is opened 102 | We need to initialize our logger there 103 | """ 104 | def is_autostart_function(self): 105 | auto_open_list = ["document_open", "workbook_open", "autoopen", "auto_open"] 106 | 107 | if any(x in self.current_function_name.lower() for x in auto_open_list): 108 | return True 109 | 110 | return False 111 | 112 | """ 113 | If current line is function declaration, get its name 114 | Its then used for checking if we return something from this function 115 | """ 116 | def is_begin_function_line(self): 117 | matched = re.search(self.BEGIN_FUNCTION_REGEXP, self.get_current_line()) 118 | if matched and not "declare" in self.get_current_line().lower(): 119 | self.current_function_name = matched.group(1).strip() 120 | return True 121 | 122 | return False 123 | 124 | """ 125 | Check if its function end 126 | We add there exception handler 127 | """ 128 | def is_end_function_line(self): 129 | if re.search(self.END_FUNCTION_REGEXP, self.get_current_line()): 130 | return True 131 | 132 | return False 133 | 134 | """ 135 | For non-object return types, you assign the value to the name of function 136 | So we can check if this function return something and add our logger here 137 | """ 138 | def is_return_string_from_function_line(self): 139 | if self.current_function_name != "": 140 | if re.search("^ *"+re.escape(self.current_function_name)+" *=", self.get_current_line(), re.IGNORECASE): 141 | return True 142 | return False 143 | 144 | """ 145 | Found all `Class.Method params` and Class.Method (params)` calls 146 | We dont support params passed by name, like name:=value 147 | We need to check if its not method assign like variable = Class.Method 148 | """ 149 | def is_method_call_line(self): 150 | matched = re.search(self.METHOD_CALL_REGEXP, self.get_current_line()) 151 | if not matched: 152 | matched = re.search(self.METHOD_CALL_REGEXP_2, self.get_current_line()) 153 | 154 | if matched: 155 | method_name = matched.group(1).strip() 156 | params = matched.group(2).strip() 157 | 158 | if len(params) > 0: 159 | # We dont support params passed by name 160 | # And Class.Method = 1 161 | if "=" in self.get_current_line(): 162 | return False 163 | 164 | return [method_name, params] 165 | 166 | return [method_name] 167 | return False 168 | 169 | """ 170 | Check if its call to previously defined external library 171 | """ 172 | def is_external_function_call_line(self): 173 | # Do we have any external declarations 174 | if self.EXTERNAL_FUNCTION_REGEXP == None: 175 | return False 176 | 177 | # Skip if its declaration, not usage 178 | if re.search(self.EXTERNAL_FUNCTION_DECLARATION_REGEXP, self.get_current_line()): 179 | return False 180 | 181 | matched = re.search(self.EXTERNAL_FUNCTION_REGEXP, self.get_current_line()) 182 | 183 | if not matched: 184 | matched = re.search(self.EXTERNAL_FUNCTION_REGEXP_2, self.get_current_line()) 185 | 186 | if matched: 187 | name = matched.group(1).strip() 188 | rest = matched.group(2).strip() 189 | if name in self.declared_function_original_names: 190 | rest = re.sub(r"ByVal", "", rest, flags=re.IGNORECASE) 191 | return [name, rest] 192 | 193 | return False 194 | 195 | """ 196 | We hook some important function like CallByName which cannot be hooked using another techniques 197 | """ 198 | def is_important_function_call_line(self): 199 | matched = re.search(self.IMPORTANT_FUNCTION_REGEXP, self.get_current_line()) 200 | 201 | if not matched: 202 | matched = re.search(self.IMPORTANT_FUNCTION_REGEXP_2, self.get_current_line()) 203 | 204 | if matched: 205 | name = matched.group(3).strip() 206 | rest = matched.group(4).strip() 207 | 208 | return [name, rest] 209 | 210 | return False 211 | 212 | """ 213 | Find all external library declarations like: 214 | Private Declare Function GetDesktopWindow Lib "user32" () As Long 215 | """ 216 | def prepare_external_function_calls(self, content): 217 | declared_function_list = [] 218 | 219 | for f in re.findall(self.EXTERNAL_FUNCTION_DECLARATION_REGEXP, content): 220 | declared_function_list.append(re.escape(f[0].strip())) 221 | if f[1] != "": 222 | self.declared_function_original_names[f[0].strip()] = f[1].strip() 223 | else: 224 | self.declared_function_original_names[f[0].strip()] = f[0].strip() 225 | 226 | if len(declared_function_list) > 0: 227 | self.print_info("[+] Found external function declarations: {}".format(",".join(self.declared_function_original_names.values()))) 228 | 229 | self.EXTERNAL_FUNCTION_REGEXP = re.compile("({})\s*\((.*)\)".format("|".join(declared_function_list))) 230 | self.EXTERNAL_FUNCTION_REGEXP_2 = re.compile("({})\s*(.*)".format("|".join(declared_function_list))) 231 | 232 | """ 233 | Get single line by ids number 234 | """ 235 | def get_line(self, i): 236 | if i < self.counter: 237 | return self.lines[i] 238 | return "" 239 | 240 | """ 241 | Get current line 242 | """ 243 | def get_current_line(self): 244 | if self.i < self.counter: 245 | return self.lines[self.i] 246 | return "" 247 | 248 | """ 249 | Set current line, so we can then use get_current_line 250 | """ 251 | def set_current_line(self): 252 | self.line = self.get_line(self.i) 253 | 254 | """ 255 | Some function have special aliases for null support 256 | """ 257 | def replace_function_aliases(self): 258 | line = self.lines[self.i] 259 | line = re.sub(r"(VBA\.CreateObject)", "CreateObject", line, flags=re.IGNORECASE) 260 | line = re.sub(r"Left\$", "Left", line, flags=re.IGNORECASE) 261 | line = re.sub(r"Right\$", "Right", line, flags=re.IGNORECASE) 262 | line = re.sub(r"Mid\$", "Mid", line, flags=re.IGNORECASE) 263 | line = re.sub(r"Environ\$", "Environ", line, flags=re.IGNORECASE) 264 | self.lines[self.i] = line 265 | 266 | """ 267 | Main program loop 268 | """ 269 | def dispatch(self): 270 | self.i = 0 271 | self.counter = len(self.lines) 272 | while self.i < self.counter: 273 | self.set_current_line() 274 | self.replace_function_aliases() 275 | 276 | is_long_line = self.is_long_line() 277 | is_method_call_line = self.is_method_call_line() 278 | is_external_function_call_line = self.is_external_function_call_line() 279 | is_important_function_call_line = self.is_important_function_call_line() 280 | 281 | if is_long_line > 0: 282 | self.print_info("[+] Found long line, skip {} lines".format(is_long_line)) 283 | 284 | for ii in range(self.i, self.i+is_long_line+1): 285 | self.add_line_to_output(ii) 286 | 287 | self.i += is_long_line+1 288 | continue 289 | elif self.is_begin_function_line(): 290 | if self.is_autostart_function(): 291 | self.print_info("[+] Found autostart function - {}".format(self.current_function_name)) 292 | 293 | self.is_auto_open_function = True 294 | 295 | self.add_current_line_to_output() 296 | self.add_content_to_output("On Error GoTo vhook_exception_handler:") 297 | self.add_content_to_output("vhook_init") 298 | else: 299 | self.print_info("[+] Found function - {}".format(self.current_function_name)) 300 | 301 | self.is_auto_open_function = False 302 | 303 | self.add_current_line_to_output() 304 | elif self.is_return_string_from_function_line(): 305 | self.print_info("\t[+] Function return string") 306 | 307 | self.add_current_line_to_output() 308 | self.add_content_to_output('log_return_from_string_function "{}", {}'.format(self.current_function_name, self.current_function_name)) 309 | elif is_method_call_line != False: 310 | if len(is_method_call_line) == 1: 311 | self.print_info("\t[+] Found call to method: {}".format(is_method_call_line[0])) 312 | self.add_content_to_output('log_call_to_method "{}"'.format(is_method_call_line[0])) 313 | else: 314 | self.print_info("\t[+] Found call to method with params: {} - {}".format(is_method_call_line[0], is_method_call_line[1])) 315 | self.add_content_to_output("log_call_to_method \"{}\", {}".format(is_method_call_line[0], is_method_call_line[1])) 316 | self.add_current_line_to_output() 317 | elif is_external_function_call_line != False: 318 | self.print_info("\t[+] Found externall call to {}".format(self.declared_function_original_names[is_external_function_call_line[0]])) 319 | 320 | self.add_content_to_output('log_call_to_function "{}", {}'.format(self.declared_function_original_names[is_external_function_call_line[0]], is_external_function_call_line[1])) 321 | self.add_current_line_to_output() 322 | elif is_important_function_call_line != False: 323 | self.print_info("\t[+] Found important function {}".format(is_important_function_call_line[0])) 324 | 325 | self.add_content_to_output('log_call_to_function "{}", {}'.format(is_important_function_call_line[0], is_important_function_call_line[1])) 326 | self.add_current_line_to_output() 327 | elif self.is_end_function_line(): 328 | if self.is_auto_open_function: 329 | self.print_info("\t[+] Add exception handler") 330 | self.add_content_to_output('vhook_exception_handler:') 331 | self.add_content_to_output('vhook_log ("Exception: " & Err.Description)') 332 | self.add_content_to_output('On Error Resume Next') 333 | 334 | self.add_current_line_to_output() 335 | else: 336 | self.add_current_line_to_output() 337 | 338 | self.i += 1 339 | print "|*&|VHOOK_SPLITTER|&*|" 340 | print "\n".join(self.output) 341 | 342 | vhook() 343 | -------------------------------------------------------------------------------- /starter.py: -------------------------------------------------------------------------------- 1 | # Code related to ESET's VBA Dynamic Hook research 2 | # For feedback or questions contact us at: github@eset.com 3 | # https://github.com/eset/vba-dynamic-hook/ 4 | # 5 | # This code is provided to the community under the two-clause BSD license as 6 | # follows: 7 | # 8 | # Copyright (C) 2016 ESET 9 | # All rights reserved. 10 | # 11 | # Redistribution and use in source and binary forms, with or without 12 | # modification, are permitted provided that the following conditions are met: 13 | # 14 | # 1. Redistributions of source code must retain the above copyright notice, this 15 | # list of conditions and the following disclaimer. 16 | # 17 | # 2. Redistributions in binary form must reproduce the above copyright notice, 18 | # this list of conditions and the following disclaimer in the documentation 19 | # and/or other materials provided with the distribution. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | # 32 | # Kacper Szurek 33 | # 34 | # Open malicious `.doc` document and close it after timeout 35 | 36 | from time import sleep 37 | from subprocess import Popen, PIPE 38 | import sys 39 | 40 | def popen_timeout(command, timeout): 41 | p = Popen(command, stdout=PIPE, stderr=PIPE) 42 | for t in xrange(timeout): 43 | sleep(1) 44 | if p.poll() is not None: 45 | return p.communicate() 46 | Popen(['taskkill', '/F', '/T', '/PID', str(p.pid)]).communicate() 47 | p.terminate() 48 | 49 | return False 50 | 51 | if len(sys.argv) != 2: 52 | print "[-] Missing path" 53 | else: 54 | popen_timeout(['taskkill', '/f', '/im', 'winword.exe'], 5) 55 | popen_timeout(['cmd', '/c', 'start', '/wait', sys.argv[1]], 10) 56 | -------------------------------------------------------------------------------- /unprotect.py: -------------------------------------------------------------------------------- 1 | # Code related to ESET's VBA Dynamic Hook research 2 | # For feedback or questions contact us at: github@eset.com 3 | # https://github.com/eset/vba-dynamic-hook/ 4 | # 5 | # This code is provided to the community under the two-clause BSD license as 6 | # follows: 7 | # 8 | # Copyright (C) 2016 ESET 9 | # All rights reserved. 10 | # 11 | # Redistribution and use in source and binary forms, with or without 12 | # modification, are permitted provided that the following conditions are met: 13 | # 14 | # 1. Redistributions of source code must retain the above copyright notice, this 15 | # list of conditions and the following disclaimer. 16 | # 17 | # 2. Redistributions in binary form must reproduce the above copyright notice, 18 | # this list of conditions and the following disclaimer in the documentation 19 | # and/or other materials provided with the distribution. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | # 32 | # Kacper Szurek 33 | # 34 | # Try to remove VBA password protection from `.doc` file 35 | 36 | import sys, re, zipfile, os, shutil 37 | 38 | if len(sys.argv) != 3: 39 | print "[-] Usage: unprotect.py [file_with_password] [new_file_without_password]" 40 | sys.exit(1) 41 | 42 | abspath = os.path.abspath(__file__) 43 | dname = os.path.dirname(abspath) 44 | os.chdir(dname) 45 | 46 | def update_zip(old_file_name, new_file_name, filename, data): 47 | with zipfile.ZipFile(old_file_name, 'r') as zin: 48 | with zipfile.ZipFile(new_file_name, 'w') as zout: 49 | for item in zin.infolist(): 50 | if item.filename != filename: 51 | zout.writestr(item, zin.read(item.filename)) 52 | zout.writestr(filename, data) 53 | 54 | REPLACE_STRING = 'Description=' 55 | 56 | old_file_name = sys.argv[1] 57 | new_file_name = sys.argv[2] 58 | is_zip = False 59 | 60 | if open(old_file_name, "rb").read(2) == "PK": 61 | if not zipfile.is_zipfile(old_file_name): 62 | print "[-] Not zip file, probably corrupted docm file" 63 | sys.exit(1) 64 | 65 | is_zip = True 66 | with zipfile.ZipFile(old_file_name, 'a') as archive: 67 | name_list = archive.namelist() 68 | 69 | if "word/vbaProject.bin" not in name_list: 70 | print "[-] Cannot find vbaProject" 71 | sys.exit(1) 72 | 73 | content = archive.read("word/vbaProject.bin") 74 | else: 75 | with open(old_file_name, "rb") as f: 76 | content = f.read() 77 | 78 | match = re.search(r'(CMG="[A-Za-z0-9]+"\s*DPB="[A-Za-z0-9]+"\s*GC="[A-Za-z0-9]+")', content) 79 | if not match: 80 | print "[+] Try advanced method" 81 | match = re.search(r'(GC="[A-Za-z0-9]+")', content) 82 | 83 | if match: 84 | new_content = "" 85 | pass_len = len(match.group()) 86 | new_content = content[:match.start()] 87 | new_content += REPLACE_STRING 88 | new_content += " " * (pass_len - len(REPLACE_STRING)) 89 | new_content += content[match.start()+pass_len:] 90 | 91 | if is_zip: 92 | update_zip(old_file_name, new_file_name, "word/vbaProject.bin", new_content) 93 | else: 94 | with open(new_file_name, "wb") as f: 95 | f.write(new_content) 96 | 97 | print "[+] Password removed" 98 | else: 99 | shutil.copyfile(old_file_name, new_file_name) 100 | print "[-] Probably without pasword" 101 | -------------------------------------------------------------------------------- /vhook.bat: -------------------------------------------------------------------------------- 1 | :: Code related to ESET's VBA Dynamic Hook research 2 | :: For feedback or questions contact us at: github@eset.com 3 | :: https://github.com/eset/vba-dynamic-hook/ 4 | :: 5 | :: This code is provided to the community under the two-clause BSD license as 6 | :: follows: 7 | :: 8 | :: Copyright (C) 2016 ESET 9 | :: All rights reserved. 10 | :: 11 | :: Redistribution and use in source and binary forms, with or without 12 | :: modification, are permitted provided that the following conditions are met: 13 | :: 14 | :: 1. Redistributions of source code must retain the above copyright notice, this 15 | :: list of conditions and the following disclaimer. 16 | :: 17 | :: 2. Redistributions in binary form must reproduce the above copyright notice, 18 | :: this list of conditions and the following disclaimer in the documentation 19 | :: and/or other materials provided with the distribution. 20 | :: 21 | :: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | :: AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | :: IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | :: DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | :: FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | :: DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | :: SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | :: CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | :: OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | :: OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | :: 32 | :: Kacper Szurek 33 | :: Start `vhook.vbs` using `cscript` so `Echo` is printed to the console 34 | 35 | cscript /Nologo vhook.vbs %1 36 | -------------------------------------------------------------------------------- /vhook.vbs: -------------------------------------------------------------------------------- 1 | ' Code related to ESET's VBA Dynamic Hook research 2 | ' For feedback or questions contact us at: github@eset.com 3 | ' https://github.com/eset/vba-dynamic-hook/ 4 | ' 5 | ' This code is provided to the community under the two-clause BSD license as 6 | ' follows: 7 | ' 8 | ' Copyright (C) 2016 ESET 9 | ' All rights reserved. 10 | ' 11 | ' Redistribution and use in source and binary forms, with or without 12 | ' modification, are permitted provided that the following conditions are met: 13 | ' 14 | ' 1. Redistributions of source code must retain the above copyright notice, this 15 | ' list of conditions and the following disclaimer. 16 | ' 17 | ' 2. Redistributions in binary form must reproduce the above copyright notice, 18 | ' this list of conditions and the following disclaimer in the documentation 19 | ' and/or other materials provided with the distribution. 20 | ' 21 | ' THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | ' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | ' IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | ' DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | ' FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | ' DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | ' SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | ' CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | ' OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | ' OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | ' 32 | ' Kacper Szurek 33 | ' 34 | ' Main script which runs `unprotect.py`, `parser.py` and `starter.py`, add `class.vba` content to file as another macro 35 | 36 | Set fso = CreateObject("Scripting.FileSystemObject") 37 | CurrentDirectory = fso.GetAbsolutePathName(".") & "\" 38 | 39 | input_file = fso.GetFile(WScript.Arguments.Item(0)) 40 | input_path = fso.GetAbsolutePathName(input_file) 41 | extension = "." & fso.GetExtensionName(fullpath) 42 | 43 | without_password_path = Replace(input_path, extension, "_without" & extension) 44 | output_path = Replace(input_path, extension, "_output" & extension) 45 | 46 | Dim shell_object : Set shell_object = CreateObject( "WScript.Shell" ) 47 | Dim python_object: Set python_object = shell_object.exec("python " & CurrentDirectory & "unprotect.py """ & input_path & """ " & """" & without_password_path & """" ) 48 | 49 | 50 | Wscript.Echo python_object.StdOut.ReadAll() 51 | 52 | Set word_object = CreateObject("Word.Application") 53 | ' Dont display window 54 | word_object.Visible = True 55 | word_object.DisplayAlerts = False 56 | ' Disable macros 57 | word_object.WordBasic.DisableAutoMacros 1 58 | 59 | On Error Resume Next 60 | 61 | Set word_document = word_object.Documents.Open(without_password_path) 62 | 63 | If Err.Number <> 0 Then 64 | shell_object.exec("taskkill /f /im winword.exe") 65 | WScript.Echo "[-] Error: " & Err.Description 66 | Err.Clear 67 | WScript.Quit 1 68 | End If 69 | 70 | For Each VBComponentVar In word_document.VBProject.VBComponents 71 | with VBComponentVar.CodeModule 72 | Wscript.Echo "[+] Parsing " & .Name 73 | lines_count = .CountOfLines 74 | ' Pass datas to python 75 | 76 | Set python_object = shell_object.exec("python " & CurrentDirectory & "parser.py") 77 | If lines_count > 1 Then 78 | python_object.StdIn.Write .Lines(1, lines_count) 79 | 80 | python_object.StdIn.Close() 81 | 82 | parsed_data = python_object.StdOut.ReadAll() 83 | parsed_data_array = Split(parsed_data, "|*&|VHOOK_SPLITTER|&*|") 84 | Wscript.Echo parsed_data_array(0) 85 | 86 | .DeleteLines 1, lines_count 87 | .InsertLines 1, parsed_data_array(1) 88 | Else 89 | Wscript.Echo "[-] Empty procedure" 90 | End if 91 | end with 92 | Next 93 | 94 | ' Add reference to Microsoft Scripting Runtime for file write 95 | word_document.VBProject.References.AddFromGUID "{420B2830-E718-11CF-893D-00A0C9054228}", 1, 0 96 | 97 | ' Add new module to existing file 98 | Dim class_file, class_file_content 99 | Set class_file = fso.OpenTextFile(CurrentDirectory & "class.vba") 100 | class_file_content = class_file.ReadAll 101 | class_file.Close 102 | 103 | Set class_module = word_document.VBProject.VBComponents.Add(1) 104 | class_module.Name = "vhook" 105 | class_module.CodeModule.AddFromString class_file_content 106 | 107 | word_document.SaveAs output_path 108 | word_object.Quit 0 109 | 110 | Wscript.Echo "[+] Open " & output_path 111 | 112 | shell_object.Run "python " & CurrentDirectory & "starter.py """ & output_path & """", 0, True 113 | 114 | Wscript.Echo "[+] END" 115 | --------------------------------------------------------------------------------