Skip to content

Instantly share code, notes, and snippets.

@obfusk
Last active February 9, 2024 22:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save obfusk/a993b1bb54f52e1f6d2f56b1f97b2100 to your computer and use it in GitHub Desktop.
Save obfusk/a993b1bb54f52e1f6d2f56b1f97b2100 to your computer and use it in GitHub Desktop.
check APK Signing Block for Google/unknown blocks
#!/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