├── .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 | FLARE-VM Logo 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 | ![Installer GUI](Images/installer-gui.png) 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 | ![Before](../Images/vbox-clean-snapshots_before.png) 59 | 60 | ##### After 61 | 62 | ![After](../Images/vbox-clean-snapshots_after.png) 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 | ![Notification](../Images/vbox-adapter-check_notification.png) 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 | --------------------------------------------------------------------------------