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:
wes
2025-12-29 09:33:20 -05:00
parent c62fbf1bd4
commit 718a13c02f
11 changed files with 29 additions and 19 deletions
+151
View File
@@ -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)
+110
View File
@@ -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()