diff --git a/.doxygen.h b/.doxygen.h index 20ce48b5a..126663b24 100644 --- a/.doxygen.h +++ b/.doxygen.h @@ -97,6 +97,10 @@ \defgroup flowgraph Flowgraph \ingroup coreapi */ +/*! + \defgroup formatstringresolutionprovider Format String Resolution Provider + \ingroup coreapi +*/ /*! \defgroup function Function \ingroup coreapi diff --git a/binaryninjaapi.cpp b/binaryninjaapi.cpp index 34a86d413..ff0487cde 100644 --- a/binaryninjaapi.cpp +++ b/binaryninjaapi.cpp @@ -24,7 +24,6 @@ using namespace BinaryNinja; using namespace std; - struct WorkerThreadActionContext { std::function action; diff --git a/binaryninjaapi.h b/binaryninjaapi.h index 073e3bc2f..4044def2c 100644 --- a/binaryninjaapi.h +++ b/binaryninjaapi.h @@ -20941,6 +20941,52 @@ namespace BinaryNinja { virtual bool DeleteData(const std::string& key) override; }; + /*! Resolves the argument types described by a format string. + + \ingroup formatstringresolutionprovider + */ + class FormatStringResolutionProvider : public StaticCoreRefCountObject + { + std::string m_nameForRegister; + + protected: + FormatStringResolutionProvider(const std::string& name); + FormatStringResolutionProvider(BNFormatStringResolutionProvider* provider); + + static bool IsValidCallback( + void* ctxt, const char* format, BNPlatform* platform, + BNTypeWithConfidence** types, size_t* count); + static void FreeTypeListCallback(void* ctxt, BNTypeWithConfidence* types, size_t count); + + public: + /*! Resolve the argument types described by a format string. + + \param format Format string to validate and resolve + \param platform Platform whose ABI and C data model should be used to resolve types + \return A list of argument types when valid, including an empty list for a valid format with no + arguments, or an empty optional when invalid + */ + virtual std::optional>>> IsValid( + const std::string& format, Platform* platform) = 0; + + std::string GetName() const; + static std::vector> GetList(); + static Ref GetByName(const std::string& name); + static void Register(FormatStringResolutionProvider* provider); + }; + + /*! + \ingroup formatstringresolutionprovider + */ + class CoreFormatStringResolutionProvider : public FormatStringResolutionProvider + { + public: + CoreFormatStringResolutionProvider(BNFormatStringResolutionProvider* provider); + + virtual std::optional>>> IsValid( + const std::string& format, Platform* platform) override; + }; + /*! Components are objects that can contain Functions and other Components. \note Components should not be instantiated directly. Instead use BinaryView::CreateComponent() diff --git a/binaryninjacore.h b/binaryninjacore.h index e64d2fec5..e32e9b812 100644 --- a/binaryninjacore.h +++ b/binaryninjacore.h @@ -37,7 +37,7 @@ // Current ABI version for linking to the core. This is incremented any time // there are changes to the API that affect linking, including new functions, // new types, or modifications to existing functions or types. -#define BN_CURRENT_CORE_ABI_VERSION 179 +#define BN_CURRENT_CORE_ABI_VERSION 180 // Minimum ABI version that is supported for loading of plugins. Plugins that // are linked to an ABI version less than this will not be able to load and @@ -316,6 +316,7 @@ extern "C" typedef struct BNDebugInfo BNDebugInfo; typedef struct BNDebugInfoParser BNDebugInfoParser; typedef struct BNSecretsProvider BNSecretsProvider; + typedef struct BNFormatStringResolutionProvider BNFormatStringResolutionProvider; typedef struct BNLogger BNLogger; typedef struct BNSymbolQueue BNSymbolQueue; typedef struct BNTypeArchive BNTypeArchive; @@ -3744,6 +3745,14 @@ extern "C" bool (*deleteData)(void* ctxt, const char* key); } BNSecretsProviderCallbacks; + typedef struct BNFormatStringResolutionProviderCallbacks + { + void* context; + bool (*isValid)(void* ctxt, const char* format, BNPlatform* platform, + BNTypeWithConfidence** types, size_t* count); + void (*freeTypeList)(void* ctxt, BNTypeWithConfidence* types, size_t count); + } BNFormatStringResolutionProviderCallbacks; + typedef struct BNMergedVariable { BNVariable target; @@ -8727,6 +8736,20 @@ extern "C" BINARYNINJACOREAPI bool BNStoreSecretsProviderData(BNSecretsProvider* provider, const char* key, const char* data); BINARYNINJACOREAPI bool BNDeleteSecretsProviderData(BNSecretsProvider* provider, const char* key); + // Format string resolution providers + BINARYNINJACOREAPI BNFormatStringResolutionProvider* BNRegisterFormatStringResolutionProvider( + const char* name, BNFormatStringResolutionProviderCallbacks* callbacks); + BINARYNINJACOREAPI BNFormatStringResolutionProvider** BNGetFormatStringResolutionProviderList(size_t* count); + BINARYNINJACOREAPI void BNFreeFormatStringResolutionProviderList( + BNFormatStringResolutionProvider** providers); + BINARYNINJACOREAPI BNFormatStringResolutionProvider* BNGetFormatStringResolutionProviderByName(const char* name); + BINARYNINJACOREAPI char* BNGetFormatStringResolutionProviderName( + BNFormatStringResolutionProvider* provider); + BINARYNINJACOREAPI bool BNFormatStringResolutionProviderIsValid( + BNFormatStringResolutionProvider* provider, const char* format, BNPlatform* platform, + BNTypeWithConfidence** types, size_t* count); + BINARYNINJACOREAPI void BNFreeTypeWithConfidenceList(BNTypeWithConfidence* types, size_t count); + BINARYNINJACOREAPI BNSymbolQueue* BNCreateSymbolQueue(void); BINARYNINJACOREAPI void BNDestroySymbolQueue(BNSymbolQueue* queue); BINARYNINJACOREAPI void BNAppendSymbolQueue(BNSymbolQueue* queue, diff --git a/cstyleformatstringresolutionprovider.cpp b/cstyleformatstringresolutionprovider.cpp new file mode 100644 index 000000000..399c99ae4 --- /dev/null +++ b/cstyleformatstringresolutionprovider.cpp @@ -0,0 +1,498 @@ +#include "cstyleformatstringresolutionprovider.h" + +#include + +using namespace BinaryNinja; +using namespace std; + +namespace +{ + enum class LengthModifier + { + None, + HH, + H, + L, + LL, + J, + Z, + T, + CapitalL + }; + + enum FormatFlag : uint8_t + { + LeftJustifyFlag = 1 << 0, + ForceSignFlag = 1 << 1, + SpaceSignFlag = 1 << 2, + AlternateFormFlag = 1 << 3, + ZeroPadFlag = 1 << 4 + }; + + bool IsWindowsPlatform(Platform* platform) + { + return platform && (platform->GetName().find("windows") != string::npos); + } + + bool IsApplePlatform(Platform* platform) + { + if (!platform) + return false; + const string name = platform->GetName(); + return name.starts_with("mac-") || name.starts_with("ios-") || name.starts_with("tvos-") + || name.starts_with("watchos-"); + } + + optional GetAddressSize(Platform* platform) + { + if (!platform) + return nullopt; + auto arch = platform->GetArchitecture(); + if (!arch) + return nullopt; + return arch->GetAddressSize(); + } + + optional GetPlatformTypeWidth(const string& name, Platform* platform) + { + auto addressSize = GetAddressSize(platform); + if (!addressSize.has_value()) + return nullopt; + + if ((name == "long") || (name == "unsigned long")) + return IsWindowsPlatform(platform) ? 4 : *addressSize; + if ((name == "ssize_t") || (name == "size_t") || (name == "ptrdiff_t") + || (name == "unsigned ptrdiff_t")) + { + return *addressSize; + } + if (name == "wchar_t") + return IsWindowsPlatform(platform) ? 2 : 4; + if (name == "long double") + { + if (IsWindowsPlatform(platform) || IsApplePlatform(platform)) + return 8; + return *addressSize == 4 ? 12 : 16; + } + return nullopt; + } + + Confidence> IntegerArgument( + size_t width, bool isSigned, uint8_t confidence = BN_FULL_CONFIDENCE, + uint8_t signednessConfidence = BN_FULL_CONFIDENCE) + { + return Confidence>( + Type::IntegerType(width, Confidence(isSigned, signednessConfidence)), confidence); + } + + Confidence> PlatformIntegerArgument( + Platform* platform, const string& name, bool isSigned, uint8_t confidence = BN_FULL_CONFIDENCE, + uint8_t signednessConfidence = BN_FULL_CONFIDENCE) + { + auto width = GetPlatformTypeWidth(name, platform); + if (!width.has_value()) + return nullptr; + return Confidence>( + Type::IntegerType(*width, Confidence(isSigned, signednessConfidence)), confidence); + } + + Confidence> FloatArgument(size_t width, uint8_t confidence = BN_FULL_CONFIDENCE) + { + return Confidence>(Type::FloatType(width), confidence); + } + + Confidence> PointerArgument( + Platform* platform, const Confidence>& child, + uint8_t confidence = BN_FULL_CONFIDENCE) + { + auto width = GetAddressSize(platform); + if (!width.has_value() || !child.GetValue()) + return nullptr; + return Confidence>(Type::PointerType(*width, child), confidence); + } + + Confidence> SignedIntegerArgument(LengthModifier length, Platform* platform) + { + switch (length) + { + case LengthModifier::None: + case LengthModifier::HH: + case LengthModifier::H: + // Signed char and signed short always promote to int. + return IntegerArgument(4, true); + case LengthModifier::L: + return PlatformIntegerArgument(platform, "long", true); + case LengthModifier::LL: + case LengthModifier::J: + return IntegerArgument(8, true); + case LengthModifier::Z: + return PlatformIntegerArgument(platform, "ssize_t", true); + case LengthModifier::T: + return PlatformIntegerArgument(platform, "ptrdiff_t", true); + default: + return nullptr; + } + } + + Confidence> UnsignedIntegerArgument(LengthModifier length, Platform* platform) + { + switch (length) + { + case LengthModifier::None: + return IntegerArgument(4, false); + case LengthModifier::HH: + case LengthModifier::H: + // The promotion is int when it can represent every value, and unsigned int otherwise. + return IntegerArgument(4, true, BN_HEURISTIC_CONFIDENCE, 0); + case LengthModifier::L: + return PlatformIntegerArgument(platform, "unsigned long", false); + case LengthModifier::LL: + case LengthModifier::J: + return IntegerArgument(8, false); + case LengthModifier::Z: + return PlatformIntegerArgument(platform, "size_t", false); + case LengthModifier::T: + return PlatformIntegerArgument(platform, "unsigned ptrdiff_t", false); + default: + return nullptr; + } + } + + Confidence> CountPointerArgument(LengthModifier length, Platform* platform) + { + Confidence> child; + switch (length) + { + case LengthModifier::None: + child = IntegerArgument(4, true); + break; + case LengthModifier::HH: + child = IntegerArgument(1, true); + break; + case LengthModifier::H: + child = IntegerArgument(2, true); + break; + case LengthModifier::L: + child = PlatformIntegerArgument(platform, "long", true); + break; + case LengthModifier::LL: + case LengthModifier::J: + child = IntegerArgument(8, true); + break; + case LengthModifier::Z: + child = PlatformIntegerArgument(platform, "ssize_t", true); + break; + case LengthModifier::T: + child = PlatformIntegerArgument(platform, "ptrdiff_t", true); + break; + default: + return nullptr; + } + return PointerArgument(platform, child); + } + + Confidence> LongDoubleArgument(Platform* platform) + { + auto width = GetPlatformTypeWidth("long double", platform); + if (!width.has_value()) + return nullptr; + return FloatArgument(*width); + } + + bool AppendArgument( + vector>>& result, const Confidence>& argument) + { + if (!argument.GetValue()) + return false; + result.push_back(argument); + return true; + } + + bool ValidateFlags(char conversion, uint8_t flags) + { + const uint8_t signFlags = ForceSignFlag | SpaceSignFlag; + switch (conversion) + { + case 'd': + case 'i': + return (flags & AlternateFormFlag) == 0; + case 'o': + case 'x': + case 'X': + return (flags & signFlags) == 0; + case 'u': + return (flags & (signFlags | AlternateFormFlag)) == 0; + case 'f': + case 'F': + case 'e': + case 'E': + case 'g': + case 'G': + case 'a': + case 'A': + return true; + case 'c': + case 's': + case 'p': + return (flags & ~LeftJustifyFlag) == 0; + case 'n': + case '%': + return flags == 0; + default: + return false; + } + } + + bool ValidateLength(char conversion, LengthModifier length) + { + switch (conversion) + { + case 'd': + case 'i': + case 'o': + case 'u': + case 'x': + case 'X': + case 'n': + return length != LengthModifier::CapitalL; + case 'f': + case 'F': + case 'e': + case 'E': + case 'g': + case 'G': + case 'a': + case 'A': + return (length == LengthModifier::None) || (length == LengthModifier::L) + || (length == LengthModifier::CapitalL); + case 'c': + case 's': + return (length == LengthModifier::None) || (length == LengthModifier::L); + case 'p': + case '%': + return length == LengthModifier::None; + default: + return false; + } + } + + bool ValidateWidthAndPrecision(char conversion, bool widthSpecified, bool precisionSpecified) + { + if ((conversion == 'n') || (conversion == '%')) + return !widthSpecified && !precisionSpecified; + if ((conversion == 'c') || (conversion == 'p')) + return !precisionSpecified; + return true; + } + + optional>>> ResolveCStyleFormatString( + const string& format, Platform* platform) + { + vector>> result; + for (size_t i = 0; i < format.size(); i++) + { + if (format[i] != '%') + continue; + + i++; + if (i == format.size()) + return nullopt; + + uint8_t flags = 0; + bool parsingFlags = true; + while (parsingFlags && (i < format.size())) + { + switch (format[i]) + { + case '-': flags |= LeftJustifyFlag; break; + case '+': flags |= ForceSignFlag; break; + case ' ': flags |= SpaceSignFlag; break; + case '#': flags |= AlternateFormFlag; break; + case '0': flags |= ZeroPadFlag; break; + default: parsingFlags = false; continue; + } + i++; + } + + bool widthSpecified = false; + bool widthArgument = false; + if ((i < format.size()) && (format[i] == '*')) + { + widthSpecified = true; + widthArgument = true; + i++; + } + else + { + size_t widthStart = i; + while ((i < format.size()) && isdigit(static_cast(format[i]))) + i++; + widthSpecified = i != widthStart; + } + + bool precisionSpecified = false; + bool precisionArgument = false; + if ((i < format.size()) && (format[i] == '.')) + { + precisionSpecified = true; + i++; + if ((i < format.size()) && (format[i] == '*')) + { + precisionArgument = true; + i++; + } + else + { + while ((i < format.size()) && isdigit(static_cast(format[i]))) + i++; + } + } + + LengthModifier length = LengthModifier::None; + if ((i + 1 < format.size()) && (format[i] == 'h') && (format[i + 1] == 'h')) + { + length = LengthModifier::HH; + i += 2; + } + else if ((i + 1 < format.size()) && (format[i] == 'l') && (format[i + 1] == 'l')) + { + length = LengthModifier::LL; + i += 2; + } + else if (i < format.size()) + { + switch (format[i]) + { + case 'h': length = LengthModifier::H; i++; break; + case 'l': length = LengthModifier::L; i++; break; + case 'j': length = LengthModifier::J; i++; break; + case 'z': length = LengthModifier::Z; i++; break; + case 't': length = LengthModifier::T; i++; break; + case 'L': length = LengthModifier::CapitalL; i++; break; + default: break; + } + } + + if (i == format.size()) + return nullopt; + char conversion = format[i]; + if (!ValidateFlags(conversion, flags) || !ValidateLength(conversion, length) + || !ValidateWidthAndPrecision(conversion, widthSpecified, precisionSpecified)) + { + return nullopt; + } + + if (widthArgument) + result.push_back(IntegerArgument(4, true)); + if (precisionArgument) + result.push_back(IntegerArgument(4, true)); + + switch (conversion) + { + case 'd': + case 'i': + if (!AppendArgument(result, SignedIntegerArgument(length, platform))) + return nullopt; + break; + case 'o': + case 'u': + case 'x': + case 'X': + if (!AppendArgument(result, UnsignedIntegerArgument(length, platform))) + return nullopt; + break; + case 'f': + case 'F': + case 'e': + case 'E': + case 'g': + case 'G': + case 'a': + case 'A': + if (!AppendArgument(result, length == LengthModifier::CapitalL + ? LongDoubleArgument(platform) : FloatArgument(8))) + { + return nullopt; + } + break; + case 'c': + if (length == LengthModifier::L) + { + if (!AppendArgument( + result, IntegerArgument(4, true, BN_HEURISTIC_CONFIDENCE, 0))) + { + return nullopt; + } + } + else + { + if (!AppendArgument(result, IntegerArgument(4, true))) + return nullopt; + } + break; + case 's': + if (length == LengthModifier::L) + { + auto width = GetPlatformTypeWidth("wchar_t", platform); + if (!width.has_value() || !AppendArgument(result, PointerArgument(platform, + Confidence>( + Type::WideCharType(*width, "wchar_t"), BN_FULL_CONFIDENCE)))) + { + return nullopt; + } + } + else + { + if (!AppendArgument(result, PointerArgument(platform, Confidence>( + Type::IntegerType(1, Confidence(true, 0), "char"), BN_FULL_CONFIDENCE)))) + { + return nullopt; + } + } + break; + case 'p': + if (!AppendArgument(result, PointerArgument(platform, + Confidence>(Type::VoidType(), BN_FULL_CONFIDENCE)))) + { + return nullopt; + } + break; + case 'n': + if (!AppendArgument(result, CountPointerArgument(length, platform))) + return nullopt; + break; + case '%': + break; + default: + return nullopt; + } + } + return result; + } +} + + +CStyleFormatStringResolutionProvider::CStyleFormatStringResolutionProvider() : + FormatStringResolutionProvider("CStyleFormatString") +{} + + +optional>>> CStyleFormatStringResolutionProvider::IsValid( + const string& format, Platform* platform) +{ + return ResolveCStyleFormatString(format, platform); +} + + +void BinaryNinja::RegisterCStyleFormatStringResolutionProvider() +{ + static bool registered = []() { + if (!FormatStringResolutionProvider::GetByName("CStyleFormatString")) + { + Ref provider = new CStyleFormatStringResolutionProvider(); + FormatStringResolutionProvider::Register(provider); + } + return true; + }(); + (void)registered; +} diff --git a/cstyleformatstringresolutionprovider.h b/cstyleformatstringresolutionprovider.h new file mode 100644 index 000000000..5241a08cf --- /dev/null +++ b/cstyleformatstringresolutionprovider.h @@ -0,0 +1,22 @@ +#pragma once + +#include "binaryninjaapi.h" + +namespace BinaryNinja +{ + /*! Resolves C-style printf format strings to their consumed argument types. + + \ingroup formatstringresolutionprovider + */ + class CStyleFormatStringResolutionProvider : public FormatStringResolutionProvider + { + public: + CStyleFormatStringResolutionProvider(); + + std::optional>>> IsValid( + const std::string& format, Platform* platform) override; + }; + + /*! Registers the built-in C-style format string provider if it is not already registered. */ + void RegisterCStyleFormatStringResolutionProvider(); +} diff --git a/formatstringresolutionprovider.cpp b/formatstringresolutionprovider.cpp new file mode 100644 index 000000000..b5326db77 --- /dev/null +++ b/formatstringresolutionprovider.cpp @@ -0,0 +1,143 @@ +// Copyright (c) 2015-2026 Vector 35 Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +#include "binaryninjaapi.h" + +using namespace BinaryNinja; +using namespace std; + + +FormatStringResolutionProvider::FormatStringResolutionProvider(const string& name) : m_nameForRegister(name) {} + + +FormatStringResolutionProvider::FormatStringResolutionProvider(BNFormatStringResolutionProvider* provider) +{ + m_object = provider; +} + + +bool FormatStringResolutionProvider::IsValidCallback( + void* ctxt, const char* format, BNPlatform* platform, + BNTypeWithConfidence** types, size_t* count) +{ + FormatStringResolutionProvider* provider = (FormatStringResolutionProvider*)ctxt; + Ref resolvedPlatform; + if (platform) + resolvedPlatform = new CorePlatform(BNNewPlatformReference(platform)); + auto result = provider->IsValid(format, resolvedPlatform); + if (!result.has_value()) + { + *types = nullptr; + *count = 0; + return false; + } + + *count = result->size(); + if (result->empty()) + { + *types = nullptr; + return true; + } + + *types = new BNTypeWithConfidence[result->size()]; + for (size_t i = 0; i < result->size(); i++) + { + (*types)[i].type = BNNewTypeReference((*result)[i].GetValue()->GetObject()); + (*types)[i].confidence = (*result)[i].GetConfidence(); + } + return true; +} + + +void FormatStringResolutionProvider::FreeTypeListCallback( + void*, BNTypeWithConfidence* types, size_t count) +{ + for (size_t i = 0; i < count; i++) + BNFreeType(types[i].type); + delete[] types; +} + + +string FormatStringResolutionProvider::GetName() const +{ + char* name = BNGetFormatStringResolutionProviderName(m_object); + string result = name; + BNFreeString(name); + return result; +} + + +vector> FormatStringResolutionProvider::GetList() +{ + size_t count; + BNFormatStringResolutionProvider** list = BNGetFormatStringResolutionProviderList(&count); + vector> result; + result.reserve(count); + for (size_t i = 0; i < count; i++) + result.push_back(new CoreFormatStringResolutionProvider(list[i])); + BNFreeFormatStringResolutionProviderList(list); + return result; +} + + +Ref FormatStringResolutionProvider::GetByName(const string& name) +{ + BNFormatStringResolutionProvider* result = BNGetFormatStringResolutionProviderByName(name.c_str()); + if (!result) + return nullptr; + return new CoreFormatStringResolutionProvider(result); +} + + +void FormatStringResolutionProvider::Register(FormatStringResolutionProvider* provider) +{ + BNFormatStringResolutionProviderCallbacks callbacks; + callbacks.context = provider; + callbacks.isValid = IsValidCallback; + callbacks.freeTypeList = FreeTypeListCallback; + provider->AddRefForRegistration(); + provider->m_object = BNRegisterFormatStringResolutionProvider(provider->m_nameForRegister.c_str(), &callbacks); +} + + +CoreFormatStringResolutionProvider::CoreFormatStringResolutionProvider(BNFormatStringResolutionProvider* provider) : + FormatStringResolutionProvider(provider) +{} + + +optional>>> CoreFormatStringResolutionProvider::IsValid( + const string& format, Platform* platform) +{ + BNTypeWithConfidence* types = nullptr; + size_t count = 0; + if (!BNFormatStringResolutionProviderIsValid( + m_object, format.c_str(), platform ? platform->GetObject() : nullptr, &types, &count)) + return nullopt; + + vector>> result; + result.reserve(count); + for (size_t i = 0; i < count; i++) + { + result.emplace_back( + new Type(BNNewTypeReference(types[i].type)), types[i].confidence); + } + BNFreeTypeWithConfidenceList(types, count); + return result; +} diff --git a/plugins/cstyle_format_string/CMakeLists.txt b/plugins/cstyle_format_string/CMakeLists.txt new file mode 100644 index 000000000..62c5f2889 --- /dev/null +++ b/plugins/cstyle_format_string/CMakeLists.txt @@ -0,0 +1,44 @@ +cmake_minimum_required(VERSION 3.15 FATAL_ERROR) + +project(cstyle_format_string) + +file(GLOB SOURCES CONFIGURE_DEPENDS + *.cpp + *.c + *.h) + +if(DEMO) + add_library(${PROJECT_NAME} STATIC ${SOURCES}) +else() + add_library(${PROJECT_NAME} SHARED ${SOURCES}) +endif() + +if(NOT BN_INTERNAL_BUILD) + # Out-of-tree build + find_path( + BN_API_PATH + NAMES binaryninjaapi.h + HINTS ../../.. binaryninjaapi $ENV{BN_API_PATH} + REQUIRED + ) + add_subdirectory(${BN_API_PATH} api) +endif() + +target_link_libraries(${PROJECT_NAME} binaryninjaapi) + +set_target_properties(${PROJECT_NAME} PROPERTIES + CXX_STANDARD 20 + CXX_VISIBILITY_PRESET hidden + CXX_STANDARD_REQUIRED ON + C_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON + POSITION_INDEPENDENT_CODE ON) + +if(BN_INTERNAL_BUILD) + plugin_rpath(${PROJECT_NAME}) + set_target_properties(${PROJECT_NAME} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${BN_CORE_PLUGIN_DIR} + RUNTIME_OUTPUT_DIRECTORY ${BN_CORE_PLUGIN_DIR}) +else() + bn_install_plugin(${PROJECT_NAME}) +endif() diff --git a/plugins/cstyle_format_string/plugin.cpp b/plugins/cstyle_format_string/plugin.cpp new file mode 100644 index 000000000..544d3b6e2 --- /dev/null +++ b/plugins/cstyle_format_string/plugin.cpp @@ -0,0 +1,18 @@ +#include "cstyleformatstringresolutionprovider.h" + +using namespace BinaryNinja; + +extern "C" +{ + BN_DECLARE_CORE_ABI_VERSION + +#ifdef DEMO_EDITION + bool CStyleFormatStringPluginInit() +#else + BINARYNINJAPLUGIN bool CorePluginInit() +#endif + { + RegisterCStyleFormatStringResolutionProvider(); + return true; + } +} diff --git a/python/__init__.py b/python/__init__.py index 1de3ff400..cf62b5070 100644 --- a/python/__init__.py +++ b/python/__init__.py @@ -84,6 +84,7 @@ from .renderlayer import * from .constantrenderer import * from .stringrecognizer import * +from .formatstringresolutionprovider import * # We import each of these by name to prevent conflicts between # log.py and the function 'log' which we don't import below from .log import ( diff --git a/python/formatstringresolutionprovider.py b/python/formatstringresolutionprovider.py new file mode 100644 index 000000000..b76c8f596 --- /dev/null +++ b/python/formatstringresolutionprovider.py @@ -0,0 +1,204 @@ +# Copyright (c) 2015-2026 Vector 35 Inc +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +import ctypes +from typing import Any, List, Optional + +import binaryninja +import binaryninja._binaryninjacore as core +from . import platform as _platform +from . import types as _types +from .log import log_error_for_exception + + +class _FormatStringResolutionProviderMetaclass(type): + def __iter__(self): + binaryninja._init_plugins() + count = ctypes.c_ulonglong() + providers = core.BNGetFormatStringResolutionProviderList(count) + try: + for i in range(count.value): + yield FormatStringResolutionProvider(providers[i]) + finally: + core.BNFreeFormatStringResolutionProviderList(providers) + + def __getitem__(self, value): + binaryninja._init_plugins() + provider = core.BNGetFormatStringResolutionProviderByName(str(value)) + if provider is None: + raise KeyError(f"'{value}' is not a valid format string resolution provider") + return FormatStringResolutionProvider(provider) + + def __contains__(cls: '_FormatStringResolutionProviderMetaclass', name: object) -> bool: + if not isinstance(name, str): + return False + try: + cls[name] + return True + except KeyError: + return False + + def get( + cls: '_FormatStringResolutionProviderMetaclass', name: str, default: Any = None + ) -> Optional['FormatStringResolutionProvider']: + try: + return cls[name] + except KeyError: + if default is not None: + return default + return None + + +class FormatStringResolutionProvider(metaclass=_FormatStringResolutionProviderMetaclass): + """ + ``FormatStringResolutionProvider`` resolves the variadic argument types described by a format string. + + To implement a provider, subclass this class, set :py:attr:`name`, implement + :py:meth:`perform_is_valid`, and call :py:meth:`register`. The confidence attached to each returned + :py:class:`Type` is preserved when the result crosses the core API boundary. + """ + name = None + _registered_providers = [] + + def __init__(self, handle=None): + self._pending_type_lists = {} + if handle is not None: + self.handle = core.handle_of_type(handle, core.BNFormatStringResolutionProvider) + self.__dict__["name"] = core.BNGetFormatStringResolutionProviderName(handle) + + def __repr__(self): + return f"" + + def register(self): + """Register this provider with the Binary Ninja core.""" + if self.__class__.name is None: + raise ValueError("name is missing") + if hasattr(self, "handle"): + raise ValueError("provider is already registered") + + self._cb = core.BNFormatStringResolutionProviderCallbacks() + self._cb.context = 0 + self._cb.isValid = self._cb.isValid.__class__(self._is_valid) + self._cb.freeTypeList = self._cb.freeTypeList.__class__(self._free_type_list) + self.handle = core.BNRegisterFormatStringResolutionProvider(self.__class__.name, self._cb) + assert self.handle is not None, "core.BNRegisterFormatStringResolutionProvider returned None" + self.__class__._registered_providers.append(self) + + def _is_valid(self, ctxt, format_string, platform, types, count) -> bool: + types[0] = None + count[0] = 0 + try: + platform_obj = None + if platform: + platform_obj = _platform.CorePlatform._from_cache( + core.BNNewPlatformReference(platform)) + + result = self.perform_is_valid(core.pyNativeStr(format_string), platform_obj) + if result is None: + return False + + resolved_types = list(result) + for resolved_type in resolved_types: + if not isinstance(resolved_type, _types.Type): + raise TypeError("perform_is_valid must return Type objects") + + count[0] = len(resolved_types) + if not resolved_types: + return True + + output_buf = (core.BNTypeWithConfidence * len(resolved_types))() + created_count = 0 + try: + for i, resolved_type in enumerate(resolved_types): + output_buf[i].type = core.BNNewTypeReference(resolved_type.handle) + output_buf[i].confidence = resolved_type.confidence + created_count += 1 + except Exception: + for i in range(created_count): + core.BNFreeType(output_buf[i].type) + raise + + output_ptr = ctypes.cast(output_buf, ctypes.POINTER(core.BNTypeWithConfidence)) + key = ctypes.cast(output_ptr, ctypes.c_void_p).value + self._pending_type_lists[key] = (output_ptr, output_buf, len(resolved_types)) + types[0] = output_ptr + return True + except Exception: + types[0] = None + count[0] = 0 + log_error_for_exception("Unhandled Python exception in FormatStringResolutionProvider._is_valid") + return False + + def _free_type_list(self, ctxt, type_list, count): + try: + key = ctypes.cast(type_list, ctypes.c_void_p).value + if key not in self._pending_type_lists: + raise ValueError("freeing type list that wasn't allocated") + _, output_buf, output_count = self._pending_type_lists.pop(key) + for i in range(output_count): + core.BNFreeType(output_buf[i].type) + except Exception: + log_error_for_exception("Unhandled Python exception in FormatStringResolutionProvider._free_type_list") + + def perform_is_valid( + self, format_string: str, platform: Optional['_platform.Platform'] + ) -> Optional[List['_types.Type']]: + """ + Resolve the argument types described by ``format_string`` for ``platform``. + + Return ``None`` when the format is invalid, an empty list for a valid format with no arguments, + or a list of :py:class:`Type` objects for a valid format. Override this method in custom providers. + """ + raise NotImplementedError("Not implemented") + + def is_valid( + self, format_string: str, platform: Optional['_platform.Platform'] + ) -> Optional[List['_types.Type']]: + """ + Resolve the argument types described by ``format_string`` for ``platform``. + + The returned type objects carry the confidence supplied by the provider. ``None`` denotes an invalid + format; an empty list denotes a valid format that consumes no arguments. + """ + if not isinstance(format_string, str): + raise TypeError("format_string must be a string") + if platform is not None and not isinstance(platform, _platform.Platform): + raise TypeError("platform must be a Platform or None") + if not hasattr(self, "handle"): + raise ValueError("provider is not registered") + + type_list = ctypes.POINTER(core.BNTypeWithConfidence)() + count = ctypes.c_ulonglong() + valid = core.BNFormatStringResolutionProviderIsValid( + self.handle, format_string, platform.handle if platform is not None else None, type_list, count) + if not valid: + if type_list: + core.BNFreeTypeWithConfidenceList(type_list, count.value) + return None + + try: + return [ + _types.Type.create( + core.BNNewTypeReference(type_list[i].type), platform=platform, + confidence=type_list[i].confidence) + for i in range(count.value) + ] + finally: + core.BNFreeTypeWithConfidenceList(type_list, count.value) diff --git a/rust/src/format_string_resolution_provider.rs b/rust/src/format_string_resolution_provider.rs new file mode 100644 index 000000000..967bbeab0 --- /dev/null +++ b/rust/src/format_string_resolution_provider.rs @@ -0,0 +1,219 @@ +//! APIs for resolving argument types described by format strings. + +use binaryninjacore_sys::*; +use std::ffi::{c_char, c_void}; +use std::fmt::Debug; +use std::ptr::NonNull; + +use crate::confidence::Conf; +use crate::platform::Platform; +use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Ref}; +use crate::string::{raw_to_string, BnString, IntoCStr}; +use crate::types::Type; + +/// A custom provider that validates format strings and resolves their argument types. +/// +/// Providers are registered for the lifetime of the process and may be invoked from multiple +/// analysis threads. +pub trait CustomFormatStringResolutionProvider: Send + Sync + 'static { + /// Resolve the argument types described by `format` for `platform`. + /// + /// Return `None` when the string is not valid for this provider. A valid string with no + /// arguments is represented by `Some(Vec::new())`. Each returned type retains its individual + /// confidence value. + fn is_valid(&self, format: &str, platform: Option<&Platform>) -> Option>>>; +} + +/// A registered format string resolution provider. +#[derive(Clone, Copy, Hash, PartialEq, Eq)] +#[repr(transparent)] +pub struct FormatStringResolutionProvider { + handle: NonNull, +} + +impl FormatStringResolutionProvider { + pub(crate) unsafe fn from_raw(handle: NonNull) -> Self { + Self { handle } + } + + /// Register a custom format string resolution provider. + /// + /// The provider is retained for the lifetime of the process. + pub fn register

