-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 KL-001-2020-003 : Cellebrite EPR Decryption Relies on Hardcoded AES Key Material Title: Cellebrite EPR Decryption Relies on Hardcoded AES Key Material Advisory ID: KL-001-2020-003 Publication Date: 2020.06.29 Publication URL: https://korelogic.com/Resources/Advisories/KL-001-2020-003.txt 1. Vulnerability Details Affected Vendor: Cellebrite Affected Product: UFED Affected Version: 5.0 - 7.5.0.845 Platform: Embedded Windows CWE Classification: CWE-321: Hardcoded Use of Cryptography Keys CVE ID: CVE-2020-14474 2. Vulnerability Description The Cellebrite UFED Physical device relies on key material hardcoded within both the executable code supporting the decryption process and within the encrypted files themselves by using a key enveloping technique. The recovered key material is the same for every device running the same version of the software and does not appear to be changed with each new build. It is possible to reconstruct the decryption process using the hardcoded key material and obtain easy access to otherwise protected data. 3. Technical Description A recursive listing of my standalone decryptor directory: $ find . . ./decrypt-epr ./input ./input/DLLs ./input/DLLs/731 ./input/DLLs/731/FileUnpacking.dll ./input/EPRs ./input/EPRs/731 ./input/EPRs/731/Android.zip.epr ./output ./output/EPRs ./output/EPRs/731 ./extract-keys ./Makefile (See the Proof of Concept section for relevant code snippets.) First, we start by running the extract-keys script on the relevant FileUnpacking.dll file. The provided Makefile will automatically output the relevant key material to the same directory where the DLL resides. $ make keys Extracting AES keys from input/DLLs/731/FileUnpacking.dll 64+0 records in 64+0 records out 64 bytes copied, 0.000186032 s, 344 kB/s 32+0 records in 32+0 records out 32 bytes copied, 0.000116104 s, 276 kB/s 636+0 records in 636+0 records out 636 bytes copied, 0.00140342 s, 453 kB/s Finished The extract-keys script contains a nested JSON-object and iterates over the bytes of the file provided creating a SHA256 hash for each DWORD. The calculated hash is compared against known matches and when found the script will automatically extract the bytes relevant. Now a selected EPR file may be decrypted. A good example is the Android.zip.epr file, which contains a set of local privilege escalation exploits. $ ./decrypt-epr --verbose --file input/EPRs/731/Android.zip.epr [+] The EPR file specified exists. [+] The specified EPR file has been read into memory. [-] Decrypter setup with key 1 for version 3 [+] Round one of the EPR decryption completed successfully. [-] Calculated that the flag will be: [REDACTED] [+] The SHA256 key flag has been calculated. [-] Found the flag: [REDACTED] [+] The SHA256 key flag has been found. [-] Decrypter setup with key 2 for version 3 [+] Round two of the EPR decryption completed successfully. Obtained the final AES key and IV. [-] AES Key: [REDACTED], IV: [REDACTED] [-] Decrypter setup with key 3 for version 3 [-] Finished decrypting all blocks. [-] Writing bytes to: input/EPRs/731/Android.zip.epr.broken [-] Wrote 2552640 bytes to a broken file. [+] Round three of the EPR decryption completed successfully. The encrypted zip archive has been decrypted. [-] Running: zip -FF input/EPRs/731/Android.zip.epr.broken --out input/EPRs/731/Android.zip.epr.zip > /dev/null 2>&1 [-] Removing the broken file. [+] Decrypted file available at output/EPRs/731/Android.zip.epr.zip [+] done. The decrypted file can then be unzipped. $ unzip Android.zip.epr.zip Archive: Android.zip.epr.zip inflating: c2a_disable_selinux_32.ko inflating: c2a_disable_selinux_64.ko inflating: com.mr.meeseeks.apk inflating: daemonize inflating: dirtycow inflating: dirtycow_32 inflating: DisableHuaweiLogging_2.1.5767a inflating: django_2.1.5767a inflating: EnableHuaweiLogging_2.1.5767a inflating: EnableSharpRead_2.1.5767a inflating: exploits_2.1.5769.csv inflating: forensics inflating: fourrunnerStatic_2.1.5767a inflating: gb_2.1.5767a inflating: nandd inflating: nandread-pie-vold inflating: nandread-pie_7182 inflating: nandread64-pie-vold inflating: nandreadStatic_7182 inflating: patcher.exe inflating: pingroot inflating: pingroot_vultest inflating: psneuter_2.1.5767a inflating: RecoveryImageMap.csv inflating: rootspotter.apk inflating: rootspot_verify_env inflating: rosecure_2.1.5767a inflating: setuid_2.1.5767a inflating: shellcode.bin inflating: shellcode_32_iptables.bin inflating: shellcode_32_oatdump.bin inflating: zergRush_2.1.5767a The encryption algorithm uses a software-only key enveloping technique where part of the key material is stored within executable code and part within a encrypted header inside of the encrypted file. The encrypted header is extracted from the encrypted file and decrypted using key material hardcoded within executable code. Some of the bytes decrypted then undergo a XOR operation to calculate the last DWORD of a SHA256 hash. Separately, a set of 254 bytes is iterated over using 64 bytes per iteration. A complete SHA256 hash is generated for each set of 64-bytes and the ending DWORD of this hash is then compared against the calculated DWORD. If there is a match the bytes used to calculate the DWORD are the next set of key material. The decryption tool outputs the following match: [-] Calculated that the flag will be: [REDACTED] [+] The SHA256 key flag has been calculated. [-] Found the flag: [REDACTED] The last DWORD matches. In fact there are a total of eight possible intermediate keys that can be chosen from based on the bytes observed. A third and final key exists within each encrypted file header. This key is decrypted using the hardcoded intermediate key used for encrypted the selected file. From here bytes 0x80 through the end of the file are decrypted in blocks of 0x10000. 4. Mitigation and Remediation Recommendation The vendor has informed KoreLogic that this vulnerability is not present on recent versions of the UFED devices. Cellebrite stated, "While the method described in the reports does not work on recent versions (we previously made multiple changes that broke it), the core key material was exposed and will be rotated effective immediately." 5. Credit This vulnerability was discovered by Matt Bergin (@thatguylevel) of KoreLogic, Inc. 6. Disclosure Timeline 2020.04.02 - KoreLogic submits vulnerability details to Cellebrite. 2020.04.02 - Cellebrite acknowledges receipt and the intention to investigate. 2020.05.13 - KoreLogic requests an update on the status of the vulnerability report. 2020.05.14 - Cellebrite responds, notifying KoreLogic that the technique is not applicable to newer UFED releases. Requests time beyond the standard 45 business day embargo to ensure all exposed keys have been changed. 2020.06.09 - 45 business days have elapsed since the report was submitted to Cellebrite. 2020.06.12 - KoreLogic requests an update from Cellebrite. 2020.06.14 - Cellebrite reports that affected key material has been retired. 2020.06.18 - CVE Requested. 2020.06.19 - MITRE issues CVE-2020-14474. 2020.06.29 - KoreLogic public disclosure. 7. Proof of Concept File Name: Makefile clean: for filepath in `find input/DLLs -type f -name '*.keys' -o -name '*.aes' -o -name '*.iv' -o -name '*.map' -o -name '*.zip'`; do \ rm -rf $$filepath ; \ done keys: @for filepath in `find input/DLLs -type f -name '*.dll'` ; do \ echo Extracting AES keys from $$filepath ; \ ./extract-keys --file $$filepath > $$filepath.keys ; \ if [ -f "$$filepath" ] ; then \ dd bs=1 if=$$filepath.keys count=64 of=$$filepath.aes ; \ dd bs=1 if=$$filepath.keys count=32 skip=64 of=$$filepath.iv ; \ dd bs=1 if=$$filepath.keys skip=96 of=$$filepath.map ; \ else \ echo Could not find extract-keys output ; \ fi \ done ; \ echo Finished Script Name: extract-keys #!/usr/bin/python from optparse import OptionParser from os.path import exists, basename from binascii import hexlify from hashlib import sha256 from os import makedirs keyMap = { # UFED 5.1 "Dump_MotGSM.dll":{ "offsets":{ "aes":{ "key":"0e282e124bb8af53357f7e8cb3460a23c94def3fe4f181a57c9fcba3f5f7f054", # Key and IV already public information "iv":"888c609edc9eb9dfb4d30dfebc9f0431" # https://github.com/cellebrited/cellebrite } } }, # UFED 7.3 "FileUnpacking.dll":[ { "offsets":{ "aes":{ "keySize":32, "keyHash":"[REDACTED]", # sha256 hash of first dword "ivSize":16, "ivHash":"[REDACTED]" # sha256 hash of first dword }, "mapSize":256, "mapHash":"[REDACTED]" # sha256 hash of first dword } } ] } if __name__ == "__main__": parser = OptionParser() parser.add_option("--file",dest="file",default='',help="Decryptor DLL") o,a = parser.parse_args() if (exists(o.file) != True): print "[!] The specified file does not exist" exit(1) try: with open(o.file,'rb') as fp: fileData = fp.read() print "[-] Read {} bytes.".format(len(fileData)) if (isinstance(keyMap[basename(o.file)], str)): if ("Dump_MotGSM.dll" == basename(o.file)): print keyMap[basename(o.file)]["offsets"]["aes"]["key"] + keyMap[basename(o.file)]["offsets"]["aes"]["iv"] else: foundKey, foundIV, foundMap = False, False, False for i in xrange(0, len(keyMap[basename(o.file)])): for pos in xrange(0,len(fileData)): nextDWORD = hexlify(fileData[pos:pos+4]) if (sha256(nextDWORD).hexdigest() == keyMap[basename(o.file)][i]["offsets"]["aes"]["keyHash"] and not foundKey): foundKey = True aesKey = hexlify(fileData[pos:pos+32]) print "[+] Found key at {}. Value: {}".format(hex(pos),aesKey) if (sha256(nextDWORD).hexdigest() == keyMap[basename(o.file)][i]["offsets"]["aes"]["ivHash"] and not foundIV): foundIV = True aesIV = hexlify(fileData[pos:pos+16]) print "[+] Found IV at {}. Value: {}".format(hex(pos),aesIV) if (sha256(nextDWORD).hexdigest() == keyMap[basename(o.file)][i]["offsets"]["mapHash"] and not foundMap): foundMap = True aesMap = hexlify(fileData[pos:pos+keyMap[basename(o.file)][i]["offsets"]["mapSize"]]) print "[+] Found map at {}. Value: {}".format(hex(pos),aesMap) if (foundKey and foundIV and foundMap): break pos+=1 except Exception as e: print "[!] Could not read the specified file. Reason: {}".format(e) exit(0) Script Name: decrypt-epr #!/usr/bin/python from logging.handlers import TimedRotatingFileHandler from optparse import OptionParser from os.path import exists, getsize, dirname, realpath from os.path import join as path_join from os import system, remove from shutil import move from Crypto.Cipher import AES from binascii import unhexlify, hexlify from hashlib import sha256 import sys import logging logging.basicConfig( format="%(asctime)s [%(levelname)s] %(message)s", level=logging.INFO, handlers=[ TimedRotatingFileHandler( path_join( dirname(realpath(__file__)), "logger.log", ), interval=1, ), logging.StreamHandler(sys.stdout), ], ) logger = logging.getLogger(__name__) bs = AES.block_size pad = lambda s: s + (bs - len(s) % bs) * chr(bs - len(s) % bs) class EPR: def __init__(self, file, version, verbose): self.epr_v1_aes_key = "0e282e124bb8af53357f7e8cb3460a23c94def3fe4f181a57c9fcba3f5f7f054" # Already public information self.epr_v1_aes_iv = "888c609edc9eb9dfb4d30dfebc9f0431" # Already public information self.epr_v2_aes_key = "[REDACTED]" self.epr_v2_aes_iv = "[REDACTED]" self.epr_v3_aes_key = self.epr_v2_aes_key self.epr_v3_aes_iv = self.epr_v2_aes_iv self.epr_v2_aes_map = "[REDACTED]" self.epr_v3_aes_map = "[REDACTED]" self.epr_v3_aes_iv_two = None self.file = file or False self.version = version self.encrypted_file = None self.encrypted_epr = None self.encrypted_magic = None self.decrypted_epr = None self.final_epr = b'' self.logging = verbose def file_exists(self): if not self.file: return False return exists(self.file) def can_read_file(self): return getsize(self.file) def read_entire_file(self): try: fp = open(self.file,'rb') self.encrypted_file = fp.read() fp.close() except Exception as e: logger.error("[!] Encountered an exception. Reason: {}".format(e)) return False return True def flat_decrypt(self): self.encrypted_magic = self.encrypted_file[:21] if (self.encrypted_magic[:-2] == "Cellebrite EPR File"): self.encrypted_epr = self.encrypted_file[21:] if self.version == 1: crypter = AES.new(unhexlify(self.epr_v1_aes_key),AES.MODE_CBC,unhexlify(self.epr_v1_aes_iv)) if self.logging: logger.info("[-] Decrypter setup with key 1 for version {}".format(self.version)) else: crypter = AES.new(unhexlify(self.epr_v3_aes_key),AES.MODE_CBC,unhexlify(self.epr_v3_aes_iv)) if self.logging: logger.info("[-] Decrypter setup with key 1 for version {}".format(self.version)) try: self.decrypted_epr = crypter.decrypt(self.encrypted_epr) if self.version == 2: self.epr_v2_aes_iv_two = hexlify(self.decrypted_epr[32:48]) elif self.version == 3: self.epr_v3_aes_iv_two = hexlify(self.decrypted_epr[32:48]) else: pass except Exception as e: logger.error("[!] Encountered an exception. Reason: {}".format(e)) return False return True return False def calc_sha256_dword(self): try: to_xor_a = hexlify(self.decrypted_epr[24:28]) to_xor_a = [to_xor_a[i:i+2] for i in range(0, len(to_xor_a), 2)] to_xor_b = hexlify(self.decrypted_epr[28:32]) to_xor_b = [to_xor_b[i:i+2] for i in range(0, len(to_xor_b), 2)] xored_1 = int(to_xor_a[-1],16) ^ int(to_xor_b[-1],16) xored_1 = "{0:0{1}x}".format(xored_1,2) xored_2 = int(to_xor_a[-2],16) ^ int(to_xor_b[-2],16) xored_2 = "{0:0{1}x}".format(xored_2,2) xored_3 = int(to_xor_a[-3],16) ^ int(to_xor_b[-3],16) xored_3 = "{0:0{1}x}".format(xored_3,2) xored_4 = int(to_xor_a[-4],16) ^ int(to_xor_b[-4],16) xored_4 = "{0:0{1}x}".format(xored_4,2) if (self.version == 2): self.epr_v2_sha256_flag = str(xored_4) + str(xored_3) + str(xored_2) + str(xored_1) if self.logging: logger.info("[-] Calculated that the flag will be: {}".format(self.epr_v2_sha256_flag)) else: self.epr_v3_sha256_flag = str(xored_4) + str(xored_3) + str(xored_2) + str(xored_1) if self.logging: logger.info("[-] Calculated that the flag will be: {}".format(self.epr_v3_sha256_flag)) except Exception as e: logger.error("[!] Encountered an exception. Reason: {}".format(e)) return False return True def key_map_check(self): found = False if (self.version == 2): for i in range(0, len(self.epr_v2_aes_map), 64): hash = sha256(unhexlify(self.epr_v2_aes_map[i:i+64])).hexdigest() if (hash.endswith(self.epr_v2_sha256_flag)): if self.logging: logger.info("[-] Found the flag: {}".format(self.epr_v2_sha256_flag)) found = True self.epr_v2_aes_key_two = self.epr_v2_aes_map[i:i+64] else: for i in range(0, len(self.epr_v3_aes_map), 64): hash = sha256(unhexlify(self.epr_v3_aes_map[i:i+64])).hexdigest() if (hash.endswith(self.epr_v3_sha256_flag)): if self.logging: logger.info("[-] Found the flag: {}".format(self.epr_v3_sha256_flag)) found = True self.epr_v3_aes_key_two = self.epr_v3_aes_map[i:i+64] return found def decrypt_key(self): try: if (self.version == 2): crypter = AES.new(unhexlify(self.epr_v2_aes_key_two),AES.MODE_CBC,unhexlify(self.epr_v2_aes_iv_two)) if self.logging: logger.info("[-] Decrypter setup with key 2 for version {}".format(self.version)) self.epr_v2_aes_key_three = hexlify(crypter.decrypt(self.decrypted_epr[48:80])) self.epr_v2_aes_iv_three = hexlify(self.decrypted_epr[112:128]) else: crypter = AES.new(unhexlify(self.epr_v3_aes_key_two),AES.MODE_CBC,unhexlify(self.epr_v3_aes_iv_two)) if self.logging: logger.info("[-] Decrypter setup with key 2 for version {}".format(self.version)) self.epr_v3_aes_key_three = hexlify(crypter.decrypt(self.decrypted_epr[48:80])) self.epr_v3_aes_iv_three = hexlify(self.decrypted_epr[112:128]) except Exception as e: logger.error("[!] Encountered an exception. Reason: {}".format(e)) return False return True def decrypt_epr(self): if (self.version == 2): crypter = AES.new(unhexlify(self.epr_v2_aes_key_three),AES.MODE_CBC,unhexlify(self.epr_v2_aes_iv_three)) if self.logging: logger.info("[-] AES Key: {}, IV: {}".format(self.epr_v2_aes_key_three,self.epr_v2_aes_iv_three)) else: crypter = AES.new(unhexlify(self.epr_v3_aes_key_three),AES.MODE_CBC,unhexlify(self.epr_v3_aes_iv_three)) if self.logging: logger.info("[-] AES Key: {}, IV: {}".format(self.epr_v3_aes_key_three,self.epr_v3_aes_iv_three)) if self.logging: logger.info("[-] Decrypter setup with key 3 for version {}".format(self.version)) self.encrypted_epr = self.encrypted_epr[128:] for pos in range(0, len(self.encrypted_epr), 65536): decryptPart = self.encrypted_epr[pos:pos+65536] try: self.final_epr+=crypter.decrypt(decryptPart) except ValueError as e: self.final_epr+=crypter.decrypt(pad(decryptPart)) if self.logging: logger.info("[-] Finished decrypting all blocks.") try: if self.logging: logger.info("[-] Writing bytes to: {}.broken".format(self.file)) fp = open("{}.broken".format(self.file),"wb") fp.write(self.final_epr) fp.close() if self.logging: logger.info("[-] Wrote {} bytes to a broken file.".format(len(self.final_epr))) except Exception as e: logger.error("[!] Encountered an exception. Reason: {}".format(e)) return False return True def zip_FF(self): if self.logging: logger.info("[-] Running: zip -FF {}.broken --out {}.zip > /dev/null 2>&1".format(self.file,self.file)) system("zip -FF {}.broken --out {}.zip > /dev/null 2>&1".format(self.file,self.file)) return True def finish(self): if self.logging: logger.info("[-] Removing the broken file.") remove("{}.broken".format(self.file)) move("{}.zip".format(self.file),"{}.zip".format(self.file.replace("input","output"))) logger.info("[+] Decrypted file available at {}.zip".format(self.file.replace("input","output"))) return True def main(): parser = OptionParser() parser.add_option("--file",dest="file",default=False,help="EPR File Path") parser.add_option("--version",dest="version",choices=(str(1),str(2),str(3)),default=str(3),help="EPR Version") parser.add_option("--verbose",dest="verbose",action="store_true",help="Enable verbose mode") o,a = parser.parse_args() o.version = int(o.version) epr = EPR(o.file,o.version,o.verbose) if not epr.file_exists(): logger.info("[!] Unable to find the encrypted EPR file specified.") return False logger.info("[+] The EPR file specified exists.") if not epr.can_read_file(): logger.info("[!] Unable to open a file object to the encrypted EPR file.") return False if not epr.read_entire_file(): logger.info("[!] Unable to read the encrypted EPR file.") return False logger.info("[+] The specified EPR file has been read into memory.") logger.info("[+] Using the version {} decryption process.".format(o.version)) if not epr.flat_decrypt(): logger.info("[!] Unable to run the initial decryption round.") return False logger.info("[+] Round one of the EPR decryption completed successfully.") if not epr.calc_sha256_dword(): logger.info("[!] Unable to calculate the SHA256 key flag.") return False if o.verbose: logger.info("[+] The SHA256 key flag has been calculated.") if not epr.key_map_check(): logger.info("[!] Unable to find a AES key match.") return False if o.verbose: logger.info("[+] The SHA256 key flag has been found.") if not epr.decrypt_key(): logger.info("[!] Could not decrypt the final AES key.") return False logger.info("[+] Round two of the EPR decryption completed successfully. Obtained the final AES key and IV.") if not epr.decrypt_epr(): logger.info("[!] Unable to decrypt the EPR file.") return False logger.info("[+] Round three of the EPR decryption completed successfully. The encrypted zip archive has been decrypted.") if not epr.zip_FF(): logger.info("[!] Could not clean up garbage.") return False return True if __name__ == "__main__": success = main() if success: logger.info("[+] done") else: logger.info("[!] failed") exit(success) The contents of this advisory are copyright(c) 2020 KoreLogic, Inc. and are licensed under a Creative Commons Attribution Share-Alike 4.0 (United States) License: http://creativecommons.org/licenses/by-sa/4.0/ KoreLogic, Inc. is a founder-owned and operated company with a proven track record of providing security services to entities ranging from Fortune 500 to small and mid-sized companies. We are a highly skilled team of senior security consultants doing by-hand security assessments for the most important networks in the U.S. and around the world. We are also developers of various tools and resources aimed at helping the security community. https://www.korelogic.com/about-korelogic.html Our public vulnerability disclosure policy is available at: https://korelogic.com/KoreLogic-Public-Vulnerability-Disclosure-Policy.v2.3.txt -----BEGIN PGP SIGNATURE----- iQJOBAEBCAA4FiEETtzSIGy8wE6Vn0geUk0uR1lFz/MFAl76UXEaHGRpc2Nsb3N1 cmVzQGtvcmVsb2dpYy5jb20ACgkQUk0uR1lFz/PfEg/+MXq81h0ykdejmG8sVSLn xCiljWxVnYGVJuNsbPdat5BwNEwVuv2p9MWOV+2w2EIHzPEwKEeSg5p32WUbuMAn tml8kr9gcX/ChFY6JVLbKpHQVEKOC2uNink68EKzQ/bExoKHfvyKfZgthkF2TBIl qI9aGxVyKD4fpytBD1wHisjGdzghGTzX/JGIoQxZiRVir+3RRu19Af+UOmi/aigA bTi1KbrqWPVCuWZZh7wsv5THnLUk1qm8U1nXhUA+8cmriv5T26D6uTi1L/bme9uL LUc3sCe46478oy8C8Ug5tz8Nj/qCQ2a/6Hd6umEOxh/VWCuxrX6kiW6aesgn5Haz vRcSWS3DDmplzp/MZ/1KYzNUjiEkJIUeOSq7An2pdD458cU9P8FYI2I8bedc5ELf 0kALVoxd4t9xzYlZMblwRd/e5THY50iSwrDWEBPLXgfi+HSGjgrhhbzzFBH0O8SE n73ogBQszo2S8eg8Ub4Fi1PyExuVC+gT6s8iZ6SqmBLgZWyAe0bp3DKJlzzpP+wV v+kXQQdxxfft0UMmDkFdm2ertd6vBhYG637o2MDN68etJ6NrFy+Zdcs3ZzL+pRvb bYnGkq7hYubtN4bykRYP7RMbuwIx1Sa+A1XbkbGLNvMmfEIYFdWTe3T/Fc7vyG7s 9Yxbg1I/O7I5mSdMyCSfE9Q= =/xRL -----END PGP SIGNATURE-----