├── zcu ├── huawei.py ├── fiberhome.py ├── constants.py ├── __init__.py ├── zte.py ├── compression.py ├── known_keys.py ├── xcryptors.py └── round_constants.py ├── config ├── config.bin └── config.xml ├── Indihome Universal Utility.cfg ├── LICENSE └── README.md /zcu/huawei.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/config.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/config.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /zcu/fiberhome.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /zcu/constants.py: -------------------------------------------------------------------------------- 1 | """Konstanta angka magis dari router ZTE""" 2 | 3 | PAYLOAD_MAGIC = 0x01020304 4 | SIGNATURE_MAGIC = 0x04030201 5 | ZTE_MAGIC = (0x99999999, 0x44444444, 0x55555555, 0xAAAAAAAA) 6 | 7 | -------------------------------------------------------------------------------- /zcu/__init__.py: -------------------------------------------------------------------------------- 1 | from . import compression # noqa: F401 2 | from . import constants # noqa: F401 3 | from . import known_keys # noqa: F401 4 | from . import xcryptors # noqa: F401 5 | from . import zte # noqa: F401 6 | from . import fiberhome # noqa: F401 7 | from . import huawei # noqa: F401 8 | -------------------------------------------------------------------------------- /Indihome Universal Utility.cfg: -------------------------------------------------------------------------------- 1 | InsertUsername=user 2 | InsertPassword=user 3 | HideUsernameAndPassword=False 4 | HideMacAndSerial=False 5 | InFileConfigBin=config/config.bin 6 | OutFileConfigXML=config/config.xml 7 | GetMacAddressText=AA:BB:CC:DD:EE:FF 8 | GetSerialNumberText=ZTE123456789 9 | UsernameLoginAdmin=admin 10 | PasswordLoginAdmin=Telkomdso123 11 | UsernamePPPoE=012345678901@telkom.net 12 | PasswordPPPoE=12345678 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 MichaelJorky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Indihome Universal Utility 2 | 3 | > **Peringatan:** :red_circle: Alat ini dibuat khusus untuk keperluan pendidikan dan penelitian. Penulis tidak bertanggung jawab atas segala bentuk penyalahgunaan atau kerusakan yang mungkin timbul dari penggunaan skrip ini. Harap gunakan dengan bijak dan hanya di lingkungan di mana Anda memiliki izin eksplisit. 4 | 5 | Indihome Universal Utility adalah Tool sederhana untuk mengekstrak data konfigurasi pada modem/router indihome seperti mac address, serial number, admin login, user dan password pppoe, acs dll 6 | 7 | # 8 | Fitur yang terdapat pada Aplikasi Indihome Universal Utility.exe 9 | - Auto Download Config 10 | - Decoder Utility 11 | - Encoder Utility 12 | - Support windows x64 only 13 | 14 | # 15 | Tutorial Penggunaan: 16 | 1. Download dan instal python3 (minimum python 3.5): 17 | https://www.python.org/downloads/ 18 | 2. Instal Pycryptodome dan Pycryptodomex: 19 | 20 | ```python -m pip install pycryptodome``` 21 | dan 22 | ```python -m pip install pycryptodomex``` 23 | 24 | Dokumentasi lengkap Pycryptodome: 25 | https://pypi.org/project/pycryptodome/ 26 | 27 | 4. Download "Indihome Universal Utility" via [https://github.com/MichaelJorky/indihome-universal-utility/releases](https://github.com/MichaelJorky/indihome-universal-utility/releases) lalu download zip kemudian silahkan ekstrak dengan WinRAR, maka akan ada folder baru dengan nama "indihome-universal-utility-x.x.x.x" kemudian silahkan buka foldernya dan jalankan aplikasi Indihome Universal Utility.exe 28 | 29 | 5. Perhatikan status koneksi dibagian bawah kiri apakah offline atau online dan pastikan type modem/routernya terdeteksi. 30 | 31 | 6. Pada input data silahkan isi username: user dan password: user/user1234 atau tinggal disesuaikan jika sebelumnya pernah diganti lalu klik "start" (tidak perlu mengisikan mac address dan serial number), lalu klik "stop" untuk membersikan koneksi API. 32 | 33 | 7. Pindah pada bagian tab "Decoder Utility" lalu klik "Start" jika berhasil maka akan didapatkan login username dan password super admin dan serta username beserta password pppoe-nya. 34 | -------------------------------------------------------------------------------- /zcu/zte.py: -------------------------------------------------------------------------------- 1 | """Berbagai fungsi pembantu untuk membaca/menulis konfigurasi ZTE""" 2 | 3 | from io import BytesIO 4 | from os import stat 5 | import struct 6 | 7 | from . import constants 8 | 9 | def read_header(infile, little_endian=False): 10 | """Mengharapkan berada pada posisi 0 dari file, mengembalikan ukuran header""" 11 | header_magic = struct.unpack('>4I', infile.read(16)) 12 | if header_magic == constants.ZTE_MAGIC: 13 | # 128 byte header 14 | endian = '<' if little_endian else '>' 15 | header = struct.unpack(endian + '28I', infile.read(112)) 16 | assert header[2] == 4 17 | header_length = header[13] 18 | signed_config_size = header[14] 19 | file_size = stat(infile.name).st_size 20 | assert header_length + signed_config_size == file_size, "ukuran file tidak cocok dengan header" 21 | else: 22 | # tidak ada header tambahan sehingga kembali ke awal file 23 | infile.seek(0) 24 | return infile.tell() 25 | 26 | 27 | def read_signature(infile): 28 | """Mengharapkan berada di awal tanda tangan magic, mengembalikan 29 | (tanda tangan, byte yang dibaca)""" 30 | signature_header = struct.unpack('>3I', infile.read(12)) 31 | signature = b'' 32 | if signature_header[0] == constants.SIGNATURE_MAGIC: 33 | # _ = signature_header[1] # 0 ? 34 | signature_length = signature_header[2] 35 | signature = infile.read(signature_length) 36 | else: 37 | # tidak ada tanda tangan sehingga kembali ke awal file 38 | infile.seek(0) 39 | return signature 40 | 41 | 42 | def read_payload(infile, raise_on_error=True): 43 | """Mengharapkan berada di awal payload magic""" 44 | payload_header = struct.unpack('>15I', infile.read(60)) 45 | if payload_header[0] != constants.PAYLOAD_MAGIC: 46 | if raise_on_error: 47 | raise ValueError("Header payload tidak dimulai dengan payload magic.") 48 | else: 49 | return None 50 | return payload_header 51 | 52 | 53 | def read_payload_type(infile, raise_on_error=True): 54 | """Mengharapkan berada di awal payload magic""" 55 | payload_header = read_payload(infile, raise_on_error) 56 | return payload_header[1] if payload_header is not None else None 57 | 58 | 59 | # TODO: memisahkan fungsionalitas 'add_signature' 60 | def add_header(payload, signature, version, include_header=False, little_endian=False): 61 | """Membuat payload 'penuh' dari (header), tanda tangan, dan payload""" 62 | full_payload = BytesIO() 63 | signature_length = len(signature) 64 | 65 | payload_data = payload.read() 66 | 67 | if include_header: 68 | full_payload_length = len(payload_data) 69 | if signature_length > 0: 70 | full_payload_length += 12 + signature_length 71 | full_payload.write(struct.pack('>4I', *constants.ZTE_MAGIC)) 72 | header = [ 73 | 0, 0, 4, 0, 74 | 0, 0, 0, 0, 75 | 0, 0, 0, 64, 76 | version, 128, full_payload_length, 0, 77 | 0, 0, 0, 0, 78 | 0, 0, 0, 0, 79 | 0, 0, 0, 0, 80 | ] 81 | endian = '<' if little_endian else '>' 82 | full_payload.write(struct.pack(endian + '28I', *header)) 83 | 84 | if signature_length > 0: 85 | signature_header = [ 86 | constants.SIGNATURE_MAGIC, 87 | 0, 88 | signature_length, 89 | ] 90 | full_payload.write(struct.pack('>3I', *signature_header)) 91 | full_payload.write(signature) 92 | 93 | full_payload.write(payload_data) 94 | full_payload.seek(0) 95 | 96 | return full_payload 97 | -------------------------------------------------------------------------------- /zcu/compression.py: -------------------------------------------------------------------------------- 1 | """Fungsi pembantu kompresi dan dekompresi""" 2 | 3 | import struct 4 | import zlib 5 | from io import BytesIO 6 | 7 | from . import constants 8 | 9 | 10 | def decompress(infile): 11 | """Mendekompresi sebuah blok, mengembalikan data dan crc 12 | Sebuah 'blok' terdiri dari header 12 byte (3x4-byte INT) dan payload ZLIB 13 | HEADER 14 | [XXXX] Panjang blok yang telah didekompresi (bytes) 15 | [XXXX] Panjang blok yang terkompresi (bytes) 16 | [XXXX] 0 jika blok terakhir selain itu kumulatif panjang blok terkompresi 17 | PAYLOAD 18 | [....] Potongan ZLIB 19 | """ 20 | decompressed_data = BytesIO() 21 | crc = 0 22 | while True: 23 | aes_header = struct.unpack('>3I', infile.read(12)) 24 | decompressed_length = aes_header[0] 25 | compressed_length = aes_header[1] 26 | compressed_chunk = infile.read(compressed_length) 27 | crc = zlib.crc32(compressed_chunk, crc) 28 | decompressed_chunk = zlib.decompress(compressed_chunk) 29 | assert decompressed_length == len(decompressed_chunk) 30 | decompressed_data.write(decompressed_chunk) 31 | if aes_header[2] == 0: 32 | break 33 | decompressed_data.seek(0) 34 | return (decompressed_data, crc) 35 | 36 | 37 | def compress_helper(infile, chunk_size): 38 | """Fungsi pembantu kompresi, mengonsumsi segmen-segmen ukuran chunk_size dari infile""" 39 | # panjang terkompresi kumulatif termasuk header payload 60-byte 40 | # ini adalah jumlah kumulatif byte yang terkompresi TIDAK TERMASUK blok terakhir 41 | cumulative_compressed_length = 60 42 | total_uncompressed_length = 0 43 | crc = 0 44 | 45 | compressed_data = BytesIO() 46 | while True: 47 | data = infile.read(chunk_size) 48 | uncompressed_length = len(data) 49 | if uncompressed_length == 0: 50 | break 51 | 52 | total_uncompressed_length += uncompressed_length 53 | 54 | compressed_chunk = zlib.compress(data, zlib.Z_BEST_COMPRESSION) 55 | crc = zlib.crc32(compressed_chunk, crc) & 0xffffffff 56 | 57 | if uncompressed_length < chunk_size: 58 | more_chunks = 0 59 | else: 60 | # increment cumulative length if not last block 61 | cumulative_compressed_length += len(compressed_chunk) + 12 62 | more_chunks = cumulative_compressed_length 63 | 64 | chunk_header = struct.pack('>3I', 65 | uncompressed_length, 66 | len(compressed_chunk), 67 | more_chunks) 68 | 69 | compressed_data.write(chunk_header) 70 | compressed_data.write(compressed_chunk) 71 | 72 | compressed_data.seek(0) 73 | stats = { 74 | 'crc': crc, 75 | 'uncompressed_size': total_uncompressed_length, 76 | 'compressed_size': cumulative_compressed_length, 77 | } 78 | 79 | if chunk_size < 65536: # ingin ukuran terkompresi penuh di header dalam beberapa kasus 80 | stats['compressed_size'] += len(compressed_chunk) + 12 81 | 82 | return (compressed_data, stats) 83 | 84 | 85 | def compress(infile, chunk_size): 86 | """Mengompres dan menambahkan header 87 | 88 | Sebuah 'blok' terdiri dari header 60 byte (15x4-byte INT) diikuti oleh 89 | >=1 PAYLOAD section(s). 90 | 91 | HEADER 92 | [XXXX] Angka magis '0x04030201' 93 | [XXXX] Tipe payload, 0 = ZLIB 94 | [XXXX] Total panjang yang sudah didekompresi 95 | [XXXX] Ukuran terkompresi kumulatif* (*tidak selalu) 96 | [XXXX] Ukuran chunk yang telah didekompresi 97 | [XXXX] CRC dari data terkompresi 98 | [XXXX] CRC dari 24 byte pertama header 99 | [XXXX....] 32 byte padding 100 | PAYLOAD 101 | HEADER 102 | 12 byte header 103 | ZLIB 104 | payload byte variabel 105 | """ 106 | compressed_data, stats = compress_helper(infile, chunk_size) 107 | 108 | header = struct.pack('>6I', 109 | constants.PAYLOAD_MAGIC, 110 | 0, # tidak ada enkripsi, hanya kompresi zlib 111 | stats['uncompressed_size'], 112 | stats['compressed_size'], 113 | chunk_size, 114 | stats['crc']) 115 | payload = BytesIO() 116 | payload.write(header) 117 | payload.write(struct.pack('>I', zlib.crc32(header) & 0xffffffff)) 118 | payload.write(struct.pack('>8I', *(0, 0, 0, 0, 0, 0, 0, 0))) 119 | payload.write(compressed_data.read()) 120 | payload.seek(0) 121 | 122 | return payload 123 | -------------------------------------------------------------------------------- /zcu/known_keys.py: -------------------------------------------------------------------------------- 1 | """Kunci enkripsi yang diketahui untuk file config.bin router ZTE""" 2 | 3 | # Elemen pertama adalah kunci, yang lain adalah awal dari tanda tangan 4 | KNOWN_KEYS = { 5 | "MIK@0STzKpB%qJZe": ["zxhn h118n e"], 6 | "MIK@0STzKpB%qJZf": ["zxhn h118n v"], 7 | "402c38de39bed665": ["zxhn h267a"], 8 | "Q#Zxn*x3kVLc": ["zxhn h168n v2"], 9 | # karena bug, aslinya "Wj%2$CjM" 10 | "Wj": ["zxhn h298n"], 11 | "m8@96&ZG3Nm7N&Iz": ["zxhn h298a"], 12 | "GrWM2Hz<vz&f^5": ["zxhn h108n"], 13 | "GrWM3Hz<vz&f^9": ["zxhn h168n v3", "zxhn h168n h"], 14 | "Renjx%2$CjM": ["zxhn h208n", "zxv10 h201l"], 15 | "tHG@Ti&GVh@ql3XN": ["zxhn h267n"], 16 | # tidak yakin, mungkin terkait dengan H108N 17 | "SDEwOE5WMi41Uk9T": ["TODO"] 18 | } 19 | 20 | 21 | def find_key(signature): 22 | signature = signature.lower() 23 | for key, sigs in KNOWN_KEYS.items(): 24 | for sig in sigs: 25 | if signature.startswith(sig): 26 | return key 27 | return None 28 | 29 | 30 | def get_all_keys(): 31 | return KNOWN_KEYS.keys() 32 | 33 | KNOWN_MODELS = ["H268Q", "H298Q", "H188A", "H288A"] 34 | 35 | def get_all_models(): 36 | return KNOWN_MODELS 37 | 38 | def mac_to_str(mac): 39 | if not len(mac): 40 | return '' 41 | if not isinstance(mac, bytes): 42 | mac = mac.strip().replace(':','') 43 | if len(mac) != 12: 44 | raise ValueError("String alamat MAC memiliki panjang yang salah") 45 | mac = bytes.fromhex(mac) 46 | if len(mac) != 6: 47 | raise ValueError("Alamat MAC memiliki panjang yang salah") 48 | 49 | return "%02x:%02x:%02x:%02x:%02x:%02x" % (mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]) 50 | 51 | def tagparams_keygen(params, key_prefix='Mcd5c46e', iv_prefix='G21b667b'): 52 | if hasattr(params, 'key_prefix'): 53 | key_prefix = params.key_prefix 54 | if hasattr(params, 'iv_prefix'): 55 | iv_prefix = params.iv_prefix 56 | 57 | try: 58 | macStr = mac_to_str(params.mac) 59 | key = params.longPass + params.serial + key_prefix 60 | iv = iv_prefix + macStr + params.longPass 61 | return (key, iv, "tagparams: mac='%s', serial='%s', longPass='%s'" % (macStr, params.serial, params.longPass)) 62 | except AttributeError: 63 | return () 64 | 65 | def serial_keygen(params, key_prefix='8cc72b05705d5c46', iv_prefix='667b02a85c61c786'): 66 | if hasattr(params, 'key_prefix'): 67 | key_prefix = params.key_prefix 68 | if hasattr(params, 'iv_prefix'): 69 | iv_prefix = params.iv_prefix 70 | 71 | try: 72 | key = key_prefix + params.serial 73 | iv = iv_prefix + params.serial 74 | return (key, iv, "serial: '%s'" % params.serial) 75 | except AttributeError: 76 | return () 77 | 78 | def signature_keygen(params, key_suffix='Key02721401', iv_suffix='Iv02721401'): 79 | if hasattr(params, 'key_suffix'): 80 | key_suffix = params.key_suffix 81 | if hasattr(params, 'iv_suffix'): 82 | iv_suffix = params.iv_suffix 83 | 84 | try: 85 | nospaces = params.signature.replace(' ', '') 86 | key = nospaces + key_suffix 87 | iv = nospaces + iv_suffix 88 | return (key, iv, "signature: '%s'" % params.signature) 89 | except AttributeError: 90 | return () 91 | 92 | # Elemen pertama adalah fungsi yang menghasilkan kunci, yang kedua adalah array awalan tanda tangan yang cocok 93 | KNOWN_KEYGENS = { 94 | (lambda p : tagparams_keygen(p)): ["H288A"], 95 | (lambda p : serial_keygen(p)): ["ZXHN H298A"], 96 | (lambda p : signature_keygen(p)): ["ZXHN H168N V3.5"], 97 | (lambda p : signature_keygen(p, key_suffix='Key02710010', iv_suffix='Iv02710010')): ["ZXHN H298Q", "ZXHN H268Q"], 98 | (lambda p : signature_keygen(p, key_suffix='Key02710001', iv_suffix='Iv02710001')): ["H188A", "H288A"], 99 | (lambda p : signature_keygen(p, key_suffix='Key02660004', iv_suffix='Iv02660004')): ["H196Q"], 100 | (lambda p : signature_keygen(p, key_suffix='8cc72b05705d5c46f412af8cbed55aa', iv_suffix='667b02a85c61c786def4521b060265e')): ["ZXHN F450(EPON ONU)"], 101 | 102 | } 103 | 104 | def run_keygen(params): 105 | for gen, sigs in KNOWN_KEYGENS.items(): 106 | matching = False 107 | for sig in sigs: 108 | if params.signature.lower().startswith(sig.lower()): 109 | matching = True 110 | break 111 | if matching: 112 | genResult = gen(params) 113 | if len(genResult): 114 | return genResult 115 | return None 116 | 117 | def run_all_keygens(params): 118 | outArr = [] 119 | for gen in KNOWN_KEYGENS.keys(): 120 | genResult = gen(params) 121 | if len(genResult): 122 | outArr.append(genResult) 123 | 124 | return outArr 125 | 126 | def run_any_keygen(params, wanted): 127 | keygened = run_keygen(params) 128 | if keygened is not None: 129 | return keygened 130 | 131 | # tidak ada kecocokan tanda tangan yang ditemukan dalam keygen, temukan keygen generik dari jenis yang diinginkan dan gunakan itu 132 | allgens = run_all_keygens(params) 133 | for gen in allgens: 134 | if gen[2].startswith(wanted): 135 | return gen 136 | 137 | # seharusnya tidak sampai ke sini selama wanted adalah tipe yang ada 138 | return None 139 | -------------------------------------------------------------------------------- /zcu/xcryptors.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from io import BytesIO 3 | from hashlib import sha256 4 | 5 | from Cryptodome.Cipher import AES 6 | 7 | from zcu.constants import PAYLOAD_MAGIC 8 | 9 | class Xcryptor(): 10 | """Enkripsi Tipe 2 Standar""" 11 | aes_cipher = None 12 | force_same_data_length = True 13 | 14 | def __init__(self, aes_key=None, chunk_size=65536, include_unencrypted_length=False): 15 | self.chunk_size = chunk_size 16 | self.include_unencrypted_length = include_unencrypted_length 17 | self.set_key(aes_key) 18 | 19 | def set_key(self, aes_key): 20 | if aes_key is None: 21 | self.aes_cipher = None 22 | return 23 | 24 | if not isinstance(aes_key, bytes): 25 | aes_key = aes_key.encode() 26 | 27 | aes_key = aes_key.ljust(16, b"\0")[:16] 28 | self.aes_cipher = AES.new(aes_key, AES.MODE_ECB) 29 | 30 | def read_chunks(self, infile): 31 | """membaca blok yang terenkripsi 32 | Sebuah 'blok' terdiri dari header 12 byte (3x4-byte INT) dan payload AES 33 | HEADER 34 | [XXXX] Panjang terdekripsi 35 | [XXXX] Panjang terenkripsi 36 | [XXXX] 0 37 | PAYLOAD 38 | [....] Chunks ZLIB 39 | """ 40 | encrypted_data = BytesIO() 41 | total_dec_size = 0 42 | while True: 43 | chunk_size, dec_size, more_chunks = struct.unpack(">3I", infile.read(12)) 44 | encrypted_data.write(infile.read(chunk_size)) 45 | total_dec_size += dec_size 46 | if more_chunks == 0: # tanda "lanjut" tidak diatur 47 | break 48 | encrypted_data.seek(total_dec_size) 49 | return encrypted_data 50 | 51 | def decrypt(self, infile): 52 | data = self.read_chunks(infile) 53 | data_size = data.tell() 54 | data.seek(0) 55 | res = BytesIO() 56 | res.write(self.aes_cipher.decrypt(data.read())[:data_size]) 57 | res.seek(0) 58 | return res 59 | 60 | def create_header(self): 61 | unencrypted_length_to_use = 0 62 | if self.include_unencrypted_length: 63 | unencrypted_length_to_use = self.unencrypted_data_length 64 | if self.force_same_data_length: 65 | unencrypted_length_to_use = self.encrypted_data_length; 66 | 67 | header = struct.pack( 68 | ">6I", 69 | PAYLOAD_MAGIC, 70 | 2, # aes128 dalam mode ECB 71 | unencrypted_length_to_use, 72 | self.encrypted_data_length + 60 + 12, 73 | self.chunk_size, 74 | 0) 75 | return header 76 | 77 | def encrypt(self, infile): 78 | """enkripsi dan tambahkan header 79 | 80 | Sebuah 'blok' terdiri dari header 60 byte (15x4-byte INT) diikuti oleh 81 | satu PAYLOAD section. 82 | 83 | HEADER 84 | [XXXX] Nomor Sihir '0x01020304' 85 | [XXXX] Tipe Payload, 2 = AES128ECB, 3 = AES256CBC(IV==Key), 4 = AES256CBC(IV!=Key) 86 | [XXXX] Panjang tidak terenkripsi 87 | [XXXX] ukuran 'blok' (termasuk header) 88 | [XXXX] ukuran Chunks 89 | [XXXX....] 40 byte padding 90 | PAYLOAD 91 | HEADER 92 | 12 byte header 93 | AES 94 | 'chunk size' payload 95 | """ 96 | 97 | data = infile.read() 98 | 99 | unencrypted_data_length = len(data) 100 | self.unencrypted_data_length = unencrypted_data_length 101 | 102 | # diisi hingga perataan 16 byte 103 | if unencrypted_data_length % 16 > 0: 104 | data = data + (16 - unencrypted_data_length % 16)*b"\0" 105 | 106 | encrypted_data = self.aes_cipher.encrypt(data) 107 | encrypted_data_length = len(encrypted_data) 108 | self.encrypted_data_length = encrypted_data_length 109 | 110 | header = self.create_header() 111 | 112 | result = BytesIO() 113 | result.write(header) 114 | # 36 byte padding 115 | result.write(struct.pack(">9I", *(9 * [0]))) 116 | # mini header untuk payload aes 117 | aes_header = struct.pack( 118 | ">3I", 119 | *(encrypted_data_length if self.force_same_data_length else unencrypted_data_length, 120 | encrypted_data_length, 121 | 0) 122 | ) 123 | result.write(aes_header) 124 | result.write(encrypted_data) 125 | result.seek(0) 126 | return result 127 | 128 | 129 | class CBCXcryptor(Xcryptor): 130 | # enkripsi tipe 3/4, AES256CBC dengan kunci/IV yang ditetapkan dari hash SHA256 131 | force_same_data_length = False 132 | aes_key_str = None 133 | aes_iv_str = None 134 | 135 | def set_key(self, aes_key=None, aes_iv=None): 136 | if aes_key is None: 137 | self.aes_cipher = None 138 | return 139 | 140 | if isinstance(aes_key, bytes): 141 | self.aes_key_str = aes_key.decode() 142 | else: 143 | self.aes_key_str = aes_key 144 | 145 | if aes_iv is None: 146 | self.aes_iv_str = self.aes_key_str 147 | elif isinstance(aes_iv, bytes): 148 | self.aes_iv_str = aes_iv.decode() 149 | else: 150 | self.aes_iv_str = aes_iv 151 | 152 | key = sha256(self.aes_key_str.encode()).digest() 153 | iv = sha256(self.aes_iv_str.encode()).digest() 154 | self.aes_cipher = AES.new(key, AES.MODE_CBC, iv[:16]) 155 | 156 | def read_chunks(self, infile): 157 | encrypted_data = BytesIO() 158 | total_dec_size = 0 159 | while True: 160 | dec_size, chunk_size, more_data = struct.unpack(">3I", infile.read(12)) 161 | encrypted_data.write(infile.read(chunk_size)) 162 | total_dec_size += dec_size 163 | if more_data == 0: 164 | break 165 | encrypted_data.seek(total_dec_size) 166 | return encrypted_data 167 | 168 | def create_header(self): 169 | header = struct.pack( 170 | ">6I", 171 | PAYLOAD_MAGIC, 172 | 3 if (self.aes_key_str == self.aes_iv_str) else 4, # aes dalam mode CBC 173 | self.encrypted_data_length if self.include_unencrypted_length else 0, 174 | 0, 175 | 0, 176 | 0) 177 | return header 178 | -------------------------------------------------------------------------------- /zcu/round_constants.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import re 3 | import struct 4 | import sys 5 | import zlib 6 | 7 | from argparse import ArgumentParser, ArgumentTypeError, FileType 8 | from io import BytesIO 9 | 10 | try: 11 | from Crypto.Cipher import AES 12 | except: 13 | print('Perpustakaan "pycryptodome" tidak ditemukan.', file=sys.stderr) 14 | print('Silakan install menggunakan "pip install pycryptodome" sebelum menjalankan alat ini.', file=sys.stderr) 15 | exit(1) 16 | 17 | # Putar kanan 32-bit 18 | def rotr32(a, c): 19 | return ((a >> c) | (a << (32 - c))) & 0xFFFFFFFF 20 | 21 | ROUND_CONSTANTS = [ 22 | 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 23 | 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 24 | 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 25 | 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 26 | 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 27 | 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 28 | 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 29 | 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 30 | ] 31 | 32 | def _sha256_raw_digest(message): 33 | """Memproses pesan SHA-256 yang sudah dipad""" 34 | digest = [ 35 | 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 36 | 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, 37 | ] 38 | for chunk in range(0, len(message), 64): 39 | chunk = message[chunk : chunk + 64] 40 | w = list(struct.unpack('>' + 'I' * 16, chunk)) 41 | for i in range(16, 64): 42 | s0 = rotr32(w[-15], 7) ^ rotr32(w[-15], 18) ^ (w[-15] >> 3) 43 | s1 = rotr32(w[-2], 17) ^ rotr32(w[-2], 19) ^ (w[-2] >> 10) 44 | w.append((w[-16] + s0 + w[-7] + s1) & 0xFFFFFFFF) 45 | a, b, c, d, e, f, g, h = digest 46 | for r_w, r_k in zip(w, ROUND_CONSTANTS): 47 | S1 = rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25) 48 | ch = (e & f) ^ ((e ^ 0xFFFFFFFF) & g) 49 | temp1 = (h + S1 + ch + r_k + r_w) 50 | S0 = rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22) 51 | maj = (a & b) ^ (a & c) ^ (b & c) 52 | temp2 = (S0 + maj) 53 | h = g 54 | g = f 55 | f = e 56 | e = (d + temp1) & 0xFFFFFFFF 57 | d = c 58 | c = b 59 | b = a 60 | a = (temp1 + temp2) & 0xFFFFFFFF 61 | digest = [(x + y) & 0xFFFFFFFF for x, y in zip(digest, (a, b, c, d, e, f, g, h))] 62 | return struct.pack('>' + 'I' * 8, *digest) 63 | 64 | def buggy_sha256(message): 65 | """ 66 | Fungsi ini mengimplementasikan fungsi SHA-256 yang bermasalah yang tersedia di 67 | https://github.com/ilvn/SHA256/blob/d8d69dbfeeb68f31e74f8e24971332e996eed76b/mark2/sha256.c, 68 | pada komit tertentu. 69 | 70 | Fungsi ini digunakan oleh router ZTE Z3600P dalam perpustakaan libsha256.so, untuk 71 | mendapatkan kunci enkripsi dan IV konfigurasi. 72 | """ 73 | last_chunk_len = len(message) % 64 74 | if last_chunk_len <= 55: 75 | return hashlib.sha256(message).digest() 76 | packed_len = struct.pack('>Q', 8 * len(message)) 77 | if last_chunk_len == 56: 78 | return _sha256_raw_digest(message + packed_len) 79 | message += b'\x80' + b'\x00' * (64 - last_chunk_len - 1) 80 | message += message[-64 : -8] + packed_len 81 | return _sha256_raw_digest(message) 82 | 83 | def chunk_reader(fin): 84 | has_next = True 85 | while has_next: 86 | header = fin.read(12) 87 | if len(header) != 12: 88 | raise IOError('Gagal membaca header chunk terenkripsi') 89 | unpacked_size, packed_size, has_next = struct.unpack('>III', header) 90 | chunk = fin.read(packed_size) 91 | if len(chunk) != packed_size: 92 | raise IOError('Gagal membaca konten chunk terenkripsi') 93 | yield chunk 94 | 95 | def parse_serial(serial): 96 | serial = serial.upper() 97 | if not re.match(r'^ZTE[A-Z0-9]{8,32}$', serial): 98 | raise ArgumentTypeError('Serial tidak valid') 99 | return serial 100 | 101 | def parse_mac(mac): 102 | mac = re.sub(r'[ :-]', '', mac).lower() 103 | if not re.match(r'^[0-9a-f]{12}$', mac): 104 | raise ArgumentTypeError('Alamat MAC tidak valid') 105 | return ':'.join([mac[0:2], mac[2:4], mac[4:6], mac[6:8], mac[8:10], mac[10:12]]) 106 | 107 | def parse_password(pwd): 108 | if len(pwd) != 32: 109 | raise ArgumentTypeError('Kata sandi tidak valid - harus terdiri dari 32 karakter') 110 | return pwd 111 | 112 | parser = ArgumentParser( 113 | description='Enkoder/decoder konfigurasi ZTE H3600P', 114 | ) 115 | parser.add_argument('-e', '--encode', help='Konversi dari XML ke biner.', action='store_true') 116 | parser.add_argument('-d', '--decode', help='Konversi dari biner ke XML.', action='store_true') 117 | 118 | parser.add_argument('-s', '--serial', help="Nomor serial perangkat", type=parse_serial) 119 | parser.add_argument('-m', '--mac', help="Alamat MAC perangkat", type=parse_mac) 120 | parser.add_argument('-p', '--password', help="Frasa sandi enkripsi perangkat", type=parse_password) 121 | 122 | parser.add_argument('input', help='File masukan. Default ke stdin.', nargs='?', type=FileType('rb'), default=sys.stdin.buffer) 123 | parser.add_argument('output', help='File keluaran. Default ke stdout.', nargs='?', type=FileType('wb'), default=sys.stdout.buffer) 124 | 125 | parser.add_argument('--decrypt-only', help='Hanya lakukan dekripsi, tanpa dekompresi.', action='store_true') 126 | parser.add_argument('--compress-only', help='Hanya kompres, tanpa enkripsi.', action='store_true') 127 | 128 | args = parser.parse_args() 129 | 130 | if not (args.encode ^ args.decode): 131 | print('Harus melakukan enkode atau dekode.', file=sys.stderr) 132 | exit(1) 133 | 134 | if args.decode: 135 | if not args.password or not args.mac or not args.serial: 136 | print('Serial, MAC, atau kata sandi hilang yang diperlukan untuk dekripsi', file=sys.stderr) 137 | exit(0) 138 | 139 | key = args.password + args.serial + 'Mcd5c46e' 140 | key = buggy_sha256(key.encode('ascii')) 141 | 142 | iv = 'G21b667b' + args.mac + args.password 143 | iv = buggy_sha256(iv.encode('ascii'))[:16] 144 | 145 | print('Kunci yang dihasilkan:', file=sys.stderr) 146 | print(f' - Kunci AES: {key.hex()}', file=sys.stderr) 147 | print(f' - IV AES: {iv.hex()}', file=sys.stderr) 148 | 149 | print('Dekripsi dimulai', file=sys.stderr) 150 | 151 | header = args.input.read(0x3C) 152 | if len(header) != 0x3C: 153 | print('Tidak dapat membaca header file terenkripsi', file=sys.stderr) 154 | exit(1) 155 | 156 | magic, fmt = struct.unpack('>II', header[0:8]) 157 | if magic != 0x01020304 or fmt != 4: 158 | print('Header file terenkripsi salah', file=sys.stderr) 159 | exit(1) 160 | 161 | compressed_config = args.output if args.decrypt_only else BytesIO() 162 | try: 163 | for chunk in chunk_reader(args.input): 164 | print(f' - Chunk: {len(chunk)} byte', file=sys.stderr) 165 | chunk = AES.new(key, AES.MODE_CBC, iv=iv).decrypt(chunk) 166 | compressed_config.write(chunk) 167 | except Exception as e: 168 | print(f'Kesalahan saat mendekripsi file: {e}', file=sys.stderr) 169 | exit(1) 170 | 171 | if not args.decrypt_only: 172 | compressed_config.seek(0) 173 | 174 | print('Dekompresi dimulai', file=sys.stderr) 175 | 176 | header = compressed_config.read(0x3C) 177 | if len(header) != 0x3C: 178 | print('Tidak dapat membaca header file terkompresi', file=sys.stderr) 179 | exit(1) 180 | 181 | magic, fmt, dec_size, _, _, correct_crc, header_crc = struct.unpack('>IIIIIII', header[0:28]) 182 | if magic != 0x01020304 or fmt != 0: 183 | print('Header file terkompresi salah. Apakah kunci AES benar?', file=sys.stderr) 184 | exit(1) 185 | 186 | if zlib.crc32(header[0:24]) != header_crc: 187 | print('CRC header file terkompresi buruk. Apakah kunci AES benar?', file=sys.stderr) 188 | exit(1) 189 | 190 | total_size = 0 191 | calculated_crc = 0 192 | try: 193 | for chunk in chunk_reader(compressed_config): 194 | print(f' - Chunk: {len(chunk)} byte', file=sys.stderr) 195 | calculated_crc = zlib.crc32(chunk, calculated_crc) 196 | chunk = zlib.decompress(chunk) 197 | total_size += len(chunk) 198 | args.output.write(chunk) 199 | except Exception as e: 200 | print(f'Kesalahan saat mendekompresi file: {e}', file=sys.stderr) 201 | exit(1) 202 | 203 | print(f'Panjang yang diharapkan: {dec_size}, panjang yang diproses: {total_size}', file=sys.stderr) 204 | if total_size != dec_size: 205 | print('Dekompresi gagal - ukuran tidak sesuai', file=sys.stderr) 206 | exit(1) 207 | 208 | print(f'CRC yang diharapkan: {correct_crc:08x}, CRC yang dihitung: {calculated_crc:08x}', file=sys.stderr) 209 | if correct_crc != calculated_crc: 210 | print('Dekompresi gagal - CRC buruk', file=sys.stderr) 211 | exit(1) 212 | else: 213 | print('Kompresi dimulai', file=sys.stderr) 214 | 215 | plain = args.input.read() 216 | compressed = zlib.compress(plain, level=9) 217 | print(f'Dikompresi dari {len(plain)} menjadi {len(compressed)} byte', file=sys.stderr) 218 | 219 | calculated_crc = zlib.crc32(compressed) 220 | print(f'CRC data: {calculated_crc:08x}', file=sys.stderr) 221 | 222 | header = struct.pack('>IIIIII', 223 | 0x01020304, # Magic (diperiksa) 224 | 0, # Versi (diperiksa) 225 | len(plain), # Panjang tidak terkompresi (diabaikan) 226 | len(compressed),# Panjang terkompresi (diabaikan) 227 | len(plain), # Memori yang diperlukan per chunk (digunakan untuk malloc) 228 | calculated_crc # CRC data (diperiksa) 229 | ) 230 | 231 | header_crc = zlib.crc32(header) 232 | print(f'CRC header: {header_crc:08x}', file=sys.stderr) 233 | header += struct.pack('>I', header_crc) + b'\x00' * (0x3C - 28) 234 | 235 | chunk = struct.pack('>III', len(plain), len(compressed), 0) + compressed 236 | 237 | result = header + chunk 238 | 239 | args.output.write(result) 240 | 241 | print('Operasi berhasil', file=sys.stderr) 242 | --------------------------------------------------------------------------------