Skip to content

Instantly share code, notes, and snippets.

@grant-h
Created August 24, 2022 15:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save grant-h/6e84e11b6323fce1b66ff2615cae1db0 to your computer and use it in GitHub Desktop.
Save grant-h/6e84e11b6323fce1b66ff2615cae1db0 to your computer and use it in GitHub Desktop.
Samsung EPIC Decryptor
#!/usr/bin/env python3
HELP="""
Samsung EPIC Decrypter
by @Digital_Cold, Aug 2022
Samsung EPIC is a power management daemon for Android. It stores its profiles
in AES CFB encrypted JSON files. The key is hardcoded per build in an ELF
section. By extracting the key and using the same decryption, we can recover
the JSON file.
To run:
pip3 install pycrypto
chmod +x ./epic_manager.py
./epic_manager.py vendor/bin/epic vendor/etc/epic.json
Only tested on Linux Ubuntu 22.04
"""
__VERSION__ = "1.0"
import argparse
import struct
import sys
import json
import base64
import logging
from Crypto.Cipher import AES
from binascii import hexlify
log = logging.getLogger("epic_manager")
def elf_read_cstring(fp):
buf = b""
pos = fp.tell()
while True:
buf += fp.read(0x20)
nul = buf.find(b"\x00")
if nul != -1:
fp.seek(pos+nul+1)
return buf[:nul]
def elf_extract_key_data(fp):
log.info("[+] Reading ELF file...")
# A very hacky ELF parser just to get a single symbol, avoiding dependencies
elf_header_fmt = "<I5B7xHHI"
e_magic, e_class, e_data, e_version, e_osabi, e_osabi_version, e_type, e_machine, e_version = struct.unpack(elf_header_fmt, fp.read(struct.calcsize(elf_header_fmt)))
# \x7fELF
assert e_magic == 0x464c457f, e_magic
# 64-bit
assert e_class == 2
elf_header_fmt = "<3Q"
e_entry, e_phoff, e_shoff = struct.unpack(elf_header_fmt, fp.read(struct.calcsize(elf_header_fmt)))
elf_header_fmt = "<I6H"
e_flags, e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx = struct.unpack(elf_header_fmt, fp.read(struct.calcsize(elf_header_fmt)))
assert e_ehsize == fp.tell()
# start reading section headers
fp.seek(e_shoff)
sh_fmt = "<II4QIIQQ"
assert struct.calcsize(sh_fmt) == e_shentsize
section_headers = []
log.info("[+] ELF has %d sections", e_shnum)
for i in range(e_shnum):
sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size, sh_link, sh_info, sh_addralign, sh_entsize = struct.unpack(sh_fmt, fp.read(e_shentsize))
section_headers.append({"name_idx": sh_name, "type": sh_type, "size": sh_size, "offset": sh_offset})
# resolve section names
shstrndx = section_headers[e_shstrndx]
for sec in section_headers:
fp.seek(shstrndx["offset"]+sec["name_idx"])
sec["name"] = elf_read_cstring(fp)
numeric_sec = None
# dump the .numeric section as this contains the key
for sec in section_headers:
if sec["name"] == b".numeric":
numeric_sec = sec
break
if numeric_sec is None:
log.error("[!] .numeric section not found. Cannot extract key")
return None
log.info("[+] Found .numeric secion at offset 0x%08x", numeric_sec["offset"])
fp.seek(numeric_sec["offset"])
data = fp.read(numeric_sec["size"])
# 16 byte, 128-bit key
assert len(data) == 16
return data
def decrypt_and_decode(enc_data, key):
# IV is null in the binary
iv = b"\x00"*0x10
# pycrypto requires that the segment_size of 128 be set
# see: https://github.com/pycrypto/pycrypto/issues/226
cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128)
if len(enc_data) % 16 != 0:
padding_req = 16 - len(enc_data) % 16
else:
padding_req = 0
enc_data_padded = enc_data + b"\x00"*padding_req
dec_data = cipher.decrypt(enc_data_padded)
# remove padding as CFB is a stream cipher
if padding_req:
dec_data = dec_data[:-padding_req]
decoded_data = base64.decodebytes(dec_data)
try:
# just to check that the document is valid. return raw string
obj = json.loads(decoded_data)
return decoded_data.decode()
except UnicodeDecodeError as e:
log.error("[!] Failed to decrypt data: %s", e)
return None
except json.decoder.JSONDecodeError as e:
log.error("[!] Failed to decode data: %s", e)
return None
def main():
logging.basicConfig(level=logging.INFO, format="%(message)s")
parser = argparse.ArgumentParser(usage=HELP)
parser.add_argument("epic")
parser.add_argument("epic_json")
log.info("EPIC Manager %s\n", __VERSION__)
args = parser.parse_args()
log.info("[+] Using epic binary %s", args.epic)
fp = open(args.epic, 'rb')
# found at symbol name _binary_server_crypto_aes_key_bin_start
key = elf_extract_key_data(fp)
if key is None:
log.info("[-] Failed to extract key data!")
return 1
log.info("[+] Extracted epic key: %s", hexlify(key, " ", 1 ).decode())
log.info("[+] Using epic json %s", args.epic_json)
enc_json = open(args.epic_json, 'rb').read()
log.info("[+] Epic json size %d bytes", len(enc_json))
json_str = decrypt_and_decode(enc_json, key)
if json_str is None:
log.error("[!] Decryption failed. Check binary/json pair")
return 1
print(json_str)
return 0
if __name__ == "__main__":
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment