def visit(self, item): import statements import expressions import lib_functions if (item in self.visited): return False self.visited.add(item) if (isinstance(item, statements.Call_Statement)): if (not isinstance(item.name, expressions.MemberAccessExpression)): self.called_funcs.add(safe_str_convert(item.name)) if (isinstance(item, expressions.Function_Call)): self.called_funcs.add(safe_str_convert(item.name)) if (isinstance(item, statements.File_Open)): self.called_funcs.add("Open") if (isinstance(item, statements.Print_Statement)): self.called_funcs.add("Print") if (isinstance(item, lib_functions.Chr)): self.called_funcs.add("Chr") if (isinstance(item, lib_functions.Asc)): self.called_funcs.add("Asc") if (isinstance(item, lib_functions.StrReverse)): self.called_funcs.add("StrReverse") if (isinstance(item, lib_functions.Environ)): self.called_funcs.add("Environ") return True
def _handle_shapes_access(r, arg, context, got_constant_math): """Finish handling a partially handled Shapes() access. @param arg (VBA_Object object) The item being evaluated. @param context (Context object) The current program state. @param got_constant_math (boolean) If True the given arg is an all numeric literal expression, if False it is not. @return (??) On success the evaluated item is returned, None is returned on error. """ # Is this a Shapes() access that still needs to be handled? poss_shape_txt = "" if isinstance(r, (VBA_Object, str)): poss_shape_txt = safe_str_convert(r) if ((poss_shape_txt.startswith("Shapes(")) or (poss_shape_txt.startswith("InlineShapes("))): if (log.getEffectiveLevel() == logging.DEBUG): log.debug("eval_arg: Handling intermediate Shapes() access for " + safe_str_convert(r)) r = eval_arg(r, context) if got_constant_math: set_cached_value(arg, r) return r # Not handled. return None
def eval(self, context, params=None): # Exit if an exit function statement was previously called. #if (context.exit_func): # return # Assign all const variables first. do_const_assignments(self.block, context) # Emulate the statements in the block. log.info("Emulating " + safe_str_convert(self) + " ...") context.global_scope = True for curr_statement in self.block: # Don't emulate declared functions. if isinstance(curr_statement, (External_Function, Function, Sub)): if (log.getEffectiveLevel() == logging.DEBUG): log.debug("Skip loose line eval of " + safe_str_convert(curr_statement)) continue # Is this something we can emulate? if (not isinstance(curr_statement, VBA_Object)): continue curr_statement.eval(context, params=params) # Was there an error that will make us jump to an error handler? if (context.must_handle_error()): break context.clear_error() # Run the error handler if we have one and we broke out of the statement # loop with an error. context.handle_error(params)
def _fix_sheet_name(sheet_name): """Replace characters given as hex with the actual character values. @param sheet_name (str) The name of the sheet to fix. @return (str) The given name with 0xNN substrings replaced with chr(0xNN). """ # Get the characters given as hex strings in the name. pat = r"(0x[0-9a-f]{2})" r = safe_str_convert(sheet_name) hex_strs = re.findall(pat, r) if (len(hex_strs) == 0): return sheet_name # Replace them with the actual values. for hex_val in hex_strs: try: chr_val = int(hex_val, 16) r = r.replace(hex_val, chr(chr_val)) except Exception as e: log.error("Fixing sheet named failed. " + safe_str_convert(e)) return r
def dump_actions(self): """Return a table of all actions recorded by trace(), as a prettytable object that can be printed or reused. @return (PrettyTable object) The actions performed during emulation saved as a PrettyTable object. """ t = prettytable.PrettyTable(('Action', 'Parameters', 'Description')) t.align = 'l' t.max_width['Action'] = 20 t.max_width['Parameters'] = 45 t.max_width['Description'] = 35 for action in self.actions: # Cut insanely large results down to size. str_action = safe_str_convert(action) if (len(str_action) > 50000): new_params = safe_str_convert(action[1]) if (len(new_params) > 50000): new_params = new_params[: 25000] + "... <SNIP> ..." + new_params[ -25000:] action = (action[0], new_params, action[2]) t.add_row(action) return t
def visit(self, item): from expressions import SimpleNameExpression from expressions import MemberAccessExpression # Already looked at this? if (item in self.visited): return False self.visited.add(item) # Simple variable? if (isinstance(item, SimpleNameExpression)): self.variables.add(safe_str_convert(item.name)) # Array access? if (("Function_Call" in safe_str_convert(type(item))) and (self.context is not None)): # Is this an array or function? if (hasattr(item, "name") and (self.context.contains(item.name))): ref = self.context.get(item.name) if isinstance(ref, (list, str)): self.variables.add(safe_str_convert(item.name)) # Member access expression used as a variable? if (isinstance(item, MemberAccessExpression)): rhs = item.rhs if (isinstance(rhs, list)): rhs = rhs[-1] if (isinstance(rhs, SimpleNameExpression)): self.variables.add(safe_str_convert(item)) return True
def eval(self, context, params=None): # Perform all of the const assignments first. for block in self.loose_lines: if isinstance(block, (External_Function, Function, Sub)): if (log.getEffectiveLevel() == logging.DEBUG): log.debug("Skip loose line const eval of " + safe_str_convert(block)) continue if (isinstance(block, LooseLines)): context.global_scope = True do_const_assignments(block.block, context) context.global_scope = False # Emulate the loose line blocks (statements that appear outside sub/func # defs) in order. done_emulation = False for block in self.loose_lines: if isinstance(block, (External_Function, Function, Sub)): if (log.getEffectiveLevel() == logging.DEBUG): log.debug("Skip loose line eval of " + safe_str_convert(block)) continue context.global_scope = True block.eval(context, params) context.global_scope = False done_emulation = True # Return if we ran anything. return done_emulation
def _read_from_excel(arg, context): """Try to evaluate an argument by reading from the loaded Excel spreadsheet. @param arg (VBA_Object object) The argument to evaluate. @param context (Context object) The current program state. @return (any) The result of the evaluation on success, None on failure. """ # Try handling reading value from an Excel spreadsheet cell. # ThisWorkbook.Sheets('YHRPN').Range('J106').Value if ("MemberAccessExpression" not in safe_str_convert(type(arg))): return None arg_str = safe_str_convert(arg) if (("sheets(" in arg_str.lower()) and (("range(" in arg_str.lower()) or ("cells(" in arg_str.lower()))): if (log.getEffectiveLevel() == logging.DEBUG): log.debug("Try as Excel cell read...") return arg.eval(context) # Not handled. return None
def eval(self, context, params=None): # The wildcard for matching propagates through operations. evaluated_args = eval_args(self.arg, context) if ((isinstance(evaluated_args, Iterable)) and ("**MATCH ANY**" in evaluated_args)): return "**MATCH ANY**" # return the floor division of all the arguments: try: if (log.getEffectiveLevel() == logging.DEBUG): log.debug("Compute floor div " + safe_str_convert(self.arg)) return reduce( lambda x, y: x // y, vba_conversion.coerce_args(evaluated_args, preferred_type="int")) except (TypeError, ValueError): # Try converting strings to ints. # TODO: Need to handle floats in strings. try: return reduce(lambda x, y: int(x) // int(y), evaluated_args) except Exception as e: if (safe_str_convert(e).strip() != "division by zero"): log.error( 'Impossible to divide arguments of different types. ' + safe_str_convert(e)) # TODO return 0 except ZeroDivisionError as e: context.set_error(safe_str_convert(e))
def set_cached_value(arg, val): """Set the cached value of an all constant numeric expression. @param arg (VBA_Object object) The unresolved expression to cache. @param val (int, float, complex) The value of the resolved expression. """ # We should be setting this to a numeric expression if ((not isinstance(val, int)) and (not isinstance(val, float)) and (not isinstance(val, complex))): if (log.getEffectiveLevel() == logging.DEBUG): log.warning("Expression '" + safe_str_convert(val) + "' is a " + safe_str_convert(type(val)) + ", not an int. Not caching.") return # Don't cache things that contain Excel sheets or workbooks. if contains_excel(arg): return # We have a number. Cache it. arg_str = safe_str_convert(arg) try: if (log.getEffectiveLevel() == logging.DEBUG): log.debug("Cache value of " + arg_str + " = " + safe_str_convert(val)) except UnicodeEncodeError: pass constant_expr_cache[arg_str] = val
def _handle_wscriptshell_run(arg, context, got_constant_math): """Handle cases where wscriptshell.run() is being called and there is a local run() function. @param arg (VBA_Object object) The item being evaluated. @param context (Context object) The current program state. @param got_constant_math (boolean) If True the given arg is an all numeric literal expression, if False it is not. @return (??) On success the evaluated item is returned, None is returned on error. """ # Handle cases where wscriptshell.run() is being called and there is a local run() function. if ((".run(" in safe_str_convert(arg).lower()) and (context.contains("run"))): # Resolve the run() call. if ("MemberAccessExpression" in safe_str_convert(type(arg))): arg_evaled = arg.eval(context) if got_constant_math: set_cached_value(arg, arg_evaled) return arg_evaled # Not handled. return None
def eval(self, context, params=None): # The wildcard for matching propagates through operations. evaluated_args = eval_args(self.arg, context) if ((isinstance(evaluated_args, Iterable)) and ("**MATCH ANY**" in evaluated_args)): return "**MATCH ANY**" # return the exponentiation of all the arguments: try: if (log.getEffectiveLevel() == logging.DEBUG): log.debug("Compute pow " + safe_str_convert(self.arg)) return reduce( lambda x, y: pow(x, y), vba_conversion.coerce_args(evaluated_args, preferred_type="int")) except (TypeError, ValueError): # Try converting strings to ints. # TODO: Need to handle floats in strings. try: return reduce(lambda x, y: pow(int(x), int(y)), evaluated_args) except Exception as e: log.error( 'Impossible to do exponentiation with arguments of different types. ' + safe_str_convert(e)) return 0
def eval(self, context, params=None): # The wildcard for matching propagates through operations. evaluated_args = eval_args(self.arg, context) if ((isinstance(evaluated_args, Iterable)) and ("**MATCH ANY**" in evaluated_args)): return "**MATCH ANY**" # return the xor of all the arguments: try: if (log.getEffectiveLevel() == logging.DEBUG): log.debug("Compute xor " + safe_str_convert(self.arg)) return reduce( lambda x, y: x ^ y, vba_conversion.coerce_args(evaluated_args, preferred_type="int")) except (TypeError, ValueError): # Try converting strings to ints. # TODO: Need to handle floats in strings. try: return reduce(lambda x, y: int(x) ^ int(y), evaluated_args) except Exception as e: log.error( 'Impossible to xor arguments of different types. Arg list = ' + safe_str_convert(self.arg) + ". " + safe_str_convert(e)) return 0 except RuntimeError as e: log.error("overflow trying eval xor: %r" % self.arg) raise e
def eval(self, context, params=None): # We are emulating some string or numeric # expression. Therefore any boolean operators we find in the # expression are actually bitwise operators. # Track that in the context. set_flag = False if (not context.in_bitwise_expression): context.in_bitwise_expression = True set_flag = True # The wildcard for matching propagates through operations. evaluated_args = eval_args(self.arg, context) if ((isinstance(evaluated_args, Iterable)) and ("**MATCH ANY**" in evaluated_args)): if set_flag: context.in_bitwise_expression = False return "**MATCH ANY**" try: args = vba_conversion.coerce_args(evaluated_args) ret = args[0] for op, arg in zip(self.operators, args[1:]): try: ret = self.operator_map[op](ret, arg) except OverflowError: log.error("overflow trying eval: %r" % safe_str_convert(self)) if set_flag: context.in_bitwise_expression = False return ret except (TypeError, ValueError): # Try converting strings to numbers. # TODO: Need to handle floats in strings. try: args = map(vba_conversion.coerce_to_num, evaluated_args) ret = args[0] for op, arg in zip(self.operators, args[1:]): ret = self.operator_map[op](ret, arg) if set_flag: context.in_bitwise_expression = False return ret except ZeroDivisionError: context.set_error("Division by 0 error. Returning 'NULL'.") if set_flag: context.in_bitwise_expression = False return 'NULL' except Exception as e: log.error( 'Impossible to operate on arguments of different types. ' + safe_str_convert(e)) if set_flag: context.in_bitwise_expression = False return 0 except ZeroDivisionError: context.set_error("Division by 0 error. Returning 'NULL'.") if set_flag: context.in_bitwise_expression = False return 'NULL'
def coerce_to_str(obj, zero_is_null=False): """Coerce a VBA object (integer, Null, etc) to a string. @param obj (VBA_Object object) The VBA object to convert to a string. @param zero_is_null (boolean) If True treat integer 0 as a zero length string, if False just convert 0 to '0'. @return (str) The given VBA object as a string. """ # in VBA, Null/None is equivalent to an empty string if ((obj is None) or (obj == "NULL")): return '' # 0 can be a NULL also. if (zero_is_null and (obj == 0)): return '' # Not NULL. We have data. # Easy case. Is this already some sort of a string? if (isinstance(obj, basestring)): # Convert to a regular str if needed. return safe_str_convert(obj) # Do we have a list of byte values? If so convert the bytes to chars. if (isinstance(obj, list)): r = "" bad = False for c in obj: # Skip null bytes. if (c == 0): continue try: r += chr(c) except (TypeError, ValueError): # Invalid character value. Don't do string # conversion of array. bad = True break # Return the byte array as a string if it makes sense. if (not bad): return r # Is this an Excel cell dict? if (isinstance(obj, dict) and ("value" in obj)): # Return the value as a string. return (coerce_to_str(obj["value"])) # Not a character byte array. Just convert to a string. return safe_str_convert(obj)
def add_compiled_module(self, m, stream): """Add an already parsed and processed module. @param m (Module object) The parsed object. @param stream (str) The OLE stream name containing the module. """ if (m is None): return self.modules.append(m) for name, _sub in m.subs.items(): # Append the stream name for duplicate subs if (name in self.globals): new_name = safe_str_convert(stream) + "::" + safe_str_convert( name) log.warn("Renaming duplicate function " + name + " to " + new_name) name = new_name # Save the sub. if (log.getEffectiveLevel() == logging.DEBUG): log.debug('(1) storing sub "%s" in globals' % name) self.globals[name.lower()] = _sub self.globals[name] = _sub # Functions. for name, _function in m.functions.items(): if (log.getEffectiveLevel() == logging.DEBUG): log.debug('(1) storing function "%s" in globals' % name) self.globals[name.lower()] = _function self.globals[name] = _function # Properties. for name, _prop in m.functions.items(): if (log.getEffectiveLevel() == logging.DEBUG): log.debug('(1) storing property let "%s" in globals' % name) self.globals[name.lower()] = _prop self.globals[name] = _prop # External DLL functions. for name, _function in m.external_functions.items(): if (log.getEffectiveLevel() == logging.DEBUG): log.debug('(1) storing external function "%s" in globals' % name) self.globals[name.lower()] = _function self.externals[name.lower()] = _function # Global variables. for name, _var in m.global_vars.items(): if (log.getEffectiveLevel() == logging.DEBUG): log.debug('(1) storing global var "%s" = %s in globals (1)' % (name, safe_str_convert(_var))) if (isinstance(name, str)): self.globals[name.lower()] = _var if (isinstance(name, list)): self.globals[name[0].lower()] = _var self.types[name[0].lower()] = name[1]
def _called_funcs_to_python(loop, context, indent): """Convert all the functions called in the given loop to Python JIT code. @param loop (VBA_Object object) The loop for which to generate Python JIT code. @param context (Context object) The current program state. @param indent (int) The number of spaces to indent the generated Python code. @return (str) Python JIT code. """ # Get the definitions for all local functions called directly in the loop. local_funcs = _get_all_called_funcs(loop, context) local_func_hashes = set() for curr_func in local_funcs: curr_func_hash = hashlib.md5(safe_str_convert(curr_func).encode()).hexdigest() local_func_hashes.add(curr_func_hash) # Now get the definitions of all the local functions called by the local # functions. seen_funcs = set() funcs_to_handle = list(local_funcs) while (len(funcs_to_handle) > 0): # Get the current function definition to check for calls. curr_func = funcs_to_handle.pop() curr_func_hash = hashlib.md5(safe_str_convert(curr_func).encode()).hexdigest() # Already looked at this one? if (curr_func_hash in seen_funcs): continue seen_funcs.add(curr_func_hash) # Get the functions called in the current function. curr_local_funcs = _get_all_called_funcs(curr_func, context) # Save the new functions for processing. for new_func in curr_local_funcs: new_func_hash = hashlib.md5(safe_str_convert(new_func).encode()).hexdigest() if (new_func_hash not in local_func_hashes): local_func_hashes.add(new_func_hash) local_funcs.append(new_func) funcs_to_handle.append(new_func) # Convert each local function to Python. r = "" for local_func in local_funcs: r += to_python(local_func, context, indent=indent) + "\n" # Done. indent_str = " " * indent r = indent_str + "# VBA Local Function Definitions\n" + r return r
def visit(self, item): if (item in self.visited): return False self.visited.add(item) if (isinstance(item, Dim_Statement)): for name, _, _, _ in item.variables: self.variables.add(safe_str_convert(name)) if (isinstance(item, Let_Statement)): self.variables.add(safe_str_convert(item.name)) return True
def visit(self, item): if (item in self.visited): return False self.visited.add(item) if (isinstance(item, statements.External_Function)): self.funcs[safe_str_convert(item.name)] = safe_str_convert( item.alias_name) self.names.add(safe_str_convert(item.alias_name)) self.aliases.add(safe_str_convert(item.name)) return True
def visit(self, item): if (safe_str_convert(item) in self.visited): return False self.visited.add(safe_str_convert(item)) if ("Let_Statement" in safe_str_convert(type(item))): if (isinstance(item.name, str)): self.variables.add(item.name) elif (isinstance(item.name, pyparsing.ParseResults) and (item.name[0].lower().replace("$", "").replace("#", "").replace("%", "") == "mid")): self.variables.add(safe_str_convert(item.name[1])) return True
def _updated_vars_to_python(loop, context, indent): """Generate Python JIT code for saving the variables updated in a loop in Python. These updates are saved in the Python var_updates variable. @param loop (VBA_Object object) The loop for which to generate Python JIT code. @param context (Context object) The current program state. @param indent (int) The number of spaces to indent the generated Python code. @return (str) Python JIT code. """ import statements indent_str = " " * indent lhs_visitor = lhs_var_visitor() loop.accept(lhs_visitor) lhs_var_names = lhs_visitor.variables # Handle With variables if needed. if (context.with_prefix_raw is not None): lhs_var_names.add(safe_str_convert(context.with_prefix_raw)) # Handle For loop index variables if needed. if (isinstance(loop, statements.For_Statement)): lhs_var_names.add(safe_str_convert(loop.name)) var_dict_str = "{" first = True for var in lhs_var_names: py_var = utils.fix_python_overlap(var) if (not first): var_dict_str += ", " first = False var = var.replace(".", "") var_dict_str += '"' + var + '" : ' + py_var var_dict_str += "}" save_vals = indent_str + "try:\n" save_vals += indent_str + " " * 4 + "var_updates\n" save_vals += indent_str + " " * 4 + "var_updates.update(" + var_dict_str + ")\n" save_vals += indent_str + "except (NameError, UnboundLocalError):\n" save_vals += indent_str + " " * 4 + "var_updates = " + var_dict_str + "\n" save_vals += indent_str + 'var_updates["__shell_code__"] = core.vba_library.get_raw_shellcode_data()\n' save_vals = indent_str + "# Save the updated variables for reading into ViperMonkey.\n" + save_vals if (log.getEffectiveLevel() == logging.DEBUG): save_vals += indent_str + "print \"UPDATED VALS!!\"\n" save_vals += indent_str + "print var_updates\n" return save_vals
def load_excel_xlrd(data): """Read in an Excel file into an ExceBook object directly with the xlrd Excel library. @param data (str) The Excel file contents. @return (core.excel.ExceBook object) On success return the Excel spreadsheet as an ExcelBook object. Returns None on error. """ # Only use this on Office 97 Excel files. if (not filetype.is_office97_file(data, True)): log.warning("File is not an Excel 97 file. Not reading with xlrd2.") return None # It is Office 97. See if we can read it with xlrd2. try: if (log.getEffectiveLevel() == logging.DEBUG): log.debug("Trying to load with xlrd...") r = xlrd.open_workbook(file_contents=data) return r except Exception as e: log.error("Reading in file as Excel with xlrd failed. " + safe_str_convert(e)) return None
def get_cached_value(arg): """Get the cached value of an all constant numeric expression if we have it. @param arg (VBA_Object object) The argument to check. @return (int or VBA_Object) The cached value of the all constant numeric expression if it is in the cache, the original given argument if not. """ # Don't do any more work if this is already a resolved value. if isinstance(arg, (dict, int)): return arg # If it is something that may be hard to convert to a string, no cached value. if contains_excel(arg): return None # This is not already resolved to an int. See if we computed this before. arg_str = safe_str_convert(arg) if (arg_str not in constant_expr_cache.keys()): return None return constant_expr_cache[arg_str]
def full_str(self): """Full string representation of the object. @return (str) Object as a string. """ return safe_str_convert(self)
def cell(self, row, col): """Get a cell from the sheet. @param row (int) The cell's row index. @param col (int) The cell's column index. @return (str) The cell value if the cell is found. @throws KeyError This is thrown if the cell is not found. """ if ((row, col) in self.cells): return self.cells[(row, col)] raise KeyError("Cell (" + safe_str_convert(row) + ", " + safe_str_convert(col) + ") not found.")
def __repr__(self): """Full string representation of the object. @return (str) Object as a string. """ raise NotImplementedError("__repr__() not implemented in " + safe_str_convert(type(self)))
def eval(self, context, params=None): # The wildcard for matching propagates through operations. evaluated_args = eval_args(self.arg, context) if ((isinstance(evaluated_args, Iterable)) and ("**MATCH ANY**" in evaluated_args)): return "**MATCH ANY**" # return the subtraction of all the arguments: try: if (log.getEffectiveLevel() == logging.DEBUG): log.debug("Compute subract " + safe_str_convert(self.arg)) return reduce( lambda x, y: x - y, vba_conversion.coerce_args(evaluated_args, preferred_type="int")) except (TypeError, ValueError): # Try converting strings to ints. # TODO: Need to handle floats in strings. try: return reduce( lambda x, y: vba_conversion.coerce_to_int(x) - vba_conversion.coerce_to_int(y), evaluated_args) except Exception as e: # Are we doing math on character ordinals? l1 = [] orig = evaluated_args for v in orig: if (isinstance(v, int)): l1.append(v) continue if (isinstance(v, str) and (len(v) == 1)): l1.append(ord(v)) continue # Do we have something that we can do math on? if (len(orig) != len(l1)): log.error( 'Impossible to subtract arguments of different types. ' + safe_str_convert(e)) return 0 # Try subtracting based on character ordinals. return reduce(lambda x, y: int(x) - int(y), l1)
def _pull_cells_sheet_internal(sheet, strip_empty): """Pull all the cells from an ExcelSheet object defined internally in excel.py. @param sheet (ExcelSheet object) The ExcelSheet sheet from which to pull cells. @param strip_empty (boolean) If True do not report cells with empty values, if False return all cells. @return (list) A list of cells from the sheet represented as a dict. Each cell dict is of the form { "value" : cell value, "row" : row index, "col" : column index, "index" : AB123 form of cell index } """ # We are going to use the internal cells field to build the list of all # cells, so this will only work with the ExcelSheet class defined in excel.py. if (not hasattr(sheet, "cells")): # This is not an internal sheet object. return None # Cycle row by row through the sheet, tracking all the cells. # Find the max row and column for the cells. max_row = -1 max_col = -1 for cell_index in sheet.cells.keys(): curr_row = cell_index[0] curr_col = cell_index[1] if (curr_row > max_row): max_row = curr_row if (curr_col > max_col): max_col = curr_col # Cycle through all the cells in order. curr_cells = [] for curr_row in range(0, max_row + 1): for curr_col in range(0, max_col + 1): try: curr_val = sheet.cell(curr_row, curr_col) if (strip_empty and (len(safe_str_convert(curr_val).strip()) == 0)): continue curr_cell = { "value": curr_val, "row": curr_row + 1, "col": curr_col + 1, "index": _get_alphanum_cell_index(curr_row, curr_col) } curr_cells.append(curr_cell) except KeyError: pass # Return the cells. return curr_cells
def __repr__(self): """String value of sheet. """ if (self.gloss is not None): return self.gloss log.info("Converting Excel sheet to str ...") r = "" if debug: r += "Sheet: '" + self.name + "'\n\n" for cell in self.cells.keys(): r += safe_str_convert(cell) + "\t=\t'" + safe_str_convert( self.cells[cell]) + "'\n" else: r += "Sheet: '" + self.name + "'\n" r += safe_str_convert(self.cells) self.gloss = r return self.gloss
def __repr__(self): """String version of workbook. """ log.info("Converting Excel workbook to str ...") r = "" for sheet in self.sheets: r += safe_str_convert(sheet) + "\n" return r