TIK/ofp_qc_decrypt.py

366 lines
11 KiB
Python

#!/usr/bin/env python3
# (c) B.Kerler 2018-2021, MIT license
import os
import sys
import xml.etree.ElementTree as ET
import zipfile
from struct import unpack
from binascii import unhexlify, hexlify
from Crypto.Cipher import AES
from Crypto.Hash import MD5
import hashlib
import shutil
def swap(ch):
return ((ch & 0xF) << 4) + ((ch & 0xF0) >> 4)
def keyshuffle(key, hkey):
for i in range(0, 0x10, 4):
key[i] = swap((hkey[i] ^ key[i]))
key[i + 1] = swap(hkey[i + 1] ^ key[i + 1])
key[i + 2] = swap(hkey[i + 2] ^ key[i + 2])
key[i + 3] = swap(hkey[i + 3] ^ key[i + 3])
return key
def ROR(x, n, bits=32):
mask = (2 ** n) - 1
mask_bits = x & mask
return (x >> n) | (mask_bits << (bits - n))
def ROL(x, n, bits=32):
return ROR(x, bits - n, bits)
def generatekey1():
key1 = "42F2D5399137E2B2813CD8ECDF2F4D72"
key2 = "F6C50203515A2CE7D8C3E1F938B7E94C"
key3 = "67657963787565E837D226B69A495D21"
key1 = bytearray.fromhex(key1)
key2 = bytearray.fromhex(key2)
key3 = bytearray.fromhex(key3)
key2 = keyshuffle(key2, key3)
aeskey = bytes(hashlib.md5(key2).hexdigest()[0:16], 'utf-8')
key1 = keyshuffle(key1, key3)
iv = bytes(hashlib.md5(key1).hexdigest()[0:16], 'utf-8')
return aeskey, iv
def bytestolow(data):
h = MD5.new()
h.update(data)
shash = h.digest()
return hexlify(shash).lower()[0:16]
def deobfuscate(data, mask):
ret = bytearray()
for i in range(0, len(data)):
v = ROL((data[i] ^ mask[i]), 4, 8)
ret.append(v)
return ret
def generatekey2(filename):
keys = [
# R9s/A57t
["V1.4.17/1.4.27",
"27827963787265EF89D126B69A495A21",
"82C50203285A2CE7D8C3E198383CE94C",
"422DD5399181E223813CD8ECDF2E4D72"],
# a3s
["V1.6.17",
"E11AA7BB558A436A8375FD15DDD4651F",
"77DDF6A0696841F6B74782C097835169",
"A739742384A44E8BA45207AD5C3700EA"],
["V1.5.13",
"67657963787565E837D226B69A495D21",
"F6C50203515A2CE7D8C3E1F938B7E94C",
"42F2D5399137E2B2813CD8ECDF2F4D72"],
# R15 Pro CPH1831 V1.6.6 / FindX CPH1871 V1.6.9 / R17 Pro CPH1877 V1.6.17 / R17 PBEM00 V1.6.17 / A5 2020 V1.7.6 / K3 CPH1955 V1.6.26 UFS
# Reno 5G CPH1921 V1.6.26 / Realme 3 Pro RMX1851 V1.6.17 / Reno 10X Zoom V1.6.26 / R17 CPH1879 V1.6.17 / R17 Neo CPH1893 / K1 PBCM30
["V1.6.6/1.6.9/1.6.17/1.6.24/1.6.26/1.7.6",
"3C2D518D9BF2E4279DC758CD535147C3",
"87C74A29709AC1BF2382276C4E8DF232",
"598D92E967265E9BCABE2469FE4A915E"],
# RM1921EX V1.7.2, Realme X RMX1901 V1.7.2, Realme 5 Pro RMX1971 V1.7.2, Realme 5 RMX1911 V1.7.2
["V1.7.2",
"8FB8FB261930260BE945B841AEFA9FD4",
"E529E82B28F5A2F8831D860AE39E425D",
"8A09DA60ED36F125D64709973372C1CF"],
# OW19W8AP_11_A.23_200715
["V2.0.3",
"E8AE288C0192C54BF10C5707E9C4705B",
"D64FC385DCD52A3C9B5FBA8650F92EDA",
"79051FD8D8B6297E2E4559E997F63B7F"]
]
for dkey in keys:
key = bytearray()
iv = bytearray()
# "Read metadata failed"
mc = bytearray.fromhex(dkey[1])
userkey = bytearray.fromhex(dkey[2])
ivec = bytearray.fromhex(dkey[3])
# userkey=bytearray(unhexlify("A3D8D358E42F5A9E931DD3917D9A3218"))
# ivec=bytearray(unhexlify("386935399137416B67416BECF22F519A"))
# mc=bytearray(unhexlify("9E4F32639D21357D37D226B69A495D21"))
key = deobfuscate(userkey, mc)
iv = deobfuscate(ivec, mc)
key = bytestolow(key)
iv = bytestolow(iv)
pagesize, data = extract_xml(filename, key, iv)
if pagesize != 0:
return pagesize, key, iv, data
return 0, None, None, None
def extract_xml(filename, key, iv):
filesize = os.stat(filename).st_size
with open(filename, 'rb') as rf:
pagesize = 0
for x in [0x200, 0x1000]:
rf.seek(filesize - x + 0x10)
if unpack("<I", rf.read(4))[0] == 0x7CEF:
pagesize = x
break
if pagesize == 0:
print("Unknown pagesize. Aborting")
exit(0)
xmloffset = filesize - pagesize
rf.seek(xmloffset + 0x14)
offset = unpack("<I", rf.read(4))[0] * pagesize
length = unpack("<I", rf.read(4))[0]
if length < 200: # A57 hack
length = xmloffset - offset - 0x57
rf.seek(offset)
data = rf.read(length)
dec = aes_cfb(data, key, iv)
# h=MD5.new()
# h.update(data)
# print(dec.decode('utf-8'))
# print(h.hexdigest())
# print("Done.")
if b"<?xml" in dec:
return pagesize, dec
else:
return 0, ""
def aes_cfb(data, key, iv):
ctx = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128)
decrypted = ctx.decrypt(data)
return decrypted
def copysub(rf, wf, start, length):
rf.seek(start)
rlen = 0
while length > 0:
if length < 0x100000:
size = length
else:
size = 0x100000
data = rf.read(size)
wf.write(data)
rlen += len(data)
length -= size
return rlen
def copy(filename, wfilename, path, start, length, checksums):
print(f"\nExtracting {wfilename}")
with open(filename, 'rb') as rf:
with open(os.path.join(path, wfilename), 'wb') as wf:
rf.seek(start)
data = rf.read(length)
wf.write(data)
checkhashfile(os.path.join(path, wfilename), checksums, True)
def decryptfile(key, iv, filename, path, wfilename, start, length, rlength, checksums, decryptsize=0x40000):
print(f"\nExtracting {wfilename}")
if rlength == length:
tlen = length
length = (length // 0x4 * 0x4)
if tlen % 0x4 != 0:
length += 0x4
with open(filename, 'rb') as rf:
with open(os.path.join(path, wfilename), 'wb') as wf:
rf.seek(start)
size = decryptsize
if rlength < decryptsize:
size = rlength
data = rf.read(size)
if size % 4:
data += (4 - (size % 4)) * b'\x00'
outp = aes_cfb(data, key, iv)
wf.write(outp[:size])
if rlength > decryptsize:
copysub(rf, wf, start + size, rlength - size)
if rlength % 0x1000 != 0:
fill = bytearray([0x00 for i in range(0x1000 - (rlength % 0x1000))])
# wf.write(fill)
checkhashfile(os.path.join(path, wfilename), checksums, False)
def checkhashfile(wfilename, checksums, iscopy):
sha256sum = checksums[0]
md5sum = checksums[1]
if iscopy:
prefix = "Copy: "
else:
prefix = "Decrypt: "
with open(wfilename, "rb") as rf:
size = os.stat(wfilename).st_size
md5 = hashlib.md5(rf.read(0x40000))
sha256bad = False
md5bad = False
md5status = "empty"
sha256status = "empty"
if sha256sum != "":
for x in [0x40000, size]:
rf.seek(0)
sha256 = hashlib.sha256(rf.read(x))
if sha256sum != sha256.hexdigest():
sha256bad = True
sha256status = "bad"
else:
sha256status = "verified"
break
if md5sum != "":
if md5sum != md5.hexdigest():
md5bad = True
md5status = "bad"
else:
md5status = "verified"
if (sha256bad and md5bad) or (sha256bad and md5sum == "") or (md5bad and sha256sum == ""):
print(f"{prefix}error on hashes. File might be broken!")
else:
print(f"{prefix}success! (md5: {md5status} | sha256: {sha256status})")
def decryptitem(item, pagesize):
sha256sum = ""
md5sum = ""
wfilename = ""
start = -1
rlength = 0
decryptsize = 0x40000
if "Path" in item.attrib:
wfilename = item.attrib["Path"]
elif "filename" in item.attrib:
wfilename = item.attrib["filename"]
if "sha256" in item.attrib:
sha256sum = item.attrib["sha256"]
if "md5" in item.attrib:
md5sum = item.attrib["md5"]
if "FileOffsetInSrc" in item.attrib:
start = int(item.attrib["FileOffsetInSrc"]) * pagesize
elif "SizeInSectorInSrc" in item.attrib:
start = int(item.attrib["SizeInSectorInSrc"]) * pagesize
if "SizeInByteInSrc" in item.attrib:
rlength = int(item.attrib["SizeInByteInSrc"])
if "SizeInSectorInSrc" in item.attrib:
length = int(item.attrib["SizeInSectorInSrc"]) * pagesize
else:
length = rlength
return wfilename, start, length, rlength, [sha256sum, md5sum], decryptsize
def main(filename, outdir):
if not os.path.exists(outdir):
os.mkdir(outdir)
pk = False
with open(filename, "rb") as rf:
if rf.read(2) == b"PK":
pk = True
if pk:
print("Zip file detected, trying to decrypt files")
zippw = bytes("flash@realme$50E7F7D847732396F1582CD62DD385ED7ABB0897", 'utf-8')
with zipfile.ZipFile(filename) as file:
for zfile in file.namelist():
print("Extracting " + zfile + " to " + outdir)
file.extract(zfile, pwd=zippw, path=outdir)
print("Files extracted to " + outdir)
exit(0)
# key,iv=generatekey1()
pagesize, key, iv, data = generatekey2(filename)
if pagesize == 0:
print("Unknown key. Aborting")
exit(0)
else:
xml = data[:data.rfind(b">") + 1].decode('utf-8')
if "/" in filename:
path = filename[:filename.rfind("/")]
elif "\\" in filename:
path = filename[:filename.rfind("\\")]
else:
path = ""
path = os.path.join(path, outdir)
if os.path.exists(path):
shutil.rmtree(path)
os.mkdir(path)
else:
os.mkdir(path)
print("Saving ProFile.xml")
file_handle = open(path + os.sep + "ProFile.xml", mode="w")
file_handle.write(xml)
file_handle.close()
root = ET.fromstring(xml)
for child in root:
for item in child:
if "Path" not in item.attrib and "filename" not in item.attrib:
for subitem in item:
wfilename, start, length, rlength, checksums, decryptsize = decryptitem(subitem, pagesize)
if wfilename == "" or start == -1:
continue
decryptfile(key, iv, filename, path, wfilename, start, length, rlength, checksums, decryptsize)
wfilename, start, length, rlength, checksums, decryptsize = decryptitem(item, pagesize)
if wfilename == "" or start == -1:
continue
if child.tag in ["Sahara"]:
decryptsize = rlength
if child.tag in ["Config", "Provision", "ChainedTableOfDigests", "DigestsToSign", "Firmware"]:
length = rlength
if child.tag in ["DigestsToSign", "ChainedTableOfDigests", "Firmware"]:
copy(filename, wfilename, path, start, length, checksums)
else:
decryptfile(key, iv, filename, path, wfilename, start, length, rlength, checksums, decryptsize)
print("\nDone. Extracted files to " + path)
exit(0)
if __name__ == "__main__":
main()