#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2023 Andrew I MacIntyre # Extract Lantiq xDSL files from decompressed firmware images # # Recognised xDSL device files: # - Lantiq VRX200 (VR9) # - Lantiq VRX300 (VR10) # - Lantiq VRX500 (VR11) # # Known source firmware images # - Draytek Vigor 130 (VR9, v3.8.4.1 - requires decompression first) # - Draytek Vigor 2760-Delight (VR9, v3.8.9.5 - requires decompression first) # - Draytek Vigor 2762 (VR10, v3.9.6.3 - requires decompression first) # - Draytek Vigor 165 (VR11, v4.4.1 - requires decompression first) # - Draytek Vigor 2765 (VR11, v4.4.1 - requires decompression first) # # Lantiq VR9, VR10 & VR11 xDSL driver files have been found to have the # following structure: # - a little endian long containing the total length of the file - 8 bytes # - one of two constant byte sequences which can be used to detect the # start of the file, starting with the 4th byte of the file: # = a 32 byte sequence which appears to be present in all non-G.vector # capable files; or # = a 78 byte sequence which appears to be present in all G.vector # capable files # - the bulk of executable code and data comprising the file # - one of two constant byte closing sequences: # = a 16 byte sequence common to most recognised files # = a 6 byte sequence present in some recent files # # Note: # # 1) Your use of this script is entirely your (the user's) # responsibility and at your own risk. # # 2) The Draytek images identified above are copyrighted and the embedded # components are licensed only for use on the devices for which the # original images are intended. Your use of this script gives you the # means to extract material to use for your personal purposes but gives # you no right to redistribute (i.e. give copies to others or make # publicly available) files produced by this script. # # 3) For the pattern recognition to work the source files must be in # decompressed form. For this reason this script won't find xDSL files # embedded in SquashFS filesystem images, though it can be used to # determine the Lantiq file version of xDSL files extracted from other # sources (but see also Martin Blumenstingl's ltq-xdsl-fw-info.sh script # mentioned in the Credits for this purpose). # # 4) The simple pattern recognition approach implemented in this script # doesn't make any guarantees as to the integrity of the output file(s). # While I have attempted to verify it's output using known good source # data there will be risks that what is output may be incomplete or # invalid and potentially place any device you use output files in at # risk of failure. # # 5) The above noted Draytek images contain 2-4 complete xDSL files and # don't appear to contain any binary difference files. Some xDSL files # are present in multiple images for devices with the same chipset. # # Credits: # - Martin Blumenstingl's respository documenting where to find and # how to extract various Lantiq VRX200 (VR9) xDSL files provided the # example files from which the recognition technique was developed # (see https://xdarklight.github.io/lantiq-xdsl-firmware-info/) # - the version string extraction is derived from the method used by # Martin Blumenstingl's ltq-xdsl-fw-info.sh script # (see https://github.com/xdarklight/lantiq-xdsl-firmware-info) import os import sys import struct ### constants # common strings NULL = b'\x00' UNDERSCORE = b'_' EMPTY = '' # target strings XDSL_START = (('A', b'\x00\x00\x00\x00\x0a\x00\x00\x00\x68\x24\x00\x00\x00\x00\xff\xff' \ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), ('B', b'\x00\x00\x00\x00\x0b\x00\x00\x00\x68\x24\x00\x00\x00\x00\xff\xff' \ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ b'\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x00\x00')) XDSL_END = (('A', b'\x0b\x46\x42\x3e\x0c\x47\x43\x3f\x0d\x48\x44\x40\x0e\x49\x45\x41'), ('B', b'\xe0\x78\x60\x02\x04\x00')) XDSL_VERSION = b'\x40\x28\x23\x29' # text format templates FMT_VERSION = ' version: %s' FMT_NO_VERS = ' version: none identified!' FMT_MATCH = '\n[%d] xDSL file found:' FMT_OFFSET = ' offset: 0x%x' FMT_TYPE_S = ' start mark: type %s' FMT_TYPE_E = ' end mark: type %s' FMT_LENGTH = ' length: %d bytes' FMT_PARTIAL = """[%d] xDSL file with type %s start marker found at 0x%x but apparent length extends beyond end of image!""" FMT_FEXIST = ' %s: file already exists - skipping!' FMT_FN_VER = 'xcpe_%s.bin' FMT_FN_NOV = '%s-dsl_%s.%d.bin' # runtime help USAGE_STR = """ usage: %s [-l] where: -l - (optional) list details of files found but don't extract them """ % sys.argv[0] ### helper routines # write a message on standard output def logln(msg): sys.stdout.write('%s\n' % msg) sys.stdout.flush() # return version number strings from a Lantiq xDSL file # - there should be two, each of 6 characters separated by full stop # characters; each string is null terminated # - return as a combined string for use in a file name # - some older files have the VDSL version prefixed with "V_" # which will be preserved def xdsl_versions(xdsl_bytes): srch_offset = 0 vers_offs_s = len(XDSL_VERSION) versions = [] for i in (0, 1): vmark_idx = xdsl_bytes.find(XDSL_VERSION, srch_offset) if vmark_idx < 0: break srch_offset = vmark_idx + vers_offs_s term_idx = xdsl_bytes.find(NULL, srch_offset) if term_idx - srch_offset > 13: break versions.append(xdsl_bytes[srch_offset: term_idx]) srch_offset = term_idx + 1 if len(versions) == 2: version_str = versions[0] + UNDERSCORE + versions[1] return version_str.decode('ascii') else: return EMPTY ### main routine def extract_xdsl_files(src_file, list_only=False): # read the source try: src_bytes = bytearray(open(src_file, 'rb').read()) except (OSError, IOError): logln('%s: error reading file' % src_file) sys.exit(1) src_length = len(src_bytes) if list_only: logln('source file: %s' % src_file) # try and find the starting string hit_count = 0 for xdsl_s_type, xdsl_start_marker in XDSL_START: search_offset = 0 xdsl_start_len = len(xdsl_start_marker) while True: offset = src_bytes.find(xdsl_start_marker, search_offset) if offset > search_offset + 3: # check for the file length which is a little endian long # immediately preceding the start marker which is also # the first 4 bytes of the file; the value found doesn't # account for itself or the first 4 bytes of the start # marker (which are all NULLs) xdsl_length = struct.unpack_from('