Exemple #1
0
class VirusTotalDynamic(ServiceBase):
    SERVICE_CATEGORY = "External"
    SERVICE_DESCRIPTION = "This service submits files/URLs to VirusTotal for analysis."
    SERVICE_ENABLED = False
    SERVICE_REVISION = ServiceBase.parse_revision('$Id: 9e6fa54878d0adbadeed565a3450f7d8d7f1bbe9 $')
    SERVICE_STAGE = "CORE"
    SERVICE_TIMEOUT = 600
    SERVICE_IS_EXTERNAL = True
    SERVICE_DEFAULT_CONFIG = {
        'private_api': False,
        'API_KEY': '',
        'BASE_URL': 'https://www.virustotal.com/vtapi/v2/'
    }

    def __init__(self, cfg=None):
        super(VirusTotalDynamic, self).__init__(cfg)
        self.api_key = self.cfg.get('API_KEY')
        self.private_api = self.cfg.get('private_api')

    # noinspection PyGlobalUndefined,PyUnresolvedReferences
    def import_service_deps(self):
        global requests
        import requests

    def start(self):
        self.log.debug("VirusTotal service started")

    def execute(self, request):
        filename = request.download()
        response = self.scan_file(request, filename)
        result = self.parse_results(response)
        if self.private_api:
            # Call some private API functions
            pass

        request.result = result

    # noinspection PyUnusedLocal
    def scan_file(self, request, filename):

        # Let's scan the file
        url = self.cfg.get('BASE_URL') + "file/scan"
        try:
            f = open(filename, "rb")
        except:
            print "Could not open file"
            return {}

        files = {"file": f}
        values = {"apikey": self.api_key}
        r = requests.post(url, values, files=files)
        try:
            json_response = r.json()
        except ValueError:
            self.log.warn("Invalid response from VirusTotal, "
                          "HTTP code: %s, "
                          "content length: %i, "
                          "headers: %s" % (r.status_code, len(r.content), repr(r.headers)))
            if len(r.content) == 0:
                raise RecoverableError("VirusTotal didn't return a JSON object, HTTP code %s" % r.status_code)
            raise

        # File has been scanned, if response is successful, let's get the response

        if json_response is not None and json_response.get('response_code') <= 0:
            return json_response

        sha256 = json_response.get('sha256', 0)
        if not sha256:
            return json_response

        # Have to wait for the file scan to be available -- might take a few minutes...
        while True:
            url = self.cfg.get('BASE_URL') + "file/report"
            params = {'apikey': self.api_key, 'resource': sha256}
            r = requests.post(url, params)
            json_response = r.json()
            if 'scans' in json_response or json_response.get('response_code') <= 0:
                break
            # Limit is 4 public API calls per minute, make sure we don't exceed quota
            # time.sleep(20)
            time.sleep(20)

        return json_response

    def parse_results(self, response):
        res = Result()
        response = response.get('results', response)

        if response is not None and response.get('response_code') == 204:
            message = "You exceeded the public API request rate limit (4 requests of any nature per minute)"
            raise VTException(message)
        elif response is not None and response.get('response_code') == 203:
            message = "You tried to perform calls to functions for which you require a Private API key."
            raise VTException(message)
        elif response is not None and response.get('response_code') == 1:
            av_hits = ResultSection(title_text='Anti-Virus Detections')
            url_section = ResultSection(
                SCORE.NULL,
                'Virus total report permalink',
                self.SERVICE_CLASSIFICATION,
                body_format=TEXT_FORMAT.URL,
                body=json.dumps({"url": response.get('permalink')}))
            res.add_section(url_section)

            scans = response.get('scans', response)
            av_hits.add_line('Found %d AV hit(s) from %d scans.' % (response.get('positives'), response.get('total')))
            for majorkey, subdict in sorted(scans.iteritems()):
                if subdict['detected']:
                    virus_name = subdict['result']
                    res.append_tag(VirusHitTag(virus_name, context="scanner:%s" % majorkey))
                    av_hits.add_section(AvHitSection(majorkey, virus_name, SCORE.SURE))
            res.add_result(av_hits)

        return res
