├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── aeacus.go ├── assets ├── fonts │ └── Raleway │ │ ├── OFL.txt │ │ ├── Raleway-VariableFont_wght.ttf │ │ └── static │ │ ├── Raleway-Black.ttf │ │ ├── Raleway-Bold.ttf │ │ ├── Raleway-ExtraBold.ttf │ │ ├── Raleway-ExtraLight.ttf │ │ ├── Raleway-Italic.ttf │ │ ├── Raleway-Light.ttf │ │ ├── Raleway-Medium.ttf │ │ ├── Raleway-Regular.ttf │ │ ├── Raleway-SemiBold.ttf │ │ └── Raleway-Thin.ttf ├── img │ ├── Buttonbackground.png │ ├── TeamIDbackground.png │ ├── background.png │ ├── logo.ico │ └── logo.png ├── scripts │ └── stop_scoring.sh └── wav │ ├── alarm.wav │ └── gain.wav ├── checks.go ├── checks_linux.go ├── checks_linux_test.go ├── checks_test.go ├── checks_windows.go ├── checks_windows_test.go ├── configs.go ├── crypto.go ├── crypto_test.go ├── docs ├── checks.md ├── conditions.md ├── config.md ├── crypto.md ├── examples │ ├── linux-remote.conf │ └── windows.conf ├── hints.md ├── regex.md ├── remote.md ├── security.md ├── securitypolicy.md ├── userproperties.md └── v2.md ├── go.mod ├── go.sum ├── gui_linux.go ├── gui_windows.go ├── info_windows.go ├── misc ├── desktop │ ├── ReadMe.desktop │ ├── ScoringReport.desktop │ ├── StopScoring.desktop │ └── TeamID.desktop ├── dev │ ├── CSSClient │ ├── ReadMe.conf │ └── gen-crypto.sh ├── gh │ ├── ReadMe.png │ ├── ScoringReport.png │ └── hint.png └── tests │ ├── TestFileContains.txt │ └── dir │ ├── hello │ └── 3.txt │ └── world │ ├── 1.txt │ └── 2.txt ├── output.go ├── phocus.go ├── phocus_linux.go ├── phocus_windows.go ├── policies_windows.go ├── release_linux.go ├── release_windows.go ├── remote.go ├── score.go ├── shell_linux.go ├── shell_windows.go ├── utility.go ├── utility_linux.go ├── utility_windows.go └── web.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.yml] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Set up Go 11 | uses: actions/setup-go@v2 12 | with: 13 | go-version: 1.18 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v2 18 | 19 | - name: Build 20 | run: make -j$(nproc) release 21 | 22 | - name: Release 23 | uses: softprops/action-gh-release@v1 24 | if: startsWith(github.ref, 'refs/tags/') 25 | with: 26 | files: aeacus-*.zip 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | scoring.dat 4 | scoring.conf 5 | ScoringReport.html 6 | ReadMe.html 7 | aeacus 8 | phocus 9 | previous.txt 10 | TeamID.txt 11 | tags 12 | *.swp 13 | *.zip 14 | *.exe 15 | .keys 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := all 2 | .SILENT: release-lin release-win release 3 | 4 | all: 5 | CGO_ENABLED=0 GOOS=windows go build -ldflags '-s -w' -tags phocus -o ./phocus.exe . && \ 6 | CGO_ENABLED=0 GOOS=windows go build -ldflags '-s -w' -o ./aeacus.exe . && \ 7 | echo "Windows production build successful!" && \ 8 | CGO_ENABLED=0 GOOS=linux go build -ldflags '-s -w' -tags phocus -o ./phocus . && \ 9 | CGO_ENABLED=0 GOOS=linux go build -ldflags '-s -w' -o ./aeacus . && \ 10 | echo "Linux production build successful!" 11 | 12 | all-dev: 13 | CGO_ENABLED=0 GOOS=windows go build -tags phocus -o ./phocus.exe . && \ 14 | CGO_ENABLED=0 GOOS=windows go build -o ./aeacus.exe . && \ 15 | echo "Windows development build successful!" && \ 16 | CGO_ENABLED=0 GOOS=linux go build -tags phocus -o ./phocus . && \ 17 | CGO_ENABLED=0 GOOS=linux go build -o ./aeacus . && \ 18 | echo "Linux development build successful!" 19 | 20 | lin: 21 | CGO_ENABLED=0 GOOS=linux go build -ldflags '-s -w' -tags phocus -o ./phocus . && \ 22 | CGO_ENABLED=0 GOOS=linux go build -ldflags '-s -w' -o ./aeacus . && \ 23 | echo "Linux production build successful!" 24 | 25 | lin-dev: 26 | CGO_ENABLED=0 GOOS=linux go build -tags phocus -o ./phocus . && \ 27 | CGO_ENABLED=0 GOOS=linux go build -o ./aeacus . && \ 28 | echo "Linux development build successful!" 29 | 30 | win: 31 | CGO_ENABLED=0 GOOS=windows go build -ldflags '-s -w' -tags phocus -o ./phocus.exe . && \ 32 | CGO_ENABLED=0 GOOS=windows go build -ldflags '-s -w' -o ./aeacus.exe . && \ 33 | echo "Windows production build successful!" 34 | 35 | win-dev: 36 | CGO_ENABLED=0 GOOS=windows go build -tags phocus -o ./phocus.exe . && GOOS=windows go build -o ./aeacus.exe . && \ 37 | echo "Windows development build successful!" 38 | 39 | release: 40 | echo "Building obfuscated binaries..." && \ 41 | sh misc/dev/gen-crypto.sh && \ 42 | CGO_ENABLED=0 GOOS=windows go build -ldflags '-s -w' -tags phocus -o ./phocus.exe . && \ 43 | CGO_ENABLED=0 GOOS=windows go build -ldflags '-s -w' -o ./aeacus.exe . && \ 44 | echo "Windows production build successful!" && \ 45 | CGO_ENABLED=0 GOOS=linux go build -ldflags '-s -w' -tags phocus -o ./phocus . && \ 46 | CGO_ENABLED=0 GOOS=linux go build -ldflags '-s -w' -o ./aeacus . && \ 47 | echo "Linux production build successful!" && \ 48 | mv crypto.go.bak crypto.go && \ 49 | echo "Restored crypto.go" && \ 50 | mkdir aeacus-win32/ && mkdir aeacus-linux/ && \ 51 | mv aeacus.exe aeacus-win32/aeacus.exe && \ 52 | mv phocus.exe aeacus-win32/phocus.exe && \ 53 | mv aeacus aeacus-linux/aeacus && \ 54 | mv phocus aeacus-linux/phocus && \ 55 | cp -Rf assets/ aeacus-win32/ && \ 56 | cp -Rf misc/ aeacus-win32/ && \ 57 | cp -Rf LICENSE aeacus-win32/ && \ 58 | cp -Rf assets/ aeacus-linux/ && \ 59 | cp -Rf misc/ aeacus-linux/ && \ 60 | cp -Rf LICENSE aeacus-linux/ && \ 61 | zip -r aeacus-win32.zip aeacus-win32/ > /dev/null && \ 62 | echo "Successfully compressed aeacus-win32!" && \ 63 | zip -r aeacus-linux.zip aeacus-linux/ > /dev/null && \ 64 | echo "Successfully compressed aeacus-linux!" && \ 65 | rm -rf aeacus-win32/ && rm -rf aeacus-linux/ 66 | 67 | release-lin: 68 | echo "Building obfuscated binaries..." && \ 69 | sh misc/dev/gen-crypto.sh && \ 70 | CGO_ENABLED=0 GOOS=linux go build -ldflags '-s -w' -tags phocus -o ./phocus . && \ 71 | CGO_ENABLED=0 GOOS=linux go build -ldflags '-s -w' -o ./aeacus . && \ 72 | echo "Linux production build successful!" && \ 73 | mv crypto.go.bak crypto.go && \ 74 | echo "Restored crypto.go" && \ 75 | mkdir aeacus-linux/ && \ 76 | mv aeacus aeacus-linux/aeacus && \ 77 | mv phocus aeacus-linux/phocus && \ 78 | cp -Rf assets/ aeacus-linux/ && \ 79 | cp -Rf misc/ aeacus-linux/ && \ 80 | cp -Rf LICENSE aeacus-linux/ && \ 81 | zip -r aeacus-linux.zip aeacus-linux/ > /dev/null && \ 82 | echo "Successfully compressed aeacus-linux!" && \ 83 | rm -rf aeacus-linux/ 84 | 85 | release-win: 86 | echo "Building obfuscated binaries..." && \ 87 | sh misc/dev/gen-crypto.sh && \ 88 | CGO_ENABLED=0 GOOS=windows go build -ldflags '-s -w' -tags phocus -o ./phocus.exe . && \ 89 | CGO_ENABLED=0 GOOS=windows go build -ldflags '-s -w' -o ./aeacus.exe . && \ 90 | echo "Windows production build successful!" && \ 91 | mv crypto.go.bak crypto.go && \ 92 | echo "Restored crypto.go" && \ 93 | mkdir aeacus-win32/ && \ 94 | mv aeacus.exe aeacus-win32/aeacus.exe && \ 95 | mv phocus.exe aeacus-win32/phocus.exe && \ 96 | cp -Rf assets/ aeacus-win32/ && \ 97 | cp -Rf misc/ aeacus-win32/ && \ 98 | cp -Rf LICENSE aeacus-win32/ && \ 99 | zip -r aeacus-win32.zip aeacus-win32/ > /dev/null && \ 100 | echo "Successfully compressed aeacus-win32!" && \ 101 | rm -rf aeacus-win32/ 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aeacus [![Go Report Card](https://goreportcard.com/badge/github.com/elysium-suite/aeacus)](https://goreportcard.com/report/github.com/elysium-suite/aeacus) 2 | 3 | aeacus logo 4 | 5 | `aeacus` is a vulnerability scoring engine for Windows and Linux, with an emphasis on simplicity. 6 | 7 | ## V2 8 | 9 | `aeacus` has recently been updated to version 2.0.0! To view the breaking changes, refer to [./docs/v2.md](./docs/v2.md). 10 | 11 | ## Installation 12 | 13 | 0. **Extract the release** into `/opt/aeacus` (Linux) or `C:\aeacus\` (Windows). 14 | 15 | > Try compiling it yourself! Or, you can [download the releases here](https://github.com/elysium-suite/aeacus/releases). 16 | 17 | 1. **Set up the environment.** 18 | 19 | - Put your **config** in `/opt/aeacus/scoring.conf` or`C:\aeacus\scoring.conf`. 20 | 21 | - _Don't have a config? See the example below._ 22 | 23 | - Put your **README data** in `ReadMe.conf`. 24 | 25 | 2. **Check that your config is valid.** 26 | 27 | ``` 28 | ./aeacus --verbose check 29 | ``` 30 | 31 | > Check out what you can do with `aeacus` with `./aeacus --help`! 32 | 33 | 3. **Score the image with the current config to verify your checks work as expected.** 34 | 35 | ``` 36 | ./aeacus --verbose score 37 | ``` 38 | 39 | > The TeamID is read from `/opt/aeacus/TeamID.txt` or `C:\aeacus\TeamID.txt`. 40 | 41 | 4. **Check your README and make sure it is to your liking!** 42 | 43 | ``` 44 | ./aeacus --verbose readme 45 | ``` 46 | 47 | > The ReadMe file will be placed in `/opt/aeacus/assets/ReadMe.html` or `C:\aeacus\assets\ReadMe.html` 48 | 49 | 4. **Prepare the image for release.** 50 | 51 | > **WARNING**: This will remove `scoring.conf`. Back it up somewhere if you want to save it! It will also remove the `aeacus` executable and other sensitive files. 52 | 53 | ``` 54 | ./aeacus --verbose release 55 | ``` 56 | 57 | ## Screenshots 58 | 59 | ### Scoring Report: 60 | 61 | ![Scoring Report](./misc/gh/ScoringReport.png) 62 | 63 | ### ReadMe: 64 | 65 | ![ReadMe](./misc/gh/ReadMe.png) 66 | 67 | ## Features 68 | 69 | - Robust yet simple vulnerability scorer 70 | - Image preparation (cleanup, README, etc) 71 | - Remote score reporting 72 | 73 | > Note: `aeacus` ships with weak crypto on purpose. You should implement your own crypto functions if you want to make it harder to crack with static analysis. See [Adding Crypto](/docs/crypto.md) for more information. 74 | 75 | ## Compiling 76 | 77 | Only Linux development environments are officially supported. Ubuntu virtual machines work great. 78 | 79 | Make sure you have a recent version of `go` installed, as well as `git` and `make`. If you want to compile Windows and Linux, install all dependencies using `go get -v -d -t ./...`. Then to compile, use `go build`, OR make: 80 | 81 | - Building for `Linux`: `make lin` 82 | - Building for `Windows`: `make win` 83 | 84 | ### Development 85 | 86 | If you're developing for `aeacus`, compile with these commands to leave debug symbols in the binaries: 87 | 88 | - Building for `Linux`: `make lin-dev` 89 | - Building for `Windows`: `make win-dev` 90 | 91 | ### Releases 92 | 93 | You can build release files (e.g., `aeacus-linux.zip`). These will have auto-randomized `crypto.go` files. 94 | 95 | - Building both platforms: `make release` 96 | 97 | ## Documentation 98 | 99 | All check condition types (with examples and notes) [are documented here](docs/checks.md). 100 | 101 | Other documentation: 102 | - [Non-Check Scoring Configuration](docs/config.md) 103 | - [Condition Precedence](docs/conditions.md) 104 | - [Adding Hints](docs/hints.md) 105 | - [Crypto](docs/crypto.md) 106 | - [Security Model](docs/security.md) 107 | - [Remote Reporting](docs/remote.md) 108 | - [Windows Security Policy](docs/securitypolicy.md) 109 | 110 | ## Remote Endpoint 111 | 112 | Set the `remote` field in the configuration, and your image will use remote scoring. If you want remote scoring, you will need to host a remote scoring endpoint. The authors of this project recommend using [sarpedon](https://github.com/elysium-suite/sarpedon). See [this example remote configuration for Linux aeacus](docs/examples/linux-remote.conf). 113 | 114 | ## Configuration 115 | 116 | The configuration is written in TOML. Here is a minimal example: 117 | 118 | ```toml 119 | name = "ubuntu-18-supercool" # Image name 120 | title = "CoolCyberStuff Practice Round" # Round title 121 | os = "Ubuntu 18.04" # OS, used for README 122 | user = "coolUser" # Main user for the image 123 | 124 | # Set the aeacus version of this scoring file. Set this to the version 125 | # of aeacus you are using. This is used to make sure your configuration, 126 | # if re-used, is compatible with the version of aeacus being used. 127 | # 128 | # You can print your version of aeacus with ./aeacus version. 129 | version = "2.1.1" 130 | 131 | [[check]] 132 | message = "Removed insecure sudoers rule" 133 | points = 10 134 | 135 | [[check.pass]] 136 | type = "FileContainsNot" 137 | path = "/etc/sudoers" 138 | value = "NOPASSWD" 139 | 140 | [[check]] 141 | # If no message is specified, one is auto-generated 142 | points = 20 143 | 144 | [[check.pass]] 145 | type = "PathExistsNot" 146 | path = "/usr/bin/ufw-backdoor" 147 | 148 | [[check.pass]] # You can code multiple pass conditions, but 149 | type = "FirewallUp" # they must ALL succeed for the check to pass! 150 | 151 | [[check]] 152 | message = "Malicious user 'user' can't read /etc/shadow" 153 | # If no points are specified, they are auto-calculated out of 100. 154 | 155 | [[check.pass]] 156 | type = "PermissionIsNot" 157 | path = "/etc/shadow" 158 | value = "??????r??" 159 | 160 | [[check.pass]] # "pass" conditions are logically AND with other pass 161 | type = "UserInGroupNot" # conditions. This means they all must pass for a check 162 | user = "user" # to be considered successful. 163 | group = "sudo" 164 | 165 | [[check.passoverride]] # If you want a check to succeed when any condition 166 | type = "UserExistsNot" # passes, regardless of other pass checks, use 167 | user = "user" # an override pass (passoverride). This is a logical OR. 168 | # passoverride is overridden by fail conditions. 169 | 170 | [[check.fail]] # If any fail conditions succeed, the entire check will fail. 171 | type = "PathExistsNot" 172 | path = "/etc/shadow" 173 | 174 | [[check]] 175 | message = "Administrator has been removed" 176 | points = -5 # This check is now a penalty, because it has negative points 177 | 178 | [[check.pass]] 179 | type = "UserExistsNot" 180 | user = "coolAdmin" 181 | 182 | ``` 183 | 184 | See more in-depth examples, including remote reporting, [here](https://github.com/elysium-suite/aeacus/tree/master/docs/examples). 185 | 186 | ## ReadMe Configuration 187 | 188 | Put your README in `ReadMe.conf`. Here's a commented template: 189 | 190 | ```html 191 | 192 |

193 | Uncomplicated Firewall (UFW) is the only company approved Firewall for use 194 | on Linux machines at this time. 195 |

196 | 197 | 198 |

199 | Congratulations! You just recruited a promising new team member. Create a 200 | new Standard user account named "bobbington" with a temporary password of 201 | your choosing. 202 |

203 | 204 | 205 |

Critical Services:

206 | 210 | 211 | 212 |

Authorized Administrators and Users

213 | 214 |
215 | Authorized Administrators:
216 | coolUser (you)
217 | 	password: coolPassword
218 | bob
219 | 	password: bob
220 | 
221 | Authorized Users:
222 | coolFriend
223 | awesomeUser
224 | radUser
225 | coolGuy
226 | niceUser
227 | 
228 | ``` 229 | 230 | ## Information Gathering 231 | 232 | The `aeacus` binary supports gathering information (on **Windows** only) in cases where it's tough to gather what the scoring system can see. 233 | 234 | Print information with `./aeacus info type` where `type` is one the following (NOTE: this is deprecated and will be removed in a future release): 235 | 236 | ### Windows 237 | 238 | - `programs` (shows installed programs) 239 | - `users` (shows local users) 240 | - `admins` (shows local administrator users) 241 | 242 | ## Tips and Tricks 243 | 244 | - Easily change the branding by replacing `assets/img/logo.png`. 245 | - Test your scoring configuration in a loop: 246 | ``` bash 247 | while true; do ./aeacus -v; sleep 20; done 248 | ``` 249 | - Set all .desktop files as launchable on Ubuntu+GNOME: 250 | ```bash 251 | for i in $HOME/Desktop/*.desktop; do 252 | # Try "yes" rather than true on Ubuntu <20 253 | gio set "$i" "metadata::trusted" true 254 | chmod +x "$i" 255 | done 256 | ``` 257 | 258 | ## Contributing and Disclaimer 259 | 260 | A huge thanks to the project contributors for help adding code and features, and to many others for help with feedback, usability, and finding bugs! 261 | 262 | If you have anything you would like to add or fix, please make a pull request! No improvement or fix is too small, and help is always appreciated. 263 | 264 | Thanks to UTSA CIAS and the CyberPatriot program for putting together such a cool competition, and for the inspiration to make this project. 265 | 266 | This project is in no way affiliated with or endorsed by the Air Force Association, University of Texas San Antonio, or the CyberPatriot program. 267 | -------------------------------------------------------------------------------- /aeacus.go: -------------------------------------------------------------------------------- 1 | //go:build !phocus 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | ////////////////////////////////////////////////////////////////// 12 | // .oooo. .ooooo. .oooo. .ooooo. oooo oooo .oooo.o // 13 | // `P )88b d88' `88b `P )88b d88' `"Y8 `888 `888 d88 "8 // 14 | // .oP"888 888ooo888 .oP"888 888 888 888 `"Y88b. // 15 | // d8( 888 888 .o d8( 888 888 .o8 888 888 o. )88b // 16 | // `Y888""8o `Y8bod8P' `Y888""8o `Y8bod8P' `V88V"V8P' 8""888P' // 17 | ////////////////////////////////////////////////////////////////// 18 | 19 | const ( 20 | DEBUG_BUILD = true 21 | ) 22 | 23 | func main() { 24 | app := &cli.App{ 25 | UseShortOptionHandling: true, 26 | EnableBashCompletion: true, 27 | Name: "aeacus", 28 | Usage: "score image vulnerabilities", 29 | Before: func(c *cli.Context) error { 30 | if debugEnabled { 31 | verboseEnabled = true 32 | } 33 | err := determineDirectory() 34 | if err != nil { 35 | return err 36 | } 37 | return nil 38 | }, 39 | Action: func(c *cli.Context) error { 40 | permsCheck() 41 | readConfig() 42 | scoreImage() 43 | return nil 44 | }, 45 | Flags: []cli.Flag{ 46 | &cli.BoolFlag{ 47 | Name: "verbose", 48 | Aliases: []string{"v"}, 49 | Usage: "Print extra information", 50 | Destination: &verboseEnabled, 51 | }, 52 | &cli.BoolFlag{ 53 | Name: "debug", 54 | Aliases: []string{"d"}, 55 | Usage: "Print a lot of information", 56 | Destination: &debugEnabled, 57 | }, 58 | &cli.BoolFlag{ 59 | Name: "yes", 60 | Aliases: []string{"y"}, 61 | Usage: "Automatically answer 'yes' to any prompts", 62 | Destination: &yesEnabled, 63 | }, 64 | &cli.StringFlag{ 65 | Name: "dir", 66 | Aliases: []string{"r"}, 67 | Usage: "Directory for aeacus and its files", 68 | Destination: &dirPath, 69 | }, 70 | }, 71 | Commands: []*cli.Command{ 72 | { 73 | Name: "score", 74 | Aliases: []string{"s"}, 75 | Usage: "Score image with current scoring config", 76 | Action: func(c *cli.Context) error { 77 | permsCheck() 78 | readConfig() 79 | scoreImage() 80 | return nil 81 | }, 82 | }, 83 | { 84 | Name: "check", 85 | Aliases: []string{"c"}, 86 | Usage: "Check that the scoring config is valid", 87 | Action: func(c *cli.Context) error { 88 | readConfig() 89 | return nil 90 | }, 91 | }, 92 | { 93 | Name: "readme", 94 | Aliases: []string{"rd"}, 95 | Usage: "Compile the README", 96 | Action: func(c *cli.Context) error { 97 | permsCheck() 98 | readConfig() 99 | genReadMe() 100 | return nil 101 | }, 102 | }, 103 | { 104 | Name: "encrypt", 105 | Aliases: []string{"e"}, 106 | Usage: "Encrypt scoring configuration", 107 | Action: func(c *cli.Context) error { 108 | permsCheck() 109 | readConfig() 110 | writeConfig() 111 | return nil 112 | }, 113 | }, 114 | { 115 | Name: "prompt", 116 | Aliases: []string{"p"}, 117 | Usage: "Launch TeamID GUI prompt", 118 | Action: func(c *cli.Context) error { 119 | launchIDPrompt() 120 | return nil 121 | }, 122 | }, 123 | { 124 | Name: "info", 125 | Aliases: []string{"i"}, 126 | Usage: "Get info about the system", 127 | Action: func(c *cli.Context) error { 128 | permsCheck() 129 | verboseEnabled = true 130 | getInfo(c.Args().Get(0)) 131 | return nil 132 | }, 133 | }, 134 | { 135 | Name: "version", 136 | Aliases: []string{"v"}, 137 | Usage: "Print the current version of aeacus", 138 | Action: func(c *cli.Context) error { 139 | println("aeacus version " + version) 140 | return nil 141 | }, 142 | }, 143 | { 144 | Name: "release", 145 | Aliases: []string{"r"}, 146 | Usage: "Prepare the image for release", 147 | Action: func(c *cli.Context) error { 148 | permsCheck() 149 | confirm("Are you sure you want to begin the image release process?") 150 | releaseImage() 151 | return nil 152 | }, 153 | }, 154 | }, 155 | } 156 | 157 | err := app.Run(os.Args) 158 | if err != nil { 159 | fail(err.Error()) 160 | } 161 | } 162 | 163 | // releaseImage goes through the process of checking the config, 164 | // writing the ReadMe/Desktop Files, installing the system service, 165 | // and cleaning the image for release. 166 | func releaseImage() { 167 | readConfig() 168 | writeConfig() 169 | genReadMe() 170 | writeDesktopFiles() 171 | configureAutologin() 172 | installFont() 173 | installService() 174 | confirm("Everything is done except cleanup. Are you sure you want to continue, and remove your scoring configuration and other aeacus files?") 175 | cleanUp() 176 | } 177 | -------------------------------------------------------------------------------- /assets/fonts/Raleway/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2010 The Raleway Project Authors (impallari@gmail.com), with Reserved Font Name "Raleway". 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /assets/fonts/Raleway/Raleway-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/fonts/Raleway/Raleway-VariableFont_wght.ttf -------------------------------------------------------------------------------- /assets/fonts/Raleway/static/Raleway-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/fonts/Raleway/static/Raleway-Black.ttf -------------------------------------------------------------------------------- /assets/fonts/Raleway/static/Raleway-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/fonts/Raleway/static/Raleway-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Raleway/static/Raleway-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/fonts/Raleway/static/Raleway-ExtraBold.ttf -------------------------------------------------------------------------------- /assets/fonts/Raleway/static/Raleway-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/fonts/Raleway/static/Raleway-ExtraLight.ttf -------------------------------------------------------------------------------- /assets/fonts/Raleway/static/Raleway-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/fonts/Raleway/static/Raleway-Italic.ttf -------------------------------------------------------------------------------- /assets/fonts/Raleway/static/Raleway-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/fonts/Raleway/static/Raleway-Light.ttf -------------------------------------------------------------------------------- /assets/fonts/Raleway/static/Raleway-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/fonts/Raleway/static/Raleway-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/Raleway/static/Raleway-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/fonts/Raleway/static/Raleway-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/Raleway/static/Raleway-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/fonts/Raleway/static/Raleway-SemiBold.ttf -------------------------------------------------------------------------------- /assets/fonts/Raleway/static/Raleway-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/fonts/Raleway/static/Raleway-Thin.ttf -------------------------------------------------------------------------------- /assets/img/Buttonbackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/img/Buttonbackground.png -------------------------------------------------------------------------------- /assets/img/TeamIDbackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/img/TeamIDbackground.png -------------------------------------------------------------------------------- /assets/img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/img/background.png -------------------------------------------------------------------------------- /assets/img/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/img/logo.ico -------------------------------------------------------------------------------- /assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/img/logo.png -------------------------------------------------------------------------------- /assets/scripts/stop_scoring.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if zenity --question \ 4 | --text="Would you like to stop scoring for this image?" \ 5 | --title="Aeacus SE"; then 6 | notify-send -i /opt/aeacus/assets/img/logo.png "Aeacus SE" "Stopping scoring, and shutting down." 7 | service CSSClient stop 8 | pkill -9 phocus 9 | rm -f /opt/aeacus/phocus /opt/aeacus/scoring.dat 10 | shutdown now 11 | else 12 | notify-send -i /opt/aeacus/assets/img/logo.png "Aeacus SE" "Confirmation failed!" 13 | fi 14 | -------------------------------------------------------------------------------- /assets/wav/alarm.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/wav/alarm.wav -------------------------------------------------------------------------------- /assets/wav/gain.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/assets/wav/gain.wav -------------------------------------------------------------------------------- /checks.go: -------------------------------------------------------------------------------- 1 | // checks.go contains checks that are identical for both Linux and Windows. 2 | 3 | package main 4 | 5 | import ( 6 | "crypto/sha256" 7 | "encoding/hex" 8 | "errors" 9 | "fmt" 10 | "os" 11 | "path/filepath" 12 | "reflect" 13 | "regexp" 14 | "strings" 15 | ) 16 | 17 | // check is the smallest unit that can show up on a scoring report. It holds all 18 | // the conditions for a check, and its message and points (autogenerated or 19 | // otherwise). 20 | type check struct { 21 | Message string 22 | Hint string 23 | Points int 24 | 25 | Fail []cond 26 | Pass []cond 27 | PassOverride []cond 28 | } 29 | 30 | // cond, or condition, is the parameters for a given test within a check. 31 | type cond struct { 32 | Hint string 33 | Type string 34 | 35 | Path string 36 | Cmd string 37 | User string 38 | Group string 39 | Name string 40 | Key string 41 | Value string 42 | After string 43 | regex bool 44 | } 45 | 46 | // requireArgs is a convenience function that prints a warning if any required 47 | // parameters for a given condition are not provided. 48 | func (c cond) requireArgs(args ...interface{}) { 49 | // Don't process internal calls -- assume the developers know what they're 50 | // doing. This also prevents extra errors being printed when they don't pass 51 | // required arguments. 52 | if c.Type == "" { 53 | return 54 | } 55 | 56 | v := reflect.ValueOf(c) 57 | vType := v.Type() 58 | for i := 0; i < v.NumField(); i++ { 59 | if vType.Field(i).Name == "Type" || vType.Field(i).Name == "regex" { 60 | continue 61 | } 62 | 63 | // Ignore hint fields, they only show up in the scoring report 64 | if vType.Field(i).Name == "Hint" { 65 | continue 66 | } 67 | 68 | required := false 69 | for _, a := range args { 70 | if vType.Field(i).Name == a { 71 | required = true 72 | break 73 | } 74 | } 75 | 76 | if required { 77 | if v.Field(i).String() == "" { 78 | fail(c.Type+":", "missing required argument '"+vType.Field(i).Name+"'") 79 | } 80 | } else if v.Field(i).String() != "" { 81 | warn(c.Type+":", "specifying unused argument '"+vType.Field(i).Name+"'") 82 | } 83 | } 84 | } 85 | 86 | func (c cond) String() string { 87 | output := "" 88 | v := reflect.ValueOf(c) 89 | typeOfS := v.Type() 90 | 91 | for i := 0; i < v.NumField(); i++ { 92 | if v.Field(i).String() == "" { 93 | continue 94 | } 95 | output += fmt.Sprintf("\t%s: %v\n", typeOfS.Field(i).Name, v.Field(i).String()) 96 | } 97 | return output 98 | } 99 | 100 | func handleReflectPanic(condFunc string) { 101 | if r := recover(); r != nil { 102 | fail("Check type does not exist: "+condFunc, "("+r.(*reflect.ValueError).Error()+")") 103 | } 104 | } 105 | 106 | // runCheck executes a single condition check. 107 | func runCheck(cond cond) bool { 108 | if err := deobfuscateCond(&cond); err != nil { 109 | fail(err.Error()) 110 | } 111 | defer obfuscateCond(&cond) 112 | debug("Running condition:\n", cond) 113 | 114 | not := "Not" 115 | regex := "Regex" 116 | condFunc := "" 117 | negation := false 118 | cond.regex = false 119 | 120 | // Ensure that condition type is a valid length 121 | if len(cond.Type) <= len(regex) { 122 | fail(`Condition type "` + cond.Type + `" is not long enough to be valid. Do you have a "type = 'CheckTypeHere'" for all check conditions?`) 123 | return false 124 | } 125 | condFunc = cond.Type 126 | if condFunc[len(condFunc)-len(not):] == not { 127 | negation = true 128 | condFunc = condFunc[:len(condFunc)-len(not)] 129 | } 130 | if condFunc[len(condFunc)-len(regex):] == regex { 131 | cond.regex = true 132 | condFunc = condFunc[:len(condFunc)-len(regex)] 133 | } 134 | 135 | // Catch panic if check type doesn't exist 136 | defer handleReflectPanic(condFunc) 137 | 138 | // Using reflection to find the correct function to call. 139 | vals := reflect.ValueOf(cond).MethodByName(condFunc).Call([]reflect.Value{}) 140 | result := vals[0].Bool() 141 | err := vals[1] 142 | 143 | if negation { 144 | debug("Result is", !result, "(was", result, "before negation) and error is", err) 145 | return err.IsNil() && !result 146 | } 147 | 148 | debug("Result is", result, "and error is", err) 149 | 150 | if verboseEnabled && !err.IsNil() { 151 | warn(condFunc, "returned an error:", err) 152 | } 153 | 154 | return err.IsNil() && result 155 | } 156 | 157 | // CommandContains checks if a given shell command contains a certain string. 158 | // This check will always fail if the command returns an error. 159 | func (c cond) CommandContains() (bool, error) { 160 | c.requireArgs("Cmd", "Value") 161 | out, err := shellCommandOutput(c.Cmd) 162 | if err != nil { 163 | return false, err 164 | } 165 | if c.regex { 166 | outTrim := strings.TrimSpace(out) 167 | return regexp.Match(c.Value, []byte(outTrim)) 168 | } 169 | return strings.Contains(strings.TrimSpace(out), c.Value), err 170 | } 171 | 172 | // CommandOutput checks if a given shell command produces an exact output. 173 | // This check will always fail if the command returns an error. 174 | func (c cond) CommandOutput() (bool, error) { 175 | c.requireArgs("Cmd", "Value") 176 | out, err := shellCommandOutput(c.Cmd) 177 | return strings.TrimSpace(out) == c.Value, err 178 | } 179 | 180 | // DirContains returns true if any file in the directory contains the string value provided. 181 | func (c cond) DirContains() (bool, error) { 182 | c.requireArgs("Path", "Value") 183 | result, err := cond{ 184 | Path: c.Path, 185 | }.PathExists() 186 | if err != nil { 187 | return false, err 188 | } 189 | if !result { 190 | return false, errors.New("path does not exist") 191 | } 192 | 193 | var files []string 194 | err = filepath.Walk(c.Path, func(path string, info os.FileInfo, err error) error { 195 | if !info.IsDir() { 196 | files = append(files, path) 197 | } 198 | if len(files) > 10000 { 199 | return errors.New("attempted to index too many files in recursive search") 200 | } 201 | return nil 202 | }) 203 | 204 | if err != nil { 205 | return false, err 206 | } 207 | 208 | for _, file := range files { 209 | c.Path = file 210 | result, err := c.FileContains() 211 | if os.IsPermission(err) { 212 | return false, err 213 | } 214 | if result { 215 | return result, nil 216 | } 217 | } 218 | return false, nil 219 | } 220 | 221 | // FileContains determines whether a file contains a given regular expression. 222 | // 223 | // Newlines in regex may not work as expected, especially on Windows. It's 224 | // best to not use these (ex. ^ and $). 225 | func (c cond) FileContains() (bool, error) { 226 | c.requireArgs("Path", "Value") 227 | fileContent, err := readFile(c.Path) 228 | if err != nil { 229 | return false, err 230 | } 231 | found := false 232 | for _, line := range strings.Split(fileContent, "\n") { 233 | if c.regex { 234 | found, err = regexp.Match(c.Value, []byte(line)) 235 | if err != nil { 236 | return false, err 237 | } 238 | } else { 239 | found = strings.Contains(line, c.Value) 240 | } 241 | if found { 242 | break 243 | } 244 | } 245 | return found, err 246 | } 247 | 248 | // FileEquals calculates the SHA256 sum of a file and compares it with the hash 249 | // provided in the check. 250 | func (c cond) FileEquals() (bool, error) { 251 | c.requireArgs("Path", "Value") 252 | fileContent, err := readFile(c.Path) 253 | if err != nil { 254 | return false, err 255 | } 256 | hasher := sha256.New() 257 | _, err = hasher.Write([]byte(fileContent)) 258 | if err != nil { 259 | return false, err 260 | } 261 | hash := hex.EncodeToString(hasher.Sum(nil)) 262 | return hash == c.Value, nil 263 | } 264 | 265 | // PathExists is a wrapper around os.Stat and os.IsNotExist, and determines 266 | // whether a file or folder exists. 267 | func (c cond) PathExists() (bool, error) { 268 | c.requireArgs("Path") 269 | _, err := os.Stat(c.Path) 270 | if err != nil && os.IsNotExist(err) { 271 | return false, nil 272 | } else if err != nil { 273 | return false, err 274 | } 275 | return true, nil 276 | } 277 | -------------------------------------------------------------------------------- /checks_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "os/user" 7 | "strconv" 8 | "strings" 9 | "syscall" 10 | ) 11 | 12 | func (c cond) AutoCheckUpdatesEnabled() (bool, error) { 13 | result, err := cond{ 14 | Path: "/etc/apt/apt.conf.d/", 15 | Value: `(?i)^\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*$`, 16 | regex: true, 17 | }.DirContains() 18 | // If /etc/apt/ does not exist, try dnf (RHEL) 19 | if err != nil { 20 | autoConf, err := cond{ 21 | Path: "/etc/dnf/automatic.conf", 22 | }.PathExists() 23 | if err != nil { 24 | return false, err 25 | } 26 | if autoConf { 27 | applyUpdates, err := cond{ 28 | Path: "/etc/dnf/automatic.conf", 29 | Value: `(?i)^\s*apply_updates\s*=\s*(1|on|yes|true)`, 30 | regex: true, 31 | }.FileContains() 32 | if err != nil { 33 | return false, err 34 | } 35 | 36 | autoTimer, err := cond{ 37 | Path: "/etc/systemd/system/timers.target.wants/dnf-automatic.timer", 38 | }.PathExists() 39 | if err != nil { 40 | return false, err 41 | } 42 | 43 | if applyUpdates && autoTimer { 44 | return true, nil 45 | } 46 | 47 | autoInstallTimer, err := cond{ 48 | Path: "/etc/systemd/system/timers.target.wants/dnf-automatic-install.timer", 49 | }.PathExists() 50 | if err != nil { 51 | return false, err 52 | } 53 | return autoInstallTimer, nil 54 | } 55 | 56 | } 57 | return result, err 58 | } 59 | 60 | // Command checks if a given shell command ran successfully (that is, did not 61 | // return or raise any errors). 62 | func (c cond) Command() (bool, error) { 63 | c.requireArgs("Cmd") 64 | if c.Cmd == "" { 65 | fail("Missing command for", c.Type) 66 | } 67 | err := shellCommand(c.Cmd) 68 | if err != nil { 69 | // This check does not return errors, since it is based on successful 70 | // execution. If any errors occurred, it means that the check failed, 71 | // not errored out. 72 | // 73 | // It would be an error if failure to execute the command resulted in 74 | // an inability to meaningfully score the check (e.g., if the uname 75 | // syscall failed for KernelVersion). 76 | return false, nil 77 | } 78 | return true, nil 79 | } 80 | 81 | func (c cond) FirewallUp() (bool, error) { 82 | result, err := cond{ 83 | Path: "/etc/ufw/ufw.conf", 84 | Value: `^\s*ENABLED=yes\s*$`, 85 | regex: true, 86 | }.FileContains() 87 | if err != nil { 88 | // If ufw.conf does not exist, check firewalld status (RHEL) 89 | return cond{ 90 | Name: "firewalld", 91 | }.ServiceUp() 92 | } 93 | return result, err 94 | } 95 | 96 | func (c cond) GuestDisabledLDM() (bool, error) { 97 | guestStr := `\s*allow-guest\s*=\s*false` 98 | result, err := cond{ 99 | Path: "/usr/share/lightdm/lightdm.conf.d/", 100 | Value: guestStr, 101 | regex: true, 102 | }.DirContains() 103 | if !result { 104 | return cond{ 105 | Path: "/etc/lightdm/", 106 | Value: guestStr, 107 | regex: true, 108 | }.DirContains() 109 | } 110 | return result, err 111 | } 112 | 113 | func (c cond) KernelVersion() (bool, error) { 114 | c.requireArgs("Value") 115 | utsname := syscall.Utsname{} 116 | err := syscall.Uname(&utsname) 117 | releaseUint := []byte{} 118 | for i := 0; i < 65; i++ { 119 | if utsname.Release[i] == 0 { 120 | break 121 | } 122 | releaseUint = append(releaseUint, uint8(utsname.Release[i])) 123 | } 124 | debug("System uname value is", string(releaseUint), "and our value is", c.Value) 125 | return string(releaseUint) == c.Value, err 126 | } 127 | 128 | func (c cond) PasswordChanged() (bool, error) { 129 | c.requireArgs("User", "Value") 130 | fileContent, err := readFile("/etc/shadow") 131 | if err != nil { 132 | return false, err 133 | } 134 | for _, line := range strings.Split(fileContent, "\n") { 135 | if strings.Contains(line, c.User+":") { 136 | if strings.Contains(line, c.User+":"+c.Value) { 137 | debug("Exact value found in /etc/shadow for user", c.User+":", line) 138 | return false, nil 139 | } 140 | debug("Differing value found in /etc/shadow for user", c.User+":", line) 141 | return true, nil 142 | } 143 | } 144 | return false, errors.New("user not found") 145 | } 146 | 147 | func (c cond) FileOwner() (bool, error) { 148 | c.requireArgs("Path", "Name") 149 | u, err := user.Lookup(c.Name) 150 | if err != nil { 151 | return false, err 152 | } 153 | 154 | f, err := os.Stat(c.Path) 155 | if err != nil { 156 | return false, err 157 | } 158 | 159 | uid := f.Sys().(*syscall.Stat_t).Uid 160 | o, err := strconv.ParseUint(u.Uid, 10, 32) 161 | if err != nil { 162 | return false, err 163 | } 164 | debug("File owner for", c.Path, "uid is", strconv.FormatUint(uint64(uid), 10)) 165 | return uint32(o) == uid, nil 166 | } 167 | 168 | func (c cond) PermissionIs() (bool, error) { 169 | c.requireArgs("Path", "Value") 170 | f, err := os.Stat(c.Path) 171 | if err != nil { 172 | return false, err 173 | } 174 | 175 | fileMode := f.Mode() 176 | modeBytes := []byte(fileMode.String()) 177 | if len(modeBytes) != 10 { 178 | fail("System permission string is wrong length:", string(modeBytes)) 179 | return false, errors.New("Invalid system permission string") 180 | } 181 | 182 | // Permission string includes suid/sgid as the special bit (MSB), while 183 | // GNU coreutils replaces the executable bit, which we need to emulate. 184 | if fileMode&os.ModeSetuid != 0 { 185 | modeBytes[0] = '-' 186 | modeBytes[3] = 's' 187 | } 188 | if fileMode&os.ModeSetgid != 0 { 189 | modeBytes[0] = '-' 190 | modeBytes[6] = 's' 191 | } 192 | 193 | c.Value = strings.TrimSpace(c.Value) 194 | 195 | if len(c.Value) == 9 { 196 | // If we're provided a mode string of only 9 characters, we'll assume 197 | // that the 0th bit is irrelevant and should be a wildcard 198 | c.Value = "?" + c.Value 199 | } else if len(c.Value) != 10 { 200 | fail("Your permission string is the wrong length (should be 9 or 10 characters):", c.Value) 201 | return false, errors.New("Invalid user permission string") 202 | } 203 | 204 | for i := 0; i < len(c.Value); i++ { 205 | if c.Value[i] == '?' { 206 | continue 207 | } 208 | if c.Value[i] != modeBytes[i] { 209 | return false, nil 210 | } 211 | } 212 | return true, nil 213 | } 214 | 215 | func (c cond) ProgramInstalled() (bool, error) { 216 | c.requireArgs("Name") 217 | result, err := cond{ 218 | Cmd: "dpkg -s " + c.Name, 219 | Value: " install", 220 | }.CommandContains() 221 | 222 | // If dpkg fails, use rpm 223 | if err != nil { 224 | return cond{ 225 | Cmd: "rpm -q " + c.Name, 226 | }.Command() 227 | } 228 | 229 | return result, err 230 | } 231 | 232 | func (c cond) ProgramVersion() (bool, error) { 233 | c.requireArgs("Name", "Value") 234 | return cond{ 235 | Cmd: `dpkg -s ` + c.Name + ` | grep Version | cut -d" " -f2`, 236 | Value: c.Value, 237 | }.CommandOutput() 238 | } 239 | 240 | func (c cond) ServiceUp() (bool, error) { 241 | // TODO: detect and use other init systems 242 | c.requireArgs("Name") 243 | return cond{ 244 | Cmd: "systemctl is-active " + c.Name, 245 | }.Command() 246 | } 247 | 248 | func (c cond) UserExists() (bool, error) { 249 | c.requireArgs("User") 250 | return cond{ 251 | Path: "/etc/passwd", 252 | Value: "^" + c.User + ":", 253 | regex: true, 254 | }.FileContains() 255 | } 256 | 257 | func (c cond) UserInGroup() (bool, error) { 258 | c.requireArgs("User", "Group") 259 | return cond{ 260 | Path: "/etc/group", 261 | Value: c.Group + `[0-9a-zA-Z,:\s+]+` + c.User, 262 | regex: true, 263 | }.FileContains() 264 | } 265 | -------------------------------------------------------------------------------- /checks_linux_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestCommand(t *testing.T) { 6 | c := cond{ 7 | Cmd: "echo 'hello, world!'", 8 | } 9 | 10 | // Should pass: command ran 11 | out, err := c.Command() 12 | if err != nil || out != true { 13 | t.Error(c, "failed:", out, err) 14 | } 15 | 16 | // Should fail: command return false 17 | c.Cmd = "commanddoesntexist" 18 | out, err = c.Command() 19 | if err != nil || out != false { 20 | t.Error(c, "failed:", out, err) 21 | } 22 | 23 | // Should fail: command return false 24 | c.Cmd = "cat /etc/file/doesnt/exist" 25 | out, err = c.Command() 26 | if err != nil || out != false { 27 | t.Error(c, "failed:", out, err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /checks_test.go: -------------------------------------------------------------------------------- 1 | // checks_test.go is responsible for testing all non-platform dependent checks. 2 | package main 3 | 4 | import ( 5 | "testing" 6 | ) 7 | 8 | func TestCommandContains(t *testing.T) { 9 | c := cond{ 10 | Cmd: "echo 'hello, world!'", 11 | Value: "hello, world!", 12 | } 13 | 14 | // Should pass: exact match 15 | out, err := c.CommandContains() 16 | if err != nil || out != true { 17 | t.Error(c, "failed:", out, err) 18 | } 19 | 20 | // Should pass: substring 21 | c.Value = "hello" 22 | out, err = c.CommandContains() 23 | if err != nil || out != true { 24 | t.Error(c, "failed:", out, err) 25 | } 26 | 27 | // Should fail: not substring 28 | c.Value = "bye" 29 | out, err = c.CommandContains() 30 | if err != nil || out != false { 31 | t.Error(c, "failed:", out, err) 32 | } 33 | } 34 | 35 | func TestCommandOutput(t *testing.T) { 36 | c := cond{ 37 | Cmd: "echo 'hello, world!'", 38 | Value: "hello, world!", 39 | } 40 | 41 | // Should pass: exact match 42 | out, err := c.CommandOutput() 43 | if err != nil || out != true { 44 | t.Error(c, "failed:", out, err) 45 | } 46 | 47 | // Should fail: just substring 48 | c.Value = "hello" 49 | out, err = c.CommandOutput() 50 | if err != nil || out != false { 51 | t.Error(c, "failed:", out, err) 52 | } 53 | 54 | // Should fail: not exact or substring 55 | c.Value = "bye" 56 | out, err = c.CommandOutput() 57 | if err != nil || out != false { 58 | t.Error(c, "failed:", out, err) 59 | } 60 | } 61 | 62 | func TestDirContains(t *testing.T) { 63 | c := cond{ 64 | Path: "misc/tests/dir", 65 | Value: "^efgh", 66 | } 67 | out, err := c.DirContains() 68 | if err != nil || out != true { 69 | t.Error(c, "failed:", out, err) 70 | } 71 | 72 | c.Value = "^efghabcd$" 73 | out, err = c.DirContains() 74 | if err != nil || out != true { 75 | t.Error(c, "failed:", out, err) 76 | } 77 | 78 | c.Value = "^aaaaaa$" 79 | out, err = c.DirContains() 80 | if err != nil || out != false { 81 | t.Error(c, "failed:", out, err) 82 | } 83 | 84 | c.Value = `spaces\s+in\s+it\s+[0-9]*\s+nums` 85 | out, err = c.DirContains() 86 | if err != nil || out != true { 87 | t.Error(c, "failed:", out, err) 88 | } 89 | 90 | c.Value = `spaces\s+in\s+it\s+[1-5]*\s+nums` 91 | out, err = c.DirContains() 92 | if err != nil || out != false { 93 | t.Error(c, "failed:", out, err) 94 | } 95 | 96 | } 97 | 98 | func TestFileContains(t *testing.T) { 99 | c := cond{ 100 | Path: "misc/tests/TestFileContains.txt", 101 | Value: "^hello", 102 | } 103 | out, err := c.FileContains() 104 | if err != nil || out != true { 105 | t.Error(c, "failed:", out, err) 106 | } 107 | 108 | c.Value = "nothere" 109 | out, err = c.FileContains() 110 | if err != nil || out != false { 111 | t.Error(c, "failed:", out, err) 112 | } 113 | } 114 | 115 | func TestPathExists(t *testing.T) { 116 | c := cond{ 117 | Path: "misc/tests/", 118 | } 119 | out, err := c.PathExists() 120 | if err != nil || out != true { 121 | t.Error(c, "failed:", out, err) 122 | } 123 | 124 | c.Path = "misc/doesntexist" 125 | out, err = c.PathExists() 126 | if err != nil || out != false { 127 | t.Error(c, "failed:", out, err) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /checks_windows_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | "github.com/hectane/go-acl" 9 | "github.com/stretchr/testify/assert" 10 | "golang.org/x/sys/windows" 11 | ) 12 | 13 | func TestPermissionIs(t *testing.T) { 14 | filePath := "misc/tests/permTest.txt" 15 | ioutil.WriteFile(filePath, []byte("testContent"), os.ModePerm) 16 | defer os.Remove(filePath) 17 | 18 | c := cond{ 19 | Path: filePath, 20 | Name: "BUILTIN\\Users", 21 | Value: "Read", 22 | } 23 | 24 | // users deny read access 25 | acl.Apply(filePath, true, false, acl.DenyName(windows.GENERIC_READ, "BUILTIN\\Users")) 26 | ok, err := c.PermissionIs() 27 | assert.Nil(t, err, "Should not have error") 28 | assert.NotEqual(t, ok, true, "must be Read") 29 | 30 | // users read access 31 | acl.Apply(filePath, true, false, acl.GrantName(windows.GENERIC_READ, "BUILTIN\\Users")) 32 | ok, err = c.PermissionIs() 33 | assert.Nil(t, err, "Should not have error") 34 | assert.Equal(t, ok, true, "must be Read") 35 | 36 | c.Name = "everyone" 37 | c.Value = "FullControl" 38 | 39 | // everyone deny full access 40 | acl.Apply(filePath, true, false, acl.DenyName(windows.GENERIC_ALL, "everyone")) 41 | ok, err = c.PermissionIs() 42 | assert.Nil(t, err, "Should not have error") 43 | assert.NotEqual(t, ok, true, "Must be FullControl") 44 | 45 | // everyone full access 46 | acl.Apply(filePath, true, false, acl.GrantName(windows.GENERIC_ALL, "everyone")) 47 | ok, err = c.PermissionIs() 48 | assert.Nil(t, err, "Should not have error") 49 | assert.Equal(t, ok, true, "Must be FullControl") 50 | } 51 | -------------------------------------------------------------------------------- /configs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "fmt" 7 | "os" 8 | "reflect" 9 | 10 | "github.com/BurntSushi/toml" 11 | ) 12 | 13 | // parseConfig takes the config content as a string and attempts to parse it 14 | // into the conf struct based on the TOML spec. 15 | func parseConfig(configContent string) { 16 | if configContent == "" { 17 | fail("Configuration is empty!") 18 | os.Exit(1) 19 | } 20 | md, err := toml.Decode(configContent, &conf) 21 | if err != nil { 22 | fail("Error decoding TOML: " + err.Error()) 23 | os.Exit(1) 24 | } 25 | if verboseEnabled { 26 | for _, undecoded := range md.Undecoded() { 27 | warn("Undecoded scoring configuration key \"" + undecoded.String() + "\" will not be used.") 28 | } 29 | } 30 | 31 | // If there's no remote, local must be enabled. 32 | if conf.Remote == "" { 33 | conf.Local = true 34 | if conf.DisableRemoteEncryption { 35 | fail("Remote encryption cannot be disabled if remote is not enabled!") 36 | os.Exit(1) 37 | } 38 | } else { 39 | if conf.Remote[len(conf.Remote)-1] == '/' { 40 | fail("Your remote URL must not end with a slash: try", conf.Remote[:len(conf.Remote)-1]) 41 | os.Exit(1) 42 | } 43 | if conf.Name == "" { 44 | fail("Need image name in config if remote is enabled.") 45 | os.Exit(1) 46 | } 47 | 48 | if conf.Password == "" && conf.DisableRemoteEncryption == false { 49 | fail("Need password in config if remote is enabled.") 50 | os.Exit(1) 51 | } 52 | 53 | if conf.DisableRemoteEncryption && conf.Password != "" { 54 | warn("Remote encryption is disabled, but a password is still defined!") 55 | } 56 | 57 | } 58 | 59 | // Check if the config version matches ours. 60 | if conf.Version != version { 61 | warn("Scoring version does not match Aeacus version! Compatibility issues may occur.") 62 | info("Consider updating your config to include:") 63 | info(" version = '" + version + "'") 64 | } 65 | 66 | // Print warnings for impossible checks and undefined check types. 67 | for i, check := range conf.Check { 68 | if len(check.Pass) == 0 && len(check.PassOverride) == 0 { 69 | warn("Check " + fmt.Sprintf("%d", i+1) + " does not define any possible ways to pass!") 70 | } 71 | allConditions := append(append(append([]cond{}, check.Pass[:]...), check.Fail[:]...), check.PassOverride[:]...) 72 | for j, cond := range allConditions { 73 | if cond.Type == "" { 74 | warn("Check " + fmt.Sprintf("%d condition %d", i+1, j+1) + " does not have a check type!") 75 | } 76 | } 77 | } 78 | } 79 | 80 | // writeConfig writes the in-memory config to disk as the an encrypted 81 | // configuration file. 82 | func writeConfig() { 83 | buf := new(bytes.Buffer) 84 | if err := toml.NewEncoder(buf).Encode(conf); err != nil { 85 | fail(err.Error()) 86 | os.Exit(1) 87 | return 88 | } 89 | 90 | dataPath := dirPath + scoringData 91 | encryptedConfig, err := encryptConfig(buf.String()) 92 | if err != nil { 93 | fail("Encrypting config failed: " + err.Error()) 94 | os.Exit(1) 95 | } else if verboseEnabled { 96 | info("Writing data to " + dataPath + "...") 97 | } 98 | 99 | writeFile(dataPath, encryptedConfig) 100 | } 101 | 102 | // ReadConfig parses the scoring configuration file. 103 | func readConfig() { 104 | fileContent, err := readFile(dirPath + scoringConf) 105 | if err != nil { 106 | fail("Configuration file (" + dirPath + scoringConf + ") not found!") 107 | os.Exit(1) 108 | } 109 | parseConfig(fileContent) 110 | assignPoints() 111 | assignDescriptions() 112 | if verboseEnabled { 113 | printConfig() 114 | } 115 | obfuscateConfig() 116 | } 117 | 118 | // PrintConfig offers a printed representation of the config, as parsed 119 | // by readData and parseConfig. 120 | func printConfig() { 121 | pass("Configuration " + dirPath + scoringConf + " validity check passed!") 122 | blue("CONF", scoringConf) 123 | if conf.Version != "" { 124 | pass("Version:", conf.Version) 125 | } 126 | if conf.Title == "" { 127 | red("MISS", "Title:", "N/A") 128 | } else { 129 | pass("Title:", conf.Title) 130 | } 131 | if conf.Name == "" { 132 | red("MISS", "Name:", "N/A") 133 | } else { 134 | pass("Name:", conf.Name) 135 | } 136 | if conf.OS == "" { 137 | red("MISS", "OS:", "N/A") 138 | } else { 139 | pass("OS:", conf.OS) 140 | } 141 | if conf.User == "" { 142 | red("MISS", "User:", "N/A") 143 | } else { 144 | pass("User:", conf.User) 145 | } 146 | if conf.Remote != "" { 147 | pass("Remote:", conf.Remote) 148 | } 149 | if conf.DisableRemoteEncryption { 150 | pass("Remote Encryption:", "Disabled") 151 | } else { 152 | pass("Remote Encryption:", "Enabled") 153 | } 154 | if conf.Local { 155 | pass("Local:", conf.Local) 156 | } 157 | if conf.EndDate != "" { 158 | pass("End Date:", conf.EndDate) 159 | } 160 | for i, check := range conf.Check { 161 | green("CHCK", fmt.Sprintf("Check %d (%d points):", i+1, check.Points)) 162 | fmt.Println("Message:", check.Message) 163 | for _, c := range check.Pass { 164 | fmt.Println("Pass Condition:") 165 | fmt.Print(c) 166 | } 167 | for _, c := range check.PassOverride { 168 | fmt.Println("PassOverride Condition:") 169 | fmt.Print(c) 170 | } 171 | for _, c := range check.Fail { 172 | fmt.Println("Fail Condition:") 173 | fmt.Print(c) 174 | } 175 | } 176 | } 177 | 178 | func obfuscateConfig() { 179 | if debugEnabled { 180 | debug("Obfuscating configuration...") 181 | } 182 | if err := obfuscateData(&conf.Password); err != nil { 183 | fail(err.Error()) 184 | } 185 | for i, check := range conf.Check { 186 | if err := obfuscateData(&conf.Check[i].Message); err != nil { 187 | fail(err.Error()) 188 | } 189 | if conf.Check[i].Hint != "" { 190 | if err := obfuscateData(&conf.Check[i].Hint); err != nil { 191 | fail(err.Error()) 192 | } 193 | } 194 | for j := range check.Pass { 195 | if err := obfuscateCond(&conf.Check[i].Pass[j]); err != nil { 196 | fail(err.Error()) 197 | } 198 | } 199 | for j := range check.PassOverride { 200 | if err := obfuscateCond(&conf.Check[i].PassOverride[j]); err != nil { 201 | fail(err.Error()) 202 | } 203 | } 204 | for j := range check.Fail { 205 | if err := obfuscateCond(&conf.Check[i].Fail[j]); err != nil { 206 | fail(err.Error()) 207 | } 208 | } 209 | } 210 | } 211 | 212 | // obfuscateCond is a convenience function to obfuscate all string fields of a 213 | // struct using reflection. It assumes all struct fields are strings. 214 | func obfuscateCond(c *cond) error { 215 | s := reflect.ValueOf(c).Elem() 216 | for i := 0; i < s.NumField(); i++ { 217 | if s.Type().Field(i).Name == "regex" { 218 | continue 219 | } 220 | datum := s.Field(i).String() 221 | if err := obfuscateData(&datum); err != nil { 222 | return err 223 | } 224 | s.Field(i).SetString(datum) 225 | } 226 | return nil 227 | } 228 | 229 | // deobfuscateCond is a convenience function to deobfuscate all string fields 230 | // of a struct using reflection. 231 | func deobfuscateCond(c *cond) error { 232 | s := reflect.ValueOf(c).Elem() 233 | for i := 0; i < s.NumField(); i++ { 234 | if s.Type().Field(i).Name == "regex" { 235 | continue 236 | } 237 | datum := s.Field(i).String() 238 | if err := deobfuscateData(&datum); err != nil { 239 | return err 240 | } 241 | s.Field(i).SetString(datum) 242 | } 243 | return nil 244 | } 245 | 246 | func xor(key, plaintext string) string { 247 | ciphertext := make([]byte, len(plaintext)) 248 | for i := 0; i < len(plaintext); i++ { 249 | ciphertext[i] = key[i%len(key)] ^ plaintext[i] 250 | } 251 | return string(ciphertext) 252 | } 253 | 254 | func hexEncode(inputString string) string { 255 | return hex.EncodeToString([]byte(inputString)) 256 | } 257 | 258 | func hexDecode(inputString string) (string, error) { 259 | result, err := hex.DecodeString(inputString) 260 | if err != nil { 261 | return "", err 262 | } 263 | return string(result), nil 264 | } 265 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | // crypto.go is an example to provides basic cryptographical functions for 2 | // aeacus. 3 | // 4 | // This file is not a good example of cryptographic security. However, with this 5 | // architecture of application (see security.md), it's good enough. 6 | // 7 | // Practically, it is more important that your implemented solution is different 8 | // than the example, to make reverse engineering more difficult. 9 | // 10 | // You could change this file each time you release an image, which would make 11 | // things more difficult for a would-be hacker. 12 | // 13 | // If you compile the source code yourself, using the Makefile, random strings 14 | // will be generated for you. This means that the pre-compiled release will no 15 | // longer work for decrypting your configs, which is good. 16 | 17 | package main 18 | 19 | import ( 20 | "bytes" 21 | "compress/zlib" 22 | "crypto/aes" 23 | "crypto/cipher" 24 | "crypto/rand" 25 | "crypto/sha256" 26 | "errors" 27 | "fmt" 28 | "io" 29 | "strings" 30 | ) 31 | 32 | // This string will be used for XORing the plaintext. 33 | // Again-- not cryptographically genius. 34 | // 35 | // This string will be autogenerated if you run `make release`, or the shell 36 | // script at `misc/dev/gen-crypto.sh`. 37 | const ( 38 | randomString = "HASH_HERE" 39 | ) 40 | 41 | // byteKey is used as the key for AES encryption. It will be autogenerated 42 | // upon running `make release`, or the script at `misc/dev/gen-crypto.sh`. 43 | var byteKey = []byte{0x01} 44 | 45 | // randomBytes are used to obfuscate the config values. It will be auto- 46 | // generated like the above two values. 47 | var randomBytes = []byte{1} 48 | 49 | // encryptConfig takes a plaintext string and returns an encrypted string that 50 | // should be written to the encrypted scoring data file. 51 | func encryptConfig(plainText string) (string, error) { 52 | var key string 53 | 54 | // If string is short, generate key by hashing string 55 | if len(randomString) < 64 { 56 | hasher := sha256.New() 57 | _, err := hasher.Write([]byte(randomString)) 58 | if err != nil { 59 | fail(err) 60 | return "", err 61 | } 62 | key = string(hasher.Sum(nil)) 63 | } else { 64 | key = randomString 65 | } 66 | 67 | // Compress the file with zlib 68 | var compressedFile bytes.Buffer 69 | writer := zlib.NewWriter(&compressedFile) 70 | 71 | // Write zlib compressed data into encryptedFile 72 | _, err := writer.Write([]byte(plainText)) 73 | if err != nil { 74 | return "", err 75 | } 76 | writer.Close() 77 | 78 | // XOR the file content with our key 79 | xorConfig := xor(key, compressedFile.String()) 80 | 81 | // Return the AES-GCM encrypted file content 82 | return encryptString(string(byteKey), xorConfig), nil 83 | } 84 | 85 | // decryptConfig is used to decrypt the scoring data file. 86 | func decryptConfig(cipherText string) (string, error) { 87 | var key string 88 | 89 | // If string is short, generate key by hashing string 90 | if len(randomString) < 64 { 91 | hasher := sha256.New() 92 | _, err := hasher.Write([]byte(randomString)) 93 | if err != nil { 94 | fail(err) 95 | return "", err 96 | } 97 | key = string(hasher.Sum(nil)) 98 | } else { 99 | key = randomString 100 | } 101 | 102 | // Decrypt with AES-GCM and the byteKey 103 | cipherText = decryptString(string(byteKey), cipherText) 104 | 105 | // Apply the XOR key to get the zlib-compressed data. 106 | cipherText = xor(key, cipherText) 107 | 108 | // Create the zlib reader 109 | reader, err := zlib.NewReader(bytes.NewReader([]byte(cipherText))) 110 | if err != nil { 111 | return "", errors.New("error creating zlib reader") 112 | } 113 | defer reader.Close() 114 | 115 | // Read into our created buffer 116 | dataBuffer := bytes.NewBuffer(nil) 117 | 118 | // HACK 119 | // 120 | // For some reason, when we use zlib in combination with AES-GCM, zlib throws 121 | // an unexpected EOF when the EOF is very expected. The hack right now is to 122 | // just ignore errors if it's an unexpected EOF. 123 | // 124 | // Likely related: https://github.com/golang/go/issues/14675 125 | _, err = io.Copy(dataBuffer, reader) 126 | if err != nil { 127 | if err.Error() == "unexpected EOF" { 128 | debug("zlib returned unexpected EOF (expected error)") 129 | err = nil 130 | } else { 131 | return "", errors.New("error decrypting or decompressing zlib data: " + err.Error()) 132 | } 133 | } 134 | 135 | // Sanity check that decryptedConfig is not empty 136 | decryptedConfig := dataBuffer.String() 137 | if decryptedConfig == "" { 138 | return "", errors.New("decrypted config is empty") 139 | } 140 | 141 | return decryptedConfig, err 142 | } 143 | 144 | // tossKey is responsible for changing up the byteKey. 145 | func tossKey() []byte { 146 | // Add your cool byte array manipulations here! 147 | return randomBytes 148 | } 149 | 150 | // obfuscateData encodes the configuration when writing to ScoringData. This 151 | // also makes exposure of sensitive memory less likely, since there is a smaller 152 | // window of opportunity for catching plaintext data. 153 | func obfuscateData(datum *string) error { 154 | var err error 155 | if *datum == "" { 156 | return nil 157 | } 158 | if *datum, err = encryptConfig(*datum); err == nil { 159 | *datum = hexEncode(xor(string(tossKey()), *datum)) 160 | } else { 161 | fail("crypto: failed to obufscate datum: " + err.Error()) 162 | return err 163 | } 164 | return nil 165 | } 166 | 167 | // deobfuscateData decodes configuration data. 168 | func deobfuscateData(datum *string) error { 169 | var err error 170 | if *datum == "" { 171 | // empty data given to deobfuscateData-- not really a concern often this 172 | // is just empty/optional struct fields 173 | return nil 174 | } 175 | *datum, err = hexDecode(*datum) 176 | if err != nil { 177 | fail("crypto: failed to deobfuscate datum hex: " + err.Error()) 178 | return err 179 | } 180 | *datum = xor(string(tossKey()), *datum) 181 | if *datum, err = decryptConfig(*datum); err != nil { 182 | fail("crypto: failed to deobfuscate datum: ", *datum, err.Error()) 183 | return err 184 | } 185 | return nil 186 | } 187 | 188 | // encryptString takes a password and a plaintext and returns an encrypted byte 189 | // sequence (as a string). It uses AES-GCM with a 12-byte IV (as is 190 | // recommended). The IV is prefixed to the string. 191 | func encryptString(password, plainText string) string { 192 | // Create a sha256sum hash of the password provided. 193 | hasher := sha256.New() 194 | _, err := hasher.Write([]byte(password)) 195 | if err != nil { 196 | fail(err) 197 | return "" 198 | } 199 | key := hasher.Sum(nil) 200 | 201 | // Pad plainText to be a 16-byte block. 202 | paddingArray := make([]byte, (aes.BlockSize - len(plainText)%aes.BlockSize)) 203 | for char := range paddingArray { 204 | paddingArray[char] = 0x20 // Padding with space character. 205 | } 206 | plainText = plainText + string(paddingArray) 207 | if len(plainText)%aes.BlockSize != 0 { 208 | fail("plainText is not a multiple of block size!") 209 | return "" 210 | } 211 | 212 | // Create cipher block with key. 213 | block, err := aes.NewCipher(key) 214 | if err != nil { 215 | fail(err) 216 | return "" 217 | } 218 | 219 | // Generate nonce. 220 | nonce := make([]byte, 12) 221 | if _, err := io.ReadFull(rand.Reader, nonce); err != nil { 222 | fail(err) 223 | return "" 224 | } 225 | 226 | // Create NewGCM cipher. 227 | aesgcm, err := cipher.NewGCM(block) 228 | if err != nil { 229 | fail(err) 230 | return "" 231 | } 232 | 233 | // Encrypt and seal plainText. 234 | ciphertext := aesgcm.Seal(nil, nonce, []byte(plainText), nil) 235 | ciphertext = []byte(fmt.Sprintf("%s%s", nonce, ciphertext)) 236 | return string(ciphertext) 237 | } 238 | 239 | // decryptString takes a password and a ciphertext and returns a decrypted 240 | // byte sequence (as a string). The function uses typical AES-GCM. 241 | func decryptString(password, ciphertext string) string { 242 | // Create a sha256sum hash of the password provided. 243 | hasher := sha256.New() 244 | if _, err := hasher.Write([]byte(password)); err != nil { 245 | fail(err) 246 | } 247 | key := hasher.Sum(nil) 248 | 249 | // Grab the IV from the first 12 bytes of the file. 250 | iv := []byte(ciphertext[:12]) 251 | ciphertext = ciphertext[12:] 252 | 253 | // Create the AES block object. 254 | block, err := aes.NewCipher(key) 255 | if err != nil { 256 | fail(err.Error()) 257 | return "" 258 | } 259 | 260 | // Create the AES-GCM cipher with the generated block. 261 | aesgcm, err := cipher.NewGCM(block) 262 | if err != nil { 263 | fail("Error creating AES cipher (please tell the developers):", err.Error()) 264 | return "" 265 | } 266 | 267 | // Decrypt (and check validity, since it's GCM) of ciphertext. 268 | plainText, err := aesgcm.Open(nil, iv, []byte(ciphertext), nil) 269 | if err != nil { 270 | fail("Error decrypting (are you using the correct aeacus/phocus? you may need to re-encrypt your config):", err.Error()) 271 | return "" 272 | } 273 | 274 | return strings.TrimSpace(string(plainText)) 275 | } 276 | -------------------------------------------------------------------------------- /crypto_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestConfigEncryption(t *testing.T) { 6 | // Testing encryptConfig 7 | plainText := "Test string." 8 | if encrypted, err := encryptConfig(plainText); err != nil { 9 | t.Errorf("encryptConfig returned an error: %s", err.Error()) 10 | } else { 11 | if decrypted, err := decryptConfig(encrypted); decrypted != plainText { 12 | t.Errorf("decryptConfig(encryptConfig('%s')) == %s, should be '%s'", plainText, decrypted, plainText) 13 | } else if err != nil { 14 | t.Errorf("decryptConfig returned an error: %s", err.Error()) 15 | } 16 | } 17 | } 18 | 19 | func TestAESEncryption(t *testing.T) { 20 | plainText := "Test string." 21 | password := "Password1!" 22 | encrypted := encryptString(password, plainText) 23 | if decrypted := decryptString(password, encrypted); decrypted != plainText { 24 | t.Errorf("decryptConfig(encryptConfig('%s')) == %s, should be '%s'", plainText, decrypted, plainText) 25 | } 26 | } 27 | 28 | func TestObfuscation(t *testing.T) { 29 | // Testing obfuscateData 30 | plainText := "I am data!" 31 | cipherText := "I am data!" 32 | if err := obfuscateData(&cipherText); err != nil { 33 | t.Errorf("obfuscateData returned an error: %s", err.Error()) 34 | } else { 35 | if err := deobfuscateData(&cipherText); cipherText != plainText { 36 | t.Errorf("deobfuscateData(obfuscateData(%s)) == %s, should be '%s'", plainText, cipherText, plainText) 37 | } else if err != nil { 38 | t.Errorf("deobufscateData returned an error: %s", err.Error()) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/checks.md: -------------------------------------------------------------------------------- 1 | # aeacus 2 | 3 | ## Checks 4 | 5 | This is a list of vulnerability checks that can be used in the configuration for `aeacus`. The notes on this page 6 | contain a lot of important information, please be sure to read them. 7 | 8 | > **Note**: Each of the commands here can check for the opposite by appending 'Not' to the check type. For 9 | > example, `PathExistsNot` to pass if a file does not exist. 10 | 11 | > **Note**: If a check has negative points assigned to it, it automatically becomes a penalty. 12 | 13 | > **Note**: Each of these check types can be used for `Pass`, `PassOverride` or `Fail` conditions, and there can be 14 | > multiple conditions per check. See [configuration](config.md) for more details. 15 | 16 | > **Note**: Regex is officially supported for `CommandContainsRegex`, `DirContainsRegex`, and `FileContainsRegex`. Read 17 | > more about regex [here](regex.md). 18 | 19 | **CommandContains**: pass if command output contains string. If executing the command fails (the check returns an 20 | error), check never passes. Use of this check is discouraged. 21 | 22 | ``` 23 | type = 'CommandContains' 24 | cmd = 'ufw status' 25 | value = 'Status: active' 26 | ``` 27 | 28 | > **Note**: `Command*` checks are prone to interception, modification, and tomfoolery. Your scoring configuration will 29 | > be much more robust if you rely on checks using native mechanisms rather than shell commands (for 30 | > example, `PathExists` 31 | > instead of ls). 32 | 33 | > **Note**: If any check returns an error (e.g., something that it was not expecting), it will _never_ pass, even if 34 | > it's a `Not` condition. This varies by check, but for example, if you try to check the content of a file that doesn't 35 | > exist, it will return an error and not succeed-- even if you were doing `FileContainsNot`. 36 | 37 | **CommandOutput**: pass if command output matches string exactly. If it returns an error, check never passes. Use of 38 | this check is discouraged. 39 | 40 | ``` 41 | type = 'CommandOutput' 42 | cmd = '(Get-NetFirewallProfile -Name Domain).Enabled' 43 | value = 'True' 44 | ``` 45 | 46 | **DirContains**: pass if directory contains a string value 47 | 48 | ``` 49 | type = 'DirContains' 50 | path = '/etc/sudoers.d/' 51 | value = 'NOPASSWD' 52 | ``` 53 | 54 | > `DirContains` is recursive! This means it checks every folder and subfolder. It currently is capped at 10,000 files, 55 | > so you should begin your search at the deepest folder possible. 56 | 57 | 58 | > **Note**: You don't have to escape any characters because we're using single quotes, which are literal strings in 59 | > TOML. If you need use single quotes, use a TOML multi-line string 60 | > literal `''' like this! that's neat! C:\path\here '''`), or just normal quotes (but you'll have to escape characters 61 | > with those). 62 | 63 | **FileContains**: pass if file contains a value 64 | 65 | > **Note**: `FileContains` will never pass if file does not exist! Add an additional PassOverride check for 66 | > PathExistsNot, if you want to score that a file does not contain a line, OR it doesn't exist. 67 | 68 | ``` 69 | type = 'FileContains' 70 | path = 'C:\Users\coolUser\Desktop\Forensic Question 1.txt' 71 | value = 'ANSWER:\s*Cool[a-zA-Z]+VariedAnswer' 72 | ``` 73 | 74 | **FileEquals**: pass if file equals sha256 hash 75 | 76 | ``` 77 | type = 'FileEquals' 78 | path = '/etc/sysctl.conf' 79 | value = 'e61ff3fb83b51fe9f2cd03cc0408afa15d4e8e69b8488b4ed1ecb854ae25da9b' 80 | ``` 81 | 82 | **FileOwner**: pass if specified user owns a given file 83 | 84 | ``` 85 | type = 'FileOwner' 86 | path = 'C:\test.txt' 87 | name = 'BUILTIN\Administrators' 88 | ``` 89 | 90 | ``` 91 | type = 'FileOwner' 92 | path = '/etc/passwd' 93 | name = 'root' 94 | ``` 95 | 96 | > Get owner of the file in both Windows and Linux. You can see the owner of a file on Windows using 97 | > PowerShell: `(Get-Acl [FILENAME]).Owner`. For Linux, use `ls -la FILENAME`. 98 | 99 | 100 | **FirewallUp**: pass if firewall is active 101 | 102 | ``` 103 | type = 'FirewallUp' 104 | ``` 105 | 106 | > **Note**: On Linux, `ufw` (checks `/etc/ufw/ufw.conf`) and `firewalld` are supported. If the `ufw` config does not 107 | > exist, the engine checks if `firewalld` is running. On Window, this passes if all three Windows Firewall profiles are 108 | > active. 109 | 110 | **PasswordChanged**: pass if user password has changed 111 | 112 | For Linux, check if user's password hash is not next to their username in `/etc/shadow`. If you don't use the whole 113 | hash, make sure you start it from the beginning (typically `$X$...` where X is a number). 114 | 115 | ``` 116 | type = 'PasswordChanged' 117 | user = 'bob' 118 | value = '$6$BgBsRlajjwVOoQCY$rw5WBSha4nkpynzfCzc3yYkV1OyDhr.ELoJOPpidwZoygUzRFBFSrtE3fyP0ITubCwN9Bb9DUqVV3mzTHL8sw/' 119 | ``` 120 | 121 | > This check will never pass if the user does not exist, so don't use this with users that should be removed. 122 | 123 | For Windows, check if password was changed after the specified date: 124 | 125 | ``` 126 | type = 'PasswordChanged' 127 | user = 'username' 128 | after = 'Monday, January 02, 2006 3:04:05 PM' 129 | ``` 130 | 131 | > You should take the value from `(Get-LocalUser ).PasswordLastSet` and use it as `after`. This check will 132 | > never pass if the user does not exist, so don't use this with users that should be removed. 133 | 134 | **PathExists**: pass if specified path exists. This works for both files AND folders (directories). 135 | 136 | ``` 137 | type = 'PathExists' 138 | path = '/var/www/backup.zip' 139 | ``` 140 | 141 | ``` 142 | type = 'PathExists' 143 | path = 'C:\importantfolder\' 144 | ``` 145 | 146 | **PermissionIs**: pass if specified user has specified permission on a given file 147 | 148 | For Linux, use the standard octal `rwx` format (`ls -la yourfile` will show them). Use question marks to omit bits you 149 | don't care about. 150 | 151 | ``` 152 | type = 'PermissionIs' 153 | path = '/etc/shadow 154 | value = 'rw-rw----' 155 | ``` 156 | 157 | For example, this one checks that /bin/bash is not SUID and world writable at the same time: 158 | 159 | ``` 160 | type = 'PermissionIsNot' 161 | path = '/bin/bash' 162 | value = '???s????w?' 163 | ``` 164 | 165 | So if `/bin/bash` is no longer world writable OR no longer SUID, the check will pass. If you want to ensure both 166 | attributes are removed, you should use two conditions in the same check (`pass` for writeable bit, in addition to `pass` 167 | for SUID bit). 168 | 169 | For Windows, get a users permission of the file using `(Get-Acl [FILENAME]).Access`. 170 | 171 | ``` 172 | type = 'PermissionIs' 173 | path = 'C:\test.txt' 174 | name = 'BUILTIN\Administrators' 175 | value = 'FullControl' 176 | ``` 177 | 178 | > **Note**: Use absolute paths when possible (rather than relative) for more reliable scoring. 179 | 180 | **ProgramInstalled**: pass if program is installed. On Linux, will use `dpkg` (or `rpm` for RHEL-based systems), and on 181 | Windows, checks if any installed programs contain your program string. 182 | 183 | ``` 184 | type = 'ProgramInstalled' 185 | name = 'Mozilla Firefox 75 (x64 en-US)' 186 | ``` 187 | 188 | **ProgramVersion**: pass if a program meets the version requirements 189 | 190 | For Linux, get version from `dpkg -s programnamehere`. 191 | 192 | ``` 193 | type = 'ProgramVersion' 194 | name = 'Firefox' 195 | value = '88.0.1+build1-0ubuntu0.20.04.2' 196 | ``` 197 | 198 | > Only works for `dpkg` based distributions (such as Debian and Ubuntu). 199 | 200 | For Windows, get versions from `.\aeacus.exe info programs`. 201 | 202 | ``` 203 | # Checks version on first matching substring. E.g., for program name 'Ace', 204 | # it may match on 'Ace Of Spades' rather than 'Ace Ventura'. Make your program 205 | # name as detailed as possible. 206 | type = 'ProgramVersion' 207 | name = 'Firefox' 208 | value = '95.0.1' 209 | ``` 210 | 211 | > **Note**: We recommend you use the `Not` version of this check to score a program's version being different from its 212 | > version at the beginning of the image. You can't guarantee that the latest version of the program you're scoring will 213 | > be 214 | > the same once your round is released, and it's unlikely that a competitor will intentionally downgrade a package. 215 | 216 | > For packages, Linux uses `dpkg`, Windows uses the Windows API 217 | 218 | **ServiceUp**: pass if service is running 219 | 220 | For Linux, use the `systemd` service name. 221 | 222 | ``` 223 | type = 'ServiceUp' 224 | name = 'sshd' 225 | ``` 226 | 227 | For Windows: check the service 'Properties' to find the real service name 228 | 229 | ``` 230 | type = 'ServiceUp' 231 | name = 'tapisrv' # this is telephony 232 | 233 | ``` 234 | 235 | > For services, Linux uses `systemctl`, Windows uses `Get-Service`. If you are using a different init system on Linux, 236 | > you can use a `Command` check. 237 | 238 | **UserExists**: pass if user exists on system 239 | 240 | ``` 241 | type = 'UserExists' 242 | user = 'ballen' 243 | ``` 244 | 245 | **UserInGroup**: pass if specified user is in specified group 246 | 247 | ``` 248 | type = 'UserInGroupNot' 249 | user = 'HackerUser' 250 | group = 'Administrators' 251 | ``` 252 | 253 | > Linux reads `/etc/group` and Windows uses the Windows API. 254 | 255 |
256 | 257 | ### Linux-Specific Checks 258 | 259 | **AutoCheckUpdatesEnabled**: pass if the system is configured to automatically check for updates (supports `apt` 260 | and `dnf-automatic`) 261 | 262 | ``` 263 | type = 'AutoCheckUpdatesEnabled' 264 | ``` 265 | 266 | **Command**: pass if command succeeds (command is executed, and has a return code of zero). Use of this check is 267 | discouraged. This check will NOT return an error if the command is not found 268 | 269 | ``` 270 | type = 'Command' 271 | cmd = 'cat coolfile.txt' 272 | ``` 273 | 274 | **GuestDisabledLDM**: pass if guest is disabled (for LightDM) 275 | 276 | ``` 277 | type = 'GuestDisabledLDM' 278 | ``` 279 | 280 | **KernelVersion**: pass if kernel version is equal to specified 281 | 282 | ``` 283 | type = 'KernelVersion' 284 | value = '5.4.0-42-generic' 285 | ``` 286 | 287 | > Tip: Check your `KernelVersion` with `uname -r`. This check performs the `uname` syscall. 288 | 289 | 290 |
291 | 292 | ### Windows-Specific Checks 293 | 294 | **BitlockerEnabled**: pass if a drive has been fully encrypted with bitlocker drive encription or is in the process of 295 | being encrypted 296 | 297 | ``` 298 | type = "BitlockerEnabled" 299 | ``` 300 | 301 | > This check will succeed if the drive is either encrypted or encryption is in progress. 302 | 303 | **FirewallDefaultBehavior**: pass if the firewall profile's default behavior is set to the specified value 304 | 305 | ``` 306 | type = 'FirewallDefaultBehavior' 307 | name = 'Domain' 308 | value = 'Allow' 309 | key = 'Inbound' 310 | ``` 311 | 312 | > Valid "name" (profile) values are: Domain, Public, Private 313 | > 314 | > Valid "value" (behavior) values are: Allow, Block 315 | > 316 | > Valid "key" (direction) values are: Inbound, Outbound 317 | 318 | 319 | **RegistryKey**: pass if key is equal to value 320 | 321 | ``` 322 | type = 'RegistryKey' 323 | key = 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\DisableCAD' 324 | value = '0' 325 | ``` 326 | 327 | > **Note**: This check will never pass if retrieving the key fails (wrong hive, key doesn't exist, etc). If you want to 328 | > check that a key was deleted, use `RegistryKeyExists`. 329 | 330 | > **Administrative Templates**: There are 4000+ admin template fields. 331 | > 332 | See [this list of registry keys and descriptions](https://docs.google.com/spreadsheets/d/1N7uuke4Jg1R9FBhj8o5dxJQtEntQlea0McYz5upaiTk/edit?usp=sharing), 333 | > then use the `RegistryKey` or `RegistryKeyExists` check. 334 | 335 | **RegistryKeyExists**: pass if key exists 336 | 337 | ``` 338 | type = 'RegistryKeyExists' 339 | key = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\DisableCAD' 340 | ``` 341 | 342 | > **Note**: Notice the single quotes `'` on the above argument! This means it's a _string literal_ in TOML. If you don't 343 | > do this, you have to make sure to escape your slashes (`\` --> `\\`) 344 | 345 | > **Note**: You can use `SOFTWARE` as a shortcut for `HKEY_LOCAL_MACHINE\SOFTWARE`. 346 | 347 | **ScheduledTaskExists**: pass if scheduled task exists 348 | 349 | ``` 350 | type = 'ScheduledTaskExists' 351 | name = 'Disk Cleanup' 352 | ``` 353 | 354 | **SecurityPolicy**: pass if key is within the bounds for value 355 | 356 | ``` 357 | type = 'SecurityPolicy' 358 | key = 'DisableCAD' 359 | value = '0' 360 | ``` 361 | 362 | > Values are checking Registry Keys and `secedit.exe` behind the scenes. This means `0` is `Disabled` and `1` 363 | > is `Enabled`. [See here for reference](securitypolicy.md). 364 | 365 | > **Note**: For all integer-based values (such as `MinimumPasswordAge`), you can provide a range of values, as seen 366 | > below. The lower value must be specified first. 367 | 368 | ``` 369 | type = 'SecurityPolicy' 370 | key = 'MaximumPasswordAge' 371 | value = '80-100' 372 | ``` 373 | 374 | **ServiceStartup**: pass if service is set to a given startup type (manual, automatic, or disabled) 375 | 376 | ``` 377 | type = "ServiceStartup" 378 | name = "TermService" 379 | value = "manual" 380 | ``` 381 | 382 | > This check is a wrapper around RegistryKey to fetch the proper key for you. Also, Automatic (Delayed) and Automatic 383 | > are the same value for the key we're checking. 384 | 385 | **ShareExists**: pass if SMB share exists 386 | 387 | ``` 388 | type = 'ShareExists' 389 | name = 'ADMIN$' 390 | ``` 391 | 392 | > **Note**: Don't use any single quotes (`'`) in your parameters for Windows options like this. If you need to, use a 393 | > double-quoted string instead (ex. `"Admin's files"`) 394 | 395 | 396 | **UserDetail**: pass if user detail key is equal to value 397 | 398 | > **Note**: The valid boolean values for this command (when the field is only True or False) are 'yes', if you want the 399 | > value to be true, or literally anything else for false (like 'no'). 400 | 401 | ``` 402 | type = 'UserDetailNot' 403 | user = 'Administrator' 404 | key = 'PasswordNeverExpires' 405 | value = 'No' 406 | ``` 407 | 408 | > See [here](userproperties.md) for all `UserDetail` properties. 409 | 410 | > **Note**: For non-boolean details, you can use modifiers in the value field to specify the comparison. 411 | > This is specified in the above property document. 412 | 413 | ``` 414 | type = 'UserDetail' 415 | user = 'Administrator' 416 | key = 'PasswordAge' 417 | value = '>90' 418 | ``` 419 | 420 | **UserRights**: pass if specified user or group has specified privilege 421 | 422 | ``` 423 | type = 'UserRights' 424 | name = 'Administrators' 425 | value = 'SeTimeZonePrivilege' 426 | ``` 427 | 428 | > A list of URA and Constant Names (which are used in the 429 | > 430 | config) [can be found here](https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/user-rights-assignment). 431 | > On your local machine, check Local Security Policy > User Rights Assignments to see the current assignments. 432 | 433 | 434 | **WindowsFeature**: pass if Windows Feature is enabled 435 | 436 | ``` 437 | type = 'WindowsFeature' 438 | name = 'SMB1Protocol' 439 | ``` 440 | 441 | > **Note**: Use the PowerShell tool `Get-WindowsOptionalFeature -Online` to find the feature you want! 442 | -------------------------------------------------------------------------------- /docs/conditions.md: -------------------------------------------------------------------------------- 1 | # Check conditions and precedence 2 | 3 | Using multiple conditions for a check can be confusing at first, but can greatly improve the quality of your images by accounting for edge cases and abuse. 4 | 5 | Given no conditions, a check does not pass. 6 | 7 | If any **Fail** conditions succeed, the check does not pass. 8 | 9 | **PassOverride** conditions act as a logical OR. This means that any can succeed for the check to pass. 10 | 11 | **Pass** conditions act as a logical AND with other pass conditions. This means they must ALL be true for a check to pass. 12 | 13 | If the outcome of a check is decided, aeacus will NOT execute the remaining conditions (it will "short circuit"). For example, if a PassOverride succeeds, any Pass conditions are NOT executed. 14 | 15 | So, it's like this: `check_passes = (NOT fails) AND (passoverride OR (AND of all pass checks))`. 16 | 17 | For example: 18 | 19 | ``` 20 | [[check]] 21 | 22 | # Ensure the scheduled task service is running AND 23 | [[check.fail]] 24 | type = 'ServiceUpNot' 25 | name = 'Schedule' 26 | 27 | # Pass if the user runnning those tasks is deleted 28 | [[check.passoverride]] 29 | type = 'UserExistsNot' 30 | name = 'CleanupBot' 31 | 32 | # OR pass if both scheduled tasks are deleted 33 | [[check.pass]] 34 | type = 'ScheduledTaskExistsNot' 35 | name = 'Disk Cleanup' 36 | [[check.pass]] 37 | type = 'ScheduledTaskExistsNot' 38 | name = 'Disk Cleanup Backup' 39 | 40 | ``` 41 | 42 | The evaluation of checks goes like this: 43 | 1. Check if any Fail are true. If any Fail checks succeed, then we're done, the check doesn't pass. 44 | 2. Check if any PassOverride conditions pass. If they do, we're done, the check passes. 45 | 3. Check status of all Pass conditions. If they all succeed, the check passes, otherwise it fails. 46 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # aeacus 2 | 3 | ## Fields 4 | 5 | This is a list of (non-check) image configuration fields for `aeacus`. For details on check configurations (ex., ensure this file has this content), see the [checks configuration](./checks.md). 6 | 7 | **name**: Image name, primarily used to organize remote scoring. 8 | 9 | > **Note!** This field is not mandatory if you use local scoring. 10 | 11 | ``` 12 | name = "ubuntu-18-dabbingdabbers" 13 | ``` 14 | 15 | **title**: Round title, shown in the image's scoring report and README. 16 | 17 | ``` 18 | title = "CyberPatio Practice Round 1337" 19 | ``` 20 | 21 | **os**: Name of the operating system, shown in the image's README. 22 | 23 | ``` 24 | os = "TempleOS 5.03" 25 | ``` 26 | 27 | **user**: Main user of the image. This is used when sending notifications. 28 | 29 | > **Note!** No other user accounts will get notifications except for this user. 30 | 31 | ``` 32 | user = "sysadmin" 33 | ``` 34 | 35 | **remote**: Address of remote server for scoring. If remote scoring is enabled, and `local` is not enabled, `aeacus` will refuse to score the image unless a connection to the server can be established. 36 | 37 | ``` 38 | remote = "http://scoring.example.com" 39 | ``` 40 | 41 | **password**: Password used for encrypting remote reporting traffic. The same password must be set on the remote side. 42 | 43 | ``` 44 | password = "H4!b5at+kWls-8yh4Guq" 45 | ``` 46 | 47 | **DisableRemoteEncryption**: Disables encryption of remote reporting traffic. This is not recommended, but can be useful for debugging or if you are using a custom remote endpoint that does not support encryption. 48 | 49 | ``` 50 | DisableRemoteEncryption = true 51 | ``` 52 | 53 | **local**: Enables local scoring. If no remote address is specified, this will automatically be set to true. 54 | 55 | ``` 56 | local = true 57 | ``` 58 | 59 | **enddate**: Defines competition end date. If the engine is run after this date, it will not score the image. 60 | 61 | ``` 62 | enddate = "2004/06/05 13:09:00 PDT" 63 | ``` 64 | 65 | **shell**: (Warning: the canonical remote endpoint (sarpedon) does not support this feature). Determines if remote shell functionality is enabled. This is disabled by default. If enabled, competition organizers can interact with images from the scoring endpoint 66 | 67 | ``` 68 | shell = false 69 | ``` 70 | 71 | **version**: Version of aeacus that the configuration was made for. Used for compatibility checks, the engine will throw a warning if the binary version does not match the version specified in this field. You should set this to the version of aeacus you are using. 72 | 73 | ``` 74 | version = "X.X.X" 75 | ``` 76 | 77 | ## Penalties 78 | 79 | Assign a check a negative point value, and it will become a penalty. Example: 80 | 81 | ``` 82 | [[check]] 83 | message = "Critical service OpenSSH stopped or removed" 84 | points = "-5" 85 | 86 | [[check.passoverride]] 87 | type = 'ServiceUpNot' 88 | name = 'sshd' 89 | 90 | [[check.passoverride]] 91 | type = 'PathExistsNot' 92 | name = '/lib/systemd/system/sshd.service' 93 | ``` 94 | 95 | 96 | -------------------------------------------------------------------------------- /docs/crypto.md: -------------------------------------------------------------------------------- 1 | # Adding Crypto 2 | 3 | The public releases of `aeacus` ship with weak crypto (cryptographic security), which means that the encryption and/or encoding of scoring data files is not very "secure". 4 | 5 | While it is non-trivial, extracting the keys from a GitHub release of aeacus is very possible, and not too difficult. If someone does that, and you use that release of the engine, your configs will be instantly compromised. 6 | 7 | You should compile it yourself to generate random keys (`make release`). This means you will be using different keys than the public release, which is good enough for most situations. 8 | 9 | If security of the configuration is very important to you, or you feel the competition integrity is at risk, (e.g., you're running a competition with prizes, or running a practice session for beginner reverse engineers), you should compile the binary for yourself after adding stronger crypto operations. 10 | 11 | Anything you want to add is good! More XOR, AES (be careful in implementing this one), mixing bytes up... As long as the encrypt and decrypt functions work, nothing should break. The functions you would want to change are all in `crypto.go`. 12 | 13 | If adding crypto is intimidating, remember that the public releases are good for many situations, and compiling it for yourself (with `make release`) is good enough for most. It's also risk-free to try changing things up-- you can always revert to the default crypto. 14 | 15 | Once you implement your functions, ensure they work. You can run built-in tests with: 16 | 17 | ```bash 18 | CGO_ENABLED=0 go test -v 19 | ``` 20 | 21 | This model of engine can never be 100% secure (see [security](security.md)), but you can get ok security. 22 | -------------------------------------------------------------------------------- /docs/examples/linux-remote.conf: -------------------------------------------------------------------------------- 1 | name = "linux-remote" # Name of image 2 | title = "A Practice Round" # Title of Round 3 | user = "sha" # Main user, used for sending notifications 4 | os = "Ubuntu 20.04" # Operating system, used for README 5 | version = "2.1.1" # The version of aeacus you're using 6 | 7 | # If remote is specified, aeacus will report its score and refuse to score if 8 | # the remote server does not accept its messages and Team ID (unless "local" is 9 | # set to "yes") Make sure to include the scheme (http, https...) 10 | # NOTE: Don't include a slash after the url. 11 | remote = "https://scoring.example.org" 12 | 13 | # If password is specified, it will be used to encrypt remote reporting traffic 14 | # NOTE: Server must have the same password set. 15 | password = "HackersArentReal" 16 | 17 | # If local is set to true, then the image will give feedback and score 18 | # regardless of whether or not remote scoring is working 19 | local = true 20 | 21 | [[check]] 22 | [[check.pass]] 23 | type = "FirewallUp" 24 | 25 | [[check]] 26 | message = "Super cool hacking tool removed" 27 | points = 5 28 | [[check.pass]] 29 | type = "ProgramInstalledNot" 30 | name = "nmap" 31 | 32 | [[check]] 33 | [[check.pass]] 34 | type = "UserInGroup" 35 | user = "sha" 36 | group = "sudo" 37 | 38 | [[check]] 39 | [[check.pass]] 40 | type = "UserInGroupNot" 41 | user = "sha" 42 | group = "nopasswdlogin" 43 | 44 | [[check]] 45 | points = 3 46 | [[check.pass]] 47 | type = "GuestDisabledLDM" 48 | 49 | [[check]] 50 | message = 'Kernel is patched against CVE-XXXX-XXXX' 51 | [[check.pass]] 52 | type = "KernelVersionNot" 53 | value = "5.4.0-42-generic" 54 | 55 | [[check]] 56 | [[check.pass]] 57 | type = "AutoCheckUpdatesEnabled" 58 | 59 | [[check]] 60 | message = "/etc/shadow is not world readable" 61 | [[check.pass]] 62 | type = 'PathExists' 63 | path = '/etc/shadow' 64 | [[check.pass]] 65 | type = 'PermissionIs' 66 | path = '/etc/shadow' 67 | value = 'rw-r-----' 68 | -------------------------------------------------------------------------------- /docs/examples/windows.conf: -------------------------------------------------------------------------------- 1 | name = 'windows-example' 2 | title = 'Super Cool Practice Round' 3 | user = 'wandow' 4 | os = 'Windows Server 2016' 5 | 6 | # This image scores remotely, but enables local, so that competitors can still 7 | # see their scoring report if the remote scoreboard rejects their ID, shuts 8 | # down, or is otherwise unavailable. 9 | remote = 'https://scoring.example.org' 10 | password = 'HackMePl0x' 11 | local = true 12 | 13 | [[check]] 14 | [[check.pass]] 15 | type='SecurityPolicy' 16 | key = 'DisableCAD' 17 | value = '0' 18 | 19 | [[check]] 20 | [[check.pass]] 21 | type = 'SecurityPolicy' 22 | key = 'MaximumPasswordAge' 23 | value = '40-100' 24 | 25 | [[check]] 26 | [[check.pass]] 27 | type = 'ScheduledTaskExistsNot' 28 | name = 'Disk Cleanup' 29 | [[check.fail]] 30 | type = 'ServiceUpNot' 31 | name = 'Schedule' 32 | 33 | [[check]] 34 | [[check.pass]] 35 | type = "ServiceStartup" 36 | name = "tapisrv" 37 | value = "disabled" 38 | [[check.pass]] 39 | type = "ServiceUpNot" 40 | name = "tapisrv" 41 | 42 | [[check]] 43 | [[check.pass]] 44 | type = 'ShareExistsNot' 45 | name = 'MaliciousShare' 46 | [[check.passoverride]] 47 | type = 'ShareExistsNot' 48 | name = 'MaliciousShare' 49 | 50 | [[check]] 51 | [[check.pass]] 52 | type = 'UserDetail' 53 | user = 'Administrator' 54 | key = 'PasswordNeverExpires' 55 | value = 'No' 56 | -------------------------------------------------------------------------------- /docs/hints.md: -------------------------------------------------------------------------------- 1 | # Hints 2 | 3 | Hints let you provide information on failing checks. 4 | 5 | ![Hint Example](../misc/gh/hint.png) 6 | 7 | Hints are a way to help make images more approachable. 8 | 9 | You can add a conditional hint or a check-wide hint. A conditional hint is printed when that specific condition is executed and fails. Make sure you understand the check precedence; this can be tricky, as sometimes your check is NOT executed ([read about conditions](conditions.md)). 10 | 11 | Example conditional hint: 12 | ``` 13 | [[check]] 14 | points = 5 15 | 16 | [[check.pass]] 17 | type = "ProgramInstalledNot" 18 | name = "john" 19 | 20 | [[check.pass]] 21 | # This hint will NOT print unless the condition above succeeds. 22 | # Pass conditions are logically AND-- they all need to succeed. 23 | # If one fails, there's no reason to execute the other ones. 24 | hint = "Ensure you're using a package manager to remove all of a tool's files, as well as the main program." 25 | type = "PathExistsNot" 26 | path = "/usr/share/john" 27 | ``` 28 | 29 | Check-wide hints are at the top level and always displayed if a check fails. Example check-wide hint: 30 | 31 | ``` 32 | [[check]] 33 | hint = "Are there any 'hacking' tools installed?" 34 | points = 5 35 | 36 | [[check.pass]] 37 | type = "ProgramInstalledNot" 38 | name = "john" 39 | 40 | [[check.pass]] 41 | type = "PathExistsNot" 42 | path = "/usr/share/john" 43 | ``` 44 | 45 | You can combine check-wide and conditional hints. If the check fails, the check-wide hint is ALWAYS displayed, in addition to any conditional hints triggered. 46 | -------------------------------------------------------------------------------- /docs/regex.md: -------------------------------------------------------------------------------- 1 | # Regular Expressions 2 | 3 | Many of the checks in `aeacus` use regular expression (regex) strings as input. This may seem inconvenient if you want 4 | to score something simple, but we think it significantly increases the overall quality of checks. Each regex is applied 5 | to each line of the input file, so currently, no multi-line regexes are currently possible. 6 | 7 | The checks that are specifically supported are `CommandContainsRegex`, `DirContainsRegex`, and `FileContainsRegex`. 8 | Please note that you **must** add `Regex` for the check to use regular expressions. You can also still append `Not` to 9 | the end to invert the condition, such as `CommandContainsRegexNot` 10 | 11 | > We're using the Golang Regular Expression package ([documentation here](https://godocs.io/regexp)). It uses RE2 12 | > syntax, which is also generally the same as Perl, Python, and other languages. 13 | 14 | If you're unfamiliar, a 'regex' is just a way of describing a pattern of text. Let's say I was trying to score this 15 | in `/etc/apt/apt.conf.d/*`: 16 | 17 | ``` 18 | APT::Periodic::Update-Package-Lists "1"; 19 | ``` 20 | 21 | The most simple regexes work the same way that normal CTRL-f searches work. It just matches what it is. A valid regex to 22 | score this would be `APT::Periodic::Update-Package-Lists "1";`, since none of those characters mean anything special to 23 | regex. With normal substring searching (like CTRL-f), that's the most specific we would be able to be. 24 | 25 | But, what about this? 26 | 27 | ``` 28 | APT::Periodic::Update-Package-Lists "1"; 29 | ``` 30 | 31 | Notice that there are now two spaces before the `"1"`. That's a bummer, because our config is still valid to Ubuntu's 32 | software updater, but it's not scored as correct. We need at least one space between those two, so we'll 33 | do `APT::Periodic::Update-Package-Lists\s+"1";`, where `\s` means any whitespace, and `+` means 'at least one.' 34 | 35 | Similarly, what about all of these? 36 | 37 | ``` 38 | APT::Periodic::Update-Package-Lists "1"; 39 | APT::Periodic::Update-Package-Lists "1" ; 40 | APT::Periodic::Update-Package-Lists "1"; 41 | ``` 42 | 43 | Our new regex, to match all of those, would be `\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*`. `*` means 'any 44 | amount of the preceding token, including none.' So this will match any amount of whitespace (including no whitespace). 45 | Which would work much better. 46 | 47 | But, what about this? 48 | 49 | ``` 50 | # APT::Periodic::Update-Package-Lists "1"; 51 | ``` 52 | 53 | That ruins everything. It would score as correct even though it's commented out. But, it's an easy fix. With the 54 | regex `^\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*$`, where we added `^` for 'start of the line' and `$` for ' 55 | end of the line', it will only match if there's nothing except whitespace before and after the directive. 56 | 57 | But, what about this? 58 | 59 | ``` 60 | APT::PERIODIC::UPDATE-PACKAGE-LISTS "1"; 61 | ``` 62 | 63 | Believe it or not, the apt configs appear to be case-insensitive. So we modify the expression to be case insensitive 64 | with `(?i)`: `(?i)^\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*$`. 65 | 66 | As far as I know, this is as correct as we can get it. 67 | 68 | Thinking about the edge cases and correct grammar for scoring these directives is very important and makes a big 69 | difference in scoring robustness, which is why we use regexes for many checks. It can take a lot of practice to get a 70 | working expression, and making mistakes is very common. If you want to test your expression interactively, you can use 71 | something like [debuggex](https://debuggex.com) or [regex101](https://regex101.com). 72 | -------------------------------------------------------------------------------- /docs/remote.md: -------------------------------------------------------------------------------- 1 | # Remote score reporting 2 | 3 | `aeacus` can report scores to a remote endpoint if the `remote` field is set in the configuration. 4 | 5 | After every scoring cycle, `aeacus` sends an update with the following structure to the `/update` endpoint on the remote URL: 6 | 7 | ``` 8 | IMAGE_POINTS 9 | TOTAL_VULNS 10 | Penalty message - 10 pts 11 | Vuln message - 5 pts 12 | Another vuln message - 3 pts 13 | ``` 14 | 15 | So a real report may look like: 16 | 17 | ``` 18 | 10 19 | 22 20 | Firewall has been activated - 5 pts 21 | ``` 22 | 23 | Each newline in the text above actually represents a delimiter, which is a sequence of two bytes (0xff followed by 0xde). This was randomly chosen and serves to separate information from one another in the config update more reliably than a newline. 24 | 25 | The report is encrypted with the configuration password and hex encoded (unless `DisableRemoteEncryption` is set to `true`), before being sent to the remote. 26 | 27 | The function `genUpdate()` creates the report while `reportScore()` sends it. 28 | -------------------------------------------------------------------------------- /docs/security.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | Engines that work like `aeacus` can never be "secure." We can only make it more difficult to crack. 4 | 5 | As long as the configuration is being loaded onto a virtual machine that is controlled by a competitor, there is no way to make it impossible to reveal what resources are being collected (files, command output, registry keys...). This is because they have control of the disk and CPU. 6 | 7 | Due to this fact, we focus on obfuscating and encrypting the configuration such that it would take at least a fair bit of reversing expertise and time in order to crack. The primary target audience for this style of competition will likely not be able or willing to do that. 8 | 9 | If you need a perfectly secure engine, there is no such thing. It is possible to do slightly better than our current architecture by either: 10 | 11 | 1. Only allowing remote scoring, and performing all checks on the remote host. (e.g., send the entire /etc/ssh/sshd_config file to the scoring server, who then checks it for a desired string.) That way, the competitor would know what resources are being collected, but not what the checks are looking for. However, this costs a lot more bandwidth and server CPU time. 12 | 2. Use "zero-knowledge" crypto algorithms. A simple version of this might be checking each substring of a file for a hash. Downsides to this approach include a much larger demand on client CPUs, and it's not perfect, since competitors can attempt to crack that hash without the scoring server in the loop. 13 | 14 | If you need an almost perfectly secure engine, you will need to write your own, and it will need to have a couple things: 15 | 16 | - Cloud-based VMs on controlled and monitored in-person thin-client kiosks with no external internet access. 17 | - Scoring engine that works at the VM infrastructure level, and reads in competitor VM disk files to score images. 18 | 19 | And even then, it's definitely breakable given some time. 20 | 21 | In any case, `aeacus` crypto that you compile yourself, or with a few tweaks, is probably suitable for your use case. 22 | -------------------------------------------------------------------------------- /docs/userproperties.md: -------------------------------------------------------------------------------- 1 | # aeacus 2 | 3 | ## Windows User Properties 4 | 5 | These are the user properties that you would normally find the user edit dialog. (Plus some extra goodies!) 6 | These properties are used with the check `UserDetail`. For more information on how this check works, please visit our [check documentation](checks.md)! 7 | 8 | **Usernames are case-sensitive.** `Yes`/`No` are not case-sensitive. If you input something other than "yes" or "no" for a boolean check, it defaults to "no". 9 | 10 | - `FullName`: Full name of user (case-sensitive) 11 | - `IsEnabled`: Yes or No 12 | - This returns for whether the account is **disabled**. 13 | - `IsLocked`: Yes or No 14 | - This returns for whether the account is **locked out**. 15 | - (incorrect password attempts, temporary) 16 | - `IsAdmin`: Yes or No 17 | - Does the account have admin access? 18 | - `PasswordNeverExpires`: Yes or No 19 | - Password does not expire. 20 | - `NoPasswordChange`: Yes or No 21 | - Password cannot be changed. 22 | 23 | For these checks, you can specify comparison through less, greater, and equal to through adding characters to the beginning of the value field. 24 | This character **must** be added as the first character of the value field, as shown below. 25 | >`<[value]`: The property is less than the `value` field. 26 | > 27 | >`>[value]`: The property is greater than the `value` field. 28 | > 29 | >`[value]`: The property is equal to the `value` field. This is the default. 30 | - `PasswordAge`: Number of Days (e.g. "7") 31 | - How old is the password? 32 | - `BadPasswordCount`: Number of incorrect passwords (e.g. "3") 33 | - How many incorrect passwords have been entered? 34 | - `NumberOfLogons`: Number of total logons (e.g. "4") 35 | - How many times has the user logged on? 36 | 37 | For the `LastLogonTime` property: 38 | > `<[value]`: The property is before the `value` field's date. 39 | > 40 | > `>[value]`: The property is after the `value` field's date. 41 | > 42 | > `[value]`: The property is equal to the `value` field's date. This is the default. 43 | - `LastLogonTime`: Date in format: Monday, January 02, 2006 3:04:05 PM 44 | - If the date is not in that exact format, the check will fail. 45 | - Time **must** be in UTC to pass. 46 | -------------------------------------------------------------------------------- /docs/v2.md: -------------------------------------------------------------------------------- 1 | # Breaking Changes 2 | 3 | - Checks now use semantic field names in `scoring.conf`. For example, the following `FileContains` check: 4 | 5 | ``` 6 | [[check]] 7 | message = "Removed insecure sudoers rule" 8 | points = 10 9 | [[check.pass]] 10 | type="FileContainsNot" 11 | arg1="/etc/sudoers" 12 | arg2="NOPASSWD" 13 | ``` 14 | 15 | Can now be written as: 16 | 17 | ``` 18 | [[check]] 19 | message = "Removed insecure sudoers rule" 20 | points = 10 21 | [[check.pass]] 22 | type = "FileContainsNot" 23 | path = "/etc/sudoers" 24 | value = "NOPASSWD" 25 | ``` 26 | 27 | Please see [checks.md](./checks.md) for a detailed list of all parameters. 28 | 29 | - `FileContains` and `DirContains` use regex by default. `FileContainsRegex` and `DirContainsRegex` call these functions for backwards compatibility reasons as of v2.0.0, but these aliases may be phased out in the future 30 | 31 | # Changes for Developers 32 | 33 | - In order to call scoring functions, you must construct _or_ use an existing `check` and call the appropriate method like so: 34 | 35 | ``` 36 | result, err := cond{ 37 | SomeKey: "value" 38 | }.Method() 39 | ``` 40 | 41 | - The `cmd` structure no longer exists, so you don't need to call functions that resided under `cmd/` using the `cmd.` prefix when referring to them in `aeacus.go` and `phocus.go` 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/elysium-suite/aeacus 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/ActiveState/termtest/conpty v0.5.0 7 | github.com/BurntSushi/toml v1.2.0 8 | github.com/DataDog/datadog-agent/pkg/util/winutil v0.36.1 9 | github.com/creack/pty v1.1.18 10 | github.com/fatih/color v1.13.0 11 | github.com/gen2brain/beeep v0.0.0-20220518085355-d7852edf42fc 12 | github.com/gorilla/websocket v1.5.0 13 | github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 14 | github.com/iamacarpet/go-win64api v0.0.0-20220720120512-241a9064deec 15 | github.com/judwhite/go-svc v1.2.1 16 | github.com/pkg/errors v0.9.1 17 | github.com/stretchr/testify v1.8.0 18 | github.com/urfave/cli/v2 v2.14.0 19 | golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 20 | golang.org/x/text v0.3.8 21 | ) 22 | 23 | require ( 24 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect 25 | github.com/DataDog/datadog-agent/pkg/util/log v0.36.1 // indirect 26 | github.com/DataDog/datadog-agent/pkg/util/scrubber v0.36.1 // indirect 27 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect 28 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 29 | github.com/davecgh/go-spew v1.1.1 // indirect 30 | github.com/go-ole/go-ole v1.2.6 // indirect 31 | github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect 32 | github.com/godbus/dbus/v5 v5.1.0 // indirect 33 | github.com/google/cabbie v1.0.2 // indirect 34 | github.com/google/glazier v0.0.0-20211029225403-9f766cca891d // indirect 35 | github.com/mattn/go-colorable v0.1.9 // indirect 36 | github.com/mattn/go-isatty v0.0.14 // indirect 37 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect 38 | github.com/pmezard/go-difflib v1.0.0 // indirect 39 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 40 | github.com/scjalliance/comshim v0.0.0-20190308082608-cf06d2532c4e // indirect 41 | github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect 42 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 43 | gopkg.in/yaml.v3 v3.0.1 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /gui_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func launchIDPrompt() { 4 | err := shellCommand(`zenity --title "Team ID Prompt" --text "Enter your Team ID below!" --entry > ` + dirPath + "TeamID.txt") 5 | if err != nil { 6 | fail("Error running ID prompt command: " + err.Error()) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /gui_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // launchIDPrompt launches an ID Prompt on Windows using PowerShell 4 | func launchIDPrompt() { 5 | powerShellPrompt := ` 6 | Start-Service -Name CSSClient -ErrorAction SilentlyContinue 7 | $teamIDContent = Get-Content C:\aeacus\TeamID.txt 8 | if ($teamIDContent -eq "YOUR-TEAMID-HERE") { 9 | Add-Type -AssemblyName System.Windows.Forms 10 | [System.Windows.Forms.Application]::EnableVisualStyles() 11 | 12 | $Form = New-Object system.Windows.Forms.Form 13 | $Form.ClientSize = New-Object System.Drawing.Point(520,265) 14 | $Form.text = "Aeacus" 15 | $Form.TopMost = $true 16 | $Form.Icon = "C:\aeacus\assets\img\logo.ico" 17 | $Form.BackgroundImage = [system.drawing.image]::FromFile("C:\aeacus\assets\img\TeamIDbackground.png") 18 | 19 | $Label1 = New-Object system.Windows.Forms.Label 20 | $Label1.text = "Enter Your Unique Team ID" 21 | $Label1.AutoSize = $true 22 | $Label1.width = 170 23 | $Label1.height = 9 24 | $Label1.location = New-Object System.Drawing.Point(117,44) 25 | $Label1.Font = New-Object System.Drawing.Font('Raleway',16,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold)) 26 | $Label1.BackColor = [System.Drawing.Color]::FromName("Transparent") 27 | $Label1.ForeColor = [System.Drawing.ColorTranslator]::FromHtml("#ffffff") 28 | 29 | $TextBox1 = New-Object system.Windows.Forms.TextBox 30 | $TextBox1.multiline = $false 31 | $TextBox1.width = 333 32 | $TextBox1.height = 38 33 | $TextBox1.location = New-Object System.Drawing.Point(80,109) 34 | $TextBox1.Font = New-Object System.Drawing.Font('Consolas',12) 35 | 36 | $Button1 = New-Object system.Windows.Forms.Button 37 | $Button1.text = "Validate" 38 | $Button1.width = 110 39 | $Button1.height = 40 40 | $Button1.location = New-Object System.Drawing.Point(202,155) 41 | $Button1.Font = New-Object System.Drawing.Font('Raleway',10) 42 | $Button1.Image = [System.Drawing.Image]::FromFile("C:\aeacus\assets\img\Buttonbackground.png") 43 | $Button1.ForeColor = [System.Drawing.ColorTranslator]::FromHtml("#ffffff") 44 | 45 | $Form.controls.AddRange(@($Label1,$TextBox1,$Button1)) 46 | $Button1.Add_Click({ setID }) 47 | 48 | function setID { 49 | $global:id=$TextBox1.Text 50 | $id = $id -replace '\s','' 51 | echo $id > C:\aeacus\TeamID.txt 52 | $form.Close() 53 | } 54 | 55 | [void]$Form.ShowDialog() 56 | } 57 | ` 58 | shellCommand(powerShellPrompt) 59 | } 60 | -------------------------------------------------------------------------------- /info_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // getInfo is a helper function to retrieve information about the 9 | // system. 10 | func getInfo(infoType string) { 11 | switch infoType { 12 | case "programs": 13 | programList, _ := getPrograms() 14 | for _, p := range programList { 15 | info(p) 16 | } 17 | case "users": 18 | userList, _ := getLocalUsers() 19 | for _, u := range userList { 20 | info(fmt.Sprint(u)) 21 | } 22 | case "admins": 23 | adminList, _ := getLocalAdmins() 24 | for _, u := range adminList { 25 | info(fmt.Sprint(u)) 26 | } 27 | default: 28 | if infoType == "" { 29 | fail("No info type provided. See the README for supported types.") 30 | } else { 31 | fail("No info for \"" + infoType + "\" found.") 32 | } 33 | os.Exit(1) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /misc/desktop/ReadMe.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Version=1.0 4 | Type=Application 5 | Terminal=false 6 | Exec=/usr/bin/firefox /opt/aeacus/assets/ReadMe.html 7 | Name=ReadMe 8 | Icon=/opt/aeacus/assets/img/logo.png 9 | -------------------------------------------------------------------------------- /misc/desktop/ScoringReport.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Version=1.0 4 | Type=Application 5 | Terminal=false 6 | Exec=/usr/bin/firefox /opt/aeacus/assets/ScoringReport.html 7 | Name=Scoring Report 8 | Icon=/opt/aeacus/assets/img/logo.png 9 | -------------------------------------------------------------------------------- /misc/desktop/StopScoring.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Version=1.0 4 | Type=Application 5 | Terminal=true 6 | Exec=/usr/bin/sudo /bin/bash /opt/aeacus/assets/scripts/stop_scoring.sh 7 | Name=Stop Scoring 8 | Icon=/opt/aeacus/assets/img/logo.png 9 | -------------------------------------------------------------------------------- /misc/desktop/TeamID.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Version=1.0 4 | Type=Application 5 | Terminal=false 6 | Exec=/usr/bin/gedit /opt/aeacus/TeamID.txt 7 | Name=Team ID 8 | Icon=/opt/aeacus/assets/img/logo.png 9 | -------------------------------------------------------------------------------- /misc/dev/CSSClient: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: aeacus-client 5 | # Required-Start: $remote_fs $syslog 6 | # Required-Stop: $remote_fs $syslog 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: Aeacus scoring system 10 | ### END INIT INFO 11 | 12 | start() { 13 | start-stop-daemon -S -m -p /run/aeacus-client.pid -b -C -x "/opt/aeacus/phocus" 14 | } 15 | 16 | stop() { 17 | start-stop-daemon -K --remove-pidfile -q -p /run/aeacus-client.pid 18 | } 19 | 20 | case "$1" in 21 | start) 22 | start $@ 23 | ;; 24 | stop) 25 | echo "Why are you trying to stop the engine? :thinking:" 26 | ;; 27 | restart) 28 | stop 29 | start $@ 30 | ;; 31 | status) 32 | start-stop-daemon -T -p /run/aeacus-client.pid && engine="running" || engine="stopped" 33 | echo "aeacus is $engine" 34 | ;; 35 | *) 36 | echo "Usage: $0 {start|stop|status}" 37 | ;; 38 | esac 39 | 40 | exit 0 41 | -------------------------------------------------------------------------------- /misc/dev/ReadMe.conf: -------------------------------------------------------------------------------- 1 | 2 |

3 | Uncomplicated Firewall (UFW) is the only company approved Firewall for use on 4 | Linux machines at this time. 5 |

6 | 7 |

8 | Congratulations! You just recruited a promising new team member. Create a new 9 | Standard user account named "bobberino" with a temporary password of your 10 | choosing. 11 |

12 | 13 |

Authorized users must be able to access this computer remotely using ssh.

14 | 15 | 16 |

Critical Services:

17 | 21 | 22 | 23 |

Authorized Administrators and Users

24 | 25 |
26 | Authorized Administrators:
27 | coolUser (you)
28 | 		password: coolPassword
29 | bob
30 | 		password: bob
31 | 
32 | Authorized Users:
33 | coolFriend
34 | awesomeUser
35 | radUser
36 | coolGuy
37 | niceUser
38 | superCoolDude
39 | 
40 | -------------------------------------------------------------------------------- /misc/dev/gen-crypto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | rand() { xxd -l 64 -c 64 -p /dev/urandom; } 5 | replace() { sed -i "s/$1/$2/g" crypto.go; } 6 | 7 | hashVal=$(rand) 8 | byteKey=$(rand | sed 's/\(..\)/0x\1, /g') 9 | randomBytes=$(rand | sed 's/\(..\)/0x\1, /g') 10 | 11 | if [ -f "crypto.go.bak" ]; then 12 | mv crypto.go.bak crypto.go 13 | fi 14 | 15 | cp crypto.go crypto.go.bak 16 | 17 | replace "HASH_HERE" "$hashVal" 18 | replace "0x01" "`echo $byteKey | head -c 382`" 19 | replace "{1}" "{`echo $randomBytes | head -c 382`}" 20 | 21 | echo "Generated random keys for crypto.go" 22 | -------------------------------------------------------------------------------- /misc/gh/ReadMe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/misc/gh/ReadMe.png -------------------------------------------------------------------------------- /misc/gh/ScoringReport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/misc/gh/ScoringReport.png -------------------------------------------------------------------------------- /misc/gh/hint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysium-suite/aeacus/5e1356c6a816603525c52e0dfa870da14bcb9c1a/misc/gh/hint.png -------------------------------------------------------------------------------- /misc/tests/TestFileContains.txt: -------------------------------------------------------------------------------- 1 | world hello! 2 | hello world! 3 | -------------------------------------------------------------------------------- /misc/tests/dir/hello/3.txt: -------------------------------------------------------------------------------- 1 | abcdabcd 2 | abcdabcd 3 | abcdabcd 4 | abcdabcd 5 | abcdabcd 6 | abcdabcd 7 | abcdabcd 8 | abcdabcd 9 | abcdabcd 10 | abcdabcd 11 | abcdabcd 12 | abcdabcd 13 | abcdabcd 14 | abcdabcd 15 | abcdabcd 16 | abcdabcd 17 | abcdabcd 18 | abcdabcd 19 | abcdabcd 20 | abcdabcd 21 | abcdabcd 22 | abcdabcd 23 | abcdabcd 24 | abcdabcd 25 | abcdabcd 26 | abcdabcd 27 | abcdabcd 28 | abcdabcd 29 | abcdabcd 30 | aaaa 31 | abcdabcd 32 | abcdabcd 33 | abcdabcd 34 | abcdabcd 35 | abcdabcd 36 | abcdabcd 37 | abcdabcd 38 | abcdabcd 39 | abcdabcd 40 | abcdabcd 41 | abcdabcd 42 | abcdabcd 43 | abcdabcd 44 | abcdabcd 45 | abcdabcd 46 | abcdabcd 47 | abcdabcd 48 | abcdabcd 49 | abcdabcd 50 | abcdabcd 51 | abcdabcd 52 | efghabcd 53 | abcdabcd 54 | abcdabcd 55 | abcdabcd 56 | abcdabcd 57 | abcdabcd 58 | spaces in it 15879163 nums 59 | abcdabcd 60 | abcdabcd 61 | abcdabcd 62 | abcdabcd 63 | abcdabcd 64 | abcdabcd 65 | abcdabcd 66 | abcdabcd 67 | abcdabcd 68 | abcdabcd 69 | abcdabcd 70 | abcd 71 | -------------------------------------------------------------------------------- /misc/tests/dir/world/1.txt: -------------------------------------------------------------------------------- 1 | abcdabcd 2 | abcdabcd 3 | abcdabcd 4 | abcdabcd 5 | abcdabcd 6 | abcdabcd 7 | abcdabcd 8 | abcdabcd 9 | abcdabcd 10 | abcdabcd 11 | abcdabcd 12 | abcdabcd 13 | abcdabcd 14 | abcdabcd 15 | abcdabcd 16 | abcdabcd 17 | abcdabcd 18 | abcdabcd 19 | abcdabcd 20 | abcdabcd 21 | abcdabcd 22 | abcdabcd 23 | abcdabcd 24 | abcdabcd 25 | abcdabcd 26 | abcdabcd 27 | abcdabcd 28 | abcdabcd 29 | abcdabcd 30 | abcdabcd 31 | abcdabcd 32 | abcdabcd 33 | abcdabcd 34 | abcdabcd 35 | abcdabcd 36 | abcdabcd 37 | abcdabcd 38 | abcdabcd 39 | abcdabcd 40 | abcdabcd 41 | abcdabcd 42 | abcdabcd 43 | abcdabcd 44 | abcdabcd 45 | abcdabcd 46 | abcdabcd 47 | abcdabcd 48 | abcdabcd 49 | abcdabcd 50 | abcdabcd 51 | abcdabcd 52 | abcdabcd 53 | abcdabcd 54 | abcdabcd 55 | abcdabcd 56 | abcdabcd 57 | abcdabcd 58 | abcdabcd 59 | abcdabcd 60 | abcdabcd 61 | abcdabcd 62 | abcdabcd 63 | abcdabcd 64 | abcdabcd 65 | abcdabcd 66 | abcdabcd 67 | abcdabcd 68 | abcdabcd 69 | -------------------------------------------------------------------------------- /misc/tests/dir/world/2.txt: -------------------------------------------------------------------------------- 1 | abcdabcd 2 | abcdabcd 3 | abcdabcd 4 | abcdabcd 5 | abcdabcd 6 | abcdabcd 7 | abcdabcd 8 | abcdabcd 9 | abcdabcd 10 | abcdabcd 11 | abcdabcd 12 | abcdabcd 13 | abcdabcd 14 | abcdabcd 15 | abcdabcd 16 | abcdabcd 17 | abcdabcd 18 | abcdabcd 19 | abcdabcd 20 | abcdabcd 21 | abcdabcd 22 | abcdabcd 23 | abcdabcd 24 | abcdabcd 25 | abcdabcd 26 | abcdabcd 27 | abcdabcd 28 | abcdabcd 29 | abcdabcd 30 | abcdabcd 31 | abcdabcd 32 | abcdabcd 33 | abcdabcd 34 | abcdabcd 35 | abcdabcd 36 | abcdabcd 37 | abcdabcd 38 | abcdabcd 39 | abcdabcd 40 | abcdabcd 41 | abcdabcd 42 | abcdabcd 43 | abcdabcd 44 | abcdabcd 45 | abcdabcd 46 | abcdabcd 47 | abcdabcd 48 | abcdabcd 49 | abcdabcd 50 | abcdabcd 51 | abcdabcd 52 | abcdabcd 53 | abcdabcd 54 | abcdabcd 55 | abcdabcd 56 | abcdabcd 57 | abcdabcd 58 | abcdabcd 59 | abcdabcd 60 | abcdabcd 61 | abcdabcd 62 | abcdabcd 63 | abcdabcd 64 | abcdabcd 65 | abcdabcd 66 | abcdabcd 67 | abcdabcd 68 | abcdabcd 69 | -------------------------------------------------------------------------------- /output.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | // confirm will prompt the user with the given toPrint string, and 12 | // exit the program if N or n is input. 13 | func confirm(p ...interface{}) { 14 | if yesEnabled { 15 | return 16 | } 17 | toPrint := fmt.Sprint(p...) 18 | toPrint = printer(color.FgYellow, "CONF", toPrint) 19 | fmt.Print(toPrint + " [Y/n]: ") 20 | var resp string 21 | fmt.Scanln(&resp) 22 | if strings.ToLower(strings.TrimSpace(resp)) == "n" { 23 | os.Exit(1) 24 | } 25 | } 26 | 27 | // ask will prompt the user with the given toPrint string, and 28 | // return a boolean. 29 | func ask(p ...interface{}) bool { 30 | if yesEnabled { 31 | return true 32 | } 33 | toPrint := fmt.Sprint(p...) 34 | toPrint = printer(color.FgBlue, "CONF", toPrint) 35 | fmt.Print(toPrint + " [Y/n]: ") 36 | var resp string 37 | fmt.Scanln(&resp) 38 | if strings.ToLower(strings.TrimSpace(resp)) == "n" { 39 | return false 40 | } 41 | return true 42 | } 43 | 44 | func pass(p ...interface{}) { 45 | toPrint := fmt.Sprintln(p...) 46 | var printStr string 47 | if checkCount != 0 { 48 | printStr = printer(color.FgGreen, fmt.Sprintf("%s:%d", "PASS", checkCount), toPrint) 49 | } else { 50 | printStr = printer(color.FgGreen, "PASS", toPrint) 51 | } 52 | fmt.Print(printStr) 53 | } 54 | 55 | func fail(p ...interface{}) { 56 | toPrint := fmt.Sprintln(p...) 57 | var printStr string 58 | if checkCount != 0 { 59 | printStr = printer(color.FgRed, fmt.Sprintf("%s:%d", "FAIL", checkCount), toPrint) 60 | } else { 61 | printStr = printer(color.FgRed, "FAIL", toPrint) 62 | } 63 | fmt.Print(printStr) 64 | } 65 | 66 | func warn(p ...interface{}) { 67 | toPrint := fmt.Sprintln(p...) 68 | var printStr string 69 | if checkCount != 0 { 70 | printStr = printer(color.FgYellow, fmt.Sprintf("%s:%d", "WARN", checkCount), toPrint) 71 | } else { 72 | printStr = printer(color.FgYellow, "WARN", toPrint) 73 | } 74 | fmt.Print(printStr) 75 | } 76 | 77 | func debug(p ...interface{}) { 78 | // Function inlining and dead code elimination means that debug calls 79 | // (and their strings) will be optimized out on phocus builds, with any 80 | // recent Go compiler. 81 | // 82 | // So, we can make as many debug calls as we want, and it won't make a 83 | // specific version of phocus easier to reverse. (Easier than it already 84 | // is, since source code is available.) This really only benefits those 85 | // with custom builds. 86 | if DEBUG_BUILD && debugEnabled { 87 | toPrint := fmt.Sprintln(p...) 88 | var printStr string 89 | if checkCount != 0 { 90 | printStr = printer(color.FgMagenta, fmt.Sprintf("%s:%d", "DBUG", checkCount), toPrint) 91 | } else { 92 | printStr = printer(color.FgMagenta, "DBUG", toPrint) 93 | } 94 | fmt.Print(printStr) 95 | } 96 | } 97 | 98 | func info(p ...interface{}) { 99 | if verboseEnabled { 100 | toPrint := fmt.Sprintln(p...) 101 | var printStr string 102 | if checkCount != 0 { 103 | printStr = printer(color.FgCyan, fmt.Sprintf("%s:%d", "INFO", checkCount), toPrint) 104 | } else { 105 | printStr = printer(color.FgCyan, "INFO", toPrint) 106 | } 107 | fmt.Print(printStr) 108 | } 109 | } 110 | 111 | func blue(head string, p ...interface{}) { 112 | toPrint := fmt.Sprintln(p...) 113 | printStr := printer(color.FgCyan, head, toPrint) 114 | fmt.Print(printStr) 115 | } 116 | 117 | func red(head string, p ...interface{}) { 118 | toPrint := fmt.Sprintln(p...) 119 | fmt.Print(printer(color.FgRed, head, toPrint)) 120 | } 121 | 122 | func green(head string, p ...interface{}) { 123 | toPrint := fmt.Sprintln(p...) 124 | fmt.Print(printer(color.FgGreen, head, toPrint)) 125 | } 126 | 127 | func printer(colorChosen color.Attribute, messageType, toPrint string) string { 128 | printer := color.New(colorChosen, color.Bold) 129 | printStr := "[" 130 | printStr += printer.Sprintf(messageType) 131 | printStr += fmt.Sprintf("] %s", toPrint) 132 | return printStr 133 | } 134 | -------------------------------------------------------------------------------- /phocus.go: -------------------------------------------------------------------------------- 1 | //go:build phocus 2 | 3 | package main 4 | 5 | import ( 6 | "math/rand" 7 | "os" 8 | "time" 9 | 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | const ( 14 | DEBUG_BUILD = false 15 | ) 16 | 17 | func phocusLoop() { 18 | info("Initializing engine context...") 19 | phocusEnvironment() 20 | if conf.Shell { 21 | go shellSocket() 22 | } 23 | for { 24 | scoreImage() 25 | jitter := time.Duration(rand.Intn(8) + 10) 26 | info("Scored image, sleeping for a bit...") 27 | time.Sleep(jitter * time.Second) 28 | } 29 | } 30 | 31 | // phocusEnvironment runs functions needed in order for phocus to successfully 32 | // run on first start. 33 | func phocusEnvironment() { 34 | // Make sure we're running as admin. 35 | permsCheck() 36 | // Make sure phocus is not being traced or debugged. 37 | checkTrace() 38 | // Read in scoring data from the scoring data file. 39 | if err := readScoringData(); err != nil { 40 | fail(err) 41 | os.Exit(1) 42 | } 43 | // Seed the random function for scoring at "random" intervals. 44 | rand.Seed(time.Now().UnixNano()) 45 | } 46 | 47 | // genPhocusApp generates a basic CLI interface that is OS-independent. 48 | func genPhocusApp() *cli.App { 49 | return &cli.App{ 50 | Name: "phocus", 51 | Usage: "score vulnerabilities", 52 | Action: func(c *cli.Context) error { 53 | phocusLoop() 54 | return nil 55 | }, 56 | Before: func(c *cli.Context) error { 57 | err := determineDirectory() 58 | if err != nil { 59 | return err 60 | } 61 | return nil 62 | }, 63 | Commands: []*cli.Command{ 64 | { 65 | Name: "prompt", 66 | Aliases: []string{"p"}, 67 | Usage: "Launch TeamID GUI prompt", 68 | Action: func(c *cli.Context) error { 69 | launchIDPrompt() 70 | return nil 71 | }, 72 | }, 73 | { 74 | Name: "version", 75 | Aliases: []string{"v"}, 76 | Usage: "Print the current version of phocus", 77 | Action: func(c *cli.Context) error { 78 | println("phocus version " + version) 79 | return nil 80 | }, 81 | }, 82 | }, 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /phocus_linux.go: -------------------------------------------------------------------------------- 1 | //go:build phocus 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | "os" 8 | ) 9 | 10 | func main() { 11 | app := genPhocusApp() 12 | err := app.Run(os.Args) 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /phocus_windows.go: -------------------------------------------------------------------------------- 1 | //go:build phocus 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | "sync" 11 | "time" 12 | 13 | "github.com/judwhite/go-svc" 14 | ) 15 | 16 | func phocusStart(quit chan struct{}) { 17 | app := genPhocusApp() 18 | err := app.Run(os.Args) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | } 23 | 24 | // idgui is set using the flag package in order to grab its value before 25 | // the Windows service is initialized. 26 | var idgui *bool = flag.Bool("i", false, "Spawn TeamID gui") 27 | 28 | // Program implements svc.Service, for Windows Services. 29 | type program struct { 30 | wg sync.WaitGroup 31 | quit chan struct{} 32 | } 33 | 34 | func main() { 35 | flag.Parse() 36 | prg := &program{} 37 | if err := svc.Run(prg); err != nil { 38 | log.Fatal(err) 39 | } 40 | } 41 | 42 | // Init for our windows service *program will prevent people from running 43 | // the production binary outside of the Windows service. 44 | func (p *program) Init(env svc.Environment) error { 45 | if !env.IsWindowsService() && !*idgui { 46 | fmt.Println("Sorry! Don't run this binary yourself. It's probably already running as a Windows service (CSSClient).") 47 | time.Sleep(5 * time.Second) 48 | os.Exit(1) 49 | } 50 | return nil 51 | } 52 | 53 | func (p *program) Start() error { 54 | p.quit = make(chan struct{}) 55 | p.wg.Add(1) 56 | if *idgui { 57 | go launchIDPromptWrapper(p.quit) 58 | } else { 59 | go phocusStart(p.quit) 60 | } 61 | return nil 62 | } 63 | 64 | func (p *program) Stop() error { 65 | log.Println("Stopping...") 66 | close(p.quit) 67 | os.Exit(1) 68 | /* 69 | Causes windows service stopping error (but it works) 70 | Quit struct doesn't work... todo 71 | p.wg.Wait() 72 | log.Println("Stopped.") 73 | */ 74 | return nil 75 | } 76 | 77 | func launchIDPromptWrapper(quit chan struct{}) { 78 | launchIDPrompt() 79 | os.Exit(0) // This is temporary solution 80 | } 81 | -------------------------------------------------------------------------------- /policies_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // secpolToKey contains a large mapping of securityPolicy names or keys to 4 | // registry locations. 5 | var secpolToKey = map[string]string{ 6 | "LimitBlankPasswordUse": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\LimitBlankPasswordUse", 7 | "AuditBaseObjects": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\AuditBaseObjects", 8 | "FullPrivilegeAuditing": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\FullPrivilegeAuditing", 9 | "SCENoApplyLegacyAuditPolicy": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\SCENoApplyLegacyAuditPolicy", 10 | "CrashOnAuditFail": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\CrashOnAuditFail", 11 | "MachineAccessRestriction": "MACHINE\\SOFTWARE\\policies\\Microsoft\\windows NT\\DCOM\\MachineAccessRestriction", 12 | "MachineLaunchRestriction": "MACHINE\\SOFTWARE\\policies\\Microsoft\\windows NT\\DCOM\\MachineLaunchRestriction", 13 | "UndockWithoutLogon": "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\UndockWithoutLogon", 14 | "AllocateDASD": "MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\AllocateDASD", 15 | "AddPrinterDrivers": "MACHINE\\System\\CurrentControlSet\\Control\\Print\\Providers\\LanMan Print Services\\Servers\\AddPrinterDrivers", 16 | "AllocateCDRoms": "MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\AllocateCDRoms", 17 | "AllocateFloppies": "MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\AllocateFloppies", 18 | "SubmitControl": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\SubmitControl", 19 | "LDAPServerIntegrity": "MACHINE\\System\\CurrentControlSet\\Services\\NTDS\\Parameters\\LDAPServerIntegrity", 20 | "RefusePasswordChange": "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\RefusePasswordChange", 21 | "RequireSignOrSeal": "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\RequireSignOrSeal", 22 | "SealSecureChannel": "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\SealSecureChannel", 23 | "SignSecureChannel": "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\SignSecureChannel", 24 | "DisablePasswordChange": "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\DisablePasswordChange", 25 | "RequireStrongKey": "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\RequireStrongKey", 26 | "DontDisplayLockedUserId": "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System, value=DontDisplayLockedUserId", 27 | "DisableCAD": "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\DisableCAD", 28 | "DontDisplayLastUserName": "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\DontDisplayLastUserName", 29 | "LegalNoticeText": "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\LegalNoticeText", 30 | "LegalNoticeCaption": "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\LegalNoticeCaption", 31 | "CachedLogonsCount": "MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\CachedLogonsCount", 32 | "PasswordExpiryWarning": "MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\PasswordExpiryWarning", 33 | "ForceUnlockLogon": "MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\ForceUnlockLogon", 34 | "ScForceOption": "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\ScForceOption", 35 | "ScRemoveOption": "MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\ScRemoveOption", 36 | "RequireSecuritySignature": "MACHINE\\System\\CurrentControlSet\\Services\\LanmanWorkstation\\Parameters\\RequireSecuritySignature", 37 | "EnablePlainTextPassword": "MACHINE\\System\\CurrentControlSet\\Services\\LanmanWorkstation\\Parameters\\EnablePlainTextPassword", 38 | "AutoDisconnect": "MACHINE\\System\\CurrentControlSet\\Services\\LanManServer\\Parameters\\AutoDisconnect", 39 | "EnableSecuritySignature": "MACHINE\\System\\CurrentControlSet\\Services\\LanManServer\\Parameters\\EnableSecuritySignature", 40 | "EnableForcedLogOff": "MACHINE\\System\\CurrentControlSet\\Services\\LanManServer\\Parameters\\EnableForcedLogOff", 41 | "SmbServerNameHardeningLevel": "MACHINE\\System\\CurrentControlSet\\Services\\LanManServer\\Parameters\\SmbServerNameHardeningLevel", 42 | "RestrictAnonymousSAM": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\RestrictAnonymousSAM", 43 | "RestrictAnonymous": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\RestrictAnonymous", 44 | "DisableDomainCreds": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\DisableDomainCreds", 45 | "EveryoneIncludesAnonymous": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\EveryoneIncludesAnonymous", 46 | "NullSessionPipes": "MACHINE\\System\\CurrentControlSet\\Services\\LanManServer\\Parameters\\NullSessionPipes", 47 | "Machine": "MACHINE\\System\\CurrentControlSet\\Control\\SecurePipeServers\\Winreg\\AllowedPaths\\Machine", 48 | "ForceGuest": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\ForceGuest", 49 | "UseMachineId": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\UseMachineId", 50 | "allownullsessionfallback": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\MSV1_0\\allownullsessionfallback", 51 | "AllowOnlineID": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\pku2u\\AllowOnlineID", 52 | "SupportedEncryptionTypes": "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\Kerberos\\Parameters\\SupportedEncryptionTypes", 53 | "NoLMHash": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\NoLMHash", 54 | "LmCompatibilityLevel": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\LmCompatibilityLevel", 55 | "LDAPClientIntegrity": "MACHINE\\System\\CurrentControlSet\\Services\\LDAP\\LDAPClientIntegrity", 56 | "NTLMMinClientSec": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\MSV1_0\\NTLMMinClientSec", 57 | "NTLMMinServerSec": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\MSV1_0\\NTLMMinServerSec", 58 | "RestrictNTLMInDomain": "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\RestrictNTLMInDomain", 59 | "ClientAllowedNTLMServers": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\MSV1_0\\ClientAllowedNTLMServers", 60 | "DCAllowedNTLMServers": "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\DCAllowedNTLMServers", 61 | "AuditReceivingNTLMTraffic": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\MSV1_0\\AuditReceivingNTLMTraffic", 62 | "AuditNTLMInDomain": "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\AuditNTLMInDomain", 63 | "RestrictReceivingNTLMTraffic": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\MSV1_0\\RestrictReceivingNTLMTraffic", 64 | "RestrictSendingNTLMTraffic": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\MSV1_0\\RestrictSendingNTLMTraffic", 65 | "SecurityLevel": "MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Setup\\RecoveryConsole\\SecurityLevel", 66 | "SetCommand": "MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Setup\\RecoveryConsole\\SetCommand", 67 | "ShutdownWithoutLogon": "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\ShutdownWithoutLogon", 68 | "ClearPageFileAtShutdown": "MACHINE\\System\\CurrentControlSet\\Control\\Session Manager\\Memory Management\\ClearPageFileAtShutdown", 69 | "ForceKeyProtection": "MACHINE\\Software\\Policies\\Microsoft\\Cryptography\\ForceKeyProtection", 70 | "FIPSAlgorithmPolicy": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\FIPSAlgorithmPolicy", 71 | "NoDefaultAdminOwner": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\NoDefaultAdminOwner", 72 | "ObCaseInsensitive": "MACHINE\\System\\CurrentControlSet\\Control\\Session Manager\\Kernel\\ObCaseInsensitive", 73 | "ProtectionMode": "MACHINE\\System\\CurrentControlSet\\Control\\Session Manager\\ProtectionMode", 74 | "optional": "MACHINE\\System\\CurrentControlSet\\Control\\Session Manager\\SubSystems\\optional", 75 | "AuthenticodeEnabled": "MACHINE\\Software\\Policies\\Microsoft\\Windows\\Safer\\CodeIdentifiers\\AuthenticodeEnabled", 76 | "FilterAdministratorToken": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\FilterAdministratorToken", 77 | "EnableUIADesktopToggle": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\EnableUIADesktopToggle", 78 | "ConsentPromptBehaviorAdmin": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\ConsentPromptBehaviorAdmin", 79 | "ConsentPromptBehaviorUser": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\ConsentPromptBehaviorUser", 80 | "EnableInstallerDetection": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\EnableInstallerDetection", 81 | "ValidateAdminCodeSignatures": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\ValidateAdminCodeSignatures", 82 | "EnableSecureUIAPaths": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\EnableSecureUIAPaths", 83 | "EnableLUA": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\PromptOnSecureDesktop", 84 | "PromptOnSecureDesktop": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\EnableLUA", 85 | } 86 | -------------------------------------------------------------------------------- /release_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // writeDesktopFiles creates TeamID.txt and its shortcut, as well as links 4 | // to the ScoringReport, ReadMe, and other needed files. 5 | func writeDesktopFiles() { 6 | info("Creating or emptying TeamID.txt...") 7 | shellCommand("echo 'YOUR-TEAMID-HERE' > " + dirPath + "TeamID.txt") 8 | shellCommand("chmod 666 " + dirPath + "TeamID.txt") 9 | shellCommand("chown " + conf.User + ":" + conf.User + " " + dirPath + "TeamID.txt") 10 | info("Writing shortcuts to Desktop...") 11 | shellCommand("mkdir -p /home/" + conf.User + "/Desktop/") 12 | shellCommand("cp " + dirPath + "misc/desktop/*.desktop /home/" + conf.User + "/Desktop/") 13 | shellCommand("chmod +x /home/" + conf.User + "/Desktop/*.desktop") 14 | shellCommand("chown " + conf.User + ":" + conf.User + " /home/" + conf.User + "/Desktop/*") 15 | } 16 | 17 | // configureAutologin configures the auto-login capability for LightDM and 18 | // GDM3, so that the image automatically logs in to the main user's account 19 | // on boot. 20 | func configureAutologin() { 21 | lightdm, _ := cond{Path: "/usr/share/lightdm"}.PathExists() 22 | gdm, _ := cond{Path: "/etc/gdm3/"}.PathExists() 23 | if lightdm { 24 | info("LightDM detected for autologin.") 25 | shellCommand(`echo "autologin-user=` + conf.User + `" >> /usr/share/lightdm/lightdm.conf.d/50-ubuntu.conf`) 26 | } else if gdm { 27 | info("GDM3 detected for autologin.") 28 | shellCommand(`echo -e "AutomaticLoginEnable=True\nAutomaticLogin=` + conf.User + `" >> /etc/gdm3/daemon.conf`) 29 | } else { 30 | fail("Unable to configure autologin! Please do so manually.") 31 | } 32 | } 33 | 34 | // installFont is skipped for Linux. 35 | func installFont() { 36 | info("Skipping font install for Linux...") 37 | } 38 | 39 | // installService for Linux installs and starts the CSSClient init.d service. 40 | func installService() { 41 | info("Installing service...") 42 | shellCommand("cp " + dirPath + "misc/dev/CSSClient /etc/init.d/") 43 | shellCommand("chmod +x /etc/init.d/CSSClient") 44 | shellCommand("systemctl enable CSSClient") 45 | shellCommand("systemctl start CSSClient") 46 | } 47 | 48 | // cleanUp for Linux is primarily focused on removing cached files, history, 49 | // and other pieces of forensic evidence. It also removes the non-required 50 | // files in the aeacus directory. 51 | func cleanUp() { 52 | findPaths := "/bin /etc /home /opt /root /sbin /srv /usr /mnt /var" 53 | 54 | info("Changing perms to 755 in " + dirPath + "...") 55 | shellCommand("chmod 755 -R " + dirPath) 56 | 57 | info("Removing aeacus binary...") 58 | shellCommand("rm " + dirPath + "aeacus") 59 | 60 | info("Removing scoring.conf...") 61 | shellCommand("rm " + dirPath + "scoring.conf*") 62 | 63 | info("Removing other setup files...") 64 | shellCommand("rm -rf " + dirPath + "misc/") 65 | shellCommand("find " + dirPath + " -name '[R|r]*.conf' -type f -delete") 66 | shellCommand("rm -rf " + dirPath + "README.md") 67 | shellCommand("rm -rf " + dirPath + ".git") 68 | shellCommand("rm -rf " + dirPath + ".github") 69 | shellCommand("rm -rf " + dirPath + "*.go") 70 | shellCommand("rm -rf " + dirPath + "Makefile") 71 | shellCommand("rm -rf " + dirPath + "go.*") 72 | shellCommand("rm -rf " + dirPath + "*.exe") 73 | shellCommand("rm -rf " + dirPath + "docs") 74 | 75 | if !ask("Do you want to remove cache and log files, overwrite timestamps, and remove other forensic data from this machine? This may impact data used for your forensic questions!") { 76 | return 77 | } 78 | 79 | info("Removing .viminfo and .swp files...") 80 | shellCommand("find " + findPaths + " -iname '*.viminfo*' -delete -iname '*.swp' -delete") 81 | 82 | info("Symlinking .bash_history and .zsh_history to /dev/null...") 83 | shellCommand(`find ` + findPaths + ` -iname '*.bash_history' -exec ln -sf /dev/null {} \;`) 84 | shellCommand(`find ` + findPaths + ` -name '.zsh_history' -exec ln -sf /dev/null {} \;`) 85 | 86 | info("Removing .mysql_history...") 87 | shellCommand(`find ` + findPaths + ` -name '.mysql_history' -exec rm {} \;`) 88 | 89 | info("Removing .local files...") 90 | shellCommand("rm -rf /root/.local /home/*/.local/") 91 | 92 | info("Removing cache...") 93 | shellCommand("rm -rf /root/.cache /home/*/.cache/") 94 | 95 | info("Removing temp root and Desktop files...") 96 | shellCommand("rm -rf /root/*~ /home/*/Desktop/*~") 97 | 98 | info("Removing crash and VMWare data...") 99 | shellCommand("rm -f /var/VMwareDnD/* /var/crash/*.crash") 100 | 101 | info("Removing apt and dpkg logs...") 102 | shellCommand("rm -rf /var/log/apt/* /var/log/dpkg.log") 103 | 104 | info("Removing logs (auth and syslog)...") 105 | shellCommand("rm -f /var/log/auth.log* /var/log/syslog*") 106 | 107 | info("Removing initial package list...") 108 | shellCommand("rm -f /var/log/installer/initial-status.gz") 109 | 110 | info("Installing BleachBit...") 111 | shellCommand("apt-get install -y bleachbit") 112 | 113 | info("Clearing Firefox cache and browsing history...") 114 | shellCommand("bleachbit --clean firefox.url_history; bleachbit --clean firefox.cache") 115 | 116 | info("Overwriting timestamps to obfuscate changes...") 117 | shellCommand(`find /etc /home /var -exec touch --date='2012-12-12 12:12' {} \; 2>/dev/null`) 118 | } 119 | -------------------------------------------------------------------------------- /release_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // writeDesktopFiles writes default scoring engine files to the desktop. 4 | func writeDesktopFiles() { 5 | firefoxBinary := `C:\Program Files\Mozilla Firefox\firefox.exe` 6 | info("Writing ScoringReport.html shortcut to Desktop...") 7 | cmdString := `$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut("C:\Users\` + conf.User + `\Desktop\ScoringReport.lnk"); $Shortcut.TargetPath = "` + firefoxBinary + `"; $Shortcut.Arguments = "C:\aeacus\assets\ScoringReport.html"; $Shortcut.Save()` 8 | shellCommand(cmdString) 9 | info("Writing ReadMe.html shortcut to Desktop...") 10 | cmdString = `$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut("C:\Users\` + conf.User + `\Desktop\ReadMe.lnk"); $Shortcut.TargetPath = "` + firefoxBinary + `"; $Shortcut.Arguments = "C:\aeacus\assets\ReadMe.html"; $Shortcut.Save()` 11 | shellCommand(cmdString) 12 | info("Creating or emptying TeamID.txt file...") 13 | cmdString = "echo 'YOUR-TEAMID-HERE' > C:\\aeacus\\TeamID.txt" 14 | shellCommand(cmdString) 15 | info("Changing Permissions of TeamID...") 16 | powershellPermission := ` 17 | $ACL = Get-ACL C:\aeacus\TeamID.txt 18 | $ACL.SetOwner([System.Security.Principal.NTAccount] $env:USERNAME) 19 | Set-Acl -Path C:\aeacus\TeamID.txt -AclObject $ACL 20 | ` 21 | shellCommand(powershellPermission) 22 | info("Writing TeamID shortcut to Desktop...") 23 | cmdString = `$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut("C:\Users\` + conf.User + `\Desktop\TeamID.lnk"); $Shortcut.TargetPath = "C:\aeacus\phocus.exe"; $Shortcut.Arguments = "-i yes"; $Shortcut.Save()` 24 | shellCommand(cmdString) 25 | 26 | // domain compatibility? doubt 27 | } 28 | 29 | // configureAutologin allows the current user to log in automatically. 30 | func configureAutologin() { 31 | info("Setting Up autologin for " + conf.User + "...") 32 | powershellAutoLogin := ` 33 | function Test-RegistryValue { 34 | 35 | param ( 36 | 37 | [parameter(Mandatory=$true)] 38 | [ValidateNotNullOrEmpty()]$Path, 39 | 40 | [parameter(Mandatory=$true)] 41 | [ValidateNotNullOrEmpty()]$Value 42 | ) 43 | 44 | try { 45 | 46 | Get-ItemProperty -Path $Path | Select-Object -ExpandProperty $Value -ErrorAction Stop | Out-Null 47 | return $true 48 | } 49 | 50 | catch { 51 | 52 | return $false 53 | 54 | } 55 | 56 | } 57 | $RegPath1Exists = Test-RegistryValue -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Value "DefaultUsername" 58 | if ($RegPath1Exists -eq $false) { 59 | New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -name "DefaultUsername" -Value $env:USERNAME -type String 60 | } 61 | elseif ($RegPath1Exists -eq $true) { 62 | Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -name "DefaultUsername" -Value $env:USERNAME -type String 63 | } 64 | 65 | $RegPath2Exists = Test-RegistryValue -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Value "AutoAdminLogon" 66 | if ($RegPath2Exists -eq $false) { 67 | New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -name "AutoAdminLogon" -Value 1 -type String 68 | } 69 | elseif ($RegPath2Exists -eq $true) { 70 | Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -name "AutoAdminLogon" -Value 1 -type String 71 | } 72 | ` 73 | shellCommand(powershellAutoLogin) 74 | } 75 | 76 | // installFont installs the Raleway font for ID Prompt. 77 | func installFont() { 78 | info("Installing Raleway font for ID Prompt...") 79 | powershellFontInstall := ` 80 | $SourceDir = "C:\aeacus\assets\fonts\Raleway" 81 | $Source = "C:\aeacus\assets\fonts\Raleway\*" 82 | $Destination = (New-Object -ComObject Shell.Application).Namespace(0x14) 83 | $TempFolder = "C:\Windows\Temp\Fonts" 84 | 85 | # Create the source directory if it doesn't already exist 86 | New-Item -ItemType Directory -Force -Path $SourceDir | Out-Null 87 | 88 | New-Item $TempFolder -Type Directory -Force | Out-Null 89 | 90 | Get-ChildItem -Path $Source -Include '*.ttf','*.ttc','*.otf' -Recurse | ForEach { 91 | If (-not(Test-Path "C:\Windows\Fonts\$($_.Name)")) { 92 | 93 | $Font = "$TempFolder\$($_.Name)" 94 | 95 | # Copy font to local temporary folder 96 | Copy-Item $($_.FullName) -Destination $TempFolder 97 | 98 | # Install font 99 | $Destination.CopyHere($Font,0x10) 100 | 101 | # Delete temporary copy of font 102 | Remove-Item $Font -Force 103 | } 104 | } 105 | ` 106 | shellCommand(powershellFontInstall) 107 | } 108 | 109 | // installService installs the Aeacus service on Windows. 110 | func installService() { 111 | info("Installing service with sc.exe...") 112 | cmdString := `sc.exe create CSSClient binPath= "C:\aeacus\phocus.exe" start= "auto" DisplayName= "CSSClient"` 113 | shellCommand(cmdString) 114 | info("Setting service description...") 115 | cmdString = `sc.exe description CSSClient "This is Aeacus's Competition Scoring System client. Don't stop or mess with this unless you want to not get points, and maybe have your registry deleted."` 116 | shellCommand(cmdString) 117 | info("Setting up TeamID scheduled task...") 118 | idTaskCreate := ` 119 | $action = New-ScheduledTaskAction -Execute "C:\aeacus\phocus.exe" -Argument "-i yes" 120 | $trigger = New-ScheduledTaskTrigger -AtLogon 121 | $principal = New-ScheduledTaskPrincipal -GroupId "BUILTIN\Administrators" -RunLevel Highest 122 | Register-ScheduledTask -TaskName "TeamID" -Description "Scheduled Task to ensure Aeacus TeamID prompt is displayed when needed" -Action $action -Trigger $trigger -Principal $principal 123 | ` 124 | serviceTaskCreate := ` 125 | $action = New-ScheduledTaskAction -Execute "net.exe" -Argument "start CSSClient" 126 | $trigger = New-ScheduledTaskTrigger -AtLogon 127 | $principal = New-ScheduledTaskPrincipal -GroupId "BUILTIN\Administrators" -RunLevel Highest 128 | Register-ScheduledTask -TaskName "CSSClient" -Description "Scheduled Task to ensure the CSSClient service remains up" -Action $action -Trigger $trigger -Principal $principal 129 | ` 130 | shellCommand(idTaskCreate) 131 | shellCommand(serviceTaskCreate) 132 | 133 | addExclusions := ` 134 | Add-MpPreference -ExclusionPath "C:\aeacus\phocus.exe" 135 | Add-MpPreference -ExclusionPath "C:\aeacus\" 136 | ` 137 | shellCommand(addExclusions) 138 | 139 | } 140 | 141 | // cleanUp clears out sensitive files left behind by image developers or the 142 | // scoring engine. 143 | func cleanUp() { 144 | info("Removing scoring.conf and ReadMe.conf...") 145 | shellCommand("Remove-Item -Force C:\\aeacus\\scoring.conf") 146 | shellCommand("Remove-Item -Path 'C:\\aeacus\\[R|r]*.conf' -Force") 147 | info("Removing previous.txt...") 148 | shellCommand("Remove-Item -Force C:\\aeacus\\previous.txt") 149 | if !ask("Do you want to remove cache and history files from this machine?") { 150 | return 151 | } 152 | info("Emptying recycle bin...") 153 | shellCommand("Clear-RecycleBin -Force") 154 | info("Clearing recently used...") 155 | shellCommand("Remove-Item -Force '${env:USERPROFILE}\\AppData\\Roaming\\Microsoft\\Windows\\Recent‌​*.lnk'") 156 | info("Clearing run.exe command history...") 157 | clearRunScript := `$path = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU" 158 | $arr = (Get-Item -Path $path).Property 159 | foreach($item in $arr) 160 | { 161 | if($item -ne "MRUList") 162 | { 163 | Remove-ItemProperty -Path $path -Name $item -ErrorAction SilentlyContinue 164 | } 165 | }` 166 | shellCommand(clearRunScript) 167 | info("Removing Command History for Powershell") 168 | shellCommand("Remove-Item (Get-PSReadlineOption).HistorySavePath") 169 | warn("Done with automatic cleanup! You need to remove aeacus.exe manually. The only things you need in the C:\\aeacus directory is phocus, scoring.dat, TeamID.txt, and the assets directory.") 170 | } 171 | -------------------------------------------------------------------------------- /remote.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "math" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // Use non-ASCII bytes as a delimiter. 17 | var delimiter = string(byte(255)) + string(byte(222)) 18 | 19 | const ( 20 | FAIL = "FAIL" 21 | GREEN = "green" 22 | RED = "red" 23 | ) 24 | 25 | func readTeamID() { 26 | fileContent, err := readFile(dirPath + "TeamID.txt") 27 | fileContent = strings.TrimSpace(fileContent) 28 | if err != nil { 29 | teamID = "" 30 | if conf.Remote != "" { 31 | fail("TeamID.txt does not exist!") 32 | conn.OverallColor = RED 33 | conn.OverallStatus = "Your TeamID files does not exist! Failed to score image." 34 | conn.Status = false 35 | } else { 36 | warn("TeamID.txt does not exist! This image is local only, so we will continue.") 37 | } 38 | sendNotification("TeamID.txt does not exist!") 39 | } else if fileContent == "" { 40 | teamID = "" 41 | fail("TeamID.txt is empty!") 42 | sendNotification("TeamID.txt is empty!") 43 | if conf.Remote != "" { 44 | conn.OverallStatus = RED 45 | conn.OverallStatus = "Your TeamID is empty! Failed to score image." 46 | conn.Status = false 47 | } 48 | } else { 49 | teamID = fileContent 50 | } 51 | } 52 | 53 | func writeString(stringToWrite *strings.Builder, key, value string) { 54 | stringToWrite.WriteString(key) 55 | stringToWrite.WriteString(delimiter) 56 | stringToWrite.WriteString(value) 57 | stringToWrite.WriteString(delimiter) 58 | } 59 | 60 | func genUpdate() (string, error) { 61 | var update strings.Builder 62 | // Write values for score update 63 | writeString(&update, "team", teamID) 64 | writeString(&update, "image", conf.Name) 65 | writeString(&update, "score", strconv.Itoa(image.Score)) 66 | writeString(&update, "vulns", genVulns()) 67 | writeString(&update, "time", strconv.Itoa(int(time.Now().Unix()))) 68 | info("Encrypting score update...") 69 | if err := deobfuscateData(&conf.Password); err != nil { 70 | fail(err) 71 | return "", err 72 | } 73 | 74 | finishedUpdate := "" 75 | 76 | // If DisableRemoteEncryption has been set to true in the configuration, don't encrypt the update. 77 | if conf.DisableRemoteEncryption { 78 | finishedUpdate = hexEncode(update.String()) 79 | } else { 80 | finishedUpdate = hexEncode(encryptString(conf.Password, update.String())) 81 | } 82 | 83 | if err := obfuscateData(&conf.Password); err != nil { 84 | fail(err) 85 | return "", err 86 | } 87 | return finishedUpdate, nil 88 | } 89 | 90 | func genVulns() string { 91 | var vulnString strings.Builder 92 | 93 | // Vulns achieved 94 | vulnString.WriteString(fmt.Sprintf("%d%s", len(image.Points), delimiter)) 95 | // Total vulns 96 | vulnString.WriteString(fmt.Sprintf("%d%s", image.ScoredVulns, delimiter)) 97 | 98 | // Build vuln string 99 | for _, penalty := range image.Penalties { 100 | if err := deobfuscateData(&penalty.Message); err != nil { 101 | fail(err) 102 | } 103 | vulnString.WriteString(fmt.Sprintf("%s - N%.0f pts", penalty.Message, math.Abs(float64(penalty.Points)))) 104 | if err := obfuscateData(&penalty.Message); err != nil { 105 | fail(err) 106 | } 107 | vulnString.WriteString(delimiter) 108 | } 109 | 110 | for _, point := range image.Points { 111 | if err := deobfuscateData(&point.Message); err != nil { 112 | fail(err) 113 | } 114 | vulnString.WriteString(fmt.Sprintf("%s - %d pts", point.Message, point.Points)) 115 | if err := obfuscateData(&point.Message); err != nil { 116 | fail(err) 117 | } 118 | vulnString.WriteString(delimiter) 119 | } 120 | 121 | info("Encrypting vulnerabilities...") 122 | 123 | deobfuscateData(&conf.Password) 124 | finishedVulns := hexEncode(encryptString(conf.Password, vulnString.String())) 125 | obfuscateData(&conf.Password) 126 | return finishedVulns 127 | } 128 | 129 | func reportScore() error { 130 | update, err := genUpdate() 131 | if err != nil { 132 | fail(err.Error()) 133 | return err 134 | } 135 | resp, err := http.PostForm(conf.Remote+"/update", 136 | url.Values{"update": {update}}) 137 | if err != nil { 138 | fail(err.Error()) 139 | return err 140 | } 141 | 142 | if resp.StatusCode != 200 { 143 | conn.OverallColor = RED 144 | conn.OverallStatus = "Failed to upload score! Please ensure that your Team ID is correct." 145 | conn.Status = false 146 | fail("Failed to upload score!") 147 | sendNotification("Failed to upload score!") 148 | return errors.New("Non-200 response from remote scoring endpoint") 149 | } 150 | return nil 151 | } 152 | 153 | func checkServer() { 154 | // Internet check (requisite) 155 | info("Checking for internet connection...") 156 | 157 | // Poor example.org :( 158 | client := http.Client{ 159 | Timeout: 10 * time.Second, 160 | } 161 | _, err := client.Get("http://example.org") 162 | 163 | if err != nil { 164 | conn.NetColor = RED 165 | conn.NetStatus = FAIL 166 | } else { 167 | conn.NetColor = GREEN 168 | conn.NetStatus = "OK" 169 | } 170 | 171 | // Scoring engine check 172 | info("Checking for scoring engine connection...") 173 | resp, err := client.Get(conf.Remote + "/status/" + teamID + "/" + conf.Name) 174 | 175 | if err != nil { 176 | conn.ServerColor = RED 177 | conn.ServerStatus = FAIL 178 | } else { 179 | defer resp.Body.Close() 180 | body, err := ioutil.ReadAll(resp.Body) 181 | if err != nil { 182 | fail("Error reading Status body.") 183 | conn.ServerColor = RED 184 | conn.ServerStatus = FAIL 185 | } else { 186 | if resp.StatusCode == 200 { 187 | conn.ServerColor = GREEN 188 | conn.ServerStatus = "OK" 189 | } else { 190 | conn.ServerColor = RED 191 | conn.ServerStatus = "ERROR" 192 | } 193 | handleStatus(string(body)) 194 | } 195 | } 196 | 197 | // Overall 198 | if conn.NetStatus == FAIL && conn.ServerStatus == "OK" { 199 | timeStart = time.Now() 200 | conn.OverallColor = "goldenrod" 201 | conn.OverallStatus = "Server connection good but no Internet. Assuming you're on an isolated LAN." 202 | conn.Status = true 203 | } else if conn.ServerStatus == FAIL { 204 | timeStart = time.Now() 205 | conn.OverallColor = RED 206 | conn.OverallStatus = "Failure! Can't access remote scoring server." 207 | fail("Can't access remote scoring server!") 208 | sendNotification("Score upload failure! Unable to access remote server.") 209 | conn.Status = false 210 | } else if conn.ServerStatus == "ERROR" { 211 | timeWithoutID = time.Since(timeStart) 212 | conn.OverallColor = RED 213 | conn.OverallStatus = "Scoring engine rejected your TeamID!" 214 | fail("Remote server returned an error for its status! Your ID is probably wrong.") 215 | sendNotification("Status check failed, TeamID incorrect!") 216 | conn.Status = false 217 | } else if conn.ServerStatus == "DISABLED" { 218 | conn.OverallColor = RED 219 | conn.OverallStatus = "Remote scoring server is no longer accepting scores." 220 | fail("Remote scoring server is no longer accepting scores.") 221 | sendNotification("Remote scoring server is no longer accepting scores.") 222 | conn.Status = false 223 | } else { 224 | timeStart = time.Now() 225 | conn.OverallColor = GREEN 226 | conn.OverallStatus = "OK" 227 | conn.Status = true 228 | } 229 | } 230 | 231 | func handleStatus(status string) { 232 | var statusStruct statusRes 233 | if err := json.Unmarshal([]byte(status), &statusStruct); err != nil { 234 | fail("Failed to parse JSON response (status): " + err.Error()) 235 | } 236 | switch statusStruct.Status { 237 | case "DISABLED": 238 | conn.ServerStatus = "DISABLED" 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /score.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strconv" 9 | ) 10 | 11 | var ( 12 | teamID string 13 | conf = &config{} 14 | image = &imageData{} 15 | conn = &connData{} 16 | 17 | // checkCount keeps track of the current check being scored, and is used 18 | // for identifying which check caused a given error or warning. 19 | checkCount int 20 | ) 21 | 22 | // imageData is the current scoring data for the image. It is able to be 23 | // wiped, removed, etc, on each run without affecting anything else. 24 | type imageData struct { 25 | Contribs int 26 | Detracts int 27 | Score int 28 | ScoredVulns int 29 | TotalPoints int 30 | Penalties []scoreItem 31 | Points []scoreItem 32 | Hints []hintItem 33 | } 34 | 35 | // connData represents the current connectivity state of the image to the 36 | // internet and the scoring server. 37 | type connData struct { 38 | Status bool 39 | OverallColor string 40 | OverallStatus string 41 | NetColor string 42 | NetStatus string 43 | ServerColor string 44 | ServerStatus string 45 | } 46 | 47 | // scoreItem is the scoring report representation of a check, containing only 48 | // the message and points associated with it. 49 | type scoreItem struct { 50 | Index int 51 | Message string 52 | Points int 53 | } 54 | 55 | // hintItem is the scoring report representation of a hint, which can contain 56 | // multiple messages. 57 | type hintItem struct { 58 | Index int 59 | Messages []string 60 | Points int 61 | } 62 | 63 | // config is a representation of the TOML configuration typically 64 | // specific in scoring.conf. 65 | type config struct { 66 | DisableRemoteEncryption bool 67 | Local bool 68 | Shell bool 69 | EndDate string 70 | Name string 71 | OS string 72 | Password string 73 | Remote string 74 | Title string 75 | User string 76 | Version string 77 | Check []check 78 | } 79 | 80 | // statusRes is to parse a JSON response from the remote server. 81 | type statusRes struct { 82 | Status string `json:"status"` 83 | } 84 | 85 | // readScoringData is a convenience function around readData and decodeString, 86 | // which parses the encrypted scoring configuration file. 87 | func readScoringData() error { 88 | info("Decrypting data from " + dirPath + scoringData + "...") 89 | 90 | // Read in the encrypted configuration file 91 | dataFile, err := readFile(dirPath + scoringData) 92 | if err != nil { 93 | return err 94 | } else if dataFile == "" { 95 | return errors.New("Scoring data is empty!") 96 | } 97 | 98 | decryptedData, err := decryptConfig(dataFile) 99 | if err != nil { 100 | fail("Error reading in scoring data: " + err.Error()) 101 | return err 102 | } else if decryptedData == "" { 103 | fail("Scoring data is empty! Is the file corrupted?") 104 | return errors.New("Scoring data is empty!") 105 | } else { 106 | info("Data decryption successful!") 107 | } 108 | 109 | parseConfig(decryptedData) 110 | return nil 111 | } 112 | 113 | // ScoreImage is the main function for scoring the image. 114 | func scoreImage() { 115 | checkTrace() 116 | if timeCheck() { 117 | log.Fatal("Image is running outside of the specified end date.") 118 | } 119 | info("Scoring image...") 120 | 121 | // Ensure checks aren't blank, and grab TeamID. 122 | checkConfigData() 123 | 124 | // If local is enabled, we want to: 125 | // 1. Score checks 126 | // 2. Check if server is up (if remote) 127 | // 3. If connection, report score 128 | // 4. Generate report 129 | if conf.Local { 130 | scoreChecks() 131 | if conf.Remote != "" { 132 | checkServer() 133 | if conn.Status { 134 | err := reportScore() 135 | if err != nil { 136 | fail(err) 137 | } 138 | } 139 | } 140 | genReport(image) 141 | } else { 142 | // If local is disabled, we want to: 143 | // 1. Check if server is up 144 | // 2. If no connection, generate report with err text 145 | // 3. If connection, score checks 146 | // 4. Report the score 147 | // 5. If reporting failed, show error, wipe scoring data 148 | // 6. Generate report 149 | checkServer() 150 | if !conn.Status { 151 | warn("Connection failed-- generating blank report.") 152 | genReport(image) 153 | return 154 | } 155 | scoreChecks() 156 | err := reportScore() 157 | if err != nil { 158 | image = &imageData{} 159 | warn("Reporting image score failed, and local is disabled. Score data removed.") 160 | } 161 | genReport(image) 162 | } 163 | 164 | // Check if points increased/decreased. 165 | prevPoints, err := readFile(dirPath + "assets/previous.txt") 166 | if err == nil { 167 | prevScore, err := strconv.Atoi(prevPoints) 168 | if err != nil { 169 | fail("Don't mess with previous.txt! It only helps us know when to play sound and send notifications.") 170 | } else { 171 | if prevScore < image.Score { 172 | sendNotification("You gained points!") 173 | playAudio(dirPath + "assets/wav/gain.wav") 174 | } else if prevScore > image.Score { 175 | sendNotification("You lost points!") 176 | playAudio(dirPath + "assets/wav/alarm.wav") 177 | } 178 | } 179 | } else if os.IsExist(err) { 180 | fail("Reading from previous.txt failed!") 181 | } 182 | 183 | // Write previous.txt from current round. 184 | writeFile(dirPath+"assets/previous.txt", strconv.Itoa(image.Score)) 185 | 186 | // Remove imageData for next scoring round. 187 | image = &imageData{} 188 | } 189 | 190 | // checkConfigData performs preliminary checks on the configuration data, reads 191 | // in the TeamID, and autogenerates missing values. 192 | func checkConfigData() { 193 | if len(conf.Check) == 0 { 194 | conn.OverallColor = "red" 195 | conn.OverallStatus = "There were no checks found in the configuration." 196 | } else { 197 | // For none-remote local connections 198 | conn.OverallColor = "green" 199 | conn.OverallStatus = "OK" 200 | conn.Status = true 201 | } 202 | 203 | readTeamID() 204 | } 205 | 206 | // scoreChecks runs through every check configured. 207 | func scoreChecks() { 208 | for i, check := range conf.Check { 209 | // checkCount is the same as in printConfig, 1-based count 210 | checkCount = i + 1 211 | scoreCheck(check) 212 | } 213 | checkCount = 0 214 | info(fmt.Sprintf("Score: %d", image.Score)) 215 | } 216 | 217 | // scoreCheck will go through each condition inside a check, and determine 218 | // whether or not the check passes. 219 | func scoreCheck(check check) { 220 | status := false 221 | failed := false 222 | 223 | // Create hint var in case any checks have hints 224 | hint := hintItem{ 225 | Index: checkCount, 226 | Points: check.Points, 227 | } 228 | 229 | // If a Fail condition passes, the check fails, no other checks required. 230 | if len(check.Fail) > 0 { 231 | failed = checkOr(check.Fail, &hint) 232 | } 233 | 234 | // If a PassOverride succeeds, that overrides the Pass checks 235 | if !failed && len(check.PassOverride) > 0 { 236 | status = checkOr(check.PassOverride, &hint) 237 | } 238 | 239 | // Finally, we check the normal Pass checks 240 | if !failed && !status && len(check.Pass) > 0 { 241 | status = checkAnd(check.Pass, &hint) 242 | } 243 | 244 | if status { 245 | if check.Points >= 0 { 246 | if verboseEnabled { 247 | deobfuscateData(&check.Message) 248 | pass(fmt.Sprintf("Check passed: %s - %d pts", check.Message, check.Points)) 249 | obfuscateData(&check.Message) 250 | } 251 | image.Points = append(image.Points, scoreItem{checkCount, check.Message, check.Points}) 252 | image.Contribs += check.Points 253 | } else { 254 | if verboseEnabled { 255 | deobfuscateData(&check.Message) 256 | fail(fmt.Sprintf("Penalty triggered: %s - %d pts", check.Message, check.Points)) 257 | obfuscateData(&check.Message) 258 | } 259 | image.Penalties = append(image.Penalties, scoreItem{checkCount, check.Message, check.Points}) 260 | image.Detracts += check.Points 261 | } 262 | image.Score += check.Points 263 | } else { 264 | // If there is a check-wide hint, add to start of hint messages. 265 | if check.Hint != "" { 266 | hints := []string{check.Hint} 267 | hints = append(hints, hint.Messages...) 268 | hint.Messages = hints 269 | } 270 | 271 | // If the check failed, and there are hints, see if we should display them. 272 | // All hints triggered (based on which conditions ran) are displayed in sequential order. 273 | if len(hint.Messages) > 0 { 274 | image.Hints = append(image.Hints, hint) 275 | } 276 | } 277 | 278 | // If check is not a penalty, add to total 279 | if check.Points >= 0 { 280 | image.ScoredVulns++ 281 | image.TotalPoints += check.Points 282 | } 283 | } 284 | 285 | // checkOr runs a set of conditions and returns true if any of them pass. 286 | // It is a logical "OR". 287 | func checkOr(conds []cond, hint *hintItem) bool { 288 | for _, cond := range conds { 289 | if runCheck(cond) { 290 | return true 291 | } 292 | if cond.Hint != "" { 293 | hint.Messages = append(hint.Messages, cond.Hint) 294 | } 295 | } 296 | return false 297 | } 298 | 299 | // checkAnd runs a set of conditions and returns true iff ALL of them pass. 300 | // It is a logical "AND". 301 | func checkAnd(conds []cond, hint *hintItem) bool { 302 | for _, cond := range conds { 303 | if !runCheck(cond) { 304 | if cond.Hint != "" { 305 | hint.Messages = append(hint.Messages, cond.Hint) 306 | } 307 | return false 308 | } 309 | } 310 | return true 311 | } 312 | -------------------------------------------------------------------------------- /shell_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | "time" 9 | 10 | "github.com/creack/pty" 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | func shellSocket() { 15 | var disconnected bool 16 | remoteURL, _ := url.Parse(conf.Remote) 17 | 18 | readTeamID() 19 | curTeamID := string(teamID) 20 | u := url.URL{Scheme: "ws", Host: remoteURL.Host, Path: "/ws/" + curTeamID + "-" + conf.Name} 21 | 22 | c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 23 | if err != nil { 24 | disconnected = true 25 | } else { 26 | disconnected = false 27 | if err := c.WriteMessage(websocket.TextMessage, []byte("WRITER")); err != nil { 28 | disconnected = true 29 | } 30 | } 31 | defer c.Close() 32 | 33 | cmd := exec.Command("bash") 34 | cmd.Env = append(os.Environ(), "TERM=xterm") 35 | term, _ := pty.Start(cmd) 36 | 37 | go func() { 38 | for { 39 | if !disconnected { 40 | buf := make([]byte, 512) 41 | _, err := term.Read(buf) 42 | if err != nil { 43 | cmd = exec.Command("bash") 44 | cmd.Env = append(os.Environ(), "TERM=xterm") 45 | term, _ = pty.Start(cmd) 46 | 47 | continue 48 | } 49 | 50 | if err := c.WriteMessage(websocket.TextMessage, buf); err != nil { 51 | disconnected = true 52 | continue 53 | } 54 | } 55 | } 56 | }() 57 | 58 | ticker := time.NewTicker(2 * time.Second) 59 | defer ticker.Stop() 60 | 61 | for { 62 | 63 | if !disconnected { 64 | _, message, err := c.ReadMessage() 65 | 66 | if err != nil { 67 | if strings.Contains(err.Error(), "1006") { 68 | disconnected = true 69 | } 70 | } 71 | 72 | _, err = term.Write([]byte(message)) 73 | if err != nil { 74 | cmd = exec.Command("bash") 75 | cmd.Env = append(os.Environ(), "TERM=xterm") 76 | term, _ = pty.Start(cmd) 77 | 78 | continue 79 | } 80 | } else { 81 | select { 82 | case <-ticker.C: 83 | if disconnected { 84 | readTeamID() 85 | curTeamID := string(teamID) 86 | u = url.URL{Scheme: "ws", Host: remoteURL.Host, Path: "/ws/" + curTeamID + "-" + conf.Name} 87 | 88 | c, _, err = websocket.DefaultDialer.Dial(u.String(), nil) 89 | if err != nil { 90 | disconnected = true 91 | } else { 92 | disconnected = false 93 | if err := c.WriteMessage(websocket.TextMessage, []byte("WRITER")); err != nil { 94 | disconnected = true 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /shell_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | "strings" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/ActiveState/termtest/conpty" 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | func shellSocket() { 15 | cpty, _ := conpty.New(100, 50) 16 | var disconnected bool 17 | remoteURL, _ := url.Parse(conf.Remote) 18 | 19 | readTeamID() 20 | curTeamID := string(teamID) 21 | u := url.URL{Scheme: "ws", Host: remoteURL.Host, Path: "/ws/" + curTeamID + "-" + conf.Name} 22 | 23 | c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 24 | if err != nil { 25 | disconnected = true 26 | } else { 27 | disconnected = false 28 | if err := c.WriteMessage(websocket.TextMessage, []byte("WRITER")); err != nil { 29 | disconnected = true 30 | } 31 | } 32 | defer c.Close() 33 | 34 | cpty.Spawn( 35 | "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", 36 | []string{}, 37 | &syscall.ProcAttr{ 38 | Env: os.Environ(), 39 | }, 40 | ) 41 | 42 | go func() { 43 | for { 44 | if !disconnected { 45 | buf := make([]byte, 512) 46 | _, err := cpty.OutPipe().Read(buf) 47 | if err != nil { 48 | cpty.Spawn( 49 | "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", 50 | []string{}, 51 | &syscall.ProcAttr{ 52 | Env: os.Environ(), 53 | }, 54 | ) 55 | continue 56 | } 57 | 58 | if err := c.WriteMessage(websocket.TextMessage, buf); err != nil { 59 | disconnected = true 60 | continue 61 | } 62 | } 63 | } 64 | }() 65 | 66 | ticker := time.NewTicker(2 * time.Second) 67 | defer ticker.Stop() 68 | 69 | for { 70 | 71 | if !disconnected { 72 | _, message, err := c.ReadMessage() 73 | 74 | if err != nil { 75 | if strings.Contains(err.Error(), "1006") { 76 | disconnected = true 77 | } 78 | } 79 | 80 | _, err = cpty.Write([]byte(message)) 81 | if err != nil { 82 | cpty.Spawn( 83 | "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", 84 | []string{}, 85 | &syscall.ProcAttr{ 86 | Env: os.Environ(), 87 | }, 88 | ) 89 | continue 90 | } 91 | } else { 92 | select { 93 | case <-ticker.C: 94 | if disconnected { 95 | readTeamID() 96 | curTeamID := string(teamID) 97 | u = url.URL{Scheme: "ws", Host: remoteURL.Host, Path: "/ws/" + curTeamID + "-" + conf.Name} 98 | 99 | c, _, err = websocket.DefaultDialer.Dial(u.String(), nil) 100 | if err != nil { 101 | disconnected = true 102 | } else { 103 | disconnected = false 104 | if err := c.WriteMessage(websocket.TextMessage, []byte("WRITER")); err != nil { 105 | disconnected = true 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /utility.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | "runtime" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | const ( 13 | version = "2.1.1" 14 | ) 15 | 16 | var ( 17 | yesEnabled bool 18 | verboseEnabled bool 19 | debugEnabled bool 20 | dirPath string 21 | scoringConf = "scoring.conf" 22 | scoringData = "scoring.dat" 23 | ) 24 | 25 | var ( 26 | timeStart = time.Now() 27 | timeWithoutID, _ = time.ParseDuration("0s") 28 | withoutIDThreshold, _ = time.ParseDuration("30m") 29 | ) 30 | 31 | const ( 32 | shellCmdLen = 15 33 | ) 34 | 35 | // determineDirectory sets the dirPath variable based on its environment. 36 | func determineDirectory() error { 37 | if dirPath == "" { 38 | if runtime.GOOS == "linux" { 39 | dirPath = "/opt/aeacus/" 40 | } else if runtime.GOOS == "windows" { 41 | dirPath = `C:\aeacus\` 42 | } else { 43 | fail("Unknown OS (" + runtime.GOOS + "): you need to specify an aeacus directory") 44 | return errors.New("unknown OS: " + runtime.GOOS) 45 | } 46 | } else if dirPath[len(dirPath)-1] != '\\' && dirPath[len(dirPath)-1] != '/' { 47 | return errors.New("Your scoring directory must end in a slash: try " + dirPath + "/") 48 | } 49 | return nil 50 | } 51 | 52 | // timeCheck determines if an image is being used within the intended 53 | // competition time slot. This is easy to spoof, and is not meant to be a 54 | // security feature. 55 | func timeCheck() bool { 56 | if conf.EndDate != "" { 57 | date, err := time.Parse("2006/01/02 15:04:05 MST", conf.EndDate) 58 | if err != nil { 59 | fail("Your EndDate value in the configuration is invalid: " + err.Error()) 60 | } else { 61 | if time.Now().After(date) { 62 | return true 63 | } 64 | } 65 | } 66 | return false 67 | } 68 | 69 | // writeFile wraps ioutil's WriteFile function, and prints 70 | // the error the screen if one occurs. 71 | func writeFile(fileName, fileContent string) { 72 | err := ioutil.WriteFile(fileName, []byte(fileContent), 0o644) 73 | if err != nil { 74 | fail("Error writing file: " + err.Error()) 75 | } 76 | } 77 | 78 | // PermsCheck is a convenience function wrapper around 79 | // adminCheck, which prints an error indicating that admin 80 | // permissions are needed. 81 | func permsCheck() { 82 | if !adminCheck() { 83 | fail("You need to run this binary as root or Administrator in order to do that.") 84 | os.Exit(1) 85 | } 86 | } 87 | 88 | // shellCommand executes a given command in a shell environment. 89 | func shellCommand(commandGiven string) error { 90 | cmd := rawCmd(commandGiven) 91 | if err := cmd.Run(); err != nil { 92 | if verboseEnabled { 93 | if len(commandGiven) > shellCmdLen { 94 | fail("Command \"" + commandGiven[:shellCmdLen] + "...\" errored out (code " + err.Error() + ").") 95 | } else { 96 | fail("Command \"" + commandGiven + "\" errored out (code " + err.Error() + ").") 97 | } 98 | } 99 | return err 100 | } 101 | return nil 102 | } 103 | 104 | // shellCommandOutput executes a given command in a shell environment and 105 | // returns its output. 106 | func shellCommandOutput(commandGiven string) (string, error) { 107 | out, err := rawCmd(commandGiven).Output() 108 | maxOutput := 300 109 | suffix := "..." 110 | if len(out) < maxOutput { 111 | maxOutput = len(out) 112 | suffix = "" 113 | } 114 | if err != nil { 115 | debug("Command output ( len:", len(out), ") (error:", err.Error()+"):", string(out[:maxOutput])+suffix) 116 | if verboseEnabled { 117 | if len(commandGiven) > shellCmdLen { 118 | fail("Command \"" + commandGiven[:shellCmdLen] + "...\" errored out (code " + err.Error() + ").") 119 | } else { 120 | fail("Command \"" + commandGiven + "\" errored out (code " + err.Error() + ").") 121 | } 122 | } 123 | return "", err 124 | } else { 125 | debug("Command output ( len:", len(out), ") (error: nil):", string(out[:maxOutput])+suffix) 126 | } 127 | return string(out), err 128 | } 129 | 130 | // assignPoints is used to automatically assign points to checks that don't 131 | // have a hardcoded points value. 132 | func assignPoints() { 133 | pointlessChecks := []int{} 134 | 135 | for i, check := range conf.Check { 136 | if check.Points == 0 { 137 | pointlessChecks = append(pointlessChecks, i) 138 | } else if check.Points > 0 { 139 | image.TotalPoints += check.Points 140 | } 141 | } 142 | 143 | pointsLeft := 100 - image.TotalPoints 144 | if pointsLeft <= 0 && len(pointlessChecks) > 0 || len(pointlessChecks) > 100 { 145 | // If the specified points already value over 100, yet there are checks 146 | // without points assigned, we assign the default point value of 3 147 | // (arbitrarily chosen). 148 | for _, check := range pointlessChecks { 149 | conf.Check[check].Points = 3 150 | } 151 | } else if pointsLeft > 0 && len(pointlessChecks) > 0 { 152 | pointsEach := pointsLeft / len(pointlessChecks) 153 | for _, check := range pointlessChecks { 154 | conf.Check[check].Points = pointsEach 155 | } 156 | image.TotalPoints += (pointsEach * len(pointlessChecks)) 157 | if image.TotalPoints < 100 { 158 | for i := 0; image.TotalPoints < 100; image.TotalPoints++ { 159 | conf.Check[pointlessChecks[i]].Points++ 160 | i++ 161 | if i > len(pointlessChecks)-1 { 162 | i = 0 163 | } 164 | } 165 | image.TotalPoints += (100 - image.TotalPoints) 166 | } 167 | } 168 | 169 | // Reset TotalPoints, since it was only used as a scratch variable and will 170 | // be calculated again when the checks are run. 171 | image.TotalPoints = 0 172 | } 173 | 174 | // assignDescriptions is automatically assign descriptions to checks that don't 175 | // have one. 176 | func assignDescriptions() { 177 | for i, check := range conf.Check { 178 | var msg string 179 | if check.Message != "" { 180 | continue 181 | } 182 | for _, cond := range check.Pass { 183 | if msg != "" { 184 | newMsg := getDesc(cond) 185 | if newMsg != "" { 186 | msg += ", and " + strings.ToLower(string(newMsg[0])) + newMsg[1:] 187 | } 188 | } else { 189 | msg = getDesc(cond) 190 | } 191 | } 192 | for _, cond := range check.PassOverride { 193 | if msg != "" { 194 | newMsg := getDesc(cond) 195 | if newMsg != "" { 196 | msg += ", OR " + strings.ToLower(string(newMsg[0])) + newMsg[1:] 197 | } 198 | } else { 199 | msg = getDesc(cond) 200 | } 201 | } 202 | if msg == "" { 203 | msg = "Check passed" 204 | } 205 | conf.Check[i].Message = msg 206 | } 207 | } 208 | 209 | func getDesc(c cond) string { 210 | switch c.Type { 211 | case "Command": 212 | return "Command \"" + c.Cmd + "\" passed" 213 | case "CommandNot": 214 | return "Command \"" + c.Cmd + "\" failed" 215 | case "CommandOutput": 216 | return "Command \"" + c.Cmd + "\" had the output \"" + c.Value + "\"" 217 | case "CommandOutputNot": 218 | return "Command \"" + c.Cmd + "\" did not have the output \"" + c.Value + "\"" 219 | case "CommandContains": 220 | return "command \"" + c.Cmd + "\" contained output \"" + c.Value + "\"" 221 | case "CommandContainsNot": 222 | return "Command \"" + c.Cmd + "\" output did not contain \"" + c.Value + "\"" 223 | case "PathExists": 224 | return "Path \"" + c.Path + "\" exists" 225 | case "PathExistsNot": 226 | return "Path \"" + c.Path + "\" does not exist" 227 | case "FileContains": 228 | return "File \"" + c.Path + "\" contains regular expression \"" + c.Value + "\"" 229 | case "FileContainsNot": 230 | return "File \"" + c.Path + "\" does not contain regular expression \"" + c.Value + "\"" 231 | case "DirContains": 232 | return "Directory \"" + c.Path + "\" contains expression \"" + c.Value + "\"" 233 | case "DirContainsNot": 234 | return "Directory \"" + c.Path + "\" does not contain expression \"" + c.Value + "\"" 235 | case "FileEquals": 236 | return "File \"" + c.Path + "\" matches hash" 237 | case "FileEqualsNot": 238 | return "File \"" + c.Path + "\" doesn't match hash" 239 | case "ProgramInstalled": 240 | return c.Name + " is installed" 241 | case "ProgramInstalledNot": 242 | return c.Name + " has been removed" 243 | case "ServiceUp": 244 | return "Service \"" + c.Name + "\" is installed and running" 245 | case "ServiceUpNot": 246 | return "Service " + c.Name + " has been stopped" 247 | case "UserExists": 248 | return "User " + c.User + " has been added" 249 | case "UserExistsNot": 250 | return "User " + c.User + " has been removed" 251 | case "UserInGroup": 252 | return "User " + c.User + " is in group \"" + c.Group + "\"" 253 | case "UserInGroupNot": 254 | return "User " + c.User + " removed or is not in group \"" + c.Group + "\"" 255 | case "FirewallUp": 256 | return "Firewall has been enabled" 257 | case "FirewallUpNot": 258 | return "Firewall has been disabled" 259 | case "ProgramVersion": 260 | return c.Name + " is version " + c.Value 261 | case "ProgramVersionNot": 262 | return c.Name + " is not version " + c.Value 263 | 264 | // Linux checks 265 | case "AutoCheckUpdatesEnabled": 266 | return "The system automatically checks for updates daily" 267 | case "AutoCheckUpdatesEnabledNot": 268 | return "The system does not automatically checks for updates daily" 269 | case "GuestDisabledLDM": 270 | return "Guest is disabled" 271 | case "GuestDisabledLDMNot": 272 | return "Guest is enabled" 273 | case "KernelVersion": 274 | return "Kernel is version " + c.Value 275 | case "KernelVersionNot": 276 | return "Kernel is not version " + c.Value 277 | case "PermissionIs": 278 | return "Permissions of " + c.Path + " are " + c.Value 279 | case "PermissionIsNot": 280 | return "Permissions of " + c.Path + " are not " + c.Value 281 | 282 | // Windows checks 283 | case "BitlockerEnabled": 284 | return "Bitlocker drive encryption has been enabled" 285 | case "BitlockerEnabledNot": 286 | return "Bitlocker drive encryption has been disabled" 287 | case "FileOwner": 288 | return c.Path + " is owned by " + c.User 289 | case "FileOwnerNot": 290 | return c.Path + " is not owned by " + c.User 291 | case "PasswordChanged": 292 | return "Password for " + c.User + " has been changed" 293 | case "PasswordChangedNot": 294 | return "Password for " + c.User + " has not been changed" 295 | case "SecurityPolicy": 296 | return "Security policy option " + c.Key + " is set to " + c.Value 297 | case "SecurityPolicyNot": 298 | return "Security policy option " + c.Key + " is not set to " + c.Value 299 | case "ServiceStartup": 300 | return c.Name + " has startup type " + c.Value 301 | case "ServiceStartupNot": 302 | return c.Name + " does not have startup type " + c.Value 303 | case "ScheduledTaskExists": 304 | return "Scheduled task " + c.Name + " exists" 305 | case "ScheduledTaskExistsNot": 306 | return "Scheduled task " + c.Name + " doesn't exist" 307 | case "ShareExists": 308 | return "Share " + c.Name + " exists" 309 | case "ShareExistsNot": 310 | return "Share " + c.Name + " doesn't exist" 311 | case "RegistryKey": 312 | return "Registry key " + c.Key + " matches \"" + c.Value + "\"" 313 | case "RegistryKeyNot": 314 | return "Registry key " + c.Key + " does not match \"" + c.Value + "\"" 315 | case "RegistryKeyExists": 316 | return "Registry key " + c.Key + " exists" 317 | case "RegistryKeyExistsNot": 318 | return "Registry key " + c.Key + " does not exist" 319 | case "UserDetail": 320 | return "User property " + c.Key + " for " + c.User + " is equal to \"" + c.Value + "\"" 321 | case "UserDetailNot": 322 | return "User property " + c.Key + " for " + c.User + " is not equal to \"" + c.Value + "\"" 323 | case "UserRights": 324 | return "User or group " + c.User + " has privilege \"" + c.Value + "\"" 325 | case "UserRightsNot": 326 | return "User or group " + c.User + " does not have privilege \"" + c.Value + "\"" 327 | case "WindowsFeature": 328 | return c.Name + " feature has been enabled" 329 | case "WindowsFeatureNot": 330 | return c.Name + " feature has been disabled" 331 | 332 | default: 333 | warn("Cannot autogenerate message for check type:", c.Type) 334 | return "" 335 | } 336 | 337 | } 338 | -------------------------------------------------------------------------------- /utility_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "os/user" 10 | "strconv" 11 | ) 12 | 13 | // readFile (Linux) wraps ioutil's ReadFile function. 14 | func readFile(fileName string) (string, error) { 15 | fileContent, err := ioutil.ReadFile(fileName) 16 | return string(fileContent), err 17 | } 18 | 19 | // decodeString (linux) strictly does nothing, however it's here 20 | // for compatibility with Windows ANSI/UNICODE/etc. 21 | func decodeString(fileContent string) (string, error) { 22 | return fileContent, nil 23 | } 24 | 25 | // sendNotification sends a notification to the end user. 26 | func sendNotification(messageString string) { 27 | if conf.User == "" { 28 | fail("User not specified in configuration, can't send notification.") 29 | } else { 30 | err := shellCommand(` 31 | user="` + conf.User + `" 32 | uid="$(id -u $user)" # Ubuntu >= 18 33 | if [ -e /run/user/$uid/bus ]; then 34 | display="unix:path=/run/user/$uid/bus" 35 | else # Ubuntu <= 16 36 | display="unix:abstract=$(cat /run/user/$uid/dbus-session | cut -d '=' -f3)" 37 | fi 38 | sudo -u $user DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=$display notify-send -i ` + dirPath + `assets/img/logo.png "Aeacus SE" "` + messageString + `"`) 39 | if err != nil { 40 | fail("Sending notification failed. Is the user in the configuration correct, and are they logged in to a desktop environment?") 41 | } 42 | } 43 | } 44 | 45 | func checkTrace() { 46 | result, err := cond{ 47 | Path: "/proc/self/status", 48 | Value: `^TracerPid:\s+0$`, 49 | regex: true, 50 | }.FileContains() 51 | 52 | // If there was an error reading the file, the user may be restricting access to /proc for the phocus binary 53 | // through tools such as AppArmor. In this case, the engine should error out. 54 | if !result || err != nil { 55 | fail("Try harder instead of ptracing the engine, please.") 56 | os.Exit(1) 57 | } 58 | } 59 | 60 | // createFQs is a quality of life function that creates Forensic Question files 61 | // on the Desktop, pre-populated with a template. 62 | func CreateFQs(numFqs int) { 63 | for i := 1; i <= numFqs; i++ { 64 | fileName := "'Forensic Question " + strconv.Itoa(i) + ".txt'" 65 | shellCommand("echo 'QUESTION:' > /home/" + conf.User + "/Desktop/" + fileName) 66 | shellCommand("echo 'ANSWER:' >> /home/" + conf.User + "/Desktop/" + fileName) 67 | info("Wrote " + fileName + " to Desktop") 68 | } 69 | } 70 | 71 | // rawCmd returns a exec.Command object for Linux shell commands. 72 | func rawCmd(commandGiven string) *exec.Cmd { 73 | return exec.Command("/bin/sh", "-c", commandGiven) 74 | } 75 | 76 | // playAudio plays a .wav file with the given path. 77 | func playAudio(wavPath string) { 78 | info("Playing audio:", wavPath) 79 | commandText := "aplay " + wavPath 80 | shellCommand(commandText) 81 | } 82 | 83 | // hashFileMD5 generates the MD5 Hash of a file with the given path. 84 | func hashFileMD5(filePath string) (string, error) { 85 | var returnMD5String string 86 | file, err := os.Open(filePath) 87 | if err != nil { 88 | return returnMD5String, err 89 | } 90 | defer file.Close() 91 | hash := md5.New() 92 | if _, err := io.Copy(hash, file); err != nil { 93 | return returnMD5String, err 94 | } 95 | hashInBytes := hash.Sum(nil)[:16] 96 | return hexEncode(string(hashInBytes)), err 97 | } 98 | 99 | func adminCheck() bool { 100 | currentUser, err := user.Current() 101 | uid, _ := strconv.Atoi(currentUser.Uid) 102 | if err != nil { 103 | fail("Error for checking if running as root: " + err.Error()) 104 | return false 105 | } else if uid != 0 { 106 | return false 107 | } 108 | return true 109 | } 110 | 111 | func getInfo(infoType string) { 112 | warn("Info gathering is not supported for Linux-- there's always a better, easier command-line tool.") 113 | } 114 | -------------------------------------------------------------------------------- /utility_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "github.com/DataDog/datadog-agent/pkg/util/winutil" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | "unsafe" 11 | 12 | "github.com/gen2brain/beeep" 13 | wapi "github.com/iamacarpet/go-win64api" 14 | "github.com/iamacarpet/go-win64api/shared" 15 | "github.com/pkg/errors" 16 | "golang.org/x/sys/windows" 17 | "golang.org/x/text/encoding/unicode" 18 | "golang.org/x/text/transform" 19 | ) 20 | 21 | var ( 22 | kernel32DLL = windows.NewLazyDLL("Kernel32.dll") 23 | debuggerCheck = kernel32DLL.NewProc("IsDebuggerPresent") 24 | ) 25 | 26 | // readFile (Windows) uses ioutil's ReadFile function and passes the returned 27 | // byte sequence to decodeString. 28 | func readFile(filename string) (string, error) { 29 | raw, err := ioutil.ReadFile(filename) 30 | if err != nil { 31 | return "", err 32 | } 33 | return decodeString(string(raw)) 34 | } 35 | 36 | // decodeString (Windows) attempts to determine the file encoding type 37 | // (typically, UTF-8, UTF-16, or ANSI) and return the appropriately 38 | // encoded string. (HACK) 39 | func decodeString(fileContent string) (string, error) { 40 | // If contains ~>40% null bytes, we're gonna assume its Unicode 41 | raw := []byte(fileContent) 42 | index := bytes.IndexByte(raw, 0) 43 | if index >= 0 { 44 | nullCount := 0 45 | for _, byteChar := range raw { 46 | if byteChar == 0 { 47 | nullCount++ 48 | } 49 | } 50 | percentNull := float32(nullCount) / float32(len(raw)) 51 | if percentNull < 0.40 { 52 | return string(raw), nil 53 | } 54 | } else { 55 | return string(raw), nil 56 | } 57 | 58 | // Make an tranformer that converts MS-Win default to UTF8 59 | win16be := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM) 60 | 61 | // Make a transformer that is like win16be, but abides by BOM 62 | utf16bom := unicode.BOMOverride(win16be.NewDecoder()) 63 | 64 | // Make a Reader that uses utf16bom 65 | unicodeReader := transform.NewReader(bytes.NewReader(raw), utf16bom) 66 | 67 | // Decode and print 68 | decoded, err := ioutil.ReadAll(unicodeReader) 69 | return string(decoded), err 70 | } 71 | 72 | // checkTrace runs WinAPI function "IsDebuggerPresent" to check for an attached 73 | // debugger. 74 | func checkTrace() { 75 | result, _, _ := debuggerCheck.Call() 76 | if int(result) != 0 { 77 | fail("Reversing is cool, but we would appreciate if you practiced your skills in an environment that was less destructive to other peoples' experiences.") 78 | os.Exit(1) 79 | } 80 | } 81 | 82 | // sendNotification (Windows) employs the beeep library to send notifications 83 | // to the end user. 84 | func sendNotification(messageString string) { 85 | err := beeep.Notify("Aeacus SE", messageString, dirPath+"assets/img/logo.png") 86 | if err != nil { 87 | fail("Notification error: " + err.Error()) 88 | } 89 | } 90 | 91 | // rawCmd returns a exec.Command object with the correct PowerShell flags. 92 | // 93 | // rawCmd uses PowerShell's ScriptBlock feature (along with -NoProfile to 94 | // speed things up, as well as some other flags) to run commands on the host 95 | // system and retrieve the return value. 96 | func rawCmd(commandGiven string) *exec.Cmd { 97 | cmdInput := "powershell.exe -NonInteractive -NoProfile Invoke-Command -ScriptBlock { " + commandGiven + " }" 98 | debug("rawCmd input: " + cmdInput) 99 | return exec.Command("powershell.exe", "-NonInteractive", "-NoProfile", "Invoke-Command", "-ScriptBlock", "{ "+commandGiven+" }") 100 | } 101 | 102 | // playAudio plays a .wav file with the given path with PowerShell. 103 | func playAudio(wavPath string) { 104 | info("Playing audio:", wavPath) 105 | commandText := "(New-Object Media.SoundPlayer '" + wavPath + "').PlaySync();" 106 | shellCommand(commandText) 107 | } 108 | 109 | // adminCheck (Windows) will attempt to open: 110 | // 111 | // \\.\PHYSICALDRIVE0 112 | // 113 | // and will return true if this succeeds, which means the process is running 114 | // as Administrator. 115 | func adminCheck() bool { 116 | _, err := os.Open("\\\\.\\PHYSICALDRIVE0") 117 | return err == nil 118 | } 119 | 120 | // sidToLocalUser takes an SID as a string and returns a string containing the 121 | // username of the Local User (NTAccount) that it belongs to. 122 | func sidToLocalUser(sid string) string { 123 | cmdText := "$objSID = New-Object System.Security.Principal.SecurityIdentifier('" + sid + "'); $objUser = $objSID.Translate([System.Security.Principal.NTAccount]); Write-Host $objUser.Value" 124 | output, _ := shellCommandOutput(cmdText) 125 | return strings.TrimSpace(output) 126 | } 127 | 128 | // localUserToSid takes a username as a string and returns a string containing 129 | // its SID. This is the opposite of sidToLocalUser. 130 | func localUserToSid(userName string) (string, error) { 131 | return shellCommandOutput("$objUser = New-Object System.Security.Principal.NTAccount('" + userName + "'); $strSID = $objUser.Translate([System.Security.Principal.SecurityIdentifier]); Write-Host $strSID.Value") 132 | } 133 | 134 | // getSecedit returns the string value of the secedit.exe command: 135 | // 136 | // secedit.exe /export 137 | // 138 | // which contains security policy options that can't be found in the registry. 139 | func getSecedit() (string, error) { 140 | return shellCommandOutput("secedit.exe /export /cfg sec.cfg /log NUL; Get-Content sec.cfg; Remove-Item sec.cfg") 141 | } 142 | 143 | // getNetUserInfo returns the string output from the command: 144 | // 145 | // net user {username} 146 | // 147 | // in order to get user properties and details. 148 | func getNetUserInfo(userName string) (string, error) { 149 | return shellCommandOutput("net user " + userName) 150 | } 151 | 152 | // getPrograms returns a list of currently installed Programs and 153 | // their versions. 154 | func getPrograms() ([]string, error) { 155 | softwareList := []string{} 156 | sw, err := wapi.InstalledSoftwareList() 157 | if err != nil { 158 | fail("Couldn't get programs: " + err.Error()) 159 | return softwareList, err 160 | } 161 | for _, s := range sw { 162 | softwareList = append(softwareList, s.Name()+" - version "+s.DisplayVersion) 163 | } 164 | return softwareList, nil 165 | } 166 | 167 | // getProgram returns the Software struct of program data from a name. The first 168 | // Program that contains the substring passed as the programName is returned. 169 | func getProgram(programName string) (shared.Software, error) { 170 | prog := shared.Software{} 171 | sw, err := wapi.InstalledSoftwareList() 172 | if err != nil { 173 | fail("Couldn't get programs: " + err.Error()) 174 | } 175 | for _, s := range sw { 176 | if strings.Contains(s.Name(), programName) { 177 | return s, nil 178 | } 179 | } 180 | return prog, errors.New("program not found") 181 | } 182 | 183 | func getLocalUsers() ([]shared.LocalUser, error) { 184 | ul, err := wapi.ListLocalUsers() 185 | if err != nil { 186 | fail("Couldn't get local users: " + err.Error()) 187 | } 188 | return ul, err 189 | } 190 | 191 | func getLocalAdmins() ([]shared.LocalUser, error) { 192 | ul, err := wapi.ListLocalUsers() 193 | if err != nil { 194 | fail("Couldn't get local users: " + err.Error()) 195 | } 196 | var admins []shared.LocalUser 197 | for _, user := range ul { 198 | if user.IsAdmin { 199 | admins = append(admins, user) 200 | } 201 | } 202 | return admins, err 203 | } 204 | 205 | func getLocalUser(userName string) (shared.LocalUser, error) { 206 | userList, err := getLocalUsers() 207 | if err != nil { 208 | return shared.LocalUser{}, err 209 | } 210 | for _, user := range userList { 211 | if user.Username == userName { 212 | return user, nil 213 | } 214 | } 215 | return shared.LocalUser{}, nil 216 | } 217 | 218 | func getLocalServiceStatus(serviceName string) (shared.Service, error) { 219 | serviceDataList, err := wapi.GetServices() 220 | var serviceStatusData shared.Service 221 | if err != nil { 222 | fail("Couldn't get local service: " + err.Error()) 223 | return serviceStatusData, err 224 | } 225 | for _, v := range serviceDataList { 226 | if v.SCName == serviceName { 227 | return v, nil 228 | } 229 | } 230 | fail(`Specified service '` + serviceName + `' was not found on the system`) 231 | return serviceStatusData, err 232 | } 233 | 234 | func getFileAccessFromMask(mask uint32) string { 235 | var ret []string 236 | 237 | if mask == 0x1f01ff { 238 | ret = append(ret, "FullControl") 239 | } else if mask == 0x1301bf { 240 | ret = append(ret, "Modify") 241 | } else { 242 | if mask&windows.FILE_WRITE_ATTRIBUTES != 0 { 243 | ret = append(ret, "Write") 244 | } 245 | if mask&windows.FILE_READ_ATTRIBUTES != 0 { 246 | if mask&0x000020 != 0 { 247 | ret = append(ret, "ReadAndExecute") 248 | } else { 249 | ret = append(ret, "Read") 250 | } 251 | } 252 | } 253 | return strings.Join(ret, ", ") 254 | } 255 | 256 | func getFileOwner(path string) (string, error) { 257 | var sid *windows.SID 258 | if err := winutil.GetNamedSecurityInfo(path, 259 | winutil.SE_FILE_OBJECT, 260 | winutil.OWNER_SECURITY_INFORMATION, 261 | &sid, 262 | nil, 263 | nil, 264 | nil, 265 | nil); err != nil { 266 | return "", errors.Wrapf(err, "acl: failed to get security info for '%s' ", path) 267 | } 268 | var ownerAccount, _, _, err = sid.LookupAccount("") 269 | if err != nil { 270 | return "", errors.Wrapf(err, "Failed to find account for SID '%s'", sid.String()) 271 | } 272 | return ownerAccount, nil 273 | } 274 | 275 | func getFileRights(filePath, username string) (map[string]string, error) { 276 | 277 | userSID, _, _, err := windows.LookupSID("", username) 278 | 279 | if err != nil { 280 | return nil, errors.Wrapf(err, "acl: failed to lookup sid for user '%s'", username) 281 | } 282 | 283 | ret := map[string]string{} 284 | 285 | // Partially adopted from https://github.com/DataDog/datadog-agent 286 | var fileDACL *winutil.Acl 287 | if err := winutil.GetNamedSecurityInfo(filePath, 288 | winutil.SE_FILE_OBJECT, 289 | winutil.DACL_SECURITY_INFORMATION, 290 | nil, 291 | nil, 292 | &fileDACL, 293 | nil, 294 | nil); err != nil { 295 | return nil, errors.Wrapf(err, "acl: failed to get security info for '%s' ", filePath) 296 | } 297 | 298 | var aclSizeInfo winutil.AclSizeInformation 299 | if err := winutil.GetAclInformation(fileDACL, &aclSizeInfo, winutil.AclSizeInformationEnum); err != nil { 300 | return nil, errors.Wrapf(err, "acl: failed to get acl size info for '%s' ", filePath) 301 | } 302 | 303 | for i := uint32(0); i < aclSizeInfo.AceCount; i++ { 304 | var pACE *winutil.AccessAllowedAce 305 | if err := winutil.GetAce(fileDACL, i, &pACE); err != nil { 306 | return nil, errors.Wrapf(err, "acl: failed to get acl for '%s' ", filePath) 307 | } 308 | 309 | sid := (*windows.SID)(unsafe.Pointer(&pACE.SidStart)) 310 | if strings.EqualFold(userSID.String(), sid.String()) { 311 | ret["identityreference"] = username 312 | if pACE.AceType == winutil.ACCESS_DENIED_ACE_TYPE { 313 | ret["accesscontroltype"] = "Deny" 314 | } 315 | if pACE.AceType == winutil.ACCESS_ALLOWED_ACE_TYPE { 316 | ret["accesscontroltype"] = "Allow" 317 | } 318 | ret["filesystemrights"] = getFileAccessFromMask(pACE.AccessMask) 319 | } 320 | } 321 | return ret, nil 322 | } 323 | -------------------------------------------------------------------------------- /web.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "html" 6 | "math" 7 | "os" 8 | "runtime" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | func genReport(img *imageData) { 14 | 15 | // displayTeamID is used for the tiling background. 16 | var displayTeamID string 17 | if len(teamID) < 7 { 18 | displayTeamID = "1010 1101" 19 | } else { 20 | displayTeamID = html.EscapeString(teamID) 21 | } 22 | 23 | header := ` Aeacus Scoring Report

` 24 | footer := `


The Aeacus project is free and open source software. This project is in no way endorsed or affiliated with the Air Force Association or the University of Texas at San Antonio.

` 25 | 26 | genTime := time.Now() 27 | 28 | var htmlFile strings.Builder 29 | htmlFile.WriteString(header) 30 | htmlFile.WriteString("

" + html.EscapeString(conf.Title) + "

") 31 | htmlFile.WriteString("

Report Generated At: " + genTime.Format("2006/01/02 15:04:05 MST") + "

") 32 | htmlFile.WriteString(``) 33 | 34 | if conf.Remote != "" { 35 | if teamID == "" { 36 | htmlFile.WriteString(`

Current Team ID: N/A

`) 37 | } else { 38 | htmlFile.WriteString(`

Current Team ID: ` + html.EscapeString(teamID) + `

`) 39 | } 40 | } 41 | 42 | htmlFile.WriteString(fmt.Sprintf(`

%d out of %d points received

`, img.Score, img.TotalPoints)) 43 | 44 | if conf.Remote != "" { 45 | htmlFile.WriteString(`Click here to view the public scoreboard
`) 46 | htmlFile.WriteString(`Click here to view the announcements
`) 47 | 48 | htmlFile.WriteString(`

Connection Status: ` + conn.OverallStatus + `

`) 49 | 50 | htmlFile.WriteString(`Internet Connectivity Check: ` + conn.NetStatus + `
`) 51 | htmlFile.WriteString(`Aeacus Server Connection Status: ` + conn.ServerStatus + `

`) 52 | } else { 53 | htmlFile.WriteString(`

Connection Status: ` + conn.OverallStatus + `

`) 54 | htmlFile.WriteString(`Internet Connectivity Check: N/A
`) 55 | htmlFile.WriteString(`Aeacus Server Connection Status: N/A
`) 56 | } 57 | 58 | htmlFile.WriteString(fmt.Sprintf(`

%d penalties assessed, for a loss of %.0f points:

`, len(img.Penalties), math.Abs(float64(img.Detracts)))) 59 | 60 | // for each penalty: 61 | for _, penalty := range img.Penalties { 62 | deobfuscateData(&penalty.Message) 63 | htmlFile.WriteString(fmt.Sprintf("%s - %.0f pts
", html.EscapeString(penalty.Message), math.Abs(float64(penalty.Points)))) 64 | obfuscateData(&penalty.Message) 65 | } 66 | 67 | htmlFile.WriteString(fmt.Sprintf(`

%d out of %d scored security issues fixed, for a gain of %d points:

`, len(img.Points), img.ScoredVulns, img.Contribs)) 68 | 69 | // for each point: 70 | for _, point := range img.Points { 71 | deobfuscateData(&point.Message) 72 | htmlFile.WriteString(fmt.Sprintf("%s - %d pts
", html.EscapeString(point.Message), point.Points)) 73 | obfuscateData(&point.Message) 74 | } 75 | 76 | htmlFile.WriteString("

") 77 | // for each hint: 78 | for _, hint := range img.Hints { 79 | if len(hint.Messages) == 1 { 80 | deobfuscateData(&hint.Messages[0]) 81 | htmlFile.WriteString(fmt.Sprintf("(%d pts) Hint: %s
", hint.Points, html.EscapeString(hint.Messages[0]))) 82 | obfuscateData(&hint.Messages[0]) 83 | } else if len(hint.Messages) > 1 { 84 | htmlFile.WriteString(fmt.Sprintf("

(%d pts) Hints:

") 91 | } 92 | } 93 | 94 | htmlFile.WriteString(footer) 95 | 96 | info("Writing HTML to ScoringReport.html...") 97 | writeFile(dirPath+"assets/ScoringReport.html", htmlFile.String()) 98 | } 99 | 100 | // genReadMe generates a competition ReadMe with some built-in defaults from 101 | // your ReadMe.conf. 102 | func genReadMe() { 103 | header := ` 104 | 105 | 106 | 107 | 108 | 109 | 110 | Aeacus README 111 | 162 |
163 |
164 |
165 |

166 | 167 |

168 | ` 169 | 170 | footer := `

Competition Guidelines

  • In order to provide a better competition experience, you are 171 | NOT required to change the password of the primary, auto-login, user account. 172 | Changing the password of a user that is set to automatically log in may lock you out of your computer. 173 |
  • Authorized administrator passwords were correct the last time you did a password audit, 174 | but are not guaranteed to be currently accurate.
  • Do not disable or stop the CSSClient service or process. 175 |
  • Do not remove any authorized users or their home directories.
  • 176 | The time zone of this image is set to UTC. Please do not change the time zone, date, or time on this image.
  • 177 | You can view your current scoring report by double-clicking the "Scoring Report" desktop icon.
  • 178 | JavaScript is required for some error messages that appear on the "Scoring Report." 179 | To ensure that you only receive correct error messages, please do not disable JavaScript.
  • 180 | Some security settings may prevent the Stop Scoring application from running. 181 | If this happens, the safest way to stop scoring is to suspend the virtual machine. 182 | You should NOT power on the VM again before deleting.
183 |

184 | The Aeacus Project is in no way affiliated or endorsed by the Air Force Association or the University of Texas at 185 | San Antonio. 186 |

187 | ` 189 | 190 | headerTheSequel := `

Please read the entire README thoroughly before modifying anything on this computer.

191 |

Unique Identifier

192 |

If you have not yet entered a valid Team ID, please do so immediately by double clicking on the "Team ID" icon 193 | on the desktop. 194 | If you do not enter a valid Team ID this VM may stop functioning after a short period of time. 195 |

Forensics Questions

If there are "Forensics Questions" on your Desktop, you will receive points for 196 | answering these questions correctly. 197 | Valid (scored) "Forensics Questions" will only be located directly on your Desktop. 198 | Please read all "Forensics Questions" thoroughly before modifying this computer, as you may change something that 199 | prevents you from answering the question correctly. 200 |

Competition Scenario

201 | This company's security policies require that all user accounts be password protected. 202 | Employees are required to choose secure passwords, however this policy may not be currently enforced on this 203 | computer. 204 | The presence of any non-work related media files and "hacking tools" on any computers is strictly prohibited. 205 | This company currently does not use any centralized maintenance or polling tools to manage their IT equipment. 206 | This computer is for official business use only by authorized users. This is a critical computer in a production 207 | environment. 208 | Please do NOT attempt to upgrade the operating system on this machine.

` 209 | 210 | var htmlFile strings.Builder 211 | htmlFile.WriteString(header) 212 | htmlFile.WriteString("

" + conf.OS + " " + conf.Title + " README

") 213 | htmlFile.WriteString(headerTheSequel) 214 | 215 | htmlFile.WriteString("

" + conf.OS + "

") 216 | 217 | htmlFile.WriteString(`

218 | It is company policy to use only ` + conf.OS + ` on this computer. It is also company policy to use only the 219 | latest, official, stable ` + conf.OS + ` packages available for required software and services on this computer. 220 | Management has decided that the default web browser for all users on this computer should be the latest stable 221 | version of Firefox.`) 222 | 223 | if runtime.GOOS == "linux" { 224 | htmlFile.WriteString(` Company policy is to never let users log in as root. 225 | If administrators need to run commands as root, they are required to use the "sudo" command.`) 226 | } 227 | 228 | htmlFile.WriteString("

") 229 | 230 | // Check for common variations of ReadMe.conf. 231 | var userReadMe string 232 | var err error 233 | readMeFiles := []string{"ReadMe.conf", "README.conf", "readme.conf"} 234 | for _, readme := range readMeFiles { 235 | userReadMe, err = readFile(readme) 236 | if err == nil { 237 | break 238 | } 239 | } 240 | if err != nil { 241 | fail("No ReadMe.conf file found!") 242 | os.Exit(1) 243 | } 244 | 245 | htmlFile.WriteString(userReadMe) 246 | htmlFile.WriteString(footer) 247 | info("Writing HTML to ReadMe.html...") 248 | writeFile(dirPath+"assets/ReadMe.html", htmlFile.String()) 249 | } 250 | --------------------------------------------------------------------------------