Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better profiles #79

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions malboxes/config-example.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
// This example profile will attempt to load profiles/maldoc.js
// For more information on profiles check an example profile:
// https://github.com/GoSecure/malboxes/blob/master/malboxes/profile-example.js
//"profile": "maldoc",
"profile": "default",
//"input_locale": "fr-FR",

// Provision settings
Expand All @@ -53,10 +53,6 @@
// Windows Updates: true means enabled, false means disabled. Default is false.
//"windows_updates": "false",

// Chocolatey packages to install on the VM
// TODO re-add dependencywalker and regshot once upstream choco package provides a checksum
"choco_packages": "sysinternals windbg 7zip putty fiddler4 processhacker apm wireshark",

// Setting the IDA Path will copy the IDA remote debugging tools into the guest
//"ida_path": "/path/to/your/ida",

Expand Down
85 changes: 58 additions & 27 deletions malboxes/malboxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import signal
import subprocess
import sys
import tempfile
import textwrap

from appdirs import AppDirs
Expand All @@ -38,8 +39,10 @@

DIRS = AppDirs("malboxes")
DEBUG = False
tmp_cache_dir = "/tmp/malboxes"

def initialize():
global tmp_cache_dir
# create appdata directories if they don't exist
if not os.path.exists(DIRS.user_config_dir):
os.makedirs(DIRS.user_config_dir)
Expand All @@ -52,7 +55,13 @@ def initialize():
if not os.path.exists(DIRS.user_cache_dir):
os.makedirs(DIRS.user_cache_dir)

cache_scripts_dir = os.path.join(DIRS.user_cache_dir, "scripts", "user")
temp_cache = tempfile.mkdtemp(dir=DIRS.user_cache_dir)
tmp_cache_dir = temp_cache

if not os.path.exists(tmp_cache_dir):
os.makedirs(tmp_cache_dir)

cache_scripts_dir = os.path.join(tmp_cache_dir, "scripts", "user")

if not (os.path.exists(cache_scripts_dir)):
os.makedirs(cache_scripts_dir)
Expand Down Expand Up @@ -84,6 +93,8 @@ def init_parser():
parser_build.add_argument('--force', action='store_true',
help='Force the build to happen. Overwrites '
'pre-existing builds or vagrant boxes.')
parser_build.add_argument('--profile', dest='profile', action='store',
help='Override the profile setting')
parser_build.add_argument('--skip-packer-build', action='store_true',
help='Skip packer build phase. '
'Only useful for debugging.')
Expand All @@ -100,6 +111,8 @@ def init_parser():
parser_spin.add_argument('name', help='Name of the target VM. '
'Must be unique on your system. '
'Ex: Cryptolocker_XYZ.')
parser_spin.add_argument('--profile', dest='profile', action='store',
help='Override the profile setting')
parser_spin.set_defaults(func=spin)

# no command
Expand All @@ -126,14 +139,17 @@ def prepare_autounattend(config):
f.close()


def prepare_packer_template(config, template_name):
def prepare_packer_template(config, args):
"""
Prepares a packer template JSON file according to configuration and writes
it into a temporary location where packer later expects it.

Uses jinja2 template syntax to generate the resulting JSON file.
Templates are in templates/ and snippets in templates/snippets/.
"""
template_name = config["template"]
packer_template_name = config["template_name"]

try:
template_fd = resource_stream(__name__,
'templates/{}.json'.format(template_name))
Expand All @@ -147,7 +163,7 @@ def prepare_packer_template(config, template_name):
template = env.get_template("{}.json".format(template_name))

# write to temporary file
f = create_cachefd('{}.json'.format(template_name))
f = create_cachefd('{}.json'.format(packer_template_name))
f.write(template.render(config)) # pylint: disable=no-member
f.close()
return f.name
Expand All @@ -167,7 +183,7 @@ def _prepare_vagrantfile(config, source, fd_dest):
fd_dest.close()


