1094 lines
46 KiB
Python
1094 lines
46 KiB
Python
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
|
|
from flash_window import FlashWindow
|
|
|
|
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)
|
|
|
|
# Flash menu
|
|
flash_menu = tk.Menu(menubar, tearoff=0)
|
|
menubar.add_cascade(label="Flash", menu=flash_menu)
|
|
flash_menu.add_command(label="EDC15", command=self.open_flash_window)
|
|
|
|
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('<<Selection>>', create_selection_handler(original_text, original_data))
|
|
modified_text.bind('<<Selection>>', 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 open_flash_window(self):
|
|
FlashWindow(self.master, self.master)
|
|
|
|
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('<Key>', 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()
|