Oletools

This is an automation to run oletools malware analysis for office files. Oletools is a tool for analyzing Microsoft OLE2 files, such as Microsoft Office documents or Outlook messages, mainly for malware analysis, forensics, and debugging. This automation allows performing some basic oletools commands from Cortex XSOAR. Note that oletools is open source code and is subject to change.

python · Oletools

Details

IDOletools
Languagepython
From Version6.5.0
Docker Imagedemisto/parse-emails:1.0.0.3555598

README

This is an automation to run oletools malware analysis for office files. Oletools is a tool
for analyzing Microsoft OLE2 files,
such as Microsoft Office documents or Outlook messages, mainly for malware analysis,
forensics, and debugging.
Note that oletools is open source code and is subject to change.

Script Data


Name Description
Script Type python3
Cortex XSOAR Version 6.5.0

Inputs


Argument Name Description
ole_command The OLE command to activate.
- oleid - to analyze OLE files to detect specific characteristics usually found in malicious files.
- oleobj - to extract embedded objects from OLE files.
- olevba- to extract and analyze VBA Macro source code from MS Office documents (OLE and OpenXML).
entryID The file to activate the oletools analysis on.
decode Display all the obfuscated strings with their decoded content (Hex, Base64, StrReverse, Dridex, VBA). Note that this works only with the olevba command
password If encrypted office files are encountered, try decryption with this password. May be repeated.

Outputs


Path Description Type
Oletools.Oleid.ole_command_result Indicator list from the oleid command. Unknown
Oletools.Oleid.file_name File name Unknown
Oletools.Oleid.sha256 SHA256 hash. Unknown
Oletools.Oleid.ole_command_result.File_format Indicator file format. Unknown
Oletools.Oleid.ole_command_result.File_format.Value Indicator file format value. Unknown
Oletools.Oleid.ole_command_result.File_format.Ole_Risk Indicator file format OLE risk. Unknown
Oletools.Oleid.ole_command_result.File_format.Description Indicator file format description. Unknown
Oletools.Oleid.ole_command_result.Container_format Indicator container format. Unknown
Oletools.Oleid.ole_command_result.Container_format.Value Indicator container format value Unknown
Oletools.Oleid.ole_command_result.Container_format.Ole_Risk Indicator container format OLE risk. Unknown
Oletools.Oleid.ole_command_result.Container_format.Description Indicator container format description. Unknown
Oletools.Oleid.ole_command_result.Encrypted Indicator encrypted. Unknown
Oletools.Oleid.ole_command_result.Encrypted.Value Indicator encrypted value. Unknown
Oletools.Oleid.ole_command_result.Encrypted.Ole_Risk Indicator encrypted OLE risk. Unknown
Oletools.Oleid.ole_command_result.Encrypted.Description Indicator encrypted description. Unknown
Oletools.Oleid.ole_command_result.VBA_Macros Indicator VBA macros. Unknown
Oletools.Oleid.ole_command_result.VBA_Macros.Value Indicator VBA macros value. Unknown
Oletools.Oleid.ole_command_result.VBA_Macros.Ole_Risk Indicator VBA macros OLE risk. Unknown
Oletools.Oleid.ole_command_result.VBA_Macros.Description Indicator VBA macros description. Unknown
Oletools.Oleid.ole_command_result.XLM_Macros Indicator XLM macros. Unknown
Oletools.Oleid.ole_command_result.XLM_Macros.Value Indicator XLM macros value. Unknown
Oletools.Oleid.ole_command_result.XLM_Macros.Ole_Risk Indicator XLM macros OLE risk. Unknown
Oletools.Oleid.ole_command_result.XLM_Macros.Description Indicator XLM macros description. Unknown
Oletools.Oleid.ole_command_result.External_Relationships Indicator external relationships. Unknown
Oletools.Oleid.ole_command_result.External_Relationships.Value Indicator XLM macros value. Unknown
Oletools.Oleid.ole_command_result.External_Relationships.Ole_Risk Indicator XLM macros OLE risk. Unknown
Oletools.Oleid.ole_command_result.External_Relationships.Description Indicator XLM macros description. Unknown
Oletools.Oleid.ole_command_result.ObjectPool Indicator object pool. Unknown
Oletools.Oleid.ole_command_result.ObjectPool.Value Indicator object pool value. Unknown
Oletools.Oleid.ole_command_result.ObjectPool.Ole_Risk Indicator object pool OLE risk. Unknown
Oletools.Oleid.ole_command_result.ObjectPool.Description Indicator object pool description. Unknown
Oletools.Oleid.ole_command_result.Flash_objects Indicator flash objects. Unknown
Oletools.Oleid.ole_command_result.Flash_objects.Value Indicator flash objects value. Unknown
Oletools.Oleid.ole_command_result.Flash_objects.Ole_Risk Indicator flash objects OLE risk. Unknown
Oletools.Oleid.ole_command_result.Flash_objects.Description Indicator flash objects description. Unknown
Oletools.Oleobj.ole_command_result.hyperlinks List of hyperlinks. Unknown
Oletools.Oleobj.file_name File name. Unknown
Oletools.Oleobj.sha256 SHA256 hash. Unknown
Oletools.Olevba.file_name File name. Unknown
Oletools.Olevba.sha256 SHA256 hash. Unknown
Oletools.Olevba.ole_command_result.macro_analyze Macro analyze. Unknown
Oletools.Olevba.ole_command_result.macro_src_code Macro source code. Unknown
Oletools.Olevba.ole_command_result.macro_list Macro list. Unknown
import hashlib
import logging
import os
import subprocess

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401


