Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix URL-encoded filesystem entries in the Velociraptor loader #700

Merged
merged 47 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2e2a120
Log warning in case file entry can't be mapped
Zawadidone May 5, 2024
f13bd46
Fix Velociraptor URL-encoding of FS entries
Zawadidone May 5, 2024
d69abb0
Adjust comment
Zawadidone May 5, 2024
934023c
Revert the loading of a directory as target
Zawadidone May 8, 2024
a433dd0
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Jul 8, 2024
90d875c
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Aug 6, 2024
432fc72
Revert change
Zawadidone Aug 6, 2024
2ebffc0
Merge remote-tracking branch 'upstream/main' into fix/velociraptor_zip
Zawadidone Aug 6, 2024
25201a8
Separate mapping of filesystem entries (ZIP)
Zawadidone Aug 6, 2024
530bdd0
Merge branch 'fix/velociraptor_zip' of github.com:Zawadidone/dissect.…
Zawadidone Aug 6, 2024
b42101e
Merge remote-tracking branch 'upstream/main' into fix/velociraptor_zip
Zawadidone Aug 14, 2024
57a038a
Fix merge and change name of argument
Zawadidone Aug 14, 2024
78eb3f6
Add tests
Zawadidone Aug 14, 2024
31fc6d4
Use VFS for the `DirectoryFilesystem`
Zawadidone Aug 14, 2024
1e46bc2
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Aug 15, 2024
1b3f927
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Aug 20, 2024
5528a1d
Revert changes
Zawadidone Aug 21, 2024
bdd2310
Merge branch 'fix/velociraptor_zip' of github.com:Zawadidone/dissect.…
Zawadidone Aug 21, 2024
26197a9
Don't reopen the archive for the compression type
Zawadidone Aug 26, 2024
df99293
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Aug 26, 2024
84a9cc0
Add VelociraptorDirectoryFilesystem
Zawadidone Aug 28, 2024
fdaee1f
Suggestions
Schamper Aug 28, 2024
3a0fead
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Sep 27, 2024
3d14d2a
Fix loader
Zawadidone Sep 28, 2024
bc076da
Explain bug
Zawadidone Sep 29, 2024
2e13ed2
Only replace first occurrence of a string
Zawadidone Oct 14, 2024
e34e771
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Oct 14, 2024
f0881ee
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Nov 18, 2024
c3b208d
Apply code review suggestion
Zawadidone Nov 18, 2024
cc09421
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Nov 18, 2024
3a067d0
Fix ZIP `_resolve_path`
Zawadidone Nov 19, 2024
7922897
Extend tests
Zawadidone Nov 19, 2024
f734417
Fix dir `_resolve_path`
Zawadidone Nov 19, 2024
ff14434
Remove `print` statement
Zawadidone Nov 19, 2024
ba1d362
Apply suggestions code review
Zawadidone Nov 19, 2024
e3cfc80
Fix
Zawadidone Nov 20, 2024
b0e0e14
Apply suggestion code review
Zawadidone Nov 20, 2024
ae0ab7d
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Nov 20, 2024
15e784d
Remove `str`
Zawadidone Nov 20, 2024
41c890c
Merge branch 'fix/velociraptor_zip' of github.com:Zawadidone/dissect.…
Zawadidone Nov 20, 2024
6c1c9fc
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Nov 22, 2024
33bbc3b
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Dec 4, 2024
123cc1b
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Dec 4, 2024
47cfe39
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Dec 10, 2024
4bb92dc
Merge branch 'main' into fix/velociraptor_zip
Zawadidone Dec 10, 2024
6493c8e
Fix tests
Zawadidone Dec 10, 2024
af1653b
Merge branch 'fix/velociraptor_zip' of github.com:Zawadidone/dissect.…
Zawadidone Dec 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions dissect/target/filesystems/dir.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,7 @@ def __repr__(self) -> str:
def _detect(fh: BinaryIO) -> bool:
raise TypeError("Detect is not allowed on DirectoryFilesystem class")

def get(self, path: str) -> FilesystemEntry:
path = path.strip("/")

if not path:
return DirectoryFilesystemEntry(self, "/", self.base_path)

def _resolve_path(self, path: str) -> Path:
if not self.case_sensitive:
searchpath = self.base_path

