phoenix_firmware_dumper/utils/kdztools/unkdz.py

313 lines
9.1 KiB
Python
Executable File

#!/usr/bin/env python2
"""
Copyright (C) 2016 Elliott Mitchell <ehem+android@m5p.com>
Copyright (C) 2013 IOMonster (thecubed on XDA)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from __future__ import absolute_import
from __future__ import print_function
import os
import argparse
import sys
from binascii import b2a_hex
# our tools are in "libexec"
sys.path.append(os.path.join(sys.path[0], "libexec"))
import kdz
class KDZFileTools(kdz.KDZFile):
"""
LGE KDZ File tools
"""
# Setup variables
partitions = []
outdir = "kdzextracted"
infile = None
kdz_header = {
b"\x28\x05\x00\x00"b"\x34\x31\x25\x80": 0,
b"\x18\x05\x00\x00"b"\x32\x79\x44\x50": 1,
kdz.KDZFile._dz_header: 2,
}
def readKDZHeader(self):
"""
Reads the KDZ header, and returns a single kdz_item
in the form as defined by self._dz_format_dict
"""
# Read a whole DZ header
buf = self.infile.read(self._dz_length)
# "Make the item"
# Create a new dict using the keys from the format string
# and the format string itself
# and apply the format to the buffer
kdz_item = dict(zip(
self._dz_format_dict.keys(),
self._dz_struct.unpack(buf)
))
# Collapse (truncate) each key's value if it's listed as collapsible
for key in self._dz_collapsibles:
if type(kdz_item[key]) is str or type(kdz_item[key]) is bytes:
kdz_item[key] = kdz_item[key].rstrip(b'\x00')
if b'\x00' in kdz_item[key]:
print("[!] Warning: extraneous data found IN "+key, file=sys.stderr)
#sys.exit(1)
elif type(kdz_item[key]) is int:
if kdz_item[key] != 0:
print('[!] Error: field "'+key+'" is non-zero ('+b2a_hex(kdz_item[key])+')', file=sys.stderr)
sys.exit(1)
else:
print("[!] Error: internal error", file=sys.stderr)
sys.exit(-1)
return kdz_item
def getPartitions(self):
"""
Returns the list of partitions from a KDZ file containing multiple segments
"""
# Setup initial values
last = False
cont = not last
self.dataStart = 1<<63
while cont:
# Read the current KDZ header
kdz_sub = self.readKDZHeader()
# Add it to our list
self.partitions.append(kdz_sub)
# Update start of data, if needed
if kdz_sub['offset'] < self.dataStart:
self.dataStart = kdz_sub['offset']
# Was it the last one?
cont = not last
# Check for end of headers
nextchar = self.infile.read(1)
# Is this the last KDZ header? (ctrl-C, how appropos)
if nextchar == b'\x03':
last = True
# Alternative, immediate end
elif nextchar == b'\x00':
cont = False
# Rewind file pointer 1 byte
else:
self.infile.seek(-1, os.SEEK_CUR)
# Record where headers end
self.headerEnd = self.infile.tell()
# Paranoia check for an updated file format
buf = self.infile.read(self.dataStart - self.headerEnd - 1)
if len(buf.lstrip(b'\x00')) > 0:
print("[!] Warning: Data between headers and payload! (offsets {:d} to {:d})".format(self.headerEnd, self.dataStart), file=sys.stderr)
self.hasExtra = True
# Make partition list
return [(x['name'],x['length']) for x in self.partitions]
def extractPartition(self,index):
"""
Extracts a partition from a KDZ file
"""
currentPartition = self.partitions[index]
# Seek to the beginning of the compressed data in the specified partition
self.infile.seek(currentPartition['offset'], os.SEEK_SET)
# Ensure that the output directory exists
if not os.path.exists(self.outdir):
os.makedirs(self.outdir)
# Open the new file for writing
outfile = open(os.path.join(self.outdir,currentPartition['name'].decode("utf8")), 'wb')
# Use 1024 byte chunks
chunkSize = 1024
# uncomment to prevent runaways
#for x in xrange(10):
while True:
# Read file in 1024 byte chunks
outfile.write(self.infile.read(chunkSize))
# If the output file + chunkSize would be larger than the input data
if outfile.tell() + chunkSize >= currentPartition['length']:
# Change the chunk size to be the difference between the length of the input and the current length of the output
outfile.write(self.infile.read(currentPartition['length'] - outfile.tell() ))
# Prevent runaways!
break
# Close the file
outfile.close()
def saveExtra(self):
"""
Save the extra data that has appeared between headers&files
"""
try:
if not self.hasExtra:
return
except AttributeError:
return
filename = os.path.join(self.outdir, "kdz_extras.bin")
extra = open(filename, "wb")
print("[+] Extracting extra data to " + filename)
self.infile.seek(self.headerEnd, os.SEEK_SET)
total = self.dataStart - self.headerEnd
while total > 0:
count = 4096 if 4096 < total else total
buf = self.infile.read(count)
extra.write(buf)
total -= count
extra.close()
def saveParams(self):
"""
Save the parameters for creating a compatible file
"""
params = open(os.path.join(self.outdir, ".kdz.params"), "wt")
params.write('# saved parameters from the file "{:s}"\n'.format(self.kdzfile))
params.write("version={:d}\n".format(self.header_type))
params.write("# note, this is actually quite fluid, dataStart just needs to be large enough\n")
params.write("# for headers not to overwrite data; roughly 16 bytes for overhead plus 272\n")
params.write("# bytes per file should be sufficient (but not match original)\n")
params.write("dataStart={:d}\n".format(self.dataStart))
params.write("# embedded files\n")
out = []
i = 0
for p in self.partitions:
out.append({'name': p['name'], 'data': p['offset'], 'header': i})
i += 1
out.sort(key=lambda p: p['data'])
i = 0
for p in out:
params.write("payload{:d}={:s}\n".format(i, p['name'].decode("utf8")))
params.write("payload{:d}head={:d}\n".format(i, p['header']))
i += 1
params.close()
def parseArgs(self):
# Parse arguments
parser = argparse.ArgumentParser(description='LG KDZ File Extractor originally by IOMonster')
parser.add_argument('-f', '--file', help='KDZ File to read', action='store', required=True, dest='kdzfile')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-l', '--list', help='list partitions', action='store_true', dest='listOnly')
group.add_argument('-x', '--extract', help='extract all partitions', action='store_true', dest='extractAll')
group.add_argument('-s', '--single', help='single Extract by ID', action='store', dest='extractID', type=int)
parser.add_argument('-d', '--dir', '-o', '--out', help='output directory', action='store', dest='outdir')
return parser.parse_args()
def openFile(self, kdzfile):
# Open the file
try:
self.infile = open(kdzfile, "rb")
except IOError as err:
print(err, file=sys.stderr)
sys.exit(1)
# Get length of whole file
self.infile.seek(0, os.SEEK_END)
# os.seek() doesn't return current position?!
self.kdz_length = self.infile.tell()
self.infile.seek(0, os.SEEK_SET)
# Verify KDZ header
verify_header = self.infile.read(8)
if verify_header not in self.kdz_header:
print("[!] Error: Unsupported KDZ file format.")
print('[ ] Received header "{:s}".'.format(" ".join(b2a_hex(n) for n in verify_header)))
sys.exit(1)
self.header_type = self.kdz_header[verify_header]
def cmdExtractSingle(self, partID):
print("[+] Extracting single partition from v{:d} file!\n".format(self.header_type))
print("[+] Extracting " + str(self.partList[partID][0]) + " to " + os.path.join(self.outdir,self.partList[partID][0].decode("utf8")))
self.extractPartition(partID)
def cmdExtractAll(self):
print("[+] Extracting all partitions from v{:d} file!\n".format(self.header_type))
for part in enumerate(self.partList):
print("[+] Extracting " + part[1][0].decode("utf8") + " to " + os.path.join(self.outdir,part[1][0].decode("utf8")))
self.extractPartition(part[0])
self.saveExtra()
self.saveParams()
def cmdListPartitions(self):
print("[+] KDZ Partition List (format v{:d})\n=========================================".format(self.header_type))
for part in enumerate(self.partList):
print("{:2d} : {:s} ({:d} bytes)".format(part[0], part[1][0].decode("utf8"), part[1][1]))
def main(self):
args = self.parseArgs()
self.kdzfile = args.kdzfile
self.openFile(args.kdzfile)
self.partList = self.getPartitions()
if args.outdir:
self.outdir = args.outdir
if args.listOnly:
self.cmdListPartitions()
elif args.extractID != None:
if args.extractID >= 0 and args.extractID < len(self.partList):
self.cmdExtractSingle(args.extractID)
else:
print("[!] Segment {:d} is out of range!".format(args.extractID), file=sys.stderr)
elif args.extractAll:
self.cmdExtractAll()
if __name__ == "__main__":
kdztools = KDZFileTools()
kdztools.main()