├── .gitattributes ├── Makefile ├── README.md ├── assets ├── 38138d0696414c4828e0caf498a8f0e1.png └── 8bf97de7a1fd7b1a6d56362b3eaad39b.png ├── bin ├── gocheck32.exe └── gocheck64.exe ├── cmd ├── check.go └── root.go ├── go.mod ├── go.sum ├── gocheck.go ├── scanner ├── amsi.go ├── common.go ├── customs.go ├── helpers.go ├── kaspersky.go ├── scanner.go └── windef.go └── utils └── utilities.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY_NAME = gocheck 2 | BIN_FOLDER = bin 3 | 4 | .PHONY: all windows win64 win32 5 | 6 | all: 7 | @echo [*] No default target specified, available targets: windows, win64, win32 8 | 9 | windows: win64 win32 10 | @echo [*] Done compiling for Windows x64 and x86, binary is located at: $(BIN_FOLDER) 11 | 12 | win64: 13 | @echo [*] Building $(BINARY_NAME)64.exe... 14 | @set GOOS=windows&& set GOARCH=amd64&& go build -o ./$(BIN_FOLDER)/$(BINARY_NAME)64.exe 15 | 16 | win32: 17 | @echo [*] Building $(BINARY_NAME)32.exe... 18 | @set GOOS=windows&& set GOARCH=386&& go build -o ./$(BIN_FOLDER)/$(BINARY_NAME)32.exe 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gocheck 2 | 3 | gocheck is a golang implementation of Matterpreter's [DefenderCheck](https://github.com/matterpreter/DefenderCheck) that aims to aid red teams in their malware development capabilities by identifying the exact bytes in their malware that are flagged by security solutions (incomplete integration with enterprise AV, see [External Scanners](#external-scanners)) 4 | 5 | I also wrote a blog post showcasing this project: [Identifying Malicious Bytes in Malware](https://gatari.dev/posts/identifying-malicious-bytes-in-malware/) 6 | 7 | ![both](https://i.gyazo.com/5bb7681b57cd8736329ccd22ac7e9d7c.png) 8 | 9 | ![kaspersky](https://i.gyazo.com/346a57bb13a2b6fef5f6ae889c9e45d2.png) 10 | 11 | ## Installation 12 | You can install `gocheck` from `go install` 13 | ```bash 14 | go install github.com/gatariee/gocheck@latest 15 | ``` 16 | 17 | Alternatively, you can download the precompiled binaries from the [releases](https://github.com/gatariee/gocheck/releases) or build it yourself. 18 | ```bash 19 | git clone https://github.com/gatariee/gocheck 20 | make [ windows / win64 / win32 ] 21 | ``` 22 | 23 | ## Usage 24 | ```cmd 25 | $ gocheck check --help 26 | Usage: 27 | gocheck check [path_to_bin] /optional [flags] 28 | 29 | Flags: 30 | -a, --amsi Use AMSI to scan the binary 31 | -D, --debug Enable debug mode 32 | -d, --defender Use Windows Defender to scan the binary 33 | -h, --help help for check 34 | 35 | [!] UNSTABLE 36 | -k, --kaspersky Use Kaspersky's AV Engine to scan the binary 37 | ``` 38 | 39 | ## Quick Use 40 | The `check` cobra flag is only used for ease of extensibility, you can completely omit the `check` flag and directly pass the file to `gocheck` as an argument. 41 | 42 | ```cmd 43 | $ gocheck /optional:args 44 | ``` 45 | 46 | > This may be changed in the future. 47 | 48 | ## Windows Defender 49 | Real-time protection is optional when scanning using Windows Defender. If real-time protection is enabled, the file may be nuked on first scan. In order to prevent the file from being nuked on first scan, you can set an exclusion for the original file in Windows Defender as `gocheck` creates temporary copies and chucks them into C:\Temp. 50 | ```cmd 51 | gocheck [path_to_binary] /optional: --defender 52 | ``` 53 | ![windef](https://i.gyazo.com/3c9b5366f9565e0b3891d70ee78e70a2.png) 54 | 55 | ## AMSI 56 | When scanning using AMSI, do ensure that real-time protection is enabled. However, at first your file may be nuked. 57 | ```cmd 58 | gocheck [path_to_file] /optional: --amsi 59 | ``` 60 | ![nuked](https://i.gyazo.com/0ca26f2f63d0118df6fbd1e6e786eee8.png) 61 | 62 | In order to prevent the file from being nuked on first scan, you can set an exclusion for the original file in Windows Defender as `gocheck` creates temporary copies and chucks them into C:\Temp. 63 | ```ps 64 | Add-MpPreference -ExclusionPath [path_to_folder] 65 | ``` 66 | ![amsi](https://i.gyazo.com/0c0a437eafe2c945c7d1188fdd9ec86d.png) 67 | 68 | ## External Scanners 69 | There is currently only support for [Kaspersky](https://www.kaspersky.com/security-cloud)'s Security Cloud AV Engine. The `--kaspersky` flag can be used to scan the binary using Kaspersky's AV Engine. 70 | 71 | There **are** plans to integrate more AV engines in the future. 72 | 73 | > It is normal for Kaspersky's AV engine to take a little longer than Windows Defender to scan the binary. 74 | ```cmd 75 | gocheck [path_to_file] /optional: --kaspersky 76 | ``` 77 | ![kaspersky](https://i.gyazo.com/346a57bb13a2b6fef5f6ae889c9e45d2.png) 78 | 79 | ## Concurrency 80 | `gocheck` allows you to scan a binary using multiple AV engines simultaneously. This is done by passing multiple flags to `gocheck`. 81 | 82 | For example, to scan a binary using both **Windows Defender** and **Kaspersky's AV Engine**, you can pass the following flags to `gocheck` & the results will be returned at runtime. 83 | ```cmd 84 | gocheck [path_to_file] /optional: --defender --kaspersky 85 | ``` 86 | ![kaspersky2](https://i.gyazo.com/3cd9b23ab285c33804a11c7440b1cdfc.png) 87 | 88 | ## Debug 89 | Gocheck is in heavy WIP and may not work as expected. If you encounter any issues, please run the tool with `--debug` to provide more information about the issue. The `--debug` flag prints out which portions of the binary are being scanned, as well as sanity checks to ensure that the signatured portions are being correctly scanned. 90 | ```cmd 91 | gocheck [path_to_file] /optional: --debug 92 | ``` 93 | ![debug](https://i.gyazo.com/c6bb797e5b507b2ba7fc0d007575a410.png) 94 | 95 | ## Common Pitfalls 96 | 1. You may need to set exclusions when using `gocheck` to prevent the file from being nuked on first scan, here's how `gocheck` works under the hood: 97 | * `gocheck` first passes the original file to `MpCmdRun.exe` to scan the file using Windows Defender -> (e.g ./mimikatz.exe) 98 | * If the scan comes back malicious, we create a folder in the **current working directory** with the respective name of the scanner (e.g `windef`). 99 | * Then, we start splitting the file (with reference to the original file), and writing the split bytes to the respective folder (e.g `windef`). 100 | 101 | > There are multiple exclusions you need to set, or you can exclude the entire folder where `gocheck` is located. 102 | 103 | 2. Where possible, we try to pass in flags that are not destructive such as `-DisableRemediation` for Windows Defender and `/i0` for Kaspersky's AV Engine. However, whether the file gets sent to the cloud for further analysis **is not** within our control. 104 | * It is ultimately the responsibility of the operator to assume that the AV engine **will** try it's best to send all binaries to the cloud for further analysis; and to take the necessary precautions to prevent this from happening such as disabling internet access. 105 | 106 | 107 | 108 | ## Benchmark 109 | > ⚠️ I am not an expert in benchmarking, and the following benchmarks are conducted on a single machine, and the results may vary on different machines. The benchmarks are conducted on a single machine to provide a rough estimate of the performance difference between `gocheck` and `DefenderCheck`. 110 | 111 | The objective of `gocheck` was to implement a faster alternative to Matterpreter's [DefenderCheck](https://github.com/matterpreter/DefenderCheck) as I realized that it was painfully slow when scanning large binaries, which can be quite a headache for extremely large binaries such as those written in Golang. 112 | 113 | The following benchmarks were conducted on the following specifications: 114 | * **OS**: Windows 10 Pro 115 | * **CPU**: AMD Ryzen™ 5 3600X 116 | * **RAM**: 32 GB DDR4 3200 MHz 117 | 118 | The I/O operations were conducted on a Samsung 870 EVO SATA 2.5" SSD (1 TB), 560/530 MB/s R/W, the temporary binaries are stored in the `C:\Temp` directory. 119 | 120 | The version of `gocheck` used in the benchmark is [`v0.1.0`](https://github.com/gatariee/gocheck/releases/download/v1.3.0/gocheck64.exe) and the version of `DefenderCheck` used was the commit [`27616de`](https://github.com/matterpreter/DefenderCheck/commit/27616dea8d27a9d926f5b2178b114109f482c60b) (Sep 15, 2023). 121 | 122 | ### mimikatz.exe (1,250,056 bytes / 1.19 MB) 123 | 124 | | Tool | Time | 125 | |------|------| 126 | | GoCheck | 1.05s | 127 | | DefenderCheck | 5.56s | 128 | 129 | ![comparison1](./assets/38138d0696414c4828e0caf498a8f0e1.png) 130 | 131 | ### Sliver HTTP Beacon (10,972,160 bytes / 10.4 MB) 132 | 133 | | Tool | Time | 134 | |------|------| 135 | | GoCheck | 5.65s | 136 | | DefenderCheck | 35.69s | 137 | 138 | ![comparison2](./assets/8bf97de7a1fd7b1a6d56362b3eaad39b.png) 139 | 140 | ## Credits / References 141 | * Originally implemented by [Matterpreter](https://github.com/matterpreter) in [DefenderCheck](https://github.com/matterpreter/DefenderCheck) 142 | * https://github.com/rasta-mouse/ThreatCheck 143 | -------------------------------------------------------------------------------- /assets/38138d0696414c4828e0caf498a8f0e1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gatariee/gocheck/c95b38e2c45ffd8b093780128cb9c25a20b77ee4/assets/38138d0696414c4828e0caf498a8f0e1.png -------------------------------------------------------------------------------- /assets/8bf97de7a1fd7b1a6d56362b3eaad39b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gatariee/gocheck/c95b38e2c45ffd8b093780128cb9c25a20b77ee4/assets/8bf97de7a1fd7b1a6d56362b3eaad39b.png -------------------------------------------------------------------------------- /bin/gocheck32.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gatariee/gocheck/c95b38e2c45ffd8b093780128cb9c25a20b77ee4/bin/gocheck32.exe -------------------------------------------------------------------------------- /bin/gocheck64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gatariee/gocheck/c95b38e2c45ffd8b093780128cb9c25a20b77ee4/bin/gocheck64.exe -------------------------------------------------------------------------------- /cmd/check.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "time" 9 | 10 | "github.com/spf13/cobra" 11 | 12 | scanner "github.com/gatariee/gocheck/scanner" 13 | utils "github.com/gatariee/gocheck/utils" 14 | ) 15 | 16 | var checkCmd = &cobra.Command{ 17 | Use: "check [path_to_bin] /optional", 18 | Short: "", 19 | Long: ``, 20 | Args: cobra.ExactArgs(1), 21 | 22 | Run: func(cmd *cobra.Command, args []string) { 23 | /* 24 | @ previous usage 25 | file, _ := cmd.Flags().GetString("file") 26 | */ 27 | 28 | file := args[0] 29 | amsi, _ := cmd.Flags().GetBool("amsi") 30 | defender, _ := cmd.Flags().GetBool("defender") 31 | debug, _ := cmd.Flags().GetBool("debug") 32 | kaspersky, _ := cmd.Flags().GetBool("kaspersky") 33 | 34 | // 35 | // if no args are passed, then use defender 36 | // 37 | 38 | if !amsi && !defender && !kaspersky { 39 | defender = true 40 | } 41 | 42 | if debug { 43 | utils.PrintInfo("Debug mode enabled, verbose output will be displayed") 44 | } 45 | 46 | var ( 47 | defender_path string 48 | err error 49 | ) 50 | 51 | if defender { 52 | defender_path, err = FindDefenderPath("C:\\") 53 | if err != nil { 54 | utils.PrintErr(err.Error()) 55 | return 56 | } 57 | utils.PrintInfo(fmt.Sprintf("Found Windows Defender at %s", defender_path)) 58 | } else { 59 | /* If we're not using defender, we can assume an empty string */ 60 | defender_path = "" 61 | } 62 | 63 | var avp string 64 | if kaspersky { 65 | avp, err = scanner.FindKaspersky() 66 | if err != nil { 67 | utils.PrintErr(err.Error()) 68 | return 69 | } 70 | 71 | if avp == "" { 72 | utils.PrintErr("Kaspersky not found, please ensure it's installed and the path is correct") 73 | utils.PrintInfo("Kaspersky is probably installed at > ") 74 | fmt.Println("\t", scanner.Kaspersky.ScanPath) 75 | fmt.Println("\t", scanner.Kaspersky.AltScanPath) 76 | return 77 | } 78 | 79 | utils.PrintInfo(fmt.Sprintf("Found Kaspersky at %s", avp)) 80 | } 81 | 82 | additionals := make(map[string]string) 83 | if kaspersky { 84 | additionals["kaspersky"] = avp 85 | } 86 | 87 | token := scanner.Scanner{ 88 | File: file, 89 | Amsi: amsi, 90 | Defender: defender, 91 | EnginePath: defender_path, 92 | Additional: additionals, 93 | } 94 | 95 | start := time.Now() 96 | scanner.Run(token, debug) 97 | elapsed := time.Since(start) 98 | 99 | utils.PrintOk(fmt.Sprintf("Total time elasped: %s", elapsed)) 100 | }, 101 | } 102 | 103 | func GetFileSize(file string) (int64, error) { 104 | fileInfo, err := os.Stat(file) 105 | if err != nil { 106 | return 0, err 107 | } 108 | 109 | return fileInfo.Size(), nil 110 | } 111 | 112 | func FindDefenderPath(root string) (string, error) { 113 | /* We don't want to perform this expensive operation if we don't have to, so let's search common paths first! */ 114 | paths := []string{ 115 | "C:\\Program Files\\Windows Defender\\MpCmdRun.exe", 116 | "C:\\Program Files (x86)\\Windows Defender\\MpCmdRun.exe", 117 | } 118 | 119 | for _, path := range paths { 120 | if _, err := os.Stat(path); err == nil { 121 | return path, nil 122 | } 123 | } 124 | 125 | /* Now, we can panic and search for MpCmdRun.exe */ 126 | utils.PrintErr("Could not find Windows Defender in common paths, searching C:\\ recursively for MpCmdRun.exe...") 127 | 128 | var defenderPath string 129 | err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 130 | if err != nil { 131 | 132 | /* If error is due to permission, don't panic just yet */ 133 | if os.IsPermission(err) { 134 | return nil 135 | /* Keep walking! :) */ 136 | } 137 | 138 | return err 139 | } 140 | if !info.IsDir() && strings.Contains(info.Name(), "MpCmdRun.exe") { 141 | defenderPath = path 142 | return filepath.SkipDir 143 | } 144 | return nil 145 | }) 146 | return defenderPath, err 147 | } 148 | 149 | func init() { 150 | checkCmd.Flags().BoolP("amsi", "a", false, "Use AMSI to scan the binary") 151 | checkCmd.Flags().BoolP("defender", "d", false, "Use Windows Defender to scan the binary") 152 | checkCmd.Flags().BoolP("kaspersky", "k", false, "Use Kaspersky to scan the binary") 153 | checkCmd.Flags().BoolP("debug", "D", false, "Enable debug mode") 154 | } 155 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | const ( 10 | VERSION = "v1.4.0" 11 | URL = "https://github.com/gatariee/GoCheck" 12 | ) 13 | 14 | var rootCmd = &cobra.Command{ 15 | Use: "gocheck", 16 | Short: "A Golang CLI tool to test the evasiveness of your malware.", 17 | Long: ` 18 | 19 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⠞⢳⠀⠀⠀⠀⠀ 20 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡔⠋⠀⢰⠎⠀⠀⠀⠀⠀ 21 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢆⣤⡞⠃⠀⠀⠀⠀⠀⠀ 22 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢠⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀ 23 | ⠀⠀⠀⠀⢀⣀⣾⢳⠀⠀⠀⠀⢸⢠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 24 | ⣀⡤⠴⠊⠉⠀⠀⠈⠳⡀⠀⠀⠘⢎⠢⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀ 25 | ⠳⣄⠀⠀⡠⡤⡀⠀⠘⣇⡀⠀⠀⠀⠉⠓⠒⠺⠭⢵⣦⡀⠀⠀⠀ 26 | ⠀⢹⡆⠀⢷⡇⠁⠀⠀⣸⠇⠀⠀⠀⠀⠀⢠⢤⠀⠀⠘⢷⣆⡀⠀ 27 | ⠀⠀⠘⠒⢤⡄⠖⢾⣭⣤⣄⠀⡔⢢⠀⡀⠎⣸⠀⠀⠀⠀⠹⣿⡀ 28 | ⠀⠀⢀⡤⠜⠃⠀⠀⠘⠛⣿⢸⠀⡼⢠⠃⣤⡟⠀⠀⠀⠀⠀⣿⡇ 29 | ⠀⠀⠸⠶⠖⢏⠀⠀⢀⡤⠤⠇⣴⠏⡾⢱⡏⠁⠀⠀⠀⠀⢠⣿⠃ 30 | ⠀⠀⠀⠀⠀⠈⣇⡀⠿⠀⠀⠀⡽⣰⢶⡼⠇⠀⠀⠀⠀⣠⣿⠟⠀ 31 | ⠀⠀⠀⠀⠀⠀⠈⠳⢤⣀⡶⠤⣷⣅⡀⠀⠀⠀⣀⡠⢔⠕⠁⠀⠀ 32 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠫⠿⠿⠿⠛⠋⠁⠀⠀⠀⠀ 33 | 34 | A tool to identify byte chunks that are flagged by AMSI or Windows Defender 35 | 36 | `, 37 | } 38 | 39 | func Execute() { 40 | err := rootCmd.Execute() 41 | if err != nil { 42 | os.Exit(1) 43 | } 44 | } 45 | 46 | func init() { 47 | rootCmd.AddCommand(checkCmd) 48 | rootCmd.Version = VERSION 49 | rootCmd.SetVersionTemplate("GoCheck version {{.Version}}\n") 50 | 51 | rootCmd.Root().CompletionOptions.DisableDefaultCmd = true 52 | rootCmd.Root().CompletionOptions.DisableDescriptions = true 53 | rootCmd.SetHelpCommand(&cobra.Command{Use: "no-help", Run: func(cmd *cobra.Command, args []string) { /* Do nothing */ }}) 54 | } 55 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gatariee/gocheck 2 | 3 | go 1.21.1 4 | 5 | require ( 6 | github.com/Velocidex/amsi v0.0.0-20200608120838-e5d93b76f119 7 | github.com/spf13/cobra v1.8.0 8 | ) 9 | 10 | require ( 11 | github.com/mattn/go-colorable v0.1.13 // indirect 12 | github.com/mattn/go-isatty v0.0.20 // indirect 13 | golang.org/x/sys v0.14.0 // indirect 14 | ) 15 | 16 | require ( 17 | github.com/fatih/color v1.16.0 18 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 19 | github.com/spf13/pflag v1.0.5 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Velocidex/amsi v0.0.0-20200608120838-e5d93b76f119 h1:eUSISb8p+ebTARWKDneug7IAsUdSWcKVWa/un5pROTY= 2 | github.com/Velocidex/amsi v0.0.0-20200608120838-e5d93b76f119/go.mod h1:kDUpg5jkpbB/2Lv9tyol1AyR/JOwcljtzYkzlX8pcms= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 4 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 5 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 6 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 7 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 8 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 9 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 10 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 11 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 12 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 13 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 14 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 15 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 16 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 17 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 18 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 19 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= 21 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | -------------------------------------------------------------------------------- /gocheck.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/gatariee/gocheck/cmd" 8 | ) 9 | 10 | var suffixes = []string{".exe", ".dll", ".sys", ".drv", ".ps1"} 11 | 12 | func main() { 13 | if len(os.Args) > 1 { 14 | for _, suffix := range suffixes { 15 | if strings.HasSuffix(os.Args[1], suffix) { 16 | os.Args = append([]string{os.Args[0], "check"}, os.Args[1:]...) 17 | } 18 | } 19 | } 20 | /* 21 | What are you gonna do about it? (ง'̀-'́)ง 22 | */ 23 | 24 | cmd.Execute() 25 | } 26 | -------------------------------------------------------------------------------- /scanner/amsi.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/Velocidex/amsi" 10 | 11 | utils "github.com/gatariee/gocheck/utils" 12 | ) 13 | 14 | type AMSIScanner struct{} 15 | 16 | func newAMSIScanner() *AMSIScanner { 17 | return &AMSIScanner{} 18 | } 19 | 20 | func (as *AMSIScanner) Scan(filePath string) (amsi.ScanResult, error) { 21 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 22 | return 0, err 23 | } 24 | 25 | fileData, err := os.ReadFile(filePath) 26 | if err != nil { 27 | return 0, err 28 | } 29 | 30 | err = amsi.Initialize() 31 | if err != nil { 32 | return 0, err 33 | } 34 | defer amsi.Uninitialize() 35 | 36 | session := amsi.OpenSession() 37 | defer amsi.CloseSession(session) 38 | 39 | result := session.ScanBuffer(fileData) 40 | return result, nil 41 | } 42 | 43 | func (as *AMSIScanner) Go(amsi_instance *AMSIScanner, file_path string) (int, error) { 44 | start, err := os.ReadFile(file_path) 45 | if err != nil { 46 | return 0, err 47 | } 48 | size := len(start) 49 | utils.PrintInfo(fmt.Sprintf("Scanning %s, analysing %d bytes...", file_path, size)) 50 | 51 | tempDir := filepath.Join(".", "temp") 52 | err = os.MkdirAll(tempDir, 0o755) 53 | // TODO: Fix race condition when scanning multiple files (see: https://go.dev/tour/concurrency/9) 54 | if err != nil { 55 | return 0, err 56 | } 57 | 58 | testFilePath := filepath.Join(tempDir, "test") 59 | defer os.RemoveAll(tempDir) 60 | 61 | lastGood := 0 62 | upperBound := len(start) 63 | mid := upperBound / 2 64 | ok := false 65 | 66 | for upperBound-lastGood > 1 { 67 | err := os.WriteFile(testFilePath, start[0:mid], 0o644) 68 | if err != nil { 69 | return 0, err 70 | } 71 | 72 | result, err := amsi_instance.Scan(testFilePath) 73 | if err != nil { 74 | return 0, err 75 | } 76 | 77 | if result == amsi.ResultDetected { 78 | upperBound = mid 79 | ok = true 80 | 81 | } else { 82 | lastGood = mid 83 | } 84 | 85 | mid = (upperBound + lastGood) / 2 86 | } 87 | 88 | if !ok { 89 | return 0, fmt.Errorf("unable to isolate bad bytes, uh oh. :(") 90 | } 91 | 92 | return mid, nil 93 | } 94 | 95 | func ScanAMSI(filePath string, debug bool) error { 96 | /* Setup */ 97 | scanner := newAMSIScanner() 98 | start_time := time.Now() 99 | 100 | original_file, err := os.ReadFile(filePath) 101 | if err != nil { 102 | return err 103 | } 104 | result, err := scanner.Scan(filePath) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | switch result { 110 | /* 111 | https://learn.microsoft.com/en-us/windows/win32/api/amsi/ne-amsi-amsi_result 112 | */ 113 | 114 | case amsi.ResultClean: 115 | utils.PrintInfo("No threats found, got 'AMSI_RESULT_CLEAN'") 116 | return nil 117 | case amsi.ResultNotDetected: 118 | utils.PrintInfo("No threats found, got 'AMSI_RESULT_NOT_DETECTED'") 119 | return nil 120 | case amsi.ResultBlockedByAdminStart: 121 | utils.PrintErr("Scan was blocked for some reason, got 'AMSI_RESULT_BLOCKED_BY_ADMIN_START'") 122 | case amsi.ResultBlockedByAdminEnd: 123 | utils.PrintErr("Scan was blocked for some reason, got 'AMSI_RESULT_BLOCKED_BY_ADMIN_END'") 124 | case amsi.ResultDetected: 125 | /* Continue, let's do our binary search now! */ 126 | utils.PrintNewLine() 127 | utils.PrintInfo("Threat detected in original file, beginning AMSI binary search...") 128 | offset, err := scanner.Go(scanner, filePath) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | /* Binary search is over */ 134 | end_time := time.Since(start_time) 135 | 136 | utils.PrintNewLine() 137 | utils.PrintOk(fmt.Sprintf("AMSI - %s", end_time)) 138 | 139 | utils.PrintErr(fmt.Sprintf("Isolated bad bytes at offset 0x%X in the original file [approximately %d / %d bytes]", offset, offset, len(original_file))) 140 | 141 | start := offset - 64 142 | if start < 0 { 143 | start = 0 144 | } 145 | 146 | end := offset + 64 147 | if end > len(original_file) { 148 | end = len(original_file) 149 | } 150 | 151 | /* Start printing the hex dump */ 152 | threatData := original_file[start:end] 153 | fmt.Println(HexDump(threatData)) 154 | 155 | default: 156 | utils.PrintErr(fmt.Sprintf("Unknown result: %d", result)) 157 | } 158 | 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /scanner/common.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | /* 8 | Eventually, all scanning modules will be ported to this file; including Windows Defender. 9 | */ 10 | 11 | func IsMalicious(output string, detectionString string) bool { 12 | lines := strings.Split(output, "\n") 13 | for _, line := range lines { 14 | if strings.Contains(line, detectionString) { 15 | return true 16 | } 17 | } 18 | return false 19 | } 20 | 21 | func GetSignature(output string, sigString string) string { 22 | lines := strings.Split(output, "\n") 23 | for _, line := range lines { 24 | if strings.Contains(line, sigString) { 25 | parts := strings.Fields(line) 26 | for _, part := range parts { 27 | if strings.Contains(part, sigString) { 28 | return part 29 | } 30 | } 31 | } 32 | } 33 | 34 | return "No signature found" 35 | } 36 | -------------------------------------------------------------------------------- /scanner/customs.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | /* 4 | there are plans to add more scanners in the future, so we need to make sure that the scanner interface is implemented properly. 5 | * as of [7/3/2024] im lazy to do this, so i'll just hardcode the vendors into a struct and manually parse them XD 6 | 7 | var scanners = map[string]CustomScanner{ 8 | "Kaspersky": { 9 | ... 10 | }, 11 | "Elastic": { 12 | ... 13 | } 14 | } 15 | 16 | type Scanner interface { 17 | Scan(file string, scanPath string, args ...string) (string, error) 18 | IsMalicious(output string, detectionString string) bool 19 | GetSignature(output string) string 20 | } 21 | 22 | */ 23 | 24 | type CustomScanner struct { 25 | ScanPath string 26 | AltScanPath string 27 | DetectionString string 28 | SignatureString string 29 | Arguments []string 30 | } 31 | 32 | var Kaspersky = CustomScanner{ 33 | ScanPath: "C:\\Program Files (x86)\\Kaspersky Lab\\Kaspersky Security Cloud 21.3\\avp.com", 34 | AltScanPath: "C:\\Program Files\\Kaspersky Lab\\Kaspersky Security Cloud 21.3\\avp.com", 35 | DetectionString: "suspicion", 36 | SignatureString: "HEUR:", 37 | Arguments: []string{"SCAN", "/i0"}, 38 | } 39 | -------------------------------------------------------------------------------- /scanner/helpers.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "encoding/hex" 5 | "strings" 6 | ) 7 | 8 | func HexDump(data []byte) string { 9 | dump := hex.Dump(data) 10 | return dump 11 | } 12 | 13 | func extractThreat(scanOutput string) string { 14 | lines := strings.Split(scanOutput, "\n") 15 | threatInfo := "" 16 | 17 | for _, line := range lines { 18 | if strings.HasPrefix(line, "Threat ") { 19 | threatInfo = line 20 | break 21 | } 22 | } 23 | 24 | if threatInfo != "" { 25 | 26 | threatInfo = strings.Split(threatInfo, ": ")[1] 27 | return threatInfo 28 | } 29 | 30 | return "No specific threat information found" 31 | } 32 | -------------------------------------------------------------------------------- /scanner/kaspersky.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "sync" 11 | "time" 12 | 13 | utils "github.com/gatariee/gocheck/utils" 14 | ) 15 | 16 | func KasperskyScan(file string, scanPath string, args ...string) (string, error) { 17 | scanCmd := exec.Command(scanPath, append(Kaspersky.Arguments, file)...) 18 | var out, stderr bytes.Buffer 19 | scanCmd.Stdout = &out 20 | scanCmd.Stderr = &stderr 21 | 22 | scanCmd.Run() 23 | 24 | output := out.String() 25 | return output, nil 26 | } 27 | 28 | func KasperskyRun(file string, scanPath string, debug bool) error { 29 | original_file, err := os.ReadFile(file) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | start := time.Now() 35 | ticker := time.NewTicker(time.Duration(2 * float64(time.Second))) 36 | defer ticker.Stop() 37 | 38 | progressUpdates := make(chan Progress) 39 | var wg sync.WaitGroup 40 | 41 | wg.Add(1) 42 | go func() { 43 | defer wg.Done() 44 | for { 45 | select { 46 | case <-ticker.C: 47 | progress, ok := <-progressUpdates 48 | if !ok { 49 | return 50 | } 51 | current := time.Since(start) 52 | utils.PrintErr(fmt.Sprintf("0x%X -> 0x%X - malicious: %t - %s", progress.Low, progress.High, progress.Malicious, current)) 53 | case _, ok := <-progressUpdates: 54 | /* we don't want the scanner to wait for ticker.C to reopen, so we need to handle this case */ 55 | if !ok { 56 | return 57 | } 58 | } 59 | } 60 | }() 61 | 62 | threat_names := make(chan string) 63 | threat_list := make([]string, 0) 64 | go func() { 65 | for { 66 | threat_name := <-threat_names 67 | threat_list = append(threat_list, threat_name) 68 | } 69 | }() 70 | 71 | size := len(original_file) 72 | 73 | /* Scan original file! */ 74 | output, err := KasperskyScan(file, scanPath) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | utils.PrintNewLine() 80 | 81 | if IsMalicious(output, Kaspersky.DetectionString) { 82 | /* We found something! */ 83 | utils.PrintErr("Threat detected in the original file, beginning binary search...") 84 | threat_names <- GetSignature(output, Kaspersky.SignatureString) 85 | } else { 86 | /* found nothing, time to die */ 87 | utils.PrintErr("No threat detected in the original file, dying now") 88 | return nil 89 | } 90 | 91 | tempDir := filepath.Join(".", "kaspersky") 92 | 93 | os.MkdirAll(tempDir, 0o755) 94 | testFilePath := filepath.Join(tempDir, "testfile.exe") 95 | 96 | lastGood := 0 // lower range 97 | upperBound := len(original_file) // upper range 98 | mid := upperBound / 2 // pivot point 99 | 100 | threatFound := false 101 | tf_lower := 0 102 | 103 | for upperBound-lastGood > 1 { 104 | err := os.WriteFile(testFilePath, original_file[tf_lower:mid], 0o644) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | utils.PrintDebug(fmt.Sprintf("Scanning from %d to %d bytes", tf_lower, mid), debug) 110 | 111 | output, err := KasperskyScan(testFilePath, scanPath) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | if IsMalicious(output, Kaspersky.DetectionString) { 117 | progressUpdates <- Progress{Low: tf_lower, High: mid, Malicious: true} 118 | utils.PrintDebug(fmt.Sprintf("Threat detected in the range %d to %d bytes", tf_lower, mid), debug) 119 | /* Found a threat */ 120 | threatFound = true 121 | upperBound = mid 122 | } else { 123 | progressUpdates <- Progress{Low: tf_lower, High: mid, Malicious: false} 124 | utils.PrintDebug(fmt.Sprintf("No threat detected in the range %d to %d bytes", tf_lower, mid), debug) 125 | /* No threat found */ 126 | lastGood = mid 127 | } 128 | 129 | mid = (lastGood + upperBound) / 2 130 | } 131 | 132 | os.RemoveAll(tempDir) 133 | end := time.Since(start) 134 | 135 | if threatFound { 136 | 137 | utils.PrintNewLine() 138 | utils.PrintOk(fmt.Sprintf("Kaspersky - %s", end)) 139 | utils.PrintErr(fmt.Sprintf("Isolated bad bytes at offset 0x%X in the file [approximately %d / %d bytes]", lastGood, lastGood, size)) 140 | 141 | start := lastGood - 32 142 | if start < 0 { 143 | start = 0 144 | } 145 | 146 | end := mid + 32 147 | if end > size { 148 | end = size 149 | } 150 | 151 | threatData := original_file[start:end] 152 | dump := hex.Dump(threatData) 153 | fmt.Println(dump) 154 | 155 | uniqueThreats := make(map[string]bool) 156 | for _, threat := range threat_list { 157 | uniqueThreats[threat] = true 158 | } 159 | 160 | for threat := range uniqueThreats { 161 | utils.PrintErr(threat) 162 | } 163 | 164 | } else { 165 | utils.PrintInfo("Not malicious") 166 | } 167 | 168 | ticker.Stop() 169 | close(progressUpdates) 170 | close(threat_names) 171 | 172 | return nil 173 | } 174 | 175 | func FindKaspersky() (string, error) { 176 | var avp string 177 | for _, path := range []string{Kaspersky.ScanPath, Kaspersky.AltScanPath} { 178 | if utils.CheckIfExists(path) { 179 | avp = path 180 | break 181 | } 182 | } 183 | 184 | return avp, nil 185 | } 186 | -------------------------------------------------------------------------------- /scanner/scanner.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "sync" 5 | 6 | utils "github.com/gatariee/gocheck/utils" 7 | ) 8 | 9 | type Scanner struct { 10 | File string 11 | Amsi bool 12 | Defender bool 13 | EnginePath string 14 | Additional map[string]string 15 | } 16 | 17 | func Run(token Scanner, debug bool) { 18 | var wg sync.WaitGroup 19 | errors := make(chan error, 2) 20 | 21 | if token.Defender { 22 | wg.Add(1) 23 | go func() { 24 | defer wg.Done() 25 | err := ScanWindef(token, debug) 26 | if err != nil { 27 | errors <- err 28 | } 29 | }() 30 | } 31 | 32 | if token.Amsi { 33 | wg.Add(1) 34 | go func() { 35 | defer wg.Done() 36 | err := ScanAMSI(token.File, debug) 37 | if err != nil { 38 | errors <- err 39 | } 40 | }() 41 | } 42 | 43 | if token.Additional != nil { 44 | for k, v := range token.Additional { 45 | switch k { 46 | case "kaspersky": 47 | wg.Add(1) 48 | go func(file, value string) { 49 | defer wg.Done() 50 | err := KasperskyRun(file, value, debug) 51 | if err != nil { 52 | errors <- err 53 | } 54 | }(token.File, v) 55 | } 56 | } 57 | } 58 | 59 | wg.Wait() 60 | close(errors) 61 | 62 | for err := range errors { 63 | if err != nil { 64 | utils.PrintErr(err.Error()) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scanner/windef.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | utils "github.com/gatariee/gocheck/utils" 14 | ) 15 | 16 | type DefenderScanner struct { 17 | Path string 18 | } 19 | 20 | type ScanResult string 21 | 22 | type Progress struct { 23 | Low int 24 | High int 25 | Malicious bool 26 | } 27 | 28 | const ( 29 | NoThreatFound ScanResult = "NoThreatFound" 30 | ThreatFound ScanResult = "ThreatFound" 31 | ThreatName ScanResult = "ThreatName" 32 | FileNotFound ScanResult = "FileNotFound" 33 | Timeout ScanResult = "Timeout" 34 | Error ScanResult = "Error" 35 | ) 36 | 37 | func newDefenderScanner(path string) *DefenderScanner { 38 | return &DefenderScanner{Path: path} 39 | } 40 | 41 | func (ds *DefenderScanner) Scan(filePath string, threat_names chan string) ScanResult { 42 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 43 | return FileNotFound 44 | } 45 | 46 | // TODO: convert to abs before passing into the function, if this errors out- it's not actually handled properly in the caller. 47 | absFilePath, err := filepath.Abs(filePath) 48 | if err != nil { 49 | return Error 50 | } 51 | 52 | cmd := exec.Command(ds.Path, "-Scan", "-ScanType", "3", "-File", absFilePath, "-DisableRemediation", "-Trace", "-Level", "0x10") 53 | var out, stderr bytes.Buffer 54 | cmd.Stdout = &out 55 | cmd.Stderr = &stderr 56 | 57 | cmd.Run() 58 | 59 | stdOut := out.String() 60 | 61 | if strings.Contains(stdOut, "Threat ") { 62 | 63 | threat := extractThreat(stdOut) 64 | 65 | /* yeah, there has to be a better way to do this. */ 66 | threat_names <- threat 67 | 68 | return ThreatFound 69 | } 70 | 71 | return NoThreatFound 72 | } 73 | 74 | func ScanWindef(token Scanner, debug bool) error { 75 | /* Setup */ 76 | scanner := newDefenderScanner(token.EnginePath) 77 | start := time.Now() 78 | 79 | ticker := time.NewTicker(time.Duration(2 * float64(time.Second))) 80 | defer ticker.Stop() 81 | progressUpdates := make(chan Progress) 82 | var wg sync.WaitGroup 83 | 84 | wg.Add(1) 85 | go func() { 86 | defer wg.Done() 87 | for { 88 | select { 89 | case <-ticker.C: 90 | progress, ok := <-progressUpdates 91 | if !ok { 92 | return 93 | } 94 | current := time.Since(start) 95 | utils.PrintErr(fmt.Sprintf("0x%X -> 0x%X - malicious: %t - %s", progress.Low, progress.High, progress.Malicious, current)) 96 | case _, ok := <-progressUpdates: 97 | /* ticker.C is not ready, but the channel is closed- we don't want the scanner to wait for ticker.C to reopen */ 98 | if !ok { 99 | return 100 | } 101 | } 102 | } 103 | }() 104 | 105 | utils.PrintDebug(fmt.Sprintf("Scanning %s with Windows Defender...", token.File), debug) 106 | 107 | original_file, err := os.ReadFile(token.File) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | /* Setup a channel to keep track of threat names */ 113 | threat_names := make(chan string) 114 | threat_list := make([]string, 0) 115 | go func() { 116 | for threat := range threat_names { 117 | threat_list = append(threat_list, threat) 118 | } 119 | }() 120 | 121 | size := len(original_file) 122 | utils.PrintInfo(fmt.Sprintf("Scanning %s, analysing %d bytes...", token.File, size)) 123 | 124 | /* Scan the original file, if the original file isn't flagged as malicious, don't begin the binary search */ 125 | if scanner.Scan(token.File, threat_names) == NoThreatFound { 126 | utils.PrintInfo("File looks clean, no threat detected") 127 | return nil 128 | } else { 129 | utils.PrintInfo("Threat detected in the original file, beginning binary search...") 130 | } 131 | 132 | utils.PrintNewLine() 133 | 134 | /* Create a temporary directory to store the scanning files */ 135 | tempDir := filepath.Join(".", "windef") 136 | 137 | /* TODO: Check whether the parent directory has an exclusion, or to perform a sanity check to ensure that MpCmdRun.exe is actually working */ 138 | os.MkdirAll(tempDir, 0o755) 139 | testFilePath := filepath.Join(tempDir, "testfile.exe") 140 | 141 | lastGood := 0 // lower range 142 | upperBound := len(original_file) // upper range 143 | mid := upperBound / 2 // pivot point 144 | 145 | threatFound := false 146 | tf_lower := 0 147 | tf_upper := upperBound 148 | tnf_upper := upperBound 149 | 150 | for upperBound-lastGood > 1 { 151 | // utils.PrintInfo(fmt.Sprintf("scanning from %d to %d bytes", lastGood, mid)) 152 | 153 | err := os.WriteFile(testFilePath, original_file[tf_lower:mid], 0o644) 154 | if err != nil { 155 | utils.PrintErr(fmt.Sprintf("failed to write to test file: %s", err)) 156 | return err 157 | } 158 | 159 | utils.PrintDebug(fmt.Sprintf("scanning from 0 to %d bytes", mid), debug) 160 | 161 | if scanner.Scan(testFilePath, threat_names) == ThreatFound { 162 | progressUpdates <- Progress{Low: tf_lower, High: mid, Malicious: true} 163 | 164 | utils.PrintDebug(fmt.Sprintf("threat detected in the range 0 to %d bytes", mid), debug) 165 | 166 | /* 167 | Since we found a threat in the slice, we'll set the upper range to whatever the pivot point is. 168 | */ 169 | threatFound = true 170 | upperBound = mid 171 | 172 | /* Save the last found threat range */ 173 | tf_upper = mid 174 | } else { 175 | progressUpdates <- Progress{Low: tf_lower, High: mid, Malicious: false} 176 | utils.PrintDebug(fmt.Sprintf("no threat detected in the range 0 to %d bytes", mid), debug) 177 | /* 178 | We didn't find a threat in this slice, so we flip to the other half of the slice. 179 | */ 180 | lastGood = mid // lower range becomes the pivot (middle) 181 | tnf_upper = mid 182 | } 183 | 184 | mid = lastGood + (upperBound-lastGood)/2 185 | 186 | utils.PrintDebugNewLine(debug) 187 | } 188 | 189 | /* Binary search is over, let's start cleaning up */ 190 | os.RemoveAll(tempDir) // incase the defer doesn't work for some reason 191 | end := time.Since(start) 192 | 193 | if threatFound { 194 | 195 | if debug { 196 | 197 | if _, err := os.Stat("debug"); os.IsNotExist(err) { 198 | os.Mkdir("debug", 0o755) 199 | } else { 200 | utils.PrintInfo("debug directory already exists, deleting contents and creating new files") 201 | os.RemoveAll("debug") 202 | utils.PrintOk("deleted debug directory") 203 | os.Mkdir("debug", 0o755) 204 | } 205 | 206 | utils.PrintDebugNewLine(debug) 207 | 208 | utils.PrintDebug(fmt.Sprintf("%d to %d bytes were: NOT MALICIOUS ", tf_lower, tnf_upper), debug) 209 | utils.PrintDebug(fmt.Sprintf("%d to %d bytes were: MALICIOUS ", tf_lower, tf_upper), debug) 210 | 211 | err = os.WriteFile("./debug/last_bad_bytes.exe", original_file[tf_lower:tf_upper], 0o644) 212 | if err != nil { 213 | utils.PrintErr(fmt.Sprintf("failed to write to last_bad_bytes.exe: %s", err)) 214 | return err 215 | } 216 | 217 | utils.PrintInfo("Saving last bad bytes to: last_bad_bytes.exe") 218 | 219 | file, err := os.ReadFile("./debug/last_bad_bytes.exe") 220 | if err != nil { 221 | utils.PrintErr(fmt.Sprintf("failed to read last_bad_bytes.exe: %s", err)) 222 | return err 223 | } 224 | fs := len(file) 225 | utils.PrintInfo(fmt.Sprintf("Scanning last_bad_bytes.exe, analysing %d bytes...", fs)) 226 | 227 | if scanner.Scan("./debug/last_bad_bytes.exe", threat_names) == ThreatFound { 228 | utils.PrintOk(fmt.Sprintf("Sanity check passed, windows defender detected a threat in 'last_bad_bytes.exe' [0x0 to 0x%X]", fs)) 229 | } else { 230 | utils.PrintErr("Sanity check failed, windows defender did not detect a threat in the last bad bytes") 231 | } 232 | 233 | utils.PrintDebugNewLine(debug) 234 | utils.PrintInfo("Saving last good bytes to: last_good_bytes.exe") 235 | 236 | err = os.WriteFile("./debug/last_good_bytes.exe", original_file[tf_lower:tnf_upper], 0o644) 237 | if err != nil { 238 | utils.PrintErr(fmt.Sprintf("failed to write to last_good_bytes.exe: %s", err)) 239 | return err 240 | } 241 | file, err = os.ReadFile("./debug/last_good_bytes.exe") 242 | if err != nil { 243 | utils.PrintErr(fmt.Sprintf("failed to read last_good_bytes.exe: %s", err)) 244 | return err 245 | } 246 | 247 | fs = len(file) 248 | utils.PrintInfo(fmt.Sprintf("Scanning last_good_bytes.exe, analysing %d bytes...", fs)) 249 | 250 | if scanner.Scan("last_good_bytes.bin", threat_names) == ThreatFound { 251 | utils.PrintErr(fmt.Sprintf("Sanity check failed, windows defender detected a threat in 'last_good_bytes.exe' [0x0 to 0x%X]", fs)) 252 | } else { 253 | utils.PrintOk(fmt.Sprintf("Sanity check passed, windows defender did not detect a threat in 'last_good_bytes.exe' [0x0 to 0x%X]", fs)) 254 | } 255 | 256 | utils.PrintDebugNewLine(debug) 257 | } 258 | 259 | /* 260 | This only hits once the binary search has been exhausted, i.e: the range between the upperBound and lastGood is 0 261 | TODO: Check if an off-by-one error appears here for binaries with an odd number of bytes lol. 262 | */ 263 | 264 | utils.PrintNewLine() 265 | 266 | utils.PrintErr(fmt.Sprintf("Isolated bad bytes at offset 0x%X in the original file [approximately %d / %d bytes]", lastGood, lastGood, size)) 267 | /* Add 32 bytes before the offset */ 268 | start := lastGood - 32 269 | if start < 0 { 270 | start = 0 271 | } 272 | 273 | /* Start printing the hex dump */ 274 | threatData := original_file[start:lastGood] 275 | fmt.Println(HexDump(threatData)) 276 | 277 | uniqueThreats := make(map[string]bool) 278 | for _, threat := range threat_list { 279 | uniqueThreats[threat] = true 280 | } 281 | 282 | for threat := range uniqueThreats { 283 | utils.PrintInfo(threat) 284 | } 285 | utils.PrintOk(fmt.Sprintf("Windows Defender - %s", end)) 286 | } else { 287 | utils.PrintInfo("No threat detected, but the original file was flagged as malicious. The bad bytes are likely at the very end of the binary.") 288 | } 289 | 290 | /* End */ 291 | ticker.Stop() 292 | os.Remove(testFilePath) 293 | close(progressUpdates) 294 | close(threat_names) 295 | 296 | return nil 297 | } 298 | -------------------------------------------------------------------------------- /utils/utilities.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "github.com/fatih/color" 7 | ) 8 | 9 | func PrintNewLine() { 10 | fmt.Println("") 11 | } 12 | 13 | func Print(msg string) { 14 | fmt.Println(msg) 15 | } 16 | 17 | func PrintOk(msg string) { 18 | color.HiGreen(fmt.Sprintf("[+] %s", msg)) 19 | } 20 | 21 | func PrintInfo(msg string) { 22 | color.Yellow(fmt.Sprintf("[*] %s", msg)) 23 | } 24 | 25 | func PrintErr(msg string) { 26 | color.HiRed(fmt.Sprintf("[!] %s", msg)) 27 | } 28 | 29 | func PrintDebug(msg string, debug bool) { 30 | if debug { 31 | fmt.Println("[DEBUG]", msg) 32 | } 33 | } 34 | 35 | func PrintDebugNewLine(debug bool) { 36 | if debug { 37 | fmt.Println("") 38 | } 39 | } 40 | 41 | func CheckIfExists(path string) bool { 42 | _, err := os.Stat(path) 43 | return err == nil 44 | } 45 | --------------------------------------------------------------------------------