blob: f31499490312aebe1412062fcadc2ef8494ba4ce [file] [log] [blame]
/* **********************************************************
* Copyright (c) 2012 Google, Inc. All rights reserved.
* Copyright (c) 2004-2009 VMware, Inc. All rights reserved.
* **********************************************************/
/*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * 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.
*
* * Neither the name of VMware, Inc. 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 VMWARE, INC. 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.
*/
/* Copyright (c) 2004-2007 Determina Corp. */
/*
* rct.c
* Routines for the security features related to indirect calls and indirect jumps
* in a platform independent manner.
*
*/
#include "globals.h"
#ifdef RCT_IND_BRANCH
#include "fragment.h"
#include "rct.h"
#include "module_shared.h"
#ifdef WINDOWS
# include "nudge.h" /* for generic_nudge_target() */
#endif
#ifdef X64
# include "instr.h" /* for instr_raw_is_rip_rel_lea */
#endif
/* General assumption all indirect branch targets on X86 will have an
* absolute address encoded in the code or data sections of the binary
* (e.g. address taken functions)
*
* Should go through each module's (non-zero) image sections meaning
* [module_base,+modulesize) and look for any address pointing to the
* code section(s) [baseof_code_section,+sizeof_code_section) for each
* code section.
*
* FIXME: (optimization) Since there can be multiple code sections we
* have to do this multiple times for each one in a module. While
* this may result in multiple passes with the current interface,
* don't optimize before shown to be a hit.
*
* FIXME: (optimization) In case there are heavy-weight resource
* sections (dialogues, etc.) we may want to skip them.
*
*/
/* Only a single thread should be traversing new modules */
/* Currently this overlaps with the table_rwlock of global_rct_ind_targets */
DECLARE_CXTSWPROT_VAR(mutex_t rct_module_lock, INIT_LOCK_FREE(rct_module_lock));
/* look in text[start,end) memory range for any reference to values
* in referto[start,end) address range
* Caller is responsible for ensuring that [start,end) is readable.
*
* TODO: add any such addresses to an OUT parameter hashtable
* returns number of added references (for diagnostic purposes)
*/
uint
find_address_references(dcontext_t *dcontext,
app_pc text_start, app_pc text_end,
app_pc referto_start, app_pc referto_end
)
{
uint references_found = 0; /* only for debugging */
DEBUG_DECLARE(uint references_already_known = 0;)
app_pc cur_addr;
app_pc last_addr = text_end - sizeof(app_pc); /* inclusive */
LOG(GLOBAL, LOG_RCT, 2, "find_address_references: text["PFX", "PFX"), referto["PFX", "PFX")\n",
text_start, text_end, referto_start, referto_end);
ASSERT(text_start <= text_end); /* empty ok */
ASSERT(referto_start <= referto_end); /* empty ok */
ASSERT(sizeof(app_pc) == IF_X64_ELSE(8,4));
ASSERT((ptr_uint_t)(last_addr+1) == (((ptr_uint_t)last_addr)+1)); /* byte increments */
ASSERT(is_readable_without_exception(text_start, text_end - text_start));
/* FIXME: could try to read dword[pc] dword[pc+4] and then merging them with shifts and | */
/* to get dword[pc+1] dword[pc+2] dword[pc+3] instead of reading memory */
/* but of course only if KSTAT says the latter is indeed faster! */
KSTART(rct_no_reloc);
for (cur_addr = text_start; cur_addr <= last_addr; cur_addr++) {
DEBUG_DECLARE(bool known_ref = false;)
app_pc ref = *(app_pc*)cur_addr; /* note dereference here */
if (rct_check_ref_and_add(dcontext, ref, referto_start, referto_end
_IF_DEBUG(cur_addr)
_IF_DEBUG(&known_ref)))
references_found++;
else {
DODEBUG({
if (known_ref)
references_already_known++;
});
}
#ifdef X64
/* PR 215408: look for "lea reg, [rip+disp]" */
ref = instr_raw_is_rip_rel_lea(cur_addr, text_end);
if (ref != NULL) {
LOG(GLOBAL, LOG_RCT, 4,
"find_address_references: rip-rel @"PFX" => "PFX"\n", cur_addr, ref);
if (rct_check_ref_and_add(dcontext, ref, referto_start, referto_end
_IF_DEBUG(cur_addr) _IF_DEBUG(&known_ref))) {
STATS_INC(rct_ind_rip_rel_scan_new);
references_found++;
} else {
DODEBUG({
if (known_ref) {
references_already_known++;
STATS_INC(rct_ind_rip_rel_scan_old);
} else
STATS_INC(rct_ind_rip_rel_scan_data);
});
}
}
#endif
}
KSTOP(rct_no_reloc);
LOG(GLOBAL, LOG_RCT, 2,
"find_address_references: scanned %u addresses, touched %u pages, "
"added %u new, %u duplicate ind targets\n",
(last_addr - text_start), (last_addr - text_start) / PAGE_SIZE,
references_found, references_already_known);
return references_found;
}
/* Check if the given address refers to [referto_start, refereto_end) and
* add it to a hashtable of valid indirect branches. Returns true if ref
* is added to hashtable, false otherwise.
* Optional OUT: known_ref -- true if ref was already known
*
* FIXME: called from find_relocation_addresses, if perf critical make
* this static inline.
* FIXME: make find_address_references call this once static inlined
*/
bool
rct_check_ref_and_add(dcontext_t *dcontext, app_pc ref,
app_pc referto_start, app_pc referto_end
_IF_DEBUG(app_pc addr)
_IF_DEBUG(bool *known_ref /* OPTIONAL OUT */))
{
/* can be null when called from find_address_references */
if (ref == NULL)
return false;
DODEBUG({
if (known_ref != NULL)
*known_ref = false;
});
/* reference outside range */
if (ref < referto_start || ref >= referto_end) {
return false;
}
/* indeed points to a code section */
DOLOG(3, LOG_RCT, {
char symbuf[MAXIMUM_SYMBOL_LENGTH];
LOG(GLOBAL, LOG_RCT, 3,
"rct_check_ref_and_add: "PFX" addr taken reference at "PFX"\n",
ref, addr);
print_symbolic_address(ref, symbuf, sizeof(symbuf), true);
LOG(GLOBAL, LOG_SYMBOLS, 3, "\t%s\n", symbuf);
});
if (rct_add_valid_ind_branch_target(dcontext, ref)) {
STATS_INC(rct_ind_branch_valid_targets);
LOG(GLOBAL, LOG_RCT, 3, "\t "PFX" added\n", ref);
return true; /* ref added, known_ref == false */
} else {
STATS_INC(rct_ind_branch_existing_targets);
LOG(GLOBAL, LOG_RCT, 3, "\t known\n");
DODEBUG({
if (known_ref != NULL)
*known_ref = true;
});
return false; /* ref not added, known_ref == true */
}
ASSERT_NOT_REACHED();
}
/* There are several alternative solutions to keeping the information
* about known valid indirect branch targets: a single global
* hashtable, per-module hashtables. We can use the latter for fast flushes
* yet we'd need a binary search to find the right module hashtable,
* or we can merge all module hashtables into the global one for lookups.
*
* Considering that we will want to have hashtables that provide the
* intersection of the above allowed targets with existing traces (or
* in the near future bbs), having yet another middle level table may
* not be worth it. If we do have bb2bb then we'd have to do this
* much less often and the cost of the module search will not be
* exposed, and in case it is we can at least cache the last two
* looked up entries (like vmareas caches last_area).
*
* FIXME: for now using a single global hashtable and flushing of the whole table
* is the only provided implementation
*/
/* TODO: pass the necessary bookkeeping hashtable if done per-module */
static inline bool
is_address_taken(dcontext_t *dcontext, app_pc target)
{
return rct_ind_branch_target_lookup(dcontext, target) != NULL;
}
static inline bool
is_address_after_call(dcontext_t *dcontext, app_pc target)
{
/* FIXME: practically equivalent to find_call_site() */
return fragment_after_call_lookup(dcontext, target) != NULL;
}
/* Restricted control transfer check on indirect branches called by
* dispatch after inlined indirect branch lookup routine has failed
*
* function does not return if a security violation is blocked
* FIXME: return value is ignored
*/
int
rct_ind_branch_check(dcontext_t *dcontext, app_pc target_addr, app_pc src_addr)
{
bool is_ind_call = EXIT_IS_CALL(dcontext->last_exit->flags);
security_violation_t indirect_branch_violation = is_ind_call ?
INDIRECT_CALL_RCT_VIOLATION : INDIRECT_JUMP_RCT_VIOLATION;
int res = 0;
bool cache = true;
DEBUG_DECLARE(const char *ibranch_type =
is_ind_call ? "call" : "jmp";)
ASSERT(is_ind_call || EXIT_IS_JMP(dcontext->last_exit->flags));
ASSERT((is_ind_call && TEST(OPTION_ENABLED, DYNAMO_OPTION(rct_ind_call))) ||
(!is_ind_call && TEST(OPTION_ENABLED, DYNAMO_OPTION(rct_ind_jump))));
LOG(THREAD, LOG_RCT, 2, "RCT: ind %s target = "PFX"\n", ibranch_type,
target_addr);
STATS_INC(rct_ind_branch_validations);
DOSTATS({
/* since the exports are now added to the global hashtable,
* we have to check this first to collect these stats
*/
/* FIXME: we need symbols at loglevel 0 for this to work */
if (rct_is_exported_function(target_addr)) {
if (is_ind_call)
STATS_INC(rct_ind_call_exports);
else
STATS_INC(rct_ind_jmp_exports);
if (DYNAMO_OPTION(IAT_convert)) {
LOG(THREAD, LOG_RCT, 2,
"RCT: address taken export or IAT conversion missed for "PFX, target_addr);
/* the module entry point is in fact hit here */
/* FIXME: investigate if an export is really not used via IAT or a variation of register */
}
}
});
/* FIXME: if we use per-module hashtables, we'd first have to
* looking up the module in question */
if (!is_address_taken(dcontext, target_addr)) {
bool is_code_section;
/* FIXME: use loglevel 2 when the define is on by default */
LOG(THREAD, LOG_RCT, 1, "RCT: bad ind %s target: "PFX", source "PFX"\n",
ibranch_type, target_addr, src_addr);
DOLOG(2, LOG_RCT, {
char symbuf[MAXIMUM_SYMBOL_LENGTH];
print_symbolic_address(target_addr, symbuf, sizeof(symbuf), true);
LOG(THREAD, LOG_SYMBOLS, 2, "\t%s\n", symbuf);
});
#ifdef DR_APP_EXPORTS
/* case 9195: Allow certain API calls so we don't get security violations
* when using DR with the start / stop interface.
* NOTE: this is a security hole so should never be in product build
*/
if (target_addr == (app_pc) dr_app_start ||
target_addr == (app_pc) dr_app_take_over ||
target_addr == (app_pc) dr_app_stop ||
target_addr == (app_pc) dr_app_cleanup)
goto good;
#endif
if (!is_ind_call) {
/* for DYNAMO_OPTION(rct_ind_jump) we need to allow an
* after call location (normally a target of return) to be
* targeted.
* Instances in ole32.dll abound.
*
* 77a7f057 e8ac2ffdff call ole32!IIDFromString+0xf6 (77a52008)
* ole32!IIDFromString+0x107:
* 77a52022 8b08 mov ecx,[eax]
* 77a52024 8b4004 mov eax,[eax+0x4]
* 77a52027 ffe0 jmp eax
*
* We would need to ensure that ret_after_call is turned
* on, FIXME: should turn into a security_option_t that needs
* to be at least OPTION_ENABLED
*/
if (DYNAMO_OPTION(ret_after_call)) {
if (is_address_after_call(dcontext, target_addr)) {
LOG(THREAD, LOG_RCT, 1,
"RCT: bad ind jump targeting an after call site: "PFX"\n",
target_addr);
STATS_INC(rct_ind_jmp_allowed_to_ac);
/* the current thread's indirect jmp IBL table will cache this */
goto good;
}
} else {
/* case 4982: we can't use RAC data - we'll get a violation
* FIXME: add better option enforcement after making ret_after_call a security_option_t
*/
ASSERT_NOT_IMPLEMENTED(false);
}
}
/* PR 275723: RVA-table-based switch statements. We check this prior to
* rct_analyze_module_at_violation to avoid excessive scanning of x64
* modules from PR 277044/277064.
*/
if (DYNAMO_OPTION(rct_exempt_intra_jmp)) {
app_pc code_start, code_end;
app_pc modbase = get_module_base(target_addr);
if (modbase != NULL &&
is_in_code_section(modbase, target_addr, &code_start, &code_end) &&
src_addr >= code_start && src_addr < code_end) {
STATS_INC(rct_ind_jmp_x64switch);
STATS_INC(rct_ind_jmp_exemptions);
LOG(THREAD, LOG_RCT, 2,
"RCT: target "PFX" in same code sec as src "PFX" --ok\n",
target_addr, src_addr);
/* Though we have per-module tables (except on Linux) (PR 214107)
* we don't check by source (and xref PR 204770) and don't have
* per-module ibl tables, so we can't cache this.
*/
cache = false;
res = 2;
goto exempted;
}
}
/* Grab the rct_module_lock to ensure no duplicates if two threads
* attempt to add the same module
*/
mutex_lock(&rct_module_lock);
/* NOTE - under current default options (analyze_at_load) this routine will have
* no effect and is only being used for its is_in_code_section (&IMAGE) return
* value. For x64 it is used to scan on violation (PR 277044/277064). */
is_code_section = rct_analyze_module_at_violation(dcontext, target_addr);
mutex_unlock(&rct_module_lock);
/* In fact, we have to let all regions that are not modules - so
* .A and .B attacks will still be marked as such instead of failing here.
*/
if (!is_code_section) {
/* FIXME: assert DGC */
/* we could be targeting a .data section that is within a
* module we still don't cache it but only a performance
* hit - it will not be reported anyways
*/
STATS_INC(rct_ind_branch_not_code_section);
/* ASLR: check if is in wouldbe region, if so report as failure */
if (aslr_is_possible_attack(target_addr)) {
LOG(THREAD, LOG_RCT, 1,
"RCT: ASLR: wouldbe a preferred DLL, "PFX" --BAD\n", target_addr);
STATS_INC(aslr_rct_ind_wouldbe);
/* fall through and report */
} else {
LOG(THREAD, LOG_RCT, 1, "RCT: not a code section, ignoring "PFX" --check\n", target_addr);
/* not caching violation target if not in code section */
return 2; /* positive, ignore violation */
}
}
/* we could be running in an unload race, and is_code_section
* would still be true if target is not yet unmapped, if it
* just became unreadable we also ignore like above
*/
if (is_unreadable_or_currently_unloaded_region(target_addr)) {
STATS_INC(rct_ind_branch_unload_race);
LOG(THREAD, LOG_RCT, 1, "RCT: unload race, ignoring "PFX" --check\n", target_addr);
/* not caching violation target since it will disappear */
/* note that we should throw exception while processing code origin checks */
return 2; /* positive, ignore violation */
}
if (is_address_taken(dcontext, target_addr)) {
LOG(THREAD, LOG_RCT, 1, "RCT: new module added for "PFX" --ok\n", target_addr);
STATS_INC(rct_ok_at_vio);
goto good;
}
/* FIXME: this code will not be needed if the exports are
* automatically added to the list yet I'd like to keep it
* around for the STATS that may be relevant to case 1948
*/
/* FIXME: case 3946 we need symbols at loglevel 0 to collect these stats */
DOLOG(1, LOG_RCT|LOG_SYMBOLS, {
/* this is an expensive bsearch, so we may not want to
* collect it as other stats */
if (rct_is_exported_function(target_addr)) {
DOSTATS({
if (is_ind_call)
STATS_INC(rct_ind_call_exports);
else
STATS_INC(rct_ind_jmp_exports);
});
SYSLOG_INTERNAL_WARNING_ONCE("missed an export "PFX" caught by "
"rct_is_exported_function()", target_addr);
DOLOG(1, LOG_RCT, {
char name[MAXIMUM_SYMBOL_LENGTH];
print_symbolic_address(target_addr, name, sizeof(name), false);
LOG(THREAD, LOG_RCT, 1, "RCT: exported function "PFX" %s missed!\n", target_addr, name);
});
}
});
LOG(THREAD, LOG_RCT, 1,
"RCT: BAD[%d] problem target="PFX" src fragment="PFX" type=%s\n",
GLOBAL_STAT(rct_ind_call_violations) + GLOBAL_STAT(rct_ind_jmp_violations),
target_addr, src_addr, ibranch_type);
/* FIXME: case 4331, as a minimal change for 2.1, this
* currently reuses several .C exceptions, while in fact only
* the -exempt_rct list is needed (but not fibers)
*/
if (at_known_exception(dcontext, target_addr, src_addr)) {
LOG(THREAD, LOG_RCT, 1, "RCT: target "PFX" exempted --ok\n");
DOSTATS({
if (is_ind_call)
STATS_INC(rct_ind_call_exemptions);
else
STATS_INC(rct_ind_jmp_exemptions);
});
res = 2;
/* we'll cache violation target */
goto exempted;
}
DOSTATS({
if (is_ind_call)
STATS_INC(rct_ind_call_violations);
else
STATS_INC(rct_ind_jmp_violations);
});
SYSLOG_INTERNAL_WARNING_ONCE("indirect %s targeting unknown "PFX,
EXIT_IS_CALL(dcontext->last_exit->flags)
? "call" : "jmp", target_addr);
/* does not return when OPTION_BLOCK is enforced */
if (security_violation(dcontext, target_addr, indirect_branch_violation,
is_ind_call ? DYNAMO_OPTION(rct_ind_call) : DYNAMO_OPTION(rct_ind_jump)) ==
indirect_branch_violation) {
/* running in detect mode */
/* we'll cache violation target */
res = -2;
} else {
res = -3;
/* exempted Threat ID */
/* we'll cache violation target */
}
exempted:
/* Exempted or bad in detect mode, either way should add the
* violating address so future references to it in other
* threads do not fail.
*/
if (DYNAMO_OPTION(rct_cache_exempt) != RCT_CACHE_EXEMPT_NONE) {
mutex_lock(&rct_module_lock);
rct_add_valid_ind_branch_target(dcontext, target_addr);
mutex_unlock(&rct_module_lock);
}
return res;
}
good:
LOG(THREAD, LOG_RCT, 3, "RCT: good ind %s to "PFX"\n", ibranch_type, target_addr);
DOSTATS({
if (is_ind_call)
STATS_INC(rct_ind_call_good);
else
STATS_INC(rct_ind_jmp_good);
});
return 1;
}
#ifdef WINDOWS
/* Add allowed targets in dynamorio.dll.
* Note: currently only needed for WINDOWS
* Note: needs dynamo_dll_start to be initialized (currently done by
* vmareas_init->find_dynamo_library_vm_areas)
*/
void
rct_known_targets_init(void)
{
/* Allow targeting dynamorio!generic_nudge_target and
* dynamorio!safe_apc_or_thread_target (case 7266)
*/
mutex_lock(&rct_module_lock);
ASSERT(is_in_dynamo_dll((app_pc)generic_nudge_target));
rct_add_valid_ind_branch_target(GLOBAL_DCONTEXT, (app_pc)generic_nudge_target);
ASSERT(is_in_dynamo_dll((app_pc)safe_apc_or_thread_target));
rct_add_valid_ind_branch_target(GLOBAL_DCONTEXT,
(app_pc)safe_apc_or_thread_target);
mutex_unlock(&rct_module_lock);
}
#endif
void
rct_init(void)
{
if (!(TEST(OPTION_ENABLED, DYNAMO_OPTION(rct_ind_call)) ||
TEST(OPTION_ENABLED, DYNAMO_OPTION(rct_ind_jump)))) {
ASSERT(!(TESTANY(OPTION_REPORT|OPTION_BLOCK, DYNAMO_OPTION(rct_ind_call)) ||
TESTANY(OPTION_REPORT|OPTION_BLOCK, DYNAMO_OPTION(rct_ind_jump))));
return;
}
/* FIXME: hashtable_init currently in fragment.c should be moved here */
IF_WINDOWS(rct_known_targets_init());
}
void
rct_exit(void)
{
if (!(TEST(OPTION_ENABLED, DYNAMO_OPTION(rct_ind_call)) ||
TEST(OPTION_ENABLED, DYNAMO_OPTION(rct_ind_jump))))
return;
DELETE_LOCK(rct_module_lock);
}
#endif /* RCT_IND_BRANCH */