xdsl-modem-fw-tools/extract_ltq_xdsl_files.py
Andrew MacIntyre 937ac93f65 extract_ltq_xdsl_files.py: enhance to match newly found files
With fimrware release 4.4.2.1 for the Vigor 2765, Draytek added an
extra "modem" package which listed two firmware versions that I've
not seen references to before:
- 8.D.1.F.1.7_8.D.1.0.1.1  (VDSL vectoring, ADSL Annex A)
- 8.D.1.F.1.7_8.D.1.0.1.2  (VDSL vectoring, ADSL Annex B)

These two firmwares have a common 6 byte closing sequence that is
completely different from all the other known firmwares, while
otherwise having the same structure so enhance the script to support
the extra closing sequence.

While at it, refactor the script to move the output format strings
to constants and make several other minor editorial fixes.
2023-12-27 23:07:14 +11:00

265 lines
9.7 KiB
Python
Executable file

#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2023 Andrew I MacIntyre <andymac@pcug.org.au>
# 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 <source_file> [-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('<L', src_bytes, offset - 4)[0] + 4
xdsl_end = offset + xdsl_length
if xdsl_end <= src_length:
# check for an end marker finishing at the specified size
end_matched = False
for xdsl_e_type, end_mark in XDSL_END:
if src_bytes[xdsl_end - len(end_mark): xdsl_end] == end_mark:
end_matched = True
break
if end_matched:
hit_count += 1
hit_s = offset - 4
hit_l = xdsl_length + 4
logln(FMT_MATCH % hit_count)
logln(FMT_OFFSET % hit_s)
logln(FMT_TYPE_S % xdsl_s_type)
logln(FMT_TYPE_E % xdsl_e_type)
logln(FMT_LENGTH % hit_l)
xdsl_bytes = src_bytes[hit_s: hit_s + hit_l]
# extract version string
version_str = xdsl_versions(xdsl_bytes)
if version_str:
logln(FMT_VERSION % version_str)
xdsl_file = FMT_FN_VER % version_str
else:
logln(FMT_NO_VERS)
xdsl_file = FMT_FN_NOV % (src_file, xdsl_type, hit_count)
# save xDSL file bytes to a file if it doesn't already exist
if not list_only:
if not os.path.exists(xdsl_file):
open(xdsl_file, 'wb').write(xdsl_bytes)
else:
logln(FMT_FEXIST % xdsl_file)
# any others to be found?
search_offset = xdsl_end
else:
# any others to be found?
search_offset += offset + xdsl_start_len
else:
# anything returned would be truncated
logln(FMT_PARTIAL % (hit_count + 1, xdsl_s_type, offset))
break
else:
break
if not hit_count:
logln('! no xDSL files found')
### run as script
if __name__ == '__main__':
try:
list_opt = sys.argv[2]
if list_opt != '-l':
raise ValueError('"%s": option not recognised' % list_opt)
except IndexError:
list_opt = False
except ValueError as e:
logln(e.args[0])
logln(USAGE_STR)
sys.exit(1)
try:
extract_xdsl_files(sys.argv[1], list_opt)
except IndexError:
logln(USAGE_STR)
sys.exit(1)