104 lines
3.3 KiB
Python
104 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Static ELF analysis: header, sections, dynamic symbols, and packing hints.
|
|
|
|
Uses ``pyelftools`` when available; otherwise parses the ELF header and section table
|
|
directly so basic facts (class, type, machine, sections, entropy) are still reported.
|
|
Never executes the sample.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
import struct
|
|
import sys
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
_NETWORK = {"socket", "connect", "send", "recv", "bind", "listen", "gethostbyname"}
|
|
_PROC = {"fork", "execve", "ptrace", "clone", "kill", "system", "popen"}
|
|
_CRYPTO = {"crypt", "evp_encryptinit_ex", "aes_encrypt", "rc4"}
|
|
|
|
|
|
def entropy(data: bytes) -> float:
|
|
if not data:
|
|
return 0.0
|
|
counts = Counter(data)
|
|
n = len(data)
|
|
return -sum((c / n) * math.log2(c / n) for c in counts.values())
|
|
|
|
|
|
def analyze_with_pyelftools(path: str) -> dict:
|
|
from elftools.elf.elffile import ELFFile # type: ignore
|
|
from elftools.elf.dynamic import DynamicSection # type: ignore
|
|
with open(path, "rb") as fh:
|
|
elf = ELFFile(fh)
|
|
header = {
|
|
"class": elf.elfclass,
|
|
"type": elf["e_type"],
|
|
"machine": elf["e_machine"],
|
|
"entry": hex(elf["e_entry"]),
|
|
}
|
|
symbols: list[str] = []
|
|
needed: list[str] = []
|
|
dynsym = elf.get_section_by_name(".dynsym")
|
|
if dynsym:
|
|
symbols = [s.name for s in dynsym.iter_symbols() if s.name]
|
|
for sec in elf.iter_sections():
|
|
if isinstance(sec, DynamicSection):
|
|
for tag in sec.iter_tags():
|
|
if tag.entry.d_tag == "DT_NEEDED":
|
|
needed.append(tag.needed)
|
|
stripped = elf.get_section_by_name(".symtab") is None
|
|
low = {s.lower() for s in symbols}
|
|
return {
|
|
"header": header,
|
|
"stripped": stripped,
|
|
"needed": needed,
|
|
"dynamic_symbol_count": len(symbols),
|
|
"capabilities": {
|
|
"network": sorted(low & _NETWORK),
|
|
"process": sorted(low & _PROC),
|
|
"crypto": sorted(low & _CRYPTO),
|
|
},
|
|
}
|
|
|
|
|
|
def analyze_minimal(path: str) -> dict:
|
|
data = Path(path).read_bytes()
|
|
if data[:4] != b"\x7fELF":
|
|
raise ValueError("not an ELF")
|
|
is64 = data[4] == 2
|
|
le = data[5] == 1
|
|
endian = "<" if le else ">"
|
|
e_type = struct.unpack_from(endian + "H", data, 16)[0]
|
|
e_machine = struct.unpack_from(endian + "H", data, 18)[0]
|
|
upx = b"UPX!" in data
|
|
return {
|
|
"header": {"class": 64 if is64 else 32, "type": e_type, "machine": e_machine},
|
|
"whole_file_entropy": round(entropy(data), 3),
|
|
"upx_marker": upx,
|
|
"note": "pyelftools not installed; minimal parse",
|
|
}
|
|
|
|
|
|
def analyze(path: str) -> dict:
|
|
try:
|
|
return analyze_with_pyelftools(path)
|
|
except ImportError:
|
|
return analyze_minimal(path)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Static ELF analysis")
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
p = sub.add_parser("analyze")
|
|
p.add_argument("file")
|
|
args = parser.parse_args(argv)
|
|
print(json.dumps(analyze(args.file), indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|