Initial commit: Lippert OneControl protocol reverse engineering
✅ Protocol fully reversed from decompiled Xamarin app ✅ All 431 .NET assemblies extracted and decompiled ✅ COBS encoder/decoder implemented in Python ✅ CRC8 checksum implementation ✅ Complete BLE client for OneControl devices ✅ Comprehensive documentation Files included: - cobs_protocol.py: COBS encoding/decoding + CRC8 - onecontrol_client.py: Full BLE client implementation - Complete protocol documentation - Home Assistant integration guide - ESPHome Bluetooth Proxy setup - Extraction scripts for reference Ready for testing with RV hardware (April 2025)
This commit is contained in:
@@ -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