Exemple #2
0
class Avg(ServiceBase):
    SERVICE_ENABLED = True
    SERVICE_REVISION = ServiceBase.parse_revision(
        '$Id: d7a9d50b72c5814f42d5d8791048317ebf10dbae $')
    SERVICE_VERSION = '1'
    SERVICE_DEFAULT_CONFIG = {
        'AUTOUPDATE': True,
        'AVG_PATH': '/usr/bin/avgscan',
        'UPDATER_OFFLINE_URL': None
    }
    SERVICE_DESCRIPTION = "This services wraps AVG's linux command line scanner 'avgscan'"
    SERVICE_CPU_CORES = 0.5
    SERVICE_RAM_MB = 256
    SERVICE_CATEGORY = "Antivirus"

    def __init__(self, cfg=None):
        super(Avg, self).__init__(cfg)
        self.avg_path = self.cfg.get('AVG_PATH')
        if not os.path.exists(self.avg_path):
            self.log.error(
                "AVG not found at %s. Avg service will likely be non functional.",
                self.avg_path)
        self._av_info = ''
        self.last_update = None

    def _fetch_raw_version(self):
        proc = subprocess.Popen([self.avg_path, 'fakearg'],
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE)
        out, _err = proc.communicate()
        av_date = None
        av_version = None
        for line in out.splitlines():
            if "Virus database version" in line:
                av_version = line.split(': ')[1]
            elif "Virus database release date" in line:
                av_date = line.split(': ')[1].strip()
                dt = parse(av_date)  # pylint: disable=E0602
                av_date = dt.strftime("%Y%m%d")
            if av_version and av_date:
                break
        return av_date, av_version, out

    def execute(self, request):
        request.result = Result()
        request.set_service_context(self._av_info)
        filename = request.download()

        # Generate the temporary resulting filename which AVG is going to dump the results in
        out_file = os.path.join(self.working_directory, "scanning_results.txt")

        cmd = [
            self.avg_path, "-H", "-p", "-o", "-w", "-b", "-j", "-a",
            "--report=%s" % out_file, filename
        ]
        devnull = open('/dev/null', 'wb')
        proc = subprocess.Popen(cmd,
                                stdout=devnull,
                                stderr=devnull,
                                cwd=os.path.dirname(self.avg_path))
        proc.wait()

        try:
            # AVG does not support unicode file names, so any results it returns for these files will be filtered out
            out_file_handle = codecs.open(out_file,
                                          mode='rb',
                                          encoding="utf-8",
                                          errors="replace")
            output = out_file_handle.read()
            out_file_handle.close()

            # 2- Parse the output and fill in the result objects
            self.parse_results_seq(output, request.result,
                                   len(self.working_directory))

        except Exception, scan_exception:
            self.log.error("AVG scanning was not completed: %s" %
                           str(scan_exception))
            raise
Exemple #3
0
class Binja(ServiceBase):
    SERVICE_CATEGORY = 'Static Analysis'
    SERVICE_ACCEPTS = 'executable/.*'
    SERVICE_REVISION = ServiceBase.parse_revision(
        '$Id: 6bddd7117590abf64e901d8af7c5d24f53e5d0aa $')
    SERVICE_VERSION = '1'
    SERVICE_ENABLED = True
    SERVICE_STAGE = 'CORE'
    SERVICE_CPU_CORES = 2
    SERVICE_RAM_MB = 768
    SERVICE_DEFAULT_CONFIG = {
        'license': None,
        'binaryninja_location': '/opt/al/support/binaryninja/python',
        'signature_file': '/opt/al/pkg/al_services/alsvc_binja/sigs.json'
    }

    class ACall():
        def __init__(self, il, typ, name, args=[]):
            self.il = il
            self.b = None
            self.typ = typ
            self.name = name
            self.args = args

    def __init__(self, cfg=None):
        super(Binja, self).__init__(cfg)
        self.syms = []
        self.sym_const = {}
        self.apiscore = 0
        self.depth = 0
        self.bv = None
        self.calls = {}
        self.used_syms = {}
        self.functions = {}
        self.processed = {}
        self.pstrs = []
        self.sigs = []
        self.proc_arg = {
            "KERNEL32!LOADLIBRARY": [0],
            "KERNEL32!GETPROCADDRESS": [1],
            "KERNEL32!GETMODULEHANDLE": [0],
            "KERNEL32!OPENEVENT": [2],
            "SHELL32!SHELLEXECUTE": [1]
        }

    def start(self):
        self.log.debug("Binja service started")
        self.load_sigs(self.cfg.get('signature_file'))

    # noinspection PyUnresolvedReferences
    def import_service_deps(self):
        global binaryninja
        sys.path.extend([self.cfg.get('binaryninja_location')])
        import binaryninja

    def get_unicode_str_at(self, addr):
        br = binaryninja.BinaryReader(self.bv)
        br.seek(addr)
        c = br.read16()
        s = ''
        while (c != 0) and (c < 0x80) and c is not None:
            s += chr(c)
            c = br.read16()
        return s

    def get_ascii_str_at(self, addr):
        br = binaryninja.BinaryReader(self.bv)
        br.seek(addr)
        c = br.read8()
        s = ''
        while (c != 0) and (c < 0x80) and c is not None:
            s += chr(c)
            c = br.read8()
        return s

