mirror of
https://github.com/mandiant/capa.git
synced 2025-12-05 20:40:05 -08:00
initial commit of out-of-the box flirt-based library id
This commit is contained in:
164
capa/analysis/flirt.py
Normal file
164
capa/analysis/flirt.py
Normal file
@@ -0,0 +1,164 @@
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import rich
|
||||
from rich.console import Console
|
||||
from rich.logging import RichHandler
|
||||
|
||||
import capa.helpers
|
||||
import capa.features.extractors.ida.idalib as idalib
|
||||
|
||||
if not idalib.has_idalib():
|
||||
raise RuntimeError("cannot find IDA idalib module.")
|
||||
|
||||
if not idalib.load_idalib():
|
||||
raise RuntimeError("failed to load IDA idalib module.")
|
||||
|
||||
import idaapi
|
||||
import idapro
|
||||
import ida_auto
|
||||
import idautils
|
||||
import ida_funcs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def colorbool(v: bool) -> str:
|
||||
if v:
|
||||
return f"[green]{str(v)}[/green]"
|
||||
else:
|
||||
return f"[red]{str(v)}[/red]"
|
||||
|
||||
|
||||
def colorname(n: str) -> str:
|
||||
if n.startswith("sub_"):
|
||||
return n
|
||||
else:
|
||||
return f"[cyan]{n}[/cyan]"
|
||||
|
||||
|
||||
class FunctionId(BaseModel):
|
||||
address: int
|
||||
is_library: bool
|
||||
is_thunk: bool
|
||||
name: str
|
||||
|
||||
def to_row(self):
|
||||
row = [hex(self.address)]
|
||||
row.append(colorbool(self.is_library))
|
||||
row.append(colorbool(self.is_thunk))
|
||||
row.append(colorname(self.name))
|
||||
return row
|
||||
|
||||
|
||||
def configure_logging(args):
|
||||
if args.quiet:
|
||||
logging.getLogger().setLevel(logging.WARNING)
|
||||
elif args.debug:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
# use [/] after the logger name to reset any styling,
|
||||
# and prevent the color from carrying over to the message
|
||||
logformat = "[dim]%(name)s[/]: %(message)s"
|
||||
|
||||
# set markup=True to allow the use of Rich's markup syntax in log messages
|
||||
rich_handler = RichHandler(markup=True, show_time=False, show_path=True, console=capa.helpers.log_console)
|
||||
rich_handler.setFormatter(logging.Formatter(logformat))
|
||||
|
||||
# use RichHandler for root logger
|
||||
logging.getLogger().addHandler(rich_handler)
|
||||
|
||||
if args.debug:
|
||||
logging.getLogger("capa").setLevel(logging.DEBUG)
|
||||
logging.getLogger("viv_utils").setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.getLogger("capa").setLevel(logging.ERROR)
|
||||
logging.getLogger("viv_utils").setLevel(logging.ERROR)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
parser = argparse.ArgumentParser(description="Identify library functions using FLIRT.")
|
||||
parser.add_argument(
|
||||
"input_file",
|
||||
type=Path,
|
||||
help="path to file to analyze",
|
||||
)
|
||||
parser.add_argument("-d", "--debug", action="store_true", help="enable debugging output on STDERR")
|
||||
parser.add_argument("-q", "--quiet", action="store_true", help="disable all output but errors")
|
||||
args = parser.parse_args(args=argv)
|
||||
|
||||
configure_logging(args)
|
||||
|
||||
time0 = time.time()
|
||||
|
||||
# stderr=True is used here to redirect the spinner banner to stderr, so that users can redirect capa's output.
|
||||
console = Console(stderr=True, quiet=False)
|
||||
|
||||
logger.debug("idalib: opening database...")
|
||||
# idalib writes to stdout (ugh), so we have to capture that
|
||||
# so as not to screw up structured output.
|
||||
with capa.helpers.stdout_redirector(io.BytesIO()):
|
||||
with console.status("analyzing program...", spinner="dots"):
|
||||
if idapro.open_database(str(args.input_file), run_auto_analysis=True):
|
||||
raise RuntimeError("failed to analyze input file")
|
||||
|
||||
logger.debug("idalib: waiting for analysis...")
|
||||
|
||||
# TODO: add more signature (files)
|
||||
# TOOD: apply more signatures
|
||||
|
||||
ida_auto.auto_wait()
|
||||
logger.debug("idalib: opened database.")
|
||||
|
||||
table = rich.table.Table()
|
||||
table.add_column("FVA")
|
||||
table.add_column("library?")
|
||||
table.add_column("thunk?")
|
||||
table.add_column("name")
|
||||
|
||||
LIBONLY = True
|
||||
count = 0
|
||||
|
||||
for ea in idautils.Functions(start=None, end=None):
|
||||
f = idaapi.get_func(ea)
|
||||
is_thunk = bool(f.flags & idaapi.FUNC_THUNK)
|
||||
is_lib = bool(f.flags & idaapi.FUNC_LIB)
|
||||
fname = idaapi.get_func_name(ea)
|
||||
|
||||
if LIBONLY and not is_lib:
|
||||
continue
|
||||
|
||||
fid = FunctionId(address=ea, is_library=is_lib, is_thunk=is_thunk, name=fname)
|
||||
table.add_row(*fid.to_row())
|
||||
|
||||
count += 1
|
||||
if count > 50:
|
||||
break
|
||||
|
||||
rich.print(table)
|
||||
|
||||
# TODO can we include which signature matched per function?
|
||||
for index in range(0, ida_funcs.get_idasgn_qty()):
|
||||
signame, optlibs, nmatches = ida_funcs.get_idasgn_desc_with_matches(index)
|
||||
rich.print(signame, optlibs, nmatches)
|
||||
|
||||
idapro.close_database()
|
||||
|
||||
min, sec = divmod(time.time() - time0, 60)
|
||||
logger.debug("FLIRT-based library identification ran for ~ %02d:%02dm", min, sec)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user