├── LICENSE ├── README.md ├── demo └── demo.yara └── hunting.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 European Commission 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VirusTotal-Tools 2 | 3 | ## Hunting 4 | 5 | The script aims at retrieving Hunting result from VirusTotal. After you upload your set of YARA rules (see demo in demo folder), you can retrieve all the results by at least defining your API key to the script. 6 | 7 | ``` 8 | usage: hunting.py [-h] [-api API] [-thres THRESHOLD] [-cleanup] [-dl] 9 | [-puri PROXY_URI] [-pusr PROXY_USER] [-ppwd PROXY_PASSWORD] 10 | [-json JSON] [-out OUTPUT] [-samples SAMPLES_DIRECTORY] 11 | 12 | Retrieve results of VirusTotal Hunting. 13 | 14 | optional arguments: 15 | -h, --help show this help message and exit 16 | -api API, --api API VirusTotal API key 17 | -thres THRESHOLD, --threshold THRESHOLD 18 | Number of required infection to keep result (default 19 | 3) 20 | -cleanup, --cleanup Cleanup notifications of retreived files from 21 | VirusTotal 22 | -dl, --download Download the samples in addition to getting 23 | notifications 24 | -puri PROXY_URI, --proxy_uri PROXY_URI 25 | Proxy URI 26 | -pusr PROXY_USER, --proxy_user PROXY_USER 27 | Proxy User 28 | -ppwd PROXY_PASSWORD, --proxy_password PROXY_PASSWORD 29 | Proxy User 30 | -json JSON, --json JSON 31 | JSON file to use to store full Hunting raw result (by 32 | default not done) 33 | -out OUTPUT, --output OUTPUT 34 | File to store result (by default stdout 35 | -samples SAMPLES_DIRECTORY, --samples_directory SAMPLES_DIRECTORY 36 | Directory where to wrote all matching samples (by 37 | default not done) 38 | ``` 39 | -------------------------------------------------------------------------------- /demo/demo.yara: -------------------------------------------------------------------------------- 1 | /* 2 | * Search for traces of secret domains 3 | */ 4 | rule SecretDomain 5 | { 6 | strings: 7 | $sec_domain = "gmail.com" nocase wide ascii 8 | 9 | condition: 10 | any of them 11 | } 12 | 13 | /* 14 | * Search for traces of secret DNS 15 | */ 16 | rule SecretDNS 17 | { 18 | strings: 19 | $sec_dns = "8.8.8.8" wide ascii 20 | 21 | condition: 22 | any of them 23 | } -------------------------------------------------------------------------------- /hunting.py: -------------------------------------------------------------------------------- 1 | # 2 | # Retrieve results of VT Hunting feature 3 | # 4 | # Author: David DURVAUX 5 | # Copyright: EC DIGIT CSIRC - December 2015 6 | # 7 | # TODO: 8 | # - Improve/review proxy support 9 | # 10 | # Version 0.2 11 | # Contributions by: 12 | # Mike Sconzo - @sooshie 13 | # 14 | import urllib 15 | import urllib2 16 | import json 17 | import os 18 | import errno 19 | import argparse 20 | import csv 21 | import sys 22 | 23 | # Variables and settings 24 | vtapi = None 25 | vturl = "https://www.virustotal.com/intelligence/hunting/notifications-feed/?key=%s" 26 | vtdwl = "https://www.virustotal.com/intelligence/download/?hash=%s&apikey=%s" 27 | vtdhu = "https://www.virustotal.com/intelligence/hunting/delete-notifications/programmatic/?key=%s" 28 | vtthresh = 3 29 | directory = None 30 | download = False 31 | DEBUG = True 32 | 33 | # Proxy settings 34 | proxy_uri = None 35 | proxy_usr = None 36 | proxy_pwd = None 37 | 38 | # Output information 39 | jsonres = None 40 | outfile = sys.stdout 41 | 42 | def makeDirectory(path): 43 | try: 44 | os.makedirs(path) 45 | except OSError as ose: 46 | if ose.errno == errno.EEXIST and os.path.isdir(path): 47 | pass 48 | else: raise 49 | 50 | def splitResults(results): 51 | for i in xrange(0, len(results), 100): 52 | yield results[i:i+100] 53 | 54 | def cleanupNotifications(results): 55 | # Delete the notification from VT so they don't get double processed/retreived 56 | for group in list(splitResults(results)): 57 | data = json.dumps([str(x[-1]) for x in group]) 58 | try: 59 | req = urllib2.Request(vtdhu % (vtapi), data, {'Content-Type': 'application/json'}) 60 | f = urllib2.urlopen(req) 61 | resp = json.loads(f.read()) 62 | #{"deleted": 3, "received": 3, "result": 1} 63 | f.close() 64 | if resp['deleted'] != resp['received']: 65 | print "ERROR: Issue deleting notification IDs from VirusTotal :'('" 66 | except: 67 | print "ERROR: Connection error :'(" 68 | 69 | def getHuntingResult(): 70 | # Some funcky thinks 71 | # Create an OpenerDirector with support for Basic HTTP Authentication... 72 | if proxy_uri: 73 | proxy = None 74 | if proxy_usr and proxy_pwd: 75 | proxy = urllib2.ProxyHandler({'https' : 'http://%s:%s@%s' % (proxy_usr, proxy_pwd, proxy_uri)}) 76 | else: 77 | proxy = urllib2.ProxyHandler({'https' : 'http://%s' % (proxy_uri)}) 78 | opener = urllib2.build_opener(proxy) 79 | urllib2.install_opener(opener) 80 | 81 | try: 82 | result = [] 83 | 84 | # retrieve JSON object from Virus Total 85 | jsonfd = urllib2.urlopen(vturl % (vtapi)) 86 | jsonstr = jsonfd.read() 87 | try: 88 | while jsonstr != '': 89 | # parse JSON 90 | jsonvt = json.loads(jsonstr) 91 | for notification in jsonvt["notifications"]: 92 | positive = notification["positives"] 93 | yararule = notification["subject"] 94 | sha1 = notification["sha1"] 95 | sha256 = notification["sha256"] 96 | fseen = notification["first_seen"] 97 | lseen = notification["last_seen"] 98 | objtype = notification["type"] 99 | nid = notification["id"] 100 | if DEBUG: 101 | print "[*] " + sha256 102 | 103 | if(int(positive) >= int(vtthresh)): 104 | # add result to list of results 105 | result.append([positive, yararule, sha1, sha256, fseen, lseen, objtype, nid]) 106 | 107 | if(directory) and (download): 108 | try: 109 | # retrieve sample 110 | # The following way to retrieve is commented due to issue 111 | # with proxy 112 | # urllib.urlretrieve(vtdwl % (sha1, vtapi), "%s/%s" % (directory, sha1)) 113 | filedir = directory + '/' + yararule 114 | makeDirectory(filedir) 115 | vtfile = urllib2.urlopen(vtdwl % (sha1, vtapi), "%s/%s") 116 | output = open("%s/%s" % (filedir, sha1),'wb') 117 | output.write(vtfile.read()) 118 | output.close() 119 | except Exception as e: 120 | print "ERROR: Impossible to retrieve sample %s from VirusTotal :'(" % sha1 121 | if DEBUG: 122 | print "[*] " + str(e) 123 | # Cleanup processed results 124 | cleanupNotifications(result) 125 | jsonfd = urllib2.urlopen(vturl % (vtapi)) 126 | jsonstr = jsonfd.read() 127 | 128 | # Save JSON to file 129 | if jsonres: 130 | fd = open(jsonres, "w") 131 | fd.write(jsonstr) 132 | fd.close() 133 | except Exception as e: 134 | print "ERROR: Invalid result retrieved from VirusTotal (JSON Parsing Error) :'(" 135 | if DEBUG: 136 | print "[*] " + str(e) 137 | 138 | # Return result 139 | return result 140 | except: 141 | print "ERROR: Failed to retrieve Hunting result from VirusTotal :'(" 142 | return None 143 | 144 | def outputResults(results, outfile=sys.stdout): 145 | if len(results) > 0: 146 | if outfile != sys.stdout: 147 | ofilename = outfile 148 | filenum = 0 149 | while os.path.isfile(outfile): 150 | filenum = 1 + filenum 151 | outfile = ofilename + "." + str(filenum) 152 | with open(outfile, 'wb') as csvfile: 153 | LDwriter = csv.writer(csvfile) 154 | LDwriter.writerow(["# of detection", "YARA rule", "SHA1", "Binary type", "First seen", "Last seen"]) 155 | for row in results: 156 | LDwriter.writerow(row) 157 | else: 158 | LDwriter = csv.writer(outfile) 159 | LDwriter.writerow(["# of detection", "YARA rule", "SHA1", "Binary type", "First seen", "Last seen"]) 160 | for row in results: 161 | LDwriter.writerow(row) 162 | 163 | def main(): 164 | """ 165 | Calling the script and options handling 166 | """ 167 | 168 | # Argument definition 169 | parser = argparse.ArgumentParser(description='Retrieve results of VirusTotal Hunting.') 170 | 171 | # VirusTotal options 172 | parser.add_argument('-api', '--api', help='VirusTotal API key') 173 | parser.add_argument('-thres', '--threshold', help='Number of required infection to keep result (default 3)') 174 | parser.add_argument('-dl', '--download', action="store_true", help='Download the samples in addition to getting notifications') 175 | 176 | # Proxy Settings 177 | parser.add_argument('-puri', '--proxy_uri', help='Proxy URI') 178 | parser.add_argument('-pusr', '--proxy_user', help='Proxy User') 179 | parser.add_argument('-ppwd', '--proxy_password', help='Proxy User') 180 | 181 | # Output options 182 | parser.add_argument('-json', '--json', help='JSON file to use to store full Hunting raw result (by default not done)') 183 | parser.add_argument('-out', '--output', help='File to store result (by default stdout') 184 | parser.add_argument('-samples', '--samples_directory', help='Directory where to wrote all matching samples (by default not done)') 185 | 186 | # Parse command line 187 | args = parser.parse_args() 188 | 189 | # Check if an output directory is set 190 | global directory 191 | if args.samples_directory: 192 | directory = os.path.dirname(args.samples_directory) 193 | 194 | # If directory doesn't exists yet, create it 195 | if directory: 196 | makeDirectory(directory) 197 | 198 | global download 199 | if args.download: 200 | download = args.download 201 | 202 | # Parse Proxy Options 203 | global proxy_uri 204 | global proxy_usr 205 | global proxy_pwd 206 | if args.proxy_uri: 207 | proxy_uri = args.proxy_uri 208 | 209 | if args.proxy_user: 210 | proxy_usr = args.proxy_user 211 | 212 | if args.proxy_password: 213 | proxy_pwd = args.proxy_user 214 | 215 | # JSON OUTPUT 216 | global jsonres 217 | if args.json: 218 | jsonres = args.json 219 | 220 | # Control output instead of stdout 221 | global outfile 222 | if args.output: 223 | outfile = args.output 224 | else: 225 | outfile = sys.stdout 226 | 227 | # API KEY 228 | global vtapi 229 | if args.api: 230 | vtapi = args.api 231 | 232 | # Threshold control 233 | global vtthresh 234 | if args.threshold: 235 | vtthresh = int(args.threshold) 236 | 237 | # Check if minimum set of parameters is available 238 | if not vtapi: 239 | print("ERROR: you need to specify at least an API key. Use -h to get the manual.") 240 | return 241 | 242 | # Do all the magic now :) 243 | results = getHuntingResult() 244 | if results and len(results) > 0: 245 | outputResults(results, outfile) 246 | else: 247 | sys.stderr.write("No results returned\n") 248 | 249 | # Call the main function of this script and trigger all the magic \o/ 250 | if __name__ == "__main__": 251 | main() 252 | # That's all folk ;) 253 | --------------------------------------------------------------------------------