From 946e014375e9d09d25bd1089f363a3400a11b81e Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Sat, 16 Jul 2016 15:27:45 -0400 Subject: [PATCH 01/17] Open ports for remote IDA debugging --- TODO.adoc | 9 --------- malboxes/profiles/win10_32_analyst.json | 2 +- malboxes/profiles/win10_64_analyst.json | 2 +- malboxes/profiles/win7_32_analyst.json | 2 +- .../windows/{enablerdp.ps1 => malware_analysis.ps1} | 4 ++++ 5 files changed, 7 insertions(+), 12 deletions(-) rename malboxes/scripts/windows/{enablerdp.ps1 => malware_analysis.ps1} (65%) diff --git a/TODO.adoc b/TODO.adoc index b673b48..ed032f1 100644 --- a/TODO.adoc +++ b/TODO.adoc @@ -1,17 +1,8 @@ = TODO -== pip packaging - -* where should the built boxes go? - -== Misc - -* Make work with trial ISOs - == Minimal malware analyst use case * push sample w/ IDA debugger -* open ports for remote IDA debugging * vagrant no net, NAT * vagrant update box + send on network (archiving) * vagrant team workflow diff --git a/malboxes/profiles/win10_32_analyst.json b/malboxes/profiles/win10_32_analyst.json index 7c91391..55d461c 100644 --- a/malboxes/profiles/win10_32_analyst.json +++ b/malboxes/profiles/win10_32_analyst.json @@ -53,7 +53,7 @@ "type": "powershell", "scripts": [ "{{user `malboxes_dir`}}/scripts/windows/vmtools.ps1", - "{{user `malboxes_dir`}}/scripts/windows/enablerdp.ps1", + "{{user `malboxes_dir`}}/scripts/windows/malware_analysis.ps1", "{{user `malboxes_dir`}}/scripts/windows/installtools.ps1" ] }] diff --git a/malboxes/profiles/win10_64_analyst.json b/malboxes/profiles/win10_64_analyst.json index 2c485eb..9672f90 100644 --- a/malboxes/profiles/win10_64_analyst.json +++ b/malboxes/profiles/win10_64_analyst.json @@ -54,7 +54,7 @@ "type": "powershell", "scripts": [ "{{user `malboxes_dir`}}/scripts/windows/vmtools.ps1", - "{{user `malboxes_dir`}}/scripts/windows/enablerdp.ps1", + "{{user `malboxes_dir`}}/scripts/windows/malware_analysis.ps1", "{{user `malboxes_dir`}}/scripts/windows/installtools.ps1" ] }] diff --git a/malboxes/profiles/win7_32_analyst.json b/malboxes/profiles/win7_32_analyst.json index 3ee60d8..150014a 100644 --- a/malboxes/profiles/win7_32_analyst.json +++ b/malboxes/profiles/win7_32_analyst.json @@ -50,7 +50,7 @@ "type": "powershell", "scripts": [ "{{user `malboxes_dir`}}/scripts/windows/vmtools.ps1", - "{{user `malboxes_dir`}}/scripts/windows/enablerdp.ps1", + "{{user `malboxes_dir`}}/scripts/windows/malware_analysis.ps1", "{{user `malboxes_dir`}}/scripts/windows/installtools.ps1" ] }] diff --git a/malboxes/scripts/windows/enablerdp.ps1 b/malboxes/scripts/windows/malware_analysis.ps1 similarity index 65% rename from malboxes/scripts/windows/enablerdp.ps1 rename to malboxes/scripts/windows/malware_analysis.ps1 index a2eebe2..4bcaa1d 100644 --- a/malboxes/scripts/windows/enablerdp.ps1 +++ b/malboxes/scripts/windows/malware_analysis.ps1 @@ -1,4 +1,8 @@ +# Enable RDP Set-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server" -Name "fDenyTSConnections" -Value 0 # The rule below doesn't work for Windows 7 # Enable-NetFirewallRule -DisplayGroup "Remote Desktop" 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 From fb25dc944164499a5a6c64e53a4416a992a429aa Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Mon, 25 Jul 2016 15:01:53 -0400 Subject: [PATCH 02/17] Added -d (--debug) flag which is passed to packer as -debug --- malboxes/malboxes.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/malboxes/malboxes.py b/malboxes/malboxes.py index 5705b7e..6056d1e 100755 --- a/malboxes/malboxes.py +++ b/malboxes/malboxes.py @@ -53,6 +53,7 @@ def init_parser(): "and config generator for malware analysis.") parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__) + parser.add_argument('-d', '--debug', action='store_true', help="Debug mode") subparsers = parser.add_subparsers() # list command @@ -212,7 +213,7 @@ def run_foreground(command): return p.returncode -def run_packer(packer_config): +def run_packer(packer_config, args): print("Starting packer to generate the VM") print("----------------------------------") @@ -227,10 +228,15 @@ def run_packer(packer_config): # run packer with relevant config configfile = os.path.join(DIRS.user_config_dir, 'config.json') - cmd = [binary, 'build', - '-var-file={}'.format(configfile), - "-var", "malboxes_cache_dir={}".format(DIRS.user_cache_dir), - packer_config] + + flags = ['-var-file={}'.format(configfile), + "-var", "malboxes_cache_dir={}".format(DIRS.user_cache_dir)] + if args.debug: + flags.append('-debug') + + cmd = [binary, 'build'] + cmd.extend(flags) + cmd.append(packer_config) ret = run_foreground(cmd) print("----------------------------------") @@ -277,7 +283,8 @@ def build(parser, args): prepare_autounattend(config) print("Configuration files are ready") ret = run_packer(resource_filename(__name__, - "profiles/{}.json".format(args.profile))) + "profiles/{}.json".format(args.profile)), + args) if ret != 0: print("Packer failed. Build failed. Exiting...") From 7346eb55f6df7db4a81eb1582d4eac315d387b77 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Tue, 26 Jul 2016 10:49:26 -0400 Subject: [PATCH 03/17] packer build: Changing directory to AppDirs' cache This way we can leverage packer's packer_cache/ directory and not clutter the location the user is working from. Generated vagrant box is also in the user's cache directory and `vagrant box add` was updated to fetch box from cache directory as well --- malboxes/malboxes.py | 48 ++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/malboxes/malboxes.py b/malboxes/malboxes.py index 6056d1e..8f5127a 100755 --- a/malboxes/malboxes.py +++ b/malboxes/malboxes.py @@ -217,27 +217,34 @@ def run_packer(packer_config, args): print("Starting packer to generate the VM") print("----------------------------------") - # packer or packer-io? - binary = 'packer' - if shutil.which(binary) == None: - binary = 'packer-io' + prev_cwd = os.getcwd() + os.chdir(DIRS.user_cache_dir) + + try: + # packer or packer-io? + binary = 'packer' if shutil.which(binary) == None: - print("packer not found. Install it: " - "https://www.packer.io/intro/getting-started/setup.html") - return 254 + binary = 'packer-io' + if shutil.which(binary) == None: + print("packer not found. Install it: " + "https://www.packer.io/intro/getting-started/setup.html") + return 254 - # run packer with relevant config - configfile = os.path.join(DIRS.user_config_dir, 'config.json') + # run packer with relevant config + configfile = os.path.join(DIRS.user_config_dir, 'config.json') - flags = ['-var-file={}'.format(configfile), - "-var", "malboxes_cache_dir={}".format(DIRS.user_cache_dir)] - if args.debug: - flags.append('-debug') + flags = ['-var-file={}'.format(configfile), + "-var", "malboxes_cache_dir={}".format(DIRS.user_cache_dir)] + if args.debug: + flags.append('-debug') - cmd = [binary, 'build'] - cmd.extend(flags) - cmd.append(packer_config) - ret = run_foreground(cmd) + cmd = [binary, 'build'] + cmd.extend(flags) + cmd.append(packer_config) + ret = run_foreground(cmd) + + finally: + os.chdir(prev_cwd) print("----------------------------------") print("packer completed with return code: {}".format(ret)) @@ -249,6 +256,7 @@ def import_box(config, args): print("--------------------------") box = config['post-processors'][0]['output'] + box = os.path.join(DIRS.user_cache_dir, box) box = box.replace('{{user `name`}}', args.profile) cmd = ['vagrant', 'box', 'add', box, '--name={}'.format(args.profile)] @@ -303,13 +311,13 @@ def build(parser, args): malboxes spin {} - You can safely remove the boxes/ directory if you don't plan on - hosting or sharing your base box. + You can safely remove the {}/boxes/ + directory if you don't plan on hosting or sharing your base box. You can re-use this base box several times by using `malboxes spin`. Each VM will be independent of each other. ===============================================================""") - .format(args.profile)) + .format(args.profile, DIRS.user_cache_dir)) def spin(parser, args): From ddf345cede203f00e1c8ca0e8abf801ea74c13f8 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Tue, 26 Jul 2016 12:08:33 -0400 Subject: [PATCH 04/17] Debugging aid: Flags to skip packer build and vagrant box add Also fixed a small terminology mistach: vagrant import vs vagrant box add --- malboxes/malboxes.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/malboxes/malboxes.py b/malboxes/malboxes.py index 8f5127a..d44db94 100755 --- a/malboxes/malboxes.py +++ b/malboxes/malboxes.py @@ -68,6 +68,12 @@ def init_parser(): parser_build.add_argument('profile', help='Name of the profile to build. ' 'Use list command to view ' 'available profiles.') + parser_build.add_argument('--skip-packer-build', action='store_true', + help='Skip packer build phase. ' + 'Only useful for debugging.') + parser_build.add_argument('--skip-vagrant-box-add', action='store_true', + help='Skip vagrant box add phase. ' + 'Only useful for debugging.') parser_build.set_defaults(func=build) # spin command @@ -251,8 +257,8 @@ def run_packer(packer_config, args): return ret -def import_box(config, args): - print("Importing box into vagrant") +def add_box(config, args): + print("Adding box into vagrant") print("--------------------------") box = config['post-processors'][0]['output'] @@ -263,7 +269,7 @@ def import_box(config, args): ret = run_foreground(cmd) print("----------------------------") - print("vagrant box import completed with return code: {}".format(ret)) + print("vagrant box add completed with return code: {}".format(ret)) return ret @@ -290,15 +296,21 @@ def build(parser, args): print("Generating configuration files...") prepare_autounattend(config) print("Configuration files are ready") - ret = run_packer(resource_filename(__name__, - "profiles/{}.json".format(args.profile)), - args) + if not args.skip_packer_build: + profile = "profiles/{}.json".format(args.profile) + ret = run_packer(resource_filename(__name__, profile), args) + else: + ret = 0 if ret != 0: print("Packer failed. Build failed. Exiting...") sys.exit(3) - ret = import_box(config, args) + if not args.skip_vagrant_box_add: + ret = add_box(config, args) + else: + ret = 0 + if ret != 0: print("'vagrant box add' failed. Build failed. Exiting...") sys.exit(4) From 43a841842878b57c067f392beb9ec07e50dcf160 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Tue, 26 Jul 2016 12:53:24 -0400 Subject: [PATCH 05/17] Testing JSON profiles in `make test` --- Makefile | 1 + tests/profiles_json_valid.sh | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100755 tests/profiles_json_valid.sh diff --git a/Makefile b/Makefile index 69e10ed..358c67d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ test: pylint malboxes + ./tests/profiles_json_valid.sh pkg_clean: rm -r build/ dist/ malboxes.egg-info/ diff --git a/tests/profiles_json_valid.sh b/tests/profiles_json_valid.sh new file mode 100755 index 0000000..753f0be --- /dev/null +++ b/tests/profiles_json_valid.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +for F in `ls malboxes/profiles/*.json`; do + echo "Processing file $F"; + python3 -m json.tool $F 1>/dev/null + if [[ $? -ne 0 ]]; then + echo "Badly formatted JSON file! Failing test..."; + exit 1; + else + echo "Properly formatted JSON file"; + fi +done From a8d3856390db009639d75f51345c55ad13dd8705 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Tue, 26 Jul 2016 12:59:13 -0400 Subject: [PATCH 06/17] IDA Remote Debugger and Tools directory uploaded to box --- malboxes/profiles/win10_32_analyst.json | 10 ++++++++++ malboxes/profiles/win10_64_analyst.json | 10 ++++++++++ malboxes/profiles/win7_32_analyst.json | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/malboxes/profiles/win10_32_analyst.json b/malboxes/profiles/win10_32_analyst.json index 55d461c..c087c0d 100644 --- a/malboxes/profiles/win10_32_analyst.json +++ b/malboxes/profiles/win10_32_analyst.json @@ -56,5 +56,15 @@ "{{user `malboxes_dir`}}/scripts/windows/malware_analysis.ps1", "{{user `malboxes_dir`}}/scripts/windows/installtools.ps1" ] + }, + { + "type": "file", + "source": "{{user `ida_path`}}/dbgsrv/win32_remote.exe", + "destination": "C:\\Tools\\win32_remote.exe" + }, + { + "type": "file", + "source": "{{user `tools_path`}}/", + "destination": "C:\\Tools" }] } diff --git a/malboxes/profiles/win10_64_analyst.json b/malboxes/profiles/win10_64_analyst.json index 9672f90..4d157f6 100644 --- a/malboxes/profiles/win10_64_analyst.json +++ b/malboxes/profiles/win10_64_analyst.json @@ -57,5 +57,15 @@ "{{user `malboxes_dir`}}/scripts/windows/malware_analysis.ps1", "{{user `malboxes_dir`}}/scripts/windows/installtools.ps1" ] + }, + { + "type": "file", + "source": "{{user `ida_path`}}/dbgsrv/win64_remotex64.exe", + "destination": "C:\\Tools\\win64_remotex64.exe" + }, + { + "type": "file", + "source": "{{user `tools_path`}}/", + "destination": "C:\\Tools" }] } diff --git a/malboxes/profiles/win7_32_analyst.json b/malboxes/profiles/win7_32_analyst.json index 150014a..2a41770 100644 --- a/malboxes/profiles/win7_32_analyst.json +++ b/malboxes/profiles/win7_32_analyst.json @@ -53,5 +53,15 @@ "{{user `malboxes_dir`}}/scripts/windows/malware_analysis.ps1", "{{user `malboxes_dir`}}/scripts/windows/installtools.ps1" ] + }, + { + "type": "file", + "source": "{{user `ida_path`}}/dbgsrv/win32_remote.exe", + "destination": "C:\\Tools\\win32_remote.exe" + }, + { + "type": "file", + "source": "{{user `tools_path`}}/", + "destination": "C:\\Tools" }] } From dce739f842173394b972bd4a4fddf64dd4d71de0 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Tue, 26 Jul 2016 16:10:31 -0400 Subject: [PATCH 07/17] Compose JSON packer templates out of smaller files and use a JS config * config.js is now a javascript file that gets minified into a json file by malboxes * Comments are thus allowed and used to enable or disable features (IDA debuggers and Tools uploads) * Packer template file is built based on features enabled or disabled * Removed duplication in packer profiles --- MANIFEST.in | 2 +- Makefile | 1 + README.adoc | 2 +- config-example.js | 32 ++++++ config-example.json | 9 -- docs/windows-licenses.adoc | 4 +- malboxes/malboxes.py | 97 ++++++++++++++++--- malboxes/profiles/snippets/ida_remote_32.json | 5 + malboxes/profiles/snippets/ida_remote_64.json | 5 + malboxes/profiles/snippets/tools.json | 5 + malboxes/profiles/win10_32_analyst.json | 13 +-- malboxes/profiles/win10_64_analyst.json | 14 +-- malboxes/profiles/win7_32_analyst.json | 13 +-- requirements.txt | 1 + setup.py | 4 +- tests/config_example_valid.sh | 12 +++ tests/profiles_json_valid.sh | 4 +- 17 files changed, 154 insertions(+), 69 deletions(-) create mode 100644 config-example.js delete mode 100644 config-example.json create mode 100644 malboxes/profiles/snippets/ida_remote_32.json create mode 100644 malboxes/profiles/snippets/ida_remote_64.json create mode 100644 malboxes/profiles/snippets/tools.json create mode 100755 tests/config_example_valid.sh diff --git a/MANIFEST.in b/MANIFEST.in index 0e1ad3b..02d9f64 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,7 @@ include LICENSE README.adoc TODO.adoc # Include the data files -include config-example.json +include config-example.js graft docs prune docs/presentation/ graft malboxes diff --git a/Makefile b/Makefile index 358c67d..b709008 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ test: pylint malboxes ./tests/profiles_json_valid.sh + ./tests/config_example_valid.sh pkg_clean: rm -r build/ dist/ malboxes.egg-info/ diff --git a/README.adoc b/README.adoc index d79adfe..b6b5464 100644 --- a/README.adoc +++ b/README.adoc @@ -101,7 +101,7 @@ For example: malboxes build win10_64_analyst If you want to customize your configuration, look at the following location -for a `config.json` file: +for a `config.js` file: * Linux/Unix: `~/.config/malboxes/` * Mac OS X: `~/Library/Application Support/malboxes/` diff --git a/config-example.js b/config-example.js new file mode 100644 index 0000000..c005df1 --- /dev/null +++ b/config-example.js @@ -0,0 +1,32 @@ +{ + /* + * Malboxes Example Configuration File + * + * Uncomment a specific section of the file to trigger a particular feature. + */ + + // This allows you to use a local filestore for ISOs. + // For all versions of Windows except Windows 10 you will need this. + // "iso_path": "/path/to/your/windows/isos/", + + // Trial or registered version? + // If using a registered product update the product_key and set trial to 'false'. + // See https://github.com/GoSecure/malboxes/blob/master/docs/windows-licenses.adoc for more information. + "trial": "true", + //"trial": "false", + //"product_key": "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX", + + // VM username and password + // TODO. It doesn't work now. + //"username": "vagrant", + //"password": "vagrant", + + // Setting the IDA Path will copy the IDA remote debugging tools into the guest + //"ida_path": "/path/to/your/ida", + + // Setting Tools Path will copy all the files under the given path into the guest. + // Useful to copy proprietary or unpackaged tools. + //"tools_path": "/path/to/your/tools", + + "_comment": "last line must finish without a comma for file to be valid json" +} diff --git a/config-example.json b/config-example.json deleted file mode 100644 index e40d77a..0000000 --- a/config-example.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "iso_path": "/path/to/your/windows/isos/", - "_comment": "If using a registered product update the product_key and set trial to 'false'.", - "_comment": "See docs/windows-licenses.adoc for more information.", - "trial": "true", - "product_key": "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX", - "username": "vagrant", - "password": "vagrant" -} diff --git a/docs/windows-licenses.adoc b/docs/windows-licenses.adoc index ef9088d..03766d1 100644 --- a/docs/windows-licenses.adoc +++ b/docs/windows-licenses.adoc @@ -6,7 +6,7 @@ Trial versions of Windows use a different .ISO. You can find them here: https://www.microsoft.com/en-us/evalcenter/evaluate-windows If you want to use a trial version make sure you have the following in your -`config.json`: +`config.js`: "trial": true @@ -15,7 +15,7 @@ longer available. Open an issue if you can provide missing information. == Registered -Specify your product key in your `config.json` file. For example: +Specify your product key in your `config.js` file. For example: "trial": false, "product_key": "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" diff --git a/malboxes/malboxes.py b/malboxes/malboxes.py index d44db94..96ca104 100755 --- a/malboxes/malboxes.py +++ b/malboxes/malboxes.py @@ -32,6 +32,7 @@ from appdirs import AppDirs from jinja2 import Environment, FileSystemLoader +from jsmin import jsmin from malboxes._version import __version__ @@ -147,8 +148,7 @@ def prepare_autounattend(config): Uses jinja2 template syntax to generate the resulting XML file. """ - # os type is extracted from profile json - os_type = config['builders'][0]['guest_os_type'].lower() + os_type = _get_os_type(config) filepath = resource_filename(__name__, "installconfig/") env = Environment(loader=FileSystemLoader(filepath)) @@ -158,9 +158,51 @@ def prepare_autounattend(config): f.close() +def prepare_packer_template(config, template_name): + """ + Prepares a packer template JSON file according to configuration and writes + it into a temporary location where packer later expects it. + + We need to do this since we are composing several JSON snippets based on + features enabled in configuration. + """ + template_fd = resource_stream(__name__, + 'profiles/{}.json'.format(template_name)) + template = json.load(TextIOWrapper(template_fd)) + + # merge optional configurations + # IDA remote debugger, based on target architecture + if config.get('ida_path'): + if _is_os_32bits(config): + template['provisioners'].append(fetch_snippet('ida_remote_32')) + else: + template['provisioners'].append(fetch_snippet('ida_remote_64')) + + # tools_path for tools upload + if config.get('tools_path'): + template['provisioners'].append(fetch_snippet('tools')) + + # write to temporary file + f = create_cachefd('{}.json'.format(template_name)) + f.write(json.dumps(template)) + f.close() + return f.name + + def load_config(profile): """ - Config is in JSON since we can re-use the same in both malboxes and packer + Load Malboxes configuration and merge with Packer profile configuration + + Packer uses a configuration in JSON so we decided to go with JSON as well. + However since we have features that should be easily "toggled" by our users + I wanted to add an easy way of "commenting out" or "uncommenting" a + particular feature. JSON doesn't support comments. However JSON's author + gives a nice suggestion here[1] that I will follow. + + In a nutshell, our configuration is Javascript, which when minified gives + JSON and then it gets merged with the selected profile. + + [1]: https://plus.google.com/+DouglasCrockfordEsq/posts/RK8qyGVaGSr """ try: profile_fd = resource_stream(__name__, @@ -170,17 +212,18 @@ def load_config(profile): sys.exit(2) # if config does not exist, copy default one - config_file = os.path.join(DIRS.user_config_dir, 'config.json') + config_file = os.path.join(DIRS.user_config_dir, 'config.js') if not os.path.isfile(config_file): print("Default configuration doesn't exist. Populating one: {}" .format(config_file)) - shutil.copy(resource_filename(__name__, 'config-example.json'), + shutil.copy(resource_filename(__name__, 'config-example.js'), config_file) # load general config config = {} with open(config_file, 'r') as f: - config = json.load(f) + # minify then load as JSON + config = json.loads(jsmin(f.read())) # merge/update with profile config config.update(json.load(TextIOWrapper(profile_fd))) @@ -188,6 +231,22 @@ def load_config(profile): return config +def fetch_snippet(filename): + """Returns given snippet filename parsed as json""" + snippet = resource_stream(__name__, + 'profiles/snippets/{}.json'.format(filename)) + return json.load(TextIOWrapper(snippet)) + + +def _get_os_type(config): + """OS Type is extracted from profile json config""" + return config['builders'][0]['guest_os_type'].lower() + + +def _is_os_32bits(config): + return not _get_os_type(config)[-3:] == '_64' + + tempfiles = [] def create_cachefd(filename): tempfiles.append(filename) @@ -219,7 +278,7 @@ def run_foreground(command): return p.returncode -def run_packer(packer_config, args): +def run_packer(packer_tmpl, args): print("Starting packer to generate the VM") print("----------------------------------") @@ -233,20 +292,26 @@ def run_packer(packer_config, args): binary = 'packer-io' if shutil.which(binary) == None: print("packer not found. Install it: " - "https://www.packer.io/intro/getting-started/setup.html") + "https://www.packer.io/intro/getting-started/setup.html") return 254 - # run packer with relevant config - configfile = os.path.join(DIRS.user_config_dir, 'config.json') - - flags = ['-var-file={}'.format(configfile), - "-var", "malboxes_cache_dir={}".format(DIRS.user_cache_dir)] + # run packer with relevant config minified + configfile = os.path.join(DIRS.user_config_dir, 'config.js') + with open(configfile, 'r') as config: + f = create_cachefd('packer_var_file.json') + f.write(jsmin(config.read())) + f.close() + + filepath = resource_filename(__name__, "") + flags = ['-var-file={}'.format(f.name), + "-var", "malboxes_cache_dir={}".format(DIRS.user_cache_dir), + "-var", "malboxes_dir={}".format(filepath)] if args.debug: flags.append('-debug') cmd = [binary, 'build'] cmd.extend(flags) - cmd.append(packer_config) + cmd.append(packer_tmpl) ret = run_foreground(cmd) finally: @@ -295,10 +360,10 @@ def build(parser, args): print("Generating configuration files...") prepare_autounattend(config) + filename = prepare_packer_template(config, args.profile) print("Configuration files are ready") if not args.skip_packer_build: - profile = "profiles/{}.json".format(args.profile) - ret = run_packer(resource_filename(__name__, profile), args) + ret = run_packer(filename, args) else: ret = 0 diff --git a/malboxes/profiles/snippets/ida_remote_32.json b/malboxes/profiles/snippets/ida_remote_32.json new file mode 100644 index 0000000..44d7835 --- /dev/null +++ b/malboxes/profiles/snippets/ida_remote_32.json @@ -0,0 +1,5 @@ +{ +"type": "file", +"source": "{{user `ida_path`}}/dbgsrv/win32_remote.exe", +"destination": "C:\\Tools\\win32_remote.exe" +} diff --git a/malboxes/profiles/snippets/ida_remote_64.json b/malboxes/profiles/snippets/ida_remote_64.json new file mode 100644 index 0000000..25b7527 --- /dev/null +++ b/malboxes/profiles/snippets/ida_remote_64.json @@ -0,0 +1,5 @@ +{ +"type": "file", +"source": "{{user `ida_path`}}/dbgsrv/win64_remotex64.exe", +"destination": "C:\\Tools\\win64_remotex64.exe" +} diff --git a/malboxes/profiles/snippets/tools.json b/malboxes/profiles/snippets/tools.json new file mode 100644 index 0000000..1a1535a --- /dev/null +++ b/malboxes/profiles/snippets/tools.json @@ -0,0 +1,5 @@ +{ +"type": "file", +"source": "{{user `tools_path`}}/", +"destination": "C:\\Tools" +} diff --git a/malboxes/profiles/win10_32_analyst.json b/malboxes/profiles/win10_32_analyst.json index c087c0d..f0df9cd 100644 --- a/malboxes/profiles/win10_32_analyst.json +++ b/malboxes/profiles/win10_32_analyst.json @@ -2,8 +2,7 @@ "variables": { "winrm_user": "vagrant", "winrm_pass": "vagrant", - "name": "win10_32_analyst", - "malboxes_dir": "{{ template_dir }}/../" + "name": "win10_32_analyst" }, "builders": [{ @@ -56,15 +55,5 @@ "{{user `malboxes_dir`}}/scripts/windows/malware_analysis.ps1", "{{user `malboxes_dir`}}/scripts/windows/installtools.ps1" ] - }, - { - "type": "file", - "source": "{{user `ida_path`}}/dbgsrv/win32_remote.exe", - "destination": "C:\\Tools\\win32_remote.exe" - }, - { - "type": "file", - "source": "{{user `tools_path`}}/", - "destination": "C:\\Tools" }] } diff --git a/malboxes/profiles/win10_64_analyst.json b/malboxes/profiles/win10_64_analyst.json index 4d157f6..0046f05 100644 --- a/malboxes/profiles/win10_64_analyst.json +++ b/malboxes/profiles/win10_64_analyst.json @@ -2,8 +2,7 @@ "variables": { "winrm_user": "vagrant", "winrm_pass": "vagrant", - "name": "win10_64_analyst", - "malboxes_dir": "{{ template_dir }}/../" + "name": "win10_64_analyst" }, "builders": [{ @@ -22,7 +21,6 @@ "output_directory": "builds", - "iso_urls": [ "file://{{ user `iso_path` }}/10240.16384.150709-1700.TH1_CLIENTENTERPRISEEVAL_OEMRET_X64FRE_EN-US.ISO", "http://care.dlservice.microsoft.com/dl/download/C/3/9/C399EEA8-135D-4207-92C9-6AAB3259F6EF/10240.16384.150709-1700.TH1_CLIENTENTERPRISEEVAL_OEMRET_X64FRE_EN-US.ISO" @@ -57,15 +55,5 @@ "{{user `malboxes_dir`}}/scripts/windows/malware_analysis.ps1", "{{user `malboxes_dir`}}/scripts/windows/installtools.ps1" ] - }, - { - "type": "file", - "source": "{{user `ida_path`}}/dbgsrv/win64_remotex64.exe", - "destination": "C:\\Tools\\win64_remotex64.exe" - }, - { - "type": "file", - "source": "{{user `tools_path`}}/", - "destination": "C:\\Tools" }] } diff --git a/malboxes/profiles/win7_32_analyst.json b/malboxes/profiles/win7_32_analyst.json index 2a41770..d1a5ca2 100644 --- a/malboxes/profiles/win7_32_analyst.json +++ b/malboxes/profiles/win7_32_analyst.json @@ -2,8 +2,7 @@ "variables": { "winrm_user": "vagrant", "winrm_pass": "vagrant", - "name": "win7_32_analyst", - "malboxes_dir": "{{ template_dir }}/../" + "name": "win7_32_analyst" }, "builders": [{ @@ -53,15 +52,5 @@ "{{user `malboxes_dir`}}/scripts/windows/malware_analysis.ps1", "{{user `malboxes_dir`}}/scripts/windows/installtools.ps1" ] - }, - { - "type": "file", - "source": "{{user `ida_path`}}/dbgsrv/win32_remote.exe", - "destination": "C:\\Tools\\win32_remote.exe" - }, - { - "type": "file", - "source": "{{user `tools_path`}}/", - "destination": "C:\\Tools" }] } diff --git a/requirements.txt b/requirements.txt index 68e55ae..1da94a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ appdirs Jinja2 +jsmin diff --git a/setup.py b/setup.py index 2fcbd3d..a1a2f09 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def _prepare(): # unrelated to actual build # pip will install data_files in an odd location so I copy them in package at # build time - root_data_files = ['LICENSE', 'README.adoc', 'TODO.adoc', 'config-example.json'] + root_data_files = ['LICENSE', 'README.adoc', 'TODO.adoc', 'config-example.js'] for f in root_data_files: _tempfiles.append(shutil.copy(path.join(here, f), @@ -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'], + install_requires=['appdirs', 'Jinja2', 'jsmin'], # List additional groups of dependencies here (e.g. development # dependencies). You can install these using the following syntax, diff --git a/tests/config_example_valid.sh b/tests/config_example_valid.sh new file mode 100755 index 0000000..38659f3 --- /dev/null +++ b/tests/config_example_valid.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +F=config-example.js +echo "Processing file $F"; +# minifying first to strip comments +python3 -m jsmin $F | python3 -m json.tool 1>/dev/null +if [[ $? -ne 0 ]]; then + echo "Badly formatted JSON file! Failing test..."; + exit 1; +else + echo "Properly formatted JSON file"; +fi diff --git a/tests/profiles_json_valid.sh b/tests/profiles_json_valid.sh index 753f0be..189e710 100755 --- a/tests/profiles_json_valid.sh +++ b/tests/profiles_json_valid.sh @@ -1,6 +1,8 @@ #!/bin/bash -for F in `ls malboxes/profiles/*.json`; do +DIRS="malboxes/profiles/*.json malboxes/profiles/snippets/*.json" + +for F in `ls $DIRS`; do echo "Processing file $F"; python3 -m json.tool $F 1>/dev/null if [[ $? -ne 0 ]]; then From 16c5bb4b66016c05aa84fdea1585dcd528c27c3a Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Tue, 26 Jul 2016 16:23:59 -0400 Subject: [PATCH 08/17] doc: added requirement to README --- README.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/README.adoc b/README.adoc index b6b5464..f94e5a2 100644 --- a/README.adoc +++ b/README.adoc @@ -24,6 +24,7 @@ https://github.com/gosecure/malboxes * Python 3.3+ * appdirs * jinja2 +* jsmin * packer: https://www.packer.io/intro/getting-started/setup.html * vagrant: https://www.vagrantup.com/downloads.html From 68a32dda85ef59a6e43e9f85d612613786084078 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Tue, 2 Aug 2016 15:07:05 -0400 Subject: [PATCH 09/17] Packer Templates now built using Jinja2 template engine * Allows for logic to be kept outside of Python code making customization and contributions easier * Ported profile tests to Python and doing a Jinja test and a json test --- Makefile | 2 +- TODO.adoc | 14 ++- malboxes/malboxes.py | 87 ++++++++----------- malboxes/profiles/snippets/ida_remote_32.json | 10 +-- malboxes/profiles/snippets/ida_remote_64.json | 10 +-- malboxes/profiles/snippets/tools.json | 10 +-- malboxes/profiles/win10_32_analyst.json | 33 ++++--- malboxes/profiles/win10_64_analyst.json | 37 ++++---- malboxes/profiles/win7_32_analyst.json | 33 ++++--- tests/profiles_json_valid.py | 56 ++++++++++++ tests/profiles_json_valid.sh | 14 --- 11 files changed, 182 insertions(+), 124 deletions(-) create mode 100755 tests/profiles_json_valid.py delete mode 100755 tests/profiles_json_valid.sh diff --git a/Makefile b/Makefile index b709008..f593adc 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ test: pylint malboxes - ./tests/profiles_json_valid.sh ./tests/config_example_valid.sh + ./tests/profiles_json_valid.py pkg_clean: rm -r build/ dist/ malboxes.egg-info/ diff --git a/TODO.adoc b/TODO.adoc index ed032f1..97a504e 100644 --- a/TODO.adoc +++ b/TODO.adoc @@ -2,10 +2,12 @@ == Minimal malware analyst use case -* push sample w/ IDA debugger +* user/pass from config.js +* vagrant: punch hole through NAT for IDA in firewall +* disable Windows Defender * vagrant no net, NAT -* vagrant update box + send on network (archiving) -* vagrant team workflow +* doc: vagrant update box + send on network (archiving) +* doc: vagrant team workflow ** vagrant box repackaging covered here: http://huestones.co.uk/node/305 * git malware analysis template integrated with malboxes * Integrate virtualbox and wireshark tips: https://www.virtualbox.org/wiki/Network_tips @@ -97,3 +99,9 @@ To do malware analysis on embedded systems. == Support WinXP Talk to sholmes, he did it. + +== Optimizations + +=== Use VirtualBox's linked_clones Vagrant config + +https://www.vagrantup.com/docs/virtualbox/configuration.html diff --git a/malboxes/malboxes.py b/malboxes/malboxes.py index 96ca104..fe7690e 100755 --- a/malboxes/malboxes.py +++ b/malboxes/malboxes.py @@ -163,35 +163,30 @@ def prepare_packer_template(config, template_name): Prepares a packer template JSON file according to configuration and writes it into a temporary location where packer later expects it. - We need to do this since we are composing several JSON snippets based on - features enabled in configuration. + Uses jinja2 template syntax to generate the resulting JSON file. + Templates are in profiles/ and snippets in profiles/snippets/. """ - template_fd = resource_stream(__name__, - 'profiles/{}.json'.format(template_name)) - template = json.load(TextIOWrapper(template_fd)) - - # merge optional configurations - # IDA remote debugger, based on target architecture - if config.get('ida_path'): - if _is_os_32bits(config): - template['provisioners'].append(fetch_snippet('ida_remote_32')) - else: - template['provisioners'].append(fetch_snippet('ida_remote_64')) - - # tools_path for tools upload - if config.get('tools_path'): - template['provisioners'].append(fetch_snippet('tools')) + try: + profile_fd = resource_stream(__name__, + 'profiles/{}.json'.format(template_name)) + except FileNotFoundError: + print("Profile doesn't exist: {}".format(template_name)) + sys.exit(2) + + filepath = resource_filename(__name__, 'profiles/') + env = Environment(loader=FileSystemLoader(filepath), autoescape=False) + template = env.get_template("{}.json".format(template_name)) # write to temporary file f = create_cachefd('{}.json'.format(template_name)) - f.write(json.dumps(template)) + f.write(template.render(config)) # pylint: disable=no-member f.close() return f.name -def load_config(profile): +def prepare_config(profile): """ - Load Malboxes configuration and merge with Packer profile configuration + Prepares Malboxes configuration and merge with Packer profile configuration Packer uses a configuration in JSON so we decided to go with JSON as well. However since we have features that should be easily "toggled" by our users @@ -204,13 +199,6 @@ def load_config(profile): [1]: https://plus.google.com/+DouglasCrockfordEsq/posts/RK8qyGVaGSr """ - try: - profile_fd = resource_stream(__name__, - 'profiles/{}.json'.format(profile)) - except FileNotFoundError: - print("Profile doesn't exist: {}".format(profile)) - sys.exit(2) - # if config does not exist, copy default one config_file = os.path.join(DIRS.user_config_dir, 'config.js') if not os.path.isfile(config_file): @@ -219,34 +207,36 @@ def load_config(profile): shutil.copy(resource_filename(__name__, 'config-example.js'), config_file) - # load general config + config = load_config(config_file, profile) + + packer_tmpl = prepare_packer_template(config, profile) + + # merge/update with profile config + with open(packer_tmpl, 'r') as f: + config.update(json.loads(f.read())) + + return config, packer_tmpl + + +def load_config(config_file, profile): + """Loads the minified JSON config. Returns a dict.""" config = {} with open(config_file, 'r') as f: # minify then load as JSON config = json.loads(jsmin(f.read())) - # merge/update with profile config - config.update(json.load(TextIOWrapper(profile_fd))) - + # add packer required variables + config['cache_dir'] = DIRS.user_cache_dir + config['dir'] = resource_filename(__name__, "") + config['profile_name'] = profile return config -def fetch_snippet(filename): - """Returns given snippet filename parsed as json""" - snippet = resource_stream(__name__, - 'profiles/snippets/{}.json'.format(filename)) - return json.load(TextIOWrapper(snippet)) - - def _get_os_type(config): """OS Type is extracted from profile json config""" return config['builders'][0]['guest_os_type'].lower() -def _is_os_32bits(config): - return not _get_os_type(config)[-3:] == '_64' - - tempfiles = [] def create_cachefd(filename): tempfiles.append(filename) @@ -302,10 +292,7 @@ def run_packer(packer_tmpl, args): f.write(jsmin(config.read())) f.close() - filepath = resource_filename(__name__, "") - flags = ['-var-file={}'.format(f.name), - "-var", "malboxes_cache_dir={}".format(DIRS.user_cache_dir), - "-var", "malboxes_dir={}".format(filepath)] + flags = ['-var-file={}'.format(f.name)] if args.debug: flags.append('-debug') @@ -356,14 +343,14 @@ def list_profiles(parser, args): def build(parser, args): - config = load_config(args.profile) print("Generating configuration files...") + config, packer_tmpl = prepare_config(args.profile) prepare_autounattend(config) - filename = prepare_packer_template(config, args.profile) print("Configuration files are ready") + if not args.skip_packer_build: - ret = run_packer(filename, args) + ret = run_packer(packer_tmpl, args) else: ret = 0 @@ -401,7 +388,7 @@ def spin(parser, args): """ Creates a Vagrantfile based on a template using the jinja2 engine """ - config = load_config(args.profile) + config = prepare_config(args.profile) print("Creating a Vagrantfile") filepath = resource_filename(__name__, "vagrantfiles/") diff --git a/malboxes/profiles/snippets/ida_remote_32.json b/malboxes/profiles/snippets/ida_remote_32.json index 44d7835..80eb328 100644 --- a/malboxes/profiles/snippets/ida_remote_32.json +++ b/malboxes/profiles/snippets/ida_remote_32.json @@ -1,5 +1,5 @@ -{ -"type": "file", -"source": "{{user `ida_path`}}/dbgsrv/win32_remote.exe", -"destination": "C:\\Tools\\win32_remote.exe" -} + { + "type": "file", + "source": "{{ ida_path }}/dbgsrv/win32_remote.exe", + "destination": "C:\\Tools\\win32_remote.exe" + } diff --git a/malboxes/profiles/snippets/ida_remote_64.json b/malboxes/profiles/snippets/ida_remote_64.json index 25b7527..0658a82 100644 --- a/malboxes/profiles/snippets/ida_remote_64.json +++ b/malboxes/profiles/snippets/ida_remote_64.json @@ -1,5 +1,5 @@ -{ -"type": "file", -"source": "{{user `ida_path`}}/dbgsrv/win64_remotex64.exe", -"destination": "C:\\Tools\\win64_remotex64.exe" -} + { + "type": "file", + "source": "{{ ida_path }}/dbgsrv/win64_remotex64.exe", + "destination": "C:\\Tools\\win64_remotex64.exe" + } diff --git a/malboxes/profiles/snippets/tools.json b/malboxes/profiles/snippets/tools.json index 1a1535a..85f151f 100644 --- a/malboxes/profiles/snippets/tools.json +++ b/malboxes/profiles/snippets/tools.json @@ -1,5 +1,5 @@ -{ -"type": "file", -"source": "{{user `tools_path`}}/", -"destination": "C:\\Tools" -} + { + "type": "file", + "source": "{{ tools_path }}/", + "destination": "C:\\Tools" + } diff --git a/malboxes/profiles/win10_32_analyst.json b/malboxes/profiles/win10_32_analyst.json index f0df9cd..002ab86 100644 --- a/malboxes/profiles/win10_32_analyst.json +++ b/malboxes/profiles/win10_32_analyst.json @@ -13,8 +13,8 @@ "communicator": "winrm", "vboxmanage": [ - ["modifyvm", "{{.Name}}", "--memory", "4096"], - ["modifyvm", "{{.Name}}", "--cpus", "1"] + ["modifyvm", "{{ '{{.Name}}' }}", "--memory", "4096"], + ["modifyvm", "{{ '{{.Name}}' }}", "--cpus", "1"] ], "disk_size": "15360", @@ -22,14 +22,14 @@ "output_directory": "builds", "iso_urls": [ - "file://{{ user `iso_path` }}/10586.0.151029-1700.TH2_RELEASE_CLIENTENTERPRISEEVAL_OEMRET_X86FRE_EN-US.ISO", + "file://{{ iso_path }}/10586.0.151029-1700.TH2_RELEASE_CLIENTENTERPRISEEVAL_OEMRET_X86FRE_EN-US.ISO", "http://care.dlservice.microsoft.com/dl/download/B/B/3/BB3611B6-9781-437F-A293-AB43B85C2190/10586.0.151029-1700.TH2_RELEASE_CLIENTENTERPRISEEVAL_OEMRET_X86FRE_EN-US.ISO" ], "iso_checksum": "e431a4e259a5c056a5d58b9cd0628a3c59f112f9", "iso_checksum_type": "sha1", - "winrm_username": "{{user `winrm_user`}}", - "winrm_password": "{{user `winrm_pass`}}", + "winrm_username": "{{ '{{user `winrm_user`}}' }}", + "winrm_password": "{{ '{{user `winrm_pass`}}' }}", "winrm_timeout": "30m", "shutdown_command": "shutdown /s /f /t 10", @@ -37,23 +37,30 @@ "boot_wait": "10s", "floppy_files": [ - "{{user `malboxes_cache_dir`}}/Autounattend.xml", - "{{user `malboxes_dir`}}/installconfig/windows10/enablewinrm.ps1" + "{{ cache_dir }}/Autounattend.xml", + "{{ dir }}/installconfig/windows10/enablewinrm.ps1" ] }], "post-processors": [{ "type": "vagrant", - "output": "boxes/{{user `name`}}.box", - "vagrantfile_template": "{{user `malboxes_dir`}}/vagrantfiles/box_win.rb" + "output": "boxes/{{ profile_name }}.box", + "vagrantfile_template": "{{ dir }}/vagrantfiles/box_win.rb" }], "provisioners": [{ "type": "powershell", "scripts": [ - "{{user `malboxes_dir`}}/scripts/windows/vmtools.ps1", - "{{user `malboxes_dir`}}/scripts/windows/malware_analysis.ps1", - "{{user `malboxes_dir`}}/scripts/windows/installtools.ps1" + "{{ dir }}/scripts/windows/vmtools.ps1", + "{{ dir }}/scripts/windows/malware_analysis.ps1", + "{{ dir }}/scripts/windows/installtools.ps1" ] - }] + } + {% if tools_path %}, + {% include 'snippets/tools.json' %} + {% endif %} + {% if ida_path %}, + {% include 'snippets/ida_remote_32.json' %} + {% endif %} + ] } diff --git a/malboxes/profiles/win10_64_analyst.json b/malboxes/profiles/win10_64_analyst.json index 0046f05..0970987 100644 --- a/malboxes/profiles/win10_64_analyst.json +++ b/malboxes/profiles/win10_64_analyst.json @@ -11,10 +11,9 @@ "guest_additions_mode": "attach", "headless": "false", "communicator": "winrm", - "vboxmanage": [ - ["modifyvm", "{{.Name}}", "--memory", "4096"], - ["modifyvm", "{{.Name}}", "--cpus", "1"] + ["modifyvm", "{{ '{{.Name}}' }}", "--memory", "4096"], + ["modifyvm", "{{ '{{.Name}}' }}", "--cpus", "1"] ], "disk_size": "15360", @@ -22,38 +21,46 @@ "output_directory": "builds", "iso_urls": [ - "file://{{ user `iso_path` }}/10240.16384.150709-1700.TH1_CLIENTENTERPRISEEVAL_OEMRET_X64FRE_EN-US.ISO", + "file://{{ iso_path }}/10240.16384.150709-1700.TH1_CLIENTENTERPRISEEVAL_OEMRET_X64FRE_EN-US.ISO", "http://care.dlservice.microsoft.com/dl/download/C/3/9/C399EEA8-135D-4207-92C9-6AAB3259F6EF/10240.16384.150709-1700.TH1_CLIENTENTERPRISEEVAL_OEMRET_X64FRE_EN-US.ISO" ], "iso_checksum": "56ab095075be28a90bc0b510835280975c6bb2ce", "iso_checksum_type": "sha1", - "winrm_username": "{{user `winrm_user`}}", - "winrm_password": "{{user `winrm_pass`}}", + "winrm_username": "{{ '{{user `winrm_user`}}' }}", + "winrm_password": "{{ '{{user `winrm_pass`}}' }}", "winrm_timeout": "30m", - + "shutdown_command": "shutdown /s /f /t 10", "boot_wait": "10s", "floppy_files": [ - "{{user `malboxes_cache_dir`}}/Autounattend.xml", - "{{user `malboxes_dir`}}/installconfig/windows10_64/enablewinrm.ps1" + "{{ cache_dir }}/Autounattend.xml", + "{{ dir }}/installconfig/windows10_64/enablewinrm.ps1" ] }], "post-processors": [{ "type": "vagrant", - "output": "boxes/{{user `name`}}.box", - "vagrantfile_template": "{{user `malboxes_dir`}}/vagrantfiles/box_win.rb" + "output": "boxes/{{ profile_name }}.box", + "vagrantfile_template": "{{ dir }}/vagrantfiles/box_win.rb" }], "provisioners": [{ "type": "powershell", "scripts": [ - "{{user `malboxes_dir`}}/scripts/windows/vmtools.ps1", - "{{user `malboxes_dir`}}/scripts/windows/malware_analysis.ps1", - "{{user `malboxes_dir`}}/scripts/windows/installtools.ps1" + "{{ dir }}/scripts/windows/vmtools.ps1", + "{{ dir }}/scripts/windows/malware_analysis.ps1", + "{{ dir }}/scripts/windows/installtools.ps1" ] - }] + } + {% if tools_path %}, + {% include 'snippets/tools.json' %} + {% endif %} + {% if ida_path %}, + {% include 'snippets/ida_remote_64.json' %}, + {% include 'snippets/ida_remote_32.json' %} + {% endif %} + ] } diff --git a/malboxes/profiles/win7_32_analyst.json b/malboxes/profiles/win7_32_analyst.json index d1a5ca2..1cb39fe 100644 --- a/malboxes/profiles/win7_32_analyst.json +++ b/malboxes/profiles/win7_32_analyst.json @@ -13,20 +13,20 @@ "communicator": "winrm", "vboxmanage": [ - ["modifyvm", "{{.Name}}", "--memory", "4096"], - ["modifyvm", "{{.Name}}", "--cpus", "1"] + ["modifyvm", "{{ '{{.Name}}' }}", "--memory", "4096"], + ["modifyvm", "{{ '{{.Name}}' }}", "--cpus", "1"] ], "disk_size": "15360", "output_directory": "builds", - "iso_url": "file://{{ user `iso_path` }}/en_windows_7_professional_with_sp1_x86_dvd_u_677056.iso", + "iso_url": "file://{{ iso_path }}/en_windows_7_professional_with_sp1_x86_dvd_u_677056.iso", "iso_checksum": "d89937df3a9bc2ec1a1486195fd308cd3dade928", "iso_checksum_type": "sha1", - "winrm_username": "{{user `winrm_user`}}", - "winrm_password": "{{user `winrm_pass`}}", + "winrm_username": "{{ '{{user `winrm_user`}}' }}", + "winrm_password": "{{ '{{user `winrm_pass`}}' }}", "winrm_timeout": "30m", "shutdown_command": "shutdown /s /f /t 10", @@ -34,23 +34,30 @@ "boot_wait": "10s", "floppy_files": [ - "{{user `malboxes_cache_dir`}}/Autounattend.xml", - "{{user `malboxes_dir`}}/installconfig/windows7/enablewinrm.ps1" + "{{ cache_dir }}/Autounattend.xml", + "{{ dir }}/installconfig/windows7/enablewinrm.ps1" ] }], "post-processors": [{ "type": "vagrant", - "output": "boxes/{{user `name`}}.box", - "vagrantfile_template": "{{user `malboxes_dir`}}/vagrantfiles/box_win.rb" + "output": "boxes/{{ profile_name }}.box", + "vagrantfile_template": "{{ dir }}/vagrantfiles/box_win.rb" }], "provisioners": [{ "type": "powershell", "scripts": [ - "{{user `malboxes_dir`}}/scripts/windows/vmtools.ps1", - "{{user `malboxes_dir`}}/scripts/windows/malware_analysis.ps1", - "{{user `malboxes_dir`}}/scripts/windows/installtools.ps1" + "{{ dir }}/scripts/windows/vmtools.ps1", + "{{ dir }}/scripts/windows/malware_analysis.ps1", + "{{ dir }}/scripts/windows/installtools.ps1" ] - }] + } + {% if tools_path %}, + {% include 'snippets/tools.json' %} + {% endif %} + {% if ida_path %}, + {% include 'snippets/ida_remote_32.json' %} + {% endif %} + ] } diff --git a/tests/profiles_json_valid.py b/tests/profiles_json_valid.py new file mode 100755 index 0000000..90cb187 --- /dev/null +++ b/tests/profiles_json_valid.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# +# This file is part of the Malboxes project. +# +# Malboxes - Vagrant box builder and config generator for malware analysis +# https://github.com/gosecure/malboxes +# +# Olivier Bilodeau +# Copyright (C) 2016 GoSecure Inc. +# All rights reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +import glob +import json +import os +import re +import sys + +from jinja2 import Environment, FileSystemLoader + +from malboxes.malboxes import load_config + +env = Environment(loader=FileSystemLoader('malboxes/profiles/'), + autoescape=False) +for profile in glob.glob("malboxes/profiles/*.json"): + print("Processing file {}".format(profile)) + + # process profile + profile_name = os.path.basename(profile) + config = load_config('config-example.js', + re.match('(.*).json$', profile_name).group(1)) + + try: + template = env.get_template(os.path.basename(profile_name)) + profile_json = template.render(config) # pylint: disable=no-member + print("Properly formatted Jinja2 template") + except: + print("Badly formatted Jinja2 template! Failing test...") + sys.exit(1) + + # test if json is valid + try: + json.loads(profile_json) + print("Properly formatted JSON file") + except: + print("Badly formatted JSON file! Failing test...") + sys.exit(2) diff --git a/tests/profiles_json_valid.sh b/tests/profiles_json_valid.sh deleted file mode 100755 index 189e710..0000000 --- a/tests/profiles_json_valid.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -DIRS="malboxes/profiles/*.json malboxes/profiles/snippets/*.json" - -for F in `ls $DIRS`; do - echo "Processing file $F"; - python3 -m json.tool $F 1>/dev/null - if [[ $? -ne 0 ]]; then - echo "Badly formatted JSON file! Failing test..."; - exit 1; - else - echo "Properly formatted JSON file"; - fi -done From a3e7a8a80d0e3f161afd07d1335a28381e3f4bbb Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Tue, 2 Aug 2016 15:33:11 -0400 Subject: [PATCH 10/17] Travis fix: Converted Python test to unittest framework --- Makefile | 2 +- tests/__init__.py | 0 tests/profiles_json_valid.py | 56 ----------------------------- tests/test_packer_templates.py | 65 ++++++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 57 deletions(-) create mode 100644 tests/__init__.py delete mode 100755 tests/profiles_json_valid.py create mode 100755 tests/test_packer_templates.py diff --git a/Makefile b/Makefile index f593adc..5415f31 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ test: pylint malboxes ./tests/config_example_valid.sh - ./tests/profiles_json_valid.py + python -m unittest discover pkg_clean: rm -r build/ dist/ malboxes.egg-info/ diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/profiles_json_valid.py b/tests/profiles_json_valid.py deleted file mode 100755 index 90cb187..0000000 --- a/tests/profiles_json_valid.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# -# This file is part of the Malboxes project. -# -# Malboxes - Vagrant box builder and config generator for malware analysis -# https://github.com/gosecure/malboxes -# -# Olivier Bilodeau -# Copyright (C) 2016 GoSecure Inc. -# All rights reserved. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -import glob -import json -import os -import re -import sys - -from jinja2 import Environment, FileSystemLoader - -from malboxes.malboxes import load_config - -env = Environment(loader=FileSystemLoader('malboxes/profiles/'), - autoescape=False) -for profile in glob.glob("malboxes/profiles/*.json"): - print("Processing file {}".format(profile)) - - # process profile - profile_name = os.path.basename(profile) - config = load_config('config-example.js', - re.match('(.*).json$', profile_name).group(1)) - - try: - template = env.get_template(os.path.basename(profile_name)) - profile_json = template.render(config) # pylint: disable=no-member - print("Properly formatted Jinja2 template") - except: - print("Badly formatted Jinja2 template! Failing test...") - sys.exit(1) - - # test if json is valid - try: - json.loads(profile_json) - print("Properly formatted JSON file") - except: - print("Badly formatted JSON file! Failing test...") - sys.exit(2) diff --git a/tests/test_packer_templates.py b/tests/test_packer_templates.py new file mode 100755 index 0000000..7386ca6 --- /dev/null +++ b/tests/test_packer_templates.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# +# This file is part of the Malboxes project. +# +# Malboxes - Vagrant box builder and config generator for malware analysis +# https://github.com/gosecure/malboxes +# +# Olivier Bilodeau +# Copyright (C) 2016 GoSecure Inc. +# All rights reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +import glob +import json +import os +import re +import sys +import unittest + +from jinja2 import Environment, FileSystemLoader + +from malboxes.malboxes import load_config + +class PackerTemplateTestCase(unittest.TestCase): + + def setUp(self): + self.env = Environment(loader=FileSystemLoader('malboxes/profiles/'), + autoescape=False) + + def test_packer_template_rendering(self): + for profile in glob.glob("malboxes/profiles/*.json"): + print("Processing file {}".format(profile)) + + # process profile + profile_name = os.path.basename(profile) + config = load_config('config-example.js', + re.match('(.*).json$', profile_name).group(1)) + + try: + template = self.env.get_template(os.path.basename(profile_name)) + profile_json = template.render(config) # pylint: disable=no-member + print("Properly formatted Jinja2 template") + except: + print("Badly formatted Jinja2 template! Failing test...") + raise() + + # test if json is valid + try: + json.loads(profile_json) + print("Properly formatted JSON file") + except: + print("Badly formatted JSON file! Failing test...") + raise() + +if __name__ == '__main__': + unittest.main() From 4fb5f88e686860084b8c6d5d2bb085638f156a0d Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Tue, 2 Aug 2016 16:04:53 -0400 Subject: [PATCH 11/17] Debug: More logging and don't remove temporary files --- malboxes/malboxes.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/malboxes/malboxes.py b/malboxes/malboxes.py index fe7690e..970e6ad 100755 --- a/malboxes/malboxes.py +++ b/malboxes/malboxes.py @@ -37,6 +37,7 @@ from malboxes._version import __version__ DIRS = AppDirs("malboxes") +DEBUG = False def initialize(): # create appdata directories if they don't exist @@ -244,12 +245,15 @@ def create_cachefd(filename): def cleanup(): - """Removes temporary files""" - for f in tempfiles: - os.remove(os.path.join(DIRS.user_cache_dir, f)) + """Removes temporary files. Keep them in debug mode.""" + if not DEBUG: + for f in tempfiles: + os.remove(os.path.join(DIRS.user_cache_dir, f)) def run_foreground(command): + if DEBUG: + print("DEBUG: Executing {}".format(command)) p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) try: @@ -293,7 +297,7 @@ def run_packer(packer_tmpl, args): f.close() flags = ['-var-file={}'.format(f.name)] - if args.debug: + if DEBUG: flags.append('-debug') cmd = [binary, 'build'] @@ -527,6 +531,8 @@ def document(parser, args): def main(): try: parser, args = initialize() + if args.debug: + DEBUG = True args.func(parser, args) finally: From 9c6478e17b858ad71964bacd482972f53576a13e Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Tue, 2 Aug 2016 16:33:30 -0400 Subject: [PATCH 12/17] Windows fix: packer generated template uses forward slashes on Windows --- malboxes/malboxes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/malboxes/malboxes.py b/malboxes/malboxes.py index 970e6ad..81325f9 100755 --- a/malboxes/malboxes.py +++ b/malboxes/malboxes.py @@ -227,8 +227,9 @@ def load_config(config_file, profile): config = json.loads(jsmin(f.read())) # add packer required variables - config['cache_dir'] = DIRS.user_cache_dir - config['dir'] = resource_filename(__name__, "") + # Note: Backslashes are replaced with forward slashes (Packer on Windows) + config['cache_dir'] = DIRS.user_cache_dir.replace('\\', '/') + config['dir'] = resource_filename(__name__, "").replace('\\', '/') config['profile_name'] = profile return config From 0bb0393fc110bbce010ab49209e701ab6ea7165a Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 3 Aug 2016 09:28:05 -0400 Subject: [PATCH 13/17] Added Windows path warning in example configuration --- config-example.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config-example.js b/config-example.js index c005df1..ef76ade 100644 --- a/config-example.js +++ b/config-example.js @@ -3,6 +3,9 @@ * Malboxes Example Configuration File * * Uncomment a specific section of the file to trigger a particular feature. + * + * Paths should be written using forward slashes even on Windows. + * For ex: C:/Tools */ // This allows you to use a local filestore for ISOs. From 7db6f0eb5f2922440c2370c3adc4cdfd0e4d20a5 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 3 Aug 2016 09:46:30 -0400 Subject: [PATCH 14/17] Fixed debug mode (DEBUG global value not retained) --- malboxes/malboxes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/malboxes/malboxes.py b/malboxes/malboxes.py index 81325f9..978e768 100755 --- a/malboxes/malboxes.py +++ b/malboxes/malboxes.py @@ -530,6 +530,7 @@ def document(parser, args): def main(): + global DEBUG try: parser, args = initialize() if args.debug: From e74695f8f0f41c9a7da85b0747686c34a1024806 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 3 Aug 2016 09:47:17 -0400 Subject: [PATCH 15/17] Fixed debug mode: Got rid of duplicated main() --- malboxes/__init__.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/malboxes/__init__.py b/malboxes/__init__.py index ad8cfb0..ad59432 100755 --- a/malboxes/__init__.py +++ b/malboxes/__init__.py @@ -17,14 +17,5 @@ # def main(): - from malboxes.malboxes import initialize, cleanup - try: - parser, args = initialize() - args.func(parser, args) - - finally: - cleanup() - - -if __name__ == "__main__": + from malboxes.malboxes import main main() From b20c21f84661678b08c51184760b0a304e5cd452 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 3 Aug 2016 09:58:36 -0400 Subject: [PATCH 16/17] Profile refactoring via jinja includes --- malboxes/malboxes.py | 3 +- .../snippets/builder_virtualbox_windows.json | 15 ++++++++++ .../snippets/postprocessor_vagrant.json | 5 ++++ malboxes/profiles/win10_32_analyst.json | 26 ++--------------- malboxes/profiles/win10_64_analyst.json | 27 ++---------------- malboxes/profiles/win7_32_analyst.json | 28 ++----------------- 6 files changed, 28 insertions(+), 76 deletions(-) create mode 100644 malboxes/profiles/snippets/builder_virtualbox_windows.json create mode 100644 malboxes/profiles/snippets/postprocessor_vagrant.json diff --git a/malboxes/malboxes.py b/malboxes/malboxes.py index 978e768..f06a2b0 100755 --- a/malboxes/malboxes.py +++ b/malboxes/malboxes.py @@ -175,7 +175,8 @@ def prepare_packer_template(config, template_name): sys.exit(2) filepath = resource_filename(__name__, 'profiles/') - env = Environment(loader=FileSystemLoader(filepath), autoescape=False) + env = Environment(loader=FileSystemLoader(filepath), autoescape=False, + trim_blocks=True, lstrip_blocks=True) template = env.get_template("{}.json".format(template_name)) # write to temporary file diff --git a/malboxes/profiles/snippets/builder_virtualbox_windows.json b/malboxes/profiles/snippets/builder_virtualbox_windows.json new file mode 100644 index 0000000..af38151 --- /dev/null +++ b/malboxes/profiles/snippets/builder_virtualbox_windows.json @@ -0,0 +1,15 @@ + "type": "virtualbox-iso", + "guest_additions_mode": "attach", + "headless": "false", + "communicator": "winrm", + "winrm_username": "{{ '{{user `winrm_user`}}' }}", + "winrm_password": "{{ '{{user `winrm_pass`}}' }}", + "winrm_timeout": "30m", + "shutdown_command": "shutdown /s /f /t 10", + "vboxmanage": [ + ["modifyvm", "{{ '{{.Name}}' }}", "--memory", "4096"], + ["modifyvm", "{{ '{{.Name}}' }}", "--cpus", "1"] + ], + "boot_wait": "10s", + "disk_size": "15360", + "output_directory": "builds" diff --git a/malboxes/profiles/snippets/postprocessor_vagrant.json b/malboxes/profiles/snippets/postprocessor_vagrant.json new file mode 100644 index 0000000..d174e6f --- /dev/null +++ b/malboxes/profiles/snippets/postprocessor_vagrant.json @@ -0,0 +1,5 @@ + "post-processors": [{ + "type": "vagrant", + "output": "boxes/{{ profile_name }}.box", + "vagrantfile_template": "{{ dir }}/vagrantfiles/box_win.rb" + }] diff --git a/malboxes/profiles/win10_32_analyst.json b/malboxes/profiles/win10_32_analyst.json index 002ab86..53157e3 100644 --- a/malboxes/profiles/win10_32_analyst.json +++ b/malboxes/profiles/win10_32_analyst.json @@ -6,20 +6,9 @@ }, "builders": [{ - "type": "virtualbox-iso", "guest_os_type": "Windows10", - "guest_additions_mode": "attach", - "headless": "false", - "communicator": "winrm", - "vboxmanage": [ - ["modifyvm", "{{ '{{.Name}}' }}", "--memory", "4096"], - ["modifyvm", "{{ '{{.Name}}' }}", "--cpus", "1"] - ], - - "disk_size": "15360", - - "output_directory": "builds", + {% include 'snippets/builder_virtualbox_windows.json' %}, "iso_urls": [ "file://{{ iso_path }}/10586.0.151029-1700.TH2_RELEASE_CLIENTENTERPRISEEVAL_OEMRET_X86FRE_EN-US.ISO", @@ -28,13 +17,6 @@ "iso_checksum": "e431a4e259a5c056a5d58b9cd0628a3c59f112f9", "iso_checksum_type": "sha1", - "winrm_username": "{{ '{{user `winrm_user`}}' }}", - "winrm_password": "{{ '{{user `winrm_pass`}}' }}", - "winrm_timeout": "30m", - - "shutdown_command": "shutdown /s /f /t 10", - - "boot_wait": "10s", "floppy_files": [ "{{ cache_dir }}/Autounattend.xml", @@ -42,11 +24,7 @@ ] }], - "post-processors": [{ - "type": "vagrant", - "output": "boxes/{{ profile_name }}.box", - "vagrantfile_template": "{{ dir }}/vagrantfiles/box_win.rb" - }], + {% include 'snippets/postprocessor_vagrant.json' %}, "provisioners": [{ "type": "powershell", diff --git a/malboxes/profiles/win10_64_analyst.json b/malboxes/profiles/win10_64_analyst.json index 0970987..6525d32 100644 --- a/malboxes/profiles/win10_64_analyst.json +++ b/malboxes/profiles/win10_64_analyst.json @@ -6,19 +6,8 @@ }, "builders": [{ - "type": "virtualbox-iso", "guest_os_type": "Windows10_64", - "guest_additions_mode": "attach", - "headless": "false", - "communicator": "winrm", - "vboxmanage": [ - ["modifyvm", "{{ '{{.Name}}' }}", "--memory", "4096"], - ["modifyvm", "{{ '{{.Name}}' }}", "--cpus", "1"] - ], - - "disk_size": "15360", - - "output_directory": "builds", + {% include 'snippets/builder_virtualbox_windows.json' %}, "iso_urls": [ "file://{{ iso_path }}/10240.16384.150709-1700.TH1_CLIENTENTERPRISEEVAL_OEMRET_X64FRE_EN-US.ISO", @@ -27,25 +16,13 @@ "iso_checksum": "56ab095075be28a90bc0b510835280975c6bb2ce", "iso_checksum_type": "sha1", - "winrm_username": "{{ '{{user `winrm_user`}}' }}", - "winrm_password": "{{ '{{user `winrm_pass`}}' }}", - "winrm_timeout": "30m", - - "shutdown_command": "shutdown /s /f /t 10", - - "boot_wait": "10s", - "floppy_files": [ "{{ cache_dir }}/Autounattend.xml", "{{ dir }}/installconfig/windows10_64/enablewinrm.ps1" ] }], - "post-processors": [{ - "type": "vagrant", - "output": "boxes/{{ profile_name }}.box", - "vagrantfile_template": "{{ dir }}/vagrantfiles/box_win.rb" - }], + {% include 'snippets/postprocessor_vagrant.json' %}, "provisioners": [{ "type": "powershell", diff --git a/malboxes/profiles/win7_32_analyst.json b/malboxes/profiles/win7_32_analyst.json index 1cb39fe..a9e2c5a 100644 --- a/malboxes/profiles/win7_32_analyst.json +++ b/malboxes/profiles/win7_32_analyst.json @@ -6,44 +6,20 @@ }, "builders": [{ - "type": "virtualbox-iso", "guest_os_type": "Windows7", - "guest_additions_mode": "attach", - "headless": "false", - "communicator": "winrm", - - "vboxmanage": [ - ["modifyvm", "{{ '{{.Name}}' }}", "--memory", "4096"], - ["modifyvm", "{{ '{{.Name}}' }}", "--cpus", "1"] - ], - - "disk_size": "15360", - - "output_directory": "builds", + {% include 'snippets/builder_virtualbox_windows.json' %}, "iso_url": "file://{{ iso_path }}/en_windows_7_professional_with_sp1_x86_dvd_u_677056.iso", "iso_checksum": "d89937df3a9bc2ec1a1486195fd308cd3dade928", "iso_checksum_type": "sha1", - "winrm_username": "{{ '{{user `winrm_user`}}' }}", - "winrm_password": "{{ '{{user `winrm_pass`}}' }}", - "winrm_timeout": "30m", - - "shutdown_command": "shutdown /s /f /t 10", - - "boot_wait": "10s", - "floppy_files": [ "{{ cache_dir }}/Autounattend.xml", "{{ dir }}/installconfig/windows7/enablewinrm.ps1" ] }], - "post-processors": [{ - "type": "vagrant", - "output": "boxes/{{ profile_name }}.box", - "vagrantfile_template": "{{ dir }}/vagrantfiles/box_win.rb" - }], + {% include 'snippets/postprocessor_vagrant.json' %}, "provisioners": [{ "type": "powershell", From 68ed93693620da6c5ec1313a1e202c120493291f Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 3 Aug 2016 11:35:14 -0400 Subject: [PATCH 17/17] Fixup for bug in `spin` introduced in 68a32dda8 --- malboxes/malboxes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/malboxes/malboxes.py b/malboxes/malboxes.py index f06a2b0..e66dc54 100755 --- a/malboxes/malboxes.py +++ b/malboxes/malboxes.py @@ -394,7 +394,7 @@ def spin(parser, args): """ Creates a Vagrantfile based on a template using the jinja2 engine """ - config = prepare_config(args.profile) + config, _ = prepare_config(args.profile) print("Creating a Vagrantfile") filepath = resource_filename(__name__, "vagrantfiles/")