// // Filter.m // TaskExplorer // // Created by Patrick Wardle on 2/21/15. // Copyright (c) 2015 Objective-See. All rights reserved. // #import "Task.h" #import "Consts.h" #import "Filter.h" #import "ItemBase.h" #import "Utilities.h" #import "Connection.h" #import "AppDelegate.h" //binary filter keywords NSString * const BINARY_KEYWORDS[] = {@"#apple", @"#nonapple", @"#signed", @"#unsigned", @"#flagged", @"#encrypted", @"#packed", @"#notfound"}; @implementation Filter @synthesize binaryFilters; //init -(id)init { //init super self = [super init]; if(nil != self) { //alloc binary filter keywords binaryFilters = [NSMutableArray array]; //init binary filters for(NSUInteger i=0; i < sizeof(BINARY_KEYWORDS)/sizeof(BINARY_KEYWORDS[0]); i++) { //add [self.binaryFilters addObject:BINARY_KEYWORDS[i]]; } } return self; } //determine if search string is #keyword -(BOOL)isKeyword:(NSString*)searchString { //for now just check in binary keywords return [self.binaryFilters containsObject:searchString]; } //filter all for global search // ->tasks, dylibs, files, & connections -(void)filterAll:(NSString*)filterText items:(NSMutableDictionary*)items results:(NSMutableArray*)results { //flag BOOL isKeyword = NO; //matching tasks NSMutableArray* matchingTasks = nil; //matching dylibs NSMutableArray* matchingDylibs = nil; //matching files NSMutableArray* matchingFiles = nil; //matching connections NSMutableArray* matchingConnections = nil; //alloc for matching tasks matchingTasks = [NSMutableArray array]; //alloc for matching dylibs matchingDylibs = [NSMutableArray array]; //alloc for matching files matchingFiles = [NSMutableArray array]; //alloc for matching connections matchingConnections = [NSMutableArray array]; //set flag isKeyword = [self isKeyword:filterText]; //filter all tasks [self filterTasks:filterText items:items results:matchingTasks pane:PANE_SEARCH]; //add all to cumulative results [results addObjectsFromArray:matchingTasks]; //iterate over all tasks // ->for each, filter their dylib, files, etc for(Task* task in items.allValues) { //filter dylibs [self filterFiles:filterText items:task.dylibs results:matchingDylibs pane:PANE_SEARCH]; //when keyword search // ->skip files/connections if(YES == isKeyword) { //skip continue; } //filter files [self filterFiles:filterText items:task.files results:matchingFiles pane:PANE_SEARCH]; //filter connections [self filterConnections:filterText items:task.connections results:matchingConnections]; } //remove dups dylibs [matchingDylibs setArray:[[[NSSet setWithArray:matchingDylibs] allObjects] mutableCopy]]; //add to cumulative search results [results addObjectsFromArray:matchingDylibs]; //remove dups files [matchingFiles setArray:[[[NSSet setWithArray:matchingFiles] allObjects] mutableCopy]]; //add to cumulative search results [results addObjectsFromArray:matchingFiles]; //remove dups connections [matchingConnections setArray:[[[NSSet setWithArray:matchingConnections] allObjects] mutableCopy]]; //add to cumulative search results [results addObjectsFromArray:matchingConnections]; //call back into search object to refresh it's UI and show results dispatch_async(dispatch_get_main_queue(), ^{ //search done! [((AppDelegate*)[[NSApplication sharedApplication] delegate]).searchWindowController completeSearch]; }); return; } //filter tasks // ->name, path, pid -(void)filterTasks:(NSString*)filterText items:(NSMutableDictionary*)items results:(NSMutableArray*)results pane:(NSUInteger)pane { //task Task* task = nil; //process #keyword filtering // ->note: already checked its a full/matching keyword if(YES == [filterText hasPrefix:@"#"]) { //prep UI for filtering // ->config/show overlay, etc if(PANE_SEARCH != pane) { //prep UI [((AppDelegate*)[[NSApplication sharedApplication] delegate]) prepUIForFiltering:pane]; } //when main thread // ->do #keyword in background if(YES == [NSThread isMainThread]) { //in background filter by keyword dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //nap a 1/2second to let message show up [NSThread sleepForTimeInterval:0.5]; //filter // ->calls back in UI to refresh when done! [self filterByKeyword:(NSString*)filterText items:items.allValues results:(NSMutableArray*)results pane:pane]; }); } //already in background // ->just run on current thread else { //filter [self filterByKeyword:(NSString*)filterText items:items.allValues results:(NSMutableArray*)results pane:pane]; } } //not keyword // ->do normal search here... else { //sync @synchronized(items) { //iterate over all tasks for(NSNumber* taskKey in items) { //extract task task = items[taskKey]; //check path first // ->mostly likely to match if( (nil != task.binary.path) && (NSNotFound != [task.binary.path rangeOfString:filterText options:NSCaseInsensitiveSearch].location) ) { //save match [results addObject:task]; //next continue; } //check name if( (nil != task.binary.name) && (NSNotFound != [task.binary.name rangeOfString:filterText options:NSCaseInsensitiveSearch].location) ) { //save match [results addObject:task]; //next continue; } //check pid if(NSNotFound != [[task.pid stringValue] rangeOfString:filterText options:NSCaseInsensitiveSearch].location) { //save match [results addObject:task]; //next continue; } }//all tasks }//sync }//normal filtering return; } //filter tasks // ->name, path, pid -(void)filterByKeyword:(NSString*)filterText items:(NSArray*)items results:(NSMutableArray*)results pane:(NSUInteger)pane { //sync @synchronized(items) { //sanity check if(0 == items.count) { //bail goto bail; } //handle tasks if(YES == [items.firstObject isKindOfClass:[Task class]]) { //iterate over all items for(id item in items) { //check if task's binary matches filter if(YES == [self binaryFulfillsKeyword:filterText binary:((Task*)item).binary]) { //add [results addObject:item]; } } } //handle dylibs else if(YES == [items.firstObject isKindOfClass:[Binary class]]) { //iterate over all items for(id item in items) { //check if dylib's binary matches filter if(YES == [self binaryFulfillsKeyword:filterText binary:((Binary*)item)]) { //add [results addObject:item]; } } } } //sync //tell the UI we are done if(PANE_SEARCH != pane) { //on main thread, update UI dispatch_async(dispatch_get_main_queue(), ^{ //finalize UI [((AppDelegate*)[[NSApplication sharedApplication] delegate]) finalizeFiltration:pane]; }); } //bail bail: return; } //filter dylibs and files // ->name and path -(void)filterFiles:(NSString*)filterText items:(NSMutableArray*)items results:(NSMutableArray*)results pane:(NSUInteger)pane { //process #keyword filtering for dylibs // ->note: already checked its a full/matching keyword if( (YES == [filterText hasPrefix:@"#"]) && (YES == [items.firstObject isKindOfClass:[Binary class]]) ) { //prep UI for filtering // ->config/show overlay, etc if(PANE_SEARCH != pane) { //prep UI [((AppDelegate*)[[NSApplication sharedApplication] delegate]) prepUIForFiltering:pane]; } //when main thread // ->do #keyword search in background if(YES == [NSThread isMainThread]) { //in background filter by keyword dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //nap a 1/2second to let message show up [NSThread sleepForTimeInterval:0.5]; //filter // ->calls back in UI to refresh when done! [self filterByKeyword:(NSString*)filterText items:items results:(NSMutableArray*)results pane:pane]; }); } //already in background // ->just run on current thread else { //filter [self filterByKeyword:(NSString*)filterText items:items results:(NSMutableArray*)results pane:pane]; } } //not keyword // ->do normal search here... else { //sync @synchronized(items) { //iterate over all items for(ItemBase* item in items) { //check path first // ->most likely to match if( (nil != item.path) && (NSNotFound != [item.path rangeOfString:filterText options:NSCaseInsensitiveSearch].location) ) { //save match [results addObject:item]; //next continue; } //check name if( (nil != item.name) && (NSNotFound != [item.name rangeOfString:filterText options:NSCaseInsensitiveSearch].location) ) { //save match [results addObject:item]; //next continue; } }//all items }//sync }//normal filtering return; } //filter network connections -(void)filterConnections:(NSString*)filterText items:(NSMutableArray*)items results:(NSMutableArray*)results { //sync @synchronized(items) { //iterate over all tasks for(Connection* item in items) { //check local ip if( (nil != item.localIPAddr) && (NSNotFound != [item.localIPAddr rangeOfString:filterText options:NSCaseInsensitiveSearch].location) ) { //save match [results addObject:item]; //next continue; } //check local port if( (nil != item.localIPAddr) && (NSNotFound != [[NSString stringWithFormat:@"%d", [item.localPort unsignedShortValue]] rangeOfString:filterText options:NSCaseInsensitiveSearch].location) ) { //save match [results addObject:item]; //next continue; } //check remote ip if( (nil != item.remoteIPAddr) && (NSNotFound != [item.remoteIPAddr rangeOfString:filterText options:NSCaseInsensitiveSearch].location) ) { //save match [results addObject:item]; //next continue; } //check remote port if( (nil != item.remoteIPAddr) && (NSNotFound != [[NSString stringWithFormat:@"%d", [item.remotePort unsignedShortValue]] rangeOfString:filterText options:NSCaseInsensitiveSearch].location) ) { //save match [results addObject:item]; //next continue; } //check family if( (nil != item.family) && (NSNotFound != [item.family rangeOfString:filterText options:NSCaseInsensitiveSearch].location) ) { //save match [results addObject:item]; //next continue; } //check protocol if( (nil != item.proto) && (NSNotFound != [item.proto rangeOfString:filterText options:NSCaseInsensitiveSearch].location) ) { //save match [results addObject:item]; //next continue; } //check state if( (nil != item.state) && (NSNotFound != [item.state rangeOfString:filterText options:NSCaseInsensitiveSearch].location) ) { //save match [results addObject:item]; //next continue; } //check DNS name if( (nil != item.remoteName) && (NSNotFound != [item.remoteName rangeOfString:filterText options:NSCaseInsensitiveSearch].location) ) { //save match [results addObject:item]; //next continue; } }//all connections }//sync return; } //check if a binary fulfills a keyword -(BOOL)binaryFulfillsKeyword:(NSString*)keyword binary:(Binary*)binary { //flag BOOL fulfills = NO; //handle '#apple' // ->signed by apple or in dyld cache if( (YES == [keyword isEqualToString:@"#apple"]) && ( (YES == [self isApple:binary]) || (YES == binary.inCache) || (YES == [binary.path isEqualToString:KERNEL_YOSEMITE]) )) { //happy fulfills = YES; //bail goto bail; } //handle '#nonapple' // ->not signed by apple, and not in cache or not kernel else if( (YES == [keyword isEqualToString:@"#nonapple"]) && (YES != [self isApple:binary]) && (YES != binary.inCache) && (YES != [binary.path isEqualToString:KERNEL_YOSEMITE]) ) { //happy fulfills = YES; //bail goto bail; } //handle '#signed' // ->signed or in dyld cache else if( (YES == [keyword isEqualToString:@"#signed"]) && ( (YES == [self isSigned:binary]) || (YES == binary.inCache) ) ) { //happy fulfills = YES; //bail goto bail; } //handle '#unsigned' // ->not signed (and not in dyld cache) else if( (YES == [keyword isEqualToString:@"#unsigned"]) && (YES != [self isSigned:binary]) && (!binary.inCache) ) { //happy fulfills = YES; //bail goto bail; } //handle '#flagged' // ->flagged by VT else if( (YES == [keyword isEqualToString:@"#flagged"]) && (YES == [self isFlagged:binary]) ) { //happy fulfills = YES; //bail goto bail; } //handle '#encrypted' else if( (YES == [keyword isEqualToString:@"#encrypted"]) && (YES == [self isEncrypted:binary]) ) { //happy fulfills = YES; //bail goto bail; } //handle '#packed' else if( (YES == [keyword isEqualToString:@"#packed"]) && (YES == [self isPacked:binary]) ) { //happy fulfills = YES; //bail goto bail; } //handle '#notfound' else if( (YES == [keyword isEqualToString:@"#notfound"]) && (YES == [self notFound:binary]) ) { //happy fulfills = YES; //bail goto bail; } //bail bail: return fulfills; } //keyword filter '#apple' (and indirectly #nonapple) // ->determine if binary is signed by apple -(BOOL)isApple:(Binary*)item { //flag BOOL isApple = NO; //make sure signing info has been generated // ->when no, generate it if(nil == item.signingInfo) { //generate [item generatedSigningInfo]; } //check // ->just look at signing info if( (noErr == [item.signingInfo[KEY_SIGNATURE_STATUS] integerValue]) && (Apple == [item.signingInfo[KEY_SIGNATURE_SIGNER] intValue]) ) { //set flag isApple = YES; } return isApple; } //keyword filter '#signed' (and indirectly #unsigned) // ->determine if binary is signed -(BOOL)isSigned:(Binary*)item { //flag BOOL isSigned = NO; //make sure signing info has been generated // ->when no, generate it if(nil == item.signingInfo) { //generate [item generatedSigningInfo]; } //check // ->just look at signing info if(STATUS_SUCCESS == [item.signingInfo[KEY_SIGNATURE_STATUS] integerValue]) { //set flag isSigned = YES; } return isSigned; } //keyword filter '#flagged' // ->determine if binary is flagged by VT -(BOOL)isFlagged:(Binary*)item { //flag BOOL isFlagged = NO; //check // ->note: assumes that VT query has already completed... if( (nil != item.vtInfo) && (0 != [item.vtInfo[VT_RESULTS_POSITIVES] unsignedIntegerValue]) ) { //set flag isFlagged = YES; } return isFlagged; } //keyword filter '#encrypted' // ->determine if binary is encrypted -(BOOL)isEncrypted:(Binary *)item { //make sure item was parsed if(nil == item.parser) { //parse [item parse]; //save encrypted flag item.isEncrypted = [item.parser.binaryInfo[KEY_IS_ENCRYPTED] boolValue]; } //set flag return item.isEncrypted; } //keyword filter '#packed' // ->determine if binary is packed -(BOOL)isPacked:(Binary *)item { //make sure item was parsed if(nil == item.parser) { //make sure we have signing info // ->packer checks ('parse') ingores Apple signed files if(nil == item.signingInfo) { //generate [item generatedSigningInfo]; } //parse [item parse]; //save packed flag item.isPacked = [item.parser.binaryInfo[KEY_IS_PACKED] boolValue]; } //set flag return item.isPacked; } //keyword filter '#notfound' -(BOOL)notFound:(Binary *)item { return item.notFound; } @end