def prepare_config(template):
def prepare_config(args):
"""
Prepares Malboxes configuration and merge with Packer template configuration

Expand All @@ -190,36 +206,46 @@ def prepare_config(template):
shutil.copy(resource_filename(__name__, 'config-example.js'),
config_file)

config = load_config(config_file, template)
config = load_config(config_file, args)

if "profile" in config.keys():
profile_config = prepare_profile(template, config)
profile_config = prepare_profile(config, args)

# profile_config might contain a profile not in the config file
config.update(profile_config)

packer_tmpl = prepare_packer_template(config, template)

packer_tmpl = prepare_packer_template(config, args)
# merge/update with template config
with open(packer_tmpl, 'r') as f:
config.update(json.loads(f.read()))
a = f.read()
config.update(json.loads(a))

return config, packer_tmpl


def load_config(config_filename, template):
def load_config(config_filename, args):
"""Loads the minified JSON config. Returns a dict."""

config = {}
with open(config_filename, 'r') as config_file:
# minify then load as JSON
config = json.loads(jsmin(config_file.read()))

if getattr(args, 'profile', None):
config["profile"] = args.profile


config["template"] = args.template
if "profile" in config.keys():
config['template_name'] = "{}_{}".format(args.template, config["profile"])
else:
config['template_name'] = args.template

# add packer required variables
# Note: Backslashes are replaced with forward slashes (Packer on Windows)
config['cache_dir'] = DIRS.user_cache_dir.replace('\\', '/')
config['packer_dir'] = tmp_cache_dir.replace('\\', '/')
config['dir'] = resource_filename(__name__, "").replace('\\', '/')
config['template_name'] = template
config['config_dir'] = DIRS.user_config_dir.replace('\\', '/')

# add default values
Expand Down Expand Up @@ -260,15 +286,13 @@ def _get_os_type(config):

tempfiles = []
def create_cachefd(filename):
tempfiles.append(filename)
return open(os.path.join(DIRS.user_cache_dir, filename), 'w')
return open(os.path.join(tmp_cache_dir, filename), 'w')


def cleanup():
"""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))
shutil.rmtree(tmp_cache_dir)


def run_foreground(command, env=None):
Expand Down Expand Up @@ -302,7 +326,7 @@ def run_packer(packer_tmpl, args):
print("----------------------------------")

prev_cwd = os.getcwd()
os.chdir(DIRS.user_cache_dir)
os.chdir(tmp_cache_dir)

try:
# packer or packer-io?
Expand All @@ -315,16 +339,20 @@ def run_packer(packer_tmpl, args):
return 254

# run packer with relevant config minified
# (removes "profile_config" as packer do not support arrays in var-file)
configfile = os.path.join(DIRS.user_config_dir, 'config.js')
with open(configfile, 'r') as config:
config = json.loads(jsmin(config.read()))
if "profile_config" in config.keys():
del config["profile_config"]
f = create_cachefd('packer_var_file.json')
f.write(jsmin(config.read()))
f.write(json.dumps(config))
f.close()

flags = ['-var-file={}'.format(f.name)]

special_env = {'PACKER_CACHE_DIR': DIRS.user_cache_dir}
special_env['TMPDIR'] = DIRS.user_cache_dir
special_env['TMPDIR'] = tmp_cache_dir
if DEBUG:
special_env['PACKER_LOG'] = '1'
flags.append('-on-error=abort')
Expand All @@ -350,7 +378,7 @@ def add_box(config, args):
print("--------------------------")

box = config['post-processors'][0]['output']
box = os.path.join(DIRS.user_cache_dir, box)
box = os.path.join(tmp_cache_dir, box)
box = box.replace('{{user `name`}}', args.template)

flags = ['--name={}'.format(args.template)]
Expand Down Expand Up @@ -387,7 +415,7 @@ def list_templates(parser, args):
def build(parser, args):

