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

Add support for deleted files in deleted RecycleBin directories #718

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
62 changes: 51 additions & 11 deletions dissect/target/plugins/os/windows/recyclebin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
("path", "path"),
("filesize", "filesize"),
("path", "deleted_path"),
("string", "source"),
("path", "source"),
],
)

Expand Down Expand Up @@ -87,30 +87,34 @@ def recyclebin(self) -> Generator[RecycleBinRecord, None, None]:
)

for recyclebin in recyclebin_paths:
yield from self.read_recycle_bin(recyclebin)
yield from self.read_recycle_file(recyclebin)

def _is_recycle_file(self, path: TargetPath) -> bool:
def _is_recycle_meta_file(self, path: TargetPath) -> bool:
"""Check wether the path is a recycle bin metadata file"""
return path.name and path.name.lower().startswith("$i")

def read_recycle_bin(self, bin_path: TargetPath) -> Generator[RecycleBinRecord, None, None]:
if self._is_recycle_file(bin_path):
yield self.read_bin_file(bin_path)
def read_recycle_file(self, path: TargetPath) -> Iterator[RecycleBinRecord]:
if self._is_recycle_meta_file(path):
yield self.read_recycle_meta_file(path)
return

if bin_path.is_dir():
for new_file in bin_path.iterdir():
yield from self.read_recycle_bin(new_file)
if path.is_dir() and path.name.startswith("$R"):
yield from self.read_recycle_deleted_folder(path)
return

if path.is_dir():
for new_file in path.iterdir():
yield from self.read_recycle_file(new_file)

def read_bin_file(self, bin_path: TargetPath) -> RecycleBinRecord:
def read_recycle_meta_file(self, bin_path: TargetPath) -> RecycleBinRecord:
data = bin_path.read_bytes()

header = self.select_header(data)
entry = header(data)

sid = self.find_sid(bin_path)
source_path = str(bin_path).lstrip("/")
deleted_path = str(bin_path.parent / bin_path.name.replace("/$i", "/$r")).lstrip("/")
deleted_path = str(bin_path.parent / bin_path.name.replace("$I", "$R")).lstrip("/")

user_details = self.target.user_details.find(sid=sid)
user = user_details.user if user_details else None
Expand All @@ -125,6 +129,42 @@ def read_bin_file(self, bin_path: TargetPath) -> RecycleBinRecord:
_user=user,
)

def read_recycle_deleted_folder(self, folder_path: TargetPath) -> RecycleBinRecord:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def read_recycle_deleted_folder(self, folder_path: TargetPath) -> RecycleBinRecord:
def read_recycle_deleted_folder(self, path: TargetPath) -> RecycleBinRecord:

And rename accordingly.

# Generally speaking when deleting a file, the $R* file is the actual renamed deleted file.
# This is however also the case for deleted folders. When a folder is deleted,
# it is also renamed and placed here (with original recursive content).
#
# This function will create RecycleBin records for each file in a deleted folder.

meta_file = self.target.fs.path(str(folder_path.parent / folder_path.name.replace("$R", "$I")).lstrip("/"))
if not meta_file.exists():
return

meta_data = meta_file.read_bytes()
header = self.select_header(meta_data)
entry = header(meta_data)

sid = self.find_sid(folder_path)
original_folder_path = self.target.fs.path(entry.filename.rstrip("\x00"))

user_details = self.target.user_details.find(sid=sid)
user = user_details.user if user_details else None
for parent_path, _, childs in folder_path.walk():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rglob works better for you? Then you don't have to do the nasty path construction at line 156 and 157.

for child in childs:
child_path = self.target.fs.path(f"{str(parent_path).lstrip('/')}/{child}")
original_parent_of_child = original_folder_path / str(parent_path).split(folder_path.name)[1].lstrip(
"/"
)
yield RecycleBinRecord(
ts=wintimestamp(entry.timestamp),
path=original_parent_of_child / child,
source=meta_file,
filesize=child_path.stat().st_size,
deleted_path=child_path,
_target=self.target,
_user=user,
)

def find_sid(self, path: TargetPath) -> str:
parent_path = path.parent
if parent_path.name.lower() == "$recycle.bin":
Expand Down
18 changes: 9 additions & 9 deletions tests/plugins/os/windows/test_recyclebin.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def test_read_recycle_bin(target_win):

mocked_file.is_file.return_value = True
mocked_file.is_dir.return_value = False
with patch.object(RecyclebinPlugin, "read_bin_file") as mocked_bin_file:
assert [mocked_bin_file.return_value] == list(RecyclebinPlugin(target_win).read_recycle_bin(mocked_file))
with patch.object(RecyclebinPlugin, "read_recycle_meta_file") as mocked_bin_file:
assert [mocked_bin_file.return_value] == list(RecyclebinPlugin(target_win).read_recycle_file(mocked_file))


def test_filtered_name(target_win):
Expand All @@ -44,7 +44,7 @@ def test_filtered_name(target_win):

mocked_file.name = "hello"

assert [] == list(RecyclebinPlugin(target_win).read_recycle_bin(mocked_file))
assert [] == list(RecyclebinPlugin(target_win).read_recycle_file(mocked_file))


def test_read_recycle_bin_directory(target_win):
Expand All @@ -56,12 +56,12 @@ def test_read_recycle_bin_directory(target_win):
mocked_file = Mock()
mocked_file.is_file.return_value = True
mocked_file.is_dir.return_value = False
mocked_file.name = "$ihello"
mocked_file.name = "$Ihello"

mocked_dir.iterdir.return_value = [mocked_file] * 3

with patch.object(RecyclebinPlugin, "read_bin_file", return_value=mocked_file):
data = list(RecyclebinPlugin(target_win).read_recycle_bin(mocked_dir))
with patch.object(RecyclebinPlugin, "read_recycle_meta_file", return_value=mocked_file):
data = list(RecyclebinPlugin(target_win).read_recycle_file(mocked_dir))

assert data == [mocked_file] * 3

Expand Down Expand Up @@ -98,7 +98,7 @@ def test_read_bin_file_unknown(target_win, path):
with patch.object(Path, "open", mock_open(read_data=header_1.dumps())):
normal_path = Path(path)

output = recycle_plugin.read_bin_file(normal_path)
output = recycle_plugin.read_recycle_meta_file(normal_path)

assert output.filesize == 0x20
assert output.path == "hello_world"
Expand All @@ -110,7 +110,7 @@ def test_recyclebin_plugin_file(target_win, recycle_bin):
target_win.fs.mount("C:\\$recycle.bin", recycle_bin)
target_win.add_plugin(RecyclebinPlugin)

with patch.object(RecyclebinPlugin, "read_bin_file") as mocked_bin_file:
with patch.object(RecyclebinPlugin, "read_recycle_meta_file") as mocked_bin_file:
recycle_bin_entries = list(target_win.recyclebin())
assert recycle_bin_entries == [mocked_bin_file.return_value]

Expand All @@ -120,7 +120,7 @@ def test_recyclebin_plugin_wrong_prefix(target_win, recycle_bin):
target_win.fs.mount("C:\\$recycle.bin", recycle_bin)
target_win.add_plugin(RecyclebinPlugin)

with patch.object(RecyclebinPlugin, "read_bin_file"):
with patch.object(RecyclebinPlugin, "read_recycle_meta_file"):
recycle_bin_entries = list(target_win.recyclebin())
assert recycle_bin_entries == []

Expand Down