""" Example Rust ``&str`` string recognizer and data renderer. Rust represents a string slice (``&str``) as a two word "fat pointer": a pointer to the UTF-8 bytes followed by the length of the slice. The type for it looks like:: struct &str { char* string; // offset 0: pointer to the UTF-8 bytes uint64_t length; // offset 8: number of bytes }; This plugin allows Binary Ninja to recover the underlying text in two situations: * **Structure initializers.** When the optimizer folds the field assignments of a ``&str`` value into a single ``HLIL_STRUCT_INIT`` expression, the recognizer reads ``length`` bytes from ``string`` and renders the literal in place. * **Constant pointers to ``&str`` data variables.** When code takes the address of a ``&str`` data variable, the recognizer reads the fat pointer out of that data variable and renders the string it points at. The recognized strings use the ``rs`` prefix, so they render as ``rs"..."``. A matching data renderer renders ``&str`` data variables the same way in linear view. """ from typing import Dict, List, Optional from binaryninja import BinaryView, Type from binaryninja.datarender import DataRenderer, TypeContext from binaryninja.enums import ( DerivedStringLocationType, InstructionTextTokenType, TypeClass) from binaryninja.function import DisassemblyTextLine, InstructionTextToken from binaryninja.highlevelil import HighLevelILFunction, HighLevelILInstruction from binaryninja.stringrecognizer import CustomStringType, StringRecognizer from binaryninja.types import NamedTypeReferenceType from binaryninja.binaryview import DerivedString, DerivedStringLocation # Exact name of the Rust string slice type we recognize. str_type_name = "&str" # Register a custom string type so the core knows how to render the strings we # recover. The prefix turns "..." into rs"...". rust_str_type = CustomStringType.register(str_type_name, string_prefix="rs") def _type_name(type: Optional[Type]) -> Optional[str]: """Return the registered/reference name of a type, or None if it has none. A `&str` value arrives either as a named type reference (`type.name`) or, once resolved, as the underlying structure carrying a registered name.""" if type is None: return None if isinstance(type, NamedTypeReferenceType): return str(type.name) registered = type.registered_name if registered is not None: return str(registered.name) return None def _is_str_type(type: Optional[Type]) -> bool: """True if `type` is exactly the `&str` type.""" return _type_name(type) == str_type_name def _is_pointer_to_str(type: Optional[Type]) -> bool: """True if `type` is a pointer to the `&str` type.""" return type is not None and type.type_class == TypeClass.PointerTypeClass and _is_str_type(type.target) def _derived_string_from_slice(bv: BinaryView, pointer: int, length: int) -> Optional[DerivedString]: """Read `length` UTF-8 bytes at `pointer` and wrap them in a DerivedString. The returned string is data-backed location pointing at the bytes so that the rendered literal cross-references the underlying string data.""" if length < 0: return None data = bv.read(pointer, length) if data is None or len(data) != length: return None location = DerivedStringLocation(DerivedStringLocationType.DataBackedStringLocation, pointer, length) return DerivedString(data, location, rust_str_type) def _read_str_data_var(bv: BinaryView, addr: int) -> Optional[DerivedString]: """Reads the `&str` fat pointer stored at `addr` and renders the string it points to.""" addr_size = bv.address_size pointer = bv.read_pointer(addr) raw_length = bv.read(addr + addr_size, addr_size) if raw_length is None or len(raw_length) != addr_size: return None length = int.from_bytes(raw_length, "little") return _derived_string_from_slice(bv, pointer, length) class RustStrRecognizer(StringRecognizer): """Recognizes Rust `&str` slices in HLIL expressions.""" recognizer_name = "Rust &str" def is_valid_for_type(self, func: HighLevelILFunction, type: Type) -> bool: # Run for `&str` structure initializers and for constant pointers to a # `&str` data variable; skip every other expression type. return _is_str_type(type) or _is_pointer_to_str(type) def recognize_struct_init( self, instr: HighLevelILInstruction, type: Type, vals: Dict[int, int] ) -> Optional[DerivedString]: # `vals` maps each constant field offset to its value: offset 0 is the # pointer to the bytes, offset at address size is the length of the slice. addr_size = instr.function.view.address_size if 0 not in vals or addr_size not in vals: return None pointer = vals[0] length = vals[addr_size] return _derived_string_from_slice(instr.function.view, pointer, length) def recognize_constant_pointer( self, instr: HighLevelILInstruction, type: Type, val: int ) -> Optional[DerivedString]: # Only resolve when a `&str` data variable actually lives at the pointer. bv = instr.function.view data_var = bv.get_data_var_at(val) if data_var is None or not _is_str_type(data_var.type): return None return _read_str_data_var(bv, val) class RustStrDataRenderer(DataRenderer): """Renders `&str` data variables as `rs"..."` in linear view.""" def perform_is_valid_for_data( self, ctxt, view: BinaryView, addr: int, type: Type, context: List[TypeContext] ) -> bool: return _is_str_type(type) and _read_str_data_var(view, addr) is not None def perform_get_lines_for_data( self, ctxt, view: BinaryView, addr: int, type: Type, prefix: List[InstructionTextToken], width: int, context: List[TypeContext] ) -> List[DisassemblyTextLine]: derived = _read_str_data_var(view, addr) tokens = list(prefix) if derived is None: # We verified this in `perform_is_valid_for_data`, but handle the case of failing to # fetch the string in case the data variable has changed since the check. tokens.append(InstructionTextToken(InstructionTextTokenType.TextToken, str(type))) return [DisassemblyTextLine(tokens, addr)] # `&str` is UTF-8 by definition; escape control characters and quotes for display. text = bytes(derived.value).decode("utf-8", "replace") escaped = text.encode("unicode_escape").decode("ascii").replace('"', '\\"') # `prefix` already carries the ` = ` tokens, just append the literal. tokens.append(InstructionTextToken(InstructionTextTokenType.BraceToken, f'rs"')) tokens.append(InstructionTextToken(InstructionTextTokenType.StringToken, escaped)) tokens.append(InstructionTextToken(InstructionTextTokenType.BraceToken, '"')) return [DisassemblyTextLine(tokens, addr)] def __del__(self): pass RustStrRecognizer().register() RustStrDataRenderer().register_type_specific()