VTIL-Core/VTIL-Architecture/symex/variable.cpp

698 lines
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 "variable.hpp"
#include "../trace/tracer.hpp"
#include "../routine/call_convention.hpp"
namespace vtil::symbolic
{
// Returns the origin block of the pointer.
//
static const basic_block* get_pointer_origin( const expression& exp )
{
// If variable with valid iterator, return the block.
//
if ( exp.is_variable() )
{
auto& var = exp.uid.get<variable>();
if ( var.at.is_valid() )
return var.at.block;
}
// Otherwise try each child.
//
for ( auto& exp : { exp.lhs, exp.rhs } )
if ( auto p = exp ? get_pointer_origin( *exp ) : nullptr )
return p;
// Fail.
//
return nullptr;
}
// Calculates the displacement between two pointers and fills the access_details accordingly.
//
static void fill_displacement( access_details* details, const pointer& p1, const pointer& p2, tracer* tracer, bool xblock )
{
// If the two pointers can overlap:
//
if ( p1.can_overlap( p2 ) )
{
// Write dummy bit count and try to calculate the offset:
//
details->bit_count = -1;
// If offset is constant:
//
if ( auto disp = p1 - p2 )
{
details->bit_offset = math::narrow_cast<bitcnt_t>( *disp * 8 );
return;
}
// If valid tracer provided:
//
if ( tracer )
{
// If two pointers' origins mismatch, propagate first.
//
auto o1 = get_pointer_origin( *p1.base );
auto o2 = get_pointer_origin( *p2.base );
if ( o1 != o2 && o1 && o2 )
{
// Allocate temporary storage for new pointers.
//
pointer pn1, pn2;
std::array in = { &p1, &p2 };
std::array out = { &pn1, &pn2 };
// For each pointer:
//
for ( auto [in, out] : zip( in, out ) )
{
// Transform base pointer:
//
expression::reference base = std::move( in->base );
base.transform( [ & ] ( expression::delegate& exp )
{
// Skip if not variable.
//
if ( !exp->is_variable() )
return;
variable var = exp->uid.get<variable>();
// Skip if it has an invalid iterator.
//
if ( !var.at.is_valid() )
return;
// Determine all paths and path restrict the iterator.
//
auto& pathset_1 = o1->owner->get_path( o1, var.at.block );
auto& pathset_2 = o1->owner->get_path( o2, var.at.block );
var.at.is_path_restricted = true;
// If only one of the paths are valid for backwards iteration:
//
if ( pathset_1.empty() ^ pathset_2.empty() )
{
// Set the restriction.
//
var.at.paths_allowed = pathset_1.empty() ? &pathset_2 : &pathset_1;
exp = tracer->rtrace( std::move( var ) );
}
// If both paths are valid for backwards iteration:
//
else if ( pathset_1.size() && pathset_2.size() )
{
// Calculate for both and set if equivalent.
//
var.at.paths_allowed = &pathset_1;
auto exp1 = tracer->rtrace( var );
var.at.paths_allowed = &pathset_2;
auto exp2 = tracer->rtrace( var );
if ( exp1.equals( *exp2 ) )
exp = exp1;
}
} );
// Write the new pointer.
//
*out = pointer{ std::move( base ) };
}
// Recurse with the new pointers.
//
return fill_displacement( details, pn1, pn2, nullptr, xblock );
}
}
// If cross-block and tracer is given, try again after cross-tracing.
//
if ( xblock && tracer )
{
pointer p1r = { tracer->rtrace_exp( p1.base ) };
pointer p2r = { tracer->rtrace_exp( p2.base ) };
return fill_displacement( details, p1r, p2r, nullptr, false );
}
// If all fails, declare unknown.
//
else
{
details->unknown = 1;
}
}
// Otherwise declare no-overlap.
//
else
{
details->bit_count = 0;
details->bit_offset = 0;
}
}
// Implement generic access check for ::read_by & ::written_by.
//
static access_details test_access( const variable& var, const il_const_iterator& it, tracer* tracer, bool cwrite, bool cread, bool xblock )
{
access_details result = {};
// If variable is of register type:
//
if ( auto reg = std::get_if<variable::register_t>( &var.descriptor ) )
{
// Iterate each operand:
//
for ( size_t i = 0; i < it->base->operand_count(); i++ )
{
// Skip if not register.
//
if ( !it->operands[ i ].is_register() )
continue;
// Skip if access type does not match.
//
if ( !cread && it->base->operand_types[ i ] < operand_type::write )
continue;
if ( !cwrite && it->base->operand_types[ i ] == operand_type::write )
continue;
// Skip if no overlap.
//
auto& ref_reg = it->operands[ i ].reg();
if ( !ref_reg.overlaps( *reg ) )
continue;
// Append access details.
//
result += {
.bit_offset = ref_reg.bit_offset - reg->bit_offset,
.bit_count = ref_reg.bit_count,
.read = it->base->operand_types[ i ] != operand_type::write,
.write = it->base->operand_types[ i ] >= operand_type::write
};
}
}
// If variable is of memory type:
//
else if ( auto mem = std::get_if<variable::memory_t>( &var.descriptor ) )
{
// If fence:
//
if ( cread && it->base == &ins::sfence )
return { .bit_count = var.bit_count(), .read = true, .write = false, .unknown = true };
else if ( cwrite && it->base == &ins::lfence )
return { .bit_count = var.bit_count(), .read = false, .write = true, .unknown = true };
// If instruction accesses memory:
//
if ( it->base->accesses_memory() && ( it->base->memory_write ? cwrite : cread ) )
{
// Generate an expression for the pointer.
//
auto [base, offset] = it->memory_location();
pointer ptr = { tracer->trace( { it, base } ) + offset };
// Calculate displacement.
//
access_details details;
fill_displacement( &details, ptr, mem->base, tracer, xblock );
// If pointers can indeed overlap:
//
if ( details )
{
// Fill read/write.
//
details.read = it->base->reads_memory();
details.write = it->base->writes_memory();
// If offset is unknown, return as is.
//
if ( details.is_unknown() )
{
result += details;
}
else
{
// Check if within boundaries, set bit count and return if so.
//
bitcnt_t low_offset = details.bit_offset;
bitcnt_t high_offset = low_offset + it->access_size();
if ( low_offset < mem->bit_count && high_offset > 0 )
{
details.bit_count = it->access_size();
result += details;
}
}
}
}
}
// If external call:
//
if ( it->base->is_branching_real() )
{
// Get calling convention.
//
call_convention cc = it->base == &ins::vexit
? it.block->owner->routine_convention
: it.block->owner->get_cconv( it->vip );
// If variable is a register:
//
if ( var.is_register() )
{
auto& reg = var.reg();
// If $sp, indicate read from:
//
if ( cread && reg.is_stack_pointer() )
result += { .bit_offset = 0, .bit_count = reg.bit_count, .read = true, .write = false };
// If exiting the virtual machine:
//
if ( it->base == &ins::vexit )
{
// If virtual register, indicate discarded:
//
if ( reg.is_virtual() )
{
if ( cwrite )
result += { .bit_offset = 0, .bit_count = reg.bit_count, .read = false, .write = true };
}
// If physical register:
//
else
{
uint64_t volatile_mask = 0;
// If volatile register, indicate discarded:
//
for ( const register_desc& volreg : cc.volatile_registers )
{
if ( volreg.overlaps( reg ) )
{
volatile_mask |= volreg.get_mask();
if ( cwrite )
result += {.bit_offset = volreg.bit_offset - reg.bit_offset, .bit_count = volreg.bit_count, .read = false, .write = true };
}
}
if ( cread )
{
// If volatile, indicate read from if retval:
//
if ( ( reg.get_mask() & volatile_mask ) == reg.get_mask() )
{
for ( const register_desc& retval : cc.retval_registers )
if ( retval.overlaps( reg ) )
result += {.bit_offset = retval.bit_offset - reg.bit_offset, .bit_count = retval.bit_count, .read = true, .write = false };
}
// Otherwise directly indicate read from:
//
else
{
result += {.bit_offset = 0, .bit_count = reg.bit_count, .read = true, .write = false };
}
}
}
}
// If invoking external routine:
//
else
{
if ( cwrite )
{
// If volatile register, indicate discarded:
//
for ( const register_desc& param : cc.volatile_registers )
if ( param.overlaps( reg ) )
result += {.bit_offset = param.bit_offset - reg.bit_offset, .bit_count = param.bit_count, .read = false, .write = true };
// If retval register, indicate discarded:
//
for ( const register_desc& retval : cc.retval_registers )
if ( retval.overlaps( reg ) )
result += {.bit_offset = retval.bit_offset - reg.bit_offset, .bit_count = retval.bit_count, .read = false, .write = true };
}
if ( cread )
{
// If parameter register, indicate read from.
//
for ( const register_desc& param : cc.param_registers )
if ( param.overlaps( reg ) )
result += {.bit_offset = param.bit_offset - reg.bit_offset, .bit_count = param.bit_count, .read = true, .write = false };
}
}
}
// If variable is memory:
//
else
{
auto& mem = var.mem();
// If vmexit, declared trashed if below or at the shadow space:
//
if ( cwrite && cc.purge_stack)
{
// Determine the limit of the stack memory owned by this routine.
//
expression limit =
tracer->trace( { it, REG_SP } ) +
it.block->sp_offset +
( it->base == &ins::vexit ? 0 : cc.shadow_space );
// Calculate the displacement, if constant below 0, declare trashed.
//
access_details details;
fill_displacement( &details, mem.base, pointer{ std::move( limit ) }, tracer, xblock );
if ( !details.is_unknown() && ( details.bit_offset + var.bit_count() ) <= 0 )
{
result += { .bit_offset = 0, .bit_count = var.bit_count(), .read = false, .write = true };
return result;
}
}
// Report unknown access: (TODO: Proper parsing!)
// - We can estimate usage based on registers passed, maybe?
//
return { .bit_count = var.bit_count(), .read = true, .write = true, .unknown = true, };
}
}
// Return result.
//
return result;
}
// Constructs by iterator and the variable descriptor itself.
//
variable::variable( const il_const_iterator& it, descriptor_t desc ) :
descriptor( std::move( desc ) )
{
// If read-only register, remove the iterator.
//
if ( is_register() && reg().is_read_only() ) bind( {} );
else bind( it );
// Validate the variable.
//
is_valid( true );
}
// Construct free-form with only the descriptor itself.
//
variable::variable( descriptor_t desc )
: variable( free_form_iterator, std::move( desc ) ) {}
// Returns whether the variable is valid or not.
//
bool variable::is_valid( bool force ) const
{
// If register:
//
if ( auto* reg = std::get_if<register_t>( &descriptor ) )
{
// Iterator must be valid if not read-only.
//
cvalidate( at.is_valid() || reg->is_read_only() );
// Redirect to register descriptor validation.
//
return reg->is_valid( force );
}
// If memory:
//
else
{
auto& mem = std::get<memory_t>( descriptor );
// Iterator must be valid.
//
cvalidate( at.is_valid() );
// Must have a valid pointer of 64 bits.
//
cvalidate( mem.decay() && mem.decay().size() == arch::bit_count );
// Bit count should be within (0, 64] and byte-addressable.
//
cvalidate( 0 < mem.bit_count && mem.bit_count <= arch::bit_count && ( mem.bit_count & 7 ) == 0 );
return true;
}
}
// Returns whether it is bound to a free-form iterator or not.
//
bool variable::is_free_form() const
{
return at == free_form_iterator;
}
// Conversion to symbolic expression.
//
expression variable::to_expression( bool unpack ) const
{
// If memory, return as is.
//
if ( is_memory() )
return { *this, mem().bit_count };
// If not register (so invalid), return null.
//
if ( !is_register() )
return {};
// If no unpacking requested, return as is.
//
const register_desc& src = reg();
if ( !unpack )
return { *this, src.bit_count };
// Extend to 64-bits with offset set at 0, shift it and
// mask it to experss the value of original register.
//
expression tmp = variable{ at, register_desc{ src.flags, src.local_id, arch::bit_count, 0, src.architecture } }.to_expression( false );
if ( src.bit_offset ) tmp >>= src.bit_offset;
tmp.resize( src.bit_count );
return tmp;
}
// Conversion to human-readable format.
//
std::string variable::to_string() const
{
// If invalid, return null.
//
if ( !is_valid() )
return "null";
// Allocate temporary storage for the base name.
//
std::string base;
// If memory:
//
if ( auto* mem = std::get_if<memory_t>( &descriptor ) )
{
// Indicate dereferencing of the pointer expression.
//
base = format::str( "[%s]", mem->decay() );
// Prefix with read size:
//
switch ( mem->bit_count )
{
case 1*8: base = "byte" + base; break;
case 2*8: base = "word" + base; break;
case 4*8: base = "dword" + base; break;
case 6*8: base = "fword" + base; break;
case 8*8: base = "qword" + base; break;
default: base = "u" + std::to_string( mem->bit_count ) + base; break;
}
}
// If register:
//
else
{
// Redirect to register_desc string conversion.
//
base = std::get<register_t>( descriptor ).to_string();
}
// Indicate branch-dependence.
//
if ( is_branch_dependant )
base += "...";
// If no valid iterator, return as is.
//
if ( !at.is_valid() )
return base;
// If dummy iterator, return with free-indicator appended.
//
if ( at == free_form_iterator )
return "%" + base;
// Append the block identifier.
//
base = format::str( "%s#0x%llx", base, at.block->entry_vip );
// Append the stream index and return.
//
if ( at.is_begin() ) return base + "?";
else if ( at.is_end() ) return base + "*";
else return base + "." + std::to_string( std::distance( at.block->begin(), at ) );
}
// Packs all the variables in the expression where it'd be optimal.
//
expression::reference& variable::pack_all( expression::reference& exp )
{
// List of ideal packers.
//
static constexpr bitcnt_t ideal_packers[] = { 1, 8, 16, 32 };
return exp.transform( [ ] ( expression::delegate& exp )
{
// Skip if expression has any known 1s.
//
if ( exp->known_one() )
return;
// Skip if expression does not have exactly one variable.
//
if ( exp->count_unique_variables() != 1 )
return;
// Check if the unknown mask matches that of an ideal packer.
//
auto it = std::find_if( ideal_packers, std::end( ideal_packers ), [ & ] ( auto& n )
{
return math::fill( n ) == exp->unknown_mask();
} );
if ( it == std::end( ideal_packers ) )
return;
// For each ideal packer:
//
bitcnt_t bitsize = *it;
// Clone and resize the expression.
//
auto exp_resized = expression::reference{ exp.ref }.resize( bitsize );
// If top node is not __ucast, fail.
//
if ( exp_resized->op != math::operator_id::ucast )
return;
// If node is not shift right, use zero offset.
//
bitcnt_t offset;
auto node = exp_resized->lhs;
if ( node->op != math::operator_id::shift_right )
{
offset = 0;
}
// If rhs is constant, use as is for offset.
//
else if ( auto n = node->rhs->get<bitcnt_t>() )
{
offset = *n;
node = node->lhs;
}
// Otherwise, fail.
//
else
{
return;
}
// Fail if top node is not a variable.
//
if ( !node->is_variable() )
return;
// Fail if the variable is not a register.
//
const variable& var = node->uid.get<variable>();
if ( !var.is_register() )
return;
// Fail if cannot be fit.
//
const register_desc& reg = var.reg();
if ( reg.bit_count < bitsize )
return;
// Fail if final bit offset is not aligned.
//
if ( ( reg.bit_offset + offset ) % bitsize )
return;
// Deref top-level node, own current node, rewrite as the variable.
//
exp_resized.reset();
auto exp_node = node.own();
variable& var_new = exp_node->uid.get<variable>();
var_new.reg().bit_count = bitsize;
var_new.reg().bit_offset += offset;
exp_node->value = math::bit_vector( bitsize );
exp_node->update( false );
exp = node.resize( exp->size() );
} );
}
expression::reference variable::pack_all( const expression::reference& exp )
{
auto copy = make_copy( exp );
pack_all( copy );
return copy;
}
// Checks if the variable is read by / written by the given instruction,
// returns nullopt it could not be known at compile-time, otherwise the
// access details as described by access_details. Tracer is used for
// pointer resolving, if nullptr passed will use default tracer.
//
access_details variable::read_by( const il_const_iterator& it, tracer* tr, bool xblock ) const
{
return test_access( *this, it, tr ? tr->purify() : nullptr, false, true, xblock );
}
access_details variable::written_by( const il_const_iterator& it, tracer* tr, bool xblock ) const
{
return test_access( *this, it, tr ? tr->purify() : nullptr, true, false, xblock );
}
access_details variable::accessed_by( const il_const_iterator& it, tracer* tr, bool xblock ) const
{
return test_access( *this, it, tr ? tr->purify() : nullptr, true, true, xblock );
}
};