├── README.md └── balong-signcheck.py /README.md: -------------------------------------------------------------------------------- 1 | Balong SignCheck 2 | ================ 3 | 4 | Balong SignCheck validates Secure Boot (a.k.a. `secuboot`) signature of Huawei LTE routers and modems based on Balong V7R11 (E3372h, E5573, E5577, E5770, E8372, etc) and V7R5 (B612s, B618s, B715s) platforms, and prints MD5 hash which should be/is programmed in efuse group0-3 for HiSilicon chip to boot in secuboot mode. 5 | 6 | The utility accepts usbloader/M3Boot firmware partition/mtdblock0 flash image dump as input. 7 | 8 | Example output: 9 | ``` 10 | $ ./balong_signcheck.py usbloader-5573cs-322.bin 11 | Huawei Balong V7R11/V7R5 LTE modems firmware/usbloader signature tool 12 | https://github.com/Huawei-LTE-routers-mods 13 | NOTE: everything in the image is stored in little-endian, either fully or by 32 bits. 14 | 15 | Sec_image_len: 3412 16 | Checking Root CA… 17 | ⇒ MD5 sha256hmac: 973900451a9d22682c9067ec7a0b24f4 18 | ⇒ Found KNOWN HASH(2): E5577s/Bs .76 STC / E3372s-153 .161 Beeline / E8372s-153 .306 Zong/Warid / E8372h-608 .274 Telenor / E5573s-320 .306 Zong / E5573cs .274 Telenor 19 | 20 | Efuse group0: 45003997 21 | Efuse group1: 68229d1a 22 | Efuse group2: ec67902c 23 | Efuse group3: f4240b7a 24 | Efuses are shown little-endian, as printed by bsp_efuse_show 25 | 26 | Checking OEM CA… 27 | Checking data signature with OEM CA… 28 | ⇒ All OK, hashes match, M3Boot/usbloader.bin is signed correctly 29 | NOTE: if you loaded usbloader, this utility checks only signature of the first partition (raminit). 30 | ``` 31 | -------------------------------------------------------------------------------- /balong-signcheck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ''' 4 | Huawei Balong LTE modems firmware/usbloader signature tool, by ValdikSS , 2020 5 | 6 | Huawei's Balong V7R11 Secuboot works as follows: 7 | 1. 260 byte of "\x20\x00\x00\x04" + RSA public exponent (e) + RSA public modulus (n) (the ROOT KEY) 8 | is hashed with SHA256-HMAC with empty key. This ROOT KEY is located at offset 580 (dec) 9 | from the beginning of the flash. This corresponds to "M3Boot" partition in the firmware (offset 10 | is also 580). 11 | 2. Hash from step 1 is hashed with MD5. 12 | 3. MD5 hash is checked against efuse groups 0-3 (each group is 32 bit). 13 | 4. If this check is successful, the OEM KEY is checked if it's signed with ROOT KEY. 14 | OEM KEY is stored in sec_image_len (offset 576) + 128, the signature of it is stored in 15 | sec_image_len + 396. 16 | 5. If the check above is successful, then full M3Boot partition data SHA256HMAC-hash is 17 | checked against OEM KEY-signed data (it's stored right after sec_image_len, 128 bytes). 18 | 6. If all the checks above are successful, the booting IN THE BOOTROM continues. 19 | There are other signature checks after the bootrom passed the execution to another file. 20 | All of them are the same and either use bootrom functions directly (when its memory is still 21 | mapped), or re-implement the exact same check. 22 | 23 | Efuses could be printed with: 24 | ecall bsp_efuse_show 25 | dmesg 26 | 27 | Example from E8372s-153 21.333.64.00.1456 (ZONG BOLT+): 28 | 29 | [efuse]: efuse group0 value = 0xa4a62a27. \ 30 | [efuse]: efuse group1 value = 0x36e38101. | ⇒ ROOT KEY MD5 HASH (272aa6a40181e33667c9e80dae1544e5) 31 | [efuse]: efuse group2 value = 0xde8c967. | 32 | [efuse]: efuse group3 value = 0xe54415ae. / 33 | [efuse]: efuse group4 value = 0x5b0. \ // cust id / operator id / HUK. Value: 1456 34 | [efuse]: efuse group5 value = 0x0. | 35 | [efuse]: efuse group6 value = 0x0. | ⇒ Used in create_crypto_key_o vxworks function (possible for HWLOCK) 36 | [efuse]: efuse group7 value = 0x0. / 37 | [efuse]: efuse group8 value = 0x28109d92. \ 38 | [efuse]: efuse group9 value = 0x249. | 39 | [efuse]: efuse group10 value = 0x243e1920. | ⇒ group_dieid + chipid (DRV_GET_DIEID in vxworks/linux) 40 | [efuse]: efuse group11 value = 0xc0002c1a. | // AES (vxworks), probably used for VSIM feature (efuseWriteAes and efuseReadAes in vxworks) 41 | [efuse]: efuse group12 value = 0x3ff. / 42 | [efuse]: efuse group13 value = 0x1443f801. // efuse_secboot_id + anti-downgrade byte (see mbb_kernel_secboot_id_check linux function) 43 | [efuse]: efuse group14 value = 0x8b. 44 | [efuse]: efuse group15 value = 0x18. // boot_sel + secboot_en 45 | ''' 46 | 47 | import sys 48 | import hashlib 49 | import hmac 50 | 51 | DEBUG = False 52 | 53 | ''' 54 | HashCalc calculates SHA256-HMAC with empty key, by 512 byte blocks, reusing previous 55 | hash result as a key for further blocks, as does Huawei. 56 | ''' 57 | def HashCalc(data): 58 | hm = b"" 59 | hm_key = b"" 60 | if not data: 61 | return False 62 | for i in range(0, len(data), 512): 63 | hm = hmac.new(hm_key, data[i:i+512], hashlib.sha256) 64 | hm_key = hm.digest() 65 | if DEBUG: 66 | print("HashCalc:", hm.digest().hex()) 67 | return hm.digest() 68 | 69 | ''' 70 | RSACalc performs RSA "encryption" in RAW format to decrypt signature data. 71 | ''' 72 | def RSACalc(pubkey, signature): 73 | class pkey: 74 | e = int.from_bytes(pubkey[0], byteorder='little') 75 | n = int.from_bytes(pubkey[1], byteorder='little') 76 | secret_message_long = int.from_bytes(signature, byteorder='little') 77 | verify_long = pow(secret_message_long, pkey.e, pkey.n) 78 | verify_bytes = verify_long.to_bytes(32, byteorder='little') 79 | if DEBUG: 80 | print("RSACalc:", verify_bytes.hex()) 81 | return verify_bytes 82 | 83 | ''' 84 | data_sigcheck calculates Huawei signature and compares it against stored signed data. 85 | ''' 86 | def data_sigcheck(data, pubkey, signature): 87 | hashed = HashCalc(data) 88 | rsa = RSACalc(pubkey, signature) 89 | if hashed == rsa: 90 | if DEBUG: 91 | print("data_sigcheck: hashes match!") 92 | return True 93 | else: 94 | if DEBUG: 95 | print("data_sigcheck: hashes DO NOT match!!!") 96 | return False 97 | 98 | 99 | class BalongSecImage: 100 | class BalongNotValidImage(Exception): 101 | pass 102 | class BalongUnsupportedImage(Exception): 103 | pass 104 | class BalongSigException(Exception): 105 | pass 106 | 107 | def __init__(self, data): 108 | USBLOADER_LEN = 84 109 | SEC_IMAGE_LEN_OFFSET = 576 110 | SEC_IMAGE_LEN_LEN = 4 111 | ROOT_CA_OFFSET = 580 112 | ROOT_CA_LEN = 260 113 | OEM_CA_LEN_V7R11 = 268 114 | OEM_CA_LEN_V7R5 = 268 + 128 115 | CA_E_OFFSET = 4 116 | CA_N_OFFSET = 4 + 128 117 | CA_ELEM_LEN = 128 118 | SIGNATURE_LEN = 128 119 | 120 | self.data = data 121 | self.sec_image_len = None 122 | self.root_ca = None 123 | self.root_ca_e = None 124 | self.root_ca_n = None 125 | self.oem_ca = None 126 | self.oem_ca_e = None 127 | self.oem_ca_n = None 128 | self.data_signature = None 129 | self.oem_root_sign_data = None 130 | self.is_balong_v7r5 = False 131 | 132 | # Check if the image is usbloader 133 | if data[0:4] == b"\x00\x00\x02\x00": # usbloader 134 | data = data[USBLOADER_LEN:] # strip usbloader header 135 | self.data = data 136 | if data[4708:4712] == b"Copy": 137 | raise self.BalongUnsupportedImage("Balong V7R22 images are not supported yet!") 138 | if data[1000:1004] == b"Copy": # Check Balong V7R5 copyright message 139 | self.is_balong_v7r5 = True 140 | if data[872:876] != b"Copy" and not self.is_balong_v7r5: 141 | raise self.BalongNotValidImage("Not an M3Boot/mtdblock0/usbloader file!") 142 | 143 | self.sec_image_len = int.from_bytes(data[SEC_IMAGE_LEN_OFFSET:SEC_IMAGE_LEN_OFFSET+SEC_IMAGE_LEN_LEN], 'little') 144 | # This is where ROOT KEY is stored (the key from efuse group0-3) 145 | # 260 bytes: 4 bytes of garbage + RSA e + RSA n 146 | # (actually 4 bytes is the size, but it's not used anywhere) 147 | # ROOT KEY is RSA 1024 bit. 148 | self.root_ca = data[ROOT_CA_OFFSET:ROOT_CA_OFFSET+ROOT_CA_LEN] 149 | 150 | if not self.sec_image_len or self.root_ca == b"\x00" * ROOT_CA_LEN: 151 | raise self.BalongSigException("No signature found. The image is not signed (no secuboot/efuse).") 152 | if self.sec_image_len & 3: 153 | raise self.BalongSigException("sec_image_len & 3 is true, sanity check fail. The image is not signed?") 154 | if self.sec_image_len + 524 > 61440: 155 | raise self.BalongSigException("sec_image_len length > 61440, error!") 156 | 157 | # 128 byte DATA signature, right after the M3Boot code, signed with OEM KEY. 158 | self.data_signature = data[self.sec_image_len:self.sec_image_len+SIGNATURE_LEN] 159 | 160 | # This is where OEM KEY is stored, 268 bytes (V7R11) or 268 + 128 bytes (V7R5). 161 | # 4 bytes of garbage + RSA e + RSA n + 8 garbage "FF FF FF FF 00 00 00 00" bytes (+ another 128 bytes for V7R5) 162 | # OEM KEY is RSA 1024 bit. 163 | oem_ca_len = OEM_CA_LEN_V7R5 if self.is_balong_v7r5 else OEM_CA_LEN_V7R11 164 | self.oem_ca = data[ 165 | self.sec_image_len+len(self.data_signature) 166 | :self.sec_image_len+len(self.data_signature)+oem_ca_len 167 | ] 168 | 169 | # OEM KEY signature. 170 | self.oem_root_sign_data = data[ 171 | self.sec_image_len+len(self.data_signature)+len(self.oem_ca) 172 | :self.sec_image_len+len(self.data_signature)+len(self.oem_ca)+SIGNATURE_LEN 173 | ] 174 | 175 | # ROOT and OEM public exponent and modulus. 176 | self.root_ca_e = self.root_ca[CA_E_OFFSET:CA_E_OFFSET+CA_ELEM_LEN] 177 | self.root_ca_n = self.root_ca[CA_N_OFFSET:CA_N_OFFSET+CA_ELEM_LEN] 178 | self.oem_ca_e = self.oem_ca[CA_E_OFFSET:CA_E_OFFSET+CA_ELEM_LEN] 179 | self.oem_ca_n = self.oem_ca[CA_N_OFFSET:CA_N_OFFSET+CA_ELEM_LEN] 180 | 181 | 182 | def main(): 183 | print("Huawei Balong V7R11/V7R5 LTE modems firmware/usbloader signature tool") 184 | print("https://github.com/Huawei-LTE-routers-mods") 185 | print("NOTE: everything in the image is stored in little-endian, either fully or by 32 bits.") 186 | print() 187 | if len(sys.argv) != 2: 188 | print("Usage: {} ".format(sys.argv[0])) 189 | return 1 190 | 191 | SEC_IMAGE_LEN_MAX = 65535 192 | 193 | mtdblock = open(sys.argv[1], "rb").read(SEC_IMAGE_LEN_MAX) 194 | try: 195 | bimage = BalongSecImage(mtdblock) 196 | except BalongSecImage.BalongNotValidImage as e: 197 | print(e) 198 | sys.exit(1) 199 | except BalongSecImage.BalongUnsupportedImage as e: 200 | print(e) 201 | sys.exit(2) 202 | except BalongSecImage.BalongSigException as e: 203 | print(e) 204 | sys.exit(3) 205 | 206 | # Copy data from BalongSecImage if usbloader header was stripped 207 | mtdblock = bimage.data 208 | print("Sec_image_len:", bimage.sec_image_len) 209 | print("Checking Root CA…") 210 | hm = HashCalc(bimage.root_ca) 211 | md5 = hashlib.md5() 212 | md5.update(hm) 213 | md5hexdigest = md5.hexdigest() 214 | print("⇒ MD5 sha256hmac:", md5hexdigest) 215 | if md5hexdigest == '272aa6a40181e33667c9e80dae1544e5': 216 | print("⇒ Found KNOWN HASH(1): E8372h-608 .1460 Telenor / E8372h-153 .1456 Zong / E5573Cs-322 .1456 Zong") 217 | elif md5hexdigest == '973900451a9d22682c9067ec7a0b24f4': 218 | print("⇒ Found KNOWN HASH(2): E5577s/Bs .76 STC / E3372s-153 .161 Beeline / E8372s-153 .306 Zong/Warid / E8372h-608 .274 Telenor / E5573s-320 .306 Zong / E5573cs .274 Telenor") 219 | elif md5hexdigest == '1cbb16c5bad8b08ead3268ef4b94c908': 220 | print("⇒ Found KNOWN HASH(3): E5577x / E5573x / E5572 / B618 / E3372h-320 new -sec firmware") 221 | elif md5hexdigest == 'bd912dddbd82f798e80ea1e08148998e': 222 | print("⇒ Found KNOWN HASH(4): E8372h-320 new -sec firmware") 223 | else: 224 | print("⇒ Found UNKNOWN HASH!") 225 | 226 | print() 227 | efuses = int.from_bytes(md5.digest(), 'big').to_bytes(32, 'little').hex() 228 | print("Efuse group0:", efuses[24:32]) 229 | print("Efuse group1:", efuses[16:24]) 230 | print("Efuse group2:", efuses[8:16]) 231 | print("Efuse group3:", efuses[0:8]) 232 | print("Efuses are shown little-endian, as printed by bsp_efuse_show") 233 | 234 | print() 235 | print("Checking OEM CA…") 236 | 237 | r1 = data_sigcheck( 238 | (bimage.oem_ca), 239 | (bimage.root_ca_e, bimage.root_ca_n), 240 | bimage.oem_root_sign_data 241 | ) 242 | 243 | print("Checking data signature with OEM CA…") 244 | 245 | r2 = data_sigcheck( 246 | mtdblock[0:bimage.sec_image_len], 247 | (bimage.oem_ca_e, bimage.oem_ca_n), 248 | bimage.data_signature 249 | ) 250 | 251 | if not r1: 252 | print("⇒ OEM CA hashes do not match, file is NOT signed correctly!") 253 | return 4 254 | if not r2: 255 | print("⇒ Data signature hashes do not match, file is NOT signed correctly!") 256 | return 4 257 | 258 | print("⇒ All OK, hashes match, M3Boot/usbloader.bin is signed correctly") 259 | print("NOTE: if you loaded usbloader, this utility checks only signature of the first partition (raminit).") 260 | return 0 261 | 262 | if __name__ == '__main__': 263 | sys.exit(main()) 264 | --------------------------------------------------------------------------------