f1333b9ccc
added PI_ to C function to avoid namespace issues added 'lib' group to Xcode project
735 lines
20 KiB
Objective-C
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
|