aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/launch.py
blob: 9618668cd7e3b7f1df9a84f247e41cda0a0b8fa1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"""One-shot launcher: ``ida-tui foo.elf`` and you're in the TUI.

Spawns a private idalib worker (``idatui.worker``) that opens + auto-analyzes
THIS binary in its own process, talking to the TUI over a unix socket. No shared
supervisor, no HTTP: everything slow (open + analysis) happens behind the TUI's
loading overlay.

Usage:

    ida-tui /path/to/binary          # open a binary and drive it

Extras: --ttl, --no-keepalive, --rpc (all forwarded to the TUI).
"""
from __future__ import annotations

import argparse
import os
import sys

# The unpacked working-copy files IDA writes next to a `.i64` while a database is
# open. A hard-killed worker leaves them behind and the `.i64` then refuses to
# reopen ("Failed to open database"). Safe to delete when nothing holds the DB.
_LOCK_SUFFIXES = (".id0", ".id1", ".id2", ".nam", ".til")


def _log(msg: str) -> None:
    print(f"ida-tui: {msg}", file=sys.stderr)


def _sweep_locks(binary: str) -> int:
    """Remove stale unpacked DB files next to ``binary``. Returns how many."""
    stem = os.path.splitext(binary)[0]
    n = 0
    for base in (binary, stem):  # IDA may key on the full name or the stem
        for suf in _LOCK_SUFFIXES:
            try:
                os.remove(base + suf)
                n += 1
            except OSError:
                pass
    return n


def main(argv: list[str] | None = None) -> int:
    p = argparse.ArgumentParser(
        prog="ida-tui",
        description="Open a binary in the IDA TUI (private idalib worker).")
    p.add_argument("binary", nargs="*",
                   help="binary to open and analyze (several with --project "
                        "creates/extends that project)")
    p.add_argument("--project", metavar="FILE",
                   help="open a multi-binary project (created from the given "
                        "binaries if FILE doesn't exist)")
    p.add_argument("--ttl", type=int, default=1800,
                   help="worker idle-TTL seconds (default 1800)")
    p.add_argument("--no-keepalive", action="store_true",
                   help="do not run the keepalive heartbeat")
    p.add_argument("--rpc", metavar="PATH",
                   help="listen for RPC on this unix socket (puppeteer the TUI)")
    args = p.parse_args(argv)

    project = None
    binary = None
    if args.project:
        from .project import Project, ProjectError
        ppath = os.path.abspath(os.path.expanduser(args.project))
        try:
            if os.path.isfile(ppath):
                project = Project.load(ppath)
                if args.binary:  # extend an existing project, skipping repeats
                    before = len(project.refs)
                    for b in args.binary:
                        project.add(b)
                    added = len(project.refs) - before
                    dupes = len(args.binary) - added
                    if added:
                        project.save()
                        _log(f"added {added} binary(ies) to {ppath}")
                    if dupes:
                        _log(f"{dupes} already in the project (matched by path) "
                             f"— left alone")
            elif args.binary:
                project = Project.create(ppath, args.binary)
                _log(f"created project {ppath} with {len(project.refs)} binaries")
            else:
                _log(f"no such project: {ppath} (pass binaries to create it)")
                return 2
        except ProjectError as e:
            _log(str(e))
            return 2
        # Everything IDA writes lives in the project's sidecar, so the source
        # tree is never touched and never needs to be writable.
        try:
            project.stage_all(progress=lambda m: _log(m))
        except ProjectError as e:
            _log(str(e))
            return 2
    else:
        if len(args.binary) != 1:
            _log("give exactly one binary, or use --project for several")
            return 2
        binary = os.path.abspath(os.path.expanduser(args.binary[0]))
        if not os.path.isfile(binary):
            _log(f"no such file: {binary}")
            return 2
        if not os.access(os.path.dirname(binary), os.W_OK):
            _log(f"directory not writable (IDA writes a .i64 there): "
                 f"{os.path.dirname(binary)}")
            return 2
        swept = _sweep_locks(binary)  # a crashed worker can leave the DB wedged
        if swept:
            _log(f"cleared {swept} stale lock file(s) from a crashed worker")

    # Hand off to the TUI (imported late so --help works without textual). It
    # spawns the worker behind its loading overlay while auto-analysis runs.
    try:
        from .app import IdaTui
    except ImportError as e:
        _log(f"the TUI needs textual; run with ~/ida-venv/bin/python  ({e})")
        return 1
    rpc_path = os.path.abspath(os.path.expanduser(args.rpc)) if args.rpc else None
    IdaTui(open_path=binary, keepalive=not args.no_keepalive,
           rpc_path=rpc_path, ttl=args.ttl, project=project).run()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())