157 lines
5.7 KiB
Python
157 lines
5.7 KiB
Python
# StoneD (Stone Depacker) - unpack Unisoc SC6531 stone images (part of UniFlash)
|
|
# Created by Luxferre in 2021, released into public domain
|
|
|
|
import os
|
|
import sys
|
|
import struct
|
|
import lzma
|
|
#from custlzma.lzma_decoder import LZMADecoder # (for LZMA_SPD decompression)
|
|
from custlzma.frenchlzma import DecodeurLZMA
|
|
|
|
# common utils
|
|
|
|
def readFile(fname):
|
|
f = open(fname, 'rb')
|
|
fdata = f.read()
|
|
f.close()
|
|
return fdata
|
|
|
|
def writeFile(fname, fdata):
|
|
outf = open(fname, 'wb')
|
|
outf.write(fdata)
|
|
outf.close()
|
|
|
|
# unpack part
|
|
|
|
CMP_NONE=0
|
|
CMP_LZMA_SPRD=1
|
|
CMP_LZMA=2
|
|
|
|
def getCompType(data):
|
|
if (data[0] == 0x5d or data[0] == 0x67) and data[1] == 0:
|
|
return CMP_LZMA
|
|
elif data[0] == 0x5a and data[1] == 0:
|
|
return CMP_LZMA_SPRD
|
|
else:
|
|
return CMP_NONE
|
|
|
|
def getTblOffset(blocksOffTbl, index):
|
|
tind = index << 2
|
|
return struct.unpack('<L', blocksOffTbl[tind:tind+4])[0]
|
|
|
|
def unpack_block(blkData, blkPacSize, targetFile):
|
|
print('Extracting %s...' % targetFile)
|
|
blocksOffTbl = None
|
|
compData = blkData
|
|
npacHdr = blkData[:16]
|
|
(npacHdrMagic, npacHdrFlags, compDataSize, lzmaBlocksAmount) = struct.unpack('<LLLL', npacHdr)
|
|
# npacHdrMagic must be CAPN if using offsets
|
|
if npacHdrMagic == 0x4E504143:
|
|
blocksOffTbl = blkData[compDataSize:]
|
|
compData = blkData[getTblOffset(blocksOffTbl,0):]
|
|
else:
|
|
lzmaBlocksAmount = 1
|
|
cType = getCompType(compData)
|
|
assert cType == CMP_LZMA or cType == CMP_LZMA_SPRD, 'Only LZMA compression type is implemented as of now'
|
|
print('Found LZMA blocks: %d, decompressing...' % lzmaBlocksAmount)
|
|
dest = b''
|
|
inSizePure = blkPacSize * 2
|
|
lzmaDec = DecodeurLZMA()
|
|
for i in range(lzmaBlocksAmount):
|
|
if blocksOffTbl is not None:
|
|
dataOffset = getTblOffset(blocksOffTbl,i)
|
|
else:
|
|
dataOffset = 0
|
|
compData = blkData[dataOffset:]
|
|
lzData = compData[0:inSizePure]
|
|
if cType == CMP_LZMA_SPRD:
|
|
outdata = lzmaDec.decode(lzData)
|
|
dest += outdata
|
|
else:
|
|
dest += lzma.decompress(lzData, format=lzma.FORMAT_ALONE)
|
|
sys.stdout.write('.')
|
|
sys.stdout.flush()
|
|
writeFile(targetFile, dest)
|
|
print('\n%s decompressed!' % targetFile)
|
|
|
|
def unpack_section(sectionData, targetDir):
|
|
bzpFileHdr = sectionData[:16]
|
|
(bzpFileHdrMagic, bzpType, blocksOffset, blocksAmount) = struct.unpack('<LLLL', bzpFileHdr)
|
|
# bzpFileHdrMagic must be DRPS or RRPS
|
|
assert bzpFileHdrMagic == 0x53505244 or bzpFileHdrMagic == 0x53505252, 'Invalid BZP header: 0x%X' % bzpFileHdrMagic
|
|
bzpSize = blocksOffset + blocksAmount * 20
|
|
for i in range(blocksAmount):
|
|
blkHdrStart = blocksOffset + i*20
|
|
blkHdr = sectionData[blkHdrStart:blkHdrStart+20]
|
|
(blkHdrMagic, blkId, blkDataOffset, blkPackedSize, blkPacSize) = struct.unpack('<LLLLL', blkHdr)
|
|
# blkHdrMagic must be COLB
|
|
assert blkHdrMagic == 0x424C4F43, 'Invalid BZP block header: 0x%X' % blkHdrMagic
|
|
if bzpSize < blkDataOffset + blkPackedSize:
|
|
bzpSize = blkDataOffset + blkPackedSize
|
|
if blkId == 0x494D4147: # GAMI -> kernel image
|
|
targetFile = targetDir + '/kern.bin'
|
|
elif blkId == 0x75736572: # resu -> user image
|
|
targetFile = targetDir + '/user.bin'
|
|
elif blkId == 0x7253736F: # resources
|
|
targetFile = targetDir + '/rsrc.bin'
|
|
else:
|
|
targetFile = targetDir + ('/blk_%X.bin' % blkId)
|
|
unpack_block(sectionData[blkDataOffset:], blkPacSize, targetFile)
|
|
|
|
def unpack_stone(fname, targetDir):
|
|
fdata = readFile(fname)
|
|
flen = len(fdata)
|
|
assert flen >= 0x10, 'Input file %s is too small' % fname
|
|
|
|
# check for security header
|
|
sectionOffset = 0
|
|
if fdata[0:15] == b'SPRD-SECUREFLAG':
|
|
sectionOffset = 1024
|
|
print('Signed image detected, using section offset %d' % sectionOffset)
|
|
|
|
# look for TRAPGAMI header
|
|
startPos = -1
|
|
for i in range(flen):
|
|
if fdata[i:i+8] == b'TRAPGAMI':
|
|
startPos = i
|
|
break
|
|
assert startPos > 0, 'No stone header found in %s' % fname
|
|
print('Stone header found at 0x%X' % startPos)
|
|
|
|
psImageEnd = 0xffffffff # PS (protocol station) image is the first in the flash backup and not compressed
|
|
|
|
dfcStruct = fdata[startPos+8:startPos+120]
|
|
for i in range(0,112,4):
|
|
targetAddr = struct.unpack('<L', dfcStruct[i:i+4])[0]
|
|
if targetAddr < 0xffffffff:
|
|
if targetAddr < psImageEnd:
|
|
psImageEnd = targetAddr
|
|
print('Target section address found: 0x%X' % targetAddr)
|
|
unpack_section(fdata[sectionOffset+targetAddr:], targetDir)
|
|
print('Section 0x%X unpacked!' % targetAddr)
|
|
|
|
if psImageEnd > 0:
|
|
psPath = targetDir + '/ps.bin'
|
|
writeFile(psPath, fdata[:psImageEnd])
|
|
print('Protocol station image %s written!' % psPath)
|
|
|
|
# main code start
|
|
|
|
if __name__ == '__main__': # main app start
|
|
from argparse import ArgumentParser
|
|
rootdir = os.path.dirname(os.path.realpath(__file__))
|
|
parser = ArgumentParser(description='StoneD: an opensource Unisoc/Spreadtrum stone image unpacker', epilog='(c) Luxferre 2021 --- No rights reserved <https://unlicense.org>')
|
|
parser.add_argument('file', help='Stone image file to unpack')
|
|
parser.add_argument('-d','--directory', default=None, help='Directory where component files will be written to (defaults to the same where the main stone file resides)')
|
|
|
|
args = parser.parse_args()
|
|
|
|
imgfile = args.file
|
|
imgdir = os.path.dirname(os.path.realpath(imgfile))
|
|
if args.directory is not None:
|
|
imgdir = os.path.realpath(args.directory)
|
|
|
|
print('Unpacking %s to %s' % (imgfile, imgdir))
|
|
unpack_stone(imgfile, imgdir)
|
|
|