diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a002263 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.venv +/.vscode +/build +/dist +*.spec +.DS_Store diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..4e3820d --- /dev/null +++ b/build.bat @@ -0,0 +1,2 @@ +pyinstaller -F zte_factroymode.py +pyinstaller -F zte_hardcode_dump.py \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a6e8475 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +requests==2.28.2 +pycryptodome==3.16.0 +pyinstaller==5.7.0 \ No newline at end of file diff --git a/test/hardcode b/test/hardcode new file mode 100644 index 0000000..9094b15 --- /dev/null +++ b/test/hardcode @@ -0,0 +1 @@ +09a01cee5518b341f40d83f1cc5e7c2ac3631ee2fd87c3b85b6b586194cc5486CMCC_F7607P diff --git a/test/hardcodefile/dataprotocol b/test/hardcodefile/dataprotocol new file mode 100755 index 0000000..cebf324 Binary files /dev/null and b/test/hardcodefile/dataprotocol differ diff --git a/test/hardcodefile/dataprotocol.txt b/test/hardcodefile/dataprotocol.txt new file mode 100644 index 0000000..b067c0f --- /dev/null +++ b/test/hardcodefile/dataprotocol.txt @@ -0,0 +1,16 @@ +szDefAESCBCKey=L04&Product@5A238dc79b15726d5c06 +szDefAESCBCIV=ZTE%FN$GponNJ025678b02a85c63c706 +ZJSTBACCOUNT=ZJ.HGU@HGU +ZJSTBPW=CMCCZJHGU +HASTBACCOUNT=HA.HGU@HGU +HASTBPW=CMCCHAHGU +E8_WANCIP_DEFAULT_USER=admin +E8_WANCIP_DEFAULT_CYPHER=admin +E8_WANCIP_DEFAULT_IPTV_AUTH_VAL=CTCDHCP0001 +STRING_OPTION125_JIANGSU_OTT_WAN=JSCMCC-WAN +PPPOEPROXY_PASSWORD=123 +DBDefAESCBCKey=PON_Dkey +DBDefAESCBCIV=PON_DIV +CSP_AESCBCENCRY_PASS_KEY=608158c36497b00221db14afb845c9e3 +CSP_USER_AESCBCENCRY_KEY=8cc72b05705d5c46f412af8cbed55aad +CSP_USER_AESCBCENCRY_IV=667b02a85c61c786def4521b060265e8 \ No newline at end of file diff --git a/test/hardcodefile/gponcfg b/test/hardcodefile/gponcfg new file mode 100755 index 0000000..db04bc9 Binary files /dev/null and b/test/hardcodefile/gponcfg differ diff --git a/test/hardcodefile/gponcfg.txt b/test/hardcodefile/gponcfg.txt new file mode 100644 index 0000000..ede3c74 --- /dev/null +++ b/test/hardcodefile/gponcfg.txt @@ -0,0 +1,6 @@ +DefLoidPwdKey= +DefLoidKey= +DefPasswordKey=123456 +DefVendor_IDKey=ZTEG +DefVendor_SpecificKey=00:00:00:01:00:00 +DefRegisterIDKey=123456 \ No newline at end of file diff --git a/test/hardcodefile/oss.password b/test/hardcodefile/oss.password new file mode 100644 index 0000000..af5bd9b Binary files /dev/null and b/test/hardcodefile/oss.password differ diff --git a/test/hardcodefile/oss.password.txt b/test/hardcodefile/oss.password.txt new file mode 100644 index 0000000..6135929 --- /dev/null +++ b/test/hardcodefile/oss.password.txt @@ -0,0 +1,5 @@ +CMC_FTPUserPassword1=fTPjA%5m +CMC_SambaUserPassword1=sMBjA%5m +TelnetPassword=ZTEG00000001 +CMC_TelnetPasswordDefault=Zte521 +CMC_HYTelnetPasswordDefault=Hyy@521% \ No newline at end of file diff --git a/test/hardcodefile/webpri b/test/hardcodefile/webpri new file mode 100755 index 0000000..37bee91 Binary files /dev/null and b/test/hardcodefile/webpri differ diff --git a/test/hardcodefile/webpri.txt b/test/hardcodefile/webpri.txt new file mode 100644 index 0000000..e3c2430 --- /dev/null +++ b/test/hardcodefile/webpri.txt @@ -0,0 +1,5 @@ +WebHTTPSKey=zte12345!@#$% +WebPrivateKey=Web2019!#%& +WebRepKey=nE%jA@5b +DefAdNewPass=aDm8H%MdA +DefUsNewPass=user \ No newline at end of file diff --git a/test/hardcodefile/wlan b/test/hardcodefile/wlan new file mode 100755 index 0000000..3077851 Binary files /dev/null and b/test/hardcodefile/wlan differ diff --git a/test/hardcodefile/wlan.txt b/test/hardcodefile/wlan.txt new file mode 100644 index 0000000..7cfc4ec --- /dev/null +++ b/test/hardcodefile/wlan.txt @@ -0,0 +1,7 @@ +KeyPassphrase=!@#$%12345 +WapiKey=!@#$%12345 +Key1=11111 +Key2=22222 +Key3=33333 +Key4=44444 +WdsKey=!@#$%12345 \ No newline at end of file diff --git a/zte_factroymode.py b/zte_factroymode.py new file mode 100755 index 0000000..aa7bf85 --- /dev/null +++ b/zte_factroymode.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import requests +import argparse +from random import Random +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad, unpad + + +class WebFac: + AES_KEY_POOL = [ + 0x7B, 0x56, 0xB0, 0xF7, 0xDA, 0x0E, 0x68, 0x52, 0xC8, 0x19, + 0xF3, 0x2B, 0x84, 0x90, 0x79, 0xE5, 0x62, 0xF8, 0xEA, 0xD2, + 0x64, 0x93, 0x87, 0xDF, 0x73, 0xD7, 0xFB, 0xCC, 0xAA, 0xFE, + 0x75, 0x43, 0x1C, 0x29, 0xDF, 0x4C, 0x52, 0x2C, 0x6E, 0x7B, + 0x45, 0x3D, 0x1F, 0xF1, 0xDE, 0xBC, 0x27, 0x85, 0x8A, 0x45, + 0x91, 0xBE, 0x38, 0x13, 0xDE, 0x67, 0x32, 0x08, 0x54, 0x11, + 0x75, 0xF4, 0xD3, 0xB4, 0xA4, 0xB3, 0x12, 0x86, 0x67, 0x23, + 0x99, 0x4C, 0x61, 0x7F, 0xB1, 0xD2, 0x30, 0xDF, 0x47, 0xF1, + 0x76, 0x93, 0xA3, 0x8C, 0x95, 0xD3, 0x59, 0xBF, 0x87, 0x8E, + 0xF3, 0xB3, 0xE4, 0x76, 0x49, 0x88 + ] + + # newrand + AES_KEY_POOL_NEW = [ + 0x8C, 0x23, 0x65, 0xD1, 0xFC, 0x32, 0x45, 0x37, 0x11, 0x28, + 0x71, 0x63, 0x07, 0x20, 0x69, 0x14, 0x73, 0xE7, 0xD4, 0x53, + 0x13, 0x24, 0x36, 0xC2, 0xB5, 0xE1, 0xFC, 0xCF, 0x8A, 0x9A, + 0x41, 0x89, 0x3C, 0x49, 0xCF, 0x5C, 0x72, 0x8C, 0x9E, 0xEB, + 0x75, 0x0D, 0x3F, 0xD1, 0xFE, 0xCC, 0x57, 0x65, 0x7A, 0x35, + 0x21, 0x3E, 0x68, 0x53, 0x7E, 0x97, 0x02, 0x48, 0x74, 0x71, + 0x95, 0x34, 0x53, 0x84, 0xB4, 0xC3, 0xE2, 0xD6, 0x27, 0x3D, + 0xE6, 0x5D, 0x72, 0x9C, 0xBC, 0x3D, 0x03, 0xFD, 0x76, 0xC1, + 0x9C, 0x25, 0xA8, 0x92, 0x47, 0xE4, 0x18, 0x0F, 0x24, 0x3F, + 0x4F, 0x67, 0xEC, 0x97, 0xF4, 0x99 + ] + + def __init__(self, ip, port, user, pw) -> None: + self.ip = ip + self.port = port + self.user = user + self.pw = pw + self.S = requests.Session() + + def reset(self): + # any wrong step request should reset the facTelnetStep + resp = self.S.post(f"http://{self.ip}:{self.port}/webFac", data='SendSq.gch') + if resp.status_code == 400: + return True + return False + + def requestFactoryMode(self): + try: + self.S.post(f"http://{self.ip}:{self.port}/webFac", data='RequestFactoryMode.gch') + except requests.exceptions.ConnectionError: + # this is normal + pass + except Exception as e: + print(e) + + def sendSq(self): + try: + # rand takes from time seconds, range 0-59 + rand = Random().randint(0, 59) + + # the byte after last digital can not be null + resp = self.S.post(f"http://{self.ip}:{self.port}/webFac", data=f'SendSq.gch?rand={rand}\r\n') + if resp.status_code != 200: + return False + print(repr(resp.text)) + + if len(resp.content) == 0: + index = rand + key_pool = WebFacTelnet.AES_KEY_POOL + version = 1 + # new protocol + elif "newrand" in resp.text: + newrand = int(resp.text[len("newrand="):]) + # v62 = (0x1000193 * rand) & 0x3F; + # if ( (int)(0xFEFFFE6D * rand) >= 0 ) + # v62 = -((0xFEFFFE6D * rand) & 0x3F); + # sub_2AF88((v62 ^ newrand) % 60, AES_KEY) + index = ((0x1000193 * rand) & 0x3F ^ newrand) % 60 + key_pool = WebFacTelnet.AES_KEY_POOL_NEW + version = 2 + else: + print("protocol version not match") + return False + + key = map(lambda x: (x ^ 0xA5) & 0xFF, key_pool[index:index+24]) + key = bytes(key) + + self.chiper = AES.new(key, AES.MODE_ECB) + return version + except requests.exceptions.ConnectionError: + print("protocol version not match?") + except Exception as e: + print(e) + return False + + def sendInfo(self): + try: + # take from time seconds, range 0-59 + resp = self.S.post(f"http://{self.ip}:{self.port}/webFacEntry", + data=self.chiper.encrypt(pad(f'SendInfo.gch?info=6|'.encode(), 16))) + print(resp.status_code, repr(resp.text)) + if resp.status_code == 200: + return True + elif resp.status_code == 400: + print("protocol error") + elif resp.status_code == 401: + print("user/pass error") + except Exception as e: + print(e) + return False + + def checkLoginAuth(self): + try: + resp = self.S.post( + f"http://{self.ip}:{self.port}/webFacEntry", + data=self.chiper.encrypt( + pad(f'CheckLoginAuth.gch?version50&user={self.user}&pass={self.pw}'.encode(), 16) + )) + print(repr(resp.text)) + if resp.status_code == 200: + # resp should be "FactoryMode.gch" + url = self.chiper.decrypt(resp.content) + return url + elif resp.status_code == 400: + print("protocol error") + elif resp.status_code == 401: + print("user/pass error") + except requests.exceptions.ConnectionError: + print("wrong step?") + except Exception as e: + print(e) + return False + + +class WebFacSerial(WebFac): + def __init__(self, ip, port, user, pw) -> None: + super().__init__(ip, port, user, pw) + + def serialSlience(self, action): + try: + resp = self.S.post( + f"http://{self.ip}:{self.port}/webFacEntry", + data=self.chiper.encrypt( + pad(f'SerialSlience.gch?action={action}'.encode(), 16) + )) + print(repr(resp.text)) + if resp.status_code == 200: + return True + elif resp.status_code == 400: + print("protocol error") + except Exception as e: + print(e) + return False + + +class WebFacTelnet(WebFac): + def __init__(self, ip, port, user, pw) -> None: + super().__init__(ip, port, user, pw) + + def factoryMode(self, action): + try: + if action == 'close': + resp = self.S.post( + f"http://{self.ip}:{self.port}/webFacEntry", + data=self.chiper.encrypt( + pad(f'FactoryMode.gch?{action}'.encode(), 16) + )) + else: + resp = self.S.post( + f"http://{self.ip}:{self.port}/webFacEntry", + data=self.chiper.encrypt( + pad(f'FactoryMode.gch?mode=2&user=notused'.encode(), 16) + )) + print(repr(resp.text)) + if resp.status_code == 200: + # resp should be "FactoryModeAuth.gch?user=&pass=" + url = self.chiper.decrypt(resp.content) + return url + elif resp.status_code == 400: + print("protocol error") + elif resp.status_code == 401: + print("user/pass error") + except requests.exceptions.ConnectionError as e: + print(e) + print("wrong step?") + except Exception as e: + print(e) + return False + + +def dealSerial(ip, port, user, pw, action): + + serial = WebFacSerial(ip, port, user, pw) + print("reset facTelnetSteps:") + if serial.reset(): + print("reset OK") + + print("\nfacStep 1:") + serial.requestFactoryMode() + + print("\nfacStep 2:") + version = serial.sendSq() + + if version == 1: + print("\nfacStep 3:") + serial.checkLoginAuth() + elif version == 2: + print("\nfacStep 3:") + if not serial.sendInfo(): + print("sendInfo error") + return + print("\nfacStep 4:") + serial.checkLoginAuth() + + print("\nfacStep 5:") + serial.serialSlience(action) + + +def dealTelnet(ip, port, user, pw, action): + + telnet = WebFacTelnet(ip, port, user, pw) + print("reset facTelnetSteps:") + if telnet.reset(): + print("reset OK") + + print("\nfacStep 1:") + telnet.requestFactoryMode() + + print("\nfacStep 2:") + version = telnet.sendSq() + + if version == 1: + print("\nfacStep 3:") + if telnet.checkLoginAuth(): + print("\nfacStep 4:") + elif version == 2: + print("\nfacStep 3:") + if not telnet.sendInfo(): + print("sendInfo error") + return + print("\nfacStep 4:") + telnet.checkLoginAuth() + else: + pass + + print("\nfacStep 5:") + print(telnet.factoryMode(action)) + + +def parseArgs(): + parser = argparse.ArgumentParser(prog='zte_factroymode', usage='https://github.com/douniwan5788/zte_modem_tools', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('ip', nargs="?", help='route ip', default="192.168.1.1") + parser.add_argument('port', nargs="?", help='router http port', default=80) + parser.add_argument('--user', help='factorymode auth username', default='factorymode') + parser.add_argument('--pw', help='factorymode auth password', default='nE%jA@5b') + subparsers = parser.add_subparsers(dest='cmd', title='subcommands', + description='valid subcommands', + help='supported commands') + telnet_parser = subparsers.add_parser("telnet", help='control telnet services on/off', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + telnet_parser.add_argument('action', nargs="?", choices=['open', 'close'], help='action', default='open') + serial_parser = subparsers.add_parser("serial", help='control /proc/serial on/off', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + serial_parser.add_argument('action', nargs="?", choices=['open', 'close'], help='action', default='open') + return parser.parse_args() + + +def main(): + args = parseArgs() + # print(args) + if args.cmd == 'serial': + dealSerial(args.ip, args.port, args.user, args.pw, args.action) + elif args.cmd == 'telnet': + dealTelnet(args.ip, args.port, args.user, args.pw, args.action) + + +if __name__ == '__main__': + main() diff --git a/zte_hardcode_dump.py b/zte_hardcode_dump.py new file mode 100755 index 0000000..73cdc82 --- /dev/null +++ b/zte_hardcode_dump.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import argparse +from typing import BinaryIO, List + +from struct import pack, unpack +from Crypto.Cipher import AES +from Crypto.Hash import SHA256 +from Crypto.Util.Padding import pad, unpad + + +# for (i=0 +# i != 64 +# + +i) +# { +# if ((unsigned int)(i - 5) <= 15) +# { +# v13 = &place_holder[index_pre10++] +# * (v13 - 640) = hardcode_key[i] + 3 +# // prefix_key +# } +# v19 = i - 7 +# v20 = (unsigned int)(i - 7) > 31 +# if ((unsigned int)(i - 7) <= 31) +# { +# v13 = &place_holder[v16++] +# v19 = hardcode_key[i] +# } +# if (!v20) +# * (v13 - 576) = v19 + 1 +# } + + +def ascii_offset(s, offset): + l = [] + for b in s: + l.append(b + offset) + return bytes(l) + + +def dump(hardcoded, hardcodefiles: List[BinaryIO]): + aes_key_phrase = ascii_offset(hardcoded[5:21], 3) + hardcoded[64:] + aes_iv_phrase = ascii_offset(hardcoded[7:39], 1) + + aes_key = SHA256.new(aes_key_phrase).digest() + aes_iv = SHA256.new(aes_iv_phrase).digest()[:16] + # print(aes_key.hex(), aes_iv.hex()) + + for f in hardcodefiles: + print(f"\ndecrypting {f.name}") + header = f.read(4*15) + magic1, magic2, *_ = unpack(">" + 'I'*15, header) + if magic1 != 0x01020304 or magic2 != 0x00000003: + print(f"{f.name} is not a hardcode config file, skip") + continue + has_next = True + with open(f'{f.name}.txt', "wb") as t: + aes_chiper = AES.new(aes_key, mode=AES.MODE_CBC, iv=aes_iv) + + while has_next: + plaintext_length, chiphertext_length, has_next = unpack(">III", f.read(4*3)) + plaintext = aes_chiper.decrypt(f.read(chiphertext_length))[:plaintext_length] + t.write(plaintext) + + +def parseArgs(): + parser = argparse.ArgumentParser(prog='zte_hardcode_dump', usage='https://github.com/douniwan5788/zte_modem_tools', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('hardcode', help='the /etc/hardcodefile file which contains root key', + type=argparse.FileType('rb')) + parser.add_argument('hardcodefile', nargs="+", help='config files under /etc/hardcodefile', + type=argparse.FileType('rb')) + return parser.parse_args() + + +def main(): + args = parseArgs() + # print(args) + + dump(args.hardcode.readline().strip(), args.hardcodefile) + print('done') + + +if __name__ == '__main__': + main()