mirror of
https://github.com/mandiant/capa.git
synced 2025-12-22 07:10:29 -08:00
initial commit for backend-smda
This commit is contained in:
49
capa/features/extractors/smda/__init__.py
Normal file
49
capa/features/extractors/smda/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
from smda.common.SmdaReport import SmdaReport
|
||||||
|
from smda.common.SmdaInstruction import SmdaInstruction
|
||||||
|
|
||||||
|
import capa.features.extractors.smda.file
|
||||||
|
import capa.features.extractors.smda.insn
|
||||||
|
import capa.features.extractors.smda.function
|
||||||
|
import capa.features.extractors.smda.basicblock
|
||||||
|
from capa.features.extractors import FeatureExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class SmdaFeatureExtractor(FeatureExtractor):
|
||||||
|
def __init__(self, smda_report: SmdaReport, path):
|
||||||
|
super(SmdaFeatureExtractor, self).__init__()
|
||||||
|
self.smda_report = smda_report
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
def get_base_address(self):
|
||||||
|
return self.smda_report.base_addr
|
||||||
|
|
||||||
|
def extract_file_features(self):
|
||||||
|
for feature, va in capa.features.extractors.smda.file.extract_features(self.smda_report, self.path):
|
||||||
|
yield feature, va
|
||||||
|
|
||||||
|
def get_functions(self):
|
||||||
|
for function in self.smda_report.getFunctions():
|
||||||
|
yield function
|
||||||
|
|
||||||
|
def extract_function_features(self, f):
|
||||||
|
for feature, va in capa.features.extractors.smda.function.extract_features(f):
|
||||||
|
yield feature, va
|
||||||
|
|
||||||
|
def get_basic_blocks(self, f):
|
||||||
|
for bb in f.getBlocks():
|
||||||
|
yield bb
|
||||||
|
|
||||||
|
def extract_basic_block_features(self, f, bb):
|
||||||
|
for feature, va in capa.features.extractors.smda.basicblock.extract_features(f, bb):
|
||||||
|
yield feature, va
|
||||||
|
|
||||||
|
def get_instructions(self, f, bb):
|
||||||
|
for smda_ins in bb.getInstructions():
|
||||||
|
yield smda_ins
|
||||||
|
|
||||||
|
def extract_insn_features(self, f, bb, insn):
|
||||||
|
for feature, va in capa.features.extractors.smda.insn.extract_features(f, bb, insn):
|
||||||
|
yield feature, va
|
||||||
136
capa/features/extractors/smda/basicblock.py
Normal file
136
capa/features/extractors/smda/basicblock.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import sys
|
||||||
|
import string
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from capa.features import Characteristic
|
||||||
|
from capa.features.basicblock import BasicBlock
|
||||||
|
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
|
||||||
|
|
||||||
|
|
||||||
|
def _bb_has_tight_loop(f, bb):
|
||||||
|
"""
|
||||||
|
parse tight loops, true if last instruction in basic block branches to bb start
|
||||||
|
"""
|
||||||
|
return bb.offset in f.blockrefs[bb.offset] if bb.offset in f.blockrefs else False
|
||||||
|
|
||||||
|
|
||||||
|
def extract_bb_tight_loop(f, bb):
|
||||||
|
""" check basic block for tight loop indicators """
|
||||||
|
if _bb_has_tight_loop(f, bb):
|
||||||
|
yield Characteristic("tight loop"), bb.offset
|
||||||
|
|
||||||
|
|
||||||
|
def _bb_has_stackstring(f, bb):
|
||||||
|
"""
|
||||||
|
extract potential stackstring creation, using the following heuristics:
|
||||||
|
- basic block contains enough moves of constant bytes to the stack
|
||||||
|
"""
|
||||||
|
count = 0
|
||||||
|
for instr in bb.getInstructions():
|
||||||
|
if is_mov_imm_to_stack(instr):
|
||||||
|
count += get_printable_len(instr.getDetailed())
|
||||||
|
if count > MIN_STACKSTRING_LEN:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_operands(smda_ins):
|
||||||
|
return [o.strip() for o in smda_ins.operands.split(",")]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_stackstring(f, bb):
|
||||||
|
""" check basic block for stackstring indicators """
|
||||||
|
if _bb_has_stackstring(f, bb):
|
||||||
|
yield Characteristic("stack string"), bb.offset
|
||||||
|
|
||||||
|
|
||||||
|
def is_mov_imm_to_stack(smda_ins):
|
||||||
|
"""
|
||||||
|
Return if instruction moves immediate onto stack
|
||||||
|
"""
|
||||||
|
if not smda_ins.mnemonic.startswith("mov"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
dst, src = get_operands(smda_ins)
|
||||||
|
except ValueError:
|
||||||
|
# not two operands
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
int(src, 16)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not any(regname in dst for regname in ["ebp", "rbp", "esp", "rsp"]):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_printable_len(instr):
|
||||||
|
"""
|
||||||
|
Return string length if all operand bytes are ascii or utf16-le printable
|
||||||
|
|
||||||
|
Works on a capstone instruction
|
||||||
|
"""
|
||||||
|
# should have exactly two operands for mov immediate
|
||||||
|
if len(instr.operands) != 2:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
op_value = instr.operands[1].value.imm
|
||||||
|
|
||||||
|
if instr.imm_size == 1:
|
||||||
|
chars = struct.pack("<B", op_value & 0xFF)
|
||||||
|
elif instr.imm_size == 2:
|
||||||
|
chars = struct.pack("<H", op_value & 0xFFFF)
|
||||||
|
elif instr.imm_size == 4:
|
||||||
|
chars = struct.pack("<I", op_value & 0xFFFFFFFF)
|
||||||
|
elif instr.imm_size == 8:
|
||||||
|
chars = struct.pack("<Q", op_value & 0xFFFFFFFFFFFFFFFF)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unhandled operand data type 0x%x." % instr.imm_size)
|
||||||
|
|
||||||
|
def is_printable_ascii(chars):
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
return all(c < 127 and chr(c) in string.printable for c in chars)
|
||||||
|
else:
|
||||||
|
return all(ord(c) < 127 and c in string.printable for c in chars)
|
||||||
|
|
||||||
|
def is_printable_utf16le(chars):
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
if all(c == 0x00 for c in chars[1::2]):
|
||||||
|
return is_printable_ascii(chars[::2])
|
||||||
|
else:
|
||||||
|
if all(c == "\x00" for c in chars[1::2]):
|
||||||
|
return is_printable_ascii(chars[::2])
|
||||||
|
|
||||||
|
if is_printable_ascii(chars):
|
||||||
|
return instr.imm_size
|
||||||
|
if is_printable_utf16le(chars):
|
||||||
|
return instr.imm_size / 2
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def extract_features(f, bb):
|
||||||
|
"""
|
||||||
|
extract features from the given basic block.
|
||||||
|
|
||||||
|
args:
|
||||||
|
f (smda.common.SmdaFunction): the function from which to extract features
|
||||||
|
bb (smda.common.SmdaBasicBlock): the basic block to process.
|
||||||
|
|
||||||
|
yields:
|
||||||
|
Feature, set[VA]: the features and their location found in this basic block.
|
||||||
|
"""
|
||||||
|
yield BasicBlock(), bb.offset
|
||||||
|
for bb_handler in BASIC_BLOCK_HANDLERS:
|
||||||
|
for feature, va in bb_handler(f, bb):
|
||||||
|
yield feature, va
|
||||||
|
|
||||||
|
|
||||||
|
BASIC_BLOCK_HANDLERS = (
|
||||||
|
extract_bb_tight_loop,
|
||||||
|
extract_stackstring,
|
||||||
|
)
|
||||||
139
capa/features/extractors/smda/file.py
Normal file
139
capa/features/extractors/smda/file.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
# if we have SMDA we definitely have lief
|
||||||
|
import lief
|
||||||
|
|
||||||
|
import capa.features.extractors.helpers
|
||||||
|
import capa.features.extractors.strings
|
||||||
|
from capa.features import String, Characteristic
|
||||||
|
from capa.features.file import Export, Import, Section
|
||||||
|
|
||||||
|
|
||||||
|
def carve(pbytes, offset=0):
|
||||||
|
"""
|
||||||
|
Return a list of (offset, size, xor) tuples of embedded PEs
|
||||||
|
|
||||||
|
Based on the version from vivisect:
|
||||||
|
https://github.com/vivisect/vivisect/blob/7be4037b1cecc4551b397f840405a1fc606f9b53/PE/carve.py#L19
|
||||||
|
And its IDA adaptation:
|
||||||
|
capa/features/extractors/ida/file.py
|
||||||
|
"""
|
||||||
|
mz_xor = [
|
||||||
|
(
|
||||||
|
capa.features.extractors.helpers.xor_static(b"MZ", i),
|
||||||
|
capa.features.extractors.helpers.xor_static(b"PE", i),
|
||||||
|
i,
|
||||||
|
)
|
||||||
|
for i in range(256)
|
||||||
|
]
|
||||||
|
|
||||||
|
pblen = len(pbytes)
|
||||||
|
todo = [(pbytes.find(mzx, offset), mzx, pex, i) for mzx, pex, i in mz_xor]
|
||||||
|
todo = [(off, mzx, pex, i) for (off, mzx, pex, i) in todo if off != -1]
|
||||||
|
|
||||||
|
while len(todo):
|
||||||
|
|
||||||
|
off, mzx, pex, i = todo.pop()
|
||||||
|
|
||||||
|
# The MZ header has one field we will check
|
||||||
|
# e_lfanew is at 0x3c
|
||||||
|
e_lfanew = off + 0x3C
|
||||||
|
if pblen < (e_lfanew + 4):
|
||||||
|
continue
|
||||||
|
|
||||||
|
newoff = struct.unpack("<I", capa.features.extractors.helpers.xor_static(pbytes[e_lfanew : e_lfanew + 4], i))[0]
|
||||||
|
|
||||||
|
nextres = pbytes.find(mzx, off + 1)
|
||||||
|
if nextres != -1:
|
||||||
|
todo.append((nextres, mzx, pex, i))
|
||||||
|
|
||||||
|
peoff = off + newoff
|
||||||
|
if pblen < (peoff + 2):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if pbytes[peoff : peoff + 2] == pex:
|
||||||
|
yield (off, i)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_file_embedded_pe(smda_report, file_path):
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
fbytes = f.read()
|
||||||
|
|
||||||
|
for offset, i in carve(fbytes, 1):
|
||||||
|
yield Characteristic("embedded pe"), offset
|
||||||
|
|
||||||
|
|
||||||
|
def extract_file_export_names(smda_report, file_path):
|
||||||
|
lief_binary = lief.parse(file_path)
|
||||||
|
if lief_binary is not None:
|
||||||
|
for function in lief_binary.exported_functions:
|
||||||
|
yield function.name, function.address
|
||||||
|
|
||||||
|
|
||||||
|
def extract_file_import_names(smda_report, file_path):
|
||||||
|
# extract import table info via LIEF
|
||||||
|
lief_binary = lief.parse(file_path)
|
||||||
|
if not isinstance(lief_binary, lief.PE.Binary):
|
||||||
|
return
|
||||||
|
for imported_library in lief_binary.imports:
|
||||||
|
for func in imported_library.entries:
|
||||||
|
if func.name:
|
||||||
|
va = func.iat_address + smda_report.base_addr
|
||||||
|
for name in capa.features.extractors.helpers.generate_symbols(imported_library.name, func.name):
|
||||||
|
yield Import(name), va
|
||||||
|
elif func.is_ordinal:
|
||||||
|
for name in capa.features.extractors.helpers.generate_symbols(
|
||||||
|
imported_library.name, "#%s" % func.ordinal
|
||||||
|
):
|
||||||
|
yield Import(name), va
|
||||||
|
|
||||||
|
|
||||||
|
def extract_file_section_names(smda_report, file_path):
|
||||||
|
lief_binary = lief.parse(file_path)
|
||||||
|
if not isinstance(lief_binary, lief.PE.Binary):
|
||||||
|
return
|
||||||
|
if lief_binary and lief_binary.sections:
|
||||||
|
base_address = lief_binary.optional_header.imagebase
|
||||||
|
for section in lief_binary.sections:
|
||||||
|
yield Section(section.name), base_address + section.virtual_address
|
||||||
|
|
||||||
|
|
||||||
|
def extract_file_strings(smda_report, file_path):
|
||||||
|
"""
|
||||||
|
extract ASCII and UTF-16 LE strings from file
|
||||||
|
"""
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
b = f.read()
|
||||||
|
|
||||||
|
for s in capa.features.extractors.strings.extract_ascii_strings(b):
|
||||||
|
yield String(s.s), s.offset
|
||||||
|
|
||||||
|
for s in capa.features.extractors.strings.extract_unicode_strings(b):
|
||||||
|
yield String(s.s), s.offset
|
||||||
|
|
||||||
|
|
||||||
|
def extract_features(smda_report, file_path):
|
||||||
|
"""
|
||||||
|
extract file features from given workspace
|
||||||
|
|
||||||
|
args:
|
||||||
|
smda_report (smda.common.SmdaReport): a SmdaReport
|
||||||
|
file_path: path to the input file
|
||||||
|
|
||||||
|
yields:
|
||||||
|
Tuple[Feature, VA]: a feature and its location.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for file_handler in FILE_HANDLERS:
|
||||||
|
result = file_handler(smda_report, file_path)
|
||||||
|
for feature, va in file_handler(smda_report, file_path):
|
||||||
|
yield feature, va
|
||||||
|
|
||||||
|
|
||||||
|
FILE_HANDLERS = (
|
||||||
|
extract_file_embedded_pe,
|
||||||
|
extract_file_export_names,
|
||||||
|
extract_file_import_names,
|
||||||
|
extract_file_section_names,
|
||||||
|
extract_file_strings,
|
||||||
|
)
|
||||||
51
capa/features/extractors/smda/function.py
Normal file
51
capa/features/extractors/smda/function.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from capa.features import Characteristic
|
||||||
|
from capa.features.extractors import loops
|
||||||
|
|
||||||
|
|
||||||
|
def interface_extract_function_XXX(f):
|
||||||
|
"""
|
||||||
|
parse features from the given function.
|
||||||
|
|
||||||
|
args:
|
||||||
|
f (viv_utils.Function): the function to process.
|
||||||
|
|
||||||
|
yields:
|
||||||
|
(Feature, int): the feature and the address at which its found.
|
||||||
|
"""
|
||||||
|
yield NotImplementedError("feature"), NotImplementedError("virtual address")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_function_calls_to(f):
|
||||||
|
for inref in f.inrefs:
|
||||||
|
yield Characteristic("calls to"), inref
|
||||||
|
|
||||||
|
|
||||||
|
def extract_function_loop(f):
|
||||||
|
"""
|
||||||
|
parse if a function has a loop
|
||||||
|
"""
|
||||||
|
edges = []
|
||||||
|
for bb_from, bb_tos in f.blockrefs.items():
|
||||||
|
for bb_to in bb_tos:
|
||||||
|
edges.append((bb_from, bb_to))
|
||||||
|
|
||||||
|
if edges and loops.has_loop(edges):
|
||||||
|
yield Characteristic("loop"), f.offset
|
||||||
|
|
||||||
|
|
||||||
|
def extract_features(f):
|
||||||
|
"""
|
||||||
|
extract features from the given function.
|
||||||
|
|
||||||
|
args:
|
||||||
|
f (viv_utils.Function): the function from which to extract features
|
||||||
|
|
||||||
|
yields:
|
||||||
|
Feature, set[VA]: the features and their location found in this function.
|
||||||
|
"""
|
||||||
|
for func_handler in FUNCTION_HANDLERS:
|
||||||
|
for feature, va in func_handler(f):
|
||||||
|
yield feature, va
|
||||||
|
|
||||||
|
|
||||||
|
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop)
|
||||||
343
capa/features/extractors/smda/insn.py
Normal file
343
capa/features/extractors/smda/insn.py
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
import re
|
||||||
|
import string
|
||||||
|
|
||||||
|
from smda.common.SmdaReport import SmdaReport
|
||||||
|
|
||||||
|
import capa.features.extractors.helpers
|
||||||
|
from capa.features import (
|
||||||
|
ARCH_X32,
|
||||||
|
ARCH_X64,
|
||||||
|
MAX_BYTES_FEATURE_SIZE,
|
||||||
|
THUNK_CHAIN_DEPTH_DELTA,
|
||||||
|
Bytes,
|
||||||
|
String,
|
||||||
|
Characteristic,
|
||||||
|
)
|
||||||
|
from capa.features.insn import API, Number, Offset, Mnemonic
|
||||||
|
|
||||||
|
# security cookie checks may perform non-zeroing XORs, these are expected within a certain
|
||||||
|
# byte range within the first and returning basic blocks, this helps to reduce FP features
|
||||||
|
SECURITY_COOKIE_BYTES_DELTA = 0x40
|
||||||
|
|
||||||
|
|
||||||
|
def get_arch(smda_report: SmdaReport):
|
||||||
|
if smda_report.architecture == "intel":
|
||||||
|
if smda_report.bitness == 32:
|
||||||
|
return ARCH_X32
|
||||||
|
elif smda_report.bitness == 64:
|
||||||
|
return ARCH_X64
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def interface_extract_instruction_XXX(f, bb, insn):
|
||||||
|
"""
|
||||||
|
parse features from the given instruction.
|
||||||
|
|
||||||
|
args:
|
||||||
|
f (smda.common.SmdaFunction): the function to process.
|
||||||
|
bb (smda.common.SmdaBasicBlock): the basic block to process.
|
||||||
|
insn (smda.common.SmdaInstruction): the instruction to process.
|
||||||
|
|
||||||
|
yields:
|
||||||
|
(Feature, int): the feature and the address at which its found.
|
||||||
|
"""
|
||||||
|
yield NotImplementedError("feature"), NotImplementedError("virtual address")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_insn_api_features(f, bb, insn):
|
||||||
|
"""parse API features from the given instruction."""
|
||||||
|
if insn.offset in f.apirefs:
|
||||||
|
api_entry = f.apirefs[insn.offset]
|
||||||
|
# reformat
|
||||||
|
dll_name, api_name = api_entry.split("!")
|
||||||
|
dll_name = dll_name.split(".")[0]
|
||||||
|
name = dll_name + "." + api_name
|
||||||
|
yield API(name), insn.offset
|
||||||
|
|
||||||
|
|
||||||
|
def extract_insn_number_features(f, bb, insn):
|
||||||
|
"""parse number features from the given instruction."""
|
||||||
|
# example:
|
||||||
|
#
|
||||||
|
# push 3136B0h ; dwControlCode
|
||||||
|
operands = [o.strip() for o in insn.operands.split(",")]
|
||||||
|
for operand in operands:
|
||||||
|
if insn.mnemonic == "add" and operands[0] in ["esp", "rsp"]:
|
||||||
|
# skip things like:
|
||||||
|
#
|
||||||
|
# .text:00401140 call sub_407E2B
|
||||||
|
# .text:00401145 add esp, 0Ch
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
yield Number(int(operand, 16)), insn.offset
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def read_bytes(smda_report, va, num_bytes=None):
|
||||||
|
"""
|
||||||
|
read up to MAX_BYTES_FEATURE_SIZE from the given address.
|
||||||
|
"""
|
||||||
|
|
||||||
|
rva = va - smda_report.base_addr
|
||||||
|
if smda_report.buffer is None:
|
||||||
|
return
|
||||||
|
buffer_end = len(smda_report.buffer)
|
||||||
|
max_bytes = num_bytes if num_bytes is not None else MAX_BYTES_FEATURE_SIZE
|
||||||
|
if rva + max_bytes > buffer_end:
|
||||||
|
return smda_report.buffer[rva:]
|
||||||
|
else:
|
||||||
|
return smda_report.buffer[rva : rva + max_bytes]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_insn_bytes_features(f, bb, insn):
|
||||||
|
"""
|
||||||
|
parse byte sequence features from the given instruction.
|
||||||
|
example:
|
||||||
|
# push offset iid_004118d4_IShellLinkA ; riid
|
||||||
|
"""
|
||||||
|
for data_ref in insn.getDataRefs():
|
||||||
|
bytes_read = read_bytes(f.smda_report, data_ref)
|
||||||
|
if bytes_read is None:
|
||||||
|
continue
|
||||||
|
if capa.features.extractors.helpers.all_zeros(bytes_read):
|
||||||
|
continue
|
||||||
|
yield Bytes(bytes_read), insn.offset
|
||||||
|
|
||||||
|
|
||||||
|
def detectAsciiLen(smda_report, offset):
|
||||||
|
if smda_report.buffer is None:
|
||||||
|
return 0
|
||||||
|
ascii_len = 0
|
||||||
|
rva = offset - smda_report.base_addr
|
||||||
|
char = smda_report.buffer[rva]
|
||||||
|
while char < 127 and chr(char) in string.printable:
|
||||||
|
ascii_len += 1
|
||||||
|
rva += 1
|
||||||
|
char = smda_report.buffer[rva]
|
||||||
|
if char == 0:
|
||||||
|
return ascii_len
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def detectUnicodeLen(smda_report, offset):
|
||||||
|
if smda_report.buffer is None:
|
||||||
|
return 0
|
||||||
|
unicode_len = 0
|
||||||
|
rva = offset - smda_report.base_addr
|
||||||
|
char = smda_report.buffer[rva]
|
||||||
|
second_char = smda_report.buffer[rva + 1]
|
||||||
|
while char < 127 and chr(char) in string.printable and second_char == 0:
|
||||||
|
unicode_len += 2
|
||||||
|
rva += 2
|
||||||
|
char = smda_report.buffer[rva]
|
||||||
|
second_char = smda_report.buffer[rva + 1]
|
||||||
|
if char == 0 and second_char == 0:
|
||||||
|
return unicode_len
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def read_string(smda_report, offset):
|
||||||
|
alen = detectAsciiLen(smda_report, offset)
|
||||||
|
if alen > 1:
|
||||||
|
return read_bytes(smda_report, offset, alen).decode("utf-8")
|
||||||
|
ulen = detectUnicodeLen(smda_report, offset)
|
||||||
|
if ulen > 2:
|
||||||
|
return read_bytes(smda_report, offset, ulen).decode("utf-16")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_insn_string_features(f, bb, insn):
|
||||||
|
"""parse string features from the given instruction."""
|
||||||
|
# example:
|
||||||
|
#
|
||||||
|
# push offset aAcr ; "ACR > "
|
||||||
|
for data_ref in insn.getDataRefs():
|
||||||
|
string_read = read_string(f.smda_report, data_ref)
|
||||||
|
if string_read:
|
||||||
|
yield String(string_read.rstrip("\x00")), insn.offset
|
||||||
|
|
||||||
|
|
||||||
|
def extract_insn_offset_features(f, bb, insn):
|
||||||
|
"""parse structure offset features from the given instruction."""
|
||||||
|
# examples:
|
||||||
|
#
|
||||||
|
# mov eax, [esi + 4]
|
||||||
|
# mov eax, [esi + ecx + 16384]
|
||||||
|
operands = [o.strip() for o in insn.operands.split(",")]
|
||||||
|
for operand in operands:
|
||||||
|
number = None
|
||||||
|
number_hex = re.search(r"[+\-] (?P<num>0x[a-fA-F0-9]+)", operand)
|
||||||
|
number_int = re.search(r"[+\-] (?P<num>[0-9])", operand)
|
||||||
|
if number_hex:
|
||||||
|
number = int(number_hex.group("num"), 16)
|
||||||
|
number = -1 * number if number_hex.group().startswith("-") else number
|
||||||
|
elif number_int:
|
||||||
|
number = int(number_int.group("num"))
|
||||||
|
number = -1 * number if number_int.group().startswith("-") else number
|
||||||
|
if not operand.startswith("0") and number is not None:
|
||||||
|
yield Offset(number), insn.offset
|
||||||
|
|
||||||
|
|
||||||
|
def is_security_cookie(f, bb, insn):
|
||||||
|
"""
|
||||||
|
check if an instruction is related to security cookie checks
|
||||||
|
"""
|
||||||
|
# security cookie check should use SP or BP
|
||||||
|
operands = [o.strip() for o in insn.operands.split(",")]
|
||||||
|
if operands[0] not in ["esp", "ebp", "rsp", "rbp"]:
|
||||||
|
return False
|
||||||
|
for index, block in enumerate(f.getBlocks()):
|
||||||
|
# expect security cookie init in first basic block within first bytes (instructions)
|
||||||
|
if index == 0 and insn.offset < (block[0].offset + SECURITY_COOKIE_BYTES_DELTA):
|
||||||
|
return True
|
||||||
|
# ... or within last bytes (instructions) before a return
|
||||||
|
if block[-1].mnemonic.startswith("ret") and insn.offset > (block[-1].offset - SECURITY_COOKIE_BYTES_DELTA):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def extract_insn_nzxor_characteristic_features(f, bb, insn):
|
||||||
|
"""
|
||||||
|
parse non-zeroing XOR instruction from the given instruction.
|
||||||
|
ignore expected non-zeroing XORs, e.g. security cookies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if insn.mnemonic != "xor":
|
||||||
|
return
|
||||||
|
|
||||||
|
operands = [o.strip() for o in insn.operands.split(",")]
|
||||||
|
if operands[0] == operands[1]:
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_security_cookie(f, bb, insn):
|
||||||
|
return
|
||||||
|
|
||||||
|
yield Characteristic("nzxor"), insn.offset
|
||||||
|
|
||||||
|
|
||||||
|
def extract_insn_mnemonic_features(f, bb, insn):
|
||||||
|
"""parse mnemonic features from the given instruction."""
|
||||||
|
yield Mnemonic(insn.mnemonic), insn.offset
|
||||||
|
|
||||||
|
|
||||||
|
def extract_insn_peb_access_characteristic_features(f, bb, insn):
|
||||||
|
"""
|
||||||
|
parse peb access from the given function. fs:[0x30] on x86, gs:[0x60] on x64
|
||||||
|
"""
|
||||||
|
|
||||||
|
if insn.mnemonic not in ["push", "mov"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
operands = [o.strip() for o in insn.operands.split(",")]
|
||||||
|
for operand in operands:
|
||||||
|
if "fs:" in operand and "0x30" in operand:
|
||||||
|
yield Characteristic("peb access"), insn.offset
|
||||||
|
elif "gs:" in operand and "0x60" in operand:
|
||||||
|
yield Characteristic("peb access"), insn.offset
|
||||||
|
|
||||||
|
|
||||||
|
def extract_insn_segment_access_features(f, bb, insn):
|
||||||
|
""" parse the instruction for access to fs or gs """
|
||||||
|
operands = [o.strip() for o in insn.operands.split(",")]
|
||||||
|
for operand in operands:
|
||||||
|
if "fs:" in operand and "0x30" in operand:
|
||||||
|
yield Characteristic("fs access"), insn.offset
|
||||||
|
elif "gs:" in operand and "0x60" in operand:
|
||||||
|
yield Characteristic("gs access"), insn.offset
|
||||||
|
|
||||||
|
|
||||||
|
def get_section(vw, va):
|
||||||
|
for start, length, _, __ in vw.getMemoryMaps():
|
||||||
|
if start <= va < start + length:
|
||||||
|
return start
|
||||||
|
|
||||||
|
raise KeyError(va)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_insn_cross_section_cflow(f, bb, insn):
|
||||||
|
"""
|
||||||
|
inspect the instruction for a CALL or JMP that crosses section boundaries.
|
||||||
|
"""
|
||||||
|
if insn.mnemonic in ["call", "jmp"]:
|
||||||
|
if insn.offset in f.apirefs:
|
||||||
|
return
|
||||||
|
|
||||||
|
if insn.offset in f.outrefs:
|
||||||
|
for target in f.outrefs[insn.offset]:
|
||||||
|
if not insn.smda_function.smda_report.isAddrWithinMemoryImage(target):
|
||||||
|
yield Characteristic("cross section flow"), insn.offset
|
||||||
|
elif insn.operands.startswith("0x"):
|
||||||
|
target = int(insn.operands, 16)
|
||||||
|
if not insn.smda_function.smda_report.isAddrWithinMemoryImage(target):
|
||||||
|
yield Characteristic("cross section flow"), insn.offset
|
||||||
|
|
||||||
|
|
||||||
|
# this is a feature that's most relevant at the function scope,
|
||||||
|
# however, its most efficient to extract at the instruction scope.
|
||||||
|
def extract_function_calls_from(f, bb, insn):
|
||||||
|
if insn.mnemonic != "call":
|
||||||
|
return
|
||||||
|
|
||||||
|
if insn.offset in f.outrefs:
|
||||||
|
for outref in f.outrefs[insn.offset]:
|
||||||
|
yield Characteristic("calls from"), outref
|
||||||
|
|
||||||
|
if outref == f.offset:
|
||||||
|
# if we found a jump target and it's the function address
|
||||||
|
# mark as recursive
|
||||||
|
yield Characteristic("recursive call"), outref
|
||||||
|
|
||||||
|
|
||||||
|
# this is a feature that's most relevant at the function or basic block scope,
|
||||||
|
# however, its most efficient to extract at the instruction scope.
|
||||||
|
def extract_function_indirect_call_characteristic_features(f, bb, insn):
|
||||||
|
"""
|
||||||
|
extract indirect function call characteristic (e.g., call eax or call dword ptr [edx+4])
|
||||||
|
does not include calls like => call ds:dword_ABD4974
|
||||||
|
"""
|
||||||
|
if insn.mnemonic != "call":
|
||||||
|
return
|
||||||
|
if insn.operands.startswith("0x"):
|
||||||
|
return False
|
||||||
|
if "qword ptr" in insn.operands and "rip" in insn.operands:
|
||||||
|
return False
|
||||||
|
if insn.operands.startswith("dword ptr [0x"):
|
||||||
|
return False
|
||||||
|
# call edx
|
||||||
|
# call dword ptr [eax+50h]
|
||||||
|
# call qword ptr [rsp+78h]
|
||||||
|
yield Characteristic("indirect call"), insn.offset
|
||||||
|
|
||||||
|
|
||||||
|
def extract_features(f, bb, insn):
|
||||||
|
"""
|
||||||
|
extract features from the given insn.
|
||||||
|
|
||||||
|
args:
|
||||||
|
f (smda.common.SmdaFunction): the function to process.
|
||||||
|
bb (smda.common.SmdaBasicBlock): the basic block to process.
|
||||||
|
insn (smda.common.SmdaInstruction): the instruction to process.
|
||||||
|
|
||||||
|
yields:
|
||||||
|
Feature, set[VA]: the features and their location found in this insn.
|
||||||
|
"""
|
||||||
|
for insn_handler in INSTRUCTION_HANDLERS:
|
||||||
|
for feature, va in insn_handler(f, bb, insn):
|
||||||
|
yield feature, va
|
||||||
|
|
||||||
|
|
||||||
|
INSTRUCTION_HANDLERS = (
|
||||||
|
extract_insn_api_features,
|
||||||
|
extract_insn_number_features,
|
||||||
|
extract_insn_string_features,
|
||||||
|
extract_insn_bytes_features,
|
||||||
|
extract_insn_offset_features,
|
||||||
|
extract_insn_nzxor_characteristic_features,
|
||||||
|
extract_insn_mnemonic_features,
|
||||||
|
extract_insn_peb_access_characteristic_features,
|
||||||
|
extract_insn_cross_section_cflow,
|
||||||
|
extract_insn_segment_access_features,
|
||||||
|
extract_function_calls_from,
|
||||||
|
extract_function_indirect_call_characteristic_features,
|
||||||
|
)
|
||||||
43
capa/main.py
43
capa/main.py
@@ -295,7 +295,19 @@ class UnsupportedRuntimeError(RuntimeError):
|
|||||||
|
|
||||||
|
|
||||||
def get_extractor_py3(path, format, disable_progress=False):
|
def get_extractor_py3(path, format, disable_progress=False):
|
||||||
raise UnsupportedRuntimeError()
|
from smda.SmdaConfig import SmdaConfig
|
||||||
|
from smda.Disassembler import Disassembler
|
||||||
|
|
||||||
|
import capa.features.extractors.smda
|
||||||
|
|
||||||
|
smda_report = None
|
||||||
|
with halo.Halo(text="analyzing program", spinner="simpleDots", stream=sys.stderr, enabled=not disable_progress):
|
||||||
|
config = SmdaConfig()
|
||||||
|
config.STORE_BUFFER = True
|
||||||
|
smda_disasm = Disassembler(config)
|
||||||
|
smda_report = smda_disasm.disassembleFile(path)
|
||||||
|
|
||||||
|
return capa.features.extractors.smda.SmdaFeatureExtractor(smda_report, path)
|
||||||
|
|
||||||
|
|
||||||
def get_extractor(path, format, disable_progress=False):
|
def get_extractor(path, format, disable_progress=False):
|
||||||
@@ -446,14 +458,25 @@ def main(argv=None):
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=desc, epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter
|
description=desc, epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
# TODO: decode won't work for python3
|
||||||
# in #328 we noticed that the sample path is not handled correctly if it contains non-ASCII characters
|
if sys.version_info >= (3, 0):
|
||||||
# https://stackoverflow.com/a/22947334/ offers a solution and decoding using getfilesystemencoding works
|
parser.add_argument(
|
||||||
# in our testing, however other sources suggest `sys.stdin.encoding` (https://stackoverflow.com/q/4012571/)
|
# in #328 we noticed that the sample path is not handled correctly if it contains non-ASCII characters
|
||||||
"sample",
|
# https://stackoverflow.com/a/22947334/ offers a solution and decoding using getfilesystemencoding works
|
||||||
type=lambda s: s.decode(sys.getfilesystemencoding()),
|
# in our testing, however other sources suggest `sys.stdin.encoding` (https://stackoverflow.com/q/4012571/)
|
||||||
help="path to sample to analyze",
|
"sample",
|
||||||
)
|
type=str,
|
||||||
|
help="path to sample to analyze",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
parser.add_argument(
|
||||||
|
# in #328 we noticed that the sample path is not handled correctly if it contains non-ASCII characters
|
||||||
|
# https://stackoverflow.com/a/22947334/ offers a solution and decoding using getfilesystemencoding works
|
||||||
|
# in our testing, however other sources suggest `sys.stdin.encoding` (https://stackoverflow.com/q/4012571/)
|
||||||
|
"sample",
|
||||||
|
type=lambda s: s.decode(sys.getfilesystemencoding()),
|
||||||
|
help="path to sample to analyze",
|
||||||
|
)
|
||||||
parser.add_argument("--version", action="version", version="%(prog)s {:s}".format(capa.version.__version__))
|
parser.add_argument("--version", action="version", version="%(prog)s {:s}".format(capa.version.__version__))
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-r",
|
"-r",
|
||||||
@@ -550,7 +573,7 @@ def main(argv=None):
|
|||||||
# during the load of the RuleSet, we extract subscope statements into their own rules
|
# during the load of the RuleSet, we extract subscope statements into their own rules
|
||||||
# that are subsequently `match`ed upon. this inflates the total rule count.
|
# that are subsequently `match`ed upon. this inflates the total rule count.
|
||||||
# so, filter out the subscope rules when reporting total number of loaded rules.
|
# so, filter out the subscope rules when reporting total number of loaded rules.
|
||||||
len(filter(lambda r: "capa/subscope-rule" not in r.meta, rules.rules.values())),
|
len([i for i in filter(lambda r: "capa/subscope-rule" not in r.meta, rules.rules.values())]),
|
||||||
)
|
)
|
||||||
if args.tag:
|
if args.tag:
|
||||||
rules = rules.filter_rules_by_meta(args.tag)
|
rules = rules.filter_rules_by_meta(args.tag)
|
||||||
|
|||||||
1
setup.py
1
setup.py
@@ -28,6 +28,7 @@ requirements = [
|
|||||||
if sys.version_info >= (3, 0):
|
if sys.version_info >= (3, 0):
|
||||||
# py3
|
# py3
|
||||||
requirements.append("networkx")
|
requirements.append("networkx")
|
||||||
|
requirements.append("smda")
|
||||||
else:
|
else:
|
||||||
# py2
|
# py2
|
||||||
requirements.append("enum34==1.1.6") # v1.1.6 is needed by halo 0.0.30 / spinners 0.0.24
|
requirements.append("enum34==1.1.6") # v1.1.6 is needed by halo 0.0.30 / spinners 0.0.24
|
||||||
|
|||||||
@@ -81,6 +81,21 @@ def get_viv_extractor(path):
|
|||||||
return capa.features.extractors.viv.VivisectFeatureExtractor(vw, path)
|
return capa.features.extractors.viv.VivisectFeatureExtractor(vw, path)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def get_smda_extractor(path):
|
||||||
|
from smda.SmdaConfig import SmdaConfig
|
||||||
|
from smda.Disassembler import Disassembler
|
||||||
|
|
||||||
|
import capa.features.extractors.smda
|
||||||
|
|
||||||
|
config = SmdaConfig()
|
||||||
|
config.STORE_BUFFER = True
|
||||||
|
disasm = Disassembler(config)
|
||||||
|
report = disasm.disassembleFile(path)
|
||||||
|
|
||||||
|
return capa.features.extractors.smda.SmdaFeatureExtractor(report, path)
|
||||||
|
|
||||||
|
|
||||||
@lru_cache()
|
@lru_cache()
|
||||||
def extract_file_features(extractor):
|
def extract_file_features(extractor):
|
||||||
features = collections.defaultdict(set)
|
features = collections.defaultdict(set)
|
||||||
@@ -473,7 +488,7 @@ def do_test_feature_count(get_extractor, sample, scope, feature, expected):
|
|||||||
|
|
||||||
def get_extractor(path):
|
def get_extractor(path):
|
||||||
if sys.version_info >= (3, 0):
|
if sys.version_info >= (3, 0):
|
||||||
raise RuntimeError("no supported py3 backends yet")
|
extractor = get_smda_extractor(path)
|
||||||
else:
|
else:
|
||||||
extractor = get_viv_extractor(path)
|
extractor = get_viv_extractor(path)
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import capa.features
|
|||||||
from capa.engine import *
|
from capa.engine import *
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
|
|
||||||
def test_main(z9324d_extractor):
|
def test_main(z9324d_extractor):
|
||||||
# tests rules can be loaded successfully and all output modes
|
# tests rules can be loaded successfully and all output modes
|
||||||
path = z9324d_extractor.path
|
path = z9324d_extractor.path
|
||||||
@@ -29,7 +28,6 @@ def test_main(z9324d_extractor):
|
|||||||
assert capa.main.main([path]) == 0
|
assert capa.main.main([path]) == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
|
|
||||||
def test_main_single_rule(z9324d_extractor, tmpdir):
|
def test_main_single_rule(z9324d_extractor, tmpdir):
|
||||||
# tests a single rule can be loaded successfully
|
# tests a single rule can be loaded successfully
|
||||||
RULE_CONTENT = textwrap.dedent(
|
RULE_CONTENT = textwrap.dedent(
|
||||||
@@ -58,7 +56,6 @@ def test_main_single_rule(z9324d_extractor, tmpdir):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
|
|
||||||
def test_main_non_ascii_filename(pingtaest_extractor, tmpdir, capsys):
|
def test_main_non_ascii_filename(pingtaest_extractor, tmpdir, capsys):
|
||||||
# on py2.7, need to be careful about str (which can hold bytes)
|
# on py2.7, need to be careful about str (which can hold bytes)
|
||||||
# vs unicode (which is only unicode characters).
|
# vs unicode (which is only unicode characters).
|
||||||
@@ -71,18 +68,22 @@ def test_main_non_ascii_filename(pingtaest_extractor, tmpdir, capsys):
|
|||||||
std = capsys.readouterr()
|
std = capsys.readouterr()
|
||||||
# but here, we have to use a unicode instance,
|
# but here, we have to use a unicode instance,
|
||||||
# because capsys has decoded the output for us.
|
# because capsys has decoded the output for us.
|
||||||
assert pingtaest_extractor.path.decode("utf-8") in std.out
|
if sys.version_info >= (3, 0):
|
||||||
|
assert pingtaest_extractor.path in std.out
|
||||||
|
else:
|
||||||
|
assert pingtaest_extractor.path.decode("utf-8") in std.out
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
|
|
||||||
def test_main_non_ascii_filename_nonexistent(tmpdir, caplog):
|
def test_main_non_ascii_filename_nonexistent(tmpdir, caplog):
|
||||||
NON_ASCII_FILENAME = "täst_not_there.exe"
|
NON_ASCII_FILENAME = "täst_not_there.exe"
|
||||||
assert capa.main.main(["-q", NON_ASCII_FILENAME]) == -1
|
assert capa.main.main(["-q", NON_ASCII_FILENAME]) == -1
|
||||||
|
|
||||||
assert NON_ASCII_FILENAME.decode("utf-8") in caplog.text
|
if sys.version_info >= (3, 0):
|
||||||
|
assert NON_ASCII_FILENAME in caplog.text
|
||||||
|
else:
|
||||||
|
assert NON_ASCII_FILENAME.decode("utf-8") in caplog.text
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
|
|
||||||
def test_main_shellcode(z499c2_extractor):
|
def test_main_shellcode(z499c2_extractor):
|
||||||
path = z499c2_extractor.path
|
path = z499c2_extractor.path
|
||||||
assert capa.main.main([path, "-vv", "-f", "sc32"]) == 0
|
assert capa.main.main([path, "-vv", "-f", "sc32"]) == 0
|
||||||
@@ -137,7 +138,6 @@ def test_ruleset():
|
|||||||
assert len(rules.basic_block_rules) == 1
|
assert len(rules.basic_block_rules) == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
|
|
||||||
def test_match_across_scopes_file_function(z9324d_extractor):
|
def test_match_across_scopes_file_function(z9324d_extractor):
|
||||||
rules = capa.rules.RuleSet(
|
rules = capa.rules.RuleSet(
|
||||||
[
|
[
|
||||||
@@ -201,7 +201,6 @@ def test_match_across_scopes_file_function(z9324d_extractor):
|
|||||||
assert ".text section and install service" in capabilities
|
assert ".text section and install service" in capabilities
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
|
|
||||||
def test_match_across_scopes(z9324d_extractor):
|
def test_match_across_scopes(z9324d_extractor):
|
||||||
rules = capa.rules.RuleSet(
|
rules = capa.rules.RuleSet(
|
||||||
[
|
[
|
||||||
@@ -264,7 +263,6 @@ def test_match_across_scopes(z9324d_extractor):
|
|||||||
assert "kill thread program" in capabilities
|
assert "kill thread program" in capabilities
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
|
|
||||||
def test_subscope_bb_rules(z9324d_extractor):
|
def test_subscope_bb_rules(z9324d_extractor):
|
||||||
rules = capa.rules.RuleSet(
|
rules = capa.rules.RuleSet(
|
||||||
[
|
[
|
||||||
@@ -289,7 +287,6 @@ def test_subscope_bb_rules(z9324d_extractor):
|
|||||||
assert "test rule" in capabilities
|
assert "test rule" in capabilities
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
|
|
||||||
def test_byte_matching(z9324d_extractor):
|
def test_byte_matching(z9324d_extractor):
|
||||||
rules = capa.rules.RuleSet(
|
rules = capa.rules.RuleSet(
|
||||||
[
|
[
|
||||||
@@ -312,7 +309,6 @@ def test_byte_matching(z9324d_extractor):
|
|||||||
assert "byte match test" in capabilities
|
assert "byte match test" in capabilities
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
|
|
||||||
def test_count_bb(z9324d_extractor):
|
def test_count_bb(z9324d_extractor):
|
||||||
rules = capa.rules.RuleSet(
|
rules = capa.rules.RuleSet(
|
||||||
[
|
[
|
||||||
@@ -336,7 +332,6 @@ def test_count_bb(z9324d_extractor):
|
|||||||
assert "count bb" in capabilities
|
assert "count bb" in capabilities
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
|
|
||||||
def test_fix262(pma16_01_extractor, capsys):
|
def test_fix262(pma16_01_extractor, capsys):
|
||||||
# tests rules can be loaded successfully and all output modes
|
# tests rules can be loaded successfully and all output modes
|
||||||
path = pma16_01_extractor.path
|
path = pma16_01_extractor.path
|
||||||
@@ -347,7 +342,6 @@ def test_fix262(pma16_01_extractor, capsys):
|
|||||||
assert "www.practicalmalwareanalysis.com" not in std.out
|
assert "www.practicalmalwareanalysis.com" not in std.out
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
|
|
||||||
def test_not_render_rules_also_matched(z9324d_extractor, capsys):
|
def test_not_render_rules_also_matched(z9324d_extractor, capsys):
|
||||||
# rules that are also matched by other rules should not get rendered by default.
|
# rules that are also matched by other rules should not get rendered by default.
|
||||||
# this cuts down on the amount of output while giving approx the same detail.
|
# this cuts down on the amount of output while giving approx the same detail.
|
||||||
|
|||||||
Reference in New Issue
Block a user