###########################################################################

    def check_condition(self, cond, il, matches, depth=0, blocks=[]):
        flen = len(il.function)
        f = il.function.source_function
        b = f.get_basic_block_at(il.address)
        while il.operation != binaryninja.LowLevelILOperation.LLIL_RET:
            depth += 1
            if depth > cond['max']:
                return
            if il.operation == binaryninja.LowLevelILOperation.LLIL_JUMP_TO or il.operation == binaryninja.LowLevelILOperation.LLIL_IF:
                for edge in b.outgoing_edges:
                    if edge.target in blocks:
                        if "RECUR" in cond['tgts']:
                            matches.append({
                                "il":
                                f.get_low_level_il_at(edge.target.start),
                                "blocks":
                                blocks
                            })
                        self.check_condition(
                            cond, f.get_low_level_il_at(edge.target.start),
                            matches, depth)
                    else:
                        newb = list(blocks)
                        newb.append(edge.target)
                        self.check_condition(
                            cond, f.get_low_level_il_at(edge.target.start),
                            matches, depth, newb)
                return
            elif il.operation == binaryninja.LowLevelILOperation.LLIL_GOTO:
                il = il.function[il.dest]
                depth -= 1  # Don't count this LLIL_INSTR
            elif il.operation == binaryninja.LowLevelILOperation.LLIL_CALL:
                AC = self.process_call(il)
                if AC.name in cond['tgts'] or AC.name[:-1] in cond[
                        'tgts'] or AC.typ in cond['tgts']:
                    matches.append({"il": il, "blocks": blocks})
                il = il.function[il.instr_index + 1]
            else:
                if (il.instr_index + 1) == flen:
                    return
                il = il.function[il.instr_index + 1]

    def check_api_sig(self, sig, results):
        starts = []
        matches = []
        for i in sig['init']:
            for c in self.calls:
                if i in self.calls[c].name:
                    starts.append(self.calls[c])
        for start in starts:
            matches.append({"il": start.il, "blocks": []})
        for cond in sig['conditions']:
            if len(matches) == 0:
                return False
            preMatches = list(matches)
            matches = []
            for match in preMatches:
                self.check_condition(cond, match["il"], matches, 0,
                                     match["blocks"])
        for match in matches:
            results[match["il"].function.source_function.
                    start] = match["il"].function.source_function

        return results

    def load_sigs(self, fn):
        import json
        with open(fn) as sig_file:
            self.sigs = json.load(sig_file)
        self.load_syms()

    def load_syms(self):
        for sig in self.sigs:
            for init in sig['init']:
                if init not in self.syms:
                    self.syms.append(init)
            for cond in sig['conditions']:
                for tgt in cond['tgts']:
                    if "!" in tgt and tgt not in self.syms:
                        self.syms.append(tgt)

