Skip to content

Commit 06c0905

Browse files
committed
Add example plugin to recognize and display Rust string slices
1 parent 9f7a427 commit 06c0905

2 files changed

Lines changed: 163 additions & 0 deletions

File tree

python/examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ These plugins are able to operate in either the GUI or as stand-alone plugins
5151
* nes.py - 6502 CPU architecture including LLIL lifting and `.NES` file format parser
5252
* nfs.py - nsf file format loader (music files extracted from NES roms)
5353
* notification_callbacks.py - example plugin showing notification callbacks that can be used in the UI or headless
54+
* rust_string.py - recovers Rust `&str` strings using a [StringRecognizer](https://api.binary.ninja/binaryninja.stringrecognizer-module.html) (for structure initializers and constant pointers to `&str` data variables) and a matching [DataRenderer](https://api.binary.ninja/binaryninja.datarender-module.html#binaryninja.datarender.DataRenderer)
5455
* typelib_create.py - Example script from the [Type Library documentation](https://docs.binary.ninja/dev/annotation.html#type-libraries)
5556
* typelib_dump.py - Example script from the [Type Library documentation](https://docs.binary.ninja/dev/annotation.html#type-libraries)
5657

python/examples/rust_string.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
"""
2+
Example Rust ``&str`` string recognizer and data renderer.
3+
4+
Rust represents a string slice (``&str``) as a two word "fat pointer": a pointer
5+
to the UTF-8 bytes followed by the length of the slice. The type for it looks like::
6+
7+
struct &str
8+
{
9+
char* string; // offset 0: pointer to the UTF-8 bytes
10+
uint64_t length; // offset 8: number of bytes
11+
};
12+
13+
This plugin allows Binary Ninja to recover the underlying text in two situations:
14+
15+
* **Structure initializers.** When the optimizer folds the field assignments of
16+
a ``&str`` value into a single ``HLIL_STRUCT_INIT`` expression, the recognizer
17+
reads ``length`` bytes from ``string`` and renders the literal in place.
18+
19+
* **Constant pointers to ``&str`` data variables.** When code takes the address
20+
of a ``&str`` data variable, the recognizer reads the fat pointer out of that
21+
data variable and renders the string it points at.
22+
23+
The recognized strings use the ``rs`` prefix, so they render as ``rs"..."``. A
24+
matching data renderer renders ``&str`` data variables the same way in linear view.
25+
"""
26+
27+
from typing import Dict, List, Optional
28+
29+
from binaryninja import BinaryView, Type
30+
from binaryninja.datarender import DataRenderer, TypeContext
31+
from binaryninja.enums import (
32+
DerivedStringLocationType, InstructionTextTokenType, TypeClass)
33+
from binaryninja.function import DisassemblyTextLine, InstructionTextToken
34+
from binaryninja.highlevelil import HighLevelILFunction, HighLevelILInstruction
35+
from binaryninja.stringrecognizer import CustomStringType, StringRecognizer
36+
from binaryninja.types import NamedTypeReferenceType
37+
from binaryninja.binaryview import DerivedString, DerivedStringLocation
38+
39+
# Exact name of the Rust string slice type we recognize.
40+
str_type_name = "&str"
41+
42+
# Register a custom string type so the core knows how to render the strings we
43+
# recover. The prefix turns "..." into rs"...".
44+
rust_str_type = CustomStringType.register(str_type_name, string_prefix="rs")
45+
46+
47+
def _type_name(type: Optional[Type]) -> Optional[str]:
48+
"""Return the registered/reference name of a type, or None if it has none.
49+
50+
A `&str` value arrives either as a named type reference (`type.name`) or,
51+
once resolved, as the underlying structure carrying a registered name."""
52+
if type is None:
53+
return None
54+
if isinstance(type, NamedTypeReferenceType):
55+
return str(type.name)
56+
registered = type.registered_name
57+
if registered is not None:
58+
return str(registered.name)
59+
return None
60+
61+
62+
def _is_str_type(type: Optional[Type]) -> bool:
63+
"""True if `type` is exactly the `&str` type."""
64+
return _type_name(type) == str_type_name
65+
66+
67+
def _is_pointer_to_str(type: Optional[Type]) -> bool:
68+
"""True if `type` is a pointer to the `&str` type."""
69+
return type is not None and type.type_class == TypeClass.PointerTypeClass and _is_str_type(type.target)
70+
71+
72+
def _derived_string_from_slice(bv: BinaryView, pointer: int, length: int) -> Optional[DerivedString]:
73+
"""Read `length` UTF-8 bytes at `pointer` and wrap them in a DerivedString.
74+
75+
The returned string is data-backed location pointing at the bytes so
76+
that the rendered literal cross-references the underlying string data."""
77+
if length < 0:
78+
return None
79+
data = bv.read(pointer, length)
80+
if data is None or len(data) != length:
81+
return None
82+
location = DerivedStringLocation(DerivedStringLocationType.DataBackedStringLocation, pointer, length)
83+
return DerivedString(data, location, rust_str_type)
84+
85+
86+
def _read_str_data_var(bv: BinaryView, addr: int) -> Optional[DerivedString]:
87+
"""Reads the `&str` fat pointer stored at `addr` and renders the string it points to."""
88+
addr_size = bv.address_size
89+
pointer = bv.read_pointer(addr)
90+
raw_length = bv.read(addr + addr_size, addr_size)
91+
if raw_length is None or len(raw_length) != addr_size:
92+
return None
93+
length = int.from_bytes(raw_length, "little")
94+
return _derived_string_from_slice(bv, pointer, length)
95+
96+
97+
class RustStrRecognizer(StringRecognizer):
98+
"""Recognizes Rust `&str` slices in HLIL expressions."""
99+
recognizer_name = "Rust &str"
100+
101+
def is_valid_for_type(self, func: HighLevelILFunction, type: Type) -> bool:
102+
# Run for `&str` structure initializers and for constant pointers to a
103+
# `&str` data variable; skip every other expression type.
104+
return _is_str_type(type) or _is_pointer_to_str(type)
105+
106+
def recognize_struct_init(
107+
self, instr: HighLevelILInstruction, type: Type, vals: Dict[int, int]
108+
) -> Optional[DerivedString]:
109+
# `vals` maps each constant field offset to its value: offset 0 is the
110+
# pointer to the bytes, offset at address size is the length of the slice.
111+
addr_size = instr.function.view.address_size
112+
if 0 not in vals or addr_size not in vals:
113+
return None
114+
pointer = vals[0]
115+
length = vals[addr_size]
116+
return _derived_string_from_slice(instr.function.view, pointer, length)
117+
118+
def recognize_constant_pointer(
119+
self, instr: HighLevelILInstruction, type: Type, val: int
120+
) -> Optional[DerivedString]:
121+
# Only resolve when a `&str` data variable actually lives at the pointer.
122+
bv = instr.function.view
123+
data_var = bv.get_data_var_at(val)
124+
if data_var is None or not _is_str_type(data_var.type):
125+
return None
126+
return _read_str_data_var(bv, val)
127+
128+
129+
class RustStrDataRenderer(DataRenderer):
130+
"""Renders `&str` data variables as `rs"..."` in linear view."""
131+
132+
def perform_is_valid_for_data(
133+
self, ctxt, view: BinaryView, addr: int, type: Type, context: List[TypeContext]
134+
) -> bool:
135+
return _is_str_type(type) and _read_str_data_var(view, addr) is not None
136+
137+
def perform_get_lines_for_data(
138+
self, ctxt, view: BinaryView, addr: int, type: Type, prefix: List[InstructionTextToken],
139+
width: int, context: List[TypeContext]
140+
) -> List[DisassemblyTextLine]:
141+
derived = _read_str_data_var(view, addr)
142+
tokens = list(prefix)
143+
if derived is None:
144+
# We verified this in `perform_is_valid_for_data`, but handle the case of failing to
145+
# fetch the string in case the data variable has changed since the check.
146+
tokens.append(InstructionTextToken(InstructionTextTokenType.TextToken, str(type)))
147+
return [DisassemblyTextLine(tokens, addr)]
148+
# `&str` is UTF-8 by definition; escape control characters and quotes for display.
149+
text = bytes(derived.value).decode("utf-8", "replace")
150+
escaped = text.encode("unicode_escape").decode("ascii").replace('"', '\\"')
151+
# `prefix` already carries the `<type> <name> = ` tokens, just append the literal.
152+
tokens.append(InstructionTextToken(InstructionTextTokenType.BraceToken, f'rs"'))
153+
tokens.append(InstructionTextToken(InstructionTextTokenType.StringToken, escaped))
154+
tokens.append(InstructionTextToken(InstructionTextTokenType.BraceToken, '"'))
155+
return [DisassemblyTextLine(tokens, addr)]
156+
157+
def __del__(self):
158+
pass
159+
160+
161+
RustStrRecognizer().register()
162+
RustStrDataRenderer().register_type_specific()

0 commit comments

Comments
 (0)