Skip to content

Commit

Permalink
Merge pull request #30 from google/mmls
Browse files Browse the repository at this point in the history
Remove sluethkit/mmls as a dependency
  • Loading branch information
meeehow authored Dec 12, 2022
2 parents 35b15e8 + a0b129f commit 8fdb572
Show file tree
Hide file tree
Showing 3 changed files with 4 additions and 306 deletions.
10 changes: 3 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- [TarGz](#targz)
- [Deb](#deb)
- [RPM](#rpm)
- [Zip (and other zip-like formats)](#zip-and-other-zip-like-formats)
- [GCP (Google Cloud Platform)](#gcp-google-cloud-platform)
- [GCR (Google Container Registry)](#gcr-google-container-registry)
- [Windows](#windows)
Expand Down Expand Up @@ -92,15 +93,10 @@ HashR takes care of the heavy lifting (parsing disk images, volumes, file system
docker pull log2timeline/plaso
```

We also need two additional tools installed on the machine running HashR:

1. mmls, which is part of Sleuthkit, to list volumes on disk image
1. 7z, which is used by WSUS importer for recursive extraction in Windows Update packages

You can install both with the following command:
We also need 7z, which is used by WSUS importer for recursive extraction of Windows Update packages, to be installed on the machine running HashR:

``` shell
sudo apt install p7zip-full sleuthkit
sudo apt install p7zip-full
```

You need to allow the user, under which HashR will run, to run certain commands via sudo. Assuming that your user is `hashr` create a file `/etc/sudoers.d/hashr` and put in:
Expand Down
142 changes: 1 addition & 141 deletions processors/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,9 @@ package local
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"

"github.com/golang/glog"
)
Expand Down Expand Up @@ -74,148 +71,11 @@ func (p *Processor) ImageExport(sourcePath string) (string, error) {
exportDir := filepath.Join(baseDir, "export")
logFile := filepath.Join(baseDir, "image_export.log")

info, err := os.Stat(sourcePath)
if err != nil {
return "", err
}

var xfsMountPoints []string

// If the sourcePath is a file, check for XFS volumes and try to mount them.
if info.Mode().IsRegular() {
mmlsOut, err := shellCommand("mmls", sourcePath)
if err != nil {
glog.Infof("mmls error (%v), probably not a disk image", err)
} else {
xfsv, err := xfsVolumes(sourcePath, parseMmlsOutput(mmlsOut))
if err != nil {
glog.Warningf("error reading source image: %v", err)
}
xfsMountPoints = mountXfs(sourcePath, baseDir, xfsv)
}
}

// If there is at least one XFS volume that was mounted successfully, we'll pass the mount
// directory to Plaso.
if len(xfsMountPoints) > 0 {
glog.Info("Disk image contains XFS volume(s)")
defer unmountXfs(xfsMountPoints)
sourcePath = filepath.Join(baseDir, "mnt")
}

args := []string{"run", "-v", "/tmp/:/tmp", "log2timeline/plaso", "image_export", "--logfile", logFile, "--partitions", "all", "--volumes", "all", "-w", exportDir, sourcePath}
_, err = shellCommand("docker", args...)
_, err := shellCommand("docker", args...)
if err != nil {
return "", fmt.Errorf("error while running Plaso: %v", err)
}

return exportDir, nil
}

func unmountXfs(mountPoints []string) {
for _, mountPoint := range mountPoints {
_, err := shellCommand("sudo", "umount", mountPoint)
if err != nil {
glog.Errorf("error while unmounting volume: %v", err)
}
}
}

func mountXfs(imagePath, baseDir string, xfsVolumes []volume) []string {
var mountPoints []string

for _, volume := range xfsVolumes {
mountSubdir := filepath.Join(baseDir, "mnt", fmt.Sprintf("p%d", volume.id))
if err := os.MkdirAll(mountSubdir, 0755); err != nil {
glog.Errorf("could not create mount directory: %v", err)
continue
}

_, err := shellCommand("sudo", "mount", "-t", "xfs", "-o", fmt.Sprintf("loop,offset=%d", volume.start*512), imagePath, mountSubdir)
if err != nil {
glog.Errorf("error while executing mount cmd: %v", err)
continue
}
mountPoints = append(mountPoints, mountSubdir)
}

return mountPoints
}

func xfsVolumes(imagePath string, volumes []volume) ([]volume, error) {
file, err := os.Open(imagePath)
if err != nil {
return nil, err
}
defer file.Close()

var xfsPartitions []volume
for _, volume := range volumes {
data := make([]byte, 4)
_, err := file.ReadAt(data, int64(volume.start*512))
if err != nil {
glog.Errorf("Could not read image file: %v", err)
continue
}
// Check if first 4 bytes of the volume matches XFS signature.
if bytes.Equal(data, []byte{88, 70, 83, 66}) {
xfsPartitions = append(xfsPartitions, volume)
}
}

return xfsPartitions, nil
}

func parseMmlsOutput(output string) []volume {
matches := reRow.FindAllString(output, -1)
var volumes []volume

for _, match := range matches {
// Replace multiple, consecutive spaces with one.
s := reSpace.ReplaceAllString(match, " ")
// Split the row.
row := strings.Split(s, " ")

if len(row) < 5 {
glog.Warningf("Skipping row %q due to wrong format", row)
continue
}

var start, length, id int
var err error

if row[0] == "000:" {
id = 0
} else {
id, err = strconv.Atoi(strings.TrimRight(strings.TrimLeft(row[0], "0"), ":"))
if err != nil {
glog.Warningf("Skipping row %q due to error: %v", row, err)
continue
}
}

if row[2] == "0000000000" {
start = 0
} else {
start, err = strconv.Atoi(strings.TrimLeft(row[2], "0"))
if err != nil {
glog.Warningf("Skipping row %q due to error: %v", row, err)
continue
}
}

if row[4] == "0000000000" {
length = 0
} else {
length, err = strconv.Atoi(strings.TrimLeft(row[4], "0"))
if err != nil {
glog.Warningf("Skipping row %q due to error: %v", row, err)
continue
}
}

volumes = append(volumes, volume{id: id, start: start, length: length})
}

return volumes
}
158 changes: 0 additions & 158 deletions processors/local/local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,151 +22,8 @@ import (
"os/exec"
"path/filepath"
"testing"

"github.com/google/go-cmp/cmp"
)

func TestParseMmlsOutput(t *testing.T) {
cases := []struct {
output string
want []volume
}{
{
output: `DOS Partition Table
Offset Sector: 0
Units are in 512-byte sectors
Slot Start End Length Description
000: Meta 0000000000 0000000000 0000000001 Primary Table (#0)
001: ------- 0000000000 0000002047 0000002048 Unallocated
002: 000:000 0000002048 0020971519 0020969472 Linux (0x83)`,
want: []volume{
{id: 0, start: 0, length: 1},
{id: 1, start: 0, length: 2048},
{id: 2, start: 2048, length: 20969472},
},
},
{
output: `GUID Partition Table (EFI)
Offset Sector: 0
Units are in 512-byte sectors
Slot Start End Length Description
000: Meta 0000000000 0000000000 0000000001 Safety Table
001: ------- 0000000000 0000000063 0000000064 Unallocated
002: Meta 0000000001 0000000001 0000000001 GPT Header
003: Meta 0000000002 0000000033 0000000032 Partition Table
004: 010 0000000064 0000016447 0000016384 RWFW
005: 005 0000016448 0000016448 0000000001 KERN-C
006: 006 0000016449 0000016449 0000000001 ROOT-C
007: 008 0000016450 0000016450 0000000001 reserved
008: 009 0000016451 0000016451 0000000001 reserved
009: ------- 0000016452 0000020479 0000004028 Unallocated
010: 001 0000020480 0000053247 0000032768 KERN-A
011: 003 0000053248 0000086015 0000032768 KERN-B
012: 007 0000086016 0000118783 0000032768 OEM
013: ------- 0000118784 0000249855 0000131072 Unallocated
014: 011 0000249856 0000315391 0000065536 EFI-SYSTEM
015: 004 0000315392 0004509695 0004194304 ROOT-B
016: 002 0004509696 0008703999 0004194304 ROOT-A
017: 000 0008704000 0018874476 0010170477 STATE
018: ------- 0018874477 0020971519 0002097043 Unallocated`,
want: []volume{
{id: 0, start: 0, length: 1},
{id: 1, start: 0, length: 64},
{id: 2, start: 1, length: 1},
{id: 3, start: 2, length: 32},
{id: 4, start: 64, length: 16384},
{id: 5, start: 16448, length: 1},
{id: 6, start: 16449, length: 1},
{id: 7, start: 16450, length: 1},
{id: 8, start: 16451, length: 1},
{id: 9, start: 16452, length: 4028},
{id: 10, start: 20480, length: 32768},
{id: 11, start: 53248, length: 32768},
{id: 12, start: 86016, length: 32768},
{id: 13, start: 118784, length: 131072},
{id: 14, start: 249856, length: 65536},
{id: 15, start: 315392, length: 4194304},
{id: 16, start: 4509696, length: 4194304},
{id: 17, start: 8704000, length: 10170477},
{id: 18, start: 18874477, length: 2097043},
},
},
{
output: `GUID Partition Table (EFI)
Offset Sector: 0
Units are in 512-byte sectors
Slot Start End Length Description
000: Meta 0000000000 0000000000 0000000001 Safety Table
001: ------- 0000000000 0000002047 0000002048 Unallocated
002: Meta 0000000001 0000000001 0000000001 GPT Header
003: Meta 0000000002 0000000033 0000000032 Partition Table
004: 000 0000002048 0000006143 0000004096 p.legacy
005: 001 0000006144 0000047103 0000040960 p.UEFI
006: 002 0000047104 0020971486 0020924383 p.lxroot
007: ------- 0020971487 0020971519 0000000033 Unallocated`,
want: []volume{
{id: 0, start: 0, length: 1},
{id: 1, start: 0, length: 2048},
{id: 2, start: 1, length: 1},
{id: 3, start: 2, length: 32},
{id: 4, start: 2048, length: 4096},
{id: 5, start: 6144, length: 40960},
{id: 6, start: 47104, length: 20924383},
{id: 7, start: 20971487, length: 33},
},
},
{
output: `GUID Partition Table (EFI)
Offset Sector: 0
Units are in 512-byte sectors
Slot Start End Length Description
000: Meta 0000000000 0000000000 0000000001 Safety Table
001: ------- 0000000000 0000002047 0000002048 Unallocated
002: Meta 0000000001 0000000001 0000000001 GPT Header
003: Meta 0000000002 0000000033 0000000032 Partition Table
004: 000 0000002048 0000411647 0000409600 EFI System Partition
005: 001 0000411648 0041940991 0041529344
006: ------- 0041940992 0041943039 0000002048 Unallocated`,
want: []volume{
{id: 0, start: 0, length: 1},
{id: 1, start: 0, length: 2048},
{id: 2, start: 1, length: 1},
{id: 3, start: 2, length: 32},
{id: 4, start: 2048, length: 409600},
{id: 5, start: 411648, length: 41529344},
{id: 6, start: 41940992, length: 2048},
},
},
}

for _, tc := range cases {
got := parseMmlsOutput(tc.output)

if !cmp.Equal(tc.want, got, cmp.AllowUnexported(volume{})) {
t.Errorf("parseMmlsOutput() unexpected diff (-want/+got):\n%s", cmp.Diff(tc.want, got, cmp.AllowUnexported(volume{})))
}
}
}

func TestXfsVolumes(t *testing.T) {
gotVolumes, err := xfsVolumes("testdata/disk_2_xfs_volumes.raw", []volume{{id: 1, start: 2048, length: 6144}, {id: 2, start: 8192, length: 4096}, {id: 3, start: 12288, length: 6144}})
if err != nil {
t.Fatalf("unexpected error while running xfsVolumes(): %v", err)
}

wantVolumes := []volume{
{id: 1, start: 2048, length: 6144},
{id: 3, start: 12288, length: 6144},
}
if !cmp.Equal(wantVolumes, gotVolumes, cmp.AllowUnexported(volume{})) {
t.Errorf("xfsVolumes() unexpected diff (-want/+got):\n%s", cmp.Diff(wantVolumes, gotVolumes, cmp.AllowUnexported(volume{})))
}
}

func TestExecute(t *testing.T) {
bytes, err := execute("echo", "test").Output()
if err != nil {
Expand Down Expand Up @@ -215,23 +72,8 @@ func TestImageExport(t *testing.T) {

}

var mmlsOut = `DOS Partition Table
Offset Sector: 0
Units are in 512-byte sectors
Slot Start End Length Description
000: Meta 0000000000 0000000000 0000000001 Primary Table (#0)
001: ------- 0000000000 0000002047 0000002048 Unallocated
002: 000:000 0000002048 0000008191 0000006144 Linux (0x83)
003: 000:001 0000008192 0000012287 0000004096 Linux (0x83)
004: 000:002 0000012288 0000018431 0000006144 Linux (0x83)
005: ------- 0000018432 0000020479 0000002048 Unallocated`

func fakeExecute(command string, args ...string) *exec.Cmd {
var mockStdOut string
if command == "mmls" {
mockStdOut = mmlsOut
}

cs := []string{"-test.run=TestHelperProcess", "--", command}
cs = append(cs, args...)
Expand Down

0 comments on commit 8fdb572

Please sign in to comment.