Expand All @@ -48,6 +43,16 @@ def get(self, path: str) -> FilesystemEntry:
else:
entry = self.base_path.joinpath(path.strip("/"))

return entry

def get(self, path: str) -> FilesystemEntry:
path = path.strip("/")

Zawadidone marked this conversation as resolved.
Show resolved Hide resolved
if not (path := path.strip("/")):
return DirectoryFilesystemEntry(self, "/", self.base_path)

entry = self._resolve_path(path)

try:
entry.lstat()
return DirectoryFilesystemEntry(self, path, entry)
Expand Down
5 changes: 4 additions & 1 deletion dissect/target/filesystems/zip.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,17 @@ def __init__(
if not mname.startswith(self.base) or mname == ".":
continue

rel_name = fsutil.normpath(mname[len(self.base) :], alt_separator=self.alt_separator)
rel_name = self._resolve_path(mname)
self._fs.map_file_entry(rel_name, ZipFilesystemEntry(self, rel_name, member))

@staticmethod
def _detect(fh: BinaryIO) -> bool:
"""Detect a zip file on a given file-like object."""
return zipfile.is_zipfile(fh)

def _resolve_path(self, path: str) -> str:
return fsutil.normpath(path[len(self.base) :], alt_separator=self.alt_separator)

def get(self, path: str, relentry: FilesystemEntry = None) -> FilesystemEntry:
"""Returns a ZipFilesystemEntry object corresponding to the given path."""
return self._fs.get(path, relentry=relentry)
Expand Down
16 changes: 13 additions & 3 deletions dissect/target/loaders/dir.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,23 @@ def find_entry_path(path: Path) -> str | None:
return prefix


def map_dirs(target: Target, dirs: list[Path | tuple[str, Path]], os_type: str, **kwargs) -> None:
def map_dirs(
target: Target,
dirs: list[Path | tuple[str, Path]],
os_type: str,
*,
dirfs: type[DirectoryFilesystem] = DirectoryFilesystem,
zipfs: type[ZipFilesystem] = ZipFilesystem,
**kwargs,
) -> None:
"""Map directories as filesystems into the given target.

Args:
target: The target to map into.
dirs: The directories to map as filesystems. If a list member is a tuple, the first element is the drive letter.
os_type: The operating system type, used to determine how the filesystem should be mounted.
dirfs: The filesystem class to use for directory filesystems.
zipfs: The filesystem class to use for ZIP filesystems.
"""
alt_separator = ""
case_sensitive = True
Expand All @@ -59,9 +69,9 @@ def map_dirs(target: Target, dirs: list[Path | tuple[str, Path]], os_type: str,
drive_letter = path.name[0]

if isinstance(path, zipfile.Path):
dfs = ZipFilesystem(path.root.fp, path.at, alt_separator=alt_separator, case_sensitive=case_sensitive)
dfs = zipfs(path.root.fp, path.at, alt_separator=alt_separator, case_sensitive=case_sensitive)
else:
dfs = DirectoryFilesystem(path, alt_separator=alt_separator, case_sensitive=case_sensitive)
dfs = dirfs(path, alt_separator=alt_separator, case_sensitive=case_sensitive)

drive_letter_map[drive_letter].append(dfs)

Expand Down
50 changes: 35 additions & 15 deletions dissect/target/loaders/velociraptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
import zipfile
from pathlib import Path
from typing import TYPE_CHECKING
from urllib.parse import quote, unquote

from dissect.target.filesystems.dir import DirectoryFilesystem
from dissect.target.filesystems.zip import ZipFilesystem
from dissect.target.helpers.fsutil import basename, dirname, join
from dissect.target.loaders.dir import DirLoader, find_dirs, map_dirs
from dissect.target.plugin import OperatingSystem

Expand Down Expand Up @@ -87,11 +91,13 @@ def __init__(self, path: Path, **kwargs):
super().__init__(path)

if path.suffix == ".zip":
log.warning(
f"Velociraptor target {path!r} is compressed, which will slightly affect performance. "
"Consider uncompressing the archive and passing the uncompressed folder to Dissect."
)
self.root = zipfile.Path(path.open("rb"))
if self.root.root.getinfo("uploads.json").compress_type > 0:
log.warning(
"Velociraptor target '%s' is compressed, which will slightly affect performance. "
"Consider uncompressing the archive and passing the uncompressed folder to Dissect.",
path,
)
else:
self.root = path

Expand All @@ -116,14 +122,28 @@ def detect(path: Path) -> bool:

def map(self, target: Target) -> None:
os_type, dirs = find_fs_directories(self.root)
if os_type == OperatingSystem.WINDOWS:
# Velociraptor doesn't have the correct filenames for the paths "$J" and "$Secure:$SDS"
map_dirs(
target,
dirs,
os_type,
usnjrnl_path="$Extend/$UsnJrnl%3A$J",
sds_path="$Secure%3A$SDS",
)
else:
map_dirs(target, dirs, os_type)

# Velociraptor URL encodes paths before storing these in a collection, this leads plugins not being able to find
# these paths. To circumvent this issue, for a zip file the path names are URL decoded before mapping into the
# VFS and for a directory the paths are URL encoded at lookup time.
map_dirs(
target,
dirs,
os_type,
dirfs=VelociraptorDirectoryFilesystem,
zipfs=VelociraptorZipFilesystem,
)


class VelociraptorDirectoryFilesystem(DirectoryFilesystem):
def _resolve_path(self, path: str) -> Path:
path = quote(path, safe="$/% ")
path = (
join(dirname(path), str(basename(path)).replace(".", "%2E", 1)) if basename(path).startswith(".") else path
)
Zawadidone marked this conversation as resolved.
Show resolved Hide resolved
return super()._resolve_path(path)


class VelociraptorZipFilesystem(ZipFilesystem):
def _resolve_path(self, path: str) -> str:
return unquote(super()._resolve_path(path))
48 changes: 34 additions & 14 deletions tests/loaders/test_velociraptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ def create_root(sub_dir: str, tmp_path: Path) -> Path:
f"uploads/{sub_dir}/%5C%5C%3F%5CGLOBALROOT%5CDevice%5CHarddiskVolumeShadowCopy1/",
f"uploads/{sub_dir}/%5C%5C%3F%5CGLOBALROOT%5CDevice%5CHarddiskVolumeShadowCopy1/$Extend",
f"uploads/{sub_dir}/%5C%5C%3F%5CGLOBALROOT%5CDevice%5CHarddiskVolumeShadowCopy1/windows/system32",
f"uploads/{sub_dir}/%5C%5C.%5CC%3A/%2ETEST",
Schamper marked this conversation as resolved.
Show resolved Hide resolved
]
root = tmp_path
mkdirs(root, paths)

(root / "uploads.json").write_bytes(b"{}")
(root / f"uploads/{sub_dir}/%5C%5C.%5CC%3A/C-DRIVE.txt").write_bytes(b"{}")
(root / f"uploads/{sub_dir}/%5C%5C.%5CC%3A/Microsoft-Windows-Windows Defender%254WHC.evtx").write_bytes(b"{}")
(root / f"uploads/{sub_dir}/%5C%5C.%5CC%3A/other.txt").write_text("my first file")

with open(absolute_path("_data/plugins/filesystem/ntfs/mft/mft.raw"), "rb") as fh:
mft = fh.read(10 * 1025)
Expand Down Expand Up @@ -54,8 +57,6 @@ def create_root(sub_dir: str, tmp_path: Path) -> Path:
)
def test_windows_ntfs(sub_dir: str, other_dir: str, target_bare: Target, tmp_path: Path) -> None:
root = create_root(sub_dir, tmp_path)
root.joinpath(f"uploads/{other_dir}/C%3A").mkdir(parents=True, exist_ok=True)
root.joinpath(f"uploads/{other_dir}/C%3A/other.txt").write_text("my first file")

