Files
ProcInfo/procInfo/ProcessMonitor.m
2019-02-10 17:36:36 -10:00

787 lines
21 KiB
Objective-C

//
// File: ProcessMonitor.m
// Project: Proc Info
//
// Created by: Patrick Wardle
// Copyright: 2017 Objective-See
// License: Creative Commons Attribution-NonCommercial 4.0 International License
//
//disable incomplete/umbrella warnings
// otherwise complains about 'audit_kevents.h'
#pragma clang diagnostic ignored "-Wincomplete-umbrella"
#import "Consts.h"
#import "procInfo.h"
#import "Utilities.h"
#import <unistd.h>
#import <libproc.h>
#import <pthread.h>
#import <bsm/audit.h>
#import <sys/ioctl.h>
#import <sys/types.h>
#import <bsm/libbsm.h>
#import <Cocoa/Cocoa.h>
#import <bsm/audit_kevents.h>
#import <security/audit/audit_ioctl.h>
@interface ProcInfo()
/* INSTANCE VARIABLES */
//skip CPU-intensive logic
@property BOOL goEasy;
//callback block
@property(nonatomic, copy)ProcessCallbackBlock processCallback;
//stop flag
@property BOOL shouldStop;
@end
@implementation ProcInfo
//init
// just check OS version
-(id _Nullable)init
{
//super
self = [super init];
if(self)
{
//make sure OS is supported
// for now, OS X 10.8+ though could be earlier?
if(YES != PI_isSupportedOS())
{
//err msg
NSLog(@"ERROR: %@ is not a supported OS", PI_getOSVersion());
//unset
self = nil;
//bail
goto bail;
}
}
bail:
return self;
}
//init w/ flag
// flag dictates if CPU-intensive logic (code signing, etc) should be preformed
-(id _Nullable)init:(BOOL)goEasy;
{
//init
// calls 'super' too
self = [self init];
if(self)
{
//save mode
self.goEasy = goEasy;
}
bail:
return self;
}
//start monitoring
// note: requires root/macOS 10.12.4+ for full monitoring
-(void)start:(ProcessCallbackBlock)callback
{
//OS version info
NSDictionary* osVersionInfo = nil;
//save
self.processCallback = callback;
//get OS version info
osVersionInfo = PI_getOSVersion();
//do basic (app) monitoring
// if not root, or OS version is < 10.12.4 (due to kernel bug)
if( (0 != getuid()) ||
([osVersionInfo[@"minorVersion"] intValue] < OS_MINOR_VERSION_SIERRA) ||
(([osVersionInfo[@"minorVersion"] intValue] == OS_MINOR_VERSION_SIERRA) && ([osVersionInfo[@"bugfixVersion"] intValue] < 4)) )
{
//setup app monitoring
[self appMonitor];
}
//otherwise, enable full monitoring
else
{
//start process monitoring via openBSM to get apps & procs
// sits in while(YES) loop, so we invoke call in a background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//monitor
[self monitor];
});
}
return;
}
//stop monitoring
-(void)stop
{
//stop app monitoring
// can always call, even if we didn't setup app monitor
[[NSNotificationCenter defaultCenter] removeObserver: self];
//set 'stop' monitor bool
// is checked in 'monitor' method as termination condition
self.shouldStop = YES;
return;
}
//monitor for new process events
-(void)monitor
{
//event mask
// what event classes to watch for
u_int eventClasses = AUDIT_CLASS_EXEC | AUDIT_CLASS_PROCESS;
//file pointer to audit pipe
FILE* auditFile = NULL;
//file descriptor for audit pipe
int auditFileDescriptor = -1;
//status var
int status = -1;
//preselect mode
int mode = -1;
//queue length
int maxQueueLength = -1;
//record buffer
u_char* recordBuffer = NULL;
//token struct
tokenstr_t tokenStruct = {0};
//total length of record
int recordLength = -1;
//amount of record left to process
int recordBalance = -1;
//amount currently processed
int processedLength = -1;
//process record obj
Process* process = nil;
//last fork
Process* lastFork = nil;
//argument
NSString* argument = nil;
//open audit pipe for reading
auditFile = fopen(AUDIT_PIPE, "r");
if(auditFile == NULL)
{
#ifdef DEBUG
//err msg
NSLog(@"ERROR: failed to open audit pipe %s", AUDIT_PIPE);
#endif
//bail
goto bail;
}
//grab file descriptor
auditFileDescriptor = fileno(auditFile);
//init mode
mode = AUDITPIPE_PRESELECT_MODE_LOCAL;
//set preselect mode
status = ioctl(auditFileDescriptor, AUDITPIPE_SET_PRESELECT_MODE, &mode);
if(-1 == status)
{
//bail
goto bail;
}
//grab max queue length
status = ioctl(auditFileDescriptor, AUDITPIPE_GET_QLIMIT_MAX, &maxQueueLength);
if(-1 == status)
{
//bail
goto bail;
}
//set queue length to max
status = ioctl(auditFileDescriptor, AUDITPIPE_SET_QLIMIT, &maxQueueLength);
if(-1 == status)
{
//bail
goto bail;
}
//set preselect flags
// event classes we're interested in
status = ioctl(auditFileDescriptor, AUDITPIPE_SET_PRESELECT_FLAGS, &eventClasses);
if(-1 == status)
{
//bail
goto bail;
}
//set non-attributable flags
// event classes we're interested in
status = ioctl(auditFileDescriptor, AUDITPIPE_SET_PRESELECT_NAFLAGS, &eventClasses);
if(-1 == status)
{
//bail
goto bail;
}
//forever
// read/parse/process audit records
while(YES)
{
@autoreleasepool
{
//first check termination flag/condition
if(YES == self.shouldStop)
{
//bail
goto bail;
}
//reset process record object
process = nil;
//free prev buffer
if(NULL != recordBuffer)
{
//free
free(recordBuffer);
//unset
recordBuffer = NULL;
}
//read a single audit record
// note: buffer is allocated by function, so must be freed when done
recordLength = au_read_rec(auditFile, &recordBuffer);
//sanity check
if(-1 == recordLength)
{
//continue
continue;
}
//init (remaining) balance to record's total length
recordBalance = recordLength;
//init processed length to start (zer0)
processedLength = 0;
//parse record
// read all tokens/process
while(0 != recordBalance)
{
//extract token
// and sanity check
if(-1 == au_fetch_tok(&tokenStruct, recordBuffer + processedLength, recordBalance))
{
//error
// skip record
break;
}
//ignore records that are not related to process exec'ing/spawning
// gotta wait till we hit/capture a AUT_HEADER* though, as this has the event type
if( (nil != process) &&
(YES != [self shouldProcessRecord:process.type]) )
{
//bail
// skips rest of record
break;
}
//process token(s)
// create Process object, etc
switch(tokenStruct.id)
{
//handle start of record
// grab event type, which allows us to ignore events not of interest
case AUT_HEADER32:
case AUT_HEADER32_EX:
case AUT_HEADER64:
case AUT_HEADER64_EX:
{
//create a new process
process = [[Process alloc] init];
//save type
process.type = tokenStruct.tt.hdr32.e_type;
break;
}
//path
// note: this might be updated/replaced later (if it's '/dev/null', etc)
case AUT_PATH:
{
//save path
process.path = [NSString stringWithUTF8String:tokenStruct.tt.path.path];
break;
}
//subject
// extract/save pid || ppid
// all these cases can be treated as subj32 cuz only accessing initial members
case AUT_SUBJECT32:
case AUT_SUBJECT32_EX:
case AUT_SUBJECT64:
case AUT_SUBJECT64_EX:
{
//SPAWN (pid/ppid)
// if there was an AUT_ARG32 (which always come first), that's the pid! so this will be the ppid
if(AUE_POSIX_SPAWN == process.type)
{
//no AUT_ARG32?
// set as pid, and try manually to get ppid
if(-1 == process.pid)
{
//set pid
process.pid = tokenStruct.tt.subj32.pid;
//manually get parent
process.ppid = [Process getParentID:process.pid];
}
//pid already set (via AUT_ARG32)
// this then, is the ppid
else
{
//set ppid
process.ppid = tokenStruct.tt.subj32.pid;
}
}
//FORK
// ppid (pid is in AUT_ARG32)
else if(AUE_FORK == process.type)
{
//set ppid
process.ppid = tokenStruct.tt.subj32.pid;
}
//AUE_EXEC/VE & AUE_EXIT
// this is the pid
else
{
//save pid
process.pid = tokenStruct.tt.subj32.pid;
//manually get parent
process.ppid = [Process getParentID:process.pid];
}
//get effective user id
process.uid = tokenStruct.tt.subj32.euid;
break;
}
//args
// SPAWN/FORK this is pid
case AUT_ARG32:
case AUT_ARG64:
{
//save pid
if( (AUE_POSIX_SPAWN == process.type) ||
(AUE_FORK == process.type) )
{
//32bit
if(AUT_ARG32 == tokenStruct.id)
{
//save
process.pid = tokenStruct.tt.arg32.val;
}
//64bit
else
{
//save
process.pid = (pid_t)tokenStruct.tt.arg64.val;
}
}
//FORK
// doesn't have token for path, so try manually find it now
if(AUE_FORK == process.type)
{
//set path
[process pathFromPid];
}
break;
}
//exec args
// just save into args
case AUT_EXEC_ARGS:
{
//save args
for(int i = 0; i<tokenStruct.tt.execarg.count; i++)
{
//try create arg
// this sometimes fails, not sure why?
argument = [NSString stringWithUTF8String:tokenStruct.tt.execarg.text[i]];
if(nil == argument)
{
//next
continue;
}
//add argument
[process.arguments addObject:argument];
}
break;
}
//exit
// save status
case AUT_EXIT:
{
//save
process.exit = tokenStruct.tt.exit.status;
break;
}
//record trailer
// end/save, etc
case AUT_TRAILER:
{
//end
if( (nil != process) &&
(YES == [self shouldProcessRecord:process.type]) )
{
//handle process exits
if(AUE_EXIT == process.type)
{
//handle
[self handleProcessExit:process];
}
//handle process starts
else
{
//also try get process path
// this is the most 'trusted way' (since exec_args can change)
[process pathFromPid];
//failed to get path at runtime
// if 'AUT_PATH' was something like '/dev/null' or '/dev/console' use arg[0]...yes this can be spoofed :/
if( ((0 == process.path.length) || (YES == [process.path hasPrefix:@"/dev/"])) &&
(0 != process.arguments.count) )
{
//use arg[0]
process.path = process.arguments.firstObject;
}
//save fork events
// this will have ppid that can be used for child events (exec/spawn, etc)
if(AUE_FORK == process.type)
{
//save
lastFork = process;
}
//when we don't have a ppid
// see if there was a 'matching' fork() that has it (only for non AUE_FORK events)
else if( (-1 == process.ppid) &&
(lastFork.pid == process.pid) )
{
//update
process.ppid = lastFork.ppid;
}
//handle new process
[self handleProcessStart:process];
}
}
//unset
process = nil;
break;
}
default:
;
}//process token
//add length of current token
processedLength += tokenStruct.len;
//subtract lenght of current token
recordBalance -= tokenStruct.len;
}
}//autorelease
} //while(YES)
bail:
//free buffer
if(NULL != recordBuffer)
{
//free
free(recordBuffer);
//unset
recordBuffer = NULL;
}
//close audit pipe
if(NULL != auditFile)
{
//close
fclose(auditFile);
//unset
auditFile = NULL;
}
return;
}
//check if event is one we care about
// for now, just anything associated with new processes/exits
-(BOOL)shouldProcessRecord:(u_int16_t)eventType
{
//flag
BOOL shouldProcess = NO;
//check
if( (eventType == AUE_EXEC) ||
(eventType == AUE_EXIT) ||
(eventType == AUE_FORK) ||
(eventType == AUE_EXECVE) ||
(eventType == AUE_POSIX_SPAWN) )
{
//set flag
shouldProcess = YES;
}
return shouldProcess;
}
//register for app launchings
-(void)appMonitor
{
//notification center
NSNotificationCenter* center = nil;
//get shared center
center = [[NSWorkspace sharedWorkspace] notificationCenter];
//register app start notification
[center addObserver:self selector:@selector(appEvent:) name:NSWorkspaceDidLaunchApplicationNotification object:nil];
//register app exit notifcation
[center addObserver:self selector:@selector(appEvent:) name:NSWorkspaceDidTerminateApplicationNotification object:nil];
return;
}
//automatically invoked when an app is started/exits
// create new process object and add to dictionary
-(void)appEvent:(NSNotification *)notification
{
//process object
Process* process = nil;
//pid
pid_t processID = -1;
//get process id
processID = [notification.userInfo[@"NSApplicationProcessIdentifier"] intValue];
//app start?
if(YES == [notification.name isEqualToString:@"NSWorkspaceDidLaunchApplicationNotification"])
{
//create a new process obj
process = [[Process alloc] init:processID];
if(nil == process)
{
#ifdef DEBUG
//err msg
NSLog(@"ERROR: failed to create process object for %d/%@", processID, notification.userInfo);
#endif
//bail
goto bail;
}
//start event
[self handleProcessStart:process];
}
//app exit
else
{
//alloc new process obj
process = [[Process alloc] init];
//manually set pid
process.pid = processID;
//manully set type to exit
process.type = EVENT_EXIT;
//exit event
[self handleProcessExit:process];
}
bail:
return;
}
//handle new process
// create Binary obj/enum/process ancestors, etc
-(void)handleProcessStart:(Process*)process
{
//default cs flags
// note: since this is dynamic check, we don't need to check all architectures, or skip resources, etc...
SecCSFlags flags = kSecCSDefaultFlags;
//sanity check
// should only occur for fork() events, which normally get superceeded by an exec(), etc
if( (-1 == process.pid) ||
(nil == process.path) )
{
//bail
goto bail;
}
//get parent
if(-1 == process.ppid)
{
//get ppid
process.ppid = [Process getParentID:process.pid];
}
//enumerate process ancestry
if(0 == process.ancestors.count)
{
//enumerate
[process enumerateAncestors];
}
//generate binary
process.binary = [[Binary alloc] init:process.path];
if(nil == process.binary)
{
#ifdef DEBUG
//err msg
NSLog(@"ERROR: failed to create binary object for %d/%@", process.pid, process.path);
#endif
//bail
goto bail;
}
//automatically generate signing info/icon?
// these can be skipped for performance reasons
if(YES != self.goEasy)
{
//generate signing info
// first will try dynamic, falling back to static
[process generateSigningInfo:flags];
//set icon
[process.binary getIcon];
}
//invoke user callback
self.processCallback(process);
bail:
return;
}
//handle process exit event
// as only have pid, just alert user
-(void)handleProcessExit:(Process*)process
{
//invoke user callback
self.processCallback(process);
return;
}
//return array of running processes
-(NSMutableArray*)currentProcesses
{
//default cs flags
// note: since this is dynamic check, we don't need to check all architectures, or skip resources, etc...
SecCSFlags flags = kSecCSDefaultFlags;
//current process
Process* currentProcess = nil;
//processes
NSMutableArray* processes = nil;
//alloc array
processes = [NSMutableArray array];
//iterate over all pids
// init process object w/ pid/path, etc
for(NSNumber* pid in PI_enumerateProcesses())
{
//create process obj
currentProcess = [[Process alloc] init:pid.intValue];
if(nil == currentProcess)
{
//skip
continue;
}
//generate signing info
[currentProcess generateSigningInfo:flags];
//add
[processes addObject:currentProcess];
}
return processes;
}
@end