-
Notifications
You must be signed in to change notification settings - Fork 294
Expand file tree
/
Copy pathrust_string.py
More file actions
162 lines (128 loc) · 6.58 KB
/
Copy pathrust_string.py
File metadata and controls
162 lines (128 loc) · 6.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
"""
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 `<type> <name> = ` 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()