From be23d13852f4efd062641957b4dcec10622fc0c6 Mon Sep 17 00:00:00 2001 From: blasty Date: Sat, 25 Jul 2026 00:51:28 +0200 Subject: strings: browse every string in the binary and jump to it (IDA's Shift+F12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ida-pro-mcp exposes no full strings list (only a filtered/capped "interesting" survey), so this is a new injected tool plus a filterable browser. * server/patch_server.py: list_strings(offset,count,min_len,refresh) — every literal from idautils.Strings() as {addr,text,len,type}, paginated, with a module-level cache keyed by min_len (rebuilding is O(n) and the browser pages the whole list). * domain: StrLit dataclass + Program.strings() — pages the full list once and caches it. * app: StringsPalette modal (mirrors SymbolPalette) — case-insensitive substring filter with the match highlighted, addr/len/text columns, ↑↓/Enter/Esc. Bodies are sanitized to one printable line (\n/\r/\t escaped, non-printables dropped, long strings clipped) so control chars can't break the layout; display strings are pre-rendered+pre-lowered once since filtering runs per keystroke. Enter jumps to the literal in the unified listing via _goto_ea. Bound to '"' and Shift+F12, plus a "Strings…" command-palette entry. Verified on echo: 150 strings listed with addr/len/text, filtering 'usage' narrows to 2 (case-insensitive), Enter lands the listing cursor on the literal. Pilot `strings` scenario 6/6; full suite 165/2-flake. --- server/patch_server.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) (limited to 'server/patch_server.py') diff --git a/server/patch_server.py b/server/patch_server.py index c217561..96c4689 100644 --- a/server/patch_server.py +++ b/server/patch_server.py @@ -635,6 +635,75 @@ def decomp_map( eas.append(hex(e)) lines.append({"ea": eas[0] if eas else None, "eas": eas}) return {"addr": hex(func.start_ea), "lines": lines} + + +_idatui_strings_cache = {} + + +def _idatui_build_strings(min_len): + """[(ea, text, length, typename)] for every string IDA found, cached by + min_len (rebuilding the list is O(n) and the browser pages through it).""" + import idautils + import ida_nalt + hit = _idatui_strings_cache.get(min_len) + if hit is not None: + return hit + tnames = {} + for nm, lbl in (("STRTYPE_C", "C"), ("STRTYPE_C_16", "utf16"), + ("STRTYPE_C_32", "utf32"), ("STRTYPE_PASCAL", "pascal")): + v = getattr(ida_nalt, nm, None) + if v is not None: + tnames[v & 0xFF] = lbl + items = [] + for s in idautils.Strings(): + if s is None: + continue + try: + text = str(s) + except Exception: # noqa: BLE001 -- undecodable literal + continue + if len(text) < min_len: + continue + st = getattr(s, "strtype", 0) & 0xFF + items.append((s.ea, text, getattr(s, "length", len(text)), + tnames.get(st, "t%d" % st))) + _idatui_strings_cache[min_len] = items + return items + + +@tool +@idasync +def list_strings( + offset: Annotated[int, "Start index into the strings list"] = 0, + count: Annotated[int, "Max strings to return (page size)"] = 2000, + min_len: Annotated[int, "Minimum string length to include"] = 4, + refresh: Annotated[bool, "Rebuild the cached strings list"] = False, +) -> dict: + """Every string literal IDA found in the binary (IDA's Shift+F12 window), + paginated: {strings:[{addr,text,len,type}], total, next_offset}. Feeds the + TUI's strings browser.""" + try: + min_len = max(int(min_len), 1) + except (TypeError, ValueError): + min_len = 4 + try: + offset = max(int(offset), 0) + except (TypeError, ValueError): + offset = 0 + try: + count = max(int(count), 1) + except (TypeError, ValueError): + count = 2000 + if refresh: + _idatui_strings_cache.pop(min_len, None) + items = _idatui_build_strings(min_len) + page = items[offset:offset + count] + return { + "strings": [{"addr": hex(ea), "text": text, "len": ln, "type": ty} + for (ea, text, ln, ty) in page], + "total": len(items), + "next_offset": offset + len(page), + } ''' SNIPPET = f"{BEGIN}\n{BODY.strip()}\n{END}\n" -- cgit v1.3.1-sl0p