Last active
February 9, 2024 22:38
-
-
Save obfusk/a993b1bb54f52e1f6d2f56b1f97b2100 to your computer and use it in GitHub Desktop.
check APK Signing Block for Google/unknown blocks
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/python3 | |
# encoding: utf-8 | |
# SPDX-FileCopyrightText: 2024 FC (Fay) Stegerman <flx@obfusk.net> | |
# SPDX-FileCopyrightText: 2024 Izzy | |
# SPDX-License-Identifier: GPL-3.0-or-later | |
import argparse | |
import logging | |
import os | |
import sys | |
from typing import Any, List, Tuple | |
try: | |
import apksigtool | |
apksigtool.NonZeroVerityPaddingBlock # new enough | |
except (ImportError, AttributeError): | |
apksigtool = None # type: ignore[assignment] | |
from androguard.core.bytecodes import apk as ag_apk # type: ignore[import-untyped] | |
OK_BLOCKS = dict( | |
# https://source.android.com/docs/security/features/apksigning/v2#apk-signing-block-format | |
APK_SIGNATURE_SCHEME_V2_BLOCK=0x7109871a, | |
APK_SIGNATURE_SCHEME_V3_BLOCK=0xf05368c0, | |
APK_SIGNATURE_SCHEME_V31_BLOCK=0x1b93ad61, | |
VERITY_PADDING_BLOCK=0x42726577, | |
) | |
GOOGLE_BLOCKS = dict( | |
# https://developer.android.com/build/dependencies#dependency-info-play | |
DEPENDENCY_INFO_BLOCK=0x504b4453, | |
# https://bi-zone.medium.com/easter-egg-in-apk-files-what-is-frosting-f356aa9f4d1 | |
GOOGLE_PLAY_FROSTING_BLOCK=0x2146444e, | |
# https://apt.izzysoft.de/fdroid/index/info#signingblock | |
SOURCE_STAMP_V1_BLOCK=0x2b09189e, | |
SOURCE_STAMP_V2_BLOCK=0x6dff800d, | |
) | |
PAYLOAD_BLOCKS = dict( | |
# https://gitlab.com/IzzyOnDroid/repo/-/issues/475#note_1729235542 | |
# https://apt.izzysoft.de/fdroid/index/info#signingblock | |
MEITUAN_APK_CHANNEL_BLOCK=0x71777777, | |
) | |
# FIXME: attributes are not checked yet | |
OK_ATTRS = dict( | |
STRIPPING_PROTECTION_ATTR=0xbeeff00d, | |
PROOF_OF_ROTATION_ATTR=0x3ba06f8c, | |
ROTATION_MIN_SDK_VERSION_ATTR=0x559f8b02, | |
ROTATION_ON_DEV_RELEASE_ATTR=0xc2a6b3ba, | |
) | |
OK_BLOCKS_REV = {v: k for k, v in OK_BLOCKS.items()} | |
GOOGLE_BLOCKS_REV = {v: k for k, v in GOOGLE_BLOCKS.items()} | |
PAYLOAD_BLOCKS_REV = {v: k for k, v in PAYLOAD_BLOCKS.items()} | |
class HDict(dict): # type: ignore[type-arg] | |
def __init__(self) -> None: | |
self.history: List[Tuple[Any, Any]] = [] | |
def __setitem__(self, k: Any, v: Any) -> None: | |
self.history.append((k, v)) | |
super().__setitem__(k, v) | |
def apk_blocks(apk: str) -> List[Tuple[int, bytes]]: | |
if apksigtool is not None: | |
_, sig_block = apksigtool.extract_v2_sig(apk) | |
blk = apksigtool.parse_apk_signing_block(sig_block, allow_nonzero_verity=True) | |
return [(p.id, p.value.dump()) for p in blk.pairs] | |
else: | |
instance = ag_apk.APK(apk) | |
instance._v2_blocks = hdict = HDict() | |
instance.parse_v2_signing_block() | |
return hdict.history | |
# NOTES: | |
# * androguard will not see multiple blocks if a duplicate ID is used (but we work around that) | |
# * androguard does not parse the attributes so we cannot check them | |
# * androguard does not verify the signatures | |
# * android/apksigner will only verify the "strongest supported" signature | |
# * we do check if the verity padding block is all zeroes | |
# * apksigtool might do better but does not have a stable release yet | |
def check_apks(*apks: str, verbosity: int = 0, report: bool = True, | |
with_filename: bool = False) -> bool: | |
ok = True | |
for apk in apks: | |
if verbosity > 0: | |
print(f"Checking {apk} ...") | |
not_ok_blocks = [] | |
for block_id, block in apk_blocks(apk): | |
if block_id in OK_BLOCKS_REV: | |
name = OK_BLOCKS_REV[block_id] | |
if block_id == OK_BLOCKS["VERITY_PADDING_BLOCK"] and not all(b == 0 for b in block): | |
ok = False | |
msg = f"0x{block_id:x} ({name}; NONZERO)" | |
not_ok_blocks.append(msg) | |
if verbosity > 0: | |
print(f" Found {msg}", file=sys.stderr) | |
else: | |
msg = f"0x{block_id:x} ({name}; OK)" | |
if verbosity > 1: | |
print(f" Found {msg}") | |
else: | |
ok = False | |
if block_id in GOOGLE_BLOCKS_REV: | |
name = GOOGLE_BLOCKS_REV[block_id] | |
msg = f"0x{block_id:x} ({name}; GOOGLE)" | |
elif block_id in PAYLOAD_BLOCKS_REV: | |
full_name = PAYLOAD_BLOCKS_REV[block_id] | |
source, name = full_name.split("_", 1) | |
msg = f"0x{block_id:x} (PAYLOAD {name}; {source})" | |
else: | |
msg = f"0x{block_id:x} (UNKNOWN)" | |
not_ok_blocks.append(msg) | |
if verbosity > 0: | |
print(f" Found {msg}", file=sys.stderr) | |
if report and not_ok_blocks: | |
msg = ", ".join(not_ok_blocks) | |
if with_filename: | |
msg = f"{os.path.basename(apk)}: {msg}" | |
print(msg, file=sys.stderr) | |
return ok | |
if __name__ == "__main__": | |
# disable androguard warnings | |
logging.getLogger().setLevel(logging.ERROR) | |
parser = argparse.ArgumentParser( | |
description="Check APK Signing Block for Google/payload/unknown blocks.") | |
parser.add_argument("-v", "--verbose", action="count", default=0, | |
help="increase verbosity level") | |
parser.add_argument("-R", "--no-report", action="store_true", | |
help="don't show single-line report") | |
parser.add_argument("-f", "--with-filename", action="store_true", | |
help="show APK file basename in report") | |
parser.add_argument("apks", metavar="APK", nargs="*", help="APK file(s) to check") | |
args = parser.parse_args() | |
if not check_apks(*args.apks, verbosity=args.verbose, report=not args.no_report, | |
with_filename=args.with_filename): | |
sys.exit(1) | |
# vim: set tw=80 sw=4 sts=4 et fdm=marker : |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment