trueToastedCode/idatool

★ 0Forks 0PythonGitHub ↗Compare

README

idatool

Python-Bibliothek + CLI zum Einlesen, Verknüpfen und Abfragen von Text-Exporten, wie sie die IDA-Pro-Plugins copy_deep.py / ea_tools_common.py erzeugen.

Was das Projekt macht

Die IDA-Plugins exportieren eine Funktion (und optional rekursiv alle von ihr aufgerufenen Funktionen) als reinen Text: pro Funktion ein Header-Block, darunter ihre Basic Blocks mit Disassembly-Zeilen und Sprungziel-Kommentaren ([True/Take] / [False/Fall]).

idatool parst diesen Text zurück in ein strukturiertes Modell (Function, Block), berechnet daraus zusätzlich den vollständigen Call-Graphen (wer ruft wen auf – in beide Richtungen, nicht nur wie im Text-Export als informativer Kommentar), und bietet mehrere Sichten darauf: eine reine Struktur-Übersicht ("Skelett"), einzelne Blöcke, einzelne Funktionen (flach oder rekursiv mit allen Callees), sowie Caller/Callee-Abfragen.

Das Modell ist intern ein Graph, keine feste Baumstruktur. Die Baum-Darstellung (skeleton(), function_deep()) wird bei Bedarf per Tiefensuche ab einem gewählten Startpunkt erzeugt. Dabei wird zwischen zwei Sonderfällen unterschieden:

  • Rekursion: Eine Funktion ruft (direkt oder über Umwege) sich selbst wieder auf. Wird erkannt und nicht endlos weiterverfolgt.
  • Diamant: Eine Funktion wird von mehreren Stellen aus aufgerufen, ist aber keine Rekursion. Sie wird nur an ihrer ersten Fundstelle vollständig ausgegeben, an weiteren Stellen nur referenziert ("bereits gezeigt, Pfad: ...").

Funktionen, die im Export nie eine eigene Sektion haben (z.B. externe Bibliotheks-/API-Aufrufe wie GetModuleFileNameA, oder Ziele, die im Export schlicht nicht enthalten waren), werden als "Stub" geführt: has_body=False, keine Blocks, kein weiteres Aufklappen möglich.

Installation

Im Projektordner (enthält pyproject.toml):

pip install -e ".[dev]"

Das installiert:

  • die Bibliothek idatool (importierbar als import idatool)
  • das Kommandozeilentool idatool
  • pytest (Test-Framework, weil [dev] mitinstalliert wurde)

Neuinstallation erzwingen (z.B. nach Änderungen an pyproject.toml):

pip install -e ".[dev]" --force-reinstall --no-deps

