diff --git a/.gitignore b/.gitignore index 6eba9a8..080e932 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ # Compiled command /cmd/yapscan/yapscan -/build +/cicd/build/ # Dependency directories (remove the comment below to include it) vendor/ @@ -25,6 +25,3 @@ vendor/ # Logfiles *.log - -# Generated Mocks (at least, while I'm working on the tests) -mock_*_test.go \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 194609a..3368f90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ before_install: - pkg-config --cflags --libs yara install: - - ./prepare.sh + - go mod vendor script: - go test -race -coverprofile=coverage.txt -covermode=atomic -v ./... @@ -31,13 +31,15 @@ script: - pushd cmd/yapscan - go build -trimpath -o ../../build/yapscan - popd - - ./buildForWindows.sh + - pushd cicd/ + - ./crossBuildForWindows.sh + - popd after_success: - bash <(curl -s https://codecov.io/bash) before_deploy: - - pushd build + - pushd cicd/build - 7z a yapscan_windows_amd64.zip yapscan.exe yapscan.dll - 7z a yapscan_linux_amd64.zip yapscan - tar -cvzf yapscan_windows_amd64.tar.gz yapscan.exe yapscan.dll @@ -50,10 +52,10 @@ deploy: api_key: secure: SEqzkWiVZTZlK3FvJGku7a10rMVqSPTakdAP2M7p6z+S2SE3yRI4R4aiH3t73Yt2nDPGrW1ie43EsJ0WcPIIVaSHUArNBwdyoGRBtdAq461ZSwsskuMLVz28SreyiTwEb4cmRrx5eggGyAoO6kzuGyBVdnBY8Bzxtj0CVV/qeHxvvD6ARYxBQtq6izomd4hoMnCRKNApIUDFbLo2gnF4YOuAMgi45ALe5jchSHPESsa/e8x2e13WEOCIwmiHxRR4EXS/oOxr8kO792HEa324V7uPE1HaIH3z7fBWWcUMXJZSAYynbzsC2WAHzWgIcFHyqVUcs6IKMU8rweJcNNDIVrC/zY7Jm4/aEdyy/Kq3iReuSUkvv9HY0C+CGf8RK/7x/1LolYXDnXGIeEXF/dkqE4p/bOCKrlW8TePoEf8zUlSiZRd0+XdodyxrbjFdx0E5kePxLGeqkopfR/ubKN5DJY93ueFsVlKYGpWLOKxtZTJoxEcwhSm1EF2Lsd77+xySjeJbh00Ozu51C7lu8Obn4wzWCVLDnlt3X5pSqYEHKphIfVYr21LPD9uQc++XR7IfPQG/Gnz/CTBEGvwTYm29jaD0m5e1pshL5nr5lhCOQIH4iZWSmoCwa8GP3Kbix9tYtPAdOkd6crz3vj0EUEWmd9ioEutTOL3wduGzNTcBvXM= file: - - build/yapscan_windows_amd64.zip - - build/yapscan_windows_amd64.tar.gz - - build/yapscan_linux_amd64.zip - - build/yapscan_linux_amd64.tar.gz + - cicd/build/yapscan_windows_amd64.zip + - cicd/build/yapscan_windows_amd64.tar.gz + - cicd/build/yapscan_linux_amd64.zip + - cicd/build/yapscan_linux_amd64.tar.gz on: repo: fkie-cad/yapscan tags: true diff --git a/README.md b/README.md index 5f46e36..30ef5df 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# yapscan [![Build Status](https://travis-ci.org/fkie-cad/yapscan.svg?branch=master)](https://travis-ci.org/fkie-cad/yapscan) [![codecov](https://codecov.io/gh/fkie-cad/yapscan/branch/master/graph/badge.svg?token=Y2ANV37QH6)](https://codecov.io/gh/fkie-cad/yapscan) +# yapscan [![Build Status](https://travis-ci.org/fkie-cad/yapscan.svg?branch=master)](https://travis-ci.org/fkie-cad/yapscan) [![codecov](https://codecov.io/gh/fkie-cad/yapscan/branch/master/graph/badge.svg?token=Y2ANV37QH6)](https://codecov.io/gh/fkie-cad/yapscan) [![Go Report Card](https://goreportcard.com/badge/github.com/fkie-cad/yapscan)](https://goreportcard.com/report/github.com/fkie-cad/yapscan) Yapscan is a **YA**ra based **P**rocess **SCAN**ner, aimed at giving more control about what to scan and giving detailed reports on matches. @@ -155,16 +155,13 @@ Yapscan copies one memory segment at a time into a buffer in its own memory and ## Building Yapscan -This project can only be built on Linux at this time. -To build natively on Linux, for Linux you need install Go and the yara library. +To build **natively on Linux**, for Linux you need install Go and the yara library. Once you have installed the dependencies it's as easy as: ```bash # Install Golang and libyara git clone https://github.com/fkie-cad/yapscan -cd yapscan -./prepare.sh -cd cmd/yapscan +cd yapscan/cmd/yapscan go build ``` @@ -173,13 +170,47 @@ If you want to build on Linux for Windows, all you need installed is docker. ```bash # Install docker git clone https://github.com/fkie-cad/yapscan -cd yapscan -./buildForWindows.sh +cd yapscan/cicd/ +./crossBuildForWindows.sh ``` -### Why can I not build this project on Windows? - -I have been unable, so far, to build libyara on Windows and marry the result to the go toolchain to create a static build. - -If you have experience with cgo on Windows, it would be great if you could help out. -This is relatively important for automated testing. +The resulting binaries will be placed in `cicd/build/`. + +Building **natively on Windows**, using MSYS2 follow these instructions + +1. Install Go +2. Install MSYS2 and follow the first steps on [the MSYS2 Website] of updating via pacman. +3. Install build dependencies `pacman --needed -S base-devel git autoconf automake libtool mingw-w64-{x86_64,i686}-{gcc,make,pkgconf}` +4. Open PowerShell in the `cicd/` directory and execute `.\buildOnWindows.ps1 -MsysPath -BuildDeps` + where `` is the install directory for MSYS2, default is `C:\msys64`. + **NOTE:** You'll have to press `Enter` on the MSYS window, once the dependencies are finished. +5. Enjoy the built files in `cicd/build/` + +If you want to run tests on Windows, you have to run `.\cicd\buildOnWindows.ps1 -BuildDeps` only once. +Then you open PowerShell and execute `.\cicd\enableMingw.ps1 -MsysPath ` to set the appropriate environment variables. +Now it's as easy as `go test -tags yara_static ./...`. +The `-tags yara_static` is necessary if you use the build scripts, as they do not install any windows DLLs but only the static libraries. + +**NOTE:** You might get inexplicable failures with `-race` on Windows with golagn >=1.14. +According to the [golang release notes for 1.14](https://golang.org/doc/go1.14#compiler), the new pointer arithmetic checks are somewhat overzealous on Windows. +In Golang v1.15 it seems this may not have been fixed, but the checks are enabled automatically. +You can deactivate them like this `go test -tags yara_static -race -gcflags=all=-d=checkptr=0 ./...`. + +You don't have to rely on the powershell/bash scripts, but they are intended to make things as easy as possible at the cost of control over the compilation. +If you want more control, take a look at the scripts use and modify them or execute the commands individually. +The scripts perform the following tasks. + +1. Start "MSYS2 MinGW 64-bit" + 1. Download OpenSSL from github + 2. Static-Build OpenSSL and install the development files + 3. Download libyara from github + 4. Static-Build libyara and install it +2. Set some environment variables in powershell, to allow the use of the mingw toolchain +3. Call the `go build` command with the appropriate build tag for static builds + +Thanks to [@hillu] (author of [go-yara]), for pointing me in the right direction for building natively on windows. +See #7 and the links therein if you want some more details. + +[the MSYS2 Website]: https://www.msys2.org/ +[@hillu]: https://github.com/hillu/ +[go-yara]: https://github.com/hillu/go-yara/ \ No newline at end of file diff --git a/WinApi.md b/WinApi.md index c50a74f..78dbae7 100644 --- a/WinApi.md +++ b/WinApi.md @@ -2,7 +2,7 @@ In this document, an overview of the win32 APIs is given regarding process and memory scanning. If the scanner is a 64-bit application, these calls result in an executable which can scan memory of both 64- and 32-bit processes. -The shown code samples are not valid C code. They are supposed to be more akin to pseudo-code to give you a starting point for researching how to access another processes memory. You can find the shown API calls in the context of Go in these files: [procIO/process_windows.go](procIO/process_windows.go), [procIO/memory_windows.go](procIO/memory_windows.go) and [procIO/reader_windows.go](procIO/reader_windows.go). +The shown code samples are not valid C code. They are supposed to be more akin to pseudo-code to give you a starting point for researching how to access another processes memory. You can find the shown API calls in the context of Go in these files: [procio/process_windows.go](procio/process_windows.go), [procio/memory_windows.go](procio/memory_windows.go) and [procio/reader_windows.go](procio/reader_windows.go). ## Listing Processes diff --git a/app/app.go b/app/app.go index 81efc2d..ef00a2f 100644 --- a/app/app.go +++ b/app/app.go @@ -10,7 +10,6 @@ import ( "github.com/fkie-cad/yapscan/output" "github.com/sirupsen/logrus" - "github.com/targodan/go-errors" "github.com/urfave/cli/v2" ) @@ -41,7 +40,7 @@ func initAppAction(c *cli.Context) error { default: logfile, err := os.OpenFile(c.String("log-path"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { - return errors.Errorf("could not open logfile for writing, reason: %w", err) + return fmt.Errorf("could not open logfile for writing, reason: %w", err) } logrus.SetOutput(logfile) logrus.RegisterExitHandler(func() { @@ -60,32 +59,32 @@ func filterFromArgs(c *cli.Context) (yapscan.MemorySegmentFilter, error) { filters[i], err = BuildFilterPermissions(c.String("filter-permissions")) if err != nil { - return nil, errors.Errorf("invalid flag \"--filter-permissions\", reason: %w", err) + return nil, fmt.Errorf("invalid flag \"--filter-permissions\", reason: %w", err) } i += 1 filters[i], err = BuildFilterPermissionsExact(c.StringSlice("filter-permissions-exact")) if err != nil { - return nil, errors.Errorf("invalid flag \"--filter-permissions-exact\", reason: %w", err) + return nil, fmt.Errorf("invalid flag \"--filter-permissions-exact\", reason: %w", err) } i += 1 filters[i], err = BuildFilterType(c.StringSlice("filter-type")) if err != nil { - return nil, errors.Errorf("invalid flag \"--filter-type\", reason: %w", err) + return nil, fmt.Errorf("invalid flag \"--filter-type\", reason: %w", err) } i += 1 filters[i], err = BuildFilterState(c.StringSlice("filter-state")) if err != nil { - return nil, errors.Errorf("invalid flag \"--filter-state\", reason: %w", err) + return nil, fmt.Errorf("invalid flag \"--filter-state\", reason: %w", err) } i += 1 filters[i], err = BuildFilterSizeMax(c.String("filter-size-max")) if err != nil { - return nil, errors.Errorf("invalid flag \"--filter-size-max\", reason: %w", err) + return nil, fmt.Errorf("invalid flag \"--filter-size-max\", reason: %w", err) } i += 1 filters[i], err = BuildFilterSizeMin(c.String("filter-size-min")) if err != nil { - return nil, errors.Errorf("invalid flag \"--filter-size-min\", reason: %w", err) + return nil, fmt.Errorf("invalid flag \"--filter-size-min\", reason: %w", err) } i += 1 @@ -204,7 +203,7 @@ func RunApp(args []string) { Name: "yapscan", HelpName: "yapscan", Description: "A yara based scanner for files and process memory with some extras.", - Version: "0.4.0", + Version: "0.5.0", Writer: os.Stdout, ErrWriter: os.Stderr, Authors: []*cli.Author{ @@ -410,9 +409,40 @@ func RunApp(args []string) { }, }, }, + &cli.Command{ + Name: "crash-processe", + Aliases: []string{"crash"}, + Usage: "crash a processe", + Action: crashProcess, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "method", + Aliases: []string{"m"}, + Usage: "output errors if any are encountered", + Value: "CreateThreadOnNull", + }, + }, + }, }, } + if runtime.GOOS == "windows" { + app.Commands = append(app.Commands, + &cli.Command{ + Name: "as-service", + Usage: "executes yapscan as a windows service", + Action: func(c *cli.Context) error { + // This is a dummy + return cli.Exit("\"as-service\" must be the first argument", 1) + }, + }) + + if len(args) >= 2 && args[1] == "as-service" { + asService(args) + return + } + } + err := app.Run(args) if err != nil { fmt.Println(err) diff --git a/app/asService.go b/app/asService.go new file mode 100644 index 0000000..6714838 --- /dev/null +++ b/app/asService.go @@ -0,0 +1,55 @@ +package app + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" +) + +func run(cmdName string, cmdArgs ...string) error { + cmd := exec.Command(cmdName, cmdArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func asService(args []string) { + serviceName := "yapscan" + + binpath, err := filepath.Abs(args[0]) + if err != nil { + fmt.Printf("ERROR: Could not determine absolute path of yapscan.exe, %v\n", err) + os.Exit(1) + } + + args = args[2:] + scStartArguments := []string{"start", serviceName} + scStartArguments = append(scStartArguments, args...) + + fmt.Println("WARNING: This feature is experimental!") + fmt.Println("WARNING: You will not see any output of the executed service. Using --log-path is strongly advised.") + + fmt.Println("Removing service in case it exists already...") + run("sc.exe", "delete", "yapscan") + fmt.Println("Done removing.") + + fmt.Println("Installing service...") + err = run("sc.exe", "create", serviceName, "type=", "own", "start=", "demand", "binpath=", binpath) + if err != nil { + fmt.Println("FAILURE") + fmt.Print(err) + os.Exit(10) + } + fmt.Println("Done installing service.") + + fmt.Println("Starting service with arguments...") + err = run("sc.exe", scStartArguments...) + if err != nil { + fmt.Println("FAILURE") + fmt.Print(err) + os.Exit(11) + } + fmt.Println("Done starting service, yapscan should now be running as a service with the following arguments") + fmt.Println(args) +} diff --git a/app/crash.go b/app/crash.go new file mode 100644 index 0000000..160c30f --- /dev/null +++ b/app/crash.go @@ -0,0 +1,38 @@ +package app + +import ( + "fmt" + "strconv" + + "github.com/fkie-cad/yapscan/procio" + "github.com/targodan/go-errors" + "github.com/urfave/cli/v2" +) + +func crashProcess(c *cli.Context) error { + err := initAppAction(c) + if err != nil { + return err + } + + if c.NArg() != 1 { + return errors.Newf("expected exactly one arguments, got %d", c.NArg()) + } + pid_, err := strconv.ParseUint(c.Args().Get(0), 10, 64) + if err != nil { + return errors.Newf("\"%s\" is not a pid", c.Args().Get(0)) + } + pid := int(pid_) + + crashMethod, err := procio.ParseCrashMethod(c.String("method")) + if err != nil { + return fmt.Errorf("invalid parameter, %w", err) + } + + proc, err := procio.OpenProcess(pid) + if err != nil { + return errors.Newf("could not open process %d, reason: %w", pid, err) + } + + return proc.Crash(crashMethod) +} diff --git a/app/dump.go b/app/dump.go index 6989ddf..331bd40 100644 --- a/app/dump.go +++ b/app/dump.go @@ -8,8 +8,7 @@ import ( "path" "strconv" - "github.com/fkie-cad/yapscan/procIO" - + "github.com/fkie-cad/yapscan/procio" "github.com/targodan/go-errors" "github.com/urfave/cli/v2" ) @@ -51,7 +50,7 @@ func dumpMemory(c *cli.Context) error { } } - proc, err := procIO.OpenProcess(pid) + proc, err := procio.OpenProcess(pid) if err != nil { return errors.Newf("could not open process %d, reason: %w", pid, err) } @@ -61,7 +60,7 @@ func dumpMemory(c *cli.Context) error { return errors.Newf("could not retrieve memory segments of process %d, reason: %w", pid, err) } // Unpack segments - segments := make([]*procIO.MemorySegmentInfo, 0, len(baseSegments)) + segments := make([]*procio.MemorySegmentInfo, 0, len(baseSegments)) for _, seg := range baseSegments { if seg.SubSegments == nil || len(seg.SubSegments) == 0 { segments = append(segments, seg) @@ -83,7 +82,7 @@ func dumpMemory(c *cli.Context) error { continue } if found { - rdr, err := procIO.NewMemoryReader(proc, seg) + rdr, err := procio.NewMemoryReader(proc, seg) if err != nil { fmt.Println(errors.Newf("could not read memory of process %d at address 0x%016X, reason %w", pid, seg.BaseAddress, err)) continue diff --git a/app/filter.go b/app/filter.go index 607a04a..ef002ae 100644 --- a/app/filter.go +++ b/app/filter.go @@ -1,12 +1,13 @@ package app import ( + "fmt" "regexp" "strconv" "strings" "github.com/fkie-cad/yapscan" - "github.com/fkie-cad/yapscan/procIO" + "github.com/fkie-cad/yapscan/procio" "github.com/fkie-cad/yapscan/system" "github.com/sirupsen/logrus" @@ -21,9 +22,9 @@ func BuildFilterPermissions(fStr string) (yapscan.MemorySegmentFilter, error) { return nil, nil } - perm, err := procIO.ParsePermissions(fStr) + perm, err := procio.ParsePermissions(fStr) if err != nil { - return nil, errors.Errorf("could not parse permissions \"%s\", reason: %w", fStr, err) + return nil, fmt.Errorf("could not parse permissions \"%s\", reason: %w", fStr, err) } return yapscan.NewPermissionsFilter(perm), nil @@ -36,11 +37,11 @@ func BuildFilterPermissionsExact(fStr []string) (yapscan.MemorySegmentFilter, er return nil, nil } - perms := make([]procIO.Permissions, len(fStr)) + perms := make([]procio.Permissions, len(fStr)) for i, s := range fStr { - perms[i], err = procIO.ParsePermissions(s) + perms[i], err = procio.ParsePermissions(s) if err != nil { - return nil, errors.Errorf("could not parse permissions \"%s\", reason: %w", s, err) + return nil, fmt.Errorf("could not parse permissions \"%s\", reason: %w", s, err) } } @@ -54,14 +55,14 @@ func BuildFilterType(fStr []string) (yapscan.MemorySegmentFilter, error) { return nil, nil } - types := make([]procIO.Type, len(fStr)) + types := make([]procio.Type, len(fStr)) for i, s := range fStr { if s == "" { continue } - types[i], err = procIO.ParseType(strings.ToUpper(s[0:1]) + strings.ToLower(s[1:])) + types[i], err = procio.ParseType(strings.ToUpper(s[0:1]) + strings.ToLower(s[1:])) if err != nil { - return nil, errors.Errorf("could not parse type \"%s\", reason: %w", s, err) + return nil, fmt.Errorf("could not parse type \"%s\", reason: %w", s, err) } } @@ -75,14 +76,14 @@ func BuildFilterState(fStr []string) (yapscan.MemorySegmentFilter, error) { return nil, nil } - states := make([]procIO.State, len(fStr)) + states := make([]procio.State, len(fStr)) for i, s := range fStr { if s == "" { continue } - states[i], err = procIO.ParseState(strings.ToUpper(s[0:1]) + strings.ToLower(s[1:])) + states[i], err = procio.ParseState(strings.ToUpper(s[0:1]) + strings.ToLower(s[1:])) if err != nil { - return nil, errors.Errorf("could not parse state \"%s\", reason: %w", s, err) + return nil, fmt.Errorf("could not parse state \"%s\", reason: %w", s, err) } } @@ -96,7 +97,7 @@ func BuildFilterSizeMin(fStr string) (yapscan.MemorySegmentFilter, error) { size, err := ParseSizeArgument(fStr) if err != nil { - return nil, errors.Errorf("could not parse size \"%s\", reason: %w", fStr, err) + return nil, fmt.Errorf("could not parse size \"%s\", reason: %w", fStr, err) } logrus.Infof("Filtering for minimum size %s", humanize.Bytes(uint64(size))) @@ -111,7 +112,7 @@ func BuildFilterSizeMax(fStr string) (yapscan.MemorySegmentFilter, error) { size, err := ParseSizeArgument(fStr) if err != nil { - return nil, errors.Errorf("could not parse size \"%s\", reason: %w", fStr, err) + return nil, fmt.Errorf("could not parse size \"%s\", reason: %w", fStr, err) } logrus.Infof("Filtering for maximum size %s", humanize.Bytes(uint64(size))) @@ -149,16 +150,16 @@ func ParseRelativeSize(s string) (uintptr, error) { case "t": fallthrough case "total": - max, err = system.GetTotalRAM() + max, err = system.TotalRAM() if err != nil { - err = errors.Errorf("could not get total RAM, reason: %w", err) + err = fmt.Errorf("could not get total RAM, reason: %w", err) } case "f": fallthrough case "free": - max, err = system.GetFreeRAM() + max, err = system.FreeRAM() if err != nil { - err = errors.Errorf("could not get free RAM, reason: %w", err) + err = fmt.Errorf("could not get free RAM, reason: %w", err) } default: err = errors.Newf("unknown relative definition \"%s\", must be \"[t]otal\" or \"[f]ree\"", relToWhat) diff --git a/app/listMem.go b/app/listMem.go index 1fd4e59..8dfd286 100644 --- a/app/listMem.go +++ b/app/listMem.go @@ -4,9 +4,8 @@ import ( "fmt" "strconv" - "github.com/fkie-cad/yapscan/procIO" - "github.com/dustin/go-humanize" + "github.com/fkie-cad/yapscan/procio" "github.com/targodan/go-errors" "github.com/urfave/cli/v2" ) @@ -31,7 +30,7 @@ func listMemory(c *cli.Context) error { return err } - proc, err := procIO.OpenProcess(pid) + proc, err := procio.OpenProcess(pid) if err != nil { return errors.Newf("could not open process with pid %d, reason: %w", pid, err) } @@ -48,11 +47,11 @@ func listMemory(c *cli.Context) error { format := "%19s %8s %3s %7s %7s %s\n" - fmt.Printf(format, procIO.FormatMemorySegmentAddress(seg), humanize.Bytes(uint64(seg.Size)), seg.CurrentPermissions, seg.Type, seg.State, seg.FilePath) + fmt.Printf(format, procio.FormatMemorySegmentAddress(seg), humanize.Bytes(uint64(seg.Size)), seg.CurrentPermissions, seg.Type, seg.State, seg.FilePath) if c.Bool("list-subdivided") { for i, sseg := range seg.SubSegments { - addr := procIO.FormatMemorySegmentAddress(sseg) + addr := procio.FormatMemorySegmentAddress(sseg) if i+1 < len(seg.SubSegments) { addr = "├" + addr } else { diff --git a/app/listProcs.go b/app/listProcs.go index a4f6440..6348c8d 100644 --- a/app/listProcs.go +++ b/app/listProcs.go @@ -6,8 +6,7 @@ import ( "strconv" "strings" - "github.com/fkie-cad/yapscan/procIO" - + "github.com/fkie-cad/yapscan/procio" "github.com/targodan/go-errors" "github.com/urfave/cli/v2" ) @@ -18,19 +17,19 @@ func listProcesses(c *cli.Context) error { return err } - pids, err := procIO.GetRunningPIDs() + pids, err := procio.GetRunningPIDs() if err != nil { return errors.Newf("could not enumerate PIDs, reason: %w", err) } - procInfos := make([]*procIO.ProcessInfo, len(pids)) + procInfos := make([]*procio.ProcessInfo, len(pids)) maxPidlen := 0 maxNamelen := 0 maxUserlen := 0 errorsOutput := false for i, pid := range pids { // Default info in case of errors - info := &procIO.ProcessInfo{ + info := &procio.ProcessInfo{ PID: pid, ExecutablePath: "ERROR", ExecutableMD5: "ERROR", @@ -39,7 +38,7 @@ func listProcesses(c *cli.Context) error { MemorySegments: nil, } - proc, err := procIO.OpenProcess(pid) + proc, err := procio.OpenProcess(pid) if err != nil { err = errors.Newf("could not open process %d, reason: %w", pid, err) } else { diff --git a/app/scan.go b/app/scan.go index 72d1eeb..f550633 100644 --- a/app/scan.go +++ b/app/scan.go @@ -9,9 +9,9 @@ import ( "strconv" "github.com/fkie-cad/yapscan" - "github.com/fkie-cad/yapscan/fileIO" + "github.com/fkie-cad/yapscan/fileio" "github.com/fkie-cad/yapscan/output" - "github.com/fkie-cad/yapscan/procIO" + "github.com/fkie-cad/yapscan/procio" "github.com/sirupsen/logrus" "github.com/targodan/go-errors" @@ -61,7 +61,7 @@ func scan(c *cli.Context) error { } if c.Bool("all-processes") { - pids, err = procIO.GetRunningPIDs() + pids, err = procio.GetRunningPIDs() if err != nil { return errors.Newf("could not enumerate PIDs, reason: %w", err) } @@ -69,7 +69,7 @@ func scan(c *cli.Context) error { if c.Bool("all-drives") { // TODO: Expose the drive types to flags - drives, err := fileIO.Enumerate(fileIO.DriveTypeFixed | fileIO.DriveTypeRemovable) + drives, err := fileio.Enumerate(fileio.DriveTypeFixed | fileio.DriveTypeRemovable) if err != nil { return fmt.Errorf("could not enumerate local drives, reason: %w", err) } @@ -78,7 +78,7 @@ func scan(c *cli.Context) error { if c.Bool("all-shares") { // TODO: Expose the drive types to flags - drives, err := fileIO.Enumerate(fileIO.DriveTypeRemote) + drives, err := fileio.Enumerate(fileio.DriveTypeRemote) if err != nil { return fmt.Errorf("could not enumerate net-shares, reason: %w", err) } @@ -92,16 +92,17 @@ func scan(c *cli.Context) error { logrus.Debug("Full report temp dir: ", tmpDir) gatherRep, err := output.NewGatheredAnalysisReporter(tmpDir) if err != nil { - return errors.Errorf("could not initialize analysis reporter, reason: %w", err) + return fmt.Errorf("could not initialize analysis reporter, reason: %w", err) } gatherRep.ZIP = filepath.Join(c.String("report-dir"), gatherRep.SuggestZIPName()) gatherRep.DeleteAfterZipping = !c.Bool("keep") fmt.Printf("Full report will be written to \"%s\".\n", gatherRep.ZIP) if c.Bool("store-dumps") { - err = gatherRep.WithFileDumpStorage("dumps") + ds, err := output.NewFileDumpStorage(filepath.Join(gatherRep.Directory(), "dumps")) if err != nil { - return errors.Errorf("could not initialize analysis reporter, reason: %w", err) + return fmt.Errorf("could not initialize dump storage reporter, reason: %w", err) } + gatherRep.WithDumpStorage(ds) gatherRep.ZIPPassword = c.String("password") } reporter = &output.MultiReporter{ @@ -139,7 +140,7 @@ func scan(c *cli.Context) error { continue } - proc, err := procIO.OpenProcess(pid) + proc, err := procio.OpenProcess(pid) if err != nil { logrus.WithError(err).Errorf("could not open process %d for scanning", pid) continue @@ -219,22 +220,22 @@ func scan(c *cli.Context) error { } iteratorCtx := context.Background() - var pathIterator fileIO.Iterator + var pathIterator fileio.Iterator for _, path := range paths { - pIt, err := fileIO.IteratePath(path, fileExtensions, iteratorCtx) + pIt, err := fileio.IteratePath(iteratorCtx, path, fileExtensions) if err != nil { fmt.Printf("- %s ERROR: could not intialize scanner for path, reason: %v", path, err) logrus.WithError(err).Errorf("Could not initialize scanner for path \"%s\".", path) continue } - pathIterator = fileIO.Concurrent(pathIterator, pIt) + pathIterator = fileio.Concurrent(pathIterator, pIt) fmt.Printf("- %s\n", path) } if pathIterator != nil { defer pathIterator.Close() - fsScanner := fileIO.NewFSScanner(yaraScanner) + fsScanner := fileio.NewFSScanner(yaraScanner) fsScanner.NGoroutines = c.Int("threads") progress, _ := fsScanner.Scan(pathIterator) diff --git a/arch/arch.go b/arch/arch.go index 44bf763..73c8f7f 100644 --- a/arch/arch.go +++ b/arch/arch.go @@ -1,10 +1,16 @@ +// Package arch provides information about the currently +// CPU architecture. package arch +// T describes a CPU architecture. type T int const ( + // Invalid describes an unknown architecture or an invalid enum value. Invalid T = iota + // AMD64 describes the amd64 architecture. AMD64 + // I386 describes the i386 architecture. I386 ) @@ -14,6 +20,7 @@ var bitness = map[T]Bitness{ I386: Bitness32Bit, } +// Bitness returns the Bitness of the architecture. func (t T) Bitness() Bitness { return bitness[t] } diff --git a/arch/bitness.go b/arch/bitness.go index 2af31a4..50880e5 100644 --- a/arch/bitness.go +++ b/arch/bitness.go @@ -1,6 +1,7 @@ //go:generate go-enum -f=$GOFILE --marshal --lower --names package arch +// Bitness describes the bitness of an architecture. /* ENUM( Invalid @@ -16,6 +17,7 @@ var bitnessShortNames = map[Bitness]string{ Bitness32Bit: "32", } +// Short returns a short, human readable representation of a Bitness. func (b Bitness) Short() string { return bitnessShortNames[b] } diff --git a/arch/error.go b/arch/error.go new file mode 100644 index 0000000..1415762 --- /dev/null +++ b/arch/error.go @@ -0,0 +1,9 @@ +package arch + +type ErrNotImplemented struct { + Message string +} + +func (e *ErrNotImplemented) Error() string { + return e.Message +} diff --git a/arch/get.go b/arch/get.go index 15c7a34..7f936b9 100644 --- a/arch/get.go +++ b/arch/get.go @@ -1,5 +1,6 @@ package arch +// Native returns the native architecture T of the running process. func Native() T { return get() } diff --git a/cgo.go b/cgo.go new file mode 100644 index 0000000..8eda65a --- /dev/null +++ b/cgo.go @@ -0,0 +1,4 @@ +package yapscan + +// #cgo yara_static LDFLAGS: -static +import "C" diff --git a/Dockerfile.xwin b/cicd/Dockerfile.xwin similarity index 66% rename from Dockerfile.xwin rename to cicd/Dockerfile.xwin index bc66d93..3ca65fb 100644 --- a/Dockerfile.xwin +++ b/cicd/Dockerfile.xwin @@ -10,20 +10,23 @@ RUN apt-get install -y build-essential pkg-config \ git gcc-multilib gcc-mingw-w64 autoconf automake \ libtool libjansson-dev libmagic-dev libssl-dev -RUN mkdir /opt/docker +RUN mkdir /opt/cicd ARG BUILD_THREADS=4 ENV BUILD_THREADS=$BUILD_THREADS +COPY determineBuildEnvironment.sh /opt/cicd + ARG OPENSSL_VERSION RUN git clone --depth=1 --branch=$OPENSSL_VERSION https://github.com/openssl/openssl.git /opt/openssl -COPY docker/buildOpenssl.sh /opt/docker -RUN /opt/docker/buildOpenssl.sh +COPY buildOpenssl.sh /opt/cicd +RUN /opt/cicd/buildOpenssl.sh /opt/openssl /opt/yapscan-deps ARG YARA_VERSION RUN git clone --depth=1 --branch=$YARA_VERSION https://github.com/VirusTotal/yara.git /opt/yara -COPY docker/buildYara.sh /opt/docker -RUN /opt/docker/buildYara.sh +COPY buildYara.sh /opt/cicd +ENV PKG_CONFIG_LIBDIR=/opt/yapscan-deps/lib/pkgconfig +RUN /opt/cicd/buildYara.sh /opt/yara /opt/yapscan-deps ENTRYPOINT /bin/bash CMD ["-"] \ No newline at end of file diff --git a/cicd/buildAndInstallDependencies.sh b/cicd/buildAndInstallDependencies.sh new file mode 100755 index 0000000..e7e92a2 --- /dev/null +++ b/cicd/buildAndInstallDependencies.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +DEFAULT_INSTALL_PREFIX=/opt/yapscan-deps + +buildEnv=$("$(dirname "$0")/determineBuildEnvironment.sh") || (echo "$buildEnv"; exit 42) + +if [[ "$buildEnv" != "mingw" ]]; then + echo "ERROR: This script is only meant for use with MSYS2 MinGW." + echo "If you want to use docker to cross compile yapscan for windows, use \"buildForWindows.sh\" instead." + echo "If you want to build natively build for linux, please manually install OpenSSL and libyara." + exit 42 +fi + +function showHelp() { + echo "Usage: ./buildAndInstallDependencies.sh [--overwrite|-o] " + echo " If --overwrite | -o is set, the source directories will be deleted and freshly cloned." + echo " is the directory, the source of all dependencies will be cloned into." + echo " The dependencies will be install into the prefix given by the INSTALL_PREFIX environment variable, or \"$DEFAULT_INSTALL_PREFIX\" by default." +} + +overwrite=0 +if [[ "$1" == "--overwrite" || "$1" == "-o" ]]; then + overwrite=1 + srcDir="$2" +else + srcDir="$1" +fi + +if [[ "$srcDir" == "" ]]; then + showHelp + exit 1 +fi + +mkdir -p "$srcDir" &>/dev/null + +if [[ "$overwrite" == "1" ]]; then + rm -rf "$srcDir/openssl" + rm -rf "$srcDir/yara" +fi + +cicd="$(dirname "$0")" + +OPENSSL_VERSION=$("$cicd/opensslVersion.sh") || exit $? +YARA_VERSION=$("$cicd/yaraVersion.sh") || exit $? + +INSTALL_PREFIX=${INSTALL_PREFIX:-$DEFAULT_INSTALL_PREFIX} + +if [ ! -d "$srcDir/openssl" ]; then + # openssl dir does not exist + git clone --depth=1 --branch=$OPENSSL_VERSION https://github.com/openssl/openssl.git "$srcDir/openssl" || exit $? +fi + +if [ ! -d "$srcDir/yara" ]; then + # yara dir does not exist + git clone --depth=1 --branch=$YARA_VERSION https://github.com/VirusTotal/yara.git "$srcDir/yara" || exit $? +fi + +"$cicd/buildOpenssl.sh" "$srcDir/openssl" "$INSTALL_PREFIX" || exit $? + +export PKG_CONFIG_LIBDIR="$INSTALL_PREFIX/lib/pkgconfig" +"$cicd/buildYara.sh" "$srcDir/yara" "$INSTALL_PREFIX" || exit $? diff --git a/cicd/buildOnWindows.ps1 b/cicd/buildOnWindows.ps1 new file mode 100644 index 0000000..009c537 --- /dev/null +++ b/cicd/buildOnWindows.ps1 @@ -0,0 +1,50 @@ +Param( + [Parameter(Mandatory=$False)] + [switch]$BuildDeps, + + [Parameter(Mandatory=$False)] + [switch]$OverwriteDeps, + + [Parameter(Mandatory=$False)] + [string]$MsysPath +) + +$ENV:INSTALL_PREFIX = "/opt/yapscan-deps" # Note: This be compatible with $ENV:PKG_CONFIG_PATH in enableMingw.ps1. +$SOURCES_DIR = "/opt/yapscan-src" # This variable can be set to an arbitrary linux-directory. + +# PowerShell <3.0 compatibility +if ($PSScriptRoot -like "") { + $PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Definition +} + +$OverwriteFlag="" +if ($OverwriteDeps) { + $BuildDeps=$TRUE + $OverwriteFlag="-o" +} + +if ($BuildDeps) { + echo "Building dependencies..." + Start -FilePath "$MsysPath\msys2_shell.cmd" -ArgumentList "-mingw64","-no-start","-defterm","-c","`"\`"$PSScriptRoot\buildAndInstallDependencies.sh\`" $OverwriteFlag \`"$SOURCES_DIR\`"; res=`$?; echo Press Enter to exit...; read; exit `$res`"" -Wait + echo "Done." +} + +"$PSScriptRoot\loadMingw.ps1" -MsysPath "$MsysPath" + +New-Item -Path . -Name "build" -ItemType "directory" -Erroraction "silentlycontinue" + +echo "Building yapscan..." +Push-Location "$PSScriptRoot\..\cmd\yapscan" + +go build -trimpath -tags yara_static -o .\build\yapscan.exe + +Pop-Location +echo "Done." + +echo "Building yapscan-dll..." +Push-Location "$PSScriptRoot\..\cmd\yapscan-dll" + +go build -trimpath -tags yara_static -o .\build\yapscan.dll -buildmode=c-shared + +Pop-Location +echo "Done." diff --git a/cicd/buildOpenssl.sh b/cicd/buildOpenssl.sh new file mode 100755 index 0000000..24d3ada --- /dev/null +++ b/cicd/buildOpenssl.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +function showHelp() { + echo "Usage: ./buildOpenssl.sh [installPrefix]" +} + +buildEnv=$("$(dirname "$0")/determineBuildEnvironment.sh") || (echo "$buildEnv"; exit 42) + +sourceDir="$1" +if [[ "$sourceDir" == "" ]]; then + showHelp + exit 1 +fi +installPrefix="$2" +if [[ "$installPrefix" != "" ]]; then + installPrefix="--prefix=$installPrefix" +fi + +cd "$sourceDir" || exit 2 + +# By default use only one thread +BUILD_THREADS=${BUILD_THREADS:-1} + +if [[ "$buildEnv" == "docker" ]]; then + ./Configure "$installPrefix" --cross-compile-prefix=x86_64-w64-mingw32- no-idea no-mdc2 no-rc5 shared mingw64 || exit $? +elif [[ "$buildEnv" == "mingw" ]]; then + ./Configure "$installPrefix" no-idea no-mdc2 no-rc5 shared mingw64 || exit $? +fi + +make -j$BUILD_THREADS || exit $? +make install_dev || exit $? diff --git a/cicd/buildYara.sh b/cicd/buildYara.sh new file mode 100755 index 0000000..1c79838 --- /dev/null +++ b/cicd/buildYara.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +function showHelp() { + echo "Usage: ./buildOpenssl.sh [installPrefix]" +} + +buildEnv=$("$(dirname "$0")/determineBuildEnvironment.sh") || (echo "$buildEnv"; exit 42) + +sourceDir="$1" +if [[ "$sourceDir" == "" ]]; then + showHelp + exit 1 +fi +installPrefix="$2" +if [[ "$installPrefix" != "" ]]; then + installPrefix="--prefix=$installPrefix" +fi + +cd "$sourceDir" || exit 2 + +# By default use only one thread +BUILD_THREADS=${BUILD_THREADS:-1} + +./bootstrap.sh +if [[ "$buildEnv" == "docker" ]]; then + ./configure CPPFLAGS="$(pkg-config --static --cflags openssl)" LDFLAGS="$(pkg-config --static --libs openssl)" \ + "$installPrefix" \ + --host=x86_64-w64-mingw32 \ + --disable-shared \ + --with-crypto || exit $? # --with-cuckoo --with-magic --with-dotnet +elif [[ "$buildEnv" == "mingw" ]]; then + ./configure CPPFLAGS="$(pkg-config --static --cflags openssl)" LDFLAGS="$(pkg-config --static --libs openssl)" \ + "$installPrefix" \ + --disable-shared \ + --with-crypto || exit $? # --with-cuckoo --with-magic --with-dotnet +fi + +make -j$BUILD_THREADS || exit $? +make install || exit $? diff --git a/buildForWindows.sh b/cicd/crossBuildForWindows.sh similarity index 52% rename from buildForWindows.sh rename to cicd/crossBuildForWindows.sh index 53ffe07..a60211a 100755 --- a/buildForWindows.sh +++ b/cicd/crossBuildForWindows.sh @@ -35,23 +35,27 @@ for arg in "$@"; do done -cores=`cat /proc/cpuinfo | grep "cpu cores" | head -n1 | cut -d: -f2 | cut -d' ' -f2` +cores=$(cat /proc/cpuinfo | grep "cpu cores" | head -n1 | cut -d: -f2 | cut -d' ' -f2) cores=$((cores*2)) -./prepare.sh +cicd="$(dirname "$0")" +if [[ "$cicd" == "." ]]; then + # Necessary for docker volume + cicd="$(pwd)" +fi mkdir -p build/ &>/dev/null -OPENSSL_VERSION=${OPENSSL_VERSION:-OpenSSL_1_1_1-stable} -YARA_VERSION=${YARA_VERSION:-v4.0.2} +OPENSSL_VERSION=$("$cicd/opensslVersion.sh") || exit $? +YARA_VERSION=$("$cicd/yaraVersion.sh") || exit $? docker build \ --build-arg BUILD_THREADS=$cores \ --build-arg OPENSSL_VERSION=$OPENSSL_VERSION --build-arg YARA_VERSION=$YARA_VERSION \ - --network=host -t yapscan-xcompile -f Dockerfile.xwin . + --network=host -t yapscan-xcompile -f Dockerfile.xwin . || exit $? -docker run --rm --network=host --volume $(pwd):/opt/yapscan -i yapscan-xcompile </dev/null fi if [[ "$buildYapscan" == "1" ]]; then - export CGO_CFLAGS="-I/opt/yara/libyara/include \$(pkg-config --static --cflags openssl)" - export CGO_LDFLAGS="-L/opt/yara/libyara/.libs -lyara -static \$(pkg-config --static --libs openssl)" - pushd yapscan/cmd/yapscan - go build -trimpath -o /opt/yapscan/build/yapscan.exe -tags yara_no_pkg_config + go build -trimpath -o /opt/yapscan/cicd/build/yapscan.exe -tags yara_static -buildmode=exe popd &>/dev/null fi if [[ "$buildYapscanDll" == "1" ]]; then - export CGO_CFLAGS="-I/opt/yara/libyara/include \$(pkg-config --static --cflags openssl) -fvisibility=hidden" - export CGO_LDFLAGS="-L/opt/yara/libyara/.libs -lyara -static \$(pkg-config --static --libs openssl)" - pushd yapscan/cmd/yapscan-dll - go build -trimpath -o /opt/yapscan/build/yapscan.dll -tags yara_no_pkg_config -buildmode=c-shared + go build -trimpath -o /opt/yapscan/cicd/build/yapscan.dll -tags yara_static -buildmode=c-shared popd &>/dev/null fi diff --git a/cicd/determineBuildEnvironment.sh b/cicd/determineBuildEnvironment.sh new file mode 100755 index 0000000..f5f3b22 --- /dev/null +++ b/cicd/determineBuildEnvironment.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Determine build environment for windows target + +sys=$(uname) + +if [[ "$sys" == "Linux" ]]; then + # We are running on Linux => use docker + echo "docker" +elif [[ "$sys" == "MINGW"* ]]; then + # MINGW, use that + echo "mingw" +elif [[ "$sys" == "MSYS"* ]]; then + # MSYS, error, use mingw instead + echo "Invalid build environment: $sys! Please use \"MSYS MinGW\" instead!" + exit 2 +else + # unknown environment + echo "Unknown build environment: $sys" + exit 1 +fi diff --git a/cicd/enableMingw.ps1 b/cicd/enableMingw.ps1 new file mode 100644 index 0000000..2909b9a --- /dev/null +++ b/cicd/enableMingw.ps1 @@ -0,0 +1,22 @@ +Param( + [Parameter(Mandatory=$False)] + [string]$MsysPath +) + +if ($MsysPath -like "") { + # Try to detect msys + if (Test-Path "C:\msys64") { + $MsysPath = "C:\msys64" + } else { + echo "ERROR: Could not find MSYS2, please specify via `"-MsysPath `"" + Exit 1 + } +} + +$ENV:PKG_CONFIG_PATH = "$MsysPath\opt\yapscan-deps\lib\pkgconfig" +$ENV:PATH += ";$MsysPath\mingw64\bin" + +# This should theoretically not be needed, as pkg-config is supposed to handle this. +# However, on my test-system this was needed although `pkg-config --libs yara` did report +# the correct flags: `-LC:\msys64\opt\yapscan-deps\lib -lyara` +$ENV:CGO_LDFLAGS="-L$MsysPath\opt\yapscan-deps\lib" diff --git a/cicd/opensslVersion.sh b/cicd/opensslVersion.sh new file mode 100755 index 0000000..d5970d1 --- /dev/null +++ b/cicd/opensslVersion.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +DEFAULT_VERSION=OpenSSL_1_1_1-stable +OPENSSL_VERSION=${OPENSSL_VERSION:-$DEFAULT_VERSION} + +echo "$OPENSSL_VERSION" diff --git a/cicd/yaraVersion.sh b/cicd/yaraVersion.sh new file mode 100755 index 0000000..3d70c4f --- /dev/null +++ b/cicd/yaraVersion.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +DEFAULT_VERSION=v4.0.2 +YARA_VERSION=${YARA_VERSION:-$DEFAULT_VERSION} + +echo "$YARA_VERSION" diff --git a/cmd/memtest/main.go b/cmd/memtest/main.go new file mode 100644 index 0000000..381bf41 --- /dev/null +++ b/cmd/memtest/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/fkie-cad/yapscan/testutil/memory" + +func main() { + memory.Main() +} diff --git a/cmd/memtest/main_windows.go b/cmd/memtest/main_windows.go deleted file mode 100644 index 6ab8a0e..0000000 --- a/cmd/memtest/main_windows.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -//#include -import "C" - -import ( - "fmt" - "io" - "io/ioutil" - "log" - "os" - "strconv" - "unsafe" - - "golang.org/x/sys/windows" -) - -func main() { - if len(os.Args) < 3 { - log.Fatalf("Usage: %s [file]") - } - - filename := "" - if len(os.Args) >= 4 { - filename = os.Args[3] - } - - size, err := strconv.ParseUint(os.Args[1], 10, 64) - if err != nil { - log.Fatalf("Invalid size value, %v", err) - } - - protect, err := strconv.ParseUint(os.Args[2], 10, 32) - if err != nil { - log.Fatalf("Invalid protect value, %v", err) - } - - var data []byte - - if filename != "" { - f, err := os.Open(filename) - if err != nil { - log.Fatalf("Could not open file, reason: %v", err) - } - data, err = ioutil.ReadAll(f) - if err != nil { - log.Fatalf("Could not read from file, reason: %v", err) - } - f.Close() - - size = uint64(len(data)) - } else { - data, err = ioutil.ReadAll(io.LimitReader(os.Stdin, int64(size))) - if err != nil { - log.Fatalf("Could not read from stdin, reason: %v", err) - } - } - - addr, err := windows.VirtualAlloc(0, uintptr(size), windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) - if err != nil { - log.Fatalf("Could not alloc, reason: %v", err) - } - defer func() { - windows.VirtualFree(addr, 0, windows.MEM_RELEASE) - }() - - C.memcpy(unsafe.Pointer(addr), unsafe.Pointer(&data[0]), C.size_t(size)) - - var oldProtect uint32 - err = windows.VirtualProtect(addr, uintptr(len(data)), uint32(protect), &oldProtect) - if err != nil { - log.Fatalf("Failed to set protect, reason: %v", err) - } - - fmt.Println(addr) - - if filename != "" { - fmt.Println("Press Enter to close application...") - // Wait for user enter - fmt.Scanln() - } else { - // Wait for stdin close - ioutil.ReadAll(os.Stdin) - } -} diff --git a/cmd/yapscan/main.go b/cmd/yapscan/main.go index ef84648..c91f80b 100644 --- a/cmd/yapscan/main.go +++ b/cmd/yapscan/main.go @@ -3,9 +3,26 @@ package main import ( "os" + "github.com/fkie-cad/yapscan/service" + "github.com/sirupsen/logrus" + "github.com/fkie-cad/yapscan/app" ) +func svcMain(args []string) error { + app.RunApp(args) + return nil +} + func main() { - app.RunApp(os.Args) + err := service.Initialize(svcMain) + if service.IsNotInServiceModeError(err) { + // Not a service, run normally + app.RunApp(os.Args) + } else if err != nil { + logrus.Fatal(err) + } + // Started as service. + // The ServiceMain is called by the service manager, we can just exit. + return } diff --git a/docker/build.env b/docker/build.env deleted file mode 100644 index e69de29..0000000 diff --git a/docker/buildOpenssl.sh b/docker/buildOpenssl.sh deleted file mode 100755 index c933938..0000000 --- a/docker/buildOpenssl.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -cd /opt/openssl - -BUILD_THREADS=${BUILD_THREADS:-1} - -./Configure --prefix=$PWD/dist --cross-compile-prefix=x86_64-w64-mingw32- no-idea no-mdc2 no-rc5 shared mingw64 || exit $? -make -j$BUILD_THREADS || exit $? -mkdir -p dist/bin dist/include dist/lib -make install || exit 0 \ No newline at end of file diff --git a/docker/buildYara.sh b/docker/buildYara.sh deleted file mode 100755 index 7e90e00..0000000 --- a/docker/buildYara.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -BUILD_THREADS=${BUILD_THREADS:-1} - -export PKG_CONFIG_LIBDIR=/opt/openssl/dist/lib/pkgconfig - -cd yara -./bootstrap.sh -./configure CPPFLAGS="`pkg-config --static --cflags openssl`" LDFLAGS="`pkg-config --static --libs openssl`" \ - --host=x86_64-w64-mingw32 \ - --disable-shared \ - --with-crypto || exit $? # --with-cuckoo --with-magic --with-dotnet -make -j$BUILD_THREADS diff --git a/experiments/listSegmentsOfProcess/listSegmentsOfProcess.go b/experiments/listSegmentsOfProcess/listSegmentsOfProcess.go index f6137b7..7481755 100644 --- a/experiments/listSegmentsOfProcess/listSegmentsOfProcess.go +++ b/experiments/listSegmentsOfProcess/listSegmentsOfProcess.go @@ -5,9 +5,8 @@ import ( "os" "strconv" - "github.com/fkie-cad/yapscan/procIO" - "github.com/dustin/go-humanize" + "github.com/fkie-cad/yapscan/procio" ) func main() { @@ -27,7 +26,7 @@ func main() { } fmt.Printf("Reading segments from process %d...\n", pid) - proc, err := procIO.OpenProcess(pid) + proc, err := procio.OpenProcess(pid) if err != nil { panic(err) } diff --git a/experiments/readFirstMemoryOfProc/readFirstMemoryOfProc.go b/experiments/readFirstMemoryOfProc/readFirstMemoryOfProc.go index 6f78000..ac9978d 100644 --- a/experiments/readFirstMemoryOfProc/readFirstMemoryOfProc.go +++ b/experiments/readFirstMemoryOfProc/readFirstMemoryOfProc.go @@ -7,7 +7,7 @@ import ( "os" "strconv" - "github.com/fkie-cad/yapscan/procIO" + "github.com/fkie-cad/yapscan/procio" ) func main() { @@ -27,7 +27,7 @@ func main() { } fmt.Printf("Reading segments from process %d...\n", pid) - proc, err := procIO.OpenProcess(pid) + proc, err := procio.OpenProcess(pid) if err != nil { panic(err) } @@ -39,7 +39,7 @@ func main() { readSeg := segments[0] - rdr, _ := procIO.NewMemoryReader(proc, readSeg) + rdr, _ := procio.NewMemoryReader(proc, readSeg) defer rdr.Close() if length > 0 { diff --git a/fileIO/driveType.go b/fileio/driveType.go similarity index 69% rename from fileIO/driveType.go rename to fileio/driveType.go index 28165ce..222c934 100644 --- a/fileIO/driveType.go +++ b/fileio/driveType.go @@ -1,6 +1,7 @@ //go:generate go-enum -f=$GOFILE --marshal --lower --names -package fileIO +package fileio +// DriveType describes the type of a system drive. /* ENUM( Unknown=0 diff --git a/fileIO/driveType_enum.go b/fileio/driveType_enum.go similarity index 99% rename from fileIO/driveType_enum.go rename to fileio/driveType_enum.go index 762afe9..4650a9f 100644 --- a/fileIO/driveType_enum.go +++ b/fileio/driveType_enum.go @@ -1,7 +1,7 @@ // Code generated by go-enum // DO NOT EDIT! -package fileIO +package fileio import ( "fmt" diff --git a/fileIO/enumerate.go b/fileio/enumerate.go similarity index 53% rename from fileIO/enumerate.go rename to fileio/enumerate.go index 382f1c8..bc805a5 100644 --- a/fileIO/enumerate.go +++ b/fileio/enumerate.go @@ -1,5 +1,6 @@ -package fileIO +package fileio +// Enumerate enumerates all mounted drives of the given type. func Enumerate(typeMask DriveType) ([]string, error) { return enumerateImpl(typeMask) } diff --git a/fileIO/enumerate_linux.go b/fileio/enumerate_linux.go similarity index 92% rename from fileIO/enumerate_linux.go rename to fileio/enumerate_linux.go index c6c0cf1..a1bef2b 100644 --- a/fileIO/enumerate_linux.go +++ b/fileio/enumerate_linux.go @@ -1,4 +1,4 @@ -package fileIO +package fileio func enumerateImpl(typeMask DriveType) ([]string, error) { // TODO: Actually implement this. diff --git a/fileIO/enumerate_windows.go b/fileio/enumerate_windows.go similarity index 99% rename from fileIO/enumerate_windows.go rename to fileio/enumerate_windows.go index 79ddba0..aca4fdb 100644 --- a/fileIO/enumerate_windows.go +++ b/fileio/enumerate_windows.go @@ -1,4 +1,4 @@ -package fileIO +package fileio import ( "context" diff --git a/fileIO/file.go b/fileio/file.go similarity index 59% rename from fileIO/file.go rename to fileio/file.go index a787d31..5c01939 100644 --- a/fileIO/file.go +++ b/fileio/file.go @@ -1,7 +1,8 @@ -package fileIO +package fileio import "os" +// File is a small abstraction layer for a file. type File interface { Path() string Stat() (os.FileInfo, error) @@ -11,10 +12,12 @@ type file struct { path string } +// Path returns the path of the file. func (f *file) Path() string { return f.path } +// Stat returns the os.FileInfo associated with the file. func (f *file) Stat() (os.FileInfo, error) { return os.Stat(f.path) } diff --git a/fileio/fileio.go b/fileio/fileio.go new file mode 100644 index 0000000..3f6889e --- /dev/null +++ b/fileio/fileio.go @@ -0,0 +1,4 @@ +// Package fileio provides functionality to interact with the +// OSs filesystem. This includes iteration over (parts of) the +// filesystem, as well as enumeration of network shares. +package fileio diff --git a/fileIO/filesystem.go b/fileio/filesystem.go similarity index 82% rename from fileIO/filesystem.go rename to fileio/filesystem.go index 4c5715f..c9b918f 100644 --- a/fileIO/filesystem.go +++ b/fileio/filesystem.go @@ -1,4 +1,4 @@ -package fileIO +package fileio import ( "context" @@ -12,6 +12,8 @@ import ( ) var ( + // FilesBuffer sets how many files can be buffered at one time + // during iteration. FilesBuffer = 8 ) @@ -32,7 +34,10 @@ type fsIterator struct { next chan *nextEntry } -func IteratePath(path string, validExtensions []string, ctx context.Context) (Iterator, error) { +// IteratePath starts an asynchronous, recursive Iterator over all files and +// subdirectores in the given path. For each file with one of the given +// validExtensions, a File will be emitted, which can be read using Iterator.Next. +func IteratePath(ctx context.Context, path string, validExtensions []string) (Iterator, error) { stat, err := os.Stat(path) if err != nil { return nil, err @@ -150,6 +155,7 @@ func (it *fsIterator) dirScanner() { } } +// Next blocks until the next file is available and returns it or any encountered error. func (it *fsIterator) Next() (File, error) { if it.closed { return nil, io.EOF @@ -163,6 +169,7 @@ func (it *fsIterator) Next() (File, error) { return next.File, next.Err } +// Close stops the iterator and frees all of its resources. func (it *fsIterator) Close() error { if it.closed { return nil diff --git a/fileIO/filesystemScanner.go b/fileio/filesystemScanner.go similarity index 76% rename from fileIO/filesystemScanner.go rename to fileio/filesystemScanner.go index e3e774c..7e5c177 100644 --- a/fileIO/filesystemScanner.go +++ b/fileio/filesystemScanner.go @@ -1,4 +1,4 @@ -package fileIO +package fileio import ( "io" @@ -7,27 +7,33 @@ import ( "github.com/hillu/go-yara/v4" ) +// FileScanner provides functionality to scan files. type FileScanner interface { ScanFile(filename string) (results []yara.MatchRule, err error) } +// FSScanner is a scanner for the filesystem. type FSScanner struct { NGoroutines int scanner FileScanner } +// NewFSScanner create a new FSScanner, which will use the given FileScanner +// to scan any encountered file. func NewFSScanner(scanner FileScanner) *FSScanner { return &FSScanner{ scanner: scanner, } } +// FSScanProgress provides information about the scanning progress. type FSScanProgress struct { File File Matches []yara.MatchRule Error error } +// Scan scans all files, emitted by the given Iterator. func (s *FSScanner) Scan(it Iterator) (<-chan *FSScanProgress, error) { if s.NGoroutines <= 0 { s.NGoroutines = 1 diff --git a/fileIO/filesystem_linux.go b/fileio/filesystem_linux.go similarity index 97% rename from fileIO/filesystem_linux.go rename to fileio/filesystem_linux.go index 5f187c1..bfc29b8 100644 --- a/fileIO/filesystem_linux.go +++ b/fileio/filesystem_linux.go @@ -1,4 +1,4 @@ -package fileIO +package fileio import ( "path/filepath" diff --git a/fileIO/filesystem_test.go b/fileio/filesystem_test.go similarity index 83% rename from fileIO/filesystem_test.go rename to fileio/filesystem_test.go index 4b39113..0f2b84a 100644 --- a/fileIO/filesystem_test.go +++ b/fileio/filesystem_test.go @@ -1,4 +1,4 @@ -package fileIO +package fileio import ( "context" @@ -12,13 +12,13 @@ import ( ) func testDataDir(path ...string) string { - path = append([]string{"..", "testdata", "fileIO"}, path...) + path = append([]string{"..", "testdata", "fileio"}, path...) return filepath.Join(path...) } func TestIterateFail(t *testing.T) { Convey("Iterating through a non-existent directory", t, func() { - it, err := IteratePath(filepath.Join("thispath", "shouldnot", "exist"), nil, context.Background()) + it, err := IteratePath(context.Background(), filepath.Join("thispath", "shouldnot", "exist"), nil) Convey("should error.", func() { So(it, ShouldBeNil) @@ -27,7 +27,7 @@ func TestIterateFail(t *testing.T) { }) Convey("Opening a file for iteration", t, func() { - it, err := IteratePath(testDataDir("filesystem", "f1"), nil, context.Background()) + it, err := IteratePath(context.Background(), testDataDir("filesystem", "f1"), nil) Convey("should error.", func() { So(it, ShouldBeNil) @@ -38,7 +38,7 @@ func TestIterateFail(t *testing.T) { func TestIterateSuccess(t *testing.T) { Convey("Iterating through a directory with a single goroutine", t, func() { - it, err := IteratePath(testDataDir("filesystem"), nil, context.Background()) + it, err := IteratePath(context.Background(), testDataDir("filesystem"), nil) Convey("should not error.", func() { So(err, ShouldBeNil) diff --git a/fileIO/filesystem_windows.go b/fileio/filesystem_windows.go similarity index 86% rename from fileIO/filesystem_windows.go rename to fileio/filesystem_windows.go index f6d57a6..e1b2d06 100644 --- a/fileIO/filesystem_windows.go +++ b/fileio/filesystem_windows.go @@ -1,4 +1,4 @@ -package fileIO +package fileio func doScanDir(path string) bool { // no special dirs like "/dev" on windows diff --git a/fileIO/iterator.go b/fileio/iterator.go similarity index 91% rename from fileIO/iterator.go rename to fileio/iterator.go index 96c3da4..b8d955c 100644 --- a/fileIO/iterator.go +++ b/fileio/iterator.go @@ -1,4 +1,4 @@ -package fileIO +package fileio import ( "io" @@ -7,6 +7,7 @@ import ( "github.com/targodan/go-errors" ) +// Iterator provides capability to iterate over files. type Iterator interface { Next() (File, error) Close() error @@ -44,6 +45,8 @@ func concat(it1 Iterator, it2 Iterator) Iterator { } } +// Concat concatenates multiple Iterators. +// When one Iterator is exhausted, the next one is used. func Concat(iterators ...Iterator) Iterator { var ret Iterator for _, it := range iterators { @@ -121,6 +124,8 @@ func concurrent(it1 Iterator, it2 Iterator) Iterator { return cit } +// Concurrent combines the given Iterators concurrently. +// Each given Iterator will run in its own goroutine. func Concurrent(iterators ...Iterator) Iterator { var cit Iterator for _, it := range iterators { diff --git a/filter.go b/filter.go index 04f436f..ac7258a 100644 --- a/filter.go +++ b/filter.go @@ -7,21 +7,25 @@ import ( "strings" "text/template" - "github.com/fkie-cad/yapscan/procIO" - "github.com/dustin/go-humanize" + "github.com/fkie-cad/yapscan/procio" ) +// FilterMatch contains information about the matching of a MemorySegmentFilter. type FilterMatch struct { Result bool - MSI *procIO.MemorySegmentInfo + MSI *procio.MemorySegmentInfo Reason string // Reason for filter mismatch, if Result is false } -type MemorySegmentFilterFunc func(info *procIO.MemorySegmentInfo) bool +// MemorySegmentFilterFunc is a callback, used to filter *procio.MemorySegmentInfo +// instances. +type MemorySegmentFilterFunc func(info *procio.MemorySegmentInfo) bool +// MemorySegmentFilter describes an interface, capable of filtering +// *procio.MemorySegmentInfo instances. type MemorySegmentFilter interface { - Filter(info *procIO.MemorySegmentInfo) *FilterMatch + Filter(info *procio.MemorySegmentInfo) *FilterMatch } type baseFilter struct { @@ -30,7 +34,7 @@ type baseFilter struct { reasonTemplate string } -func (f *baseFilter) renderReason(info *procIO.MemorySegmentInfo) string { +func (f *baseFilter) renderReason(info *procio.MemorySegmentInfo) string { t := template.New("filterReason") t.Funcs(template.FuncMap{ @@ -64,7 +68,7 @@ func (f *baseFilter) renderReason(info *procIO.MemorySegmentInfo) string { buf := &bytes.Buffer{} err = t.Execute(buf, &struct { Filter MemorySegmentFilter - MSI *procIO.MemorySegmentInfo + MSI *procio.MemorySegmentInfo }{ Filter: f, MSI: info, @@ -77,7 +81,7 @@ func (f *baseFilter) renderReason(info *procIO.MemorySegmentInfo) string { return buf.String() } -func (f *baseFilter) Filter(info *procIO.MemorySegmentInfo) *FilterMatch { +func (f *baseFilter) Filter(info *procio.MemorySegmentInfo) *FilterMatch { var reasonForMismatch string matches := f.filter(info) @@ -92,6 +96,7 @@ func (f *baseFilter) Filter(info *procIO.MemorySegmentInfo) *FilterMatch { } } +// NewFilterFromFunc creates a new filter from a given MemorySegmentFilterFunc. func NewFilterFromFunc(filter MemorySegmentFilterFunc, parameter interface{}, reasonTemplate string) MemorySegmentFilter { return &baseFilter{ filter: filter, @@ -100,9 +105,11 @@ func NewFilterFromFunc(filter MemorySegmentFilterFunc, parameter interface{}, re } } +// NewMaxSizeFilter creates a new filter, matching *procio.MemorySegmentInfo +// with the given maximum size. func NewMaxSizeFilter(size uintptr) MemorySegmentFilter { return NewFilterFromFunc( - func(info *procIO.MemorySegmentInfo) bool { + func(info *procio.MemorySegmentInfo) bool { return info.Size <= size }, size, @@ -110,9 +117,11 @@ func NewMaxSizeFilter(size uintptr) MemorySegmentFilter { ) } +// NewMinSizeFilter creates a new filter, matching *procio.MemorySegmentInfo +// with the given minimum size. func NewMinSizeFilter(size uintptr) MemorySegmentFilter { return NewFilterFromFunc( - func(info *procIO.MemorySegmentInfo) bool { + func(info *procio.MemorySegmentInfo) bool { return info.Size >= size }, size, @@ -120,9 +129,11 @@ func NewMinSizeFilter(size uintptr) MemorySegmentFilter { ) } -func NewStateFilter(states []procIO.State) MemorySegmentFilter { +// NewStateFilter creates a new filter, matching *procio.MemorySegmentInfo +// with a procio.State equal to one of the given states. +func NewStateFilter(states []procio.State) MemorySegmentFilter { return NewFilterFromFunc( - func(info *procIO.MemorySegmentInfo) bool { + func(info *procio.MemorySegmentInfo) bool { for _, s := range states { if info.State == s { return true @@ -135,9 +146,11 @@ func NewStateFilter(states []procIO.State) MemorySegmentFilter { ) } -func NewTypeFilter(types []procIO.Type) MemorySegmentFilter { +// NewTypeFilter creates a new filter, matching *procio.MemorySegmentInfo +// with a procio.Type equal to one of the given types. +func NewTypeFilter(types []procio.Type) MemorySegmentFilter { return NewFilterFromFunc( - func(info *procIO.MemorySegmentInfo) bool { + func(info *procio.MemorySegmentInfo) bool { for _, t := range types { if info.Type == t { return true @@ -150,9 +163,11 @@ func NewTypeFilter(types []procIO.Type) MemorySegmentFilter { ) } -func NewPermissionsFilterExact(perms []procIO.Permissions) MemorySegmentFilter { +// NewPermissionsFilterExact creates a new filter, matching *procio.MemorySegmentInfo +// with procio.Permissions exactly equal to one of the given perms. +func NewPermissionsFilterExact(perms []procio.Permissions) MemorySegmentFilter { return NewFilterFromFunc( - func(info *procIO.MemorySegmentInfo) bool { + func(info *procio.MemorySegmentInfo) bool { for _, p := range perms { if info.CurrentPermissions.EqualTo(p) { return true @@ -165,9 +180,11 @@ func NewPermissionsFilterExact(perms []procIO.Permissions) MemorySegmentFilter { ) } -func NewPermissionsFilter(perm procIO.Permissions) MemorySegmentFilter { +// NewPermissionsFilter creates a new filter, matching *procio.MemorySegmentInfo +// with procio.Permissions equal to or more permissive than the given perm. +func NewPermissionsFilter(perm procio.Permissions) MemorySegmentFilter { return NewFilterFromFunc( - func(info *procIO.MemorySegmentInfo) bool { + func(info *procio.MemorySegmentInfo) bool { return info.CurrentPermissions.IsMoreOrEquallyPermissiveThan(perm) }, perm, @@ -179,13 +196,15 @@ type andFilter struct { filters []MemorySegmentFilter } +// NewAndFilter creates a new filter, which is the logical AND-combination +// of all given MemorySegmentFilter instances. func NewAndFilter(filters ...MemorySegmentFilter) MemorySegmentFilter { return &andFilter{ filters: filters, } } -func (f *andFilter) Filter(info *procIO.MemorySegmentInfo) *FilterMatch { +func (f *andFilter) Filter(info *procio.MemorySegmentInfo) *FilterMatch { result := &FilterMatch{ Result: true, MSI: info, diff --git a/generate.ps1 b/generate.ps1 new file mode 100644 index 0000000..6ff204a --- /dev/null +++ b/generate.ps1 @@ -0,0 +1,29 @@ +Param( + [Parameter(Mandatory=$False)] + [switch]$UpdateTools +) + +# PowerShell <3.0 compatibility +if ($PSScriptRoot -like "") { + $PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Definition +} + +$update="" +if ($UpdateTools) { + $update="-u" +} + +Push-Location $PSScriptRoot + +go mod tidy +go mod vendor + +go get -v $update github.com/abice/go-enum +go get -v $update github.com/vektra/mockery/v2/.../ +go mod tidy + +# TODO: Remove all old mocks + +go generate ./... + +Pop-Location \ No newline at end of file diff --git a/generate.sh b/generate.sh new file mode 100755 index 0000000..7d4ef78 --- /dev/null +++ b/generate.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +cd $(dirname "$0") || exit 1 + +update="" +if [[ "$1" == "-u" ]]; then + update="-u" +fi + +go mod tidy +go mod vendor + +go get -v $update github.com/abice/go-enum +go get -v $update github.com/vektra/mockery/v2/.../ +go mod tidy + +find . -name 'mock_*_test.go' -type f -delete + +go generate ./... \ No newline at end of file diff --git a/go.mod b/go.mod index d7bdce4..1c156e4 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,9 @@ require ( github.com/dustin/go-humanize v1.0.0 github.com/fatih/color v1.10.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect + github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe // indirect github.com/hillu/go-yara/v4 v4.0.3 - github.com/kr/pretty v0.1.0 // indirect + github.com/kr/pretty v0.2.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.7.0 github.com/smartystreets/assertions v1.2.0 // indirect @@ -22,6 +22,8 @@ require ( github.com/urfave/cli/v2 v2.3.0 github.com/yeka/zip v0.0.0-20180914125537-d046722c6feb golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c // indirect - golang.org/x/sys v0.0.0-20201202213521-69691e467435 + golang.org/x/mod v0.4.2 // indirect + golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 + golang.org/x/tools v0.1.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index c21d6fe..e5eee8d 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,9 @@ -github.com/0xrawsec/golang-utils v1.1.3 h1:ESJhyY4aGuiP4hmDcDNjoL/cc7SWDZVfgg4dEON9eIc= github.com/0xrawsec/golang-utils v1.1.3/go.mod h1:DADTtCFY10qXjWmUVhhJqQIZdSweaHH4soYUDEi8mj0= github.com/0xrawsec/golang-utils v1.1.8 h1:9TzAKzC7+V2IXaV/3Y1aXiUFHeShL3BXcfL6BheAEH0= github.com/0xrawsec/golang-utils v1.1.8/go.mod h1:DADTtCFY10qXjWmUVhhJqQIZdSweaHH4soYUDEi8mj0= github.com/0xrawsec/golang-win32 v1.0.6 h1:wVvfd+trSeUkG6m5TFzeBtWHSHetfhPO3b5MVjTgsWk= github.com/0xrawsec/golang-win32 v1.0.6/go.mod h1:MAxVU7dr8lujwknuhf4TwjYm8tVEELi2zwx1zDTu/RM= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= @@ -18,17 +16,18 @@ github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe h1:rcf1P0fm+1l0EjG16p06mYLj9gW9X36KgdHJ/88hS4g= +github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/hillu/go-yara/v4 v4.0.3 h1:ktYuhB6fI1VKZCehCuEO08U3WWdgdhMKKn9uZGezlrc= github.com/hillu/go-yara/v4 v4.0.3/go.mod h1:rkb/gSAoO8qcmj+pv6fDZN4tOa3N7R+qqGlEkzT4iys= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -36,26 +35,21 @@ github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.0/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -68,36 +62,49 @@ github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/yeka/zip v0.0.0-20180914125537-d046722c6feb h1:OJYP70YMddlmGq//EPLj8Vw2uJXmrA+cGSPhXTDpn2E= github.com/yeka/zip v0.0.0-20180914125537-d046722c6feb/go.mod h1:9BnoKCcgJ/+SLhfAXj15352hTOuVmG5Gzo8xNRINfqI= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c h1:9HhBz5L/UjnK9XLtiZhYAdue5BVKep3PMmS2LuPDt8k= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201202213521-69691e467435 h1:25AvDqqB9PrNqj1FLf2/70I4W0L19qqoaFq3gjNwbKk= golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190320215829-36c10c0a621f/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190625160430-252024b82959 h1:VuVDXrx1TCtYGfuoKtAHdDqlDBF4b1ygX9yjx+WokdE= golang.org/x/tools v0.0.0-20190625160430-252024b82959/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/helper_test.go b/helper_test.go index d9b71d6..41e6de8 100644 --- a/helper_test.go +++ b/helper_test.go @@ -98,7 +98,7 @@ func TestFormatSlice(t *testing.T) { Convey("Formatting a slice of ints", t, func() { ints := []int{42, 666, 1337} - Convey("with no additional agruments", func() { + Convey("with no additional arguments", func() { Convey("should yield correctly formatted strings.", func() { So(FormatSlice("int: %d", ints), ShouldResemble, []string{ "int: 42", "int: 666", "int: 1337", diff --git a/mock_MemoryScanner_test.go b/mock_MemoryScanner_test.go new file mode 100644 index 0000000..e34d4e8 --- /dev/null +++ b/mock_MemoryScanner_test.go @@ -0,0 +1,36 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package yapscan + +import ( + yara "github.com/hillu/go-yara/v4" + mock "github.com/stretchr/testify/mock" +) + +// MockMemoryScanner is an autogenerated mock type for the MemoryScanner type +type MockMemoryScanner struct { + mock.Mock +} + +// ScanMem provides a mock function with given fields: buf +func (_m *MockMemoryScanner) ScanMem(buf []byte) ([]yara.MatchRule, error) { + ret := _m.Called(buf) + + var r0 []yara.MatchRule + if rf, ok := ret.Get(0).(func([]byte) []yara.MatchRule); ok { + r0 = rf(buf) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]yara.MatchRule) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(buf) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/mock_MemorySegmentFilterFunc_test.go b/mock_MemorySegmentFilterFunc_test.go new file mode 100644 index 0000000..2d730cb --- /dev/null +++ b/mock_MemorySegmentFilterFunc_test.go @@ -0,0 +1,27 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package yapscan + +import ( + procio "github.com/fkie-cad/yapscan/procio" + mock "github.com/stretchr/testify/mock" +) + +// MockMemorySegmentFilterFunc is an autogenerated mock type for the MemorySegmentFilterFunc type +type MockMemorySegmentFilterFunc struct { + mock.Mock +} + +// Execute provides a mock function with given fields: info +func (_m *MockMemorySegmentFilterFunc) Execute(info *procio.MemorySegmentInfo) bool { + ret := _m.Called(info) + + var r0 bool + if rf, ok := ret.Get(0).(func(*procio.MemorySegmentInfo) bool); ok { + r0 = rf(info) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} diff --git a/mock_MemorySegmentFilter_test.go b/mock_MemorySegmentFilter_test.go new file mode 100644 index 0000000..409815e --- /dev/null +++ b/mock_MemorySegmentFilter_test.go @@ -0,0 +1,29 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package yapscan + +import ( + procio "github.com/fkie-cad/yapscan/procio" + mock "github.com/stretchr/testify/mock" +) + +// MockMemorySegmentFilter is an autogenerated mock type for the MemorySegmentFilter type +type MockMemorySegmentFilter struct { + mock.Mock +} + +// Filter provides a mock function with given fields: info +func (_m *MockMemorySegmentFilter) Filter(info *procio.MemorySegmentInfo) *FilterMatch { + ret := _m.Called(info) + + var r0 *FilterMatch + if rf, ok := ret.Get(0).(func(*procio.MemorySegmentInfo) *FilterMatch); ok { + r0 = rf(info) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*FilterMatch) + } + } + + return r0 +} diff --git a/mock_Rules_test.go b/mock_Rules_test.go new file mode 100644 index 0000000..a9a6965 --- /dev/null +++ b/mock_Rules_test.go @@ -0,0 +1,44 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package yapscan + +import ( + time "time" + + mock "github.com/stretchr/testify/mock" + + yara "github.com/hillu/go-yara/v4" +) + +// MockRules is an autogenerated mock type for the Rules type +type MockRules struct { + mock.Mock +} + +// ScanFile provides a mock function with given fields: filename, flags, timeout, cb +func (_m *MockRules) ScanFile(filename string, flags yara.ScanFlags, timeout time.Duration, cb yara.ScanCallback) error { + ret := _m.Called(filename, flags, timeout, cb) + + var r0 error + if rf, ok := ret.Get(0).(func(string, yara.ScanFlags, time.Duration, yara.ScanCallback) error); ok { + r0 = rf(filename, flags, timeout, cb) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ScanMem provides a mock function with given fields: buf, flags, timeout, cb +func (_m *MockRules) ScanMem(buf []byte, flags yara.ScanFlags, timeout time.Duration, cb yara.ScanCallback) error { + ret := _m.Called(buf, flags, timeout, cb) + + var r0 error + if rf, ok := ret.Get(0).(func([]byte, yara.ScanFlags, time.Duration, yara.ScanCallback) error); ok { + r0 = rf(buf, flags, timeout, cb) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mock_memoryReaderFactory_test.go b/mock_memoryReaderFactory_test.go new file mode 100644 index 0000000..9713969 --- /dev/null +++ b/mock_memoryReaderFactory_test.go @@ -0,0 +1,36 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package yapscan + +import ( + procio "github.com/fkie-cad/yapscan/procio" + mock "github.com/stretchr/testify/mock" +) + +// mockMemoryReaderFactory is an autogenerated mock type for the memoryReaderFactory type +type mockMemoryReaderFactory struct { + mock.Mock +} + +// NewMemoryReader provides a mock function with given fields: proc, seg +func (_m *mockMemoryReaderFactory) NewMemoryReader(proc procio.Process, seg *procio.MemorySegmentInfo) (procio.MemoryReader, error) { + ret := _m.Called(proc, seg) + + var r0 procio.MemoryReader + if rf, ok := ret.Get(0).(func(procio.Process, *procio.MemorySegmentInfo) procio.MemoryReader); ok { + r0 = rf(proc, seg) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(procio.MemoryReader) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(procio.Process, *procio.MemorySegmentInfo) error); ok { + r1 = rf(proc, seg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/mock_memoryReader_test.go b/mock_memoryReader_test.go new file mode 100644 index 0000000..0c65706 --- /dev/null +++ b/mock_memoryReader_test.go @@ -0,0 +1,66 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package yapscan + +import mock "github.com/stretchr/testify/mock" + +// mockMemoryReader is an autogenerated mock type for the memoryReader type +type mockMemoryReader struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *mockMemoryReader) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Read provides a mock function with given fields: p +func (_m *mockMemoryReader) Read(p []byte) (int, error) { + ret := _m.Called(p) + + var r0 int + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(p) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(p) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Seek provides a mock function with given fields: offset, whence +func (_m *mockMemoryReader) Seek(offset int64, whence int) (int64, error) { + ret := _m.Called(offset, whence) + + var r0 int64 + if rf, ok := ret.Get(0).(func(int64, int) int64); ok { + r0 = rf(offset, whence) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64, int) error); ok { + r1 = rf(offset, whence) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/mock_process_test.go b/mock_process_test.go new file mode 100644 index 0000000..b544114 --- /dev/null +++ b/mock_process_test.go @@ -0,0 +1,159 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package yapscan + +import ( + procio "github.com/fkie-cad/yapscan/procio" + mock "github.com/stretchr/testify/mock" +) + +// mockProcess is an autogenerated mock type for the process type +type mockProcess struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *mockProcess) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Crash provides a mock function with given fields: _a0 +func (_m *mockProcess) Crash(_a0 procio.CrashMethod) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(procio.CrashMethod) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Handle provides a mock function with given fields: +func (_m *mockProcess) Handle() interface{} { + ret := _m.Called() + + var r0 interface{} + if rf, ok := ret.Get(0).(func() interface{}); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + return r0 +} + +// Info provides a mock function with given fields: +func (_m *mockProcess) Info() (*procio.ProcessInfo, error) { + ret := _m.Called() + + var r0 *procio.ProcessInfo + if rf, ok := ret.Get(0).(func() *procio.ProcessInfo); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*procio.ProcessInfo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MemorySegments provides a mock function with given fields: +func (_m *mockProcess) MemorySegments() ([]*procio.MemorySegmentInfo, error) { + ret := _m.Called() + + var r0 []*procio.MemorySegmentInfo + if rf, ok := ret.Get(0).(func() []*procio.MemorySegmentInfo); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*procio.MemorySegmentInfo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PID provides a mock function with given fields: +func (_m *mockProcess) PID() int { + ret := _m.Called() + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// Resume provides a mock function with given fields: +func (_m *mockProcess) Resume() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// String provides a mock function with given fields: +func (_m *mockProcess) String() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Suspend provides a mock function with given fields: +func (_m *mockProcess) Suspend() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mock_segmentScanner_test.go b/mock_segmentScanner_test.go new file mode 100644 index 0000000..66963fd --- /dev/null +++ b/mock_segmentScanner_test.go @@ -0,0 +1,47 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package yapscan + +import ( + procio "github.com/fkie-cad/yapscan/procio" + mock "github.com/stretchr/testify/mock" + + yara "github.com/hillu/go-yara/v4" +) + +// mockSegmentScanner is an autogenerated mock type for the segmentScanner type +type mockSegmentScanner struct { + mock.Mock +} + +// ScanSegment provides a mock function with given fields: seg +func (_m *mockSegmentScanner) ScanSegment(seg *procio.MemorySegmentInfo) ([]yara.MatchRule, []byte, error) { + ret := _m.Called(seg) + + var r0 []yara.MatchRule + if rf, ok := ret.Get(0).(func(*procio.MemorySegmentInfo) []yara.MatchRule); ok { + r0 = rf(seg) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]yara.MatchRule) + } + } + + var r1 []byte + if rf, ok := ret.Get(1).(func(*procio.MemorySegmentInfo) []byte); ok { + r1 = rf(seg) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]byte) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(*procio.MemorySegmentInfo) error); ok { + r2 = rf(seg) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} diff --git a/output/dumpStorage.go b/output/dumpStorage.go index 3056733..e14c330 100644 --- a/output/dumpStorage.go +++ b/output/dumpStorage.go @@ -7,40 +7,49 @@ import ( "os" "path" - "github.com/fkie-cad/yapscan/procIO" - + "github.com/fkie-cad/yapscan/procio" "github.com/targodan/go-errors" ) +// Dump contains the dump of a memory segment. type Dump struct { PID int - Segment *procIO.MemorySegmentInfo + Segment *procio.MemorySegmentInfo Data io.ReadCloser } +// Filename returns a filename with the PID of the process +// and the address of the Segment of the dump. func (d *Dump) Filename() string { return fmt.Sprintf("%d_0x%s.bin", d.PID, d.Segment.String()) } +// DumpOrError contains either a Dump or an Err. type DumpOrError struct { Dump *Dump Err error } +// DumpStorage provides capability to store dumps. type DumpStorage interface { + // Store stores a Dump. Store(dump *Dump) error + // Hint returns a human readable hint about where/how dumps are stored. Hint() string io.Closer } +// ReadableDumpStorage is a DumpStorage that can also Retrieve +// dumps after storing. type ReadableDumpStorage interface { DumpStorage + // Retrieve retrieves the dumps stored in this DumpStorage. Retrieve(ctx context.Context) <-chan *DumpOrError } type fileDump struct { - Process *procIO.ProcessInfo - Segment *procIO.MemorySegmentInfo + Process *procio.ProcessInfo + Segment *procio.MemorySegmentInfo Filename string } @@ -49,10 +58,12 @@ type fileDumpStorage struct { storedFiles []*fileDump } +// NewFileDumpStorage create a new DumpStorage with a filesystem backend. +// Dumps will be stored in the given directory. func NewFileDumpStorage(dir string) (ReadableDumpStorage, error) { isEmpty, err := isDirEmpty(dir) if err != nil { - return nil, errors.Errorf("could not determine if dump directory is empty, reason: %w", err) + return nil, fmt.Errorf("could not determine if dump directory is empty, reason: %w", err) } if !isEmpty { return nil, errors.New("dump directory is not empty") diff --git a/output/output.go b/output/output.go index a67449b..4c2800a 100644 --- a/output/output.go +++ b/output/output.go @@ -18,8 +18,8 @@ import ( "sync" "github.com/fkie-cad/yapscan" - "github.com/fkie-cad/yapscan/fileIO" - "github.com/fkie-cad/yapscan/procIO" + "github.com/fkie-cad/yapscan/fileio" + "github.com/fkie-cad/yapscan/procio" "github.com/fkie-cad/yapscan/system" "github.com/fatih/color" @@ -29,26 +29,40 @@ import ( "github.com/yeka/zip" ) +// Reporter provides capability to report on scanning progress. type Reporter interface { ReportSystemInfo() error ReportRules(rules *yara.Rules) error ConsumeMemoryScanProgress(progress <-chan *yapscan.MemoryScanProgress) error - ConsumeFSScanProgress(progress <-chan *fileIO.FSScanProgress) error + ConsumeFSScanProgress(progress <-chan *fileio.FSScanProgress) error io.Closer } +// SystemInfoFileName is the name of the file, where system info is stored. const SystemInfoFileName = "systeminfo.json" + +// RulesFileName is the name of the file, where the used rules will be stored. const RulesFileName = "rules.yarc" + +// ProcessFileName is the name of the file used to report information about processes. const ProcessFileName = "processes.json" + +// MemoryProgressFileName is the name of the file used to report information about memory scans. const MemoryProgressFileName = "memory-scans.json" + +// FSProgressFileName is the name of the file used to report information about file scans. const FSProgressFileName = "file-scans.json" +// DefaultZIPPassword is the password used for creating the report ZIP file. const DefaultZIPPassword = "infected" +// MultiReporter is a Reporter which reports all information it recieves +// to all given Reporters. type MultiReporter struct { Reporters []Reporter } +// ReportSystemInfo retrieves and reports info about the running system. func (r *MultiReporter) ReportSystemInfo() error { var err error for _, rep := range r.Reporters { @@ -57,6 +71,7 @@ func (r *MultiReporter) ReportSystemInfo() error { return err } +// ReportRules reports the given *yara.Rules. func (r *MultiReporter) ReportRules(rules *yara.Rules) error { var err error for _, rep := range r.Reporters { @@ -65,6 +80,8 @@ func (r *MultiReporter) ReportRules(rules *yara.Rules) error { return err } +// ConsumeMemoryScanProgress consumes and reports all *yapscan.MemoryScanProgress +// instances sent in the given channel. func (r *MultiReporter) ConsumeMemoryScanProgress(progress <-chan *yapscan.MemoryScanProgress) error { wg := &sync.WaitGroup{} chans := make([]chan *yapscan.MemoryScanProgress, len(r.Reporters)) @@ -89,12 +106,14 @@ func (r *MultiReporter) ConsumeMemoryScanProgress(progress <-chan *yapscan.Memor return nil } -func (r *MultiReporter) ConsumeFSScanProgress(progress <-chan *fileIO.FSScanProgress) error { +// ConsumeFSScanProgress consumes and reports all *yapscan.FSScanProgress +// instances sent in the given channel. +func (r *MultiReporter) ConsumeFSScanProgress(progress <-chan *fileio.FSScanProgress) error { wg := &sync.WaitGroup{} - chans := make([]chan *fileIO.FSScanProgress, len(r.Reporters)) + chans := make([]chan *fileio.FSScanProgress, len(r.Reporters)) wg.Add(len(chans)) for i := range chans { - chans[i] = make(chan *fileIO.FSScanProgress) + chans[i] = make(chan *fileio.FSScanProgress) go func(i int) { r.Reporters[i].ConsumeFSScanProgress(chans[i]) @@ -113,6 +132,7 @@ func (r *MultiReporter) ConsumeFSScanProgress(progress <-chan *fileIO.FSScanProg return nil } +// Close closes all reporters. func (r *MultiReporter) Close() error { var err error for _, rep := range r.Reporters { @@ -121,6 +141,8 @@ func (r *MultiReporter) Close() error { return err } +// GatheredAnalysisReporter wraps an *AnalysisReporter and creates a single +// encrypted ZIP file on Close. type GatheredAnalysisReporter struct { directory string // If ZIP is set, the output files will be zipped into the @@ -137,26 +159,29 @@ func isDirEmpty(dir string) (bool, error) { if os.IsNotExist(err) { os.MkdirAll(dir, 0777) return true, nil - } else { - return false, err - } - } else { - if !fInfo.IsDir() { - return false, errors.New("path is not a directory") } + return false, err + } - contents, err := filepath.Glob(path.Join(dir, "*")) - if err != nil { - return false, err - } - return len(contents) == 0, nil + if !fInfo.IsDir() { + return false, errors.New("path is not a directory") + } + + contents, err := filepath.Glob(path.Join(dir, "*")) + if err != nil { + return false, err } + return len(contents) == 0, nil } +// NewGatheredAnalysisReporter creates a new *GatheredAnalysisReporter +// which will store temporary report files in the given outPath and +// create an encrypted ZIP with the path *GatheredAnalysisReporter.ZIP on +// *GatheredAnalysisReporter.Close. func NewGatheredAnalysisReporter(outPath string) (*GatheredAnalysisReporter, error) { isEmpty, err := isDirEmpty(outPath) if err != nil { - return nil, errors.Errorf("could not determine if analysis directory is empty, reason: %w", err) + return nil, fmt.Errorf("could not determine if analysis directory is empty, reason: %w", err) } if !isEmpty { return nil, errors.New("analysis output directory is not empty") @@ -164,23 +189,23 @@ func NewGatheredAnalysisReporter(outPath string) (*GatheredAnalysisReporter, err sysinfo, err := os.OpenFile(path.Join(outPath, SystemInfoFileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { - return nil, errors.Errorf("could not open systeminfo file, reason: %w", err) + return nil, fmt.Errorf("could not open systeminfo file, reason: %w", err) } rules, err := os.OpenFile(path.Join(outPath, RulesFileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { - return nil, errors.Errorf("could not open rules file, reason: %w", err) + return nil, fmt.Errorf("could not open rules file, reason: %w", err) } process, err := os.OpenFile(path.Join(outPath, ProcessFileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { - return nil, errors.Errorf("could not open processes file, reason: %w", err) + return nil, fmt.Errorf("could not open processes file, reason: %w", err) } memProgress, err := os.OpenFile(path.Join(outPath, MemoryProgressFileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { - return nil, errors.Errorf("could not open memory progress file, reason: %w", err) + return nil, fmt.Errorf("could not open memory progress file, reason: %w", err) } fileProgress, err := os.OpenFile(path.Join(outPath, FSProgressFileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { - return nil, errors.Errorf("could not open filesystem progress file, reason: %w", err) + return nil, fmt.Errorf("could not open filesystem progress file, reason: %w", err) } return &GatheredAnalysisReporter{ @@ -197,27 +222,42 @@ func NewGatheredAnalysisReporter(outPath string) (*GatheredAnalysisReporter, err }, nil } -func (r *GatheredAnalysisReporter) WithFileDumpStorage(outPath string) (err error) { - r.reporter.DumpStorage, err = NewFileDumpStorage(path.Join(r.directory, outPath)) - return +// Directory returns the output directory of the underlying *AnalysisReporter. +func (r *GatheredAnalysisReporter) Directory() string { + return r.directory } +// WithDumpStorage registers the given DumpStorage with the underlying +// *AnalysisReporter and +func (r *GatheredAnalysisReporter) WithDumpStorage(ds DumpStorage) { + r.reporter.DumpStorage = ds +} + +// ReportSystemInfo retrieves and reports info about the running system +// using the underlying *AnalysisReporter. func (r *GatheredAnalysisReporter) ReportSystemInfo() error { return r.reporter.ReportSystemInfo() } +// ReportRules reports the given *yara.Rules using the underlying *AnalysisReporter. func (r *GatheredAnalysisReporter) ReportRules(rules *yara.Rules) error { return r.reporter.ReportRules(rules) } +// ConsumeMemoryScanProgress consumes and reports all *yapscan.MemoryScanProgress +// instances sent in the given channel using the underlying *AnalysisReporter. func (r *GatheredAnalysisReporter) ConsumeMemoryScanProgress(progress <-chan *yapscan.MemoryScanProgress) error { return r.reporter.ConsumeMemoryScanProgress(progress) } -func (r *GatheredAnalysisReporter) ConsumeFSScanProgress(progress <-chan *fileIO.FSScanProgress) error { +// ConsumeFSScanProgress consumes and reports all *yapscan.FSScanProgress +// instances sent in the given channel using the underlying *AnalysisReporter. +func (r *GatheredAnalysisReporter) ConsumeFSScanProgress(progress <-chan *fileio.FSScanProgress) error { return r.reporter.ConsumeFSScanProgress(progress) } +// SuggestZIPName returns a suggestion for the zip name, based on the +// hostname of the running system. func (r *GatheredAnalysisReporter) SuggestZIPName() string { var fname string hostname, err := os.Hostname() @@ -233,7 +273,7 @@ func (r *GatheredAnalysisReporter) SuggestZIPName() string { func (r *GatheredAnalysisReporter) zip() error { zFile, err := os.OpenFile(r.ZIP, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) if err != nil { - return errors.Errorf("could not create zip file, reason: %w", err) + return fmt.Errorf("could not create zip file, reason: %w", err) } defer zFile.Close() @@ -266,58 +306,58 @@ func (r *GatheredAnalysisReporter) zip() error { out, err = zipper(path.Join(hostname, SystemInfoFileName)) if err != nil { - return errors.Errorf("could not write to zip file, reason: %w", err) + return fmt.Errorf("could not write to zip file, reason: %w", err) } in = r.reporter.SystemInfoOut.(*os.File) _, err = in.Seek(0, io.SeekStart) if err != nil { - return errors.Errorf("could not write to zip file, reason: %w", err) + return fmt.Errorf("could not write to zip file, reason: %w", err) } _, err = io.Copy(out, in) if err != nil { - return errors.Errorf("could not write to zip file, reason: %w", err) + return fmt.Errorf("could not write to zip file, reason: %w", err) } out, err = zipper(path.Join(hostname, RulesFileName)) if err != nil { - return errors.Errorf("could not write to zip file, reason: %w", err) + return fmt.Errorf("could not write to zip file, reason: %w", err) } in = r.reporter.RulesOut.(*os.File) _, err = in.Seek(0, io.SeekStart) if err != nil { - return errors.Errorf("could not write to zip file, reason: %w", err) + return fmt.Errorf("could not write to zip file, reason: %w", err) } _, err = io.Copy(out, in) if err != nil { - return errors.Errorf("could not write to zip file, reason: %w", err) + return fmt.Errorf("could not write to zip file, reason: %w", err) } out, err = zipper(path.Join(hostname, ProcessFileName)) if err != nil { - return errors.Errorf("could not write to zip file, reason: %w", err) + return fmt.Errorf("could not write to zip file, reason: %w", err) } in = r.reporter.ProcessInfoOut.(*os.File) _, err = in.Seek(0, io.SeekStart) if err != nil { - return errors.Errorf("could not write to zip file, reason: %w", err) + return fmt.Errorf("could not write to zip file, reason: %w", err) } _, err = io.Copy(out, in) if err != nil { - return errors.Errorf("could not write to zip file, reason: %w", err) + return fmt.Errorf("could not write to zip file, reason: %w", err) } out, err = zipper(path.Join(hostname, MemoryProgressFileName)) if err != nil { - return errors.Errorf("could not write to zip file, reason: %w", err) + return fmt.Errorf("could not write to zip file, reason: %w", err) } in = r.reporter.MemoryScanProgressOut.(*os.File) _, err = in.Seek(0, io.SeekStart) if err != nil { - return errors.Errorf("could not write to zip file, reason: %w", err) + return fmt.Errorf("could not write to zip file, reason: %w", err) } _, err = io.Copy(out, in) if err != nil { - return errors.Errorf("could not write to zip file, reason: %w", err) + return fmt.Errorf("could not write to zip file, reason: %w", err) } if r.reporter.DumpStorage != nil { @@ -333,12 +373,12 @@ func (r *GatheredAnalysisReporter) zip() error { out, err := zipper(path.Join(hostname, "dumps", dump.Dump.Filename())) if err != nil { dump.Dump.Data.Close() - return errors.Errorf("could not write dump to zip file, reason: %w", err) + return fmt.Errorf("could not write dump to zip file, reason: %w", err) } _, err = io.Copy(out, dump.Dump.Data) if err != nil { dump.Dump.Data.Close() - return errors.Errorf("could not write dump to zip file, reason: %w", err) + return fmt.Errorf("could not write dump to zip file, reason: %w", err) } dump.Dump.Data.Close() } @@ -348,9 +388,16 @@ func (r *GatheredAnalysisReporter) zip() error { return nil } +// Close creates the combined zip if GatheredAnalysisReporter.ZIP is set +// and closes the underlying *AnalysisReporter. func (r *GatheredAnalysisReporter) Close() error { var err error + err = r.reporter.Close() + if err != nil { + return err + } + if r.ZIP != "" { err = r.zip() if err != nil { @@ -371,10 +418,6 @@ func (r *GatheredAnalysisReporter) Close() error { } } - err = r.reporter.Close() - if err != nil { - return err - } return nil } @@ -395,6 +438,7 @@ func tryFlush(w io.Writer) error { return nil } +// Match represents the match of a yara Rule. type Match struct { Rule string `json:"rule"` Namespace string `json:"namespace"` @@ -408,7 +452,9 @@ type MatchString struct { Offset uint64 `json:"offset"` } -func FilterMatches(mr []yara.MatchRule) []*Match { +// ConvertYaraMatchRules converts the given slice of yara.MatchRule to +// a slice of *Match. +func ConvertYaraMatchRules(mr []yara.MatchRule) []*Match { ret := make([]*Match, len(mr)) for i, match := range mr { ret[i] = &Match{ @@ -427,6 +473,8 @@ func FilterMatches(mr []yara.MatchRule) []*Match { return ret } +// MemoryScanProgressReport represents all matches on a single memory +// segment of a process. type MemoryScanProgressReport struct { PID int `json:"pid"` MemorySegment uintptr `json:"memorySegment"` @@ -434,6 +482,7 @@ type MemoryScanProgressReport struct { Error interface{} `json:"error"` } +// FSScanProgressReport represents all matches on a file. type FSScanProgressReport struct { Path string `json:"path"` Matches []*Match `json:"match"` @@ -454,6 +503,7 @@ type AnalysisReporter struct { seen map[int]bool } +// ReportSystemInfo retrieves and reports info about the running system. func (r *AnalysisReporter) ReportSystemInfo() error { if r.SystemInfoOut == nil { return nil @@ -476,6 +526,7 @@ func (r *AnalysisReporter) ReportSystemInfo() error { return nil } +// ReportRules reports the given *yara.Rules. func (r *AnalysisReporter) ReportRules(rules *yara.Rules) error { if r.RulesOut == nil { return nil @@ -493,7 +544,7 @@ func (r *AnalysisReporter) ReportRules(rules *yara.Rules) error { return nil } -func (r *AnalysisReporter) reportProcess(info *procIO.ProcessInfo) error { +func (r *AnalysisReporter) reportProcess(info *procio.ProcessInfo) error { if r.ProcessInfoOut == nil { return nil } @@ -501,6 +552,8 @@ func (r *AnalysisReporter) reportProcess(info *procIO.ProcessInfo) error { return json.NewEncoder(r.ProcessInfoOut).Encode(info) } +// ConsumeMemoryScanProgress consumes and reports all *yapscan.MemoryScanProgress +// instances sent in the given channel. func (r *AnalysisReporter) ConsumeMemoryScanProgress(progress <-chan *yapscan.MemoryScanProgress) error { if r.seen == nil { r.seen = make(map[int]bool) @@ -527,7 +580,7 @@ func (r *AnalysisReporter) ConsumeMemoryScanProgress(progress <-chan *yapscan.Me err = json.NewEncoder(r.MemoryScanProgressOut).Encode(&MemoryScanProgressReport{ PID: info.PID, MemorySegment: prog.MemorySegment.BaseAddress, - Matches: FilterMatches(prog.Matches), + Matches: ConvertYaraMatchRules(prog.Matches), Error: jsonErr, }) if err != nil { @@ -547,7 +600,9 @@ func (r *AnalysisReporter) ConsumeMemoryScanProgress(progress <-chan *yapscan.Me return nil } -func (r *AnalysisReporter) ConsumeFSScanProgress(progress <-chan *fileIO.FSScanProgress) error { +// ConsumeFSScanProgress consumes and reports all *yapscan.FSScanProgress +// instances sent in the given channel. +func (r *AnalysisReporter) ConsumeFSScanProgress(progress <-chan *fileio.FSScanProgress) error { if r.seen == nil { r.seen = make(map[int]bool) } @@ -559,7 +614,7 @@ func (r *AnalysisReporter) ConsumeFSScanProgress(progress <-chan *fileIO.FSScanP } err := json.NewEncoder(r.FSScanProgressOut).Encode(&FSScanProgressReport{ Path: prog.File.Path(), - Matches: FilterMatches(prog.Matches), + Matches: ConvertYaraMatchRules(prog.Matches), Error: jsonErr, }) if err != nil { @@ -570,6 +625,7 @@ func (r *AnalysisReporter) ConsumeFSScanProgress(progress <-chan *fileIO.FSScanP return nil } +// Close closes the AnalysisReporter and all associated files. func (r *AnalysisReporter) Close() error { var err error err = errors.NewMultiError(err, r.SystemInfoOut.Close()) @@ -591,6 +647,11 @@ type progressReporter struct { allClean bool } +// NewProgressReporter creates a new Reporter, which will write memory and file scanning +// progress to the given io.WriteCloser out using the ProgressFormatter formatter for +// formatting. +// This Reporter is intended for live updates to the console, hence ReportSystemInfo() +// and ReportRules() do nothing. func NewProgressReporter(out io.WriteCloser, formatter ProgressFormatter) Reporter { return &progressReporter{out: out, formatter: formatter, pid: -1, allClean: true} } @@ -615,7 +676,7 @@ func (r *progressReporter) Close() error { return r.out.Close() } -func (r *progressReporter) reportProcess(proc procIO.Process) error { +func (r *progressReporter) reportProcess(proc procio.Process) error { info, err := proc.Info() if err != nil { logrus.WithError(err).Warn("Could not retrieve complete process info.") @@ -637,7 +698,7 @@ func (r *progressReporter) receiveMem(progress *yapscan.MemoryScanProgress) { if l > 0 { r.procSegmentCount += l } else { - r.procSegmentCount += 1 + r.procSegmentCount++ } } r.procSegmentIndex = 0 @@ -660,7 +721,7 @@ func (r *progressReporter) receiveMem(progress *yapscan.MemoryScanProgress) { fmt.Fprintln(r.out, "\r", matchOut) } - r.procSegmentIndex += 1 + r.procSegmentIndex++ percent := int(float64(r.procSegmentIndex)/float64(r.procSegmentCount)*100. + 0.5) var format string @@ -698,7 +759,7 @@ func (r *progressReporter) ConsumeMemoryScanProgress(progress <-chan *yapscan.Me return nil } -func (r *progressReporter) receiveFS(progress *fileIO.FSScanProgress) { +func (r *progressReporter) receiveFS(progress *fileio.FSScanProgress) { if progress.Matches != nil && len(progress.Matches) > 0 { r.allClean = false } @@ -728,7 +789,7 @@ func (r *progressReporter) receiveFS(progress *fileIO.FSScanProgress) { } } -func (r *progressReporter) ConsumeFSScanProgress(progress <-chan *fileIO.FSScanProgress) error { +func (r *progressReporter) ConsumeFSScanProgress(progress <-chan *fileio.FSScanProgress) error { for prog := range progress { r.receiveFS(prog) } @@ -736,14 +797,16 @@ func (r *progressReporter) ConsumeFSScanProgress(progress <-chan *fileIO.FSScanP return nil } +// ProgressFormatter formats progress information. type ProgressFormatter interface { FormatMemoryScanProgress(progress *yapscan.MemoryScanProgress) string - FormatFSScanProgress(progress *fileIO.FSScanProgress) string + FormatFSScanProgress(progress *fileio.FSScanProgress) string FormatPath(path string, maxlen int) string } type prettyFormatter struct{} +// NewPrettyFormatter creates a new pretty formatter for human readable console output. func NewPrettyFormatter() ProgressFormatter { return &prettyFormatter{} } @@ -753,9 +816,9 @@ func (p prettyFormatter) FormatMemoryScanProgress(progress *yapscan.MemoryScanPr msg := "" // TODO: Maybe enable via a verbose flag //if progress.Error == ErrSkipped { - // msg = "Skipped " + procIO.FormatMemorySegmentAddress(progress.MemorySegment) + // msg = "Skipped " + procio.FormatMemorySegmentAddress(progress.MemorySegment) //} else { - // msg = "Error during scan of segment " + procIO.FormatMemorySegmentAddress(progress.MemorySegment) + ": " + progress.Error.Error() + // msg = "Error during scan of segment " + procio.FormatMemorySegmentAddress(progress.MemorySegment) + ": " + progress.Error.Error() //} return msg } @@ -768,7 +831,7 @@ func (p prettyFormatter) FormatMemoryScanProgress(progress *yapscan.MemoryScanPr for i, match := range progress.Matches { txt[i] = fmt.Sprintf( color.RedString("MATCH:")+" Rule \"%s\" matches segment %s.", - match.Rule, procIO.FormatMemorySegmentAddress(progress.MemorySegment), + match.Rule, procio.FormatMemorySegmentAddress(progress.MemorySegment), ) if len(match.Strings) > 0 { addrs := yapscan.FormatSlice("0x%X", yapscan.AddressesFromMatches(match.Strings, uint64(progress.MemorySegment.BaseAddress))) @@ -814,7 +877,7 @@ func (p prettyFormatter) FormatPath(path string, maxlen int) string { return "..." + res[len(res)-maxlen-3:] } -func (p prettyFormatter) FormatFSScanProgress(progress *fileIO.FSScanProgress) string { +func (p prettyFormatter) FormatFSScanProgress(progress *fileio.FSScanProgress) string { if progress.Error != nil { // TODO: Maybe enable via a verbose flag return "" diff --git a/prepare.sh b/prepare.sh deleted file mode 100755 index 2d49a11..0000000 --- a/prepare.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -go mod tidy -go mod vendor - -go get -v github.com/abice/go-enum -go get github.com/vektra/mockery/v2/.../ -go mod tidy - -go generate ./... \ No newline at end of file diff --git a/procIO/memory.go b/procIO/memory.go deleted file mode 100644 index 6ba1751..0000000 --- a/procIO/memory.go +++ /dev/null @@ -1,187 +0,0 @@ -//go:generate go-enum -f=$GOFILE --marshal --lower --names -package procIO - -import ( - "errors" - "fmt" - "strings" -) - -type MemorySegmentInfo struct { - // On windows: _MEMORY_BASIC_INFORMATION->AllocationBase - ParentBaseAddress uintptr `json:"parentBaseAddress"` - // On windows: _MEMORY_BASIC_INFORMATION->BaseAddress - BaseAddress uintptr `json:"baseAddress"` - // On windows: _MEMORY_BASIC_INFORMATION->AllocationProtect - AllocatedPermissions Permissions `json:"allocatedPermissions"` - // On windows: _MEMORY_BASIC_INFORMATION->Protect - CurrentPermissions Permissions `json:"currentPermissions"` - // On windows: _MEMORY_BASIC_INFORMATION->RegionSize - Size uintptr `json:"size"` - // On windows: _MEMORY_BASIC_INFORMATION->State - State State `json:"state"` - // On windows: _MEMORY_BASIC_INFORMATION->Type - Type Type `json:"type"` - - FilePath string `json:"filePath"` - - SubSegments []*MemorySegmentInfo `json:"subSegments"` -} - -func (s *MemorySegmentInfo) String() string { - return FormatMemorySegmentAddress(s) -} - -func (s *MemorySegmentInfo) CopyWithoutSubSegments() *MemorySegmentInfo { - return &MemorySegmentInfo{ - ParentBaseAddress: s.ParentBaseAddress, - BaseAddress: s.BaseAddress, - AllocatedPermissions: s.AllocatedPermissions, - CurrentPermissions: s.CurrentPermissions, - Size: s.Size, - State: s.State, - Type: s.Type, - SubSegments: make([]*MemorySegmentInfo, 0), - } -} - -type Permissions struct { - // Is read-only access allowed - Read bool - // Is write access allowed (also true if COW is enabled) - Write bool - // Is copy-on-write access allowed (if this is true, then so is Write) - COW bool - // Is execute access allowed - Execute bool -} - -var PermR = Permissions{ - Read: true, -} -var PermRW = Permissions{ - Read: true, - Write: true, -} -var PermRX = Permissions{ - Read: true, - Execute: true, -} -var PermRC = Permissions{ - Read: true, - Write: true, - COW: true, -} -var PermRWX = Permissions{ - Read: true, - Write: true, - Execute: true, -} -var PermRCX = Permissions{ - Read: true, - Write: true, - COW: true, - Execute: true, -} - -func ParsePermissions(s string) (Permissions, error) { - perm := Permissions{ - Read: false, - Write: false, - COW: false, - Execute: false, - } - for _, c := range strings.ToLower(s) { - switch c { - case 'r': - perm.Read = true - case 'w': - perm.Write = true - case 'c': - perm.Write = true - perm.COW = true - case 'e': - fallthrough - case 'x': - perm.Execute = true - case '-': - continue - default: - return perm, errors.New(fmt.Sprintf("character '%c' is not a valid permission character", c)) - } - } - return perm, nil -} - -func (p Permissions) EqualTo(other Permissions) bool { - return p.Read == other.Read && p.Write == other.Write && p.COW == other.COW && p.Execute == other.Execute -} - -func (p Permissions) IsMoreOrEquallyPermissiveThan(other Permissions) bool { - if other.Read && !p.Read { - return false - } - if other.Write && !p.Write { - return false - } - if other.Execute && !p.Execute { - return false - } - return true -} - -func (p Permissions) IsMorePermissiveThan(other Permissions) bool { - if other.Read && !p.Read { - return false - } - if other.Write && !p.Write { - return false - } - if other.Execute && !p.Execute { - return false - } - return !p.EqualTo(other) -} - -func (p Permissions) String() string { - ret := "" - if p.Read { - ret += "R" - } else { - ret += "-" - } - if p.Write { - if p.COW { - ret += "C" - } else { - ret += "W" - } - } else { - ret += "-" - } - if p.Execute { - ret += "X" - } else { - ret += "-" - } - return ret -} - -/* -ENUM( -Commit -Free -Reserve -) -*/ -type State int - -// TODO: Consider additional type "Shared" -/* -ENUM( -Image -Mapped -Private -) -*/ -type Type int diff --git a/processScanner.go b/processScanner.go index f51af2f..c68d35c 100644 --- a/processScanner.go +++ b/processScanner.go @@ -4,24 +4,23 @@ import ( "io/ioutil" "os" + "github.com/fkie-cad/yapscan/procio" "github.com/hillu/go-yara/v4" "github.com/sirupsen/logrus" - "github.com/fkie-cad/yapscan/procIO" - "github.com/targodan/go-errors" ) type segmentScanner interface { - ScanSegment(seg *procIO.MemorySegmentInfo) ([]yara.MatchRule, []byte, error) + ScanSegment(seg *procio.MemorySegmentInfo) ([]yara.MatchRule, []byte, error) } // ProcessScanner implements scanning of memory segments, allocated by a process. // This scanning is done using an underlying MemoryScanner on segments, matching // a MemorySegmentFilter. type ProcessScanner struct { - proc procIO.Process + proc procio.Process scanner segmentScanner } @@ -32,35 +31,35 @@ type MemoryScanner interface { } type process interface { - procIO.Process + procio.Process } type memoryReader interface { - procIO.MemoryReader + procio.MemoryReader } type memoryReaderFactory interface { - procIO.MemoryReaderFactory + procio.MemoryReaderFactory } type defaultSegmentScanner struct { - proc procIO.Process + proc procio.Process filter MemorySegmentFilter scanner MemoryScanner - rdrFactory procIO.MemoryReaderFactory + rdrFactory procio.MemoryReaderFactory } -// NewProcessScanner create a new ProcessScanner with for the given procIO.Process. +// NewProcessScanner create a new ProcessScanner with for the given procio.Process. // It uses the given MemoryScanner in order to scan memory segments of the process, // which match the given MemoryScanner. -func NewProcessScanner(proc procIO.Process, filter MemorySegmentFilter, scanner MemoryScanner) *ProcessScanner { +func NewProcessScanner(proc procio.Process, filter MemorySegmentFilter, scanner MemoryScanner) *ProcessScanner { return &ProcessScanner{ proc: proc, scanner: &defaultSegmentScanner{ proc: proc, filter: filter, scanner: scanner, - rdrFactory: &procIO.DefaultMemoryReaderFactory{}, + rdrFactory: &procio.DefaultMemoryReaderFactory{}, }, } } @@ -72,9 +71,9 @@ var ErrSkipped = errors.New("skipped") // MemoryScanProgress contains all information, generated during scanning. type MemoryScanProgress struct { // Process contains information about the process being scanned. - Process procIO.Process + Process procio.Process // MemorySegment contains information about the specific memory segment which was just scanned. - MemorySegment *procIO.MemorySegmentInfo + MemorySegment *procio.MemorySegmentInfo // Dump contains the raw contents of the memory segment. Dump []byte // Matches contains the yara.MatchRule results. @@ -83,7 +82,7 @@ type MemoryScanProgress struct { Error error } -func (s *ProcessScanner) handleSegment(progress chan<- *MemoryScanProgress, segment *procIO.MemorySegmentInfo) bool { +func (s *ProcessScanner) handleSegment(progress chan<- *MemoryScanProgress, segment *procio.MemorySegmentInfo) bool { if len(segment.SubSegments) == 0 { // Only scan leaf segments matches, data, err := s.scanner.ScanSegment(segment) @@ -136,7 +135,7 @@ func (s *ProcessScanner) Scan() (<-chan *MemoryScanProgress, error) { return progress, nil } -func (s *defaultSegmentScanner) ScanSegment(seg *procIO.MemorySegmentInfo) ([]yara.MatchRule, []byte, error) { +func (s *defaultSegmentScanner) ScanSegment(seg *procio.MemorySegmentInfo) ([]yara.MatchRule, []byte, error) { match := s.filter.Filter(seg) if !match.Result { logrus.WithFields(logrus.Fields{ diff --git a/processScanner_test.go b/processScanner_test.go index 9e5554f..03cc35f 100644 --- a/processScanner_test.go +++ b/processScanner_test.go @@ -6,8 +6,7 @@ import ( "os" "testing" - "github.com/fkie-cad/yapscan/procIO" - + "github.com/fkie-cad/yapscan/procio" "github.com/hillu/go-yara/v4" "github.com/targodan/go-errors" @@ -44,33 +43,33 @@ func TestPorcessScan(t *testing.T) { }) Convey("scanning a valid process", func() { - segments := []*procIO.MemorySegmentInfo{ - &procIO.MemorySegmentInfo{ // This should not be scanned, it's not a leaf + segments := []*procio.MemorySegmentInfo{ + &procio.MemorySegmentInfo{ // This should not be scanned, it's not a leaf ParentBaseAddress: 1, BaseAddress: 1, - AllocatedPermissions: procIO.Permissions{}, - CurrentPermissions: procIO.Permissions{}, + AllocatedPermissions: procio.Permissions{}, + CurrentPermissions: procio.Permissions{}, Size: 0, State: 0, Type: 0, FilePath: "", - SubSegments: []*procIO.MemorySegmentInfo{ - &procIO.MemorySegmentInfo{ + SubSegments: []*procio.MemorySegmentInfo{ + &procio.MemorySegmentInfo{ ParentBaseAddress: 1, BaseAddress: 2, - AllocatedPermissions: procIO.Permissions{}, - CurrentPermissions: procIO.Permissions{}, + AllocatedPermissions: procio.Permissions{}, + CurrentPermissions: procio.Permissions{}, Size: 0, State: 0, Type: 0, FilePath: "", SubSegments: nil, }, - &procIO.MemorySegmentInfo{ + &procio.MemorySegmentInfo{ ParentBaseAddress: 1, BaseAddress: 3, - AllocatedPermissions: procIO.Permissions{}, - CurrentPermissions: procIO.Permissions{}, + AllocatedPermissions: procio.Permissions{}, + CurrentPermissions: procio.Permissions{}, Size: 0, State: 0, Type: 0, @@ -79,22 +78,22 @@ func TestPorcessScan(t *testing.T) { }, }, }, - &procIO.MemorySegmentInfo{ + &procio.MemorySegmentInfo{ ParentBaseAddress: 4, BaseAddress: 4, - AllocatedPermissions: procIO.Permissions{}, - CurrentPermissions: procIO.Permissions{}, + AllocatedPermissions: procio.Permissions{}, + CurrentPermissions: procio.Permissions{}, Size: 0, State: 0, Type: 0, FilePath: "", SubSegments: nil, }, - &procIO.MemorySegmentInfo{ + &procio.MemorySegmentInfo{ ParentBaseAddress: 5, BaseAddress: 5, - AllocatedPermissions: procIO.Permissions{}, - CurrentPermissions: procIO.Permissions{}, + AllocatedPermissions: procio.Permissions{}, + CurrentPermissions: procio.Permissions{}, Size: 0, State: 0, Type: 0, @@ -239,12 +238,12 @@ func (rdr *mockMemoryReaderWithBuffer) Read(p []byte) (n int, err error) { return } -func (rdr *mockMemoryReaderWithBuffer) Close() error { - return nil +func (rdr *mockMemoryReaderWithBuffer) Seek(offset int64, whence int) (int64, error) { + panic("seeking not implemented in mock") } -func (rdr *mockMemoryReaderWithBuffer) Reset() (procIO.MemoryReader, error) { - return rdr, nil +func (rdr *mockMemoryReaderWithBuffer) Close() error { + return nil } func TestSegmentScanner(t *testing.T) { @@ -268,7 +267,7 @@ func TestSegmentScanner(t *testing.T) { Reason: "skipped", }) - match, data, err := sc.ScanSegment(&procIO.MemorySegmentInfo{}) + match, data, err := sc.ScanSegment(&procio.MemorySegmentInfo{}) Convey("should yield the appropriate skip error.", func() { So(match, ShouldBeNil) @@ -305,7 +304,7 @@ func TestSegmentScanner(t *testing.T) { mockedFactory.On("NewMemoryReader", mock.Anything, mock.Anything).Return(nil, expErr) sc.rdrFactory = mockedFactory - match, data, err := sc.ScanSegment(&procIO.MemorySegmentInfo{}) + match, data, err := sc.ScanSegment(&procio.MemorySegmentInfo{}) Convey("should yield the underlying error.", func() { So(match, ShouldBeNil) @@ -329,7 +328,7 @@ func TestSegmentScanner(t *testing.T) { Return(mockRdr, nil) sc.rdrFactory = mockedFactory - match, data, err := sc.ScanSegment(&procIO.MemorySegmentInfo{}) + match, data, err := sc.ScanSegment(&procio.MemorySegmentInfo{}) Convey("should yield the error.", func() { So(match, ShouldBeNil) @@ -357,7 +356,7 @@ func TestSegmentScanner(t *testing.T) { mockedMemoryScanner.On("ScanMem", memoryData).Return(expMatches, expErr).Once() sc.rdrFactory = mockedFactory - match, data, err := sc.ScanSegment(&procIO.MemorySegmentInfo{}) + match, data, err := sc.ScanSegment(&procio.MemorySegmentInfo{}) Convey("should yield the expected data.", func() { So(match, ShouldResemble, expMatches) diff --git a/procio/crash.go b/procio/crash.go new file mode 100644 index 0000000..a682202 --- /dev/null +++ b/procio/crash.go @@ -0,0 +1,10 @@ +//go:generate go-enum -f=$GOFILE --marshal --lower --names +package procio + +// CrashMethod selects a method to crash a process. +/* +ENUM( +CreateThreadOnNull +) +*/ +type CrashMethod int diff --git a/procio/crash_enum.go b/procio/crash_enum.go new file mode 100644 index 0000000..5a80153 --- /dev/null +++ b/procio/crash_enum.go @@ -0,0 +1,68 @@ +// Code generated by go-enum +// DO NOT EDIT! + +package procio + +import ( + "fmt" + "strings" +) + +const ( + // CrashMethodCreateThreadOnNull is a CrashMethod of type CreateThreadOnNull + CrashMethodCreateThreadOnNull CrashMethod = iota +) + +const _CrashMethodName = "CreateThreadOnNull" + +var _CrashMethodNames = []string{ + _CrashMethodName[0:18], +} + +// CrashMethodNames returns a list of possible string values of CrashMethod. +func CrashMethodNames() []string { + tmp := make([]string, len(_CrashMethodNames)) + copy(tmp, _CrashMethodNames) + return tmp +} + +var _CrashMethodMap = map[CrashMethod]string{ + 0: _CrashMethodName[0:18], +} + +// String implements the Stringer interface. +func (x CrashMethod) String() string { + if str, ok := _CrashMethodMap[x]; ok { + return str + } + return fmt.Sprintf("CrashMethod(%d)", x) +} + +var _CrashMethodValue = map[string]CrashMethod{ + _CrashMethodName[0:18]: 0, + strings.ToLower(_CrashMethodName[0:18]): 0, +} + +// ParseCrashMethod attempts to convert a string to a CrashMethod +func ParseCrashMethod(name string) (CrashMethod, error) { + if x, ok := _CrashMethodValue[name]; ok { + return x, nil + } + return CrashMethod(0), fmt.Errorf("%s is not a valid CrashMethod, try [%s]", name, strings.Join(_CrashMethodNames, ", ")) +} + +// MarshalText implements the text marshaller method +func (x CrashMethod) MarshalText() ([]byte, error) { + return []byte(x.String()), nil +} + +// UnmarshalText implements the text unmarshaller method +func (x *CrashMethod) UnmarshalText(text []byte) error { + name := string(text) + tmp, err := ParseCrashMethod(name) + if err != nil { + return err + } + *x = tmp + return nil +} diff --git a/procIO/customWin32/advapi32_windows.go b/procio/customWin32/advapi32_windows.go similarity index 95% rename from procIO/customWin32/advapi32_windows.go rename to procio/customWin32/advapi32_windows.go index 900c77a..dfa0be4 100644 --- a/procIO/customWin32/advapi32_windows.go +++ b/procio/customWin32/advapi32_windows.go @@ -1,10 +1,9 @@ package customWin32 import ( + "fmt" "syscall" "unsafe" - - "github.com/targodan/go-errors" ) // #include @@ -90,7 +89,7 @@ func UsernameFromSID(sid *syscall.SID) (string, error) { nil, ) if err != syscall.ERROR_INSUFFICIENT_BUFFER || nameLength == 0 || domainLength == 0 { - return "", errors.Errorf("could not determine username length, reason: %w", err) + return "", fmt.Errorf("could not determine username length, reason: %w", err) } accountName := make([]uint16, nameLength/2+1) diff --git a/procIO/customWin32/kernel32_windows.go b/procio/customWin32/kernel32_windows.go similarity index 81% rename from procIO/customWin32/kernel32_windows.go rename to procio/customWin32/kernel32_windows.go index e6de8f2..82f712c 100644 --- a/procIO/customWin32/kernel32_windows.go +++ b/procio/customWin32/kernel32_windows.go @@ -6,11 +6,10 @@ package customWin32 import ( + "fmt" "syscall" "unsafe" - "github.com/targodan/go-errors" - "github.com/0xrawsec/golang-win32/win32" k32 "github.com/0xrawsec/golang-win32/win32/kernel32" ) @@ -22,8 +21,11 @@ var ( process32NextW = kernel32.NewProc("Process32NextW") thread32First = kernel32.NewProc("Thread32First") thread32Next = kernel32.NewProc("Thread32Next") + createRemoteThread = kernel32.NewProc("CreateRemoteThread") ) +const ERROR_NOT_ENOUGH_MEMORY = 0x8 + func ReadProcessMemory(hProcess win32.HANDLE, lpBaseAddress win32.LPCVOID, buffer []byte) (int, error) { numberOfBytesRead := win32.SIZE_T(0) r1, _, lastErr := readProcessMemory.Call( @@ -120,18 +122,18 @@ func ListThreads(pid int) ([]int, error) { func SuspendProcess(pid int) error { threads, err := ListThreads(pid) if err != nil { - return errors.Errorf("could not list process threads, reason: %w", err) + return fmt.Errorf("could not list process threads, reason: %w", err) } for _, tid := range threads { hThread, err := k32.OpenThread(k32.THREAD_SUSPEND_RESUME, win32.FALSE, win32.DWORD(tid)) if err != nil { - return errors.Errorf("could not open thread %d, reason: %w", tid, err) + return fmt.Errorf("could not open thread %d, reason: %w", tid, err) } _, err = k32.SuspendThread(hThread) k32.CloseHandle(hThread) if err != nil { - return errors.Errorf("could not open suspend thread %d, reason: %w", tid, err) + return fmt.Errorf("could not open suspend thread %d, reason: %w", tid, err) } } return nil @@ -140,19 +142,35 @@ func SuspendProcess(pid int) error { func ResumeProcess(pid int) error { threads, err := ListThreads(pid) if err != nil { - return errors.Errorf("could not list process threads, reason: %w", err) + return fmt.Errorf("could not list process threads, reason: %w", err) } for _, tid := range threads { hThread, err := k32.OpenThread(k32.THREAD_SUSPEND_RESUME, win32.FALSE, win32.DWORD(tid)) if err != nil { - return errors.Errorf("could not open thread %d, reason: %w", tid, err) + return fmt.Errorf("could not open thread %d, reason: %w", tid, err) } _, err = k32.ResumeThread(hThread) k32.CloseHandle(hThread) if err != nil { - return errors.Errorf("could not open resume thread %d, reason: %w", tid, err) + return fmt.Errorf("could not open resume thread %d, reason: %w", tid, err) } } return nil } + +func CreateRemoteThreadMinimal(hProcess win32.HANDLE, startAddress uintptr) error { + _, _, lastErr := createRemoteThread.Call( + uintptr(hProcess), + uintptr(0), + uintptr(0), + startAddress, + uintptr(0), + uintptr(0), + uintptr(0), + ) + if lastErr.(syscall.Errno) == 0 { + return nil + } + return lastErr +} diff --git a/procIO/customWin32/structs_windows.go b/procio/customWin32/structs_windows.go similarity index 100% rename from procIO/customWin32/structs_windows.go rename to procio/customWin32/structs_windows.go diff --git a/procIO/format.go b/procio/format.go similarity index 54% rename from procIO/format.go rename to procio/format.go index 27e849d..3dbf441 100644 --- a/procIO/format.go +++ b/procio/format.go @@ -1,9 +1,12 @@ -package procIO +package procio import ( "fmt" ) +// FormatMemorySegmentAddress formats the *MemorySegmentInfo.BaseAddress +// to a hex string with prefix '0x' and either 8 or 16 digits (based on +// the address value) with leading zeros. func FormatMemorySegmentAddress(seg *MemorySegmentInfo) string { format := "" if seg.BaseAddress < (1 << 32) { @@ -14,6 +17,7 @@ func FormatMemorySegmentAddress(seg *MemorySegmentInfo) string { return fmt.Sprintf(format, seg.BaseAddress) } +// FormatPID formats the given process ID. func FormatPID(pid int) string { return fmt.Sprint(pid) } diff --git a/procio/memory.go b/procio/memory.go new file mode 100644 index 0000000..9cac8a6 --- /dev/null +++ b/procio/memory.go @@ -0,0 +1,240 @@ +//go:generate go-enum -f=$GOFILE --marshal --lower --names +package procio + +import ( + "fmt" + "strings" +) + +// MemorySegmentInfo contains information about a memory segment. +type MemorySegmentInfo struct { + // ParentBaseAddress is the base address of the parent segment. + // If no parent segment exists, this is equal to the BaseAddress. + // Equivalence on windows: _MEMORY_BASIC_INFORMATION->AllocationBase + ParentBaseAddress uintptr `json:"parentBaseAddress"` + + // BaseAddress is the base address of the current memory segment. + // Equivalence on windows: _MEMORY_BASIC_INFORMATION->BaseAddress + BaseAddress uintptr `json:"baseAddress"` + + // AllocatedPermissions is the Permissions that were used to initially + // allocate this segment. + // Equivalence on windows: _MEMORY_BASIC_INFORMATION->AllocationProtect + AllocatedPermissions Permissions `json:"allocatedPermissions"` + + // CurrentPermissions is the Permissions that the segment currently has. + // This may differ from AllocatedPermissions if the permissions where changed + // at some point (e.g. via VirtualProtect). + // Equivalence on windows: _MEMORY_BASIC_INFORMATION->Protect + CurrentPermissions Permissions `json:"currentPermissions"` + + // Size contains the size of the segment in bytes. + // Equivalence on windows: _MEMORY_BASIC_INFORMATION->RegionSize + Size uintptr `json:"size"` + + // State contains the current State of the segment. + // Equivalence on windows: _MEMORY_BASIC_INFORMATION->State + State State `json:"state"` + + // Type contains the Type of the segment. + // Equivalence on windows: _MEMORY_BASIC_INFORMATION->Type + Type Type `json:"type"` + + // FilePath contains the path to the mapped file, or empty string if + // no file mapping is associated with this memory segment. + FilePath string `json:"filePath"` + + // SubSegments contains sub-segments, i.e. segment where their ParentBaseAddress + // is equal to this segments BaseAddress. + // If no such segments exist, this will be a slice of length 0. + SubSegments []*MemorySegmentInfo `json:"subSegments"` +} + +// String returns a human readable representation of the BaseAddress. +func (s *MemorySegmentInfo) String() string { + return FormatMemorySegmentAddress(s) +} + +// CopyWithoutSubSegments creates a copy of this *MemorySegmentInfo, but +// the SubSegments of the returned *MemorySegmentInfo will be of length 0. +func (s *MemorySegmentInfo) CopyWithoutSubSegments() *MemorySegmentInfo { + return &MemorySegmentInfo{ + ParentBaseAddress: s.ParentBaseAddress, + BaseAddress: s.BaseAddress, + AllocatedPermissions: s.AllocatedPermissions, + CurrentPermissions: s.CurrentPermissions, + Size: s.Size, + State: s.State, + Type: s.Type, + SubSegments: make([]*MemorySegmentInfo, 0), + } +} + +// Permissions describes the permissions of a memory segment. +type Permissions struct { + // Is read-only access allowed + Read bool + // Is write access allowed (also true if COW is enabled) + Write bool + // Is copy-on-write access allowed (if this is true, then so is Write) + COW bool + // Is execute access allowed + Execute bool +} + +// PermR is readonly Permissions. +var PermR = Permissions{ + Read: true, +} + +// PermRW is the read-write Permissions. +var PermRW = Permissions{ + Read: true, + Write: true, +} + +// PermRX is the read-execute Permissions. +var PermRX = Permissions{ + Read: true, + Execute: true, +} + +// PermRC is the read and copy-on-write Permissions. +var PermRC = Permissions{ + Read: true, + Write: true, + COW: true, +} + +// PermRWX is the read-write-execute Permissions. +var PermRWX = Permissions{ + Read: true, + Write: true, + Execute: true, +} + +// PermRCX is the read-execute and copy-on-write Permissions. +var PermRCX = Permissions{ + Read: true, + Write: true, + COW: true, + Execute: true, +} + +// ParsePermissions parses the string representation of a Permissions, +// as output by Permissions.String and returns the resulting Permissions. +// +// Each character of the string is interpreted individually and case insensitive. +// A '-' is ignored, 'r' stands for read, 'w' for write, 'c' for copy-on-write, +// and 'e' or 'x' for execute. Any other character results in an error. +// The resulting Permissions is the combination of all character interpretations. +func ParsePermissions(s string) (Permissions, error) { + perm := Permissions{ + Read: false, + Write: false, + COW: false, + Execute: false, + } + for _, c := range strings.ToLower(s) { + switch c { + case 'r': + perm.Read = true + case 'w': + perm.Write = true + case 'c': + perm.Write = true + perm.COW = true + case 'e': + fallthrough + case 'x': + perm.Execute = true + case '-': + continue + default: + return perm, fmt.Errorf("character '%c' is not a valid permission character", c) + } + } + return perm, nil +} + +// EqualTo returns true if the other Permissions is exactly equal to this one. +func (p Permissions) EqualTo(other Permissions) bool { + return p.Read == other.Read && p.Write == other.Write && p.COW == other.COW && p.Execute == other.Execute +} + +// IsMoreOrEquallyPermissiveThan returns true if the other Permissions is equally or +// more permissive than this one. +// See IsMorePermissiveThan for more information +func (p Permissions) IsMoreOrEquallyPermissiveThan(other Permissions) bool { + if other.Read && !p.Read { + return false + } + if other.Write && !p.Write { + return false + } + if other.Execute && !p.Execute { + return false + } + return true +} + +// IsMorePermissiveThan returns true if the other Permissions is more permissive than +// this one. +// E.g. "rx" is more permissive than "r". +func (p Permissions) IsMorePermissiveThan(other Permissions) bool { + if other.Read && !p.Read { + return false + } + if other.Write && !p.Write { + return false + } + if other.Execute && !p.Execute { + return false + } + return !p.EqualTo(other) +} + +// String returns the string representation of this Permissions. +func (p Permissions) String() string { + ret := "" + if p.Read { + ret += "R" + } else { + ret += "-" + } + if p.Write { + if p.COW { + ret += "C" + } else { + ret += "W" + } + } else { + ret += "-" + } + if p.Execute { + ret += "X" + } else { + ret += "-" + } + return ret +} + +// State represents the state of a memory segment. +/* +ENUM( +Commit +Free +Reserve +) +*/ +type State int + +// Type represents the type of a memory segment. +/* +ENUM( +Image +Mapped +Private +) +*/ +type Type int diff --git a/procIO/memory_enum.go b/procio/memory_enum.go similarity index 99% rename from procIO/memory_enum.go rename to procio/memory_enum.go index e89c88e..072cee5 100644 --- a/procIO/memory_enum.go +++ b/procio/memory_enum.go @@ -1,7 +1,7 @@ // Code generated by go-enum // DO NOT EDIT! -package procIO +package procio import ( "fmt" diff --git a/procIO/memory_linux.go b/procio/memory_linux.go similarity index 65% rename from procIO/memory_linux.go rename to procio/memory_linux.go index a8a39f8..a6f73af 100644 --- a/procIO/memory_linux.go +++ b/procio/memory_linux.go @@ -1,6 +1,10 @@ -package procIO +package procio + +//#include +import "C" import ( + "fmt" "strconv" "strings" @@ -30,11 +34,11 @@ func memorySegmentFromLine(line string) (*MemorySegmentInfo, error) { } addrStart, err := strconv.ParseUint(addrS[0], 16, 64) if err != nil { - return nil, errors.Errorf("addr is not of format \"-\", %w", err) + return nil, fmt.Errorf("addr is not of format \"-\", %w", err) } addrEnd, err := strconv.ParseUint(addrS[1], 16, 64) if err != nil { - return nil, errors.Errorf("addr is not of format \"-\", %w", err) + return nil, fmt.Errorf("addr is not of format \"-\", %w", err) } ret.BaseAddress = uintptr(addrStart) ret.ParentBaseAddress = uintptr(addrStart) @@ -68,3 +72,25 @@ func memorySegmentFromLine(line string) (*MemorySegmentInfo, error) { return ret, nil } + +// PermissionsToNative converts the given Permissions to the +// native linux representation. +func PermissionsToNative(perms Permissions) int { + switch perms.String() { + case "R--": + return C.PROT_READ + case "RW-": + return C.PROT_READ | C.PROT_WRITE + case "RC-": + // Isn't actually COW, but RW is close enough + return C.PROT_READ | C.PROT_WRITE + case "--X": + return C.PROT_EXEC + case "RWX": + return C.PROT_READ | C.PROT_WRITE | C.PROT_EXEC + case "RCX": + return C.PROT_READ | C.PROT_WRITE | C.PROT_EXEC + default: + return C.PROT_NONE + } +} diff --git a/procIO/memory_test.go b/procio/memory_test.go similarity index 98% rename from procIO/memory_test.go rename to procio/memory_test.go index ca4eded..0508fe9 100644 --- a/procIO/memory_test.go +++ b/procio/memory_test.go @@ -1,4 +1,4 @@ -package procIO +package procio import ( "fmt" diff --git a/procIO/memory_windows.go b/procio/memory_windows.go similarity index 74% rename from procIO/memory_windows.go rename to procio/memory_windows.go index a94bd93..1bfca1e 100644 --- a/procIO/memory_windows.go +++ b/procio/memory_windows.go @@ -1,10 +1,12 @@ -package procIO +package procio import ( "github.com/0xrawsec/golang-win32/win32" "github.com/0xrawsec/golang-win32/win32/kernel32" ) +// SegmentFromMemoryBasicInformation converts the winapi win32.MemoryBasicInformation +// into a *MemorySegmentInfo. func SegmentFromMemoryBasicInformation(info win32.MemoryBasicInformation) *MemorySegmentInfo { return &MemorySegmentInfo{ ParentBaseAddress: uintptr(info.AllocationBase), @@ -18,6 +20,8 @@ func SegmentFromMemoryBasicInformation(info win32.MemoryBasicInformation) *Memor } } +// LookupFilePathOfSegment attempts to lookup the module filename associated +// with the given *MemorySegmentInfo. func LookupFilePathOfSegment(procHandle win32.HANDLE, seg *MemorySegmentInfo) (string, error) { if seg.BaseAddress != seg.ParentBaseAddress { // Only check root segments @@ -29,6 +33,27 @@ func LookupFilePathOfSegment(procHandle win32.HANDLE, seg *MemorySegmentInfo) (s return "", nil } +// PermissionsToNative converts the given Permissions to the +// native windows representation. +func PermissionsToNative(perms Permissions) win32.DWORD { + switch perms.String() { + case "R--": + return win32.PAGE_READONLY + case "RW-": + return win32.PAGE_READWRITE + case "RC-": + return win32.PAGE_WRITECOPY + case "--X": + return win32.PAGE_EXECUTE + case "RWX": + return win32.PAGE_EXECUTE_READWRITE + case "RCX": + return win32.PAGE_EXECUTE_WRITECOPY + default: + return win32.PAGE_NOACCESS + } +} + func permissionsFromProtectDWORD(protect win32.DWORD) Permissions { mp := Permissions{ Read: false, diff --git a/procIO/process.go b/procio/process.go similarity index 74% rename from procIO/process.go rename to procio/process.go index 4b4fd13..874f9f8 100644 --- a/procIO/process.go +++ b/procio/process.go @@ -1,4 +1,4 @@ -package procIO +package procio import ( "crypto/md5" @@ -12,9 +12,15 @@ import ( "github.com/fkie-cad/yapscan/arch" ) +// ErrProcIsSelf is returned when trying to suspend the current process. var ErrProcIsSelf = errors.New("not supported on self") + +// ErrProcIsParent is returned when trying to suspend the immediate parent process. +// Reason for this is the assumption that the parent process always is some form of +// console, which needs to be running in order to handle IO. var ErrProcIsParent = errors.New("not supported on parent") +// ProcessInfo represents information about a Process. type ProcessInfo struct { PID int `json:"pid"` Bitness arch.Bitness `json:"bitness"` @@ -25,6 +31,8 @@ type ProcessInfo struct { MemorySegments []*MemorySegmentInfo `json:"memorySegments"` } +// Process provides capability to interact with or retrieve information about +// other processes. type Process interface { io.Closer fmt.Stringer @@ -35,13 +43,18 @@ type Process interface { MemorySegments() ([]*MemorySegmentInfo, error) Suspend() error Resume() error + Crash(CrashMethod) error } +// CachingProcess is a Process that caches *ProcessInfo and +// *MemorySegmentInfo. +// This cache will only be updated after InvalidateCache was called. type CachingProcess interface { Process InvalidateCache() } +// OpenProcess opens another process. func OpenProcess(pid int) (CachingProcess, error) { proc, err := open(pid) return &cachingProcess{ @@ -100,6 +113,7 @@ func (c *cachingProcess) InvalidateCache() { c.infoCache = nil } +// ComputeHashes computes the md5 and sha256 hashes of a given file. func ComputeHashes(file string) (md5sum, sha256sum string, err error) { var f *os.File f, err = os.OpenFile(file, os.O_RDONLY, 0666) @@ -122,3 +136,7 @@ func ComputeHashes(file string) (md5sum, sha256sum string, err error) { return } + +func (c *cachingProcess) Crash(m CrashMethod) error { + return c.proc.Crash(m) +} diff --git a/procIO/process_linux.go b/procio/process_linux.go similarity index 90% rename from procIO/process_linux.go rename to procio/process_linux.go index c3b6a7d..62591f3 100644 --- a/procIO/process_linux.go +++ b/procio/process_linux.go @@ -1,4 +1,4 @@ -package procIO +package procio import ( "bufio" @@ -22,6 +22,7 @@ type processLinux struct { paused bool } +// GetRunningPIDs returns the PIDs of all running processes. func GetRunningPIDs() ([]int, error) { maps, _ := filepath.Glob("/proc/*/maps") @@ -124,10 +125,9 @@ func (p *processLinux) Suspend() error { if err != nil { var exitErr *exec.ExitError if errors.As(err, exitErr) { - return errors.Errorf("could not suspend process, reason: %w", errors.New(string(exitErr.Stderr))) - } else { - return errors.Errorf("could not suspend process, reason: %w", err) + return fmt.Errorf("could not suspend process, reason: %w", errors.New(string(exitErr.Stderr))) } + return fmt.Errorf("could not suspend process, reason: %w", err) } p.paused = true return nil @@ -138,7 +138,7 @@ func (p *processLinux) Resume() error { cmd := exec.Command("kill", "-CONT", strconv.Itoa(p.pid)) err := cmd.Run() if err != nil { - return errors.Errorf("could not resume process, reason: %w", err) + return fmt.Errorf("could not resume process, reason: %w", err) } } return nil @@ -177,3 +177,7 @@ func (p *processLinux) MemorySegments() ([]*MemorySegmentInfo, error) { return segments, nil } + +func (p *processLinux) Crash(m CrashMethod) error { + return &arch.ErrNotImplemented{"crashing processes is not implemented on linux"} +} diff --git a/procIO/process_test.go b/procio/process_test.go similarity index 85% rename from procIO/process_test.go rename to procio/process_test.go index bb6779a..1bf59f2 100644 --- a/procIO/process_test.go +++ b/procio/process_test.go @@ -1,4 +1,4 @@ -package procIO +package procio import ( "context" @@ -6,10 +6,13 @@ import ( "os/exec" "runtime" "testing" + "time" . "github.com/smartystreets/goconvey/convey" ) +const cmdPathTimeout = 1 * time.Second + func TestPIDInEnumeration(t *testing.T) { var testExe string if runtime.GOOS == "windows" { @@ -56,10 +59,13 @@ func TestPIDInEnumeration(t *testing.T) { func TestProcessInformation(t *testing.T) { var testExe string + var testArgs []string if runtime.GOOS == "windows" { testExe = "cmd.exe" + testArgs = []string{"/C", "timeout 6000"} } else { testExe = "bash" + testArgs = []string{"-c", "sleep 6000"} } path, err := exec.LookPath(testExe) @@ -70,10 +76,10 @@ func TestProcessInformation(t *testing.T) { panic(err) } Convey("With a started process", t, func() { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithTimeout(context.Background(), cmdPathTimeout) defer cancel() - cmd := exec.CommandContext(ctx, path) + cmd := exec.CommandContext(ctx, path, testArgs...) cmdIn, err := cmd.StdinPipe() if err != nil { panic(err) @@ -88,6 +94,9 @@ func TestProcessInformation(t *testing.T) { Convey("opening the process should not error.", func() { So(err, ShouldBeNil) }) + if err != nil { + return + } defer proc.Close() Convey("the PID should match.", func() { @@ -100,6 +109,9 @@ func TestProcessInformation(t *testing.T) { Convey("should not error.", func() { So(err, ShouldBeNil) }) + if err != nil { + return + } Convey("should return the correct PID.", func() { So(info.PID, ShouldEqual, cmd.Process.Pid) @@ -109,7 +121,5 @@ func TestProcessInformation(t *testing.T) { So(info.ExecutablePath, ShouldEqual, path) }) }) - - fmt.Fprintln(cmdIn, "exit") }) } diff --git a/procIO/process_windows.go b/procio/process_windows.go similarity index 75% rename from procIO/process_windows.go rename to procio/process_windows.go index c727a9b..84ca551 100644 --- a/procIO/process_windows.go +++ b/procio/process_windows.go @@ -1,11 +1,12 @@ -package procIO +package procio import ( + "fmt" "os" "syscall" "github.com/fkie-cad/yapscan/arch" - "github.com/fkie-cad/yapscan/procIO/customWin32" + "github.com/fkie-cad/yapscan/procio/customWin32" "golang.org/x/sys/windows" @@ -36,6 +37,7 @@ var specialPIDs = map[int]*ProcessInfo{ }, } +// GetRunningPIDs returns the PIDs of all running processes. func GetRunningPIDs() ([]int, error) { snap, err := kernel32.CreateToolhelp32Snapshot(kernel32.TH32CS_SNAPPROCESS, 0) if err != nil { @@ -76,9 +78,10 @@ func open(pid int) (Process, error) { // We'll create special processes without handle, so the info can at least be retreived return &processWindows{pid: pid, procHandle: 0}, nil } - handle, err := kernel32.OpenProcess( - kernel32.PROCESS_VM_READ|kernel32.PROCESS_QUERY_INFORMATION|kernel32.PROCESS_SUSPEND_RESUME, + kernel32.PROCESS_VM_READ|kernel32.PROCESS_QUERY_INFORMATION|kernel32.PROCESS_SUSPEND_RESUME| + // Specifically needed for CreateRemoteThread: + kernel32.PROCESS_CREATE_THREAD|kernel32.PROCESS_VM_OPERATION|kernel32.PROCESS_VM_WRITE, win32.FALSE, win32.DWORD(pid), ) @@ -106,13 +109,13 @@ func (p *processWindows) Info() (*ProcessInfo, error) { info.MemorySegments, tmpErr = p.MemorySegments() if tmpErr != nil { - err = errors.NewMultiError(err, errors.Errorf("could not retrieve memory segments info, reason: %w", tmpErr)) + err = errors.NewMultiError(err, fmt.Errorf("could not retrieve memory segments info, reason: %w", tmpErr)) } var isWow64 bool err = windows.IsWow64Process(windows.Handle(p.procHandle), &isWow64) if tmpErr != nil { - err = errors.NewMultiError(err, errors.Errorf("could not determine process bitness, reason: %w", tmpErr)) + err = errors.NewMultiError(err, fmt.Errorf("could not determine process bitness, reason: %w", tmpErr)) } // Note: This is good for windows on x86 and x86_64. // Docs: https://docs.microsoft.com/en-us/windows/win32/api/wow64apiset/nf-wow64apiset-iswow64process?redirectedfrom=MSDN @@ -124,31 +127,31 @@ func (p *processWindows) Info() (*ProcessInfo, error) { info.ExecutablePath, tmpErr = kernel32.GetModuleFilenameExW(p.procHandle, 0) if tmpErr != nil { - err = errors.NewMultiError(err, errors.Errorf("could not retrieve executable path, reason: %w", tmpErr)) + err = errors.NewMultiError(err, fmt.Errorf("could not retrieve executable path, reason: %w", tmpErr)) } else { info.ExecutableMD5, info.ExecutableSHA256, tmpErr = ComputeHashes(info.ExecutablePath) if tmpErr != nil { - err = errors.NewMultiError(err, errors.Errorf("could not compute hashes of executable, reason: %w", tmpErr)) + err = errors.NewMultiError(err, fmt.Errorf("could not compute hashes of executable, reason: %w", tmpErr)) } } tokenHandle, tmpErr := customWin32.OpenProcessToken(syscall.Handle(p.procHandle), syscall.TOKEN_QUERY) if tmpErr != nil { - err = errors.NewMultiError(err, errors.Errorf("could not retrieve process token, reason: %w", tmpErr)) + err = errors.NewMultiError(err, fmt.Errorf("could not retrieve process token, reason: %w", tmpErr)) } else { sid, tmpErr := customWin32.GetTokenOwner(tokenHandle) if tmpErr != nil { - err = errors.NewMultiError(err, errors.Errorf("could not get process token owner, reason: %w", tmpErr)) + err = errors.NewMultiError(err, fmt.Errorf("could not get process token owner, reason: %w", tmpErr)) } else { accout, domain, _, tmpErr := sid.LookupAccount("") if tmpErr == nil { info.Username = domain + "\\" + accout } else { - err = errors.NewMultiError(err, errors.Errorf("could not lookup username from SID, reason: %w", tmpErr)) + err = errors.NewMultiError(err, fmt.Errorf("could not lookup username from SID, reason: %w", tmpErr)) info.Username, tmpErr = customWin32.ConvertSidToStringSid(sid) if tmpErr != nil { - err = errors.NewMultiError(err, errors.Errorf("could not convert SID to string, reason: %w", tmpErr)) + err = errors.NewMultiError(err, fmt.Errorf("could not convert SID to string, reason: %w", tmpErr)) } } } @@ -252,3 +255,17 @@ func (p *processWindows) MemorySegments() ([]*MemorySegmentInfo, error) { return segmentsSlice, err } + +func (p *processWindows) Crash(m CrashMethod) error { + if m == CrashMethodCreateThreadOnNull { + err := customWin32.CreateRemoteThreadMinimal(p.procHandle, 0) + if err != nil { + if err.(syscall.Errno) == customWin32.ERROR_NOT_ENOUGH_MEMORY { + return fmt.Errorf("could not crash process, \"%w\", this may be due to service/non-service mode, note that only services can inject into services and services cannot inject into non-service processes", err) + } + return fmt.Errorf("could not crash process, %w", err) + } + return nil + } + return &arch.ErrNotImplemented{fmt.Sprintf("crash method \"%s\" is not implemented on windows", m.String())} +} diff --git a/procIO/reader.go b/procio/reader.go similarity index 63% rename from procIO/reader.go rename to procio/reader.go index 3dca6b8..9551b94 100644 --- a/procIO/reader.go +++ b/procio/reader.go @@ -1,26 +1,32 @@ -package procIO +package procio import ( "io" ) +// MemoryReader provides capabilities to read and seek through +// another processes memory. type MemoryReader interface { io.ReadCloser - Reset() (MemoryReader, error) + io.Seeker } +// MemoryReaderFactory is a factory for MemoryReader. type MemoryReaderFactory interface { NewMemoryReader(proc Process, seg *MemorySegmentInfo) (MemoryReader, error) } +// DefaultMemoryReaderFactory is the default MemoryReaderFactory. type DefaultMemoryReaderFactory struct{} +// NewMemoryReader calls NewMemoryReader. func (f *DefaultMemoryReaderFactory) NewMemoryReader(proc Process, seg *MemorySegmentInfo) (MemoryReader, error) { return NewMemoryReader(proc, seg) } type memoryReaderImpl interface { io.ReadCloser + io.Seeker Process() Process Segment() *MemorySegmentInfo } @@ -29,6 +35,8 @@ type memoryReader struct { impl memoryReaderImpl } +// NewMemoryReader creates a new MemoryReader to read the given +// segment of the given Process. func NewMemoryReader(proc Process, seg *MemorySegmentInfo) (MemoryReader, error) { impl, err := newMemoryReader(proc, seg) return &memoryReader{ @@ -44,11 +52,6 @@ func (rdr *memoryReader) Close() error { return rdr.impl.Close() } -func (rdr *memoryReader) Reset() (MemoryReader, error) { - seeker, ok := rdr.impl.(io.Seeker) - if ok { - _, err := seeker.Seek(0, io.SeekStart) - return rdr, err - } - return NewMemoryReader(rdr.impl.Process(), rdr.impl.Segment()) +func (rdr *memoryReader) Seek(offset int64, whence int) (int64, error) { + return rdr.impl.Seek(offset, whence) } diff --git a/procIO/reader_linux.go b/procio/reader_linux.go similarity index 56% rename from procIO/reader_linux.go rename to procio/reader_linux.go index 611b853..e0aed06 100644 --- a/procIO/reader_linux.go +++ b/procio/reader_linux.go @@ -1,4 +1,4 @@ -package procIO +package procio import ( "fmt" @@ -20,12 +20,7 @@ type memfileReader struct { func newMemoryReader(proc Process, seg *MemorySegmentInfo) (memoryReaderImpl, error) { memfile, err := os.OpenFile(fmt.Sprintf("/proc/%d/mem", proc.PID()), os.O_RDONLY, 0600) if err != nil { - return nil, errors.Errorf("could not open process memory for reading, reason: %w", err) - } - - _, err = memfile.Seek(int64(seg.BaseAddress), io.SeekStart) - if err != nil { - return nil, errors.Errorf("could not access process memory at address 0x%016X, reason: %w", seg.BaseAddress, err) + return nil, fmt.Errorf("could not open process memory for reading, reason: %w", err) } rdr := &memfileReader{ @@ -37,9 +32,20 @@ func newMemoryReader(proc Process, seg *MemorySegmentInfo) (memoryReaderImpl, er position: 0, } + rdr.seekToPosition() + return rdr, nil } +func (rdr *memfileReader) seekToPosition() error { + filePos := rdr.seg.BaseAddress + rdr.position + _, err := rdr.memfile.Seek(int64(filePos), io.SeekStart) + if err != nil { + return fmt.Errorf("could not access process memory at address 0x%016X, reason: %w", filePos, err) + } + return nil +} + func (rdr *memfileReader) Read(data []byte) (int, error) { if rdr.position >= rdr.seg.Size { return 0, io.EOF @@ -55,6 +61,24 @@ func (rdr *memfileReader) Read(data []byte) (int, error) { return n, err } +func (rdr *memfileReader) Seek(offset int64, whence int) (pos int64, err error) { + switch whence { + case io.SeekStart: + pos = offset + case io.SeekCurrent: + pos = int64(rdr.position) + offset + case io.SeekEnd: + pos = int64(rdr.seg.Size) + offset + } + if pos < 0 { + pos = 0 + err = errors.New("cannot seek before start of segment") + } + rdr.position = uintptr(pos) + err = rdr.seekToPosition() + return +} + func (rdr *memfileReader) Process() Process { return rdr.proc } diff --git a/procio/reader_test.go b/procio/reader_test.go new file mode 100644 index 0000000..471c86e --- /dev/null +++ b/procio/reader_test.go @@ -0,0 +1,216 @@ +package procio + +import ( + "bytes" + "context" + "io" + "io/ioutil" + "testing" + "time" + + "github.com/fkie-cad/yapscan/testutil/memory" + + . "github.com/smartystreets/goconvey/convey" +) + +const testCompilerTimeout = 30 * time.Second +const testerTimeout = 1 * time.Second + +func testWithData(c C, tc *memory.TesterCompiler, data []byte) { + ctx, cancel := context.WithTimeout(context.Background(), testerTimeout) + defer cancel() + + tester, err := memory.NewTester(ctx, tc, data, uintptr(PermissionsToNative(Permissions{Read: true}))) + c.Convey("process creation should not fail.", func() { + So(err, ShouldBeNil) + }) + if err != nil { + return + } + defer tester.Close() + + address, err := tester.WriteDataAndGetAddress() + c.Convey("writing the data via pipes should not fail.", func() { + So(err, ShouldBeNil) + So(address, ShouldNotEqual, 0) + }) + if err != nil { + return + } + + proc, err := OpenProcess(tester.PID()) + c.Convey("opening the created process should not fail.", func() { + So(err, ShouldBeNil) + }) + if err != nil { + return + } + + segments, err := proc.MemorySegments() + c.Convey("reading the remote segments should work.", func() { + So(err, ShouldBeNil) + }) + if err != nil { + return + } + + var seg *MemorySegmentInfo + for _, segment := range segments { + if segment.SubSegments == nil || len(segment.SubSegments) == 0 { + if segment.BaseAddress <= address && address < segment.BaseAddress+segment.Size { + // Found segment containing our target + seg = segment + break + } + } else { + for _, subseg := range segment.SubSegments { + if subseg.BaseAddress <= address && address < subseg.BaseAddress+subseg.Size { + // Found segment containing our target + seg = subseg + break + } + } + if seg != nil { + break + } + } + } + + c.Convey("there should be a segment containing our target.", func() { + So(seg, ShouldNotBeNil) + }) + if seg == nil { + return + } + + testFullRead(c, proc, seg, address, data) +} + +func testFullRead(c C, proc Process, seg *MemorySegmentInfo, address uintptr, expectedData []byte) { + rdr, err := NewMemoryReader(proc, seg) + c.Convey("creating a reader should not fail.", func() { + So(err, ShouldBeNil) + }) + defer rdr.Close() + + readData, err := ioutil.ReadAll(rdr) + c.Convey("reading the remote segment should not fail.", func() { + So(err, ShouldBeNil) + }) + + offset := address - seg.BaseAddress + c.Convey("the data should be correct.", func() { + So(len(readData), ShouldBeGreaterThan, offset) + readData = readData[offset:] + So(len(readData), ShouldBeGreaterThanOrEqualTo, len(expectedData)) + So(readData[:len(expectedData)], ShouldResemble, expectedData) + }) + + _, err = rdr.Seek(0, io.SeekStart) + c.Convey("resetting the reader should not fail", func() { + So(err, ShouldBeNil) + }) + + readData, err = ioutil.ReadAll(rdr) + c.Convey("reading the remote segment again, should not fail.", func() { + So(err, ShouldBeNil) + }) + + offset = address - seg.BaseAddress + c.Convey("the data should still be correct.", func() { + So(len(readData), ShouldBeGreaterThan, offset) + readData = readData[offset:] + So(len(readData), ShouldBeGreaterThanOrEqualTo, len(expectedData)) + So(readData[:len(expectedData)], ShouldResemble, expectedData) + }) +} + +func testPartialRead(c C, proc Process, seg *MemorySegmentInfo, address uintptr, expectedData []byte) { + rdr, err := NewMemoryReader(proc, seg) + c.Convey("creating a reader should not fail.", func() { + So(err, ShouldBeNil) + }) + defer rdr.Close() + + start := 2 + oldPos, err := rdr.Seek(int64(start), io.SeekCurrent) + c.Convey("seeking relative should not error", func() { + So(oldPos, ShouldEqual, 0) + So(err, ShouldBeNil) + }) + + readData, err := ioutil.ReadAll(io.LimitReader(rdr, int64(len(expectedData)-start))) + c.Convey("reading the remote segment should not fail.", func() { + So(err, ShouldBeNil) + }) + + expectedData = expectedData[start:] + + offset := address - seg.BaseAddress + c.Convey("the data should be correct.", func() { + So(len(readData), ShouldBeGreaterThan, offset) + readData = readData[offset:] + So(len(readData), ShouldBeGreaterThanOrEqualTo, len(expectedData)) + So(readData[:len(expectedData)], ShouldResemble, expectedData) + }) +} + +func TestReader(t *testing.T) { + if testing.Short() { + t.Skip("skipping memory reader test in short mode") + } + + tc, err := memory.NewTesterCompiler() + if err != nil { + t.Skip("could not build memory test utility, skipping", err) + } + compileCtx, cancel := context.WithTimeout(context.Background(), testCompilerTimeout) + err = tc.Compile(compileCtx) + cancel() + if err != nil { + t.Skip("could not build memory test utility, skipping", err) + } + defer tc.Close() + + Convey("Reading text from a remote process", t, func(c C) { + data := []byte("this data should be copied into a new process and we should then be able to read it from the remote process") + testWithData(c, tc, data) + }) + Convey("Reading 12 Zeros from a remote process", t, func(c C) { + data := bytes.Repeat([]byte{0}, 12) + testWithData(c, tc, data) + }) + Convey("Reading 4094 Zeros from a remote process", t, func(c C) { + data := bytes.Repeat([]byte{0}, 4094) + testWithData(c, tc, data) + }) + Convey("Reading 4095 Zeros from a remote process", t, func(c C) { + data := bytes.Repeat([]byte{0}, 4095) + testWithData(c, tc, data) + }) + Convey("Reading 4096 Zeros from a remote process", t, func(c C) { + data := bytes.Repeat([]byte{0}, 4096) + testWithData(c, tc, data) + }) + Convey("Reading 1048575 (1MiB-1) Zeros from a remote process", t, func(c C) { + data := bytes.Repeat([]byte{0}, 1048575) + testWithData(c, tc, data) + }) + Convey("Reading 1048576 (1MiB) Zeros from a remote process", t, func(c C) { + data := bytes.Repeat([]byte{0}, 1048576) + testWithData(c, tc, data) + }) + Convey("Reading 1048577 (1MiB+1) Zeros from a remote process", t, func(c C) { + data := bytes.Repeat([]byte{0}, 1048577) + testWithData(c, tc, data) + }) + Convey("Reading 4096 255's from a remote process", t, func(c C) { + data := bytes.Repeat([]byte{255}, 4096) + testWithData(c, tc, data) + }) + Convey("Reading a pattern from a remote process", t, func(c C) { + pattern := []byte{0, 1, 10, 20, 35, 60, 98, 125, 128, 190, 200, 230, 254, 255} + data := bytes.Repeat(pattern, 4096/len(pattern)) + testWithData(c, tc, data) + }) +} diff --git a/procIO/reader_windows.go b/procio/reader_windows.go similarity index 96% rename from procIO/reader_windows.go rename to procio/reader_windows.go index 98cc6aa..8a5b73c 100644 --- a/procIO/reader_windows.go +++ b/procio/reader_windows.go @@ -1,4 +1,4 @@ -package procIO +package procio import ( "errors" @@ -7,7 +7,7 @@ import ( "github.com/sirupsen/logrus" - "github.com/fkie-cad/yapscan/procIO/customWin32" + "github.com/fkie-cad/yapscan/procio/customWin32" "github.com/0xrawsec/golang-win32/win32" ) diff --git a/service/error.go b/service/error.go new file mode 100644 index 0000000..bf656fd --- /dev/null +++ b/service/error.go @@ -0,0 +1,22 @@ +package service + +import ( + "fmt" +) + +type NotInServiceModeError struct { + Underlying error +} + +func (e *NotInServiceModeError) Error() string { + return fmt.Sprintf("not running in service mode, %v", e.Underlying) +} + +func (e *NotInServiceModeError) Unwrap() error { + return e.Underlying +} + +func IsNotInServiceModeError(err error) bool { + _, ok := err.(*NotInServiceModeError) + return ok +} diff --git a/service/service.go b/service/service.go new file mode 100644 index 0000000..abe3b83 --- /dev/null +++ b/service/service.go @@ -0,0 +1,16 @@ +package service + +import "fmt" + +type MainFunction func(args []string) error + +var registeredMainFunction MainFunction = nil + +func Initialize(mainFunc MainFunction) error { + if registeredMainFunction != nil { + return fmt.Errorf("can only initialize one service") + } + registeredMainFunction = mainFunc + + return initializeNative() +} diff --git a/service/service_linux.go b/service/service_linux.go new file mode 100644 index 0000000..8ae476e --- /dev/null +++ b/service/service_linux.go @@ -0,0 +1,7 @@ +package service + +import "fmt" + +func initializeNative() error { + return &NotInServiceModeError{Underlying: fmt.Errorf("not implemented")} +} diff --git a/service/service_windows.go b/service/service_windows.go new file mode 100644 index 0000000..b20af2f --- /dev/null +++ b/service/service_windows.go @@ -0,0 +1,124 @@ +package service + +import ( + "os" + "syscall" + + "github.com/fkie-cad/yapscan/app" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +// Built using this example: https://docs.microsoft.com/en-us/windows/win32/services/writing-a-servicemain-function + +// #include +// +// static char* SERVICE_NAME = "yapscan"; +// static SERVICE_STATUS gSvcStatus = {0}; +// static SERVICE_STATUS_HANDLE gSvcStatusHandle = NULL; +// static HANDLE ghSvcStopEvent = NULL; +// +// extern void __declspec(dllexport) SvcMain(DWORD dwNumServicesArgs, LPSTR *lpServiceArgVectors); +// extern void __declspec(dllexport) SvcCtrlHandler(DWORD dwCtrl); +// +// static int startServiceDispatcher() { +// SERVICE_TABLE_ENTRY serviceTable[] = { +// {SERVICE_NAME, (LPSERVICE_MAIN_FUNCTION)SvcMain}, +// {NULL, NULL} +// }; +// if(StartServiceCtrlDispatcher(serviceTable) == FALSE) { +// return 1; +// } +// return 0; +// } +// +// static const char* arg_index(const char** argv, int i) { +// return argv[i]; +// } +// +// static void ReportSvcStatus(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint) { +// static DWORD dwCheckPoint = 1; +// +// // Fill in the SERVICE_STATUS structure. +// +// gSvcStatus.dwCurrentState = dwCurrentState; +// gSvcStatus.dwWin32ExitCode = dwWin32ExitCode; +// gSvcStatus.dwWaitHint = dwWaitHint; +// +// if(dwCurrentState == SERVICE_START_PENDING) +// gSvcStatus.dwControlsAccepted = 0; +// else gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP; +// +// if ((dwCurrentState == SERVICE_RUNNING) || (dwCurrentState == SERVICE_STOPPED)) +// gSvcStatus.dwCheckPoint = 0; +// else gSvcStatus.dwCheckPoint = dwCheckPoint++; +// +// // Report the status of the service to the SCM. +// SetServiceStatus( gSvcStatusHandle, &gSvcStatus ); +// } +// +// static int init_status() { +// gSvcStatusHandle = RegisterServiceCtrlHandler(SERVICE_NAME, SvcCtrlHandler); +// if(!gSvcStatusHandle) { +// return 1; +// } +// +// gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; +// gSvcStatus.dwServiceSpecificExitCode = 0; +// ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000); +// +// ghSvcStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); +// if(ghSvcStopEvent == NULL) { +// ReportSvcStatus(SERVICE_STOPPED, GetLastError(), 0); +// return 2; +// } +// +// return 0; +// } +// +// static void report_running() { +// ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0); +// } +// static void report_stopped() { +// ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0); +// } +import "C" + +//export SvcCtrlHandler +func SvcCtrlHandler(dwCtrl C.DWORD) { + // We could handle commands from the service manager, but let's just not... +} + +//export SvcMain +func SvcMain(dwNumServicesArgs C.DWORD, lpServiceArgVectors **C.char) { + if C.init_status() != C.int(0) { + return + } + + args := make([]string, dwNumServicesArgs) + for i := range args { + args[i] = C.GoString(C.arg_index(lpServiceArgVectors, C.int(i))) + } + os.Args = args + + C.report_running() + + exiter := func(code int) { + C.report_stopped() + } + defer exiter(0) + + cli.OsExiter = exiter + logrus.RegisterExitHandler(func() { + exiter(-1) + }) + + app.RunApp(args) +} + +func initializeNative() error { + if C.startServiceDispatcher() != C.int(0) { + return &NotInServiceModeError{Underlying: syscall.GetLastError()} + } + return nil +} diff --git a/system/info.go b/system/info.go index 617460c..ee44a81 100644 --- a/system/info.go +++ b/system/info.go @@ -1,14 +1,14 @@ package system import ( + "fmt" "net" "os" "github.com/fkie-cad/yapscan/arch" - - "github.com/targodan/go-errors" ) +// Info contains information about the running system. type Info struct { OSName string `json:"osName"` OSVersion string `json:"osVersion"` @@ -20,6 +20,7 @@ type Info struct { var info *Info +// GetInfo retrieves the Info about the currently running system. func GetInfo() (*Info, error) { if info == nil { var err error @@ -28,17 +29,17 @@ func GetInfo() (*Info, error) { info.OSArch = arch.Native() info.OSName, info.OSVersion, info.OSFlavour, err = getOSInfo() if err != nil { - err = errors.Errorf("could not determine OS info, reason: %w", err) + err = fmt.Errorf("could not determine OS info, reason: %w", err) return info, err } info.Hostname, err = os.Hostname() if err != nil { - err = errors.Errorf("could not determine hostname, reason: %w", err) + err = fmt.Errorf("could not determine hostname, reason: %w", err) return info, err } addrs, err := net.InterfaceAddrs() if err != nil { - err = errors.Errorf("could not determine IPs, reason: %w", err) + err = fmt.Errorf("could not determine IPs, reason: %w", err) return info, err } info.IPs = make([]string, len(addrs)) diff --git a/system/info_windows.go b/system/info_windows.go index e201a34..bc04196 100644 --- a/system/info_windows.go +++ b/system/info_windows.go @@ -2,29 +2,28 @@ package system import ( "encoding/csv" + "fmt" "os/exec" "strings" - - "github.com/targodan/go-errors" ) func getOSInfo() (name, version, flavour string, err error) { cmd := exec.Command("systeminfo", "/FO", "CSV") buf, err := cmd.Output() if err != nil { - err = errors.Errorf("could not execute systeminfo, reason: %s", err) + err = fmt.Errorf("could not execute systeminfo, reason: %s", err) return } info := csv.NewReader(strings.NewReader(string(buf))) headings, err := info.Read() if err != nil { - err = errors.Errorf("could not parse systeminfo output, reason: %s", err) + err = fmt.Errorf("could not parse systeminfo output, reason: %s", err) return } data, err := info.Read() if err != nil { - err = errors.Errorf("could not parse systeminfo output, reason: %s", err) + err = fmt.Errorf("could not parse systeminfo output, reason: %s", err) return } @@ -43,7 +42,7 @@ func getOSInfo() (name, version, flavour string, err error) { parts := strings.Split(strings.TrimSpace(data[iOSName]), " ") if len(parts) < 3 { - err = errors.Errorf("invalid OS name \"%s\"", data[iOSName]) + err = fmt.Errorf("invalid OS name \"%s\"", data[iOSName]) return } // Examples: diff --git a/system/memory_linux.go b/system/memory_linux.go index 1ad6e94..4ec9203 100644 --- a/system/memory_linux.go +++ b/system/memory_linux.go @@ -6,7 +6,8 @@ import ( "github.com/targodan/go-errors" ) -func GetTotalRAM() (uintptr, error) { +// TotalRAM returns the total amount of installed RAM in bytes. +func TotalRAM() (uintptr, error) { si := &syscall.Sysinfo_t{} // XXX is a raw syscall thread safe? @@ -18,7 +19,8 @@ func GetTotalRAM() (uintptr, error) { return uintptr(si.Totalram), nil } -func GetFreeRAM() (uintptr, error) { +// FreeRAM returns the amount of free RAM available for allocation in bytes. +func FreeRAM() (uintptr, error) { si := &syscall.Sysinfo_t{} // XXX is a raw syscall thread safe? diff --git a/system/memory_windows.go b/system/memory_windows.go index b2ff96e..62eaa30 100644 --- a/system/memory_windows.go +++ b/system/memory_windows.go @@ -1,10 +1,11 @@ package system import ( - "github.com/fkie-cad/yapscan/procIO/customWin32" + "github.com/fkie-cad/yapscan/procio/customWin32" ) -func GetTotalRAM() (uintptr, error) { +// TotalRAM returns the total amount of installed RAM in bytes. +func TotalRAM() (uintptr, error) { status, err := customWin32.GlobalMemoryStatusEx() if err != nil { return 0, err @@ -12,7 +13,8 @@ func GetTotalRAM() (uintptr, error) { return uintptr(status.TotalPhys), nil } -func GetFreeRAM() (uintptr, error) { +// FreeRAM returns the amount of free RAM available for allocation in bytes. +func FreeRAM() (uintptr, error) { status, err := customWin32.GlobalMemoryStatusEx() if err != nil { return 0, err diff --git a/testdata/fileIO/filesystem/dir1/dir3/f3 b/testdata/fileio/filesystem/dir1/dir3/f3 similarity index 100% rename from testdata/fileIO/filesystem/dir1/dir3/f3 rename to testdata/fileio/filesystem/dir1/dir3/f3 diff --git a/testdata/fileIO/filesystem/dir2/f4 b/testdata/fileio/filesystem/dir2/f4 similarity index 100% rename from testdata/fileIO/filesystem/dir2/f4 rename to testdata/fileio/filesystem/dir2/f4 diff --git a/testdata/fileIO/filesystem/f1 b/testdata/fileio/filesystem/f1 similarity index 100% rename from testdata/fileIO/filesystem/f1 rename to testdata/fileio/filesystem/f1 diff --git a/testdata/fileIO/filesystem/f2 b/testdata/fileio/filesystem/f2 similarity index 100% rename from testdata/fileIO/filesystem/f2 rename to testdata/fileio/filesystem/f2 diff --git a/testutil/memory/api_linux.go b/testutil/memory/api_linux.go new file mode 100644 index 0000000..2dc2b4a --- /dev/null +++ b/testutil/memory/api_linux.go @@ -0,0 +1,60 @@ +package memory + +//#include +//#include +//#include +// +// int getErrno() { +// return errno; +// } +// +// void* negativeOne() { +// return ((void*)-1); +// } +import "C" +import ( + "unsafe" + + "github.com/targodan/go-errors" +) + +func getLastError() error { + text := C.GoString(C.strerror(C.getErrno())) + return errors.New(text) +} + +func ensureStdinBinary() {} + +func alloc(size uint64) (uintptr, error) { + addr := C.mmap( + unsafe.Pointer(uintptr(0)), + C.size_t(size), + C.PROT_READ|C.PROT_WRITE, + C.MAP_PRIVATE|C.MAP_ANONYMOUS|C.MAP_POPULATE, + -1, + 0) + if addr == C.negativeOne() { + return uintptr(addr), getLastError() + } + return uintptr(addr), nil +} + +func free(addr uintptr, size uint64) { + C.munmap(unsafe.Pointer(addr), C.size_t(size)) +} + +func memset(addr uintptr, value byte, count uint64) { + C.memset(unsafe.Pointer(addr), 0xAA, C.size_t(count)) +} + +func memcpy(addr uintptr, data []byte) { + C.memcpy(unsafe.Pointer(addr), unsafe.Pointer(&data[0]), C.size_t(len(data))) +} + +func protect(addr uintptr, size uint64, protect uint64) error { + ret := C.mprotect(unsafe.Pointer(addr), C.size_t(size), C.int(protect)) + if ret == -1 { + return getLastError() + } + return nil +} diff --git a/testutil/memory/api_windows.go b/testutil/memory/api_windows.go new file mode 100644 index 0000000..47990e9 --- /dev/null +++ b/testutil/memory/api_windows.go @@ -0,0 +1,39 @@ +package memory + +//#include +//#include +// +// void makeStdinBinary() { +// freopen(NULL, "rb", stdin); +// } +import "C" +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +func ensureStdinBinary() { + C.makeStdinBinary() +} + +func alloc(size uint64) (uintptr, error) { + return windows.VirtualAlloc(0, uintptr(size), windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_READWRITE) +} + +func free(addr uintptr, size uint64) { + windows.VirtualFree(addr, 0, windows.MEM_RELEASE) +} + +func memset(addr uintptr, value byte, count uint64) { + C.memset(unsafe.Pointer(addr), 0xAA, C.size_t(count)) +} + +func memcpy(addr uintptr, data []byte) { + C.memcpy(unsafe.Pointer(addr), unsafe.Pointer(&data[0]), C.size_t(len(data))) +} + +func protect(addr uintptr, size uint64, protect uint64) error { + var oldProtect uint32 + return windows.VirtualProtect(addr, uintptr(size), uint32(protect), &oldProtect) +} diff --git a/testutil/memory/main.go b/testutil/memory/main.go new file mode 100644 index 0000000..6a87f1b --- /dev/null +++ b/testutil/memory/main.go @@ -0,0 +1,103 @@ +package memory + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "math" + "os" + "strconv" +) + +func Main() { + if len(os.Args) < 3 { + fmt.Println(OutputErrorPrefix + "Invalid arguments") + fmt.Printf("Usage: %s [file]\n", os.Args[0]) + os.Exit(1) + } + + filename := "" + if len(os.Args) >= 4 { + filename = os.Args[3] + } + + ensureStdinBinary() + + size, err := strconv.ParseUint(os.Args[1], 10, 64) + if err != nil { + fmt.Printf(OutputErrorPrefix+"Invalid size value, %v\n", err) + os.Exit(1) + } + + prot, err := strconv.ParseUint(os.Args[2], 10, 32) + if err != nil { + fmt.Printf(OutputErrorPrefix+"Invalid protect value, %v\n", err) + os.Exit(1) + } + + addr, err := alloc(size) + if err != nil { + fmt.Printf(OutputErrorPrefix+"Could not alloc, reason: %v\n", err) + os.Exit(5) + } + defer func() { + free(addr, size) + }() + + // Round up to next 4096 + segmentSize := uint64(math.Ceil(float64(size)/4096.) * 4096.) + + memset(addr, 0xAA, size) + memset(addr+uintptr(size), 0xBB, segmentSize-size) + + fmt.Printf("Allocated: 0x%X\n", addr) + + data := bytes.Repeat([]byte{0xA1}, int(size)) + + if filename != "" { + f, err := os.Open(filename) + if err != nil { + fmt.Printf(OutputErrorPrefix+"Could not open file, reason: %v\n", err) + os.Exit(2) + } + data, err = ioutil.ReadAll(f) + if err != nil { + fmt.Printf(OutputErrorPrefix+"Could not read from file, reason: %v\n", err) + os.Exit(3) + } + f.Close() + + size = uint64(len(data)) + } else { + fmt.Println(OutputReady) + data, err = ioutil.ReadAll(io.LimitReader(os.Stdin, int64(size))) + if err != nil { + fmt.Printf(OutputErrorPrefix+"Could not read from stdin, reason: %v\n", err) + os.Exit(4) + } + if uint64(len(data)) != size { + fmt.Printf(OutputErrorPrefix+"Invalid number of bytes received! Expected %d, got %d!\n", size, len(data)) + os.Exit(4) + } + } + + memcpy(addr, data) + + protect(addr, size, prot) + if err != nil { + fmt.Printf(OutputErrorPrefix+"Failed to set protect, reason: %v\n", err) + os.Exit(2) + } + + fmt.Printf(OutputAddressPrefix+"%d\n", addr) + + if filename != "" { + fmt.Println("Press Enter to close application...") + // Wait for user enter + fmt.Scanln() + } else { + // Wait for stdin close + ioutil.ReadAll(os.Stdin) + } +} diff --git a/testutil/memory/tester.go b/testutil/memory/tester.go new file mode 100644 index 0000000..276eb15 --- /dev/null +++ b/testutil/memory/tester.go @@ -0,0 +1,231 @@ +package memory + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + + "github.com/targodan/go-errors" +) + +const ( + OutputErrorPrefix = "ERROR: " + OutputReady = "READY" + OutputAddressPrefix = "ADDRESS: " +) + +func getMemtestPath() (string, error) { + _, filename, _, ok := runtime.Caller(0) + if !ok { + return "", errors.New("could not determine caller") + } + + dir := filepath.Dir(filename) + path := filepath.Join(dir, "..", "..", "cmd", "memtest", "main.go") + + // See if it exists for early exit + _, err := os.Stat(path) + + return path, err +} + +type TesterCompiler struct { + srcPath string + binPath string + compiled bool +} + +func memtestUtilBinaryName() string { + name := "yapscan-memtest-util" + if runtime.GOOS == "windows" { + name += ".exe" + } + return name +} + +func NewTesterCompiler() (*TesterCompiler, error) { + srcPath, err := getMemtestPath() + if err != nil { + return nil, fmt.Errorf("could not determine path to main.go, reason: %w", err) + } + + binPath := filepath.Join(os.TempDir(), memtestUtilBinaryName()) + + return &TesterCompiler{ + srcPath: srcPath, + binPath: binPath, + compiled: false, + }, nil +} + +func (c *TesterCompiler) Compile(ctx context.Context) error { + cmd := exec.CommandContext(ctx, + "go", "build", "-o", c.binPath, c.srcPath) + output, err := cmd.Output() + if err != nil { + exitErr, ok := err.(*exec.ExitError) + if ok { + return fmt.Errorf("could not build test utility\n==== STDOUT ====\n%s\n==== STDERR ====\n%s", output, exitErr.Stderr) + } else { + return fmt.Errorf("could not build test utility, reason: %w", err) + } + } + + c.compiled = true + return nil +} + +func (c *TesterCompiler) BinaryPath() string { + if !c.compiled { + panic("binary path not available, compile first") + } + return c.binPath +} + +func (c *TesterCompiler) Close() error { + if c.compiled { + return os.Remove(c.binPath) + } + return nil +} + +type Tester struct { + ctx context.Context + + cmd *exec.Cmd + cmdCtx context.Context + cmdCancel context.CancelFunc + + cmdIn io.WriteCloser + cmdOut io.ReadCloser + + out *bufio.Reader + + data []byte +} + +func NewTester(ctx context.Context, c *TesterCompiler, data []byte, nativePermis uintptr) (*Tester, error) { + cmdCtx, cmdCancel := context.WithCancel(ctx) + + cmd := exec.CommandContext(ctx, + c.BinaryPath(), + fmt.Sprintf("%d", len(data)), fmt.Sprintf("%d", nativePermis)) + + cmdIn, err := cmd.StdinPipe() + if err != nil { + cmdCancel() + return nil, fmt.Errorf("could not create process pipe, reason: %w", err) + } + cmdOut, err := cmd.StdoutPipe() + if err != nil { + cmdIn.Close() + cmdCancel() + return nil, fmt.Errorf("could not create process pipe, reason: %w", err) + } + + t := &Tester{ + ctx: ctx, + + cmd: cmd, + cmdCtx: cmdCtx, + cmdCancel: cmdCancel, + + cmdIn: cmdIn, + cmdOut: cmdOut, + + out: bufio.NewReader(cmdOut), + + data: data, + } + + err = t.cmd.Start() + if err != nil { + t.Close() + return nil, fmt.Errorf("could not start memtester process, reason: %w", err) + } + + return t, nil +} + +func (t *Tester) Close() error { + t.cmdIn.Close() + t.cmdOut.Close() + + t.cmd.Wait() + t.cmdCancel() + + return nil +} + +func (t *Tester) PID() int { + return t.cmd.Process.Pid +} + +func (t *Tester) waitForPrefix(prefix string) (string, error) { + for { + select { + case <-t.ctx.Done(): + return "", t.ctx.Err() + default: + } + + line, err := t.out.ReadString('\n') + if err != nil { + return "", err + } + + line = strings.TrimSpace(line) + if strings.HasPrefix(line, prefix) { + // Found it + return line, nil + } + if strings.HasPrefix(line, OutputErrorPrefix) { + // Found an error + return "", errors.New(line[len(OutputErrorPrefix):]) + } + } +} + +func (t *Tester) writeData() error { + _, err := t.waitForPrefix(OutputReady) + if err != nil { + return fmt.Errorf("command did not become READY for data input, reason: %w", err) + } + + _, err = io.Copy(t.cmdIn, bytes.NewBuffer(t.data)) + if err != nil { + return fmt.Errorf("encountered error during writing data: %w", err) + } + + return nil +} + +func (t *Tester) getMemoryAddress() (uintptr, error) { + addressLine, err := t.waitForPrefix(OutputAddressPrefix) + if err != nil { + return 0, fmt.Errorf("command did not report memory address: %w", err) + } + + address, err := strconv.ParseUint(addressLine[len(OutputAddressPrefix):], 10, 64) + if err != nil { + return 0, fmt.Errorf("could not parse memory address: %w", err) + } + return uintptr(address), nil +} + +func (t *Tester) WriteDataAndGetAddress() (uintptr, error) { + err := t.writeData() + if err != nil { + return 0, err + } + + return t.getMemoryAddress() +} diff --git a/yara.go b/yara.go index 144a06c..6a0eaf5 100644 --- a/yara.go +++ b/yara.go @@ -91,9 +91,8 @@ func LoadYaraRules(path string, recurseIfDir bool) (*yara.Rules, error) { } if stat.IsDir() { return loadYaraRulesDirectory(path, recurseIfDir) - } else { - return loadYaraRulesSingleFile(path) } + return loadYaraRulesSingleFile(path) } // IsYaraRulesFile returns true, if the given filename has one of the extensions diff --git a/yara_test.go b/yara_test.go index bff1ef7..c656756 100644 --- a/yara_test.go +++ b/yara_test.go @@ -158,7 +158,7 @@ func TestLoadYaraRules(t *testing.T) { }) Convey("Loading multiple uncompiled yara rules", t, func() { - Convey("non-resursively", func() { + Convey("non-recursively", func() { rules, err := LoadYaraRules(testDataDir("rules_uncompiled"), false) So(rules, ShouldNotBeNil) So(err, ShouldBeNil) @@ -176,7 +176,7 @@ func TestLoadYaraRules(t *testing.T) { }) }) - Convey("resursively", func() { + Convey("recursively", func() { rules, err := LoadYaraRules(testDataDir("rules_uncompiled"), true) So(rules, ShouldNotBeNil) So(err, ShouldBeNil)