diff options
| author | Glenn Smith <glenn@vector35.com> | 2025-07-30 17:41:52 -0400 |
|---|---|---|
| committer | Glenn Smith <glenn@vector35.com> | 2025-08-01 21:37:38 -0400 |
| commit | 2060680204071270772751237542247038184451 (patch) | |
| tree | 964082345369d7636faf370c5c35dd065850bd8b /examples | |
| parent | be8c0c03e5d784ff7d2909d120cda6566ac2cea6 (diff) | |
Add unflatten workflow example C++ port
Diffstat (limited to 'examples')
| -rw-r--r-- | examples/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | examples/workflows/unflatten/CMakeLists.txt | 40 | ||||
| -rw-r--r-- | examples/workflows/unflatten/library.cpp | 456 |
3 files changed, 497 insertions, 0 deletions
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 69f8064e..57355026 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory(breakpoint) add_subdirectory(x86_extension) add_subdirectory(workflows/inliner) add_subdirectory(workflows/tailcall) +add_subdirectory(workflows/unflatten) # Personal+ UI plugins (headless UI binaries is a weird concept but I guess technically possible) if(NOT HEADLESS) diff --git a/examples/workflows/unflatten/CMakeLists.txt b/examples/workflows/unflatten/CMakeLists.txt new file mode 100644 index 00000000..3a87a11e --- /dev/null +++ b/examples/workflows/unflatten/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.15 FATAL_ERROR) + +project(workflow_unflatten CXX) + +file(GLOB SOURCES + *.cpp + *.c + *.h) + +add_library(${PROJECT_NAME} SHARED ${SOURCES}) + +if(NOT BN_API_BUILD_EXAMPLES AND 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} PUBLIC binaryninjaapi) + +set_target_properties(${PROJECT_NAME} PROPERTIES + CXX_STANDARD 20 + CXX_VISIBILITY_PRESET hidden + CXX_STANDARD_REQUIRED ON + C_STANDARD 99 + C_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}) +endif()
\ No newline at end of file diff --git a/examples/workflows/unflatten/library.cpp b/examples/workflows/unflatten/library.cpp new file mode 100644 index 00000000..062b928e --- /dev/null +++ b/examples/workflows/unflatten/library.cpp @@ -0,0 +1,456 @@ +// MIT License +// +// Copyright (c) 2015-2024 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" +#include "mediumlevelilinstruction.h" + +/* + This workflow reverses the control-flow flattening algorithm of Limoncello[1], + at least some of the time. It does this in a relatively simple manner: + + 1. Find "dispatcher blocks" which contain a MLIL_JUMP_TO whose targets all have the + dispatcher as a post-dominator (ie all targets must flow back to the dispatcher) + 2. Walk backwards from all unconditional branches into the dispatcher until finding blocks + with conditional branches and build a list of all unconditional continuation blocks + for each block that flows into the dispatcher. + 3. Copy those continuation blocks and the dispatcher into each block that calls them. + This leaves you with a copy of the dispatcher block (and intermediate bookkeeping) in + every path that would have normally flowed into the dispatcher. + 4. Since each path now has its own copy of the dispatcher, use MLIL dataflow to solve for + which branch of the dispatcher is taken at each copy. When a dispatcher with a solved + target is encountered, rewrite it as an MLIL_GOTO with the target directly. + + [1] https://github.com/jonpalmisc/limoncello + */ + +using namespace BinaryNinja; + +/*! + * Determine if a block looks like a CFF dispatcher, i.e. all outgoing edges + * post-dominate it (or return) + * @param block The block to check + * @return True if it's a dispatcher + */ +bool IsDispatcher(Ref<BasicBlock> block) +{ + for (auto i = block->GetStart(); i < block->GetEnd(); i++) + { + auto ins = block->GetMediumLevelILFunction()->GetInstruction(i); + if (ins.operation == MLIL_JUMP_TO) + { + for (auto& edge: block->GetOutgoingEdges()) + { + auto outBlock = edge.target; + // This is too trivial for fancier cases with multiple blocks that all lead + // to a return, but whatever this is just a sample plugin + if (outBlock->GetOutgoingEdges().empty()) + { + continue; + } + auto postDominators = outBlock->GetDominators(true); + if (postDominators.find(block) == postDominators.end()) + { + return false; + } + } + return true; + } + } + return false; +} + + +Ref<FlowGraph> GraphPls(Ref<MediumLevelILFunction> fn) +{ + // For ReportCollection, create graph with the settings I want + Ref<DisassemblySettings> settings = new DisassemblySettings(); + settings->SetOption(ShowAddress, true); + return fn->CreateFunctionGraphImmediate(settings); +} + + +BNHighlightColor GetHighlightColor(BNHighlightStandardColor color) +{ + BNHighlightColor highlight; + highlight.style = StandardHighlightColor; + highlight.color = color; + highlight.mixColor = NoHighlightColor; + highlight.mix = 0; + highlight.r = 0; + highlight.g = 0; + highlight.b = 0; + highlight.alpha = 255; + return highlight; +} + + +void RewriteAction(Ref<AnalysisContext> context, bool doIt) +{ + // Main workflow action + + // ================================================================================== + // Custom debug report + + Ref<ReportCollection> report; + if (context->GetFunction()->CheckForDebugReport("unflatten")) + { + report = new ReportCollection(); + } + + try + { + [&]() + { + if (report) + { + auto graph = GraphPls(context->GetMediumLevelILFunction()); + report->AddGraphReport(context->GetBinaryView(), "Initial", graph); + } + + // ============================================================================== + // Finding flattened control flow and resolving continuations + + // Look for dispatcher block + Ref<BasicBlock> dispatcher; + for (auto block: context->GetMediumLevelILFunction()->GetBasicBlocks()) + { + if (IsDispatcher(block)) + { + dispatcher = block; + break; + } + } + if (!dispatcher) + { + return; + } + + if (report) + { + auto graph = GraphPls(context->GetMediumLevelILFunction()); + auto nodes = graph->GetNodes(); + for (size_t i = 0; i < nodes.size(); i++) + { + auto node = nodes[i]; + if (node->GetBasicBlock()->GetStart() == dispatcher->GetStart()) + { + node->SetHighlight(GetHighlightColor(RedHighlightColor)); + } + graph->ReplaceNode(i, node); + } + report->AddGraphReport(context->GetBinaryView(), "Finding Dispatcher", graph); + } + + // Find blocks that flow directly into the dispatcher + std::deque<Ref<BasicBlock>> queue; + std::map<Ref<BasicBlock>, std::deque<Ref<BasicBlock>>> toCopy; + for (auto& incoming: dispatcher->GetIncomingEdges()) + { + auto dominators = incoming.target->GetDominators(true); + if (dominators.find(dispatcher) != dominators.end()) + { + toCopy[incoming.target] = {dispatcher}; + queue.push_back(incoming.target); + } + } + + // For each of these, walk back along unconditional branch edges to find their + // list of continuation blocks which will be copied at their end + while (!queue.empty()) + { + // This is sorf of a backwards BFS + auto top = queue.front(); + queue.pop_front(); + bool anyConditional = false; + for (auto& incoming: top->GetIncomingEdges()) + { + if (incoming.type != UnconditionalBranch) + { + anyConditional = true; + } + } + // Having any conditional branches ends the unconditional chain + if (!anyConditional) + { + for (auto& incoming: top->GetIncomingEdges()) + { + if (incoming.type == UnconditionalBranch) + { + if (toCopy.find(incoming.target) == toCopy.end()) + { + queue.push_back(incoming.target); + } + toCopy[incoming.target] = toCopy[top]; + toCopy[incoming.target].push_front(top); + } + } + toCopy[top] = {}; + } + } + + // Ignore empty continuation paths + std::vector<Ref<BasicBlock>> toPrune; + for (auto& [bb, path]: toCopy) + { + if (path.empty()) + { + toPrune.push_back(bb); + } + } + for (auto& bb: toPrune) + { + toCopy.erase(bb); + } + + if (report) + { + for (auto& [bb, path]: toCopy) + { + auto graph = GraphPls(context->GetMediumLevelILFunction()); + auto nodes = graph->GetNodes(); + for (size_t i = 0; i < nodes.size(); i++) + { + auto node = nodes[i]; + if (node->GetBasicBlock()->GetStart() == bb->GetStart()) + { + node->SetHighlight(GetHighlightColor(RedHighlightColor)); + } + else if (std::find_if(path.begin(), path.end(), [node](Ref<BasicBlock> b) { return b->GetStart() == node->GetBasicBlock()->GetStart(); }) != path.end()) + { + node->SetHighlight(GetHighlightColor(GreenHighlightColor)); + } + graph->ReplaceNode(i, node); + } + report->AddGraphReport(context->GetBinaryView(), " Blocks flowing into", graph); + } + } + + // ========================================================================== + // Modify the IL to copy the continuations into all the blocks calling the dispatcher + + auto oldMLIL = context->GetMediumLevelILFunction(); + Ref<MediumLevelILFunction> newMLIL = new MediumLevelILFunction(oldMLIL->GetArchitecture(), oldMLIL->GetFunction(), context->GetLowLevelILFunction()); + newMLIL->PrepareToCopyFunction(oldMLIL); + std::map<Ref<BasicBlock>, size_t> blockMapStarts; + + // Copy all instructions in all blocks of the old version of the function + for (auto& block: oldMLIL->GetBasicBlocks()) + { + newMLIL->PrepareToCopyBlock(block); + blockMapStarts[block] = newMLIL->GetInstructionCount(); + + newMLIL->SetCurrentAddress(block->GetArchitecture(), oldMLIL->GetInstruction(block->GetStart()).address); + for (size_t instrIndex = block->GetStart(); instrIndex < block->GetEnd(); instrIndex++) + { + auto oldInstr = oldMLIL->GetInstruction(instrIndex); + + // Copy continuation blocks to end of block calling dispatcher + if (toCopy.find(block) != toCopy.end()) + { + if (instrIndex == block->GetEnd() - 1) + { + // For every block in the continuation, copy it at the end of this block + for (auto& copyBlock: toCopy[block]) + { + newMLIL->PrepareToCopyBlock(copyBlock); + + // Skip the final instruction in the continuations because it is a MLIL_GOTO + size_t end = copyBlock->GetEnd() - 1; + if (copyBlock == dispatcher) + { + end = copyBlock->GetEnd(); + } + + for (size_t copyBlockInstrIndex = copyBlock->GetStart(); copyBlockInstrIndex < end; copyBlockInstrIndex++) + { + // Copy instruction as-is + auto copyBlockInstr = oldMLIL->GetInstruction(copyBlockInstrIndex); + newMLIL->SetCurrentAddress(copyBlock->GetArchitecture(), copyBlockInstr.address); + newMLIL->AddInstruction(copyBlockInstr.CopyTo(newMLIL)); + } + } + continue; + } + } + + // Otherwise, copy the instruction as-is + newMLIL->SetCurrentAddress(block->GetArchitecture(), oldInstr.address); + newMLIL->AddInstruction(oldInstr.CopyTo(newMLIL)); + } + } + + // Generate blocks and SSA (for dataflow) for the next part + newMLIL->Finalize(); + newMLIL->GenerateSSAForm(); + + // Since we're constructing a new function twice, we need to commit the mappings + // of the intermediate function before copying again so that mappings will resolve + // all the way to the end (gross) + // TODO: Construct from another function without needing this + context->SetMediumLevelILFunction(newMLIL); + + if (report) + { + auto graph = GraphPls(context->GetMediumLevelILFunction()); + report->AddGraphReport(context->GetBinaryView(), "Swapped dispatch with jump_to", graph); + } + + // ========================================================================== + // Now convert all MLIL_JUMP_TO with a known dest to a jump + + // Maybe this should be a separate workflow action (so it can be composed) + oldMLIL = newMLIL; + newMLIL = new MediumLevelILFunction(oldMLIL->GetArchitecture(), oldMLIL->GetFunction(), context->GetLowLevelILFunction()); + newMLIL->PrepareToCopyFunction(oldMLIL); + + // Keep a running list of blocks and labels so we can line up the MLIL_GOTOs + std::map<size_t, MediumLevelILLabel> blockLabels; + + for (auto& block: oldMLIL->GetBasicBlocks()) + { + newMLIL->PrepareToCopyBlock(block); + newMLIL->SetCurrentAddress(block->GetArchitecture(), oldMLIL->GetInstruction(block->GetStart()).address); + + // Update block label list for the MLIL_GOTOs + if (blockLabels.find(block->GetStart()) == blockLabels.end()) + { + blockLabels[block->GetStart()] = MediumLevelILLabel{}; + } + MediumLevelILLabel* label = &blockLabels[block->GetStart()]; + newMLIL->MarkLabel(*label); + + for (size_t instrIndex = block->GetStart(); instrIndex < block->GetEnd(); instrIndex++) + { + auto oldInstr = oldMLIL->GetInstruction(instrIndex); + // If we find a MLIL_JUMP_TO with a known constant dest, then rewrite it + // to a MLIL_GOTO with the known dest filled in + if (oldInstr.operation == MLIL_JUMP_TO) + { + if (oldInstr.GetDestExpr<MLIL_JUMP_TO>().GetValue().state == ConstantPointerValue) + { + size_t destValue = oldInstr.GetDestExpr<MLIL_JUMP_TO>().GetValue().value; + auto targets = oldInstr.GetTargets<MLIL_JUMP_TO>(); + if (std::find_if(targets.begin(), targets.end(), [&](std::pair<size_t, size_t> target) { + return target.first == destValue; + }) != targets.end()) { + auto oldTargetIndex = targets[destValue]; + if (blockLabels.find(oldTargetIndex) == blockLabels.end()) + { + blockLabels[oldTargetIndex] = MediumLevelILLabel{}; + } + MediumLevelILLabel* targetLabel = &blockLabels[oldTargetIndex]; + newMLIL->AddInstruction(newMLIL->Goto(*targetLabel, oldInstr)); + continue; + } + } + } + + // Otherwise, copy the instruction as-is + newMLIL->SetCurrentAddress(block->GetArchitecture(), oldInstr.address); + newMLIL->AddInstruction(oldInstr.CopyTo(newMLIL)); + } + } + + newMLIL->Finalize(); + newMLIL->GenerateSSAForm(); + + if (report) + { + auto graph = GraphPls(context->GetMediumLevelILFunction()); + report->AddGraphReport(context->GetBinaryView(), "Resolved constant jump_to's", graph); + } + + // ========================================================================== + // And we're done + + if (doIt) + { + context->SetMediumLevelILFunction(newMLIL); + } + }(); + // Show debug report if requested, even on exception thrown + if (report) + { + ShowReportCollection("Unflatten Debug Report", report); + } + } + catch (...) + { + // Show debug report if requested, even on exception thrown + if (report) + { + ShowReportCollection("Unflatten Debug Report", report); + } + } +} + + +extern "C" +{ + BN_DECLARE_CORE_ABI_VERSION + + BINARYNINJAPLUGIN bool CorePluginInit() + { + auto wf = Workflow::Instance("core.function.metaAnalysis")->Clone("core.function.metaAnalysis"); + wf->RegisterActivity(new Activity(R"~( + { + "name": "extension.unflatten_limoncello_cpp.unflatten.dry_run", + "title": "Unflatten (Limoncello C++) Dry Run", + "description": "Detect and reverse Limoncello's Control Flow Flattening scheme.", + "eligibility": { + "auto": { + "default": false + } + } + } + )~", + [](Ref<AnalysisContext> context) { + RewriteAction(context, false); + })); + wf->RegisterActivity(new Activity(R"~( + { + "name": "extension.unflatten_limoncello_cpp.unflatten", + "title": "Unflatten (Limoncello C++)", + "description": "Detect and reverse Limoncello's Control Flow Flattening scheme.", + "eligibility": { + "auto": { + "default": false + } + } + } + )~", + [](Ref<AnalysisContext> context) { + RewriteAction(context, true); + })); + wf->InsertAfter("core.function.generateMediumLevelIL", std::vector<std::string>{ + "extension.unflatten_limoncello_cpp.unflatten.dry_run", + "extension.unflatten_limoncello_cpp.unflatten" + }); + Workflow::RegisterWorkflow(wf); + return true; + } + + BINARYNINJAPLUGIN void CorePluginDependencies() + { + } +} |
