Skip to content

Commit

Permalink
Added pe_dump VQL function (#2554)
Browse files Browse the repository at this point in the history
This function dumps a PE file from memory. This is useful for extracting
injected PE files.
  • Loading branch information
scudette committed Mar 20, 2023
1 parent 85462af commit 500bffd
Show file tree
Hide file tree
Showing 10 changed files with 1,035 additions and 4 deletions.
151 changes: 151 additions & 0 deletions accessors/sparse/ranged.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package sparse

import (
"bufio"
"bytes"
"fmt"
"io"
"sync"

"www.velocidex.com/golang/velociraptor/accessors"
"www.velocidex.com/golang/velociraptor/accessors/zip"
actions_proto "www.velocidex.com/golang/velociraptor/actions/proto"
"www.velocidex.com/golang/velociraptor/json"
"www.velocidex.com/golang/velociraptor/utils"
vql_subsystem "www.velocidex.com/golang/velociraptor/vql"
vfilter "www.velocidex.com/golang/vfilter"
)

type RangedReaderPath struct {
JsonlRanges string `json:"jsonl"`
}

func parseIndexRanges(serialized []byte) (*actions_proto.Index, error) {
arg := &RangedReaderPath{}
err := json.Unmarshal(serialized, arg)
if err != nil {
return nil, err
}

index := &actions_proto.Index{}

if arg.JsonlRanges != "" {
reader := bufio.NewReader(bytes.NewReader([]byte(arg.JsonlRanges)))
for {
row_data, err := reader.ReadBytes('\n')
if err != nil || len(row_data) == 0 {
return index, nil
}

item := &actions_proto.Range{}
err = json.Unmarshal(row_data, item)
if err == nil {
index.Ranges = append(index.Ranges, item)
}
}
}

return index, nil
}

type RangedReader struct {
mu sync.Mutex
size int64
offset int64

// A file handle to the underlying file.
handle accessors.ReadSeekCloser
reader_at io.ReaderAt
}

func (self *RangedReader) Read(buf []byte) (int, error) {
self.mu.Lock()
defer self.mu.Unlock()

n, err := self.reader_at.ReadAt(buf, self.offset)
self.offset += int64(n)

// Range is past the end of file
return n, err
}

func (self *RangedReader) Seek(offset int64, whence int) (int64, error) {
self.mu.Lock()
defer self.mu.Unlock()

switch whence {
case 0:
self.offset = offset
case 1:
self.offset += offset
case 2:
self.offset = self.size
}

return int64(self.offset), nil
}

func (self RangedReader) Close() error {
return self.handle.Close()
}

func (self RangedReader) LStat() (accessors.FileInfo, error) {
return &SparseFileInfo{size: self.size}, nil
}

func GetRangedReaderFile(full_path *accessors.OSPath, scope vfilter.Scope) (
zip.ReaderStat, error) {
if len(full_path.Components) == 0 {
return nil, fmt.Errorf("Ranged accessor expects a JSON sparse definition.")
}

// The Path is a serialized ranges map.
index, err := parseIndexRanges([]byte(full_path.Components[0]))
if err != nil {
scope.Log("Ranged accessor expects ranges as path, for example: '[{Offset:0, Length: 10},{Offset:10,length:20}]'")
return nil, err
}

pathspec := full_path.PathSpec()

err = vql_subsystem.CheckFilesystemAccess(scope, pathspec.DelegateAccessor)
if err != nil {
scope.Log("%v: DelegateAccessor denied", err)
return nil, err
}

accessor, err := accessors.GetAccessor(pathspec.DelegateAccessor, scope)
if err != nil {
scope.Log("%v: did you provide a PathSpec?", err)
return nil, err
}

fd, err := accessor.Open(pathspec.GetDelegatePath())
if err != nil {
scope.Log("sparse: Failed to open delegate %v: %v",
pathspec.GetDelegatePath(), err)
return nil, err
}

// Devices can not be stat'ed
size := int64(0)
if len(index.Ranges) > 0 {
last := index.Ranges[len(index.Ranges)-1]
size = last.FileOffset + last.FileLength
}

return &RangedReader{
handle: fd,
size: size,
reader_at: &utils.RangedReader{
ReaderAt: utils.MakeReaderAtter(fd),
Index: index,
},
}, nil
}

func init() {
accessors.Register("ranged", zip.NewGzipFileSystemAccessor(
accessors.MustNewPathspecOSPath(""), GetRangedReaderFile),
`Reconstruct sparse files from idx and base`)
}
45 changes: 45 additions & 0 deletions artifacts/definitions/Windows/Memory/PEDump.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Windows.Memory.PEDump
description: |
This artifact dumps a PE file from memory and uploads the file to
the server.
NOTE: The output is not exactly the same as the original binary:
1. Relocations are not fixed
2. Due to ASLR the base address of the binary will not be the same as the original.
The result is usully much better than the binaries dumped from a
physical memory image (using e.g. Volatility) because reading
process memory will page in any mmaped pages as we copy them
out. Therefore we do not expect to have holes in the produced binary
as is often the case in memory analysis.
parameters:
- name: Pid
type: int
description: The pid to dump
- name: BaseOffset
type: int
description: |
The base offset to dump from memory. If not provided, we dump
all pe files from the PID.
- name: FilenameRegex
default: .+exe$
description: Applies to the PE mapping filename to upload

sources:
- query: |
LET GetFilename(MappingName, BaseOffset) = if(
condition=MappingName,
then=format(format="dump_%#x_%s", args=[BaseOffset, basename(path=MappingName)]),
else=format(format="dump_%#x", args=BaseOffset))
SELECT format(format="%#x", args=Address) AS Address, Size, MappingName,
State, Type, Protection, ProtectionMsg, read_file(
accessor="process",
filename=format(format="/%d", args=Pid),
offset=Address,
length=10) AS Header,
upload(file=pe_dump(pid=Pid, base_offset=Address),
name=GetFilename(MappingName=MappingName, BaseOffset=Address)) AS Upload
FROM vad(pid=9604)
WHERE Header =~ "^MZ" AND MappingName =~ FilenameRegex
Binary file added artifacts/testdata/files/memory/9604
Binary file not shown.
Loading

0 comments on commit 500bffd

Please sign in to comment.