-
-
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 | |
# | |
# 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 checkendian(cfg): | |
if (cfg[0:4] == b'\x00\x12\x31\x23'): | |
return True | |
elif (cfg[0:4] == b'\x23\x31\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 and sys.argv[1] == '-u'): | |
# line feed | |
print('') | |
# read the cfg file | |
cf = open(sys.argv[2], 'rb') | |
cfg_data = cf.read() | |
# check cfg file magic (0x123123) 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 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 | |
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 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 encrypted_cfg or pkcsPass: | |
recompInfo += 'e' | |
if pkcsPass: | |
recompInfo += pkcsPass | |
print('\n# repack with:') | |
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 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 | |
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 | |
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 | |
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(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 # ...\n') | |
print('# decrypt/encrypt secret values within xml (ealgo="ab")\n') | |
print(sys.argv[0] + ' -d OYdLWUVDdKQTPaCIeTqniA==') | |
print(sys.argv[0] + ' -e admin\n') |
Hi there! I have a Nokia G1425 from Telemex (Mexican ISP): Chipset: MTK7528 Hardware: 3FE77771BEAA Software: 3FE49568IJLJ07(1.2402.307) Made in Nov 1, 2024. This one does not let me decode, I get the dreaded: -> little endian CPU detected -> fw_magic = 0xfffffffe Traceback (most recent call last): File "/home/alisi/Downloads/nokia.py", line 137, in xml_data = zlib.decompress(compressed) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ zlib.error: Error -3 while decompressing data: incorrect header check
I understand this is a decoding problem (I use Python and these libraries myself!) - The question here is, is there anything I can do to help the project/community to get this version of the firmware cracked?
I am more than willing to share my config.cfg with someone if needed; the credentials are non-relevant outside Telmex's network anyway: https://drive.google.com/file/d/1NEp0mSZUdCpTH5w9QxykT00Mxmmgn1aj/view?usp=sharing
Thanks in advance! -Alisi
I am getting this exact same error. I guess the new firmware changed something. Any luck? Here is my device info:
Device name
G-1425G-A
Vendor
Nokia
Serial Number:
ALCLFE08B0A0
Hardware Version:
3FE77771BEAA
Boot Version
Bootbase1.1-Jul-02-2024--22:11:45
Software Version:
3FE49568IJLJ07(1.2402.307)
Chipset
MTK7528
Lot Number
Nov 01 2024
PSA: You need pycryptodome
library installed from pip for this script to work! Remove any previously installed crypto libraries.
PSA: You need
pycryptodome
library installed from pip for this script to work! Remove any previously installed crypto libraries.
That is already installed:
(base) cursor@MacBookPro temp % pip show pycryptodome
Name: pycryptodome
Version: 3.22.0
Summary: Cryptographic library for Python
Home-page: https://www.pycryptodome.org
Author: Helder Eijs
Author-email: [email protected]
License: BSD, Public Domain
Location: /Users/cursor/micromamba/lib/python3.9/site-packages
I can run the script with the -D option and get back: decrypted: admin
But when running with the -u option I get an error:
-> little endian CPU detected
-> fw_magic = 0xfffffffe
Traceback (most recent call last):
File "/Users/cursor/temp/nokia-router-cfg-tool.py", line 201, in
xml_data = zlib.decompress(compressed)
zlib.error: Error -3 while decompressing data: incorrect header check
From what I have read this is probably because of the firmware revision of the modem.
Hello, sir @rajkosto . It worked, thanks! One thing though I want to set a static ARP.
When I run
arp -s xxx xxx
, it works, but after a reboot, I lose the changes.Is there any way to persist this ARP?
I tried modifying init.d inside /etc, but /etc is not persisted. I also tried using cron, but nothing worked. If you have any ideas, I'd appreciate it. Thanks!