Bei reinen Code-Änderungen innerhalb von idatool/*.py ist keine Neuinstallation nötig – editable install verweist direkt auf den Quellordner. Ein neuer Python-Prozess (z.B. Interpreter neu starten) reicht, um die Änderung zu sehen.

Kommandozeilennutzung

idatool skeleton export.txt
idatool skeleton export.txt --root 0x6939A0 --max-depth 3
idatool block export.txt 0x6939A8
idatool function-flat export.txt 0x4035B8
idatool function-deep export.txt 0x6939A0
idatool callers export.txt 0x4035B8
idatool callees export.txt 0x4035B8
  • export.txt ist die vom IDA-Plugin erzeugte Textdatei.
  • EAs (Adressen) werden als Hex-String übergeben, z.B. 0x4035B8.
  • --root bei skeleton ist optional; ohne Angabe wird automatisch die im Export als [TOP LEVEL] markierte Funktion verwendet.
  • --max-depth begrenzt zusätzlich, wie tief die Baum-Ansicht aufgeklappt wird (unabhängig von Rekursions-/Diamant-Erkennung).

Nutzung als Python-Bibliothek

Einlesen

from idatool.program import Program

prog = Program.from_file("export.txt")
# alternativ, wenn der Text schon als String vorliegt:
# prog = Program.from_text(text)

Program.from_file öffnet die Datei aktuell mit Standard-Encoding (open(path, "r"), also UTF-8 unter Linux). Bekannte Einschränkung: Ältere Exporte, die von ea_tools_common.py unter Windows ohne explizites Encoding geschrieben wurden, können einzelne nicht-UTF-8-Bytes enthalten (z.B. IDA-Auto-Kommentare wie mov eax, 0E2h ; 'â') und dann beim Einlesen einen UnicodeDecodeError auslösen. Das ist kein Fehler im Export selbst, sondern eine Encoding-Inkonsistenz zwischen Schreiben (Windows-Default, vermutlich cp1252) und Lesen (UTF-8-Default unter Linux).

Grundlegende Abfragen

prog.top_level()                 # Function-Objekt der [TOP LEVEL]-Funktion, oder None
prog.all_functions()             # Liste aller bekannten Function-Objekte (inkl. Stubs)

f = prog.function(0x4035B8)      # per EA (int, z.B. 0x4035B8)
f = prog.function_by_name("sub_4035B8")   # per Namen (z.B. für Stubs ohne EA)

b = prog.block(0x6939A8)         # Block-Objekt per EA

prog.function(...) wirft idatool.program.FunctionNotFound, wenn die EA/der Name unbekannt ist. prog.block(...) wirft entsprechend idatool.program.BlockNotFound.

Textausgaben

print(prog.skeleton())                          # Baum-Übersicht ab [TOP LEVEL], ohne Disassembly-Inhalt
print(prog.skeleton(root_ea=0x4035B8))           # ab beliebiger Funktion starten
print(prog.skeleton(max_depth=2))                # Tiefe begrenzen

print(prog.block_text(0x6939A8))                 # ein einzelner Block: Disassembly + Take/Fall + wer springt/ruft hierher

print(prog.function_flat(0x4035B8))              # eine Funktion, nur ihre eigenen Blocks (keine Callees)

print(prog.function_deep(0x6939A0))              # eine Funktion + rekursiv alle Callees (mit Diamant-/Rekursions-Markierung)

Call-Graph-Abfragen

# Wer ruft diese Funktion auf? (Liste von Function-Objekten)
for caller in prog.callers_of(0x4035B8):
    print(caller.name, hex(caller.start_ea) if caller.start_ea else "(kein EA, Stub)")

# Was ruft diese Funktion auf? (inkl. externer Stubs ohne eigene EA)
for callee in prog.callees_of(0x4035B8):
    print(callee.name, hex(callee.start_ea) if callee.start_ea else "(kein EA, Stub)")

Direkter Zugriff auf das Datenmodell

Falls die fertigen Textausgaben/Abfragen nicht reichen, sind die zugrunde liegenden Objekte frei zugänglich (idatool/models.py):

f = prog.function(0x4035B8)

f.name                    # z.B. "sub_4035B8"
f.start_ea                # EA als int, oder None wenn Stub ohne bekannte EA
f.has_body                # False = extern/Stub, keine Blocks vorhanden
f.is_top_level            # True für die Export-Startfunktion
f.blocks                  # dict[int, Block], EA -> Block, in Einlesereihenfolge
f.calls_to                # set[int]: EAs aller aufgerufenen Funktionen (mit bekannter EA)
f.calls_to_stub_names     # set[str]: Namen aufgerufener Funktionen OHNE bekannte EA
f.called_by               # set[int]: EAs aller Funktionen, die diese Funktion aufrufen

b = prog.block(0x6939A8)
b.start_ea, b.end_ea       # EA-Grenzen des Blocks (end_ea kann None sein, letzter Block einer Funktion)
b.raw_lines                # rohe Disassembly-Textzeilen (ohne Take/Fall-Kommentare)
b.takes, b.falls           # Listen von Ziel-EAs (Sprung genommen / nicht genommen)
b.calls                    # Liste von CallRef(target_name, target_ea, source_line_ea)
b.jumped_from              # von welchen Block-EAs aus per Sprung erreichbar
b.called_from              # Liste von CallRef, die per 'call' hierher zeigen

Beispiel-Ablauf (typische Session)

from idatool.program import Program

prog = Program.from_file("export.txt")

top = prog.top_level()
print(f"Einstiegspunkt: {top.name} @ 0x{top.start_ea:X}")

print(prog.skeleton(max_depth=3))

meist_gerufene = sorted(prog.all_functions(), key=lambda f: len(f.called_by), reverse=True)
for f in meist_gerufene[:5]:
    ea_str = f"0x{f.start_ea:X}" if f.start_ea else "(Stub)"
    print(f"{f.name} {ea_str}: {len(f.called_by)}x aufgerufen")

print(prog.function_deep(top.start_ea))

Tests ausführen

pytest            # alle Tests
pytest -v         # mit Testnamen
pytest tests/test_linker.py -v      # nur eine Datei
pytest -k "recursion"               # alle Tests mit "recursion" im Namen

Bekannte Einschränkungen

  • Nur ein einzelner Export wird pro Program-Instanz verarbeitet (kein Zusammenführen mehrerer Export-Dateien zu einem gemeinsamen Graphen).
  • Call-Ziele über Register/indirekte Calls (call eax, call [ebx+4]) werden als Stub mit dem Register-/Ausdruckstext als "Namen" geführt, nicht aufgelöst.
  • Encoding-Problem bei älteren, nicht explizit UTF-8 geschriebenen Exporten (siehe oben, Abschnitt "Einlesen") ist aktuell nicht automatisch behoben.

Contributors

trueToastedCode

Issues