Created
March 4, 2024 07:25
-
-
Save zainarbani/723d1387bec9e1559de7a1029d08aa91 to your computer and use it in GitHub Desktop.
F670L decrypt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys | |
import argparse | |
import struct | |
import zlib | |
from os import stat | |
from io import BytesIO | |
from hashlib import sha256 | |
from Cryptodome.Cipher import AES | |
"""Magic number constants from ZTE routers""" | |
PAYLOAD_MAGIC = 0x01020304 | |
SIGNATURE_MAGIC = 0x04030201 | |
ZTE_MAGIC = (0x99999999, 0x44444444, 0x55555555, 0xAAAAAAAA) | |
ZTE_IV = b'ZTE%FN$GponNJ025' | |
class zteUtil(): | |
def read_header(infile, little_endian=False): | |
"""expects to be at position 0 of the file, returns size of header""" | |
header_magic = struct.unpack('>4I', infile.read(16)) | |
if header_magic == ZTE_MAGIC: | |
# 128 byte header | |
endian = '<' if little_endian else '>' | |
header = struct.unpack(endian + '28I', infile.read(112)) | |
assert header[2] == 4 | |
header_length = header[13] | |
signed_config_size = header[14] | |
file_size = stat(infile.name).st_size | |
assert header_length + signed_config_size == file_size, "file size does not match header" | |
else: | |
# no extra header so return to start of the file | |
infile.seek(0) | |
return infile.tell() | |
def read_signature(infile): | |
"""expects to be at the start of the signature magic, returns | |
(signature, bytes read)""" | |
signature_header = struct.unpack('>3I', infile.read(12)) | |
signature = b'' | |
if signature_header[0] == SIGNATURE_MAGIC: | |
# _ = signature_header[1] # 0 ? | |
signature_length = signature_header[2] | |
signature = infile.read(signature_length) | |
else: | |
# no signature so return to start of the file | |
infile.seek(0) | |
return signature | |
def read_payload(infile, raise_on_error=True): | |
"""expects to be at the start of the payload magic""" | |
payload_header = struct.unpack('>15I', infile.read(60)) | |
if payload_header[0] != PAYLOAD_MAGIC: | |
if raise_on_error: | |
raise ValueError("Payload header does not start with the payload magic.") | |
else: | |
return None | |
return payload_header | |
def read_payload_type(infile, raise_on_error=True): | |
"""expects to be at the start of the payload magic""" | |
payload_header = zteUtil.read_payload(infile, raise_on_error) | |
return payload_header[1] if payload_header is not None else None | |
def mac_to_str(mac): | |
if not len(mac): | |
return '' | |
if not isinstance(mac, bytes): | |
mac = mac.strip().replace(':','') | |
if len(mac) != 12: | |
raise ValueError("MAC address string has wrong length") | |
mac = bytes.fromhex(mac) | |
if len(mac) != 6: | |
raise ValueError("MAC address has wrong length") | |
return "%02x%02x%02x%02x%02x%02x" % (mac[5], mac[4], mac[3], mac[2], mac[1], mac[0]) | |
class zteXcryptor(): | |
# type 3/4 encryption, AES256CBC with the key/IV set from SHA256 hashes | |
force_same_data_length = False | |
aes_key_str = None | |
aes_iv_str = None | |
def set_key(self, aes_key=None, aes_iv=None): | |
if aes_key is None: | |
self.aes_cipher = None | |
return | |
if isinstance(aes_key, bytes): | |
self.aes_key_str = aes_key.decode() | |
else: | |
self.aes_key_str = aes_key | |
if aes_iv is None: | |
self.aes_iv_str = self.aes_key_str | |
elif isinstance(aes_iv, bytes): | |
self.aes_iv_str = aes_iv.decode() | |
else: | |
self.aes_iv_str = aes_iv | |
key = sha256(self.aes_key_str.encode()).digest() | |
iv = sha256(self.aes_iv_str.encode()).digest() | |
self.aes_cipher = AES.new(key, AES.MODE_CBC, iv[:16]) | |
def read_chunks(self, infile): | |
encrypted_data = BytesIO() | |
total_dec_size = 0 | |
while True: | |
dec_size, chunk_size, more_data = struct.unpack(">3I", infile.read(12)) | |
encrypted_data.write(infile.read(chunk_size)) | |
total_dec_size += dec_size | |
if more_data == 0: | |
break | |
encrypted_data.seek(total_dec_size) | |
return encrypted_data | |
def decrypt(self, infile): | |
data = self.read_chunks(infile) | |
data_size = data.tell() | |
data.seek(0) | |
res = BytesIO() | |
res.write(self.aes_cipher.decrypt(data.read())[:data_size]) | |
res.seek(0) | |
return res | |
class zteCompression(): | |
def decompress(infile): | |
"""decompress a block, return data and crc | |
A 'block' consists of a 12 byte (3x4-byte INT) header and a ZLIB payload | |
HEADER | |
[XXXX] Decompressed length of block (bytes) | |
[XXXX] Compressed length of block (bytes) | |
[XXXX] 0 if last block else cumulative compressed blocks length | |
PAYLOAD | |
[....] ZLIB chunk | |
""" | |
decompressed_data = BytesIO() | |
crc = 0 | |
while True: | |
aes_header = struct.unpack('>3I', infile.read(12)) | |
decompressed_length = aes_header[0] | |
compressed_length = aes_header[1] | |
compressed_chunk = infile.read(compressed_length) | |
crc = zlib.crc32(compressed_chunk, crc) | |
decompressed_chunk = zlib.decompress(compressed_chunk) | |
assert decompressed_length == len(decompressed_chunk) | |
decompressed_data.write(decompressed_chunk) | |
if aes_header[2] == 0: | |
break | |
decompressed_data.seek(0) | |
return (decompressed_data, crc) | |
def main(): | |
"""the main function""" | |
parser = argparse.ArgumentParser(description="Decode config.bin from ZTE Routers", | |
formatter_class=argparse.RawDescriptionHelpFormatter) | |
parser.add_argument("infile", type=argparse.FileType("rb"), | |
help="Encoded configuration file e.g. config.bin") | |
parser.add_argument("outfile", type=argparse.FileType("wb"), | |
help="Output file e.g. config.xml") | |
parser.add_argument("--mac", type=str, default="", | |
help="Router MAC address") | |
parser.add_argument("--sn", type=str, default="", | |
help="Router serial number") | |
args = parser.parse_args() | |
infile = args.infile | |
outfile = args.outfile | |
zteUtil.read_header(infile, True) | |
signature = zteUtil.read_signature(infile).decode() | |
if signature: | |
print("Detected signature: %s" % signature) | |
payload_type = zteUtil.read_payload_type(infile) | |
print("Detected payload type %d" % payload_type) | |
start_pos = infile.tell() | |
matched = False | |
if payload_type == 6: | |
if args.mac is None or args.sn is None: | |
error("mac, sn cannot be null" % len(generated)) | |
print("MAC: %s\nS/N: %s" % (args.mac, args.sn)) | |
user_key = args.sn[4:] + zteUtil.mac_to_str(args.mac) | |
decryptor = zteXcryptor() | |
decryptor.set_key(user_key.encode(), ZTE_IV) | |
infile.seek(start_pos) | |
decrypted = decryptor.decrypt(infile) | |
if zteUtil.read_payload_type(decrypted, raise_on_error=False) is not None: | |
matched = True | |
infile = decrypted | |
if matched is None: | |
error("Failed to decrypt type 6 payload, tried %d generated key(s)!" % len(generated)) | |
return 1 | |
elif payload_type == 0: | |
pass | |
else: | |
error("Unknown payload type %d encountered!" % payload_type) | |
return 1 | |
res, _ = zteCompression.decompress(infile) | |
outfile.write(res.read()) | |
if matched: | |
print("\nSuccessfully decrypted!") | |
return 0 | |
def error(err): | |
print(err, file=sys.stderr) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment