diff options
| author | blasty <blasty@local> | 2026-07-23 01:53:36 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-23 01:53:36 +0200 |
| commit | 0aba8653f4b585165d06714be32a0560ec5d5341 (patch) | |
| tree | 6f66c42fb8f0067dd1d94880ade26932293aa342 | |
| parent | perf: derive segment map from file_regions, not survey_binary (hex open ~3400x) (diff) | |
| download | ida-tui-0aba8653f4b585165d06714be32a0560ec5d5341.tar.gz ida-tui-0aba8653f4b585165d06714be32a0560ec5d5341.tar.xz ida-tui-0aba8653f4b585165d06714be32a0560ec5d5341.zip | |
app: 'a' — make string literal in the listing (M4 richness)
IDA's 'A' key. New make_string server tool (ida_bytes.create_strlit, auto-length
to the terminator, undefines items in the way first, returns size + decoded
contents). Program.make_string wraps it; 'a' on a listing head routes through
the existing edit plumbing (EditItemRequested kind=string -> _do_edit_item ->
make_string -> bump_items -> reopen).
Verified: make_string at a .rodata addr creates the literal and IDA auto-names
it (aUsage for 'usage'). New listing_make_string scenario drives it through the
UI; listing suite 15/0. Needs a supervisor restart.
| -rw-r--r-- | idatui/app.py | 9 | ||||
| -rw-r--r-- | idatui/domain.py | 10 | ||||
| -rw-r--r-- | server/patch_server.py | 35 | ||||
| -rw-r--r-- | tests/test_scenarios.py | 49 |
4 files changed, 102 insertions, 1 deletions
diff --git a/idatui/app.py b/idatui/app.py index 570ed0a..cc9ab03 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -900,6 +900,7 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru Binding("G,end", "goto_bottom", "Bottom", show=False), Binding("c", "define_code", "Code", show=False), Binding("d", "make_data", "Data", show=False), + Binding("a", "make_string", "Str", show=False), Binding("p", "define_func", "Func", show=False), Binding("u", "undefine", "Undef", show=False), *SearchMixin.SEARCH_BINDINGS, @@ -1071,6 +1072,9 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru def action_make_data(self) -> None: self.post_message(MakeDataRequested(self)) + def action_make_string(self) -> None: + self.post_message(EditItemRequested(self, "string")) + def cur_head(self) -> Head | None: return self._head(self.cursor) @@ -3079,12 +3083,15 @@ class IdaTui(App): def _do_edit_item(self, kind: str, ea: int) -> None: # worker context assert self.program is not None verb = {"code": "defined code", "func": "created function", - "undef": "undefined"}[kind] + "undef": "undefined", "string": "made string"}[kind] try: if kind == "code": self.program.define_code(ea) elif kind == "func": self.program.define_func(ea) + elif kind == "string": + s = self.program.make_string(ea) + verb = f"made string ({s[:24]!r})" if s else verb else: self.program.undefine(ea) except Exception as e: # noqa: BLE001 -- surface soft/hard tool errors diff --git a/idatui/domain.py b/idatui/domain.py index 58e4be9..f26354c 100644 --- a/idatui/domain.py +++ b/idatui/domain.py @@ -1127,6 +1127,16 @@ class Program: raise IDAToolError( f"make data @ {ea:#x}: {res.get('error') or 'rejected'}") + def make_string(self, ea: int, length: int = 0, kind: str = "c") -> str: + """Create a string literal at ``ea`` (IDA's 'A'); auto-length when 0. + Returns the decoded contents.""" + r = self.client.call("make_string", addr=hex(ea), length=int(length), kind=kind) + res = r if isinstance(r, dict) else {} + if not res.get("ok"): + raise IDAToolError( + f"make string @ {ea:#x}: {res.get('error') or 'rejected'}") + return res.get("text", "") + def region_label(self, ea: int) -> str: """Display name for a non-function address (segment-qualified).""" try: diff --git a/server/patch_server.py b/server/patch_server.py index 4b2661f..e283cab 100644 --- a/server/patch_server.py +++ b/server/patch_server.py @@ -187,6 +187,41 @@ def file_regions() -> dict: @tool @idasync +def make_string( + addr: Annotated[str, "Address of the string start"], + length: Annotated[int, "Length in bytes (0 = auto-detect to the terminator)"] = 0, + kind: Annotated[str, "String kind: c | c16 | c32 | pascal"] = "c", +) -> dict: + """Create a string literal at ``addr`` (IDA's 'A'). ``length`` 0 auto-detects + to the terminator. Undefines any items in the way first, like the UI does. + Returns the created byte size and the decoded contents.""" + import ida_bytes + import ida_nalt + + ea = parse_address(addr) + strtype = { + "c": ida_nalt.STRTYPE_C, + "c16": ida_nalt.STRTYPE_C_16, + "c32": ida_nalt.STRTYPE_C_32, + "pascal": ida_nalt.STRTYPE_PASCAL, + }.get(str(kind).lower(), ida_nalt.STRTYPE_C) + n = max(int(length), 0) + # Free any existing item(s) so create_strlit can carve the literal. + ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE, n if n > 0 else 1) + ok = bool(ida_bytes.create_strlit(ea, n, strtype)) + if not ok: + return {"addr": addr, "ok": False, "error": "create_strlit failed"} + size = int(ida_bytes.get_item_size(ea)) + try: + raw = ida_bytes.get_strlit_contents(ea, -1, strtype) + text = raw.decode("utf-8", "replace") if raw else "" + except Exception: + text = "" + return {"addr": addr, "ok": True, "size": size, "text": text} + + +@tool +@idasync def read_raw( addr: Annotated[str, "Start address (hex or name)"], size: Annotated[int, "Number of bytes to read"], diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 26e916f..62d86dd 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -1507,6 +1507,55 @@ async def s_listing_name_addr(c: Ctx): c.prog.bump_items() +@scenario("listing_make_string") +async def s_listing_make_string(c: Ctx): + """'a' in the listing makes a string literal at the cursor (IDA's 'A').""" + app = c.app + target = None + for start, end, name in c.prog.sections(): + if start >= end: + continue + lm = c.prog.listing(start) + if lm is None: + continue + lm.ensure(80) + h = next((h for h in lm.window(0, 80) + if h.kind == "data" and "'" in h.text), None) + if h is not None: + target = h.ea + break + if target is None: + c.check("found a string data item to remake", False) + return + A = target + try: + c.prog.undefine(A, size=8) + c.prog.bump_items() + await c.goto_ui(hex(A)) + await c.wait(lambda: app._active == "listing" and app._cur is not None + and app._cur.ea == A, 25) + c.check("target is undefined before 'a'", + c.lst.cur_head() is not None and c.lst.cur_head().kind == "unknown", + f"head={c.lst.cur_head()}") + c.lst.focus() + await c.press("a") + await c.wait(lambda: c.lst.model.index_of_ea(A) >= 0 + and c.lst.model.get(c.lst.model.index_of_ea(A)) is not None + and c.lst.model.get(c.lst.model.index_of_ea(A)).kind == "data" + and "'" in c.lst.model.get(c.lst.model.index_of_ea(A)).text, 25) + hi = c.lst.model.index_of_ea(A) + c.check("'a' creates a string literal at the cursor", + hi >= 0 and c.lst.model.get(hi).kind == "data" + and "'" in c.lst.model.get(hi).text, + f"head={c.lst.model.get(hi) if hi >= 0 else None}") + finally: + try: + c.prog.make_string(A) # restore the original string + except Exception: # noqa: BLE001 + pass + c.prog.bump_items() + + # --------------------------------------------------------------------------- # # Runner # --------------------------------------------------------------------------- # |
