Reorganize repository structure into logical folders
Structure: - src/ - Python implementation (cobs_protocol.py, onecontrol_client.py) - docs/ - All documentation markdown files - scripts/ - Extraction scripts (for reference only) Changes: - Moved Python files to src/ - Moved all .md docs to docs/ - Moved extraction scripts to scripts/ - Updated README.md with new structure - Updated import paths in README examples - Added placeholder for future Quartz documentation URL Benefits: - Cleaner repository organization - Easier to navigate - Separates code from documentation - Follows standard project conventions
This commit is contained in:
Executable
+151
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
XABA v2 Assembly Extractor
|
||||
Extracts .NET DLLs from Xamarin XABA v2.x format blob files
|
||||
"""
|
||||
|
||||
import struct
|
||||
import os
|
||||
import sys
|
||||
|
||||
def extract_xaba_v2(blob_path, output_dir):
|
||||
"""Extract assemblies from XABA v2.x format"""
|
||||
|
||||
with open(blob_path, 'rb') as f:
|
||||
# Skip ELF header (first 0x4000 bytes)
|
||||
f.seek(0x4000)
|
||||
data = f.read()
|
||||
|
||||
# Check XABA magic
|
||||
magic = data[0:4]
|
||||
if magic != b'XABA':
|
||||
print(f"Error: Not a XABA file (magic: {magic})")
|
||||
return
|
||||
|
||||
# Parse header
|
||||
version = struct.unpack('<I', data[4:8])[0]
|
||||
local_entry_count = struct.unpack('<I', data[8:12])[0]
|
||||
global_entry_count = struct.unpack('<I', data[12:16])[0]
|
||||
store_id = struct.unpack('<I', data[16:20])[0]
|
||||
|
||||
print(f"XABA Format v{version >> 16}.{version & 0xFFFF}")
|
||||
print(f"Store ID: {store_id}")
|
||||
print(f"Local entries: {local_entry_count}")
|
||||
print(f"Global entries: {global_entry_count}")
|
||||
print()
|
||||
|
||||
# Create output directory
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# The index starts after header
|
||||
# Based on observed format, try different offsets
|
||||
# XABA v2.2 appears to have entries listed after header
|
||||
|
||||
# Search for DLL names in the data to find the index
|
||||
# Look for common DLL name pattern
|
||||
dll_markers = [b'OneControl.dll\x00', b'Plugin.BLE.dll\x00', b'App.LippertConnect.dll\x00']
|
||||
|
||||
index_start = None
|
||||
for marker in dll_markers:
|
||||
pos = data.find(marker)
|
||||
if pos != -1 and pos < 100000: # Should be early in file
|
||||
# Found a name, work backwards to find structure start
|
||||
# Names are typically after some fixed-size header fields
|
||||
index_start = pos - 200 # Rough estimate
|
||||
break
|
||||
|
||||
if index_start:
|
||||
print(f"Found assembly index around offset {hex(index_start)}")
|
||||
else:
|
||||
print("Could not locate assembly index, using fallback method...")
|
||||
# Fallback: extract all PE files
|
||||
extract_all_pe_files(data, output_dir)
|
||||
return
|
||||
|
||||
# Try to parse entries (this is approximate)
|
||||
# Format appears to be variable, so we'll use PE extraction as fallback
|
||||
extract_all_pe_files(data, output_dir)
|
||||
|
||||
def extract_all_pe_files(data, output_dir):
|
||||
"""Extract all valid PE/DLL files from data"""
|
||||
|
||||
print("Scanning for PE/DLL files...")
|
||||
dll_count = 0
|
||||
pos = 0
|
||||
extracted_names = {}
|
||||
|
||||
while pos < len(data) - 64:
|
||||
# Look for MZ header
|
||||
pos = data.find(b'MZ', pos + 1)
|
||||
if pos == -1:
|
||||
break
|
||||
|
||||
try:
|
||||
# Verify PE signature
|
||||
pe_offset = struct.unpack('<I', data[pos+60:pos+64])[0]
|
||||
if pos + pe_offset + 4 < len(data):
|
||||
if data[pos+pe_offset:pos+pe_offset+4] == b'PE\x00\x00':
|
||||
# Valid PE file found
|
||||
|
||||
# Try to find assembly name in the PE
|
||||
# Look for strings near the PE header
|
||||
name_search = data[max(0, pos-1000):pos+5000]
|
||||
assembly_name = None
|
||||
|
||||
# Look for .dll in nearby strings
|
||||
for match_pos in range(len(name_search)):
|
||||
if name_search[match_pos:match_pos+4] == b'.dll':
|
||||
# Found .dll, extract name before it
|
||||
start = match_pos
|
||||
while start > 0 and name_search[start-1:start].isalnum() or name_search[start-1:start] in [b'.', b'_', b'-']:
|
||||
start -= 1
|
||||
possible_name = name_search[start:match_pos+4].decode('utf-8', errors='ignore')
|
||||
if 20 < len(possible_name) < 150 and possible_name.endswith('.dll'):
|
||||
assembly_name = possible_name
|
||||
break
|
||||
|
||||
# Estimate size
|
||||
next_mz = data.find(b'MZ', pos + 10000)
|
||||
if next_mz == -1:
|
||||
size = min(5000000, len(data) - pos)
|
||||
else:
|
||||
size = min(next_mz - pos, 5000000)
|
||||
|
||||
# Extract
|
||||
dll_data = data[pos:pos+size]
|
||||
|
||||
if assembly_name and assembly_name not in extracted_names:
|
||||
filename = assembly_name
|
||||
extracted_names[assembly_name] = True
|
||||
else:
|
||||
filename = f"assembly_{dll_count:03d}.dll"
|
||||
|
||||
output_path = os.path.join(output_dir, filename)
|
||||
|
||||
with open(output_path, 'wb') as out:
|
||||
out.write(dll_data)
|
||||
|
||||
print(f"[{dll_count+1}] {filename} ({size:,} bytes)")
|
||||
dll_count += 1
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
print(f"\nExtracted {dll_count} assemblies to {output_dir}/")
|
||||
|
||||
# List important ones
|
||||
print("\nKey assemblies extracted:")
|
||||
for fname in sorted(os.listdir(output_dir)):
|
||||
if any(x in fname for x in ['OneControl', 'Plugin.BLE', 'IDS', 'LCI']):
|
||||
size = os.path.getsize(os.path.join(output_dir, fname))
|
||||
print(f" {fname:50s} ({size:>10,} bytes)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 extract_xaba_v2.py <blob_file> [output_dir]")
|
||||
sys.exit(1)
|
||||
|
||||
blob_file = sys.argv[1]
|
||||
output_dir = sys.argv[2] if len(sys.argv) > 2 else "extracted_dlls"
|
||||
|
||||
extract_xaba_v2(blob_file, output_dir)
|
||||
@@ -0,0 +1,110 @@
|
||||
import struct
|
||||
import os
|
||||
import lz4.block
|
||||
import re
|
||||
|
||||
def extract():
|
||||
print("Reading payload.bin...")
|
||||
with open('payload.bin', 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
# Find all XALZ offsets
|
||||
print("Scanning for XALZ blocks...")
|
||||
offsets = []
|
||||
pos = 0
|
||||
while True:
|
||||
pos = data.find(b'XALZ', pos)
|
||||
if pos == -1:
|
||||
break
|
||||
offsets.append(pos)
|
||||
pos += 4
|
||||
|
||||
print(f"Found {len(offsets)} XALZ blocks.")
|
||||
|
||||
os.makedirs('extracted_final', exist_ok=True)
|
||||
|
||||
success_count = 0
|
||||
|
||||
for i, offset in enumerate(offsets):
|
||||
# Determine end of compressed data
|
||||
if i < len(offsets) - 1:
|
||||
end = offsets[i+1]
|
||||
else:
|
||||
end = len(data)
|
||||
|
||||
# Header parsing
|
||||
# XALZ (4) + ID (4) + UncompressedSize (4)
|
||||
header = data[offset:offset+12]
|
||||
magic, blob_id, uncomp_size = struct.unpack('<4sII', header)
|
||||
|
||||
# print(f"Block {i}: Offset {offset}, Size {uncomp_size}")
|
||||
|
||||
compressed_data = data[offset+12:end]
|
||||
|
||||
try:
|
||||
# lz4.block.decompress expects the compressed data.
|
||||
# If uncompressed_size is provided, it helps allocation.
|
||||
decompressed = lz4.block.decompress(compressed_data, uncompressed_size=uncomp_size)
|
||||
except Exception as e:
|
||||
print(f"Error extracting block {i} at {offset}: {e}")
|
||||
# Try extracting just uncompressed if size matches?
|
||||
# If failed, maybe it wasn't compressed? But we saw MZ in literals.
|
||||
continue
|
||||
|
||||
# Determine name
|
||||
name = f"assembly_{i:03d}.dll"
|
||||
|
||||
# Scan for internal name
|
||||
# Search first 20KB for .dll names
|
||||
search_area = decompressed[:min(20000, len(decompressed))]
|
||||
# Regex: alphanumeric + . _ -
|
||||
matches = re.finditer(b'([a-zA-Z0-9_.-]+\.dll)', search_area)
|
||||
best_name = None
|
||||
for match in matches:
|
||||
try:
|
||||
cand = match.group(1).decode('utf-8')
|
||||
# Filter
|
||||
if cand.lower() != 'mscoree.dll' and len(cand) > 4:
|
||||
# Heuristic: usually starts with capital or specific words
|
||||
if 'System' in cand or 'Microsoft' in cand or 'OneControl' in cand or 'Lippert' in cand or 'Plugin' in cand or 'Xamarin' in cand or 'Android' in cand:
|
||||
best_name = cand
|
||||
break
|
||||
# Fallback
|
||||
if not best_name:
|
||||
best_name = cand
|
||||
except:
|
||||
continue
|
||||
|
||||
if best_name:
|
||||
name = best_name
|
||||
|
||||
# Handle duplicates
|
||||
output_path = os.path.join('extracted_final', name)
|
||||
if os.path.exists(output_path):
|
||||
counter = 2
|
||||
base_name = name
|
||||
while True:
|
||||
name = f"{base_name[:-4]}_{counter}.dll"
|
||||
output_path = os.path.join('extracted_final', name)
|
||||
if not os.path.exists(output_path):
|
||||
break
|
||||
counter += 1
|
||||
|
||||
with open(output_path, 'wb') as out:
|
||||
out.write(decompressed)
|
||||
|
||||
success_count += 1
|
||||
if i % 50 == 0:
|
||||
print(f"Processed {i}/{len(offsets)}...")
|
||||
|
||||
print(f"Successfully extracted {success_count} assemblies to extracted_final/")
|
||||
|
||||
# List Key Assemblies
|
||||
print("\nKey Assemblies Found:")
|
||||
for fname in sorted(os.listdir('extracted_final')):
|
||||
if any(x in fname for x in ['OneControl', 'Plugin.BLE', 'IdsCan', 'MyRvLink']):
|
||||
sz = os.path.getsize(os.path.join('extracted_final', fname))
|
||||
print(f" {fname} ({sz} bytes)")
|
||||
|
||||
if __name__ == '__main__':
|
||||
extract()
|
||||
Reference in New Issue
Block a user