print("Generating configuration files...")
config, packer_tmpl = prepare_config(args.template)
config, packer_tmpl = prepare_config(args)
prepare_autounattend(config)
_prepare_vagrantfile(config, "box_win.rb", create_cachefd('box_win.rb'))
print("Configuration files are ready")
Expand Down Expand Up @@ -425,7 +453,7 @@ def build(parser, args):
You can re-use this base box several times by using `malboxes
spin`. Each VM will be independent of each other.
===============================================================""")
.format(args.template, DIRS.user_cache_dir))
.format(args.template, tmp_cache_dir))


def spin(parser, args):
Expand All @@ -436,7 +464,7 @@ def spin(parser, args):
print("Vagrantfile already exists. Please move it away. Exiting...")
sys.exit(5)

config, _ = prepare_config(args.template)
config, _ = prepare_config(args)

config['template'] = args.template
config['name'] = args.name
Expand All @@ -452,8 +480,9 @@ def spin(parser, args):
"and issue a `vagrant up` to get started with your VM.")


def prepare_profile(template, config):
def prepare_profile(config, args):
"""Converts the profile to a powershell script."""
template = args.template

profile_name = config["profile"]

Expand All @@ -469,6 +498,8 @@ def prepare_profile(template, config):

profile = load_profile(profile_name)

config["profile_config"] = profile

fd = create_cachefd('profile-{}.ps1'.format(profile_name))

if "registry" in profile:
Expand All @@ -483,9 +514,9 @@ def prepare_profile(template, config):
for doc_mod in profile["document"]:
document(profile_name, doc_mod["modtype"], doc_mod["docpath"], fd)

if "package" in profile:
for package_mod in profile["package"]:
package(profile_name, package_mod["package"], fd)
if "packages" in profile:
for pkg in profile["packages"]:
package(profile_name, pkg, fd)

if "packer" in profile:
packer = profile["packer"]
Expand Down Expand Up @@ -543,7 +574,7 @@ def directory(profile_name, modtype, dirpath, fd):

def package(profile_name, package_name, fd):
""" Adds a package to install with Chocolatey."""
line = "choco install {} -y\r\n".format(package_name)
line = "choco install -y {}\r\n".format(package_name)
print("Adding Chocolatey package: {}".format(package_name))

fd.write(line)
Expand Down
14 changes: 13 additions & 1 deletion malboxes/profile-example.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
{
"package": [{"package": "thunderbird"}],
"packages": [
// "dependencywalker --ignorechecksum",
// "regshot --ignorechecksum",
"npcap --package-parameters '/winpcap_mode=yes' -y",
"sysinternals",
"windbg",
"7zip",
"putty",
"fiddler4",
"processhacker",
"apm",
"wireshark"
],
"document": [{"modtype": "add", "docpath": "C:\\Test.doc"}],
"directory": [{"modtype": "add", "dirpath": "C:\\mlbxs\\"}],
"registry": [
Expand Down
2 changes: 1 addition & 1 deletion malboxes/templates/snippets/postprocessor_vagrant.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"post-processors": [{
"type": "vagrant",
"output": "boxes/{{ template_name }}.box",
"vagrantfile_template": "{{ cache_dir }}/box_win.rb"
"vagrantfile_template": "{{ packer_dir }}/box_win.rb"
}]
31 changes: 24 additions & 7 deletions malboxes/templates/snippets/provision_powershell.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,34 @@
{% if hypervisor == "virtualbox" %}
"{{ dir }}/scripts/windows/vmtools.ps1",
{% endif %}
"{{ dir }}/scripts/windows/installtools.ps1",
{% if profile is defined %}"{{ cache_dir }}/profile-{{ profile }}.ps1",{% endif %}
"{{ dir }}/scripts/windows/malware_analysis.ps1"
]
}
{% if choco_packages %},
{% if profile and profile_config and profile_config.packages and profile_config.packages|length > 0 %}
,
{
"type": "windows-shell",
"inline": [
"choco install npcap --package-parameters '/winpcap_mode=yes' -y",
"choco install {{ choco_packages }} -y"
"type": "powershell",
"elevated_user": "{{ username }}",
"elevated_password": "{{ password }}",
"scripts": [
"{{ dir }}/scripts/windows/installtools.ps1"
],
"valid_exit_codes": [ 0, 5888 ]
}
{% endif %}
{% if "win7" in template %}
{# Windows 7 needs to reboot after .net installation #}
,
{
"type": "windows-restart"
}
{% endif %}
{% if profile is defined %}
,
{
"type": "powershell",
"scripts": [
"{{ packer_dir }}/profile-{{ profile }}.ps1"
]
}
{% endif %}
Loading