From 4022c9cad7cf06f4c5ee5ea9b27897c79c63e9c4 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 6 Jul 2016 16:10:53 -0400 Subject: [PATCH 01/21] moved doc/ to docs/ following python convention --- config-example.json | 2 +- {doc => docs}/Autounattend-fixing.adoc | 0 {doc => docs}/windows-licenses.adoc | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename {doc => docs}/Autounattend-fixing.adoc (100%) rename {doc => docs}/windows-licenses.adoc (100%) diff --git a/config-example.json b/config-example.json index e29fc49..e40d77a 100644 --- a/config-example.json +++ b/config-example.json @@ -1,7 +1,7 @@ { "iso_path": "/path/to/your/windows/isos/", "_comment": "If using a registered product update the product_key and set trial to 'false'.", - "_comment": "See doc/windows-licenses.adoc for more information.", + "_comment": "See docs/windows-licenses.adoc for more information.", "trial": "true", "product_key": "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX", "username": "vagrant", diff --git a/doc/Autounattend-fixing.adoc b/docs/Autounattend-fixing.adoc similarity index 100% rename from doc/Autounattend-fixing.adoc rename to docs/Autounattend-fixing.adoc diff --git a/doc/windows-licenses.adoc b/docs/windows-licenses.adoc similarity index 100% rename from doc/windows-licenses.adoc rename to docs/windows-licenses.adoc From 592a67d885b79b183f920808898bbe586cff9b56 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 6 Jul 2016 16:14:05 -0400 Subject: [PATCH 02/21] Clarified documentation licensing (CC-BY-SA 4.0) --- README.adoc | 6 ++++++ docs/LICENSE | 1 + 2 files changed, 7 insertions(+) create mode 100644 docs/LICENSE diff --git a/README.adoc b/README.adoc index c609bce..53d6fba 100644 --- a/README.adoc +++ b/README.adoc @@ -99,6 +99,12 @@ by link:{twob}[Olivier Bilodeau] and link:{twhg}[Hugo Genesse] (PDF, degraded) * Video (coming soon) +== License + +Code is licensed under the GPLv3+, see `LICENSE` for details. Documentation +and presentation material is licensed under the Creative Commons +Attribution-ShareAlike 4.0, see `docs/LICENSE` for details. + == Credits After I had the idea for an improved malware analyst workflow based on what diff --git a/docs/LICENSE b/docs/LICENSE new file mode 100644 index 0000000..e711509 --- /dev/null +++ b/docs/LICENSE @@ -0,0 +1 @@ +TODO: https://creativecommons.org/licenses/by-sa/4.0/ From 63343c2e8201ec6d8ce8ff1fff1fcf703b7d6bb3 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 6 Jul 2016 17:02:13 -0400 Subject: [PATCH 03/21] pip: all mandatory files are present under site-package/ --- .gitignore | 5 + MANIFEST.in | 7 + Makefile | 5 +- README.adoc | 4 + TODO.adoc | 13 ++ malboxes.py => malboxes/__init__.py | 11 +- .../installconfig}/debian/preseed.cfg | 0 .../installconfig}/windows10/Autounattend.xml | 0 .../installconfig}/windows10/enablewinrm.ps1 | 0 .../windows10_64/Autounattend.xml | 0 .../windows10_64/enablewinrm.ps1 | 0 .../installconfig}/windows7/Autounattend.xml | 0 .../installconfig}/windows7/enablewinrm.ps1 | 0 .../windows_7x64/Autounattend.xml | 0 .../windows_7x64/enablewinrm.ps1 | 0 .../profiles}/win10_32_analyst.json | 0 .../profiles}/win10_64_analyst.json | 0 .../profiles}/win7_32_analyst.json | 0 .../scripts}/common/vagrantkey.sh | 0 .../scripts}/debian/cleanup.sh | 0 .../scripts}/debian/installtools.sh | 0 .../scripts}/debian/setnetwork.sh | 0 .../scripts}/debian/update.sh | 0 .../scripts}/debian/vmtools.sh | 0 .../scripts}/windows/enablerdp.ps1 | 0 .../scripts}/windows/installtools.ps1 | 0 .../scripts}/windows/vmtools.ps1 | 0 .../vagrantfiles}/analyst_single.rb | 0 .../vagrantfiles}/box_win.rb | 0 .../vagrantfiles}/debian-8.2.0-amd64.rb | 0 .../vagrantfiles}/windows-7x64.rb | 0 setup.py | 124 ++++++++++++++++++ 32 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 MANIFEST.in rename malboxes.py => malboxes/__init__.py (99%) rename {installconfig => malboxes/installconfig}/debian/preseed.cfg (100%) rename {installconfig => malboxes/installconfig}/windows10/Autounattend.xml (100%) rename {installconfig => malboxes/installconfig}/windows10/enablewinrm.ps1 (100%) rename {installconfig => malboxes/installconfig}/windows10_64/Autounattend.xml (100%) rename {installconfig => malboxes/installconfig}/windows10_64/enablewinrm.ps1 (100%) rename {installconfig => malboxes/installconfig}/windows7/Autounattend.xml (100%) rename {installconfig => malboxes/installconfig}/windows7/enablewinrm.ps1 (100%) rename {installconfig => malboxes/installconfig}/windows_7x64/Autounattend.xml (100%) rename {installconfig => malboxes/installconfig}/windows_7x64/enablewinrm.ps1 (100%) rename {profiles => malboxes/profiles}/win10_32_analyst.json (100%) rename {profiles => malboxes/profiles}/win10_64_analyst.json (100%) rename {profiles => malboxes/profiles}/win7_32_analyst.json (100%) rename {scripts => malboxes/scripts}/common/vagrantkey.sh (100%) rename {scripts => malboxes/scripts}/debian/cleanup.sh (100%) rename {scripts => malboxes/scripts}/debian/installtools.sh (100%) rename {scripts => malboxes/scripts}/debian/setnetwork.sh (100%) rename {scripts => malboxes/scripts}/debian/update.sh (100%) rename {scripts => malboxes/scripts}/debian/vmtools.sh (100%) rename {scripts => malboxes/scripts}/windows/enablerdp.ps1 (100%) rename {scripts => malboxes/scripts}/windows/installtools.ps1 (100%) rename {scripts => malboxes/scripts}/windows/vmtools.ps1 (100%) rename {vagrantfiles => malboxes/vagrantfiles}/analyst_single.rb (100%) rename {vagrantfiles => malboxes/vagrantfiles}/box_win.rb (100%) rename {vagrantfiles => malboxes/vagrantfiles}/debian-8.2.0-amd64.rb (100%) rename {vagrantfiles => malboxes/vagrantfiles}/windows-7x64.rb (100%) create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index b15ac10..f9c9a36 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,11 @@ builds/ # Python __pycache__/ +# Python pip/setuptools packaging +build/ +dist/ +*.egg-info/ + # Generic *~ *.swp diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..0e1ad3b --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +include LICENSE README.adoc TODO.adoc + +# Include the data files +include config-example.json +graft docs +prune docs/presentation/ +graft malboxes diff --git a/Makefile b/Makefile index 33f35b1..69e10ed 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,5 @@ test: - pylint malboxes.py + pylint malboxes + +pkg_clean: + rm -r build/ dist/ malboxes.egg-info/ diff --git a/README.adoc b/README.adoc index 53d6fba..baf4ae6 100644 --- a/README.adoc +++ b/README.adoc @@ -25,6 +25,10 @@ https://github.com/gosecure/malboxes * packer (sometimes called packer-io) * vagrant +== Installation + + pip install git+https://github.com/GoSecure/malboxes.git@pip-packaging#egg=malboxes + == Usage === Box creation diff --git a/TODO.adoc b/TODO.adoc index 4e38318..2617dac 100644 --- a/TODO.adoc +++ b/TODO.adoc @@ -1,5 +1,18 @@ = TODO +== pip packaging + +* Needs to port code to use pkg resource API + +See: +* https://setuptools.readthedocs.io/en/latest/setuptools.html#accessing-data-files-at-runtime +* http://stackoverflow.com/a/26533921 + +* Push caches into .local/share/ (and Windows equivalent) - avoids permissions issues +* Allow config overrides in .config/malboxes (and Windows equivalent) + +== Misc + * Make work with trial ISOs == Minimal malware analyst use case diff --git a/malboxes.py b/malboxes/__init__.py similarity index 99% rename from malboxes.py rename to malboxes/__init__.py index 5cc3c7c..8324ae8 100755 --- a/malboxes.py +++ b/malboxes/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 -# # Malboxes - Vagrant box builder and config generator for malware analysis # https://github.com/gosecure/malboxes # @@ -31,7 +29,7 @@ from jinja2 import Environment, FileSystemLoader CONFIG_CACHE = 'config_cache' - +__version__ = "0.1.0" def initialize(): parser = argparse.ArgumentParser( @@ -395,11 +393,14 @@ def document(parser, args): """ Adds the modified script to the user scripts.""" add_to_user_scripts(args.profile) - -if __name__ == "__main__": +def main(): try: parser, args = initialize() args.func(parser, args) finally: cleanup() + + +if __name__ == "__main__": + main() diff --git a/installconfig/debian/preseed.cfg b/malboxes/installconfig/debian/preseed.cfg similarity index 100% rename from installconfig/debian/preseed.cfg rename to malboxes/installconfig/debian/preseed.cfg diff --git a/installconfig/windows10/Autounattend.xml b/malboxes/installconfig/windows10/Autounattend.xml similarity index 100% rename from installconfig/windows10/Autounattend.xml rename to malboxes/installconfig/windows10/Autounattend.xml diff --git a/installconfig/windows10/enablewinrm.ps1 b/malboxes/installconfig/windows10/enablewinrm.ps1 similarity index 100% rename from installconfig/windows10/enablewinrm.ps1 rename to malboxes/installconfig/windows10/enablewinrm.ps1 diff --git a/installconfig/windows10_64/Autounattend.xml b/malboxes/installconfig/windows10_64/Autounattend.xml similarity index 100% rename from installconfig/windows10_64/Autounattend.xml rename to malboxes/installconfig/windows10_64/Autounattend.xml diff --git a/installconfig/windows10_64/enablewinrm.ps1 b/malboxes/installconfig/windows10_64/enablewinrm.ps1 similarity index 100% rename from installconfig/windows10_64/enablewinrm.ps1 rename to malboxes/installconfig/windows10_64/enablewinrm.ps1 diff --git a/installconfig/windows7/Autounattend.xml b/malboxes/installconfig/windows7/Autounattend.xml similarity index 100% rename from installconfig/windows7/Autounattend.xml rename to malboxes/installconfig/windows7/Autounattend.xml diff --git a/installconfig/windows7/enablewinrm.ps1 b/malboxes/installconfig/windows7/enablewinrm.ps1 similarity index 100% rename from installconfig/windows7/enablewinrm.ps1 rename to malboxes/installconfig/windows7/enablewinrm.ps1 diff --git a/installconfig/windows_7x64/Autounattend.xml b/malboxes/installconfig/windows_7x64/Autounattend.xml similarity index 100% rename from installconfig/windows_7x64/Autounattend.xml rename to malboxes/installconfig/windows_7x64/Autounattend.xml diff --git a/installconfig/windows_7x64/enablewinrm.ps1 b/malboxes/installconfig/windows_7x64/enablewinrm.ps1 similarity index 100% rename from installconfig/windows_7x64/enablewinrm.ps1 rename to malboxes/installconfig/windows_7x64/enablewinrm.ps1 diff --git a/profiles/win10_32_analyst.json b/malboxes/profiles/win10_32_analyst.json similarity index 100% rename from profiles/win10_32_analyst.json rename to malboxes/profiles/win10_32_analyst.json diff --git a/profiles/win10_64_analyst.json b/malboxes/profiles/win10_64_analyst.json similarity index 100% rename from profiles/win10_64_analyst.json rename to malboxes/profiles/win10_64_analyst.json diff --git a/profiles/win7_32_analyst.json b/malboxes/profiles/win7_32_analyst.json similarity index 100% rename from profiles/win7_32_analyst.json rename to malboxes/profiles/win7_32_analyst.json diff --git a/scripts/common/vagrantkey.sh b/malboxes/scripts/common/vagrantkey.sh similarity index 100% rename from scripts/common/vagrantkey.sh rename to malboxes/scripts/common/vagrantkey.sh diff --git a/scripts/debian/cleanup.sh b/malboxes/scripts/debian/cleanup.sh similarity index 100% rename from scripts/debian/cleanup.sh rename to malboxes/scripts/debian/cleanup.sh diff --git a/scripts/debian/installtools.sh b/malboxes/scripts/debian/installtools.sh similarity index 100% rename from scripts/debian/installtools.sh rename to malboxes/scripts/debian/installtools.sh diff --git a/scripts/debian/setnetwork.sh b/malboxes/scripts/debian/setnetwork.sh similarity index 100% rename from scripts/debian/setnetwork.sh rename to malboxes/scripts/debian/setnetwork.sh diff --git a/scripts/debian/update.sh b/malboxes/scripts/debian/update.sh similarity index 100% rename from scripts/debian/update.sh rename to malboxes/scripts/debian/update.sh diff --git a/scripts/debian/vmtools.sh b/malboxes/scripts/debian/vmtools.sh similarity index 100% rename from scripts/debian/vmtools.sh rename to malboxes/scripts/debian/vmtools.sh diff --git a/scripts/windows/enablerdp.ps1 b/malboxes/scripts/windows/enablerdp.ps1 similarity index 100% rename from scripts/windows/enablerdp.ps1 rename to malboxes/scripts/windows/enablerdp.ps1 diff --git a/scripts/windows/installtools.ps1 b/malboxes/scripts/windows/installtools.ps1 similarity index 100% rename from scripts/windows/installtools.ps1 rename to malboxes/scripts/windows/installtools.ps1 diff --git a/scripts/windows/vmtools.ps1 b/malboxes/scripts/windows/vmtools.ps1 similarity index 100% rename from scripts/windows/vmtools.ps1 rename to malboxes/scripts/windows/vmtools.ps1 diff --git a/vagrantfiles/analyst_single.rb b/malboxes/vagrantfiles/analyst_single.rb similarity index 100% rename from vagrantfiles/analyst_single.rb rename to malboxes/vagrantfiles/analyst_single.rb diff --git a/vagrantfiles/box_win.rb b/malboxes/vagrantfiles/box_win.rb similarity index 100% rename from vagrantfiles/box_win.rb rename to malboxes/vagrantfiles/box_win.rb diff --git a/vagrantfiles/debian-8.2.0-amd64.rb b/malboxes/vagrantfiles/debian-8.2.0-amd64.rb similarity index 100% rename from vagrantfiles/debian-8.2.0-amd64.rb rename to malboxes/vagrantfiles/debian-8.2.0-amd64.rb diff --git a/vagrantfiles/windows-7x64.rb b/malboxes/vagrantfiles/windows-7x64.rb similarity index 100% rename from vagrantfiles/windows-7x64.rb rename to malboxes/vagrantfiles/windows-7x64.rb diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..63a1ddd --- /dev/null +++ b/setup.py @@ -0,0 +1,124 @@ +"""pip/setuptools packaging + +Based off https://github.com/pypa/sampleproject/blob/master/setup.py +""" + +# Always prefer setuptools over distutils +from setuptools import setup, find_packages +# To use a consistent encoding +from codecs import open +from os import path, remove +import shutil + +from malboxes import __version__ + +here = path.abspath(path.dirname(__file__)) + +_tempfiles = [] +def _prepare(): + """Preparing files for package""" + # Here are files I want packaged but also in top-level directory as it is mostly + # 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'] + + for f in root_data_files: + _tempfiles.append(shutil.copy(path.join(here, f), + path.join(here, 'malboxes'))) + + # docs + shutil.copytree(path.join(here, 'docs'), path.join(here, 'malboxes/docs'), + ignore=shutil.ignore_patterns('presentation')) + +def _teardown(): + """Removing temporary files""" + + for f in _tempfiles: + remove(path.join(here, f)) + + shutil.rmtree(path.join(here, 'malboxes/docs')) + +# Get the long description from the README file +# TODO process README to make it pure plaintext +with open(path.join(here, 'README.adoc'), encoding='utf-8') as f: + long_description = f.read() + +_prepare() + +setup( + name='malboxes', + version=__version__, + + description='Build Malware VMs (boxes) or whole environments based on ' + 'templates. Useful for analysts, sandboxes or honeypots. ' + 'Leverages devops workflow with Vagrant and Packer.', + long_description=long_description, + + url='https://github.com/gosecure/malboxes', + + author='Malboxes Team', + author_email='obilodeau@gosecure.ca', + + license='GPLv3+', + + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + 'Development Status :: 4 - Beta', + + # Indicate who your project is intended for + 'Intended Audience :: Information Technology', + 'Topic :: Security', + + # Pick your license as you wish (should match "license" above) + 'License :: OSI Approved :: GNU General Public License v3 or later ' + '(GPLv3+)', + + # Specify the Python versions you support here. In particular, ensure + # that you indicate whether you support Python 2, Python 3 or both. + #'Programming Language :: Python :: 2', + #'Programming Language :: Python :: 2.6', + #'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + ], + + # What does your project relate to? + keywords='virtual-machine malware reverse-engineering vagrant packer', + + # Once we have more code we'll migrate to a package and use find_packages() + packages=find_packages(exclude=['contrib', 'docs', 'tests']), + + # List run-time dependencies here. These will be installed by pip when + # 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=['Jinja2'], + + # List additional groups of dependencies here (e.g. development + # dependencies). You can install these using the following syntax, + # for example: + # $ pip install -e .[dev,test] + #extras_require={ + # 'dev': ['check-manifest'], + # 'test': ['coverage'], + #}, + + include_package_data = True, + zip_safe = False, + + # install malboxes executable + entry_points={ + 'console_scripts': [ + 'malboxes=malboxes:main', + ], + }, +) + +_teardown() From 0e650937b2ccde8012b553baa02917ae6373794d Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Tue, 12 Jul 2016 16:35:02 -0400 Subject: [PATCH 04/21] Docs: How to do development while using installed malboxes --- docs/devel.adoc | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/devel.adoc diff --git a/docs/devel.adoc b/docs/devel.adoc new file mode 100644 index 0000000..5af9469 --- /dev/null +++ b/docs/devel.adoc @@ -0,0 +1,7 @@ += Development guide + +== Install malboxes in development mode + +From the project's git repository root directory, execute: + + sudo pip install -e . --no-deps From b09a1e5aaf260a8335e83ccec27843f3b60e1765 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Tue, 12 Jul 2016 16:35:59 -0400 Subject: [PATCH 05/21] pip package: First conversion to pkg_resource to see if it's ok with git --- README.adoc | 2 +- malboxes/__init__.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.adoc b/README.adoc index baf4ae6..c3f70f8 100644 --- a/README.adoc +++ b/README.adoc @@ -27,7 +27,7 @@ https://github.com/gosecure/malboxes == Installation - pip install git+https://github.com/GoSecure/malboxes.git@pip-packaging#egg=malboxes + sudo pip install git+https://github.com/GoSecure/malboxes.git@pip-packaging#egg=malboxes == Usage diff --git a/malboxes/__init__.py b/malboxes/__init__.py index 8324ae8..9b63ead 100755 --- a/malboxes/__init__.py +++ b/malboxes/__init__.py @@ -21,6 +21,7 @@ import glob import json import os +from pkg_resources import resource_filename import re import signal import subprocess @@ -227,8 +228,10 @@ def default(parser, args): def list_profiles(parser, args): print("supported profiles:\n") - for f in sorted(glob.glob(os.path.join('profiles', '*.json'))): - m = re.search(r'^profiles[\/\\](.*).json$', f) + + filepath = resource_filename(__name__, "profiles/") + for f in sorted(glob.glob(os.path.join(filepath, '*.json'))): + m = re.search(r'profiles[\/\\](.*).json$', f) print(m.group(1)) print() From e7d6b6151c3adf018c65d73d8771f829db140291 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 13 Jul 2016 10:30:31 -0400 Subject: [PATCH 06/21] Missing space in help string --- malboxes/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/malboxes/__init__.py b/malboxes/__init__.py index 9b63ead..00bc0f1 100755 --- a/malboxes/__init__.py +++ b/malboxes/__init__.py @@ -54,7 +54,7 @@ def initialize(): # spin command parser_spin = subparsers.add_parser('spin', - help="Creates a Vagrantfile for" + help="Creates a Vagrantfile for " "your profile / Vagrant box.") parser_spin.add_argument('profile', help='Name of the profile to spin.') parser_spin.add_argument('name', help='Name of the target VM. ' From b40f2864058634e7293f23a8532e228e999e9b28 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 13 Jul 2016 12:31:05 -0400 Subject: [PATCH 07/21] README: consistency fixes --- README.adoc | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/README.adoc b/README.adoc index c3f70f8..4f51246 100644 --- a/README.adoc +++ b/README.adoc @@ -73,21 +73,27 @@ You can modify (add, modify or delete) registry keys, directories and files like Registry keys: - ./malboxes.py registry profile modtype key name value valuetype + ./malboxes.py registry - Ex: ./malboxes registry win10_64_analyst add HKCU:\Software Malboxes IsAwesome String +Example: + + ./malboxes registry win10_64_analyst add HKCU:\Software "Malboxes IsAwesome" String Directories and files: - ./malboxes.py directory profile modtype dirpath + ./malboxes.py directory + +Example: - Ex: ./malboxes.py directory BadAPT57 delete C:\Windows\System32 + ./malboxes.py directory BadAPT57 delete C:\Windows\System32 You can add packages to install that are specific to the profile: - ./malboxes.py package profile package + ./malboxes.py package + +Example: - Ex: ./malboxes.py package RansomwareThatINeedRevengeOn chrome + ./malboxes.py package RansomwareThatINeedRevengeOn chrome == More information From e690cf2d1f3653c5a82ea6f4f7183b2ab8488033 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 13 Jul 2016 12:32:39 -0400 Subject: [PATCH 08/21] build and spin are now using default system paths for all platforms For example: * Config files in ~/.config/malboxes/ * Temporary cache files in ~/.cache/malboxes/ --- README.adoc | 33 ++++++++++++++-------- malboxes/__init__.py | 66 ++++++++++++++++++++++++++++---------------- requirements.txt | 1 + setup.py | 2 +- 4 files changed, 65 insertions(+), 37 deletions(-) diff --git a/README.adoc b/README.adoc index 4f51246..70ed9f9 100644 --- a/README.adoc +++ b/README.adoc @@ -21,6 +21,7 @@ https://github.com/gosecure/malboxes == Requirements * Python 3 +* appdirs * jinja2 * packer (sometimes called packer-io) * vagrant @@ -33,24 +34,32 @@ https://github.com/gosecure/malboxes === Box creation -Copy `config-example.json` to `config.json`. Modify it and run: +Run: - ./malboxes.py build + malboxes build You can also list all supported profiles with: - ./malboxes.py list + malboxes list This will build a Vagrant box ready for malware investigation you can now include it in a Vagrantfile afterwards. For example: - ./malboxes.py build win10_64_analyst + malboxes build win10_64_analyst + +If you want to customize your configuration, look at the following location +for a `config.json` file: + +* Linux/Unix: `~/.config/malboxes/` +* Mac OS X: `~/Library/Application Support/malboxes/` +* Win 7+: `C:\Users\\AppData\Local\malboxes\malboxes\` + === Per analysis instances - ./malboxes.py spin win10_64_analyst + malboxes spin win10_64_analyst This will create a `Vagrantfile` prepared to use for malware analysis. Move it into the analysis folder of your choice and issue: @@ -63,7 +72,7 @@ shared in the VM. This can be changed by commenting the relevant part of the For example: - ./malboxes.py spin win7_32_analyst 20160519.cryptolocker.xyz + malboxes spin win7_32_analyst 20160519.cryptolocker.xyz // FIXME @@ -73,27 +82,27 @@ You can modify (add, modify or delete) registry keys, directories and files like Registry keys: - ./malboxes.py registry + malboxes registry Example: - ./malboxes registry win10_64_analyst add HKCU:\Software "Malboxes IsAwesome" String + malboxes registry win10_64_analyst add HKCU:\Software "Malboxes IsAwesome" String Directories and files: - ./malboxes.py directory + malboxes directory Example: - ./malboxes.py directory BadAPT57 delete C:\Windows\System32 + malboxes directory BadAPT57 delete C:\Windows\System32 You can add packages to install that are specific to the profile: - ./malboxes.py package + malboxes package Example: - ./malboxes.py package RansomwareThatINeedRevengeOn chrome + malboxes package RansomwareThatINeedRevengeOn chrome == More information diff --git a/malboxes/__init__.py b/malboxes/__init__.py index 00bc0f1..316f15b 100755 --- a/malboxes/__init__.py +++ b/malboxes/__init__.py @@ -19,20 +19,33 @@ # import argparse import glob +from io import TextIOWrapper import json import os -from pkg_resources import resource_filename +from pkg_resources import resource_filename, resource_stream import re +import shutil import signal import subprocess import sys +from appdirs import AppDirs from jinja2 import Environment, FileSystemLoader -CONFIG_CACHE = 'config_cache' +DIRS = AppDirs("malboxes") __version__ = "0.1.0" def initialize(): + # create appdata directories if they don't exist + if not os.path.exists(DIRS.user_config_dir): + os.mkdir(DIRS.user_config_dir) + + if not os.path.exists(DIRS.user_cache_dir): + os.mkdir(DIRS.user_cache_dir) + + return init_parser() + +def init_parser(): parser = argparse.ArgumentParser( description="Vagrant box builder " "and config generator for malware analysis.") @@ -126,10 +139,10 @@ def prepare_autounattend(config): # os type is extracted from profile json os_type = config['builders'][0]['guest_os_type'].lower() - # Jinja2 splits on '/' so dont change for os.path.join - env = Environment(loader=FileSystemLoader('installconfig/')) + filepath = resource_filename(__name__, "installconfig/") + env = Environment(loader=FileSystemLoader(filepath)) template = env.get_template("{}/Autounattend.xml".format(os_type)) - f = create_configfd('Autounattend.xml') + f = create_cachefd('Autounattend.xml') f.write(template.render(config)) # pylint: disable=no-member f.close() @@ -138,39 +151,42 @@ def load_config(profile): """ Config is in JSON since we can re-use the same in both malboxes and packer """ - profile_file = os.path.join('profiles', '{}.json'.format(profile)) - # validate if profile is present - if not os.path.isfile(profile_file): - print("Profile doesn't exist") + 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.json') + 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'), + config_file) + # load general config config = {} - with open('config.json', 'r') as f: + with open(config_file, 'r') as f: config = json.load(f) # merge/update with profile config - with open(profile_file, 'r') as f: - config.update(json.load(f)) + config.update(json.load(TextIOWrapper(profile_fd))) return config tempfiles = [] -def create_configfd(filename): - try: - os.mkdir(CONFIG_CACHE) - except FileExistsError: - pass - +def create_cachefd(filename): tempfiles.append(filename) - return open(CONFIG_CACHE + filename, 'w') + return open(os.path.join(DIRS.user_cache_dir, filename), 'w') def cleanup(): """Removes temporary files""" for f in tempfiles: - os.remove(CONFIG_CACHE + f) + os.remove(os.path.join(DIRS.user_cache_dir, f)) def run_foreground(command): @@ -180,7 +196,7 @@ def run_foreground(command): for line in iter(p.stdout.readline, b''): print(line.rstrip().decode('utf-8')) - # send Ctrl-C to packer + # send Ctrl-C to subprocess except KeyboardInterrupt: p.send_signal(signal.SIGINT) for line in iter(p.stdout.readline, b''): @@ -242,8 +258,9 @@ def build(parser, args): print("Generating configuration files...") prepare_autounattend(config) print("Configuration files are ready") - ret = run_packer(os.path.join('profiles', - '{}.json'.format(args.profile))) + ret = run_packer(resource_filename(__name__, + "profiles/{}.json".format(args.profile))) + if ret != 0: print("Packer failed. Build failed. Exiting...") sys.exit(3) @@ -266,7 +283,8 @@ def spin(parser, args): config = load_config(args.profile) print("Creating a Vagrantfile") - env = Environment(loader=FileSystemLoader('vagrantfiles')) + filepath = resource_filename(__name__, "vagrantfiles/") + env = Environment(loader=FileSystemLoader(filepath)) template = env.get_template("analyst_single.rb") if os.path.isfile('Vagrantfile'): diff --git a/requirements.txt b/requirements.txt index 8ce973e..68e55ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ +appdirs Jinja2 diff --git a/setup.py b/setup.py index 63a1ddd..15250fb 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=['Jinja2'], + install_requires=['appdirs', 'Jinja2'], # List additional groups of dependencies here (e.g. development # dependencies). You can install these using the following syntax, From 81f80c3529c1acb5f739584a1ab303f1a60cc5a4 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 13 Jul 2016 12:35:11 -0400 Subject: [PATCH 09/21] Added generated documentation to .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index f9c9a36..4e9e388 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ dist/ # Generic *~ *.swp + +# Documentation artifacts +README.html +docs/*.html From 3d1edba5c0257b5a0ffc7e0a02df9827b788bb1c Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 13 Jul 2016 12:43:49 -0400 Subject: [PATCH 10/21] converted customization commands to user config directory --- malboxes/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/malboxes/__init__.py b/malboxes/__init__.py index 316f15b..ab0e137 100755 --- a/malboxes/__init__.py +++ b/malboxes/__init__.py @@ -309,7 +309,8 @@ def append_to_script(filename, line): def add_to_user_scripts(profile): """ Adds the modified script to the user scripts file.""" """ File names for the user scripts file and the script to be added.""" - filename = os.path.join("scripts", "windows", "user_scripts.ps1") + filename = os.path.join(DIRS.user_config_dir, "scripts", "windows", + "user_scripts.ps1") line = "{}.ps1".format(profile) """ Check content of the user scripts file.""" @@ -345,7 +346,7 @@ def reg(parser, args): print("Registry modification type invalid.") print("Valid ones are: add, delete and modify.") - filename = os.path.join("scripts", "user", + filename = os.path.join(DIRS.user_config_dir, "scripts", "user", "windows", "{}.ps1".format(args.profile)) append_to_script(filename, line) @@ -369,7 +370,7 @@ def directory(parser, args): print("Directory modification type invalid.") print("Valid ones are: add, delete.") - filename = os.path.join("scripts", "user", + filename = os.path.join(DIRS.user_config_dir, "scripts", "user", "windows", "{}.ps1".format(args.profile)) append_to_script(filename, line) @@ -382,7 +383,7 @@ def package(parser, args): line = "cinst {} -y\r\n".format(args.package) print("Adding Chocolatey package: {}".format(args.package)) - filename = os.path.join("scripts", "user", + filename = os.path.join(DIRS.user_config_dir, "scripts", "user", "windows", "{}.ps1".format(args.profile)) append_to_script(filename, line) @@ -405,7 +406,7 @@ def document(parser, args): print("Directory modification type invalid.") print("Valid ones are: add, delete.") - filename = os.path.join( + filename = os.path.join(DIRS.user_config_dir, "scripts", "user", "windows", "{}.ps1".format(args.profile)) From e6509a1c910387be5f3b108c6639f64a9446c965 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 13 Jul 2016 13:27:29 -0400 Subject: [PATCH 11/21] cherry-picked fix for #3: 0e0255f --- malboxes/__init__.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/malboxes/__init__.py b/malboxes/__init__.py index ab0e137..37e7bcb 100755 --- a/malboxes/__init__.py +++ b/malboxes/__init__.py @@ -18,6 +18,7 @@ # GNU General Public License for more details. # import argparse +from distutils import spawn import glob from io import TextIOWrapper import json @@ -212,7 +213,17 @@ def run_packer(packer_config): print("Starting packer to generate the VM") print("----------------------------------") - cmd = ['packer', 'build', '-var-file=config.json', packer_config] + # packer or packer-io? + binary = 'packer' + # TODO starting with python 3.3 we could use shutil.which() + if spawn.find_executable(binary) == None: + binary = 'packer-io' + if spawn.find_executable(binary) == None: + print("packer not found. Install it: " + "https://www.packer.io/intro/getting-started/setup.html") + return 254 + + cmd = [binary, 'build', '-var-file=config.json', packer_config] ret = run_foreground(cmd) print("----------------------------------") From 226926ca0638731de715f84d43f64b579458d39d Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 13 Jul 2016 14:54:25 -0400 Subject: [PATCH 12/21] TODO / Doc misc updates --- README.adoc | 4 ++-- TODO.adoc | 9 +-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/README.adoc b/README.adoc index 70ed9f9..9a103cb 100644 --- a/README.adoc +++ b/README.adoc @@ -23,8 +23,8 @@ https://github.com/gosecure/malboxes * Python 3 * appdirs * jinja2 -* packer (sometimes called packer-io) -* vagrant +* packer: https://www.packer.io/intro/getting-started/setup.html +* vagrant: https://www.vagrantup.com/downloads.html == Installation diff --git a/TODO.adoc b/TODO.adoc index 2617dac..c796d29 100644 --- a/TODO.adoc +++ b/TODO.adoc @@ -2,14 +2,7 @@ == pip packaging -* Needs to port code to use pkg resource API - -See: -* https://setuptools.readthedocs.io/en/latest/setuptools.html#accessing-data-files-at-runtime -* http://stackoverflow.com/a/26533921 - -* Push caches into .local/share/ (and Windows equivalent) - avoids permissions issues -* Allow config overrides in .config/malboxes (and Windows equivalent) +* where should the built boxes go? == Misc From 840b287d2cfc0356a9bbf91090ba47a2df784b2f Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 13 Jul 2016 15:39:04 -0400 Subject: [PATCH 13/21] Win 10 x86: Turns out image name is different between x86 and x64 (fixes #4) --- malboxes/installconfig/windows10/Autounattend.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/malboxes/installconfig/windows10/Autounattend.xml b/malboxes/installconfig/windows10/Autounattend.xml index 0de5494..b310a5b 100644 --- a/malboxes/installconfig/windows10/Autounattend.xml +++ b/malboxes/installconfig/windows10/Autounattend.xml @@ -34,7 +34,7 @@ /IMAGE/NAME - Windows 10 Enterprise Evaluation + Windows 10 Enterprise Evaluation Technical Preview From 2dbbec45bc97f7528bc93b4582686193b0f5b4b0 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 13 Jul 2016 16:29:40 -0400 Subject: [PATCH 14/21] End-to-end VM generation works now * malboxes provides new variables to profiles for cache_dir * Path fixes in all supported packer profiles --- malboxes/__init__.py | 7 ++++++- malboxes/profiles/win10_32_analyst.json | 21 ++++++++++++--------- malboxes/profiles/win10_64_analyst.json | 22 +++++++++++++--------- malboxes/profiles/win7_32_analyst.json | 15 ++++++++------- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/malboxes/__init__.py b/malboxes/__init__.py index 37e7bcb..143d28d 100755 --- a/malboxes/__init__.py +++ b/malboxes/__init__.py @@ -223,7 +223,12 @@ def run_packer(packer_config): "https://www.packer.io/intro/getting-started/setup.html") return 254 - cmd = [binary, 'build', '-var-file=config.json', 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] ret = run_foreground(cmd) print("----------------------------------") diff --git a/malboxes/profiles/win10_32_analyst.json b/malboxes/profiles/win10_32_analyst.json index c4b415d..7c91391 100644 --- a/malboxes/profiles/win10_32_analyst.json +++ b/malboxes/profiles/win10_32_analyst.json @@ -2,7 +2,8 @@ "variables": { "winrm_user": "vagrant", "winrm_pass": "vagrant", - "name": "win10_32_analyst" + "name": "win10_32_analyst", + "malboxes_dir": "{{ template_dir }}/../" }, "builders": [{ @@ -21,7 +22,10 @@ "output_directory": "builds", - "iso_url": "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_urls": [ + "file://{{ user `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", @@ -34,24 +38,23 @@ "boot_wait": "10s", "floppy_files": [ - "installconfig/windows10_64/Autounattend.xml", - "installconfig/windows10_64/enablewinrm.ps1" + "{{user `malboxes_cache_dir`}}/Autounattend.xml", + "{{user `malboxes_dir`}}/installconfig/windows10/enablewinrm.ps1" ] }], "post-processors": [{ "type": "vagrant", "output": "boxes/{{user `name`}}.box", - "vagrantfile_template": "vagrantfiles/box_win.rb" + "vagrantfile_template": "{{user `malboxes_dir`}}/vagrantfiles/box_win.rb" }], "provisioners": [{ "type": "powershell", "scripts": [ - "scripts/windows/vmtools.ps1", - "scripts/windows/enablerdp.ps1", - "scripts/windows/installchocolatey.ps1", - "scripts/windows/installtools.ps1" + "{{user `malboxes_dir`}}/scripts/windows/vmtools.ps1", + "{{user `malboxes_dir`}}/scripts/windows/enablerdp.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 23cea04..2c485eb 100644 --- a/malboxes/profiles/win10_64_analyst.json +++ b/malboxes/profiles/win10_64_analyst.json @@ -2,7 +2,8 @@ "variables": { "winrm_user": "vagrant", "winrm_pass": "vagrant", - "name": "win10_64_analyst" + "name": "win10_64_analyst", + "malboxes_dir": "{{ template_dir }}/../" }, "builders": [{ @@ -21,7 +22,11 @@ "output_directory": "builds", - "iso_url": "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_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" + ], "iso_checksum": "56ab095075be28a90bc0b510835280975c6bb2ce", "iso_checksum_type": "sha1", @@ -34,24 +39,23 @@ "boot_wait": "10s", "floppy_files": [ - "installconfig/windows10_64/Autounattend.xml", - "installconfig/windows10_64/enablewinrm.ps1" + "{{user `malboxes_cache_dir`}}/Autounattend.xml", + "{{user `malboxes_dir`}}/installconfig/windows10_64/enablewinrm.ps1" ] }], "post-processors": [{ "type": "vagrant", "output": "boxes/{{user `name`}}.box", - "vagrantfile_template": "vagrantfiles/box_win.rb" + "vagrantfile_template": "{{user `malboxes_dir`}}/vagrantfiles/box_win.rb" }], "provisioners": [{ "type": "powershell", "scripts": [ - "scripts/windows/vmtools.ps1", - "scripts/windows/enablerdp.ps1", - "scripts/windows/installtools.ps1", - "scripts/windows/user_scripts.ps1" + "{{user `malboxes_dir`}}/scripts/windows/vmtools.ps1", + "{{user `malboxes_dir`}}/scripts/windows/enablerdp.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 1e36d30..3ee60d8 100644 --- a/malboxes/profiles/win7_32_analyst.json +++ b/malboxes/profiles/win7_32_analyst.json @@ -2,7 +2,8 @@ "variables": { "winrm_user": "vagrant", "winrm_pass": "vagrant", - "name": "win7_32_analyst" + "name": "win7_32_analyst", + "malboxes_dir": "{{ template_dir }}/../" }, "builders": [{ @@ -34,23 +35,23 @@ "boot_wait": "10s", "floppy_files": [ - "config_cache/Autounattend.xml", - "installconfig/windows7/enablewinrm.ps1" + "{{user `malboxes_cache_dir`}}/Autounattend.xml", + "{{user `malboxes_dir`}}/installconfig/windows7/enablewinrm.ps1" ] }], "post-processors": [{ "type": "vagrant", "output": "boxes/{{user `name`}}.box", - "vagrantfile_template": "vagrantfiles/box_win.rb" + "vagrantfile_template": "{{user `malboxes_dir`}}/vagrantfiles/box_win.rb" }], "provisioners": [{ "type": "powershell", "scripts": [ - "scripts/windows/vmtools.ps1", - "scripts/windows/enablerdp.ps1", - "scripts/windows/installtools.ps1" + "{{user `malboxes_dir`}}/scripts/windows/vmtools.ps1", + "{{user `malboxes_dir`}}/scripts/windows/enablerdp.ps1", + "{{user `malboxes_dir`}}/scripts/windows/installtools.ps1" ] }] } From 1cd5719a42917b5c395431ff15880cd47ab00301 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 13 Jul 2016 16:34:00 -0400 Subject: [PATCH 15/21] A more portable way to detect packer binary (requires python 3.3) --- .travis.yml | 2 -- README.adoc | 2 +- malboxes/__init__.py | 6 ++---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index f901ceb..0d15aa8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ language: python python: -# disabled since pylint is currently broken on 3.2 -#- "3.2" - "3.3" - "3.4" - "3.5" diff --git a/README.adoc b/README.adoc index 9a103cb..2e3f0a4 100644 --- a/README.adoc +++ b/README.adoc @@ -20,7 +20,7 @@ https://github.com/gosecure/malboxes == Requirements -* Python 3 +* Python 3.3+ * appdirs * jinja2 * packer: https://www.packer.io/intro/getting-started/setup.html diff --git a/malboxes/__init__.py b/malboxes/__init__.py index 143d28d..60998e8 100755 --- a/malboxes/__init__.py +++ b/malboxes/__init__.py @@ -18,7 +18,6 @@ # GNU General Public License for more details. # import argparse -from distutils import spawn import glob from io import TextIOWrapper import json @@ -215,10 +214,9 @@ def run_packer(packer_config): # packer or packer-io? binary = 'packer' - # TODO starting with python 3.3 we could use shutil.which() - if spawn.find_executable(binary) == None: + if shutil.which(binary) == None: binary = 'packer-io' - if spawn.find_executable(binary) == None: + if shutil.which(binary) == None: print("packer not found. Install it: " "https://www.packer.io/intro/getting-started/setup.html") return 254 From 02fc4c871a3ee9f690018e3a467035e13fabdc9b Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Wed, 13 Jul 2016 22:13:18 -0400 Subject: [PATCH 16/21] review: Doc fix --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 2e3f0a4..95eaef5 100644 --- a/README.adoc +++ b/README.adoc @@ -86,7 +86,7 @@ Registry keys: Example: - malboxes registry win10_64_analyst add HKCU:\Software "Malboxes IsAwesome" String + malboxes registry win10_64_analyst add HKCU:\Software Malboxes IsAwesome String Directories and files: From 784938798067b4594ed44ceb9efe470921e4cd49 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Thu, 14 Jul 2016 08:28:50 -0400 Subject: [PATCH 17/21] doc: specify pip version on cli --- README.adoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 95eaef5..30c62d5 100644 --- a/README.adoc +++ b/README.adoc @@ -28,7 +28,9 @@ https://github.com/gosecure/malboxes == Installation - sudo pip install git+https://github.com/GoSecure/malboxes.git@pip-packaging#egg=malboxes +=== Linux/Unix + + sudo pip3 install git+https://github.com/GoSecure/malboxes.git@pip-packaging#egg=malboxes == Usage From 67a49b5224663ec3e21c18f855aa7033271c2a6d Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Thu, 14 Jul 2016 08:44:47 -0400 Subject: [PATCH 18/21] Pushed out version so we can pip install even w/o deps setup.py needed to be able to import the main mailboxes package which pulled in all dependencies unconditionally resulting in an error if you were missing one. Version is now in it's own file without dependencies --- malboxes/__init__.py | 7 +++++-- malboxes/_version.py | 1 + setup.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 malboxes/_version.py diff --git a/malboxes/__init__.py b/malboxes/__init__.py index 60998e8..2976a04 100755 --- a/malboxes/__init__.py +++ b/malboxes/__init__.py @@ -32,8 +32,9 @@ from appdirs import AppDirs from jinja2 import Environment, FileSystemLoader +from malboxes._version import __version__ + DIRS = AppDirs("malboxes") -__version__ = "0.1.0" def initialize(): # create appdata directories if they don't exist @@ -49,6 +50,8 @@ def init_parser(): parser = argparse.ArgumentParser( description="Vagrant box builder " "and config generator for malware analysis.") + parser.add_argument('-V', '--version', action='version', + version='%(prog)s ' + __version__) subparsers = parser.add_subparsers() # list command @@ -152,7 +155,7 @@ def load_config(profile): Config is in JSON since we can re-use the same in both malboxes and packer """ try: - profile_fd = resource_stream(__name__, + profile_fd = resource_stream(__name__, 'profiles/{}.json'.format(profile)) except FileNotFoundError: print("Profile doesn't exist: {}".format(profile)) diff --git a/malboxes/_version.py b/malboxes/_version.py new file mode 100644 index 0000000..44ecbe1 --- /dev/null +++ b/malboxes/_version.py @@ -0,0 +1 @@ +__version__ = "0.2.0dev" diff --git a/setup.py b/setup.py index 15250fb..2fcbd3d 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ from os import path, remove import shutil -from malboxes import __version__ +from malboxes._version import __version__ here = path.abspath(path.dirname(__file__)) From 2fd51ef1beeb72cfb85d54c95f298174e6b34f90 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Thu, 14 Jul 2016 09:52:36 -0400 Subject: [PATCH 19/21] prevent package from loading all dependencies unless called from cli --- malboxes/__init__.py | 417 +--------------------------------------- malboxes/malboxes.py | 446 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 447 insertions(+), 416 deletions(-) create mode 100755 malboxes/malboxes.py diff --git a/malboxes/__init__.py b/malboxes/__init__.py index 2976a04..ad8cfb0 100755 --- a/malboxes/__init__.py +++ b/malboxes/__init__.py @@ -2,9 +2,7 @@ # https://github.com/gosecure/malboxes # # Olivier Bilodeau -# Hugo Genesse # Copyright (C) 2016 GoSecure Inc. -# Copyright (C) 2016 Hugo Genesse # All rights reserved. # # This program is free software: you can redistribute it and/or modify @@ -17,422 +15,9 @@ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # -import argparse -import glob -from io import TextIOWrapper -import json -import os -from pkg_resources import resource_filename, resource_stream -import re -import shutil -import signal -import subprocess -import sys - -from appdirs import AppDirs -from jinja2 import Environment, FileSystemLoader - -from malboxes._version import __version__ - -DIRS = AppDirs("malboxes") - -def initialize(): - # create appdata directories if they don't exist - if not os.path.exists(DIRS.user_config_dir): - os.mkdir(DIRS.user_config_dir) - - if not os.path.exists(DIRS.user_cache_dir): - os.mkdir(DIRS.user_cache_dir) - - return init_parser() - -def init_parser(): - parser = argparse.ArgumentParser( - description="Vagrant box builder " - "and config generator for malware analysis.") - parser.add_argument('-V', '--version', action='version', - version='%(prog)s ' + __version__) - subparsers = parser.add_subparsers() - - # list command - parser_list = subparsers.add_parser('list', - help="Lists available profiles.") - parser_list.set_defaults(func=list_profiles) - - # build command - parser_build = subparsers.add_parser('build', - help="Builds a Vagrant box based on " - "a given profile.") - parser_build.add_argument('profile', help='Name of the profile to build. ' - 'Use list command to view ' - 'available profiles.') - parser_build.set_defaults(func=build) - - # spin command - parser_spin = subparsers.add_parser('spin', - help="Creates a Vagrantfile for " - "your profile / Vagrant box.") - parser_spin.add_argument('profile', help='Name of the profile to spin.') - parser_spin.add_argument('name', help='Name of the target VM. ' - 'Must be unique on your system. ' - 'Ex: Cryptolocker_XYZ.') - parser_spin.set_defaults(func=spin) - - # reg command - parser_reg = subparsers.add_parser('registry', - help='Modifies a registry key.') - parser_reg.add_argument('profile', help='Name of the profile to modify.') - parser_reg.add_argument('modtype', - help='Modification type (add, delete or modify).') - parser_reg.add_argument('key', help='Location of the key to modify.') - parser_reg.add_argument('name', help='Name of the key.') - parser_reg.add_argument('value', help='Value of the key.') - parser_reg.add_argument('valuetype', - help='Type of the value of the key: ' - 'DWORD for integer, String for string.') - parser_reg.set_defaults(func=reg) - - # dir command - parser_dir = subparsers.add_parser('directory', - help='Modifies a directory.') - parser_dir.add_argument('profile', - help='Name of the profile to modify.') - parser_dir.add_argument('modtype', - help='Modification type (delete or add).') - parser_dir.add_argument('dirpath', - help='Path of the directory to modify.') - parser_dir.set_defaults(func=directory) - - # wallpaper command - - # package command - parser_package = subparsers.add_parser('package', - help='Adds package to install.') - parser_package.add_argument('profile', - help='Name of the profile to modify.') - parser_package.add_argument('package', - help='Name of the package to install.') - parser_package.set_defaults(func=package) - - # document command - parser_document = subparsers.add_parser('document', - help='Adds a file') - parser_document.add_argument('profile', - help='Name of the profile to modify.') - parser_document.add_argument('modtype', - help='Modification type (delete or add).') - parser_document.add_argument('docpath', - help='Path of the file to add.') - parser_document.set_defaults(func=document) - - # no command - parser.set_defaults(func=default) - - args = parser.parse_args() - return parser, args - - -def prepare_autounattend(config): - """ - Prepares an Autounattend.xml file according to configuration and writes it - into a temporary location where packer later expects it. - - 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() - - filepath = resource_filename(__name__, "installconfig/") - env = Environment(loader=FileSystemLoader(filepath)) - template = env.get_template("{}/Autounattend.xml".format(os_type)) - f = create_cachefd('Autounattend.xml') - f.write(template.render(config)) # pylint: disable=no-member - f.close() - - -def load_config(profile): - """ - Config is in JSON since we can re-use the same in both malboxes and packer - """ - 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.json') - 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'), - config_file) - - # load general config - config = {} - with open(config_file, 'r') as f: - config = json.load(f) - - # merge/update with profile config - config.update(json.load(TextIOWrapper(profile_fd))) - - return config - - -tempfiles = [] -def create_cachefd(filename): - tempfiles.append(filename) - return open(os.path.join(DIRS.user_cache_dir, filename), 'w') - - -def cleanup(): - """Removes temporary files""" - for f in tempfiles: - os.remove(os.path.join(DIRS.user_cache_dir, f)) - - -def run_foreground(command): - p = subprocess.Popen(command, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - try: - for line in iter(p.stdout.readline, b''): - print(line.rstrip().decode('utf-8')) - - # send Ctrl-C to subprocess - except KeyboardInterrupt: - p.send_signal(signal.SIGINT) - for line in iter(p.stdout.readline, b''): - print(line.rstrip().decode('utf-8')) - raise - - finally: - p.wait() - return p.returncode - - -def run_packer(packer_config): - print("Starting packer to generate the VM") - print("----------------------------------") - - # packer or packer-io? - binary = 'packer' - if shutil.which(binary) == None: - 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') - cmd = [binary, 'build', - '-var-file={}'.format(configfile), - "-var", "malboxes_cache_dir={}".format(DIRS.user_cache_dir), - packer_config] - ret = run_foreground(cmd) - - print("----------------------------------") - print("packer completed with return code: {}".format(ret)) - return ret - - -def import_box(config, args): - print("Importing box into vagrant") - print("--------------------------") - - box = config['post-processors'][0]['output'] - box = box.replace('{{user `name`}}', args.profile) - - cmd = ['vagrant', 'box', 'add', box, '--name={}'.format(args.profile)] - ret = run_foreground(cmd) - - print("----------------------------") - print("vagrant box import completed with return code: {}".format(ret)) - return ret - - -def default(parser, args): - parser.print_help() - print("\n") - list_profiles(parser, args) - sys.exit(1) - - -def list_profiles(parser, args): - print("supported profiles:\n") - - filepath = resource_filename(__name__, "profiles/") - for f in sorted(glob.glob(os.path.join(filepath, '*.json'))): - m = re.search(r'profiles[\/\\](.*).json$', f) - print(m.group(1)) - print() - - -def build(parser, args): - config = load_config(args.profile) - - print("Generating configuration files...") - prepare_autounattend(config) - print("Configuration files are ready") - ret = run_packer(resource_filename(__name__, - "profiles/{}.json".format(args.profile))) - - if ret != 0: - print("Packer failed. Build failed. Exiting...") - sys.exit(3) - - ret = import_box(config, args) - if ret != 0: - print("'vagrant box add' failed. Build failed. Exiting...") - sys.exit(4) - - print("A base box was imported into your local Vagrant box repository") - print("You can re-use this base box several times by using the " - "following statement in your Vagrantfile:") - print('config.vm.box = "{}"'.format(args.profile)) - - -def spin(parser, args): - """ - Creates a Vagrantfile based on a template using the jinja2 engine - """ - config = load_config(args.profile) - - print("Creating a Vagrantfile") - filepath = resource_filename(__name__, "vagrantfiles/") - env = Environment(loader=FileSystemLoader(filepath)) - template = env.get_template("analyst_single.rb") - - if os.path.isfile('Vagrantfile'): - print("Vagrantfile already exists. Please move away.") - sys.exit(5) - - config['profile'] = args.profile - config['name'] = args.name - - with open("Vagrantfile", 'w') as f: - f.write(template.render(config)) # pylint: disable=no-member - print("Vagrantfile generated. You can move it in your analysis directory " - "and issue a `vagrant up` to get started with your VM.") - - -def append_to_script(filename, line): - """ Appends a line to a file.""" - with open(filename, 'a') as f: - f.write(line) - - -def add_to_user_scripts(profile): - """ Adds the modified script to the user scripts file.""" - """ File names for the user scripts file and the script to be added.""" - filename = os.path.join(DIRS.user_config_dir, "scripts", "windows", - "user_scripts.ps1") - line = "{}.ps1".format(profile) - - """ Check content of the user scripts file.""" - with open(filename, "r") as f: - content = f.read() - - """ If script isnt present, add it.""" - if content.find(line) == -1: - with open(filename, "a") as f: - f.write(line) - - -def reg(parser, args): - """ - Adds a registry key modification to a profile with PowerShell commands. - """ - if args.modtype == "add": - command = "New-ItemProperty" - line = "{} -Path {} -Name {} -Value {} -PropertyType {}\r\n".format( - command, args.key, args.name, args.value, args.valuetype) - print("Adding: " + line) - elif args.modtype == "modify": - command = "Set-ItemProperty" - line = "{0} -Path {1} -Name {2} -Value {3}\r\n".format( - command, args.key, args.name, args.value) - print("Adding: " + line) - elif args.modtype == "delete": - command = "Remove-ItemProperty" - line = "{0} -Path {1} -Name {2}\r\n".format( - command, args.key, args.name) - print("Adding: " + line) - else: - print("Registry modification type invalid.") - print("Valid ones are: add, delete and modify.") - - filename = os.path.join(DIRS.user_config_dir, "scripts", "user", - "windows", "{}.ps1".format(args.profile)) - append_to_script(filename, line) - - """ Adds the modified script to the user scripts.""" - add_to_user_scripts(args.profile) - - -def directory(parser, args): - """ Adds the directory manipulation commands to the profile.""" - if args.modtype == "add": - command = "New-Item" - line = "{0} -Path {1} -Type directory\r\n".format(command, - args.dirpath) - print("Adding directory: {}".format(args.dirpath)) - elif args.modtype == "delete": - command = "Remove-Item" - line = "{0} -Path {1}\r\n".format( - command, args.dirpath) - print("Removing directory: {}".format(args.dirpath)) - else: - print("Directory modification type invalid.") - print("Valid ones are: add, delete.") - - filename = os.path.join(DIRS.user_config_dir, "scripts", "user", - "windows", "{}.ps1".format(args.profile)) - append_to_script(filename, line) - - """ Adds the modified script to the user scripts.""" - add_to_user_scripts(args.profile) - - -def package(parser, args): - """ Adds a package to install with Chocolatey.""" - line = "cinst {} -y\r\n".format(args.package) - print("Adding Chocolatey package: {}".format(args.package)) - - filename = os.path.join(DIRS.user_config_dir, "scripts", "user", - "windows", "{}.ps1".format(args.profile)) - append_to_script(filename, line) - - """ Adds the modified script to the user scripts.""" - add_to_user_scripts(args.profile) - - -def document(parser, args): - """ Adds the file manipulation commands to the profile.""" - if args.modtype == "add": - command = "New-Item" - line = "{0} -Path {1}\r\n".format(command, args.docpath) - print("Adding file: {}".format(args.docpath)) - elif args.modtype == "delete": - command = "Remove-Item" - line = "{0} -Path {1}\r\n".format( - command, args.docpath) - print("Removing file: {}".format(args.docpath)) - else: - print("Directory modification type invalid.") - print("Valid ones are: add, delete.") - - filename = os.path.join(DIRS.user_config_dir, - "scripts", "user", "windows", - "{}.ps1".format(args.profile)) - - append_to_script(filename, line) - - """ Adds the modified script to the user scripts.""" - add_to_user_scripts(args.profile) def main(): + from malboxes.malboxes import initialize, cleanup try: parser, args = initialize() args.func(parser, args) diff --git a/malboxes/malboxes.py b/malboxes/malboxes.py new file mode 100755 index 0000000..c5c8ddd --- /dev/null +++ b/malboxes/malboxes.py @@ -0,0 +1,446 @@ +# Malboxes - Vagrant box builder and config generator for malware analysis +# https://github.com/gosecure/malboxes +# +# Olivier Bilodeau +# Hugo Genesse +# Copyright (C) 2016 GoSecure Inc. +# Copyright (C) 2016 Hugo Genesse +# 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 argparse +import glob +from io import TextIOWrapper +import json +import os +from pkg_resources import resource_filename, resource_stream +import re +import shutil +import signal +import subprocess +import sys + +from appdirs import AppDirs +from jinja2 import Environment, FileSystemLoader + +from malboxes._version import __version__ + +DIRS = AppDirs("malboxes") + +def initialize(): + # create appdata directories if they don't exist + if not os.path.exists(DIRS.user_config_dir): + os.mkdir(DIRS.user_config_dir) + + if not os.path.exists(DIRS.user_cache_dir): + os.mkdir(DIRS.user_cache_dir) + + return init_parser() + +def init_parser(): + parser = argparse.ArgumentParser( + description="Vagrant box builder " + "and config generator for malware analysis.") + parser.add_argument('-V', '--version', action='version', + version='%(prog)s ' + __version__) + subparsers = parser.add_subparsers() + + # list command + parser_list = subparsers.add_parser('list', + help="Lists available profiles.") + parser_list.set_defaults(func=list_profiles) + + # build command + parser_build = subparsers.add_parser('build', + help="Builds a Vagrant box based on " + "a given profile.") + parser_build.add_argument('profile', help='Name of the profile to build. ' + 'Use list command to view ' + 'available profiles.') + parser_build.set_defaults(func=build) + + # spin command + parser_spin = subparsers.add_parser('spin', + help="Creates a Vagrantfile for " + "your profile / Vagrant box.") + parser_spin.add_argument('profile', help='Name of the profile to spin.') + parser_spin.add_argument('name', help='Name of the target VM. ' + 'Must be unique on your system. ' + 'Ex: Cryptolocker_XYZ.') + parser_spin.set_defaults(func=spin) + + # reg command + parser_reg = subparsers.add_parser('registry', + help='Modifies a registry key.') + parser_reg.add_argument('profile', help='Name of the profile to modify.') + parser_reg.add_argument('modtype', + help='Modification type (add, delete or modify).') + parser_reg.add_argument('key', help='Location of the key to modify.') + parser_reg.add_argument('name', help='Name of the key.') + parser_reg.add_argument('value', help='Value of the key.') + parser_reg.add_argument('valuetype', + help='Type of the value of the key: ' + 'DWORD for integer, String for string.') + parser_reg.set_defaults(func=reg) + + # dir command + parser_dir = subparsers.add_parser('directory', + help='Modifies a directory.') + parser_dir.add_argument('profile', + help='Name of the profile to modify.') + parser_dir.add_argument('modtype', + help='Modification type (delete or add).') + parser_dir.add_argument('dirpath', + help='Path of the directory to modify.') + parser_dir.set_defaults(func=directory) + + # wallpaper command + + # package command + parser_package = subparsers.add_parser('package', + help='Adds package to install.') + parser_package.add_argument('profile', + help='Name of the profile to modify.') + parser_package.add_argument('package', + help='Name of the package to install.') + parser_package.set_defaults(func=package) + + # document command + parser_document = subparsers.add_parser('document', + help='Adds a file') + parser_document.add_argument('profile', + help='Name of the profile to modify.') + parser_document.add_argument('modtype', + help='Modification type (delete or add).') + parser_document.add_argument('docpath', + help='Path of the file to add.') + parser_document.set_defaults(func=document) + + # no command + parser.set_defaults(func=default) + + args = parser.parse_args() + return parser, args + + +def prepare_autounattend(config): + """ + Prepares an Autounattend.xml file according to configuration and writes it + into a temporary location where packer later expects it. + + 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() + + filepath = resource_filename(__name__, "installconfig/") + env = Environment(loader=FileSystemLoader(filepath)) + template = env.get_template("{}/Autounattend.xml".format(os_type)) + f = create_cachefd('Autounattend.xml') + f.write(template.render(config)) # pylint: disable=no-member + f.close() + + +def load_config(profile): + """ + Config is in JSON since we can re-use the same in both malboxes and packer + """ + 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.json') + 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'), + config_file) + + # load general config + config = {} + with open(config_file, 'r') as f: + config = json.load(f) + + # merge/update with profile config + config.update(json.load(TextIOWrapper(profile_fd))) + + return config + + +tempfiles = [] +def create_cachefd(filename): + tempfiles.append(filename) + return open(os.path.join(DIRS.user_cache_dir, filename), 'w') + + +def cleanup(): + """Removes temporary files""" + for f in tempfiles: + os.remove(os.path.join(DIRS.user_cache_dir, f)) + + +def run_foreground(command): + p = subprocess.Popen(command, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + try: + for line in iter(p.stdout.readline, b''): + print(line.rstrip().decode('utf-8')) + + # send Ctrl-C to subprocess + except KeyboardInterrupt: + p.send_signal(signal.SIGINT) + for line in iter(p.stdout.readline, b''): + print(line.rstrip().decode('utf-8')) + raise + + finally: + p.wait() + return p.returncode + + +def run_packer(packer_config): + print("Starting packer to generate the VM") + print("----------------------------------") + + # packer or packer-io? + binary = 'packer' + if shutil.which(binary) == None: + 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') + cmd = [binary, 'build', + '-var-file={}'.format(configfile), + "-var", "malboxes_cache_dir={}".format(DIRS.user_cache_dir), + packer_config] + ret = run_foreground(cmd) + + print("----------------------------------") + print("packer completed with return code: {}".format(ret)) + return ret + + +def import_box(config, args): + print("Importing box into vagrant") + print("--------------------------") + + box = config['post-processors'][0]['output'] + box = box.replace('{{user `name`}}', args.profile) + + cmd = ['vagrant', 'box', 'add', box, '--name={}'.format(args.profile)] + ret = run_foreground(cmd) + + print("----------------------------") + print("vagrant box import completed with return code: {}".format(ret)) + return ret + + +def default(parser, args): + parser.print_help() + print("\n") + list_profiles(parser, args) + sys.exit(1) + + +def list_profiles(parser, args): + print("supported profiles:\n") + + filepath = resource_filename(__name__, "profiles/") + for f in sorted(glob.glob(os.path.join(filepath, '*.json'))): + m = re.search(r'profiles[\/\\](.*).json$', f) + print(m.group(1)) + print() + + +def build(parser, args): + config = load_config(args.profile) + + print("Generating configuration files...") + prepare_autounattend(config) + print("Configuration files are ready") + ret = run_packer(resource_filename(__name__, + "profiles/{}.json".format(args.profile))) + + if ret != 0: + print("Packer failed. Build failed. Exiting...") + sys.exit(3) + + ret = import_box(config, args) + if ret != 0: + print("'vagrant box add' failed. Build failed. Exiting...") + sys.exit(4) + + print("A base box was imported into your local Vagrant box repository") + print("You can re-use this base box several times by using the " + "following statement in your Vagrantfile:") + print('config.vm.box = "{}"'.format(args.profile)) + + +def spin(parser, args): + """ + Creates a Vagrantfile based on a template using the jinja2 engine + """ + config = load_config(args.profile) + + print("Creating a Vagrantfile") + filepath = resource_filename(__name__, "vagrantfiles/") + env = Environment(loader=FileSystemLoader(filepath)) + template = env.get_template("analyst_single.rb") + + if os.path.isfile('Vagrantfile'): + print("Vagrantfile already exists. Please move away.") + sys.exit(5) + + config['profile'] = args.profile + config['name'] = args.name + + with open("Vagrantfile", 'w') as f: + f.write(template.render(config)) # pylint: disable=no-member + print("Vagrantfile generated. You can move it in your analysis directory " + "and issue a `vagrant up` to get started with your VM.") + + +def append_to_script(filename, line): + """ Appends a line to a file.""" + with open(filename, 'a') as f: + f.write(line) + + +def add_to_user_scripts(profile): + """ Adds the modified script to the user scripts file.""" + """ File names for the user scripts file and the script to be added.""" + filename = os.path.join(DIRS.user_config_dir, "scripts", "windows", + "user_scripts.ps1") + line = "{}.ps1".format(profile) + + """ Check content of the user scripts file.""" + with open(filename, "r") as f: + content = f.read() + + """ If script isnt present, add it.""" + if content.find(line) == -1: + with open(filename, "a") as f: + f.write(line) + + +def reg(parser, args): + """ + Adds a registry key modification to a profile with PowerShell commands. + """ + if args.modtype == "add": + command = "New-ItemProperty" + line = "{} -Path {} -Name {} -Value {} -PropertyType {}\r\n".format( + command, args.key, args.name, args.value, args.valuetype) + print("Adding: " + line) + elif args.modtype == "modify": + command = "Set-ItemProperty" + line = "{0} -Path {1} -Name {2} -Value {3}\r\n".format( + command, args.key, args.name, args.value) + print("Adding: " + line) + elif args.modtype == "delete": + command = "Remove-ItemProperty" + line = "{0} -Path {1} -Name {2}\r\n".format( + command, args.key, args.name) + print("Adding: " + line) + else: + print("Registry modification type invalid.") + print("Valid ones are: add, delete and modify.") + + filename = os.path.join(DIRS.user_config_dir, "scripts", "user", + "windows", "{}.ps1".format(args.profile)) + append_to_script(filename, line) + + """ Adds the modified script to the user scripts.""" + add_to_user_scripts(args.profile) + + +def directory(parser, args): + """ Adds the directory manipulation commands to the profile.""" + if args.modtype == "add": + command = "New-Item" + line = "{0} -Path {1} -Type directory\r\n".format(command, + args.dirpath) + print("Adding directory: {}".format(args.dirpath)) + elif args.modtype == "delete": + command = "Remove-Item" + line = "{0} -Path {1}\r\n".format( + command, args.dirpath) + print("Removing directory: {}".format(args.dirpath)) + else: + print("Directory modification type invalid.") + print("Valid ones are: add, delete.") + + filename = os.path.join(DIRS.user_config_dir, "scripts", "user", + "windows", "{}.ps1".format(args.profile)) + append_to_script(filename, line) + + """ Adds the modified script to the user scripts.""" + add_to_user_scripts(args.profile) + + +def package(parser, args): + """ Adds a package to install with Chocolatey.""" + line = "cinst {} -y\r\n".format(args.package) + print("Adding Chocolatey package: {}".format(args.package)) + + filename = os.path.join(DIRS.user_config_dir, "scripts", "user", + "windows", "{}.ps1".format(args.profile)) + append_to_script(filename, line) + + """ Adds the modified script to the user scripts.""" + add_to_user_scripts(args.profile) + + +def document(parser, args): + """ Adds the file manipulation commands to the profile.""" + if args.modtype == "add": + command = "New-Item" + line = "{0} -Path {1}\r\n".format(command, args.docpath) + print("Adding file: {}".format(args.docpath)) + elif args.modtype == "delete": + command = "Remove-Item" + line = "{0} -Path {1}\r\n".format( + command, args.docpath) + print("Removing file: {}".format(args.docpath)) + else: + print("Directory modification type invalid.") + print("Valid ones are: add, delete.") + + filename = os.path.join(DIRS.user_config_dir, + "scripts", "user", "windows", + "{}.ps1".format(args.profile)) + + append_to_script(filename, line) + + """ Adds the modified script to the user scripts.""" + add_to_user_scripts(args.profile) + + +def main(): + try: + parser, args = initialize() + args.func(parser, args) + + finally: + cleanup() + + +if __name__ == "__main__": + main() From 129898ce83904a1f89625e5f3b53a62232888223 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Thu, 14 Jul 2016 09:57:39 -0400 Subject: [PATCH 20/21] Recursively make config / cache directories On Windows settings are stored under malboxes\\malboxes and so recursive creation was required on that platform. --- malboxes/malboxes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/malboxes/malboxes.py b/malboxes/malboxes.py index c5c8ddd..e46a169 100755 --- a/malboxes/malboxes.py +++ b/malboxes/malboxes.py @@ -39,10 +39,10 @@ def initialize(): # create appdata directories if they don't exist if not os.path.exists(DIRS.user_config_dir): - os.mkdir(DIRS.user_config_dir) + os.makedirs(DIRS.user_config_dir) if not os.path.exists(DIRS.user_cache_dir): - os.mkdir(DIRS.user_cache_dir) + os.makedirs(DIRS.user_cache_dir) return init_parser() From b1f2bc1660c9ef2e71bbb923972fa7e872a16c19 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Fri, 15 Jul 2016 16:30:18 -0400 Subject: [PATCH 21/21] Windows install documentation: both manually and through chocolatey --- README.adoc | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/README.adoc b/README.adoc index 30c62d5..b61fbfb 100644 --- a/README.adoc +++ b/README.adoc @@ -18,6 +18,7 @@ Vagrant box builder and config generator for malware analysis https://github.com/gosecure/malboxes + == Requirements * Python 3.3+ @@ -26,12 +27,57 @@ https://github.com/gosecure/malboxes * packer: https://www.packer.io/intro/getting-started/setup.html * vagrant: https://www.vagrantup.com/downloads.html + == Installation === Linux/Unix +* Install git, vagrant and packer using your distribution's packaging tool + (packer is sometimes called packer-io) +* `pip install` malboxes: ++ sudo pip3 install git+https://github.com/GoSecure/malboxes.git@pip-packaging#egg=malboxes + +=== Windows + +==== Manually + +* Install https://www.virtualbox.org/wiki/Downloads[VirtualBox], + https://www.vagrantup.com/downloads.html[Vagrant] and + https://git-scm.com/downloads[git] +* https://www.packer.io/downloads.html[Install Packer], drop the packer binary + in a folder in your user's PATH like `C:\Windows\System32\` +* https://www.python.org/downloads/[Install Python 3] (make sure to add + Python to your environment variables) +* Open a console (Windows-Key + cmd) ++ + pip3 install git+https://github.com/GoSecure/malboxes.git@pip-packaging#egg=malboxes + +==== Using Chocolatey + +[NOTE] +Two issues are preventing chocolatey install to work right now: +https://github.com/chocolatey/chocolatey-coreteampackages/pull/261[Python3 +Scripts directory is not in the PATH] and +https://github.com/chocolatey/choco/issues/836[problems with zip files on +32-bit Windows]. + +Assuming you have https://chocolatey.org/[Chocolatey] installed: + +* Install dependencies: ++ + choco install python vagrant packer git virtualbox ++ +* Refresh the console ++ + refreshenv ++ +* Install malboxes: ++ + pip3 install git+https://github.com/GoSecure/malboxes.git@pip-packaging#egg=malboxes + + == Usage === Box creation @@ -106,6 +152,7 @@ Example: malboxes package RansomwareThatINeedRevengeOn chrome + == More information === Applying DevOps Principles for Better Malware Analysis @@ -120,12 +167,14 @@ by link:{twob}[Olivier Bilodeau] and link:{twhg}[Hugo Genesse] (PDF, degraded) * Video (coming soon) + == License Code is licensed under the GPLv3+, see `LICENSE` for details. Documentation and presentation material is licensed under the Creative Commons Attribution-ShareAlike 4.0, see `docs/LICENSE` for details. + == Credits After I had the idea for an improved malware analyst workflow based on what