mirror of
https://github.com/lief-project/LIEF
synced 2026-06-08 15:30:44 +00:00
287 lines
8.2 KiB
Python
287 lines
8.2 KiB
Python
#!/usr/bin/env python
|
|
"""Extract PE binary features for analysis and machine learning.
|
|
|
|
Extracts structural features from PE files suitable for malware detection,
|
|
binary classification, or general analysis. Outputs a JSON report of the
|
|
binary's characteristics.
|
|
|
|
Example:
|
|
|
|
$ python pe_extract_features.py malware.exe
|
|
{
|
|
"filename": "malware.exe",
|
|
"header": { ... },
|
|
"sections": [ ... ],
|
|
"imports": { ... },
|
|
"exports": { ... },
|
|
"rich_header": { ... },
|
|
...
|
|
}
|
|
|
|
$ python pe_extract_features.py --summary program.exe
|
|
Feature Summary: program.exe
|
|
================================================
|
|
File Size: 102400 bytes
|
|
Sections: 5
|
|
Imports: 3 libraries, 47 functions
|
|
...
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
import sys
|
|
from collections import Counter
|
|
|
|
import lief
|
|
|
|
|
|
def compute_entropy(data):
|
|
"""Compute Shannon entropy of a byte sequence."""
|
|
if not data:
|
|
return 0.0
|
|
counter = Counter(data)
|
|
length = len(data)
|
|
entropy = 0.0
|
|
for count in counter.values():
|
|
p = count / length
|
|
if p > 0:
|
|
entropy -= p * math.log2(p)
|
|
return round(entropy, 4)
|
|
|
|
|
|
def extract_header_features(binary):
|
|
"""Extract PE header features."""
|
|
hdr = binary.header
|
|
opt = binary.optional_header
|
|
|
|
return {
|
|
"machine": str(hdr.machine),
|
|
"number_of_sections": hdr.numberof_sections,
|
|
"timestamp": hdr.time_date_stamps,
|
|
"characteristics": [str(c) for c in hdr.characteristics_list],
|
|
"magic": str(opt.magic),
|
|
"major_linker_version": opt.major_linker_version,
|
|
"minor_linker_version": opt.minor_linker_version,
|
|
"sizeof_code": opt.sizeof_code,
|
|
"sizeof_initialized_data": opt.sizeof_initialized_data,
|
|
"sizeof_uninitialized_data": opt.sizeof_uninitialized_data,
|
|
"entrypoint": opt.addressof_entrypoint,
|
|
"imagebase": opt.imagebase,
|
|
"section_alignment": opt.section_alignment,
|
|
"file_alignment": opt.file_alignment,
|
|
"sizeof_image": opt.sizeof_image,
|
|
"sizeof_headers": opt.sizeof_headers,
|
|
"dll_characteristics": [str(c) for c in opt.dll_characteristics_list],
|
|
"subsystem": str(opt.subsystem),
|
|
}
|
|
|
|
|
|
def extract_section_features(binary):
|
|
"""Extract features for each PE section."""
|
|
sections = []
|
|
for section in binary.sections:
|
|
raw = bytes(section.content)
|
|
sections.append(
|
|
{
|
|
"name": section.name,
|
|
"virtual_size": section.virtual_size,
|
|
"virtual_address": section.virtual_address,
|
|
"sizeof_raw_data": section.size,
|
|
"entropy": compute_entropy(raw),
|
|
"characteristics": [str(c) for c in section.characteristics_lists],
|
|
}
|
|
)
|
|
return sections
|
|
|
|
|
|
def extract_import_features(binary):
|
|
"""Extract import table features."""
|
|
imports = {}
|
|
total_functions = 0
|
|
|
|
for imp in binary.imports:
|
|
functions = [
|
|
entry.name if not entry.is_ordinal else f"ord({entry.ordinal})"
|
|
for entry in imp.entries
|
|
]
|
|
imports[imp.name] = functions
|
|
total_functions += len(functions)
|
|
|
|
return {
|
|
"libraries": list(imports.keys()),
|
|
"library_count": len(imports),
|
|
"function_count": total_functions,
|
|
"details": imports,
|
|
}
|
|
|
|
|
|
def extract_export_features(binary):
|
|
"""Extract export table features."""
|
|
export = binary.get_export()
|
|
if export is None:
|
|
return {"name": None, "count": 0, "entries": []}
|
|
|
|
entries = []
|
|
for entry in export.entries:
|
|
e = {"name": entry.name, "ordinal": entry.ordinal}
|
|
if entry.is_forwarded:
|
|
fwd = entry.forward_information
|
|
e["forwarded_to"] = f"{fwd.library}.{fwd.function}"
|
|
entries.append(e)
|
|
|
|
return {
|
|
"name": export.name,
|
|
"count": len(entries),
|
|
"entries": entries,
|
|
}
|
|
|
|
|
|
def extract_rich_header_features(binary):
|
|
"""Extract Rich header features (compiler toolchain info)."""
|
|
if binary.rich_header is None:
|
|
return None
|
|
|
|
entries = []
|
|
for entry in binary.rich_header.entries:
|
|
entries.append(
|
|
{
|
|
"build_id": entry.build_id,
|
|
"count": entry.count,
|
|
"id": entry.id,
|
|
}
|
|
)
|
|
|
|
return {
|
|
"entries": entries,
|
|
"key": binary.rich_header.key,
|
|
}
|
|
|
|
|
|
def extract_signature_features(binary):
|
|
"""Extract Authenticode signature info."""
|
|
if not binary.signatures:
|
|
return {"signed": False}
|
|
|
|
sigs = []
|
|
for sig in binary.signatures:
|
|
certs = []
|
|
for cert in sig.certificates:
|
|
certs.append(
|
|
{
|
|
"subject": cert.subject,
|
|
"issuer": cert.issuer,
|
|
"serial_number": cert.serial_number.hex(),
|
|
"valid_from": str(cert.valid_from),
|
|
"valid_to": str(cert.valid_to),
|
|
}
|
|
)
|
|
sigs.append(
|
|
{
|
|
"version": sig.version,
|
|
"digest_algorithm": str(sig.digest_algorithm),
|
|
"certificates": certs,
|
|
}
|
|
)
|
|
|
|
result = binary.verify_signature()
|
|
return {
|
|
"signed": True,
|
|
"valid": result == lief.PE.Signature.VERIFICATION_FLAGS.OK,
|
|
"verification": str(result),
|
|
"signatures": sigs,
|
|
}
|
|
|
|
|
|
def extract_all_features(filename):
|
|
"""Extract all features from a PE binary."""
|
|
binary = lief.PE.parse(filename)
|
|
if binary is None:
|
|
return None
|
|
|
|
return {
|
|
"filename": filename,
|
|
"header": extract_header_features(binary),
|
|
"sections": extract_section_features(binary),
|
|
"imports": extract_import_features(binary),
|
|
"exports": extract_export_features(binary),
|
|
"rich_header": extract_rich_header_features(binary),
|
|
"signature": extract_signature_features(binary),
|
|
}
|
|
|
|
|
|
def print_summary(features):
|
|
"""Print a human-readable summary of extracted features."""
|
|
print(f"Feature Summary: {features['filename']}")
|
|
print("=" * 48)
|
|
|
|
hdr = features["header"]
|
|
print(f" {'Machine:':<28} {hdr['machine']}")
|
|
print(f" {'Subsystem:':<28} {hdr['subsystem']}")
|
|
print(f" {'Entrypoint:':<28} 0x{hdr['entrypoint']:x}")
|
|
print(f" {'Image Size:':<28} {hdr['sizeof_image']} bytes")
|
|
print()
|
|
|
|
sections = features["sections"]
|
|
print(f" Sections ({len(sections)}):")
|
|
for sec in sections:
|
|
flag = ""
|
|
if sec["entropy"] > 7.0:
|
|
flag = " [HIGH ENTROPY]"
|
|
elif sec["entropy"] < 0.5 and sec["sizeof_raw_data"] > 0:
|
|
flag = " [LOW ENTROPY]"
|
|
print(
|
|
f" {sec['name']:<12} size={sec['sizeof_raw_data']:<8} entropy={sec['entropy']:.2f}{flag}"
|
|
)
|
|
print()
|
|
|
|
imp = features["imports"]
|
|
print(
|
|
f" Imports: {imp['library_count']} libraries, {imp['function_count']} functions"
|
|
)
|
|
for lib in imp["libraries"]:
|
|
print(f" {lib} ({len(imp['details'][lib])} functions)")
|
|
print()
|
|
|
|
exp = features["exports"]
|
|
if exp["count"] > 0:
|
|
print(f" Exports: {exp['count']} functions (DLL name: {exp['name']})")
|
|
print()
|
|
|
|
sig = features["signature"]
|
|
print(f" Signed: {'Yes' if sig['signed'] else 'No'}")
|
|
if sig["signed"]:
|
|
print(f" Signature Valid: {'Yes' if sig['valid'] else 'No'}")
|
|
print()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Extract PE binary features for analysis"
|
|
)
|
|
parser.add_argument("binaries", nargs="+", help="PE binaries to analyze")
|
|
parser.add_argument(
|
|
"--summary",
|
|
action="store_true",
|
|
help="Print human-readable summary instead of JSON",
|
|
)
|
|
parser.add_argument(
|
|
"--indent", type=int, default=2, help="JSON indentation level (default: 2)"
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
for path in args.binaries:
|
|
features = extract_all_features(path)
|
|
if features is None:
|
|
print(f"Error: failed to parse '{path}' as PE", file=sys.stderr)
|
|
continue
|
|
|
|
if args.summary:
|
|
print_summary(features)
|
|
else:
|
|
print(json.dumps(features, indent=args.indent))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|