-
-
Save rajkosto/e2b2455d457cc2be82dbb5c85e22d708 to your computer and use it in GitHub Desktop.
| #!/usr/bin/env python3 | |
| # | |
| # Nokia/Alcatel-Lucent router backup configuration tool | |
| # G2425 support added by rajkosto on 20/11/2022 | |
| # XS-2426G-B support added by rajkosto on 28/02/2023 | |
| # Newer dmdb_encryption.cfg support added by rajkosto on 03/10/2026 | |
| # (requires device specific info, generate extfshash with https://gist.github.com/rajkosto/d003f2426310bd62bd9fd32548d04dd5) | |
| # (DMDB files can be converted to/from familiar XML format using https://gist.github.com/rajkosto/737876e062840cfdec2d6ce23d318d0d) | |
| # | |
| # Features: | |
| # - Unpack/repack .cfg files generated from the backup and restore functionnality | |
| # in order to modify the full router configuration | |
| # - Decrypt/encrypt the passwords/secret values present in the configuration | |
| # | |
| # Original author blog post: https://0x41.cf/reversing/2019/10/08/unlocking-nokia-g240wa.html | |
| # | |
| # Released under the MIT License (http://opensource.org/licenses/MIT) | |
| # Copyright (c) Sami Alaoui Kendil (thedroidgeek) | |
| # Copyright (c) Rajko Stojadinovic (rajkosto) | |
| # | |
| import io | |
| import sys | |
| import zlib | |
| import struct | |
| import base64 | |
| import binascii | |
| import datetime | |
| import hashlib | |
| import secrets | |
| big_endian = True | |
| encrypted_cfg = False | |
| def u32(val): | |
| return struct.unpack('>I' if big_endian else '<I', val)[0] | |
| def p32(val): | |
| return struct.pack('>I' if big_endian else '<I', val) | |
| def deriveKey10(serial, progdate, extfshash): | |
| source = (serial + progdate + extfshash).encode() | |
| raw = hashlib.pbkdf2_hmac('sha256', source, b'key10', 100, dklen=64) | |
| return base64.b64encode(raw).decode()[:64] | |
| def normalizeSerial(serial): | |
| if serial.startswith('ALCL') or serial.startswith('NOKA'): | |
| serial = serial[4:] | |
| elif len(serial) != 8 or any(c not in '0123456789abcdefABCDEF' for c in serial): | |
| raise ValueError('serial must be ALCL/NOKA-prefixed or exactly 8 hex digits') | |
| return serial.lower() | |
| def expandProgdate(progdate): | |
| if len(progdate) == 6 and progdate.isdigit(): | |
| return [progdate] | |
| elif len(progdate) == 4 and progdate.isdigit(): | |
| return [progdate + '%02d' % day for day in range(1,32)] | |
| raise ValueError('progdate must be 4 or 6 digits') | |
| def checkendian(cfg): | |
| if (cfg[0:4] == b'\x00\x12\x31\x23' or | |
| cfg[0:4] == b'\x00\x12\x41\x26'): | |
| return True | |
| elif (cfg[0:4] == b'\x23\x31\x12\x00' or | |
| cfg[0:4] == b'\x26\x41\x12\x00'): | |
| return False | |
| else: | |
| return None | |
| class RouterCrypto: | |
| def __init__(self): | |
| from Crypto.Cipher import AES | |
| # key and IV for AES | |
| key = '3D A3 73 D7 DC 82 2E 2A 47 0D EC 37 89 6E 80 D7 2C 49 B3 16 29 DD C9 97 35 4B 84 03 91 77 9E A4' | |
| iv = 'D0 E6 DC CD A7 4A 00 DF 76 0F C0 85 11 CB 05 EA' | |
| # create AES-128-CBC cipher | |
| self.cipher = AES.new(bytes(bytearray.fromhex(key)), AES.MODE_CBC, bytes(bytearray.fromhex(iv))) | |
| def decrypt(self, data): | |
| output = self.cipher.decrypt(data) | |
| # verify and remove PKCS#7 padding | |
| padLen = ord(output[-1:]) | |
| if padLen <= 0 or padLen > 16: #cannot be 0 or > blocksize | |
| return None | |
| padBytes = output[-padLen:] | |
| validPad = all(padByte == padLen for padByte in padBytes) | |
| if validPad: | |
| return output[:-padLen] | |
| else: | |
| return None | |
| def encrypt(self, data): | |
| # add PKCS#7 padding for 128-bit AES | |
| pad_num = (16 - (len(data) % 16)) | |
| data += chr(pad_num).encode() * pad_num | |
| return self.cipher.encrypt(data) | |
| class PKCSPassCrypto(RouterCrypto): | |
| def __init__(self, pkcsPass, pkcsSalt): | |
| from Crypto.Cipher import AES | |
| from hashlib import pbkdf2_hmac | |
| keyLen = 32 #AES-256 | |
| ivLen = 16 #AES blocksize | |
| if not isinstance(pkcsPass, bytes): | |
| pkcsPass = pkcsPass.encode() | |
| pkcs = pbkdf2_hmac('sha256', pkcsPass, pkcsSalt, 10, dklen=keyLen+ivLen) | |
| keyBytes = pkcs[:keyLen] | |
| ivBytes = pkcs[keyLen:] | |
| self.cipher = AES.new(keyBytes, AES.MODE_CBC, ivBytes) | |
| #G2425 and newer config pkcs password | |
| PKCSPasswords = ["S23l7nZm47XyMGs6y6oJpN9CR4nbfIZHJ4VRwp7HcdV6o2YvUmeNYFlz08Otwz78"] | |
| # | |
| # unpack xml from cfg | |
| # | |
| if ((len(sys.argv) == 3 or len(sys.argv) == 6) and sys.argv[1] == '-u'): | |
| # line feed | |
| print('') | |
| # derive device-specific key10 when serial/progdate/extfshash are supplied | |
| serial = None | |
| progdate = None | |
| extfshash = None | |
| derivedKey10 = None | |
| derivedKey10s = {} | |
| if len(sys.argv) == 6: | |
| try: | |
| serial = normalizeSerial(sys.argv[3]) | |
| progdate = sys.argv[4] | |
| extfshash = sys.argv[5] | |
| for currProgdate in expandProgdate(progdate): | |
| currKey = deriveKey10(serial, currProgdate, extfshash) | |
| derivedKey10s[currKey] = currProgdate | |
| PKCSPasswords.append(currKey) | |
| derivedKey10 = next(iter(derivedKey10s)) | |
| except ValueError as e: | |
| print('\ninvalid device info: ' + str(e) + '\n') | |
| exit() | |
| # read the cfg file | |
| cf = open(sys.argv[2], 'rb') | |
| cfg_data = cf.read() | |
| # check cfg file magic and determine endianness | |
| big_endian = checkendian(cfg_data) | |
| if big_endian == None: | |
| # check if config is encrypted | |
| decrypted = None | |
| try: | |
| # decrypt and check validity | |
| decrypted = RouterCrypto().decrypt(cfg_data) | |
| big_endian = checkendian(decrypted) | |
| except ValueError: | |
| pass | |
| # if decryption failed, or still invalid, bail out | |
| if big_endian == None: | |
| print('invalid cfg file/magic :(\n') | |
| exit() | |
| # set decrypted cfg buffer and encryption flag | |
| print('-> encrypted cfg detected') | |
| cfg_data = decrypted | |
| encrypted_cfg = True | |
| # log endianness | |
| if big_endian: | |
| print('-> big endian CPU detected') | |
| else: | |
| print('-> little endian CPU detected') | |
| # get cfg header magic | |
| cfg_magic = u32(cfg_data[0x00:0x04]) | |
| new_header = cfg_magic == 0x124126 | |
| if derivedKey10 is not None and not new_header: | |
| print('\ninvalid cfg file/magic, expected 0x124126 when serial/progdate/extfshash are supplied :(\n') | |
| exit() | |
| # get the size of the compressed data | |
| data_size = u32(cfg_data[0x04:0x08]) | |
| large_header = False | |
| if data_size == 0: | |
| data_size = u32(cfg_data[0x08:0x0C]) | |
| large_header = True | |
| if data_size == 0: | |
| print('\nERROR: config data size is 0!\n') | |
| exit() | |
| # get fw_magic (unknown, could be fw version/compile time, hw serial number, etc.) | |
| fw_magic = 0 | |
| if large_header: | |
| fw_magic = u32(cfg_data[0x20:0x24]) | |
| else: | |
| fw_magic = u32(cfg_data[0x10:0x14]) | |
| print('-> fw_magic = ' + hex(fw_magic)) | |
| # get the compressed data | |
| compressed = [] | |
| if large_header: | |
| compressed = cfg_data[0x28 : 0x28 + data_size] | |
| else: | |
| compressed = cfg_data[0x14 : 0x14 + data_size] | |
| # get the checksum of the compressed data | |
| checksum = 0 | |
| if large_header: | |
| checksum = u32(cfg_data[0x10:0x14]) | |
| else: | |
| checksum = u32(cfg_data[0x08:0x0C]) | |
| # verify the checksum | |
| if (binascii.crc32(compressed) & 0xFFFFFFFF != checksum): | |
| print('\nCRC32 checksum failed :(\n') | |
| exit() | |
| uncomp_size = 0 | |
| if large_header: | |
| uncomp_size = u32(cfg_data[0x18:0x1C]) | |
| else: | |
| uncomp_size = u32(cfg_data[0x0C:0x10]) | |
| # unpack the config | |
| xml_data = None | |
| try: | |
| xml_data = zlib.decompress(compressed) | |
| pkcsPass = None | |
| except zlib.error: | |
| encData = None | |
| pkcsSalt = None | |
| tryPasswords = [] | |
| if compressed[0] == 0xFF: #pkcs encrypted payload | |
| tryPasswords = PKCSPasswords | |
| with io.BytesIO(compressed) as payload: | |
| payload.seek(1) | |
| pkcsSalt = payload.read(8) | |
| encData = payload.read() | |
| for currPass in tryPasswords: | |
| decryptor = PKCSPassCrypto(currPass,pkcsSalt) | |
| compressed = decryptor.decrypt(encData) | |
| if compressed is None: #pkcs padding verification failed, key is wrong | |
| continue | |
| try: | |
| xml_data = zlib.decompress(compressed) | |
| pkcsPass = currPass | |
| if currPass in derivedKey10s: | |
| derivedKey10 = currPass | |
| progdate = derivedKey10s[currPass] | |
| except zlib.error: | |
| pass | |
| if xml_data is None: | |
| if len(tryPasswords): | |
| raise RuntimeError('Exhausted all known encryption passwords') | |
| else: | |
| raise | |
| if len(xml_data) != uncomp_size: | |
| print('WARNING: uncompressed size does not match value in header!') | |
| # output the xml file | |
| out_filename = 'config-%s.xml' % datetime.datetime.now().strftime('%d%m%Y-%H%M%S') | |
| if derivedKey10 is not None: | |
| out_filename = out_filename.replace('.xml','.dmdb') | |
| elif xml_data[0] != ord('<'): | |
| out_filename = out_filename.replace('.xml','.ini') | |
| of = open(out_filename, 'wb') | |
| of.write(xml_data) | |
| print('\nunpacked as: ' + out_filename) | |
| recompInfo = ('-pb' if big_endian else '-pl') | |
| if large_header: | |
| recompInfo += '64' | |
| if new_header: | |
| recompInfo += 'n' | |
| if encrypted_cfg or pkcsPass: | |
| recompInfo += 'e' | |
| if pkcsPass and derivedKey10 is None: | |
| recompInfo += pkcsPass | |
| print('\n# repack with:') | |
| if derivedKey10 is not None: | |
| print('%s %s %s %s %s %s %s\n' % (sys.argv[0], recompInfo, out_filename, hex(fw_magic), serial, progdate, extfshash)) | |
| else: | |
| print('%s %s %s %s\n' % (sys.argv[0], recompInfo, out_filename, hex(fw_magic))) | |
| cf.close() | |
| of.close() | |
| # | |
| # generate cfg from xml | |
| # | |
| elif ((len(sys.argv) == 4 or len(sys.argv) == 7) and (sys.argv[1][:3] == '-pb' or sys.argv[1][:3] == '-pl')): | |
| fw_magic = 0 | |
| try: | |
| # parse hex string | |
| fw_magic = int(sys.argv[3], 16) | |
| # 32-bit check | |
| p32(fw_magic) | |
| except: | |
| print('\ninvalid magic value specified (32-bit hex)\n') | |
| exit() | |
| big_endian = sys.argv[1][:3] == '-pb' | |
| large_header = False | |
| new_header = False | |
| serial = None | |
| progdate = None | |
| extfshash = None | |
| derivedKey10 = None | |
| derivedKey10s = [] | |
| param_len = 3 | |
| if sys.argv[1][3:5] == '64': | |
| large_header = True | |
| param_len += 2 | |
| elif sys.argv[1][3:5] == '32': | |
| large_header = False | |
| param_len += 2 | |
| # 'n' selects the newer 0x124126 header magic | |
| if len(sys.argv[1]) > param_len and sys.argv[1][param_len] == 'n': | |
| new_header = True | |
| large_header = True | |
| param_len += 1 | |
| # serial/progdate/extfshash imply the newer 0x124126 header and key10 | |
| if len(sys.argv) == 7: | |
| try: | |
| serial = normalizeSerial(sys.argv[4]) | |
| progdate = sys.argv[5] | |
| extfshash = sys.argv[6] | |
| derivedKey10s = [deriveKey10(serial,currProgdate,extfshash) for currProgdate in expandProgdate(progdate)] | |
| PKCSPasswords.extend(derivedKey10s) | |
| if len(derivedKey10s) == 1: | |
| derivedKey10 = derivedKey10s[0] | |
| except ValueError as e: | |
| print('\ninvalid device info: ' + str(e) + '\n') | |
| exit() | |
| new_header = True | |
| large_header = True | |
| encrypted_cfg = False | |
| if len(sys.argv[1]) > param_len and sys.argv[1][param_len] == 'e': | |
| encrypted_cfg = True | |
| param_len += 1 | |
| pkcsPass = None | |
| if encrypted_cfg and len(sys.argv[1]) > param_len: | |
| pkcsPass = sys.argv[1][param_len:] | |
| encrypted_cfg = False | |
| elif encrypted_cfg and len(derivedKey10s) > 1: | |
| print('\n4-digit progdate is ambiguous when packing, specify the exact 6-digit progdate\n') | |
| exit() | |
| elif encrypted_cfg and derivedKey10 is not None: | |
| pkcsPass = derivedKey10 | |
| encrypted_cfg = False | |
| out_filename = 'config-%s.cfg' % datetime.datetime.now().strftime('%d%m%Y-%H%M%S') | |
| # read the xml file | |
| xf = open(sys.argv[2], 'rb') | |
| xml_data = xf.read() | |
| xf.close() | |
| # compress using default zlib compression | |
| compressed = zlib.compress(xml_data) | |
| # pkcs encrypt the inner data if needed | |
| extraDecompLen = 1 #non pkcs encrypted configs have +1 to decomp len | |
| if pkcsPass is not None: | |
| extraDecompLen = 0 | |
| with io.BytesIO() as payload: | |
| payload.write(b'\xFF') | |
| pkcsSalt = secrets.token_bytes(8) | |
| payload.write(pkcsSalt) | |
| cryptor = PKCSPassCrypto(pkcsPass,pkcsSalt) | |
| payload.write(cryptor.encrypt(compressed)) | |
| compressed = payload.getvalue() | |
| ## construct the header ## | |
| # magic | |
| cfg_data = p32(0x124126 if new_header else 0x123123) | |
| if large_header: | |
| cfg_data += p32(0) | |
| # size of compressed data | |
| cfg_data += p32(len(compressed)) | |
| if large_header: | |
| cfg_data += p32(0) | |
| # crc32 checksum | |
| cfg_data += p32(binascii.crc32(compressed) & 0xFFFFFFFF) | |
| if large_header: | |
| cfg_data += p32(0) | |
| # size of xml file | |
| cfg_data += p32(len(xml_data) + extraDecompLen) | |
| if large_header: | |
| cfg_data += p32(0) | |
| # fw_magic | |
| cfg_data += p32(fw_magic) | |
| if large_header: | |
| cfg_data += p32(0) | |
| # add the compressed xml | |
| cfg_data += compressed | |
| # encrypt overall file if necessary | |
| if encrypted_cfg: | |
| cfg_data = RouterCrypto().encrypt(cfg_data) | |
| # write the cfg file | |
| of = open(out_filename, 'wb') | |
| of.write(cfg_data) | |
| of.close() | |
| print('\npacked as: ' + out_filename + '\n') | |
| # | |
| # decrypt/encrypt secret value | |
| # | |
| elif (len(sys.argv) == 3 and (sys.argv[1] == '-d' or sys.argv[1] == '-e')): | |
| decrypt_mode = sys.argv[1] == '-d' | |
| if decrypt_mode: | |
| # base64 decode + AES decrypt | |
| print('\ndecrypted: ' + RouterCrypto().decrypt(base64.b64decode(sys.argv[2])).decode('UTF-8') + '\n') | |
| else: | |
| # AES encrypt + base64 encode | |
| print('\nencrypted: ' + base64.b64encode(RouterCrypto().encrypt(sys.argv[2].encode())).decode('UTF-8') + '\n') | |
| else: | |
| print('\n#\n# Nokia/Alcatel-Lucent router backup configuration tool\n#\n') | |
| print('# unpack (cfg to xml)\n') | |
| print(sys.argv[0] + ' -u config.cfg\n') | |
| print('# pack (xml to cfg)\n') | |
| print(sys.argv[0] + ' -pb config.xml 0x13377331 # big endian, no encryption, fw_magic = 0x13377331') | |
| print(sys.argv[0] + ' -pl config.xml 0x13377331 # little endian, ...') | |
| print(sys.argv[0] + ' -pbe config.xml 0x13377331 # big endian, with encryption, ...') | |
| print(sys.argv[0] + ' -ple config.xml 0x13377331 # ...') | |
| print(sys.argv[0] + ' -u dmdb_encryption.cfg SERIAL(HEX) PROGDATE(YYMM) extfshash # 0x124126 header, derive key10') | |
| print(sys.argv[0] + ' -pl64e config.xml 0x48256a95 SERIAL(HEX) PROGDATE(YYMMDD) extfshash # 0x124126 header, derive key10 + PKCS encrypt\n') | |
| print('# decrypt/encrypt secret values within xml (ealgo="ab")\n') | |
| print(sys.argv[0] + ' -d OYdLWUVDdKQTPaCIeTqniA==') | |
| print(sys.argv[0] + ' -e admin\n') |
For a Nokia G1425g-A im presenting this error, seems no one else has encountered somethink like that
fw_magic = 0xfffffffe Traceback (most recent call last): File "C:\Development\Nokia\nokia-router-cfg-tool.py", line 201, in <module> xml_data = zlib.decompress(compressed) zlib.error: Error -3 while decompressing data: unknown compression method
nokia updated its routers. the decryption code does not works. you need to ask the developer to update the code
does anyone successfully tried with G-1425G-B ?
G-1425G-B how does the realfirmware guy unlock them, cant even access a menu where to upload modified bin files or .img files to it
G-1425G-B how does the realfirmware guy unlock them, cant even access a menu where to upload modified bin files or .img files to it
u must use the AdminGPON user and password ALC#FGU and not use the userAdmin
Good morning, colleagues. I’d like to start by commending each of you for your efforts to contribute to the community.
I am reaching out for help with reusing some Nokia ONTs. I need to find the adminGPON credentials, as they have been changed. Does anyone know if this is possible or if there is a way to do it? The credentials on the label don't allow me to do anything—not even download the config file. Could you please guide me? I’m not sure if there is a post or video covering this. Thanks in advance; here are the details of my equipment.
Nombre del dispositivo G-1425G-A
Proveedor Nokia
Número de serie ALCLFCD40B70
Versión de hardware 3FE77771DAAA
Versión de arranque
Bootbase1.1-28-dic-2023--05:42:14
Versión del software 3FE49568IJJJ31(1.2202.331)
Chipset MTK7528
For a Nokia G1425g-A im presenting this error, seems no one else has encountered somethink like that