Files
ProcInfo/procInfo/ProcessMonitor.m
T
Patrick Wardle f1333b9ccc namespace/project cleanup
added PI_ to C function to avoid namespace issues
added 'lib' group to Xcode project
2017-08-09 13:26:24 -10:00

735 lines
20 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
//
//TODO: add support for execve?
//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 */
//callback block
@property(nonatomic, copy)ProcessCallbackBlock processCallback;
//stop flag
@property BOOL shouldStop;
@end
@implementation ProcInfo
//init
-(id)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;
}
//start monitoring
// ->note: requires root/macOS 10.12.4+ for full monitoring
-(BOOL)start:(ProcessCallbackBlock)callback
{
//OS version info
NSDictionary* osVersionInfo = nil;
//save
self.processCallback = callback;
//get OS version info
osVersionInfo = PI_getOSVersion();
//for full monitoring, gotta be root and macOS 10.12.4 ('safe', as fixed kernel crash)
if( (0 == geteuid()) &&
([osVersionInfo[@"minorVersion"] intValue] >= OS_MINOR_VERSION_SIERRA) &&
([osVersionInfo[@"bugfixVersion"] intValue] >= 4))
{
//start process monitoring via openBSM to get apps & procs
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//monitor
[self monitor];
});
}
//otherwise, just do app monitoring
// ->yes, this will miss commandline utils, but doesn't require root, and won't panic kernel :/
else
{
//setup callback for app monitoring
[self appMonitor];
}
return NO;
}
//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)
{
//err msg
NSLog(@"ERROR: failed to open audit pipe %s", AUDIT_PIPE);
//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)
{
//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.user = 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
{
//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( ((nil == process.path) || (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;
}
}
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;
}
//TODO: also app exits
//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)
{
//err msg
NSLog(@"ERROR: failed to create process object for %d/%@", processID, notification.userInfo);
//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
{
//sanity check
// ->should only occur for fork() events, which normal 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)
{
//err msg
NSLog(@"ERROR: failed to create binary object for %d/%@", process.pid, process.path);
//bail
goto bail;
}
//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
{
//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())
{
//skip 'blank' pids
if(0 == pid.unsignedShortValue)
{
//skip
continue;
}
//create process obj
currentProcess = [[Process alloc] init:pid.unsignedShortValue];
if(nil == currentProcess)
{
//skip
continue;
}
//add
[processes addObject:currentProcess];
}
return processes;
}
@end