Last active
June 21, 2023 13:50
-
-
Save obfusk/8ce62af591a0dd3c2020107b28ffa2da to your computer and use it in GitHub Desktop.
apk signature copying
This file contains 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 | |
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