├── .gitignore ├── service ├── common.go ├── windows.go └── linux.go ├── go.mod ├── LICENSE ├── README.md ├── .github └── workflows │ └── publish.yml ├── go.sum └── cmd └── broom └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /service/common.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | var instance Checker 4 | 5 | type Checker interface { 6 | HasService(service string) (bool, error) 7 | } 8 | 9 | func GetChecker() Checker { 10 | return instance 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/berrybyte-net/broom 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 7 | github.com/fatih/color v1.13.0 // indirect 8 | github.com/mattn/go-colorable v0.1.9 // indirect 9 | github.com/mattn/go-isatty v0.0.14 // indirect 10 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 11 | github.com/urfave/cli/v2 v2.17.1 // indirect 12 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 13 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /service/windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package service 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "github.com/fatih/color" 9 | "golang.org/x/sys/windows" 10 | "golang.org/x/sys/windows/svc/mgr" 11 | ) 12 | 13 | func init() { 14 | var s *uint16 15 | h, err := windows.OpenSCManager(s, nil, windows.SC_MANAGER_ENUMERATE_SERVICE) 16 | if err != nil { 17 | color.Red("could not query services (%s)", err.Error()) 18 | } else { 19 | instance = &WindowsChecker{svcMgr: &mgr.Mgr{Handle: h}} 20 | } 21 | } 22 | 23 | type WindowsChecker struct { 24 | svcMgr *mgr.Mgr 25 | } 26 | 27 | func (wc *WindowsChecker) HasService(service string) (bool, error) { 28 | found := false 29 | services, err := wc.svcMgr.ListServices() 30 | if err != nil { 31 | return false, errors.New(fmt.Sprintf("could not query services (%s)", err.Error())) 32 | } else { 33 | for _, svc := range services { 34 | if svc == service { 35 | found = true 36 | break 37 | } 38 | } 39 | } 40 | return found, nil 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 BerryByte 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # broom 2 | 3 | A Go program for scanning JAR files to uncover [the 29-09-2022 Minecraft malware](https://forums.papermc.io/threads/malware-announcement.529/) infections. 4 | 5 | ## Disclaimer 6 | 7 | This software is by no means a sophisticated antimalware, it only catches one or possibly more variants of the 'Updater' malware; run [OpticFusion1's antimalware](https://github.com/OpticFusion1/MCAntiMalware) for a thorough check. 8 | 9 | ## Usage 10 | 11 | Download a binary for your architecture and operating system in the [Releases](https://github.com/berrybyte-net/broom/releases) tab. (amd64 is x64 - 64-bit, 386 is x86 - 32-bit, Darwin is MacOS, arm64 is ARM - Apple Silicon for Apple users). 12 | 13 | ### Windows & MacOS 14 | 15 | Drag and drop the binary into your server's directory (next to the plugins folder) and double click on it. 16 | 17 | ### Linux 18 | 19 | Invoke the binary through your shell (most likely bash or a derivative). 20 | 21 | Example: 22 | ````bash 23 | curl -LO https://github.com/berrybyte-net/broom/releases/latest/download/broom_linux_amd64 24 | chmod +x broom_linux_amd64 25 | ./broom_linux_amd64 26 | ```` 27 | 28 | ## Support 29 | 30 | This was hastily made and tested minimally, so please report any issues in the [issues](https://github.com/berrybyte-net/broom/issues) tab and/or reach out to us via Discord. 31 | -------------------------------------------------------------------------------- /service/linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin 2 | 3 | package service 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "io/fs" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | // SystemdSearchPaths are the base directories to search in for service files. 15 | var SystemdSearchPaths = []string{ 16 | // TODO: include user unit paths 17 | "/etc/systemd/system.control/", 18 | "/run/systemd/system.control/", 19 | "/run/systemd/transient/", 20 | "/run/systemd/generator.early/", 21 | "/etc/systemd/system/", 22 | "/etc/systemd/system.attached/", 23 | "/run/systemd/system/", 24 | "/run/systemd/system.attached/", 25 | "/run/systemd/generator/", 26 | "/usr/lib/systemd/system/", 27 | "/run/systemd/generator.late/", 28 | } 29 | 30 | func init() { 31 | instance = &LinuxChecker{} 32 | } 33 | 34 | type LinuxChecker struct { 35 | } 36 | 37 | func (lc *LinuxChecker) HasService(service string) (bool, error) { 38 | found := false 39 | for _, dir := range SystemdSearchPaths { 40 | if info, err := os.Stat(dir); !os.IsNotExist(err) && info.IsDir() { 41 | if found { 42 | break 43 | } 44 | err := filepath.Walk(dir, func(path string, info0 fs.FileInfo, err error) error { 45 | if err != nil { 46 | return err 47 | } 48 | if !info0.IsDir() && strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) == service { 49 | found = true 50 | } 51 | 52 | return nil 53 | }) 54 | if err != nil { 55 | return found, errors.New(fmt.Sprintf("could not query services at path %s", dir)) 56 | } 57 | } 58 | } 59 | return found, nil 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish binaries 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | publish: 10 | strategy: 11 | matrix: 12 | goos: [linux, windows, darwin] 13 | goarch: [386, amd64, arm64] 14 | exclude: 15 | - goos: darwin 16 | goarch: 386 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | with: 22 | submodules: true 23 | - name: Set up Go 24 | uses: actions/setup-go@v3 25 | with: 26 | go-version: '>=1.19' 27 | - name: Build 28 | env: 29 | GOOS: ${{ matrix.goos }} 30 | GOARCH: ${{ matrix.goarch }} 31 | GOEXT: ${{ matrix.goos == 'windows' && '.exe' || '' }} 32 | CGO_ENABLED: 0 33 | run: | 34 | go build -v -ldflags="-s -w" -o build/broom_${GOOS}_${GOARCH}${GOEXT} ./cmd/broom 35 | - name: Truncate SHA 36 | id: truncate_sha 37 | shell: bash 38 | run: | 39 | echo "::set-output name=truncated_sha::${GITHUB_SHA::7}" 40 | - name: Publish a release 41 | if: startsWith(github.event.head_commit.message, '[release]') 42 | uses: ncipollo/release-action@v1 43 | with: 44 | artifacts: build/broom_${{ matrix.goos }}_${{ matrix.goarch }}${{ matrix.goos == 'windows' && '.exe' || '' }} 45 | artifactContentType: application/octet-stream 46 | draft: true 47 | tag: ${{ steps.truncate_sha.outputs.truncated_sha }} 48 | allowUpdates: true 49 | - name: Publish an artifact 50 | uses: actions/upload-artifact@v3 51 | with: 52 | name: broom_${{ matrix.goos }}_${{ matrix.goarch }}${{ matrix.goos == 'windows' && '.exe' || '' }} 53 | path: build/broom_${{ matrix.goos }}_${{ matrix.goarch }}${{ matrix.goos == 'windows' && '.exe' || '' }} -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 4 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 5 | github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= 6 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 7 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 8 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 9 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 10 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 11 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 12 | github.com/urfave/cli/v2 v2.17.1 h1:UzjDEw2dJQUE3iRaiNQ1VrVFbyAtKGH3VdkMoHA58V0= 13 | github.com/urfave/cli/v2 v2.17.1/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= 14 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 15 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 16 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 18 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 19 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= 21 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | -------------------------------------------------------------------------------- /cmd/broom/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/zip" 5 | "bufio" 6 | "fmt" 7 | "github.com/berrybyte-net/broom/service" 8 | "github.com/fatih/color" 9 | "github.com/urfave/cli/v2" 10 | "os" 11 | "path/filepath" 12 | ) 13 | 14 | // main is the application entrypoint. 15 | func main() { 16 | app := &cli.App{ 17 | Name: "broom", 18 | Description: "scans JAR files to uncover the 29-09-2022 Minecraft malware infections", 19 | Flags: []cli.Flag{ 20 | &cli.StringFlag{ 21 | Name: "dir", 22 | Usage: "the root directory to recursively scan", 23 | Aliases: []string{"d"}, 24 | Value: ".", 25 | }, 26 | }, 27 | Action: func(cCtx *cli.Context) error { 28 | foundInfected := false 29 | err := filepath.Walk(filepath.Clean(cCtx.String("dir")), func(path string, info os.FileInfo, err error) error { 30 | if err != nil { 31 | return err 32 | } 33 | 34 | if info.IsDir() { 35 | fmt.Printf("walking directory %s\n", path) 36 | } else { 37 | fmt.Printf("walking file %s\n", path) 38 | } 39 | 40 | if !info.IsDir() && filepath.Ext(path) == ".jar" { // found jar file 41 | fmt.Printf("found jar file %s\n", path) 42 | 43 | zf, err := zip.OpenReader(path) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | for _, file := range zf.File { 49 | if !file.Mode().IsDir() { 50 | switch filepath.Base(file.Name) { 51 | case "plugin-config.bin": 52 | color.Red("found %s file in jar file %s", file.Name, path) 53 | foundInfected = true 54 | } 55 | } 56 | } 57 | 58 | if err := zf.Close(); err != nil { 59 | return err 60 | } 61 | } 62 | return nil 63 | }) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | foundService := false 69 | checker := service.GetChecker() 70 | if checker != nil { 71 | foundService, err = checker.HasService("vmd-gnu") 72 | if err != nil { 73 | color.Red(err.Error()) 74 | } 75 | } 76 | 77 | fmt.Print("\n\n\n\n") // a bit of space before the assessment 78 | if foundInfected { 79 | color.Red("JAR files containing files that are a known part of the malware were found.") 80 | } else { 81 | color.Green("No files related to the malware were found!") 82 | } 83 | if foundService { 84 | color.Red("A system service that is a known part of the malware was found.") 85 | } else { 86 | color.Green("No services related to the malware were found!") 87 | } 88 | 89 | fmt.Print("Press Enter to exit...") 90 | bufio.NewReader(os.Stdin).ReadBytes('\n') 91 | 92 | return nil 93 | }, 94 | } 95 | 96 | if err := app.Run(os.Args); err != nil { 97 | panic(err) 98 | } 99 | } 100 | --------------------------------------------------------------------------------