class CustomHandler(logging.Handler):
    def __init__(self):
        super().__init__()
        self.last_log_msg = None

    def emit(self, record):
        self.last_log_msg = record.msg

    def get_last_log_msg(self):
        return self.last_log_msg


custom_handler = CustomHandler()
root_logger = logging.getLogger()
root_logger.addHandler(custom_handler)
root_logger.setLevel(logging.DEBUG)

# should be imported after adding log handler to the root logger
from oletools import crypto, oleid  # noqa: E402
from oletools.olevba import VBA_Parser  # noqa: E402


class OleClient:
    def __init__(self, file_info, ole_command, password=None, decoded=False):
        self.name = file_info["name"]
        self.file_path = file_info["path"]
        self.password = password
        self.show_decoded = decoded
        self.decrypted_file_path = None
        self.processed_file_path = file_info["path"]
        self.ole_command = ole_command
        self.hash = None

    def __del__(self):
        try:
            if self.password and self.decrypted_file_path:
                os.unlink(self.decrypted_file_path)
        except Exception:  # e.g. file does not exist or is None
            pass

    def decryption(self):
        if crypto.is_encrypted(self.file_path) and self.password:
            try:
                passwords = [self.password] + crypto.DEFAULT_PASSWORDS
                self.decrypted_file_path = crypto.decrypt(self.file_path, passwords)
                if not self.decrypted_file_path:
                    raise crypto.WrongEncryptionPassword(self.file_path)
            except Exception as e:
                raise DemistoException(f"The file decryption failed with the following message:\n {e}")

    @staticmethod
    def calc_hash(file_path: str):
        with open(file_path, "rb") as f:
            b = f.read()  # read entire file as bytes
            return hashlib.sha256(b).hexdigest()

    def run(self):
        self.decryption()

        if self.decrypted_file_path:
            self.processed_file_path = self.decrypted_file_path

        # calculate the file hash
        self.hash = self.calc_hash(self.processed_file_path)

        if self.ole_command == "oleid":
            cr = self.oleid()
        elif self.ole_command == "oleobj":
            cr = self.oleobj()
        elif self.ole_command == "olevba":
            cr = self.olevba()
        else:
            raise NotImplementedError(f'Command "{self.ole_command}" is not implemented.')

        self.wrap_command_result(cr)
        return cr

    def wrap_command_result(self, cr: CommandResults):
        cr.outputs = {"sha256": self.hash, "file_name": self.name, "ole_command_result": cr.outputs}
        cr.outputs_key_field = "sha256"

    @staticmethod
    def replace_space_with_underscore(indicator: str):
        return indicator.replace(" ", "_")

    def oleid(self):
        oid = oleid.OleID(self.processed_file_path)
        indicators = oid.check()
        indicators_list = []
        dbot_score = None
        indicators_dict = {}
        for i in indicators:
            indicators_list.append(
                {"Indicator": str(i.name), "Value": str(i.value), "Ole Risk": str(i.risk), "Description": str(i.description)}
            )

            if str(i.name):
                indicators_dict[self.replace_space_with_underscore(str(i.name))] = {
                    "Value": str(i.value),
                    "Ole_Risk": str(i.risk),
                    "Description": str(i.description),
                }

            if str(i.name) == "VBA Macros" and str(i.risk) == "HIGH":
                dbot_score = Common.DBotScore(self.hash, DBotScoreType.FILE, "Oletools", Common.DBotScore.BAD)

        indicator = Common.File(dbot_score, sha256=self.hash) if dbot_score else None
        cr = CommandResults(
            readable_output=tableToMarkdown(self.name, indicators_list, headers=["Indicator", "Value", "Ole Risk", "Description"])
            + f"\n file hash: {self.hash}",
            outputs=indicators_dict,
            outputs_prefix="Oletools.Oleid",
            indicator=indicator,
        )
        return cr

    def oleobj(self):
        import re

        args = []
        command = "oleobj"
        file = self.processed_file_path
        args.append(command)
        args.append(file)

        output = subprocess.run(args, capture_output=True)

        regex = r"Found relationship 'hyperlink' with external link (.*?)\n"
        str_output = output.stdout.decode("utf-8")
        matches = re.findall(regex, str_output, re.MULTILINE)
        readable_md = '### Found the following relationship "hyperlink" with external links\n'
        hyperlink_list = []

        if not matches:
            readable_md = '### No "hyperlink" with external links were found'
        else:
            for match in matches:
                readable_md += f"- {match}\n"
                hyperlink_list.append(match)

        cr = CommandResults(
            readable_output=readable_md,
            outputs_prefix="Oletools.Oleobj",
            outputs={"hyperlinks": hyperlink_list},
            raw_response=str_output,
        )
        return cr

    def olevba(self):
        file_data = open(self.processed_file_path, "rb").read()
        vbaparser = VBA_Parser(self.processed_file_path, data=file_data, disable_pcode=True)

        if not vbaparser.detect_vba_macros():
            return CommandResults(readable_output="### No VBA Macros found\n")

        found = "### VBA Macros found\n"
        all_macros = vbaparser.extract_all_macros()
        macros_list = []

        for macro in all_macros:
            macros_list.append({"VBA Macro": macro[2], "Found in file": macro[0], "Ole stream": macro[1]})

        macros_list_md = tableToMarkdown("Macros found", macros_list, headers=["VBA Macro", "Found in file", "Ole stream"])

        macro_source_code = vbaparser.reveal()
        readable_macro = f"\n### Macro source code\n {macro_source_code}\n"

        results = vbaparser.analyze_macros(show_decoded_strings=self.show_decoded)
        results_list = []
        for result in results:
            results_list.append({"Type": result[0], "Keyword": result[1], "Description": result[2]})

        results_md = tableToMarkdown("Macro Analyze", results_list, headers=["Type", "Keyword", "Description"])
        vbaparser.close()

        readable_output = found + macros_list_md + readable_macro + results_md
        outputs = {"macro_list": macros_list, "macro_src_code": macro_source_code, "macro_analyze": results_list}
        cr = CommandResults(readable_output=readable_output, outputs_prefix="Oletools.Olevba", outputs=outputs)
        return cr


def handle_password(non_secret_password: str, password: str) -> str:
    if non_secret_password and not password:
        return non_secret_password
    elif password and non_secret_password:
        raise ValueError("Please insert a password or a non_secret_password not both")
    return password


def main():  # pragma: no cover
    args = demisto.args()
    ole_command = args.get("ole_command")
    attach_id = args.get("entryID", "")
    file_info = demisto.getFilePath(attach_id)
    show_decoded = argToBoolean(args.get("decode", False))
    password = args.get("password", "")
    non_secret_password = args.get("non_secret_password", "")

    try:
        password = handle_password(password=password, non_secret_password=non_secret_password)
        ole_client = OleClient(file_info, ole_command, password=password, decoded=show_decoded)
        return_results(ole_client.run())
    except Exception as e:
        return_error(
            f"The script failed with the following error:\n {e}\n Logs form oletools:\n {custom_handler.get_last_log_msg()}"
        )


if __name__ in ("__builtin__", "builtins", "__main__"):
    main()