assert VelociraptorLoader.detect(root) is True

Expand All @@ -72,9 +73,21 @@ def test_windows_ntfs(sub_dir: str, other_dir: str, target_bare: Target, tmp_pat
usnjrnl_records += len(list(fs.ntfs.usnjrnl.records()))
assert usnjrnl_records == 2
assert len(target_bare.filesystems) == 4

assert target_bare.fs.path("sysvol/C-DRIVE.txt").exists()
assert target_bare.fs.path("sysvol/other.txt").read_text() == "my first file"

paths = list(target_bare.fs.path("sysvol").iterdir())

assert paths[2].exists()
assert paths[2] == target_bare.fs.path("sysvol/C-DRIVE.txt")

assert paths[5].exists()
assert paths[5] == target_bare.fs.path("sysvol/other.txt")

assert target_bare.fs.path("sysvol/.TEST").exists()
assert target_bare.fs.path("Microsoft-Windows-Windows Defender%4WHC.evtx")


@pytest.mark.parametrize(
"sub_dir",
Expand Down Expand Up @@ -102,17 +115,19 @@ def test_windows_ntfs_zip(sub_dir: str, target_bare: Target, tmp_path: Path) ->
assert usnjrnl_records == 2
assert len(target_bare.filesystems) == 4
assert target_bare.fs.path("sysvol/C-DRIVE.txt").exists()
assert target_bare.fs.path("sysvol/.TEST").exists()
assert target_bare.fs.path("Microsoft-Windows-Windows Defender%4WHC.evtx")


@pytest.mark.parametrize(
"paths",
[
(["uploads/file/etc", "uploads/file/var"]),
(["uploads/auto/etc", "uploads/auto/var"]),
(["uploads/file/etc", "uploads/file/var", "uploads/file/opt"]),
(["uploads/auto/etc", "uploads/auto/var", "uploads/auto/opt"]),
(["uploads/file/Library", "uploads/file/Applications"]),
(["uploads/auto/Library", "uploads/auto/Applications"]),
(["uploads/file/etc", "uploads/file/var", "uploads/file/%2ETEST"]),
(["uploads/auto/etc", "uploads/auto/var", "uploads/auto/%2ETEST"]),
(["uploads/file/etc", "uploads/file/var", "uploads/file/opt", "uploads/file/%2ETEST"]),
(["uploads/auto/etc", "uploads/auto/var", "uploads/auto/opt", "uploads/auto/%2ETEST"]),
(["uploads/file/Library", "uploads/file/Applications", "uploads/file/%2ETEST"]),
(["uploads/auto/Library", "uploads/auto/Applications", "uploads/auto/%2ETEST"]),
],
)
def test_unix(paths: list[str], target_bare: Target, tmp_path: Path) -> None:
Expand All @@ -125,19 +140,22 @@ def test_unix(paths: list[str], target_bare: Target, tmp_path: Path) -> None:

loader = VelociraptorLoader(root)
loader.map(target_bare)
target_bare.apply()

assert len(target_bare.filesystems) == 1
assert target_bare.fs.path("/.TEST").exists()


@pytest.mark.parametrize(
"paths",
[
(["uploads/file/etc", "uploads/file/var"]),
(["uploads/auto/etc", "uploads/auto/var"]),
(["uploads/file/etc", "uploads/file/var", "uploads/file/opt"]),
(["uploads/auto/etc", "uploads/auto/var", "uploads/auto/opt"]),
(["uploads/file/Library", "uploads/file/Applications"]),
(["uploads/auto/Library", "uploads/auto/Applications"]),
(["uploads/file/etc", "uploads/file/var", "uploads/file/%2ETEST"]),
(["uploads/file/etc", "uploads/file/var", "uploads/file/%2ETEST"]),
(["uploads/auto/etc", "uploads/auto/var", "uploads/auto/%2ETEST"]),
(["uploads/file/etc", "uploads/file/var", "uploads/file/opt", "uploads/file/%2ETEST"]),
(["uploads/auto/etc", "uploads/auto/var", "uploads/auto/opt", "uploads/auto/%2ETEST"]),
(["uploads/file/Library", "uploads/file/Applications", "uploads/file/%2ETEST"]),
(["uploads/auto/Library", "uploads/auto/Applications", "uploads/auto/%2ETEST"]),
],
)
def test_unix_zip(paths: list[str], target_bare: Target, tmp_path: Path) -> None:
Expand All @@ -153,5 +171,7 @@ def test_unix_zip(paths: list[str], target_bare: Target, tmp_path: Path) -> None

loader = VelociraptorLoader(zip_path)
loader.map(target_bare)
target_bare.apply()

assert len(target_bare.filesystems) == 1
assert target_bare.fs.path("/.TEST").exists()