(name: &str, provider: P) -> Self + where + P: CustomFormatStringResolutionProvider, + { + let name = name.to_cstr(); + // The core registry has no unregister operation, so the callback context must remain valid + // for the lifetime of the process. + let provider = Box::leak(Box::new(provider)); + let mut callbacks = BNFormatStringResolutionProviderCallbacks { + context: provider as *mut P as *mut c_void, + isValid: Some(cb_is_valid::

), + freeTypeList: Some(cb_free_type_list), + }; + let result = + unsafe { BNRegisterFormatStringResolutionProvider(name.as_ptr(), &mut callbacks) }; + let handle = + NonNull::new(result).expect("failed to register format string resolution provider"); + unsafe { Self::from_raw(handle) } + } + + /// Retrieve all registered format string resolution providers. + pub fn all() -> Array { + let mut count = 0; + let result = unsafe { BNGetFormatStringResolutionProviderList(&mut count) }; + assert!(!result.is_null()); + unsafe { Array::new(result, count, ()) } + } + + /// Retrieve a registered format string resolution provider by name. + pub fn by_name(name: &str) -> Option { + let name = name.to_cstr(); + let result = unsafe { BNGetFormatStringResolutionProviderByName(name.as_ptr()) }; + NonNull::new(result).map(|handle| unsafe { Self::from_raw(handle) }) + } + + /// Return the provider's registration name. + pub fn name(&self) -> String { + let result = unsafe { BNGetFormatStringResolutionProviderName(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::into_string(result) } + } + + /// Resolve the argument types described by `format` for `platform`. + /// + /// Return `None` when the string is not valid for this provider. A valid string with no + /// arguments is represented by `Some(Vec::new())`. + pub fn is_valid( + &self, + format: &str, + platform: Option<&Platform>, + ) -> Option>>> { + let format = format.to_cstr(); + let mut types = std::ptr::null_mut(); + let mut count = 0; + let valid = unsafe { + BNFormatStringResolutionProviderIsValid( + self.handle.as_ptr(), + format.as_ptr(), + platform.map_or(std::ptr::null_mut(), |platform| platform.handle), + &mut types, + &mut count, + ) + }; + + if !valid { + if !types.is_null() { + unsafe { BNFreeTypeWithConfidenceList(types, count) }; + } + return None; + } + + if count == 0 { + if !types.is_null() { + unsafe { BNFreeTypeWithConfidenceList(types, count) }; + } + return Some(Vec::new()); + } + + if types.is_null() { + return None; + } + + let result = unsafe { std::slice::from_raw_parts(types, count) } + .iter() + .map(Conf::>::from_raw) + .collect(); + unsafe { BNFreeTypeWithConfidenceList(types, count) }; + Some(result) + } +} + +impl Debug for FormatStringResolutionProvider { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FormatStringResolutionProvider") + .field("name", &self.name()) + .finish() + } +} + +unsafe impl Send for FormatStringResolutionProvider {} +unsafe impl Sync for FormatStringResolutionProvider {} + +impl CoreArrayProvider for FormatStringResolutionProvider { + type Raw = *mut BNFormatStringResolutionProvider; + type Context = (); + type Wrapped<'a> = Self; +} + +unsafe impl CoreArrayProviderInner for FormatStringResolutionProvider { + unsafe fn free(raw: *mut Self::Raw, _count: usize, _context: &Self::Context) { + BNFreeFormatStringResolutionProviderList(raw); + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + let handle = + NonNull::new(*raw).expect("format string resolution provider list contained null"); + Self::from_raw(handle) + } +} + +unsafe extern "C" fn cb_is_valid

