7 minute read

Hello, cybersecurity enthusiasts and white hackers!

virustotal

This post is the result of my own research on how the VirusTotal API works.

about VirusTotal API

You have probably used the services of the https://virustotal.com site more than once to check whether the binaries contain malicious functions, or to test your own developments.

This service have a free API. After registering an account, you can get an API key:

virustotal

But there is a limitation that is not critical for research purposes: 4 requests per minute:

virustotal

API documentation is located at here

practical example

For practical purposes, I wrote a simple script that uploads a local file to VirusTotal (so far only less than 32 MB) and gives the result of its analysis in numbers, how many AV-engines consider it malicious.

Let’s go. Firstly, according to the documentation, we indicate our API key in all requests:

#...

# for terminal colors
class Colors:
    BLUE = '\033[94m'
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    RED = '\033[91m'
    PURPLE = '\033[95m'
    ENDC = '\033[0m'

VT_API_KEY = "My VirusTotal API key"

#...
class VTScan:
    def __init__(self):
        self.headers = {
            "x-apikey" : VT_API_KEY,
            "User-Agent" : "vtscan v.1.0",
            "Accept-Encoding" : "gzip, deflate",
        }
#...

Then I created function which upload local file to virustotal:


class VTScan:
#...

    def upload(self, malware_path):
        print (Colors.BLUE + "upload file: " + malware_path + "..." + Colors.ENDC)
        self.malware_path = malware_path
        upload_url = VT_API_URL + "files"
        files = {"file" : (
            os.path.basename(malware_path),
            open(os.path.abspath(malware_path), "rb"))
        }
        print (Colors.YELLOW + "upload to " + upload_url + Colors.ENDC)
        res = requests.post(upload_url, headers = self.headers, files = files)
        if res.status_code == 200:
            result = res.json()
            self.file_id = result.get("data").get("id")
            print (Colors.YELLOW + self.file_id + Colors.ENDC)
            print (Colors.GREEN + "successfully upload PE file: OK" + Colors.ENDC)
        else:
            print (Colors.RED + "failed to upload PE file :(" + Colors.ENDC)
            print (Colors.RED + "status code: " + str(res.status_code) + Colors.ENDC)
            sys.exit()

#...

Then, created function which get information about the results of analysis:

    def analyse(self):
        print (Colors.BLUE + "get info about the results of analysis..." + Colors.ENDC)
        analysis_url = VT_API_URL + "analyses/" + self.file_id
        res = requests.get(analysis_url, headers = self.headers)
        if res.status_code == 200:
            result = res.json()
            status = result.get("data").get("attributes").get("status")
            if status == "completed":
                stats = result.get("data").get("attributes").get("stats")
                results = result.get("data").get("attributes").get("results")
                print (Colors.RED + "malicious: " + str(stats.get("malicious")) + Colors.ENDC)
                print (Colors.YELLOW + "undetected : " + str(stats.get("undetected")) + Colors.ENDC)
                print ()
                for k in results:
                    if results[k].get("category") == "malicious":
                        print ("==================================================")
                        print (Colors.GREEN + results[k].get("engine_name") + Colors.ENDC)
                        print ("version : " + results[k].get("engine_version"))
                        print ("category : " + results[k].get("category"))
                        print ("result : " + Colors.RED + results[k].get("result") + Colors.ENDC)
                        print ("method : " + results[k].get("method"))
                        print ("update : " + results[k].get("engine_update"))
                        print ("==================================================")
                        print ()
                print (Colors.GREEN + "successfully analyse: OK" + Colors.ENDC)
                sys.exit()
            elif status == "queued":
                print (Colors.BLUE + "status QUEUED..." + Colors.ENDC)
                with open(os.path.abspath(self.malware_path), "rb") as malware_path:
                    b = f.read()
                    hashsum = hashlib.sha256(b).hexdigest()
                    self.info(hashsum)
        else:
            print (Colors.RED + "failed to get results of analysis :(" + Colors.ENDC)
            print (Colors.RED + "status code: " + str(res.status_code) + Colors.ENDC)
            sys.exit()

Here, if the file that we uploaded is analyzed and ready, then we output the result to the console: how many engines consider our file to be malicious in total, if the file is in the queue, then we read the analysis results using the SHA-256 sum of our file as an identifier:

    def info(self, file_hash):
        print (Colors.BLUE + "get file info by ID: " + file_hash + Colors.ENDC)
        info_url = VT_API_URL + "files/" + file_hash
        res = requests.get(info_url, headers = self.headers)
        if res.status_code == 200:
            result = res.json()
            if result.get("data").get("attributes").get("last_analysis_results"):
                stats = result.get("data").get("attributes").get("last_analysis_stats")
                results = result.get("data").get("attributes").get("last_analysis_results")
                print (Colors.RED + "malicious: " + str(stats.get("malicious")) + Colors.ENDC)
                print (Colors.YELLOW + "undetected : " + str(stats.get("undetected")) + Colors.ENDC)
                print ()
                for k in results:
                    if results[k].get("category") == "malicious":
                        print ("==================================================")
                        print (Colors.GREEN + results[k].get("engine_name") + Colors.ENDC)
                        print ("version : " + results[k].get("engine_version"))
                        print ("category : " + results[k].get("category"))
                        print ("result : " + Colors.RED + results[k].get("result") + Colors.ENDC)
                        print ("method : " + results[k].get("method"))
                        print ("update : " + results[k].get("engine_update"))
                        print ("==================================================")
                        print ()
                print (Colors.GREEN + "successfully analyse: OK" + Colors.ENDC)
                sys.exit()
            else:
                print (Colors.BLUE + "failed to analyse :(..." + Colors.ENDC)

        else:
            print (Colors.RED + "failed to get information :(" + Colors.ENDC)
            print (Colors.RED + "status code: " + str(res.status_code) + Colors.ENDC)
            sys.exit()

So, full source code of our tool is:

# upload PE file to VirusTotal
# then get info about the results
# of analysis, print if malicious
import os
import sys
import time
import json
import requests
import argparse
import hashlib

# for terminal colors
class Colors:
    BLUE = '\033[94m'
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    RED = '\033[91m'
    PURPLE = '\033[95m'
    ENDC = '\033[0m'

# VirusTotal API key
VT_API_KEY = "My VirusTotal API key"

# VirusTotal API v3 URL
VT_API_URL = "https://www.virustotal.com/api/v3/"

