#!/bin/bash # # Uncompress LZMA-packed firmware kernel. # # Based on the format description available at https://web.archive.org/20200701000000/www.wehavemorefun.de/fritzbox/LZMA-Kernel # SELF=$(readlink -f ${0}) usage() { cat << EOF Usage: $(basename $SELF) [-h|--help] [-u|--unpack] [-d|--dump] [-i|--info] [output-file] -h|--help print this help and exit -u|--unpack unpack contained LZMA-record(s), default -d|--dump dump contained LZMA-record(s) without unpacking -i|--info print meta information without writing any file input-file file in EVA-kernel-format, i.e. kernel.image or kernel.raw output-file name of the file output to be written to, optional if omitted automatically derived from the name of the input-file EOF } # process command line parameters ARGS=$(getopt -o hudi --long help,unpack,dump,info -n "${SELF}" -- "$@") [ $? -eq 0 ] || { usage >&2; exit 1; } eval set -- "${ARGS}" while true; do case "$1" in -h|--help) usage exit 0 ;; -u|--unpack) METHOD=unpack shift ;; -d|--dump) METHOD=dump shift ;; -i|--info) METHOD=info shift ;; --) shift break ;; *) echo >&2 "ERROR: internal error!" exit 1 ;; esac done METHOD=${METHOD:-unpack} # default to unpack if unset if [ $# -eq 1 ] || [ "$METHOD" != "info" -a $# -eq 2 ]; then INPUT_FILE="$1" [ -e "$INPUT_FILE" ] || { echo >&2 "ERROR: \"$INPUT_FILE\" could not be read"; exit 1; } OUTPUT_FILE="$2" if [ -z "$OUTPUT_FILE" ]; then # no OUTPUT_FILE provided => derive OUTPUT_FILE from INPUT_FILE if [ "$METHOD" == "unpack" ]; then OUTPUT_FILE="${INPUT_FILE}.unpacked" elif [ "$METHOD" == "dump" ]; then OUTPUT_FILE="${INPUT_FILE}.lzma-compressed-dump" fi fi else usage >&2; exit 1; fi if [ "$METHOD" == "unpack" ]; then declare -x UNLZMA UNLZMA="${UNLZMA:-$(dirname $SELF)/unlzma}" [ -x "$UNLZMA" ] || { echo >&2 "ERROR: this script requires unlzma tool which is not found on your system, expected location of the tool is \"$UNLZMA\""; exit 1; } fi declare -x CRC32 CRC32="${CRC32:-$(dirname $SELF)/crc32}" declare -x SUM SUM="${SUM:-$(dirname $SELF)/sum}" source "$(dirname $SELF)/freetz_bin_functions" ### print08X() { printf '0x%08X\n' "$@" } # # $1 - magic sequence (in little-endian notation) # $2 - magic sequence name (used in error messages) # $3 - max expected number of matches (unlimited if omitted) # $4 - tight lower bound of the match, i.e. the returned offset # must fulfill the condition "tight-lower-bound < offset" # (no lower bound restriction if omitted) # $5 - optional usually heuristic based function intended to reduce # the number of potential matches by doing some additional checks # getDecOffsetOf1stMagicSequenceMatch() { local sequenceNamePrefix=${2}${2:+ } # sequence name with space added local maxNumberOfMatches=$3 # unlimited if omitted local lowerBound=${4:--1} # -1 if omitted local deeperCheckFct=$5 # deeper check function declare -a offsetCandidates=($(getDecOffsetsOfAllMatches "$INPUT_FILE" bin $(echo -n $1 | invertEndianness))) if [ -n "${deeperCheckFct}" ]; then declare -a filteredOffsetCandidates for ((i=0; i<${#offsetCandidates[@]}; i++)); do if eval "${deeperCheckFct} ${offsetCandidates[i]}"; then filteredOffsetCandidates+=(${offsetCandidates[i]}) fi done offsetCandidates=("${filteredOffsetCandidates[@]}") fi for ((i=0; i<${#offsetCandidates[@]}; i++)); do if [ $((lowerBound)) -lt ${offsetCandidates[i]} ]; then local numberOfMatchesFound=$((${#offsetCandidates[@]} - i)) if [ -n "$maxNumberOfMatches" ] && [ ! $numberOfMatchesFound -le $maxNumberOfMatches ]; then echo >&2 "ERROR: number of ${sequenceNamePrefix}magic sequence (0x$1) matches ($numberOfMatchesFound) exceeds the expected limit ($maxNumberOfMatches)" return 1 fi # match fulfilling all restrictions found echo -n ${offsetCandidates[i]} return 0 fi done local fullfiling=${4:+(fulfilling the restriction $(print08X $lowerBound) < offset) } echo >&2 "ERROR: no ${sequenceNamePrefix}magic sequence (0x$1) ${fullfiling}found" return 1 } # # $1 - LZMA-record offset # $2 - TI-record name # $3 - name of the file output to be written to # # returns the error code of unlzma or output redirection (depending on the $METHOD) # processLzmaRecord() { local lzmaRecordOffset=$1 local tiRecordPrefix=$(echo -n $2 | tr -c '_a-zA-Z0-9' '_') tiRecordPrefix=${tiRecordPrefix}${tiRecordPrefix:+.} local outputFile="$3" local lzmaCompressedLen=$(getLEu32AtOffset "$INPUT_FILE" $((lzmaRecordOffset + 4))) local lzmaUncompressedLen=$(getLEu32AtOffset "$INPUT_FILE" $((lzmaRecordOffset + 8))) local lzmaCompressedChecksum=$(getLEu32AtOffset "$INPUT_FILE" $((lzmaRecordOffset + 12))) echo "${tiRecordPrefix}LzmaCompressedStreamLength=$(print08X $lzmaCompressedLen)" echo "${tiRecordPrefix}LzmaUncompressedStreamLength=$(print08X $lzmaUncompressedLen)" echo "${tiRecordPrefix}LzmaCompressedStreamChecksum=$(print08X $lzmaCompressedChecksum)" local lzmaStreamOffset=$((lzmaRecordOffset + 16)) # verify checksum local lzmaCompressedChecksumComputed=$(( 0x$(tail -c "+$(((lzmaStreamOffset + 1) + 8))" "$INPUT_FILE" 2>/dev/null | head -c $lzmaCompressedLen 2>/dev/null | $CRC32 2>/dev/null | cut -d ' ' -f 1) )) if [ $((lzmaCompressedChecksumComputed)) -ne $((lzmaCompressedChecksum)) ]; then echo >&2 "WARNING: calculated LZMA-stream checksum ($(print08X $lzmaCompressedChecksumComputed)) does not match the saved one ($(print08X $lzmaCompressedChecksum))" fi if [ "$METHOD" == "info" ]; then return 0 fi local lzmaUncompressedLenOffset=$((lzmaRecordOffset + 8)) { # LZMA properties (1 byte) + LZMA dictionary size (4 bytes) dd if="$INPUT_FILE" bs=1 skip=$lzmaStreamOffset count=5 2>/dev/null # uncompressed length dd if="$INPUT_FILE" bs=1 skip=$lzmaUncompressedLenOffset count=4 2>/dev/null # padding dd if=/dev/zero bs=1 count=4 2>/dev/null # compressed data # +1 = tail expects number of bytes, whereas lzmaStreamOffset is zero-based # +8 = skip LZMA properties (1 byte) + LZMA dictionary size (4 bytes) + padding (3 bytes) tail -c "+$(((lzmaStreamOffset + 1) + 8))" "$INPUT_FILE" 2>/dev/null | head -c $lzmaCompressedLen 2>/dev/null } | { if [ "$METHOD" == "unpack" ]; then "$UNLZMA" > "$outputFile" 2>/dev/null elif [ "$METHOD" == "dump" ]; then cat - > "$outputFile" 2>/dev/null fi } } # # $1 - bzImage offset # $2 - bzImage length # $3 - name of the file output to be written to # processX86bzImage() { local bzImageOffset=$1 local bzImageLen=$2 local outputFile="$3" if [ "$METHOD" == "info" ]; then return 0 fi # in case of bzImage we do not distinguish between "unpack" and "dump" methods tail -c "+$((bzImageOffset + 1))" "$INPUT_FILE" 2>/dev/null | head -c $bzImageLen >"$outputFile" 2>/dev/null } getTiRecordLen() { local tiRecordOffset=$1 getLEu32AtOffset "$INPUT_FILE" $((tiRecordOffset + 4)) } getLoadAddr() { local tiRecordOffset=$1 getLEu32AtOffset "$INPUT_FILE" $((tiRecordOffset + 8)) } getEntryAddr() { local tiRecordOffset=$1 local tiRecordLen=$(getTiRecordLen $tiRecordOffset) local tiRecordEntryZero=$(getLEu32AtOffset "$INPUT_FILE" $((tiRecordOffset + tiRecordLen + 16))) if [ "$tiRecordEntryZero" -ne 0 ]; then echo >&2 "WARNING: entry address is expected to be prefixed with one 32-bit zero word" fi getLEu32AtOffset "$INPUT_FILE" $((tiRecordOffset + tiRecordLen + 20)) } getTiRecordChecksum() { local tiRecordOffset=$1 local tiRecordLen=$(getTiRecordLen $tiRecordOffset) getLEu32AtOffset "$INPUT_FILE" $((tiRecordOffset + tiRecordLen + 12)) } maybeTiRecord() { local tiRecordOffset=$1 # a _potential_ tiRecordOffset local tiRecordPayloadOffset local payloadFirst16Bytes local TI_AR7_MAGIC_BE=$(echo -n "$TI_AR7_MAGIC" | invertEndianness) tiRecordPayloadOffset=$((tiRecordOffset+12)) payloadFirst16Bytes=$(getHexContentAtOffset "$INPUT_FILE" ${tiRecordPayloadOffset} 16 2>/dev/null) || return 1 # heuristic, check if payload contains one of the known magic sequences [ "${payloadFirst16Bytes:0:${#EVA_LZMA_RECORD_MAGIC_BE}}" == "$EVA_LZMA_RECORD_MAGIC_BE" ] \ || \ [ "${payloadFirst16Bytes:0:${#X86_BOOT_SECTOR_MAGIC_BE}}" == "$X86_BOOT_SECTOR_MAGIC_BE" ] \ || \ [ "${payloadFirst16Bytes:0:${#TI_AR7_MAGIC_BE}}" == "$TI_AR7_MAGIC_BE" ] } # # $1 - magic sequence (in little-endian notation) # $2 - TI-record name # $3 - name of the file output to be written to # $4 - tight lower bound of the magic sequence match, optional # # returns offset of the magic sequence by setting the global variable TI_RECORD_OFFSET # findAndProcessTiRecord() { local magic="$1" local tiRecordName="$2" local tiRecordPrefix=$(echo -n "$tiRecordName" | tr -c '_a-zA-Z0-9' '_') tiRecordPrefix=${tiRecordPrefix}${tiRecordPrefix:+.} local outputFile="$3" local lowerBound="$4" # unset return value TI_RECORD_OFFSET="" local tiRecordOffset="" { tiRecordOffset=$(getDecOffsetOf1stMagicSequenceMatch $magic "$tiRecordName" 1 "$lowerBound" "maybeTiRecord"); } || return 1 local tiRecordLoadAddr=$(getLoadAddr $tiRecordOffset) echo "${tiRecordPrefix}LoadAddress=$(print08X $tiRecordLoadAddr)" local tiRecordEntryAddr=$(getEntryAddr $tiRecordOffset) echo "${tiRecordPrefix}EntryAddress=$(print08X $tiRecordEntryAddr)" local tiRecordLen=$(getTiRecordLen $tiRecordOffset) echo "${tiRecordPrefix}RecordLength=$(print08X $tiRecordLen)" local tiRecordChecksum=$(getTiRecordChecksum $tiRecordOffset) echo "${tiRecordPrefix}RecordChecksum=$(print08X $tiRecordChecksum)" local tiRecordPayloadOffset=$((tiRecordOffset+12)) # verify TI-record checksum local tiRecordPayloadSum=$(tail -c "+$((tiRecordPayloadOffset + 1))" "$INPUT_FILE" 2>/dev/null | head -c $tiRecordLen 2>/dev/null | $SUM -p 2>/dev/null | cut -d ' ' -f 1) local tiRecordChecksumComputed=$(( (((tiRecordLen + tiRecordLoadAddr + tiRecordPayloadSum) ^ 0xFFFFFFFF) & 0xFFFFFFFF) + 1 )) if [ $((tiRecordChecksumComputed)) -ne $((tiRecordChecksum)) ]; then echo >&2 "WARNING: calculated TI-record checksum ($(print08X $tiRecordChecksumComputed)) does not match the saved one ($(print08X $tiRecordChecksum))" fi local payloadFirst16Bytes=$(getHexContentAtOffset "$INPUT_FILE" ${tiRecordPayloadOffset} 16) if [ "${payloadFirst16Bytes:0:${#EVA_LZMA_RECORD_MAGIC_BE}}" == "$EVA_LZMA_RECORD_MAGIC_BE" ]; then processLzmaRecord $tiRecordPayloadOffset "$tiRecordName" "$outputFile" elif [ "${payloadFirst16Bytes:0:${#X86_BOOT_SECTOR_MAGIC_BE}}" == "$X86_BOOT_SECTOR_MAGIC_BE" ]; then processX86bzImage $tiRecordPayloadOffset $tiRecordLen "$outputFile" else echo >&2 "ERROR: unsupported payload within ${tiRecordName}-record (${payloadFirst16Bytes})"; return 1; fi [ $? -ne 0 ] && { echo >&2 "ERROR: failed to ${METHOD} kernel"; return 1; } # set return value TI_RECORD_OFFSET=$tiRecordOffset } ## TI_AR7_MAGIC=FEED1281 TI_PUMA6_MAGIC=FEED8112 DUAL_KERNEL_MAGIC=FEED9112 TI_AR7_2ND_MAGIC=FEEDB007 EVA_LZMA_RECORD_MAGIC_BE=$(echo -n 075A0201 | invertEndianness) X86_BOOT_SECTOR_MAGIC_BE=EA0500C0078CC88ED88EC08ED031E4FB # 1st kernel (PUMA6 boxes, x86 kernel) if getDecOffsetOf1stMagicSequenceMatch $TI_PUMA6_MAGIC "TI-PUMA6" 1 "" "maybeTiRecord" >/dev/null 2>&1; then findAndProcessTiRecord $TI_PUMA6_MAGIC "TI-PUMA6" "$OUTPUT_FILE" || exit 1 exit 0 fi # 1st kernel (all boxes) findAndProcessTiRecord $TI_AR7_MAGIC "TI-AR7" "$OUTPUT_FILE" || exit 1 feed1281_Offset=$TI_RECORD_OFFSET # 2nd kernel (GRX5 boxes only) feed9112_Offset=$(getDecOffsetOf1stMagicSequenceMatch $DUAL_KERNEL_MAGIC "DUAL-kernel" 1 "" "maybeTiRecord" 2>/dev/null) if [ -n "$feed9112_Offset" ]; then if [ $((feed1281_Offset - feed9112_Offset)) -ne 12 ]; then echo >&2 "ERROR: DUAL-kernel magic sequence (0x$DUAL_KERNEL_MAGIC) is expected to be found exactly 12 bytes before TI-AR7 record"; exit 1 fi feed1281_LoadAddr=$(getLoadAddr $feed1281_Offset) feed9112_LoadAddr=$(getLoadAddr $feed9112_Offset) if [ "$feed1281_LoadAddr" != "$feed9112_LoadAddr" ]; then echo >&2 "WARNING: load addresses stored in TI-AR7 record ($(print08X $feed1281_LoadAddr)) and DUAL-kernel record ($(print08X $feed9112_LoadAddr)) do not match" fi feed1281_EntryAddr=$(getEntryAddr $feed1281_Offset) feed9112_EntryAddr=$(getEntryAddr $feed9112_Offset) if [ "$feed1281_EntryAddr" != "$feed9112_EntryAddr" ]; then echo >&2 "WARNING: entry addresses stored in TI-AR7 record ($(print08X $feed1281_EntryAddr)) and DUAL-kernel record ($(print08X $feed9112_EntryAddr)) do not match" fi feedb007_LowerBound=$((feed1281_Offset + $(getTiRecordLen $feed1281_Offset) + 24)) feedb007_LowerBound=$((feedb007_LowerBound + (4 - feedb007_LowerBound%4)%4)) # pad to a 4-byte boundary findAndProcessTiRecord $TI_AR7_2ND_MAGIC "TI-AR7-2ND" "${OUTPUT_FILE}.2ND" $((feedb007_LowerBound-1)) || exit 1 feedb007_Offset=$TI_RECORD_OFFSET fi