( + ctxt: *mut c_void, + format: *const c_char, + platform: *mut BNPlatform, + types: *mut *mut BNTypeWithConfidence, + count: *mut usize, +) -> bool +where + P: CustomFormatStringResolutionProvider, +{ + ffi_wrap!("CustomFormatStringResolutionProvider::is_valid", unsafe { + if types.is_null() || count.is_null() { + return false; + } + *types = std::ptr::null_mut(); + *count = 0; + + let Some(format) = raw_to_string(format) else { + return false; + }; + let provider = &*(ctxt as *const P); + let platform = NonNull::new(platform).map(|handle| Platform::from_raw(handle.as_ptr())); + let Some(result) = provider.is_valid(&format, platform.as_ref()) else { + return false; + }; + + let raw_types: Box<[BNTypeWithConfidence]> = result + .into_iter() + .map(Conf::>::into_raw) + .collect(); + *count = raw_types.len(); + if raw_types.is_empty() { + true + } else { + *types = Box::leak(raw_types).as_mut_ptr(); + true + } + }) +} + +unsafe extern "C" fn cb_free_type_list( + _ctxt: *mut c_void, + types: *mut BNTypeWithConfidence, + count: usize, +) { + ffi_wrap!( + "CustomFormatStringResolutionProvider::free_type_list", + unsafe { + if types.is_null() { + return; + } + let types = Box::from_raw(std::ptr::slice_from_raw_parts_mut(types, count)); + for ty in types { + Conf::>::free_raw(ty); + } + } + ) +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index e0da22564..340213409 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -53,6 +53,7 @@ pub mod external_library; pub mod file_accessor; pub mod file_metadata; pub mod flowgraph; +pub mod format_string_resolution_provider; pub mod function; pub mod function_recognizer; pub mod headless; diff --git a/rust/tests/format_string_resolution_provider.rs b/rust/tests/format_string_resolution_provider.rs new file mode 100644 index 000000000..fb7fe7332 --- /dev/null +++ b/rust/tests/format_string_resolution_provider.rs @@ -0,0 +1,97 @@ +use binaryninja::confidence::Conf; +use binaryninja::format_string_resolution_provider::{ + CustomFormatStringResolutionProvider, FormatStringResolutionProvider, +}; +use binaryninja::headless::Session; +use binaryninja::platform::Platform; +use binaryninja::rc::Ref; +use binaryninja::types::Type; +use serial_test::serial; + +struct TestProvider; + +impl CustomFormatStringResolutionProvider for TestProvider { + fn is_valid(&self, format: &str, platform: Option<&Platform>) -> Option>>> { + match format { + "platform" => { + let platform = platform?; + Some(vec![ + Conf::new(Type::int(platform.address_size(), true), 173), + Conf::new(Type::float(8), 91), + ]) + } + "empty" => Some(Vec::new()), + _ => None, + } + } +} + +#[test] +#[serial] +fn register_list_and_lookup_provider() { + let _session = Session::new().expect("Failed to initialize session"); + let provider = FormatStringResolutionProvider::register( + "RustFormatStringResolutionProvider.List", + TestProvider, + ); + assert_eq!(provider.name(), "RustFormatStringResolutionProvider.List"); + + let provider = + FormatStringResolutionProvider::by_name("RustFormatStringResolutionProvider.List") + .expect("registered provider is available by name"); + assert_eq!(provider.name(), "RustFormatStringResolutionProvider.List"); + assert!(FormatStringResolutionProvider::all() + .iter() + .any(|candidate| candidate.name() == "RustFormatStringResolutionProvider.List")); +} + +#[test] +#[serial] +fn custom_provider_round_trip_preserves_platform_types_and_confidence() { + let _session = Session::new().expect("Failed to initialize session"); + let provider = FormatStringResolutionProvider::register( + "RustFormatStringResolutionProvider.RoundTrip", + TestProvider, + ); + let platform = Platform::by_name("windows-x86_64").expect("windows-x86_64 exists"); + + let types = provider + .is_valid("platform", Some(&platform)) + .expect("format is valid"); + assert_eq!(types.len(), 2); + assert_eq!(types[0].contents.width(), platform.address_size() as u64); + assert_eq!(types[0].confidence, 173); + assert_eq!(types[1].contents.width(), 8); + assert_eq!(types[1].confidence, 91); + + assert!(provider + .is_valid("empty", Some(&platform)) + .expect("empty format is valid") + .is_empty()); + assert!(provider.is_valid("invalid", Some(&platform)).is_none()); + assert!(provider.is_valid("platform", None).is_none()); +} + +#[test] +#[serial] +fn native_c_style_provider_is_loaded_in_headless_sessions() { + let _session = Session::new().expect("Failed to initialize session"); + let provider = FormatStringResolutionProvider::by_name("CStyleFormatString") + .expect("native C-style provider is loaded without explicit registration"); + let windows = Platform::by_name("windows-x86_64").expect("windows-x86_64 exists"); + let linux = Platform::by_name("linux-x86_64").expect("linux-x86_64 exists"); + + let windows_types = provider + .is_valid("%ld", Some(&windows)) + .expect("%ld is a valid Windows format string"); + assert_eq!(windows_types.len(), 1); + assert_eq!(windows_types[0].contents.width(), 4); + assert_eq!(windows_types[0].confidence, u8::MAX); + + let linux_types = provider + .is_valid("%ld", Some(&linux)) + .expect("%ld is a valid Linux format string"); + assert_eq!(linux_types.len(), 1); + assert_eq!(linux_types[0].contents.width(), 8); + assert_eq!(linux_types[0].confidence, u8::MAX); +}