mirror of
https://github.com/vtil-project/VTIL-Core
synced 2026-08-17 08:23:03 -04:00
605 lines
No EOL
19 KiB
C++
605 lines
No EOL
19 KiB
C++
// Copyright (c) 2020 Can Boluk and contributors of the VTIL Project
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
//
|
|
// 1. Redistributions of source code must retain the above copyright notice,
|
|
// this list of conditions and the following disclaimer.
|
|
// 2. Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
// 3. Neither the name of VTIL Project nor the names of its contributors
|
|
// may be used to endorse or promote products derived from this software
|
|
// without specific prior written permission.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
//
|
|
#include "tracer.hpp"
|
|
#include <vtil/io>
|
|
#include "../vm/lambda.hpp"
|
|
#include <vtil/utility>
|
|
#include <unordered_map>
|
|
|
|
namespace vtil
|
|
{
|
|
// Re entry guards for cycle heavy traces.
|
|
// - #52: direct/indirect reentry in tracer::trace via VM callbacks.
|
|
// - #78: recursive rtrace propagation over cyclic path expansions.
|
|
//
|
|
template<typename key_t>
|
|
struct recursion_guard
|
|
{
|
|
std::unordered_map<key_t, size_t>& depth_map;
|
|
key_t key;
|
|
bool reentrant = false;
|
|
|
|
recursion_guard( std::unordered_map<key_t, size_t>& depth_map, const key_t& key )
|
|
: depth_map( depth_map ), key( key )
|
|
{
|
|
auto& depth = depth_map[ key ];
|
|
reentrant = depth != 0;
|
|
depth++;
|
|
}
|
|
|
|
~recursion_guard()
|
|
{
|
|
auto it = depth_map.find( key );
|
|
if ( it != depth_map.end() && --it->second == 0 )
|
|
depth_map.erase( it );
|
|
}
|
|
};
|
|
|
|
inline static thread_local std::unordered_map<symbolic::variable, size_t> active_trace;
|
|
inline static thread_local std::unordered_map<symbolic::variable, size_t> active_rtrace;
|
|
// Internal type definitions.
|
|
//
|
|
using path_map_t = std::map<std::pair<const basic_block*, const basic_block*>, int>;
|
|
|
|
// Forward defs.
|
|
//
|
|
static symbolic::expression::reference rtrace_primitive( const symbolic::variable& lookup, tracer* tracer, path_map_t& path_map, const basic_block* target );
|
|
|
|
// Given a partial tracer, this routine will determine the full value of the variable
|
|
// at the given position where a partial write was found.
|
|
//
|
|
static symbolic::expression::reference resolve_partial( const symbolic::variable& origin,
|
|
const symbolic::access_details& access,
|
|
function_view<symbolic::expression::reference( const symbolic::variable& )> ptracer )
|
|
{
|
|
using namespace logger;
|
|
|
|
auto select = [ & ] ( bitcnt_t bit_offset, bitcnt_t bit_count ) -> symbolic::variable
|
|
{
|
|
if ( origin.is_register() )
|
|
return { origin.at, origin.reg().select( bit_count, origin.reg().bit_offset + bit_offset ) };
|
|
else
|
|
return { origin.at, { origin.mem().base + ( bit_offset / 8 ), bit_count } };
|
|
};
|
|
bitcnt_t bit_count = origin.bit_count();
|
|
|
|
// Fetch the result of this operation.
|
|
//
|
|
auto base = ptracer( select( access.bit_offset, access.bit_count ) );
|
|
|
|
// Trace a low part if we have to.
|
|
//
|
|
if ( access.bit_offset > 0 )
|
|
{
|
|
bitcnt_t low_bcnt = access.bit_offset;
|
|
auto res = ptracer( select( 0, low_bcnt ) );
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
// Log the low and middle bits.
|
|
//
|
|
log<CON_RED>( "dst[00..%02d] := %s\n", low_bcnt, res );
|
|
log<CON_YLW>( "dst[%02d..%02d] := %s\n", access.bit_offset, access.bit_offset + access.bit_count, base );
|
|
#endif
|
|
base = res | ( base.resize( bit_count ) << low_bcnt );
|
|
}
|
|
// Shift the result if we have to.
|
|
//
|
|
else if ( access.bit_offset < 0 )
|
|
{
|
|
base = ( base >> -access.bit_offset ).resize( bit_count );
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
// Log the low bits after shifting.
|
|
//
|
|
log<CON_YLW>( "dst[00..%02d] := %s\n", access.bit_offset + access.bit_count, base );
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
// Log the low bits.
|
|
//
|
|
log<CON_YLW>( "dst[00..%02d] := %s\n", access.bit_offset + access.bit_count, base );
|
|
#endif
|
|
}
|
|
|
|
// Trace a high part if we have to.
|
|
//
|
|
if ( bit_count > ( access.bit_offset + access.bit_count ) )
|
|
{
|
|
bitcnt_t high_bnct = bit_count - ( access.bit_offset + access.bit_count );
|
|
auto res = ptracer( select( access.bit_offset + access.bit_count, high_bnct ) );
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
// Log the high bits.
|
|
//
|
|
log<CON_PRP>( "dst[%02d..%02d] := %s\n", access.bit_offset + access.bit_count, bit_count, res );
|
|
#endif
|
|
base = base | ( res.resize( bit_count ) << ( access.bit_offset + access.bit_count ) );
|
|
}
|
|
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
// Log the final result.
|
|
//
|
|
log<CON_GRN>( "dst := %s\n", base );
|
|
#endif
|
|
// Resize and return.
|
|
//
|
|
return base.resize( bit_count );
|
|
}
|
|
|
|
// Applies transformation per each unique variable in the expression.
|
|
//
|
|
static void transform_variables( symbolic::expression::reference& inout, function_view<symbolic::expression::reference(const symbolic::variable&)> fn )
|
|
{
|
|
// Take fast path if single variable.
|
|
//
|
|
if ( inout->is_variable() )
|
|
{
|
|
if ( symbolic::expression::reference res = fn( inout->uid.get<symbolic::variable>() ) )
|
|
inout = res;
|
|
return;
|
|
}
|
|
|
|
std::unordered_map<symbolic::variable, symbolic::expression::reference> cache;
|
|
cache.reserve( inout->depth );
|
|
|
|
inout.transform( [ &cache, &fn ] ( symbolic::expression::delegate& exp )
|
|
{
|
|
// Skip if not variable.
|
|
//
|
|
if ( !exp->is_variable() )
|
|
return;
|
|
|
|
// Apply transformation.
|
|
//
|
|
auto& var = exp->uid.get<symbolic::variable>();
|
|
if ( auto it = cache.find( var ); it != cache.end() )
|
|
{
|
|
if ( it->second && *it->second )
|
|
exp = it->second;
|
|
}
|
|
else
|
|
{
|
|
auto res = fn( var );
|
|
auto [cit, _] = cache.emplace( var, std::move( res ) );
|
|
|
|
if ( cit->second && *cit->second )
|
|
exp = cit->second;
|
|
}
|
|
}, true, false );
|
|
}
|
|
|
|
// Propagates all variables in the reference expression onto the new iterator, if no history pointer given will do trace instead of rtrace.
|
|
// Returns an additional boolean parameter that indicates, if the propagation failed, it was due to a total failure or not; total failure
|
|
// meaning the origin expression was a variable and it infinite-looped during propagation by itself.
|
|
// - Note: New iterator should be a connected block's end.
|
|
//
|
|
static bool propagate( symbolic::expression::reference& ref, const il_const_iterator& it, tracer* tracer, path_map_t* path_map, const basic_block* target )
|
|
{
|
|
using namespace logger;
|
|
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
scope_padding _p( 1 );
|
|
#endif
|
|
|
|
std::optional<bool> result = {};
|
|
transform_variables( ref, [ & ] ( const symbolic::variable& _var ) -> symbolic::expression::reference
|
|
{
|
|
// If result is already decided, return as is.
|
|
//
|
|
if ( result.has_value() )
|
|
return {};
|
|
|
|
// Move the variable to reference the previous block.
|
|
//
|
|
symbolic::variable var = _var;
|
|
|
|
// Skip if variable is position indepdendent or not at the beginning of the block.
|
|
//
|
|
if ( !var.at.is_valid() || !var.at.is_begin() )
|
|
return {};
|
|
|
|
// If register:
|
|
//
|
|
if ( var.is_register() )
|
|
{
|
|
// Local temporary must not exist in an expression being propagated
|
|
// from the beginning of the block as that indicates use before assignment.
|
|
// Make sure this is not the case.
|
|
//
|
|
if ( var.reg().flags & register_local )
|
|
{
|
|
warning(
|
|
"Local variable %s is used before value assignment (Block %x).\n",
|
|
var,
|
|
var.at.block->entry_vip
|
|
);
|
|
}
|
|
|
|
// If volatile iterator cannot be moved, skip.
|
|
//
|
|
if ( var.reg().flags & register_volatile )
|
|
return {};
|
|
}
|
|
// If memory, propagate the pointer.
|
|
//
|
|
else if ( var.is_memory() )
|
|
{
|
|
auto& mem = var.mem();
|
|
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
// Log original pointer.
|
|
//
|
|
log<CON_PRP>( "Propagating pointer: %s\n", mem.decay() );
|
|
#endif
|
|
// Fail if propagation fails.
|
|
//
|
|
symbolic::expression::reference mem_ptr = std::move( mem.base.base );
|
|
propagate( mem_ptr, it, tracer->purify(), nullptr, nullptr );
|
|
if ( !mem_ptr )
|
|
{
|
|
result = false;
|
|
return {};
|
|
}
|
|
mem = { mem_ptr, mem.bit_count };
|
|
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
// Log new pointer.
|
|
//
|
|
log<CON_PRP>( "Pointer' => %s\n", mem.decay() );
|
|
#endif
|
|
}
|
|
|
|
// Move the assigned iterator.
|
|
//
|
|
var.bind( it );
|
|
|
|
// Trace the variable in the destination block, fail if it fails.
|
|
//
|
|
symbolic::expression::reference var_traced;
|
|
if ( path_map )
|
|
var_traced = rtrace_primitive( var, tracer, *path_map, target );
|
|
else
|
|
var_traced = tracer->trace( var );
|
|
if ( !var_traced )
|
|
{
|
|
result = ref->is_variable();
|
|
return {};
|
|
}
|
|
|
|
// If we are tracing the value of RSP, add the stack pointer delta between blocks.
|
|
//
|
|
if ( var.is_register() && var.reg().is_stack_pointer() && it.block->sp_offset )
|
|
var_traced = var_traced + it.block->sp_offset;
|
|
return var_traced;
|
|
} );
|
|
|
|
// Return the result.
|
|
//
|
|
if ( !result.has_value() )
|
|
{
|
|
ref.simplify();
|
|
return false;
|
|
}
|
|
ref = {};
|
|
return *result;
|
|
}
|
|
|
|
// Internal implementation of ::rtrace with a path history.
|
|
//
|
|
static symbolic::expression::reference rtrace_primitive( const symbolic::variable& lookup, tracer* tracer, path_map_t& path_map, const basic_block* target )
|
|
{
|
|
using namespace logger;
|
|
|
|
recursion_guard guard_rtrace{ active_rtrace, lookup };
|
|
if ( guard_rtrace.reentrant )
|
|
{
|
|
auto cyclic = lookup;
|
|
cyclic.is_branch_dependant = true;
|
|
return cyclic.to_expression();
|
|
}
|
|
|
|
// Save whether this is the call whose result will reach the user.
|
|
//
|
|
bool initial_call = path_map.empty();
|
|
|
|
// Trace through the current block first.
|
|
//
|
|
auto result = tracer->trace( lookup );
|
|
|
|
// If result has any variables:
|
|
//
|
|
if ( result->value.is_unknown() )
|
|
{
|
|
// Save current result as default result and clear it.
|
|
//
|
|
symbolic::expression::reference default_result = {};
|
|
std::swap( result, default_result );
|
|
|
|
// If there may be paths to enumerate:
|
|
//
|
|
size_t count = 0;
|
|
if ( lookup.at.is_valid() )
|
|
{
|
|
// Determine whether we're in a loop or not.
|
|
//
|
|
bool potential_loop = lookup.at.block->owner->is_looping( lookup.at.block );
|
|
|
|
// If block does not touch our variable, skip the logic.
|
|
//
|
|
/*if ( default_result->is_variable() &&
|
|
default_result->uid.get<symbolic::variable>().at.is_begin() &&
|
|
default_result->uid.get<symbolic::variable>().descriptor == lookup.descriptor )
|
|
potential_loop = false;
|
|
|
|
// Make an exception for self looping blocks.
|
|
//
|
|
for ( auto& it : it_list )
|
|
potential_loop |= it.block == lookup.at.block;*/
|
|
|
|
// Enumerate each path:
|
|
//
|
|
lookup.at.enum_paths( false, [ & ] ( const il_const_iterator& it )
|
|
{
|
|
// Skip if it does not reach target.
|
|
//
|
|
#if _DEBUG
|
|
if ( !target->owner->has_path( it.block, target ) )
|
|
{
|
|
warning( "Iterator %s has no path to %s but is still being considered in backpropagation.",
|
|
it, target->begin() );
|
|
}
|
|
#endif
|
|
|
|
// Increment path count.
|
|
//
|
|
if ( ++count == 0 )
|
|
{
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
// Log recursive tracing of the expression.
|
|
//
|
|
log<CON_GRN>( "Base case: %s\n", result );
|
|
#endif
|
|
}
|
|
|
|
// If we've taken this path more than twice, skip it.
|
|
//
|
|
if ( potential_loop )
|
|
{
|
|
int& counter = path_map[ { lookup.at.block, it.block } ];
|
|
if ( counter >= 2 )
|
|
{
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
// Log skipping of path.
|
|
//
|
|
log<CON_CYN>( "Path [%llx->%llx] is not taken as it's n-looping.\n", lookup.at.block->entry_vip, it.block->entry_vip );
|
|
#endif
|
|
return enumerator::ocontinue;
|
|
}
|
|
++counter;
|
|
}
|
|
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
// Log tracing of path.
|
|
//
|
|
log<CON_YLW>( "Taking path [%llx->%llx]\n", lookup.at.block->entry_vip, it.block->entry_vip );
|
|
#endif
|
|
// Propagate each variable onto to the destination block, if total fail, skip path.
|
|
//
|
|
symbolic::expression::reference exp = default_result;
|
|
bool total_fail = propagate( exp, it, tracer, &path_map, target );
|
|
if ( potential_loop )
|
|
path_map[ { lookup.at.block, it.block } ]--;
|
|
if ( total_fail )
|
|
return enumerator::ocontinue;
|
|
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
// Log result.
|
|
//
|
|
log<CON_BLU>( "= %s\n", exp );
|
|
#endif
|
|
// If no result is set yet, assign the current expression.
|
|
//
|
|
if ( !result )
|
|
result = exp;
|
|
|
|
// If expression is invalid or not equal to previous result, fail.
|
|
//
|
|
if ( !exp || !exp->equals( *result ) )
|
|
{
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
// Log decision.
|
|
//
|
|
log<CON_RED>( "Halting tracer as it was not deterministic.\n" );
|
|
#endif
|
|
// If result was null, return lookup.
|
|
//
|
|
if ( !exp )
|
|
{
|
|
result = lookup.to_expression();
|
|
}
|
|
// If it was mismatching, return default result as branch dependant.
|
|
//
|
|
else
|
|
{
|
|
result = std::move( default_result );
|
|
result.transform( [ ] ( symbolic::expression::delegate& exp )
|
|
{
|
|
if ( exp->is_variable() )
|
|
{
|
|
symbolic::variable&& var = std::move( ( +exp )->uid.get<symbolic::variable>() );
|
|
var.is_branch_dependant = true;
|
|
*+exp = { var, exp->size() };
|
|
}
|
|
}, true, false );
|
|
}
|
|
return enumerator::obreak;
|
|
}
|
|
return enumerator::ocontinue;
|
|
} );
|
|
}
|
|
|
|
// If result is null, use default result instead if the call will reach the user,
|
|
// or if there were simply no paths to take.
|
|
//
|
|
if ( !result && ( initial_call || count == 0 ) )
|
|
result = std::move( default_result );
|
|
}
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
// Log result.
|
|
//
|
|
log<CON_BRG>( "= %s\n", result );
|
|
#endif
|
|
return result.simplify();
|
|
}
|
|
|
|
// Traces a variable across the basic block it belongs to and generates a symbolic expression
|
|
// that describes it's value at the bound point. The provided variable should not contain a
|
|
// pointer with out-of-block expressions.
|
|
//
|
|
symbolic::expression::reference tracer::trace( const symbolic::variable& lookup )
|
|
{
|
|
using namespace logger;
|
|
|
|
recursion_guard guard_trace{ active_trace, lookup };
|
|
if ( guard_trace.reentrant )
|
|
return lookup.to_expression();
|
|
|
|
// If invalid/.begin() iterator or register with "no-trace" flags, return as is.
|
|
//
|
|
if ( lookup.at.is_begin() || ( lookup.is_register() && ( lookup.reg().flags & ( register_volatile | register_readonly ) ) ) )
|
|
return lookup.to_expression();
|
|
|
|
// Fast forward until iterator writes to the lookup, if none found return as is.
|
|
//
|
|
symbolic::access_details details = {};
|
|
il_const_iterator it = lookup.at;
|
|
while ( true )
|
|
{
|
|
// If we reached the beginning, return as is.
|
|
//
|
|
if ( it.is_begin() )
|
|
return symbolic::variable{ it, lookup.descriptor }.to_expression();
|
|
|
|
// Decrement iterator.
|
|
//
|
|
--it;
|
|
|
|
// If variable is being written to, break.
|
|
//
|
|
if ( details = lookup.written_by( it, this, recursive_flag ) )
|
|
{
|
|
// If unknown access, return unknown.
|
|
//
|
|
if ( details.is_unknown() )
|
|
{
|
|
#if VTIL_OPT_TRACE_VERBOSE
|
|
// Log the state.
|
|
//
|
|
log<CON_RED>( "[Unknown symbolic state.]\n" );
|
|
#endif
|
|
return lookup.to_expression();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If fails due to offset/size mismatch, invoke partial tracer.
|
|
//
|
|
bitcnt_t result_bcnt = lookup.bit_count();
|
|
if ( details.bit_offset != 0 || details.bit_count != result_bcnt )
|
|
{
|
|
// Redirect to partial resolver.
|
|
//
|
|
symbolic::variable origin = { std::next( it ), lookup.descriptor };
|
|
return resolve_partial( origin, details, [ & ] ( const symbolic::variable& var ) { return tracer::trace( std::move( var ) ); } );
|
|
}
|
|
|
|
// Create a lambda virtual machine and allocate a temporary result.
|
|
//
|
|
lambda_vm lvm;
|
|
symbolic::expression::reference result = {};
|
|
|
|
lvm.hooks.read_register = [ & ] ( const register_desc& desc )
|
|
{
|
|
return trace( { it, desc } );
|
|
};
|
|
lvm.hooks.read_memory = [ & ] ( const symbolic::expression::reference& pointer, size_t byte_count )
|
|
{
|
|
auto exp = trace( symbolic::variable{ it, { pointer, math::narrow_cast<bitcnt_t>( byte_count * 8 ) } } );
|
|
return exp ? exp.resize( result_bcnt ) : exp;
|
|
};
|
|
lvm.hooks.write_register = [ & ] ( const register_desc& desc, symbolic::expression::reference value )
|
|
{
|
|
if ( desc == lookup.reg() )
|
|
result = std::move( value );
|
|
};
|
|
|
|
lvm.hooks.write_memory = [ & ] ( const symbolic::expression::reference& pointer, deferred_value<symbolic::expression::reference> value, bitcnt_t size )
|
|
{
|
|
if ( pointer->equals( *lookup.mem().decay() ) )
|
|
result = std::move( value.get() );
|
|
return true;
|
|
};
|
|
|
|
// Step one instruction, if result was successfuly captured, return.
|
|
//
|
|
if ( lvm.execute( *it ), result )
|
|
return result;
|
|
|
|
// If we could not describe the behaviour, increment iterator and return.
|
|
//
|
|
return symbolic::variable{ std::next( it ), lookup.descriptor }.to_expression();
|
|
}
|
|
|
|
// Traces a variable across the entire routine and tries to generates a symbolic expression
|
|
// for it at the specified point of the block, limit determines the maximum number of blocks
|
|
// to trace backwards, any negative number implies infinite since it won't reach 0.
|
|
//
|
|
symbolic::expression::reference tracer::rtrace( const symbolic::variable& lookup )
|
|
{
|
|
bool recursive_flag_prev = std::exchange( recursive_flag, true );
|
|
path_map_t path_map = {};
|
|
auto exp = rtrace_primitive( lookup, this, path_map, lookup.at.block );
|
|
recursive_flag = recursive_flag_prev;
|
|
return exp;
|
|
}
|
|
|
|
// Wrappers around trace and rtrace that can trace an entire expression.
|
|
//
|
|
symbolic::expression::reference tracer::trace_exp( const symbolic::expression::reference& exp )
|
|
{
|
|
symbolic::expression::reference out = exp;
|
|
transform_variables( out, [ & ] ( const symbolic::variable& var ) { return trace( var ); } );
|
|
return out.simplify();
|
|
}
|
|
symbolic::expression::reference tracer::rtrace_exp( const symbolic::expression::reference& exp )
|
|
{
|
|
symbolic::expression::reference out = exp;
|
|
transform_variables( out, [ & ] ( const symbolic::variable& var ) { return rtrace( var ); } );
|
|
return out.simplify();
|
|
}
|
|
}; |