diff --git a/README.md b/README.md index 40586d3..235fa3c 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,15 @@ xlmdeobfuscator can be executed on any OS to extract and deobfuscate macros in x Note: if you want to use MS Excel (on Windows), you need to install pywin32 library and use --with-ms-excel switch. If --with-ms-excel is used, xlmdeobfuscator, first, attempts to load xls files with MS Excel, if it fails it uses [xlrd2 library](https://github.com/DissectMalware/xlrd2). +# Project Using XLMMacroDeofuscator +XLMMacroDeofuscator is adopted in the following projects: +* [CAPE Sandbox](https://github.com/ctxis/CAPE) +* [FAME](https://certsocietegenerale.github.io/fame/) +* [REMNUX](https://remnux.org/) +* [IntlOwl](https://github.com/certego/IntelOwl) + +Please contact me if you incorporated XLMMacroDeofuscator in your project. + # How to Contribute If you found a bug or would like to suggest an improvement, please create a new issue on the [issues page](https://github.com/DissectMalware/XLMMacroDeobfuscator/issues). diff --git a/XLMMacroDeobfuscator/boundsheet.py b/XLMMacroDeobfuscator/boundsheet.py index c1e4b31..5c30e93 100644 --- a/XLMMacroDeobfuscator/boundsheet.py +++ b/XLMMacroDeobfuscator/boundsheet.py @@ -12,11 +12,19 @@ def __init__(self): self.formula = None self.value = None self.attributes = {} + self.times_visited = 0 + self.emulated = False def get_attribute(self, attribute_name): # return default value if attributes doesn't cointain the attribute_name pass + def visit(self): + self.times_visited += 1 + + def visited_too_many_times(self): + return (self.times_visited > 1000) + def __deepcopy__(self, memodict={}): copy = type(self)() memodict[id(self)] = copy @@ -26,6 +34,7 @@ def __deepcopy__(self, memodict={}): copy.formula = self.formula copy.value = self.value copy.attributes = self.attributes + copy.emulated = self.emulated return copy def get_local_address(self): @@ -34,6 +43,21 @@ def get_local_address(self): def __str__(self): return "'{}'!{}".format(self.sheet.name,self.get_local_address()) + def debug(self): + """ + Return a string with full details about the cell. + """ + r = "" + r += "Address:\t" + str(self) + "\n" + r += "Sheet:\t\t" + str(self.sheet) + "\n" + r += "Column:\t\t" + str(self.column) + "\n" + r += "Row:\t\t" + str(self.row) + "\n" + r += "Formula:\t" + str(self.formula) + "\n" + r += "Value:\t\t" + str(self.value) + "\n" + r += "Attributes:\t" + str(self.attributes) + "\n" + r += "Emulated:\t" + str(self.emulated) + "\n" + return r + @staticmethod def convert_to_column_index(s): number = 0 diff --git a/XLMMacroDeobfuscator/deobfuscator.py b/XLMMacroDeobfuscator/deobfuscator.py index 768600d..794fdc1 100644 --- a/XLMMacroDeobfuscator/deobfuscator.py +++ b/XLMMacroDeobfuscator/deobfuscator.py @@ -1,3 +1,4 @@ +import traceback import argparse import base64 import hashlib @@ -38,6 +39,8 @@ import copy from distutils.util import strtobool +#debug = True +debug = False class EvalStatus(Enum): FullEvaluation = 1 @@ -49,8 +52,11 @@ class EvalStatus(Enum): FullBranching = 7 IGNORED = 8 - +intermediate_iocs = set() +URL_REGEX = r'.*([hH][tT][tT][pP][sS]?://(([a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-\.]+(:[0-9]+)?)+(/([/\?&\~=a-zA-Z0-9_\-\.](?!http))+)?)).*' + class EvalResult: + def __init__(self, next_cell, status, value, text): self.next_cell = next_cell self.status = status @@ -59,6 +65,15 @@ def __init__(self, next_cell, status, value, text): self.output_level = 0 self.set_text(text) + def __repr__(self): + r = "EvalResult:\n" + r += "\tNext Cell:\t\t" + str(self.next_cell) + "\n" + r += "\tValue:\t\t\t" + str(self.value) + "\n" + r += "\tStatus:\t\t\t" + str(self.status) + "\n" + r += "\tText:\t\t\t" + str(self.text) + "\n" + r += "\tOutput Level:\t\t" + str(self.output_level) + "\n" + return r + @staticmethod def is_int(text): try: @@ -114,13 +129,17 @@ def get_text(self, unwrap=False): return result - def set_text(self, data, wrap=False): + def set_text(self, data, wrap=False): if data is not None: if wrap: self.text = self.wrap_str_literal(data) else: self.text = str(data) + # Save intermediate URL IOCs if we find them. + for url in re.findall(URL_REGEX, self.text): + url = url[0] + intermediate_iocs.add(url) class XLMInterpreter: def __init__(self, xlm_wrapper, output_level=0): @@ -176,6 +195,7 @@ def __init__(self, xlm_wrapper, output_level=0): 'CLOSE': self.halt_handler, 'CONCATENATE': self.concatenate_handler, 'DAY': self.day_handler, + 'DEFINE.NAME': self.define_name_handler, 'DIRECTORY': self.directory_handler, 'ERROR': self.error_handler, 'FORMULA': self.formula_handler, @@ -269,7 +289,7 @@ def get_formula_cell(self, macrosheet, col, row): current_row = row current_addr = col + str(current_row) while current_addr not in macrosheet.cells or \ - macrosheet.cells[current_addr].formula is None: + macrosheet.cells[current_addr].formula is None: if (current_row - row) < 10000: current_row += 1 else: @@ -288,6 +308,24 @@ def get_range_parts(self, parse_tree): else: return None, None + def get_cell_from_workbook(self, cell_addr): + """ + Get a cell from the current workbook given a cell addr of the form + 'SHEET_NAME!COLROW', where SHEET_NAME is the sheet name, COL is the column name + (alphabetic characters) and ROW is the row (integer). + """ + + # Pull out the sheet name, column, and row. + addr_pat = r"'(\w+)'!([A-Z]+)(\d+)" + addr_info = re.findall(addr_pat, cell_addr) + if (len(addr_info) == 0): + # Invalid addr string. + return None + sheet_name, col, row = addr_info[0] + + # Get the referenced cell. + return self.get_cell(sheet_name, col, row) + def get_cell_addr(self, current_cell, cell_parse_tree): res_sheet = res_col = res_row = None @@ -396,13 +434,14 @@ def set_cell(self, sheet_name, col, row, text): cell.value = text - def convert_ptree_to_str(self, parse_tree_root): + @staticmethod + def convert_ptree_to_str(parse_tree_root): if type(parse_tree_root) == Token: return str(parse_tree_root) else: result = '' for child in parse_tree_root.children: - result += self.convert_ptree_to_str(child) + result += XLMInterpreter.convert_ptree_to_str(child) return result def get_window(self, number): @@ -455,13 +494,25 @@ def get_default_cell_info(self, number): def evaluate_formula(self, current_cell, name, arguments, interactive, destination_arg=1): + current_cell.emulated = True source, destination = (arguments[0], arguments[1]) if destination_arg == 1 else (arguments[1], arguments[0]) src_eval_result = self.evaluate_parse_tree(current_cell, source, interactive) + if isinstance(destination, Token): + # TODO: get_defined_name must return a list; currently it returns list or one item + destination = self.xlm_wrapper.get_defined_name(destination) + if isinstance(destination, list): + destination = [] if not destination else destination[0] + + #if (not hasattr(destination, "data")): + # return EvalResult(current_cell, EvalStatus.Error, 0, "") if destination.data == 'defined_name' or destination.data=='name': - formula_str = self.xlm_wrapper.get_defined_name(destination.children[2]) - destination = self.xlm_parser.parse('='+formula_str).children[0] + defined_name_formula = self.xlm_wrapper.get_defined_name(destination.children[2]) + if isinstance(defined_name_formula, Tree): + destination = defined_name_formula + else: + destination = self.xlm_parser.parse('='+defined_name_formula).children[0] if destination.data == 'range': dst_start_sheet, dst_start_col, dst_start_row = self.get_cell_addr(current_cell, destination.children[0]) @@ -470,7 +521,7 @@ def evaluate_formula(self, current_cell, name, arguments, interactive, destinati dst_start_sheet, dst_start_col, dst_start_row = self.get_cell_addr(current_cell, destination) dst_end_sheet, dst_end_col, dst_end_row = dst_start_sheet, dst_start_col, dst_start_row - destination_str = self.convert_ptree_to_str(destination) + destination_str = XLMInterpreter.convert_ptree_to_str(destination) text = src_eval_result.get_text(unwrap=True) if src_eval_result.status == EvalStatus.FullEvaluation: @@ -506,6 +557,7 @@ def evaluate_formula(self, current_cell, name, arguments, interactive, destinati return EvalResult(None, src_eval_result.status, return_val, text) def evaluate_argument_list(self, current_cell, name, arguments): + current_cell.emulated = True args_str = '' for argument in arguments: if type(argument) is Token or type(argument) is Tree: @@ -519,21 +571,30 @@ def evaluate_argument_list(self, current_cell, name, arguments): return EvalResult(None, status, return_val, text) def evaluate_function(self, current_cell, parse_tree_root, interactive): + current_cell.emulated = True function_name = parse_tree_root.children[0] + if debug: + print("FUNCTION NAME!!") + print(function_name) + # OFFSET()() if isinstance(function_name, Tree) and function_name.data == 'function_call': + if debug: + print("HERE: 1") func_eval_result = self.evaluate_parse_tree(current_cell, function_name, False) if func_eval_result.status != EvalStatus.FullEvaluation: - return EvalResult(func_eval_result.next_cell, func_eval_result.status, 0, self.convert_ptree_to_str(parse_tree_root)) + return EvalResult(func_eval_result.next_cell, func_eval_result.status, 0, XLMInterpreter.convert_ptree_to_str(parse_tree_root)) else: - func_eval_result.text = self.convert_ptree_to_str(parse_tree_root) + func_eval_result.text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return func_eval_result # handle alias name for a function (REGISTER) # c45ed3a0ce5df27ac29e0fab99dc4d462f61a0d0c025e9161ced3b2c913d57d8 if function_name in self._registered_functions: + if debug: + print("HERE: 2") parse_tree_root.children[0] = parse_tree_root.children[0].update(None, self._registered_functions[function_name][ 'name']) @@ -541,10 +602,14 @@ def evaluate_function(self, current_cell, parse_tree_root, interactive): # cell_function_call if isinstance(function_name, Tree) and function_name.data == 'cell': + if debug: + print("HERE: 3") self._function_call_stack.append(current_cell) return self.goto_handler([function_name], current_cell, interactive, parse_tree_root) if function_name.lower() in self.defined_names: + if debug: + print("HERE: 4") try: ref_parsed = self.xlm_parser.parse('='+ self.defined_names[function_name.lower()]) if isinstance(ref_parsed.children[0],Tree) and ref_parsed.children[0].data =='cell': @@ -556,16 +621,22 @@ def evaluate_function(self, current_cell, parse_tree_root, interactive): # cell_function_call if isinstance(function_name, Tree) and function_name.data == 'cell': + if debug: + print("HERE: 5") self._function_call_stack.append(current_cell) return self.goto_handler([function_name], current_cell, interactive, parse_tree_root) if self.ignore_processing and function_name != 'NEXT': + if debug: + print("HERE: 6") if function_name == 'WHILE': self.next_count += 1 return EvalResult(None, EvalStatus.IGNORED, 0, '') arguments = [] for i in parse_tree_root.children[2].children: + if debug: + print("HERE: 7") if type(i) is not Token: if len(i.children) > 0: arguments.append(i.children[0]) @@ -573,9 +644,13 @@ def evaluate_function(self, current_cell, parse_tree_root, interactive): arguments.append(i.children) if function_name in self._handlers: + if debug: + print("HERE: 8") eval_result = self._handlers[function_name](arguments, current_cell, interactive, parse_tree_root) else: + if debug: + print("HERE: 9") eval_result = self.evaluate_argument_list(current_cell, function_name, arguments) if function_name in XLMInterpreter.jump_functions: @@ -634,7 +709,7 @@ def active_cell_handler(self, arguments, current_cell, interactive, parse_tree_r return_val = val text = str(return_val) else: - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return_val = text return EvalResult(None, status, return_val, text) @@ -656,22 +731,22 @@ def get_cell_handler(self, arguments, current_cell, interactive, parse_tree_root text = str(return_val) status = EvalStatus.FullEvaluation elif not_implemented: - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return_val = '' else: text = str(data) if data is not None else None return_val = data status = EvalStatus.FullEvaluation else: - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return_val = '' status = EvalStatus.PartialEvaluation return EvalResult(None, status, return_val, text) def set_name_handler(self, arguments, current_cell, interactive, parse_tree_root): - label = EvalResult.unwrap_str_literal(self.convert_ptree_to_str(arguments[0])).lower() + label = EvalResult.unwrap_str_literal(XLMInterpreter.convert_ptree_to_str(arguments[0])).lower() if isinstance(arguments[1], Tree) and arguments[1].data == 'cell': - arg2_text = self.convert_ptree_to_str(arguments[1]) + arg2_text = XLMInterpreter.convert_ptree_to_str(arguments[1]) names = self.xlm_wrapper.get_defined_names() names[label] = arguments[1] text = 'SET.NAME({},{})'.format(label, arg2_text) @@ -687,7 +762,7 @@ def set_name_handler(self, arguments, current_cell, interactive, parse_tree_root return_val = 0 status = EvalStatus.FullEvaluation else: - return_val = text = self.convert_ptree_to_str(parse_tree_root) + return_val = text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) status = arg2_eval_result.status return EvalResult(None, status, return_val, text) @@ -713,7 +788,7 @@ def get_workspace_handler(self, arguments, current_cell, interactive, parse_tree next_cell = None if status == EvalStatus.Error: - return_val = text = self.convert_ptree_to_str(parse_tree_root) + return_val = text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return EvalResult(None, status, return_val, text) @@ -725,14 +800,14 @@ def get_window_handler(self, arguments, current_cell, interactive, parse_tree_ro if arg_eval_result.status == EvalStatus.FullEvaluation and self.is_float(arg_eval_result.get_text()): window_param = self.get_window(int(float(arg_eval_result.get_text()))) current_cell.value = window_param - text = window_param # self.convert_ptree_to_str(parse_tree_root) + text = window_param # XLMInterpreter.convert_ptree_to_str(parse_tree_root) return_val = window_param status = EvalStatus.FullEvaluation else: return_val = text = 'GET.WINDOW({})'.format(arg_eval_result.get_text()) status = arg_eval_result.status if status == EvalStatus.Error: - return_val = text = self.convert_ptree_to_str(parse_tree_root) + return_val = text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return EvalResult(None, status, return_val, text) @@ -749,7 +824,7 @@ def on_time_handler(self, arguments, current_cell, interactive, parse_tree_root) return_val = 0 if status == EvalStatus.Error: - return_val = text = self.convert_ptree_to_str(parse_tree_root) + return_val = text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) next_cell = None return EvalResult(next_cell, status, return_val, text) @@ -781,7 +856,7 @@ def day_handler(self, arguments, current_cell, interactive, parse_tree_root): text = 'DAY(Serial Date)' status = EvalStatus.NotImplemented else: - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) status = arg1_eval_result.status else: text = str(self.day_of_month) @@ -836,6 +911,9 @@ def if_handler(self, arguments, current_cell, interactive, parse_tree_root): if visited is False: self._indent_level += 1 size = len(arguments) + if debug: + print("IF HANDLER!!") + print(arguments) if size == 3: cond_eval_result = self.evaluate_parse_tree(current_cell, arguments[0], interactive) if self.is_bool(cond_eval_result.value): @@ -861,7 +939,7 @@ def if_handler(self, arguments, current_cell, interactive, parse_tree_root): status = EvalStatus.Branching else: status = EvalStatus.FullEvaluation - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) else: memory_state = copy.deepcopy(current_cell.sheet.cells) @@ -873,15 +951,15 @@ def if_handler(self, arguments, current_cell, interactive, parse_tree_root): self._branch_stack.append( (current_cell, arguments[1], current_cell.sheet.cells, self._indent_level, '[TRUE]')) - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) status = EvalStatus.FullBranching else: status = EvalStatus.FullEvaluation - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) else: # loop detected - text = '[[LOOP]]: ' + self.convert_ptree_to_str(parse_tree_root) + text = '[[LOOP]]: ' + XLMInterpreter.convert_ptree_to_str(parse_tree_root) status = EvalStatus.End return EvalResult(None, status, 0, text) @@ -900,13 +978,44 @@ def mid_handler(self, arguments, current_cell, interactive, parse_tree_root): text = str(return_val) status = EvalStatus.FullEvaluation if status == EvalStatus.PartialEvaluation: - text = 'MID({},{},{})'.format(self.convert_ptree_to_str(arguments[0]), - self.convert_ptree_to_str(arguments[1]), - self.convert_ptree_to_str(arguments[2])) + text = 'MID({},{},{})'.format(XLMInterpreter.convert_ptree_to_str(arguments[0]), + XLMInterpreter.convert_ptree_to_str(arguments[1]), + XLMInterpreter.convert_ptree_to_str(arguments[2])) return EvalResult(None, status, return_val, text) + def define_name_handler(self, arguments, current_cell, interactive, parse_tree_root): + + # DEFINE.NAME(name_text, refers_to, macro_type, shortcut_text, hidden, category, local) + + # Evaluate the arguments to DEFINE.NAME() + if debug: + print("DEFINE.NAME HANDLER!!") + arg_eval_results = self.evaluate_argument_list(current_cell, "DEFINE.NAME", arguments) + if debug: + print("ARGS!!") + print(arg_eval_results) + + # Set the defined name to the resolved value. + # DEFINE.NAME("HxoCNvuiUvSesa","http://195.123.242.72/IQ2Ytf5113.php",3,"J",TRUE,"Tc",FALSE) + name_pat = r'DEFINE\.NAME\("([^"]*)","([^"]*)"' + name_info = re.findall(name_pat, arg_eval_results.text) + if debug: + print(name_info) + if (len(name_info) > 0): + name, val = name_info[0] + if debug: + print("SET '" + name + "' = '" + val + "'") + self.xlm_wrapper.get_defined_names()[name] = val + + # NOT CORRECT. + return arg_eval_results + def goto_handler(self, arguments, current_cell, interactive, parse_tree_root): + if debug: + print("GOTO HANDLER!!") + print(current_cell) + print(parse_tree_root) next_sheet, next_col, next_row = self.get_cell_addr(current_cell, arguments[0]) next_cell = None if next_sheet is not None and next_sheet in self.xlm_wrapper.get_macrosheets(): @@ -916,21 +1025,45 @@ def goto_handler(self, arguments, current_cell, interactive, parse_tree_root): status = EvalStatus.FullEvaluation else: status = EvalStatus.Error - text = self.convert_ptree_to_str(parse_tree_root) + + # Emulate the cell we are jumping to. + if (next_cell is not None): + + # Parse the contents of the cell we jumped to. + if debug: + print("NEXT CELL!!") + print(next_cell.debug()) + if (next_cell.formula is not None): + parse_tree = self.xlm_parser.parse(next_cell.formula) + func_eval_result = self.evaluate_parse_tree(next_cell, parse_tree, False) + if debug: + print("GOTO EVAL OF " + str(next_cell)) + print(func_eval_result) + + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return_val = 0 return EvalResult(next_cell, status, return_val, text) def halt_handler(self, arguments, current_cell, interactive, parse_tree_root): - return_val = text = self.convert_ptree_to_str(parse_tree_root) - status = EvalStatus.End + return_val = text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) + #status = EvalStatus.End + status = EvalStatus.FullEvaluation self._indent_level -= 1 return EvalResult(None, status, return_val, text) def call_handler(self, arguments, current_cell, interactive, parse_tree_root): + if debug: + print("CALL HANDLER!!") + print(current_cell.debug()) argument_texts = [] status = EvalStatus.FullEvaluation for argument in arguments: + if debug: + print("START ARG EVAL!!" + str(argument)) arg_eval_result = self.evaluate_parse_tree(current_cell, argument, interactive) + if debug: + print("DONE ARG EVAL!!" + str(argument)) + print(arg_eval_result) if arg_eval_result.status != EvalStatus.FullEvaluation: status = arg_eval_result.status argument_texts.append(arg_eval_result.get_text()) @@ -999,7 +1132,7 @@ def char_handler(self, arguments, current_cell, interactive, parse_tree_root): cell.value = text status = EvalStatus.FullEvaluation else: - return_val = text = self.convert_ptree_to_str(parse_tree_root) + return_val = text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) self.char_error_count += 1 status = EvalStatus.Error else: @@ -1021,14 +1154,14 @@ def run_handler(self, arguments, current_cell, interactive, parse_tree_root): text = 'RUN({}!{}{})'.format(next_sheet, next_col, next_row) else: text = 'RUN({}!{}{}, {})'.format(next_sheet, next_col, next_row, - self.convert_ptree_to_str(arguments[1])) + XLMInterpreter.convert_ptree_to_str(arguments[1])) status = EvalStatus.FullEvaluation else: - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) status = EvalStatus.Error return_val = 0 else: - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) status = EvalStatus.Error return EvalResult(next_cell, status, return_val, text) @@ -1043,7 +1176,7 @@ def set_value_handler(self, arguments, current_cell, interactive, parse_tree_roo return self.evaluate_formula(current_cell, 'SET.VALUE', arguments, interactive, destination_arg=2) def error_handler(self, arguments, current_cell, interactive, parse_tree_root): - return EvalResult(None, EvalStatus.FullEvaluation, 0, self.convert_ptree_to_str(parse_tree_root)) + return EvalResult(None, EvalStatus.FullEvaluation, 0, XLMInterpreter.convert_ptree_to_str(parse_tree_root)) def select_handler(self, arguments, current_cell, interactive, parse_tree_root): status = EvalStatus.PartialEvaluation @@ -1061,7 +1194,7 @@ def select_handler(self, arguments, current_cell, interactive, parse_tree_root): self.active_cell = self.get_cell(sheet, col, row) status = EvalStatus.FullEvaluation elif isinstance(arguments[0], Token): - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return_val = 0 elif arguments[0].data == 'range': # e.g., SELECT(D1:D10:D1) @@ -1079,7 +1212,7 @@ def select_handler(self, arguments, current_cell, interactive, parse_tree_root): self.active_cell = self.get_cell(sheet, col, row) status = EvalStatus.FullEvaluation - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return_val = 0 return EvalResult(None, status, return_val, text) @@ -1095,14 +1228,17 @@ def while_handler(self, arguments, current_cell, interactive, parse_tree_root): if condition_eval_result.status == EvalStatus.FullEvaluation: if str(condition_eval_result.value).lower() == 'true': stack_record['status'] = True - text = '{} -> [{}]'.format(self.convert_ptree_to_str(parse_tree_root), + text = '{} -> [{}]'.format(XLMInterpreter.convert_ptree_to_str(parse_tree_root), str(condition_eval_result.value)) if not text: - text = '{}'.format(self.convert_ptree_to_str(parse_tree_root)) + text = '{}'.format(XLMInterpreter.convert_ptree_to_str(parse_tree_root)) self._while_stack.append(stack_record) + current_cell.visit() + if current_cell.visited_too_many_times(): + stack_record['status'] = False if stack_record['status'] == False: self.ignore_processing = True self.next_count = 0 @@ -1113,6 +1249,7 @@ def while_handler(self, arguments, current_cell, interactive, parse_tree_root): def next_handler(self, arguments, current_cell, interactive, parse_tree_root): status = EvalStatus.FullEvaluation + next_cell = None if self.next_count == 0: self.ignore_processing = False next_cell = None @@ -1137,7 +1274,7 @@ def len_handler(self, arguments, current_cell, interactive, parse_tree_root): text = str(return_val) status = EvalStatus.FullEvaluation else: - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return_val = text status = EvalStatus.PartialEvaluation return EvalResult(None, status, return_val, text) @@ -1160,7 +1297,7 @@ def register_handler(self, arguments, current_cell, interactive, parse_tree_root text = self.evaluate_argument_list(current_cell, 'REGISTER', arguments).get_text(unwrap=True) else: status = EvalStatus.Error - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return_val = 0 return EvalResult(None, status, return_val, text) @@ -1171,8 +1308,8 @@ def return_handler(self, arguments, current_cell, interactive, parse_tree_root): return_cell = self._function_call_stack.pop() return_cell.value = arg1_eval_res.value arg1_eval_res.next_cell = self.get_formula_cell(return_cell.sheet, - return_cell.column, - str(int(return_cell.row) + 1)) + return_cell.column, + str(int(return_cell.row) + 1)) if arg1_eval_res.text =='': arg1_eval_res.text = 'RETURN()' @@ -1225,7 +1362,7 @@ def offset_handler(self, arguments, current_cell, interactive, parse_tree_root): status = EvalStatus.FullEvaluation next = self.get_formula_cell(self.xlm_wrapper.get_macrosheets()[cell[0]], col, row) - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return EvalResult(next, status, value, text) @@ -1250,7 +1387,7 @@ def VirtualAlloc_handler(self, arguments, current_cell, interactive, parse_tree_ status = EvalStatus.PartialEvaluation return_val = 0 - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return EvalResult(None, status, return_val, text) def WriteProcessMemory_handler(self, arguments, current_cell, interactive, parse_tree_root): @@ -1282,7 +1419,7 @@ def WriteProcessMemory_handler(self, arguments, current_cell, interactive, parse return_val = 0 if status != EvalStatus.FullEvaluation: - text = self.convert_ptree_to_str(current_cell, parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return_val = 0 return EvalResult(None, status, return_val, text) @@ -1306,8 +1443,8 @@ def RtlCopyMemory_handler(self, arguments, current_cell, interactive, parse_tree mem_data.hex(), size_res.get_text()) - if status == status.PartialEvaluation: - text = self.convert_ptree_to_str(parse_tree_root) + if status == EvalStatus.PartialEvaluation: + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return_val = 0 return EvalResult(None, status, return_val, text) @@ -1328,17 +1465,24 @@ def write_memory(self, base_address, mem_data, size): return result def evaluate_parse_tree(self, current_cell, parse_tree_root, interactive=True): + current_cell.emulated = True next_cell = None status = EvalStatus.NotImplemented text = None return_val = None + if debug: + print("EVALUATE_PARSE_TREE!!") + print(current_cell) + print(parse_tree_root) if type(parse_tree_root) is Token: + if debug: + print("THERE: 1") if parse_tree_root.value in self.defined_names: # this formula has a defined name that can be changed # current formula must be removed from cache self._remove_current_formula_from_cache = True - parse_tree_root.value = self.defined_names[parse_tree_root.value] + parse_tree_root.value = self.defined_names[parse_tree_root.value.lower()] text = parse_tree_root.value status = EvalStatus.FullEvaluation @@ -1346,20 +1490,30 @@ def evaluate_parse_tree(self, current_cell, parse_tree_root, interactive=True): result = EvalResult(next_cell, status, return_val, text) elif type(parse_tree_root) is list: + if debug: + print("THERE: 2") return_val = text = '' status = EvalStatus.FullEvaluation result = EvalResult(next_cell, status, return_val, text) elif parse_tree_root.data == 'function_call': + if debug: + print("THERE: 3") result = self.evaluate_function(current_cell, parse_tree_root, interactive) elif parse_tree_root.data == 'cell': + if debug: + print("THERE: 4") result = self.evaluate_cell(current_cell, interactive, parse_tree_root) elif parse_tree_root.data == 'range': + if debug: + print("THERE: 5") result = self.evaluate_range(current_cell, interactive, parse_tree_root) elif parse_tree_root.data in self._expr_rule_names: + if debug: + print("THERE: 6") text_left = None concat_status = EvalStatus.FullEvaluation for index, child in enumerate(parse_tree_root.children): @@ -1408,7 +1562,7 @@ def evaluate_parse_tree(self, current_cell, parse_tree_root, interactive=True): op_res = self._operators[op_str](value_left, value_right) value_left = op_res else: - value_left = self.convert_ptree_to_str(parse_tree_root) + value_left = XLMInterpreter.convert_ptree_to_str(parse_tree_root) left_arg_eval_res.status = EvalStatus.PartialEvaluation text_left = value_left else: @@ -1423,15 +1577,21 @@ def evaluate_parse_tree(self, current_cell, parse_tree_root, interactive=True): value_left = left_arg_eval_res.value if concat_status == EvalStatus.PartialEvaluation and left_arg_eval_res.status == EvalStatus.FullEvaluation: + if debug: + print("THERE: 7") left_arg_eval_res.status = concat_status result = EvalResult(next_cell, left_arg_eval_res.status, return_val, EvalResult.wrap_str_literal(text_left)) elif parse_tree_root.data == 'final': + if debug: + print("THERE: 8") arg = parse_tree_root.children[1] result = self.evaluate_parse_tree(current_cell, arg, interactive) else: + if debug: + print("THERE: 9") status = EvalStatus.FullEvaluation for child_node in parse_tree_root.children: if child_node is not None: @@ -1445,6 +1605,7 @@ def evaluate_parse_tree(self, current_cell, parse_tree_root, interactive=True): return result def evaluate_cell(self, current_cell, interactive, parse_tree_root): + current_cell.emulated = True sheet_name, col, row = self.get_cell_addr(current_cell, parse_tree_root) return_val = '' text = '' @@ -1483,11 +1644,12 @@ def evaluate_cell(self, current_cell, interactive, parse_tree_root): text = '' status = EvalStatus.FullEvaluation else: - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) return EvalResult(None, status, return_val, text) def evaluate_range(self, current_cell, interactive, parse_tree_root): + current_cell.emulated = True status = EvalStatus.PartialEvaluation if len(parse_tree_root.children) >= 3: start_address = self.get_cell_addr(current_cell, parse_tree_root.children[0]) @@ -1497,7 +1659,7 @@ def evaluate_range(self, current_cell, interactive, parse_tree_root): selected = self.get_cell_addr(current_cell, parse_tree_root.children[4]) self.selected_range = (start_address, end_address, selected) status = EvalStatus.FullEvaluation - text = self.convert_ptree_to_str(parse_tree_root) + text = XLMInterpreter.convert_ptree_to_str(parse_tree_root) retunr_val = 0 return EvalResult(None, status, retunr_val, text) @@ -1556,7 +1718,41 @@ def extract_strings(self, string): result.append(match.string[match.start(0):match.end(0)]) return result - def deobfuscate_macro(self, interactive, start_point="", silent_mode=False): + def do_brute_force_emulation(self, silent_mode=False): + + # Emulate each previously unemulated cell. Just do this on the same sheet as + # the original cells used to start emulation. + for start_point in self.auto_open_labels: + start_point = start_point[1] + sheet_name, _, _ = Cell.parse_cell_addr(start_point) + sheets = self.xlm_wrapper.get_macrosheets() + #print("START BRUTE!!") + #print(sheet_name) + #print(start_point) + if sheet_name in sheets: + sheet = sheets[sheet_name] + #print("\nBRUTE CELLS!!") + for cell_addr in sheet.cells.keys(): + + # Do we need to emulate this cell? + curr_cell = sheet.cells[cell_addr] + if ((curr_cell.formula is None) or (curr_cell.emulated)): + # No, skip it. + continue + + # Yes, we need to emulate this cell. + + # Parse and emulate the cell formula. + #print(sheet.cells[cell_addr].debug()) + parse_tree = None + try: + parse_tree = self.xlm_parser.parse(curr_cell.formula) + except ParseError: + continue + evaluation_result = self.evaluate_parse_tree(curr_cell, parse_tree, interactive=False) + print(evaluation_result) + + def deobfuscate_macro(self, interactive, start_point="", silent_mode=False, brute_force=False): result = [] self.auto_open_labels = self.xlm_wrapper.get_defined_name('auto_open', full_match=False) @@ -1625,10 +1821,10 @@ def deobfuscate_macro(self, interactive, start_point="", silent_mode=False): if evaluation_result.value is not None: current_cell.value = str(evaluation_result.value) if evaluation_result.next_cell is None and \ - (evaluation_result.status == EvalStatus.FullEvaluation or - evaluation_result.status == EvalStatus.PartialEvaluation or - evaluation_result.status == EvalStatus.NotImplemented or - evaluation_result.status == EvalStatus.IGNORED): + (evaluation_result.status == EvalStatus.FullEvaluation or + evaluation_result.status == EvalStatus.PartialEvaluation or + evaluation_result.status == EvalStatus.NotImplemented or + evaluation_result.status == EvalStatus.IGNORED): evaluation_result.next_cell = self.get_formula_cell(current_cell.sheet, current_cell.column, str(int(current_cell.row) + 1)) @@ -1654,12 +1850,28 @@ def deobfuscate_macro(self, interactive, start_point="", silent_mode=False): evaluation_result.get_text(unwrap=False), previous_indent) + if debug: + print("END OF LOOP!!") + print("CURRENT CELL:") + print(current_cell.debug()) if evaluation_result.next_cell is not None: current_cell = evaluation_result.next_cell + if debug: + print("NEXT CELL:") + print(current_cell.debug()) else: + if debug: + print("NEXT CELL:") + print("NO NEXT CELL") break formula = current_cell.formula stack_record = False + + # We are done with the proper emulation loop. Now perform a + # "brute force" emulation of any unemulated cells if needed. + if brute_force: + self.do_brute_force_emulation(silent_mode=silent_mode) + except Exception as exp: exc_type, exc_obj, traceback = sys.exc_info() frame = traceback.tb_frame @@ -1667,6 +1879,8 @@ def deobfuscate_macro(self, interactive, start_point="", silent_mode=False): filename = frame.f_code.co_filename linecache.checkcache(filename) line = linecache.getline(filename, lineno, frame.f_globals) + if debug: + raise exp uprint('Error [{}:{} {}]: {}'.format(os.path.basename(filename), lineno, line.strip(), @@ -1783,11 +1997,17 @@ def convert_to_json_str(file, defined_names, records, memory=None, files=None): md5 = hashlib.md5(file_content).hexdigest() sha256 = hashlib.sha256(file_content).hexdigest() + if defined_names: + for key, val in defined_names.items(): + if isinstance(val, Tree): + defined_names[key]= XLMInterpreter.convert_ptree_to_str(val) + res = {'file_path': file, 'md5_hash': md5, 'sha256_hash': sha256, 'analysis_timestamp': int(time.time()), 'format_version': 1, 'analyzed_by': 'XLMMacroDeobfuscator', 'link': 'https://github.com/DissectMalware/XLMMacroDeobfuscator', 'defined_names': defined_names, 'records': [], 'memory_records': [], 'files':[]} - + res["iocs"] = list(intermediate_iocs) + for index, i in enumerate(records): if len(i) == 4: res['records'].append({'index': index, @@ -1848,6 +2068,8 @@ def try_decrypt(file, password=''): os.remove(tmp_file_path) tmp_file_path = None except Exception as exp: + if debug: + raise exp uprint(str(exp), silent_mode=SILENT) return tmp_file_path, is_encrypted @@ -1907,7 +2129,7 @@ def process_file(**kwargs): uprint('Encrypted {} file'.format(file_type), silent_mode=SILENT) if decrypted_file_path is None: raise Exception( - 'Failed to decrypt the file\nUse --password switch to provide the correct password'.format(file_path)) + 'Failed to decrypt {}\nUse --password switch to provide the correct password'.format(file_path)) file_path = decrypted_file_path else: uprint('Unencrypted {} file\n'.format(file_type), silent_mode=SILENT) @@ -1955,6 +2177,7 @@ def process_file(**kwargs): try: output_file_path = kwargs.get("export_json") + print(res) with open(output_file_path, 'w', encoding='utf_8') as output_file: output_file.write(json.dumps(res, indent=4)) uprint('Result is dumped into {}'.format(output_file_path), silent_mode=SILENT) @@ -2006,7 +2229,7 @@ def process_file(**kwargs): output_format = kwargs.get("output_formula_format", 'CELL:[[CELL-ADDR]], [[STATUS]], [[INT-FORMULA]]') start_point = kwargs.get("start_point", '') - for step in interpreter.deobfuscate_macro(interactive, start_point): + for step in interpreter.deobfuscate_macro(interactive, start_point, brute_force=kwargs["brute"]): if kwargs.get("return_deobfuscated"): deobfuscated.append( get_formula_output(step, output_format, not kwargs.get("no_indent"))) @@ -2030,6 +2253,10 @@ def process_file(**kwargs): interpreter._files[file]['file_content'])) uprint('[END of Deobfuscation]', silent_mode=SILENT) + uprint('\n[Intermediate IOCs]\n', silent_mode=SILENT) + for ioc in intermediate_iocs: + uprint(ioc, silent_mode=SILENT) + uprint('\n', silent_mode=SILENT) if kwargs.get("export_json"): uprint('[Dumping Json]', silent_mode=SILENT) @@ -2087,6 +2314,8 @@ def main(): help="The path of a XLSM file", metavar=('FILE_PATH')) arg_parser.add_argument("-n", "--noninteractive", default=False, action='store_true', help="Disable interactive shell") + arg_parser.add_argument("-b", "--brute", default=False, action='store_true', + help="Brute force emulate any cells not covered by structured emulation") arg_parser.add_argument("-x", "--extract-only", default=False, action='store_true', help="Only extract cells without any emulation") arg_parser.add_argument("-2", "--no-ms-excel", default=False, action='store_true', @@ -2130,6 +2359,8 @@ def main(): try: process_file(**vars(args)) except Exception as exp: + if debug: + raise exp exc_type, exc_obj, traceback = sys.exc_info() frame = traceback.tb_frame lineno = traceback.tb_lineno diff --git a/XLMMacroDeobfuscator/xls_wrapper_2.py b/XLMMacroDeobfuscator/xls_wrapper_2.py index 2b61371..e160736 100644 --- a/XLMMacroDeobfuscator/xls_wrapper_2.py +++ b/XLMMacroDeobfuscator/xls_wrapper_2.py @@ -49,7 +49,8 @@ def get_defined_names(self): else: index = 0 - filtered_name = self.replace_nonprintable_chars(name, replace_char='_').lower() + # filtered_name = self.replace_nonprintable_chars(name, replace_char='_').lower() + filtered_name = name.lower() if name != filtered_name: if filtered_name in self._defined_names: filtered_name = filtered_name + str(index) @@ -59,11 +60,34 @@ def get_defined_names(self): if name in self._defined_names: name = name + str(index) if cells[0].result is not None: - self._defined_names[name] = cells[0].result.text - + cell_location = cells[0].result.text + if '$' in cell_location: + self._defined_names[name] = cells[0].result.text + else: + # By @JohnLaTwC: + # handled mangled cell name as in: + # 8a868633be770dc26525884288c34ba0621170af62f0e18c19b25a17db36726a + # defined name auto_open found at Operand(kind=oREF, value=[Ref3D(coords=(1, 2, 321, 322, 14, 15))], text='sgd7t!\x00\x00\x00\x00\x00\x00\x00\x00') + curr_cell = cells[0].result + if ('auto_open' in name): + coords = curr_cell.value[0].coords + r = int(coords[3]) + c = int(coords[5]) + sheet_name = curr_cell.text.split('!')[0].replace("'", '') + cell_location_xlref = sheet_name + '!' + self.xlref(row = r,column = c, zero_indexed = False) + self._defined_names[name] = cell_location_xlref return self._defined_names + + def xlref(self, row, column, zero_indexed=True): + + if zero_indexed: + row += 1 + column += 1 + return '$'+ Cell.convert_to_column_name(column) + '$'+ str(row) + + def get_defined_name(self, name, full_match=True): result = [] name = name.lower().replace('[', '') @@ -76,6 +100,27 @@ def get_defined_name(self, name, full_match=True): if defined_name.startswith(name): result.append((defined_name, cell_address)) + # By @JohnLaTwC: + # if no matches, try matching 'name' by looking for its characters + # in the same order (ignoring junk chars from UTF16 etc in between. Eg: + # Auto_open: + # match: 'a_u_t_o___o__p____e_n' + # not match:'o_p_e_n_a_u_to__' + # Reference: https://malware.pizza/2020/05/12/evading-av-with-excel-macros-and-biff8-xls/ + # Sample: e23f9f55e10f3f31a2e76a12b174b6741a2fa1f51cf23dbd69cf169d92c56ed5 + if isinstance(result, list) and len(result) == 0: + for defined_name, cell_address in self.get_defined_names().items(): + lastidx = 0 + fMatch = True + for c in name: + idx = defined_name.find(c, lastidx) + if idx == -1: + fMatch = False + lastidx = idx + if fMatch: + result.append((defined_name, cell_address)) + ##print("fMatch for %s in %s is %d:" % (name,defined_name, fMatch)) + return result def load_cells(self, macrosheet, xls_sheet): diff --git a/XLMMacroDeobfuscator/xlsb_wrapper.py b/XLMMacroDeobfuscator/xlsb_wrapper.py index 3c02164..cddbb83 100644 --- a/XLMMacroDeobfuscator/xlsb_wrapper.py +++ b/XLMMacroDeobfuscator/xlsb_wrapper.py @@ -33,11 +33,11 @@ def get_defined_names(self): def get_defined_name(self, name, full_match=True): result = [] if full_match: - if name in self.get_defined_names(): - result.append(self.get_defined_names()[name]) + if name.lower() in self.get_defined_names(): + result.append(self.get_defined_names()[name.lower()]) else: for defined_name, cell_address in self.get_defined_names().items(): - if defined_name.startswith(name): + if defined_name.startswith(name.lower()): result.append(cell_address) return result