This commit is contained in:
Jean-Marie Mineau 2024-10-16 17:03:56 +02:00
commit 43eef100f1
24 changed files with 2041734 additions and 0 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
*.zip filter=lfs diff=lfs merge=lfs -text

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# Android class shadowing scanner
Detect if an Android application is in a situation that may lead to class spoofing.

View file

@ -0,0 +1,214 @@
import zipfile
import io
from dataclasses import dataclass, asdict
from enum import IntEnum
import androguard.core.dex # type: ignore
from androguard.core.dex import DEX # type: ignore
# loguru -> logging framework used by androgard
from loguru import logger # type: ignore
from .platform_classes import (
PLATFORM_32_CLASSES,
SDK_32_CLASSES,
PLATFORM_33_CLASSES,
SDK_33_CLASSES,
PLATFORM_34_CLASSES,
SDK_34_CLASSES,
)
# Remove Androguard logs
logger.remove()
# Patch Androguard
class PatchedDomapiApiFlag(IntEnum):
NONE = 0
CORE_PLATFORM_API = 1
TEST_API = 2
UNKN_3 = 3
UNKN_4 = 4
UNKN_5 = 5
UNKN_6 = 6
UNKN_7 = 7
UNKN_8 = 8
UNKN_9 = 9
INKN_10 = 10
class PatchedRestrictionApiFlag(IntEnum):
WHITELIST = 0
GREYLIST = 1
BLACKLIST = 2
GREYLIST_MAX_O = 3
GREYLIST_MAX_P = 4
GREYLIST_MAX_Q = 5
GREYLIST_MAX_R = 6
GREYLIST_MAX_7 = 7
GREYLIST_MAX_8 = 8
GREYLIST_MAX_9 = 9
GREYLIST_MAX_10 = 10
androguard.core.dex.HiddenApiClassDataItem.DomapiApiFlag = PatchedDomapiApiFlag
androguard.core.dex.HiddenApiClassDataItem.RestrictionApiFlag = (
PatchedRestrictionApiFlag
)
@dataclass
class Entry:
# sha256: str
nb_duplicate_classes: int
nb_platform_32_classes: int
nb_platform_non_sdk_32_classes: int
nb_sdk_32_classes: int
nb_platform_33_classes: int
nb_platform_non_sdk_33_classes: int
nb_sdk_33_classes: int
nb_platform_34_classes: int
nb_platform_non_sdk_34_classes: int
nb_sdk_34_classes: int
has_classes0_dex: bool
has_classes1_dex: bool
has_classes0X_dex: bool
has_classes_dex_over_10: bool
has_non_numeric_classes_dex: bool
has_non_consecutive_classes_dex: bool
@dataclass
class PlatformClassesData:
nb_duplicate_classes: int
nb_platform_32_classes: int
nb_platform_non_sdk_32_classes: int
nb_sdk_32_classes: int
nb_platform_33_classes: int
nb_platform_non_sdk_33_classes: int
nb_sdk_33_classes: int
nb_platform_34_classes: int
nb_platform_non_sdk_34_classes: int
nb_sdk_34_classes: int
def scan_classes(apk: zipfile.ZipFile, file_names: set[str]) -> PlatformClassesData:
all_classes = set()
duplicated_classes = set()
platform_32_classes = set()
sdk_32_classes = set()
platform_non_sdk_32_classes = set()
platform_33_classes = set()
sdk_33_classes = set()
platform_non_sdk_33_classes = set()
platform_34_classes = set()
sdk_34_classes = set()
platform_non_sdk_34_classes = set()
for name in file_names:
with apk.open(name) as dex_f:
dex = DEX(dex_f.read())
for clazz in map(lambda c: c.name, dex.get_classes()):
if clazz in all_classes:
duplicated_classes.add(clazz)
if clazz in PLATFORM_32_CLASSES:
platform_32_classes.add(clazz)
if clazz in SDK_32_CLASSES:
sdk_32_classes.add(clazz)
if clazz in PLATFORM_32_CLASSES and clazz not in SDK_32_CLASSES:
platform_non_sdk_32_classes.add(clazz)
if clazz in PLATFORM_33_CLASSES:
platform_33_classes.add(clazz)
if clazz in SDK_33_CLASSES:
sdk_33_classes.add(clazz)
if clazz in PLATFORM_33_CLASSES and clazz not in SDK_33_CLASSES:
platform_non_sdk_33_classes.add(clazz)
if clazz in PLATFORM_34_CLASSES:
platform_34_classes.add(clazz)
if clazz in SDK_34_CLASSES:
sdk_34_classes.add(clazz)
if clazz in PLATFORM_34_CLASSES and clazz not in SDK_34_CLASSES:
platform_non_sdk_34_classes.add(clazz)
all_classes.add(clazz)
return PlatformClassesData(
nb_duplicate_classes=len(duplicated_classes),
nb_platform_32_classes=len(platform_32_classes),
nb_platform_non_sdk_32_classes=len(platform_non_sdk_32_classes),
nb_sdk_32_classes=len(sdk_32_classes),
nb_platform_33_classes=len(platform_33_classes),
nb_platform_non_sdk_33_classes=len(platform_non_sdk_33_classes),
nb_sdk_33_classes=len(sdk_33_classes),
nb_platform_34_classes=len(platform_34_classes),
nb_platform_non_sdk_34_classes=len(platform_non_sdk_34_classes),
nb_sdk_34_classes=len(sdk_34_classes),
)
def analyze(apk: zipfile.ZipFile) -> Entry:
classes_dex = set(
filter(
lambda name: name.startswith("classes") and name.endswith(".dex"),
apk.namelist(),
)
)
dex_numbers = list(
map(
int,
filter(
lambda string: string not in ("", "1")
and string.isnumeric()
and string[0] != "0",
map(
lambda name: name.removeprefix("classes").removesuffix(".dex"),
classes_dex,
),
),
)
)
dex_numbers.sort()
has_non_numeric_classes_dex = False
for name in classes_dex:
if name == "classes.dex":
continue
if not name.removeprefix("classes").removesuffix(".dex").isnumeric():
has_non_numeric_classes_dex = True
has_non_consecutive_classes_dex = False
if "classes.dex" in classes_dex and dex_numbers:
has_non_consecutive_classes_dex = True
last_number = 1
for i in range(len(dex_numbers)):
if dex_numbers[i] == 0 or dex_numbers[i] == 1:
continue
# the list is sorted
if dex_numbers[i] != last_number + 1:
has_non_consecutive_classes_dex = True
break
platform_classes_data = scan_classes(apk, classes_dex)
return Entry(
**asdict(platform_classes_data),
has_classes0_dex="classes0.dex" in classes_dex,
has_classes1_dex="classes1.dex" in classes_dex,
has_classes0X_dex=any(
map(lambda name: name.startswith("classes0"), classes_dex)
),
has_classes_dex_over_10=any(map(lambda x: x >= 10, dex_numbers)),
has_non_numeric_classes_dex=has_non_numeric_classes_dex,
has_non_consecutive_classes_dex=has_non_consecutive_classes_dex,
)
def main():
import sys
import pprint
with open(sys.argv[1], "rb") as file:
apk = file.read()
with zipfile.ZipFile(io.BytesIO(apk)) as apk:
entry = analyze(apk)
pprint.pprint(entry)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
from pathlib import Path
local_dir = Path(__file__).parent
PLATFORM_32_CLASSES = set()
SDK_32_CLASSES = set()
PLATFORM_33_CLASSES = set()
SDK_33_CLASSES = set()
PLATFORM_34_CLASSES = set()
SDK_34_CLASSES = set()
with (local_dir / "android-32" / "classes.txt").open() as file:
for line in file:
class_name = line.strip()
if class_name:
PLATFORM_32_CLASSES.add(class_name)
with (local_dir / "android-32" / "sdk_classes.txt").open() as file:
for line in file:
class_name = line.strip()
if class_name:
SDK_32_CLASSES.add(class_name)
with (local_dir / "android-33" / "classes.txt").open() as file:
for line in file:
class_name = line.strip()
if class_name:
PLATFORM_33_CLASSES.add(class_name)
with (local_dir / "android-33" / "sdk_classes.txt").open() as file:
for line in file:
class_name = line.strip()
if class_name:
SDK_33_CLASSES.add(class_name)
with (local_dir / "android-34" / "classes.txt").open() as file:
for line in file:
class_name = line.strip()
if class_name:
PLATFORM_34_CLASSES.add(class_name)
with (local_dir / "android-34" / "sdk_classes.txt").open() as file:
for line in file:
class_name = line.strip()
if class_name:
SDK_34_CLASSES.add(class_name)

1543
poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

18
pyproject.toml Normal file
View file

@ -0,0 +1,18 @@
[tool.poetry]
name = "android-class-shadowing-scanner"
version = "0.1.0"
description = "Detect if an Android application is in a situation that may lead to class spoofing."
authors = ["Jean-Marie Mineau <jean-marie.mineau@inria.fr>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
androguard = "^4.1.2"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
scan = 'android_class_shadowing_scanner.__init__:main'