#!/usr/bin/env python3 """Minimal Qualcomm DIAG EFS2 client over a serial diag port. Command-line tool to list, pull, push and back up files in a rooted phone's modem EFS. Packet layouts and opcodes replicate JohnBel/EfsTools exactly. Originally written to back up and replace the policyman RF-band config on a Sony Xperia 1 VI. Run with -h for usage. """ import sys, os, time, struct, glob, argparse, serial DIAG_SUBSYS = 0x4B SUBSYS_EFS = 19 # EFS opcodes (EfsTools QcdmEfsCommand) HELLO, QUERY, OPEN, CLOSE, READ, WRITE = 0, 1, 2, 3, 4, 5 UNLINK, MKDIR, OPENDIR, READDIR, CLOSEDIR, STAT = 8, 9, 11, 12, 13, 15 # open flags O_WRONLY, O_RDONLY, O_CREAT = 0o1, 0o0, 0o100 # CRC-16 CCITT reflected (HDLC FCS-16) _TAB = [] for _i in range(256): _c = _i for _ in range(8): _c = (_c >> 1) ^ 0x8408 if (_c & 1) else (_c >> 1) _TAB.append(_c) def fcs16(data): fcs = 0xFFFF for b in data: fcs = (fcs >> 8) ^ _TAB[(fcs ^ b) & 0xFF] return (~fcs) & 0xFFFF def hdlc_encode(payload): crc = fcs16(payload) frame = bytes(payload) + struct.pack("= 3 and dec[0] == 0x00 and \ fcs16(dec[:-2]) == struct.unpack("= 4 and p[0] == DIAG_SUBSYS and p[1] == SUBSYS_EFS \ and struct.unpack(" ignore raise TimeoutError("no matching response for efs cmd %d" % efs_cmd) # ---- EFS operations ---- def hello(self): win, wbyte, ver = 0x100000, 0x100000, 1 body = struct.pack(" %s (%d bytes)" % (args.path, local, len(data))) def cmd_push(d, args): with open(args.local, "rb") as f: data = f.read() n = d.write_file(args.path, data) verified = d.read_file(args.path) == data print("pushed %s -> %s (%d bytes), read-back %s" % (args.local, args.path, n, "matches" if verified else "MISMATCH")) if not verified: raise SystemExit(2) def cmd_backup(d, args): written = [0] def walk(efs_dir, local_dir): os.makedirs(local_dir, exist_ok=True) for name, etype, size in d.listdir(efs_dir): efs_path = efs_dir.rstrip("/") + "/" + name local_path = os.path.join(local_dir, name) if etype == 1: # directory walk(efs_path, local_path) elif etype in (0, 15): # regular file or item file try: data = d.read_file(efs_path) except Exception as e: sys.stderr.write(" skip %s: %s\n" % (efs_path, e)) continue with open(local_path, "wb") as f: f.write(data) written[0] += 1 print(" %s (%d bytes)" % (efs_path, len(data))) walk(args.path, args.dest) print("backed up %d files from %s to %s" % (written[0], args.path, args.dest)) def build_parser(): p = argparse.ArgumentParser( prog="efs2.py", description="Minimal Qualcomm DIAG EFS2 client: list, pull, push and " "back up files in a rooted phone's modem EFS.", epilog="Put the phone in diag mode first (adb shell su -c 'setprop " "sys.usb.config diag,serial_cdev,rmnet,adb') and bind the option " "driver. The diag port is auto-detected; override with --port or " "the DIAGPORT env var.") p.add_argument("-p", "--port", help="diag serial port (default: auto-detect)") sub = p.add_subparsers(dest="command", required=True) s = sub.add_parser("ls", help="list an EFS directory") s.add_argument("path") s.set_defaults(func=cmd_ls) s = sub.add_parser("stat", help="stat an EFS file") s.add_argument("path") s.set_defaults(func=cmd_stat) s = sub.add_parser("pull", help="download one EFS file") s.add_argument("path", help="EFS path, e.g. /policyman/band_set_01.xml") s.add_argument("local", nargs="?", help="local path (default: the basename)") s.set_defaults(func=cmd_pull) s = sub.add_parser("push", help="upload/overwrite one EFS file, then verify") s.add_argument("local", help="local file to upload") s.add_argument("path", help="destination EFS path") s.set_defaults(func=cmd_push) s = sub.add_parser("backup", help="recursively download an EFS directory") s.add_argument("path", help="EFS directory, e.g. /policyman") s.add_argument("dest", help="local directory to write into") s.set_defaults(func=cmd_backup) return p def main(): args = build_parser().parse_args() d = Diag(args.port) sys.stderr.write("using diag port %s (efs v%d)\n" % (d.port, d.hello())) try: args.func(d, args) finally: d.s.close() if __name__ == "__main__": main()