248 lines
8.0 KiB
Python
248 lines
8.0 KiB
Python
"""One-button forensic artifact wipe.
|
|
|
|
Surfaces purged:
|
|
|
|
• %TEMP% — per-user temp
|
|
• %SystemRoot%\\Prefetch\\*.pf (Admin only) — file-launch history
|
|
• %APPDATA%\\Microsoft\\Windows\\Recent\\* — recent files
|
|
• %APPDATA%\\Microsoft\\Windows\\Recent\\AutomaticDestinations\\* — Jump Lists
|
|
• %APPDATA%\\Microsoft\\Windows\\Recent\\CustomDestinations\\* — Jump Lists
|
|
• HKCU\\…\\Explorer\\RunMRU — Win+R history
|
|
• HKCU\\…\\Explorer\\TypedPaths — Explorer typed paths
|
|
• HKCU\\…\\Explorer\\WordWheelQuery — Start / Explorer search history
|
|
• HKCU\\…\\Explorer\\RecentDocs — recent docs MRU
|
|
• Clipboard — current contents
|
|
|
|
Counts are reported but specific filenames are never logged (this is the
|
|
opposite of what we want to leak). All deletes use ``ignore_errors=True``
|
|
because files in use by other apps are expected.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import ctypes
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import winreg
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from .firewall import is_admin
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
_REG_MRU_KEYS = (
|
|
r"Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU",
|
|
r"Software\Microsoft\Windows\CurrentVersion\Explorer\TypedPaths",
|
|
r"Software\Microsoft\Windows\CurrentVersion\Explorer\WordWheelQuery",
|
|
r"Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs",
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class WipeReport:
|
|
files_deleted: int = 0
|
|
bytes_freed: int = 0
|
|
folders_skipped: int = 0
|
|
registry_keys_cleared: int = 0
|
|
clipboard_cleared: bool = False
|
|
errors: list[str] = field(default_factory=list)
|
|
|
|
def summary(self) -> str:
|
|
mb = self.bytes_freed / (1024 * 1024)
|
|
return (
|
|
f"{self.files_deleted} files / {mb:.1f} MB freed, "
|
|
f"{self.registry_keys_cleared} MRU entries cleared, "
|
|
f"clipboard={'yes' if self.clipboard_cleared else 'no'}, "
|
|
f"errors={len(self.errors)}"
|
|
)
|
|
|
|
|
|
def _walk_size(path: Path) -> int:
|
|
total = 0
|
|
for p in path.rglob("*"):
|
|
try:
|
|
if p.is_file():
|
|
total += p.stat().st_size
|
|
except OSError:
|
|
pass
|
|
return total
|
|
|
|
|
|
def _purge_dir(path: Path, rep: WipeReport, keep_root: bool = True) -> None:
|
|
"""Delete contents of `path`. Preserves the directory itself when keep_root."""
|
|
if not path.exists():
|
|
return
|
|
try:
|
|
size_before = _walk_size(path)
|
|
except Exception:
|
|
size_before = 0
|
|
|
|
count = 0
|
|
for child in path.iterdir():
|
|
try:
|
|
if child.is_dir():
|
|
shutil.rmtree(child, ignore_errors=True)
|
|
else:
|
|
child.unlink(missing_ok=True)
|
|
count += 1
|
|
except OSError:
|
|
rep.folders_skipped += 1
|
|
|
|
if not keep_root:
|
|
try:
|
|
path.rmdir()
|
|
except OSError:
|
|
pass
|
|
|
|
rep.files_deleted += count
|
|
try:
|
|
size_after = _walk_size(path)
|
|
except Exception:
|
|
size_after = 0
|
|
rep.bytes_freed += max(0, size_before - size_after)
|
|
|
|
|
|
def _clear_mru_key(path: str, rep: WipeReport) -> None:
|
|
"""Remove every value under a Run/Typed/Search MRU key."""
|
|
try:
|
|
with winreg.OpenKey(
|
|
winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_ALL_ACCESS
|
|
) as key:
|
|
count = 0
|
|
try:
|
|
while True:
|
|
name, _, _ = winreg.EnumValue(key, 0)
|
|
try:
|
|
winreg.DeleteValue(key, name)
|
|
count += 1
|
|
except OSError:
|
|
break
|
|
except OSError:
|
|
pass
|
|
# Recurse into subkeys (e.g. RecentDocs/.png)
|
|
sub_count = 0
|
|
try:
|
|
while True:
|
|
sub_count += 1
|
|
sub_name = winreg.EnumKey(key, 0)
|
|
try:
|
|
winreg.DeleteKey(key, sub_name)
|
|
except OSError:
|
|
break
|
|
if sub_count > 200: # safety cap
|
|
break
|
|
except OSError:
|
|
pass
|
|
if count or sub_count:
|
|
rep.registry_keys_cleared += 1
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def _clear_clipboard(rep: WipeReport) -> None:
|
|
if sys.platform == "darwin":
|
|
try:
|
|
subprocess.run(["pbcopy"], input="", text=True, timeout=5)
|
|
rep.clipboard_cleared = True
|
|
except Exception as e:
|
|
rep.errors.append(f"clipboard: {e}")
|
|
return
|
|
try:
|
|
user32 = ctypes.windll.user32 # type: ignore[attr-defined]
|
|
if user32.OpenClipboard(None):
|
|
try:
|
|
user32.EmptyClipboard()
|
|
rep.clipboard_cleared = True
|
|
finally:
|
|
user32.CloseClipboard()
|
|
except Exception as e:
|
|
rep.errors.append(f"clipboard: {e}")
|
|
|
|
|
|
def wipe_artifacts(include_prefetch: bool = True) -> WipeReport:
|
|
rep = WipeReport()
|
|
if sys.platform == "darwin":
|
|
import tempfile
|
|
temp = Path(tempfile.gettempdir())
|
|
_purge_dir(temp, rep, keep_root=True)
|
|
for path in (
|
|
Path.home() / "Library" / "Caches" / "ProxyChainManager",
|
|
Path.home() / "Library" / "Logs" / "ProxyGod.out.log",
|
|
Path.home() / "Library" / "Logs" / "ProxyGod.err.log",
|
|
):
|
|
try:
|
|
if path.is_dir():
|
|
_purge_dir(path, rep, keep_root=False)
|
|
elif path.is_file():
|
|
sz = path.stat().st_size
|
|
path.unlink(missing_ok=True)
|
|
rep.files_deleted += 1
|
|
rep.bytes_freed += sz
|
|
except OSError as exc:
|
|
rep.errors.append(f"{path.name}: {exc}")
|
|
_clear_clipboard(rep)
|
|
return rep
|
|
appdata = Path(os.environ.get("APPDATA", "")) if os.environ.get("APPDATA") else None
|
|
temp = Path(os.environ.get("TEMP", "")) if os.environ.get("TEMP") else None
|
|
sysroot = Path(os.environ.get("SystemRoot", r"C:\Windows"))
|
|
|
|
# %TEMP%
|
|
if temp and temp.exists():
|
|
_purge_dir(temp, rep, keep_root=True)
|
|
|
|
# Prefetch (Admin)
|
|
if include_prefetch and is_admin():
|
|
pf = sysroot / "Prefetch"
|
|
if pf.exists():
|
|
count = 0
|
|
for f in pf.glob("*.pf"):
|
|
try:
|
|
sz = f.stat().st_size
|
|
f.unlink(missing_ok=True)
|
|
count += 1
|
|
rep.bytes_freed += sz
|
|
except OSError as e:
|
|
rep.errors.append(f"prefetch: {e}")
|
|
rep.files_deleted += count
|
|
|
|
# Recent / Jump Lists
|
|
if appdata:
|
|
recent = appdata / "Microsoft" / "Windows" / "Recent"
|
|
if recent.exists():
|
|
_purge_dir(recent / "AutomaticDestinations", rep, keep_root=True)
|
|
_purge_dir(recent / "CustomDestinations", rep, keep_root=True)
|
|
_purge_dir(recent, rep, keep_root=True)
|
|
|
|
# MRU registry keys
|
|
for path in _REG_MRU_KEYS:
|
|
_clear_mru_key(path, rep)
|
|
|
|
# Clipboard
|
|
_clear_clipboard(rep)
|
|
|
|
# Trigger Explorer's "Clear recent items" via Shell API (covers Win10/11
|
|
# Quick Access pinned ↔ recent lists not covered by raw file delete).
|
|
try:
|
|
shell32 = ctypes.windll.shell32 # type: ignore[attr-defined]
|
|
# SHCNE_ASSOCCHANGED tells Explorer to refresh its caches.
|
|
shell32.SHChangeNotify(0x08000000, 0x0000, None, None)
|
|
except Exception:
|
|
pass
|
|
|
|
# Best-effort Defender quick-scan history flush — non-fatal.
|
|
try:
|
|
subprocess.run(
|
|
["powershell", "-NoProfile", "-NonInteractive", "-Command",
|
|
"Clear-RecycleBin -Force -ErrorAction SilentlyContinue"],
|
|
capture_output=True, text=True, timeout=20,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
return rep
|