├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug.yml
│ ├── config.yml
│ └── feature.yml
└── workflows
│ └── linter.yml
├── .gitignore
├── CONTRIBUTING.md
├── Images
├── flarevm-background.png
├── flarevm-logo-old.png
├── flarevm-logo.png
├── installer-gui.png
├── vbox-adapter-check_notification.png
├── vbox-clean-snapshots_after.png
└── vbox-clean-snapshots_before.png
├── LICENSE.txt
├── LayoutModification.xml
├── README.md
├── config.xml
├── install.ps1
├── scripts
└── lint.ps1
└── virtualbox
├── README.md
├── configs
├── remnux.yaml
└── win10_flare-vm.yaml
├── vbox-adapter-check.py
├── vbox-build-flare-vm.py
├── vbox-build-remnux.py
├── vbox-clean-snapshots.py
├── vbox-export-snapshot.py
└── vboxcommon.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set default behaviour, in case users don't have core.autocrlf set.
2 | * text=auto
3 |
4 | # Explicitly declare text files we want to always be normalized and converted
5 | # to native line endings on checkout.
6 | *.md text
7 | *.gitattributes text
8 |
9 | # Declare files that will always have CRLF line endings on checkout.
10 | *.ps1 text eol=crlf
11 | *.psm1 text eol=crlf
12 | *.psd1 text eol=crlf
13 | *.psc1 text eol=crlf
14 | *.ps1xml text eol=crlf
15 | *.clixml text eol=crlf
16 | *.xml text eol=crlf
17 | *.txt text eol=crlf
18 | *.nuspec text eol=crlf
19 | *.reg text eol=crlf
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.yml:
--------------------------------------------------------------------------------
1 | name: 🐛 Bug
2 | description: You need help installing FLARE-VM or something doesn't work as expected
3 | labels: [":bug: bug"]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Thanks for helping improving FLARE-VM. Before submitting your issue:
9 | - Read the [Troubleshooting section in the README](https://github.com/mandiant/flare-vm#troubleshooting).
10 | - We track only bugs related to the installer in this repository. If the issue is related to a concrete tool or package (for example a single package that fails to install), please report it in [VM-Packages](https://github.com/mandiant/VM-Packages/issues/new?assignees=&labels=%3Abug%3A+bug&template=bug.yml).
11 | - Check the [open issues](https://github.com/mandiant/flare-vm/issues) and ensure there is not already a similar issue. If there is already a similar issue, please add more details there instead of opening a new one.
12 | - Ensure you are running the [latest version of the FLARE-VM installer](https://github.com/mandiant/flare-vm/blob/main/install.ps1).
13 | - Ensure your VM satisfies the [requirements](https://github.com/mandiant/flare-vm#requirements) such as having internet connection.
14 | - Fill all the requested information accurately in this issue to ensure we are able to help you.
15 | - If you know how to solve this problem, please send also a pull request! :pray:
16 | - type: textarea
17 | id: problem
18 | attributes:
19 | label: What's the problem?
20 | description: Include the actual and expected behavior. The more details, the better!
21 | validations:
22 | required: true
23 | - type: textarea
24 | id: steps
25 | attributes:
26 | label: Steps to Reproduce
27 | placeholder: |
28 | 1. First Step
29 | 2. Second Step
30 | 3. and so on…
31 | validations:
32 | required: true
33 | - type: textarea
34 | id: environment
35 | attributes:
36 | label: Environment
37 | description: |
38 | Include the following details about your environment:
39 | - **Virtualization software**: VMWare, VirtualBox, etc.
40 | - **VM OS version**: run `(Get-CimInstance Win32_OperatingSystem).version` in Powershell
41 | - **VM PowerShell version**: run `$PSVersionTable.PSVersion.ToString()` in Powershell
42 | - **VM Chocolatey version**: run `choco --version`
43 | - **VM Boxstarter version**: run `choco info -l -r "boxstarter"`
44 | - **Output of `VM-Get-Host-Info`** that will be available if the `vm.common` package has been install: run `VM-Get-Host-Info` in PowerShell with admin rights
45 | placeholder: |
46 | - Virtualization software:
47 | - VM OS version:
48 | - VM PowerShell version:
49 | - VM Chocolatey version:
50 | - VM Boxstarter version:
51 | - Output of `VM-Get-Host-Info`:
52 |
53 | validations:
54 | required: true
55 | - type: textarea
56 | id: extra-info
57 | attributes:
58 | label: Additional Information
59 | description: |
60 | Any additional information, configuration or data that might be necessary to understand and reproduce the issue. For example:
61 | - Console output
62 | - The log files `C:\ProgramData\_VM\log.txt` and `C:\ProgramData\chocolatey\logs\chocolatey.log`
63 |
64 | Text logs are preferred over screenshots.
65 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature.yml:
--------------------------------------------------------------------------------
1 | name: 💡 Feature proposal
2 | description: Propose a new feature or improvement.
3 | body:
4 | - type: markdown
5 | attributes:
6 | value: |
7 | Thanks for helping improving FLARE-VM. Before submitting your issue:
8 | - **If you need help installing FLARE-VM or want to report a bug, use the [bug issue type](https://github.com/mandiant/flare-vm/issues/new?assignees=&labels=%3Abug%3A+bug&template=bug.yml) instead and provide all the information requested there.** Otherwise we won't be able to help you.
9 | - We track only features related to the installer in this repository. If the issue is related to a concrete tool or package, please report it in [VM-Packages](https://github.com/mandiant/VM-Packages/issues/new).
10 | - Check the [open issues](https://github.com/mandiant/flare-vm/issues) and ensure there is not already a similar issue. If there is already a similar issue, please add more details there instead of opening a new one.
11 | - type: textarea
12 | id: problem
13 | attributes:
14 | label: Details
15 | description: The more details, the better!
16 | validations:
17 | required: true
18 |
--------------------------------------------------------------------------------
/.github/workflows/linter.yml:
--------------------------------------------------------------------------------
1 | name: Linter
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | lint:
11 | runs-on: windows-2022
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
15 | - name: Install dependencies
16 | run: pip install black==25.* isort==6.* flake8==7.*
17 | # Different line limits: Black/isort (120), Flake8 (150).
18 | # Flake8 allows longer lines for better long string readability. Black doesn't enforce string length.
19 | - name: Run black
20 | run: black --line-length=120 --check --diff .
21 | - name: Run flake8
22 | run: flake8 --max-line-length=150
23 | - name: Run isort
24 | run: isort --check --diff --profile black --line-length=120 .
25 | - name: Run PowerShell linter
26 | run: scripts/lint.ps1
27 |
28 |
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled source #
2 | ###################
3 | *.com
4 | *.class
5 | *.o
6 | *.so
7 | __pycache__/
8 |
9 | # Packages #
10 | ############
11 | # it's better to unpack these files and commit the raw source
12 | # git has its own built in compression methods
13 | *.7z
14 | *.dmg
15 | *.gz
16 | *.rar
17 | *.tar
18 | *.nupkg
19 |
20 | # Logs and databases #
21 | ######################
22 | *.log
23 | *.sql
24 | *.sqlite
25 |
26 | # OS generated files #
27 | ######################
28 | .DS_Store
29 | .DS_Store?
30 | ._*
31 | .Spotlight-V100
32 | .Trashes
33 | ehthumbs.db
34 | Thumbs.db
35 |
36 | # Pycharm artifacts
37 | ###################
38 | .idea
39 |
40 | # vscode
41 | # #################
42 | .vscode/
43 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | Want to open an issue or send a code contribution?
4 | Read the information below to learn how.
5 | We are looking forward working with you to improve FLARE-VM! :sparkling_heart:
6 |
7 | ## Repository structure of FLARE-VM
8 |
9 | The FLARE-VM code is spited in two repositories:
10 | - **[FLARE-VM](https://github.com/mandiant/flare-vm) (this repository)**: FLARE-VM installation script, and configuration
11 | - [Submit improvement proposals and report issues related to the installer](https://github.com/mandiant/flare-vm/issues/new/choose)
12 |
13 | - **[VM-Packages](https://github.com/mandiant/VM-Packages)**: Source code of tool packages used by FLARE-VM (this repository) and [CommandoVM](https://github.com/mandiant/commando-vm)
14 | - [Documentation and contribution guides for tool packages](https://github.com/mandiant/VM-Packages/wiki)
15 | - [Submit new tool packages or report package related issues](https://github.com/mandiant/VM-Packages/issues/new/choose)
16 |
17 | Before opening an issue, ensure you select the correct repository ([FLARE-VM](https://github.com/mandiant/flare-vm) for the FLARE-VM installer, [VM-Packages](https://github.com/mandiant/VM-Packages) for concrete tools and packages).
18 | Select the correct issue type and read the issue template carefully to ensure you provide all needed information.
19 |
20 | ## Before contributing code
21 |
22 | ### Sign our Contributor License Agreement
23 |
24 | Contributions to this project must be accompanied by a [Contributor License Agreement](https://cla.developers.google.com/about) (CLA).
25 | You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project.
26 |
27 | If you or your current employer have already signed the Google CLA (even if it was for a different project), you probably don't need to do it again.
28 |
29 | Visit to see your current agreements or to sign a new one.
30 |
31 | ## Review our community guidelines
32 |
33 | This project follows [Google's Open Source Community Guidelines](https://opensource.google/conduct).
34 |
--------------------------------------------------------------------------------
/Images/flarevm-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mandiant/flare-vm/43b69f7d61f43782f54afb9a64128ee382825fba/Images/flarevm-background.png
--------------------------------------------------------------------------------
/Images/flarevm-logo-old.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mandiant/flare-vm/43b69f7d61f43782f54afb9a64128ee382825fba/Images/flarevm-logo-old.png
--------------------------------------------------------------------------------
/Images/flarevm-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mandiant/flare-vm/43b69f7d61f43782f54afb9a64128ee382825fba/Images/flarevm-logo.png
--------------------------------------------------------------------------------
/Images/installer-gui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mandiant/flare-vm/43b69f7d61f43782f54afb9a64128ee382825fba/Images/installer-gui.png
--------------------------------------------------------------------------------
/Images/vbox-adapter-check_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mandiant/flare-vm/43b69f7d61f43782f54afb9a64128ee382825fba/Images/vbox-adapter-check_notification.png
--------------------------------------------------------------------------------
/Images/vbox-clean-snapshots_after.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mandiant/flare-vm/43b69f7d61f43782f54afb9a64128ee382825fba/Images/vbox-clean-snapshots_after.png
--------------------------------------------------------------------------------
/Images/vbox-clean-snapshots_before.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mandiant/flare-vm/43b69f7d61f43782f54afb9a64128ee382825fba/Images/vbox-clean-snapshots_before.png
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/LayoutModification.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FLARE-VM
2 | Welcome to FLARE-VM - a collection of software installations scripts for Windows systems that allows you to easily setup and maintain a reverse engineering environment on a virtual machine (VM). FLARE-VM was designed to solve the problem of reverse engineering tool curation and relies on two main technologies: [Chocolatey](https://chocolatey.org) and [Boxstarter](https://boxstarter.org). Chocolatey is a Windows-based Nuget package management system, where a "package" is essentially a ZIP file containing PowerShell installation scripts that download and configure a specific tool. Boxstarter leverages Chocolatey packages to automate the installation of software and create repeatable, scripted Windows environments.
3 |
4 |
5 |
6 |
7 |
8 | ## Requirements
9 | **FLARE-VM should ONLY be installed on a virtual machine**.
10 | The VM should satisfy the following requirements:
11 |
12 | * Windows >= 10
13 | * PowerShell >= 5
14 | * Disk capacity of at least 60 GB and memory of at least 2GB
15 | * Usernames without spaces or other special characters
16 | * Internet connection
17 | * Tamper Protection and any Anti-Malware solution (e.g., Windows Defender) Windows Defender disabled, preferably via Group Policy
18 | * Windows Updates Disabled
19 |
20 | ## Installation instruction
21 | This section documents the steps to install FLARE-VM. You may also find useful the [_Building a VM for Reverse Engineering and Malware Analysis! Installing the FLARE-VM_ video](https://www.youtube.com/watch?v=i8dCyy8WMKY).
22 |
23 | ### Pre-installation
24 | * Prepare a Windows 10+ virtual machine
25 | * Install Windows in the virtual machine, for example using the raw Windows 10 ISO from https://www.microsoft.com/en-us/software-download/windows10ISO
26 | * Ensure the [requirements above](#requirements) are satisfied, including:
27 | * Disable Windows Updates (at least until installation is finished)
28 | * https://www.windowscentral.com/how-stop-updates-installing-automatically-windows-10
29 | * Disable Tamper Protection and any Anti-Malware solution (e.g., Windows Defender), preferably via Group Policy.
30 | * GPO: [https://stackoverflow.com/questions/62174426/how-to-permanently-disable-windows-defender-real-time-protection-with-gpo](https://superuser.com/a/1757341)
31 | * Non-GPO - Manual: [https://www.maketecheasier.com/permanently-disable-windows-defender-windows-10/](https://www.maketecheasier.com/permanently-disable-windows-defender-windows-10)
32 | * Non-GPO - Automated: [https://github.com/ionuttbara/windows-defender-remover](https://github.com/ionuttbara/windows-defender-remover)
33 | * Non-GPO - Semi-Automated (User needs to toggle off Tamper Protection): [https://github.com/AveYo/LeanAndMean/blob/main/ToggleDefender.ps1] (https://github.com/AveYo/LeanAndMean/blob/main/ToggleDefender.ps1)
34 | * Take a VM snapshot so you can always revert to a state before the FLARE-VM installation
35 |
36 | ### FLARE-VM installation
37 | * Open a `PowerShell` prompt as administrator
38 | * Download the installation script [`installer.ps1`](https://raw.githubusercontent.com/mandiant/flare-vm/main/install.ps1) to your Desktop:
39 | * `(New-Object net.webclient).DownloadFile('https://raw.githubusercontent.com/mandiant/flare-vm/main/install.ps1',"$([Environment]::GetFolderPath("Desktop"))\install.ps1")`
40 | * Unblock the installation script:
41 | * `Unblock-File .\install.ps1`
42 | * Enable script execution:
43 | * `Set-ExecutionPolicy Unrestricted -Force`
44 | * If you receive an error saying the execution policy is overridden by a policy defined at a more specific scope, you may need to pass a scope in via `Set-ExecutionPolicy Unrestricted -Scope CurrentUser -Force`. To view execution policies for all scopes, execute `Get-ExecutionPolicy -List`
45 | * Finally, execute the installer script as follow:
46 | * `.\install.ps1`
47 | * To pass your password as an argument: `.\install.ps1 -password `
48 | * To use the CLI-only mode with minimal user interaction: `.\install.ps1 -password -noWait -noGui`
49 | * To use the CLI-only mode with minimal user interaction and a custom config file: `.\install.ps1 -customConfig -password -noWait -noGui`
50 | * After installation it is recommended to switch to `host-only` networking mode and take a VM snapshot
51 |
52 | #### Installer Parameters
53 | Below are the CLI parameter descriptions.
54 |
55 | ```
56 | PARAMETERS
57 | -password
58 | Current user password to allow reboot resiliency via Boxstarter. The script prompts for the password if not provided.
59 |
60 | -noPassword []
61 | Switch parameter indicating a password is not needed for reboots.
62 |
63 | -customConfig
64 | Path to a configuration XML file. May be a file path or URL.
65 |
66 | -customLayout
67 | Path to a taskbar layout XML file. May be a file path or URL.
68 |
69 | -noWait []
70 | Switch parameter to skip installation message before installation begins.
71 |
72 | -noGui []
73 | Switch parameter to skip customization GUI.
74 |
75 | -noReboots []
76 | Switch parameter to prevent reboots (not recommended).
77 |
78 | -noChecks []
79 | Switch parameter to skip validation checks (not recommended).
80 | ```
81 |
82 | Get full usage information by running `Get-Help .\install.ps1 -Detailed`.
83 |
84 | #### Installer GUI
85 |
86 | The Installer GUI is display after executing the validation checks and installing Boxstarter and Chocolatey (if they are not installed already).
87 | Using the installer GUI you may customize:
88 | * Package selection
89 | * Environment variable paths
90 |
91 | 
92 |
93 | #### Configuration
94 |
95 | The installer will download [config.xml](https://raw.githubusercontent.com/mandiant/flare-vm/main/config.xml) from the FLARE-VM repository. This file contains the default configuration, including the list of packages to install and the environment variable paths. You may use your own configuration by specifying the CLI-argument `-customConfig` and providing either a local file path or URL to your `config.xml` file. For example:
96 |
97 | ```
98 | .\install.ps1 -customConfig "https://raw.githubusercontent.com/mandiant/flare-vm/main/config.xml"
99 | ```
100 |
101 | #### Taskbar Layout
102 | The installer will use [CustomStartLayout.xml](https://raw.githubusercontent.com/mandiant/flare-vm/main/CustomStartLayout.xml) from the FLARE-VM repository. This file contains the default taskbar layout. You may use your own configuration by specifying the CLI-argument `-customLayout` and providing a local file path or URL to your `CustomStartLayout.xml` file. For example:
103 |
104 | ```
105 | .\install.ps1 -customLayout "https://raw.githubusercontent.com/mandiant/flare-vm/main/CustomStartLayout.xml"
106 | ```
107 |
108 | ##### Things to Consider:
109 | - Items in the .xml that are not installed will not display in the taskbar (no broken links will be pinned)
110 | - Only applications (`.exe` files) or shortcuts to applications can be pinned.
111 | - If you would like to pin something that isn't an application, consider creating a shortcut that points to `cmd.exe` or `powershell` with arguments supplied that will perform that actions you would like.
112 | - If you would like to make something run with admin rights, consider making a shortcut using `VM-Install-Shortcut` with the flag `-runAsAdmin` and pinning the shortcut.
113 |
114 |
115 | #### Post installation steps
116 | You can include any post installation step you like in the configuration inside the tags `apps`, `services`, `path-items`, `registry-items`, and `custom-items`.
117 |
118 | For example:
119 | - To show known file extensions:
120 | ```xml
121 |
122 |
123 |
124 | ```
125 |
126 | For more examples, check the default configuration file: [config.xml](https://raw.githubusercontent.com/mandiant/flare-vm/main/config.xml).
127 |
128 |
129 | ## Contributing
130 |
131 | - Check our [CONTRIBUTING guide](/CONTRIBUTING.md) to learn how to contribute to the project.
132 |
133 | ## Troubleshooting
134 | If your installation fails, please attempt to identify the reason for the installation error by reading through the log files listed below on your system:
135 | * `%VM_COMMON_DIR%\log.txt`
136 | * `%PROGRAMDATA%\chocolatey\logs\chocolatey.log`
137 | * `%LOCALAPPDATA%\Boxstarter\boxstarter.log`
138 |
139 | Ensure you are running the latest version of the FLARE-VM installer and that your VM satisfies the [requirements](#requirements).
140 |
141 | ### Installer Error
142 | If the installation failed due to an issue in the installation script (e.g., `install.ps1`), [report the bug in FLARE-VM](https://github.com/mandiant/flare-vm/issues/new?labels=%3Abug%3A+bug&template=bug.yml).
143 | Provide all the information requested to ensure we are able to help you.
144 |
145 | > **Note:** Rarely should `install.ps1` be the reason for an installation failure. Most likely it is a specific package or set of packages that are failing (see below).
146 |
147 | ### Package Error
148 | Packages fail to install from time to time -- this is normal. The most common reasons are outlined below:
149 |
150 | 1. Failure or timeout from Chocolatey or MyGet to download a `.nupkg` file
151 | 2. Failure or timeout due to remote host when downloading a tool
152 | 3. Intrusion Detection System (IDS) or AV product (e.g., Windows Defender) prevents a tool download or removes the tool from the system
153 | 4. Host specific issue, for example when using an untested version
154 | 5. Tool fails to build due to dependencies
155 | 6. Old tool URL (e.g., `HTTP STATUS 404`)
156 | 7. Tool's SHA256 hash has changed from what is hardcoded in the package installation script
157 |
158 | Reasons **1-4** are difficult for us to fix since we do not control them. If an issue related to reasons **1-4** is filed, it is unlikely we will be able to assist.
159 |
160 | We can help with reasons **5-7** and welcome the community to contribute fixes as well!
161 | Please [report the bug in VM-Packages](https://github.com/mandiant/VM-Packages/issues/new?labels=%3Abug%3A+bug&template=bug.yml) providing all the information requested.
162 |
163 | ### Updates
164 |
165 | Note that package updates are best effort and that updates are not being tested.
166 | If you encounter errors, perform a fresh FLARE-VM install.
167 |
168 | ## Legal Notice
169 | > This download configuration script is provided to assist cyber security analysts in creating handy and versatile toolboxes for malware analysis environments. It provides a convenient interface for them to obtain a useful set of analysis tools directly from their original sources. Installation and use of this script is subject to the Apache 2.0 License. You as a user of this script must review, accept and comply with the license terms of each downloaded/installed package. By proceeding with the installation, you are accepting the license terms of each package, and acknowledging that your use of each package will be subject to its respective license terms.
170 |
171 |
--------------------------------------------------------------------------------
/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
163 |
164 |
165 |
173 |
174 |
175 |
183 |
184 |
185 |
191 |
192 |
193 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
213 |
214 |
215 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
236 |
237 |
238 |
239 |
240 |
241 |
--------------------------------------------------------------------------------
/install.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | Copyright 2017 Google LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | #>
16 |
17 | <#
18 | .SYNOPSIS
19 | Installation script for FLARE VM.
20 | ** Only install on a virtual machine! **
21 |
22 | .DESCRIPTION
23 | Installation script for FLARE VM that leverages Chocolatey and Boxstarter.
24 | Script verifies minimal settings necessary to install FLARE VM on a virtual machine.
25 | Script allows users to customize package selection and envrionment variables used in FLARE VM via a GUI before installation begins.
26 | A CLI-only mode is also available by providing specific command-line arugment switches.
27 |
28 | To execute this script:
29 | 1) Open PowerShell window as administrator
30 | 2) Allow script execution by running command "Set-ExecutionPolicy Unrestricted"
31 | 3) Unblock the install script by running "Unblock-File .\install.ps1"
32 | 4) Execute the script by running ".\install.ps1"
33 |
34 | .PARAMETER password
35 | Current user password to allow reboot resiliency via Boxstarter. The script prompts for the password if not provided.
36 |
37 | .PARAMETER noPassword
38 | Switch parameter indicating a password is not needed for reboots.
39 |
40 | .PARAMETER customConfig
41 | Path to a configuration XML file. May be a file path or URL.
42 |
43 | .PARAMETER customLayout
44 | Path to a taskbar layout XML file. May be a file path or URL.
45 |
46 | .PARAMETER noWait
47 | Switch parameter to skip installation message before installation begins.
48 |
49 | .PARAMETER noGui
50 | Switch parameter to skip customization GUI.
51 |
52 | .PARAMETER noReboots
53 | Switch parameter to prevent reboots (not recommended).
54 |
55 | .PARAMETER noChecks
56 | Switch parameter to skip validation checks (not recommended).
57 |
58 | .EXAMPLE
59 | .\install.ps1
60 |
61 | Description
62 | ---------------------------------------
63 | Execute the installer to configure FLARE VM.
64 |
65 | .EXAMPLE
66 | .\install.ps1 -password Passw0rd! -noWait -noGui -noChecks
67 |
68 | Description
69 | ---------------------------------------
70 | CLI-only installation with minimal user interaction (some packages may require user interaction).
71 | To prevent reboots, also add the "-noReboots" switch.
72 |
73 | .EXAMPLE
74 | .\install.ps1 -customConfig "https://raw.githubusercontent.com/mandiant/flare-vm/main/config.xml"
75 |
76 | Description
77 | ---------------------------------------
78 | Use a custom configuration XML file hosted on the internet.
79 |
80 | .LINK
81 | https://github.com/mandiant/flare-vm
82 | https://github.com/mandiant/VM-Packages
83 | #>
84 |
85 | param (
86 | [string]$password = $null,
87 | [switch]$noPassword,
88 | [string]$customConfig = $null,
89 | [string]$customLayout = $null,
90 | [switch]$noWait,
91 | [switch]$noGui,
92 | [switch]$noReboots,
93 | [switch]$noChecks
94 | )
95 | $ErrorActionPreference = 'Stop'
96 | $ProgressPreference = 'SilentlyContinue'
97 |
98 | # Function to download files and handle errors consistently
99 | function Save-FileFromUrl {
100 | param (
101 | [string]$fileSource,
102 | [string]$fileDestination,
103 | [switch]$exitOnError
104 | )
105 | Write-Host "[+] Downloading file from '$fileSource'"
106 | try {
107 | (New-Object net.webclient).DownloadFile($fileSource,$FileDestination)
108 | } catch {
109 | Write-Host "`t[!] Failed to download '$fileSource'"
110 | Write-Host "`t[!] $_"
111 | if ($exitOnError) {
112 | Start-Sleep 3
113 | exit 1
114 | }
115 | }
116 | }
117 |
118 | # Function to test the network stack. Ping/GET requests to the resource to ensure that network stack looks good for installation
119 | function Test-WebConnection {
120 | param (
121 | [string]$url
122 | )
123 |
124 | Write-Host "[+] Checking for Internet connectivity ($url)..."
125 |
126 | if (-not (Test-Connection $url -Quiet)) {
127 | Write-Host "`t[!] It looks like you cannot ping $url. Check your network settings." -ForegroundColor Red
128 | Start-Sleep 3
129 | exit 1
130 | }
131 |
132 | $response = $null
133 | try {
134 | $response = Invoke-WebRequest -Uri "https://$url" -UseBasicParsing -DisableKeepAlive
135 | }
136 | catch {
137 | Write-Host "`t[!] Error accessing $url. Exception: $($_.Exception.Message)`n`t[!] Check your network settings." -ForegroundColor Red
138 | Start-Sleep 3
139 | exit 1
140 | }
141 |
142 | if ($response -and $response.StatusCode -ne 200) {
143 | Write-Host "`t[!] Unable to access $url. Status code: $($response.StatusCode)`n`t[!] Check your network settings." -ForegroundColor Red
144 | Start-Sleep 3
145 | exit 1
146 | }
147 |
148 | Write-Host "`t[+] Internet connectivity check for $url passed" -ForegroundColor Green
149 | }
150 |
151 | # Function used for getting configuration files (such as config.xml and LayoutModification.xml)
152 | function Get-ConfigFile {
153 | param (
154 | [string]$fileDestination,
155 | [string]$fileSource
156 | )
157 | # Check if the source is an existing file path.
158 | if (-not (Test-Path $fileSource)) {
159 | # If the source doesn't exist, assume it's a URL and download the file.
160 | Save-FileFromUrl -fileSource $fileSource -fileDestination $fileDestination
161 | } else {
162 | # If the source exists as a file, move it to the destination.
163 | Write-Host "[+] Using existing file as configuration file."
164 | Move-Item -Path $fileSource -Destination $fileDestination -Force
165 | }
166 | }
167 |
168 | # Set path to user's desktop
169 | $desktopPath = [Environment]::GetFolderPath("Desktop")
170 | Set-Location -Path $desktopPath -PassThru | Out-Null
171 |
172 | if (-not $noChecks.IsPresent) {
173 | # Check PowerShell version
174 | Write-Host "[+] Checking if PowerShell version is compatible..."
175 | $psVersion = $PSVersionTable.PSVersion
176 | if ($psVersion -lt [System.Version]"5.0.0") {
177 | Write-Host "`t[!] You are using PowerShell version $psVersion. This is an old version and it is not supported" -ForegroundColor Red
178 | Read-Host "Press any key to exit..."
179 | exit 1
180 | } else {
181 | Write-Host "`t[+] Installing with PowerShell version $psVersion" -ForegroundColor Green
182 | }
183 |
184 | # Ensure script is ran as administrator
185 | Write-Host "[+] Checking if script is running as administrator..."
186 | $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
187 | if (-Not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
188 | Write-Host "`t[!] Please run this script as administrator" -ForegroundColor Red
189 | Read-Host "Press any key to exit..."
190 | exit 1
191 | } else {
192 | Write-Host "`t[+] Running as administrator" -ForegroundColor Green
193 | Start-Sleep -Milliseconds 500
194 | }
195 |
196 | # Ensure execution policy is unrestricted
197 | Write-Host "[+] Checking if execution policy is unrestricted..."
198 | if ((Get-ExecutionPolicy).ToString() -ne "Unrestricted") {
199 | Write-Host "`t[!] Please run this script after updating your execution policy to unrestricted" -ForegroundColor Red
200 | Write-Host "`t[-] Hint: Set-ExecutionPolicy Unrestricted" -ForegroundColor Yellow
201 | Read-Host "Press any key to exit..."
202 | exit 1
203 | } else {
204 | Write-Host "`t[+] Execution policy is unrestricted" -ForegroundColor Green
205 | Start-Sleep -Milliseconds 500
206 | }
207 |
208 | # Check if Windows < 10
209 | $os = Get-CimInstance -Class Win32_OperatingSystem
210 | $osMajorVersion = $os.Version.Split('.')[0] # Version examples: "6.1.7601", "10.0.19045"
211 | Write-Host "[+] Checking Operating System version compatibility..."
212 | if ($osMajorVersion -lt 10) {
213 | Write-Host "`t[!] Only Windows >= 10 is supported" -ForegroundColor Yellow
214 | Write-Host "[-] Do you still wish to proceed? (Y/N): " -ForegroundColor Yellow -NoNewline
215 | $response = Read-Host
216 | if ($response -notin @("y","Y")) {
217 | exit 1
218 | }
219 | }
220 |
221 | # Check if host has been tested
222 | # 17763: the version used by windows-2019 in GH actions
223 | # 19045: https://www.microsoft.com/en-us/software-download/windows10ISO downloaded on April 25 2023.
224 | # 20348: the version used by windows-2022 in GH actions
225 | $testedVersions = @(17763, 19045, 20348)
226 | if ($os.BuildNumber -notin $testedVersions) {
227 | Write-Host "`t[!] Windows version $osVersion has not been tested. Tested versions: $($testedVersions -join ', ')" -ForegroundColor Yellow
228 | Write-Host "`t[+] You are welcome to continue, but may experience errors downloading or installing packages" -ForegroundColor Yellow
229 | Write-Host "[-] Do you still wish to proceed? (Y/N): " -ForegroundColor Yellow -NoNewline
230 | $response = Read-Host
231 | if ($response -notin @("y","Y")) {
232 | exit 1
233 | }
234 | } else {
235 | Write-Host "`t[+] Installing on Windows version $osVersion" -ForegroundColor Green
236 | }
237 |
238 | # Check if system is a virtual machine
239 | $virtualModels = @('VirtualBox', 'VMware', 'Virtual Machine', 'Hyper-V')
240 | $computerSystemModel = (Get-CimInstance -Class Win32_ComputerSystem).Model
241 | $isVirtualModel = $false
242 |
243 | foreach ($model in $virtualModels) {
244 | if ($computerSystemModel.Contains($model)) {
245 | $isVirtualModel = $true
246 | break
247 | }
248 | }
249 |
250 | if (!$isVirtualModel) {
251 | Write-Host "`t[!] You are not on a virual machine or have hardened your machine to not appear as a virtual machine" -ForegroundColor Red
252 | Write-Host "`t[!] Please do NOT install this on your host system as it can't be uninstalled completely" -ForegroundColor Red
253 | Write-Host "`t[!] ** Please only install on a virtual machine **" -ForegroundColor Red
254 | Write-Host "`t[!] ** Only continue if you know what you are doing! **" -ForegroundColor Red
255 | Write-Host "[-] Do you still wish to proceed? (Y/N): " -ForegroundColor Yellow -NoNewline
256 | $response = Read-Host
257 | if ($response -notin @("y","Y")) {
258 | exit 1
259 | }
260 | }
261 |
262 | # Check for spaces in the username, exit if identified
263 | Write-Host "[+] Checking for spaces in the username..."
264 | if (${Env:UserName} -match '\s') {
265 | Write-Host "`t[!] Username '${Env:UserName}' contains a space and will break installation." -ForegroundColor Red
266 | Write-Host "`t[!] Exiting..." -ForegroundColor Red
267 | Start-Sleep 3
268 | exit 1
269 | } else {
270 | Write-Host "`t[+] Username '${Env:UserName}' does not contain any spaces." -ForegroundColor Green
271 | }
272 |
273 | # Check if host has enough disk space
274 | Write-Host "[+] Checking if host has enough disk space..."
275 | $disk = Get-PSDrive (Get-Location).Drive.Name
276 | Start-Sleep -Seconds 1
277 | if (-Not (($disk.used + $disk.free)/1GB -gt 58.8)) {
278 | Write-Host "`t[!] A minimum of 60 GB hard drive space is preferred. Please increase hard drive space of the VM, reboot, and retry install" -ForegroundColor Red
279 | Write-Host "`t[+] If you have multiple drives, you may change the tool installation location via the envrionment variable %RAW_TOOLS_DIR% in config.xml or GUI" -ForegroundColor Yellow
280 | Write-Host "`t[+] However, packages provided from the Chocolatey community repository will install to their default location" -ForegroundColor Yellow
281 | Write-Host "`t[+] See: https://stackoverflow.com/questions/19752533/how-do-i-set-chocolatey-to-install-applications-onto-another-drive" -ForegroundColor Yellow
282 | Write-Host "[-] Do you still wish to proceed? (Y/N): " -ForegroundColor Yellow -NoNewline
283 | $response = Read-Host
284 | if ($response -notin @("y","Y")) {
285 | exit 1
286 | }
287 | } else {
288 | Write-Host "`t[+] Disk is larger than 60 GB" -ForegroundColor Green
289 | }
290 |
291 | # Internet connectivity checks
292 | Test-WebConnection 'google.com'
293 | Test-WebConnection 'github.com'
294 | Test-WebConnection 'raw.githubusercontent.com'
295 |
296 | Write-Host "`t[+] Network connectivity looks good" -ForegroundColor Green
297 |
298 | # Check if Tamper Protection is disabled
299 | Write-Host "[+] Checking if Windows Defender Tamper Protection is disabled..."
300 | try {
301 | $tpEnabled = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows Defender\Features" -Name "TamperProtection" -ErrorAction Stop
302 | if ($tpEnabled.TamperProtection -eq 5) {
303 | Write-Host "`t[!] Please disable Tamper Protection, reboot, and rerun installer" -ForegroundColor Red
304 | Write-Host "`t[+] Hint: https://support.microsoft.com/en-us/windows/prevent-changes-to-security-settings-with-tamper-protection-31d51aaa-645d-408e-6ce7-8d7f8e593f87" -ForegroundColor Yellow
305 | Write-Host "`t[+] Hint: https://www.tenforums.com/tutorials/123792-turn-off-tamper-protection-windows-defender-antivirus.html" -ForegroundColor Yellow
306 | Write-Host "`t[+] Hint: https://github.com/jeremybeaume/tools/blob/master/disable-defender.ps1" -ForegroundColor Yellow
307 | Write-Host "`t[+] Hint: https://lazyadmin.nl/win-11/turn-off-windows-defender-windows-11-permanently/" -ForegroundColor Yellow
308 | Write-Host "`t[+] You are welcome to continue, but may experience errors downloading or installing packages" -ForegroundColor Yellow
309 | Write-Host "`t[-] Do you still wish to proceed? (Y/N): " -ForegroundColor Yellow -NoNewline
310 | $response = Read-Host
311 | if ($response -notin @("y","Y")) {
312 | exit 1
313 | }
314 | } else {
315 | Write-Host "`t[+] Tamper Protection is disabled" -ForegroundColor Green
316 | Start-Sleep -Milliseconds 500
317 | }
318 | } catch {
319 | Write-Host "`t[+] Tamper Protection is either not enabled or not detected" -ForegroundColor Yellow
320 | Write-Host "`t[-] Do you still wish to proceed? (Y/N): " -ForegroundColor Yellow -NoNewline
321 | $response = Read-Host
322 | if ($response -notin @("y","Y")) {
323 | exit 1
324 | }
325 | Start-Sleep -Milliseconds 500
326 | }
327 |
328 | # Check if Defender is disabled
329 | Write-Host "[+] Checking if Windows Defender service is disabled..."
330 | $defender = Get-Service -Name WinDefend -ea 0
331 | if ($null -ne $defender) {
332 | if ($defender.Status -eq "Running") {
333 | Write-Host "`t[!] Please disable Windows Defender through Group Policy, reboot, and rerun installer" -ForegroundColor Red
334 | Write-Host "`t[+] Hint: https://stackoverflow.com/questions/62174426/how-to-permanently-disable-windows-defender-real-time-protection-with-gpo" -ForegroundColor Yellow
335 | Write-Host "`t[+] Hint: https://www.windowscentral.com/how-permanently-disable-windows-defender-windows-10" -ForegroundColor Yellow
336 | Write-Host "`t[+] Hint: https://github.com/jeremybeaume/tools/blob/master/disable-defender.ps1" -ForegroundColor Yellow
337 | Write-Host "`t[+] You are welcome to continue, but may experience errors downloading or installing packages" -ForegroundColor Yellow
338 | Write-Host "`t[-] Do you still wish to proceed? (Y/N): " -ForegroundColor Yellow -NoNewline
339 | $response = Read-Host
340 | if ($response -notin @("y","Y")) {
341 | exit 1
342 | }
343 | } else {
344 | Write-Host "`t[+] Defender is disabled" -ForegroundColor Green
345 | Start-Sleep -Milliseconds 500
346 | }
347 | }
348 |
349 | Write-Host "[+] Setting password to never expire to avoid that a password expiration blocks the installation..."
350 | $UserNoPasswd = Get-CimInstance Win32_UserAccount -Filter "Name='${Env:UserName}'"
351 | $UserNoPasswd | Set-CimInstance -Property @{ PasswordExpires = $false }
352 |
353 | # Prompt user to remind them to take a snapshot
354 | Write-Host "[-] Have you taken a VM snapshot to ensure you can revert to pre-installation state? (Y/N): " -ForegroundColor Yellow -NoNewline
355 | $response = Read-Host
356 | if ($response -notin @("y","Y")) {
357 | exit 1
358 | }
359 | }
360 |
361 | if (-not $noPassword.IsPresent) {
362 | # Get user credentials for autologin during reboots
363 | if ([string]::IsNullOrEmpty($password)) {
364 | Write-Host "[+] Getting user credentials ..."
365 | Set-ItemProperty "HKLM:\SOFTWARE\Microsoft\PowerShell\1\ShellIds" -Name "ConsolePrompting" -Value $True
366 | Start-Sleep -Milliseconds 500
367 | $credentials = Get-Credential ${Env:UserName}
368 | } else {
369 | $securePassword = ConvertTo-SecureString -String $password -AsPlainText -Force
370 | $credentials = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList ${Env:UserName}, $securePassword
371 | }
372 | }
373 |
374 | # Check Boxstarter version
375 | $boxstarterVersionGood = $false
376 | if (${Env:ChocolateyInstall} -and (Test-Path "${Env:ChocolateyInstall}\bin\choco.exe")) {
377 | choco info -l -r "boxstarter" | ForEach-Object { $name, $version = $_ -split '\|' }
378 | $boxstarterVersionGood = [System.Version]$version -ge [System.Version]"3.0.2"
379 | }
380 |
381 | # Install Boxstarter if needed
382 | if (-not $boxstarterVersionGood) {
383 | Write-Host "[+] Installing Boxstarter..." -ForegroundColor Cyan
384 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
385 | Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://boxstarter.org/bootstrapper.ps1'))
386 | Get-Boxstarter -Force
387 |
388 | Start-Sleep -Milliseconds 500
389 | }
390 | Import-Module "${Env:ProgramData}\boxstarter\boxstarter.chocolatey\boxstarter.chocolatey.psd1" -Force
391 |
392 | # Check Chocolatey version
393 | $version = choco --version
394 | $chocolateyVersionGood = [System.Version]$version -ge [System.Version]"2.0.0"
395 |
396 | # Update Chocolatey if needed
397 | if (-not ($chocolateyVersionGood)) { choco upgrade chocolatey }
398 |
399 | # Attempt to disable updates (i.e., windows updates and store updates)
400 | Write-Host "[+] Attempting to disable updates..."
401 | Disable-MicrosoftUpdate
402 | try {
403 | New-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\WindowsStore" -Name "AutoDownload" -PropertyType DWord -Value 2 -ErrorAction Stop -Force | Out-Null
404 | } catch {
405 | Write-Host "`t[!] Failed to disable Microsoft Store updates" -ForegroundColor Yellow
406 | }
407 |
408 | # Set Boxstarter options
409 | $Boxstarter.RebootOk = (-not $noReboots.IsPresent)
410 | $Boxstarter.NoPassword = $noPassword.IsPresent
411 | $Boxstarter.AutoLogin = $true
412 | $Boxstarter.SuppressLogging = $True
413 | $global:VerbosePreference = "SilentlyContinue"
414 | Set-BoxstarterConfig -NugetSources "$desktopPath;.;https://www.myget.org/F/vm-packages/api/v2;https://myget.org/F/vm-packages/api/v2;https://chocolatey.org/api/v2"
415 | Set-WindowsExplorerOptions -EnableShowHiddenFilesFoldersDrives -EnableShowProtectedOSFiles -EnableShowFileExtensions -EnableShowFullPathInTitleBar
416 |
417 | # Set Chocolatey options
418 | Write-Host "[+] Updating Chocolatey settings..."
419 | choco sources add -n="vm-packages" -s "$desktopPath;.;https://www.myget.org/F/vm-packages/api/v2;https://myget.org/F/vm-packages/api/v2" --priority 1
420 | choco feature enable -n allowGlobalConfirmation
421 | choco feature enable -n allowEmptyChecksums
422 | $cache = "${Env:LocalAppData}\ChocoCache"
423 | New-Item -Path $cache -ItemType directory -Force | Out-Null
424 | choco config set cacheLocation $cache
425 |
426 | # Set power options to prevent installs from timing out
427 | powercfg -change -monitor-timeout-ac 0 | Out-Null
428 | powercfg -change -monitor-timeout-dc 0 | Out-Null
429 | powercfg -change -disk-timeout-ac 0 | Out-Null
430 | powercfg -change -disk-timeout-dc 0 | Out-Null
431 | powercfg -change -standby-timeout-ac 0 | Out-Null
432 | powercfg -change -standby-timeout-dc 0 | Out-Null
433 | powercfg -change -hibernate-timeout-ac 0 | Out-Null
434 | powercfg -change -hibernate-timeout-dc 0 | Out-Null
435 |
436 | Write-Host "[+] Checking for configuration file..."
437 | $configPath = Join-Path $desktopPath "config.xml"
438 | if ([string]::IsNullOrEmpty($customConfig)) {
439 | Write-Host "[+] Using github configuration file..."
440 | $configSource = 'https://raw.githubusercontent.com/mandiant/flare-vm/main/config.xml'
441 | } else {
442 | Write-Host "[+] Using custom configuration file..."
443 | $configSource = $customConfig
444 | }
445 |
446 | Get-ConfigFile $configPath $configSource
447 |
448 | Write-Host "Configuration file path: $configPath"
449 |
450 | # Check the configuration file exists
451 | if (-Not (Test-Path $configPath)) {
452 | Write-Host "`t[!] Configuration file missing: " $configPath -ForegroundColor Red
453 | Write-Host "`t[-] Please download config.xml from $configPathUrl to your desktop" -ForegroundColor Yellow
454 | Write-Host "`t[-] Is the file on your desktop? (Y/N): " -ForegroundColor Yellow -NoNewline
455 | $response = Read-Host
456 | if ($response -notin @("y","Y")) {
457 | exit 1
458 | }
459 | if (-Not (Test-Path $configPath)) {
460 | Write-Host "`t[!] Configuration file still missing: " $configPath -ForegroundColor Red
461 | Write-Host "`t[!] Exiting..." -ForegroundColor Red
462 | Start-Sleep 3
463 | exit 1
464 | }
465 | }
466 |
467 | # Get config contents
468 | Start-Sleep 1
469 | $configXml = [xml](Get-Content $configPath)
470 |
471 | if (-not $noGui.IsPresent) {
472 | Write-Host "[+] Starting GUI to allow user to edit configuration file..."
473 | ################################################################################
474 | ## BEGIN GUI
475 | ################################################################################
476 | Add-Type -AssemblyName System.Windows.Forms
477 |
478 |
479 | function Get-Folder($textBox, $envVar) {
480 | $folderBrowserDialog = New-Object System.Windows.Forms.FolderBrowserDialog
481 | $folderBrowserDialog.RootFolder = 'MyComputer'
482 | if ($folderBrowserDialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
483 | $textbox.text = (Join-Path $folderBrowserDialog.SelectedPath (Split-Path $envs[$envVar] -Leaf))
484 | }
485 | }
486 |
487 | # Function that accesses MyGet vm-packages API URL to process packages that are the latest version and have a category
488 | # Saves vm-packages.xml into disk and follows the link after the tag to retrieve a new version of the XML file
489 | # Returns $packagesByCategory, a hashtable of arrays, where each entry is a PSCustomObject
490 | function Get-Packages-Categories {
491 | # MyGet API URL that contains a filter to display only the latest packages
492 | # This URL displays the last two versions of a package
493 | # Minimize the number of HTTP requests to display all the packages due to the number of versions a package might have
494 | $vmPackagesUrl = "https://www.myget.org/F/vm-packages/api/v2/Packages?$filter=IsLatestVersion%20eq%20true"
495 | $vmPackagesFile = "${Env:VM_COMMON_DIR}\vm-packages.xml"
496 | $packagesByCategory=@{}
497 | do {
498 | # Download the XML from MyGet API
499 | Save-FileFromUrl -fileSource $vmPackagesUrl -fileDestination $vmPackagesFile --exitOnError
500 |
501 | # Load the XML content
502 | [xml]$vm_packages = Get-Content $vmPackagesFile
503 |
504 | # Define the namespaces defined in vm-packages.xml to access nodes
505 | # Each package resides in the entry node that is defined in the dataservices namespace
506 | # Each node has properties that are defined in the metadata namespace
507 | $ns = New-Object System.Xml.XmlNamespaceManager($vm_packages.NameTable)
508 | $ns.AddNamespace("atom", "http://www.w3.org/2005/Atom")
509 | $ns.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices")
510 | $ns.AddNamespace("m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata")
511 |
512 | # Extract package information from the XML
513 | $vm_packages.feed.entry | ForEach-Object {
514 | $isLatestVersion = $_.SelectSingleNode("m:properties/d:IsLatestVersion", $ns).InnerText
515 | $category = $_.SelectSingleNode("m:properties/d:Tags", $ns).InnerText
516 | # Select only packages that have the latest version and contain a category
517 | if (($isLatestVersion -eq "true") -and ($category -ne "")) {
518 | $packageName = $_.properties.Id
519 | $description = $_.properties.Description
520 |
521 | # Initialize category as an empty array
522 | if (-not ($packagesByCategory.ContainsKey($category))) {
523 | $packagesByCategory[$category] = @()
524 | }
525 | # Add the PackageName and PackageDesccription to each entry in the array
526 | $packagesByCategory[$category] += [PSCustomObject]@{
527 | PackageName = $packageName
528 | PackageDescription = $description
529 | }
530 | }
531 | }
532 | # Check if there is a next link in the XML and set the API URL to that link if it exists
533 | $nextLink = $vm_packages.SelectSingleNode("//atom:link[@rel='next']/@href", $ns)
534 | $vmPackagesUrl = $nextLink."#text"
535 |
536 | } while ($vmPackagesUrl)
537 |
538 | return $packagesByCategory
539 | }
540 |
541 | # Gather lists of packages
542 | $envs = [ordered]@{}
543 | $configXml.config.envs.env.ForEach({ $envs[$_.name] = $_.value })
544 | $packagesByCategory = Get-Packages-Categories
545 |
546 | $formEnv = New-Object system.Windows.Forms.Form
547 | $formEnv.ClientSize = New-Object System.Drawing.Point(750,350)
548 | $formEnv.text = "FLARE VM Install Customization"
549 | $formEnv.TopMost = $true
550 | $formEnv.MaximizeBox = $false
551 | $formEnv.FormBorderStyle = 'FixedDialog'
552 | $formEnv.StartPosition = 'CenterScreen'
553 |
554 | $envVarGroup = New-Object system.Windows.Forms.Groupbox
555 | $envVarGroup.height = 201
556 | $envVarGroup.width = 690
557 | $envVarGroup.text = "Environment Variable Customization"
558 | $envVarGroup.location = New-Object System.Drawing.Point(15,59)
559 |
560 | $welcomeLabel = New-Object system.Windows.Forms.Label
561 | $welcomeLabel.text = "Welcome to FLARE VM's custom installer. Please select your options below.`nDefault values will be used if you make no modifications."
562 | $welcomeLabel.AutoSize = $true
563 | $welcomeLabel.width = 25
564 | $welcomeLabel.height = 10
565 | $welcomeLabel.location = New-Object System.Drawing.Point(15,14)
566 | $welcomeLabel.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
567 |
568 | $vmCommonDirText = New-Object system.Windows.Forms.TextBox
569 | $vmCommonDirText.multiline = $false
570 | $vmCommonDirText.width = 385
571 | $vmCommonDirText.height = 20
572 | $vmCommonDirText.location = New-Object System.Drawing.Point(190,21)
573 | $vmCommonDirText.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
574 | $vmCommonDirText.text = $envs['VM_COMMON_DIR']
575 |
576 | $vmCommonDirSelect = New-Object system.Windows.Forms.Button
577 | $vmCommonDirSelect.text = "Select Folder"
578 | $vmCommonDirSelect.width = 95
579 | $vmCommonDirSelect.height = 30
580 | $vmCommonDirSelect.location = New-Object System.Drawing.Point(588,17)
581 | $vmCommonDirSelect.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
582 | $selectFolderArgs1 = @{textBox=$vmCommonDirText; envVar="VM_COMMON_DIR"}
583 | $vmCommonDirSelect.Add_Click({Get-Folder @selectFolderArgs1})
584 |
585 | $vmCommonDirLabel = New-Object system.Windows.Forms.Label
586 | $vmCommonDirLabel.text = "%VM_COMMON_DIR%"
587 | $vmCommonDirLabel.AutoSize = $true
588 | $vmCommonDirLabel.width = 25
589 | $vmCommonDirLabel.height = 10
590 | $vmCommonDirLabel.location = New-Object System.Drawing.Point(2,24)
591 | $vmCommonDirLabel.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',9.5,[System.Drawing.FontStyle]::Bold)
592 |
593 | $vmCommonDirNote = New-Object system.Windows.Forms.Label
594 | $vmCommonDirNote.text = "Shared module and metadata for VM (e.g., config, logs, etc...)"
595 | $vmCommonDirNote.AutoSize = $true
596 | $vmCommonDirNote.width = 25
597 | $vmCommonDirNote.height = 10
598 | $vmCommonDirNote.location = New-Object System.Drawing.Point(190,46)
599 | $vmCommonDirNote.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
600 |
601 | $toolListDirText = New-Object system.Windows.Forms.TextBox
602 | $toolListDirText.multiline = $false
603 | $toolListDirText.width = 385
604 | $toolListDirText.height = 20
605 | $toolListDirText.location = New-Object System.Drawing.Point(190,68)
606 | $toolListDirText.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
607 | $toolListDirText.text = $envs['TOOL_LIST_DIR']
608 |
609 | $toolListDirSelect = New-Object system.Windows.Forms.Button
610 | $toolListDirSelect.text = "Select Folder"
611 | $toolListDirSelect.width = 95
612 | $toolListDirSelect.height = 30
613 | $toolListDirSelect.location = New-Object System.Drawing.Point(588,64)
614 | $toolListDirSelect.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
615 | $selectFolderArgs2 = @{textBox=$toolListDirText; envVar="TOOL_LIST_DIR"}
616 | $toolListDirSelect.Add_Click({Get-Folder @selectFolderArgs2})
617 |
618 | $toolListDirLabel = New-Object system.Windows.Forms.Label
619 | $toolListDirLabel.text = "%TOOL_LIST_DIR%"
620 | $toolListDirLabel.AutoSize = $true
621 | $toolListDirLabel.width = 25
622 | $toolListDirLabel.height = 10
623 | $toolListDirLabel.location = New-Object System.Drawing.Point(2,71)
624 | $toolListDirLabel.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',9.5,[System.Drawing.FontStyle]::Bold)
625 |
626 | $toolListDirNote = New-Object system.Windows.Forms.Label
627 | $toolListDirNote.text = "Folder to store tool categories and shortcuts"
628 | $toolListDirNote.AutoSize = $true
629 | $toolListDirNote.width = 25
630 | $toolListDirNote.height = 10
631 | $toolListDirNote.location = New-Object System.Drawing.Point(190,94)
632 | $toolListDirNote.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
633 |
634 | $rawToolsDirText = New-Object system.Windows.Forms.TextBox
635 | $rawToolsDirText.multiline = $false
636 | $rawToolsDirText.width = 385
637 | $rawToolsDirText.height = 20
638 | $rawToolsDirText.location = New-Object System.Drawing.Point(190,113)
639 | $rawToolsDirText.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
640 | $rawToolsDirText.text = $envs['RAW_TOOLS_DIR']
641 |
642 | $rawToolsDirSelect = New-Object system.Windows.Forms.Button
643 | $rawToolsDirSelect.text = "Select Folder"
644 | $rawToolsDirSelect.width = 95
645 | $rawToolsDirSelect.height = 30
646 | $rawToolsDirSelect.location = New-Object System.Drawing.Point(588,109)
647 | $rawToolsDirSelect.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
648 | $selectFolderArgs4 = @{textBox=$rawToolsDirText; envVar="RAW_TOOLS_DIR"}
649 | $rawToolsDirSelect.Add_Click({Get-Folder @selectFolderArgs4})
650 |
651 | $rawToolsDirLabel = New-Object system.Windows.Forms.Label
652 | $rawToolsDirLabel.text = "%RAW_TOOLS_DIR%"
653 | $rawToolsDirLabel.AutoSize = $true
654 | $rawToolsDirLabel.width = 25
655 | $rawToolsDirLabel.height = 10
656 | $rawToolsDirLabel.location = New-Object System.Drawing.Point(2,116)
657 | $rawToolsDirLabel.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',9.5,[System.Drawing.FontStyle]::Bold)
658 |
659 | $rawToolsDirNote = New-Object system.Windows.Forms.Label
660 | $rawToolsDirNote.text = "Folder to store downloaded tools"
661 | $rawToolsDirNote.AutoSize = $true
662 | $rawToolsDirNote.width = 25
663 | $rawToolsDirNote.height = 10
664 | $rawToolsDirNote.location = New-Object System.Drawing.Point(190,137)
665 | $rawToolsDirNote.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
666 |
667 | $okButton = New-Object system.Windows.Forms.Button
668 | $okButton.text = "Continue"
669 | $okButton.width = 97
670 | $okButton.height = 37
671 | $okButton.location = New-Object System.Drawing.Point(480,280)
672 | $okButton.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',11)
673 | $okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
674 |
675 | $cancelButton = New-Object system.Windows.Forms.Button
676 | $cancelButton.text = "Cancel"
677 | $cancelButton.width = 97
678 | $cancelButton.height = 37
679 | $cancelButton.location = New-Object System.Drawing.Point(580,280)
680 | $cancelButton.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',11)
681 | $cancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
682 |
683 | $formEnv.controls.AddRange(@($envVarGroup,$okButton,$cancelButton,$welcomeLabel))
684 | $formEnv.AcceptButton = $okButton
685 | $formEnv.CancelButton = $cancelButton
686 |
687 | $envVarGroup.controls.AddRange(@($vmCommonDirText,$vmCommonDirSelect,$vmCommonDirLabel,$toolListDirText,$toolListDirSelect,$toolListDirLabel,$toolListShortCutText,$toolListShortcutSelect,$toolListShortcutLabel,$vmCommonDirNote,$toolListDirNote,$toolListShortcutNote,$rawToolsDirText,$rawToolsDirSelect,$rawToolsDirLabel,$rawToolsDirNote))
688 |
689 | $formEnv.Topmost = $true
690 | $Result = $formEnv.ShowDialog()
691 |
692 | if ($Result -eq [System.Windows.Forms.DialogResult]::OK) {
693 | # Remove default environment variables
694 | $nodes = $configXml.SelectNodes('//config/envs/env')
695 | foreach($node in $nodes) {
696 | $node.ParentNode.RemoveChild($node) | Out-Null
697 | }
698 |
699 | # Add environment variables
700 | $envs = $configXml.SelectSingleNode('//envs')
701 | $newXmlNode = $envs.AppendChild($configXml.CreateElement("env"))
702 | $newXmlNode.SetAttribute("name", "VM_COMMON_DIR")
703 | $newXmlNode.SetAttribute("value", $vmCommonDirText.text);
704 | $newXmlNode = $envs.AppendChild($configXml.CreateElement("env"))
705 | $newXmlNode.SetAttribute("name", "TOOL_LIST_DIR")
706 | $newXmlNode.SetAttribute("value", $toolListDirText.text);
707 | $newXmlNode = $envs.AppendChild($configXml.CreateElement("env"))
708 | $newXmlNode.SetAttribute("name", "RAW_TOOLS_DIR")
709 | $newXmlNode.SetAttribute("value", $rawToolsDirText.text)
710 |
711 | [void]$formEnv.Close()
712 |
713 | } else {
714 | Write-Host "[+] Cancel pressed, stopping installation..."
715 | Start-Sleep 3
716 | exit 1
717 | }
718 |
719 | ################################################################################
720 | ## PACKAGE SELECTION BY CATEGORY
721 | ################################################################################
722 |
723 | # Function that adds the selected packages to the config.xml for the installation
724 | function Install-Selected-Packages{
725 | $selectedPackages = @()
726 | $packages = $configXml.SelectSingleNode('//packages')
727 |
728 | # Remove all child nodes inside
729 | while ($packages.HasChildNodes) {
730 | $packages.RemoveChild($packages.FirstChild)
731 | }
732 |
733 | foreach ($checkBox in $checkboxesPackages){
734 | if ($checkBox.Checked){
735 | $package =$checkbox.Text.split(":")[0]
736 | $selectedPackages+=$package
737 | }
738 | }
739 | # Add selected packages
740 | foreach($package in $selectedPackages) {
741 | $newXmlNode = $packages.AppendChild($configXml.CreateElement("package"))
742 | $newXmlNode.SetAttribute("name", $package)
743 | }
744 | }
745 |
746 | # Function that resets the checkboxes to match the config.xml
747 | function Set-InitialPackages {
748 | foreach ($checkBox in $checkboxesPackages){
749 | $package =$checkbox.Text.split(":")[0]
750 | if (($checkbox.Checked) -and ($package -notin $packagesToInstall)){
751 | $checkBox.Checked = $false
752 | }else{
753 | if ((-not $checkbox.Checked ) -and ($package -in $packagesToInstall)){
754 | $checkBox.Checked = $true
755 | }
756 | }
757 | }
758 | }
759 |
760 | # Function that checks all the checkboxes
761 | function Select-AllPackages {
762 | foreach ($checkBox in $checkboxesPackages){
763 | $checkBox.Checked = $true
764 | }
765 | }
766 |
767 | # Function that unchecks all the checkboxes
768 | function Clear-AllPackages {
769 | foreach ($checkBox in $checkboxesPackages){
770 | $checkBox.Checked = $false
771 | }
772 | }
773 |
774 | # Funtion that returns an array of packages that belong to a specific category
775 | function Get-PackagesByCategory{
776 | param (
777 | [string]$category
778 | )
779 | return $packagesByCategory[$category]
780 | }
781 |
782 | Add-Type -AssemblyName System.Windows.Forms
783 | [System.Windows.Forms.Application]::EnableVisualStyles()
784 | $packagesByCategory = Get-Packages-Categories
785 |
786 | $formCategories = New-Object system.Windows.Forms.Form
787 | $formCategories.ClientSize = New-Object System.Drawing.Point(1015,800)
788 | $formCategories.text = "FLAREVM Package selection"
789 | $formCategories.StartPosition = 'CenterScreen'
790 | $formCategories.TopMost = $true
791 |
792 | if ([string]::IsNullOrEmpty($customConfig)) {
793 | $textLabel = "Select packages to install. The default configuration (recommended for reverse engineering) is pre-selected.`nClick on the reset button to restore the default configuration."
794 | } else {
795 | $textLabel = "Select packages to install. The provided custom configuration is pre-selected.`nClick on the reset button to restore the custom configuration."
796 | }
797 |
798 | $labelCategories = New-Object system.Windows.Forms.Label
799 | $labelCategories.text = $textLabel
800 | $labelCategories.AutoSize = $true
801 | $labelCategories.width = 25
802 | $labelCategories.height = 10
803 | $labelCategories.location = New-Object System.Drawing.Point(30,20)
804 | $labelCategories.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
805 |
806 | $panelCategories = New-Object system.Windows.Forms.Panel
807 | $panelCategories.height = 700
808 | $panelCategories.width = 970
809 | $panelCategories.location = New-Object System.Drawing.Point(30,30)
810 | $panelCategories.AutoScroll = $true
811 |
812 | $resetButton = New-Object system.Windows.Forms.Button
813 | $resetButton.text = "Reset"
814 | $resetButton.AutoSize = $true
815 | $resetButton.location = New-Object System.Drawing.Point(50,750)
816 | $resetButton.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
817 | $resetButton.Add_Click({Set-InitialPackages})
818 |
819 | $allPackagesButton = New-Object system.Windows.Forms.Button
820 | $allPackagesButton.text = "Select All"
821 | $allPackagesButton.AutoSize = $true
822 | $allPackagesButton.location = New-Object System.Drawing.Point(130,750)
823 | $allPackagesButton.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
824 | $allPackagesButton.Add_Click({
825 | [System.Windows.Forms.MessageBox]::Show('Selecting all packages considerable increases installation time and it is not desirable for most use cases','Warning')
826 | Select-AllPackages
827 | })
828 |
829 | $clearPackagesButton = New-Object system.Windows.Forms.Button
830 | $clearPackagesButton.text = "Deselect All"
831 | $clearPackagesButton.AutoSize = $true
832 | $clearPackagesButton.location = New-Object System.Drawing.Point(210,750)
833 | $clearPackagesButton.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
834 | $clearPackagesButton.Add_Click({Clear-AllPackages})
835 |
836 | $installButton = New-Object system.Windows.Forms.Button
837 | $installButton.text = "Install"
838 | $installButton.width = 97
839 | $installButton.height = 37
840 | $installButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
841 | $installButton.location = New-Object System.Drawing.Point(750,750)
842 | $installButton.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12)
843 |
844 | $cancelButton = New-Object system.Windows.Forms.Button
845 | $cancelButton.text = "Cancel"
846 | $cancelButton.width = 97
847 | $cancelButton.height = 37
848 | $cancelButton.location = New-Object System.Drawing.Point(850,750)
849 | $cancelButton.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12)
850 | $cancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
851 |
852 | $formCategories.AcceptButton = $installButton
853 | $formCategories.CancelButton = $cancelButton
854 |
855 | # Read packages to install from the config
856 | $packagesToInstall = $configXml.config.packages.package.name
857 |
858 | # Create checkboxes for each package
859 | $checkboxesPackages = New-Object System.Collections.Generic.List[System.Object]
860 | # Initial vertical position for checkboxes
861 | $verticalPosition = 30
862 | $numCheckBoxPackages = 1
863 | $packages = @()
864 | foreach ($category in $packagesByCategory.Keys |Sort-Object) {
865 | # Create Labels for categories
866 | $labelCategory = New-Object System.Windows.Forms.Label
867 | $labelCategory.Text = $category
868 | $labelCategory.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',11,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))
869 | $labelCategory.AutoSize = $true
870 | $labelCategory.Location = New-Object System.Drawing.Point(10, $verticalPosition)
871 | $panelCategories.Controls.Add($labelCategory)
872 |
873 | $NumPackages = 0
874 | $verticalPosition2 = $verticalPosition + 20
875 | $packages= Get-PackagesByCategory -category $category
876 | foreach ($package in $packages)
877 | {
878 | $NumPackages++
879 | $checkBox = New-Object System.Windows.Forms.CheckBox
880 | $checkBox.Text = $package.PackageName + ": " + $package.PackageDescription
881 | $checkBox.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
882 | $checkBox.AutoSize = $true
883 | $checkBox.Location = New-Object System.Drawing.Point(10, $verticalPosition2)
884 | $checkBox.Name = "checkBox$numCheckBoxPackages"
885 | $checkboxesPackages.Add($checkBox)
886 | $panelCategories.Controls.Add($checkBox)
887 | $verticalPosition2 += 20
888 | $numCheckBoxPackages ++
889 | }
890 | # Increment to space checkboxes vertically
891 | $verticalPosition += 20 * ($NumPackages ) + 30
892 | $numCategories ++
893 | }
894 |
895 | # Create empty label and add it to the form categories to add some space
896 | $posEnd = $verticalPosition2 +10
897 | $emptyLabel = New-Object system.Windows.Forms.Label
898 | $emptyLabel.Width = 20
899 | $emptyLabel.Height = 10
900 | $emptyLabel.location = New-Object System.Drawing.Point(10,$posEnd)
901 | $panelCategories.Controls.Add($emptyLabel)
902 |
903 | # Select packages that are in the config.xml
904 | Set-InitialPackages
905 |
906 | $formCategories.controls.AddRange(@($labelCategories,$panelCategories,$installButton,$resetButton,$allPackagesButton,$cancelButton,$clearPackagesButton))
907 | $formCategories.Add_Shown({$formCategories.Activate()})
908 | $resultCategories = $formCategories.ShowDialog()
909 | if ($resultCategories -eq [System.Windows.Forms.DialogResult]::OK){
910 | Install-Selected-Packages
911 | } else {
912 | Write-Host "[+] Cancel pressed, stopping installation..."
913 | Start-Sleep 3
914 | exit 1
915 | }
916 |
917 | ################################################################################
918 | ## END GUI
919 | ################################################################################
920 | }
921 |
922 | # Save the config file
923 | Write-Host "[+] Saving configuration file..."
924 | $configXml.save($configPath)
925 |
926 | # Parse config and set initial environment variables
927 | Write-Host "[+] Parsing configuration file..."
928 | foreach ($env in $configXml.config.envs.env) {
929 | $path = [Environment]::ExpandEnvironmentVariables($($env.value))
930 | Write-Host "`t[+] Setting %$($env.name)% to: $path" -ForegroundColor Green
931 | [Environment]::SetEnvironmentVariable("$($env.name)", $path, "Machine")
932 | [Environment]::SetEnvironmentVariable('VMname', 'FLARE-VM', [EnvironmentVariableTarget]::Machine)
933 | }
934 | refreshenv
935 |
936 | # Install the common module
937 | # This creates all necessary folders based on custom environment variables
938 | Write-Host "[+] Installing shared module..."
939 | choco install common.vm -y --force
940 | refreshenv
941 |
942 | # Use single config
943 | $configXml.save((Join-Path ${Env:VM_COMMON_DIR} "config.xml"))
944 | $configXml.save((Join-Path ${Env:VM_COMMON_DIR} "packages.xml"))
945 |
946 | # Custom Start Layout setup
947 | Write-Host "[+] Checking for custom Start Layout file..."
948 | $layoutPath = Join-Path "C:\Users\Default\AppData\Local\Microsoft\Windows\Shell" "LayoutModification.xml"
949 | if ([string]::IsNullOrEmpty($customLayout)) {
950 | $layoutSource = 'https://raw.githubusercontent.com/mandiant/flare-vm/main/LayoutModification.xml'
951 | } else {
952 | $layoutSource = $customLayout
953 | }
954 |
955 | Get-ConfigFile $layoutPath $layoutSource
956 |
957 | # Log basic system information to assist with troubleshooting
958 | Write-Host "[+] Logging basic system information to assist with any future troubleshooting..."
959 | Import-Module "${Env:VM_COMMON_DIR}\vm.common\vm.common.psm1" -Force -DisableNameChecking
960 | VM-Get-Host-Info
961 |
962 | Write-Host "[+] Installing the debloat.vm debloater and performance package"
963 | choco install debloat.vm -y --force
964 |
965 | # Download FLARE VM background image
966 | $backgroundImage = "${Env:VM_COMMON_DIR}\background.png"
967 | Save-FileFromUrl -fileSource 'https://raw.githubusercontent.com/mandiant/flare-vm/main/Images/flarevm-background.png' -fileDestination $backgroundImage
968 | # Use background image for lock screen as well
969 | $lockScreenImage = "${Env:VM_COMMON_DIR}\lockscreen.png"
970 | Copy-Item $backgroundImage $lockScreenImage
971 |
972 | if (-not $noWait.IsPresent) {
973 | # Show install notes and wait for timeout
974 | function Wait-ForInstall ($seconds) {
975 | $doneDT = (Get-Date).AddSeconds($seconds)
976 | while($doneDT -gt (Get-Date)) {
977 | $secondsLeft = $doneDT.Subtract((Get-Date)).TotalSeconds
978 | $percent = ($seconds - $secondsLeft) / $seconds * 100
979 | Write-Progress -Activity "Please read install notes on console below" -Status "Beginning install in..." -SecondsRemaining $secondsLeft -PercentComplete $percent
980 | [System.Threading.Thread]::Sleep(500)
981 | }
982 | Write-Progress -Activity "Waiting" -Status "Beginning install..." -SecondsRemaining 0 -Completed
983 | }
984 |
985 | Write-Host @"
986 | [!] INSTALL NOTES - PLEASE READ CAREFULLY [!]
987 |
988 | - This install is not 100% unattended. Please monitor the install for possible failures. If install
989 | fails, you may restart the install by re-running the install script with the following command:
990 |
991 | .\install.ps1 -password -noWait -noGui -noChecks
992 |
993 | - You can check which packages failed to install by listing the C:\ProgramData\chocolatey\lib-bad
994 | directory. Failed packages are stored by folder name. You may attempt manual installation with the
995 | following command:
996 |
997 | choco install -y
998 |
999 | - For any issues, please submit to GitHub:
1000 |
1001 | Installer related: https://github.com/mandiant/flare-vm
1002 | Package related: https://github.com/mandiant/VM-Packages
1003 |
1004 | [!] Please copy this note for reference [!]
1005 | "@ -ForegroundColor Red -BackgroundColor White
1006 | Wait-ForInstall -seconds 30
1007 | }
1008 |
1009 | # Begin the package install
1010 | Write-Host "[+] Beginning install of configured packages..." -ForegroundColor Green
1011 | $PackageName = "installer.vm"
1012 | if ($noPassword.IsPresent) {
1013 | Install-BoxstarterPackage -packageName $PackageName
1014 | } else {
1015 | Install-BoxstarterPackage -packageName $PackageName -credential $credentials
1016 | }
1017 |
1018 |
--------------------------------------------------------------------------------
/scripts/lint.ps1:
--------------------------------------------------------------------------------
1 | # Exclude rules that make the code less readable or involve changing the functionality
2 | $excludedRules = "PSAvoidUsingPlainTextForPassword", "PSAvoidUsingConvertToSecureStringWithPlainText", "PSAvoidUsingWriteHost", "PSUseShouldProcessForStateChangingFunctions", "PSUseSingularNouns", "PSAvoidUsingInvokeExpression"
3 |
4 | choco install psscriptanalyzer --version 1.23.0 --no-progress
5 |
6 | # Manually iterate over all files instead of using -Recurse because
7 | # PSScriptAnalyzer only outputs the script name (and most have the name
8 | # chocolateyinstall.ps1)
9 | $scripts = Get-ChildItem . -Filter *.ps*1 -Recurse -File -Name
10 | $errorsCount = 0
11 | foreach ($script in $scripts) {
12 | Write-Host -ForegroundColor Yellow $script
13 | ($errors = Invoke-ScriptAnalyzer $script -Recurse -ReportSummary -ExcludeRule $excludedRules)
14 | $errorsCount += $errors.Count
15 | }
16 |
17 | Exit($errorsCount)
18 |
--------------------------------------------------------------------------------
/virtualbox/README.md:
--------------------------------------------------------------------------------
1 | # VirtualBox scripts
2 |
3 | **This folder contains several scripts related to enhance building, exporting, and using FLARE-VM in VirtualBox.**
4 |
5 |
6 | ## Clean up snapshots
7 |
8 | It is not possible to select and delete several snapshots in VirtualBox, making cleaning up your virtual machine (VM) manually after having creating a lot snapshots time consuming and tedious (possible errors when deleting several snapshots simultaneously).
9 |
10 | [`vbox-clean-snapshots.py`](vbox-clean-snapshots.py) cleans a VirtualBox VM up by deleting a snapshot and its children recursively skipping snapshots with a substring in the name.
11 |
12 | ### Example
13 |
14 | ```
15 | $ ./vbox-remove-snapshots.py FLARE-VM.20240604
16 |
17 | Snapshots with the following strings in the name (case insensitive) won't be deleted:
18 | clean
19 | done
20 |
21 | Cleaning FLARE-VM.20240604 🫧 Snapshots to delete:
22 | Snapshot 1
23 | wip unpacked
24 | JS downloader deobfuscated
25 | Snapshot 6
26 | C2 decoded
27 | Snapshot 5
28 | wip
29 | Snapshot 4
30 | Snapshot 3
31 | Snapshot 2
32 | complicated chain - all samples ready
33 |
34 | VM state: Paused
35 | ⚠️ Snapshot deleting is slower in a running VM and may fail in a changing state
36 |
37 | Confirm deletion (press 'y'):y
38 |
39 | Deleting... (this may take some time, go for an 🍦!)
40 | 🫧 DELETED 'Snapshot 1'
41 | 🫧 DELETED 'wip unpacked'
42 | 🫧 DELETED 'JS downloader deobfuscated '
43 | 🫧 DELETED 'Snapshot 6'
44 | 🫧 DELETED 'C2 decoded'
45 | 🫧 DELETED 'Snapshot 5'
46 | 🫧 DELETED 'wip'
47 | 🫧 DELETED 'Snapshot 4'
48 | 🫧 DELETED 'Snapshot 3'
49 | 🫧 DELETED 'Snapshot 2'
50 | 🫧 DELETED 'complicated chain - all samples ready'
51 |
52 | See you next time you need to clean up your VMs! ✨
53 |
54 | ```
55 |
56 | ##### Before
57 |
58 | 
59 |
60 | ##### After
61 |
62 | 
63 |
64 |
65 | ## Check internet adapter status
66 |
67 | [`vbox-adapter-check.py`](vbox-adapter-check.py) prints the status of all internet adapters of all VMs in VirtualBox.
68 | The script also notifies if any dynamic analysis VM (with `.dynamic` in the name) has an adapter whose type is not allowed (internet access is undesirable for dynamic malware analysis).
69 | Unless the argument `--do_not_modify` is provided, the script changes the type of the adapters with non-allowed type to Host-Only.
70 | Unless the argument `--skip_disabled` is provided, the script also explores the disabled adapters, printing their status and possibly changing their type.
71 | The script has been tested in Debian 12 with GNOME 44.9.
72 |
73 | ### Example
74 |
75 | ```
76 | $ ./vbox-adapter-check.py
77 | windows10 1: Enabled HostOnly
78 | windows10 2: Disabled Null
79 | windows10 3: Disabled Null
80 | windows10 4: Disabled Null
81 | windows10 5: Disabled Null
82 | windows10 6: Disabled Null
83 | windows10 7: Disabled Null
84 | windows10 8: Disabled Null
85 | FLARE-VM.20240808.dynamic 1: Enabled NAT
86 | FLARE-VM.20240808.dynamic 2: Disabled NAT
87 | FLARE-VM.20240808.dynamic 3: Disabled Bridged
88 | FLARE-VM.20240808.dynamic 4: Enabled Internal
89 | FLARE-VM.20240808.dynamic 5: Disabled Null
90 | FLARE-VM.20240808.dynamic 6: Disabled Null
91 | FLARE-VM.20240808.dynamic 7: Disabled Null
92 | FLARE-VM.20240808.dynamic 8: Disabled Null
93 | ```
94 |
95 | #### Notification
96 |
97 | 
98 |
99 |
100 | ## Export snapshot
101 |
102 | [`vbox-export-snapshot.py`](vbox-export-snapshot.py) exports a VirtualBox snapshot as an Open Virtual Appliance (OVA) file.
103 | The script configures the exported VM with a single Host-Only network interface, and the resulting OVA file is named after the snapshot.
104 | A separate file containing the SHA256 hash of the OVA is also generated for verification.
105 | The script accepts an optional description for the OVA and the name of the export directory within the user's home directory (`$HOME`) where the OVA and SHA256 hash file will be saved.
106 | If no export directory is provided, the default directory name is `EXPORTED VMS`.
107 |
108 | ### Example
109 |
110 | ```
111 | $ ./vbox-export-snapshots.py "FLARE-VM.testing" "FLARE-VM" --description "Windows 10 VM with FLARE-VM default configuration"
112 |
113 | Exporting snapshot "FLARE-VM" from "FLARE-VM.testing" {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d}...
114 | VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ✨ restored snapshot "FLARE-VM"
115 | VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: saved. Starting VM...
116 | VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: running. Shutting down VM...
117 | VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ⚙️ network set to single hostonly adapter
118 | VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} 🔄 power cycling before export... (it will take some time, go for an 🍦!)
119 | VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: poweroff. Starting VM...
120 | VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: running. Shutting down VM...
121 | VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} 🚧 exporting ... (it will take some time, go for an 🍦!)
122 | VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ✅ EXPORTED "/home/anamg/None/FLARE-VM.ova"
123 | VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ✅ GENERATED "/home/anamg/None/FLARE-VM.ova.sha256": 987eed68038ce7c5072e7dc219ba82d11745267d8ab2ea7f76158877c13e3aa9
124 | ```
125 |
126 | ## Build FLARE-VM VM(s)
127 |
128 | [`vbox-build-flare-vm.py`](vbox-build-flare-vm.py) automates the creation and export of customized FLARE-VM VMs.
129 | The script begins by restoring a pre-existing `BUILD-READY` snapshot of a clean Windows installation.
130 | The script then copies the required installation files (such as the IDA Pro installer, FLARE-VM configuration, and legal notices) into the guest VM.
131 | After installing FLARE-VM, a `base` snapshot is taken.
132 | This snapshot serves as the foundation for generating subsequent snapshots and exporting OVA images, all based on the configuration provided in a YAML file.
133 | This configuration file specifies the VM name, the exported VM name, and details for each snapshot.
134 | Individual snapshot configurations can include custom commands to be executed within the guest, legal notices to be applied, and file/folder exclusions for the automated cleanup process.
135 | See the configuration example file [`configs/win10_flare-vm.yaml`](configs/win10_flare-vm.yaml).
136 |
137 | The `BUILD-READY` snapshot is expected to be an empty Windows installation that satisfies the FLARE-VM installation requirements and has UAC disabled
138 | To disable UAC execute in a cmd console with admin rights and restart the VM for the change to take effect:
139 | ```
140 | %windir%\System32\reg.exe ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v EnableLUA /t REG_DWORD /d 0 /f
141 | ```
142 |
143 | ## Build REMnux VM
144 |
145 | Similarly to [`vbox-build-flare-vm.py`](vbox-build-flare-vm.py), [`vbox-build-remnux.py`](vbox-build-remnux.py) automates the creation and export of customized REMnux virtual machines (VMs).
146 | The script begins by restoring a pre-existing "BUILD-READY" snapshot of a clean REMnux OVA.
147 | Required installation files (such as the IDA Pro installer and ZIPs with GNOME extensions) are then copied into the guest VM.
148 | The configuration file specifies the VM name, the exported VM name, and details for each snapshot.
149 | Individual snapshot configurations include the extension, description, and custom commands to be executed within the guest.
150 | See the configuration example file [`configs/remnux.yaml`](configs/remnux.yaml).
151 |
--------------------------------------------------------------------------------
/virtualbox/configs/remnux.yaml:
--------------------------------------------------------------------------------
1 | VM_NAME: REMnux.testing
2 | EXPORTED_VM_NAME: REMnux
3 | SNAPSHOT:
4 | extension: ".dynamic"
5 | description: "REMnux (based on Ubuntu) with improved configuration"
6 | CMDS:
7 | - |
8 | # Install additional useful packages
9 | sudo apt-get --assume-yes install libwrap0-dev gdb-multiarch qemu gcc-multilib libcurl4:i386
10 |
11 | - |
12 | # Uninstall distro-info to fix "Invalid version: '0.23ubuntu1'" Python install warning
13 | # Uninstall it in both the default and Python 3.9
14 | sudo pip uninstall -y distro-info
15 | sudo /usr/bin/python3.9 -m pip uninstall -y distro-info
16 |
17 | - |
18 | # Install additional Python libraries
19 | pip install -U pip
20 | pip install rpyc flare-capa lznt1
21 |
22 | - |
23 | # Install additional Python libraries using Python 3.9
24 | /usr/bin/python3.9 -m pip install -U pip
25 | /usr/bin/python3.9 -m pip install rpyc flare-capa lznt1
26 |
27 | - |
28 | # Install IDA
29 | # Expected IDA 9 installer in the Desktop
30 | cd /home/remnux/Desktop
31 | sudo chmod +x ida-pro_*.run
32 | ./ida-pro_*.run --mode unattended
33 |
34 | # Add IDA to favourite apps on startup (/usr/local/share/remnux/gnome-config.sh replaces it on startup)
35 | ida_app=$(basename /home/remnux/.local/share/applications/com.hex_rays.IDA.pro*.desktop)
36 | favourite_apps=$(gsettings get org.gnome.shell favorite-apps | sed 's/.$//')
37 | echo '' | sudo tee -a /usr/local/share/remnux/gnome-config.sh
38 | echo "gsettings set org.gnome.shell favorite-apps \"$favourite_apps, '$ida_app']\"" | sudo tee -a /usr/local/share/remnux/gnome-config.sh
39 |
40 | # Ensure files are written to persistent storage as the script shut down the VM abruptly
41 | sync
42 |
43 | - |
44 | # Install Dash to Panel extension
45 | # Expected a ZIP with a version for the GNOME shell 3.36 in the Desktop
46 | cd /home/remnux/Desktop
47 | gnome-extensions install dash-to-panel*.shell-extension.zip --force
48 |
49 | # Enable Dash to Panel extension on startup as logout is needed after install
50 | echo gnome-extensions enable dash-to-panel@jderose9.github.com | sudo tee -a /usr/local/share/remnux/gnome-config.sh
51 |
52 | # Ensure files are written to persistent storage as the script shut down the VM abruptly
53 | sync
54 |
55 |
--------------------------------------------------------------------------------
/virtualbox/configs/win10_flare-vm.yaml:
--------------------------------------------------------------------------------
1 | VM_NAME: FLARE-VM.testing
2 | EXPORTED_VM_NAME: FLARE-VM.Win10
3 | SNAPSHOTS:
4 | - extension: ".dynamic"
5 | description: "Windows 10 VM with FLARE-VM default configuration + idapro.vm + microsoft-office.vm"
6 | cmd: "choco install idapro.vm microsoft-office.vm"
7 | legal_notice: "legal_notice_win10.txt"
8 | - extension: ".full.dynamic"
9 | description: "Windows 10 VM with FLARE-VM default configuration + idapro.vm + microsoft-office.vm + pdbs.pdbresym.vm + visualstudio.vm"
10 | cmd: "choco install idapro.vm microsoft-office.vm pdbs.pdbresym.vm visualstudio.vm --execution-timeout 10000"
11 | legal_notice: "legal_notice_win10.txt"
12 | - extension: ".EDU"
13 | description: "Windows 10 VM with FLARE-VM default configuration + FLARE-EDU materials"
14 | cmd: |
15 | $desktop = "C:\Users\flare\Desktop";
16 | Set-Location $desktop
17 |
18 | # Unzip EDU labs
19 | VM-Unzip-Recursively;
20 |
21 | # Install Office 2016. the installation takes 30 minutes
22 | $path = "$desktop\en_office_professional_plus_2016_x86_x64_dvd_6962141.iso";
23 | $drive = (Mount-DiskImage -ImagePath $path | Get-Volume).DriveLetter;
24 | Set-Location "$drive`:\";
25 | .\setup.exe;
26 | Start-Sleep 1800;
27 | Dismount-DiskImage -ImagePath $path;
28 | legal_notice: "legal_notice_edu.txt"
29 | protected_folders: "'ATMA', 'MACC', 'MAF', 'MDA'"
30 | protected_files: "'Labs.zip', 'MICROSOFT Windows 10 License Terms.txt', 'MICROSOFT Office 2016 License Terms.txt'"
31 |
--------------------------------------------------------------------------------
/virtualbox/vbox-adapter-check.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # Copyright 2024 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 |
17 | import argparse
18 | import re
19 | import sys
20 | import textwrap
21 |
22 | import gi
23 | from vboxcommon import ensure_hostonlyif_exists, get_vm_state, run_vboxmanage
24 |
25 | gi.require_version("Notify", "0.7")
26 | from gi.repository import Notify # noqa: E402
27 |
28 | DYNAMIC_VM_NAME = ".dynamic"
29 | DISABLED_ADAPTER_TYPE = "hostonly"
30 | ALLOWED_ADAPTER_TYPES = ("hostonly", "intnet", "none")
31 |
32 | DESCRIPTION = f"""Print the status of all internet adapters of all VMs in VirtualBox.
33 | Notify if any VM with {DYNAMIC_VM_NAME} in the name has an adapter whose type is not allowed.
34 | This is useful to detect internet access which is undesirable for dynamic malware analysis.
35 | Optionally change the type of the adapters with non-allowed type to Host-Only."""
36 |
37 | EPILOG = textwrap.dedent(
38 | f"""
39 | Example usage:
40 | # Print status of all interfaces and disable internet access in VMs whose name contain {DYNAMIC_VM_NAME}
41 | vbox-adapter-check.vm
42 |
43 | # Print status of all interfaces without modifying any of them
44 | vbox-adapter-check.vm --do_not_modify
45 | """
46 | )
47 |
48 |
49 | def get_vm_uuids(dynamic_only):
50 | """Gets the machine UUID(s) for a given VM name using 'VBoxManage list vms'."""
51 | vm_uuids = []
52 | try:
53 | # regex VM name and extract the GUID
54 | # "FLARE-VM.testing" {b76d628b-737f-40a3-9a16-c5f66ad2cfcc}
55 | vms_info = run_vboxmanage(["list", "vms"])
56 | pattern = r'"(.*?)" \{(.*?)\}'
57 | matches = re.findall(pattern, vms_info)
58 | for match in matches:
59 | vm_name = match[0]
60 | vm_uuid = match[1]
61 | # either get all vms if dynamic_only false, or just the dynamic vms if true
62 | if (not dynamic_only) or DYNAMIC_VM_NAME in vm_name:
63 | vm_uuids.append((vm_name, vm_uuid))
64 | except Exception as e:
65 | raise Exception("Error finding machines UUIDs") from e
66 | return vm_uuids
67 |
68 |
69 | def change_network_adapters_to_hostonly(vm_uuid, vm_name, hostonly_ifname, do_not_modify):
70 | """Verify all adapters are in an allowed configuration. Must be poweredoff"""
71 | try:
72 | # gather adapters in incorrect configurations
73 | nics_with_internet = []
74 | invalid_nics_msg = ""
75 |
76 | # nic1="hostonly"
77 | # nictype1="82540EM"
78 | # nicspeed1="0"
79 | # nic2="none"
80 | # nic3="none"
81 | # nic4="none"
82 | # nic5="none"
83 | # nic6="none"
84 | # nic7="none"
85 | # nic8="none"
86 |
87 | vminfo = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"])
88 | for nic_number, nic_value in re.findall(r'^nic(\d+)="(\S+)"', vminfo, flags=re.M):
89 | if nic_value not in ALLOWED_ADAPTER_TYPES:
90 | nics_with_internet.append(f"nic{nic_number}")
91 | invalid_nics_msg += f"{nic_number} "
92 |
93 | # modify the invalid adapters if allowed
94 | if nics_with_internet:
95 | for nic in nics_with_internet:
96 | if do_not_modify:
97 | message = f"{vm_name} may be connected to the internet on adapter(s): {nic}. Please double check your VMs settings."
98 | else:
99 | message = (
100 | f"{vm_name} may be connected to the internet on adapter(s): {nic}."
101 | "The network adapter(s) have been disabled automatically to prevent an undesired internet connectivity."
102 | "Please double check your VMs settings."
103 | )
104 | # different commands are necessary if the machine is running.
105 | if get_vm_state(vm_uuid) == "poweroff":
106 | run_vboxmanage(
107 | [
108 | "modifyvm",
109 | vm_uuid,
110 | f"--{nic}",
111 | DISABLED_ADAPTER_TYPE,
112 | ]
113 | )
114 | else:
115 | run_vboxmanage(
116 | [
117 | "controlvm",
118 | vm_uuid,
119 | nic,
120 | "hostonly",
121 | hostonly_ifname,
122 | ]
123 | )
124 | print(f"Set VM {vm_name} adaper {nic} to hostonly")
125 |
126 | if do_not_modify:
127 | message = f"{vm_name} may be connected to the internet on adapter(s): {invalid_nics_msg}. Please double check your VMs settings."
128 | else:
129 | message = (
130 | f"{vm_name} may be connected to the internet on adapter(s): {invalid_nics_msg}."
131 | "The network adapter(s) have been disabled automatically to prevent an undesired internet connectivity."
132 | "Please double check your VMs settings."
133 | )
134 |
135 | # Show notification using PyGObject
136 | Notify.init("VirtualBox adapter check")
137 | notification = Notify.Notification.new(f"INTERNET IN VM: {vm_name}", message, "dialog-error")
138 | # Set highest priority
139 | notification.set_urgency(2)
140 | notification.show()
141 | print(f"{vm_name} network configuration not ok, sent notifaction")
142 | return
143 | else:
144 | print(f"{vm_name} network configuration is ok")
145 | return
146 |
147 | except Exception as e:
148 | raise Exception("Failed to verify VM adapter configuration") from e
149 |
150 |
151 | def main(argv=None):
152 | if argv is None:
153 | argv = sys.argv[1:]
154 |
155 | parser = argparse.ArgumentParser(
156 | description=DESCRIPTION,
157 | epilog=EPILOG,
158 | formatter_class=argparse.RawDescriptionHelpFormatter,
159 | )
160 | parser.add_argument(
161 | "--do_not_modify",
162 | action="store_true",
163 | help="Only print the status of the internet adapters without modifying them.",
164 | )
165 | parser.add_argument(
166 | "--dynamic_only",
167 | action="store_true",
168 | help="Only scan VMs with .dynamic in the name",
169 | )
170 | args = parser.parse_args(args=argv)
171 |
172 | try:
173 | hostonly_ifname = ensure_hostonlyif_exists()
174 | vm_uuids = get_vm_uuids(args.dynamic_only)
175 | if len(vm_uuids) > 0:
176 | for vm_name, vm_uuid in vm_uuids:
177 | change_network_adapters_to_hostonly(vm_uuid, vm_name, hostonly_ifname, args.do_not_modify)
178 | else:
179 | print("[Warning ⚠️] No VMs found")
180 | except Exception as e:
181 | print(f"Error verifying dynamic VM hostonly configuration: {e}")
182 |
183 |
184 | if __name__ == "__main__":
185 | main()
186 |
--------------------------------------------------------------------------------
/virtualbox/vbox-build-flare-vm.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # Copyright 2024 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | import argparse
17 | import os
18 | import sys
19 | import time
20 | from datetime import datetime
21 |
22 | import yaml
23 | from vboxcommon import (
24 | LONG_WAIT,
25 | control_guest,
26 | ensure_vm_running,
27 | export_vm,
28 | get_vm_state,
29 | get_vm_uuid,
30 | restore_snapshot,
31 | set_network_to_hostonly,
32 | take_snapshot,
33 | )
34 |
35 | DESCRIPTION = """
36 | Automates the creation and export of customized FLARE-VM virtual machines (VMs).
37 | Begins by restoring a pre-existing "BUILD-READY" snapshot of a clean Windows installation (with UAC disabled).
38 | Required installation files (such as the IDA Pro installer, FLARE-VM configuration, and legal notices) are then copied into the guest VM.
39 | After installing FLARE-VM, a "base" snapshot is taken.
40 | This snapshot serves as the foundation for generating subsequent snapshots and exporting OVA images,
41 | all based on the configuration provided in a YAML file.
42 | This configuration file specifies the VM name, the exported VM name, and details for each snapshot.
43 | Individual snapshot configurations can include custom commands to be executed within the guest, legal notices to be applied,
44 | and file/folder exclusions for the automated cleanup process.
45 | """
46 |
47 | EPILOG = """
48 | Example usage:
49 | # Build FLARE-VM and export several OVAs using the information in the provided configuration file, using '19930906' as date
50 | #./vbox-build-vm.py configs/win10_flare-vm.yaml --custom_config --date='19930906'
51 | """
52 |
53 | # The base snapshot is expected to be an empty Windows installation that satisfies the FLARE-VM installation requirements and has UAC disabled
54 | # To disable UAC execute in a cmd console with admin rights and restart the VM for the change to take effect:
55 | # %windir%\System32\reg.exe ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v EnableLUA /t REG_DWORD /d 0 /f
56 | BASE_SNAPSHOT = "BUILD-READY"
57 |
58 | # Guest username and password, needed to execute commands in the guest
59 | GUEST_USERNAME = "flare"
60 | GUEST_PASSWORD = "password"
61 |
62 | # Logs
63 | LOGS_DIR = os.path.expanduser("~/FLARE-VM LOGS")
64 | LOG_FILE_GUEST = r"C:\ProgramData\_VM\log.txt"
65 | LOG_FILE_HOST = rf"{LOGS_DIR}/flare-vm-log.txt"
66 | FAILED_PACKAGES_GUEST = r"C:\ProgramData\_VM\failed_packages.txt"
67 | FAILED_PACKAGES_HOST = rf"{LOGS_DIR}/flare-vm-failed_packages.txt"
68 |
69 | # Required files
70 | REQUIRED_FILES_DIR = os.path.expanduser("~/FLARE-VM REQUIRED FILES")
71 | REQUIRED_FILES_DEST = rf"C:\Users\{GUEST_USERNAME}\Desktop"
72 |
73 | # Executable paths in guest
74 | POWERSHELL_PATH = r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
75 | CMD_PATH = r"C:\Windows\System32\cmd.exe"
76 |
77 | # Cleanup command to be executed in cmd to delete the PowerShel logs
78 | CMD_CLEANUP_CMD = r"/C rmdir /s /q %UserProfile%\Desktop\PS_Transcripts && start timeout 3"
79 |
80 |
81 | def run_command(vm_uuid, cmd, executable="PS"):
82 | """Run a command in the guest of the specified VM, displaying the output in real time to the console.
83 |
84 | Args:
85 | vm_uuid: VM UUID
86 | cmd: The command string to execute in the guest.
87 | executable: Specifies the executable to use for running the command, either `PS` (`powershell.exe) or `CMD` (`cmd.exe`).
88 | """
89 | ensure_vm_running(vm_uuid)
90 |
91 | exe_path = POWERSHELL_PATH if executable == "PS" else CMD_PATH
92 |
93 | print(f"VM {vm_uuid} 🚧 {executable}: {cmd}")
94 | control_guest(vm_uuid, GUEST_USERNAME, GUEST_PASSWORD, ["run", exe_path, cmd], True)
95 |
96 |
97 | def create_log_folder():
98 | """Ensure log folder exists and is empty."""
99 | # Create directory if it does not exist
100 | os.makedirs(LOGS_DIR, exist_ok=True)
101 | print(f"Log folder: {LOGS_DIR}\n")
102 |
103 | # Remove all files in the logs directory. Note the directory only files (the logs).
104 | for file_name in os.listdir(LOGS_DIR):
105 | file_path = os.path.join(LOGS_DIR, file_name)
106 | os.remove(file_path)
107 |
108 |
109 | def install_flare_vm(vm_uuid, snapshot_name, custom_config):
110 | """Install FLARE-VM"""
111 | additional_arg = r"-customConfig '$desktop\config.xml'" if custom_config else ""
112 | flare_vm_installation_cmd = rf"""
113 | $desktop=[Environment]::GetFolderPath("Desktop")
114 | cd $desktop
115 | Set-ExecutionPolicy Unrestricted -Force
116 | $url="https://raw.githubusercontent.com/mandiant/flare-vm/main/install.ps1"
117 | $file = "$desktop\install.ps1"
118 | (New-Object net.webclient).DownloadFile($url,$file)
119 | Unblock-File .\install.ps1
120 |
121 | start powershell "$file -password password -noWait -noGui -noChecks {additional_arg}"
122 | """
123 | run_command(vm_uuid, flare_vm_installation_cmd)
124 | print(f"VM {vm_uuid} ✅ FLARE-VM is being installed...{LONG_WAIT}")
125 |
126 | index = 0
127 | while True:
128 | time.sleep(120) # Wait 2 minutes
129 | try:
130 | control_guest(
131 | vm_uuid,
132 | GUEST_USERNAME,
133 | GUEST_PASSWORD,
134 | ["copyfrom", f"--target-directory={FAILED_PACKAGES_HOST}", FAILED_PACKAGES_GUEST],
135 | )
136 | break
137 | except RuntimeError:
138 | index += 1
139 | if (index % 10) == 0: # Print an "I am alive" message every ~20 minutes
140 | time_str = datetime.now().strftime("%Y-%m-%d %H:%M")
141 | print(f"VM {vm_uuid} 🕑 {time_str} still waiting")
142 | # Take snaphost that can be restore if the VM crashes
143 | # Avoid taking a snapshot during a restart (as it could crash the VM) by checking the VM is running
144 | if get_vm_state(vm_uuid) == "running":
145 | wip_snapshot_name = f"WIP {snapshot_name} {time_str}"
146 | take_snapshot(vm_uuid, wip_snapshot_name)
147 |
148 | print(f"VM {vm_uuid} ✅ FLARE-VM installed!")
149 |
150 | control_guest(
151 | vm_uuid, GUEST_USERNAME, GUEST_PASSWORD, ["copyfrom", f"--target-directory={LOG_FILE_HOST}", LOG_FILE_GUEST]
152 | )
153 | print(f"VM {vm_uuid} 📁 Copied FLARE-VM log: {REQUIRED_FILES_DIR}")
154 |
155 | # Read failed packages from log file and print them
156 | try:
157 | if os.path.getsize(FAILED_PACKAGES_HOST):
158 | print(" ❌ FAILED PACKAGES")
159 | with open(FAILED_PACKAGES_HOST, "r") as f:
160 | for failed_package in f:
161 | print(f" - {failed_package}")
162 | except Exception:
163 | print(f" ❌ Reading {FAILED_PACKAGES_HOST} failed")
164 |
165 |
166 | def build_vm(vm_name, exported_vm_name, snapshots, date, custom_config, do_not_install_flare_vm):
167 | """
168 | Build and export multiple FLARE-VM VMs as OVAs based on provided configurations.
169 |
170 | This function first prepares a base FLARE-VM VM by restoring a BASE_SNAPSHOT,
171 | copying necessary files, and installing the FLARE-VM software. A base snapshot
172 | of this installation is then taken. Subsequently, for each configuration
173 | specified in the `snapshots` list, the base snapshot is restored, customized
174 | with specific commands and settings, and then exported as an OVA.
175 |
176 | Args:
177 | vm_name: The name of the VM.
178 | exported_vm_name: The base name to use for naming the exported VMs and their snapshots.
179 | snapshots: A list of dictionaries, where each dictionary defines the configuration for a specific exported VM.
180 | Each dictionary can contain the following keys:
181 | - cmd: A command to execute in the guest VM.
182 | - legal_notice: The filename of a legal notice to set on the VM.
183 | - protected_files: A string of files to exclude during the cleanup process.
184 | - protected_folders:: A string of folders to exclude during the cleanup process.
185 | - extension: An extension to add to the exported VM's filename.
186 | - description: A description to embed in the exported OVA.
187 | date: A date string appended to the names of the base snapshot and exported OVAs.
188 | custom_config: Custom configuration parameters passed to the FLARE-VM installation script.
189 | do_not_install_flare_vm: If True, the FLARE-VM installation step is skipped and an existent base snapshot used.
190 | It also does not copy the required files.
191 | """
192 | vm_uuid = get_vm_uuid(vm_name)
193 | if not vm_uuid:
194 | print(f'❌ ERROR: "{vm_name}" not found')
195 | exit()
196 |
197 | print(f'\nGetting the installation VM "{vm_name}" {vm_uuid} ready...')
198 | create_log_folder()
199 |
200 | base_snapshot_name = f"{exported_vm_name}.{date}.base"
201 |
202 | if not do_not_install_flare_vm:
203 | restore_snapshot(vm_uuid, BASE_SNAPSHOT)
204 |
205 | # Copy required files
206 | control_guest(
207 | vm_uuid,
208 | GUEST_USERNAME,
209 | GUEST_PASSWORD,
210 | ["copyto", "--recursive", f"--target-directory={REQUIRED_FILES_DEST}", REQUIRED_FILES_DIR],
211 | )
212 | print(f"VM {vm_uuid} 📁 Copied required files in: {REQUIRED_FILES_DIR}")
213 |
214 | install_flare_vm(vm_uuid, exported_vm_name, custom_config)
215 | take_snapshot(vm_uuid, base_snapshot_name, False, True)
216 |
217 | for snapshot in snapshots:
218 | restore_snapshot(vm_uuid, base_snapshot_name)
219 |
220 | # Run snapshot configured command
221 | cmd = snapshot.get("cmd", None)
222 | if cmd:
223 | run_command(vm_uuid, cmd)
224 |
225 | set_network_to_hostonly(vm_uuid)
226 |
227 | # Set snapshot configured legal notice
228 | notice_file_name = snapshot.get("legal_notice", None)
229 | if notice_file_name:
230 | notice_file_path = rf"C:\Users\{GUEST_USERNAME}\Desktop\{notice_file_name}"
231 | set_notice_cmd = f"VM-Set-Legal-Notice (Get-Content '{notice_file_path}' -Raw)"
232 | run_command(vm_uuid, set_notice_cmd)
233 |
234 | # Perform clean up: run 'VM-Clean-Up' excluding configured files and folders
235 | ps_cleanup_cmd = "VM-Clean-Up"
236 | protected_files = snapshot.get("protected_files", None)
237 | if protected_files:
238 | ps_cleanup_cmd += f" -excludeFiles {protected_files}"
239 | protected_folders = snapshot.get("protected_folders", None)
240 | if protected_folders:
241 | ps_cleanup_cmd += f" -excludeFolders {protected_folders}"
242 | run_command(vm_uuid, ps_cleanup_cmd)
243 |
244 | # Perform clean up: delete PowerShells logs (using cmd.exe)
245 | run_command(vm_uuid, CMD_CLEANUP_CMD, "CMD")
246 |
247 | # Take snapshot turning the VM off
248 | extension = snapshot.get("extension", "")
249 | snapshot_name = f"{exported_vm_name}.{date}{extension}"
250 | take_snapshot(vm_uuid, snapshot_name, True, True)
251 |
252 | # Export the snapshot with the configured description
253 | export_vm(vm_uuid, snapshot_name, snapshot.get("description", ""))
254 |
255 |
256 | def main(argv=None):
257 | if argv is None:
258 | argv = sys.argv[1:]
259 |
260 | parser = argparse.ArgumentParser(
261 | description=DESCRIPTION,
262 | epilog=EPILOG,
263 | formatter_class=argparse.RawDescriptionHelpFormatter,
264 | )
265 | parser.add_argument("config_path", help="path of the YAML configuration file.")
266 | parser.add_argument(
267 | "--date",
268 | help="Date to include in the snapshots and the exported VMs in YYYYMMDD format. Today's date by default.",
269 | default=datetime.today().strftime("%Y%m%d"),
270 | )
271 | parser.add_argument(
272 | "--custom_config",
273 | action="store_true",
274 | default=False,
275 | help=f"flag to use a custom configuration file named 'config.xml' (expected to be in {REQUIRED_FILES_DIR}) for the FLARE-VM installation.",
276 | )
277 | parser.add_argument(
278 | "--do-not-install-flare-vm",
279 | action="store_true",
280 | default=False,
281 | help="flag to not install FLARE-VM and used an existent base snapshot. It also does not copy the required files.",
282 | )
283 | args = parser.parse_args(args=argv)
284 |
285 | try:
286 | with open(args.config_path) as f:
287 | config = yaml.safe_load(f)
288 | except Exception as e:
289 | print(f'Invalid "{args.config_path}": {e}')
290 | exit()
291 |
292 | build_vm(
293 | config["VM_NAME"],
294 | config["EXPORTED_VM_NAME"],
295 | config["SNAPSHOTS"],
296 | args.date,
297 | args.custom_config,
298 | args.do_not_install_flare_vm,
299 | )
300 |
301 |
302 | if __name__ == "__main__":
303 | main()
304 |
--------------------------------------------------------------------------------
/virtualbox/vbox-build-remnux.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # Copyright 2024 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | import argparse
17 | import os
18 | import sys
19 | from datetime import datetime
20 |
21 | import yaml
22 | from vboxcommon import (
23 | control_guest,
24 | ensure_vm_running,
25 | export_vm,
26 | get_vm_uuid,
27 | restore_snapshot,
28 | set_network_to_hostonly,
29 | take_snapshot,
30 | )
31 |
32 | DESCRIPTION = """
33 | Automates the creation and export of customized REMnux virtual machines (VMs).
34 | Begins by restoring a pre-existing "BUILD-READY" snapshot of a clean REMnux OVA.
35 | Required installation files (such as the IDA Pro installer and ZIPs with GNOME extensions) are then copied into the guest VM.
36 | The configuration file specifies the VM name, the exported VM name, and details for each snapshot.
37 | Individual snapshot configurations include the extension, description, and custom commands to be executed within the guest.
38 | """
39 |
40 | EPILOG = """
41 | Example usage:
42 | #./vbox-build-remnux.py configs/remnux.yaml --date='19930906'
43 | """
44 |
45 | BASE_SNAPSHOT = "BUILD-READY"
46 |
47 | # Guest username and password, needed to execute commands in the guest
48 | GUEST_USERNAME = "remnux"
49 | GUEST_PASSWORD = "malware"
50 |
51 | # Required files
52 | REQUIRED_FILES_DIR = os.path.expanduser("~/REMNUX REQUIRED FILES")
53 | REQUIRED_FILES_DEST = rf"/home/{GUEST_USERNAME}/Desktop"
54 |
55 |
56 | def run_command(vm_uuid, cmd):
57 | """Run a command in the guest of the specified VM, displaying the output in real time to the console.
58 |
59 | Args:
60 | vm_uuid: VM UUID
61 | cmd: The command string to execute in the guest using `/bin/sh -c`.
62 | """
63 | ensure_vm_running(vm_uuid)
64 |
65 | executable = "/bin/sh"
66 | print(f"VM {vm_uuid} 🚧 {executable}: {cmd}")
67 | control_guest(vm_uuid, GUEST_USERNAME, GUEST_PASSWORD, ["run", executable, "--", "-c", cmd], True)
68 |
69 |
70 | def build_vm(vm_name, exported_vm_name, snapshot, cmds, date, do_not_upgrade):
71 | """
72 | Build a REMnux VM and export it as OVA.
73 |
74 | Build a REMnux VM by restoring the BASE_SNAPSHOT, upgrading the REMnux distro,
75 | copying required files, running given commands, removing copied required files.
76 | Take several snapshots that can be used for debugging issues.
77 | Set the network to hostonly and export the resulting VM as OVA.
78 |
79 | Args:
80 | vm_name: The name of the VM.
81 | exported_vm_name: The base name to use for the final exported VM and snapshots.
82 | snapshot: A dictionary containing information about the final snapshot,
83 | including optional `extension` and `description`.
84 | cmds: A list of string commands to execute sequentially within the guest VM.
85 | A snapshot is taken after executing each command.
86 | date: A date string to incorporate into snapshot names and the exported OVA.
87 | do_not_upgrade: If True, the initial upgrade step is skipped and an existent UPGRADED snapshot used.
88 | It also does not copy the required files.
89 | """
90 | vm_uuid = get_vm_uuid(vm_name)
91 | if not vm_uuid:
92 | print(f'❌ ERROR: "{vm_name}" not found')
93 | exit()
94 |
95 | print(f'\nGetting the installation VM "{vm_name}" {vm_uuid} ready...')
96 |
97 | base_snapshot_name = f"UPGRADED.{date}"
98 |
99 | if not do_not_upgrade:
100 | restore_snapshot(vm_uuid, BASE_SNAPSHOT)
101 |
102 | # Copy required files
103 | control_guest(
104 | vm_uuid,
105 | GUEST_USERNAME,
106 | GUEST_PASSWORD,
107 | ["copyto", "--recursive", f"--target-directory={REQUIRED_FILES_DEST}", REQUIRED_FILES_DIR],
108 | )
109 | print(f"VM {vm_uuid} 📁 Copied required files in: {REQUIRED_FILES_DIR}")
110 |
111 | # Update REMnux distro and take a snapshot
112 | run_command(vm_uuid, "sudo remnux upgrade")
113 | take_snapshot(vm_uuid, base_snapshot_name)
114 | else:
115 | restore_snapshot(vm_uuid, base_snapshot_name)
116 |
117 | # Run snapshot configured commands taking a snapshot after running every command
118 | for i, cmd in enumerate(cmds):
119 | run_command(vm_uuid, cmd)
120 | take_snapshot(vm_uuid, f"{exported_vm_name}.{date} CMD {cmd.splitlines()[0]}")
121 |
122 | # Delete required files copied to the VM
123 | files = f"{REQUIRED_FILES_DEST}/*"
124 | # Sync is needed ti ensure the files deletion is written to persistent storage as the script shut down the VM abruptly
125 | run_command(vm_uuid, f"ls {files}; rm {files}; sync")
126 |
127 | set_network_to_hostonly(vm_uuid)
128 |
129 | # Take snapshot turning the VM off
130 | extension = snapshot.get("extension", "")
131 | snapshot_name = f"{exported_vm_name}.{date}{extension}"
132 | take_snapshot(vm_uuid, snapshot_name, True)
133 |
134 | # Export the snapshot with the configured description
135 | export_vm(vm_uuid, snapshot_name, snapshot.get("description", ""))
136 |
137 |
138 | def main(argv=None):
139 | if argv is None:
140 | argv = sys.argv[1:]
141 |
142 | parser = argparse.ArgumentParser(
143 | description=DESCRIPTION,
144 | epilog=EPILOG,
145 | formatter_class=argparse.RawDescriptionHelpFormatter,
146 | )
147 | parser.add_argument("config_path", help="path of the YAML configuration file.")
148 | parser.add_argument(
149 | "--date",
150 | help="Date to include in the snapshots and the exported VMs in YYYYMMDD format. Today's date by default.",
151 | default=datetime.today().strftime("%Y%m%d"),
152 | )
153 | parser.add_argument(
154 | "--do-not-upgrade",
155 | action="store_true",
156 | default=False,
157 | help="flag to not upgrade the REMnux distro and use an existent UPGRADED snapshot. It also does not copy the required files.",
158 | )
159 | args = parser.parse_args(args=argv)
160 |
161 | try:
162 | with open(args.config_path) as f:
163 | config = yaml.safe_load(f)
164 | except Exception as e:
165 | print(f'Invalid "{args.config_path}": {e}')
166 | exit()
167 |
168 | build_vm(
169 | config["VM_NAME"],
170 | config["EXPORTED_VM_NAME"],
171 | config["SNAPSHOT"],
172 | config["CMDS"],
173 | args.date,
174 | args.do_not_upgrade,
175 | )
176 |
177 |
178 | if __name__ == "__main__":
179 | main()
180 |
--------------------------------------------------------------------------------
/virtualbox/vbox-clean-snapshots.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # Copyright 2024 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 |
17 | import argparse
18 | import re
19 | import sys
20 | import textwrap
21 |
22 | from vboxcommon import get_vm_state, run_vboxmanage
23 |
24 | DESCRIPTION = "Clean a VirtualBox VM up by deleting a snapshot and its children recursively skipping snapshots with a substring in the name."
25 |
26 | EPILOG = textwrap.dedent(
27 | """
28 | Example usage:
29 | # Delete all snapshots excluding the default protected ones (with 'clean' or 'done' in the name, case insensitive) in the 'FLARE-VM.20240604' VM
30 | vbox-clean-snapshots.py FLARE-VM.20240604
31 |
32 | # Delete all snapshots that do not include 'clean', 'done', or 'important' (case insensitive) in the name in the 'FLARE-VM.20240604' VM
33 | vbox-clean-snapshots.py FLARE-VM.20240604 --protected_snapshots "clean,done,important"
34 |
35 | # Delete the 'Snapshot 3' snapshot and its children recursively skipping the default protected ones in the 'FLARE-VM.20240604' VM
36 | vbox-clean-snapshots.py FLARE-VM.20240604 --root_snapshot "Snapshot 3"
37 |
38 | # Delete the 'CLEAN with IDA 8.4"' children snapshots recursively skipping the default protected ones in the 'FLARE-VM.20240604' VM
39 | # NOTE: the 'CLEAN with IDA 8.4' root snapshot is skipped in this case
40 | vbox-clean-snapshots.py FLARE-VM.20240604 --root_snapshot "CLEAN with IDA 8.4"
41 |
42 | # Delete all snapshots in the 'FLARE-VM.20240604' VM
43 | vbox-clean-snapshots.py FLARE-VM.20240604 --protected_snapshots ""
44 | """
45 | )
46 |
47 |
48 | def is_protected(protected_snapshots, snapshot_name):
49 | """Check if snapshot_name contains any of the strings in the protected_snapshots list (case insensitive)"""
50 | return any(p.lower() in snapshot_name.lower() for p in protected_snapshots)
51 |
52 |
53 | def get_snapshot_children(vm_name, root_snapshot_name, protected_snapshots):
54 | """Get the children of a snapshot (including the snapshot) using 'VBoxManage snapshot' with the 'list' option.
55 |
56 | Args:
57 | vm_name: The name of the VM.
58 | root_snapshot_name: The name of the root snapshot we want the children of. If no provided or not found, return all snapshots.
59 | protected_snapshots: Snapshots we ignore and do not include in the returned list.
60 |
61 | Returns:
62 | A list of snapshot names that are children of the given snapshot. The list is ordered by dependent relationships.
63 | """
64 | # Example of `VBoxManage snapshot VM_NAME list --machinereadable` output:
65 | # SnapshotName="ROOT"
66 | # SnapshotUUID="86b38fc9-9d68-4e4b-a033-4075002ab570"
67 | # SnapshotName-1="Snapshot 1"
68 | # SnapshotUUID-1="e383e702-fee3-4e0b-b1e0-f3b869dbcaea"
69 | # CurrentSnapshotName="Snapshot 1"
70 | # CurrentSnapshotUUID="e383e702-fee3-4e0b-b1e0-f3b869dbcaea"
71 | # CurrentSnapshotNode="SnapshotName-1"
72 | # SnapshotName-1-1="Snapshot 2"
73 | # SnapshotUUID-1-1="8cc12787-99df-466e-8a51-80e373d3447a"
74 | # SnapshotName-2="Snapshot 3"
75 | # SnapshotUUID-2="f42533a8-7c14-4855-aa66-7169fe8187fe"
76 | #
77 | # ROOT
78 | # ├─ Snapshot 1
79 | # │ └─ Snapshot 2
80 | # └─ Snapshot 3
81 | snapshots_info = run_vboxmanage(["snapshot", vm_name, "list", "--machinereadable"])
82 |
83 | root_snapshot_index = ""
84 | if root_snapshot_name:
85 | # Find root snapshot: first snapshot with name root_snapshot_name (case sensitive)
86 | root_snapshot_regex = rf'^SnapshotName(?P(?:-\d+)*)="{root_snapshot_name}"\n'
87 | root_snapshot = re.search(root_snapshot_regex, snapshots_info, flags=re.M)
88 | if root_snapshot:
89 | root_snapshot_index = root_snapshot["index"]
90 | else:
91 | print(f"\n⚠️ Root snapshot not found: {root_snapshot_name} 🫧 Cleaning all snapshots in the VM")
92 |
93 | # Find all root and child snapshots as (snapshot_name, snapshot_id)
94 | # Children of a snapshot share the same prefix index
95 | index_regex = rf"{root_snapshot_index}(?:-\d+)*"
96 | snapshot_regex = f'^SnapshotName{index_regex}="(.*?)"\nSnapshotUUID{index_regex}="(.*?)"'
97 | snapshots = re.findall(snapshot_regex, snapshots_info, flags=re.M)
98 |
99 | # Return non protected snapshots as list of (snapshot_name, snapshot_id)
100 | return [snapshot for snapshot in snapshots if not is_protected(protected_snapshots, snapshot[0])]
101 |
102 |
103 | def delete_snapshot_and_children(vm_name, snapshot_name, protected_snapshots):
104 | snaps_to_delete = get_snapshot_children(vm_name, snapshot_name, protected_snapshots)
105 |
106 | if protected_snapshots:
107 | print("\nSnapshots with the following strings in the name (case insensitive) won't be deleted:")
108 | for protected_snapshot in protected_snapshots:
109 | print(f" {protected_snapshot}")
110 |
111 | if snaps_to_delete:
112 | print(f"\nCleaning {vm_name} 🫧 Snapshots to delete:")
113 | for snapshot_name, _ in snaps_to_delete:
114 | print(f" {snapshot_name}")
115 |
116 | vm_state = get_vm_state(vm_name)
117 | if vm_state not in ("poweroff", "saved"):
118 | print(
119 | f"\nVM state: {vm_state}\n⚠️ Snapshot deleting is slower in a running VM and may fail in a changing state"
120 | )
121 |
122 | answer = input("\nConfirm deletion (press 'y'): ")
123 | if answer.lower() == "y":
124 | print("\nDELETING SNAPSHOTS... (this may take some time, go for an 🍦!)")
125 | # Delete snapshots in reverse order to avoid issues with child snapshots,
126 | # as a snapshot with more than 1 child can not be deleted
127 | for snapshot_name, snapshot_id in reversed(snaps_to_delete):
128 | try:
129 | run_vboxmanage(["snapshot", vm_name, "delete", snapshot_id])
130 | print(f"🫧 DELETED '{snapshot_name}'")
131 | except Exception as e:
132 | print(f"❌ ERROR '{snapshot_name}'\n{e}")
133 | else:
134 | print(f"\n{vm_name} is clean 🫧")
135 |
136 | print("\nSee you next time you need to clean up your VMs! ✨\n")
137 |
138 |
139 | def main(argv=None):
140 | if argv is None:
141 | argv = sys.argv[1:]
142 |
143 | epilog = EPILOG
144 | parser = argparse.ArgumentParser(
145 | description=DESCRIPTION,
146 | epilog=epilog,
147 | formatter_class=argparse.RawDescriptionHelpFormatter,
148 | )
149 | parser.add_argument("vm_name", help="Name of the VM to clean up")
150 | parser.add_argument(
151 | "--root_snapshot",
152 | help="""Snapshot name (case sensitive) to delete (and its children recursively).
153 | Leave empty to clean all snapshots in the VM.""",
154 | )
155 | parser.add_argument(
156 | "--protected_snapshots",
157 | default="clean,done",
158 | type=lambda s: s.split(",") if s else [],
159 | help='''Comma-separated list of strings.
160 | Snapshots with any of the strings included in the name (case insensitive) are not deleted.
161 | Default: "clean,done"''',
162 | )
163 | args = parser.parse_args(args=argv)
164 |
165 | delete_snapshot_and_children(args.vm_name, args.root_snapshot, args.protected_snapshots)
166 |
167 |
168 | if __name__ == "__main__":
169 | main()
170 |
--------------------------------------------------------------------------------
/virtualbox/vbox-export-snapshot.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # Copyright 2024 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | import argparse
17 | import sys
18 |
19 | from vboxcommon import LONG_WAIT, ensure_vm_running, export_vm, get_vm_uuid, restore_snapshot, set_network_to_hostonly
20 |
21 | DESCRIPTION = """Export a snapshot to OVA (named after the snapshot) with a single Host-Only network interface.
22 | Generate a file containing the SHA256 hash of the OVA that can be used for verification."""
23 |
24 | EPILOG = """
25 | Example usage:
26 | # Export snapshot "FLARE-VM" from the "FLARE-VM.testing" VM with a description
27 | ./vbox-export-snapshot.py "FLARE-VM.testing" "FLARE-VM" --description "Windows 10 VM with FLARE-VM default configuration"
28 | """
29 |
30 |
31 | def export_snapshot(vm_name, snapshot, description, export_dir_name):
32 | """Restore a snapshot, set the network to hostonly and then export it with the snapshot as name."""
33 | vm_uuid = get_vm_uuid(vm_name)
34 | if not vm_uuid:
35 | print(f'❌ ERROR: "{vm_name}" not found')
36 | exit()
37 |
38 | print(f'\nExporting snapshot "{snapshot}" from "{vm_name}" {vm_uuid}...')
39 | try:
40 | restore_snapshot(vm_uuid, snapshot)
41 |
42 | set_network_to_hostonly(vm_uuid)
43 |
44 | # Start the VM to ensure everything is good
45 | print(f"VM {vm_uuid} 🔄 power cycling before export{LONG_WAIT}")
46 | ensure_vm_running(vm_uuid)
47 | export_vm(vm_uuid, snapshot, description, export_dir_name)
48 | except Exception as e:
49 | print(f'VM {vm_uuid} ❌ ERROR exporting "{snapshot}":{e}\n')
50 |
51 |
52 | def main(argv=None):
53 | if argv is None:
54 | argv = sys.argv[1:]
55 |
56 | parser = argparse.ArgumentParser(
57 | description=DESCRIPTION,
58 | epilog=EPILOG,
59 | formatter_class=argparse.RawDescriptionHelpFormatter,
60 | )
61 | parser.add_argument("vm_name", help="name of the VM to export a snapshot from.")
62 | parser.add_argument("snapshot", help="name of the snapshot to export.")
63 | parser.add_argument("--description", help="description of the exported OVA. Empty by default.")
64 | parser.add_argument(
65 | "--export_dir_name",
66 | help="name of the directory in HOME to export the VMs The directory is created if it does not exist. Default: {EXPORTED_DIR_NAME}",
67 | )
68 | args = parser.parse_args(args=argv)
69 |
70 | export_snapshot(args.vm_name, args.snapshot, args.description, args.export_dir_name)
71 |
72 |
73 | if __name__ == "__main__":
74 | main()
75 |
--------------------------------------------------------------------------------
/virtualbox/vboxcommon.py:
--------------------------------------------------------------------------------
1 | # Copyright 2024 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import hashlib
16 | import os
17 | import re
18 | import subprocess
19 | import sys
20 | import time
21 | from datetime import datetime
22 |
23 | # Message to add to the output when waiting for a long operation to complete.
24 | LONG_WAIT = "... (it will take some time, go for an 🍦!)"
25 |
26 | # Default name of the directory in HOME to export VMs to
27 | EXPORT_DIR_NAME = "EXPORTED VMS"
28 |
29 |
30 | def format_arg(arg):
31 | """Add quotes to the string arg if it contains special characters like spaces."""
32 | if any(c in arg for c in (" ", "\\", "/")):
33 | if "'" not in arg:
34 | return f"'{arg}'"
35 | if '"' not in arg:
36 | return f'"{arg}"'
37 | return arg
38 |
39 |
40 | def cmd_to_str(cmd):
41 | """Convert a list of string arguments to a string."""
42 | return " ".join(format_arg(arg) for arg in cmd)
43 |
44 |
45 | def __run_vboxmanage(cmd, real_time=False):
46 | """Run a command using 'subprocess.run' and return the output.
47 |
48 | Args:
49 | cmd: list with the command and its arguments
50 | real_time: Boolean that determines if displaying the output in realtime or returning it.
51 | """
52 | if real_time:
53 | return subprocess.run(cmd, stderr=sys.stderr, stdout=sys.stdout)
54 | else:
55 | return subprocess.run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
56 |
57 |
58 | def run_vboxmanage(cmd, real_time=False):
59 | """Run a VBoxManage command and return the output.
60 |
61 | Args:
62 | cmd: list of string arguments to pass to VBoxManage
63 | real_time: Boolean that determines if displaying the output in realtime or returning it.
64 | """
65 | cmd = ["VBoxManage"] + cmd
66 | result = __run_vboxmanage(cmd, real_time)
67 |
68 | if result.returncode:
69 | # Check if we are affect by the following VERR_NO_LOW_MEMORY bug: https://www.virtualbox.org/ticket/22185
70 | # and re-run the command every minute until the VERR_NO_LOW_MEMORY error is resolved
71 | while result.stdout and "VERR_NO_LOW_MEMORY" in result.stdout:
72 | print("❌ VirtualBox VERR_NO_LOW_MEMORY error (likely https://www.virtualbox.org/ticket/22185)")
73 | print("🩹 Fit it running 'echo 3 | sudo tee /proc/sys/vm/drop_caches'")
74 | print("⏳ I'll re-try the command in ~ 1 minute\n")
75 | time.sleep(60) # wait 1 minutes
76 |
77 | # Re-try command
78 | result = __run_vboxmanage(cmd, real_time)
79 |
80 | if result.returncode:
81 | error = f"Command '{cmd_to_str(cmd)}' failed"
82 | # Use only the first "VBoxManage: error:" line to prevent using the long
83 | # VBoxManage help message or noisy information like the details and context.
84 | if result.stdout:
85 | match = re.search("^VBoxManage: error: (?P.*)", result.stdout, flags=re.M)
86 | if match:
87 | error += f": {match['err_info']}"
88 | raise RuntimeError(error)
89 |
90 | return result.stdout
91 |
92 |
93 | def control_guest(vm_uuid, user, password, args, real_time=False):
94 | """Run a 'VBoxManage guestcontrol' command providing the username and password.
95 | Args:
96 | vm_uuid: VM UUID
97 | args: list of arguments starting with the guestcontrol sub-command
98 | real_time: Boolean that determines if displaying the output in realtime or returning it.
99 | """
100 | # VM must be running to control the guest
101 | ensure_vm_running(vm_uuid)
102 | cmd = ["guestcontrol", vm_uuid, f"--username={user}", f"--password={password}"] + args
103 | try:
104 | return run_vboxmanage(cmd, real_time)
105 | except RuntimeError:
106 | # The guest additions take a bit to load after the user is logged in
107 | # In slow environments this may cause the command to fail, wait a bit and re-try
108 | time.sleep(120) # Wait 2 minutes
109 | return run_vboxmanage(cmd, real_time)
110 |
111 |
112 | def get_hostonlyif_name():
113 | """Get the name of the host-only interface. Return None if there is no host-only interface"""
114 | # Example of `VBoxManage list hostonlyifs` relevant output:
115 | # Name: vboxnet0
116 | hostonlyifs_info = run_vboxmanage(["list", "hostonlyifs"])
117 |
118 | match = re.search(r"^Name: *(?P\S+)", hostonlyifs_info, flags=re.M)
119 | if match:
120 | return match["hostonlyif_name"]
121 |
122 |
123 | def ensure_hostonlyif_exists():
124 | """Get the name of the host-only interface. Create the interface if it doesn't exist."""
125 | hostonlyif_name = get_hostonlyif_name()
126 |
127 | if not hostonlyif_name:
128 | # No host-only interface found, create one
129 | run_vboxmanage(["hostonlyif", "create"])
130 |
131 | hostonlyif_name = get_hostonlyif_name()
132 | if not hostonlyif_name:
133 | raise RuntimeError("Failed to create new hostonly interface.")
134 |
135 | print(f"Hostonly interface created: {hostonlyif_name}")
136 |
137 | return hostonlyif_name
138 |
139 |
140 | def set_network_to_hostonly(vm_uuid):
141 | """Set the NIC 1 to hostonly and disable the rest."""
142 | # VM must be shutdown before changing the adapters
143 | ensure_vm_shutdown(vm_uuid)
144 |
145 | # Ensure a hostonly interface exists to prevent issues starting the VM
146 | ensure_hostonlyif_exists()
147 |
148 | # Example of `VBoxManage showvminfo --machinereadable` relevant output:
149 | # nic1="none"
150 | # bridgeadapter2="wlp9s0"
151 | # macaddress2="0800271DDA9D"
152 | # cableconnected2="on"
153 | # nic2="bridged"
154 | # nictype2="82540EM"
155 | # nicspeed2="0"
156 | # nic3="none"
157 | # nic4="none"
158 | # nic5="none"
159 | # nic6="none"
160 | # nic7="none"
161 | # nic8="none"
162 | vm_info = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"])
163 |
164 | # Set all NICs to none to avoid running into strange situations
165 | for nic_number, nic_value in re.findall(r'^nic(\d+)="(\S+)"', vm_info, flags=re.M):
166 | if nic_value != "none": # Ignore NICs that are already none
167 | run_vboxmanage(["modifyvm", vm_uuid, f"--nic{nic_number}", "none"])
168 |
169 | # Set NIC 1 to hostonly
170 | run_vboxmanage(["modifyvm", vm_uuid, "--nic1", "hostonly"])
171 |
172 | # Ensure changes applied
173 | vm_info = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"])
174 | nic_values = re.findall(r'^nic\d+="(\S+)"', vm_info, flags=re.M)
175 | if nic_values[0] != "hostonly" or any(nic_value != "none" for nic_value in nic_values[1:]):
176 | raise RuntimeError(f"Unable to change NICs to a single hostonly in VM {vm_uuid}")
177 |
178 | print(f"VM {vm_uuid} ⚙️ network set to single hostonly adapter")
179 |
180 |
181 | def sha256_file(filepath):
182 | """Return the SHA256 of the content of the file provided as argument."""
183 | with open(filepath, "rb") as f:
184 | return hashlib.file_digest(f, "sha256").hexdigest()
185 |
186 |
187 | def export_vm(vm_uuid, exported_vm_name, description="", export_dir_name=EXPORT_DIR_NAME):
188 | """Export VM as OVA and generate a file with the SHA256 of the exported OVA."""
189 | # Create export directory
190 | export_directory = os.path.expanduser(f"~/{export_dir_name}")
191 | os.makedirs(export_directory, exist_ok=True)
192 |
193 | exported_ova_filepath = os.path.join(export_directory, f"{exported_vm_name}.ova")
194 |
195 | # Rename OVA if it already exists (for example if the script is called twice) or exporting will fail
196 | if os.path.exists(exported_ova_filepath):
197 | time_str = datetime.now().strftime("%H_%M")
198 | old_ova_filepath = os.path.join(export_directory, f"{exported_vm_name}.{time_str}.ova")
199 | os.rename(exported_ova_filepath, old_ova_filepath)
200 | print(f"⚠️ Renamed old OVA to export new one: {old_ova_filepath}")
201 |
202 | # Turn off VM and export it to .ova
203 | ensure_vm_shutdown(vm_uuid)
204 | print(f"VM {vm_uuid} 🚧 exporting {LONG_WAIT}")
205 | run_vboxmanage(
206 | [
207 | "export",
208 | vm_uuid,
209 | f"--output={exported_ova_filepath}",
210 | "--vsys=0", # We need to specify the index of the VM, 0 as we only export 1 VM
211 | f"--vmname={exported_vm_name}",
212 | f"--description={description}",
213 | ]
214 | )
215 | print(f'VM {vm_uuid} ✅ EXPORTED "{exported_ova_filepath}"')
216 |
217 | # Generate file with SHA256
218 | sha256 = sha256_file(exported_ova_filepath)
219 | sha256_filepath = f"{exported_ova_filepath}.sha256"
220 | with open(sha256_filepath, "w") as f:
221 | f.write(sha256)
222 |
223 | print(f'VM {vm_uuid} ✅ GENERATED "{sha256_filepath}": {sha256}\n')
224 |
225 |
226 | def get_vm_uuid(vm_name):
227 | """Get the machine UUID for a given VM name using 'VBoxManage list vms'. Return None if not found."""
228 | # regex VM name and extract the GUID
229 | # Example of `VBoxManage list vms` output:
230 | # "FLARE-VM.testing" {b76d628b-737f-40a3-9a16-c5f66ad2cfcc}
231 | # "FLARE-VM" {a23c0c37-2062-4cf0-882b-9e9747dd33b6}
232 | vms_info = run_vboxmanage(["list", "vms"])
233 |
234 | match = re.search(rf'^"{vm_name}" (?P\{{.*?\}})', vms_info, flags=re.M)
235 | if match:
236 | return match.group("uuid")
237 |
238 |
239 | def get_vm_state(vm_uuid):
240 | """Get the VM state using 'VBoxManage showvminfo'."""
241 | # Example of `VBoxManage showvminfo --machinereadable` relevant output:
242 | # VMState="poweroff"
243 | vm_info = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"])
244 |
245 | match = re.search(r'^VMState="(?P\S+)"', vm_info, flags=re.M)
246 | if match:
247 | return match["state"]
248 |
249 | raise Exception(f"Unable to get state of VM {vm_uuid}")
250 |
251 |
252 | def get_num_logged_in_users(vm_uuid):
253 | """Return the number of logged in users using 'VBoxManage guestproperty'."""
254 | # Examples of 'VBoxManage guestproperty get "/VirtualBox/GuestInfo/OS/LoggedInUsers"' output:
255 | # - 'Value: 1'
256 | # - 'Value: 0'
257 | # - 'No value set!'
258 | logged_in_users_info = run_vboxmanage(["guestproperty", "get", vm_uuid, "/VirtualBox/GuestInfo/OS/LoggedInUsers"])
259 |
260 | if logged_in_users_info:
261 | match = re.search(r"^Value: (?P\d+)", logged_in_users_info)
262 | if match:
263 | return int(match["logged_in_users"])
264 | return 0
265 |
266 |
267 | def wait_until(vm_uuid, condition):
268 | """Wait for VM to verify a condition
269 |
270 | Return True if the condition is met within one minute.
271 | Return False otherwise.
272 | """
273 | timeout = 60 # seconds
274 | check_interval = 5 # seconds
275 | start_time = time.time()
276 | while time.time() - start_time < timeout:
277 | if eval(condition):
278 | time.sleep(5) # wait a bit to be careful and avoid any weird races
279 | return True
280 | time.sleep(check_interval)
281 | return False
282 |
283 |
284 | def ensure_vm_running(vm_uuid):
285 | """Start the VM if its state is not 'running' and ensure the user is logged in."""
286 | vm_state = get_vm_state(vm_uuid)
287 | if not vm_state == "running":
288 | print(f"VM {vm_uuid} state: {vm_state}. Starting VM...")
289 | run_vboxmanage(["startvm", vm_uuid, "--type", "gui"])
290 |
291 | # Wait until at least 1 user is logged in.
292 | if not wait_until(vm_uuid, "get_num_logged_in_users(vm_uuid)"):
293 | raise RuntimeError(f"Unable to start VM {vm_uuid}.")
294 |
295 |
296 | def ensure_vm_shutdown(vm_uuid):
297 | """Shut down the VM if its state is not 'poweroff'. If the VM status is 'saved' start it before shutting it down."""
298 | vm_state = get_vm_state(vm_uuid)
299 | if vm_state == "poweroff":
300 | return
301 |
302 | # If the state is aborted, the VM is not running and can't be turned off
303 | # Log the state and return
304 | if vm_state in ("aborted-saved", "aborted"):
305 | print(f"VM {vm_uuid} state: {vm_state}")
306 | return
307 |
308 | if vm_state == "saved":
309 | ensure_vm_running(vm_uuid)
310 | vm_state = get_vm_state(vm_uuid)
311 |
312 | print(f"VM {vm_uuid} state: {vm_state}. Shutting down VM...")
313 | run_vboxmanage(["controlvm", vm_uuid, "poweroff"])
314 |
315 | if not wait_until(vm_uuid, "get_vm_state(vm_uuid) == 'poweroff'"):
316 | raise RuntimeError(f"Unable to shutdown VM {vm_uuid}.")
317 |
318 |
319 | def restore_snapshot(vm_uuid, snapshot_name):
320 | """Restore a given snapshot in the given VM."""
321 | # VM must be shutdown before restoring snapshot
322 | ensure_vm_shutdown(vm_uuid)
323 |
324 | run_vboxmanage(["snapshot", vm_uuid, "restore", snapshot_name])
325 | print(f'VM {vm_uuid} ✨ restored snapshot "{snapshot_name}"')
326 |
327 |
328 | def rename_old_snapshot(vm_uuid, snapshot_name):
329 | """Append 'OLD' to the name of all snapshots with the given name within the specified VM.
330 |
331 | Args:
332 | vm_uuid: VM UUID
333 | snapshot_name: The current name of the snapshot(s) to rename.
334 | """
335 | # Example of 'VBoxManage snapshot VM_NAME list --machinereadable' output:
336 | # SnapshotName="ROOT"
337 | # SnapshotUUID="86b38fc9-9d68-4e4b-a033-4075002ab570"
338 | # SnapshotName-1="Snapshot 1"
339 | # SnapshotUUID-1="e383e702-fee3-4e0b-b1e0-f3b869dbcaea"
340 | snapshots_info = run_vboxmanage(["snapshot", vm_uuid, "list", "--machinereadable"])
341 |
342 | # Find how many snapshots have the given name and edit a snapshot with that name as many times
343 | snapshots = re.findall(rf'^SnapshotName(-\d+)*="{snapshot_name}"\n', snapshots_info, flags=re.M)
344 | for _ in range(len(snapshots)):
345 | run_vboxmanage(["snapshot", vm_uuid, "edit", snapshot_name, f"--name='{snapshot_name} OLD"])
346 |
347 |
348 | def take_snapshot(vm_uuid, snapshot_name, shutdown=False, rename=False):
349 | """Take a snapshot of the specified VM with the given name, optionally shutting down first and renaming duplicates.
350 |
351 | Args:
352 | vm_uuid: VM UUID
353 | snapshot_name: The name for the new snapshot.
354 | shutdown: If True, shut down the VM before taking the snapshot.
355 | rename: If True, renames any existing snapshots with the same `snapshot_name`
356 | by appending ' OLD' to their names before taking the new snapshot.
357 | """
358 | if shutdown:
359 | ensure_vm_shutdown(vm_uuid)
360 |
361 | if rename:
362 | rename_old_snapshot(vm_uuid, snapshot_name)
363 |
364 | run_vboxmanage(["snapshot", vm_uuid, "take", snapshot_name])
365 | print(f'VM {vm_uuid} 📷 took snapshot "{snapshot_name}"')
366 |
--------------------------------------------------------------------------------