Files
TaskExplorer/Task.m
T
Objective-See Foundation c8a0f6498f bug fixes/project cleanup
v2.2.0
friends updates
removed error reporting
removed old code (pre-macOS 11)

...more changes / improvements soon!
2025-04-30 14:16:30 -07:00

826 lines
24 KiB
Objective-C

//
// Task.m
// TaskExplorer
//
// Created by Patrick Wardle on 5/2/15.
// Copyright (c) 2015 Objective-See, LLC. All rights reserved.
//
#import "File.h"
#import "Task.h"
#import "Consts.h"
#import "Utilities.h"
#import "Connection.h"
#import "AppDelegate.h"
#import "remoteTaskService.h"
#import <syslog.h>
#import <libproc.h>
#import <sys/sysctl.h>
#import <sys/proc_info.h>
@implementation Task
@synthesize pid;
@synthesize uid;
@synthesize ppid;
@synthesize files;
@synthesize binary;
@synthesize dylibs;
@synthesize children;
@synthesize arguments;
@synthesize connections;
//init w/ a pid
// note: time consuming init's are done in other methods
-(id)initWithPID:(NSNumber*)taskPID
{
//task's path
// ->not iVar, as assigned into task's binary obj
NSString* taskPath = nil;
//existing binaries
NSMutableDictionary* existingBinaries = nil;
//existing binary
// ->can re-use for tasks w/ same binary
Binary* existingBinary = nil;
//init super
self = [super init];
if(nil != self)
{
//grab existings binaries
existingBinaries = taskEnumerator.executables;
//since root UID is zero
// ->init UID to -1
//self.uid = -1;
//save pid
self.pid = taskPID;
//alloc array for children
children = [NSMutableArray array];
//alloc array for dylibs
dylibs = [NSMutableArray array];
//alloc array for open files
files = [NSMutableArray array];
//alloc array for network connections
connections = [NSMutableArray array];
//get parent id
self.ppid = [NSNumber numberWithInteger:getParentID([taskPID intValue])];
//get task's path
taskPath = [self getPath];
//try extract existing binary
// ->but only if task's path is known
if(YES != [taskPath isEqualToString:TASK_PATH_UNKNOWN])
{
//lookup
existingBinary = existingBinaries[taskPath];
}
//re-use existing binaries
if(nil != existingBinary)
{
//re-use
self.binary = existingBinary;
}
//generate new binary
else
{
//generate binary obj
// ->time-consuming tasks are preformed in background block
self.binary = [[Binary alloc] initWithParams:@{KEY_RESULT_PATH:taskPath}];
//skip those that error out
if(nil == self.binary)
{
//bail
goto bail;
}
//indicate that binary is a task (main) executable
self.binary.isTaskBinary = YES;
//add to queue
// ->this will process in background
[taskEnumerator.binaryQueue enqueue:self.binary];
//sync
@synchronized(existingBinaries)
{
//add it to 'global' list
existingBinaries[taskPath] = self.binary;
}
}
}//init self
//bail
bail:
return self;
}
//get task's path
// ->via 'proc_pidpath()' or via task's args (via XPC) if that fails...
-(NSString*)getPath
{
//task path
NSString* taskPath = nil;
//buffer for process path
char pathBuffer[PROC_PIDPATHINFO_MAXSIZE] = {0};
//status
int status = -1;
//reset buffer
bzero(pathBuffer, PROC_PIDPATHINFO_MAXSIZE);
//kernel 'task' is special
if(0 == self.pid.intValue)
{
//set
taskPath = path2Kernel();
//all set
goto bail;
}
//get task's path via 'proc_pidpath()'
// ->this might fail, so will then attempt via task's args ('KERN_PROCARGS2')
status = proc_pidpath(self.pid.intValue, pathBuffer, sizeof(pathBuffer));
if(0 != status)
{
//init task's name
taskPath = [NSString stringWithUTF8String:pathBuffer];
}
//try via task's args (via XPC)
else
{
//grab args
// ->set's 'arguments' iVar
[self getArguments];
//sanity check
if( (nil == self.arguments) ||
(0 == self.arguments.count) )
{
//bail
goto bail;
}
//arg[0] should be full path
taskPath = self.arguments.firstObject;
}
//bail
bail:
//when task path is still nil
// ->set to const for unknown path...
if(nil == taskPath)
{
//set
taskPath = TASK_PATH_UNKNOWN;
}
return taskPath;
}
//get command-line args via XPC request to remote service
// ->waits for XPC, then sets 'arguments' iVar
-(void)getArguments
{
//xpc connection
__block NSXPCConnection* xpcConnection = nil;
//wait semaphore
dispatch_semaphore_t waitSema = nil;
//alloc XPC connection
xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.objective-see.remoteTaskService"];
//set remote object interface
xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(remoteTaskProto)];
//set classes
// ->arrays & strings are what is ok to vend
[xpcConnection.remoteObjectInterface setClasses: [NSSet setWithObjects: [NSMutableArray class], [NSString class], nil]
forSelector: @selector(getTaskArgs:withReply:) argumentIndex: 0 ofReply: YES];
//resume
[xpcConnection resume];
//init wait semaphore
waitSema = dispatch_semaphore_create(0);
//invoke XPC service (running as r00t)
// ->will enumerate files, then invoke reply block so can save into iVar
[[xpcConnection remoteObjectProxy] getTaskArgs:self.pid withReply:^(NSMutableArray* taskArguments)
{
//close connection
[xpcConnection invalidate];
//nil out
xpcConnection = nil;
//grab array
self.arguments = taskArguments;
//signal sema
dispatch_semaphore_signal(waitSema);
}];
//wait until XPC is done
// ->XPC reply block will signal semaphore
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
return;
}
//enumerate all dylibs
// ->new ones are added to 'existingDylibs' (global) dictionary
-(void)enumerateDylibs:(NSMutableDictionary*)allDylibs shouldWait:(BOOL)shouldWait
{
//xpc connection
__block NSXPCConnection* xpcConnection = nil;
//dylib instance (as Binary) obj
__block Binary* dylib = nil;
//new dylibs
// ->ones that should be hashed/processed
__block NSMutableArray* newDylibs = nil;
//wait semaphore
dispatch_semaphore_t waitSema = nil;
//alloc array for new dylibs
newDylibs = [NSMutableArray array];
//skip window server
// hangs the UI otherwise!
if(YES == [self.binary.path isEqualToString:WINDOW_SERVER])
{
//skip
return;
}
//alloc XPC connection
xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.objective-see.remoteTaskService"];
//set remote object interface
xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(remoteTaskProto)];
//set classes
// ->arrays, dictionaries, & strings are what is ok to vend
[xpcConnection.remoteObjectInterface
setClasses: [NSSet setWithObjects: [NSMutableArray class], [NSMutableDictionary class], [NSString class], nil]
forSelector: @selector(enumerateDylibs:withReply:)
argumentIndex: 0 // the first parameter
ofReply: YES // in the method itself.
];
//resume
[xpcConnection resume];
//init wait semaphore
waitSema = dispatch_semaphore_create(0);
//invoke XPC service (running as r00t)
// ->will enumerate dylibs, then invoke reply block to save into iVar
[[xpcConnection remoteObjectProxy] enumerateDylibs:self.pid withReply:^(NSMutableArray* dylibPaths) {
//close connection
[xpcConnection invalidate];
//nil out
xpcConnection = nil;
//sync
@synchronized(self.dylibs)
{
//reset existing dylibs
[self.dylibs removeAllObjects];
//add all dylibs
for(NSString* dylibPath in dylibPaths)
{
//skip main executable image
// ->making sure to resolve symlinks
if(YES == [dylibPath isEqualToString:[self.binary.path stringByResolvingSymlinksInPath]])
{
//skip
continue;
}
//skip 'cl_kernels'
// ->not an on-disk/'real' dylib
if(YES == [dylibPath isEqualToString:@"cl_kernels"])
{
//skip
continue;
}
//first try grab from 'global' list of all dylibs
// ->will be non-nil if its already been processed
dylib = allDylibs[dylibPath];
//first time seen?
// ->create Binary obj & save into 'global' list
if(nil == dylib)
{
//create Binary obj
dylib = [[Binary alloc] initWithParams:@{KEY_RESULT_PATH:dylibPath}];
//skip any that error out
if(nil == dylib)
{
//skip
continue;
}
//add to queue
// ->this will trigger background processing
[taskEnumerator.binaryQueue enqueue:dylib];
//add to list of new dylibs
// ->will allow for post processing
[newDylibs addObject:dylib];
//sync
// ->add to global list
@synchronized(allDylibs)
{
//add
allDylibs[dylib.path] = dylib;
}
}
//add to task's dylibs
[self.dylibs addObject:dylib];
}//all dylibs
//sort by name
self.dylibs = [[self.dylibs sortedArrayUsingComparator:^NSComparisonResult(id a, id b)
{
//sort
return [[(Binary*)a name] compare:[(Binary*)b name] options:NSCaseInsensitiveSearch];
}] mutableCopy];
}//sync
//reload bottom pane now
// note: this will only reload if new task is the currently selected one, etc
if(YES != cmdlineMode)
{
//reload
[((AppDelegate*)[[NSApplication sharedApplication] delegate]) reloadBottomPane:self itemView:DYLIBS_VIEW];
}
//complete dylib processing for new dylib
// ->get signing info, hash, etc, & save into global list
// ...reloads each row (if task is still current)
for(Binary* newDylib in newDylibs)
{
//generate signing info
// ->do this before macho parsing!
[newDylib generatedSigningInfo];
//parse
if(YES == [newDylib parse])
{
//save encrypted flag
newDylib.isEncrypted = [newDylib.parser.binaryInfo[KEY_IS_ENCRYPTED] boolValue];
//save packed flag
newDylib.isPacked = [newDylib.parser.binaryInfo[KEY_IS_PACKED] boolValue];
}
//need to load?
if(YES != cmdlineMode)
{
//no need to reload if task is now longer current/selected
if(((AppDelegate*)[[NSApplication sharedApplication] delegate]).currentTask != self)
{
//skip reload
continue;
}
//reload row
[((AppDelegate*)[[NSApplication sharedApplication] delegate]) reloadRow:newDylib];
}
}
//signal sema
dispatch_semaphore_signal(waitSema);
}];
//wait until XPC is done?
if(YES == shouldWait)
{
//wait
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
}
return;
}
//enumerate all file descriptors
-(void)enumerateFiles:(BOOL)shouldWait
{
//xpc connection
__block NSXPCConnection* xpcConnection = nil;
//File object
__block File* file = nil;
//wait semaphore
dispatch_semaphore_t waitSema = nil;
//file path
__block NSString* filePath = nil;
//alloc XPC connection
xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.objective-see.remoteTaskService"];
//set remote object interface
xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(remoteTaskProto)];
//set classes
// ->arrays, dictionaries, & strings are what is ok to vend
[xpcConnection.remoteObjectInterface
setClasses: [NSSet setWithObjects: [NSMutableArray class], [NSMutableDictionary class], [NSString class], nil]
forSelector: @selector(enumerateFiles:withReply:)
argumentIndex: 0 // the first parameter
ofReply: YES // in the method itself.
];
//resume
[xpcConnection resume];
//init wait semaphore
waitSema = dispatch_semaphore_create(0);
//invoke XPC service (running as r00t)
// ->will enumerate files, then invoke reply block so can save into iVar
[[xpcConnection remoteObjectProxy] enumerateFiles:self.pid withReply:^(NSMutableArray* fileDescriptors) {
//close connection
[xpcConnection invalidate];
//nil out
xpcConnection = nil;
//sync
@synchronized(self.files)
{
//reset existing files
[self.files removeAllObjects];
//create/add all files
for(NSMutableDictionary* fileDescriptor in fileDescriptors)
{
//extract file path
filePath = fileDescriptor[KEY_FILE_PATH];
//alloc/init File obj
file = [[File alloc] initWithParams:@{KEY_RESULT_PATH:filePath}];
//sync
@synchronized(self.files)
{
//add to task's files
[self.files addObject:file];
}
}
//sort by name
self.files = [[self.files sortedArrayUsingComparator:^NSComparisonResult(id a, id b)
{
//sort
return [[(File*)a name] compare:[(File*)b name] options:NSCaseInsensitiveSearch];
}] mutableCopy];
//reload bottom pane
if(YES != cmdlineMode)
{
//reload
[((AppDelegate*)[[NSApplication sharedApplication] delegate]) reloadBottomPane:self itemView:FILES_VIEW];
}
}//sync
//signal sema
dispatch_semaphore_signal(waitSema);
}];
//wait until XPC is done?
if(YES == shouldWait)
{
//wait
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
}
return;
}
//enumerate network sockets/connections
-(void)enumerateNetworking:(BOOL)shouldWait
{
//xpc connection
__block NSXPCConnection* xpcConnection = nil;
//Connection object
__block Connection* connection = nil;
//wait semaphore
dispatch_semaphore_t waitSema = nil;
//alloc XPC connection
xpcConnection = [[NSXPCConnection alloc] initWithServiceName:@"com.objective-see.remoteTaskService"];
//set remote object interface
xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(remoteTaskProto)];
//set classes
// ->arrays & strings are what is ok to vend
[xpcConnection.remoteObjectInterface
setClasses: [NSSet setWithObjects: [NSMutableArray class], [NSMutableDictionary class], [NSString class], [NSNumber class], nil]
forSelector: @selector(enumerateNetwork:withReply:)
argumentIndex: 0 // the first parameter
ofReply: YES // in the method itself.
];
//resume
[xpcConnection resume];
//init wait semaphore
waitSema = dispatch_semaphore_create(0);
//invoke XPC service (running as r00t)
// ->will enumerate network sockets/connections, then invoke reply block so can save into iVar
[[xpcConnection remoteObjectProxy] enumerateNetwork:self.pid withReply:^(NSMutableArray* networkItems) {
//close connection
[xpcConnection invalidate];
//nil out
xpcConnection = nil;
//sync
@synchronized(self.connections)
{
//remove any existing enum'd networking sockets/connections
[self.connections removeAllObjects];
//create/add all network sockets/connection
for(NSMutableDictionary* networkItem in networkItems)
{
//alloc/init File obj
connection = [[Connection alloc] initWithParams:networkItem];
//add connection obj
if(nil != connection)
{
//add
[self.connections addObject:connection];
}
}
//reload bottom pane
if(YES != cmdlineMode)
{
//reload
[((AppDelegate*)[[NSApplication sharedApplication] delegate]) reloadBottomPane:self itemView:NETWORKING_VIEW];
}
}//sync
//signal sema
dispatch_semaphore_signal(waitSema);
}];
//wait until XPC is done?
if(YES == shouldWait)
{
//wait
dispatch_semaphore_wait(waitSema, DISPATCH_TIME_FOREVER);
}
return;
}
//compare
// ->uses binary name
-(NSComparisonResult)compare:(Task*)otherTask
{
return [self.binary.name compare:otherTask.binary.name options:NSCaseInsensitiveSearch];
}
//convert self to JSON string
-(NSString*)toJSON:(BOOL)detailed
{
//json string
NSString *json = nil;
//json data
// ->for intermediate conversions
NSData *jsonData = nil;
//task command line
NSString* taskCommandLine = nil;
//hashes
NSString* fileHashes = nil;
//signing info
NSString* fileSigs = nil;
//VT detection ratio
NSString* vtDetectionRatio = nil;
//dylibs
NSMutableString* dylibsJSON = nil;
//files
NSMutableString* filesJSON = nil;
//network connections
NSMutableString* connectionsJSON = nil;
//init string for dylibs
dylibsJSON = [NSMutableString string];
//init string for files
filesJSON = [NSMutableString string];
//init string for connections
connectionsJSON = [NSMutableString string];
//init task's command line
taskCommandLine = [self.arguments componentsJoinedByString:@" "];
//no args
// ->provide default
if((nil == taskCommandLine) ||
(0 == taskCommandLine.length))
{
//default
taskCommandLine = @"no arguments/unknown";
}
//init file hash to default string
// ->used when hashes are nil, or serialization fails
fileHashes = @"\"unknown\"";
//init file signature to default string
// ->used when signatures are nil, or serialization fails
fileSigs = @"\"unknown\"";
//convert hashes to JSON
if(nil != self.binary.hashes)
{
//convert hash dictionary
// ->wrap since we are serializing JSON
@try
{
//convert
jsonData = [NSJSONSerialization dataWithJSONObject:self.binary.hashes options:kNilOptions error:NULL];
if(nil != jsonData)
{
//convert data to string
fileHashes = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
}
//ignore exceptions
// ->file hashes will just be 'unknown'
@catch(NSException *exception)
{
;
}
}
//convert signing dictionary to JSON
if(nil != self.binary.signingInfo)
{
//convert signing dictionary
// ->wrap since we are serializing JSON
@try
{
//convert
jsonData = [NSJSONSerialization dataWithJSONObject:self.binary.signingInfo options:kNilOptions error:NULL];
if(nil != jsonData)
{
//convert data to string
fileSigs = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
}
//ignore exceptions
// ->file sigs will just be 'unknown'
@catch(NSException *exception)
{
;
}
}
//init VT detection ratio
vtDetectionRatio = [NSString stringWithFormat:@"%lu/%lu", (unsigned long)[self.binary.vtInfo[VT_RESULTS_POSITIVES] unsignedIntegerValue], (unsigned long)[self.binary.vtInfo[VT_RESULTS_TOTAL] unsignedIntegerValue]];
//detailed?
// generate full json
if(YES == detailed)
{
//sync
@synchronized(self.dylibs)
{
//convert all dylibs and add
for(Binary* dylib in self.dylibs)
{
//convert/add
[dylibsJSON appendFormat:@"{%@},", [dylib toJSON]];
}
}
//remove last ','
if(YES == [dylibsJSON hasSuffix:@","])
{
//remove
[dylibsJSON deleteCharactersInRange:NSMakeRange([dylibsJSON length]-1, 1)];
}
//sync
@synchronized(self.files)
{
//convert all file and add
for(File* file in self.files)
{
//convert/add
[filesJSON appendFormat:@"{%@},", [file toJSON]];
}
}
//remove last ','
if(YES == [filesJSON hasSuffix:@","])
{
//remove
[filesJSON deleteCharactersInRange:NSMakeRange([filesJSON length]-1, 1)];
}
//sync
@synchronized(self.connections)
{
//convert all connections and add
for(Connection* connection in self.connections)
{
//convert/add
[connectionsJSON appendFormat:@"{%@},", [connection toJSON]];
}
}
//remove last ','
if(YES == [connectionsJSON hasSuffix:@","])
{
//remove
[connectionsJSON deleteCharactersInRange:NSMakeRange([connectionsJSON length]-1, 1)];
}
//init json
json = [NSString stringWithFormat:@"\"name\": \"%@\", \"path\": \"%@\", \"pid\": \"%@\", \"command line\": \"%@\", \"hashes\": %@, \"signature(s)\": %@, \"VT detection\": \"%@\", \"encrypted\": %d, \"packed\": %d, \"not found\": %d, \"dylibs\": [%@], \"files\": [%@], \"connections\": [%@]", self.binary.name, self.binary.path, self.pid, taskCommandLine, fileHashes, fileSigs, vtDetectionRatio, self.binary.isEncrypted, self.binary.isPacked, self.binary.notFound, dylibsJSON, filesJSON, connectionsJSON];
}
//basic
else
{
//init json
json = [NSString stringWithFormat:@"\"name\": \"%@\", \"path\": \"%@\", \"pid\": \"%@\", \"command line\": \"%@\", \"hashes\": %@, \"signature(s)\": %@, \"VT detection\": \"%@\", \"encrypted\": %d, \"packed\": %d, \"not found\": %d", self.binary.name, self.binary.path, self.pid, taskCommandLine, fileHashes, fileSigs, vtDetectionRatio, self.binary.isEncrypted, self.binary.isPacked, self.binary.notFound];
}
return json;
}
@end