├── .circleci └── config.yml ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── LICENSE ├── Makefile ├── README.md ├── cmd └── main.go ├── collector.go ├── collector_test.go ├── configs └── config.yml ├── filefinder.go ├── filefinder_test.go ├── go.mod ├── go.sum ├── keywords.go ├── keywords_test.go ├── mft.go ├── mft_test.go ├── readers.go ├── readers_test.go ├── test └── testdata │ ├── dummyntfs │ ├── dummyntfs-badvbr1 │ └── dummyntfs-badvbr2 ├── volume.go ├── volume_test.go ├── writers.go └── writers_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Golang CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | version: 2.1 5 | orbs: 6 | windows: circleci/windows@2.1.0 7 | jobs: 8 | build: 9 | executor: 10 | name: windows/default 11 | shell: powershell.exe 12 | steps: 13 | - checkout 14 | - run: $ProgressPreference = "SilentlyContinue" 15 | - run: rm -r C:\Go 16 | - run: (New-Object System.Net.WebClient).DownloadFile("https://dl.google.com/go/go1.14.2.windows-amd64.zip", "go1.14.2.windows-amd64.zip") 17 | - run: Expand-Archive go1.14.2.windows-amd64.zip 18 | - run: mv .\go1.14.2.windows-amd64\go C:\ 19 | - run: go get -v -t -d ./... 20 | - run: go test -v . -race -coverprofile=C:\coverage.txt -covermode=atomic 21 | - run: (New-Object System.Net.WebClient).DownloadFile("https://github.com/codecov/codecov-exe/releases/download/1.10.0/codecov-windows-x64.exe", "codecov.exe") 22 | - run: ./codecov.exe -f C:\coverage.txt -t %CODECOV_TOKEN% 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: AlecRandazzo 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alec Randazzo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Go parameters 2 | GOCMD=go 3 | GOBUILD=$(GOCMD) build 4 | GOTEST=$(GOCMD) test 5 | BINARY_NAME=forensic-collector.exe 6 | 7 | default: build 8 | all: test build 9 | build: 10 | $(GOBUILD) -o $(BINARY_NAME) -v ./cmd/main.go 11 | test: 12 | $(GOTEST) -race -v . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/AlecRandazzo/Packrat.svg?style=svg)](https://circleci.com/gh/AlecRandazzo/Packrat) [![codecov](https://codecov.io/gh/AlecRandazzo/Packrat/branch/master/graph/badge.svg)](https://codecov.io/gh/AlecRandazzo/Packrat) [![Go Report Card](https://goreportcard.com/badge/github.com/AlecRandazzo/Packrat)](https://goreportcard.com/report/github.com/AlecRandazzo/Packrat) [![GoDoc](https://godoc.org/github.com/AlecRandazzo/Packrat/pkg/gofor?status.png)](https://godoc.org/github.com/AlecRandazzo/Packrat) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/AlecRandazzo/Packrat/issues) 2 | 3 | # Packrat 4 | Packrat is a forensic collector geared towards augmenting EDR toolsets. Unfortunately, not all EDR toolsets have the capability of collecting forensically relevant files from endpoints. The GoFor Collector looks to remedy that. 5 | 6 | ## Usage 7 | 8 | ```usage: forensic-collector.exe [] 9 | 10 | Flags: 11 | --help Show context-sensitive help (also try 12 | --help-long and --help-man). 13 | --debug Enable debug mode. 14 | --all Collect all forensic artifacts. 15 | --mft Collect the system drive MFT. 16 | --mft-all Collect all attached volume MFTs. 17 | --mft-letters=MFT-LETTERS ... Collect volume MFTs by volume letter. 18 | --reg Collect all registry hives, both system and 19 | user hives. 20 | --events Collect all event logs. 21 | --browser Collect browser history 22 | --custom-config=CUSTOM-CONFIG Custom configuration file that will overwrite 23 | built in config. 24 | --throttle This setting will limit the process to a single 25 | thread. This will reduce the CPU load. 26 | --output=OUTPUT Specify the name of the output file. If not 27 | specified, the file name defaults to the host 28 | name and a timestamp. 29 | ``` 30 | 31 | ### Examples 32 | 33 | Collect all the things: `forensic-collector.exe --all` 34 | 35 | Collect just the system drive MFT and export to a custom name zip file: `forensic-collector.exe --mft --output out.zip` 36 | 37 | Collect event logs and registry hives: `forensic-collector.exe --events --reg` 38 | 39 | Use a custom configuration for collection (see example config in `config/config.yml`): `forensic-collector.exe --custom-config config.yml` 40 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Alec Randazzo 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | */ 9 | 10 | package main 11 | 12 | import ( 13 | "archive/zip" 14 | "fmt" 15 | collector "github.com/AlecRandazzo/Packrat" 16 | "github.com/shirou/gopsutil/disk" 17 | log "github.com/sirupsen/logrus" 18 | "gopkg.in/alecthomas/kingpin.v2" 19 | "gopkg.in/yaml.v2" 20 | "io/ioutil" 21 | "os" 22 | "runtime" 23 | "time" 24 | ) 25 | 26 | var ( 27 | debug = kingpin.Flag("debug", "Enable debug mode.").Bool() 28 | all = kingpin.Flag("all", "Collect all forensic artifacts.").Bool() 29 | mft = kingpin.Flag("mft", "Collect the system drive MFT.").Bool() 30 | mftAll = kingpin.Flag("mft-all", "Collect all attached volume MFTs.").Bool() 31 | mftLetters = kingpin.Flag("mft-letters", "Collect all attached volume MFTs.").Strings() 32 | reg = kingpin.Flag("reg", "Collect all registry hives, both system and user hives.").Bool() 33 | events = kingpin.Flag("events", "Collect all event logs.").Bool() 34 | browser = kingpin.Flag("browser", "Collect browser history").Bool() 35 | config = kingpin.Flag("custom-config", "Custom configuration file that will overwrite built in config.").File() 36 | throttle = kingpin.Flag("throttle", "This setting will limit the process to a single thread. This will reduce the CPU load.").Bool() 37 | output = kingpin.Flag("output", "Specify the name of the output file. If not specified, the file name defaults to the host name and a timestamp.").String() 38 | ) 39 | 40 | func main() { 41 | log.SetFormatter(&log.JSONFormatter{}) 42 | kingpin.Parse() 43 | if *throttle { 44 | runtime.GOMAXPROCS(1) 45 | } 46 | 47 | if *debug { 48 | debugLog, _ := os.Create("debug.log") 49 | log.SetOutput(debugLog) 50 | log.SetLevel(log.DebugLevel) 51 | } else { 52 | log.SetOutput(os.Stdout) 53 | log.SetLevel(log.ErrorLevel) 54 | } 55 | 56 | exportList := make(collector.ListOfFilesToExport, 0) 57 | if *config != nil { 58 | configData, err := ioutil.ReadAll(*config) 59 | if err != nil { 60 | log.Panic(err) 61 | } 62 | 63 | err = yaml.Unmarshal(configData, &exportList) 64 | if err != nil { 65 | log.Panic(err) 66 | } 67 | } else { 68 | if *all { 69 | *mftAll = true 70 | *events = true 71 | *reg = true 72 | *browser = true 73 | } 74 | if *mftAll { 75 | volumes, _ := disk.Partitions(true) 76 | for _, volume := range volumes { 77 | file := collector.FileToExport{ 78 | FullPath: fmt.Sprintf(`%s\$MFT`, volume.Mountpoint), 79 | IsFullPathRegex: false, 80 | FileName: `$MFT`, 81 | IsFileNameRegex: false, 82 | } 83 | exportList = append(exportList, file) 84 | } 85 | } else if len(*mftLetters) > 0 { 86 | for _, v := range *mftLetters { 87 | file := collector.FileToExport{ 88 | FullPath: fmt.Sprintf(`%s:\$MFT`, v), 89 | IsFullPathRegex: false, 90 | FileName: `$MFT`, 91 | IsFileNameRegex: false, 92 | } 93 | exportList = append(exportList, file) 94 | } 95 | } else if *mft { 96 | file := collector.FileToExport{ 97 | FullPath: `%SYSTEMDRIVE%:\$MFT`, 98 | IsFullPathRegex: false, 99 | FileName: `$MFT`, 100 | IsFileNameRegex: false, 101 | } 102 | exportList = append(exportList, file) 103 | } 104 | 105 | if *events { 106 | file := collector.FileToExport{ 107 | FullPath: `%SYSTEMDRIVE%:\\Windows\\System32\\winevt\\Logs\\.*\.evtx$`, 108 | IsFullPathRegex: true, 109 | FileName: `.*\.evtx$`, 110 | IsFileNameRegex: true, 111 | } 112 | exportList = append(exportList, file) 113 | } 114 | 115 | if *reg { 116 | files := collector.ListOfFilesToExport{ 117 | { 118 | FullPath: `%SYSTEMDRIVE%:\Windows\System32\config\SYSTEM`, 119 | IsFullPathRegex: false, 120 | FileName: `SYSTEM`, 121 | IsFileNameRegex: false, 122 | }, 123 | { 124 | FullPath: `%SYSTEMDRIVE%:\Windows\System32\config\SOFTWARE`, 125 | IsFullPathRegex: false, 126 | FileName: `SOFTWARE`, 127 | IsFileNameRegex: false, 128 | }, 129 | { 130 | FullPath: `%SYSTEMDRIVE%:\\users\\([^\\]+)\\ntuser.dat`, 131 | IsFullPathRegex: true, 132 | FileName: `ntuser.dat`, 133 | IsFileNameRegex: false, 134 | }, 135 | { 136 | FullPath: `%SYSTEMDRIVE%:\\Users\\([^\\]+)\\AppData\\Local\\Microsoft\\Windows\\usrclass.dat`, 137 | IsFullPathRegex: true, 138 | FileName: `usrclass.dat`, 139 | IsFileNameRegex: false, 140 | }, 141 | } 142 | for _, v := range files { 143 | exportList = append(exportList, v) 144 | } 145 | } 146 | 147 | if *browser { 148 | file := collector.FileToExport{ 149 | FullPath: `%SYSTEMDRIVE%:\\Users\\([^\\]+)\\AppData\\Local\\Microsoft\\Windows\\WebCache\\WebCacheV01.dat`, 150 | IsFullPathRegex: true, 151 | FileName: `WebCacheV01.dat`, 152 | IsFileNameRegex: false, 153 | } 154 | exportList = append(exportList, file) 155 | } 156 | } 157 | 158 | var zipName string 159 | if *output != "" { 160 | zipName = *output 161 | } else { 162 | hostName, _ := os.Hostname() 163 | zipName = fmt.Sprintf("%s_%s.zip", hostName, time.Now().Format("2006-01-02T15.04.05Z")) 164 | } 165 | fileHandle, err := os.Create(zipName) 166 | if err != nil { 167 | err = fmt.Errorf("failed to create zip file %s", zipName) 168 | } 169 | defer fileHandle.Close() 170 | 171 | zipWriter := zip.NewWriter(fileHandle) 172 | resultWriter := collector.ZipResultWriter{ 173 | ZipWriter: zipWriter, 174 | FileHandle: fileHandle, 175 | } 176 | defer zipWriter.Close() 177 | var volume collector.VolumeHandler 178 | err = collector.Collect(volume, exportList, &resultWriter) 179 | if err != nil { 180 | log.Panic(err) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /collector.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Alec Randazzo 2 | 3 | package packrat 4 | 5 | import ( 6 | "fmt" 7 | mft "github.com/AlecRandazzo/MFT-Parser" 8 | log "github.com/sirupsen/logrus" 9 | "io" 10 | "sync" 11 | ) 12 | 13 | // Collect will find and collect target files into a format depending on the resultWriter type 14 | func Collect(injectedHandlerDependency handler, exportList ListOfFilesToExport, resultWriter resultWriter) (err error) { 15 | // volumeHandler as an arg is a dependency injection 16 | log.Debugf("Attempting to acquire the following files %+v", exportList) 17 | volumesOfInterest, err := identifyVolumesOfInterest(&exportList) 18 | if err != nil { 19 | err = fmt.Errorf("identifyVolumesOfInterest() returned an error: %w", err) 20 | return 21 | } 22 | 23 | searchTerms, err := setupSearchTerms(exportList) 24 | if err != nil { 25 | err = fmt.Errorf("setupSearchTerms() returned the following error: %w", err) 26 | return 27 | } 28 | 29 | for _, volumeLetter := range volumesOfInterest { 30 | var volumeHandler VolumeHandler 31 | volumeHandler, err = GetVolumeHandler(volumeLetter, injectedHandlerDependency) 32 | if err != nil { 33 | continue 34 | } 35 | 36 | err = getFiles(&volumeHandler, resultWriter, searchTerms) 37 | if err != nil { 38 | err = fmt.Errorf("getFiles() failed to get files: %w", err) 39 | return 40 | } 41 | } 42 | return 43 | } 44 | 45 | func getFiles(volumeHandler *VolumeHandler, resultWriter resultWriter, listOfSearchKeywords listOfSearchTerms) (err error) { 46 | // Init a few things 47 | fileReaders := make(chan fileReader, 100) 48 | waitForFileCopying := sync.WaitGroup{} 49 | waitForFileCopying.Add(1) 50 | go resultWriter.ResultWriter(fileReaders, &waitForFileCopying) 51 | 52 | // parse the mft's mft record to get its dataruns 53 | mftRecord0, err := parseMFTRecord0(volumeHandler) 54 | if err != nil { 55 | err = fmt.Errorf("parseMFTRecord0() failed to parse mft record 0 from the volume %s: %w", volumeHandler.VolumeLetter, err) 56 | return 57 | } 58 | log.Debugf("Parsed the MFT's MFT record and got the following: %+v", mftRecord0) 59 | 60 | // Go back to the beginning of the mft record 61 | _, _ = volumeHandler.Handle.Seek(volumeHandler.Vbr.MftByteOffset, 0) 62 | log.Debugf("Seeked back to the beginning offset to the MFT at offset %d", volumeHandler.Vbr.MftByteOffset) 63 | 64 | // Open a raw reader on the MFT 65 | foundFile := foundFile{ 66 | dataRuns: mftRecord0.DataAttribute.NonResidentDataAttribute.DataRuns, 67 | fullPath: "$mft", 68 | } 69 | mftReader := rawFileReader(volumeHandler, foundFile) 70 | log.Debug("Obtained a raw io.Reader to the MFT's dataruns.") 71 | 72 | // Do we need to stream a copy of the mft while we read it? 73 | areWeCopyingTheMFT := false 74 | directoryTree := mft.DirectoryTree{} 75 | possibleMatches := possibleMatches{} 76 | 77 | for index, value := range listOfSearchKeywords { 78 | if value.fileNameString == "$mft" { 79 | areWeCopyingTheMFT = true 80 | 81 | // delete this from our search list 82 | listOfSearchKeywords[index] = listOfSearchKeywords[len(listOfSearchKeywords)-1] 83 | listOfSearchKeywords = listOfSearchKeywords[:len(listOfSearchKeywords)-1] 84 | 85 | break 86 | } 87 | } 88 | 89 | if areWeCopyingTheMFT == true { 90 | log.Debug("We are configured to grab a copy of the MFT, so we'll set up a io.TeeReader with an io.Pipe so we can copy the mft as we read it. We do this so we only have to read the MFT's data runs once and only once.") 91 | pipeReader, pipeWriter := io.Pipe() 92 | teeReader := io.TeeReader(mftReader, pipeWriter) 93 | fileReader := fileReader{ 94 | fullPath: fmt.Sprintf("%s__$mft", volumeHandler.VolumeLetter), 95 | reader: pipeReader, 96 | } 97 | fileReaders <- fileReader 98 | volumeHandler.mftReader = teeReader 99 | possibleMatches, directoryTree, err = findPossibleMatches(volumeHandler, listOfSearchKeywords) 100 | if err != nil { 101 | err = fmt.Errorf("findPossibleMatches() failed: %w", err) 102 | return 103 | } 104 | err = pipeWriter.Close() 105 | if err != nil { 106 | err = fmt.Errorf("failed to close writer pipe: %w", err) 107 | return 108 | } 109 | } else { 110 | volumeHandler.mftReader = mftReader 111 | possibleMatches, directoryTree, err = findPossibleMatches(volumeHandler, listOfSearchKeywords) 112 | if err != nil { 113 | err = fmt.Errorf("findPossibleMatches() failed: %w", err) 114 | return 115 | } 116 | } 117 | 118 | foundFiles := confirmFoundFiles(listOfSearchKeywords, possibleMatches, directoryTree) 119 | if err != nil { 120 | err = fmt.Errorf("confirmFoundFiles() failed with error: %w", err) 121 | return 122 | } 123 | 124 | for _, file := range foundFiles { 125 | // try to get an io.reader via api first 126 | reader, err := apiFileReader(file) 127 | if err != nil { 128 | log.Debugf("Got a raw io.Reader for '%s' with data runs: %+v", file.fullPath, file.dataRuns) 129 | // failed to get an API handle, trying to get an io.reader via raw method 130 | reader = rawFileReader(volumeHandler, file) 131 | } else { 132 | log.Debugf("Got an API io.Reader for '%s'.", file.fullPath) 133 | } 134 | fileReader := fileReader{ 135 | fullPath: file.fullPath, 136 | reader: reader, 137 | } 138 | fileReaders <- fileReader 139 | } 140 | close(fileReaders) 141 | err = nil 142 | waitForFileCopying.Wait() 143 | return 144 | } 145 | -------------------------------------------------------------------------------- /collector_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Alec Randazzo 2 | 3 | package packrat 4 | 5 | import ( 6 | "archive/zip" 7 | "crypto/md5" 8 | "encoding/hex" 9 | vbr "github.com/AlecRandazzo/VBR-Parser" 10 | log "github.com/sirupsen/logrus" 11 | "io" 12 | "os" 13 | "testing" 14 | ) 15 | 16 | func TestCollect(t *testing.T) { 17 | type args struct { 18 | exportList ListOfFilesToExport 19 | resultWriter ZipResultWriter 20 | handler handler 21 | } 22 | tests := []struct { 23 | name string 24 | args args 25 | wantErr bool 26 | zipTestOutput string 27 | wantZipHash string 28 | }{ 29 | { 30 | name: "test1", 31 | args: args{ 32 | exportList: ListOfFilesToExport{ 33 | 0: { 34 | FullPath: `%SYSTEMDRIVE%:\$MFT`, 35 | IsFullPathRegex: false, 36 | FileName: `$MFT`, 37 | IsFileNameRegex: false, 38 | }, 39 | }, 40 | resultWriter: ZipResultWriter{}, 41 | handler: dummyHandler{ 42 | Handle: nil, 43 | VolumeLetter: "", 44 | Vbr: vbr.VolumeBootRecord{}, 45 | mftReader: nil, 46 | lastReadVolumeOffset: 0, 47 | filePath: `test\testdata\dummyntfs`, 48 | }, 49 | }, 50 | wantErr: false, 51 | zipTestOutput: `test\testdata\collecttestzip.zip`, 52 | wantZipHash: "29f689d96a790b68df7e84c9e04ef741", 53 | }, 54 | } 55 | for _, tt := range tests { 56 | t.Run(tt.name, func(t *testing.T) { 57 | fileHandle, _ := os.Create(tt.zipTestOutput) 58 | zipWriter := zip.NewWriter(fileHandle) 59 | tt.args.resultWriter = ZipResultWriter{ 60 | ZipWriter: zipWriter, 61 | FileHandle: fileHandle, 62 | } 63 | _ = Collect(tt.args.handler, tt.args.exportList, &tt.args.resultWriter) 64 | zipWriter.Close() 65 | fileHandle.Close() 66 | 67 | // Get file hash 68 | file, _ := os.Open(tt.zipTestOutput) 69 | hash := md5.New() 70 | _, _ = io.Copy(hash, file) 71 | hashInBytes := hash.Sum(nil)[:] 72 | gotZipHash := hex.EncodeToString(hashInBytes) 73 | file.Close() 74 | if gotZipHash != tt.wantZipHash { 75 | t.Errorf("collect() gotZipHash = %v, want %v", gotZipHash, tt.wantZipHash) 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func Test_getFiles(t *testing.T) { 82 | type args struct { 83 | volumeHandler *VolumeHandler 84 | resultWriter ZipResultWriter 85 | listOfSearchKeywords listOfSearchTerms 86 | } 87 | tests := []struct { 88 | name string 89 | args args 90 | wantErr bool 91 | dummyFile string 92 | testZip string 93 | wantZipHash string 94 | }{ 95 | { 96 | name: "test1", 97 | args: args{ 98 | volumeHandler: &VolumeHandler{}, 99 | resultWriter: ZipResultWriter{}, 100 | listOfSearchKeywords: listOfSearchTerms{ 101 | 0: searchTerms{ 102 | fullPathString: `c:\$mft`, 103 | fullPathRegex: nil, 104 | fileNameString: "$mft", 105 | fileNameRegex: nil, 106 | }, 107 | 1: searchTerms{ 108 | fullPathString: `c:\\$mftmirr`, 109 | fullPathRegex: nil, 110 | fileNameString: "$mftmirr", 111 | fileNameRegex: nil, 112 | }, 113 | }, 114 | }, 115 | dummyFile: `test\testdata\dummyntfs`, 116 | testZip: `test\testdata\getFilesTest1.zip`, 117 | wantErr: false, 118 | wantZipHash: "a50b885249c709ae97eeba0e2d6ec78d", 119 | }, 120 | { 121 | name: "test2", 122 | args: args{ 123 | volumeHandler: &VolumeHandler{}, 124 | resultWriter: ZipResultWriter{}, 125 | listOfSearchKeywords: listOfSearchTerms{ 126 | 0: searchTerms{ 127 | fullPathString: `c:\\$mftmirr`, 128 | fullPathRegex: nil, 129 | fileNameString: "$mftmirr", 130 | fileNameRegex: nil, 131 | }, 132 | }, 133 | }, 134 | dummyFile: `test\testdata\dummyntfs`, 135 | testZip: `test\testdata\getFilesTest2.zip`, 136 | wantErr: false, 137 | wantZipHash: "75c57f05d2879cb723dbec6e2e1e8f83", 138 | }, 139 | } 140 | for _, tt := range tests { 141 | t.Run(tt.name, func(t *testing.T) { 142 | fileHandle, _ := os.Create(tt.testZip) 143 | zipWriter := zip.NewWriter(fileHandle) 144 | tt.args.resultWriter = ZipResultWriter{ 145 | ZipWriter: zipWriter, 146 | FileHandle: fileHandle, 147 | } 148 | dummyHandle := &dummyHandler{ 149 | Handle: nil, 150 | VolumeLetter: "c", 151 | Vbr: vbr.VolumeBootRecord{}, 152 | mftReader: nil, 153 | lastReadVolumeOffset: 0, 154 | filePath: tt.dummyFile, 155 | } 156 | var err error 157 | *tt.args.volumeHandler, err = GetVolumeHandler("c", dummyHandle) 158 | if err != nil { 159 | log.Panic(err) 160 | } 161 | defer tt.args.volumeHandler.Handle.Close() 162 | 163 | _ = getFiles(tt.args.volumeHandler, &tt.args.resultWriter, tt.args.listOfSearchKeywords) 164 | zipWriter.Close() 165 | fileHandle.Close() 166 | 167 | // Get file hash 168 | file, _ := os.Open(tt.testZip) 169 | hash := md5.New() 170 | _, _ = io.Copy(hash, file) 171 | hashInBytes := hash.Sum(nil)[:] 172 | gotZipHash := hex.EncodeToString(hashInBytes) 173 | file.Close() 174 | if gotZipHash != tt.wantZipHash { 175 | t.Errorf("getFiles() gotZipHash = %v, want %v", gotZipHash, tt.wantZipHash) 176 | } 177 | }) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /configs/config.yml: -------------------------------------------------------------------------------- 1 | - FullPath: "C:\\$MFT" 2 | IsFullPathRegex: false 3 | FileName: "$MFT" 4 | IsFileNameRegex: false 5 | - FullPath: "%SYSTEMDRIVE%:\\\\Windows\\\\System32\\\\winevt\\\\Logs\\\\.*\\.evtx$" 6 | IsFullPathRegex: true 7 | FileName: ".*\\.evtx$" 8 | IsFileNameRegex: true 9 | - FullPath: "%SYSTEMDRIVE%:\\Windows\\System32\\config\\SYSTEM" 10 | IsFullPathRegex: false 11 | FileName: "SYSTEM" 12 | IsFileNameRegex: false 13 | - FullPath: "%SYSTEMDRIVE%:\\Windows\\System32\\config\\SOFTWARE" 14 | IsFullPathRegex: false 15 | FileName: "SOFTWARE" 16 | IsFileNameRegex: false 17 | - FullPath: "%SYSTEMDRIVE%:\\\\users\\\\([^\\\\]+)\\\\ntuser.dat" 18 | IsFullPathRegex: true 19 | FileName: "ntuser.dat" 20 | IsFileNameRegex: false 21 | - FullPath: "%SYSTEMDRIVE%:\\\\Users\\\\([^\\\\]+)\\\\AppData\\\\Local\\\\Microsoft\\\\Windows\\\\usrclass.dat" 22 | IsFullPathRegex: true 23 | FileName: "usrclass.dat" 24 | IsFileNameRegex: false 25 | - FullPath: "%SYSTEMDRIVE%:\\\\Users\\\\([^\\\\]+)\\\\AppData\\\\Local\\\\Microsoft\\\\Windows\\\\WebCache\\\\WebCacheV01.dat" 26 | IsFullPathRegex: true 27 | FileName: "WebCacheV01.dat" 28 | IsFileNameRegex: false -------------------------------------------------------------------------------- /filefinder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Alec Randazzo 2 | 3 | package packrat 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | mft "github.com/AlecRandazzo/MFT-Parser" 9 | log "github.com/sirupsen/logrus" 10 | "io" 11 | "strings" 12 | ) 13 | 14 | type possibleMatch struct { 15 | fileNameAttribute mft.FileNameAttribute 16 | dataRuns mft.DataRuns 17 | } 18 | 19 | type possibleMatches []possibleMatch 20 | 21 | type mftRecordVolumeOffsetTracker map[uint32]int64 22 | 23 | type mftRecordWithNonResidentAttributes struct { 24 | fnAttribute mft.FileNameAttribute 25 | dataAttribute mft.DataAttribute 26 | attributeListAttributes mft.AttributeListAttributes 27 | } 28 | 29 | type listOfMftRecordWithNonResidentAttributes []mftRecordWithNonResidentAttributes 30 | 31 | func checkForPossibleMatch(listOfSearchKeywords listOfSearchTerms, fileNameAttributes mft.FileNameAttributes) (result bool, fileNameAttribute mft.FileNameAttribute, err error) { 32 | // Sanity Checking 33 | if len(listOfSearchKeywords) == 0 { 34 | err = errors.New("checkForPossibleMatch() received an empty listOfSearchTerms") 35 | return 36 | } 37 | if len(fileNameAttributes) == 0 { 38 | err = errors.New("checkForPossibleMatch() received an empty fileNameAttributes") 39 | return 40 | } 41 | 42 | for _, attribute := range fileNameAttributes { 43 | if strings.Contains(attribute.FileNamespace, "WIN32") == true || strings.Contains(attribute.FileNamespace, "POSIX") { 44 | for _, value := range listOfSearchKeywords { 45 | if value.fileNameRegex != nil { 46 | if value.fileNameRegex.MatchString(strings.ToLower(attribute.FileName)) == true { 47 | result = true 48 | fileNameAttribute = attribute 49 | return 50 | } 51 | } else { 52 | if value.fileNameString == strings.ToLower(attribute.FileName) { 53 | result = true 54 | fileNameAttribute = attribute 55 | return 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | result = false 63 | return 64 | } 65 | 66 | func findPossibleMatches(volumeHandler *VolumeHandler, listOfSearchKeywords listOfSearchTerms) (listOfPossibleMatches possibleMatches, directoryTree mft.DirectoryTree, err error) { 67 | log.Debugf("Starting to scan the MFT's dataruns to create a tree of directories and to search for the for the following search terms: %+v", listOfSearchKeywords) 68 | 69 | // Init memory 70 | unresolvedDirectorTree := make(mft.UnresolvedDirectoryTree) 71 | listOfPossibleMatches = make(possibleMatches, 0) 72 | recordOffsetTracker := make(mftRecordVolumeOffsetTracker) 73 | listOfMftRecordWithNonResidentAttributes := make(listOfMftRecordWithNonResidentAttributes, 0) 74 | 75 | for err != io.EOF { 76 | buffer := mft.RawMasterFileTableRecord(make([]byte, volumeHandler.Vbr.MftRecordSize)) 77 | _, err = volumeHandler.mftReader.Read(buffer) 78 | if err == io.EOF { 79 | err = nil 80 | break 81 | } 82 | 83 | result, _ := buffer.IsThisAnMftRecord() 84 | if result == false { 85 | continue 86 | } 87 | 88 | result, err = buffer.IsThisADirectory() 89 | if result == true { 90 | unresolvedDirectory, _ := mft.ConvertRawMFTRecordToDirectory(buffer) 91 | unresolvedDirectorTree[unresolvedDirectory.RecordNumber] = unresolvedDirectory 92 | recordOffsetTracker[unresolvedDirectory.RecordNumber] = volumeHandler.lastReadVolumeOffset 93 | } else { 94 | // Parse what we need out of the entry for us to copy the file 95 | rawRecordHeader, _ := buffer.GetRawRecordHeader() 96 | recordHeader, _ := rawRecordHeader.Parse() 97 | recordOffsetTracker[recordHeader.RecordNumber] = volumeHandler.lastReadVolumeOffset 98 | rawAttributes, _ := buffer.GetRawAttributes(recordHeader) 99 | fileNameAttributes, _, dataAttribute, attributeListAttributes, _ := rawAttributes.Parse(volumeHandler.Vbr.BytesPerCluster) 100 | result, fileNameAttribute, err := checkForPossibleMatch(listOfSearchKeywords, fileNameAttributes) 101 | if err != nil || result == false { 102 | continue 103 | } 104 | 105 | if attributeListAttributes == nil { 106 | log.Debugf("Found a possible match. File name is '%s' and its MFT offset is %d. Here is the MFT record hex: %x", fileNameAttribute.FileName, volumeHandler.lastReadVolumeOffset, []byte(buffer)) 107 | aPossibleMatch := possibleMatch{ 108 | fileNameAttribute: fileNameAttribute, 109 | dataRuns: dataAttribute.NonResidentDataAttribute.DataRuns, 110 | } 111 | listOfPossibleMatches = append(listOfPossibleMatches, aPossibleMatch) 112 | continue 113 | } else { 114 | log.Debugf("Found a possible match which has an attribute list. File name is '%s' and its MFT offset is %d. Here is the attribute list: %+v Here is the MFT record hex: %x", fileNameAttribute.FileName, volumeHandler.lastReadVolumeOffset, attributeListAttributes, buffer) 115 | trackThisForLater := mftRecordWithNonResidentAttributes{ 116 | fnAttribute: fileNameAttribute, 117 | dataAttribute: dataAttribute, 118 | attributeListAttributes: attributeListAttributes, 119 | } 120 | listOfMftRecordWithNonResidentAttributes = append(listOfMftRecordWithNonResidentAttributes, trackThisForLater) 121 | continue 122 | } 123 | } 124 | } 125 | 126 | // Resolve the possible matches that had attribute lists 127 | if len(listOfMftRecordWithNonResidentAttributes) != 0 { 128 | newVolumeHandle, _ := volumeHandler.GetHandle(volumeHandler.VolumeLetter) 129 | for _, record := range listOfMftRecordWithNonResidentAttributes { 130 | attributeCounter := 0 131 | sizeOfAttributeListAttributes := len(record.attributeListAttributes) 132 | dataRuns := make(mft.DataRuns) 133 | for attributeCounter < sizeOfAttributeListAttributes { 134 | switch record.attributeListAttributes[attributeCounter].Type { 135 | case 0x80: 136 | nonResidentRecordNumber := record.attributeListAttributes[attributeCounter].MFTReferenceRecordNumber 137 | absoluteVolumeOffset := recordOffsetTracker[nonResidentRecordNumber] 138 | _, _ = newVolumeHandle.Seek(absoluteVolumeOffset, 0) 139 | buffer := mft.RawMasterFileTableRecord(make([]byte, volumeHandler.Vbr.BytesPerCluster)) 140 | _, _ = newVolumeHandle.Read(buffer) 141 | mftRecord, _ := buffer.Parse(volumeHandler.Vbr.BytesPerCluster) 142 | log.Debugf("Went to absolute offset %d to get a non resident data attribute with record number %d. Parsed the record for the values %+v. Raw hex: %x", absoluteVolumeOffset, nonResidentRecordNumber, mftRecord, buffer) 143 | tempDataRunCounter := 0 144 | numberOfDataRuns := len(mftRecord.DataAttribute.NonResidentDataAttribute.DataRuns) 145 | for tempDataRunCounter < numberOfDataRuns { 146 | index := len(dataRuns) 147 | dataRuns[index] = mftRecord.DataAttribute.NonResidentDataAttribute.DataRuns[tempDataRunCounter] 148 | tempDataRunCounter++ 149 | } 150 | attributeCounter++ 151 | default: 152 | attributeCounter++ 153 | } 154 | } 155 | aPossibleMatch := possibleMatch{ 156 | fileNameAttribute: record.fnAttribute, 157 | dataRuns: dataRuns, 158 | } 159 | log.Debugf("Pieced together a series of non resident data attributes and got the following: %+v", aPossibleMatch) 160 | listOfPossibleMatches = append(listOfPossibleMatches, aPossibleMatch) 161 | } 162 | } 163 | 164 | log.Debugf("Resolving %d directories we found to build their full paths.", len(unresolvedDirectorTree)) 165 | directoryTree, _ = unresolvedDirectorTree.Resolve(volumeHandler.VolumeLetter) 166 | log.Debugf("Successfully resolved %d directories.", len(directoryTree)) 167 | return 168 | } 169 | 170 | type foundFile struct { 171 | dataRuns mft.DataRuns 172 | fullPath string 173 | fileSize int64 174 | } 175 | 176 | type foundFiles []foundFile 177 | 178 | func confirmFoundFiles(listOfSearchKeywords listOfSearchTerms, listOfPossibleMatches possibleMatches, directoryTree mft.DirectoryTree) (foundFilesList foundFiles) { 179 | log.Debug("Determining what possible matches are true matches.") 180 | foundFilesList = make(foundFiles, 0) 181 | for _, possibleMatch := range listOfPossibleMatches { 182 | // First make sure that the parent directory is in the directory tree 183 | if _, ok := directoryTree[possibleMatch.fileNameAttribute.ParentDirRecordNumber]; ok { 184 | // check against all the list of possible full paths 185 | possibleMatchFullPath := fmt.Sprintf(`%s\%s`, strings.ToLower(directoryTree[possibleMatch.fileNameAttribute.ParentDirRecordNumber]), strings.ToLower(possibleMatch.fileNameAttribute.FileName)) 186 | numberOfSearchTerms := len(listOfSearchKeywords) 187 | counter := 0 188 | for _, searchTerms := range listOfSearchKeywords { 189 | if searchTerms.fullPathRegex != nil { 190 | if searchTerms.fullPathRegex.MatchString(possibleMatchFullPath) == true { 191 | foundFile := foundFile{ 192 | dataRuns: possibleMatch.dataRuns, 193 | fullPath: possibleMatchFullPath, 194 | fileSize: int64(possibleMatch.fileNameAttribute.PhysicalFileSize), 195 | } 196 | log.Debugf("Found a true match: %+v", foundFile) 197 | foundFilesList = append(foundFilesList, foundFile) 198 | break 199 | } 200 | } else { 201 | if searchTerms.fullPathString == possibleMatchFullPath { 202 | foundFile := foundFile{ 203 | dataRuns: possibleMatch.dataRuns, 204 | fullPath: possibleMatchFullPath, 205 | } 206 | log.Debugf("Found a true match: %+v", foundFile) 207 | foundFilesList = append(foundFilesList, foundFile) 208 | break 209 | } 210 | } 211 | counter++ 212 | if counter == numberOfSearchTerms { 213 | log.Debugf("The file %s did not end up being a true positive", possibleMatchFullPath) 214 | } 215 | } 216 | } else { 217 | // continue if parent directory is not in the directory tree map 218 | continue 219 | } 220 | } 221 | return 222 | } 223 | -------------------------------------------------------------------------------- /filefinder_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Alec Randazzo 2 | 3 | package packrat 4 | 5 | import ( 6 | mft "github.com/AlecRandazzo/MFT-Parser" 7 | vbr "github.com/AlecRandazzo/VBR-Parser" 8 | log "github.com/sirupsen/logrus" 9 | "reflect" 10 | "regexp" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func Test_checkForPossibleMatch(t *testing.T) { 16 | type args struct { 17 | listOfSearchKeywords listOfSearchTerms 18 | fileNameAttributes mft.FileNameAttributes 19 | } 20 | tests := []struct { 21 | name string 22 | args args 23 | wantResult bool 24 | wantFileNameAttribute mft.FileNameAttribute 25 | wantErr bool 26 | }{ 27 | { 28 | name: "null keywords", 29 | wantErr: true, 30 | args: args{ 31 | listOfSearchKeywords: nil, 32 | fileNameAttributes: mft.FileNameAttributes{ 33 | 0: mft.FileNameAttribute{ 34 | FnCreated: time.Time{}, 35 | FnModified: time.Time{}, 36 | FnAccessed: time.Time{}, 37 | FnChanged: time.Time{}, 38 | FlagResident: true, 39 | ParentDirRecordNumber: 0, 40 | LogicalFileSize: 0, 41 | PhysicalFileSize: 0, 42 | FileNameFlags: mft.FileNameFlags{}, 43 | FileNamespace: "", 44 | FileNameLength: 16, 45 | FileName: "test", 46 | }, 47 | }, 48 | }, 49 | }, 50 | { 51 | name: "null fn attribute", 52 | wantErr: true, 53 | args: args{ 54 | listOfSearchKeywords: listOfSearchTerms{ 55 | 0: searchTerms{ 56 | fullPathString: `c:\test`, 57 | fullPathRegex: nil, 58 | fileNameString: "test", 59 | fileNameRegex: nil, 60 | }, 61 | }, 62 | fileNameAttributes: nil, 63 | }, 64 | }, 65 | { 66 | name: "file name exact match", 67 | wantErr: false, 68 | args: args{ 69 | listOfSearchKeywords: listOfSearchTerms{ 70 | 0: searchTerms{ 71 | fullPathString: `c:\test`, 72 | fullPathRegex: nil, 73 | fileNameString: "test", 74 | fileNameRegex: nil, 75 | }, 76 | }, 77 | fileNameAttributes: mft.FileNameAttributes{ 78 | 0: mft.FileNameAttribute{ 79 | FnCreated: time.Time{}, 80 | FnModified: time.Time{}, 81 | FnAccessed: time.Time{}, 82 | FnChanged: time.Time{}, 83 | ParentDirRecordNumber: 0, 84 | LogicalFileSize: 0, 85 | PhysicalFileSize: 0, 86 | FileNameFlags: mft.FileNameFlags{}, 87 | FileNamespace: "WIN32", 88 | FileName: "nope", 89 | }, 90 | 1: mft.FileNameAttribute{ 91 | FnCreated: time.Time{}, 92 | FnModified: time.Time{}, 93 | FnAccessed: time.Time{}, 94 | FnChanged: time.Time{}, 95 | ParentDirRecordNumber: 0, 96 | LogicalFileSize: 0, 97 | PhysicalFileSize: 0, 98 | FileNameFlags: mft.FileNameFlags{}, 99 | FileNamespace: "WIN32", 100 | FileName: "test", 101 | }, 102 | }, 103 | }, 104 | wantResult: true, 105 | wantFileNameAttribute: mft.FileNameAttribute{ 106 | FnCreated: time.Time{}, 107 | FnModified: time.Time{}, 108 | FnAccessed: time.Time{}, 109 | FnChanged: time.Time{}, 110 | ParentDirRecordNumber: 0, 111 | LogicalFileSize: 0, 112 | PhysicalFileSize: 0, 113 | FileNameFlags: mft.FileNameFlags{}, 114 | FileNamespace: "WIN32", 115 | FileName: "test", 116 | }, 117 | }, 118 | { 119 | name: "file name regex match", 120 | wantErr: false, 121 | args: args{ 122 | listOfSearchKeywords: listOfSearchTerms{ 123 | 0: searchTerms{ 124 | fullPathString: `c:\test`, 125 | fullPathRegex: nil, 126 | fileNameString: "", 127 | fileNameRegex: regexp.MustCompile("^test$"), 128 | }, 129 | }, 130 | fileNameAttributes: mft.FileNameAttributes{ 131 | 0: mft.FileNameAttribute{ 132 | FnCreated: time.Time{}, 133 | FnModified: time.Time{}, 134 | FnAccessed: time.Time{}, 135 | FnChanged: time.Time{}, 136 | ParentDirRecordNumber: 0, 137 | LogicalFileSize: 0, 138 | PhysicalFileSize: 0, 139 | FileNameFlags: mft.FileNameFlags{}, 140 | FileNamespace: "WIN32", 141 | FileName: "nope", 142 | }, 143 | 1: mft.FileNameAttribute{ 144 | FnCreated: time.Time{}, 145 | FnModified: time.Time{}, 146 | FnAccessed: time.Time{}, 147 | FnChanged: time.Time{}, 148 | ParentDirRecordNumber: 0, 149 | LogicalFileSize: 0, 150 | PhysicalFileSize: 0, 151 | FileNameFlags: mft.FileNameFlags{}, 152 | FileNamespace: "WIN32", 153 | FileName: "test", 154 | }, 155 | }, 156 | }, 157 | wantResult: true, 158 | wantFileNameAttribute: mft.FileNameAttribute{ 159 | FnCreated: time.Time{}, 160 | FnModified: time.Time{}, 161 | FnAccessed: time.Time{}, 162 | FnChanged: time.Time{}, 163 | ParentDirRecordNumber: 0, 164 | LogicalFileSize: 0, 165 | PhysicalFileSize: 0, 166 | FileNameFlags: mft.FileNameFlags{}, 167 | FileNamespace: "WIN32", 168 | FileName: "test", 169 | }, 170 | }, 171 | { 172 | name: "file name no match", 173 | wantErr: false, 174 | args: args{ 175 | listOfSearchKeywords: listOfSearchTerms{ 176 | 0: searchTerms{ 177 | fullPathString: `c:\test`, 178 | fullPathRegex: nil, 179 | fileNameString: "test", 180 | fileNameRegex: nil, 181 | }, 182 | }, 183 | fileNameAttributes: mft.FileNameAttributes{ 184 | 0: mft.FileNameAttribute{ 185 | FnCreated: time.Time{}, 186 | FnModified: time.Time{}, 187 | FnAccessed: time.Time{}, 188 | FnChanged: time.Time{}, 189 | ParentDirRecordNumber: 0, 190 | LogicalFileSize: 0, 191 | PhysicalFileSize: 0, 192 | FileNameFlags: mft.FileNameFlags{}, 193 | FileNamespace: "WIN32", 194 | FileName: "nope", 195 | }, 196 | 1: mft.FileNameAttribute{ 197 | FnCreated: time.Time{}, 198 | FnModified: time.Time{}, 199 | FnAccessed: time.Time{}, 200 | FnChanged: time.Time{}, 201 | ParentDirRecordNumber: 0, 202 | LogicalFileSize: 0, 203 | PhysicalFileSize: 0, 204 | FileNameFlags: mft.FileNameFlags{}, 205 | FileNamespace: "WIN32", 206 | FileName: "test2", 207 | }, 208 | }, 209 | }, 210 | wantResult: false, 211 | wantFileNameAttribute: mft.FileNameAttribute{}, 212 | }, 213 | } 214 | for _, tt := range tests { 215 | t.Run(tt.name, func(t *testing.T) { 216 | gotResult, gotFileNameAttribute, err := checkForPossibleMatch(tt.args.listOfSearchKeywords, tt.args.fileNameAttributes) 217 | if (err != nil) != tt.wantErr { 218 | t.Errorf("checkForPossibleMatch() error = %v, wantErr %v", err, tt.wantErr) 219 | return 220 | } 221 | if gotResult != tt.wantResult { 222 | t.Errorf("checkForPossibleMatch() gotResult = %v, want %v", gotResult, tt.wantResult) 223 | } 224 | if !reflect.DeepEqual(gotFileNameAttribute, tt.wantFileNameAttribute) { 225 | t.Errorf("checkForPossibleMatch() gotFileNameAttribute = %v, want %v", gotFileNameAttribute, tt.wantFileNameAttribute) 226 | } 227 | }) 228 | } 229 | } 230 | 231 | func Test_confirmFoundFiles(t *testing.T) { 232 | type args struct { 233 | listOfSearchKeywords listOfSearchTerms 234 | listOfPossibleMatches possibleMatches 235 | directoryTree mft.DirectoryTree 236 | } 237 | tests := []struct { 238 | name string 239 | args args 240 | wantFoundFilesList foundFiles 241 | }{ 242 | { 243 | name: "matches and no matches", 244 | wantFoundFilesList: foundFiles{ 245 | 0: foundFile{ 246 | dataRuns: nil, 247 | fullPath: `c:\exactmatch`, 248 | fileSize: 0, 249 | }, 250 | 1: foundFile{ 251 | dataRuns: nil, 252 | fullPath: `c:\regexmatch`, 253 | fileSize: 0, 254 | }, 255 | }, 256 | args: args{ 257 | listOfSearchKeywords: listOfSearchTerms{ 258 | 0: searchTerms{ 259 | fullPathString: `c:\exactmatch`, 260 | fullPathRegex: nil, 261 | fileNameString: "exactmatch", 262 | fileNameRegex: nil, 263 | }, 264 | 1: searchTerms{ 265 | fullPathString: "", 266 | fullPathRegex: regexp.MustCompile(`^c:\\regexmatch$`), 267 | fileNameString: "", 268 | fileNameRegex: regexp.MustCompile("^regexmatch$"), 269 | }, 270 | 2: searchTerms{ 271 | fullPathString: `c:\nomatch`, 272 | fullPathRegex: nil, 273 | fileNameString: "nomatch", 274 | fileNameRegex: nil, 275 | }, 276 | }, 277 | listOfPossibleMatches: possibleMatches{ 278 | 0: possibleMatch{ 279 | fileNameAttribute: mft.FileNameAttribute{ 280 | FnCreated: time.Time{}, 281 | FnModified: time.Time{}, 282 | FnAccessed: time.Time{}, 283 | FnChanged: time.Time{}, 284 | ParentDirRecordNumber: 5, 285 | LogicalFileSize: 0, 286 | PhysicalFileSize: 0, 287 | FileNameFlags: mft.FileNameFlags{}, 288 | FileNamespace: "WIN32", 289 | FileName: "exactmatch", 290 | }, 291 | dataRuns: nil, 292 | }, 293 | 1: possibleMatch{ 294 | fileNameAttribute: mft.FileNameAttribute{ 295 | FnCreated: time.Time{}, 296 | FnModified: time.Time{}, 297 | FnAccessed: time.Time{}, 298 | FnChanged: time.Time{}, 299 | ParentDirRecordNumber: 5, 300 | LogicalFileSize: 0, 301 | PhysicalFileSize: 0, 302 | FileNameFlags: mft.FileNameFlags{}, 303 | FileNamespace: "WIN32", 304 | FileName: "regexmatch", 305 | }, 306 | dataRuns: nil, 307 | }, 308 | 2: possibleMatch{ 309 | fileNameAttribute: mft.FileNameAttribute{ 310 | FnCreated: time.Time{}, 311 | FnModified: time.Time{}, 312 | FnAccessed: time.Time{}, 313 | FnChanged: time.Time{}, 314 | ParentDirRecordNumber: 7, 315 | LogicalFileSize: 0, 316 | PhysicalFileSize: 0, 317 | FileNameFlags: mft.FileNameFlags{}, 318 | FileNamespace: "WIN32", 319 | FileName: "exactmatch", // this wont be confirmed since parent dir record num is 7 not 5 320 | }, 321 | dataRuns: nil, 322 | }, 323 | 3: possibleMatch{ 324 | fileNameAttribute: mft.FileNameAttribute{ 325 | FnCreated: time.Time{}, 326 | FnModified: time.Time{}, 327 | FnAccessed: time.Time{}, 328 | FnChanged: time.Time{}, 329 | ParentDirRecordNumber: 6, 330 | LogicalFileSize: 0, 331 | PhysicalFileSize: 0, 332 | FileNameFlags: mft.FileNameFlags{}, 333 | FileNamespace: "WIN32", 334 | FileName: "exactmatch", // this wont be confirmed since parent dir record num is 6 not 5 335 | }, 336 | dataRuns: nil, 337 | }, 338 | }, 339 | directoryTree: mft.DirectoryTree{ 340 | 5: `c:`, 341 | 6: `d:`, 342 | }, 343 | }, 344 | }, 345 | } 346 | for _, tt := range tests { 347 | t.Run(tt.name, func(t *testing.T) { 348 | gotFoundFilesList := confirmFoundFiles(tt.args.listOfSearchKeywords, tt.args.listOfPossibleMatches, tt.args.directoryTree) 349 | if !reflect.DeepEqual(gotFoundFilesList, tt.wantFoundFilesList) { 350 | t.Errorf("confirmFoundFiles() gotFoundFilesList = %v, want %v", gotFoundFilesList, tt.wantFoundFilesList) 351 | } 352 | }) 353 | } 354 | } 355 | 356 | func Test_findPossibleMatches(t *testing.T) { 357 | type args struct { 358 | volumeHandler *VolumeHandler 359 | listOfSearchKeywords listOfSearchTerms 360 | } 361 | tests := []struct { 362 | name string 363 | args args 364 | wantListOfPossibleMatches possibleMatches 365 | wantDirectoryTree mft.DirectoryTree 366 | wantErr bool 367 | dummyFile string 368 | }{ 369 | { 370 | name: "find possible matches", 371 | args: args{ 372 | volumeHandler: &VolumeHandler{}, 373 | listOfSearchKeywords: listOfSearchTerms{ 374 | 0: searchTerms{ 375 | fullPathString: `c:\$mftmirr`, 376 | fullPathRegex: nil, 377 | fileNameString: "$mftmirr", 378 | fileNameRegex: nil, 379 | }, 380 | 1: searchTerms{ 381 | fullPathString: `c:\software`, 382 | fullPathRegex: nil, 383 | fileNameString: "software", 384 | fileNameRegex: nil, 385 | }, 386 | }, 387 | }, 388 | dummyFile: `test\testdata\dummyntfs`, 389 | wantErr: false, 390 | wantDirectoryTree: mft.DirectoryTree{ 391 | 5: `c:\`, 392 | 11: `c:\$Extend`, 393 | }, 394 | wantListOfPossibleMatches: possibleMatches{ 395 | 0: possibleMatch{ 396 | fileNameAttribute: mft.FileNameAttribute{ 397 | FnCreated: time.Date(2018, 2, 25, 00, 10, 45, 642455000, time.UTC), 398 | FnModified: time.Date(2018, 2, 25, 00, 10, 45, 642455000, time.UTC), 399 | FnAccessed: time.Date(2018, 2, 25, 00, 10, 45, 642455000, time.UTC), 400 | FnChanged: time.Date(2018, 2, 25, 00, 10, 45, 642455000, time.UTC), 401 | FlagResident: true, 402 | ParentDirRecordNumber: 5, 403 | ParentDirSequenceNumber: 5, 404 | LogicalFileSize: 4096, 405 | PhysicalFileSize: 4096, 406 | FileNameFlags: mft.FileNameFlags{ 407 | ReadOnly: false, 408 | Hidden: true, 409 | System: true, 410 | Archive: false, 411 | Device: false, 412 | Normal: false, 413 | Temporary: false, 414 | Sparse: false, 415 | Reparse: false, 416 | Compressed: false, 417 | Offline: false, 418 | NotContentIndexed: false, 419 | Encrypted: false, 420 | Directory: false, 421 | IndexView: false, 422 | }, 423 | AttributeSize: 112, 424 | FileNameLength: 16, 425 | FileNamespace: "WIN32 & DOS", 426 | FileName: "$MFTMirr", 427 | }, 428 | dataRuns: mft.DataRuns{ 429 | 0: mft.DataRun{ 430 | AbsoluteOffset: 8192, 431 | Length: 4096, 432 | }, 433 | }, 434 | }, 435 | 1: possibleMatch{ 436 | fileNameAttribute: mft.FileNameAttribute{ 437 | FnCreated: time.Date(2019, 8, 21, 6, 43, 46, 194743600, time.UTC), 438 | FnModified: time.Date(2019, 8, 21, 6, 43, 46, 194743600, time.UTC), 439 | FnAccessed: time.Date(2019, 8, 21, 6, 43, 46, 194743600, time.UTC), 440 | FnChanged: time.Date(2019, 8, 21, 6, 43, 46, 194743600, time.UTC), 441 | FlagResident: true, 442 | NameLength: mft.NameLength{ 443 | FlagNamed: false, 444 | NamedSize: 0, 445 | }, 446 | AttributeSize: 112, 447 | ParentDirRecordNumber: 506651, 448 | ParentDirSequenceNumber: 27, 449 | LogicalFileSize: 0, 450 | PhysicalFileSize: 0, 451 | FileNameFlags: mft.FileNameFlags{ 452 | ReadOnly: false, 453 | Hidden: false, 454 | System: false, 455 | Archive: true, 456 | Device: false, 457 | Normal: false, 458 | Temporary: false, 459 | Sparse: false, 460 | Reparse: false, 461 | Compressed: false, 462 | Offline: false, 463 | NotContentIndexed: false, 464 | Encrypted: false, 465 | Directory: false, 466 | IndexView: false, 467 | }, 468 | FileNameLength: 16, 469 | FileNamespace: "POSIX", 470 | FileName: "SOFTWARE", 471 | }, 472 | dataRuns: mft.DataRuns{}, 473 | }, 474 | }, 475 | }, 476 | } 477 | for _, tt := range tests { 478 | t.Run(tt.name, func(t *testing.T) { 479 | handle := dummyHandler{ 480 | Handle: nil, 481 | VolumeLetter: "", 482 | Vbr: vbr.VolumeBootRecord{}, 483 | mftReader: nil, 484 | lastReadVolumeOffset: 0, 485 | filePath: tt.dummyFile, 486 | } 487 | 488 | var err error 489 | *tt.args.volumeHandler, err = GetVolumeHandler("c", handle) 490 | if err != nil { 491 | log.Panic(err) 492 | } 493 | defer tt.args.volumeHandler.Handle.Close() 494 | 495 | mftRecord0, _ := parseMFTRecord0(tt.args.volumeHandler) 496 | _, _ = tt.args.volumeHandler.Handle.Seek(tt.args.volumeHandler.Vbr.MftByteOffset, 0) 497 | 498 | foundFile := foundFile{ 499 | dataRuns: mftRecord0.DataAttribute.NonResidentDataAttribute.DataRuns, 500 | fullPath: "$mft", 501 | } 502 | tt.args.volumeHandler.mftReader = rawFileReader(tt.args.volumeHandler, foundFile) 503 | 504 | gotListOfPossibleMatches, gotDirectoryTree, err := findPossibleMatches(tt.args.volumeHandler, tt.args.listOfSearchKeywords) 505 | if (err != nil) != tt.wantErr { 506 | t.Errorf("findPossibleMatches() error = %v, wantErr %v", err, tt.wantErr) 507 | return 508 | } 509 | if !reflect.DeepEqual(gotListOfPossibleMatches, tt.wantListOfPossibleMatches) { 510 | t.Errorf("findPossibleMatches() gotListOfPossibleMatches = %+v, want %+v", gotListOfPossibleMatches, tt.wantListOfPossibleMatches) 511 | } 512 | if !reflect.DeepEqual(gotDirectoryTree, tt.wantDirectoryTree) { 513 | t.Errorf("findPossibleMatches() gotDirectoryTree = %v, want %v", gotDirectoryTree, tt.wantDirectoryTree) 514 | } 515 | }) 516 | } 517 | } 518 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AlecRandazzo/Packrat 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/AlecRandazzo/MFT-Parser v0.6.6 7 | github.com/AlecRandazzo/VBR-Parser v1.1.3 8 | github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect 9 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 10 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect 11 | github.com/go-ole/go-ole v1.2.4 // indirect 12 | github.com/shirou/gopsutil v2.20.4+incompatible 13 | github.com/sirupsen/logrus v1.5.0 14 | golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 15 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 16 | gopkg.in/yaml.v2 v2.2.2 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AlecRandazzo/BinaryTransforms v1.3.5 h1:Nbx54afOidnnYmth92R7YJtfZLc7vuXh1IVEAlmms1I= 2 | github.com/AlecRandazzo/BinaryTransforms v1.3.5/go.mod h1:a+Gn6CWbYTn1I7ZTr1xI0HoW9cvYBshuS7t7vvCEhF0= 3 | github.com/AlecRandazzo/MFT-Parser v0.6.6 h1:ACeIDEEBm73Ei7Ojqi+FCt30jS2G83qV2W37dFPN8Dg= 4 | github.com/AlecRandazzo/MFT-Parser v0.6.6/go.mod h1:KBupbr1fcNpUu563/UHE75eWR40v4me1jDqW0/8DRIc= 5 | github.com/AlecRandazzo/Timestamp-Parser v1.4.3 h1:RC42wi1506BrA2QHh+OeGBIvKufCZDurhlKzOmig00A= 6 | github.com/AlecRandazzo/Timestamp-Parser v1.4.3/go.mod h1:ZXLMmRQYV7MhuEIeB9leqr60N47Pq7lAp0CBtYUY/UY= 7 | github.com/AlecRandazzo/VBR-Parser v1.1.3 h1:ivvcd2pbFoiB+mj0UTYCOTeyM44m8+jZQKSAXsnZ1Hc= 8 | github.com/AlecRandazzo/VBR-Parser v1.1.3/go.mod h1:Q1rS1PyUIPx0PZm2En2EHu3h49W8jQdm3EATb3RkkAI= 9 | github.com/Go-Forensics/BinaryTransforms v1.3.3 h1:W5ya4DS4hhjGE2DnrL7H5GpYu3pTCN8zwRI45Ltnklo= 10 | github.com/Go-Forensics/BinaryTransforms v1.3.3/go.mod h1:5WYD8lkQ4ISk9RSdHsB0pVl9RlAvw3NJQquWrLu2QV0= 11 | github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= 12 | github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= 13 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 14 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 15 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= 16 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= 21 | github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= 22 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 23 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 24 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 25 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/shirou/gopsutil v2.20.4+incompatible h1:cMT4rxS55zx9NVUnCkrmXCsEB/RNfG9SwHY9evtX8Ng= 29 | github.com/shirou/gopsutil v2.20.4+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 30 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 31 | github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= 32 | github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= 33 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 34 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 35 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 36 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 37 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 38 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 39 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w= 41 | golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 43 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 46 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 47 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 48 | -------------------------------------------------------------------------------- /keywords.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Alec Randazzo 2 | 3 | package packrat 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | // FileToExport is the file that you want to export. 13 | type FileToExport struct { 14 | FullPath string `yaml:"FullPath"` 15 | IsFullPathRegex bool `yaml:"IsFullPathRegex"` 16 | FileName string `yaml:"FileName"` 17 | IsFileNameRegex bool `yaml:"IsFileNameRegex"` 18 | } 19 | 20 | // ListOfFilesToExport is a slice of files that you want to export. 21 | type ListOfFilesToExport []FileToExport 22 | 23 | type searchTerms struct { 24 | fullPathString string 25 | fullPathRegex *regexp.Regexp 26 | fileNameString string 27 | fileNameRegex *regexp.Regexp 28 | } 29 | 30 | type listOfSearchTerms []searchTerms 31 | 32 | func setupSearchTerms(exportList ListOfFilesToExport) (listOfSearchKeywords listOfSearchTerms, err error) { 33 | for _, value := range exportList { 34 | // Sanity checking inputs 35 | if value.FileName == "" { 36 | err = errors.New("received empty filename string") 37 | return 38 | } else if value.FullPath == "" { 39 | err = errors.New("received empty filepath string") 40 | return 41 | } 42 | 43 | // Normalize everything 44 | value.FullPath = strings.ToLower(value.FullPath) 45 | value.FileName = strings.ToLower(value.FileName) 46 | 47 | if value.IsFullPathRegex == false && strings.HasSuffix(value.FullPath, `\`) == true { 48 | err = fmt.Errorf("file path '%s' has a trailing '\\'", value.FullPath) 49 | return 50 | } else if value.IsFullPathRegex == true && strings.HasSuffix(value.FullPath, `\`) == true { 51 | err = fmt.Errorf("file path '%s' has missing a trailing '\\\\'", value.FullPath) 52 | return 53 | } 54 | 55 | searchKeywords := searchTerms{} 56 | switch value.IsFullPathRegex { 57 | case false: 58 | searchKeywords.fullPathString = value.FullPath 59 | searchKeywords.fullPathRegex = nil 60 | case true: 61 | searchKeywords.fullPathString = "" 62 | searchKeywords.fullPathRegex = regexp.MustCompile(value.FullPath) 63 | } 64 | 65 | switch value.IsFileNameRegex { 66 | case false: 67 | searchKeywords.fileNameString = value.FileName 68 | searchKeywords.fileNameRegex = nil 69 | case true: 70 | searchKeywords.fileNameString = "" 71 | searchKeywords.fileNameRegex = regexp.MustCompile(value.FileName) 72 | } 73 | 74 | listOfSearchKeywords = append(listOfSearchKeywords, searchKeywords) 75 | } 76 | 77 | return 78 | } 79 | -------------------------------------------------------------------------------- /keywords_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Alec Randazzo 2 | 3 | package packrat 4 | 5 | import ( 6 | "reflect" 7 | "regexp" 8 | "testing" 9 | ) 10 | 11 | func Test_setupSearchTerms(t *testing.T) { 12 | type args struct { 13 | exportList ListOfFilesToExport 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | wantListOfSearchKeywords listOfSearchTerms 19 | wantErr bool 20 | }{ 21 | { 22 | name: "this should be successful", 23 | args: args{exportList: ListOfFilesToExport{ 24 | 0: FileToExport{ 25 | FullPath: `C:\windows`, 26 | IsFullPathRegex: false, 27 | FileName: "fake.exe", 28 | IsFileNameRegex: false, 29 | }, 30 | 1: FileToExport{ 31 | FullPath: `C:\\windows\\.*`, 32 | IsFullPathRegex: true, 33 | FileName: `.*\.evtx`, 34 | IsFileNameRegex: true, 35 | }, 36 | }}, 37 | wantErr: false, 38 | wantListOfSearchKeywords: listOfSearchTerms{ 39 | 0: searchTerms{ 40 | fullPathString: `c:\windows`, 41 | fullPathRegex: nil, 42 | fileNameString: "fake.exe", 43 | fileNameRegex: nil, 44 | }, 45 | 1: searchTerms{ 46 | fullPathString: "", 47 | fullPathRegex: regexp.MustCompile(`c:\\windows\\.*`), 48 | fileNameString: "", 49 | fileNameRegex: regexp.MustCompile(`.*\.evtx`), 50 | }, 51 | }, 52 | }, 53 | { 54 | name: "empty filepath string", 55 | args: args{exportList: ListOfFilesToExport{ 56 | 0: FileToExport{ 57 | FullPath: "", 58 | IsFullPathRegex: false, 59 | FileName: "blah.exe", 60 | IsFileNameRegex: false, 61 | }, 62 | }}, 63 | wantErr: true, 64 | wantListOfSearchKeywords: nil, 65 | }, 66 | { 67 | name: "empty filename string", 68 | args: args{exportList: ListOfFilesToExport{ 69 | 0: FileToExport{ 70 | FullPath: `C:\windows`, 71 | IsFullPathRegex: false, 72 | FileName: "", 73 | IsFileNameRegex: false, 74 | }, 75 | }}, 76 | wantErr: true, 77 | wantListOfSearchKeywords: nil, 78 | }, 79 | { 80 | name: "trailing slash in file path non regex", 81 | args: args{exportList: ListOfFilesToExport{ 82 | 0: FileToExport{ 83 | FullPath: `C:\windows\`, 84 | IsFullPathRegex: false, 85 | FileName: "whoa.exe", 86 | IsFileNameRegex: false, 87 | }, 88 | }}, 89 | wantErr: true, 90 | wantListOfSearchKeywords: nil, 91 | }, 92 | { 93 | name: "trailing slash in file path regex", 94 | args: args{exportList: ListOfFilesToExport{ 95 | 0: FileToExport{ 96 | FullPath: `C:\windows\`, 97 | IsFullPathRegex: true, 98 | FileName: "whoa.exe", 99 | IsFileNameRegex: false, 100 | }, 101 | }}, 102 | wantErr: true, 103 | wantListOfSearchKeywords: nil, 104 | }, 105 | } 106 | for _, tt := range tests { 107 | t.Run(tt.name, func(t *testing.T) { 108 | gotListOfSearchKeywords, err := setupSearchTerms(tt.args.exportList) 109 | if (err != nil) != tt.wantErr { 110 | t.Errorf("setupSearchTerms() error = %v, wantErr %v", err, tt.wantErr) 111 | return 112 | } 113 | if !reflect.DeepEqual(gotListOfSearchKeywords, tt.wantListOfSearchKeywords) { 114 | t.Errorf("setupSearchTerms() gotListOfSearchKeywords = %v, want %v", gotListOfSearchKeywords, tt.wantListOfSearchKeywords) 115 | } 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /mft.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Alec Randazzo 2 | 3 | package packrat 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | mft "github.com/AlecRandazzo/MFT-Parser" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func parseMFTRecord0(volume *VolumeHandler) (mftRecord0 mft.MasterFileTableRecord, err error) { 13 | // Move handle pointer back to beginning of volume 14 | _, err = volume.Handle.Seek(0x00, 0) 15 | if err != nil { 16 | err = fmt.Errorf("failed to see back to volume offset 0x00: %w", err) 17 | return 18 | } 19 | 20 | // Seek to the offset where the MFT starts. If it errors, bomb. 21 | _, err = volume.Handle.Seek(volume.Vbr.MftByteOffset, 0) 22 | if err != nil { 23 | err = fmt.Errorf("failed to seek to mft: %w", err) 24 | return 25 | } 26 | 27 | // Read the first entry in the MFT. The first record in the MFT always is for the MFT itself. If it errors, bomb. 28 | buffer := make([]byte, volume.Vbr.MftRecordSize) 29 | _, err = volume.Handle.Read(buffer) 30 | if err != nil { 31 | err = fmt.Errorf("failed to read the mft: %w", err) 32 | return 33 | } 34 | 35 | // Sanity check that this is indeed an mft record 36 | result, err := mft.RawMasterFileTableRecord(buffer).IsThisAnMftRecord() 37 | if err != nil { 38 | err = fmt.Errorf("IsThisAnMftRecord() returned an error: %v", err) 39 | } else if result == false { 40 | err = errors.New("VolumeHandler.parseMFTRecord0() received an invalid mft record") 41 | return 42 | } 43 | 44 | // Parse the MFT record 45 | 46 | mftRecord0, err = mft.RawMasterFileTableRecord(buffer).Parse(volume.Vbr.BytesPerCluster) 47 | if err != nil { 48 | err = fmt.Errorf("VolumeHandler.parseMFTRecord0() failed to parse the mft's mft record: %w", err) 49 | return 50 | } 51 | log.Debugf("Identified the following data runs for the MFT itself: %+v", mftRecord0.DataAttribute.NonResidentDataAttribute.DataRuns) 52 | 53 | return 54 | } 55 | -------------------------------------------------------------------------------- /mft_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Alec Randazzo 2 | 3 | package packrat 4 | 5 | import ( 6 | mft "github.com/AlecRandazzo/MFT-Parser" 7 | vbr "github.com/AlecRandazzo/VBR-Parser" 8 | "log" 9 | "reflect" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func Test_parseMFTRecord0(t *testing.T) { 15 | type args struct { 16 | volume *VolumeHandler 17 | } 18 | tests := []struct { 19 | name string 20 | args args 21 | wantMftRecord0 mft.MasterFileTableRecord 22 | wantErr bool 23 | dummyFile string 24 | }{ 25 | { 26 | name: "test1", 27 | dummyFile: `test\testdata\dummyntfs`, 28 | wantErr: false, 29 | wantMftRecord0: mft.MasterFileTableRecord{ 30 | RecordHeader: mft.RecordHeader{ 31 | AttributesOffset: 56, 32 | RecordNumber: 0, 33 | Flags: mft.RecordHeaderFlags{ 34 | FlagDeleted: false, 35 | FlagDirectory: false, 36 | }, 37 | }, 38 | StandardInformationAttributes: mft.StandardInformationAttribute{ 39 | SiCreated: time.Date(2018, 2, 25, 0, 10, 45, 642455000, time.UTC), 40 | SiModified: time.Date(2018, 2, 25, 0, 10, 45, 642455000, time.UTC), 41 | SiAccessed: time.Date(2018, 2, 25, 0, 10, 45, 642455000, time.UTC), 42 | SiChanged: time.Date(2018, 2, 25, 0, 10, 45, 642455000, time.UTC), 43 | FlagResident: true, 44 | }, 45 | FileNameAttributes: mft.FileNameAttributes{ 46 | 0: mft.FileNameAttribute{ 47 | FnCreated: time.Date(2018, 2, 25, 0, 10, 45, 642455000, time.UTC), 48 | FnModified: time.Date(2018, 2, 25, 0, 10, 45, 642455000, time.UTC), 49 | FnAccessed: time.Date(2018, 2, 25, 0, 10, 45, 642455000, time.UTC), 50 | FnChanged: time.Date(2018, 2, 25, 0, 10, 45, 642455000, time.UTC), 51 | FlagResident: true, 52 | NameLength: mft.NameLength{ 53 | FlagNamed: false, 54 | NamedSize: 0, 55 | }, 56 | AttributeSize: 104, 57 | ParentDirRecordNumber: 5, 58 | ParentDirSequenceNumber: 5, 59 | LogicalFileSize: 16384, 60 | PhysicalFileSize: 16384, 61 | FileNameFlags: mft.FileNameFlags{ 62 | ReadOnly: false, 63 | Hidden: true, 64 | System: true, 65 | Archive: false, 66 | Device: false, 67 | Normal: false, 68 | Temporary: false, 69 | Sparse: false, 70 | Reparse: false, 71 | Compressed: false, 72 | Offline: false, 73 | NotContentIndexed: false, 74 | Encrypted: false, 75 | Directory: false, 76 | IndexView: false, 77 | }, 78 | FileNameLength: 8, 79 | FileNamespace: "WIN32 & DOS", 80 | FileName: "$MFT", 81 | }, 82 | }, 83 | DataAttribute: mft.DataAttribute{ 84 | TotalSize: 0, 85 | FlagResident: false, 86 | ResidentDataAttribute: nil, 87 | NonResidentDataAttribute: mft.NonResidentDataAttribute{ 88 | DataRuns: mft.DataRuns{ 89 | 0: mft.DataRun{ 90 | AbsoluteOffset: 4096, 91 | Length: 32768, 92 | }, 93 | }, 94 | }, 95 | }, 96 | AttributeList: nil, 97 | }, 98 | args: args{volume: &VolumeHandler{}}, 99 | }, 100 | } 101 | for _, tt := range tests { 102 | t.Run(tt.name, func(t *testing.T) { 103 | handle := dummyHandler{ 104 | Handle: nil, 105 | VolumeLetter: "", 106 | Vbr: vbr.VolumeBootRecord{}, 107 | mftReader: nil, 108 | lastReadVolumeOffset: 0, 109 | filePath: tt.dummyFile, 110 | } 111 | var err error 112 | *tt.args.volume, err = GetVolumeHandler("c", handle) 113 | if err != nil { 114 | log.Panic(err) 115 | } 116 | defer tt.args.volume.Handle.Close() 117 | gotMftRecord0, err := parseMFTRecord0(tt.args.volume) 118 | if (err != nil) != tt.wantErr { 119 | t.Errorf("parseMFTRecord0() error = %v, wantErr %v", err, tt.wantErr) 120 | return 121 | } 122 | if !reflect.DeepEqual(gotMftRecord0, tt.wantMftRecord0) { 123 | t.Errorf("parseMFTRecord0() gotMftRecord0 = %v, want %v", gotMftRecord0, tt.wantMftRecord0) 124 | } 125 | }) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /readers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Alec Randazzo 2 | 3 | package packrat 4 | 5 | import ( 6 | mft "github.com/AlecRandazzo/MFT-Parser" 7 | log "github.com/sirupsen/logrus" 8 | "io" 9 | "os" 10 | ) 11 | 12 | // dataRunsReader contains all the information needed to support the data runs reader function 13 | type dataRunsReader struct { 14 | VolumeHandler *VolumeHandler 15 | DataRuns mft.DataRuns 16 | fileName string 17 | dataRunTracker int 18 | dataRunBytesLeftToReadTracker int64 19 | totalFileSize int64 20 | totalByesRead int64 21 | initialized bool 22 | } 23 | 24 | func (dataRunReader *dataRunsReader) Read(byteSliceToPopulate []byte) (numberOfBytesRead int, err error) { 25 | bufferSize := int64(len(byteSliceToPopulate)) 26 | 27 | // Sanity checking 28 | if len(dataRunReader.DataRuns) == 0 { 29 | err = io.ErrUnexpectedEOF 30 | log.Warnf("failed to read %s, received: %v", dataRunReader.fileName, err) 31 | return 32 | } 33 | 34 | // Check if this reader has been initialized, if not, do so. 35 | if dataRunReader.initialized != true { 36 | if dataRunReader.totalFileSize == 0 { 37 | for _, dataRun := range dataRunReader.DataRuns { 38 | dataRunReader.totalFileSize += dataRun.Length 39 | } 40 | } 41 | dataRunReader.dataRunTracker = 0 42 | dataRunReader.dataRunBytesLeftToReadTracker = dataRunReader.DataRuns[dataRunReader.dataRunTracker].Length 43 | dataRunReader.VolumeHandler.lastReadVolumeOffset, _ = dataRunReader.VolumeHandler.Handle.Seek(dataRunReader.DataRuns[dataRunReader.dataRunTracker].AbsoluteOffset, 0) 44 | dataRunReader.VolumeHandler.lastReadVolumeOffset -= bufferSize 45 | dataRunReader.initialized = true 46 | 47 | // These are for debug purposes 48 | if log.GetLevel() == log.DebugLevel { 49 | totalSize := int64(0) 50 | for _, dataRun := range dataRunReader.DataRuns { 51 | totalSize += dataRun.Length 52 | } 53 | log.Debugf("Reading data run number 1 of %d for file '%s' which has a length of %d bytes at absolute offset %d", 54 | len(dataRunReader.DataRuns), 55 | dataRunReader.fileName, 56 | totalSize, 57 | dataRunReader.DataRuns[0].AbsoluteOffset, 58 | ) 59 | } 60 | 61 | } 62 | 63 | // Figure out how many bytes are left to read 64 | if dataRunReader.dataRunBytesLeftToReadTracker-bufferSize == 0 { 65 | dataRunReader.dataRunBytesLeftToReadTracker -= bufferSize 66 | } else if dataRunReader.dataRunBytesLeftToReadTracker-bufferSize < 0 { 67 | bufferSize = dataRunReader.dataRunBytesLeftToReadTracker 68 | dataRunReader.dataRunBytesLeftToReadTracker = 0 69 | } else { 70 | dataRunReader.dataRunBytesLeftToReadTracker -= bufferSize 71 | } 72 | 73 | // Read from the data run 74 | if dataRunReader.totalByesRead+bufferSize > dataRunReader.totalFileSize { 75 | bufferSize = dataRunReader.totalFileSize - dataRunReader.totalByesRead 76 | } 77 | buffer := make([]byte, bufferSize) 78 | dataRunReader.VolumeHandler.lastReadVolumeOffset += bufferSize 79 | numberOfBytesRead, _ = dataRunReader.VolumeHandler.Handle.Read(buffer) 80 | copy(byteSliceToPopulate, buffer) 81 | dataRunReader.totalByesRead += bufferSize 82 | if dataRunReader.totalFileSize == dataRunReader.totalByesRead { 83 | err = io.EOF 84 | return 85 | } 86 | 87 | // Check to see if there are any bytes left to read in the current data run 88 | if dataRunReader.dataRunBytesLeftToReadTracker == 0 { 89 | // Increment our tracker 90 | dataRunReader.dataRunTracker++ 91 | 92 | // Get the size of the next datarun 93 | dataRunReader.dataRunBytesLeftToReadTracker = dataRunReader.DataRuns[dataRunReader.dataRunTracker].Length 94 | 95 | // Seek to the offset of the next datarun 96 | dataRunReader.VolumeHandler.lastReadVolumeOffset, _ = dataRunReader.VolumeHandler.Handle.Seek(dataRunReader.DataRuns[dataRunReader.dataRunTracker].AbsoluteOffset, 0) 97 | dataRunReader.VolumeHandler.lastReadVolumeOffset -= bufferSize 98 | 99 | log.Debugf("Reading data run number %d of %d for file '%s' which has a length of %d bytes at absolute offset %d", 100 | dataRunReader.dataRunTracker+1, 101 | len(dataRunReader.DataRuns), 102 | dataRunReader.fileName, 103 | dataRunReader.DataRuns[dataRunReader.dataRunTracker].Length, 104 | dataRunReader.VolumeHandler.lastReadVolumeOffset+bufferSize, 105 | ) 106 | } 107 | 108 | return 109 | } 110 | 111 | func apiFileReader(file foundFile) (reader io.Reader, err error) { 112 | reader, err = os.Open(file.fullPath) 113 | return 114 | } 115 | 116 | func rawFileReader(handler *VolumeHandler, file foundFile) (reader io.Reader) { 117 | reader = &dataRunsReader{ 118 | VolumeHandler: handler, 119 | DataRuns: file.dataRuns, 120 | fileName: file.fullPath, 121 | dataRunTracker: 0, 122 | dataRunBytesLeftToReadTracker: 0, 123 | totalFileSize: file.fileSize, 124 | initialized: false, 125 | } 126 | return 127 | } 128 | -------------------------------------------------------------------------------- /readers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Alec Randazzo 2 | 3 | package packrat 4 | 5 | import ( 6 | mft "github.com/AlecRandazzo/MFT-Parser" 7 | vbr "github.com/AlecRandazzo/VBR-Parser" 8 | log "github.com/sirupsen/logrus" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | func TestDataRunsReader_Read(t *testing.T) { 14 | type fields struct { 15 | VolumeHandler *VolumeHandler 16 | DataRuns mft.DataRuns 17 | fileName string 18 | dataRunTracker int 19 | dataRunBytesLeftToReadTracker int64 20 | totalFileSize int64 21 | totalByesRead int64 22 | initialized bool 23 | } 24 | type args struct { 25 | byteSliceToPopulate []byte 26 | } 27 | tests := []struct { 28 | name string 29 | fields fields 30 | args args 31 | logLevel log.Level 32 | dummy dummyHandler 33 | dummyFile string 34 | wantBytes []byte 35 | }{ 36 | { 37 | name: "fake data run", 38 | logLevel: log.DebugLevel, 39 | dummy: dummyHandler{ 40 | Handle: nil, 41 | VolumeLetter: "c", 42 | Vbr: vbr.VolumeBootRecord{ 43 | VolumeLetter: "c", 44 | BytesPerSector: 0, 45 | SectorsPerCluster: 0, 46 | BytesPerCluster: 0, 47 | MftByteOffset: 0, 48 | MftRecordSize: 0, 49 | ClustersPerIndexRecord: 0, 50 | }, 51 | mftReader: nil, 52 | lastReadVolumeOffset: 0, 53 | filePath: `test\testdata\dummyntfs`, 54 | }, 55 | fields: fields{ 56 | VolumeHandler: &VolumeHandler{}, 57 | DataRuns: mft.DataRuns{ 58 | 0: mft.DataRun{ 59 | AbsoluteOffset: 0, 60 | Length: 2048, 61 | }, 62 | 1: mft.DataRun{ 63 | AbsoluteOffset: 2048, 64 | Length: 4096, 65 | }, 66 | }, 67 | fileName: "blah", 68 | dataRunTracker: 0, 69 | dataRunBytesLeftToReadTracker: 0, 70 | totalFileSize: 4096, 71 | totalByesRead: 0, 72 | initialized: false, 73 | }, 74 | wantBytes: []byte{235, 82, 144, 78, 84, 70, 83, 32, 32, 32, 32, 0, 2, 8, 0, 0, 0, 0, 0, 0, 0, 248, 0, 0, 63, 0, 255, 0, 0, 48, 17, 0, 0, 0, 0, 0, 128, 0, 128, 0, 255, 31, 11, 29, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 246, 0, 0, 0, 1, 0, 0, 0, 91, 57, 21, 172, 133, 21, 172, 126, 0, 0, 0, 0, 250, 51, 192, 142, 208, 188, 0, 124, 251, 104, 192, 7, 31, 30, 104, 102, 0, 203, 136, 22, 14, 0, 102, 129, 62, 3, 0, 78, 84, 70, 83, 117, 21, 180, 65, 187, 170, 85, 205, 19, 114, 12, 129, 251, 85, 170, 117, 6, 247, 193, 1, 0, 117, 3, 233, 221, 0, 30, 131, 236, 24, 104, 26, 0, 180, 72, 138, 22, 14, 0, 139, 244, 22, 31, 205, 19, 159, 131, 196, 24, 158, 88, 31, 114, 225, 59, 6, 11, 0, 117, 219, 163, 15, 0, 193, 46, 15, 0, 4, 30, 90, 51, 219, 185, 0, 32, 43, 200, 102, 255, 6, 17, 0, 3, 22, 15, 0, 142, 194, 255, 6, 22, 0, 232, 75, 0, 43, 200, 119, 239, 184, 0, 187, 205, 26, 102, 35, 192, 117, 45, 102, 129, 251, 84, 67, 80, 65, 117, 36, 129, 249, 2, 1, 114, 30, 22, 104, 7, 187, 22, 104, 82, 17, 22, 104, 9, 0, 102, 83, 102, 83, 102, 85, 22, 22, 22, 104, 184, 1, 102, 97, 14, 7, 205, 26, 51, 192, 191, 10, 19, 185, 246, 12, 252, 243, 170, 233, 254, 1, 144, 144, 102, 96, 30, 6, 102, 161, 17, 0, 102, 3, 6, 28, 0, 30, 102, 104, 0, 0, 0, 0, 102, 80, 6, 83, 104, 1, 0, 104, 16, 0, 180, 66, 138, 22, 14, 0, 22, 31, 139, 244, 205, 19, 102, 89, 91, 90, 102, 89, 102, 89, 31, 15, 130, 22, 0, 102, 255, 6, 17, 0, 3, 22, 15, 0, 142, 194, 255, 14, 22, 0, 117, 188, 7, 31, 102, 97, 195, 161, 246, 1, 232, 9, 0, 161, 250, 1, 232, 3, 0, 244, 235, 253, 139, 240, 172, 60, 0, 116, 9, 180, 14, 187, 7, 0, 205, 16, 235, 242, 195, 13, 10, 65, 32, 100, 105, 115, 107, 32, 114, 101, 97, 100, 32, 101, 114, 114, 111, 114, 32, 111, 99, 99, 117, 114, 114, 101, 100, 0, 13, 10, 66, 79, 79, 84, 77, 71, 82, 32, 105, 115, 32, 99, 111, 109, 112, 114, 101, 115, 115, 101, 100, 0, 13, 10, 80, 114, 101, 115, 115, 32, 67, 116, 114, 108, 43, 65, 108, 116, 43, 68, 101, 108, 32, 116, 111, 32, 114, 101, 115, 116, 97, 114, 116, 13, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 138, 1, 167, 1, 191, 1, 0, 0, 85, 170, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 75 | }, 76 | } 77 | for _, tt := range tests { 78 | t.Run(tt.name, func(t *testing.T) { 79 | if tt.logLevel == log.DebugLevel { 80 | log.SetLevel(tt.logLevel) 81 | } 82 | handler, _ := GetVolumeHandler("c", tt.dummy) 83 | defer handler.Handle.Close() 84 | dataRunReader := &dataRunsReader{ 85 | VolumeHandler: &handler, 86 | DataRuns: tt.fields.DataRuns, 87 | fileName: tt.fields.fileName, 88 | dataRunTracker: tt.fields.dataRunTracker, 89 | dataRunBytesLeftToReadTracker: tt.fields.dataRunBytesLeftToReadTracker, 90 | totalFileSize: tt.fields.totalFileSize, 91 | totalByesRead: tt.fields.totalByesRead, 92 | initialized: tt.fields.initialized, 93 | } 94 | tt.args.byteSliceToPopulate = make([]byte, 1024) 95 | gotBytes := make([]byte, 0) 96 | for { 97 | _, err := dataRunReader.Read(tt.args.byteSliceToPopulate) 98 | if err != nil { 99 | break 100 | } 101 | gotBytes = append(gotBytes, tt.args.byteSliceToPopulate...) 102 | } 103 | if !reflect.DeepEqual(gotBytes, tt.wantBytes) { 104 | t.Errorf("TestDataRunsReader_Read() gotBytes = %v, want %v", gotBytes, tt.wantBytes) 105 | } 106 | 107 | }) 108 | } 109 | } 110 | 111 | func Test_apiFileReader(t *testing.T) { 112 | type args struct { 113 | file foundFile 114 | } 115 | tests := []struct { 116 | name string 117 | args args 118 | wantErr bool 119 | }{ 120 | { 121 | name: "found file", 122 | args: args{file: foundFile{ 123 | dataRuns: nil, 124 | fullPath: `test\testdata\dummyntfs`, 125 | fileSize: 0, 126 | }}, 127 | wantErr: false, 128 | }, 129 | } 130 | for _, tt := range tests { 131 | t.Run(tt.name, func(t *testing.T) { 132 | _, err := apiFileReader(tt.args.file) 133 | if (err != nil) != tt.wantErr { 134 | t.Errorf("apiFileReader() error = %v, wantErr %v", err, tt.wantErr) 135 | return 136 | } 137 | }) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /test/testdata/dummyntfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecRandazzo/Packrat/75bb7df6883abd04f855970743c4b32c77e77de0/test/testdata/dummyntfs -------------------------------------------------------------------------------- /test/testdata/dummyntfs-badvbr1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecRandazzo/Packrat/75bb7df6883abd04f855970743c4b32c77e77de0/test/testdata/dummyntfs-badvbr1 -------------------------------------------------------------------------------- /test/testdata/dummyntfs-badvbr2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlecRandazzo/Packrat/75bb7df6883abd04f855970743c4b32c77e77de0/test/testdata/dummyntfs-badvbr2 -------------------------------------------------------------------------------- /volume.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Alec Randazzo 2 | 3 | package packrat 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | vbr "github.com/AlecRandazzo/VBR-Parser" 9 | log "github.com/sirupsen/logrus" 10 | syscall "golang.org/x/sys/windows" 11 | "io" 12 | "os" 13 | "regexp" 14 | "strings" 15 | "unicode" 16 | ) 17 | 18 | type handler interface { 19 | GetHandle(volumeLetter string) (handle *os.File, err error) 20 | } 21 | 22 | // VolumeHandler contains everything needed for basic collection functionality 23 | type VolumeHandler struct { 24 | Handle *os.File 25 | VolumeLetter string 26 | Vbr vbr.VolumeBootRecord 27 | mftReader io.Reader 28 | lastReadVolumeOffset int64 29 | } 30 | 31 | // GetHandle will get a file handle to the underlying NTFS volume. We need this in order to bypass file locks. 32 | func (volume VolumeHandler) GetHandle(volumeLetter string) (handle *os.File, err error) { 33 | dwDesiredAccess := uint32(0x80000000) //0x80 FILE_READ_ATTRIBUTES 34 | dwShareMode := uint32(0x02 | 0x01) 35 | dwCreationDisposition := uint32(0x03) 36 | dwFlagsAndAttributes := uint32(0x00) 37 | 38 | volumePath, _ := syscall.UTF16PtrFromString(fmt.Sprintf("\\\\.\\%s:", volumeLetter)) 39 | syscallHandle, err := syscall.CreateFile(volumePath, dwDesiredAccess, dwShareMode, nil, dwCreationDisposition, dwFlagsAndAttributes, 0) 40 | if err != nil { 41 | err = fmt.Errorf("getHandle() failed to get handle to volume %s: %w", volumeLetter, err) 42 | return 43 | } 44 | handle = os.NewFile(uintptr(syscallHandle), "") 45 | return 46 | } 47 | 48 | // GetVolumeHandler gets a file handle to the specified volume and parses its volume boot record. 49 | func GetVolumeHandler(volumeLetter string, handler handler) (volume VolumeHandler, err error) { 50 | const volumeBootRecordSize = 512 51 | volume.VolumeLetter = volumeLetter 52 | volume.Handle, err = handler.GetHandle(volumeLetter) 53 | if err != nil { 54 | err = fmt.Errorf("GetVolumeHandler() failed to get handle to volume %s: %w", volumeLetter, err) 55 | return 56 | } 57 | 58 | // Parse the VBR to get details we need about the volume. 59 | volumeBootRecord := make([]byte, volumeBootRecordSize) 60 | _, err = volume.Handle.Read(volumeBootRecord) 61 | if err != nil { 62 | err = fmt.Errorf("GetVolumeHandler() failed to read the volume boot record on volume %v: %w", volumeLetter, err) 63 | return 64 | } 65 | volume.Vbr, err = vbr.RawVolumeBootRecord(volumeBootRecord).Parse() 66 | if err != nil { 67 | err = fmt.Errorf("GetVolumeHandler() failed to parse vbr from volume letter %s: %w", volumeLetter, err) 68 | return 69 | } 70 | log.Debugf("Successfully got a file handle to volume %v and read its volume boot record.", volumeLetter) 71 | return 72 | } 73 | 74 | func isLetter(s string) (result bool, err error) { 75 | // Sanity checking 76 | if s == "" { 77 | err = errors.New("isLetter() received a null string") 78 | return 79 | } else if len(s) > 1 { 80 | err = fmt.Errorf("isLetter() received the string %s which is too many letters, function expected a single letter", s) 81 | return 82 | } 83 | 84 | for _, r := range s { 85 | if unicode.IsLetter(r) { 86 | result = true 87 | return 88 | } 89 | } 90 | 91 | result = false 92 | return 93 | } 94 | 95 | func identifyVolumesOfInterest(exportList *ListOfFilesToExport) (volumesOfInterest []string, err error) { 96 | volumesOfInterest = make([]string, 0) 97 | re := regexp.MustCompile(`[^:]+`) 98 | for index, fileToExport := range *exportList { 99 | volume := re.FindString(strings.ToLower(fileToExport.FullPath)) 100 | if volume == "%systemdrive%" { 101 | systemDrive := os.Getenv("SYSTEMDRIVE") 102 | volume = re.FindString(systemDrive) 103 | (*exportList)[index].FullPath = strings.Replace(strings.ToLower(fileToExport.FullPath), "%systemdrive%", volume, -1) 104 | } else { 105 | var result bool 106 | result, err = isLetter(volume) 107 | if err != nil { 108 | err = fmt.Errorf("isLetter() returned an error: %w", err) 109 | volumesOfInterest = nil 110 | return 111 | } else if result == false { 112 | err = fmt.Errorf("isLetter() indicated that the full path string %s does not start with a letter", fileToExport.FullPath) 113 | volumesOfInterest = nil 114 | return 115 | } 116 | } 117 | 118 | isTracked := false 119 | for _, trackedVolumes := range volumesOfInterest { 120 | if trackedVolumes == volume { 121 | isTracked = true 122 | break 123 | } 124 | } 125 | 126 | if isTracked == true { 127 | continue 128 | } else { 129 | volumesOfInterest = append(volumesOfInterest, volume) 130 | } 131 | } 132 | 133 | return 134 | } 135 | -------------------------------------------------------------------------------- /volume_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Alec Randazzo 2 | 3 | package packrat 4 | 5 | import ( 6 | "errors" 7 | vbr "github.com/AlecRandazzo/VBR-Parser" 8 | "io" 9 | "os" 10 | "reflect" 11 | "testing" 12 | ) 13 | 14 | func TestGetVolumeHandler(t *testing.T) { 15 | type args struct { 16 | volumeLetter string 17 | volumeHandler dummyHandler 18 | } 19 | tests := []struct { 20 | name string 21 | args args 22 | wantVBR vbr.VolumeBootRecord 23 | wantErr bool 24 | filePath string 25 | }{ 26 | { 27 | name: "success", 28 | args: args{ 29 | volumeLetter: "C", 30 | volumeHandler: dummyHandler{}, 31 | }, 32 | wantVBR: vbr.VolumeBootRecord{ 33 | VolumeLetter: "", 34 | BytesPerSector: 512, 35 | SectorsPerCluster: 8, 36 | BytesPerCluster: 4096, 37 | MftByteOffset: 4096, 38 | MftRecordSize: 1024, 39 | ClustersPerIndexRecord: 1, 40 | }, 41 | wantErr: false, 42 | filePath: `test\testdata\dummyntfs`, 43 | }, 44 | { 45 | name: "bad volume letter", 46 | args: args{ 47 | volumeLetter: "error", 48 | volumeHandler: dummyHandler{}, 49 | }, 50 | wantErr: true, 51 | filePath: `test\testdata\dummyntfs`, 52 | }, 53 | { 54 | name: "bad vbr1", 55 | args: args{ 56 | volumeLetter: "C", 57 | volumeHandler: dummyHandler{}, 58 | }, 59 | wantErr: true, 60 | filePath: `test\testdata\dummyntfs-badvbr1`, 61 | }, 62 | { 63 | name: "bad vbr2", 64 | args: args{ 65 | volumeLetter: "C", 66 | volumeHandler: dummyHandler{}, 67 | }, 68 | wantErr: true, 69 | filePath: `test\testdata\dummyntfs-badvbr2`, 70 | }, 71 | } 72 | for _, tt := range tests { 73 | t.Run(tt.name, func(t *testing.T) { 74 | tt.args.volumeHandler.filePath = tt.filePath 75 | gotVolume, err := GetVolumeHandler(tt.args.volumeLetter, tt.args.volumeHandler) 76 | if (err != nil) != tt.wantErr { 77 | t.Errorf("GetVolumeHandler() error = %v, wantErr %v", err, tt.wantErr) 78 | return 79 | } 80 | if !reflect.DeepEqual(gotVolume.Vbr, tt.wantVBR) { 81 | t.Errorf("GetVolumeHandler() gotVBR = %+v, want %+v", gotVolume.Vbr, tt.wantVBR) 82 | } 83 | }) 84 | } 85 | } 86 | 87 | type dummyHandler struct { 88 | Handle *os.File 89 | VolumeLetter string 90 | Vbr vbr.VolumeBootRecord 91 | mftReader io.Reader 92 | lastReadVolumeOffset int64 93 | filePath string 94 | } 95 | 96 | func (dummy dummyHandler) GetHandle(volumeLetter string) (handle *os.File, err error) { 97 | if volumeLetter == "error" { 98 | err = errors.New("faux error") 99 | return 100 | } 101 | handle, _ = os.Open(dummy.filePath) 102 | return 103 | } 104 | 105 | func Test_GetHandle(t *testing.T) { 106 | type args struct { 107 | volumeLetter string 108 | } 109 | tests := []struct { 110 | name string 111 | args args 112 | volume VolumeHandler 113 | wantErr bool 114 | }{ 115 | { 116 | name: "no error", 117 | args: args{volumeLetter: "C"}, 118 | wantErr: false, 119 | }, 120 | { 121 | name: "nil string input", 122 | args: args{volumeLetter: ""}, 123 | wantErr: true, 124 | }, 125 | { 126 | name: "bad input", 127 | args: args{volumeLetter: "CD"}, 128 | wantErr: true, 129 | }, 130 | } 131 | for _, tt := range tests { 132 | t.Run(tt.name, func(t *testing.T) { 133 | _, err := tt.volume.GetHandle(tt.args.volumeLetter) 134 | if (err != nil) != tt.wantErr { 135 | t.Errorf("getHandle() error = %v, wantErr %v", err, tt.wantErr) 136 | return 137 | } 138 | }) 139 | } 140 | } 141 | 142 | func Test_identifyVolumesOfInterest(t *testing.T) { 143 | type args struct { 144 | exportList *ListOfFilesToExport 145 | } 146 | tests := []struct { 147 | name string 148 | args args 149 | wantVolumesOfInterest []string 150 | wantErr bool 151 | }{ 152 | { 153 | name: "systemdrive and d", 154 | args: args{exportList: &ListOfFilesToExport{ 155 | 0: FileToExport{ 156 | FullPath: `%SYSTEMDRIVE%:\$MFT`, 157 | IsFullPathRegex: false, 158 | FileName: "$MFT", 159 | IsFileNameRegex: false, 160 | }, 161 | 1: FileToExport{ 162 | FullPath: `D:\$MFT`, 163 | IsFullPathRegex: false, 164 | FileName: "$MFT", 165 | IsFileNameRegex: false, 166 | }, 167 | 2: FileToExport{ 168 | FullPath: `D:\blah`, 169 | IsFullPathRegex: false, 170 | FileName: "blah", 171 | IsFileNameRegex: false, 172 | }, 173 | }}, 174 | wantVolumesOfInterest: []string{"C", "d"}, 175 | wantErr: false, 176 | }, 177 | { 178 | name: "not a real volume", 179 | args: args{exportList: &ListOfFilesToExport{ 180 | 0: FileToExport{ 181 | FullPath: `1:\$MFT`, 182 | IsFullPathRegex: false, 183 | FileName: "$MFT", 184 | IsFileNameRegex: false, 185 | }, 186 | }}, 187 | wantVolumesOfInterest: nil, 188 | wantErr: true, 189 | }, 190 | { 191 | name: "bad input", 192 | args: args{exportList: &ListOfFilesToExport{ 193 | 0: FileToExport{ 194 | FullPath: `CD:\$MFT`, 195 | IsFullPathRegex: false, 196 | FileName: "$MFT", 197 | IsFileNameRegex: false, 198 | }, 199 | }}, 200 | wantVolumesOfInterest: nil, 201 | wantErr: true, 202 | }, 203 | } 204 | for _, tt := range tests { 205 | t.Run(tt.name, func(t *testing.T) { 206 | gotVolumesOfInterest, err := identifyVolumesOfInterest(tt.args.exportList) 207 | if (err != nil) != tt.wantErr { 208 | t.Errorf("identifyVolumesOfInterest() error = %v, wantErr %v", err, tt.wantErr) 209 | return 210 | } 211 | if !reflect.DeepEqual(gotVolumesOfInterest, tt.wantVolumesOfInterest) { 212 | t.Errorf("identifyVolumesOfInterest() gotVolumesOfInterest = %v, want %v", gotVolumesOfInterest, tt.wantVolumesOfInterest) 213 | } 214 | }) 215 | } 216 | } 217 | 218 | func Test_isLetter(t *testing.T) { 219 | type args struct { 220 | s string 221 | } 222 | tests := []struct { 223 | name string 224 | args args 225 | wantResult bool 226 | wantErr bool 227 | }{ 228 | { 229 | name: "letter c", 230 | args: args{s: "C"}, 231 | wantResult: true, 232 | wantErr: false, 233 | }, 234 | { 235 | name: "nil input", 236 | args: args{s: ""}, 237 | wantResult: false, 238 | wantErr: true, 239 | }, 240 | { 241 | name: "string length of 2", 242 | args: args{s: "CC"}, 243 | wantResult: false, 244 | wantErr: true, 245 | }, 246 | { 247 | name: "number input", 248 | args: args{s: "1"}, 249 | wantResult: false, 250 | wantErr: false, 251 | }, 252 | } 253 | for _, tt := range tests { 254 | t.Run(tt.name, func(t *testing.T) { 255 | gotResult, err := isLetter(tt.args.s) 256 | if (err != nil) != tt.wantErr { 257 | t.Errorf("isLetter() error = %v, wantErr %v", err, tt.wantErr) 258 | return 259 | } 260 | if gotResult != tt.wantResult { 261 | t.Errorf("isLetter() gotResult = %v, want %v", gotResult, tt.wantResult) 262 | } 263 | }) 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /writers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Alec Randazzo 2 | 3 | package packrat 4 | 5 | import ( 6 | "archive/zip" 7 | "fmt" 8 | log "github.com/sirupsen/logrus" 9 | "io" 10 | "os" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | type resultWriter interface { 16 | ResultWriter(chan fileReader, *sync.WaitGroup) (err error) 17 | } 18 | 19 | // ZipResultWriter contains the handles to the file and zip structure 20 | type ZipResultWriter struct { 21 | ZipWriter *zip.Writer 22 | FileHandle *os.File 23 | } 24 | 25 | type fileReader struct { 26 | fullPath string 27 | reader io.Reader 28 | } 29 | 30 | // ResultWriter will export found files to a zip file. 31 | func (zipResultWriter *ZipResultWriter) ResultWriter(fileReaders chan fileReader, waitForFileCopying *sync.WaitGroup) (err error) { 32 | defer waitForFileCopying.Done() 33 | 34 | // We receive io.Readers from the fileReaders channel. These are files that the collector identified as ones to collect. 35 | openChannel := true 36 | for openChannel == true { 37 | fileReader := fileReader{} 38 | fileReader, openChannel = <-fileReaders 39 | if openChannel == false { 40 | break 41 | } 42 | 43 | // Normalize the file path so we can make the path a valid file name 44 | normalizedFilePath := strings.ReplaceAll(fileReader.fullPath, "\\", "_") 45 | normalizedFilePath = strings.ReplaceAll(normalizedFilePath, ":", "_") 46 | 47 | // Create a new file inside the zip file 48 | var writer io.Writer 49 | writer, err = zipResultWriter.ZipWriter.Create(normalizedFilePath) 50 | if err != nil { 51 | err = fmt.Errorf("resultWriter failed to add a file to the output zip: %w", err) 52 | return 53 | } 54 | 55 | // Copy contents from the file we want to collect to the output file in the zip 56 | _, readErr := io.Copy(writer, fileReader.reader) 57 | if readErr == io.EOF { 58 | log.Debugf("Successfully collected '%s'", fileReader.fullPath) 59 | } else { 60 | log.Debugf("Failed to collect '%s' due to %v", fileReader.fullPath, readErr) 61 | } 62 | } 63 | err = nil 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /writers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Alec Randazzo 2 | 3 | package packrat 4 | 5 | import ( 6 | "archive/zip" 7 | "bytes" 8 | "crypto/md5" 9 | "encoding/hex" 10 | "io" 11 | "os" 12 | "sync" 13 | "testing" 14 | ) 15 | 16 | func TestZipResultWriter_ResultWriter(t *testing.T) { 17 | type args struct { 18 | fileReaders chan fileReader 19 | waitForFileCopying *sync.WaitGroup 20 | } 21 | tests := []struct { 22 | name string 23 | args args 24 | wantErr bool 25 | dummyData []byte 26 | listOfFileReaders []fileReader 27 | zipToCreate string 28 | wantZipHash string 29 | zipResultWriter ZipResultWriter 30 | }{ 31 | { 32 | name: "test1", 33 | zipResultWriter: ZipResultWriter{ 34 | ZipWriter: nil, 35 | FileHandle: nil, 36 | }, 37 | wantErr: false, 38 | args: args{ 39 | fileReaders: nil, 40 | waitForFileCopying: &sync.WaitGroup{}, 41 | }, 42 | dummyData: []byte{0x00, 0x00, 0x00}, 43 | listOfFileReaders: []fileReader{}, 44 | zipToCreate: `test\testdata\test.zip`, 45 | wantZipHash: "d333bc8a8de2682d40e8db32ffb090d8", 46 | }, 47 | } 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | tt.zipResultWriter.FileHandle, _ = os.Create(tt.zipToCreate) 51 | tt.zipResultWriter.ZipWriter = zip.NewWriter(tt.zipResultWriter.FileHandle) 52 | reader := bytes.NewReader(tt.dummyData) 53 | tt.listOfFileReaders = make([]fileReader, 1) 54 | tt.listOfFileReaders[0] = fileReader{ 55 | fullPath: "test", 56 | reader: reader, 57 | } 58 | tt.args.waitForFileCopying.Add(1) 59 | tt.args.fileReaders = make(chan fileReader, 0) 60 | go tt.zipResultWriter.ResultWriter(tt.args.fileReaders, tt.args.waitForFileCopying) 61 | for _, each := range tt.listOfFileReaders { 62 | tt.args.fileReaders <- each 63 | } 64 | close(tt.args.fileReaders) 65 | tt.args.waitForFileCopying.Wait() 66 | 67 | tt.zipResultWriter.ZipWriter.Close() 68 | tt.zipResultWriter.FileHandle.Close() 69 | 70 | // Get file hash 71 | file, _ := os.Open(tt.zipToCreate) 72 | defer file.Close() 73 | hash := md5.New() 74 | _, _ = io.Copy(hash, file) 75 | hashInBytes := hash.Sum(nil)[:] 76 | gotZipHash := hex.EncodeToString(hashInBytes) 77 | if gotZipHash != tt.wantZipHash { 78 | t.Errorf("ZipResultWriter.resultWriter() gotZipHash = %v, want %v", gotZipHash, tt.wantZipHash) 79 | } 80 | }) 81 | } 82 | } 83 | --------------------------------------------------------------------------------