From 130d70b3d29c946edc8de1aac92e8598c8645e64 Mon Sep 17 00:00:00 2001 From: godax84 Date: Wed, 4 Dec 2024 00:53:51 -0800 Subject: [PATCH] Carregar ficheiros para "/" --- README.md | 150 +++++++ main.py | 1084 ++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 + 3 files changed, 1237 insertions(+) create mode 100644 README.md create mode 100644 main.py create mode 100644 requirements.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..472e2fb --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# HexaWork - Byte Inspector + +## Overview +HexaWork is a Python Tkinter application for binary file inspection and analysis. It provides a hexadecimal viewer and advanced byte analysis capabilities. + +## Features + +### 1. Hexadecimal View +- Three-column display: offset, hexadecimal values, and ASCII representation +- Uppercase hexadecimal values for better readability +- Monospace font for perfect alignment +- Horizontal and vertical scrollbars + +### 2. Byte Analysis +#### Normal Order +- HEX: Hexadecimal representation of selected bytes +- DEC: Corresponding decimal value +- !HEX: Bitwise complement in hexadecimal +- !DEC: Bitwise complement in decimal + +#### Reverse Order +- HEX: Hexadecimal representation of bytes in reverse order +- DEC: Decimal value of reverse order +- !HEX: Bitwise complement in hexadecimal +- !DEC: Bitwise complement in decimal + +### 3. Flexible Selection +- Direct selection of hexadecimal values +- Full line selection including offset and ASCII +- Automatic analysis update on selection + +### 4. ASCII Pattern Analysis +- Comprehensive file-wide ASCII text scanning +- Detects various patterns: + * Phone numbers + * Short letter sequences + * Alphanumeric codes + * Potential 3-letter abbreviations +- Interactive results popup +- Supports multiple pattern types + +### 5. Advanced ASCII Pattern Recognition +- Comprehensive file-wide text scanning +- Multiple pattern detection: + * Possible Software Numbers (10-11 digits) + * Vehicle Identification Numbers (VIN) + * Potential Code Sequences + * Possible PIN Codes +- Unique pattern occurrence counting +- Interactive results display +- Supports complex alphanumeric pattern matching + +### 6. Pattern Analysis Features +- Detailed pattern identification: + * Detects 3+ letter codes + * Finds long numeric sequences + * Highlights repeated patterns +- Occurrence frequency tracking +- User-friendly results popup +- Flexible pattern recognition + +## Installation + +1. Requirements: + - Python 3.7 or higher + - Tkinter (usually included with Python) + +2. Install dependencies: +```bash +pip install -r requirements.txt +``` + +## Usage + +1. Run the program: +```bash +python main.py +``` + +2. Features: + - Click "Select File" to choose a binary file + - Select bytes in the hex viewer (using mouse or keyboard) + - View automatic analysis in Normal Order and Reverse Order panels + - Use scrollbars to navigate large files + +## Examples + +### Byte Analysis +If you select bytes "4E 31": +- Normal Order: + * HEX: 4E31 + * DEC: 20017 + * !HEX: [complement] + * !DEC: [decimal complement] + +- Reverse Order: + * HEX: 314E + * DEC: 12622 + * !HEX: [complement] + * !DEC: [decimal complement] + +## Detailed Update History + +### Version 0.2.0 - Advanced Editing Capabilities +#### Hexadecimal Editing Enhancements +- Implemented sophisticated in-place hexadecimal editing +- Developed smart cursor positioning algorithm + * Maintains cursor location during edits + * Handles edits at any file location +- Preserved line formatting during byte modifications +- Added real-time hex value validation + +#### File Modification Safety +- Introduced file size change warning mechanism +- Implemented confirmation dialog for file size alterations +- Ensured user awareness of potential file structure changes + +#### Checksum Refinements +- Enhanced checksum calculation methods +- Supported variable-length byte sequences +- Implemented three distinct checksum types: + * 1-byte Sum Checksum + * 1-byte XOR Checksum + * 16-bit Checksum +- Provided hexadecimal representation of checksums + +#### Technical Improvements +- Added comprehensive error logging +- Improved input validation +- Enhanced user interaction during file editing + +### Upcoming Features +- Large file performance optimization +- Advanced search functionality +- More configurable display options +- Comprehensive unit testing + +## Logging +- Log file: hexawork.log +- Records operations and errors for debugging +- Format: timestamp, module, level, message + +## Contributing +Feel free to: +- Report bugs +- Suggest improvements +- Submit pull requests + +## License +This project is under the MIT license. diff --git a/main.py b/main.py new file mode 100644 index 0000000..dd5188f --- /dev/null +++ b/main.py @@ -0,0 +1,1084 @@ +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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..14a6e48 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +tkinter +logging +binascii