From 9db051e6b63a2325fa5170f02e2c4e1c5af51c56 Mon Sep 17 00:00:00 2001 From: n0stal6ic Date: Tue, 10 Mar 2026 17:36:49 -0500 Subject: [PATCH] Delete wvdxtractor.py --- wvdxtractor.py | 705 ------------------------------------------------- 1 file changed, 705 deletions(-) delete mode 100644 wvdxtractor.py diff --git a/wvdxtractor.py b/wvdxtractor.py deleted file mode 100644 index 85fdfd8..0000000 --- a/wvdxtractor.py +++ /dev/null @@ -1,705 +0,0 @@ -import os -import re -import sys -import json -import mmap -import hashlib -import logging -import argparse -import struct -import subprocess -import tempfile -from typing import List, Iterable, Tuple, Optional, Dict -from dataclasses import dataclass, field - -PREFIX_HEX = "4D 53 54 41 52 5F 53 45 43 55 52 45 5F 53 54 4F 52 45 5F 46 49 4C 45 5F 4D 41 47 49 43 5F 49 44" -POSTFIX_HEX_LIST = ["00 00"] -REQUIRE_POSTFIX_AT_EXTRACT_END = True -PRINT_HEXVIEW = True -EXTRACT_LEN = 228 -HEXVIEW_WIDTH = 16 -MAX_HITS = 0 -MAX_ZERO_FRACTION = 0.25 -MAX_ZERO_RUN = 16 -TAIL_START = 0x90 -MIN_TAIL_NONZERO_RATIO = 0.70 -SCRIPT_PATH = os.path.realpath(__file__) -SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) -SCRIPT_NAME = os.path.basename(SCRIPT_PATH) - -AES_ECB_KEYS: List[Tuple[str, str]] = [ - ("214BF3C129547AF31D32A5ECB4742192", "unknown"), - ("B9C956919B48E1671564F4CADB5FE63C", "unknown"), - ("224420CC1EBCA39100010001846276CC", "unknown"), - ("088085E0077085E08990A0E18AA0A0E1", "unknown"), - ("0C6E750369A106355D7290F482205D61", "unknown"), - ("0BB085E004608DE58880A0E1B95093E1", "unknown"), - ("1100E63045E019B5A8A1CB49EA9625B4", "unknown"), - ("F72F0FE303CC80E1BC3406E3B11095E1", "unknown"), - ("203040E30100A0E3B010CCE100C094E5", "unknown"), - ("13541C00B0801C00A01820006D801C00", "unknown"), - ("0007FF4154534D92FC55AA0FFF0110E0", "default"), - ("BC1197CA30AA0FC84F7FE62E09FD3D9F", "Hisense"), - ("8981D083B3D53B3DF1AC529A70F244C0", "Vestel"), - ("24490B4CC95F739CE34138478E47139E", "Vestel"), - ("1234123412341234", "mb97 to mb130"), - ("3503B1CDE3401EC06030C12A4311F4A5", "KTC"), - ("E33AB4C45C2570B8AD15A921F752DEB6", "LG"), - ("1F1E1D1C1B1A19180706050403020100", "BCM35230/early MTK5369 and LG1152 (common)"), - ("7184C9C428D03C445188234D5A827196", "mtk5369 - Mediatek GP4 - HE_DTV_GP4I_AFAAATAA"), - ("385A992430196A8C44F1985823C01440", "mtk5398 (a2) - Mediatek NetCast 4/4.5 - HE_LCD_NC5M_AAAAAIAA"), - ("8E32E4608871ECE9B6301999D5155A07", "mtka5lr (mt5882) - Mediatek webOS 2 (2015) - HE_DTV_W15L_AFAAABAA"), - ("6856A0482475A8B41728A35474810203", "new BCM35230"), - ("2F2E2D2C2B2A29281716151413121110", "Saturn7/BCM3556"), - ("212D3B2A5C2D3A2C4D5B234B1A321D2A", "new Saturn7/old LM1"), - ("4F836AAEB4301F26172A9B0E1120EAF4", "LM1 PDP"), - ("4EE662C7A2C0917F7328DE73A0836C6B", "LM1 LCD"), - ("2F534ABE34801A36B7DA6B3EB1C04AD4", "m1 - MStar non-webOS - LN45*, LN53*, LN54*, LN565*, LA61*, LA643*, MA53*, PN45*, PN65*"), - ("7B2CA5943D2E752CF58606228C5B2DAD", "m1a - MStar non-webOS (L15 signage) - BS_LCD_LE15_AAAAAIAM"), - ("D55C6864035A8C8A2B35A6D6C4565596", "m2 - MStar SimpleSmart - HE_LCD_SS1A_AFAAAIAA"), - ("ADB92D9E23035522F4708CC259B31EA2", "m2 - MStar webOS 3.0 (2016) - HE_DTV_W16R_AFAAABAA"), - ("D2E6EE17639DFE2F81D3840FA0BC334A", "m2r - MStar webOS 3.5 (2017) - HE_DTV_W17R_AFAAABAA"), - ("4F6DE80C0362FD562464BC2073D15567", "m3 - MStar webOS 4.0 (2018) - HE_DTV_W18R_AFAAABAA"), - ("88723D91920712D0BAFE87A25E6E8EC7", "m3r - MStar webOS 4.5 (2019) - HE_DTV_W19R_AFAAATAA"), - ("68A284B4953CAD15024BED2C4F852A09", "lm14 - MStar NetCast 4.5 (2014/2015) - HE_LCD_NC5U_AAADABAA"), - ("19F51EE9B949C89E41AE136F48BB405C", "lm14a - MStar webOS 2 (2015) - HE_DTV_W15A_AFADABAA"), - ("F8F6BD1AA24506C2759E1BE1D51BB43C", "lm14alite - MStar webOS 2 (2015) - HE_DTV_W15B_AFADABAA"), - ("96F464CB29CDFF5441FD87D47D084FF8", "lm15u - MStar webOS 2 (2015) - HE_DTV_W15U_AFADABAA"), - ("6FCCC4AA3389B614BABE462498D2020A", "lm18a - MStar webOS 4.0 (2018) - HE_DTV_W18A_AFADATAA"), - ("806B982279521809DBAD9E2E6BF377763903565A7EB4604BAB1E1503DBFC4326", "lm21a - MStar webOS 6 (2021) - HE_DTV_W21A_AFADATAA"), - ("B65119E0E6CB5DB19C69B4CC78FAC3A87C747E5AEFDE8FF58F2CD47128D9E16D", "lm21u (mt5889) - MStar webOS 6 (2021) - HE_DTV_W21U_AFADATAA"), - ("FC9D81DEC206BA62614C949C43D2DA91D23E9FF3DF9674D69A444D13277BDF96", "lm21ut - MStar webOS 6 StanbyME (2022) - HE_DTV_N21D_AFAAATAA"), - ("3435663331313732316538383063663538306161643131653335323334613034", "lm21an - MStar webOS 7 (2022) - HE_DTV_W22A_AFADATAA"), - ("1FB2C3B789D5EA48ED16E79A0343986C691DACEC872BB07787D0F722AF5D1E2C", "lm21ann - MStar webOS 8 (2023) - HE_DTV_W23A_AFADATAA"), - ("4813B5B63C998A2874EF3320684AC8D9", "lg1152 - LX GP4 - HE_DTV_GP4H_AFAAATAA"), - ("14B3623488212250C7C992AACD537447", "lg115x - LX NC4 - HE_LCD_NC4H_AAADABAA"), - ("12C344FDD2871C983CD0FBBC25143974", "lg1154 (h13, goldfinger) - LX webOS 1 (2014) - HE_DTV_WT1H_AFAAABAA"), - ("34CC219D3AFC102433109BBC1DA44095", "m14 (m14tv) - LX webOS 1 (2014) - HE_DTV_WT1M_AFAAATAA"), - ("5A167D8C342EF094800E7CFA2D10F2D0", "m14 (m14tv) - LX webOS 2 (2015) - HE_DTV_W15M_AFAAATAA"), - ("13F56BE4B4A0829598DB8F74065A263B", "h15 - LX webOS 2 (2015) - HE_DTV_W15H_AFADATAA"), - ("3679EF1840B7FDEBC1FBF95A0CAFCE3E", "m16 - LX webOS 3.0 (2016) - HE_DTV_W16M_AFADABAA"), - ("5804DF78CB8DC6A71C05DAB0F1EDE3E1", "m16lite - LX webOS 3.0 (2016) - HE_DTV_W16N_AFADABAA"), - ("1B3C76ADD3F5EE6B089DB253747A8CD4", "m16p - LX webOS 3.5 (2017) - HE_DTV_W17H_AFADABAA"), - ("3C9D30DF3A95C1AA41928813292BD947", "m16plite - LX webOS 3.5 (2017) - HE_DTV_W17M_AFADATAA"), - ("89E11D498392F5A521145738EF036AE5", "m16pstb - LX webOS 3.5 (2017) - HE_DTV_W17S_AFADATAA"), - ("437C02F0DF99F2072D1A64EEBBD2953B", "m16pp - LX webOS 4.0 (2018) - HE_DTV_W18H_AFADABAA"), - ("3471D9BFC5F4B34A8997D56932F34D94", "m16pplite - LX webOS 4.0 (2018) - HE_DTV_W18M_AFADATAA"), - ("3FE1CBE11BD658BB37813E05052D5FE5", "m16p3 - LX webOS 4.5 (2019) - HE_DTV_W19H_AFADABAA"), - ("A2FA48FCC1A22FD2F1944BEFA8403765EF178D4F4AB0E81AC7B5B267ACBDF14D", "m23 - LX webOS 8 (2023) - HE_DTV_W23M_AFADATAA"), - ("E529BCDEDF8E49667C0FA3A81174B65E", "o18 - LX webOS 4.0 (2018) - HE_DTV_W18O_AFABABAA"), - ("4ACD2CA8425BBA6C49FD03A174300239", "o18 - LX webOS 4.5 (2019) - HE_DTV_W19O_AFABABAA"), - ("C9EF645424A625BBAE7521394564025EC6252658FB650D33633111BD40C76011", "o20 - LX webOS 5 (2020) - HE_DTV_W20O_AFABATAA"), - ("944288798A122C6130B661BEE52DF4FE42120F60A61E312DCFC1411E300A29AE", "o20n - LX webOS 6 (2021) - HE_DTV_W21O_AFABATAA"), - ("3861333237633238613136633438663239623238623037656335623433353862", "o22 - LX webOS 7 (2022) - HE_DTV_W22O_AFABATAA"), - ("CF5D6DC934F18618B968382368E17BA971DEAA2ECDFC906874B327D87076E228", "o22n - LX webOS 8 (2023) - HE_DTV_W23O_AFABATAA"), - ("53D6DC79418C1A2371DC9F926CD3A3A06F4E7E4396464B5F41248083C2C65637", "o22n2 - LX webOS 9 (2024) - HE_DTV_W24G_AFABATAA"), - ("B7724DBBF2AEA073131E8E7D62D114E2AA02F99D17CD7350C14466624528ED79", "o24 - LX webOS 9 (2024) - HE_DTV_W24O_AFABATAA"), - ("FE90B0C1BE8CC28A9738333F95AC2C58777BDE7D4E8CABABA73B24FB7D1781C7", "o22n3 - LX webOS 10 (2025) - HE_DTV_W25G_AFABATAA"), - ("52A208FA24E7E70730A40999B1C22C148F4920484BC50B515D243E35D14689F1", "o24n - LX webOS 10 (2025) - HE_DTV_W25O_AFABATAA"), - ("CD4171FC9C06869627A67EA7B66D739D", "o18k - LX webOS 4.5 (2019) - HE_DTV_W19K_AFADATAA"), - ("0EE52A12A2EB5DE2E13999187B14913F6D3367A79B39AC35979A51E5C12A4FDF", "o208k - LX webOS 5 (2020) - HE_DTV_W20K_AFADATAA"), - ("E27E6AFE44B7866D60C24ED27904ECB296CA69B4251478B5248C03851F08ECF5", "e60n - LX webOS 6 (2021) - HE_DTV_W21K_AFADATAA"), - ("F2B78AEBAD6D86A17B4742B2B84B60F4", "k2l - Realtek webOS 3.0 (2016) - HE_DTV_W16K_AFADATAA"), - ("C2FBBC5DDD9D366B7FD6CAEB90F86039", "k2lp - Realtek webOS 3.0 (2016) - HE_DTV_W16P_AFADABAA"), - ("1C966DFA0E5AE9946AAF8D2EC06B9E18", "k3lp - Realtek webOS 3.5 (2017) - HE_DTV_W17P_AFADABAA"), - ("AD17A5923B525FD21DB765A5B6822FBD", "k5lp - Realtek webOS 4.5 (2019) - HE_DTV_W19P_AFADABAA"), - ("388BE4B04BD98E7C3CA45A4C6CA346DD2EB32BDCD05DC28FC4A87C9625294A5E", "k6lp - Realtek webOS 5 (2020) - HE_DTV_W20P_AFADATAA"), - ("377050F9B9D91CD803ACAACCEA4046DD99B01CFBB0010451F4F87A1620C4BAEF", "k6lpfhd - Realtek webOS 5 (2020) - HE_DTV_W20L_AFAAJAAA"), - ("395324AD369A529EABAC71FE1E72C25CE25594294D47303BCB2629241AFA4C98", "k6hp - Realtek webOS 5 (2020) - HE_DTV_W20H_AFADABAA"), - ("74514676D68B9A72A0093CEF56D3067484E1F4D5CF7D4B4ED389BED030FA1B09", "k7lp - Realtek webOS 6 (2021) - HE_DTV_W21P_AFADATAA"), - ("6A42D2485B716B25AE5C9921176588D167C25B902D4EF2903AF5C1FCC61D34C9", "k8lp - Realtek webOS 7 (2022) - HE_DTV_W22P_AFADATAA"), - ("A35A57DFDD8266F7CE1AF991EC67BABF6723653ABB9A7D48A4B8AB2A2485BCFE", "k8hp - Realtek webOS 7 (2022) - HE_DTV_W22H_AFADATAA"), - ("703373367638792F423F4528482B4D6251655468576D5A7134743777217A2443", "k8lp - Realtek webOS 7 hospitality (2022) - HE_IDD_H22P_AHAAATAA"), - ("6251655468576D5A7133743677397A24432646294A404E635266556A586E3272", "k8hp - Realtek webOS 7 hospitality (2022) - HE_IDD_H22H_AHAAATAA"), - ("3764336361633437326166373639383663353863363039316332383031626637", "k8ap - Realtek webOS 7 (2022) - HE_DTV_W22L_AFAAATAA"), - ("DFFD1E4F093E305451D4F3752E63BA9A3E6A6404922D986DF36C00818F5595C1", "k8hpp - Realtek webOS 8 (2023) - HE_DTV_W23H_AFADATAA"), - ("EC2C89B4AF45B5EB7EA9A83DD2387810C0815BD31BBFE1D17C809E7D68339112", "k8lpn - Realtek webOS 8 (2023) - HE_DTV_W23P_AFADATAA"), - ("F322A9CA1D523C358DD2FD97D5660E25386C9C60E423632AEC9723D282BE971D", "kf23f - Realtek webOS 8 smart monitors (2023) - HE_MNT_S23Y_AAAAGLAA"), - ("6252A0816884997B2FCA30662561A721A4BCC40B18CBEA5D363FA844F17D7DE9", "kid23q - Realtek webOS 8 ultrawide monitors (2023) - HE_MNT_S23Z_AAACGLAA"), - ("7638792F423F4528482B4D6251655368566D597133743677397A24432646294A", "k8lpn - Realtek webOS 8 hospitality (2023) - HE_IDD_H23P_AHAAATAA"), - ("6B5AD1BE81D7A1A494F58EB659431850C1B681826EE4428394D4897052691756", "k8lpn2 - Realtek webOS 9 (2024) - HE_DTV_W24P_AFADATAA"), - ("FA9EBB838B7BAFBA75EFE8D5A3560374EB0699A113411CA924051B4ADB52E10D", "k24 - Realtek webOS 9 (2024) - HE_DTV_W24H_AFADATAA"), - ("1A8ADAB21D9FF995677DB32BCE2E0CD559AE86840EBF4A696872076E37DFFE8F", "k24t - Realtek webOS 9 StanbyME 2 (2024) - HE_DTV_N24D_AFADATAA"), - ("25DF24166745B52EAB661455BED43DE376320FAA1F7824877B938DB869308B18", "k25lp - Realtek webOS 10 (2025) - HE_DTV_W25P_AFADATAA"), - ("B2E8C3E214F044B823916E48FA074E606C7C5CD5E6902B6F99BD903DAC0C792F", "k24n - Realtek webOS 10 (2025) - HE_DTV_W25H_AFADATAA"), - ("B23981FD3642CDF401E7A0C2FADBDA4399B6AAF9600B802144933B4F4E5855EA", "k6lpwee - Realtek webOS 5 (2020) - HE_DTV_C20P_AFADATAA"), - ("3263653764623932376235323637653637633035363066353833303235383466", "k8lpwee - Realtek webOS 7 (2022) - HE_DTV_C22P_AFADATAA"), - ("46715DFBDE23C2D8CBA7EC4F36BA41AAD28E7EC00FEC51F2843F24654DB09BD3", "k8hpwee - Realtek webOS 7 (2022) - HE_DTV_C22H_AFABATAA"), - ("6238323334663232396632613762316537333731333832306664666136333564", "k8apwee - Realtek webOS 7 (2022) - HE_DTV_C22L_AFAAATAA"), -] - -MTK_CBC_KEYS: List[Tuple[str, str, str]] = [ - ("09291094092910940929109409291094", "00000000000000000000000000000000", "Mediatek Generic (Hisense, Sharp, Philips)"), - ("53686172536861725348617253686172", "00000000000000000000000000000000", "SharSharSHarShar (Sharp key for Sharp external partition)"), - ("5450565F5450565F5450565F5450565F", "00000000000000000000000000000000", "TPVision(Philips) 2012"), - ("D378EAF81D378A801B556985789A7C31", "73079FD19183715E130858588479C652", "Philips 2012"), - ("47FBF8CAD62BB95AF3AD9509E5C2175D", "63120FB321B0410F216D6DC2D8641A11", "Philips 2013"), - ("55555555555555555555555555555555", "B1BFAA407F70C80C650379DFEAFAA40F", "TCL 2017"), - ("1B569AA7D2E4CCE66584A7A3D8A45679", "A0E88D5D52A813260D3A34A14AA89416", "TPV_MTK2K17PLF_EU0"), - ("135AFB6DE91CD56496244BC7C0E08D63", "C6A38C89F0AF5637EB6E19D35E12E257", "TPV_MTK2K14PLF_EU1"), - ("7B17F7764818AE2C897BA69D428D0CB3", "672041CCB9FCD4272B11E57AB6047163", "TPV_MTK2K16PLF_EU0"), -] - -logging.basicConfig(level=logging.INFO, format="%(message)s") -log = logging.getLogger(__name__) - -@dataclass -class ScanStats: - files_scanned: int = 0 - candidates_found: int = 0 - filter_rejected: int = 0 - duplicates_skipped: int = 0 - saved: int = 0 - decrypted: int = 0 - decrypt_failed: int = 0 - wvd_generated: int = 0 - - def report(self): - log.info("\n─────────────────────────────────────────") - log.info(" Complete") - log.info("─────────────────────────────────────────") - log.info(f" Files scanned : {self.files_scanned}") - log.info(f" Candidates found : {self.candidates_found}") - log.info(f" Filter rejected : {self.filter_rejected}") - log.info(f" Duplicates skipped : {self.duplicates_skipped}") - log.info(f" Keyboxes saved : {self.saved}") - log.info(f" Decrypted : {self.decrypted}") - log.info(f" Decrypt failed : {self.decrypt_failed}") - if self.wvd_generated > 0: - log.info(f" WVD files made : {self.wvd_generated}") - log.info("─────────────────────────────────────────") - -def _check_crypto() -> bool: - try: - from Crypto.Cipher import AES as _AES - return True - except ImportError: - return False - -HAS_CRYPTO = _check_crypto() - -def load_keys_from_file(path: str) -> Tuple[List[Tuple[str,str]], List[Tuple[str,str,str]]]: - ecb: List[Tuple[str,str]] = [] - cbc: List[Tuple[str,str,str]] = [] - try: - with open(path) as f: - for line in f: - line = line.strip() - if not line or line.startswith('#'): - continue - parts = line.split('#', 1) - key_part = parts[0].strip() - comment = parts[1].strip() if len(parts) > 1 else 'external' - if key_part.startswith("b'") or key_part.startswith('b"'): - continue - if ',' in key_part: - k, iv = key_part.split(',', 1) - k = k.strip().replace(' ','') - iv = iv.strip().replace(' ','') - try: - bytes.fromhex(k) - bytes.fromhex(iv) - if len(k) >= 32: - cbc.append((k.upper(), iv.upper(), comment)) - except ValueError: - pass - else: - k = key_part.replace(' ','') - try: - bytes.fromhex(k) - if len(k) >= 32: - ecb.append((k.upper(), comment)) - except ValueError: - pass - except Exception as e: - log.warning(f" Could not load key file {path}: {e}") - return ecb, cbc - -def _aes_ecb_decrypt(data: bytes, key: bytes) -> Optional[bytes]: - try: - from Crypto.Cipher import AES - from Crypto.Util.Padding import pad - padded = pad(data, 16) - cipher = AES.new(key, AES.MODE_ECB) - return cipher.decrypt(padded) - except Exception: - return None - -def _aes_cbc_decrypt(data: bytes, key: bytes, iv: bytes) -> Optional[bytes]: - try: - from Crypto.Cipher import AES - from Crypto.Util.Padding import pad - padded = pad(data, 16) - cipher = AES.new(key, AES.MODE_CBC, iv=iv) - return cipher.decrypt(padded) - except Exception: - return None - -def _extract_payload(dec: bytes) -> Optional[bytes]: - for offset in (64, 96): - candidate = dec[offset:] - if b"CHAI" in candidate or b"kbox" in candidate: - if b"CHAI" in candidate: - return candidate - else: - return candidate[:128] - return dec[64:] - -def try_decrypt( - blob: bytes, - manual_key_hex: Optional[str], - extra_ecb: List[Tuple[str,str]], - extra_cbc: List[Tuple[str,str,str]], -) -> Tuple[Optional[bytes], Optional[str], Optional[str]]: - if not HAS_CRYPTO: - return None, None, None - - ecb_candidates: List[Tuple[str,str]] = [] - cbc_candidates: List[Tuple[str,str,str]] = [] - - if manual_key_hex: - k = manual_key_hex.replace(' ','').upper() - ecb_candidates.append((k, 'manual')) - - ecb_candidates.extend(extra_ecb) - ecb_candidates.extend(AES_ECB_KEYS) - cbc_candidates.extend(extra_cbc) - cbc_candidates.extend(MTK_CBC_KEYS) - - for key_hex, desc in ecb_candidates: - try: - key = bytes.fromhex(key_hex) - except ValueError: - continue - dec = _aes_ecb_decrypt(blob, key) - if dec and b"INNER_MSTAR" in dec: - payload = _extract_payload(dec) - return payload, key_hex, f"AES-ECB | {desc}" - - for key_hex, iv_hex, desc in cbc_candidates: - try: - key = bytes.fromhex(key_hex) - iv = bytes.fromhex(iv_hex) - except ValueError: - continue - dec = _aes_cbc_decrypt(blob, key, iv) - if dec and b"INNER_MSTAR" in dec: - payload = _extract_payload(dec) - return payload, key_hex, f"AES-CBC | {desc}" - - return None, None, None - -def _check_pywidevine() -> bool: - try: - import importlib - importlib.import_module('pywidevine') - return True - except ImportError: - return False - -HAS_PYWIDEVINE = _check_pywidevine() - -def try_generate_wvd(payload: bytes, out_stem: str, output_dir: str) -> Optional[str]: - if not HAS_PYWIDEVINE: - return None - - try: - import tempfile - tmpdir = tempfile.mkdtemp() - - payload_path = os.path.join(tmpdir, "payload.bin") - with open(payload_path, "wb") as f: - f.write(payload) - - wvd_path = os.path.join(output_dir, f"{out_stem}.wvd") - - result = subprocess.run( - [sys.executable, "-m", "pywidevine", "create-device", - "--client-id", payload_path, - "--type", "ANDROID", - "--security-level", "1", - "-o", wvd_path], - capture_output=True, text=True, timeout=30 - ) - - if result.returncode == 0 and os.path.exists(wvd_path): - return wvd_path - - try: - from pywidevine.device import Device, DeviceTypes - from pywidevine.cdm import Cdm - device = Device.loads(payload) - device.dump(wvd_path) - if os.path.exists(wvd_path): - return wvd_path - except Exception: - pass - - except Exception as e: - log.debug(f" WVD generation error: {e}") - - return None - -def clean_hex(s: str) -> bytes: - s = s.replace("0x", "").replace("0X", "") - s = "".join(s.split()) - if not s: - return b"" - if len(s) % 2 != 0: - raise ValueError("Hex string must have an even number of digits.") - return bytes.fromhex(s) - -def iter_all(mm: mmap.mmap, needle: bytes, start: int = 0) -> Iterable[int]: - i = start - while True: - pos = mm.find(needle, i) - if pos == -1: - return - yield pos - i = pos + 1 - -def is_printable_ascii(b: int) -> bool: - return 32 <= b <= 126 - -def hexview(data: bytes, base_offset: int = 0, width: int = 16) -> str: - lines = [] - for i in range(0, len(data), width): - chunk = data[i:i + width] - hex_part = " ".join(f"{x:02X}" for x in chunk) - hex_part_padded = hex_part.ljust(width * 3 - 1) - ascii_part = "".join(chr(x) if is_printable_ascii(x) else "." for x in chunk) - lines.append(f"{base_offset + i:08X} {hex_part_padded} |{ascii_part}|") - return "\n".join(lines) - -def best_postfix_window(postfixes: List[bytes]) -> int: - return max((len(p) for p in postfixes if p), default=0) - -def find_postfix_after_extract( - mm: mmap.mmap, - postfixes: List[bytes], - prefix_pos: int, - file_size: int, - window: int -) -> Tuple[int, bytes]: - start = prefix_pos + EXTRACT_LEN - if start >= file_size: - return -1, b"" - end = min(file_size, start + window) - for pf in postfixes: - if not pf: - continue - if start + len(pf) > file_size: - continue - pos = mm.find(pf, start, end) - if pos != -1: - return pos, pf - return -1, b"" - -def max_zero_run(b: bytes) -> int: - best = 0 - cur = 0 - for x in b: - if x == 0: - cur += 1 - if cur > best: - best = cur - else: - cur = 0 - return best - -def zero_fraction(b: bytes) -> float: - if not b: - return 1.0 - return sum(1 for x in b if x == 0) / len(b) - -def nonzero_ratio(b: bytes) -> float: - if not b: - return 0.0 - return sum(1 for x in b if x != 0) / len(b) - -def passes_filters(block: bytes, verbose: bool = False) -> bool: - zf = zero_fraction(block) - if zf > MAX_ZERO_FRACTION: - if verbose: - log.debug(f" REJECT zero_fraction={zf:.2%} > threshold {MAX_ZERO_FRACTION:.2%}") - return False - zr = max_zero_run(block) - if zr > MAX_ZERO_RUN: - if verbose: - log.debug(f" REJECT max_zero_run={zr} > threshold {MAX_ZERO_RUN}") - return False - tail = block[TAIL_START:] if TAIL_START < len(block) else b"" - if tail: - nr = nonzero_ratio(tail) - if nr < MIN_TAIL_NONZERO_RATIO: - if verbose: - log.debug(f" REJECT tail_nonzero_ratio={nr:.2%} < threshold {MIN_TAIL_NONZERO_RATIO:.2%}") - return False - return True - -def safe_prefix_from_filename(path: str) -> str: - base = os.path.basename(path) - root, _ = os.path.splitext(base) - safe = [] - for ch in root: - if ch.isalnum() or ch in "._-": - safe.append(ch) - else: - safe.append("_") - s = "".join(safe).strip("._-") - return s or "file" - -_KEYBOX_OUTPUT_RE = re.compile(r'_Keybox(_\d+)?\.bin$', re.IGNORECASE) - -def is_skipped_file(path: str) -> bool: - base = os.path.basename(path) - if base == SCRIPT_NAME: - return True - if _KEYBOX_OUTPUT_RE.search(base): - return True - return False - -def out_name_for_file(prefix: str, hit_idx: int, output_dir: str) -> str: - name = f"{prefix}_Keybox.bin" if hit_idx == 1 else f"{prefix}_Keybox_{hit_idx}.bin" - return os.path.join(output_dir, name) - -def out_stem_for_hit(prefix: str, hit_idx: int) -> str: - return f"{prefix}_Keybox" if hit_idx == 1 else f"{prefix}_Keybox_{hit_idx}" - -def write_manifest(entries: list, output_dir: str): - manifest_path = os.path.join(output_dir, "manifest.json") - with open(manifest_path, "w") as f: - json.dump(entries, f, indent=2) - log.info(f"\nManifest written -> {manifest_path}") - -def extract_from_file( - path: str, - prefix: bytes, - postfixes: List[bytes], - output_dir: str, - stats: ScanStats, - manifest_entries: list, - verbose_filters: bool = False, - no_decrypt: bool = False, - manual_key_hex: Optional[str] = None, - extra_ecb: Optional[List[Tuple[str,str]]] = None, - extra_cbc: Optional[List[Tuple[str,str,str]]] = None, - no_wvd: bool = False, -) -> int: - if not os.path.isfile(path): - return 0 - file_size = os.path.getsize(path) - if file_size <= 0: - return 0 - - stats.files_scanned += 1 - per_file_prefix = safe_prefix_from_filename(path) - saved = 0 - seen = set() - window = best_postfix_window(postfixes) - _extra_ecb = extra_ecb or [] - _extra_cbc = extra_cbc or [] - - with open(path, "rb") as f: - mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) - try: - for ppos in iter_all(mm, prefix, 0): - if ppos + EXTRACT_LEN > file_size: - continue - - qpos, _ = find_postfix_after_extract(mm, postfixes, ppos, file_size, window) - if qpos == -1: - continue - - if REQUIRE_POSTFIX_AT_EXTRACT_END and qpos != (ppos + EXTRACT_LEN): - continue - - stats.candidates_found += 1 - block = bytes(mm[ppos:ppos + EXTRACT_LEN]) - - if not passes_filters(block, verbose=verbose_filters): - stats.filter_rejected += 1 - continue - - h = hashlib.sha256(block).digest() - if h in seen: - stats.duplicates_skipped += 1 - continue - seen.add(h) - - saved += 1 - stats.saved += 1 - stem = out_stem_for_hit(per_file_prefix, saved) - out_name = os.path.join(output_dir, stem + ".bin") - - with open(out_name, "wb") as out: - out.write(block) - - log.info(f"[{os.path.basename(path)} | Hit #{saved}] -> {out_name} prefix@0x{ppos:X} postfix@0x{qpos:X}") - - if PRINT_HEXVIEW: - log.info(hexview(block, base_offset=ppos, width=HEXVIEW_WIDTH)) - log.info("") - - manifest_entry: Dict = { - "source_file": path, - "offset_hex": hex(ppos), - "sha256": hashlib.sha256(block).hexdigest(), - "keybox_file": out_name, - "decrypted": False, - "decrypt_key": None, - "decrypt_method": None, - "payload_file": None, - "wvd_file": None, - } - - if not no_decrypt: - if not HAS_CRYPTO: - log.warning(" [!] pycryptodome not installed.") - else: - log.info(f" [~] Attempting decryption ({len(AES_ECB_KEYS)} ECB + {len(MTK_CBC_KEYS)} CBC keys)...") - payload, used_key, used_method = try_decrypt(block, manual_key_hex, _extra_ecb, _extra_cbc) - - if payload is not None: - stats.decrypted += 1 - payload_path = os.path.join(output_dir, stem + "_payload.bin") - with open(payload_path, "wb") as pf: - pf.write(payload) - log.info(f" [+] Decrypted -> {payload_path}") - log.info(f" [+] Key used : {used_key}") - log.info(f" [+] Method : {used_method}") - - manifest_entry["decrypted"] = True - manifest_entry["decrypt_key"] = used_key - manifest_entry["decrypt_method"] = used_method - manifest_entry["payload_file"] = payload_path - - if not no_wvd: - if not HAS_PYWIDEVINE: - log.info(" [i] pywidevine not installed.") - log.info(" Install with: pip install pywidevine") - else: - wvd_path = try_generate_wvd(payload, stem, output_dir) - if wvd_path: - stats.wvd_generated += 1 - manifest_entry["wvd_file"] = wvd_path - log.info(f" [+] WVD -> {wvd_path}") - else: - log.info(" [!] WVD generation failed.") - else: - stats.decrypt_failed += 1 - log.info(" [-] No matching decryption key found in database.") - log.info(" Use --key to provide a key manually.") - - manifest_entries.append(manifest_entry) - log.info("") - - if MAX_HITS and saved >= MAX_HITS: - break - finally: - mm.close() - - return saved - -def main() -> int: - parser = argparse.ArgumentParser( - prog="MStarToWVD", - description="Extract MStar/MediaTek Widevine L1 keyboxes from raw eMMC dumps and firmware images, decrypt them, and generates .wvd files." - ) - parser.add_argument("files", nargs="*", help="One or more binary files to scan.") - parser.add_argument("--all", action="store_true", help="Scan all files in the script's directory.") - parser.add_argument("--output-dir", "-o", default=".", help="Directory to save all output files.") - parser.add_argument("--no-hexview", action="store_true", help="Remove hex dump output.") - parser.add_argument("--verbose-filters", action="store_true", help="Log candidate block rejection reasons.") - parser.add_argument("--quiet", "-q", action="store_true", help="Remove all output except errors.") - parser.add_argument("--no-decrypt", action="store_true", help="Skip decryption.") - parser.add_argument("--key", metavar="HEX", help="Manual AES key in hex.") - parser.add_argument("--keys-file", metavar="FILE", help="Path to an external key database file.") - parser.add_argument("--no-wvd", action="store_true", help="Skip WVD generation.") - - args = parser.parse_args() - - if args.quiet: - logging.getLogger().setLevel(logging.ERROR) - elif args.verbose_filters: - logging.getLogger().setLevel(logging.DEBUG) - - global PRINT_HEXVIEW - if args.no_hexview: - PRINT_HEXVIEW = False - - try: - prefix = clean_hex(PREFIX_HEX) - postfixes = [clean_hex(x) for x in POSTFIX_HEX_LIST] - except (ValueError, Exception) as e: - log.error(f"Failed to parse hex constants: {e}") - return 2 - - postfixes = [p for p in postfixes if p] - if not prefix or not postfixes or EXTRACT_LEN <= 0: - log.error("Error: prefix, postfix, or EXTRACT_LEN is missing.") - return 2 - - extra_ecb: List[Tuple[str,str]] = [] - extra_cbc: List[Tuple[str,str,str]] = [] - if args.keys_file: - if not os.path.isfile(args.keys_file): - log.error(f"Key file not found: {args.keys_file}") - return 2 - extra_ecb, extra_cbc = load_keys_from_file(args.keys_file) - log.info(f"Loaded {len(extra_ecb)} ECB + {len(extra_cbc)} CBC keys from {args.keys_file}") - - os.makedirs(args.output_dir, exist_ok=True) - stats = ScanStats() - manifest_entries: list = [] - - if not args.quiet and not args.no_decrypt: - if not HAS_CRYPTO: - log.info("[!] pycryptodome not found! Decryption disabled. Install: pip install pycryptodome") - if not HAS_PYWIDEVINE and not args.no_wvd: - log.info("[i] pywidevine not found! WVD generation disabled. Install: pip install pywidevine") - log.info("") - - kwargs = dict( - output_dir=args.output_dir, - stats=stats, - manifest_entries=manifest_entries, - verbose_filters=args.verbose_filters, - no_decrypt=args.no_decrypt, - manual_key_hex=args.key, - extra_ecb=extra_ecb, - extra_cbc=extra_cbc, - no_wvd=args.no_wvd, - ) - - if args.all: - total = 0 - for name in sorted(os.listdir(SCRIPT_DIR)): - path = os.path.join(SCRIPT_DIR, name) - if not os.path.isfile(path): - continue - if is_skipped_file(path): - continue - total += extract_from_file(path, prefix, postfixes, **kwargs) - stats.report() - if manifest_entries: - write_manifest(manifest_entries, args.output_dir) - return 0 if total else 1 - - if args.files: - total = 0 - for path in args.files: - if not os.path.isfile(path): - log.error(f"File not found: {path}") - continue - total += extract_from_file(path, prefix, postfixes, **kwargs) - stats.report() - if manifest_entries: - write_manifest(manifest_entries, args.output_dir) - return 0 if total else 1 - - parser.print_help() - return 2 - -if __name__ == "__main__": - raise SystemExit(main())