import tkinter as tk from tkinter import filedialog, messagebox, ttk, scrolledtext import logging import binascii import os import functools import re import struct import traceback import unicodedata class HexaWorkApp: def __init__(self, master): self.master = master master.title("HexaWork - Byte Inspector") master.geometry("1000x800") # Configure logging logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('hexawork.log', mode='w'), logging.StreamHandler() ] ) self.logger = logging.getLogger(__name__) # Create main frame self.main_frame = ttk.Frame(master) self.main_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) # Create menu self.create_menu() # File selection self.create_file_selection_section() # Hex view self.create_hex_view_section() # Order Analysis section self.create_order_analysis_section() # Inspector section self.create_inspector_section() # Search section self.create_search_section() # Selected bytes for analysis self.selected_bytes = None def create_menu(self): """Create application menu""" menubar = tk.Menu(self.master) self.master.config(menu=menubar) # File menu file_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="File", menu=file_menu) file_menu.add_command(label="Open", command=self.select_file) file_menu.add_command(label="Save", command=self.save_edited_file, state=tk.NORMAL) file_menu.add_command(label="Analyze ASCII", command=self.analyze_ascii_patterns, state=tk.NORMAL) file_menu.add_separator() file_menu.add_command(label="Exit", command=self.master.quit) def show_compare_dialog(self): """Show dialog to select files for comparison""" try: # Get original file original_file = filedialog.askopenfilename(title="Select Original File") if not original_file: return # Get modified file modified_file = filedialog.askopenfilename(title="Select Modified File") if not modified_file: return self.compare_files(original_file, modified_file) except Exception as e: self.logger.error(f"Error in compare dialog: {str(e)}") messagebox.showerror("Error", str(e)) def compare_files(self, original_path, modified_path): """Compare two binary files and show differences side by side""" try: # Read files with open(original_path, 'rb') as f1, open(modified_path, 'rb') as f2: original_data = f1.read() modified_data = f2.read() # Check file sizes if len(original_data) != len(modified_data): raise ValueError(f"File sizes do not match!\nOriginal: {len(original_data)} bytes\nModified: {len(modified_data)} bytes") # Create comparison window compare_window = tk.Toplevel(self.master) compare_window.title("File Comparison") compare_window.geometry("1400x850") # Create main frame with paned window main_pane = ttk.PanedWindow(compare_window, orient=tk.VERTICAL) main_pane.pack(fill=tk.BOTH, expand=True) # Top pane for file comparison top_pane = ttk.Frame(main_pane) main_pane.add(top_pane) # Navigation frame nav_frame = ttk.Frame(top_pane) nav_frame.pack(fill=tk.X, pady=5) # Create horizontal paned window for files and diff file_pane = ttk.PanedWindow(top_pane, orient=tk.HORIZONTAL) file_pane.pack(fill=tk.BOTH, expand=True) # Create frames for original and modified files with their order analysis original_container = ttk.Frame(file_pane) modified_container = ttk.Frame(file_pane) diff_frame = ttk.Frame(file_pane) file_pane.add(original_container, weight=1) file_pane.add(modified_container, weight=1) file_pane.add(diff_frame, weight=1) # Original file section original_file_frame = ttk.Frame(original_container) original_file_frame.pack(fill=tk.BOTH, expand=True) # Create labels for file names ttk.Label(original_file_frame, text=f"Original: {os.path.basename(original_path)}").pack() # Create scrolled text widgets for hex views original_text = scrolledtext.ScrolledText(original_file_frame, wrap=tk.NONE, font=('Courier', 10)) original_text.pack(fill=tk.BOTH, expand=True) # Modified file section modified_file_frame = ttk.Frame(modified_container) modified_file_frame.pack(fill=tk.BOTH, expand=True) # Create labels for file names ttk.Label(modified_file_frame, text=f"Modified: {os.path.basename(modified_path)}").pack() modified_text = scrolledtext.ScrolledText(modified_file_frame, wrap=tk.NONE, font=('Courier', 10)) modified_text.pack(fill=tk.BOTH, expand=True) # Order analysis frames for original and modified files original_order_container = ttk.Frame(original_container) original_order_container.pack(fill=tk.X) modified_order_container = ttk.Frame(modified_container) modified_order_container.pack(fill=tk.X) # Create differences table columns = ('Address', 'Original', 'Modified') diff_table = ttk.Treeview(diff_frame, columns=columns, show='headings') for col in columns: diff_table.heading(col, text=col) diff_table.column(col, width=100) # Add scrollbar to table table_scroll = ttk.Scrollbar(diff_frame, orient=tk.VERTICAL, command=diff_table.yview) diff_table.configure(yscrollcommand=table_scroll.set) # Pack table and scrollbar table_scroll.pack(side=tk.RIGHT, fill=tk.Y) diff_table.pack(fill=tk.BOTH, expand=True) # Find and store differences differences = list(self.find_differences(original_data, modified_data)) # Populate differences table for offset, orig, mod in differences: diff_table.insert('', tk.END, values=( f"{offset:08X}", orig, mod )) # Populate hex views for i in range(0, len(original_data), 16): # Format offset offset = f"{i:08X}" # Original file line orig_line = [offset, ": "] mod_line = [offset, ": "] # Process 16 bytes per line for j in range(16): if i + j < len(original_data): orig_byte = original_data[i + j] mod_byte = modified_data[i + j] # Check for differences if orig_byte != mod_byte: orig_line.append(("normal", f"{orig_byte:02X} ")) mod_line.append(("red", f"{mod_byte:02X} ")) else: orig_line.append(("normal", f"{orig_byte:02X} ")) mod_line.append(("normal", f"{mod_byte:02X} ")) # Add ASCII representation orig_line.append(" ") mod_line.append(" ") for j in range(16): if i + j < len(original_data): orig_byte = original_data[i + j] mod_byte = modified_data[i + j] # ASCII for original if 32 <= orig_byte <= 126: orig_line.append(("normal", chr(orig_byte))) else: orig_line.append(("normal", ".")) # ASCII for modified if 32 <= mod_byte <= 126: mod_line.append(("normal" if orig_byte == mod_byte else "red", chr(mod_byte))) else: mod_line.append(("normal" if orig_byte == mod_byte else "red", ".")) orig_line.append("\n") mod_line.append("\n") # Display lines original_text.tag_config("red", foreground="black", background="lightcoral") modified_text.tag_config("red", foreground="black", background="lightcoral") # Display original line for part in orig_line: if isinstance(part, tuple): color, text = part original_text.insert(tk.END, text) else: original_text.insert(tk.END, part) # Display modified line for part in mod_line: if isinstance(part, tuple): color, text = part if color == "red": modified_text.insert(tk.END, text, "red") else: modified_text.insert(tk.END, text) else: modified_text.insert(tk.END, part) # Initial order analysis for first 16 bytes original_analysis = self.generate_order_analysis(original_data[:16]) modified_analysis = self.generate_order_analysis(modified_data[:16]) # Create initial order analysis frames self.original_order_frame = self.create_order_analysis_frame(original_order_container, original_analysis) self.modified_order_frame = self.create_order_analysis_frame(modified_order_container, modified_analysis) # Make text views read-only original_text.configure(state='disabled') modified_text.configure(state='disabled') # Navigation variables current_diff_index = tk.IntVar(value=-1) # Navigation buttons def navigate_differences(direction): nonlocal current_diff_index if not differences: messagebox.showinfo("Info", "No differences found.") return # Update index based on direction if direction == 'next': current_diff_index.set(min(current_diff_index.get() + 1, len(differences) - 1)) else: # previous current_diff_index.set(max(current_diff_index.get() - 1, 0)) # Get current difference offset, orig_byte, mod_byte = differences[current_diff_index.get()] # Calculate line and scroll position line_number = offset // 16 byte_in_line = offset % 16 # Enable text widgets temporarily to scroll original_text.configure(state='normal') modified_text.configure(state='normal') # Scroll to the specific line original_text.see(f"{line_number + 1}.0") modified_text.see(f"{line_number + 1}.0") # Remove previous highlights original_text.tag_remove("current_diff", "1.0", tk.END) modified_text.tag_remove("current_diff", "1.0", tk.END) original_text.tag_remove("exact_diff", "1.0", tk.END) modified_text.tag_remove("exact_diff", "1.0", tk.END) # Highlight the entire line in yellow original_text.tag_add("current_diff", f"{line_number + 1}.0", f"{line_number + 2}.0") modified_text.tag_add("current_diff", f"{line_number + 1}.0", f"{line_number + 2}.0") # Configure tags original_text.tag_config("current_diff", background="lightyellow") modified_text.tag_config("current_diff", background="lightyellow") original_text.tag_config("exact_diff", background="lightcoral") modified_text.tag_config("exact_diff", background="lightcoral") # Calculate exact byte position in the text # Each hex value is 3 characters long (2 hex + 1 space) # Offset starts after the line number and ": " hex_start_offset = 10 + (byte_in_line * 3) # Add exact difference highlight original_text.tag_add("exact_diff", f"{line_number + 1}.{hex_start_offset}", f"{line_number + 1}.{hex_start_offset + 2}") modified_text.tag_add("exact_diff", f"{line_number + 1}.{hex_start_offset}", f"{line_number + 1}.{hex_start_offset + 2}") # Disable text widgets again original_text.configure(state='disabled') modified_text.configure(state='disabled') # Update diff table selection diff_table.selection_remove(diff_table.selection()) diff_table.selection_add(diff_table.get_children()[current_diff_index.get()]) diff_table.focus(diff_table.get_children()[current_diff_index.get()]) diff_table.see(diff_table.get_children()[current_diff_index.get()]) # Create navigation buttons prev_button = ttk.Button(nav_frame, text="Previous Difference", command=lambda: navigate_differences('previous')) prev_button.pack(side=tk.LEFT, padx=5) next_button = ttk.Button(nav_frame, text="Next Difference", command=lambda: navigate_differences('next')) next_button.pack(side=tk.LEFT, padx=5) # Difference count label diff_count_label = ttk.Label(nav_frame, text=f"Total Differences: {len(differences)}") diff_count_label.pack(side=tk.LEFT, padx=10) # Enable text selection and analysis def create_selection_handler(text_widget, data): def on_select(event=None): try: # Get selected text if not text_widget.tag_ranges(tk.SEL): return start = text_widget.index(tk.SEL_FIRST) end = text_widget.index(tk.SEL_LAST) selected_text = text_widget.get(start, end) # Extract hex values hex_values = [] for part in selected_text.split(): if len(part) == 2 and all(c in '0123456789ABCDEFabcdef' for c in part): hex_values.append(part.upper()) if hex_values: # Convert to bytes hex_str = ''.join(hex_values) selected_bytes = bytes.fromhex(hex_str) # Generate and update order analysis analysis = self.generate_order_analysis(selected_bytes) # Update the corresponding order analysis frame if text_widget == original_text: # Destroy existing frame and create new one for widget in original_order_container.winfo_children(): widget.destroy() self.create_order_analysis_frame(original_order_container, analysis) else: # Destroy existing frame and create new one for widget in modified_order_container.winfo_children(): widget.destroy() self.create_order_analysis_frame(modified_order_container, analysis) except Exception as e: self.logger.error(f"Error in selection handler: {e}") return on_select # Bind selection handlers original_text.bind('<>', create_selection_handler(original_text, original_data)) modified_text.bind('<>', create_selection_handler(modified_text, modified_data)) except Exception as e: self.logger.error(f"Error comparing files: {str(e)}") messagebox.showerror("Error", str(e)) def find_differences(self, original_data, modified_data): """Find unique byte differences between two files""" differences = [] i = 0 while i < min(len(original_data), len(modified_data)): # If bytes are different if original_data[i] != modified_data[i]: # Look ahead to find the extent of the difference diff_length = 1 while (i + diff_length < len(original_data) and i + diff_length < len(modified_data) and original_data[i + diff_length] != modified_data[i + diff_length]): diff_length += 1 # Only add if this is the start of a unique difference sequence # Check if this difference is not a continuation of a previous one if not differences or i > differences[-1][0] + len(differences[-1][2]): # Convert to hex for easier comparison orig_sequence = ' '.join(f'{original_data[i + j]:02X}' for j in range(min(6, diff_length))) mod_sequence = ' '.join(f'{modified_data[i + j]:02X}' for j in range(min(6, diff_length))) differences.append((i, orig_sequence, mod_sequence)) # Skip to the end of this difference i += diff_length else: i += 1 return differences def generate_order_analysis(self, data): """ Generate order analysis for a given byte sequence Args: data (bytes): Byte sequence to analyze Returns: dict: Dictionary with order analysis results """ try: # Normal Order Analysis normal_hex_str = ''.join(f'{b:02X}' for b in data) normal_int = int(normal_hex_str, 16) # Reverse Order Analysis reverse_hex_str = ''.join(f'{b:02X}' for b in reversed(data)) reverse_int = int(reverse_hex_str, 16) # Calculate number of bits num_bits = len(data) * 8 return { 'normal': { 'hex': normal_hex_str, 'dec': str(normal_int), 'ihex': f'{~normal_int & ((1 << num_bits) - 1):X}', 'idec': str(~normal_int & ((1 << num_bits) - 1)) }, 'reverse': { 'hex': reverse_hex_str, 'dec': str(reverse_int), 'ihex': f'{~reverse_int & ((1 << num_bits) - 1):X}', 'idec': str(~reverse_int & ((1 << num_bits) - 1)) } } except Exception as e: self.logger.error(f"Error in order analysis: {e}") return None def create_order_analysis_frame(self, parent, analysis): """ Create a frame to display order analysis Args: parent (tk.Widget): Parent widget analysis (dict): Order analysis results Returns: ttk.LabelFrame: Frame with order analysis """ order_frame = ttk.LabelFrame(parent, text="Order Analysis") order_frame.pack(fill=tk.X, pady=5) # Normal Order normal_frame = ttk.Frame(order_frame) normal_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) ttk.Label(normal_frame, text="Normal Order", font=('Helvetica', 10, 'bold')).pack() ttk.Label(normal_frame, text=f"HEX: {analysis['normal']['hex']}").pack(anchor='w') ttk.Label(normal_frame, text=f"DEC: {analysis['normal']['dec']}").pack(anchor='w') ttk.Label(normal_frame, text=f"!HEX: {analysis['normal']['ihex']}").pack(anchor='w') ttk.Label(normal_frame, text=f"!DEC: {analysis['normal']['idec']}").pack(anchor='w') # Reverse Order reverse_frame = ttk.Frame(order_frame) reverse_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) ttk.Label(reverse_frame, text="Reverse Order", font=('Helvetica', 10, 'bold')).pack() ttk.Label(reverse_frame, text=f"HEX: {analysis['reverse']['hex']}").pack(anchor='w') ttk.Label(reverse_frame, text=f"DEC: {analysis['reverse']['dec']}").pack(anchor='w') ttk.Label(reverse_frame, text=f"!HEX: {analysis['reverse']['ihex']}").pack(anchor='w') ttk.Label(reverse_frame, text=f"!DEC: {analysis['reverse']['idec']}").pack(anchor='w') return order_frame def create_file_selection_section(self): file_frame = ttk.LabelFrame(self.main_frame, text="File Selection") file_frame.pack(fill=tk.X, pady=5) self.file_path = tk.StringVar() ttk.Label(file_frame, textvariable=self.file_path).pack(side=tk.LEFT, padx=5) ttk.Button(file_frame, text="Select File", command=self.select_file).pack(side=tk.RIGHT, padx=5) def create_hex_view_section(self): """Create hex view section with scrollable text widget""" # Hex view frame hex_frame = ttk.LabelFrame(self.main_frame, text="Hex View") hex_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) # Scrolled text widget for hex view self.hex_text = tk.Text(hex_frame, wrap=tk.NONE, font=('Courier', 10)) self.hex_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # Add vertical scrollbar v_scrollbar = ttk.Scrollbar(hex_frame, orient=tk.VERTICAL, command=self.hex_text.yview) v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.hex_text.configure(yscrollcommand=v_scrollbar.set) # Add horizontal scrollbar h_scrollbar = ttk.Scrollbar(hex_frame, orient=tk.HORIZONTAL, command=self.hex_text.xview) h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) self.hex_text.configure(xscrollcommand=h_scrollbar.set) # Bind key events for custom processing self.hex_text.bind('', self.on_hex_text_key) def on_hex_text_key(self, event): """Custom key event handler for hex text editing""" # Allow navigation and editing keys if event.keysym in ['Left', 'Right', 'Up', 'Down', 'Home', 'End', 'Prior', 'Next']: return # Allow backspace and delete if event.keysym in ['BackSpace', 'Delete']: self.master.after(10, self.process_hex_text_edit) return # Only process for printable hex characters if event.char and event.char not in '0123456789ABCDEFabcdef': return 'break' # Delay processing to allow default key handling self.master.after(10, self.process_hex_text_edit) def process_hex_text_edit(self): """Process hex text edits to maintain formatting""" try: # Get entire text content full_text = self.hex_text.get('1.0', tk.END) # Get current cursor position cursor_index = self.hex_text.index(tk.INSERT) # Log current state for debugging self.logger.debug(f"Current cursor index: {cursor_index}") self.logger.debug(f"Full text length: {len(full_text)}") # Split text into lines lines = full_text.split('\n') # Process each line to extract hex values clean_hex_values = [] for line in lines: # Extract hex values from each line (between ':' and ASCII representation) if ':' in line: hex_part = line.split(':', 1)[1].split(' ', 1)[0] # Remove spaces and collect hex values line_hex = hex_part.replace(' ', '') if line_hex: clean_hex_values.append(line_hex) # Join all hex values clean_text = ''.join(clean_hex_values) # Reformat the text formatted_text = self.reformat_hex_text(clean_text) # Update text widget self.hex_text.delete('1.0', tk.END) self.hex_text.insert('1.0', formatted_text) # Restore cursor position total_lines = len(formatted_text.split('\n')) cursor_line, cursor_column = map(int, cursor_index.split('.')) # Ensure cursor line is within new text if cursor_line > total_lines: cursor_line = total_lines # Get the last line's length last_line = formatted_text.split('\n')[-1] last_line_hex = last_line.split(':', 1)[1].split(' ', 1)[0].replace(' ', '') # Adjust column if it exceeds line length if cursor_column > len(last_line_hex) + 1: cursor_column = len(last_line_hex) + 1 # Set cursor position new_cursor_index = f'{cursor_line}.{cursor_column}' self.logger.debug(f"Restored cursor index: {new_cursor_index}") self.hex_text.mark_set(tk.INSERT, new_cursor_index) self.hex_text.see(new_cursor_index) except Exception as e: self.logger.error(f"Error processing hex text edit: {str(e)}") def reformat_hex_text(self, hex_string): """Reformat hex string to maintain original view""" # Ensure hex string length is even if len(hex_string) % 2 != 0: hex_string = hex_string + '0' # Convert to bytes for consistent formatting try: data = bytes.fromhex(hex_string) return self.format_hex_view(data) except ValueError: # Fallback if conversion fails return hex_string def create_order_analysis_section(self): order_frame = ttk.LabelFrame(self.main_frame, text="Order Analysis") order_frame.pack(fill=tk.X, pady=5) # Normal Order normal_frame = ttk.LabelFrame(order_frame, text="Normal Order") normal_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5, pady=5) self.normal_hex = tk.StringVar() self.normal_dec = tk.StringVar() self.normal_ihex = tk.StringVar() self.normal_idec = tk.StringVar() ttk.Label(normal_frame, text="HEX:").grid(row=0, column=0, sticky=tk.W) ttk.Label(normal_frame, textvariable=self.normal_hex).grid(row=0, column=1, sticky=tk.W) ttk.Label(normal_frame, text="DEC:").grid(row=1, column=0, sticky=tk.W) ttk.Label(normal_frame, textvariable=self.normal_dec).grid(row=1, column=1, sticky=tk.W) ttk.Label(normal_frame, text="!HEX:").grid(row=0, column=2, sticky=tk.W) ttk.Label(normal_frame, textvariable=self.normal_ihex).grid(row=0, column=3, sticky=tk.W) ttk.Label(normal_frame, text="!DEC:").grid(row=1, column=2, sticky=tk.W) ttk.Label(normal_frame, textvariable=self.normal_idec).grid(row=1, column=3, sticky=tk.W) # Reverse Order reverse_frame = ttk.LabelFrame(order_frame, text="Reverse Order") reverse_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5, pady=5) self.reverse_hex = tk.StringVar() self.reverse_dec = tk.StringVar() self.reverse_ihex = tk.StringVar() self.reverse_idec = tk.StringVar() ttk.Label(reverse_frame, text="HEX:").grid(row=0, column=0, sticky=tk.W) ttk.Label(reverse_frame, textvariable=self.reverse_hex).grid(row=0, column=1, sticky=tk.W) ttk.Label(reverse_frame, text="DEC:").grid(row=1, column=0, sticky=tk.W) ttk.Label(reverse_frame, textvariable=self.reverse_dec).grid(row=1, column=1, sticky=tk.W) ttk.Label(reverse_frame, text="!HEX:").grid(row=0, column=2, sticky=tk.W) ttk.Label(reverse_frame, textvariable=self.reverse_ihex).grid(row=0, column=3, sticky=tk.W) ttk.Label(reverse_frame, text="!DEC:").grid(row=1, column=2, sticky=tk.W) ttk.Label(reverse_frame, textvariable=self.reverse_idec).grid(row=1, column=3, sticky=tk.W) def create_inspector_section(self): inspector_frame = ttk.LabelFrame(self.main_frame, text="Inspector") inspector_frame.pack(fill=tk.X, pady=5) # Create labels for various representations self.checksum_sum = tk.StringVar() self.checksum_xor = tk.StringVar() self.checksum_16 = tk.StringVar() ttk.Label(inspector_frame, text="Checksum Sum:").grid(row=0, column=0, sticky=tk.W) ttk.Label(inspector_frame, textvariable=self.checksum_sum).grid(row=0, column=1, sticky=tk.W) ttk.Label(inspector_frame, text="Checksum XOR:").grid(row=1, column=0, sticky=tk.W) ttk.Label(inspector_frame, textvariable=self.checksum_xor).grid(row=1, column=1, sticky=tk.W) ttk.Label(inspector_frame, text="Checksum 16-bit:").grid(row=2, column=0, sticky=tk.W) ttk.Label(inspector_frame, textvariable=self.checksum_16).grid(row=2, column=1, sticky=tk.W) def create_search_section(self): search_frame = ttk.LabelFrame(self.main_frame, text="Search") search_frame.pack(fill=tk.X, pady=5) ttk.Label(search_frame, text="Search Value:").grid(row=0, column=0) self.search_entry = ttk.Entry(search_frame, width=30) self.search_entry.grid(row=0, column=1, padx=5) ttk.Button(search_frame, text="Search", command=self.perform_search).grid(row=0, column=2) ttk.Button(search_frame, text="Next Result", command=self.next_result).grid(row=0, column=3) ttk.Button(search_frame, text="Clear", command=self.clear_search).grid(row=0, column=4) def select_file(self): file_path = filedialog.askopenfilename() if file_path: self.file_path.set(file_path) self.inspect_file(file_path) def inspect_file(self, file_path): """Inspect and display contents of a binary file""" try: # Read file in binary mode with open(file_path, 'rb') as f: data = f.read() # Clear previous content self.hex_text.config(state=tk.NORMAL) self.hex_text.delete('1.0', tk.END) # Format and display hex view hex_view = self.format_hex_view(data) self.hex_text.insert(tk.END, hex_view) # Make text editable and highlight in light red self.hex_text.tag_config('editable', background='#FFE6E6') self.hex_text.tag_add('editable', '1.0', tk.END) # Configure text widget for editing self.hex_text.config(state=tk.NORMAL) # Calculate and update checksums checksums = self.calculate_checksums(data) self.checksum_sum.set(checksums['sum']) self.checksum_xor.set(checksums['xor']) self.checksum_16.set(checksums['sum16']) # Store original file path for potential saving self.current_file_path = file_path # Update window title self.master.title(f"HexaWork - {os.path.basename(file_path)}") except Exception as e: self.logger.error(f"Error inspecting file: {e}") messagebox.showerror("Error", f"Could not inspect file: {e}") def save_edited_file(self): """Save the edited binary file""" if not hasattr(self, 'current_file_path') or not self.current_file_path: messagebox.showerror("Error", "No file is currently open") return try: # Get the original file size original_size = os.path.getsize(self.current_file_path) # Get the edited text and convert to bytes edited_text = self.hex_text.get('1.0', tk.END).strip() # Remove spaces and convert to bytes hex_values = edited_text.replace('\n', '').replace(' ', '') # Validate hex string if not all(c in '0123456789ABCDEFabcdef' for c in hex_values): messagebox.showerror("Error", "Invalid hex characters") return # Convert to bytes edited_bytes = bytes.fromhex(hex_values) # Check file size change if len(edited_bytes) != original_size: # Ask user about file size change response = messagebox.askyesno( "File Size Change", f"Warning: File size will change!\n\n" f"Original Size: {original_size} bytes\n" f"New Size: {len(edited_bytes)} bytes\n\n" "Do you want to continue saving?" ) if not response: return # Save the file with open(self.current_file_path, 'wb') as f: f.write(edited_bytes) messagebox.showinfo("Success", "File saved successfully") # Update the current file view after saving self.inspect_file(self.current_file_path) except Exception as e: self.logger.error(f"Error saving file: {e}") messagebox.showerror("Error", f"Could not save file: {e}") def format_hex_view(self, data, bytes_per_row=16): """Format binary data into hex view with offset, hex values, and ASCII representation""" hex_view = [] # Determine the maximum length of the offset for consistent formatting max_offset = len(f'{len(data):X}') for i in range(0, len(data), bytes_per_row): # Get the current chunk of bytes chunk = data[i:i+bytes_per_row] # Format offset (right-aligned with leading zeros) offset = f'{i:0{max_offset}X}' # Format hex values (2 digits per byte, space-separated) hex_values = ' '.join(f'{byte:02X}' for byte in chunk) # Pad hex values to ensure consistent width hex_values = hex_values.ljust(bytes_per_row * 3 - 1) # Format ASCII representation ascii_repr = ''.join(chr(byte) if 32 <= byte <= 126 else '.' for byte in chunk) # Combine all parts line = f'{offset}: {hex_values} {ascii_repr}' hex_view.append(line) return '\n'.join(hex_view) def calculate_checksums(self, data): """Calculate various checksums for the given data""" # Ensure data is converted to bytes if it's not already if not isinstance(data, bytes): data = bytes(data) # Sum checksum (1 byte) sum_checksum = sum(data) & 0xFF # XOR checksum (1 byte) xor_checksum = 0 for byte in data: xor_checksum ^= byte xor_checksum &= 0xFF # 16-bit checksum (2 bytes) bit16_checksum = sum(data) & 0xFFFF return { 'sum': f'{sum_checksum:02X}', 'xor': f'{xor_checksum:02X}', 'sum16': f'{bit16_checksum:04X}' } def perform_search(self): search_value = self.search_entry.get() if not search_value: return # Placeholder search implementation hex_content = self.hex_text.get('1.0', tk.END) results = [m.start() for m in re.finditer(re.escape(search_value), hex_content)] if results: self.current_search_results = results self.current_search_index = 0 self.highlight_current_result() else: messagebox.showinfo("Search", "No results found.") def next_result(self): if not hasattr(self, 'current_search_results') or not self.current_search_results: return self.current_search_index = (self.current_search_index + 1) % len(self.current_search_results) self.highlight_current_result() def highlight_current_result(self): if not hasattr(self, 'current_search_results'): return result_pos = self.current_search_results[self.current_search_index] self.hex_text.tag_remove('search_highlight', '1.0', tk.END) start_index = self.hex_text.index(f'1.0 + {result_pos}c') end_index = self.hex_text.index(f'{start_index} + {len(self.search_entry.get())}c') self.hex_text.tag_add('search_highlight', start_index, end_index) self.hex_text.tag_config('search_highlight', background='yellow') self.hex_text.see(start_index) def clear_search(self): self.search_entry.delete(0, tk.END) self.hex_text.tag_remove('search_highlight', '1.0', tk.END) if hasattr(self, 'current_search_results'): del self.current_search_results self.current_search_index = -1 def on_text_select(self, event=None): """Handle text selection in hex view""" try: # Get selected text if not self.hex_text.tag_ranges(tk.SEL): return start = self.hex_text.index(tk.SEL_FIRST) end = self.hex_text.index(tk.SEL_LAST) selected_text = self.hex_text.get(start, end) # Extract hex values hex_values = [] for part in selected_text.split(): if len(part) == 2 and all(c in '0123456789ABCDEFabcdef' for c in part): hex_values.append(part.upper()) if hex_values: # Convert to bytes hex_str = ''.join(hex_values) self.selected_bytes = bytes.fromhex(hex_str) self.logger.info(f"Converted to bytes: {self.selected_bytes.hex().upper()}") # Update order analysis self.update_order_analysis() # Update checksums checksums = self.calculate_checksums(self.selected_bytes) self.checksum_sum.set(checksums['sum']) self.checksum_xor.set(checksums['xor']) self.checksum_16.set(checksums['sum16']) else: self.logger.warning("No valid hex values found in selection") self.clear_order_analysis() # Clear checksum values self.checksum_sum.set("") self.checksum_xor.set("") self.checksum_16.set("") except Exception as e: self.logger.error(f"Error processing selection: {str(e)}\nTraceback: {traceback.format_exc()}") self.clear_order_analysis() # Clear checksum values self.checksum_sum.set("") self.checksum_xor.set("") self.checksum_16.set("") def clear_order_analysis(self): """Clear all order analysis values""" for var in [self.normal_hex, self.normal_dec, self.normal_ihex, self.normal_idec, self.reverse_hex, self.reverse_dec, self.reverse_ihex, self.reverse_idec]: var.set("") def update_order_analysis(self): if not self.selected_bytes: return try: # Normal Order Analysis normal_hex_str = ''.join(f'{b:02X}' for b in self.selected_bytes) self.normal_hex.set(normal_hex_str) # Convert to integer (normal order) normal_int = int(normal_hex_str, 16) self.normal_dec.set(str(normal_int)) # Calculate !HEX (bitwise NOT) num_bits = len(self.selected_bytes) * 8 normal_complement = ~normal_int & ((1 << num_bits) - 1) self.normal_ihex.set(f'{normal_complement:X}') # Calculate !DEC (decimal complement) self.normal_idec.set(str(normal_complement)) # Reverse Order Analysis reverse_hex_str = ''.join(f'{b:02X}' for b in reversed(self.selected_bytes)) self.reverse_hex.set(reverse_hex_str) # Convert to integer (reverse order) reverse_int = int(reverse_hex_str, 16) self.reverse_dec.set(str(reverse_int)) # Calculate !HEX for reverse reverse_complement = ~reverse_int & ((1 << num_bits) - 1) self.reverse_ihex.set(f'{reverse_complement:X}') # Calculate !DEC for reverse self.reverse_idec.set(str(reverse_complement)) except Exception as e: self.logger.error(f"Error updating order analysis: {e}") self.clear_order_analysis() def analyze_ascii_patterns(self): """Analyze ASCII patterns in the current file""" if not hasattr(self, 'current_file_path') or not self.current_file_path: messagebox.showerror("Error", "No file is currently open") return try: # Read file in binary mode with open(self.current_file_path, 'rb') as f: data = f.read() # Convert to ASCII string, replacing non-printable characters ascii_text = ''.join(chr(byte) if 32 <= byte <= 126 else ' ' for byte in data) # Patterns to search patterns = { 'Possible Software Number': r'\b\d{10,11}\b', 'VIN (Vehicle Identification Number)': r'\b[A-Z0-9]{17}\b', } # Collect results results = {} for pattern_name, pattern in patterns.items(): matches = re.findall(pattern, ascii_text) if matches: # Count unique occurrences unique_matches = {} for match in matches: unique_matches[match] = unique_matches.get(match, 0) + 1 results[pattern_name] = unique_matches # Additional custom pattern searches with minimum 3 character requirement custom_patterns = [ ('Potential Codes', r'\b[A-Z]{3,}\b'), # 3 or more letter codes ('Long Numbers', r'\b\d{10,}\b'), ('Possible Pin Code', r'\b[A-Z0-9]{4}\b') # 4-character alphanumeric codes ] for pattern_name, pattern in custom_patterns: matches = re.findall(pattern, ascii_text) if matches: # Count unique occurrences unique_matches = {} for match in matches: unique_matches[match] = unique_matches.get(match, 0) + 1 results[pattern_name] = unique_matches # Create results window if results: self.show_ascii_analysis_results(results) else: messagebox.showinfo("ASCII Analysis", "No interesting patterns found.") except Exception as e: self.logger.error(f"Error in ASCII analysis: {e}") messagebox.showerror("Error", f"Could not perform ASCII analysis: {e}") def show_ascii_analysis_results(self, results): """Display ASCII analysis results in a popup window""" # Create top-level window result_window = tk.Toplevel(self.master) result_window.title("ASCII Pattern Analysis") result_window.geometry("600x400") # Create text widget to display results results_text = tk.Text(result_window, wrap=tk.WORD, font=('Courier', 10)) results_text.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) # Add scrollbar scrollbar = ttk.Scrollbar(result_window, command=results_text.yview) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) results_text.configure(yscrollcommand=scrollbar.set) # Format and insert results results_text.insert(tk.END, "ASCII Pattern Analysis Results:\n\n") for category, matches in results.items(): results_text.insert(tk.END, f"{category}:\n") for match, count in matches.items(): results_text.insert(tk.END, f" - {match} (Occurrences: {count})\n") results_text.insert(tk.END, "\n") # Add a close button close_button = ttk.Button(result_window, text="Close", command=result_window.destroy) close_button.pack(pady=10) def main(): root = tk.Tk() app = HexaWorkApp(root) root.mainloop() if __name__ == "__main__": main()