###########################################################################

    def process_call(self, il):
        name = ''
        typ = 'UNK'
        if il.address in self.calls:
            return self.calls[il.address]
        if il.operands[
                0].operation == binaryninja.LowLevelILOperation.LLIL_REG:
            #rval = il.function.source_function.get_reg_value_at_low_level_il_instruction(il.instr_index, str(il.dest))
            rval = il.get_reg_value(str(il.dest))
            if rval.type is binaryninja.RegisterValueType.ConstantValue:
                if rval.value in self.sym_const:
                    typ = "API"
                    name = self.sym_const[rval.value].name.split("@IAT")[0]
                else:
                    name = "%x" % rval.value
            else:
                typ = "REG"
                name = str(il.dest)
        elif il.operands[
                0].operation == binaryninja.LowLevelILOperation.LLIL_LOAD:
            if il.operands[
                    0].src.operation == binaryninja.LowLevelILOperation.LLIL_CONST:
                try:
                    typ = "API"
                    name = self.bv.get_symbol_at(
                        il.operands[0].src.constant).name.split("@IAT")[0]
                except AttributeError:
                    name = "%s" % il
        elif il.operands[
                0].operation == binaryninja.LowLevelILOperation.LLIL_CONST:
            try:
                typ = "API"
                name = self.bv.get_symbol_at(il.operands[0].constant).name
            except AttributeError:
                try:
                    typ = "LOC"
                    name = self.bv.get_function_at(
                        il.operands[0].constant).name
                    if il.operands[
                            0].constant == il.function.source_function.start:
                        typ = "SELF"
                        name = name
                except AttributeError:
                    name = "%s" % il
        else:
            name = "%d" % il.operands[0].operation
        if name[0:3] == 'sub':
            typ = "LOC"
        AC = self.ACall(il, typ, name.upper())
        self.calls[il.address] = AC
        return AC

    def dump_function_linear(self, func, depth=0, dump_str=[]):
        pre = "*" * depth
        dump_str.append(pre + func.name)
        for s in func.stack_layout:
            dump_str.append("%s\t%s" % (pre, str(s)))
        for block in func.low_level_il:
            dump_str.append("%s\t\t%s" % (pre, block))
            for il in block:
                dump_str.append("%s\t\t%d: %s" % (pre, il.instr_index, il))
                if il.operation == binaryninja.LowLevelILOperation.LLIL_CALL:
                    AC = self.process_call(il)
                    func.set_comment(
                        il.address, "%s(%s)" %
                        (AC.name, ','.join([str(x) for x in AC.args])))
                    if AC.typ == 'LOC':
                        if depth < self.depth:
                            dump_str.append("%s\t\t\t\t[%s] %s(%s)" %
                                            (pre, AC.typ, AC.name, ','.join(
                                                [str(x) for x in AC.args])))
                            nf = self.bv.get_function_at(
                                il.operands[0].constant)
                            self.dump_function_linear(nf, depth + 1, dump_str)
                        else:
                            if "-D" not in AC.name:
                                AC.name = AC.name + "-D"
                                dump_str.append(
                                    "%s\t\t\t\t[%s] %s(%s)" %
                                    (pre, AC.typ, AC.name, ','.join(
                                        [str(x) for x in AC.args])))
                    else:
                        dump_str.append("%s\t\t\t\t[%s] %s(%s)" %
                                        (pre, AC.typ, AC.name, ','.join(
                                            [str(x) for x in AC.args])))
                        # dump_str.append("%s\t\t\t\t[%s] %s(%s)" % (pre, AC.typ, AC.name, ','.join([str(x) for x in AC.args])))
        if "-A" not in func.name:
            func.name = func.name + "-A"

    def process_target_functions(self):
        for strt in self.functions:
            dstr = []
            if strt not in self.processed:
                self.dump_function_linear(self.functions[strt], dump_str=dstr)
                self.processed[strt] = dstr
        return

###########################################################################

    def get_symbol_xrefs(self, sym_str):
        try:
            sym = self.bv.symbols[sym_str]
        except KeyError:
            return
        xrefs = self.bv.get_code_refs(sym.address)
        for xref in xrefs:
            if xref.function.start not in self.functions:
                self.functions[xref.function.start] = xref.function
        if len(xrefs):
            self.apiscore += 1
            if sym.name.split("@IAT")[0].upper() not in self.used_syms:
                self.used_syms[sym.name.split("@IAT")[0].upper()] = len(xrefs)
            else:
                self.used_syms[sym.name.split("@IAT")[0].upper()] += len(xrefs)

    def symbol_usage(self, tgt_syms=[]):
        br = binaryninja.BinaryReader(self.bv)
        for sym in self.bv.get_symbols():
            br.seek(sym.address)
            c = br.read32()
            self.sym_const[c] = sym
        for s in self.bv.symbols:
            ts = s.split("@IAT")[0].upper()
            if ts in self.syms:
                self.get_symbol_xrefs(s)
            if ts[:-1] in self.syms:
                self.get_symbol_xrefs(s)

