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.
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.
Im Projektordner (enthält pyproject.toml):
pip install -e ".[dev]"Das installiert:
- die Bibliothek
idatool(importierbar alsimport 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-depsBei 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.
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 0x4035B8export.txtist die vom IDA-Plugin erzeugte Textdatei.- EAs (Adressen) werden als Hex-String übergeben, z.B.
0x4035B8. --rootbeiskeletonist optional; ohne Angabe wird automatisch die im Export als[TOP LEVEL]markierte Funktion verwendet.--max-depthbegrenzt zusätzlich, wie tief die Baum-Ansicht aufgeklappt wird (unabhängig von Rekursions-/Diamant-Erkennung).
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).
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 EAprog.function(...) wirft idatool.program.FunctionNotFound, wenn die
EA/der Name unbekannt ist. prog.block(...) wirft entsprechend
idatool.program.BlockNotFound.
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)# 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)")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 zeigenfrom 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))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- 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.