mirror of
https://github.com/google/santa.git
synced 2026-04-24 03:00:12 -04:00
419 lines
12 KiB
C++
419 lines
12 KiB
C++
/// Copyright 2015 Google Inc. All rights reserved.
|
|
///
|
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
/// you may not use this file except in compliance with the License.
|
|
/// You may obtain a copy of the License at
|
|
///
|
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
///
|
|
/// Unless required by applicable law or agreed to in writing, software
|
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
/// See the License for the specific language governing permissions and
|
|
/// limitations under the License.
|
|
|
|
#include "SantaDecisionManager.h"
|
|
|
|
#define super OSObject
|
|
OSDefineMetaClassAndStructors(SantaDecisionManager, OSObject);
|
|
|
|
#pragma mark Object Lifecycle
|
|
|
|
bool SantaDecisionManager::init() {
|
|
sdm_lock_grp_ = lck_grp_alloc_init("santa-locks", lck_grp_attr_alloc_init());
|
|
dataqueue_lock_ = lck_mtx_alloc_init(sdm_lock_grp_, lck_attr_alloc_init());
|
|
cached_decisions_lock_ = lck_rw_alloc_init(sdm_lock_grp_, lck_attr_alloc_init());
|
|
|
|
cached_decisions_ = OSDictionary::withCapacity(1000);
|
|
|
|
return kIOReturnSuccess;
|
|
}
|
|
|
|
void SantaDecisionManager::free() {
|
|
if (cached_decisions_) {
|
|
cached_decisions_->release();
|
|
cached_decisions_ = NULL;
|
|
}
|
|
|
|
if (cached_decisions_lock_) {
|
|
lck_rw_free(cached_decisions_lock_, sdm_lock_grp_);
|
|
cached_decisions_lock_ = NULL;
|
|
}
|
|
|
|
if (dataqueue_lock_ ) {
|
|
lck_mtx_free(dataqueue_lock_, sdm_lock_grp_);
|
|
dataqueue_lock_ = NULL;
|
|
}
|
|
|
|
if (sdm_lock_grp_) {
|
|
lck_grp_free(sdm_lock_grp_);
|
|
sdm_lock_grp_ = NULL;
|
|
}
|
|
|
|
super::free();
|
|
}
|
|
|
|
#pragma mark Client Management
|
|
|
|
void SantaDecisionManager::ConnectClient(IOSharedDataQueue *queue, pid_t pid) {
|
|
if (!pid) return;
|
|
if (!queue) return;
|
|
|
|
// Any decisions made while the daemon wasn't
|
|
// connected should be cleared
|
|
ClearCache();
|
|
|
|
lck_mtx_lock(dataqueue_lock_);
|
|
dataqueue_ = queue;
|
|
dataqueue_->retain();
|
|
lck_mtx_unlock(dataqueue_lock_);
|
|
|
|
owning_pid_ = pid;
|
|
owning_proc_ = proc_find(pid);
|
|
}
|
|
|
|
void SantaDecisionManager::DisconnectClient() {
|
|
if (owning_pid_ < 1) return;
|
|
|
|
owning_pid_ = -1;
|
|
|
|
// Ask santad to shutdown, in case it's running.
|
|
santa_message_t message;
|
|
message.action = ACTION_REQUEST_SHUTDOWN;
|
|
message.userId = 0;
|
|
message.pid = 0;
|
|
message.ppid = 0;
|
|
message.vnode_id = 0;
|
|
PostToQueue(message);
|
|
|
|
lck_mtx_lock(dataqueue_lock_);
|
|
dataqueue_->release();
|
|
dataqueue_ = NULL;
|
|
lck_mtx_unlock(dataqueue_lock_);
|
|
|
|
proc_rele(owning_proc_);
|
|
owning_proc_ = NULL;
|
|
}
|
|
|
|
bool SantaDecisionManager::ClientConnected() {
|
|
return owning_pid_ > 0;
|
|
}
|
|
|
|
# pragma mark Listener Control
|
|
|
|
kern_return_t SantaDecisionManager::StartListener() {
|
|
vnode_listener_ = kauth_listen_scope(KAUTH_SCOPE_VNODE,
|
|
vnode_scope_callback,
|
|
reinterpret_cast<void *>(this));
|
|
if (!vnode_listener_) return kIOReturnInternalError;
|
|
|
|
LOGD("Vnode listener started.");
|
|
|
|
return kIOReturnSuccess;
|
|
}
|
|
|
|
kern_return_t SantaDecisionManager::StopListener() {
|
|
kauth_unlisten_scope(vnode_listener_);
|
|
vnode_listener_ = NULL;
|
|
|
|
// Wait for any active invocations to finish before returning
|
|
do {
|
|
IOSleep(5);
|
|
} while (listener_invocations_);
|
|
|
|
// Delete any cached decisions
|
|
ClearCache();
|
|
|
|
LOGD("Vnode listener stopped.");
|
|
|
|
return kIOReturnSuccess;
|
|
}
|
|
|
|
#pragma mark Cache Management
|
|
|
|
void SantaDecisionManager::AddToCache(
|
|
const char *identifier, santa_action_t decision, uint64_t microsecs) {
|
|
lck_rw_lock_exclusive(cached_decisions_lock_);
|
|
|
|
if (cached_decisions_->getCount() > kMaxCacheSize) {
|
|
// This could be made a _lot_ smarter, say only removing entries older
|
|
// than a certain time period. However, with a kMaxCacheSize set
|
|
// sufficiently large and a kMaxAllowCacheTimeMilliseconds set
|
|
// sufficiently low, this should only ever occur if someone is purposefully
|
|
// trying to make the cache grow.
|
|
LOGD("Cache too large, flushing.");
|
|
cached_decisions_->flushCollection();
|
|
}
|
|
|
|
if (decision == ACTION_REQUEST_CHECKBW) {
|
|
SantaMessage *pending = new SantaMessage();
|
|
pending->setAction(ACTION_REQUEST_CHECKBW, 0);
|
|
cached_decisions_->setObject(identifier, pending);
|
|
pending->release(); // it was retained when added to the dictionary
|
|
} else {
|
|
SantaMessage *pending = OSDynamicCast(
|
|
SantaMessage, cached_decisions_->getObject(identifier));
|
|
if (pending) {
|
|
pending->setAction(decision, microsecs);
|
|
}
|
|
}
|
|
|
|
lck_rw_unlock_exclusive(cached_decisions_lock_);
|
|
}
|
|
|
|
void SantaDecisionManager::CacheCheck(const char *identifier) {
|
|
lck_rw_lock_shared(cached_decisions_lock_);
|
|
bool shouldInvalidate = (cached_decisions_->getObject(identifier) != NULL);
|
|
if (shouldInvalidate) {
|
|
lck_rw_lock_shared_to_exclusive(cached_decisions_lock_);
|
|
cached_decisions_->removeObject(identifier);
|
|
lck_rw_unlock_exclusive(cached_decisions_lock_);
|
|
} else {
|
|
lck_rw_unlock_shared(cached_decisions_lock_);
|
|
}
|
|
}
|
|
|
|
uint64_t SantaDecisionManager::CacheCount() {
|
|
return cached_decisions_->getCount();
|
|
}
|
|
|
|
void SantaDecisionManager::ClearCache() {
|
|
lck_rw_lock_exclusive(cached_decisions_lock_);
|
|
cached_decisions_->flushCollection();
|
|
lck_rw_unlock_exclusive(cached_decisions_lock_);
|
|
}
|
|
|
|
santa_action_t SantaDecisionManager::GetFromCache(const char *identifier) {
|
|
santa_action_t result = ACTION_UNSET;
|
|
uint64_t decision_time = 0;
|
|
|
|
lck_rw_lock_shared(cached_decisions_lock_);
|
|
SantaMessage *cached_decision = OSDynamicCast(
|
|
SantaMessage, cached_decisions_->getObject(identifier));
|
|
if (cached_decision) {
|
|
result = cached_decision->getAction();
|
|
decision_time = cached_decision->getMicrosecs();
|
|
}
|
|
lck_rw_unlock_shared(cached_decisions_lock_);
|
|
|
|
if (CHECKBW_RESPONSE_VALID(result)) {
|
|
uint64_t diff_time = GetCurrentUptime();
|
|
|
|
if (result == ACTION_RESPOND_CHECKBW_ALLOW) {
|
|
if ((kMaxAllowCacheTimeMilliseconds * 1000) > diff_time) {
|
|
diff_time = 0;
|
|
} else {
|
|
diff_time -= (kMaxAllowCacheTimeMilliseconds * 1000);
|
|
}
|
|
} else if (result == ACTION_RESPOND_CHECKBW_DENY) {
|
|
if ((kMaxDenyCacheTimeMilliseconds * 1000) > diff_time) {
|
|
diff_time = 0;
|
|
} else {
|
|
diff_time -= (kMaxDenyCacheTimeMilliseconds * 1000);
|
|
}
|
|
}
|
|
|
|
if (decision_time < diff_time) {
|
|
lck_rw_lock_exclusive(cached_decisions_lock_);
|
|
cached_decisions_->removeObject(identifier);
|
|
lck_rw_unlock_exclusive(cached_decisions_lock_);
|
|
return ACTION_UNSET;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
# pragma mark Queue Management
|
|
|
|
bool SantaDecisionManager::PostToQueue(santa_message_t message) {
|
|
lck_mtx_lock(dataqueue_lock_);
|
|
bool kr = false;
|
|
if (dataqueue_) {
|
|
kr = dataqueue_->enqueue(&message, sizeof(message));
|
|
}
|
|
lck_mtx_unlock(dataqueue_lock_);
|
|
return kr;
|
|
}
|
|
|
|
santa_action_t SantaDecisionManager::FetchDecision(
|
|
const kauth_cred_t credential,
|
|
const vfs_context_t vfs_context,
|
|
const vnode_t vnode) {
|
|
santa_action_t return_action = ACTION_UNSET;
|
|
|
|
// Fetch Vnode ID & string
|
|
uint64_t vnode_id = GetVnodeIDForVnode(vfs_context, vnode);
|
|
char vnode_id_str[MAX_VNODE_ID_STR];
|
|
snprintf(vnode_id_str, MAX_VNODE_ID_STR, "%llu", vnode_id);
|
|
|
|
// Check to see if item is in cache
|
|
return_action = GetFromCache(vnode_id_str);
|
|
|
|
// If item wasn't in cache, fetch decision from daemon.
|
|
if (!CHECKBW_RESPONSE_VALID(return_action)) {
|
|
// Add pending request to cache
|
|
AddToCache(vnode_id_str, ACTION_REQUEST_CHECKBW, 0);
|
|
|
|
// Get path
|
|
char path[MAX_PATH_LEN];
|
|
int name_len = MAX_PATH_LEN;
|
|
if (vn_getpath(vnode, path, &name_len) != 0) {
|
|
path[0] = '\0';
|
|
}
|
|
|
|
if (!ClientConnected()) {
|
|
LOGI("Execution request without daemon running: %s", path);
|
|
AddToCache(vnode_id_str,
|
|
ACTION_RESPOND_CHECKBW_ALLOW,
|
|
GetCurrentUptime());
|
|
return ACTION_RESPOND_CHECKBW_ALLOW;
|
|
}
|
|
|
|
// Prepare to send message to daemon
|
|
santa_message_t message;
|
|
strncpy(message.path, path, MAX_PATH_LEN);
|
|
message.userId = kauth_cred_getuid(credential);
|
|
message.pid = proc_selfpid();
|
|
message.ppid = proc_selfppid();
|
|
message.action = ACTION_REQUEST_CHECKBW;
|
|
message.vnode_id = vnode_id;
|
|
|
|
// Wait for the daemon to respond or die.
|
|
do {
|
|
// Send request to daemon...
|
|
if (!PostToQueue(message)) {
|
|
LOGE("Failed to queue request for %s.", path);
|
|
CacheCheck(vnode_id_str);
|
|
return ACTION_ERROR;
|
|
}
|
|
|
|
// ... and wait for it to respond. If after kRequestLoopSleepMilliseconds
|
|
// * kMaxRequestLoops it still hasn't responded, send request again.
|
|
for (int i = 0; i < kMaxRequestLoops; ++i) {
|
|
IOSleep(kRequestLoopSleepMilliseconds);
|
|
return_action = GetFromCache(vnode_id_str);
|
|
if (CHECKBW_RESPONSE_VALID(return_action)) break;
|
|
}
|
|
} while (!CHECKBW_RESPONSE_VALID(return_action) &&
|
|
proc_exiting(owning_proc_) == 0);
|
|
|
|
// If response is still not valid, the daemon exited
|
|
if (!CHECKBW_RESPONSE_VALID(return_action)) {
|
|
LOGE("Daemon process did not respond correctly. Allowing executions "
|
|
"until it comes back.");
|
|
CacheCheck(vnode_id_str);
|
|
return ACTION_ERROR;
|
|
}
|
|
}
|
|
|
|
return return_action;
|
|
}
|
|
|
|
# pragma mark Misc
|
|
|
|
uint64_t SantaDecisionManager::GetVnodeIDForVnode(const vfs_context_t context,
|
|
const vnode_t vp) {
|
|
struct vnode_attr vap;
|
|
VATTR_INIT(&vap);
|
|
VATTR_WANTED(&vap, va_fileid);
|
|
vnode_getattr(vp, &vap, context);
|
|
return vap.va_fileid;
|
|
}
|
|
|
|
uint64_t SantaDecisionManager::GetCurrentUptime() {
|
|
clock_sec_t sec;
|
|
clock_usec_t usec;
|
|
clock_get_system_microtime(&sec, &usec);
|
|
return (uint64_t)((sec * 1000000) + usec);
|
|
}
|
|
|
|
# pragma mark Invocation Tracking & PID comparison
|
|
|
|
void SantaDecisionManager::IncrementListenerInvocations() {
|
|
OSIncrementAtomic(&listener_invocations_);
|
|
}
|
|
|
|
void SantaDecisionManager::DecrementListenerInvocations() {
|
|
OSDecrementAtomic(&listener_invocations_);
|
|
}
|
|
|
|
#undef super
|
|
|
|
#pragma mark Kauth Callback
|
|
|
|
extern "C" int vnode_scope_callback(
|
|
kauth_cred_t credential, void *idata, kauth_action_t action,
|
|
uintptr_t arg0, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3) {
|
|
// The default action is to defer
|
|
int returnResult = KAUTH_RESULT_DEFER;
|
|
|
|
// Cast arguments to correct types
|
|
if (idata == NULL) {
|
|
LOGE("Vnode callback established without valid decision manager.");
|
|
return returnResult;
|
|
}
|
|
SantaDecisionManager *sdm = OSDynamicCast(
|
|
SantaDecisionManager, reinterpret_cast<OSObject *>(idata));
|
|
vfs_context_t vfs_context = reinterpret_cast<vfs_context_t>(arg0);
|
|
vnode_t vnode = reinterpret_cast<vnode_t>(arg1);
|
|
|
|
// Only operate on regular files (not directories, symlinks, etc.)
|
|
vtype vt = vnode_vtype(vnode);
|
|
if (vt != VREG) return returnResult;
|
|
|
|
// Don't operate on ACCESS events, as they're advisory
|
|
if (action & KAUTH_VNODE_ACCESS) return returnResult;
|
|
|
|
// Filter for only writes
|
|
if (action & KAUTH_VNODE_WRITE_DATA ||
|
|
action & KAUTH_VNODE_APPEND_DATA ||
|
|
action & KAUTH_VNODE_DELETE) {
|
|
char vnode_id_str[MAX_VNODE_ID_STR];
|
|
snprintf(vnode_id_str, MAX_VNODE_ID_STR, "%llu",
|
|
sdm->GetVnodeIDForVnode(vfs_context, vnode));
|
|
|
|
// If an execution request is pending, deny write
|
|
if (sdm->GetFromCache(vnode_id_str) == ACTION_REQUEST_CHECKBW) {
|
|
LOGD("Denying write due to pending execution: %s", vnode_id_str);
|
|
*(reinterpret_cast<int *>(arg3)) = EACCES;
|
|
return KAUTH_RESULT_DENY;
|
|
}
|
|
|
|
// Otherwise remove from cache
|
|
sdm->CacheCheck(vnode_id_str);
|
|
|
|
return returnResult;
|
|
}
|
|
|
|
// Filter for only EXECUTE actions
|
|
if (action & KAUTH_VNODE_EXECUTE) {
|
|
sdm->IncrementListenerInvocations();
|
|
|
|
// Fetch decision
|
|
santa_action_t returnedAction = sdm->FetchDecision(
|
|
credential, vfs_context, vnode);
|
|
|
|
switch (returnedAction) {
|
|
case ACTION_RESPOND_CHECKBW_ALLOW:
|
|
returnResult = KAUTH_RESULT_ALLOW;
|
|
break;
|
|
case ACTION_RESPOND_CHECKBW_DENY:
|
|
*(reinterpret_cast<int *>(arg3)) = EACCES;
|
|
returnResult = KAUTH_RESULT_DENY;
|
|
break;
|
|
default:
|
|
// NOTE: Any unknown response or error condition causes us to fail open.
|
|
// Whilst from a security perspective this is bad, it's important that
|
|
// we don't break user's machines.
|
|
break;
|
|
}
|
|
|
|
sdm->DecrementListenerInvocations();
|
|
|
|
return returnResult;
|
|
}
|
|
|
|
return returnResult;
|
|
}
|