介绍
使用OpenBSM库对macOS进行实时审核,使用名为auditpipe的特定设备,位于/dev/auditpipe中获取实时的审计事件,重点关注进程的创建和退出
审计管道操作与配置
AUDITPIPE(4) - 用于实时审计事件跟踪的伪设备 (gsp.com)
基于OpenBSM库从管道获取事件
基本安全模块库提供了一些功能来读取这些事件并自动解析它们。
au_read_rec()
int au_read_rec(FILE *fp, u_char **buf);
这个函数让我们从文件描述符中读取一个事件记录,并将内容放在缓冲区buf作为参数传递(必须在使用后被释放)。该函数返回读取的字节数。
au_fetch_tok()
int au_fetch_tok(tokenstr_t *tok, u_char *buf, int len)
从au_read_rec获取的缓冲区包含令牌,每个令牌都是具有不同信息的结构,根据令牌id。缓冲区的第一个令牌始终是AUT_HEADER *令牌:它包含一个字段,指示缓冲区中的哪种事件。下一个令牌包含有关引发事件的进程的路径,事件感兴趣的文件的路径以及其他信息(如用户,时间戳)的信息…要使用记录读取缓冲区,我们必须获取每个令牌它依次使用au_fetch_tok
au_print_tok()
void au_print_tok(FILE *outfp, tokenstr_t *tok, char *del, char raw, char sfrm)
每个事件都是由一些令牌组成的。令牌只是一个C结构,它包含一些根据令牌ID的信息。例如,读取事件具有 3个主令牌:AUT_HEADER,AUT_SUBJECT和AUT_PATH。
AUT_HEADER包含有关事件的信息。在一个读取事件中,它显示事件实际上是一个文件读取(fr)。
AUT_SUBJECT定义哪些过程引发此事件
AUT_PATH指定路线由读AUT_SUBJECT。
代码
-(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;
}
项目地址: