Skip to content

Instantly share code, notes, and snippets.

@obfusk
Last active June 21, 2023 13:50
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save obfusk/8ce62af591a0dd3c2020107b28ffa2da to your computer and use it in GitHub Desktop.
Save obfusk/8ce62af591a0dd3c2020107b28ffa2da to your computer and use it in GitHub Desktop.
apk signature copying
#!/usr/bin/env python3
import os
import shutil
import subprocess
import sys
import zipfile
from collections import namedtuple
ZipData = namedtuple("ZipData", "contents cd_offset eocd_offset cd_and_eocd".split())
# FIXME
class ZeroedZipInfo(zipfile.ZipInfo):
def __init__(self, zinfo):
for k in self.__slots__:
setattr(self, k, getattr(zinfo, k))
def __getattribute__(self, name):
if name == "date_time":
return (1980, 0, 0, 0, 0, 0)
if name == "external_attr":
return 0
return object.__getattribute__(self, name)
def _encodeFilenameFlags(self):
return self.filename.encode('utf-8'), 0x800
def is_meta(filename):
return filename.startswith("META-INF") and \
any(filename.endswith(ext) for ext in ".SF .RSA .MF".split())
def gen_dummy_key(size=4096):
args = f"""
keytool -genkey -v -alias dummy -keyalg RSA -keysize {size}
-sigalg SHA512withRSA -validity 10000
-keystore dummy-keystore -storepass dummy-password
-dname CN=dummy
""".split()
subprocess.run(args, check=True)
def sign_with_dummy_key(out):
args = """
apksigner sign -v --ks dummy-keystore --ks-key-alias dummy
--ks-pass pass:dummy-password
""".split() + [out]
subprocess.run(args, check=True)
def replace_meta(signed, out):
with zipfile.ZipFile(out, "r") as zf_out:
meta = [info.filename for info in zf_out.infolist() if is_meta(info.filename)]
subprocess.run(["zip", "-d", out] + meta, check=True)
with zipfile.ZipFile(signed, "r") as zf_sig:
with zipfile.ZipFile(out, "a") as zf_out:
for info in zf_sig.infolist():
if is_meta(info.filename):
zf_out.writestr(ZeroedZipInfo(info),
zf_sig.read(info.filename),
compresslevel=9)
# https://source.android.com/security/apksigning/v2#apk-signing-block-format
# https://en.wikipedia.org/wiki/ZIP_(file_format)#End_of_central_directory_record_(EOCD)
#
# =================================
# | Contents of ZIP entries |
# =================================
# | APK Signing Block |
# | ----------------------------- |
# | | size (w/o this) uint64 LE | |
# | | ... | |
# | | size (again) uint64 LE | |
# | | "APK Sig Block 42" (16b) | |
# | ----------------------------- |
# =================================
# | ZIP Central Directory |
# =================================
# | ZIP End of Central Directory |
# | ----------------------------- |
# | | 0x06054b50 ( 4b) | |
# | | ... (12b) | |
# | | CD Offset ( 4b) | |
# | | ... | |
# | ----------------------------- |
# =================================
def apk_sig_block(apkfile, count=4096):
with open(apkfile, "rb") as fh:
# fh.seek(-count, os.SEEK_END)
data = fh.read()
fh.seek(data.rindex(b"APK Sig Block 4") - len(data) - 8, os.SEEK_CUR)
sb_size2 = int.from_bytes(fh.read(8), "little")
fh.seek(-sb_size2 + 8, os.SEEK_CUR)
sb_size1 = int.from_bytes(fh.read(8), "little")
assert sb_size1 == sb_size2
fh.seek(-8, os.SEEK_CUR)
sb_offset = fh.tell()
sig_block = fh.read(sb_size2 + 8)
return sb_offset, sig_block
def zip_data(apkfile, count=1024):
with open(apkfile, "rb") as fh:
fh.seek(-count, os.SEEK_END)
data = fh.read()
fh.seek(data.rindex(b"\x50\x4b\x05\x06") - len(data), os.SEEK_CUR)
eocd_offset = fh.tell()
fh.seek(16, os.SEEK_CUR)
cd_offset = int.from_bytes(fh.read(4), "little")
fh.seek(0)
contents = fh.read(cd_offset)
cd_and_eocd = fh.read()
return ZipData(contents, cd_offset, eocd_offset, cd_and_eocd)
def replace_v2_sig(signed, out):
signed_sb_offset, signed_sb = apk_sig_block(signed)
data_out = zip_data(out)
padding = b"\x00" * (signed_sb_offset - data_out.cd_offset)
offset = len(signed_sb) + len(padding)
with open(out, "wb") as fh:
fh.write(data_out.contents)
fh.write(padding)
fh.write(signed_sb)
fh.write(data_out.cd_and_eocd)
fh.seek(data_out.eocd_offset + offset + 16)
fh.write(int.to_bytes(data_out.cd_offset + offset, 4, "little"))
def main(signed, unsigned, out):
if not os.path.exists("dummy-keystore"):
gen_dummy_key()
shutil.copy(unsigned, out)
sign_with_dummy_key(out)
replace_meta(signed, out)
replace_v2_sig(signed, out)
if __name__ == "__main__":
main(*sys.argv[1:])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment