diff --git a/README.adoc b/README.adoc index 00a4f69..05eef4e 100644 --- a/README.adoc +++ b/README.adoc @@ -26,6 +26,7 @@ https://github.com/gosecure/malboxes * vagrant: https://www.vagrantup.com/downloads.html * https://www.virtualbox.org/wiki/Downloads[VirtualBox] or an vSphere / ESXi server + === Minimum specs for the build machine * At least 5 GB of RAM @@ -40,7 +41,6 @@ https://github.com/gosecure/malboxes apt install vagrant git python3-pip - == Installation === Linux/Unix @@ -51,7 +51,6 @@ https://github.com/gosecure/malboxes + sudo pip3 install git+https://github.com/GoSecure/malboxes.git#egg=malboxes - === Windows NOTE: Starting with Windows 10 Hyper-V is always running below the operating @@ -93,6 +92,12 @@ installed. Otherwise, follow the <>. pip3 install setuptools pip3 install -U git+https://github.com/GoSecure/malboxes.git#egg=malboxes +=== To deploy on AWS (optional) +Run this command after normal installation: + + vagrant plugin install vagrant-aws + +NOTE: The AWS feature has only been tested on Linux for the moment and EC2 does not support 32-bit desktop version of Windows 10. == Usage @@ -136,6 +141,54 @@ For example: malboxes spin win7_32_analyst 20160519.cryptolocker.xyz +=== To deploy on AWS (optional) + +Malboxes can upload and interact with a VM on the Amazon Web serivces. To do so, follow these steps: + +. Malboxes will need a S3 bucket on AWS to upload the VM before converting it to an AMI (Amazon Machine Image). If you don't have one, +link:https://docs.aws.amazon.com/quickstarts/latest/s3backup/step-1-create-bucket.html[create one now.] + +. Your instance also requires a link:https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html#CreatingSecurityGroups[security group] with at least a rule allowing inbound connections for WinRM (Type: WinRM-HTTP, Protocol: TCP, Port Range: 5985, Source: host's public IP). + +. Next, you need a `vmimport` service role configured. + Follow the section named _VM Import Service Role_ of https://docs.aws.amazon.com/vm-import/latest/userguide/vmimport-image-import.html[this guide]. + These steps must be performed with an account that has `iam:CreateRole` and `iam:PutRolePolicy` permissions. + +. If the <<_configuration,default config>> is used, change the hypervisor to aws and fill the mandatory options related. Otherwise, be sure to add all the options about AWS to your custom config. + +. Finally, you can follow the same steps described in the <> and the <> sections to launch your instance! + +NOTE: The AMI import can take a very long time (about an hour), however you can verify the status of the task by doing <>. At the moment, only one AMI can be build per template. + +==== AMI import status +Install awscli using pip: + + pip install awscli + +link:https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html#cli-quick-configuration[Configure] awscli with: + + aws configure + +Then run: + + aws ec2 describe-import-image-tasks + +==== RDP + +To connect to an instance on the cloud using RDP, run this command at the same location of your `Vagrantfile`: + + vagrant rdp -- /cert-ignore + +For this to work, the instance will require a security group allowing RDP inbound connections (Type: RDP, Protocol: TCP, Port Range: 3389, Source: host's public IP). + +NOTE: You can safely ignore the following error because rsync is not yet implemented: `No host IP was given to the Vagrant core NFS helper. This is an internal error that should be reported as a bug.` + + +==== Stopping an Instance + +To stop an instance on the cloud, run this command at the same location of your `Vagrantfile`: + + vagrant halt == Configuration @@ -167,6 +220,9 @@ link:malboxes/profile-example.js[profile-example.js] for an example configuration. This new capacity is experimental and subject to change as we experiment with it. +=== AWS security groups + +Currently, Malboxes does not support the automatic creation of the security groups, so you'll have to use the AWS console to create yours. However, using the library link:https://boto3.amazonaws.com/v1/documentation/api/latest/index.html[Boto3] there should be a way to implement this. == More information diff --git a/docs/aws.adoc b/docs/aws.adoc new file mode 100644 index 0000000..8a610d1 --- /dev/null +++ b/docs/aws.adoc @@ -0,0 +1,32 @@ += Amazon Web Services (AWS) Helpers + +== Setup + +Install the `aws` command-line tool (pip or your OS' version) + +Configure it + + aws configure + +You are good to go! + + +== Built Templates + +They are available as images or AMIs in AWS' language. +You can find them under EC2 -> Images -> AMIs. + +=== List malboxes images hosted available to you + + aws ec2 describe-images --filters "Name=tag:Name,Values=Malboxes" + + +== Operations + +=== Map a drive + +Connecting to the instance with the following command will map the current +working directory as a drive letter on the Windows box using RDP: + + vagrant rdp -- /cert-ignore /a:drive,home,./ + diff --git a/malboxes/config-example.js b/malboxes/config-example.js index 4957655..3732f13 100644 --- a/malboxes/config-example.js +++ b/malboxes/config-example.js @@ -32,9 +32,9 @@ //"input_locale": "fr-FR", // Provision settings - // Which Hypervisor for privisoning and deployment? (Options are: "virtualbox" and "vsphere") Default is "virtualbox" + // Which Hypervisor for privisoning and deployment? (Options are: "virtualbox", "vsphere" and "aws") Default is "virtualbox" "hypervisor": "virtualbox", - //If vsphere, the following configuration options are mandatory + // If vsphere, the following configuration options are mandatory "remote_host": "", "remote_datastore": "", "remote_username": "", @@ -45,6 +45,15 @@ "vsphere_user": "", "vsphere_password": "", "vsphere_insecure": "true", + // If AWS, the following configuration options are mandatory + "aws_access_key": "", + "aws_secret_key": "", + "aws_s3_bucket": "", + "aws_keypair": "", + "aws_security_group": "", // See Usage/AWS in doc to understand why you need to create one. + // Optional + "aws_region": "us-east-1", + "aws_instance_type" : "m3.medium", //"proxy": "company_proxy:3128", diff --git a/malboxes/malboxes.py b/malboxes/malboxes.py index c6b549c..9e73b0a 100644 --- a/malboxes/malboxes.py +++ b/malboxes/malboxes.py @@ -31,6 +31,7 @@ import textwrap from appdirs import AppDirs +import boto3 from jinja2 import Environment, FileSystemLoader from jsmin import jsmin @@ -38,6 +39,12 @@ DIRS = AppDirs("malboxes") DEBUG = False +EXIT_WITHOUT_ERROR = 1 +EXIT_TEMPLATE_NOT_FOUND = 2 +EXIT_PACKER_FAILED = 3 +EXIT_VAGRANT_BOX_ADD_FAILED = 4 +EXIT_VAGRANTFILE_ALREADY_EXISTS = 5 +EXIT_TEMPLATE_ALREADY_AMI = 6 def initialize(): # create appdata directories if they don't exist @@ -59,6 +66,7 @@ def initialize(): return init_parser() + def init_parser(): parser = argparse.ArgumentParser( description="Vagrant box builder " @@ -142,7 +150,7 @@ def prepare_packer_template(config, template_name): 'templates/{}.json'.format(template_name)) except FileNotFoundError: print("Template doesn't exist: {}".format(template_name)) - sys.exit(2) + sys.exit(EXIT_TEMPLATE_NOT_FOUND) filepath = resource_filename(__name__, 'templates/') env = Environment(loader=FileSystemLoader(filepath), autoescape=False, @@ -386,7 +394,7 @@ def default(parser, args): parser.print_help() print("\n") list_templates(parser, args) - sys.exit(1) + sys.exit(EXIT_WITHOUT_ERROR) def list_templates(parser, args): @@ -399,13 +407,63 @@ def list_templates(parser, args): print() -def build(parser, args): +def create_EC2_client(config): + """ + Creates a client to interact with Amazon Elastic Compute Cloud. + It's Currently only used to retrieve the AMI ID. + """ + return boto3.client( + 'ec2', + aws_access_key_id=config['aws_access_key'], + aws_secret_access_key=config['aws_secret_key'], + region_name=config['aws_region'], + ) + +def get_AMI_ID_by_template(config, template): + """ + Gets the ID of an AMI by the template tag on it. + """ + images = create_EC2_client(config).describe_images(Owners=['self'], + Filters=[{'Name': 'tag:Template', 'Values': [template]}]) + return images['Images'][0]['ImageId'] + + +def is_template_already_AMI(config, template): + """ + Verifies if there's already an AMI based on a template. + If so, returns True. + Otherwise, returns False. + """ + try: + get_AMI_ID_by_template(config, template) + except IndexError: + return False + return True + + +def build(parser, args): print("Generating configuration files...") config, packer_tmpl = prepare_config(args) prepare_autounattend(config) _prepare_vagrantfile(config, "box_win.rb", create_cachefd('box_win.rb')) print("Configuration files are ready") + if ( config['hypervisor'] == 'aws' and + is_template_already_AMI(config, args.template) + ): + print(textwrap.dedent(""" + =============================================================== + This template has already been converted to an AMI. + + You should generate a Vagrantfile configuration in order to + launch an instance of this AMI: + + malboxes spin {} + + Exiting... + ===============================================================""") + .format(args.template, DIRS.user_cache_dir)) + sys.exit(EXIT_TEMPLATE_ALREADY_AMI) if not args.skip_packer_build: ret = run_packer(packer_tmpl, args) @@ -414,18 +472,33 @@ def build(parser, args): if ret != 0: print("Packer failed. Build failed. Exiting...") - sys.exit(3) + sys.exit(EXIT_PACKER_FAILED) - if not args.skip_vagrant_box_add: + if not (args.skip_vagrant_box_add or config['hypervisor'] == 'aws'): ret = add_box(config, args) else: ret = 0 if ret != 0: print("'vagrant box add' failed. Build failed. Exiting...") - sys.exit(4) + sys.exit(EXIT_VAGRANT_BOX_ADD_FAILED) - if not args.skip_vagrant_box_add: + if config['hypervisor'] == 'aws': + print(textwrap.dedent(""" + =============================================================== + The AMI was successfully created on the Amazon Elastic Compute Cloud. + + You should generate a Vagrantfile configuration in order to + launch an instance of the AMI: + + malboxes spin {} + + You can re-use this box several times by using `malboxes + spin`. Each EC2 instance will be independent of each other. + ===============================================================""") + .format(args.template, DIRS.user_cache_dir)) + + elif not args.skip_vagrant_box_add: print(textwrap.dedent(""" =============================================================== A base box was imported into your local Vagrant box repository. @@ -449,7 +522,7 @@ def spin(parser, args): """ if os.path.isfile('Vagrantfile'): print("Vagrantfile already exists. Please move it away. Exiting...") - sys.exit(5) + sys.exit(EXIT_VAGRANTFILE_ALREADY_EXISTS) config, _ = prepare_config(args) @@ -463,6 +536,11 @@ def spin(parser, args): elif config['hypervisor'] == 'vsphere': with open("Vagrantfile", 'w') as f: _prepare_vagrantfile(config, "analyst_vsphere.rb", f) + elif config['hypervisor'] == 'aws': + with open("Vagrantfile", 'w') as f: + config['aws_ami_id'] = get_AMI_ID_by_template(config, + config['template']) + _prepare_vagrantfile(config, "analyst_aws.rb", f) print("Vagrantfile generated. You can move it in your analysis directory " "and issue a `vagrant up` to get started with your VM.") @@ -592,6 +670,7 @@ def document(profile_name, modtype, docpath, fd): fd.write(line) + def shortcut_function(fd): """ Add shortcut function to the profile """ filename = resource_filename(__name__, "scripts/windows/add-shortcut.ps1") @@ -599,6 +678,7 @@ def shortcut_function(fd): fd.write(add_shortcut_file.read()) add_shortcut_file.close(); + def shortcut(dest, target, arguments, fd): """ Create shortcut on Desktop """ if arguments is None: @@ -609,6 +689,7 @@ def shortcut(dest, target, arguments, fd): print("Adding shortcut {}: {} with arguments {}".format(dest, target, arguments)) fd.write(line) + def main(): global DEBUG try: diff --git a/malboxes/scripts/windows/allow-WinRM-public.ps1 b/malboxes/scripts/windows/allow-WinRM-public.ps1 new file mode 100644 index 0000000..74b7901 --- /dev/null +++ b/malboxes/scripts/windows/allow-WinRM-public.ps1 @@ -0,0 +1,2 @@ +# Allow WinRM to communicate on public network +netsh advfirewall firewall add rule name="Public network WinRM" dir=in action=allow protocol=TCP localport=5985 profile=public \ No newline at end of file diff --git a/malboxes/scripts/windows/malware_analysis.ps1 b/malboxes/scripts/windows/malware_analysis.ps1 index 4bcaa1d..6b3343b 100644 --- a/malboxes/scripts/windows/malware_analysis.ps1 +++ b/malboxes/scripts/windows/malware_analysis.ps1 @@ -5,4 +5,4 @@ Set-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server" netsh advfirewall firewall set rule group="remote desktop" new enable=Yes # IDA Remote Debugging -netsh advfirewall firewall add rule name="IDA Remote Debugging" dir=in action=allow protocol=TCP localport=23946 +netsh advfirewall firewall add rule name="IDA Remote Debugging" dir=in action=allow protocol=TCP localport=23946 \ No newline at end of file diff --git a/malboxes/templates/snippets/builder_virtualbox_windows.json b/malboxes/templates/snippets/builder_virtualbox_windows.json index b2cf75f..58933f3 100644 --- a/malboxes/templates/snippets/builder_virtualbox_windows.json +++ b/malboxes/templates/snippets/builder_virtualbox_windows.json @@ -13,4 +13,9 @@ ], "boot_wait": "10s", "disk_size": "{{ disk_size }}", + {# The ouput format of the virtual machine requires to be .OVA + for the amazon-import post-processor #} + {% if hypervisor == "aws" %} + "format": "ova", + {% endif %} "output_directory": "builds" diff --git a/malboxes/templates/snippets/postprocessor_aws.json b/malboxes/templates/snippets/postprocessor_aws.json new file mode 100644 index 0000000..af32027 --- /dev/null +++ b/malboxes/templates/snippets/postprocessor_aws.json @@ -0,0 +1,14 @@ +"post-processors": [ + {# Exports the .OVA created by the builder to the S3 bucket then converts the file to an AMI + (This can take a very long time). + Use this command to see the status of the import: aws ec2 describe-import-image-tasks #} + { + "type": "amazon-import", + "access_key": "{{ aws_access_key }}", + "secret_key": "{{ aws_secret_key }}", + "region": "{{ aws_region }}", + "s3_bucket_name": "{{ aws_s3_bucket }}", + "license_type": "BYOL", + "tags": {"Name" : "Malboxes", "Template": "{{ template_name }}"} + } +] diff --git a/malboxes/templates/snippets/provision_powershell.json b/malboxes/templates/snippets/provision_powershell.json index c1d0106..aef8966 100644 --- a/malboxes/templates/snippets/provision_powershell.json +++ b/malboxes/templates/snippets/provision_powershell.json @@ -1,6 +1,7 @@ { "type": "powershell", "scripts": [ + "{{ dir }}/scripts/windows/allow-WinRM-public.ps1", {% if not windows_updates == "true" %}"{{ dir }}/scripts/windows/disable_auto-updates.ps1",{% endif %} {% if not windows_defender == "true" %}"{{ dir }}/scripts/windows/disable_defender.ps1",{% endif %} {% if hypervisor == "virtualbox" %} @@ -9,6 +10,7 @@ "{{ dir }}/scripts/windows/installtools.ps1", {% if profile is defined %}"{{ cache_dir }}/profile-{{ profile }}.ps1",{% endif %} "{{ dir }}/scripts/windows/malware_analysis.ps1" + ] } {% if choco_packages %}, diff --git a/malboxes/templates/snippets/provision_powershell_win7.json b/malboxes/templates/snippets/provision_powershell_win7.json index dcdc747..0dad13f 100644 --- a/malboxes/templates/snippets/provision_powershell_win7.json +++ b/malboxes/templates/snippets/provision_powershell_win7.json @@ -5,8 +5,9 @@ {% if not windows_updates == "true" %}"{{ dir }}/scripts/windows/disable_auto-updates.ps1",{% endif %} {% if not windows_defender == "true" %}"{{ dir }}/scripts/windows/disable_defender.ps1",{% endif %} {% if hypervisor == "virtualbox" %} - "{{ dir }}/scripts/windows/vmtools.ps1" + "{{ dir }}/scripts/windows/vmtools.ps1", {% endif %} + "{{ dir }}/scripts/windows/allow-WinRM-public.ps1" ] }, { diff --git a/malboxes/templates/win10_32_analyst.json b/malboxes/templates/win10_32_analyst.json index 689b936..5b79623 100644 --- a/malboxes/templates/win10_32_analyst.json +++ b/malboxes/templates/win10_32_analyst.json @@ -22,8 +22,6 @@ ] }], - {% include 'snippets/postprocessor_vagrant.json' %}, - {% if hypervisor == 'virtualbox' %} {% include 'snippets/postprocessor_vagrant.json' %}, {% endif %} diff --git a/malboxes/templates/win10_64_analyst.json b/malboxes/templates/win10_64_analyst.json index 911e6fe..d8c1a5e 100644 --- a/malboxes/templates/win10_64_analyst.json +++ b/malboxes/templates/win10_64_analyst.json @@ -1,7 +1,7 @@ { "builders": [{ - {% if hypervisor == "virtualbox" %} + {% if hypervisor == "virtualbox" or hypervisor == "aws" %} "guest_os_type": "Windows10_64", {% include 'snippets/builder_virtualbox_windows.json' %}, {% elif hypervisor == "vsphere" %} @@ -11,7 +11,7 @@ "iso_urls": [ "file://{{ iso_path }}/14393.0.160715-1616.RS1_RELEASE_CLIENTENTERPRISEEVAL_OEMRET_X64FRE_EN-US.ISO", - "http://care.dlservice.microsoft.com/dl/download/2/5/4/254230E8-AEA5-43C5-94F6-88CE222A5846/14393.0.160715-1616.RS1_RELEASE_CLIENTENTERPRISEEVAL_OEMRET_X64FRE_EN-US.ISO" + "http://download.microsoft.com/download/2/5/4/254230E8-AEA5-43C5-94F6-88CE222A5846/14393.0.160715-1616.RS1_RELEASE_CLIENTENTERPRISEEVAL_OEMRET_X64FRE_EN-US.ISO" ], "iso_checksum": "a86ae3d664553cd0ee9a6bcd83a5dbe92e3dc41a", "iso_checksum_type": "sha1", @@ -24,6 +24,8 @@ {% if hypervisor == 'virtualbox' %} {% include 'snippets/postprocessor_vagrant.json' %}, + {% elif hypervisor == 'aws' %} + {% include 'snippets/postprocessor_aws.json' %}, {% endif %} "provisioners": [ diff --git a/malboxes/templates/win7_32_analyst.json b/malboxes/templates/win7_32_analyst.json index b5b2d79..31e7ed1 100644 --- a/malboxes/templates/win7_32_analyst.json +++ b/malboxes/templates/win7_32_analyst.json @@ -23,7 +23,11 @@ ] }], - {% include 'snippets/postprocessor_vagrant.json' %}, + {% if hypervisor == 'virtualbox' %} + {% include 'snippets/postprocessor_vagrant.json' %}, + {% elif hypervisor == 'aws' %} + {% include 'snippets/postprocessor_aws.json' %}, + {% endif %} "provisioners": [ diff --git a/malboxes/templates/win7_64_analyst.json b/malboxes/templates/win7_64_analyst.json index 97dd8e1..eeeaf33 100644 --- a/malboxes/templates/win7_64_analyst.json +++ b/malboxes/templates/win7_64_analyst.json @@ -23,7 +23,11 @@ ] }], - {% include 'snippets/postprocessor_vagrant.json' %}, + {% if hypervisor == 'aws' %} + {% include 'snippets/postprocessor_aws.json' %}, + {% else %} + {% include 'snippets/postprocessor_vagrant.json' %}, + {% endif %} "provisioners": [ diff --git a/malboxes/vagrantfiles/analyst_aws.rb b/malboxes/vagrantfiles/analyst_aws.rb new file mode 100644 index 0000000..697da9f --- /dev/null +++ b/malboxes/vagrantfiles/analyst_aws.rb @@ -0,0 +1,33 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : +Vagrant.configure(2) do |config| + # Using dummy box with provider=aws metadata + config.vm.box = "dummy" + config.vm.box_url = "https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box" + + config.vm.guest = :windows + config.vm.communicator = :winrm + config.winrm.username = "{{ username }}" + config.winrm.password = "{{ password }}" + + # Giving plenty of times for updates + config.vm.boot_timeout = 600 + config.vm.graceful_halt_timeout = 600 + + #Configuration information for AWS + config.vm.provider :aws do |aws, override| + aws.access_key_id = "{{ aws_access_key }}" + aws.secret_access_key = "{{ aws_secret_key }}" + aws.instance_type = "{{ aws_instance_type }}" + aws.security_groups = "{{ aws_security_group }}" + aws.keypair_name = "{{ aws_keypair }}" + aws.ami = "{{ aws_ami_id }}" + aws.tags = { + 'Name' => "Malboxes", + 'Template' => "{{ template_name }}" + } + end + + # Disable the default synced folder (vagrant-aws only supports rsync and our images don't) + config.vm.synced_folder ".", "/vagrant", disabled: true +end \ No newline at end of file diff --git a/malboxes/vagrantfiles/box_win.rb b/malboxes/vagrantfiles/box_win.rb index 32f4bbe..1396768 100644 --- a/malboxes/vagrantfiles/box_win.rb +++ b/malboxes/vagrantfiles/box_win.rb @@ -7,6 +7,7 @@ # Giving plenty of times for updates config.vm.boot_timeout = 600 config.vm.graceful_halt_timeout = 600 + config.vm.provider "virtualbox" do |vb| vb.gui = true vb.customize ["modifyvm", :id, "--vram", "128"] diff --git a/requirements.txt b/requirements.txt index a91552e..65c72cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ appdirs Jinja2>=2.9 jsmin +boto3 + diff --git a/setup.py b/setup.py index 42dca75..6b774a6 100644 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ def _teardown(): # your project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/requirements.html - install_requires=['appdirs', 'Jinja2', 'jsmin'], + install_requires=['appdirs', 'Jinja2', 'jsmin', 'boto3'], # List additional groups of dependencies here (e.g. development # dependencies). You can install these using the following syntax,