diff --git a/Tools/hg8145v5_ntp_password.py b/Tools/hg8145v5_ntp_password.py new file mode 100644 index 00000000..4b6fe7a9 --- /dev/null +++ b/Tools/hg8145v5_ntp_password.py @@ -0,0 +1,392 @@ +#!/usr/bin/env python3 +""" +Huawei HG8145V5 password management and recovery tool. + +The ISP (Megacable) changes admin passwords daily via TR-069. +This tool provides three methods to regain access: + +Method 1 - set-password (RECOMMENDED, no NTP needed): + Patches the config XML to set a permanent custom password + and disables TR-069 so the ISP cannot overwrite it. + +Method 2 - login (use cracked/known password): + The original admin password is 'admintelecom' (cracked from config). + Works if TR-069 hasn't changed it yet. + +Method 3 - ntp-server (fallback): + Runs a fake NTP server so the router uses a date whose + daily password you already know. + +Requirements: + pip install pycryptodome + +Usage: + # Set a permanent admin password (recommended) + python3 hg8145v5_ntp_password.py set-password \\ + --password MySecurePass123 \\ + --input configs/hw_ctree_optimized.xml \\ + --output configs/hw_ctree_custom.xml + + # Show credentials + python3 hg8145v5_ntp_password.py login --date 2025-03-16 + + # Fake NTP server (fallback, needs root) + sudo python3 hg8145v5_ntp_password.py ntp-server --date 2025-03-16 +""" +import argparse +import datetime +import hashlib +import os +import socket +import struct +import sys + +# Known daily credentials (captured from the device) +KNOWN_CREDENTIALS = { + "2025-03-16": { + "Mega_gpon": "eef90b1496430707", + "Meg4_root": "eb52daf690f49e85", + }, +} + +# CRACKED: Original admin password stored in config before TR-069 changes it. +# Mega_gpon: "admintelecom" verified via PBKDF2-SHA256 +ORIGINAL_PASSWORD = "admintelecom" + + +def _generate_password_hash(password, salt, iterations=5000): + """Generate PBKDF2-SHA256 hash matching the router's format.""" + dk = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), + salt.encode('utf-8'), iterations, dklen=32) + return dk.hex() + + +def _generate_salt(length=24): + """Generate a random Base64-like salt matching the router's format.""" + charset = ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs' + 'tuvwxyz0123456789+/') + return ''.join(charset[b % len(charset)] for b in os.urandom(length)) + + +def get_credentials(date_str): + """Get known credentials for a specific date.""" + if date_str in KNOWN_CREDENTIALS: + return KNOWN_CREDENTIALS[date_str] + normalized = date_str.replace("-", "") + for k, v in KNOWN_CREDENTIALS.items(): + if k.replace("-", "") == normalized: + return v + return None + + +def set_password_in_config(input_path, output_path, web_password, + ssh_password=None, disable_tr069=True): + """Patch the config XML to set custom admin passwords. + + Sets permanent passwords for Mega_gpon (web) and Meg4_root (SSH) + by generating new PBKDF2-SHA256 hashes and encrypting them. + Optionally disables TR-069 to prevent the ISP from overwriting. + """ + try: + import xml.etree.ElementTree as ET + except ImportError: + print("ERROR: xml.etree.ElementTree not available") + sys.exit(1) + + # Import the encryption toolkit + toolkit_dir = os.path.dirname(os.path.abspath(__file__)) + if toolkit_dir not in sys.path: + sys.path.insert(0, toolkit_dir) + from huawei_fw_toolkit import value_encrypt + + if ssh_password is None: + ssh_password = web_password + + tree = ET.parse(input_path) + root = tree.getroot() + changes = 0 + + # Set Mega_gpon (web admin) password + for user in root.iter('X_HW_WebUserInfoInstance'): + username = user.get('UserName', '') + if username == 'Mega_gpon': + salt = user.get('Salt', '') + if not salt: + salt = _generate_salt() + user.set('Salt', salt) + new_hash = _generate_password_hash(web_password, salt, 5000) + encrypted_hash = value_encrypt(new_hash) + user.set('Password', encrypted_hash) + user.set('PassMode', '3') + changes += 1 + print(" [OK] Mega_gpon password set (web admin)") + + # Set Meg4_root (SSH/CLI) password + for user in root.iter('X_HW_CLIUserInfoInstance'): + username = user.get('Username', '') + if username == 'Meg4_root': + salt = user.get('Salt', '') + if not salt: + salt = _generate_salt() + user.set('Salt', salt) + new_hash = _generate_password_hash(ssh_password, salt, 5000) + encrypted_hash = value_encrypt(new_hash) + user.set('Userpassword', encrypted_hash) + user.set('EncryptMode', '2') + changes += 1 + print(" [OK] Meg4_root password set (SSH/CLI)") + + # Disable TR-069 to prevent ISP from overwriting passwords + if disable_tr069: + for ms in root.iter('ManagementServer'): + ms.set('EnableCWMP', '0') + ms.set('X_HW_EnableCWMP', '0') + ms.set('PeriodicInformEnable', '0') + changes += 1 + print(" [OK] TR-069 (CWMP) disabled") + + if changes == 0: + print(" WARNING: No user accounts found to update") + return False + + tree.write(output_path, encoding='unicode', xml_declaration=False) + + print(f"\n Config saved: {output_path}") + print("\n Credentials after uploading config:") + print(" Web (https://192.168.100.1):") + print(" User: Mega_gpon") + print(f" Pass: {web_password}") + print(" SSH/Telnet:") + print(" User: Meg4_root") + print(f" Pass: {ssh_password}") + if disable_tr069: + print("\n TR-069 disabled: ISP cannot change the password.") + print(" WARNING: Disabling TR-069 may prevent ISP firmware updates") + print(" and remote management. Re-enable if needed.") + return True + + +def show_credentials(date_str): + """Show known credentials for a date.""" + creds = get_credentials(date_str) + + print() + print('=' * 58) + print(" HG8145V5 Password Recovery") + print('=' * 58) + + print() + print(" METHOD 1: Original password (cracked from config)") + print('─' * 58) + print(f" User: Mega_gpon Pass: {ORIGINAL_PASSWORD}") + print(" (Works if TR-069 hasn't changed it)") + + print() + print(" METHOD 2: Set a permanent custom password") + print('─' * 58) + print(f" python3 {sys.argv[0]} set-password \\") + print(" --password YOUR_PASSWORD \\") + print(" --input configs/hw_ctree_optimized.xml \\") + print(" --output configs/hw_ctree_custom.xml") + print(" Then upload hw_ctree_custom.xml to the router.") + print(" TR-069 will be disabled so the ISP can't change it.") + + if creds: + print() + print(f" METHOD 3: Daily credentials for {date_str}") + print('─' * 58) + if "Mega_gpon" in creds: + print(f" User: Mega_gpon Pass: {creds['Mega_gpon']}") + if "Meg4_root" in creds: + print(f" User: Meg4_root Pass: {creds['Meg4_root']}") + print(f" (Use NTP trick to set router date to {date_str})") + else: + print() + print(f" No known daily credentials for {date_str}") + print(" Available dates:") + for d in sorted(KNOWN_CREDENTIALS.keys()): + print(f" {d}") + + print() + print('=' * 58) + + +def ntp_timestamp(dt): + """Convert datetime to NTP timestamp (seconds since 1900-01-01).""" + ntp_epoch = datetime.datetime(1900, 1, 1) + delta = dt - ntp_epoch + return int(delta.total_seconds()) + + +def build_ntp_response(recv_data, target_date): + """Build an NTP response packet with the target date/time.""" + li_vn_mode = (0 << 6) | (4 << 3) | 4 + stratum = 1 + poll = 6 + precision = -20 + + root_delay = 0 + root_dispersion = 0 + ref_id = b'LOCL' + + target_dt = datetime.datetime.strptime(target_date, "%Y-%m-%d") + target_dt = target_dt.replace(hour=12, minute=0, second=0) + + ts = ntp_timestamp(target_dt) + ts_frac = 0 + + if len(recv_data) >= 48: + orig_ts = recv_data[40:48] + else: + orig_ts = b'\x00' * 8 + + packet = struct.pack('!BBBb', li_vn_mode, stratum, poll, precision) + packet += struct.pack('!II', root_delay, root_dispersion) + packet += ref_id + packet += struct.pack('!II', ts, ts_frac) + packet += orig_ts + packet += struct.pack('!II', ts, ts_frac) + packet += struct.pack('!II', ts, ts_frac) + + return packet + + +def run_ntp_server(target_date, bind_addr="0.0.0.0", port=123): + """Run a fake NTP server that returns a fixed date.""" + creds = get_credentials(target_date) + + print("╔══════════════════════════════════════════════════════╗") + print("║ HG8145V5 NTP Password Recovery Server ║") + print("╠══════════════════════════════════════════════════════╣") + print(f"║ Target Date: {target_date:<39s} ║") + print(f"║ Listening: {bind_addr}:{port:<33} ║") + print("╠══════════════════════════════════════════════════════╣") + if creds: + print(f"║ Credentials for {target_date}: ║") + for user, pwd in creds.items(): + print(f"║ User: {user:<15s} Pass: {pwd:<16s} ║") + else: + print(f"║ No known credentials for {target_date} ║") + print("║ Capture them after NTP sync and add to script ║") + print("╠══════════════════════════════════════════════════════╣") + print("║ Steps: ║") + print("║ 1. Set router NTP to this PC's IP ║") + print("║ 2. Reboot router or wait for NTP sync ║") + print("║ 3. Login with credentials above ║") + print("║ Press Ctrl+C to stop ║") + print("╚══════════════════════════════════════════════════════╝") + print() + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + try: + sock.bind((bind_addr, port)) + except PermissionError: + print(f"ERROR: Port {port} requires root/admin privileges.") + print(f" sudo python3 {sys.argv[0]} ntp-server --date {target_date}") + sys.exit(1) + except OSError as e: + print(f"ERROR: Cannot bind to {bind_addr}:{port}: {e}") + sys.exit(1) + + print(f"[*] NTP server running on {bind_addr}:{port}") + print(f"[*] Serving date: {target_date}") + print() + + try: + while True: + data, addr = sock.recvfrom(1024) + response = build_ntp_response(data, target_date) + sock.sendto(response, addr) + print(f"[NTP] {addr[0]}:{addr[1]} -> Served date {target_date}") + except KeyboardInterrupt: + print("\n[*] NTP server stopped.") + finally: + sock.close() + + +def main(): + parser = argparse.ArgumentParser( + description='HG8145V5 password management and recovery tool', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Set a permanent custom password (RECOMMENDED, no NTP needed) + %(prog)s set-password --password MyPass123 \\ + --input configs/hw_ctree_optimized.xml \\ + --output configs/hw_ctree_custom.xml + + # Show all recovery methods + %(prog)s login --date 2025-03-16 + + # NTP server fallback (requires root) + sudo %(prog)s ntp-server --date 2025-03-16 + + # List known credential dates + %(prog)s list + """) + + subparsers = parser.add_subparsers(dest='command', help='Command') + + # set-password command (MAIN FEATURE) + sp = subparsers.add_parser('set-password', + help='Set permanent admin password in config') + sp.add_argument('--password', '-p', required=True, + help='New admin password for web and SSH') + sp.add_argument('--ssh-password', + help='Separate SSH password (default: same as --password)') + sp.add_argument('--input', '-i', required=True, + help='Input config XML file') + sp.add_argument('--output', '-o', required=True, + help='Output config XML file') + sp.add_argument('--keep-tr069', action='store_true', + help='Do NOT disable TR-069 (ISP can still change pwd)') + + # login command + lp = subparsers.add_parser('login', + help='Show credentials / recovery methods') + lp.add_argument('--date', default='2025-03-16', + help='Date for daily credentials (YYYY-MM-DD)') + + # ntp-server command + np = subparsers.add_parser('ntp-server', + help='Run fake NTP server (fallback)') + np.add_argument('--date', required=True, + help='Target date (YYYY-MM-DD)') + np.add_argument('--bind', default='0.0.0.0', + help='Bind address (default: 0.0.0.0)') + np.add_argument('--port', type=int, default=123, + help='Port (default: 123)') + + # list command + subparsers.add_parser('list', help='List known credential dates') + + args = parser.parse_args() + + if args.command == 'set-password': + print("\n Setting password in config...") + set_password_in_config( + args.input, args.output, + web_password=args.password, + ssh_password=args.ssh_password, + disable_tr069=not args.keep_tr069, + ) + elif args.command == 'login': + show_credentials(args.date) + elif args.command == 'ntp-server': + run_ntp_server(args.date, args.bind, args.port) + elif args.command == 'list': + print("\nKnown daily credential dates:") + for d in sorted(KNOWN_CREDENTIALS.keys()): + creds = KNOWN_CREDENTIALS[d] + print(f" {d}: {', '.join(creds.keys())}") + if not KNOWN_CREDENTIALS: + print(" (none)") + print(f"\nOriginal password (cracked): {ORIGINAL_PASSWORD}") + else: + parser.print_help() + + +if __name__ == '__main__': + main() diff --git a/Tools/huawei_fw_toolkit.py b/Tools/huawei_fw_toolkit.py new file mode 100644 index 00000000..a11aaa17 --- /dev/null +++ b/Tools/huawei_fw_toolkit.py @@ -0,0 +1,518 @@ +#!/usr/bin/env python3 +""" +Huawei ONT/Router configuration and firmware toolkit. + +Decrypts configuration files, extracts credentials, and parses HWNP firmware +containers from Huawei devices (HG8145V5, HG8247H, HG658b, etc). + +Integrates decryption logic from: + - palmerc/AESCrypt2 — aescrypt2 config file decryption (AES-256-CBC) + - scratchmex/huawei-decode-8145 — XML config + $2..$ value decryption + - andreluis034/huawei-utility-page — value cipher/decipher + password modes + - Cristi075/HG658b_data_dumper — AES-128-CBC config extraction + - jameskeenan295/huawei_router_aes_keys — key extraction from memory dumps + - k0r0pt/rom0Decoder — rom-0 LZS decompression + - Jakiboy/Ratr (Hwdecode) — XML config batch decryption + - staaldraad/huaweiDecrypt — DES-ECB VRP password decryption + +Usage: + python3 huawei_fw_toolkit.py config-dec + python3 huawei_fw_toolkit.py config-enc + python3 huawei_fw_toolkit.py values-dec + python3 huawei_fw_toolkit.py value-dec '$2...$' + python3 huawei_fw_toolkit.py vrp-dec + python3 huawei_fw_toolkit.py find-keys [encrypted_config] + python3 huawei_fw_toolkit.py hwnp-extract [output_dir] +""" +import gzip +import hashlib +import html +import io +import json +import os +import struct +import sys +import xml.etree.ElementTree as ET + +try: + from Crypto.Cipher import AES, DES + HAS_CRYPTO = True +except ImportError: + HAS_CRYPTO = False + +# AES-256 Value Key — SHA256("9jK0lk5kLmxn8sjojW962llHY76xAc2zDf7!ui%s9(lmV1L8") +VALUE_KEY = bytes.fromhex( + "6fc6e3436a53b6310dc09a475494ac774e7afb21b9e58fc8e58b5660e48e2498" +) + +# Config file key (aescrypt2 key derivation) +CONFIG_INIT_KEY = b"hex:13395537D2730554A176799F6D56A239" + +# HG658b key/IV (AES-128-CBC) +HG658B_KEY = bytes.fromhex('3E4F5612EF64305955D543B0AE350880') +HG658B_IV = bytes.fromhex('8049E91025A6B54876C3B4868090D3FC') + +# DES key for VRP passwords +VRP_DES_KEY = b'\x01\x02\x03\x04\x05\x06\x07\x08' + + +def _value_decode_ascii(s): + b = bytearray(s.encode('latin-1')) + for i in range(len(b)): + if b[i] == 0x7E: + b[i] = 0x1E + else: + b[i] -= 0x21 + result = bytearray() + for ci in range(len(b) // 5): + chunk = b[5 * ci:5 * ci + 5] + v = 0 + ex = 1 + for j in range(5): + v += ex * chunk[j] + ex *= 93 + result.extend(struct.pack(' 0: + plaintext = plaintext[:-(16 - lastn)] + + inner_hash = sha_inner.digest() + sha_outer = hashlib.sha256() + sha_outer.update(bytes(k_opad)) + sha_outer.update(inner_hash) + if sha_outer.digest() != stored_hmac: + raise ValueError("HMAC verification failed — wrong key or corrupted file") + + try: + buf = io.BytesIO(bytes(plaintext)) + with gzip.GzipFile(fileobj=buf) as gz: + return gz.read() + except Exception: + return bytes(plaintext) + + +def config_encrypt(data, user_key=CONFIG_INIT_KEY, filename="hw_tree.xml"): + """Encrypt plaintext XML back to Huawei config backup format.""" + buf = io.BytesIO() + with gzip.GzipFile(fileobj=buf, mode='wb') as gz: + gz.write(data) + compdata = buf.getvalue() + lenmod = len(compdata) % 16 + + dig = hashlib.sha256() + dig.update(struct.pack('>Q', len(data))) + dig.update(filename.encode()) + iv = bytearray(dig.digest()[:16]) + iv[15] = (iv[15] & 0xF0) | lenmod + iv = bytes(iv) + + key = _derive_aescrypt2_key(iv, user_key) + k_ipad = bytearray(b'\x36' * 64) + k_opad = bytearray(b'\x5c' * 64) + for i in range(32): + k_ipad[i] ^= key[i] + k_opad[i] ^= key[i] + + sha_inner = hashlib.sha256() + sha_inner.update(bytes(k_ipad)) + padded_len = len(compdata) + (16 - lenmod if lenmod else 0) + cipher = AES.new(key, AES.MODE_CBC, iv) + ciphertext = bytearray() + for off in range(0, padded_len, 16): + block = compdata[off:off + 16] if off + 16 <= len(compdata) else compdata[off:] + if len(block) < 16: + block = block + b'\x00' * (16 - len(block)) + enc_block = cipher.encrypt(bytes(block)) + ciphertext.extend(enc_block) + sha_inner.update(enc_block) + + inner_hash = sha_inner.digest() + sha_outer = hashlib.sha256() + sha_outer.update(bytes(k_opad)) + sha_outer.update(inner_hash) + + header = bytearray(8) + header[0] = 1 + return bytes(header) + iv + bytes(ciphertext) + sha_outer.digest() + + +def _vrp_decode_char(c): + return ord('?' if c == 'a' else c) - ord('!') + + +def vrp_password_decrypt(cipher_text): + """Decrypt a VRP-style DES-ECB password (24-char encoded string).""" + if len(cipher_text) != 24: + raise ValueError("VRP cipher must be 24 chars") + out = [0] * 18 + j = 0 + for i in range(0, 24, 4): + y = _vrp_decode_char(cipher_text[i]) + y = (y << 6) & 0xFFFFFF + y = (y | _vrp_decode_char(cipher_text[i + 1])) & 0xFFFFFF + y = (y << 6) & 0xFFFFFF + y = (y | _vrp_decode_char(cipher_text[i + 2])) & 0xFFFFFF + y = (y << 6) & 0xFFFFFF + y = (y | _vrp_decode_char(cipher_text[i + 3])) & 0xFFFFFF + out[j + 2] = y & 0xFF + out[j + 1] = (y >> 8) & 0xFF + out[j] = (y >> 16) & 0xFF + j += 3 + raw = bytes(out)[:16] + des = DES.new(VRP_DES_KEY, DES.MODE_ECB) + return des.decrypt(raw).rstrip(b'\x00').decode('ascii', errors='replace') + + +def hg658b_decrypt(b64_data): + """Decrypt a base64-encoded field from HG658b configs.""" + import base64 + decoded = base64.b64decode(b64_data) + cipher = AES.new(HG658B_KEY, AES.MODE_CBC, HG658B_IV) + return cipher.decrypt(decoded).rstrip(b'\x00').decode('utf-8', errors='replace') + + +def generate_password_hash(password, mode, salt=None): + """Generate password hash using Huawei's modes (1=MD5, 2=SHA256(MD5), 3=PBKDF2).""" + if mode == 1: + return hashlib.md5(password.encode()).hexdigest() + elif mode == 2: + md5 = hashlib.md5(password.encode()).hexdigest() + return hashlib.sha256(md5.encode()).hexdigest() + elif mode == 3: + if salt is None: + raise ValueError("PBKDF2 mode requires a salt") + dk = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 5000, dklen=32) + return dk.hex() + raise ValueError(f"Unknown password mode: {mode}") + + +def decrypt_xml_values(xml_text): + """Decrypt all $2...$ values in an XML config. Returns (xml_str, credentials).""" + credentials = [] + if isinstance(xml_text, bytes): + xml_text = xml_text.decode('utf-8', errors='replace') + root = ET.fromstring(xml_text) + for elem in root.iter(): + for attr_name, attr_value in list(elem.attrib.items()): + if attr_value.startswith("$2") and attr_value.endswith("$"): + try: + decrypted = value_decrypt(attr_value) + elem.set(attr_name, decrypted) + if any(kw in attr_name.lower() for kw in ['password', 'key', 'secret', 'psk']): + credentials.append({ + 'element': elem.tag, 'field': attr_name, + 'value': decrypted, + 'username': elem.get('Username', elem.get('UserName', '')), + }) + except Exception: + pass + output = io.BytesIO() + ET.ElementTree(root).write(output, encoding='utf-8', xml_declaration=True) + return output.getvalue().decode('utf-8'), credentials + + +def extract_vrp_passwords(config_text): + """Extract and decrypt VRP-style passwords from config text.""" + results = [] + for line in config_text.splitlines(): + if 'local-user' in line and 'password' in line: + parts = line.split() + if len(parts) >= 5: + entry = {'username': parts[1], 'type': parts[3]} + if parts[3] == 'cipher' and len(parts[4]) == 24: + try: + entry['password'] = vrp_password_decrypt(parts[4]) + except Exception: + entry['password'] = '(decryption failed)' + else: + entry['password'] = parts[4] + results.append(entry) + return results + + +def find_keys_from_memdump(memdump_data, encrypted_config_data=None): + """Extract potential AES keys from a router memory dump.""" + pattern = b'\x00\x00\x00\x00\xff\xff\xff\xd8' + candidates = [] + pos = 0 + while True: + pos = memdump_data.find(pattern, pos) + if pos == -1: + break + key_data = memdump_data[pos + 8:pos + 40] + if len(key_data) == 32 and memdump_data[pos + 40:pos + 44] == b'\x00\x00\x00\x00': + if key_data not in candidates: + candidates.append(key_data) + pos += 1 + print(f"Found {len(candidates)} unique key candidates") + return candidates + + +def _write_hex_dump(filepath, data): + with open(filepath, 'w') as f: + for i in range(0, len(data), 16): + chunk = data[i:min(i + 16, len(data))] + hex_s = ' '.join(f'{b:02x}' for b in chunk) + asc_s = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk) + f.write(f'{i:08x}: {hex_s:<48s} {asc_s}\n') + + +def extract_hwnp(filepath, output_dir): + """Extract HWNP firmware container to output directory.""" + with open(filepath, 'rb') as f: + data = f.read() + if data[:4] != b'HWNP': + print("Error: Not an HWNP file") + return False + + entry_count = struct.unpack(' Encrypted (type {enc_type}): device-specific key required") + print(" Config files are inside this encrypted rootfs.") + print(" Obtain key from device /etc/wap/aes_string or bootloader.") + + rootfs_info = { + 'section_name': e1_name, 'label': e1_label, + 'encryption_type': enc_type, 'data_size': data_size, + 'md5_hash': md5_hash, 'version': version, 'decrypted': decrypted, + } + with open(os.path.join(rootfs_dir, 'rootfs_info.json'), 'w') as f: + json.dump(rootfs_info, f, indent=2) + + efs_dir = os.path.join(output_dir, 'efs') + os.makedirs(efs_dir, exist_ok=True) + efs_data = data[e2_offset:e2_offset + e2_size] + _write_hex_dump(os.path.join(efs_dir, 'efs.hex'), efs_data) + + efs_info = {} + if len(efs_data) >= 0x34: + year = struct.unpack('>H', efs_data[0x1E:0x20])[0] + efs_info = { + 'marker': efs_data[0:2].decode('ascii', errors='replace'), + 'olt_model': efs_data[4:16].split(b'\x00')[0].decode('ascii', errors='replace'), + 'region_code': efs_data[0x14:0x17].decode('ascii', errors='replace'), + 'board_id': efs_data[0x2C:0x34].split(b'\x00')[0].decode('ascii', errors='replace'), + 'manufacture_date': f'{year}-{efs_data[0x20]:02d}-{efs_data[0x21]:02d} ' + f'{efs_data[0x22]:02d}:{efs_data[0x23]:02d}:{efs_data[0x24]:02d}', + } + with open(os.path.join(efs_dir, 'efs_info.json'), 'w') as f: + json.dump(efs_info, f, indent=2) + + print(f"\n Section 2: {e2_name}") + print(f" Size: {e2_size} bytes") + if efs_info: + print(f" Model: {efs_info.get('olt_model')}, Board: {efs_info.get('board_id')}") + + firmware_info = { + 'firmware_file': os.path.basename(filepath), 'format': 'HWNP', + 'file_size': len(data), 'version': version, 'entry_count': entry_count, + 'rootfs': rootfs_info, 'efs': efs_info, + } + with open(os.path.join(output_dir, 'firmware_info.json'), 'w') as f: + json.dump(firmware_info, f, indent=2) + print(f"\n Extracted to: {output_dir}") + return True + + +def main(): + if len(sys.argv) < 2: + print(__doc__) + sys.exit(1) + cmd = sys.argv[1] + + if cmd == 'config-dec': + with open(sys.argv[2], 'rb') as f: + data = f.read() + result = config_decrypt(data) + with open(sys.argv[3], 'wb') as f: + f.write(result) + print(f"Decrypted config: {len(result)} bytes -> {sys.argv[3]}") + xml_text, creds = decrypt_xml_values(result) + if creds: + dec_path = sys.argv[3].replace('.xml', '_decrypted.xml') + with open(dec_path, 'w') as f: + f.write(xml_text) + print(f"\nExtracted {len(creds)} credentials:") + for c in creds: + print(f" {c['username'] or c['element']}: {c['field']}={c['value']}") + + elif cmd == 'config-enc': + with open(sys.argv[2], 'rb') as f: + data = f.read() + result = config_encrypt(data) + with open(sys.argv[3], 'wb') as f: + f.write(result) + print(f"Encrypted -> {sys.argv[3]}") + + elif cmd == 'values-dec': + with open(sys.argv[2], 'rb') as f: + data = f.read() + xml_text, creds = decrypt_xml_values(data) + print(xml_text) + if creds: + for c in creds: + print(f" {c['username'] or c['element']}: {c['field']}={c['value']}", file=sys.stderr) + + elif cmd == 'value-dec': + print(value_decrypt(html.unescape(sys.argv[2]))) + + elif cmd == 'vrp-dec': + with open(sys.argv[2], 'r') as f: + results = extract_vrp_passwords(f.read()) + for r in results: + print(f" User: {r['username']}, Password: {r['password']}") + + elif cmd == 'find-keys': + with open(sys.argv[2], 'rb') as f: + memdump = f.read() + config_data = None + if len(sys.argv) >= 4: + with open(sys.argv[3], 'rb') as f: + config_data = f.read() + keys = find_keys_from_memdump(memdump, config_data) + for i, k in enumerate(keys): + print(f" Key {i}: {k.hex()}") + + elif cmd == 'hwnp-extract': + out = sys.argv[3] if len(sys.argv) > 3 else 'firmware_out' + extract_hwnp(sys.argv[2], out) + + else: + print(f"Unknown command: {cmd}") + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/configs/README_OPTIMIZATIONS.md b/configs/README_OPTIMIZATIONS.md new file mode 100644 index 00000000..fc68762f --- /dev/null +++ b/configs/README_OPTIMIZATIONS.md @@ -0,0 +1,256 @@ +# HG8145V5 Configuration Optimizations + +Optimized configuration for Huawei HG8145V5-12 GPON Terminal (Megacable Mexico). + +## Device Information + +| Field | Value | +|-------|-------| +| Model | HG8145V5-12 (EchoLife) | +| SN | 4857544347020CB1 (HWTC47020CB1) | +| Hardware | 43ED.A | +| Software | V5R022C00S368 | +| ONT ID | 6 | +| MAC (TR069) | 6C:71:D2:39:AA:30 | +| MAC (INTERNET) | 6C:71:D2:39:AA:31 | +| VLAN Internet | 557 | +| VLAN TR-069 | 567 | +| ISP | Megacable (Mexico) | + +## Files + +- `hw_ctree_original.xml` — Original unmodified config from the router +- `hw_ctree_optimized.xml` — Config with minimal changes to unblock acsvip.megared.net.mx +- `README_OPTIMIZATIONS.md` — This documentation +- `../Tools/hg8145v5_ntp_password.py` — NTP-based password recovery tool +- `../Tools/huawei_fw_toolkit.py` — Config encryption/decryption toolkit + +## Password Recovery + +### Cracked Original Password + +The original admin password stored in the config was cracked: + +| User | Password | Method | +|------|----------|--------| +| **Mega_gpon** | **admintelecom** | PBKDF2-SHA256 (salt: LSFP5fcMS6yNUN1i0qYnBU+F, 5000 iterations) | + +The ISP (Megacable) changes this password daily via TR-069 (ACS server at +`acsvip.megared.net.mx:7547`). If TR-069 is disabled in the config, the +original `admintelecom` password remains valid permanently. + +### How the Daily Password Works + +1. The config stores `admintelecom` as the Mega_gpon password +2. The ISP's ACS server connects via TR-069 (VLAN 567) and pushes a new password daily +3. The pushed password (e.g., `eef90b1496430707` for March 16, 2025) replaces the stored hash +4. **Solution**: Disable TR-069 to keep `admintelecom` as a permanent password + +### Method 1: Set Custom Password (Recommended, no NTP needed) + +Set your own permanent admin password and disable TR-069 so the ISP +cannot overwrite it: + +```bash +python3 Tools/hg8145v5_ntp_password.py set-password \ + --password YOUR_PASSWORD \ + --input configs/hw_ctree_optimized.xml \ + --output configs/hw_ctree_custom.xml +``` + +Then upload `hw_ctree_custom.xml` to the router. Both Mega_gpon (web) and +Meg4_root (SSH) will use your chosen password. TR-069 is automatically +disabled to prevent the ISP from changing it. + +### Method 2: Use Original Password + +Upload the optimized config with TR-069 disabled. The cracked `admintelecom` +password will remain valid: + +``` +Web: https://192.168.100.1 -> Mega_gpon / admintelecom +``` + +### Method 3: NTP Trick (Use Known Daily Password) + +If TR-069 has already changed the password and you can't upload a config, +use the NTP trick to set the router date to a day whose password you know: + +| Date | User | Password | +|------|------|----------| +| 2025-03-16 | Mega_gpon | eef90b1496430707 | +| 2025-03-16 | Meg4_root | eb52daf690f49e85 | + +```bash +# Run fake NTP server on your PC (needs root for port 123) +sudo python3 Tools/hg8145v5_ntp_password.py ntp-server --date 2025-03-16 + +# Step 4: Login with the known daily password +``` + +### Adding New Dates + +When you capture credentials for a new date, add them to the +`KNOWN_CREDENTIALS` dictionary in `Tools/hg8145v5_ntp_password.py`. + +## Changes Applied (38 optimizations + 2 ACS unblock fixes) + +The optimized config is based on the original with all formatting preserved exactly: +- Self-closing tags use `/>` (no space before) +- All XML entities (`'`, `"`, `&`, `>`, `<`) in encrypted values are preserved +- All 87 encrypted `$2...$` values are identical to the original + +### Unblock acsvip.megared.net.mx from LAN +| Setting | Before | After | Benefit | +|---------|--------|-------|---------| +| X_HW_FirewallGeneralLevel | 4 (highest) | 1 (low) | Allows LAN to access WAN services including acsvip.megared.net.mx | +| WANSrcWhiteListEnable | 1 (enabled, 0 entries) | 0 (disabled) | Removes empty whitelist that blocks all WAN source access | + +### WiFi 2.4GHz (ath0) +| Setting | Before | After | Benefit | +|---------|--------|-------|---------| +| Channel | 6 | 1 | Less congested channel | +| Channel Width | HT20 (20MHz) | HT40 (Auto) | ~2x throughput | +| Guard Interval | Auto | Short (400ns) | Lower latency | +| Encryption | TKIP+AES | AES only | Faster, more secure | +| RSSI Threshold | -88 dBm | -75 dBm | Better roaming to 2nd router | + +### WiFi 5GHz (ath4) +| Setting | Before | After | Benefit | +|---------|--------|-------|---------| +| Channel | 6 (invalid for 5GHz!) | 36 (UNII-1) | Correct 5GHz channel | +| ChannelsInUse | 6 | 36 | Matches channel setting | +| Band Setting | 2.4GHz (wrong!) | 5GHz | Correct band | +| Guard Interval | Auto | Short | Lower latency | +| Encryption | TKIP+AES | AES only | Faster encryption | +| RSSI Threshold | -88 dBm | -70 dBm | Aggressive roaming | + +### DHCP IPv4 +| Setting | Before | After | Benefit | +|---------|--------|-------|---------| +| Pool Range | .2-.100 (98 IPs) | .2-.254 (253 IPs) | More devices | +| Lease Time | 86400s (24h) | 43200s (12h) | Faster IP recycling | +| DNS Servers | 1.1.1.1, 1.0.0.1 | 1.1.1.1, 8.8.8.8 | Redundant DNS providers | + +### WAN INTERNET (VLAN 557) +| Setting | Before | After | Benefit | +|---------|--------|-------|---------| +| IPv6 | Disabled | Dual-stack (IPMode=2) | Full IPv6 connectivity | +| DHCPv6 | Off | Enabled | IPv6 address assignment | +| Prefix Delegation | Off | Enabled | IPv6 for all LAN devices | +| DNS Override | Disabled | 1.1.1.1, 8.8.8.8 | Faster DNS resolution | +| Ping Response | Disabled | Enabled | Latency testing | +| NAT Type | Symmetric (1) | Full Cone (0) | Lower latency for gaming/VoIP | +| IGMP | Disabled | Enabled | Multicast support | + +### WAN TR-069 (VLAN 567) +| Setting | Before | After | Benefit | +|---------|--------|-------|---------| +| IPv4 | Disabled | Enabled (dual-stack) | TR-069 works over IPv4+IPv6 | +| Priority | 0 | 4 | Higher QoS for management | +| DHCPv6 + PD | Off | Enabled | IPv6 address management | + +### Security +| Setting | Before | After | Benefit | +|---------|--------|-------|---------| +| Firewall | Disabled | Enabled (stateful) | Network protection | +| Anti-DNS Rebind | Off | On | Prevents DNS rebind attacks | +| WPS (2.4GHz) | Enabled | Disabled | Prevents WPS PIN attacks | +| WPS (5GHz) | Enabled | Disabled | Prevents WPS PIN attacks | +| Remote Access | HTTP/HTTPS/Telnet/FTP (all ports) | HTTPS only (443) | Reduced attack surface | +| UPnP | Disabled | Enabled | Auto port forwarding | + +### ALG (Application Layer Gateway) +| Setting | Before | After | Benefit | +|---------|--------|-------|---------| +| TFTP | Enabled | Disabled | Less processing overhead | +| H323 | Enabled | Disabled | Unused protocol | +| RTSP | Disabled | Enabled | Streaming support | +| L2TP/IPSec/PPTP | Disabled | Enabled | VPN passthrough support | + +### QoS / Latency +| Setting | Before | After | Benefit | +|---------|--------|-------|---------| +| QoS Enable | Not set | Enabled | Traffic prioritization | +| Upstream Mode | Default | Optimized (1) | Better upload scheduling | + +### IPv6 LAN +| Setting | Before | After | Benefit | +|---------|--------|-------|---------| +| IPv6 Capable (TR-069 WAN) | No | Yes | IPv6 on management WAN | +| IPv6 Capable (Internet WAN) | No | Yes | IPv6 on Internet WAN | +| IPv6 Capable (LAN) | No (0) | Yes (1) | LAN IPv6 support | +| IPv6 DNS Source | TR-069 WAN | INTERNET WAN | Use correct WAN for DNS | + +### NTP / Time +| Setting | Before | After | Benefit | +|---------|--------|-------|---------| +| NTP Server 1 | 192.168.100.3 (local) | time.google.com | Reliable time sync | +| NTP Server 2 | clock.nyc.he.net | pool.ntp.org | Redundant NTP | +| Timezone Name | Guadalajara, Mexico City | CST6CDT (with DST) | Correct DST handling | + +### Secondary DHCP Pool +| Setting | Before | After | Benefit | +|---------|--------|-------|---------| +| Enable | Disabled | Enabled | Dedicated subnet for 2nd router | + +**How to connect second router:** +1. Connect 2nd router WAN port to HG8145V5 LAN port +2. Set 2nd router WAN to DHCP +3. Set 2nd router LAN to 192.168.2.0/24 subnet +4. Use same SSID + password on 2nd router for seamless roaming +5. Set 2nd router to a different WiFi channel (e.g., channel 11 for 2.4GHz, channel 149 for 5GHz) + +### Why the Previous Config Caused Rollback + +The previous version of `hw_ctree_optimized.xml` caused error +`AlarmID:104512 Configuration has rolled back due to restoration failure` because: + +1. **Extra space before `/>` in self-closing tags**: The Huawei XML parser + requires `/>` (no space), but the previous version used ` />` (with space) +2. **Missing `'` XML entities**: Encrypted `$2...$` values contain single + quotes encoded as `'` — the previous version had bare `'` characters +3. These formatting issues have been fixed in this version + +### How to safely make further changes + +When editing `hw_ctree_optimized.xml`: +- **Never add spaces before `/>` in self-closing tags** +- **Never unescape `'`, `"`, `&`, `>`, `<`** in encrypted values +- **Make one change at a time** and test by uploading to the router +- **Always keep `hw_ctree_original.xml` as reference** + +## Encrypted Values + +All 87 encrypted `$2...$` values in the config are preserved in their +original encrypted form. The toolkit can decrypt and re-encrypt them: + +```bash +# Decrypt all values +python3 Tools/huawei_fw_toolkit.py values-dec configs/hw_ctree_optimized.xml + +# Decrypt a single value +python3 Tools/huawei_fw_toolkit.py value-dec '$2...encrypted...$' +``` + +## How to Apply + +1. **Backup current config** from router web interface (192.168.100.1) +2. Go to **System Tools → Configuration File** +3. Upload `hw_ctree_optimized.xml` (or `hw_ctree_ntp.xml` for NTP trick) +4. Router will reboot and apply changes + +## ISP Details + +| Field | Value | +|-------|-------| +| ISP | Megacable (Mexico) | +| WAN Type | IPoE/DHCP on VLAN 557 | +| TR-069 | VLAN 567, ACS: acsvip.megared.net.mx:7547 | +| Public IP | 189.195.69.137/26 | +| Gateway | 189.195.69.129 | +| ISP DNS (IPv4) | 10.0.196.139, 189.195.40.36 | +| ISP DNS (IPv6) | 2806:265:500:0:10:2:196:218, 2806:265:500:0:10:2:196:219 | +| IPv6 Prefix | 2806:265:200:32::/64 | +| TR-069 IPv6 | 2806:265:200:32::332 | +| DHCP Lease | 86400s | diff --git a/configs/hw_ctree_optimized.xml b/configs/hw_ctree_optimized.xml new file mode 100644 index 00000000..8d1937dd --- /dev/null +++ b/configs/hw_ctree_optimized.xml @@ -0,0 +1,1887 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/configs/hw_ctree_original.xml b/configs/hw_ctree_original.xml new file mode 100644 index 00000000..7c8b5d05 --- /dev/null +++ b/configs/hw_ctree_original.xml @@ -0,0 +1,1887 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +