Files
Patrick Wardle 205e24d309 version 2.0.0
-avoid UI calls when run in cmdline mode
 -hide icon when run in cmdline mode
 -option to scan single process
2018-12-21 20:19:09 -10:00

620 lines
16 KiB
Objective-C

//
// VirusTotal.m
// TaskExplorer
//
// Created by Patrick Wardle on 3/8/15.
// Copyright (c) 2015 Objective-See. All rights reserved.
//
#import "File.h"
#import "Consts.h"
#import "ItemBase.h"
#import "VirusTotal.h"
#import "AppDelegate.h"
#import <syslog.h>
@implementation VirusTotal
@synthesize items;
@synthesize vtThreads;
//init
-(id)init
{
//init super
self = [super init];
if(nil != self)
{
//alloc array for items
items = [NSMutableArray array];
//init array for virus total threads
vtThreads = [NSMutableArray array];
//kick of thread to watch/flush queue
// ->will flush if item not processed in 3 seconds
[NSThread detachNewThreadSelector:@selector(queueFlusher) toTarget:self withObject:nil];
}
return self;
}
//watch queue
// ->manaully flush if any item in for more than 3.0 seconds
-(void)queueFlusher
{
//last item
Binary* lastItem = nil;
//items to process
NSMutableArray* vtItems = nil;
//forever
// ->watch/flush if needed
while(YES)
{
//grab last item
lastItem = [self.items lastObject];
//sleep
[NSThread sleepForTimeInterval:3.0];
//sync
@synchronized(self.items)
{
//no items?
// ->just loop/re-nap
if(0 == self.items.count)
{
//loop
continue;
}
//check if last item, is still last
// ->if not, loop/re-nap
if(lastItem != [self.items lastObject])
{
//loop
continue;
}
//make copy
vtItems = [NSMutableArray arrayWithArray:self.items];
//last item is same after timeout
// ->flush the queue
[NSThread detachNewThreadSelector:@selector(queryVT:) toTarget:self withObject:vtItems];
//remove all items
[self.items removeAllObjects];
}
}//forever
return;
}
//add item
// ->will query VT when 25 items are hit
-(void)addItem:(Binary*)binary
{
//items to process
NSMutableArray* vtItems = nil;
//virus total thread
NSThread* virusTotalThread = nil;
//sync
@synchronized(self.items)
{
//add item
[self.items addObject:binary];
//query VT once 25 items have been gathered
if(VT_MAX_QUERY_COUNT == self.items.count)
{
//make copy
vtItems = [NSMutableArray arrayWithArray:self.items];
//alloc thread
// ->will query virus total to get info about all detected items
virusTotalThread = [[NSThread alloc] initWithTarget:self selector:@selector(queryVT:) object:vtItems];
//start thread
[virusTotalThread start];
//sync
@synchronized(self.vtThreads)
{
//save it into array
[self.vtThreads addObject:virusTotalThread];
}
//remove all items
[self.items removeAllObjects];
}
}//sync
return;
}
//make query to VT
-(void)queryVT:(NSMutableArray*)vtItems
{
//item data
NSMutableDictionary* itemData = nil;
//VT query URL
NSURL* queryURL = nil;
//array of queried items
// ->needed so can save VT results back into binaries
NSMutableDictionary* queriedItems = nil;
//parameters
NSMutableArray* parameters = nil;
//results
NSDictionary* results = nil;
//alloc list for items
parameters = [NSMutableArray array];
//alloc dictionary for queried items
queriedItems = [NSMutableDictionary dictionary];
//init query URL
queryURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", VT_QUERY_URL, VT_API_KEY]];
//add all binaries to VT query
for(Binary* item in vtItems)
{
//skip items with blank hashes
// ->not sure why this would happen...
if(nil == item.hashes[KEY_HASH_SHA1])
{
//skip
continue;
}
//alloc item data
itemData = [NSMutableDictionary dictionary];
//auto start location
itemData[@"autostart_location"] = @"n/a";
//set item name
itemData[@"autostart_entry"] = item.name;
//set item path
itemData[@"image_path"] = item.path;
//set hash
itemData[@"hash"] = item.hashes[KEY_HASH_SHA1];
//set creation times
itemData[@"creation_datetime"] = [item.attributes.fileCreationDate description];
//add item to parameters
[parameters addObject:itemData];
//save as queried item
queriedItems[item.hashes[KEY_HASH_SHA1]] = item;
}
//make query to VT
results = [self postRequest:queryURL parameters:parameters];
if(nil != results)
{
//process results
[self processResults:queriedItems results:results];
}
return;
}
//get VT info for a single item
// ->will then callback into AppDelegate to reload item in UI
-(void)getInfoForItem:(Binary*)item scanID:(NSString*)scanID
{
//VT query URL
NSURL* queryURL = nil;
//results
NSDictionary* results = nil;
//init query URL
queryURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@?apikey=%@&resource=%@", VT_REQUERY_URL, VT_API_KEY, scanID]];
//make queries until response is recieved
while(YES)
{
//make query to VT
results = [self postRequest:queryURL parameters:nil];
//check if scan is complete
if( (nil != results) &&
(1 == [results[VT_RESULTS_RESPONSE] integerValue]) )
{
//save result
item.vtInfo = results;
//save flagged item
if(0 != [results[VT_RESULTS_POSITIVES] unsignedIntegerValue])
{
//sync to check/add
@synchronized(taskEnumerator.flaggedItems)
{
if(YES != [taskEnumerator.flaggedItems containsObject:item])
{
//save
[taskEnumerator.flaggedItems addObject:item];
}
}
}
//for non-flagged items
// remove from list, if it was previously flagged
else
{
//sync to check/remove
@synchronized(taskEnumerator.flaggedItems)
{
//check if previously flagged
// ...if so, then remove
if(YES == [taskEnumerator.flaggedItems containsObject:item])
{
//remove
[taskEnumerator.flaggedItems removeObject:item];
}
}
}
//call up into app delegate to smartly reload
[((AppDelegate*)[[NSApplication sharedApplication] delegate]) reloadBinary:item];
//exit loop
break;
}
//nap
[NSThread sleepForTimeInterval:60.0f];
}
return;
}
//make the (POST)query to VT
-(NSDictionary*)postRequest:(NSURL*)url parameters:(id)params
{
//results
NSDictionary* results = nil;
//request
NSMutableURLRequest *request = nil;
//post data
// ->JSON'd items
NSData* postData = nil;
//error var
NSError* error = nil;
//data from VT
NSData* vtData = nil;
//response (HTTP) from VT
NSURLResponse* httpResponse = nil;
//alloc/init request
request = [[NSMutableURLRequest alloc] initWithURL:url];
//set user agent
[request setValue:VT_USER_AGENT forHTTPHeaderField:@"User-Agent"];
//serialize JSON
if(nil != params)
{
//convert items to JSON'd data for POST request
// ->wrap since we are serializing JSON
@try
{
//convert items
postData = [NSJSONSerialization dataWithJSONObject:params options:kNilOptions error:nil];
if(nil == postData)
{
//bail
goto bail;
}
}
//bail on exceptions
@catch(NSException *exception)
{
//bail
goto bail;
}
//set content type
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
//set content length
[request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)[postData length]] forHTTPHeaderField:@"Content-length"];
//add POST data
[request setHTTPBody:postData];
}
//set method type
[request setHTTPMethod:@"POST"];
//send request
// ->synchronous, so will block
vtData = [NSURLConnection sendSynchronousRequest:request returningResponse:&httpResponse error:&error];
//sanity check(s)
if( (nil == vtData) ||
(nil != error) ||
(200 != (long)[(NSHTTPURLResponse *)httpResponse statusCode]) )
{
//err msg
syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: failed to query VirusTotal (%s, %s)", [[error description] UTF8String], [[httpResponse description] UTF8String]);
//bail
goto bail;
}
//serialize response into NSData obj
// ->wrap since we are serializing JSON
@try
{
//serialized
results = [NSJSONSerialization JSONObjectWithData:vtData options:kNilOptions error:nil];
}
//bail on any exceptions
@catch (NSException *exception)
{
//bail
goto bail;
}
//sanity check
if(nil == results)
{
//bail
goto bail;
}
//bail
bail:
return results;
}
//submit a file to VT
-(NSDictionary*)submit:(Binary*)item
{
//results
NSDictionary* results = nil;
//submit URL
NSURL* submitURL = nil;
//request
NSMutableURLRequest *request = nil;
//body of request
NSMutableData* body = nil;
//file data
NSData* fileContents = nil;
//error var
NSError* error = nil;
//data from Vt
NSData* vtData = nil;
//response (HTTP) from VT
NSURLResponse* httpResponse = nil;
//remove item's vt info
// ->as its about to be outdated
item.vtInfo = nil;
//call up into app delegate to smartly reload
// ->it's VT button will be '...'
[((AppDelegate*)[[NSApplication sharedApplication] delegate]) reloadBinary:item];
//init submit URL
submitURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@?apikey=%@&resource=%@", VT_SUBMIT_URL, VT_API_KEY, item.hashes[KEY_HASH_MD5]]];
//init request
request = [[NSMutableURLRequest alloc] initWithURL:submitURL];
//set boundary string
NSString *boundary = @"qqqq___taskexplorer___qqqq";
//set HTTP method (POST)
[request setHTTPMethod:@"POST"];
//set the HTTP header 'Content-type' to the boundary
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary] forHTTPHeaderField: @"Content-Type"];
//set HTTP header, 'User-Agent'
[request setValue:VT_USER_AGENT forHTTPHeaderField:@"User-Agent"];
//init body
body = [NSMutableData data];
//load file into memory
fileContents = [NSData dataWithContentsOfFile:item.pathForFinder];
//sanity check
if(nil == fileContents)
{
//err msg
syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: failed to load %s into memory for submission", [item.path UTF8String]);
//bail
goto bail;
}
//append boundary
[body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
//append 'Content-Disposition' file name, etc
[body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"file\"; filename=\"%@\"\r\n", item.name] dataUsingEncoding:NSUTF8StringEncoding]];
//append 'Content-Type'
[body appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
//append file's contents
[body appendData:fileContents];
//append '\r\n'
[body appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
//append final boundary
[body appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
//set body
[request setHTTPBody:body];
//set content length
[request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)[body length]] forHTTPHeaderField:@"Content-length"];
//send request
// ->synchronous, so will block
vtData = [NSURLConnection sendSynchronousRequest:request returningResponse:&httpResponse error:&error];
//sanity check(s)
if( (nil == vtData) ||
(nil != error) ||
(200 != (long)[(NSHTTPURLResponse *)httpResponse statusCode]) )
{
//err msg
syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: failed to query VirusTotal (%s, %s)", [[error description] UTF8String], [[httpResponse description] UTF8String]);
//bail
goto bail;
}
//serialize response into NSData obj
// ->wrap since we are serializing JSON
@try
{
//serialize
results = [NSJSONSerialization JSONObjectWithData:vtData options:kNilOptions error:nil];
}
//bail on any exceptions
@catch (NSException *exception)
{
//err msg
syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: converting response %s to JSON threw %s", [[vtData description] UTF8String], [[exception description] UTF8String]);
//bail
goto bail;
}
//sanity check
if(nil == results)
{
//err msg
syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: failed to convert response %s to JSON", [[vtData description] UTF8String]);
//bail
goto bail;
}
bail:
return results;
}
//submit a rescan request
-(NSDictionary*)reScan:(Binary*)item
{
//result data
NSDictionary* result = nil;
//scan url
NSURL* reScanURL = nil;
//remove item's vt info
// ->as its about to be outdates
item.vtInfo = nil;
//call up into app delegate to smartly reload
// ->will change item's VT button back to '...'
[((AppDelegate*)[[NSApplication sharedApplication] delegate]) reloadBinary:item];
//init scan url
reScanURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@?apikey=%@&resource=%@", VT_RESCAN_URL, VT_API_KEY, item.hashes[KEY_HASH_MD5]]];
//make request to VT
result = [self postRequest:reScanURL parameters:nil];
if(nil == result)
{
//err msg
syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: failed to re-scan %s", [item.name UTF8String]);
//bail
goto bail;
}
bail:
return result;
}
//process results
// ->save VT info into Binary object & reload relevant pane
-(void)processResults:(NSMutableDictionary*)queriedItems results:(NSDictionary*)results
{
//queried binary obj
Binary* queriedItem = nil;
//process all results
// ->save VT result dictionary into File obj
for(NSDictionary* result in results[VT_RESULTS])
{
//extract ('match') queried item
// ->VT gives us back a hash
queriedItem = queriedItems[result[@"hash"]];
if(nil == queriedItem)
{
//skip
continue;
}
//save VT results into item
queriedItem.vtInfo = result;
//sync to check/add
@synchronized(taskEnumerator.flaggedItems)
{
//save flagged item
if( (0 != [result[VT_RESULTS_POSITIVES] unsignedIntegerValue]) &&
(YES != [taskEnumerator.flaggedItems containsObject:queriedItem]) )
{
//save
[taskEnumerator.flaggedItems addObject:queriedItem];
}
}
//call up into app delegate to smartly reload
if(YES != cmdlineMode)
{
//reload
[((AppDelegate*)[[NSApplication sharedApplication] delegate]) reloadBinary:queriedItem];
}
}
return;
}
@end