###########################################################################

    def get_str_arg(self, f, il, argv_index, api):
        p = f.get_parameter_at_low_level_il_instruction(
            il.instr_index, f.function_type, argv_index)
        v = -1
        if p.type == binaryninja.function.RegisterValueType.ConstantValue:
            v = p.constant
        elif p.type == binaryninja.function.RegisterValueType.UndeterminedValue:
            block_start = f.get_basic_block_at(il.address).start
            pil = f.low_level_il[il.instr_index - 1]
            pcount = 0
            while pil.address >= block_start:
                if pil.operation == binaryninja.LowLevelILOperation.LLIL_PUSH:
                    if pcount == argv_index:
                        if pil.operands[
                                0].operation == binaryninja.LowLevelILOperation.LLIL_CONST:
                            v = pil.operands[0].constant
                            break
                    pcount += 1
                pil = f.low_level_il[pil.instr_index - 1]
        if v == -1:
            val = "Undetermined"
        elif v == 0:
            val = "Self"
        elif api[-1] == "A":
            val = self.get_ascii_str_at(v)
        elif api[-1] == "W":
            val = self.get_unicode_str_at(v)
        else:
            val = self.get_ascii_str_at(v)
        if self.calls.has_key(il.address):
            self.calls[il.address].args.append(val)
        else:
            AC = self.ACall(il, "API", api.upper(), [val])
            self.calls[il.address] = AC

    def find_str_arg(self, sym, arg):
        # Try to identify string used as an arg
        xrefs = self.bv.get_code_refs(sym.address)
        for xref in xrefs:
            try:
                il = xref.function.low_level_il[
                    xref.function.get_low_level_il_at(xref.address)]
                if il.operation == binaryninja.LowLevelILOperation.LLIL_CALL:
                    self.get_str_arg(xref.function, il, arg,
                                     sym.name.split("@IAT")[0])
                    # elif il.operation == binaryninja.LowLevelILOperation.LLIL_SET_REG:
                    # Track REG and find CALLS
                    # Iterate over calls, calling preproc_get_arg
            except IndexError:
                pass
        return

    def preprocess(self):
        for s in self.bv.symbols:
            s_ = s.upper()
            for api in self.proc_arg.keys():
                if api in s_:
                    for arg in sorted(self.proc_arg[api]):
                        self.find_str_arg(self.bv.symbols[s], arg)

    def linear_sweep(self):
        prologues = [
            "\x55\x8b\xec", "\x8b\xff\x56", "\x8b\xff\x55", "\xff\x25"
        ]
        for prologue in prologues:
            cur = self.bv.find_next_data(self.bv.start, prologue)
            nf = self.bv.get_next_function_start_after(self.bv.start)
            while cur:
                if cur < nf:
                    self.bv.add_function(cur)
                    cur = self.bv.find_next_data(cur + 1, prologue)
                elif cur == nf:
                    nf = self.bv.get_next_function_start_after(cur)
                    cur = self.bv.find_next_data(cur + 1, prologue)
                else:
                    nf = self.bv.get_next_function_start_after(cur)

###########################################################################

    def clean_structures(self):
        self.sym_const = {}
        self.apiscore = 0
        self.calls = {}
        self.used_syms = {}
        self.functions = {}
        self.processed = {}
        self.pstrs = []
        self.bv = None

    def execute(self, request):
        file_path = request.download()
        filename = os.path.basename(file_path)
        bndb = os.path.join(self.working_directory, "%s.bndb" % filename)
        disas = os.path.join(self.working_directory, filename)

        self.clean_structures()

        if request.tag.startswith("executable/windows/"):
            self.bv = binaryninja.BinaryViewType['PE'].open(file_path)
        else:
            return

        if self.bv is None:
            return

        result = Result()
        self.bv.update_analysis_and_wait()
        # Preparation
        self.linear_sweep()
        self.preprocess()
        self.symbol_usage()
        self.process_target_functions()
        # Check Signatures
        for sig in self.sigs:
            results = {}
            self.check_api_sig(sig, results)
            if len(results) > 0:
                for res in results:
                    rn = "%s - %s" % (results[res].name.split("-A")[0],
                                      sig['name'])
                    section = ResultSection(sig['score'], rn)
                    if res in self.processed:
                        fn = "%s_%s" % (disas, rn.replace(" ", "_"))
                        with open(fn, "wb") as fp:
                            fp.write("\n".join("%s" % l
                                               for l in self.processed[res]))
                            request.add_supplementary(
                                fn, "Linear Disassembly of Matched Function",
                                rn + ".disas")
                    results[res].name = rn
                    result.add_section(section)
        # Finalize Results and Store BNDB
        self.bv.create_database(bndb)
        request.add_supplementary(bndb, "Binary Ninja DB", filename + ".bndb")
        section = ResultSection(self.apiscore, "Target Symbols X-refs")
        for sym in sorted(self.used_syms.items(),
                          key=lambda x: x[1],
                          reverse=True):
            section.add_line("%d\t%s" % (sym[1], sym[0]))
        result.add_section(section)
        request.result = result

        self.clean_structures()