# upload malicious file to VirusTotal and analyse
class VTScan:
    def __init__(self):
        self.headers = {
            "x-apikey" : VT_API_KEY,
            "User-Agent" : "vtscan v.1.0",
            "Accept-Encoding" : "gzip, deflate",
        }

    def upload(self, malware_path):
        print (Colors.BLUE + "upload file: " + malware_path + "..." + Colors.ENDC)
        self.malware_path = malware_path
        upload_url = VT_API_URL + "files"
        files = {"file" : (
            os.path.basename(malware_path),
            open(os.path.abspath(malware_path), "rb"))
        }
        print (Colors.YELLOW + "upload to " + upload_url + Colors.ENDC)
        res = requests.post(upload_url, headers = self.headers, files = files)
        if res.status_code == 200:
            result = res.json()
            self.file_id = result.get("data").get("id")
            print (Colors.YELLOW + self.file_id + Colors.ENDC)
            print (Colors.GREEN + "successfully upload PE file: OK" + Colors.ENDC)
        else:
            print (Colors.RED + "failed to upload PE file :(" + Colors.ENDC)
            print (Colors.RED + "status code: " + str(res.status_code) + Colors.ENDC)
            sys.exit()

    def analyse(self):
        print (Colors.BLUE + "get info about the results of analysis..." + Colors.ENDC)
        analysis_url = VT_API_URL + "analyses/" + self.file_id
        res = requests.get(analysis_url, headers = self.headers)
        if res.status_code == 200:
            result = res.json()
            status = result.get("data").get("attributes").get("status")
            if status == "completed":
                stats = result.get("data").get("attributes").get("stats")
                results = result.get("data").get("attributes").get("results")
                print (Colors.RED + "malicious: " + str(stats.get("malicious")) + Colors.ENDC)
                print (Colors.YELLOW + "undetected : " + str(stats.get("undetected")) + Colors.ENDC)
                print ()
                for k in results:
                    if results[k].get("category") == "malicious":
                        print ("==================================================")
                        print (Colors.GREEN + results[k].get("engine_name") + Colors.ENDC)
                        print ("version : " + results[k].get("engine_version"))
                        print ("category : " + results[k].get("category"))
                        print ("result : " + Colors.RED + results[k].get("result") + Colors.ENDC)
                        print ("method : " + results[k].get("method"))
                        print ("update : " + results[k].get("engine_update"))
                        print ("==================================================")
                        print ()
                print (Colors.GREEN + "successfully analyse: OK" + Colors.ENDC)
                sys.exit()
            elif status == "queued":
                print (Colors.BLUE + "status QUEUED..." + Colors.ENDC)
                with open(os.path.abspath(self.malware_path), "rb") as malware_path:
                    b = f.read()
                    hashsum = hashlib.sha256(b).hexdigest()
                    self.info(hashsum)
        else:
            print (Colors.RED + "failed to get results of analysis :(" + Colors.ENDC)
            print (Colors.RED + "status code: " + str(res.status_code) + Colors.ENDC)
            sys.exit()

    def run(self, malware_path):
        self.upload(malware_path)
        self.analyse()

    def info(self, file_hash):
        print (Colors.BLUE + "get file info by ID: " + file_hash + Colors.ENDC)
        info_url = VT_API_URL + "files/" + file_hash
        res = requests.get(info_url, headers = self.headers)
        if res.status_code == 200:
            result = res.json()
            if result.get("data").get("attributes").get("last_analysis_results"):
                stats = result.get("data").get("attributes").get("last_analysis_stats")
                results = result.get("data").get("attributes").get("last_analysis_results")
                print (Colors.RED + "malicious: " + str(stats.get("malicious")) + Colors.ENDC)
                print (Colors.YELLOW + "undetected : " + str(stats.get("undetected")) + Colors.ENDC)
                print ()
                for k in results:
                    if results[k].get("category") == "malicious":
                        print ("==================================================")
                        print (Colors.GREEN + results[k].get("engine_name") + Colors.ENDC)
                        print ("version : " + results[k].get("engine_version"))
                        print ("category : " + results[k].get("category"))
                        print ("result : " + Colors.RED + results[k].get("result") + Colors.ENDC)
                        print ("method : " + results[k].get("method"))
                        print ("update : " + results[k].get("engine_update"))
                        print ("==================================================")
                        print ()
                print (Colors.GREEN + "successfully analyse: OK" + Colors.ENDC)
                sys.exit()
            else:
                print (Colors.BLUE + "failed to analyse :(..." + Colors.ENDC)

        else:
            print (Colors.RED + "failed to get information :(" + Colors.ENDC)
            print (Colors.RED + "status code: " + str(res.status_code) + Colors.ENDC)
            sys.exit()

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('-m','--mal', required = True, help = "PE file path for scanning")
    args = vars(parser.parse_args())
    vtscan = VTScan()
    vtscan.run(args["mal"])

I don’t consider that retelling the documentation is a good idea, since everything is well written there.

demo

Let’s go to see everything in action. It’s pretty simple. Firstly, compile malware from one of my previous posts. It’s a classic process injection example:

x86_64-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -Wint-to-pointer-cast -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive

virustotal

Then run our python script with --mal hack.exe params:

python3 vtscan.py --mal hack.exe

virustotal

virustotal

virustotal

virustotal

As you can see, everything is work perfectly.

The VirusTotal API allows you to do more fancy and custom things, and I think my script can serve as a starting point for your projects.

VirusTotal API v3 documentation
hack.exe in VirusTotal
Classic code injection technique
source code on Github

This is a practical case for educational purposes only.

Thanks for your time happy hacking and good bye!
PS. All drawings and screenshots are mine