Created
August 24, 2022 15:39
-
-
Save grant-h/6e84e11b6323fce1b66ff2615cae1db0 to your computer and use it in GitHub Desktop.
Samsung EPIC Decryptor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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