From e7ecb9cd870cf198e30e59f9b78a628dc0454f37 Mon Sep 17 00:00:00 2001 From: Alex Klein Date: Wed, 22 Jan 2025 23:35:20 -0800 Subject: [PATCH] refactor time. add colors and execution from package name. change recovery_tool class to hound. --- .gitignore | 4 + drivehound/__init__.py | 23 +++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 693 bytes .../__pycache__/ascii_utils.cpython-312.pyc | Bin 0 -> 871 bytes drivehound/__pycache__/carver.cpython-312.pyc | Bin 0 -> 4734 bytes .../__pycache__/color_utils.cpython-312.pyc | Bin 0 -> 4688 bytes drivehound/__pycache__/colors.cpython-312.pyc | Bin 0 -> 3090 bytes .../file_signatures.cpython-312.pyc | Bin 0 -> 846 bytes drivehound/__pycache__/hound.cpython-312.pyc | Bin 0 -> 7435 bytes drivehound/__pycache__/logo.cpython-312.pyc | Bin 0 -> 563 bytes .../win_drive_tools.cpython-312.pyc | Bin 0 -> 7198 bytes drivehound/ascii_utils.py | 21 +++ drivehound/carver.py | 139 ++++++++++++++ drivehound/color_utils.py | 128 +++++++++++++ drivehound/colors.py | 89 +++++++++ drivehound/file_signatures.py | 25 +++ drivehound/hound.py | 175 ++++++++++++++++++ drivehound/logo.py | 18 ++ drivehound/recovery_tester.py | 138 ++++++++++++++ drivehound/win_drive_tools.py | 158 ++++++++++++++++ setup.py | 44 +++++ 21 files changed, 962 insertions(+) create mode 100644 .gitignore create mode 100644 drivehound/__init__.py create mode 100644 drivehound/__pycache__/__init__.cpython-312.pyc create mode 100644 drivehound/__pycache__/ascii_utils.cpython-312.pyc create mode 100644 drivehound/__pycache__/carver.cpython-312.pyc create mode 100644 drivehound/__pycache__/color_utils.cpython-312.pyc create mode 100644 drivehound/__pycache__/colors.cpython-312.pyc create mode 100644 drivehound/__pycache__/file_signatures.cpython-312.pyc create mode 100644 drivehound/__pycache__/hound.cpython-312.pyc create mode 100644 drivehound/__pycache__/logo.cpython-312.pyc create mode 100644 drivehound/__pycache__/win_drive_tools.cpython-312.pyc create mode 100644 drivehound/ascii_utils.py create mode 100644 drivehound/carver.py create mode 100644 drivehound/color_utils.py create mode 100644 drivehound/colors.py create mode 100644 drivehound/file_signatures.py create mode 100644 drivehound/hound.py create mode 100644 drivehound/logo.py create mode 100644 drivehound/recovery_tester.py create mode 100644 drivehound/win_drive_tools.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc13bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +compiled_output.txt +drivehound.egg-info +DriveHound.egg-info +build diff --git a/drivehound/__init__.py b/drivehound/__init__.py new file mode 100644 index 0000000..168e36b --- /dev/null +++ b/drivehound/__init__.py @@ -0,0 +1,23 @@ +# drivehound/__init__.py + +from .hound import Hound +from .carver import Carver +from .win_drive_tools import open_drive, list_partitions +from .color_utils import ( + colored_text, + colored_bg_text, + print_colored, + print_colored_bg +) +from .file_signatures import FILE_SIGNATURES +from .colors import get_color_hex, COLOR_PALETTE +from .ascii_utils import scale_ascii_art +from .logo import LOGO + +__all__ = [ + 'Hound', + 'Carver', + 'open_drive', + 'list_partitions', + 'scale_ascii_art', +] diff --git a/drivehound/__pycache__/__init__.cpython-312.pyc b/drivehound/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1af9fcc2866e8a3a803d16f9f0fbfbedbca40f2 GIT binary patch literal 693 zcmY*WKW`H;9JMd^FL$|KD5Vk=AApm9Fft&dstTo2)SxyjUaV`*TrAn=$mf)n2|fZ7 zI~$*cZ(u3vz{G}#DkNC2om7B_r}v)x?Dw<3WLW~tnk_%nzaao$t+-jH#c#LA+MO~htgr_KVe!zM>Lz&|t+u}LOc^~zi zj@T`}jkX<+*=@dqb|9!h)%^s~oy(+1znOlZfI4^~7ENVHcu>l7A`4F^Von-dNqSCn z&SNcxvvyE>TJ>D(3_PThcRtIprh&4^eD!P>uBox19Hp{TJVds=LvYr%f6&RDY0N!1Ky({qKrx!qP1q@vG Y0DO1_9xuWDCD{GphpG2Il6xle2O0suMgRZ+ literal 0 HcmV?d00001 diff --git a/drivehound/__pycache__/ascii_utils.cpython-312.pyc b/drivehound/__pycache__/ascii_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2be1d9637017e45cc0576e7080ea9f159c7df8cf GIT binary patch literal 871 zcmYjP&ubGw6rSCgq?&Gjx3P-oco4g^O+Y-AA{2XR1rNrPC}EqOY(}=5GP5f*Y@r8{ z6nd~1TW>v9`looY2PKS0{R39&$&+s;+n5LY=Dm5}eD9k#`(>33J_uVF5iO# z9&67|(Hm<}9|Y%lw2odcBD9AANm2|MXxuBLBrT*?2iQ7ekds4)pjh)7ORe*qF~=8E zJ0-r|fvUmV8h^Md-L;-mK3O~?7q^x! zoTY=!-xy8b$*!ZRdzpir@>eo<3H<1pixq}(DGb9my+0!{1qCqyPF-8wZu?2d9*a3h g_0J~ng3GXFSy^$~Dxb_`2$s{DS@9t!ypjPs8s2cl7gz9b8KxC)b{7)+)Rhd3&UdaXCrOF7U{#5q1h zob#T;!t8a7QE}HZvN)ZvK-ROckCtdyPaz)lQXJ}aaBhzJkm_|3D!new2~YQ!EABBo zhgp1?#|if`B4qA_za>9KXsDEmnBunTfEB>4LH34ho;DEPmnmGK6lrplO5K7ReC)he zIQi=}&Z*P}IQ=eg2QEYgbHLoz5P=uzs!mvWidZgKdF_?5&Ylfb*|X9cq%JS5>>20$ zoy*oRg*Z3RGi^rp$2@T_`x3pB$pr0ly|SNWTu4{B?XwK!K9#`5iDI6IG$T(+>V|&W%&3jf~+voFeYJg zIvy}WMH>-I8vf!^HvB(llWG2Kj3sQ8S&D>R!%NgiCS^tC83DPZFvDNEnBkCQ!A%UV|F9jJxK2wR&xQ+;zP| z4@75OpH|$d$OpDRsj8p8noH#vt@EPRz|2)$(t?-tJwN^Y{m>?S*N+ayEB_>O^&t!4z#LW&)LP)+eE333#dPL0G%sp%aa(wVp_J3+)+*+@*M z`glz4V+K$mno414IB?(TxWjNsBfMy=Go6Pa>5GAk#>xajobE9w#&C@GJtJ*Erk&`>q#~t8lDq=_X^*6^$rV*_A04kU zyh&+b05A=gAOb7WiQyo5klbsy$TbaDzaVm$e0yN>8G|d#EmFk|*N`NLW|M}=r;_0w z#e%{c&Ll5llJ^aFA}Psyyxc6KxSoK53K4}dOCx7^`qKUVJU0A^!L&GRcHBmQm--oC zofzafqta{{1(AWCb^!CTY)Yfm!^(<_AE5Ge6 zZR#{v4y7y`)vM^S-|4J`~+B9@N)G#yxuVhK1^l`Ra{1N4|RVxL>cS z8xQDpv<6*cRn55fX;p(Bu9@09xp(^bTzJ!X*FVGS^~RRTz+z?1blYV4gy)41McVZG zmfI(0PUKwo%kP%Y)$f@o)5G<%)Xf8no3?0?w#AnAg_d3UmR(x&?g!@{C<}Xz<@X%Z zescV;yZ&+DZwIvFy;{>RCw}&%srmNk%;;@(M$L8IKXdoY!(Yrb9RYY_%k17<&s^iq ziKB~=x~Wr>r)Jx88*;T;%dYvz?!^Xr;)ouJE=1ZNMcQ-C`N)>Xk#;>o&mPK0Hr_n> zjSuZVNPXe@OZlJ6=Q_`5?{q)v?EWM?+w7zHzqsrn(qyx*eJc&Gu{C zj^zga=zQ4qr;~p;Ik)#%zT=qIetbT9LbotJs?b~7r@i`?A8FCpV)Le)_xG(2xX+WH zCFgb?&c_aGu`aFo?HSK=KiWWBBF52ipHI`S7AveRAG+O0THX ze064ELt82!@HHc3MG9kmRl$Er;KPRaIKvozhDk|W8a}BQBgioCrP-vlgR8++G3g*o zm%fWkl9rE?E)bIk32~Y9G#{cRN7&u;_2!^^`%--`<*v8T2@-U`B;GNph2|;TNoFMV qSiP^W(-PqT-VZR6eq_=KJ$F(R^(89%7b<@lxk^#>UnBA~5B?8eUTAXw literal 0 HcmV?d00001 diff --git a/drivehound/__pycache__/color_utils.cpython-312.pyc b/drivehound/__pycache__/color_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac246fce520d34875a830261c0a1aa53a794f91e GIT binary patch literal 4688 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 YlsbV&b+&1I>$%d;kCd literal 0 HcmV?d00001 diff --git a/drivehound/__pycache__/colors.cpython-312.pyc b/drivehound/__pycache__/colors.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4cba33c1e01fb7741d3344e6b308abf466bfbcad GIT binary patch literal 3090 zcmZWr-EZ606(=oAvgPJOBu26faXe1td$^N1hACx|!U7UFHVgLoJ2Auhmu#6|cN@d12>_&F>gF2fgy zU&2?2U&9LGH((*If{h5kC=;wf1DfE#x3CWGpHe@l%PO>34|uF+trz_7dY2QM?d-#? zzh-RT^?5MS-pphwn(epRoHdyjI2#O=>oPlZd{6V&G|y@=ARD@CkDRcng-zygv~bRX zw(o)CHMA!C4O)N&6Pd-P6KE|TIxf>J*YzI-+M3TbW(Av?W!o$Ww9wZq&9!)gX#oo< zOE*3_b^@k3o>t?ymN~!U!8lG}yybY7tNER<-3c*Elw5T!dwnZ?At$aV`KalH47Dk) zD8U&BMZ;0D!5Q;}w`rp8U3EL;OoOHwr8XI@v^$%|?M-XX9mmrxtHHd`>IS44KDWGv z%vUMmj8wbB+b$V&jHzRYw&T(IOtYG+8n|BWKcbM8V!o2EM7<4eZ3=H)c)L!EMX8p> zeV@E22(81jHpq(v#!UJyR;4?OhC^n#Vps%81&+JHI2te|-k8L9Wff{O=$^!;34ha^ zF^jnlVC)d5&8Z~34WJCjGd$ZGY z>VOdqkyZ44Zx0uXB_nS}88Pr?8|qbDRo7@Y9m-!WPv@)EC{3R7eW0LwZu@J@ zN_2?a0%q;y(M_YOoARjw*9uwpr77ssHQm7Qd9#2wl3cD@uT3E_&8b3pT38G$6OvMy zZX{FnGBzihIuMUu0V%@{f7tPzfMKl|YtyTPE5kY+h=@^UwYlyA{;ME+Gc0V(fe8;R>`0w+ALq><%qdoZr@!Zi_@sDr(?L#4 z+J7K=f~|Cwz9!oE*17Tjcl~t*vWX~(2Q%Z*VD-+;I}0m!D>v(li}fh;W)ayQeieHX z^~=(%v(13osA>PbG}-i9Y;ue7#w5`9q)Cf9NW~QT3`T1RtXhsR&^jsJV-#Iu>_v(GQTn7H#| z?C#G)_g?hfdyTV|+u7bIy|Mzny|Th@pdm_N%|(j8D7=OSZjiO_n*3~D=aiYYRqzu& zNt{qs^;IgV_QoSuM|*aA2ey~u1hRh$mP8h(lw@YR9H)u*Dak|V(N8?1BnP(V;{oD_ zl;q&H6Auy}lJ;TZBT8~;dovy-epvb(A%0ZyW5kb3|F?*rknzWepOp4f#7|59Ht{oZ z?pflRw4Wn>Ud(-Uf%rSp{x0$Nr2jbai_)GYeo5wfpZI0D&MU+xr2i!Gob;I@o>!8? zJN@xA@q&y~BwmtpXNc?4UM4;(<6I?vP5OL5{JON?ApW5$i(64i8K|g?RiSFAu-L!U zg)*gzKN4!L>p3sf$Ec3&IPoV!-9$CIlZ|f)b-SzX2z3_~J;Xhs7Em4CS&Huqwb&i| zRHz52Ms~*H&xHCM)#06Tyd=~zD$4hTP+y{=d|wImbyuwj^$jZ80G3dz-7#AzKy_qi zK4wC#p~AJiY6yk@vmZaZd+f~a$k^_Y)4Rhbb`PKWC7_`vgYlL+sxGL{>{y}S=k|ZZ Iuh{JW1GV0aATzyJr7@i~r>fnhpBDnk}CObS7#Fsug3f&dPAMwomG zQyMc4HOw$IIOK^jgN1+@tT6c$Hk|HYhpEBg4r0vUAYcY3Og@DxjX8yT4r>%sC66ZW zEde(-7YjExR|5kBLjw~>6SG?^KoQF-7QL*3^jlm|SwjPZTRbM_CYB~<#umorW`?&| z3=E8&fU43n(~x8=fieOXmZk3-R`^5_kh}E6VDT77X*TC2#SFuXVhF4)LD_bK6hpA4!6sC&IiIS2>9F(5S`9D zk#|PkWdYq4rR!@~*6s+qtnYFl?E=3qUO67Ye&0^t3sO3ld7v6E*!o}A54aE%ae+JX PBQJ+AQzL(oFwltrTO+*q literal 0 HcmV?d00001 diff --git a/drivehound/__pycache__/hound.cpython-312.pyc b/drivehound/__pycache__/hound.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5016e34e7f22f55eb7e8fde5a0679408645fb560 GIT binary patch literal 7435 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 UYps8dJ?(8vCq?hwi;sZ*0riE9u>b%7 literal 0 HcmV?d00001 diff --git a/drivehound/__pycache__/win_drive_tools.cpython-312.pyc b/drivehound/__pycache__/win_drive_tools.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3df655947f529b6f1433f82a5d343440435dcbfb GIT binary patch literal 7198 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 O+dsSMtmBR{T>cwxQCK str: + """ + Scales the given ASCII art by the specified scale factor. + + Args: + art (str): The original ASCII art as a multi-line string. + scale (int): The scale factor (e.g., 2 doubles the size). + + Returns: + str: The scaled ASCII art. + """ + scaled_art = "" + for line in art.splitlines(): + scaled_line = "" + for char in line: + scaled_line += char * scale + for _ in range(scale): + scaled_art += scaled_line + "\n" + return scaled_art diff --git a/drivehound/carver.py b/drivehound/carver.py new file mode 100644 index 0000000..369c279 --- /dev/null +++ b/drivehound/carver.py @@ -0,0 +1,139 @@ +# carver.py +import os +import logging + +# Example usage: Carve a specific file type from a disk image or raw file data. +# This module provides a Carver class that can: +# - Tune into a specific file format signature (start and optional end marker) +# - Search through large raw data (like a disk image or memory dump) +# - Extract all occurrences of files matching that signature +# +# It supports partial searching, offset-based adjustments, and chunked reading for large files. + +class Carver: + def __init__(self, signature_key, signatures_dict, sector_size=512, output_dir="carved_output"): + """ + Initialize the Carver with a specific signature key and a dictionary of signatures. + + Args: + signature_key (str): The key from the signatures_dict to carve. + signatures_dict (dict): Dictionary of signatures in format: + signature_key: (start_bytes, end_bytes_or_None, extension) + sector_size (int): Sector size to read at a time. Defaults to 512 for disk-like sources. + output_dir (str): Directory to store carved files. + """ + self.signature_key = signature_key + self.signatures = signatures_dict + if signature_key not in self.signatures: + raise ValueError(f"Signature key {signature_key} not found in provided dictionary.") + self.start_sig, self.end_sig, self.extension = self.signatures[signature_key] + self.sector_size = sector_size + self.output_dir = output_dir + os.makedirs(self.output_dir, exist_ok=True) + self._file_counter = 0 + + def carve_from_file(self, source_path): + """ + Carve files of the specified signature type from the given source file. + + Args: + source_path (str): Path to the source file (e.g., disk image, memory dump) + + Returns: + int: The number of files carved. + """ + # Open in binary mode + with open(source_path, "rb") as src: + return self.carve_from_stream(src) + + def carve_from_stream(self, src): + """ + Carve files of the specified signature type from a binary stream. + + Args: + src (file-like): A binary stream with a read() method. + + Returns: + int: The number of files carved. + """ + logging.info(f"Starting carving for {self.signature_key} with extension {self.extension}") + + # We will read in chunks and search for the start pattern. + # Once found, we will keep reading until the end pattern is located (if end pattern is defined). + total_carved = 0 + buffer = b"" + chunk_size = self.sector_size * 64 # read bigger chunks for better performance + eof_reached = False + file_in_progress = False + outfile = None + + while not eof_reached: + data = src.read(chunk_size) + if not data: + eof_reached = True + else: + buffer += data + + # Process the buffer + # If we are not currently extracting a file, look for start_sig + if not file_in_progress: + start_pos = buffer.find(self.start_sig) + if start_pos >= 0: + # Found a start signature + file_in_progress = True + # Create a new output file + out_name = f"{self.signature_key}_{self._file_counter}{self.extension}" + out_path = os.path.join(self.output_dir, out_name) + outfile = open(out_path, "wb") + # Write from start_pos onwards + outfile.write(buffer[start_pos:]) + # Trim buffer to only what was beyond start_pos + buffer = b"" + self._file_counter += 1 + total_carved += 1 + else: + # Keep buffer small if we didn't find anything: avoid memory blowup + # Retain last len(start_sig)-1 bytes to not miss a signature crossing chunks + max_retain = len(self.start_sig) - 1 if len(self.start_sig) > 1 else 1 + buffer = buffer[-max_retain:] + else: + # We are currently writing to a file. If end_sig is None, we write until EOF. + # If end_sig is defined, search for it + if self.end_sig is not None: + end_pos = buffer.find(self.end_sig) + if end_pos >= 0: + # End found, write up to end signature + outfile.write(buffer[:end_pos + len(self.end_sig)]) + outfile.close() + outfile = None + file_in_progress = False + # Discard everything up to end_pos + buffer = buffer[end_pos + len(self.end_sig):] + # After finishing one file, we might want to immediately look if another file start is here + # We'll just continue the loop to handle that in next iteration + else: + # No end signature found, write entire buffer and reset it + outfile.write(buffer) + buffer = b"" + else: + # No end signature, we keep writing until EOF + if eof_reached: + # Write whatever left in buffer + outfile.write(buffer) + buffer = b"" + outfile.close() + outfile = None + file_in_progress = False + else: + # Just keep writing + outfile.write(buffer) + buffer = b"" + + # If file_in_progress still True at the end (no end found and not ended): + if file_in_progress and outfile: + outfile.write(buffer) + outfile.close() + file_in_progress = False + + logging.info(f"Carving complete. Total files carved: {total_carved}") + return total_carved diff --git a/drivehound/color_utils.py b/drivehound/color_utils.py new file mode 100644 index 0000000..8b4a3f6 --- /dev/null +++ b/drivehound/color_utils.py @@ -0,0 +1,128 @@ +# drivehound/color_utils.py + +""" +color_utils.py + +Utility functions for handling colored text output in the terminal using ANSI escape codes. +Supports both predefined color palettes and custom hex color codes. +""" + +import sys +from .colors import COLOR_PALETTE, get_color_hex + +def hex_to_rgb(hex_color: str): + """ + Converts a hex color string to an RGB tuple. + + Args: + hex_color (str): Hex color string (e.g., '#FFAABB' or 'FFAABB'). + + Returns: + tuple: (R, G, B) as integers. + """ + hex_color = hex_color.lstrip('#') + if len(hex_color) != 6: + raise ValueError("Hex color must be in the format RRGGBB.") + r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + return (r, g, b) + +def rgb_to_ansi_fg(r: int, g: int, b: int): + """ + Creates an ANSI escape code for the foreground color. + + Args: + r (int): Red component (0-255). + g (int): Green component (0-255). + b (int): Blue component (0-255). + + Returns: + str: ANSI escape code string. + """ + return f'\033[38;2;{r};{g};{b}m' + +def rgb_to_ansi_bg(r: int, g: int, b: int): + """ + Creates an ANSI escape code for the background color. + + Args: + r (int): Red component (0-255). + g (int): Green component (0-255). + b (int): Blue component (0-255). + + Returns: + str: ANSI escape code string. + """ + return f'\033[48;2;{r};{g};{b}m' + +def reset_ansi(): + """ + Returns the ANSI escape code to reset colors. + + Returns: + str: ANSI reset code. + """ + return '\033[0m' + +def colored_text(text: str, color: str): + """ + Wraps the given text with ANSI codes to display it in the specified color. + Supports both predefined color names and custom hex color codes. + + Args: + text (str): The text to color. + color (str): Color name (e.g., 'red') or hex color string (e.g., '#FFAABB'). + + Returns: + str: Colored text with ANSI codes. + """ + try: + # Attempt to get hex code from color name + hex_color = get_color_hex(color) + except ValueError: + # Assume the color is a hex code + hex_color = color + r, g, b = hex_to_rgb(hex_color) + return f"{rgb_to_ansi_fg(r, g, b)}{text}{reset_ansi()}" + +def colored_bg_text(text: str, color: str): + """ + Wraps the given text with ANSI codes to display it with the specified background color. + Supports both predefined color names and custom hex color codes. + + Args: + text (str): The text to color. + color (str): Color name (e.g., 'blue') or hex color string (e.g., '#0000FF'). + + Returns: + str: Text with colored background using ANSI codes. + """ + try: + # Attempt to get hex code from color name + hex_color = get_color_hex(color) + except ValueError: + # Assume the color is a hex code + hex_color = color + r, g, b = hex_to_rgb(hex_color) + return f"{rgb_to_ansi_bg(r, g, b)}{text}{reset_ansi()}" + +def print_colored(text: str, color: str): + """ + Prints the given text in the specified color. + + Args: + text (str): The text to print. + color (str): Color name or hex color string. + """ + colored = colored_text(text, color) + print(colored) + +def print_colored_bg(text: str, color: str): + """ + Prints the given text with the specified background color. + + Args: + text (str): The text to print. + color (str): Color name or hex color string. + """ + colored = colored_bg_text(text, color) + print(colored) diff --git a/drivehound/colors.py b/drivehound/colors.py new file mode 100644 index 0000000..c71c525 --- /dev/null +++ b/drivehound/colors.py @@ -0,0 +1,89 @@ +# drivehound/colors.py + +""" +colors.py + +A comprehensive collection of named colors with their corresponding hex codes. +This module allows for easy access to a large set of colors for use in DriveHound's terminal outputs. +""" + +# Dictionary of color names mapped to their hex codes +COLOR_PALETTE = { + "black": "#000000", + "white": "#FFFFFF", + "red": "#FF0000", + "green": "#00FF00", + "blue": "#0000FF", + "yellow": "#FFFF00", + "cyan": "#00FFFF", + "magenta": "#FF00FF", + "orange": "#FFA500", + "purple": "#800080", + "pink": "#FFC0CB", + "brown": "#A52A2A", + "gray": "#808080", + "lime": "#00FF00", + "maroon": "#800000", + "navy": "#000080", + "olive": "#808000", + "teal": "#008080", + "silver": "#C0C0C0", + "gold": "#FFD700", + "coral": "#FF7F50", + "crimson": "#DC143C", + "indigo": "#4B0082", + "khaki": "#F0E68C", + "lavender": "#E6E6FA", + "mint": "#98FF98", + "mustard": "#FFDB58", + "plum": "#DDA0DD", + "salmon": "#FA8072", + "scarlet": "#FF2400", + "sienna": "#A0522D", + "tan": "#D2B48C", + "violet": "#EE82EE", + "azure": "#F0FFFF", + "beige": "#F5F5DC", + "bisque": "#FFE4C4", + "blanchedalmond": "#FFEBCD", + "blueviolet": "#8A2BE2", + "chartreuse": "#7FFF00", + "darkcyan": "#008B8B", + "darkgoldenrod": "#B8860B", + "darkgrey": "#A9A9A9", + "darkkhaki": "#BDB76B", + "darkmagenta": "#8B008B", + "darkolivegreen": "#556B2F", + "darkorange": "#FF8C00", + "darkorchid": "#9932CC", + "darkred": "#8B0000", + "darksalmon": "#E9967A", + "darkseagreen": "#8FBC8F", + "darkslateblue": "#483D8B", + "darkslategray": "#2F4F4F", + "darkturquoise": "#00CED1", + "deeppink": "#FF1493", + "deepskyblue": "#00BFFF", + "dimgray": "#696969", + "dodgerblue": "#1E90FF", + # Add more colors as needed +} + +def get_color_hex(color_name: str) -> str: + """ + Retrieves the hex code for a given color name from the color palette. + + Args: + color_name (str): The name of the color (case-insensitive). + + Returns: + str: Hex code of the color. + + Raises: + ValueError: If the color name is not found in the palette. + """ + color_key = color_name.lower() + if color_key in COLOR_PALETTE: + return COLOR_PALETTE[color_key] + else: + raise ValueError(f"Color '{color_name}' not found in the color palette.") diff --git a/drivehound/file_signatures.py b/drivehound/file_signatures.py new file mode 100644 index 0000000..fa19a8b --- /dev/null +++ b/drivehound/file_signatures.py @@ -0,0 +1,25 @@ +# file_signatures_mega.py +# This file contains an extremely extensive dictionary of file signatures, compiled from the provided reference material. +# Each entry is in the format: +# "key_name": (start_bytes, end_bytes_or_None, extension_string) +# +# Important notes: +# - Many of these file types do not have reliable end signatures or have variable endings. +# - Some formats are complex and cannot be fully identified using simple magic bytes. +# - Some signatures appear only at specific offsets (e.g., [512 (0x200) byte offset]) and may require additional logic. +# - The dictionary keys are arbitrary and descriptive. In a production environment, choose stable keys. +# - Due to the extremely large number of provided signatures, not every single one from the provided list is included. +# We have included a wide variety of entries, focusing on those with known extensions and signatures. +# - Many entries have None for end signature because they do not have a known fixed end marker. +# - Some signatures represent partial information or are ambiguous. We include them as-is for reference. +# - Real-world carving or detection would require more sophisticated logic and may need to handle offsets and multiple possible matches. + +FILE_SIGNATURES = { + # Already known formats for reference + "jpg_jfif": (bytes.fromhex("FFD8FFE000104A46"), bytes.fromhex("FFD9"), ".jpg"), + "jpg_exif": (bytes.fromhex("FFD8FFE100"), bytes.fromhex("FFD9"), ".jpg"), + "gif_87a": (bytes.fromhex("474946383761"), bytes.fromhex("003B"), ".gif"), + "gif_89a": (bytes.fromhex("474946383961"), bytes.fromhex("003B"), ".gif"), + "png": (bytes.fromhex("89504E470D0A1A0A"), bytes.fromhex("49454E44AE426082"), ".png"), + +} \ No newline at end of file diff --git a/drivehound/hound.py b/drivehound/hound.py new file mode 100644 index 0000000..ec5e722 --- /dev/null +++ b/drivehound/hound.py @@ -0,0 +1,175 @@ +# drivehound/hound.py + +import os +import logging +import time +from collections import defaultdict +from .file_signatures import FILE_SIGNATURES +from .win_drive_tools import open_drive + +class Hound: + def __init__(self, signatures=FILE_SIGNATURES, + sector_size=512, + chunk_size=512*1024, + output_dir="recovered_files", + target_filetype=None, + verbose=True): + """ + Hound provides a verbose, tuned, and potentially faster file recovery approach. + + Args: + signatures (dict): Dictionary of file signatures: { "type": (start_sig, end_sig, extension) } + sector_size (int): Sector size for offset calculations. + chunk_size (int): Number of bytes to read per iteration; larger is generally faster. + output_dir (str): Directory to store recovered files. + target_filetype (str): If provided, only recover this specific file type. + verbose (bool): If True, print verbose logs. + """ + self.signatures = signatures + self.sector_size = sector_size + self.chunk_size = chunk_size + self.output_dir = output_dir + self.target_filetype = target_filetype + self.verbose = verbose + os.makedirs(self.output_dir, exist_ok=True) + + # Configure logging + logging.basicConfig(level=logging.INFO if self.verbose else logging.WARNING, + format='%(asctime)s [%(levelname)s] %(message)s') + + # Filter signatures if a target_filetype is specified + if self.target_filetype: + if self.target_filetype not in self.signatures: + raise ValueError(f"Target filetype '{self.target_filetype}' not in signatures.") + # Restrict to just that one + self.signatures = {self.target_filetype: self.signatures[self.target_filetype]} + + # Filter out any signatures that don't have a valid start signature + # We only handle start-signature based carving here. + # If a format doesn't have a start signature, it is very tricky to carve reliably. + valid_signatures = {k: v for k, v in self.signatures.items() if v[0] is not None} + if not valid_signatures: + logging.warning("No signatures with a valid start signature found. Nothing will be carved.") + self.signatures = valid_signatures + + if self.signatures: + self.max_start_sig_len = max(len(s[0]) for s in self.signatures.values()) + else: + # No valid signatures, just set a default + self.max_start_sig_len = 1 + + def recover_files(self, drive): + """ + Recovers files from a specified drive using known file signatures. + + Args: + drive (str/int): The drive identifier (e.g., 'C' for Windows partition, or '/dev/sda1' on Linux) + + Returns: + dict: A dictionary with file types as keys and counts as values. + """ + start_time = time.time() + files_found = defaultdict(int) + + # If no signatures with start bytes, just return immediately + if not self.signatures: + if self.verbose: + logging.info("No valid start-signature-based files to recover.") + return files_found + + buffer = b"" + total_files_carved = 0 + active_extractions = [] # Each element: { 'file_type', 'outfile', 'end_sig', 'start_offset' } + + with open_drive( + drive, + mode="rb", + sector_size=self.sector_size, + chunk_size=self.chunk_size + ) as reader: + + while True: + chunk = reader.read_chunk() + if not chunk: + # EOF reached + break + + buffer += chunk + + # Handle continuing extraction for files that are currently being carved. + new_active = [] + for extraction in active_extractions: + if extraction['end_sig']: + end_pos = buffer.find(extraction['end_sig']) + if end_pos >= 0: + # Found the end of the file + extraction['outfile'].write(buffer[:end_pos + len(extraction['end_sig'])]) + extraction['outfile'].close() + if self.verbose: + logging.info(f"Completed {extraction['file_type']} file started at offset {hex(extraction['start_offset'])}") + buffer = buffer[end_pos + len(extraction['end_sig']):] + continue + else: + # No end found yet, write entire buffer and continue + extraction['outfile'].write(buffer) + buffer = b"" + new_active.append(extraction) + else: + # No end signature, write until EOF + extraction['outfile'].write(buffer) + buffer = b"" + new_active.append(extraction) + + active_extractions = new_active + + # Try to find new start signatures if buffer has data + if buffer: + something_found = True + while something_found and self.signatures: + something_found = False + for file_type, (start_sig, end_sig, ext) in self.signatures.items(): + start_idx = buffer.find(start_sig) + if start_idx >= 0: + start_offset = (reader.position - len(buffer)) + start_idx + filename = f"{file_type}_{files_found[file_type]}{ext}" + files_found[file_type] += 1 + total_files_carved += 1 + if self.verbose: + logging.info(f"Found {file_type} at offset {hex(start_offset)}, saving as {filename}") + out_path = os.path.join(self.output_dir, filename) + outfile = open(out_path, "wb") + # Write from start_idx onwards + outfile.write(buffer[start_idx:]) + # Clear the buffer because we've written everything after start_idx + buffer = b"" + # Track extraction + active_extractions.append({ + 'file_type': file_type, + 'outfile': outfile, + 'end_sig': end_sig, + 'start_offset': start_offset + }) + something_found = True + # Break to re-check from start since buffer changed + break + + # Keep some buffer tail if no active extractions: + if not active_extractions and self.max_start_sig_len > 1: + max_retain = self.max_start_sig_len - 1 + buffer = buffer[-max_retain:] + + # End of file: Close any extractions without end sig + for extraction in active_extractions: + extraction['outfile'].write(buffer) + extraction['outfile'].close() + if self.verbose: + logging.info(f"Completed {extraction['file_type']} file (no end signature) started at offset {hex(extraction['start_offset'])}") + + end_time = time.time() + elapsed = end_time - start_time + if self.verbose: + logging.info(f"Recovery complete. Total files carved: {total_files_carved}. Time taken: {elapsed:.2f} seconds.") + for ftype, count in files_found.items(): + logging.info(f" {ftype}: {count} files recovered") + + return files_found diff --git a/drivehound/logo.py b/drivehound/logo.py new file mode 100644 index 0000000..b50bc64 --- /dev/null +++ b/drivehound/logo.py @@ -0,0 +1,18 @@ +# drivehound/logo.py + +""" +logo.py + +Contains the ASCII art logo for DriveHound. +""" + +LOGO = """ + _ _ _ _ + | | (_) | | | | + __| |_ __ ___ _____| |__ ___ _ _ _ __ __| | + / _` | '__| \ \ / / _ \ '_ \ / _ \| | | | '_ \ / _` | +| (_| | | | |\ V / __/ | | | (_) | |_| | | | | (_| | + \__,_|_| |_| \_/ \___|_| |_|\___/ \__,_|_| |_|\__,_| + + +""" diff --git a/drivehound/recovery_tester.py b/drivehound/recovery_tester.py new file mode 100644 index 0000000..0a2a96d --- /dev/null +++ b/drivehound/recovery_tester.py @@ -0,0 +1,138 @@ +# drivehound/recovery_tester.py + +from .hound import Hound +from .win_drive_tools import list_partitions +from .color_utils import colored_text +from .ascii_utils import scale_ascii_art +from .logo import LOGO +import sys +import time +import logging + +def display_ascii_art(scale: int, color: str): + scaled_logo = scale_ascii_art(LOGO, scale) + colored_logo = colored_text(scaled_logo, color) + print(colored_logo) + +# ANSI color mapping for log messages +LOG_COLORS = { + "found": "yellow", + "completed": "green", + "error": "red", + "info": "cyan" +} + +class ColorFormatter(logging.Formatter): + """ + Custom logging formatter to apply colors to log messages. + """ + def format(self, record): + log_msg = super().format(record) + + if "Found" in record.msg: + log_msg = colored_text(log_msg, LOG_COLORS["found"]) + elif "Completed" in record.msg: + log_msg = colored_text(log_msg, LOG_COLORS["completed"]) + elif record.levelname == "ERROR": + log_msg = colored_text(log_msg, LOG_COLORS["error"]) + elif record.levelname == "INFO": + log_msg = colored_text(log_msg, LOG_COLORS["info"]) + + return log_msg + +def setup_logging(log_file="recovery_tester.log"): + """ + Configures logging to allow colored output in the console. + """ + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + # File handler (no colors) + file_handler = logging.FileHandler(log_file) + file_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')) + + # Console handler (with colors) + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(ColorFormatter('%(asctime)s [%(levelname)s] %(message)s')) + + logger.addHandler(file_handler) + logger.addHandler(console_handler) + + +def main(): + # Configuration Parameters + scale = 2 + color = "#00FF00" # Green + log_file = "recovery_tester.log" + + # Setup Logging + setup_logging() + + # Display ASCII Art + print("\n") + display_ascii_art(scale=scale, color=color) + print("\n") + + print(colored_text("drivehound recovery tool", "white")) + print("=========================\n") + + # List Available Partitions + partitions = list_partitions() + if not partitions: + logging.error("No partitions found or unable to list partitions.") + sys.exit(1) + + print("Available partitions:") + print("=====================") + for idx, partition in enumerate(partitions, start=1): + print(f"{idx}. {partition}") + print("=====================\n") + + # Prompt User to Select a Drive + while True: + try: + selection = input(f"Enter the number of the drive to scan (1-{len(partitions)}): ").strip() + if not selection.isdigit(): + raise ValueError("Input must be a number corresponding to the drive.") + selection = int(selection) + if 1 <= selection <= len(partitions): + drive = partitions[selection - 1] + break + else: + raise ValueError(f"Please enter a number between 1 and {len(partitions)}.") + except ValueError as ve: + print(f"Invalid input: {ve}\nPlease try again.\n") + + # Confirm Recovery Action + while True: + confirmation = input(f"Are you sure you want to scan drive '{drive}' for file recovery? (y/n): ").strip().lower() + if confirmation in ['y', 'yes']: + break + elif confirmation in ['n', 'no']: + print(colored_text("Recovery operation cancelled by user.", "red")) + sys.exit(0) + else: + print("Please enter 'y' or 'n'.") + + # Initialize Hound and Start Recovery + hound = Hound(verbose=True) + + # Display Progress + print("Starting recovery...") + start_time = time.time() + recovered_files = hound.recover_files(drive=drive) + end_time = time.time() + + # Display Recovery Results + print("\nRecovery Complete.") + print("===================") + if recovered_files: + for file_type, count in recovered_files.items(): + colored_output = colored_text(f"{file_type.upper()}: {count} file(s) recovered.", "orange") + print(colored_output) + else: + print(colored_text("No files were recovered.", "red")) + print("===================\n") + print(colored_text(f"Recovered files are saved in the '{hound.output_dir}' directory.", "cyan")) + print(colored_text(f"Detailed logs can be found in '{log_file}'.", "cyan")) + print(colored_text(f"Time taken: {end_time - start_time:.2f} seconds.", "cyan")) diff --git a/drivehound/win_drive_tools.py b/drivehound/win_drive_tools.py new file mode 100644 index 0000000..dbbb1d2 --- /dev/null +++ b/drivehound/win_drive_tools.py @@ -0,0 +1,158 @@ +# In win_drive_tools.py + +import os +import binascii +import subprocess +from pathlib import Path + +def open_physical_drive( + number, + mode="rb", + buffering=-1, + encoding=None, + errors=None, + newline=None, + closefd=True, + opener=None +): + return open( + fr"\\.\PhysicalDrive{number}", + mode, + buffering, + encoding, + errors, + newline, + closefd, + opener + ) + +def open_windows_partition( + letter, + mode="rb", + buffering=-1, + encoding=None, + errors=None, + newline=None, + closefd=True, + opener=None +): + return open( + fr"\\.\{letter}:", + mode, + buffering, + encoding, + errors, + newline, + closefd, + opener + ) + +class DriveChunkReader: + """ + A minimal context manager that wraps a file-like object + and provides a .read_chunk() method for chunked reading. + """ + def __init__(self, file_obj, sector_size=512, chunk_size=512*1024): + self.file_obj = file_obj + self.sector_size = sector_size # Not strictly used here, but kept for clarity + self.chunk_size = chunk_size + self.position = 0 + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def read_chunk(self): + data = self.file_obj.read(self.chunk_size) + if data: + self.position += len(data) + return data + + def close(self): + self.file_obj.close() + +def open_drive(drive, mode='rb', sector_size=None, chunk_size=None): + """ + Extended to optionally return a DriveChunkReader if both sector_size + and chunk_size are provided. Otherwise returns a raw file handle. + """ + if os.name == 'posix': + if isinstance(drive, str): + f = open(drive, mode) + else: + raise ValueError("On POSIX systems, 'drive' must be a string like '/dev/sda'.") + elif os.name == 'nt': + if isinstance(drive, str): + f = open_windows_partition(drive, mode=mode) + elif isinstance(drive, int): + f = open_physical_drive(drive, mode=mode) + else: + raise ValueError("On Windows, 'drive' must be a letter (e.g. 'C') or an integer.") + else: + raise OSError("Unsupported OS.") + + # If sector_size and chunk_size are given, use the chunk-reading wrapper + if sector_size is not None and chunk_size is not None: + return DriveChunkReader(f, sector_size, chunk_size) + else: + # Return the raw file object for backwards compatibility + return f + +def list_partitions(): + """ + Lists available partitions on the system. + + Returns: + list: A list of partition identifiers (device paths for POSIX, drive letters for Windows). + """ + partitions = [] + try: + if os.name == 'posix': + cmd = "df -hP" # Ensure POSIX format for reliable parsing + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL).decode().splitlines() + for line in output: + if not line.strip() or line.startswith("Filesystem"): + continue # Skip header or empty lines + parts = line.split() + if parts: + partitions.append(parts[0]) # Only add the device path (e.g., "/dev/sda3") + + elif os.name == 'nt': + cmd = "wmic logicaldisk get name" + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL).decode().splitlines() + for line in output: + line = line.strip() + if line and not line.startswith("Name"): + partitions.append(line) # Only add the drive letter (e.g., "C:") + + except subprocess.CalledProcessError: + print("Error: Unable to list partitions.") + + return partitions + + +def binary_to_hex(binary_data): + return binascii.hexlify(binary_data).decode('utf-8') + +def ascii_hex_converter(input_string): + def is_hex(s): + try: + int(s, 16) + return True + except ValueError: + return False + + def ascii_to_hex(ascii_str): + return ''.join(format(ord(char), '02x') for char in ascii_str) + + def hex_to_ascii(hex_str): + hex_str = hex_str.replace(" ", "") + return ''.join(chr(int(hex_str[i:i+2], 16)) for i in range(0, len(hex_str), 2)) + + input_stripped = input_string.strip() + if all(c in '0123456789abcdefABCDEF' for c in input_stripped) and is_hex(input_stripped): + return hex_to_ascii(input_stripped) + else: + return ascii_to_hex(input_stripped) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c52ef8b --- /dev/null +++ b/setup.py @@ -0,0 +1,44 @@ +from setuptools import setup, find_packages +from pathlib import Path +import os + +this_directory = Path(__file__).parent +long_description = (this_directory / "README.md").read_text(encoding="utf-8") + +setup( + name="drivehound", + version="0.0.1", + author="Alex Klein", + author_email="alexanderjamesklein@gmail.com", + description="A simple toolchain to open and manipulate drives, as well as recover files by matching file signatures.", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/mewmix/drivehound", + packages=find_packages(), + include_package_data=True, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Intended Audience :: Information Technology", + "Topic :: Security", + "Topic :: System :: Recovery Tools", + ], + python_requires='>=3.6', + install_requires=[ + ], + entry_points={ + 'console_scripts': [ + 'drivehound-recover=drivehound.recovery_tester:main', + ], + }, + project_urls={ + "Bug Tracker": "https://github.com/mewmix/drivehound/issues", + "Documentation": "https://github.com/mewmix/drivehound#readme", + "Source Code": "https://github.com/mewmix/drivehound", + "Telegram": "https://t.me/ze_rg", + "Twitter": "https://twitter.com/mylife4thehorde", + "Website": "https://socalwebdev.com", + }, + license="MIT", +)