├── .gitignore ├── LICENSE ├── README.md ├── README.windows-compilation.md ├── analysis.go ├── configuration.go ├── configuration.yaml ├── faker.go ├── filehelper.go ├── go.mod ├── go.sum ├── logger.go ├── main.go ├── networkcapture.go ├── procsmemory.go ├── resources ├── fake_process.exe └── winrar_sfx.exe ├── sfxbuilder.go ├── utils.go ├── w32.go ├── windowslnkparser.go ├── windowsregistry.go ├── windowstaskscheduler.go └── yaraProcessing.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | quarantine/* 3 | yara-signatures/* 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jean-Pierre GARNIER 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IRMA - Incident Response - Minimal Analysis 2 | 3 | ## What is this project designed for? 4 | _IRMA_ is a lightweight tool made for live forensics on Windows Platform. It is 5 | focused on three use cases: 6 | * enpoint detection - live analysis, quarantine and eradication of malware on a workstation 7 | * live analysis & sandbox host - logging and instant notifications for malware TTP's assessment 8 | * signatures quality test - scan your endpoint baseline and check for false positives 9 | 10 | ## How IRMA scan for malware behaviour? 11 | _IRMA_ is intended to work with both user or administrator rights. 12 | Based on your user privileges it can: 13 | * implements the YARA library and regularly scan the workstation's files and memory 14 | * search for execution context (parent process, regkey, scheduled task persistence) 15 | Every suspect behaviour could be text logged, notified to the user, and/or eradicated 16 | 17 | ## What does it scan? 18 | Currently, _IRMA_ is able to: 19 | * list running processes and log for suspiscious actions 20 | * list common persistence mecanisms (registry keys / scheduled tasks / startup folder links) 21 | * perform YARA scan on files and memory 22 | * dump / quarantine suspiscious artefacts 23 | * spawn fake analysis processes to make the computer look like an analysis platform 24 | 25 | ### Installation 26 | Feel free to download compiled release of this software. If you want to compile 27 | from sources, it could be a little bit tricky cause it's stronly depends of 28 | _go-yara_ and CGO compilation. You'll find a detailed documentation [here](README.windows-compilation.md) 29 | 30 | ### Usage 31 | ``` 32 | usage: irma [-h|--help] -c|--configuration "" [-b|--builder ""] 33 | 34 | Incident Response - Minimal Analysis 35 | 36 | Arguments: 37 | 38 | -h --help Print help information 39 | -c --configuration yaml configuration file 40 | -b --builder create a standalone launcher executable with packed 41 | rules and configuration. 42 | ``` 43 | 44 | ### Scan according to your needs 45 | _IRMA_ embeds a configuration file in order to define which files to scan, and 46 | where to scan them. 47 | 48 | ``` 49 | irma.exe -c configuration.yaml 50 | ``` 51 | 52 | ### EDR, rules and configuration packing 53 | _IRMA_ builder mode lets you create a standalone, static compiled, self-extracting 54 | archive. It contains irma binary, configuration file, and signatures. Hence, this 55 | binary could be deployed on any other system and launch without additional 56 | configuration. 57 | 58 | ``` 59 | irma.exe -c configuration.yaml -b irma-sfx-binary.exe 60 | ``` 61 | 62 | ## About this project and future versions 63 | I undertook this project initially in order to learn Go. Then little by little 64 | I tried to understand how to use the Win32 API and finally to read the process 65 | memory on a Windows system. Initially focused on system oriented live forensics, 66 | I plan to enhance _IRMA_ functionalities with network based detection & analysis. 67 | 68 | Further versions may contains: 69 | * SNORT/Suricata rules analysis 70 | * Transfer of analysis results to a SIEM 71 | * Agent management platform - Command and control ability 72 | 73 | Feel free to ask for new features or create pull request if your interested in 74 | this project. 75 | -------------------------------------------------------------------------------- /README.windows-compilation.md: -------------------------------------------------------------------------------- 1 | 2 | # Installing IRMA on Windows 3 | 4 | _IRMA_ can be installed on a Windows platform but it's a little bit tricky because it's strongly dependant of go-yara and CGO. Here's a little step by step guide: 5 | 6 | ## Before installation 7 | 8 | All the installation process will be done with msys2/mingw terminal. In order to avoid any error, you have to ensure that your installation directories don't contains space or special characters. I haven't tested to install as a simple user, I strongly advise you to install everything with admin privileges on top of your c:\ drive. 9 | 10 | For the configurations and examples below, my install paths are: 11 | 12 | * GO: c:\Go 13 | * GOPATH: C:\Users\myuser\go 14 | * Msys2: c:\msys64 15 | * Git: c:\Git 16 | 17 | ## Install msys2 and dependencies: 18 | 19 | First of all, note that you won't be able to get _IRMA_ working if the dependencies are compiled with another compiler than GCC. There is currently some problems with CGO when external libraries are compiled with Visual C++, so no need to install Visual Studio or vcpkg. 20 | 21 | * Download msys2 [from the official website](https://www.msys2.org/) and install it 22 | * there, you will find two distincts binaries shorcut "MSYS2 MSYS" and "MSYS2 MinGW 64bits". Please launch this second one. 23 | * install dependencies with the following command line: `pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-pkg-config base-devel openssl-devel` 24 | * add environment variables in mingw terminal: `export PATH=$PATH:/c/Go/bin:/c/msys64/mingw64/bin:/c/Git/bin` 25 | 26 | ## Download and compile libyara 27 | 28 | It's strongly advised NOT to clone VirusTotal's YARA repository but to download the source code of the latest release. If you compile libyara from the latest commit, it could generate some side effects when linking this library with _IRMA_ and GCO. 29 | 30 | * download latest VirusTotal release source [from here](https://github.com/VirusTotal/yara/releases) 31 | * unzip the folder in a directory without space and special char 32 | * in mingw terminal, go to yara directory (backslash have to be replace with slash eg. cd c:/yara) 33 | * compile and install using the following command: `./bootstrap.sh &&./configure && make && make install` 34 | 35 | ## Configure your OS 36 | 37 | With this step, you won't need to use mingw terminal anymore and you will be able to use Go to install _IRMA_ and compile your projects directly from Windows cmd / powershell. 38 | 39 | Make sure you have the following as system environment variables (not user env vars). If not, create them: 40 | ``` 41 | GOARCH= (eg. amd64) 42 | GOOS=windows 43 | CGO_CFLAGS=-IC:/msys64/mingw64/include 44 | CGO_LDFLAGS=-LC:/msys64/mingw64/lib -lyara -lcrypto 45 | PKG_CONFIG_PATH=C:/msys64/mingw64/lib/pkgconfig 46 | ``` 47 | You also need C:\msys64\mingw64\bin in your system PATH env vars. 48 | 49 | Make sure you have got the following user environment var (not system var): 50 | 51 | GOPATH=%USERPROFILE%\go 52 | 53 | Note that paths must be written with slashs and not backslash. As already said, don't use path with spaces or special characters. 54 | 55 | ## Download, Install and compile IRMA 56 | Now, from Windows cmd or Powershell, you can install _IRMA_: `go get github.com/codeyourweb/irma` 57 | Compilation should be done with: `go build -tags yara_static -a -ldflags '-extldflags "-static"' .` 58 | -------------------------------------------------------------------------------- /analysis.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/rc4" 6 | b64 "encoding/base64" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "runtime/debug" 11 | "time" 12 | 13 | "github.com/h2non/filetype" 14 | "github.com/hillu/go-yara/v4" 15 | ) 16 | 17 | // FileDescriptor wrap path, filehash and last update into a structure. It is used for performance improvements and avoid reading file if it has not changed 18 | type FileDescriptor struct { 19 | FilePath string 20 | FileSize int64 21 | LastModified time.Time 22 | } 23 | 24 | // FileAnalysis sub-routine for file analysis (used in registry / task scheduler / startmenu scan) 25 | func FileAnalysis(path string, pQuarantine string, pKill bool, pNotifications bool, pVerbose bool, rules *yara.Rules, sourceIndex string) { 26 | var f os.FileInfo 27 | var err error 28 | var content []byte 29 | var result yara.MatchRules 30 | 31 | if f, err = os.Stat(path); err != nil { 32 | if pVerbose { 33 | logMessage(LOG_ERROR, "[ERROR]", path, err) 34 | } 35 | } else { 36 | if RegisterFileInHistory(f, path, &filescanHistory, pVerbose) { 37 | 38 | content, err = os.ReadFile(path) 39 | if err != nil && pVerbose { 40 | logMessage(LOG_ERROR, "[ERROR]", path, err) 41 | } 42 | 43 | filetype, err := filetype.Match(content) 44 | if err != nil && pVerbose { 45 | logMessage(LOG_ERROR, "[ERROR]", path, err) 46 | } 47 | 48 | if pVerbose { 49 | logMessage(LOG_INFO, "[INFO] ["+sourceIndex+"] Analyzing", path, fmt.Sprintf("%x", md5.Sum(content))) 50 | } 51 | 52 | // cleaning memory if file size is greater than 512Mb 53 | if len(content) > 1024*1024*cleanIfFileSizeGreaterThan { 54 | defer debug.FreeOSMemory() 55 | } 56 | 57 | // archive or other file format scan 58 | if StringInSlice(filetype.MIME.Value, archivesFormats) { 59 | result = PerformArchiveYaraScan(path, rules, pVerbose) 60 | } else { 61 | result = PerformYaraScan(&content, rules, pVerbose) 62 | } 63 | 64 | if len(result) > 0 { 65 | // windows notifications 66 | if pNotifications { 67 | NotifyUser("YARA match", path+" match "+fmt.Sprint(len(result))+" rules") 68 | } 69 | 70 | // logging 71 | for _, match := range result { 72 | logMessage(LOG_INFO, "[ALERT]", "["+sourceIndex+"] YARA match", path, match.Namespace, match.Rule) 73 | } 74 | 75 | // kill 76 | if pKill { 77 | killQueue = append(killQueue, path) 78 | } 79 | 80 | // dump matching file to quarantine 81 | if len(pQuarantine) > 0 { 82 | logMessage(LOG_INFO, "[INFO]", "Dumping file", path) 83 | err := QuarantineFile(path, pQuarantine) 84 | if err != nil { 85 | logMessage(LOG_ERROR, "[ERROR]", "Cannot quarantine file", path, err) 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | // MemoryAnalysis sub-routine for running processes analysis 94 | func MemoryAnalysis(proc *ProcessInformation, pQuarantine string, pKill bool, pNotifications bool, pVerbose bool, rules *yara.Rules) { 95 | if pVerbose { 96 | logMessage(LOG_INFO, "[INFO] [MEMORY] Analyzing", proc.ProcessName, "PID:", proc.PID) 97 | } 98 | 99 | result := PerformYaraScan(&proc.MemoryDump, rules, pVerbose) 100 | if len(result) > 0 { 101 | // windows notifications 102 | if pNotifications { 103 | NotifyUser("YARA match", proc.ProcessName+" - PID:"+fmt.Sprint(proc.PID)+" match "+fmt.Sprint(len(result))+" rules") 104 | } 105 | 106 | // logging 107 | for _, match := range result { 108 | logMessage(LOG_INFO, "[ALERT]", "[MEMORY] YARA match", proc.ProcessName, "PID:", fmt.Sprint(proc.PID), match.Namespace, match.Rule) 109 | } 110 | 111 | // dump matching process to quarantine 112 | if len(pQuarantine) > 0 { 113 | logMessage(LOG_INFO, "[INFO]", "DUMPING PID", proc.PID) 114 | err := QuarantineProcess(proc, pQuarantine) 115 | if err != nil { 116 | logMessage(LOG_ERROR, "[ERROR]", "Cannot quarantine PID", proc.PID, err) 117 | } 118 | } 119 | 120 | // killing process 121 | if pKill { 122 | logMessage(LOG_INFO, "[INFO]", "KILLING PID", proc.PID) 123 | KillProcessByID(proc.PID, pVerbose) 124 | } 125 | } 126 | 127 | } 128 | 129 | // QuarantineProcess dump process memory and cipher them in quarantine folder 130 | func QuarantineProcess(proc *ProcessInformation, quarantinePath string) (err error) { 131 | 132 | err = quarantineContent(proc.MemoryDump, proc.ProcessName+fmt.Sprint(proc.PID)+".mem", quarantinePath) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | err = QuarantineFile(proc.ProcessPath, quarantinePath) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | return nil 143 | } 144 | 145 | // QuarantineFile dump specified file and cipher them in quarantine folder 146 | func QuarantineFile(path, quarantinePath string) (err error) { 147 | fileContent, err := os.ReadFile(path) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | err = quarantineContent(fileContent, filepath.Base(path), quarantinePath) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | return nil 158 | } 159 | 160 | // quarantineContent copy and encrypt suspicious content 161 | func quarantineContent(content []byte, filename string, quarantinePath string) (err error) { 162 | _, err = os.Stat(quarantinePath) 163 | if os.IsNotExist(err) { 164 | if err := os.MkdirAll(quarantinePath, 0600); err != nil { 165 | return err 166 | } 167 | } 168 | 169 | c, err := rc4.NewCipher([]byte(quarantineKey)) 170 | if err != nil { 171 | return err 172 | } 173 | 174 | xPE := make([]byte, len(content)) 175 | c.XORKeyStream(xPE, content) 176 | err = os.WriteFile(quarantinePath+"/"+filename+".irma", []byte(b64.StdEncoding.EncodeToString(xPE)), 0644) 177 | if err != nil { 178 | return err 179 | } 180 | 181 | return nil 182 | } 183 | -------------------------------------------------------------------------------- /configuration.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | type Configuration struct { 11 | Yara struct { 12 | Path string `yaml:"path"` 13 | Rulesrc4key string `yaml:"rulesRC4Key"` 14 | } 15 | Yarascan struct { 16 | Memory bool `yaml:"memory"` 17 | Registry bool `yaml:"registry"` 18 | Startmenu bool `yaml:"startmenu"` 19 | Taskscheduler bool `yaml:"taskscheduler"` 20 | SystemDrive bool `yaml:"systemdrive"` 21 | Userfilesystem bool `yaml:"userfilesystem"` 22 | AbsolutePaths []string `yaml:"absolutePaths"` 23 | InfiniteScan bool `yaml:"infinitescan"` 24 | AbsolutePathsRecursive bool `yaml:"absolutePathsRecursive"` 25 | } 26 | Response struct { 27 | DumpDirectory string `yaml:"dumpDirectory"` 28 | QuarantineDirectory string `yaml:"Directory"` 29 | QuarantineRC4Key string `yaml:"quarantineRC4Key"` 30 | Kill bool `yaml:"kill"` 31 | } 32 | Network struct { 33 | Capture bool `yaml:"capture"` 34 | Bpffilter string `yaml:"bpffilter"` 35 | Pcapfile string `yaml:"pcapfile"` 36 | } 37 | Output struct { 38 | Notifications bool `yaml:"notifications"` 39 | Verbose bool `yaml:"verbose"` 40 | } 41 | Sfx struct { 42 | Autoexec bool `yaml:"autoexec"` 43 | SilentMode bool `yaml:"silentmode"` 44 | ExtractDirectory string `yaml:"extractDirectory"` 45 | LogFile string `yaml:"logfile"` 46 | } 47 | Others struct { 48 | FakeProcesses bool `yaml:"fakeProcesses"` 49 | } 50 | Advancedparameters struct { 51 | MaxScanFilesize int `yaml:"maxScanFilesize"` 52 | CleanMemoryIfFileGreaterThanSize int `yaml:"cleanMemoryIfFileGreaterThanSize"` 53 | Extensions []string `yaml:"extensions"` 54 | } 55 | } 56 | 57 | func (c *Configuration) getConfiguration(configFile string) *Configuration { 58 | 59 | yamlFile, err := ioutil.ReadFile(configFile) 60 | if err != nil { 61 | log.Fatalf("Configuration file reading error #%v ", err) 62 | } 63 | err = yaml.Unmarshal(yamlFile, c) 64 | if err != nil { 65 | log.Fatalf("Configuration file parsing error: %v", err) 66 | } 67 | 68 | return c 69 | } 70 | -------------------------------------------------------------------------------- /configuration.yaml: -------------------------------------------------------------------------------- 1 | yara: 2 | path: "yara-signatures/" # yara rules absolute or relative folder (files have to end wih *.yar) 3 | rulesRC4Key: "" # yara rules can be ciphered with rc4 4 | yarascan: 5 | infinitescan: true # loop scan infinitely 6 | memory: true # scan process memory 7 | registry: true # scan windows registry 8 | startmenu: true # scan users start menu lnk 9 | taskscheduler: true # scan windows task scheduler 10 | userfilesystem: true # scan users %USERPROFILE% folder 11 | systemdrive: true # scan %SYSTEMDRIVE% folder 12 | absolutePaths: [] # list custom directory scan 13 | absolutePathsRecursive: true # recurse inside custom directories 14 | response: 15 | dumpDirectory: "" # dump every processes memory for further analysis 16 | quarantineDirectory: "" # when a file match a rule, dump its memory and executable to the following directory 17 | quarantineRC4Key: "irma" # key used to RC4 cipher memory dump and quarantined files 18 | kill: false # try to kill a process whether it match a yara rule 19 | network: 20 | capture: false # capture network trafic (irma will run infinitely) 21 | bpffilter: ~ # filter capture using Berkley Packet Filter 22 | pcapfile: "capture.pcap" # capture file path 23 | output: 24 | notifications: true # use Windows 10 notification panel when processes memory or files trigger a yara rule match 25 | verbose: true # verbose console output (yara match does not need this option) 26 | sfx: 27 | autoexec: true # auto execute irma after unzipping 28 | silentmode: true # prompt user to let him choose where to extract irma 29 | extractDirectory: "%temp%" # default zip extract directory 30 | logfile: "irma.log" # output irma log to the following filepath 31 | others: 32 | fakeProcesses: false # spawn and execute fake analysis process to mislead malware and make it stop if anti-debug is analysed 33 | advancedparameters: 34 | maxScanFilesize: 1024 # max file size scan (default: 1024Mb) 35 | cleanMemoryIfFileGreaterThanSize: 512 # force irma clean its memory when using lot of memory (default: after >512Mb file size scan) 36 | extensions: 37 | - ".txt" 38 | - ".csv" 39 | - ".htm" 40 | - ".html" 41 | - ".flv" 42 | - ".f4v" 43 | - ".avi" 44 | - ".3gp" 45 | - ".3g2" 46 | - ".3gp2" 47 | - ".3p2" 48 | - ".divx" 49 | - ".mp4" 50 | - ".mkv" 51 | - ".mov" 52 | - ".qt" 53 | - ".asf" 54 | - ".wmv" 55 | - ".rm" 56 | - ".rmvb" 57 | - ".vob" 58 | - ".dat" 59 | - ".mpg" 60 | - ".mpeg" 61 | - ".bik" 62 | - ".fcs" 63 | - ".mp3" 64 | - ".mpeg3" 65 | - ".flac" 66 | - ".ape" 67 | - ".ogg" 68 | - ".aac" 69 | - ".m4a" 70 | - ".wma" 71 | - ".ac3" 72 | - ".wav" 73 | - ".mka" 74 | - ".rm" 75 | - ".ra" 76 | - ".ravb" 77 | - ".mid" 78 | - ".midi" 79 | - ".cda" 80 | - ".jpg" 81 | - ".jpe" 82 | - ".jpeg" 83 | - ".jff" 84 | - ".gif" 85 | - ".png" 86 | - ".bmp" 87 | - ".tif" 88 | - ".tiff" 89 | - ".emf" 90 | - ".wmf" 91 | - ".eps" 92 | - ".psd" 93 | - ".cdr" 94 | - ".swf" 95 | - ".exe" 96 | - ".lnk" 97 | - ".dll" 98 | - ".ps1" 99 | - ".scr" 100 | - ".ocx" 101 | - ".com" 102 | - ".sys" 103 | - ".class" 104 | - ".o" 105 | - ".so" 106 | - ".elf" 107 | - ".prx" 108 | - ".vb" 109 | - ".vbs" 110 | - ".js" 111 | - ".bat" 112 | - ".cmd" 113 | - ".msi" 114 | - ".msp" 115 | - ".deb" 116 | - ".rpm" 117 | - ".sh" 118 | - ".pl" 119 | - ".dylib" 120 | - ".doc" 121 | - ".dot" 122 | - ".docx" 123 | - ".dotx" 124 | - ".docm" 125 | - ".dotm" 126 | - ".xsl" 127 | - ".xls" 128 | - ".xlsx" 129 | - ".xltx" 130 | - ".xlsm" 131 | - ".xltm" 132 | - ".xlam" 133 | - ".xlsb" 134 | - ".ppt" 135 | - ".pot" 136 | - ".pps" 137 | - ".pptx" 138 | - ".potx" 139 | - ".pptm" 140 | - ".potm" 141 | - ".ppsx" 142 | - ".ppsm" 143 | - ".rtf" 144 | - ".pdf" 145 | - ".msg" 146 | - ".eml" 147 | - ".vsd" 148 | - ".vss" 149 | - ".vst" 150 | - ".vdx" 151 | - ".vsx" 152 | - ".vtx" 153 | - ".xps" 154 | - ".oxps" 155 | - ".one" 156 | - ".onepkg" 157 | - ".xsn" 158 | - ".odt" 159 | - ".ods" 160 | - ".odp" 161 | - ".sxw" 162 | - ".pub" 163 | - ".mdb" 164 | - ".accdb" 165 | - ".accde" 166 | - ".accdr" 167 | - ".accdc" 168 | - ".chm" 169 | - ".mht" 170 | - ".zip" 171 | - ".tar" 172 | - ".7z" 173 | - ".7-z" 174 | - ".rar" 175 | - ".iso" 176 | - ".cab" 177 | - ".jar" 178 | - ".arj" 179 | - ".dmg" 180 | - ".smi" 181 | - ".img" 182 | - ".xar" -------------------------------------------------------------------------------- /faker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "os" 6 | "syscall" 7 | ) 8 | 9 | //go:embed resources/fake_process.exe 10 | var faker []byte 11 | 12 | // SpawnFakeProcess drop an useless process and execute it 13 | func SpawnFakeProcess(processName string) (err error) { 14 | err = os.WriteFile(processName, faker, 0644) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | var sI syscall.StartupInfo 20 | var pI syscall.ProcessInformation 21 | argv := syscall.StringToUTF16Ptr(processName) 22 | err = syscall.CreateProcess(nil, argv, nil, nil, true, 0, nil, nil, &sI, &pI) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /filehelper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | "strings" 9 | "time" 10 | 11 | "github.com/hillu/go-yara/v4" 12 | ) 13 | 14 | // WindowsFileSystemAnalysisRoutine analyse windows filesystem every 300 seconds 15 | func WindowsFileSystemAnalysisRoutine(pQuarantine string, pKill bool, pNotifications bool, pVerbose bool, pInfiniteLoop bool, rules *yara.Rules) { 16 | for { 17 | env := ListEnvironmentPathFiles(pVerbose) 18 | temp := ListTemporaryFiles(pVerbose) 19 | 20 | for _, p := range env { 21 | FileAnalysis(p, pQuarantine, pKill, pNotifications, pVerbose, rules, "ENV") 22 | } 23 | 24 | for _, p := range temp { 25 | FileAnalysis(p, pQuarantine, pKill, pNotifications, pVerbose, rules, "TEMP") 26 | } 27 | 28 | if !pInfiniteLoop { 29 | wg.Done() 30 | break 31 | } else { 32 | time.Sleep(300 * time.Second) 33 | } 34 | } 35 | } 36 | 37 | // UserFileSystemAnalysisRoutine analyse windows filesystem every 60 seconds 38 | func UserFileSystemAnalysisRoutine(pQuarantine string, pKill bool, pNotifications bool, pVerbose bool, pInfiniteLoop bool, rules *yara.Rules) { 39 | for { 40 | files := ListUserWorkspaceFiles(pVerbose) 41 | 42 | for _, p := range files { 43 | FileAnalysis(p, pQuarantine, pKill, pNotifications, pVerbose, rules, "USER") 44 | } 45 | 46 | if !pInfiniteLoop { 47 | wg.Done() 48 | break 49 | } else { 50 | time.Sleep(60 * time.Second) 51 | } 52 | } 53 | } 54 | 55 | // ListUserWorkspaceFiles recursively list all files in USERPROFILE directory 56 | func ListUserWorkspaceFiles(verbose bool) (files []string) { 57 | f, err := RetrivesFilesFromUserPath(os.Getenv("USERPROFILE"), true, defaultScannedFileExtensions, true, verbose) 58 | if err != nil && verbose { 59 | logMessage(LOG_INFO, err) 60 | } 61 | 62 | for _, i := range f { 63 | files = append(files, i) 64 | } 65 | return files 66 | } 67 | 68 | // ListEnvironmentPathFiles list all files in PATH directories 69 | func ListEnvironmentPathFiles(verbose bool) (files []string) { 70 | env := os.Getenv("PATH") 71 | paths := strings.Split(env, ";") 72 | for _, p := range paths { 73 | f, err := RetrivesFilesFromUserPath(p, true, defaultScannedFileExtensions, false, verbose) 74 | if err != nil { 75 | if verbose { 76 | logMessage(LOG_ERROR, err) 77 | } 78 | continue 79 | } 80 | 81 | for _, i := range f { 82 | files = append(files, i) 83 | } 84 | } 85 | 86 | return files 87 | } 88 | 89 | // ListTemporaryFiles list all files in TEMP / TMP / %SystemRoot%\Temp 90 | func ListTemporaryFiles(verbose bool) (files []string) { 91 | 92 | var folders = []string{os.Getenv("TEMP")} 93 | if os.Getenv("TMP") != os.Getenv("TEMP") { 94 | folders = append(folders, os.Getenv("TMP")) 95 | } 96 | 97 | if os.Getenv("SystemRoot")+`\Temp` != os.Getenv("TEMP") { 98 | folders = append(folders, os.Getenv("SystemRoot")+`\Temp`) 99 | } 100 | 101 | for _, p := range folders { 102 | f, err := RetrivesFilesFromUserPath(p, true, defaultScannedFileExtensions, true, verbose) 103 | if err != nil { 104 | if verbose { 105 | logMessage(LOG_INFO, err) 106 | } 107 | continue 108 | } 109 | 110 | for _, i := range f { 111 | files = append(files, i) 112 | } 113 | } 114 | 115 | return files 116 | } 117 | 118 | // FormatPathFromComplexString search for file/directory path and remove environments variables, quotes and extra parameters 119 | func FormatPathFromComplexString(command string) (paths []string) { 120 | var buffer []string 121 | 122 | // quoted path 123 | if strings.Contains(command, `"`) || strings.Contains(command, `'`) { 124 | re := regexp.MustCompile(`[\'\"](.+)[\'\"]`) 125 | matches := re.FindStringSubmatch(command) 126 | for i := range matches { 127 | if i != 0 { 128 | buffer = append(buffer, matches[i]) 129 | } 130 | } 131 | } else { 132 | for _, i := range strings.Split(strings.Replace(command, ",", "", -1), " ") { 133 | buffer = append(buffer, i) 134 | } 135 | 136 | } 137 | 138 | for _, item := range buffer { 139 | // environment variables 140 | if strings.Contains(command, `%`) { 141 | re := regexp.MustCompile(`%(\w+)%`) 142 | res := re.FindStringSubmatch(item) 143 | for i := range res { 144 | item = strings.Replace(item, "%"+res[i]+"%", os.Getenv(res[i]), -1) 145 | } 146 | } 147 | 148 | // check if file exists 149 | if _, err := os.Stat(item); !os.IsNotExist(err) { 150 | paths = append(paths, item) 151 | } 152 | } 153 | 154 | return paths 155 | } 156 | 157 | // RetrivesFilesFromUserPath return a []string of available files from given path (includeFileExtensions is available only if listFiles is true) 158 | func RetrivesFilesFromUserPath(path string, listFiles bool, includeFileExtensions []string, recursive bool, verbose bool) ([]string, error) { 159 | var p []string 160 | 161 | info, err := os.Stat(path) 162 | if os.IsNotExist(err) { 163 | return []string{}, errors.New("Input file not found") 164 | } 165 | 166 | if !info.IsDir() { 167 | p = append(p, path) 168 | } else { 169 | if !recursive { 170 | files, err := os.ReadDir(path) 171 | if err != nil { 172 | return []string{}, err 173 | } 174 | for _, f := range files { 175 | if !(f.IsDir() == listFiles) && (len(includeFileExtensions) == 0 || StringInSlice(filepath.Ext(f.Name()), includeFileExtensions)) { 176 | p = append(p, path+string(os.PathSeparator)+f.Name()) 177 | } 178 | } 179 | } else { 180 | err := filepath.Walk(path, func(walk string, info os.FileInfo, err error) error { 181 | if err != nil && verbose { 182 | logMessage(LOG_ERROR, "[ERROR]", err) 183 | } 184 | 185 | if err == nil && !(info.IsDir() == listFiles) && (len(includeFileExtensions) == 0 || StringInSlice(filepath.Ext(walk), includeFileExtensions)) { 186 | p = append(p, walk) 187 | } 188 | 189 | return nil 190 | }) 191 | 192 | if err != nil && verbose { 193 | logMessage(LOG_ERROR, "[ERROR]", err) 194 | } 195 | } 196 | } 197 | 198 | return p, nil 199 | } 200 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/codeyourweb/irma 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/akamensky/argparse v1.2.2 7 | github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28 8 | github.com/gen2brain/go-unarr v0.1.1 9 | github.com/go-ole/go-ole v1.2.5 10 | github.com/google/gopacket v1.1.19 11 | github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe // indirect 12 | github.com/h2non/filetype v1.1.1 13 | github.com/hillu/go-yara/v4 v4.0.4 14 | github.com/mattn/go-runewidth v0.0.10 // indirect 15 | github.com/olekukonko/tablewriter v0.0.5 // indirect 16 | github.com/parsiya/golnk v0.0.0-20200515071614-5db3107130ce 17 | github.com/rivo/uniseg v0.2.0 // indirect 18 | golang.org/x/net v0.0.0-20210324205630-d1beb07c2056 // indirect 19 | golang.org/x/sys v0.0.0-20210324051608-47abb6519492 20 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/akamensky/argparse v1.2.2 h1:P17T0ZjlUNJuWTPPJ2A5dM1wxarHgHqfYH+AZTo2xQA= 2 | github.com/akamensky/argparse v1.2.2/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= 3 | github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28 h1:M2Zt3G2w6Q57GZndOYk42p7RvMeO8izO8yKTfIxGqxA= 4 | github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8= 5 | github.com/gen2brain/go-unarr v0.1.1 h1:wZl53oYzEN1PEIA/dPa/FjBq9rRqPmS/Gzul8BdKYK4= 6 | github.com/gen2brain/go-unarr v0.1.1/go.mod h1:P05CsEe8jVEXhxqXqp9mFKUKFV0BKpFmtgNWf8Mcoos= 7 | github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= 8 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 9 | github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE= 10 | github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10= 11 | github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= 12 | github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 13 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 14 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 15 | github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 16 | github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe h1:rcf1P0fm+1l0EjG16p06mYLj9gW9X36KgdHJ/88hS4g= 17 | github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 18 | github.com/gopherjs/gopherwasm v1.1.0 h1:fA2uLoctU5+T3OhOn2vYP0DVT6pxc7xhTlBB1paATqQ= 19 | github.com/gopherjs/gopherwasm v1.1.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI= 20 | github.com/h2non/filetype v1.1.1 h1:xvOwnXKAckvtLWsN398qS9QhlxlnVXBjXBydK2/UFB4= 21 | github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= 22 | github.com/hillu/go-yara/v4 v4.0.4 h1:DxKUyCwk6BG2SONtvkpeuYOdjmHMZ5ybqLdaH2POLRw= 23 | github.com/hillu/go-yara/v4 v4.0.4/go.mod h1:rkb/gSAoO8qcmj+pv6fDZN4tOa3N7R+qqGlEkzT4iys= 24 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 25 | github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= 26 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 27 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= 28 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= 29 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 30 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 31 | github.com/parsiya/golnk v0.0.0-20200515071614-5db3107130ce h1:A8XpVS2Jz5/aVqmDh5lyeQA6V8d5IfjXTcDyFWj+JsY= 32 | github.com/parsiya/golnk v0.0.0-20200515071614-5db3107130ce/go.mod h1:K81/KqyRQt+tqXkg+ENusP67AeIrzJRa2uVlrCYwF5Y= 33 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 34 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 35 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 36 | github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk= 37 | github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= 38 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 39 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 40 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 41 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 42 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 43 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 44 | golang.org/x/net v0.0.0-20210324205630-d1beb07c2056 h1:sANdAef76Ioam9aQUUdcAqricwY/WUaMc4+7LY4eGg8= 45 | golang.org/x/net v0.0.0-20210324205630-d1beb07c2056/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= 46 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 47 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 48 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg= 53 | golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 55 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 56 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 57 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 58 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 59 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 60 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 61 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 62 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 63 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 64 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | const LOG_INFO = 0 9 | const LOG_ERROR = -1 10 | 11 | func logMessage(logType int, logMessage ...interface{}) { 12 | if logType == LOG_INFO { 13 | log.SetOutput(os.Stdout) 14 | } else { 15 | log.SetOutput(os.Stderr) 16 | } 17 | 18 | log.Println(logMessage...) 19 | } 20 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // #cgo !yara_no_pkg_config,!yara_static pkg-config: yara 2 | // #cgo !yara_no_pkg_config,yara_static pkg-config: --static yara 3 | // #cgo yara_no_pkg_config LDFLAGS: -lyara 4 | // compile: go build -tags yara_static -a -ldflags '-s -w -extldflags "-static"' . 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | "os/signal" 13 | "sync" 14 | "time" 15 | 16 | "github.com/akamensky/argparse" 17 | "golang.org/x/sys/windows" 18 | ) 19 | 20 | var ( 21 | notificationsHistory []string 22 | filescanHistory []FileDescriptor 23 | memoryHashHistory []string 24 | killQueue []string 25 | exit = make(chan bool) 26 | config Configuration 27 | ) 28 | 29 | var defaultScannedFileExtensions = []string{} 30 | var maxFilesizeScan int 31 | var cleanIfFileSizeGreaterThan int 32 | var quarantineKey string 33 | var archivesFormats = []string{"application/x-tar", "application/x-7z-compressed", "application/zip", "application/vnd.rar"} 34 | var wg sync.WaitGroup 35 | 36 | func main() { 37 | var err error 38 | 39 | // create mutex to avoid program running multiple instances 40 | if _, err = CreateMutex("irmaBinMutex"); err != nil { 41 | logMessage(LOG_ERROR, "Only one instance or irma can be launched") 42 | os.Exit(1) 43 | } 44 | 45 | // clean and exit on CTRL+C 46 | c := make(chan os.Signal, 1) 47 | signal.Notify(c, os.Interrupt) 48 | go func() { 49 | for sig := range c { 50 | fmt.Printf("[INFO] captured %v, stopping irma and exiting..", sig) 51 | exit <- true 52 | } 53 | }() 54 | 55 | // parse arguments 56 | parser := argparse.NewParser("irma", "Incident Response - Minimal Analysis") 57 | pConfigurationFile := parser.String("c", "configuration", &argparse.Options{Required: true, Default: "configuration.yaml", Help: "yaml configuration file"}) 58 | pBuilder := parser.String("b", "builder", &argparse.Options{Required: false, Default: "", Help: "create a standalone launcher executable with packed rules and configuration"}) 59 | pLog := parser.String("o", "outfile", &argparse.Options{Required: false, Default: "", Help: "save log informations inside the specified file path"}) 60 | 61 | err = parser.Parse(os.Args) 62 | if err != nil { 63 | fmt.Print(parser.Usage(err)) 64 | } 65 | 66 | // read configuration file 67 | config.getConfiguration(*pConfigurationFile) 68 | if len(*pBuilder) > 0 { 69 | BuildSFX(config.Yara.Path, config.Yara.Rulesrc4key, config, *pBuilder) 70 | os.Exit(0) 71 | } 72 | 73 | maxFilesizeScan = config.Advancedparameters.MaxScanFilesize 74 | cleanIfFileSizeGreaterThan = config.Advancedparameters.CleanMemoryIfFileGreaterThanSize 75 | defaultScannedFileExtensions = config.Advancedparameters.Extensions 76 | quarantineKey = config.Response.QuarantineRC4Key 77 | 78 | // log inside a file 79 | if len(*pLog) > 0 { 80 | f, err := os.OpenFile(*pLog, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | 85 | out := os.Stdout 86 | mw := io.MultiWriter(out, f) 87 | r, w, _ := os.Pipe() 88 | 89 | os.Stdout = w 90 | os.Stderr = w 91 | 92 | go func() { 93 | _, _ = io.Copy(mw, r) 94 | }() 95 | 96 | } 97 | 98 | // Retrieve current user permissions 99 | admin, elevated := CheckCurrentUserPermissions() 100 | if !admin && !elevated { 101 | logMessage(LOG_INFO, "[WARNING] IRMA is not running with admin righs. Notice that the analysis will be partial and limited to the current user scope") 102 | time.Sleep(5 * time.Second) 103 | } 104 | 105 | // spawn fake analysis processes (this binary is just a 10 seconds sleep infinite loop) 106 | if config.Others.FakeProcesses { 107 | SpawnFakeProcesses(config.Output.Verbose) 108 | } 109 | 110 | // load yara signature 111 | logMessage(LOG_INFO, "[INIT] Starting IRMA") 112 | yaraPath := config.Yara.Path 113 | yaraFiles := SearchForYaraFiles(yaraPath, config.Output.Verbose) 114 | compiler, err := LoadYaraRules(yaraFiles, config.Yara.Rulesrc4key, config.Output.Verbose) 115 | if err != nil { 116 | logMessage(LOG_ERROR, err) 117 | } 118 | 119 | if len(yaraFiles) == 0 { 120 | logMessage(LOG_ERROR, "[ERROR] No YARA rule found - Please add *.yar in "+config.Yara.Path+" folder") 121 | } 122 | 123 | logMessage(LOG_INFO, "[INIT] Loading ", len(yaraFiles), "YARA files") 124 | 125 | // compile yara rules 126 | rules, err := CompileRules(compiler) 127 | if err != nil { 128 | logMessage(LOG_ERROR, err) 129 | } 130 | logMessage(LOG_INFO, "[INIT]", len(rules.GetRules()), "YARA rules compiled") 131 | 132 | if config.Network.Capture { 133 | wg.Add(1) 134 | logMessage(LOG_INFO, "[INFO] Start network capture") 135 | go NetworkAnalysisRoutine(config.Network.Bpffilter, config.Network.Pcapfile, config.Output.Verbose) 136 | } 137 | 138 | if config.Yarascan.Memory { 139 | wg.Add(1) 140 | logMessage(LOG_INFO, "[INFO] Start scanning memory") 141 | go MemoryAnalysisRoutine(config.Response.DumpDirectory, config.Response.QuarantineDirectory, config.Response.Kill, config.Output.Notifications, config.Output.Verbose, config.Yarascan.InfiniteScan, rules) 142 | } 143 | 144 | if config.Yarascan.Registry { 145 | wg.Add(1) 146 | logMessage(LOG_INFO, "[INFO] Start scanning registry") 147 | go RegistryAnalysisRoutine(config.Response.QuarantineDirectory, config.Response.Kill, config.Output.Notifications, config.Output.Verbose, config.Yarascan.InfiniteScan, rules) 148 | } 149 | 150 | if config.Yarascan.Startmenu { 151 | wg.Add(1) 152 | logMessage(LOG_INFO, "[INFO] Start scanning startmenu") 153 | go StartMenuAnalysisRoutine(config.Response.QuarantineDirectory, config.Response.Kill, config.Output.Notifications, config.Output.Verbose, config.Yarascan.InfiniteScan, rules) 154 | } 155 | 156 | if config.Yarascan.Taskscheduler { 157 | wg.Add(1) 158 | logMessage(LOG_INFO, "[INFO] Start scanning tasks scheduler") 159 | go TaskSchedulerAnalysisRoutine(config.Response.QuarantineDirectory, config.Response.Kill, config.Output.Notifications, config.Output.Verbose, config.Yarascan.InfiniteScan, rules) 160 | } 161 | 162 | if config.Yarascan.Userfilesystem { 163 | wg.Add(1) 164 | logMessage(LOG_INFO, "[INFO] Start scanning user filesystem") 165 | go UserFileSystemAnalysisRoutine(config.Response.QuarantineDirectory, config.Response.Kill, config.Output.Notifications, config.Output.Verbose, config.Yarascan.InfiniteScan, rules) 166 | } 167 | 168 | if config.Yarascan.SystemDrive { 169 | wg.Add(1) 170 | logMessage(LOG_INFO, "[INFO] Start scanning system drive") 171 | go WindowsFileSystemAnalysisRoutine(config.Response.QuarantineDirectory, config.Response.Kill, config.Output.Notifications, config.Output.Verbose, config.Yarascan.InfiniteScan, rules) 172 | } 173 | 174 | for _, p := range config.Yarascan.AbsolutePaths { 175 | logMessage(LOG_INFO, "[INFO] Start scanning "+p) 176 | go func(path string) { 177 | wg.Add(1) 178 | for { 179 | files, err := RetrivesFilesFromUserPath(path, true, defaultScannedFileExtensions, config.Yarascan.AbsolutePathsRecursive, config.Output.Verbose) 180 | if err != nil { 181 | if config.Output.Verbose { 182 | logMessage(LOG_ERROR, err) 183 | } 184 | break 185 | } 186 | 187 | for _, f := range files { 188 | FileAnalysis(f, config.Response.QuarantineDirectory, config.Response.Kill, config.Output.Notifications, config.Output.Verbose, rules, "CUSTOMSCAN") 189 | } 190 | 191 | if !config.Yarascan.InfiniteScan { 192 | wg.Done() 193 | break 194 | } else { 195 | time.Sleep(60 * time.Second) 196 | } 197 | } 198 | }(p) 199 | } 200 | 201 | // Exit program when all goroutines have ended 202 | wg.Wait() 203 | os.Exit(0) 204 | } 205 | 206 | // CheckCurrentUserPermissions retieves the current user permissions and check if the program run with elevated privileges 207 | func CheckCurrentUserPermissions() (admin bool, elevated bool) { 208 | var err error 209 | var sid *windows.SID 210 | err = windows.AllocateAndInitializeSid( 211 | &windows.SECURITY_NT_AUTHORITY, 212 | 2, 213 | windows.SECURITY_BUILTIN_DOMAIN_RID, 214 | windows.DOMAIN_ALIAS_RID_ADMINS, 215 | 0, 0, 0, 0, 0, 0, 216 | &sid) 217 | if err != nil { 218 | log.Fatalf("[ERROR] SID Error: %s", err) 219 | return false, false 220 | } 221 | defer windows.FreeSid(sid) 222 | token := windows.Token(0) 223 | 224 | admin, err = token.IsMember(sid) 225 | if err != nil { 226 | log.Fatalf("[ERROR] Token Membership Error: %s", err) 227 | return false, false 228 | } 229 | 230 | return admin, token.IsElevated() 231 | } 232 | -------------------------------------------------------------------------------- /networkcapture.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/google/gopacket" 11 | "github.com/google/gopacket/layers" 12 | "github.com/google/gopacket/pcap" 13 | "github.com/google/gopacket/pcapgo" 14 | ) 15 | 16 | // NetworkAnalysisRoutine dump network interfaces traffic 17 | func NetworkAnalysisRoutine(bpffilter string, filename string, verbose bool) { 18 | // Check if WinPCAP installed 19 | if _, err := os.Stat(os.Getenv("SystemRoot") + "\\System32\\wpcap.dll"); os.IsNotExist(err) { 20 | logMessage(LOG_ERROR, "[ERROR] The network capture system requires the installation of WinPCAP on the workstation") 21 | return 22 | } 23 | 24 | // Get a list of all interfaces. 25 | ifaces, err := net.Interfaces() 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | // Get a list of all devices 31 | devices, err := pcap.FindAllDevs() 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | // Creating pcap file 37 | _, err = os.Stat(filepath.Dir(filename)) 38 | if os.IsNotExist(err) { 39 | if err := os.MkdirAll(filepath.Dir(filename), 0600); err != nil { 40 | logMessage(LOG_ERROR, "[ERROR] Failed to create pcap file: ", err.Error()) 41 | return 42 | } 43 | } 44 | 45 | f, err := os.Create(filename) 46 | if err != nil { 47 | logMessage(LOG_ERROR, "[ERROR] Failed to create pcap file: ", err.Error()) 48 | return 49 | } 50 | defer f.Close() 51 | 52 | // listening for all interfaces 53 | for _, iface := range ifaces { 54 | if err := CaptureInterface(&iface, &devices, bpffilter, f, verbose); err != nil && verbose { 55 | logMessage(LOG_ERROR, "[ERROR]", err) 56 | } 57 | } 58 | } 59 | 60 | // CaptureInterface dump all packet on specified network interface 61 | func CaptureInterface(iface *net.Interface, devices *[]pcap.Interface, bpffilter string, f *os.File, verbose bool) error { 62 | // We just look for IPv4 addresses, so try to find if the interface has one. 63 | var addr *net.IPNet 64 | addrs, err := iface.Addrs() 65 | if err != nil { 66 | return err 67 | } 68 | 69 | for _, a := range addrs { 70 | if ipnet, ok := a.(*net.IPNet); ok { 71 | if ip4 := ipnet.IP.To4(); ip4 != nil { 72 | addr = &net.IPNet{ 73 | IP: ip4, 74 | Mask: ipnet.Mask[len(ipnet.Mask)-4:], 75 | } 76 | break 77 | } 78 | } 79 | } 80 | 81 | fmt.Printf("[INFO] Using network range %v for interface \"%v\"", addr, iface.Name) 82 | 83 | // Try to find a match between device and interface 84 | var deviceName string 85 | for _, d := range *devices { 86 | if strings.Contains(fmt.Sprint(d.Addresses), fmt.Sprint(addr.IP)) { 87 | deviceName = d.Name 88 | } 89 | } 90 | 91 | if deviceName == "" { 92 | return fmt.Errorf("Cannot find the corresponding device for the interface \"%v\"", iface.Name) 93 | } 94 | 95 | // Open up a pcap handle for packet reads/writes. 96 | handle, err := pcap.OpenLive(deviceName, 65536, true, pcap.BlockForever) 97 | if err != nil { 98 | return err 99 | } 100 | defer handle.Close() 101 | 102 | // BPF filter 103 | if len(bpffilter) > 0 { 104 | if err = handle.SetBPFFilter(bpffilter); err != nil { 105 | return fmt.Errorf("BPF filter error: %s", err.Error()) 106 | } 107 | } 108 | 109 | // Starting capture 110 | w := pcapgo.NewWriter(f) 111 | w.WriteFileHeader(65536, layers.LinkTypeEthernet) 112 | 113 | packetSource := gopacket.NewPacketSource(handle, handle.LinkType()) 114 | for packet := range packetSource.Packets() { 115 | w.WritePacket(packet.Metadata().CaptureInfo, packet.Data()) 116 | } 117 | 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /procsmemory.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strings" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/hillu/go-yara/v4" 14 | "golang.org/x/sys/windows" 15 | ) 16 | 17 | // ProcessInformation wrap basic process information and memory dump in a structure 18 | type ProcessInformation struct { 19 | PID uint32 20 | ProcessName string 21 | ProcessPath string 22 | MemoryDump []byte 23 | } 24 | 25 | // MemoryAnalysisRoutine analyse processes memory every 5 seconds 26 | func MemoryAnalysisRoutine(pDump string, pQuarantine string, pKill bool, pNotifications bool, pVerbose bool, pInfiniteLoop bool, rules *yara.Rules) { 27 | for { 28 | // list process information and memory 29 | procs := ListProcess(pVerbose) 30 | 31 | // analyze process memory and executable 32 | for _, proc := range procs { 33 | // dump processes memory and quit the program 34 | if len(pDump) > 0 { 35 | if err := WriteProcessMemoryToFile(pDump, proc.ProcessName+fmt.Sprint(proc.PID)+".dmp", proc.MemoryDump); err != nil && pVerbose { 36 | logMessage(LOG_ERROR, "[ERROR]", err) 37 | } 38 | } 39 | 40 | // parsing kill queue 41 | if StringInSlice(proc.ProcessPath, killQueue) && pKill { 42 | logMessage(LOG_INFO, "[INFO]", "KILLING PID", proc.PID) 43 | KillProcessByID(proc.PID, pVerbose) 44 | } else { 45 | // analyzing process memory and cleaning memory buffer 46 | MemoryAnalysis(&proc, pQuarantine, pKill, pNotifications, pVerbose, rules) 47 | proc.MemoryDump = nil 48 | 49 | // analyzing process executable 50 | FileAnalysis(proc.ProcessPath, pQuarantine, pKill, pNotifications, pVerbose, rules, "MEMORY") 51 | } 52 | } 53 | 54 | if len(pDump) > 0 { 55 | logMessage(LOG_INFO, "[INFO] Processes memory dump completed - Exiting program.") 56 | os.Exit(0) 57 | } 58 | 59 | killQueue = nil 60 | if !pInfiniteLoop { 61 | wg.Done() 62 | break 63 | } else { 64 | time.Sleep(5 * time.Second) 65 | } 66 | } 67 | } 68 | 69 | // ListProcess try to get all running processes and dump their memory, return a ProcessInformation slice 70 | func ListProcess(verbose bool) (procsInfo []ProcessInformation) { 71 | runningPID := os.Getpid() 72 | 73 | procsIds, bytesReturned, err := GetProcessesList() 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | for i := uint32(0); i < bytesReturned; i++ { 78 | if procsIds[i] != 0 && procsIds[i] != uint32(runningPID) { 79 | procHandle, err := GetProcessHandle(procsIds[i], windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ) 80 | if err != nil && verbose { 81 | logMessage(LOG_ERROR, "[ERROR]", "PID", procsIds[i], err) 82 | } 83 | 84 | if err == nil && procHandle > 0 { 85 | if proc, memdump, err := GetProcessMemory(procsIds[i], procHandle, verbose); err != nil && verbose { 86 | logMessage(LOG_ERROR, "[ERROR]", err) 87 | } else { 88 | if len(memdump) > 0 { 89 | // return process memory only if it has changed since the last process scan 90 | if !StringInSlice(fmt.Sprintf("%x", md5.Sum(memdump)), memoryHashHistory) { 91 | proc.MemoryDump = memdump 92 | procsInfo = append(procsInfo, proc) 93 | memoryHashHistory = append(memoryHashHistory, fmt.Sprintf("%x", md5.Sum(memdump))) 94 | } 95 | } 96 | } 97 | 98 | } 99 | windows.CloseHandle(procHandle) 100 | } 101 | } 102 | return procsInfo 103 | } 104 | 105 | // GetProcessMemory return a process memory dump based on its handle 106 | func GetProcessMemory(pid uint32, handle windows.Handle, verbose bool) (ProcessInformation, []byte, error) { 107 | 108 | procFilename, modules, err := GetProcessModulesHandles(handle) 109 | if err != nil { 110 | return ProcessInformation{}, nil, fmt.Errorf("Unable to get PID %d memory: %s", pid, err.Error()) 111 | } 112 | 113 | for _, moduleHandle := range modules { 114 | if moduleHandle != 0 { 115 | moduleRawName, err := GetModuleFileNameEx(handle, moduleHandle, 512) 116 | if err != nil { 117 | return ProcessInformation{}, nil, err 118 | } 119 | moduleRawName = bytes.Trim(moduleRawName, "\x00") 120 | modulePath := strings.Split(string(moduleRawName), "\\") 121 | moduleFileName := modulePath[len(modulePath)-1] 122 | 123 | if procFilename == moduleFileName { 124 | return ProcessInformation{PID: pid, ProcessName: procFilename, ProcessPath: string(moduleRawName)}, DumpModuleMemory(handle, moduleHandle, verbose), nil 125 | } 126 | } 127 | } 128 | 129 | return ProcessInformation{}, nil, fmt.Errorf("Unable to get PID %d memory: no module corresponding to process name", pid) 130 | } 131 | 132 | // KillProcessByID try to kill the specified PID 133 | func KillProcessByID(procID uint32, verbose bool) (err error) { 134 | hProc, err := GetProcessHandle(procID, windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_TERMINATE) 135 | if err != nil && verbose { 136 | logMessage(LOG_ERROR, "[ERROR]", "PID", procID, err) 137 | } 138 | 139 | exitCode := GetExitCodeProcess(hProc) 140 | err = windows.TerminateProcess(hProc, exitCode) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | return nil 146 | } 147 | 148 | // GetProcessesList return PID from running processes 149 | func GetProcessesList() (procsIds []uint32, bytesReturned uint32, err error) { 150 | procsIds = make([]uint32, 2048) 151 | err = windows.EnumProcesses(procsIds, &bytesReturned) 152 | return procsIds, bytesReturned, err 153 | } 154 | 155 | // GetProcessHandle return the process handle from the specified PID 156 | func GetProcessHandle(pid uint32, desiredAccess uint32) (handle windows.Handle, err error) { 157 | handle, err = windows.OpenProcess(desiredAccess, false, pid) 158 | return handle, err 159 | } 160 | 161 | // GetProcessModulesHandles list modules handles from a process handle 162 | func GetProcessModulesHandles(procHandle windows.Handle) (processFilename string, modules []syscall.Handle, err error) { 163 | var processRawName []byte 164 | processRawName, err = GetProcessImageFileName(procHandle, 512) 165 | if err != nil { 166 | return "", nil, err 167 | } 168 | processRawName = bytes.Trim(processRawName, "\x00") 169 | processPath := strings.Split(string(processRawName), "\\") 170 | processFilename = processPath[len(processPath)-1] 171 | 172 | modules, err = EnumProcessModules(procHandle, 32) 173 | if err != nil { 174 | return "", nil, err 175 | } 176 | 177 | return processFilename, modules, nil 178 | } 179 | 180 | // DumpModuleMemory dump a process module memory and return it as a byte slice 181 | func DumpModuleMemory(procHandle windows.Handle, modHandle syscall.Handle, verbose bool) []byte { 182 | moduleInfos, err := GetModuleInformation(procHandle, modHandle) 183 | if err != nil && verbose { 184 | logMessage(LOG_ERROR, "[ERROR]", err) 185 | } 186 | 187 | memdump, err := ReadProcessMemory(procHandle, moduleInfos.BaseOfDll, uintptr(moduleInfos.SizeOfImage)) 188 | if err != nil && verbose { 189 | logMessage(LOG_ERROR, "[ERROR]", err) 190 | } 191 | 192 | memdump = bytes.Trim(memdump, "\x00") 193 | return memdump 194 | } 195 | 196 | // WriteProcessMemoryToFile try to write a byte slice to the specified directory 197 | func WriteProcessMemoryToFile(path string, file string, data []byte) (err error) { 198 | _, err = os.Stat(path) 199 | if os.IsNotExist(err) { 200 | if err := os.MkdirAll(path, 0600); err != nil { 201 | return err 202 | } 203 | } 204 | 205 | if err := os.WriteFile(path+"/"+file, data, 0644); err != nil { 206 | return err 207 | } 208 | 209 | return nil 210 | } 211 | -------------------------------------------------------------------------------- /resources/fake_process.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeyourweb/irma/33da07e24161f6fd6824550bd70a06b96b4684e1/resources/fake_process.exe -------------------------------------------------------------------------------- /resources/winrar_sfx.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeyourweb/irma/33da07e24161f6fd6824550bd70a06b96b4684e1/resources/winrar_sfx.exe -------------------------------------------------------------------------------- /sfxbuilder.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "crypto/rc4" 7 | _ "embed" 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | 14 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | //go:embed resources/winrar_sfx.exe 18 | var sfxBinary []byte 19 | 20 | func BuildSFX(yaraPath string, rc4key string, config Configuration, outputSfxExe string) error { 21 | // compress inputDirectory into archive 22 | archive := recursiveCompressFolder(yaraPath, rc4key, config) 23 | 24 | file, err := os.Create(outputSfxExe) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | defer file.Close() 30 | 31 | // pack sfx binary and customized archive together 32 | file.Write(sfxBinary) 33 | file.Write(archive.Bytes()) 34 | 35 | return nil 36 | } 37 | 38 | func recursiveCompressFolder(yaraPath string, rc4key string, config Configuration) bytes.Buffer { 39 | var buffer bytes.Buffer 40 | archive := zip.NewWriter(&buffer) 41 | 42 | files := SearchForYaraFiles(yaraPath, true) 43 | 44 | // embed irma.exe executable 45 | zipFile, err := archive.Create("irma.exe") 46 | if err != nil { 47 | log.Fatal("[ERROR] ", err) 48 | } 49 | 50 | fsFile, err := os.ReadFile(os.Args[0]) 51 | if err != nil { 52 | log.Fatal("[ERROR] ", err) 53 | } 54 | 55 | r := bytes.NewReader(fsFile) 56 | _, err = io.Copy(zipFile, r) 57 | if err != nil { 58 | log.Fatal("[ERROR] ", err) 59 | } 60 | 61 | // embed configuration file 62 | zipFile, err = archive.Create("configuration.yaml") 63 | if err != nil { 64 | log.Fatal("[ERROR] ", err) 65 | } 66 | 67 | config.Yara.Path = "yara-signatures/" 68 | d, err := yaml.Marshal(&config) 69 | r = bytes.NewReader(d) 70 | _, err = io.Copy(zipFile, r) 71 | if err != nil { 72 | log.Fatal("[ERROR] ", err) 73 | } 74 | 75 | // embed yara rules 76 | for _, f := range files { 77 | fileName := filepath.Base(f) 78 | zipFile, err := archive.Create("yara-signatures/" + fileName) 79 | if err != nil { 80 | log.Fatal("[ERROR] ", err) 81 | } 82 | 83 | fsFile, err := os.ReadFile(f) 84 | if err != nil { 85 | log.Fatal("[ERROR] ", err) 86 | } 87 | 88 | // yara rule RC4 cipher 89 | if len(rc4key) > 0 { 90 | c, err := rc4.NewCipher([]byte(rc4key)) 91 | if err != nil { 92 | log.Fatal("[ERROR] ", err) 93 | } 94 | 95 | c.XORKeyStream(fsFile, fsFile) 96 | } 97 | 98 | r := bytes.NewReader(fsFile) 99 | _, err = io.Copy(zipFile, r) 100 | if err != nil { 101 | log.Fatal("[ERROR] ", err) 102 | } 103 | } 104 | 105 | // sfx comment 106 | var b2i int = 0 107 | if config.Sfx.SilentMode { 108 | b2i = 1 109 | } 110 | var sfxcomment = "the comment below contains sfx script commands\r\n\r\n" + 111 | "Path=" + config.Sfx.ExtractDirectory + "\r\n" 112 | 113 | if config.Sfx.Autoexec { 114 | sfxcomment += "Setup=irma.exe -c configuration.yaml" 115 | } 116 | 117 | if len(config.Sfx.LogFile) > 0 { 118 | sfxcomment += " -o \"" + config.Sfx.LogFile + "\"" 119 | } 120 | 121 | sfxcomment += "\r\n" + 122 | "Silent=" + fmt.Sprint(b2i) + "\r\n" + 123 | "Overwrite=1" 124 | 125 | archive.SetComment(sfxcomment) 126 | 127 | if err != nil { 128 | return buffer 129 | } 130 | err = archive.Close() 131 | 132 | if err != nil { 133 | log.Fatal("[ERROR] ", err) 134 | } 135 | return buffer 136 | } 137 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/gen2brain/beeep" 7 | ) 8 | 9 | // RegisterFileInHistory check if file is already known and hasn't change in files history return true if file is append to history and false if it is already known as is. 10 | func RegisterFileInHistory(f os.FileInfo, path string, history *[]FileDescriptor, verbose bool) bool { 11 | if int(f.Size()) > 1024*1024*maxFilesizeScan { 12 | if verbose { 13 | logMessage(LOG_INFO, "[INFO]", path, "skipped - larger than", maxFilesizeScan, "Mb") 14 | } 15 | return true 16 | } 17 | 18 | for i, h := range *history { 19 | if h.FilePath == path { 20 | if h.LastModified == f.ModTime() && h.FileSize == f.Size() { 21 | return false 22 | } 23 | (*history)[i].LastModified = f.ModTime() 24 | (*history)[i].FileSize = f.Size() 25 | return true 26 | } 27 | } 28 | 29 | var d = FileDescriptor{FilePath: path, FileSize: f.Size(), LastModified: f.ModTime()} 30 | *history = append(*history, d) 31 | return true 32 | } 33 | 34 | // StringInSlice check wether or not a string already is inside a specified slice 35 | func StringInSlice(a string, list []string) bool { 36 | for _, b := range list { 37 | if b == a { 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | 44 | // NotifyUser use Windows notification to instant alert 45 | func NotifyUser(title string, message string) { 46 | err := beeep.Alert(title, message, "") 47 | if err != nil { 48 | panic(err) 49 | } 50 | } 51 | 52 | // SpawnFakeProcesses drop fake analysis process 53 | func SpawnFakeProcesses(verbose bool) { 54 | if err := SpawnFakeProcess("procmon.exe"); err != nil && verbose { 55 | logMessage(LOG_ERROR, "[ERROR]", err) 56 | } 57 | if err := SpawnFakeProcess("wireshark.exe"); err != nil && verbose { 58 | logMessage(LOG_ERROR, "[ERROR]", err) 59 | } 60 | if err := SpawnFakeProcess("tcpdump.exe"); err != nil && verbose { 61 | logMessage(LOG_ERROR, "[ERROR]", err) 62 | } 63 | if err := SpawnFakeProcess("sysmon.exe"); err != nil && verbose { 64 | logMessage(LOG_ERROR, "[ERROR]", err) 65 | } 66 | if err := SpawnFakeProcess("sysmon64.exe"); err != nil && verbose { 67 | logMessage(LOG_ERROR, "[ERROR]", err) 68 | } 69 | if err := SpawnFakeProcess("x86dbg.exe"); err != nil && verbose { 70 | logMessage(LOG_ERROR, "[ERROR]", err) 71 | } 72 | if err := SpawnFakeProcess("x64dbg.exe"); err != nil && verbose { 73 | logMessage(LOG_ERROR, "[ERROR]", err) 74 | } 75 | if err := SpawnFakeProcess("inetsim.exe"); err != nil && verbose { 76 | logMessage(LOG_ERROR, "[ERROR]", err) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /w32.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "syscall" 5 | "unsafe" 6 | 7 | "golang.org/x/sys/windows" 8 | ) 9 | 10 | // Windows services constants 11 | const ( 12 | SVC_SC_ENUM_PROCESS_INFO = 0 13 | SVC_SERVICE_WIN32 = 0x00000030 14 | SVC_SERVICE_STATE_ALL = 0x00000003 15 | SVC_SERVICE_ACCEPT_STOP = 0x00000001 16 | ) 17 | 18 | var ( 19 | modAdvapi32 = windows.NewLazySystemDLL("Advapi32.dll") 20 | modkernel32 = windows.NewLazySystemDLL("Kernel32.dll") 21 | modpsapi = windows.NewLazySystemDLL("Psapi.dll") 22 | procCreateMutex = modkernel32.NewProc("CreateMutexW") 23 | procGetSystemInfo = modkernel32.NewProc("GetSystemInfo") 24 | procReadProcessMemory = modkernel32.NewProc("ReadProcessMemory") 25 | procVirtualProtect = modkernel32.NewProc("VirtualProtect") 26 | procGetExitCodeProcess = modkernel32.NewProc("GetExitCodeProcess") 27 | procGetProcessImageFileName = modpsapi.NewProc("GetProcessImageFileNameA") 28 | procEnumProcessModules = modpsapi.NewProc("EnumProcessModules") 29 | procGetModuleFileNameEx = modpsapi.NewProc("GetModuleFileNameExA") 30 | procGetModuleInformation = modpsapi.NewProc("GetModuleInformation") 31 | procSvcEnumServicesStatusEx = modAdvapi32.NewProc("EnumServicesStatusExW") 32 | ) 33 | 34 | // wrapper for WIN32 API ENUM_SERVICE_STATUS_PROCESSW structure 35 | // https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-enum_service_status_processw 36 | type ENUM_SERVICE_STATUS_PROCESS struct { 37 | lpServiceName *uint16 38 | lpDisplayName *uint16 39 | ServiceStatusProcess SERVICE_STATUS_PROCESS 40 | } 41 | 42 | // wrapper for WIN32 API SERVICE_STATUS_PROCESS structure 43 | // https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_status_process 44 | type SERVICE_STATUS_PROCESS struct { 45 | dwServiceType uint32 46 | dwCurrentState uint32 47 | dwControlsAccepted uint32 48 | dwWin32ExitCode uint32 49 | dwServiceSpecificExitCode uint32 50 | dwCheckPoint uint32 51 | dwWaitHint uint32 52 | dwProcessId uint32 53 | dwServiceFlags uint32 54 | } 55 | 56 | // SystemInfo structure contains information about the current computer system. This includes the architecture and type of the processor, the number of processors in the system, the page size, and other such information. 57 | // https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/ns-sysinfoapi-system_info 58 | type SystemInfo struct { 59 | ProcessorArchitecture int16 60 | reserved int16 61 | PageSize int32 62 | MinimumApplicationAddress uintptr 63 | MaximumApplicationAddress uintptr 64 | ActiveProcessorMask uintptr 65 | NumberOfProcessors int32 66 | ProcessorType int32 67 | AllocationGranularity int32 68 | ProcessorLevel int16 69 | ProcessorRevision int16 70 | } 71 | 72 | // ModuleInfo structure contains the module load address, size, and entry point. 73 | // https://docs.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-moduleinfo 74 | type ModuleInfo struct { 75 | BaseOfDll uintptr 76 | SizeOfImage int32 77 | EntryPoint uintptr 78 | } 79 | 80 | // GetSystemInfo is a wrapper for the same WIN32 API function 81 | // https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsysteminfo 82 | func GetSystemInfo() (si SystemInfo) { 83 | _, _, _ = syscall.Syscall(procGetSystemInfo.Addr(), 1, uintptr(unsafe.Pointer(&si)), 0, 0) 84 | return si 85 | } 86 | 87 | // GetProcessImageFileName is a wrapper for the same WIN32 API function 88 | // https://docs.microsoft.com/fr-fr/windows/win32/api/psapi/nf-psapi-getprocessimagefilenamea?redirectedfrom=MSDN 89 | func GetProcessImageFileName(hProcess windows.Handle, nSize uintptr) (data []byte, err error) { 90 | data = make([]byte, nSize) 91 | ret, _, err := syscall.Syscall(procGetProcessImageFileName.Addr(), 3, uintptr(hProcess), uintptr(unsafe.Pointer(&data[0])), nSize) 92 | if ret == 0 { 93 | return nil, err 94 | } 95 | 96 | return data, nil 97 | } 98 | 99 | // EnumProcessModules is a wrapper for the same WIN32 API function 100 | // https://docs.microsoft.com/fr-fr/windows/win32/api/psapi/nf-psapi-enumprocessmodules?redirectedfrom=MSDN 101 | func EnumProcessModules(hProcess windows.Handle, nSize uintptr) (modules []syscall.Handle, err error) { 102 | modules = make([]syscall.Handle, nSize) 103 | var sizeNeeded uint32 = 0 104 | ret, _, _ := syscall.Syscall6(procEnumProcessModules.Addr(), 4, uintptr(hProcess), uintptr(unsafe.Pointer(&modules[0])), uintptr(nSize), uintptr(unsafe.Pointer(&sizeNeeded)), 0, 0) 105 | if ret == 0 { 106 | return nil, err 107 | } 108 | 109 | return modules, nil 110 | } 111 | 112 | // GetModuleFileNameEx is a wrapper for the same WIN32 API function 113 | // https://docs.microsoft.com/fr-fr/windows/win32/api/psapi/nf-psapi-getmodulefilenameexa?redirectedfrom=MSDN 114 | func GetModuleFileNameEx(hProcess windows.Handle, hModule syscall.Handle, nSize uintptr) (data []byte, err error) { 115 | data = make([]byte, nSize) 116 | ret, _, _ := syscall.Syscall6(procGetModuleFileNameEx.Addr(), 4, uintptr(hProcess), uintptr(hModule), uintptr(unsafe.Pointer(&data[0])), uintptr(nSize), 0, 0) 117 | if ret == 0 { 118 | return nil, err 119 | } 120 | 121 | return data, nil 122 | } 123 | 124 | // GetModuleInformation is a wrapper for the same WIN32 API function 125 | // https://docs.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmoduleinformation 126 | func GetModuleInformation(hProcess windows.Handle, hModule syscall.Handle) (modInfos ModuleInfo, err error) { 127 | ret, _, err := syscall.Syscall6(procGetModuleInformation.Addr(), 4, uintptr(hProcess), uintptr(hModule), uintptr(unsafe.Pointer(&modInfos)), uintptr(unsafe.Sizeof(modInfos)), 0, 0) 128 | if ret == 0 { 129 | return ModuleInfo{}, err 130 | } 131 | 132 | return modInfos, nil 133 | } 134 | 135 | // ReadProcessMemory is a wrapper for the same WIN32 API function 136 | // https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-readprocessmemory 137 | func ReadProcessMemory(hProcess windows.Handle, lpBaseAddress uintptr, nSize uintptr) (data []byte, err error) { 138 | data = make([]byte, nSize) 139 | var lpNumberOfBytesRead uint32 = 0 140 | ret, _, err := syscall.Syscall6(procReadProcessMemory.Addr(), 5, uintptr(hProcess), lpBaseAddress, uintptr(unsafe.Pointer(&data[0])), nSize, uintptr(unsafe.Pointer(&lpNumberOfBytesRead)), 0) 141 | if ret == 0 { 142 | return nil, err 143 | } 144 | 145 | return data, nil 146 | } 147 | 148 | // CreateMutex is a wrapper for CreateMutexW WIN32 API function 149 | // https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createmutexw 150 | func CreateMutex(name string) (uintptr, error) { 151 | ret, _, err := procCreateMutex.Call( 152 | 0, 153 | 0, 154 | uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(name))), 155 | ) 156 | switch int(err.(syscall.Errno)) { 157 | case 0: 158 | return ret, nil 159 | default: 160 | return ret, err 161 | } 162 | } 163 | 164 | // VirtualProtect is a wrapper for the same WIN32 API function 165 | // https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect 166 | func VirtualProtect(lpAddress unsafe.Pointer, dwSize uintptr, flNewProtect uint32, lpflOldProtect unsafe.Pointer) bool { 167 | ret, _, _ := procVirtualProtect.Call( 168 | uintptr(lpAddress), 169 | uintptr(dwSize), 170 | uintptr(flNewProtect), 171 | uintptr(lpflOldProtect)) 172 | return ret > 0 173 | } 174 | 175 | // GetExitCodeProcess is a wrapper for the same WIN32 API function 176 | // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess 177 | func GetExitCodeProcess(hProcess windows.Handle) uint32 { 178 | var lpExitCode uint32 = 0 179 | ret, _, _ := syscall.Syscall(procGetExitCodeProcess.Addr(), 2, uintptr(hProcess), uintptr(unsafe.Pointer(&lpExitCode)), 0) 180 | if ret != 0 { 181 | return lpExitCode 182 | } 183 | 184 | return 0 185 | } 186 | -------------------------------------------------------------------------------- /windowslnkparser.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "github.com/hillu/go-yara/v4" 10 | golnk "github.com/parsiya/golnk" 11 | ) 12 | 13 | // StartMenuAnalysisRoutine analyse system artefacts every 15 seconds 14 | func StartMenuAnalysisRoutine(pQuarantine string, pKill bool, pNotifications bool, pVerbose bool, pInfiniteLoop bool, rules *yara.Rules) { 15 | for { 16 | lnk, errors := ListStartMenuLnkPersistence(pVerbose) 17 | if errors != nil && pVerbose { 18 | for _, err := range errors { 19 | logMessage(LOG_ERROR, "[ERROR]", err) 20 | } 21 | } 22 | 23 | for _, l := range lnk { 24 | paths := FormatPathFromComplexString(l) 25 | for _, p := range paths { 26 | FileAnalysis(p, pQuarantine, pKill, pNotifications, pVerbose, rules, "STARTMENU") 27 | } 28 | } 29 | 30 | if !pInfiniteLoop { 31 | wg.Done() 32 | break 33 | } else { 34 | time.Sleep(15 * time.Second) 35 | } 36 | } 37 | } 38 | 39 | // ListStartMenuFolders return a []string of all available StartMenu folders 40 | func ListStartMenuFolders(verbose bool) (startMenu []string, err error) { 41 | var usersDir []string 42 | 43 | startMenu = append(startMenu, os.Getenv("SystemDrive")+`\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp`) 44 | 45 | usersDir, err = RetrivesFilesFromUserPath(os.Getenv("SystemDrive")+"\\Users", false, nil, false, verbose) 46 | if err != nil { 47 | return startMenu, err 48 | } 49 | 50 | for _, uDir := range usersDir { 51 | if !strings.HasSuffix(uDir, "Public") && !strings.HasSuffix(uDir, "Default") { 52 | startMenu = append(startMenu, uDir+`\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup`) 53 | } 54 | } 55 | 56 | return startMenu, err 57 | } 58 | 59 | // ListStartMenuLnkPersistence check for every *.lnk in Windows StartMenu folders and extract every executable path in those links 60 | func ListStartMenuLnkPersistence(verbose bool) (exePath []string, errors []error) { 61 | 62 | startMenuFolders, err := ListStartMenuFolders(verbose) 63 | if err != nil { 64 | errors = append(errors, err) 65 | } 66 | 67 | for _, path := range startMenuFolders { 68 | 69 | files, err := RetrivesFilesFromUserPath(path, true, []string{".lnk"}, false, verbose) 70 | 71 | if err != nil { 72 | errors = append(errors, fmt.Errorf("%s - %s", path, err.Error())) 73 | } 74 | 75 | for _, p := range files { 76 | lnk, lnkErr := golnk.File(p) 77 | if lnkErr != nil { 78 | errors = append(errors, fmt.Errorf("%s - Lnk parse error", p)) 79 | continue 80 | } 81 | 82 | exePath = append(exePath, lnk.LinkInfo.LocalBasePath) 83 | } 84 | } 85 | 86 | return exePath, errors 87 | } 88 | -------------------------------------------------------------------------------- /windowsregistry.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/hillu/go-yara/v4" 9 | "golang.org/x/sys/windows/registry" 10 | ) 11 | 12 | // RegistryValue describe a regkey value in a structure 13 | type RegistryValue struct { 14 | key registry.Key 15 | subKey string 16 | valueName string 17 | value string 18 | } 19 | 20 | // RegistryAnalysisRoutine analyse registry persistence keys every 15 seconds 21 | func RegistryAnalysisRoutine(pQuarantine string, pKill bool, pNotifications bool, pVerbose bool, pInfiniteLoop bool, rules *yara.Rules) { 22 | for { 23 | values, errors := EnumRegistryPeristence() 24 | 25 | if pVerbose { 26 | for _, err := range errors { 27 | logMessage(LOG_ERROR, "[ERROR]", err) 28 | } 29 | } 30 | 31 | for _, k := range values { 32 | paths := FormatPathFromComplexString(k.value) 33 | for _, p := range paths { 34 | FileAnalysis(p, pQuarantine, pKill, pNotifications, pVerbose, rules, "REGISTRY") 35 | } 36 | } 37 | 38 | if !pInfiniteLoop { 39 | wg.Done() 40 | break 41 | } else { 42 | time.Sleep(15 * time.Second) 43 | } 44 | } 45 | } 46 | 47 | // EnumRegistryPeristence get all the potential registry values used for persistence 48 | func EnumRegistryPeristence() (values []RegistryValue, errors []error) { 49 | 50 | keys := []registry.Key{registry.USERS, registry.LOCAL_MACHINE} 51 | subkeys := []string{`SOFTWARE\Microsoft\Windows\CurrentVersion\Run`, `SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce`} 52 | 53 | for _, k := range keys { 54 | for _, s := range subkeys { 55 | v, err := EnumRegHivePersistence(k, s) 56 | if err != nil { 57 | errMsg := fmt.Errorf("%s\\%s - %s", GetRegistryHiveNameFromConst(k), s, err) 58 | errors = append(errors, errMsg) 59 | } 60 | 61 | for _, value := range v { 62 | values = append(values, value) 63 | } 64 | 65 | } 66 | } 67 | 68 | return values, errors 69 | } 70 | 71 | // GetRegistryHiveNameFromConst format registry.Key hive name as a string 72 | func GetRegistryHiveNameFromConst(key registry.Key) string { 73 | if key == registry.CLASSES_ROOT { 74 | return "HKEY_CLASSES_ROOT" 75 | } else if key == registry.CURRENT_CONFIG { 76 | return "HKEY_CURRENT_CONFIG" 77 | } else if key == registry.CURRENT_USER { 78 | return "HKEY_CURRENT_USER" 79 | } else if key == registry.USERS { 80 | return "HKEY_USERS" 81 | } else if key == registry.LOCAL_MACHINE { 82 | return "HKEY_LOCAL_MACHINE" 83 | } else { 84 | return "" 85 | } 86 | } 87 | 88 | // EnumRegHivePersistence parse the specified key and subkey and return all string key/value in a []RegistryValue 89 | func EnumRegHivePersistence(key registry.Key, subkey string) (values []RegistryValue, err error) { 90 | k, err := registry.OpenKey(key, "", registry.READ) 91 | if err != nil { 92 | return nil, err 93 | } 94 | defer k.Close() 95 | 96 | subkeys, err := k.ReadSubKeyNames(0) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | if key == registry.USERS { 102 | for _, s := range subkeys { 103 | if len(s) > 10 && !strings.HasSuffix(s, "_Classes") { 104 | v, err := enumRegSubKey(key, s+`\`+subkey) 105 | if err != nil { 106 | return nil, err 107 | } 108 | for _, item := range v { 109 | values = append(values, item) 110 | } 111 | 112 | } 113 | } 114 | } else { 115 | v, err := enumRegSubKey(key, subkey) 116 | if err != nil { 117 | return nil, err 118 | } 119 | for _, item := range v { 120 | values = append(values, item) 121 | } 122 | } 123 | 124 | return values, nil 125 | } 126 | 127 | // enumRegSubKey format subkey values in []RegistryValue 128 | func enumRegSubKey(key registry.Key, subkey string) (values []RegistryValue, err error) { 129 | var ( 130 | sk registry.Key 131 | sv []string 132 | ) 133 | 134 | sk, err = registry.OpenKey(key, subkey, registry.READ) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | sv, err = sk.ReadValueNames(0) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | for _, item := range sv { 145 | v, _, _ := sk.GetStringValue(item) 146 | if len(v) > 0 { 147 | var result = RegistryValue{key: key, subKey: subkey, valueName: item, value: v} 148 | values = append(values, result) 149 | } 150 | } 151 | 152 | return values, nil 153 | } 154 | -------------------------------------------------------------------------------- /windowstaskscheduler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | ole "github.com/go-ole/go-ole" 8 | oleutil "github.com/go-ole/go-ole/oleutil" 9 | "github.com/hillu/go-yara/v4" 10 | ) 11 | 12 | // Task is a task found in Windows Task Scheduler 13 | type Task struct { 14 | Name string 15 | Path string 16 | Enabled bool 17 | LastRunTime time.Time 18 | NextRunTime time.Time 19 | ActionList []ExecAction // Other actions are ignored, we are only interested in Commandline Actions 20 | } 21 | 22 | // ExecAction is an action defined in a scheduled Task if type IExecAction. 23 | type ExecAction struct { 24 | WorkingDirectory string 25 | Path string 26 | Arguments string 27 | } 28 | 29 | var ( 30 | unknown *ole.IUnknown 31 | variant *ole.VARIANT 32 | ts *ole.IDispatch 33 | ) 34 | 35 | var taskSchedulerInitialized bool = false 36 | 37 | // TaskSchedulerAnalysisRoutine analyse Windows Task Scheduler executable every 15 seconds 38 | func TaskSchedulerAnalysisRoutine(pQuarantine string, pKill bool, pNotifications bool, pVerbose bool, pInfiniteLoop bool, rules *yara.Rules) { 39 | for { 40 | tasks, err := GetTasks() 41 | if err != nil && pVerbose { 42 | logMessage(LOG_ERROR, "[ERROR]", err) 43 | } 44 | 45 | for _, t := range tasks { 46 | for _, e := range t.ActionList { 47 | paths := FormatPathFromComplexString(e.Path) 48 | for _, p := range paths { 49 | FileAnalysis(p, pQuarantine, pKill, pNotifications, pVerbose, rules, "TASKS") 50 | } 51 | } 52 | } 53 | 54 | if !pInfiniteLoop { 55 | wg.Done() 56 | break 57 | } else { 58 | time.Sleep(15 * time.Second) 59 | } 60 | } 61 | } 62 | 63 | // InitTaskScheduler Initialize COM API & Task scheduler connect 64 | func InitTaskScheduler() error { 65 | var err error 66 | if err = ole.CoInitializeEx(0, 0); err != nil { 67 | return errors.New("Could not initialize Windows COM API") 68 | } 69 | 70 | // Create an ITaskService object 71 | unknown, err = ole.CreateInstance(ole.NewGUID("{0f87369f-a4e5-4cfc-bd3e-73e6154572dd}"), nil) 72 | if err != nil { 73 | return errors.New("Could not initialize Task Scheduler") 74 | } 75 | 76 | // Convert IUnknown to IDispatch to get more functions like CallMethod() 77 | ts, err = unknown.QueryInterface(ole.IID_IDispatch) 78 | if err != nil { 79 | return errors.New("Could not prepare Task Scheduler") 80 | } 81 | 82 | // Connect to the Task Scheduler 83 | if _, err = ts.CallMethod("Connect", "", "", "", ""); err != nil { 84 | return errors.New("Could not connect to Task Scheduler") 85 | } 86 | 87 | return nil 88 | } 89 | 90 | // UninitializeTaskScheduler Release Task Scheduler COM API 91 | func UninitializeTaskScheduler() { 92 | ole.CoUninitialize() 93 | unknown.Release() 94 | ts.Release() 95 | } 96 | 97 | // GetTasks returns a list of all scheduled Tasks in Windows Task Scheduler 98 | func GetTasks() ([]Task, error) { 99 | var err error 100 | 101 | if !taskSchedulerInitialized { 102 | err = InitTaskScheduler() 103 | if err != nil { 104 | return nil, err 105 | } 106 | taskSchedulerInitialized = true 107 | } 108 | 109 | // Get Root Directory of Task Scheduler and get all tasks recursively 110 | variant, err := oleutil.CallMethod(ts, "GetFolder", "\\") 111 | if err != nil { 112 | return nil, errors.New("Could not get root folder in Task Scheduler") 113 | } 114 | root := variant.ToIDispatch() 115 | defer root.Release() 116 | return getTasksRecursively(root), nil 117 | } 118 | 119 | func getTasksRecursively(folder *ole.IDispatch) (tasks []Task) { 120 | var ( 121 | variant *ole.VARIANT 122 | err error 123 | ) 124 | // Get Tasks in subfolders first 125 | if variant, err = oleutil.CallMethod(folder, "GetFolders", int64(0)); err != nil { 126 | return 127 | } 128 | folderIterator := variant.ToIDispatch() 129 | if variant, err = oleutil.GetProperty(folderIterator, "count"); err != nil { 130 | return 131 | } 132 | count, _ := variant.Value().(int32) 133 | for i := int32(1); i <= count; i++ { 134 | // Get Tasks of subfolder i 135 | index := ole.NewVariant(ole.VT_I4, int64(i)) 136 | if variant, err = oleutil.GetProperty(folderIterator, "item", &index); err != nil { 137 | continue 138 | } 139 | subfolder := variant.ToIDispatch() 140 | subtasks := getTasksRecursively(subfolder) 141 | tasks = append(tasks, subtasks...) 142 | subfolder.Release() 143 | } 144 | folderIterator.Release() 145 | // Get Tasks 146 | if variant, err = oleutil.CallMethod(folder, "GetTasks", int64(0)); err != nil { 147 | return 148 | } 149 | taskIterator := variant.ToIDispatch() 150 | if variant, err = oleutil.GetProperty(taskIterator, "count"); err != nil { 151 | return 152 | } 153 | count, _ = variant.Value().(int32) 154 | for i := int32(1); i <= count; i++ { 155 | // Get Task i 156 | index := ole.NewVariant(ole.VT_I4, int64(i)) 157 | if variant, err = oleutil.GetProperty(taskIterator, "item", &index); err != nil { 158 | continue 159 | } 160 | task := variant.ToIDispatch() 161 | var t Task 162 | if variant, err = oleutil.GetProperty(task, "name"); err == nil { 163 | t.Name = variant.ToString() 164 | } 165 | if variant, err = oleutil.GetProperty(task, "path"); err == nil { 166 | t.Path = variant.ToString() 167 | } 168 | if variant, err = oleutil.GetProperty(task, "enabled"); err == nil { 169 | t.Enabled, _ = variant.Value().(bool) 170 | } 171 | if variant, err = oleutil.GetProperty(task, "lastRunTime"); err == nil { 172 | t.LastRunTime, _ = variant.Value().(time.Time) 173 | } 174 | if variant, err = oleutil.GetProperty(task, "nextRunTime"); err == nil { 175 | t.NextRunTime, _ = variant.Value().(time.Time) 176 | } 177 | // Get more details, e.g. actions 178 | if variant, err = oleutil.GetProperty(task, "definition"); err == nil { 179 | definition := variant.ToIDispatch() 180 | if variant, err = oleutil.GetProperty(definition, "actions"); err == nil { 181 | actions := variant.ToIDispatch() 182 | if variant, err = oleutil.GetProperty(actions, "count"); err == nil { 183 | count2, _ := variant.Value().(int32) 184 | for i := int32(1); i <= count2; i++ { 185 | // Get Action i 186 | index := ole.NewVariant(ole.VT_I4, int64(i)) 187 | if variant, err = oleutil.GetProperty(actions, "item", &index); err != nil { 188 | continue 189 | } 190 | action := variant.ToIDispatch() 191 | if variant, err = oleutil.GetProperty(action, "type"); err != nil { 192 | action.Release() 193 | continue 194 | } 195 | actionType, _ := variant.Value().(int32) 196 | if actionType != 0 { // only handle IExecAction 197 | action.Release() 198 | continue 199 | } 200 | var a ExecAction 201 | if variant, err = oleutil.GetProperty(action, "workingDirectory"); err == nil { 202 | a.WorkingDirectory = variant.ToString() 203 | } 204 | if variant, err = oleutil.GetProperty(action, "path"); err == nil { 205 | a.Path = variant.ToString() 206 | } 207 | if variant, err = oleutil.GetProperty(action, "arguments"); err == nil { 208 | a.Arguments = variant.ToString() 209 | } 210 | t.ActionList = append(t.ActionList, a) 211 | action.Release() 212 | } 213 | } 214 | } 215 | } 216 | tasks = append(tasks, t) 217 | task.Release() 218 | } 219 | taskIterator.Release() 220 | return 221 | } 222 | -------------------------------------------------------------------------------- /yaraProcessing.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rc4" 6 | "errors" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/gen2brain/go-unarr" 11 | "github.com/hillu/go-yara/v4" 12 | ) 13 | 14 | // PerformYaraScan use provided YARA rules and search for match in the given byte slice 15 | func PerformYaraScan(data *[]byte, rules *yara.Rules, verbose bool) yara.MatchRules { 16 | result, err := YaraScan(*data, rules) 17 | if err != nil && verbose { 18 | logMessage(LOG_ERROR, "[ERROR]", err) 19 | } 20 | 21 | return result 22 | } 23 | 24 | // PerformArchiveYaraScan try to decompress archive and YARA scan every file in it 25 | func PerformArchiveYaraScan(path string, rules *yara.Rules, verbose bool) (matchs yara.MatchRules) { 26 | var buffer [][]byte 27 | 28 | a, err := unarr.NewArchive(path) 29 | if err != nil { 30 | if verbose { 31 | logMessage(LOG_ERROR, "[ERROR]", err) 32 | } 33 | return matchs 34 | } 35 | defer a.Close() 36 | 37 | list, err := a.List() 38 | if err != nil { 39 | if verbose { 40 | logMessage(LOG_ERROR, "[ERROR]", err) 41 | } 42 | return matchs 43 | } 44 | for _, f := range list { 45 | err := a.EntryFor(f) 46 | if err != nil { 47 | if verbose { 48 | logMessage(LOG_ERROR, "[ERROR]", err) 49 | } 50 | return matchs 51 | } 52 | 53 | data, err := a.ReadAll() 54 | if err != nil { 55 | if verbose { 56 | logMessage(LOG_ERROR, "[ERROR]", err) 57 | } 58 | return matchs 59 | } 60 | 61 | buffer = append(buffer, data) 62 | } 63 | 64 | matchs, err = YaraScan(bytes.Join(buffer, []byte{}), rules) 65 | if err != nil && verbose { 66 | if verbose { 67 | logMessage(LOG_ERROR, "[ERROR]", err) 68 | } 69 | return matchs 70 | } 71 | 72 | return matchs 73 | } 74 | 75 | // SearchForYaraFiles search *.yar file by walking recursively from specified input path 76 | func SearchForYaraFiles(path string, verbose bool) (rules []string) { 77 | rules, err := RetrivesFilesFromUserPath(path, true, []string{".yar"}, true, verbose) 78 | if err != nil && verbose { 79 | logMessage(LOG_INFO, err) 80 | } 81 | return rules 82 | } 83 | 84 | // LoadYaraRules compile yara rules from specified paths and return a pointer to the yara compiler 85 | func LoadYaraRules(path []string, rc4key string, verbose bool) (compiler *yara.Compiler, err error) { 86 | compiler, err = yara.NewCompiler() 87 | if err != nil { 88 | return nil, errors.New("Failed to initialize YARA compiler") 89 | } 90 | 91 | for _, dir := range path { 92 | f, err := os.ReadFile(dir) 93 | if err != nil && verbose { 94 | logMessage(LOG_ERROR, "[ERROR]", "Could not read rule file ", dir, err) 95 | } 96 | 97 | if len(rc4key) > 0 && !bytes.Contains(f, []byte("rule ")) { 98 | c, err := rc4.NewCipher([]byte(rc4key)) 99 | if err != nil { 100 | logMessage(LOG_ERROR, "[ERROR]", err) 101 | } 102 | 103 | c.XORKeyStream(f, f) 104 | } 105 | 106 | namespace := filepath.Base(dir)[:len(filepath.Base(dir))-4] 107 | if err = compiler.AddString(string(f), namespace); err != nil && verbose { 108 | logMessage(LOG_ERROR, "[ERROR]", "Could not load rule file ", dir, err) 109 | } 110 | } 111 | 112 | return compiler, nil 113 | } 114 | 115 | // CompileRules try to compile every rules from the given compiler 116 | func CompileRules(compiler *yara.Compiler) (rules *yara.Rules, err error) { 117 | 118 | rules, err = compiler.GetRules() 119 | if err != nil { 120 | return nil, errors.New("Failed to compile rules") 121 | } 122 | 123 | return rules, err 124 | } 125 | 126 | // YaraScan use libyara to scan the specified content with a compiled rule 127 | func YaraScan(content []byte, rules *yara.Rules) (match yara.MatchRules, err error) { 128 | sc, _ := yara.NewScanner(rules) 129 | var m yara.MatchRules 130 | err = sc.SetCallback(&m).ScanMem(content) 131 | return m, err 132 | } 133 | --------------------------------------------------------------------------------