Last active
November 11, 2024 23:41
-
-
Save obfusk/a993b1bb54f52e1f6d2f56b1f97b2100 to your computer and use it in GitHub Desktop.
check APK Signing Block for Google/unknown blocks
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/python3 | |
| # encoding: utf-8 | |
| # SPDX-FileCopyrightText: 2024 FC (Fay) Stegerman <[email protected]> | |
| # 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 (signing branch) | |
| except (ImportError, AttributeError): | |
| apksigtool = None # type: ignore[assignment] | |
| try: | |
| from androguard.core import apk as ag_apk # type: ignore[import-untyped] | |
| except ImportError: | |
| 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()} | |
| 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: | |
| # NB: monkey patch of sorts | |
| 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) | |
| # for androguard >= v4.1.2 which fixes duplicate block ID handling | |
| # but does not yet have an API to get all but the first | |
| def __contains__(self, k: Any) -> bool: | |
| return False | |
| 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 | |
| def _nologging() -> None: | |
| # disable androguard warnings | |
| logging.getLogger().setLevel(logging.ERROR) | |
| try: | |
| from loguru import logger # type: ignore | |
| logger.remove() | |
| except ImportError: | |
| pass | |
| if __name__ == "__main__": | |
| _nologging() | |
| 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