Custom Strings and Constants¶
A StringRecognizer decodes strings from HLIL expressions and returns them as a DerivedString. The decoded value need not exist in the binary. Derived strings render inline in decompilation, appear in the strings list, and support cross references. Use this for obfuscated strings, length-prefixed or fat-pointer string types, and stack strings.
A ConstantRenderer does the same for non-string constants, emitting tokens directly instead of returning a value.
Both receive the type of the expression, so custom type attributes can select which expressions they apply to and carry parameters such as a decoding key.
Tip
For strings stored literally but in a non-ASCII encoding (cp932, GBK, EUC-KR), enable the appropriate code pages instead. See Unicode Support.
Custom String Types¶
A CustomStringType is the registered name for a kind of string, along with the prefix and postfix used to render it. The name appears in the strings list type column.
from binaryninja import CustomStringType
rust_str_type = CustomStringType.register("Rust &str", string_prefix="rs") # rs"hello"
encoded_string_type = CustomStringType.register("Encoded", "", "_enc") # "hello"_enc
Derived Strings¶
A DerivedString holds:
value— the decoded byteslocation— optional DerivedStringLocation, eitherDataBackedStringLocation(a range in the binary) orCodeStringLocation(assembled by code). Required for cross references.custom_type— theCustomStringTypeused to render it
loc = DerivedStringLocation(DerivedStringLocationType.DataBackedStringLocation, addr, length)
DerivedString(b"decoded text", loc, encoded_string_type)
All derived strings in a view are available through bv.derived_strings, and their uses through bv.get_derived_string_code_refs(str).
String Recognizers¶
Subclass StringRecognizer, set recognizer_name, override the callbacks you need, and call register() on an instance. Each returns a DerivedString, or None to pass.
| Method | Called for |
|---|---|
recognize_constant |
A constant that is not a pointer |
recognize_constant_pointer |
A constant pointer |
recognize_extern_pointer |
An external symbol, with an offset into it |
recognize_import |
An imported symbol |
recognize_constant_data |
HLIL_CONST_DATA, produced by outlining from scattered stores |
recognize_struct_init |
HLIL_STRUCT_INIT, a run of constant field assignments folded into one initializer |
is_valid_for_type is an optional filter. Override it to skip the recognizer for types that cannot match; the callbacks otherwise run on every constant in every function.
Warning
Python string recognizers can be slow. The callbacks run on every constant in every function, and each call crosses the FFI boundary, builds wrapper objects, and takes the GIL, serializing analysis threads. Always override is_valid_for_type to reject types you don't handle — it is the cheapest way to stay off the hot path. For large binaries or recognizers that must inspect many expressions, write it in C++ instead.
String recognizers only run on code. To render matching data variables, register a DataRenderer alongside the recognizer. rust_string.py does this for Rust &str slices, recovering them from both struct initializers and data variables.
Example: Attribute-Driven Deobfuscation¶
encoded_strings.py reads both the decoder and its key out of a type attribute:
class EncodedStringRecognizer(StringRecognizer):
recognizer_name = "encoded_strings"
decoders = {
"xor_encoded": lambda encoded, key: encoded ^ key,
"sub_encoded": lambda encoded, key: (encoded - key) & 0xff,
"add_encoded": lambda encoded, key: (encoded + key) & 0xff
}
def is_valid_for_type(self, func, type):
if not isinstance(type, PointerType):
return False
return any(name in type.target.attributes for name in self.__class__.decoders)
def recognize_constant_pointer(self, instr, type, val):
... # decode using the hex key in the attribute
loc = DerivedStringLocation(DerivedStringLocationType.DataBackedStringLocation, val, i)
return DerivedString(result, loc, encoded_string_type)
EncodedStringRecognizer().register()
Declare a type carrying the key and apply it to the decoding routine's parameter. Type propagation carries it to every call site:
typedef char __attr("sub_encoded", "31656537366531313932396130373434")* deobfuscate;
The 5.2 release notes work through this on an Amadey sample.
Warning
This example in particular can seriously slow down analysis: it reads the binary one byte at a time from Python for each string it decodes. It is written for clarity, not speed.
Constant Renderers¶
Subclass ConstantRenderer, set renderer_name, and call register(). Callbacks emit to the tokens emitter and return True if they handled the constant.
| Method | Called for |
|---|---|
render_constant |
A constant that is not a pointer |
render_constant_pointer |
A constant pointer |
is_valid_for_type |
Optional filter, as with string recognizers |
The same performance warning applies: constant renderers run while rendering every constant, so filter aggressively in is_valid_for_type and use C++ for anything expensive.
bid64_constant.py renders BID64 decimal floating point constants, keying off the BID_UINT64 typedef name:
class Bid64ConstantRenderer(ConstantRenderer):
renderer_name = "bid64_constant"
def render_constant(self, instr, type, val, tokens, settings, precedence):
if not isinstance(type, IntegerType) or type.width != 8:
return False
if type.registered_name is None or type.registered_name.name != 'BID_UINT64':
return False
... # decode into `value`
tokens.append(InstructionTextToken(InstructionTextTokenType.FloatingPointToken, str(value) + "_bid"))
return True
Bid64ConstantRenderer().register()
This also covers constants that stand in for strings absent from the binary, such as precomputed API hashes.
C++ API¶
BinaryNinja::StringRecognizer, BinaryNinja::ConstantRenderer, BinaryNinja::CustomStringType::Register, and the DerivedString / DerivedStringLocation structures mirror the Python API. Recognizer callbacks return std::optional<DerivedString>. This avoids the per-expression FFI and GIL costs entirely, and is the recommended approach for anything running over a large binary.
Examples¶
- encoded_strings.py — attribute-driven XOR/add/sub string deobfuscation
- rust_string.py — Rust
&strrecognizer plus a matching data renderer - bid64_constant.py — BID64 constant renderer
- examples/encoded_strings — C++ port of
encoded_strings.py - examples/bid64_constant — C++ port of
bid64_constant.py, with a sample binary