|
| 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