Skip to content

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 bytes
  • location — optional DerivedStringLocation, either DataBackedStringLocation (a range in the binary) or CodeStringLocation (assembled by code). Required for cross references.
  • custom_type — the CustomStringType used 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