* Copyright (c) 2012-2022 Google, Inc. All rights reserved.
* Copyright (c) 2006-2010 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.
*/
#include "globals.h"
#include "utils.h"
#include "moduledb.h"
#include "module_shared.h"
* moduledb_exempt_list_t enum. We have the ugliness of an extra indirection
* and dynamic sizing to move the array into the heap and avoid self
* protection changes. */
static char **exemption_lists = NULL;
#define GET_EXEMPT_LIST(list) (exemption_lists[(list)])
* without holding the lock */
DECLARE_CXTSWPROT_VAR(read_write_lock_t moduledb_lock,
INIT_READWRITE_LOCK(moduledb_lock));
static char *
moduledb_add_to_exempt_list(char *list, const char *name)
{
size_t cur_size_less_null = (list == NULL) ? 0 : strlen(list);
size_t needed_size = cur_size_less_null + ((cur_size_less_null > 0) ? 1 : 0) +
strlen(name) + 1 ;
char *new_list =
(char *)global_heap_realloc(list, cur_size_less_null + 1 , needed_size,
sizeof(*list) HEAPACCT(ACCT_OTHER));
if (cur_size_less_null == 0)
*new_list = '\0';
else
ASSERT(strlen(new_list) == cur_size_less_null);
if (cur_size_less_null > 0) {
strncat(new_list, ";", needed_size - cur_size_less_null - 1 );
}
strncat(new_list, name, needed_size - strlen(new_list) - 1 );
return new_list;
}
static char *
moduledb_remove_from_exempt_list(char *list, const char *name)
{
* list it could use a substantial amt of memory and take a while to walk
* (though note we won't get duplicate entries in module churn situations)
* or we could an get an accidental name collision with a later module. */
return list;
}
static void
moduledb_update_exempt_list(char **list, const char *name, bool add)
{
LOG(GLOBAL, LOG_MODULEDB, 2, "\tlist before update \"%s\"\n",
(*list == NULL) ? "" : *list);
d_r_write_lock(&moduledb_lock);
if (add && (*list == NULL || !check_filter(*list, name))) {
*list = moduledb_add_to_exempt_list(*list, name);
} else if (!add && *list != NULL && check_filter(*list, name)) {
*list = moduledb_remove_from_exempt_list(*list, name);
}
d_r_write_unlock(&moduledb_lock);
LOG(GLOBAL, LOG_MODULEDB, 2, "\tlist after update \"%s\"\n",
(*list == NULL) ? "" : *list);
}
static void
moduledb_process_relaxation_flags(uint flags, const char *name, bool add)
{
ASSERT_NOT_IMPLEMENTED(
!TESTANY(~(MODULEDB_ALL_SECTIONS_BITS | MODULEDB_RCT_EXEMPT_TO |
MODULEDB_REPORT_ON_LOAD | MODULEDB_DLL2HEAP | MODULEDB_DLL2STACK),
flags));
if (TEST(MODULEDB_RCT_EXEMPT_TO, flags)) {
LOG(GLOBAL, LOG_MODULEDB, 1, "%s module %s to moduledb exempt rct\n",
add ? "Adding" : "Removing", name);
moduledb_update_exempt_list(&GET_EXEMPT_LIST(MODULEDB_EXEMPT_RCT), name, add);
}
if (TEST(MODULEDB_DLL2HEAP, flags)) {
LOG(GLOBAL, LOG_MODULEDB, 1, "%s module %s to moduledb exempt dll2heap\n",
add ? "Adding" : "Removing", name);
moduledb_update_exempt_list(&GET_EXEMPT_LIST(MODULEDB_EXEMPT_DLL2HEAP), name,
add);
}
if (TEST(MODULEDB_DLL2STACK, flags)) {
LOG(GLOBAL, LOG_MODULEDB, 1, "%s module %s to moduledb exempt dll2stack\n",
add ? "Adding" : "Removing", name);
moduledb_update_exempt_list(&GET_EXEMPT_LIST(MODULEDB_EXEMPT_DLL2STACK), name,
add);
}
if (TESTANY(MODULEDB_ALL_SECTIONS_BITS, flags)) {
uint all_sections =
(flags & MODULEDB_ALL_SECTIONS_BITS) >> MODULEDB_ALL_SECTIONS_SHIFT;
ASSERT_NOT_IMPLEMENTED(all_sections == SECTION_ALLOW);
if (all_sections == SECTION_ALLOW) {
LOG(GLOBAL, LOG_MODULEDB, 1, "%s module %s to moduledb exec if image\n",
add ? "Adding" : "Removing", name);
moduledb_update_exempt_list(&GET_EXEMPT_LIST(MODULEDB_EXEMPT_IMAGE), name,
add);
}
}
}
START_DATA_SECTION(FREQ_PROTECTED_SECTION, "w")
void
moduledb_report_exemption(const char *fmt, app_pc addr1, app_pc addr2, const char *name)
{
ASSERT(exemption_lists != NULL);
DODEBUG({
DO_THRESHOLD_SAFE(DYNAMO_OPTION(moduledb_exemptions_report_max),
FREQ_PROTECTED_SECTION,
{
SYSLOG_INTERNAL_WARNING(fmt, addr1, addr2, name);
},
{ });
});
}
#define MAX_COMPANY_NAME 256
void
moduledb_process_image(const char *name, app_pc base, bool add)
{
char company_name[MAX_COMPANY_NAME];
bool got_company_name;
bool relax = true;
ASSERT(exemption_lists != NULL);
if (!DYNAMO_OPTION(use_moduledb))
return;
got_company_name =
get_module_company_name(base, company_name, BUFFER_SIZE_ELEMENTS(company_name));
if (!got_company_name)
company_name[0] = '\0';
if (got_company_name && company_name[0] != '\0' &&
(!IS_STRING_OPTION_EMPTY(allowlist_company_names_default) ||
!IS_STRING_OPTION_EMPTY(allowlist_company_names)) &&
check_list_default_and_append(dynamo_options.allowlist_company_names_default,
dynamo_options.allowlist_company_names,
company_name)) {
relax = false;
LOG(GLOBAL, LOG_MODULEDB, 1,
"Found module \"%s\" from allowlisted company \"%s\"\n",
name == NULL ? "no-name" : name, company_name);
* field set (drpreinject & liveshields don't), need to avoid
* relaxing for those. Should add version info and also check
* nodgemgr and our other tools etc. xref case 8723 */
}
if (relax) {
if (name == NULL || *name == '\0') {
if (add) {
* too many of these once we also fall back to the version original
* filename for modules with no exports and we'll eventually exempt by
* area in the modules list instead of by name anyways. */
* the module at least, but this a bad time w/respect to the loader to
* be walking the lists. */
SYSLOG_INTERNAL_WARNING("Unable to relax for nameless unknown module "
"from \"%s\" @" PFX,
company_name, base);
}
} else {
LOG(GLOBAL, LOG_MODULEDB, 1, "Loaded unknown module %s\n", name);
moduledb_process_relaxation_flags(DYNAMO_OPTION(unknown_module_policy), name,
add);
* usually 5 of these per process, two for logitech mouse hook
* dlls, 1 for drpreinject and 2 for Norton av. */
if (add &&
TEST(MODULEDB_REPORT_ON_LOAD, DYNAMO_OPTION(unknown_module_policy))) {
DODEBUG({
DO_THRESHOLD_SAFE(DYNAMO_OPTION(unknown_module_load_report_max),
FREQ_PROTECTED_SECTION,
{
SYSLOG_INTERNAL_INFO(
"Relaxing protections for unknown module "
"%s @" PFX " from \"%s\"",
name, base, company_name);
},
{ });
});
}
}
}
}
END_DATA_SECTION()
static const char *const exempt_list_names[MODULEDB_EXEMPT_NUM_LISTS] = { "rct", "image",
"dll2heap",
"dll2stack" };
void
moduledb_init()
{
uint exempt_array_size = (MODULEDB_EXEMPT_NUM_LISTS) * sizeof(char *);
ASSERT(exemption_lists == NULL);
exemption_lists = (char **)global_heap_alloc(exempt_array_size HEAPACCT(ACCT_OTHER));
ASSERT(exemption_lists != NULL);
memset(exemption_lists, 0, exempt_array_size);
DOCHECK(1, {
int i;
for (i = 0; i < MODULEDB_EXEMPT_NUM_LISTS; i++)
ASSERT(exempt_list_names[i] != NULL);
});
}
void
moduledb_exit()
{
int i;
ASSERT(exemption_lists != NULL);
DOLOG(1, LOG_MODULEDB, {
LOG(GLOBAL, LOG_MODULEDB, 1, "Moduledb exit:\n");
print_moduledb_exempt_lists(GLOBAL);
});
for (i = 0; i < MODULEDB_EXEMPT_NUM_LISTS; i++) {
char *list = GET_EXEMPT_LIST(i);
if (list != NULL) {
global_heap_free(list,
strlen(list) +
1
HEAPACCT(ACCT_OTHER));
}
}
global_heap_free(exemption_lists,
(MODULEDB_EXEMPT_NUM_LISTS) * sizeof(char *) HEAPACCT(ACCT_OTHER));
DELETE_READWRITE_LOCK(moduledb_lock);
if (doing_detach)
exemption_lists = NULL;
}
bool
moduledb_exempt_list_empty(moduledb_exempt_list_t list)
{
return GET_EXEMPT_LIST(list) == NULL;
}
* to by list */
bool
moduledb_check_exempt_list(moduledb_exempt_list_t list, const char *name)
{
bool found = false;
ASSERT(exemption_lists != NULL);
d_r_read_lock(&moduledb_lock);
if (GET_EXEMPT_LIST(list) != NULL) {
LOG(GLOBAL, LOG_MODULEDB, 2,
"Moduledb checking %s exempt list =\"%s\" for module \"%s\"\n",
exempt_list_names[list],
GET_EXEMPT_LIST(list) == NULL ? "" : GET_EXEMPT_LIST(list), name);
found = check_filter(GET_EXEMPT_LIST(list), name);
}
d_r_read_unlock(&moduledb_lock);
return found;
}
void
print_moduledb_exempt_lists(file_t file)
{
int i;
ASSERT(exemption_lists != NULL);
d_r_read_lock(&moduledb_lock);
for (i = 0; i < MODULEDB_EXEMPT_NUM_LISTS; i++) {
ASSERT(exempt_list_names[i] != NULL);
print_file(file, "moduledb %s exemption list =\"%s\"\n", exempt_list_names[i],
GET_EXEMPT_LIST(i) == NULL ? "" : GET_EXEMPT_LIST(i));
}
d_r_read_unlock(&moduledb_lock);
}
#ifdef PROCESS_CONTROL
* create a separate file; for now let it be here.
*/
* according to the PRD. In that case there is no need to distinguish between
* "not matched" and "no hashlist". However, if we decide that both need to
* co-exist (Windows sw restriction policies do a mix), then this is needed.
* The core is designed to be flexible to handle both.
*/
enum {
* the hashlist registry key doesn't exist.
*/
PROCESS_CONTROL_NO_HASHLIST,
PROCESS_CONTROL_HASHLIST_EMPTY,
* means we can't use this to make any decision about process control,
* i.e., don't do any process control.
*/
PROCESS_CONTROL_LONG_LIST,
PROCESS_CONTROL_NOT_MATCHED,
PROCESS_CONTROL_MATCHED
};
* being empty; empty hashlist is a wildcard match for allow and block list
* modes, but not for allowlist integrity mode. Note, this macro is used only
* for allow and block list modes, not allowlist integrity mode.
*/
# define IS_PROCESS_CONTROL_MATCHED(x) \
((x) == PROCESS_CONTROL_MATCHED || (x) == PROCESS_CONTROL_HASHLIST_EMPTY)
* Note: the reg_key here is (char *) as opposed to (wchar_t *) because our
* syslogging can only handle regular chars!
*/
static void
process_control_report_long_list(const char *reg_key)
{
char num_hash_str[6];
snprintf(num_hash_str, BUFFER_SIZE_ELEMENTS(num_hash_str), "%d",
DYNAMO_OPTION(pc_num_hashes));
NULL_TERMINATE_BUFFER(num_hash_str);
SYSLOG(SYSLOG_WARNING, PROC_CTL_HASH_LIST_TOO_LONG, 4, get_application_name(),
get_application_pid(), reg_key, num_hash_str);
}
* reg_key for the app and checks if md5_hash is on that list of keys.
* If so it returns true, if not false.
*/
static int
process_control_match(const char *md5_hash,
# ifdef PARAMS_IN_REGISTRY
const IF_WINDOWS_ELSE_NP(wchar_t, char) * reg_key
# else
const char *reg_key
# endif
)
{
int ret_val, res;
char *hash_list;
* that anyone will exceed it even for anonymous hashes. If they do then
* process control will be disabled, i.e., no enforcement will be done.
* Case 9252. BTW, the +1 is for the delimiting ';'.
* FIXME: when we read md5 from a file this should go.
*/
uint list_size = DYNAMO_OPTION(pc_num_hashes) * (MD5_STRING_LENGTH + 1);
* semicolon separated string, just like all core option string.
*/
hash_list = heap_alloc(GLOBAL_DCONTEXT, list_size HEAPACCT(ACCT_OTHER));
hash_list[0] = '\0';
* this only depends on the exe (not its cmdline). */
res = get_unqualified_parameter(reg_key, hash_list, list_size);
hash_list[list_size - 1] = '\0';
if (IS_GET_PARAMETER_SUCCESS(res)) {
if (hash_list[0] == '\0') {
* modes, not for allowlist integrity mode. */
ret_val = PROCESS_CONTROL_HASHLIST_EMPTY;
} else if (check_filter(hash_list, md5_hash)) {
ret_val = PROCESS_CONTROL_MATCHED;
} else {
ret_val = PROCESS_CONTROL_NOT_MATCHED;
}
* specific hashes in app specific keys. Though there is the facility
* to do otherwise, we should restrict them because combinations
* like app specific anonymous hashes don't make sense. But how to?
*/
} else if (res == GET_PARAMETER_BUF_TOO_SMALL) {
ret_val = PROCESS_CONTROL_LONG_LIST;
} else {
ASSERT(res == GET_PARAMETER_FAILURE);
ret_val = PROCESS_CONTROL_NO_HASHLIST;
}
heap_free(GLOBAL_DCONTEXT, hash_list, list_size HEAPACCT(ACCT_OTHER));
return ret_val;
}
* executable's hash matches a hash either on the app specific list or the
* anonymous list, or if any of those lists are empty.
*
* In the allowlist integrity mode, the process will be allowed to run if its
* executable's hash matches a hash on its app specific hashlist or there is no
* app specific hashlist at all. The idea is to ascertain that an executable
* hasn't changed. If there is no need to track the change, then those exe's
* won't have a hashlist.
* Though the regular allowlist mode can be used to do the same, there are
* holes in it like the ones below that have to be manually fixed.
* 1. support for anonymous hashes; would have to be disabled or not used.
* 2. support exe names without hashes; same resolution as point #1.
* 3. apps would be killed if there is no hashlist; would have to add empty
* global hashlists.
*
* Note: xref case 10969 for allowlist integrity mode.
*/
static void
process_control_allowlist(const char *md5_hash)
{
security_option_t type_handling;
action_type_t desired_action;
const char *threat_id = NULL;
int anonymous = PROCESS_CONTROL_NOT_MATCHED;
int app_specific =
process_control_match(md5_hash, PARAM_STR(DYNAMORIO_VAR_APP_PROCESS_ALLOWLIST));
* accidentally; a matter of precedence.
*/
if (IS_PROCESS_CONTROL_MODE_ALLOWLIST()) {
* specific or anonymous hash list.
*/
if (IS_PROCESS_CONTROL_MATCHED(app_specific))
return;
anonymous = process_control_match(
md5_hash, PARAM_STR(DYNAMORIO_VAR_ANON_PROCESS_ALLOWLIST));
if (IS_PROCESS_CONTROL_MATCHED(anonymous))
return;
threat_id = "WHIT.NOMA";
* long, then we can't decide to kill the process because we didn't
* search the full list. Do no harm and ignore process control. Case 9252.
*/
if (anonymous == PROCESS_CONTROL_LONG_LIST) {
process_control_report_long_list(DYNAMORIO_VAR_ANON_PROCESS_ALLOWLIST);
return;
}
} else if (IS_PROCESS_CONTROL_MODE_ALLOWLIST_INTEGRITY()) {
* specific hash list; this is the integrity tracking part. If there
* is no hash list, it means that this process's exe wasn't added to
* the integrity mode, so let it run.
*/
if (app_specific == PROCESS_CONTROL_MATCHED ||
app_specific == PROCESS_CONTROL_NO_HASHLIST)
return;
* for the integrity mode.
*/
ASSERT(app_specific != PROCESS_CONTROL_HASHLIST_EMPTY);
threat_id = "WHIT.INTG";
} else {
ASSERT_NOT_REACHED();
return;
}
* long, then we can't decide to kill the process because we didn't search
* the full list. Do no harm and ignore process control. Case 9252.
*/
if (app_specific == PROCESS_CONTROL_LONG_LIST) {
process_control_report_long_list(DYNAMORIO_VAR_APP_PROCESS_ALLOWLIST);
return;
}
* Note: No hashlist is equivalent to no match; for allowlist, this means
* kill.
*/
ASSERT((app_specific == PROCESS_CONTROL_NOT_MATCHED ||
app_specific == PROCESS_CONTROL_NO_HASHLIST));
if (IS_PROCESS_CONTROL_MODE_ALLOWLIST()) {
ASSERT((anonymous == PROCESS_CONTROL_NOT_MATCHED ||
anonymous == PROCESS_CONTROL_NO_HASHLIST));
}
# ifdef PROGRAM_SHEPHERDING
if (DYNAMO_OPTION(pc_detect_mode)) {
type_handling = OPTION_REPORT;
desired_action = ACTION_CONTINUE;
} else {
type_handling = OPTION_REPORT | OPTION_BLOCK_IGNORE_DETECT;
desired_action = ACTION_TERMINATE_PROCESS;
}
* pid are already in the event, the threat ID has nothing else to
* convey, hence a constant string is used.
*/
security_violation_internal(GLOBAL_DCONTEXT, NULL, PROCESS_CONTROL_VIOLATION,
type_handling, threat_id, desired_action, NULL);
ASSERT(DYNAMO_OPTION(pc_detect_mode));
# endif
}
static void
process_control_blocklist(const char *md5_hash)
{
security_option_t type_handling;
action_type_t desired_action;
int app_specific =
process_control_match(md5_hash, PARAM_STR(DYNAMORIO_VAR_APP_PROCESS_BLOCKLIST));
int anonymous =
process_control_match(md5_hash, PARAM_STR(DYNAMORIO_VAR_ANON_PROCESS_BLOCKLIST));
ASSERT(IS_PROCESS_CONTROL_MODE_BLOCKLIST());
if (IS_PROCESS_CONTROL_MATCHED(app_specific) ||
IS_PROCESS_CONTROL_MATCHED(anonymous)) {
# ifdef PROGRAM_SHEPHERDING
if (DYNAMO_OPTION(pc_detect_mode)) {
type_handling = OPTION_REPORT;
desired_action = ACTION_CONTINUE;
} else {
type_handling = OPTION_REPORT | OPTION_BLOCK_IGNORE_DETECT;
desired_action = ACTION_TERMINATE_PROCESS;
}
* if the current executable's process is either on the anonymous
* or on the app specific allowlist. As the exe name and
* pid are already in the event, the threat ID will only convey what
* list was used, anonymous ("ANON.BLAC") or app-specific ("ANON.BLAC").
*/
security_violation_internal(
GLOBAL_DCONTEXT, NULL, PROCESS_CONTROL_VIOLATION, type_handling,
app_specific == PROCESS_CONTROL_MATCHED ? "BLAC.APPS" : "BLAC.ANON",
desired_action, NULL);
ASSERT(DYNAMO_OPTION(pc_detect_mode));
# endif
} else if (app_specific == PROCESS_CONTROL_LONG_LIST) {
process_control_report_long_list(DYNAMORIO_VAR_APP_PROCESS_BLOCKLIST);
} else if (anonymous == PROCESS_CONTROL_LONG_LIST) {
process_control_report_long_list(DYNAMORIO_VAR_ANON_PROCESS_BLOCKLIST);
} else {
* Note: No hashlist is equivalent to no match; for blocklist, this
* means don't kill.
*/
ASSERT((app_specific == PROCESS_CONTROL_NOT_MATCHED ||
app_specific == PROCESS_CONTROL_NO_HASHLIST) &&
(anonymous == PROCESS_CONTROL_NOT_MATCHED ||
anonymous == PROCESS_CONTROL_NO_HASHLIST));
}
}
* both the startup scenario and the nudge scenario. The bool is to identify
* whether or not we are in the startup phase because only in that phase we
* dump a process-start-event.
*/
void
process_control(void)
{
const char *md5_hash = get_application_md5();
bool is_black, is_white, is_white_intg;
if (strlen(md5_hash) != MD5_STRING_LENGTH) {
* today the default is to ignore and keep going, but is that ok?
* should ev be notified?
*/
ASSERT_NOT_REACHED();
return;
}
is_black = IS_PROCESS_CONTROL_MODE_BLOCKLIST();
is_white = IS_PROCESS_CONTROL_MODE_ALLOWLIST();
is_white_intg = IS_PROCESS_CONTROL_MODE_ALLOWLIST_INTEGRITY();
* However, there is nothing preventing us from using all of them (just
* make each process control mode below a separate if) with a precedence
* order. That was how it was originally; however if there is a bug in EV,
* we would accidentally run multiple modes at once. So to safeguard
* against that the if-else construct below is used.
*/
ASSERT((is_black && !is_white && !is_white_intg) ||
(!is_black && is_white && !is_white_intg) ||
(!is_black && !is_white && is_white_intg));
if (is_black)
process_control_blocklist(md5_hash);
else if (is_white || is_white_intg)
process_control_allowlist(md5_hash);
else
ASSERT_NOT_REACHED();
}
void
process_control_init(void)
{
process_control();
}
#endif