diff options
| author | blasty <blasty@local> | 2026-07-25 00:51:28 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-25 00:51:28 +0200 |
| commit | be23d13852f4efd062641957b4dcec10622fc0c6 (patch) | |
| tree | b2f020fca47fc0afb472d317e91e60ba468a514f /server | |
| parent | split: follow the decomp across functions as the listing cursor crosses bounds (diff) | |
| download | ida-tui-be23d13852f4efd062641957b4dcec10622fc0c6.tar.gz ida-tui-be23d13852f4efd062641957b4dcec10622fc0c6.tar.xz ida-tui-be23d13852f4efd062641957b4dcec10622fc0c6.zip | |
strings: browse every string in the binary and jump to it (IDA's Shift+F12)
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.
Diffstat (limited to 'server')
| -rw-r--r-- | server/patch_server.py | 69 |
1 files changed, 69 insertions, 0 deletions
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" |
