mirror of
https://github.com/androguard/androguard.git
synced 2025-02-17 04:08:13 +00:00
1052 lines
42 KiB
Python
1052 lines
42 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import binascii
|
|
import glob
|
|
import hashlib
|
|
import os
|
|
import unittest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from androguard.core import apk, axml
|
|
from androguard.core.analysis.analysis import Analysis
|
|
from androguard.core.apk import APK
|
|
from androguard.core.axml import AXMLPrinter
|
|
from androguard.core.dex import DEX
|
|
from androguard.misc import AnalyzeAPK
|
|
|
|
test_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
class APKTest(unittest.TestCase):
|
|
def testAPK(self):
|
|
for f in glob.glob(os.path.join(test_dir, 'data/APK/*.apk')):
|
|
with open(f, "rb") as fd:
|
|
a = apk.APK(fd.read(), True)
|
|
self.assertTrue(a)
|
|
|
|
def testAPKWrapper(self):
|
|
a, d, dx = AnalyzeAPK(
|
|
os.path.join(test_dir, 'data/APK/TestActivity.apk')
|
|
)
|
|
|
|
self.assertIsInstance(a, APK)
|
|
self.assertIsInstance(d[0], DEX)
|
|
self.assertIsInstance(dx, Analysis)
|
|
|
|
self.assertEqual(a.get_signature_name(), "META-INF/CERT.RSA")
|
|
self.assertEqual(a.get_signature_names(), ["META-INF/CERT.RSA"])
|
|
|
|
self.assertIsNotNone(a.get_certificate(a.get_signature_name()))
|
|
|
|
def testAPKWrapperRaw(self):
|
|
with open(
|
|
os.path.join(test_dir, 'data/APK/TestActivity.apk'), 'rb'
|
|
) as file_obj:
|
|
file_contents = file_obj.read()
|
|
a, d, dx = AnalyzeAPK(file_contents, raw=True)
|
|
self.assertIsInstance(a, APK)
|
|
self.assertIsInstance(d[0], DEX)
|
|
self.assertIsInstance(dx, Analysis)
|
|
|
|
self.assertEqual(a.get_signature_name(), "META-INF/CERT.RSA")
|
|
self.assertEqual(a.get_signature_names(), ["META-INF/CERT.RSA"])
|
|
|
|
self.assertIsNotNone(a.get_certificate(a.get_signature_name()))
|
|
|
|
def testMultiDexAPK(self):
|
|
a, d, dx = AnalyzeAPK(
|
|
os.path.join(test_dir, 'data/APK/app-prod-debug.apk')
|
|
)
|
|
|
|
self.assertIsInstance(a, APK)
|
|
self.assertIsInstance(d[0], DEX)
|
|
self.assertIsInstance(d[1], DEX)
|
|
self.assertIsInstance(dx, Analysis)
|
|
|
|
def testAPKCert(self):
|
|
"""
|
|
Test if certificates are correctly unpacked from the SignatureBlock files
|
|
:return:
|
|
"""
|
|
a = APK(
|
|
os.path.join(test_dir, 'data/APK/TestActivity.apk'),
|
|
skip_analysis=True,
|
|
)
|
|
cert = a.get_certificate_der(a.get_signature_name())
|
|
expected = (
|
|
"308201E53082014EA00302010202045114FECF300D06092A864886F70D010105"
|
|
"05003037310B30090603550406130255533110300E060355040A1307416E6472"
|
|
"6F6964311630140603550403130D416E64726F6964204465627567301E170D31"
|
|
"33303230383133333430375A170D3433303230313133333430375A3037310B30"
|
|
"090603550406130255533110300E060355040A1307416E64726F696431163014"
|
|
"0603550403130D416E64726F696420446562756730819F300D06092A864886F7"
|
|
"0D010101050003818D00308189028181009903975EC93F0F3CCB54BD1A415ECF"
|
|
"3505993715B8B9787F321104ACC7397D186F01201341BCC5771BB28695318E00"
|
|
"6E47C888D3C7EE9D952FF04DF06EDAB1B511F51AACDCD02E0ECF5AA7EC6B51BA"
|
|
"08C601074CF2DA579BD35054E4F77BAAAAF0AA67C33C1F1C3EEE05B5862952C0"
|
|
"888D39179C0EDD785BA4F47FB7DF5D5F030203010001300D06092A864886F70D"
|
|
"0101050500038181006B571D685D41E77744F5ED20822AE1A14199811CE649BB"
|
|
"B29248EB2F3CC7FB70F184C2A3D17C4F86B884FCA57EEB289ECB5964A1DDBCBD"
|
|
"FCFC60C6B7A33D189927845067C76ED29B42D7F2C7F6E2389A4BC009C01041A3"
|
|
"6E666D76D1D66467416E68659D731DC7328CB4C2E989CF59BB6D2D2756FDE7F2"
|
|
"B3FB733EBB4C00FD3B"
|
|
)
|
|
|
|
self.assertEqual(
|
|
binascii.hexlify(cert).decode("ascii").upper(), expected
|
|
)
|
|
|
|
def testAPKCertFingerprint(self):
|
|
"""
|
|
Test if certificates are correctly unpacked from the SignatureBlock files
|
|
Check if fingerprints matches
|
|
:return:
|
|
"""
|
|
|
|
from hashlib import md5, sha1, sha256
|
|
|
|
a = APK(
|
|
os.path.join(test_dir, 'data/APK/TestActivity.apk'),
|
|
skip_analysis=True,
|
|
)
|
|
# this one is not signed v2, it is v1 only
|
|
self.assertTrue(a.is_signed_v1())
|
|
self.assertFalse(a.is_signed_v2())
|
|
self.assertTrue(a.is_signed())
|
|
self.assertEqual(a.get_certificates_der_v2(), [])
|
|
self.assertEqual(a.get_certificates_v2(), [])
|
|
|
|
self.assertEqual(a.get_signature_name(), "META-INF/CERT.RSA")
|
|
self.assertEqual(a.get_signature_names(), ["META-INF/CERT.RSA"])
|
|
|
|
cert = a.get_certificate(a.get_signature_name())
|
|
cert_der = a.get_certificate_der(a.get_signature_name())
|
|
|
|
# Keytool are the hashes collected by keytool -printcert -file CERT.RSA
|
|
for h2, keytool in [
|
|
(md5, "99:FF:FC:37:D3:64:87:DD:BA:AB:F1:7F:94:59:89:B5"),
|
|
(
|
|
sha1,
|
|
"1E:0B:E4:01:F9:34:60:E0:8D:89:A3:EF:6E:27:25:55:6B:E1:D1:6B",
|
|
),
|
|
(
|
|
sha256,
|
|
"6F:5C:31:60:8F:1F:9E:28:5E:B6:34:3C:7C:8A:F0:7D:E8:1C:1F:B2:14:8B:53:49:BE:C9:06:44:41:44:57:6D",
|
|
),
|
|
]:
|
|
x = h2()
|
|
x.update(cert_der)
|
|
hash_hashlib = x.hexdigest()
|
|
|
|
self.assertEqual(
|
|
hash_hashlib.lower(), keytool.replace(":", "").lower()
|
|
)
|
|
|
|
def testAPKv2Signature(self):
|
|
a = APK(
|
|
os.path.join(test_dir, 'data/APK/TestActivity_signed_both.apk')
|
|
)
|
|
|
|
self.assertTrue(a.is_signed_v1())
|
|
self.assertTrue(a.is_signed_v2())
|
|
self.assertTrue(a.is_signed())
|
|
|
|
# Signing name is maximal 8 chars...
|
|
self.assertEqual(a.get_signature_name(), "META-INF/ANDROGUA.RSA")
|
|
self.assertEqual(len(a.get_certificates_der_v2()), 1)
|
|
# As we signed with the same certificate, both methods should return the
|
|
# same content
|
|
self.assertEqual(
|
|
a.get_certificate_der(a.get_signature_name()),
|
|
a.get_certificates_der_v2()[0],
|
|
)
|
|
|
|
from asn1crypto import x509
|
|
|
|
self.assertIsInstance(a.get_certificates_v2()[0], x509.Certificate)
|
|
|
|
# Test if the certificate is also the same as on disk
|
|
with open(
|
|
os.path.join(test_dir, 'data/APK/certificate.der'), "rb"
|
|
) as f:
|
|
cert = f.read()
|
|
cert_der_v1 = a.get_certificate_der(a.get_signature_name())
|
|
cert_der_v2 = a.get_certificates_der_v2()[0]
|
|
|
|
for fun in [hashlib.md5, hashlib.sha1, hashlib.sha256, hashlib.sha512]:
|
|
h1 = fun(cert).hexdigest()
|
|
h2 = fun(cert_der_v1).hexdigest()
|
|
h3 = fun(cert_der_v2).hexdigest()
|
|
|
|
self.assertEqual(h1, h2)
|
|
self.assertEqual(h1, h3)
|
|
self.assertEqual(h2, h3)
|
|
|
|
def testApksignAPKs(self):
|
|
# These APKs are from the apksign testcases and cover
|
|
# all different signature algorithms as well as some error cases
|
|
from asn1crypto import pem, x509
|
|
|
|
root = os.path.join(test_dir, 'data/APK/apksig')
|
|
|
|
# Correct values generated with openssl:
|
|
# In the apksig repo:src/test/resources/com/android/apksig
|
|
# for f in *.pem; do openssl x509 -in $f -noout -sha256 -fingerprint; done
|
|
certfp = {
|
|
'dsa-1024.x509.pem': 'fee7c19ff9bfb4197b3727b9fd92d95406b1bd96db99ea642f5faac019a389d7',
|
|
'dsa-2048.x509.pem': '97cce0bab292c2d5afb9de90e1810b41a5d25c006a10d10982896aa12ab35a9e',
|
|
'dsa-3072.x509.pem': '966a4537058d24098ea213f12d4b24e37ff5a1d8f68deb8a753374881f23e474',
|
|
'ec-p256.x509.pem': '6a8b96e278e58f62cfe3584022cec1d0527fcb85a9e5d2e1694eb0405be5b599',
|
|
'ec-p384.x509.pem': '5e7777ada7ee7ce8f9c4d1b07094876e5604617b7988b4c5d5b764a23431afbe',
|
|
'ec-p521.x509.pem': '69b50381d98bebcd27df6d7df8af8c8b38d0e51e9168a95ab992d1a9da6082da',
|
|
'rsa-1024_2.x509.pem': 'eba3685e799f59804684abebf0363e14ccb1c213e2b954a22669714ed97f61e9',
|
|
'rsa-1024.x509.pem': 'bc5e64eab1c4b5137c0fbc5ed05850b3a148d1c41775cffa4d96eea90bdd0eb8',
|
|
'rsa-16384.x509.pem': 'f3c6b37909f6df310652fbd7c55ec27d3079dcf695dc6e75e22ba7c4e1c95601',
|
|
'rsa-2048_2.x509.pem': '681b0e56a796350c08647352a4db800cc44b2adc8f4c72fa350bd05d4d50264d',
|
|
'rsa-2048_3.x509.pem': 'bb77a72efc60e66501ab75953af735874f82cfe52a70d035186a01b3482180f3',
|
|
'rsa-2048.x509.pem': 'fb5dbd3c669af9fc236c6991e6387b7f11ff0590997f22d0f5c74ff40e04fca8',
|
|
'rsa-3072.x509.pem': '483934461229a780010bc07cd6eeb0b67025fc4fe255757abbf5c3f2ed249e89',
|
|
'rsa-4096.x509.pem': '6a46158f87753395a807edcc7640ac99c9125f6b6e025bdbf461ff281e64e685',
|
|
'rsa-8192.x509.pem': '060d0a24fea9b60d857225873f78838e081795f7ef2d1ea401262bbd75a58234',
|
|
}
|
|
|
|
will_not_validate_correctly = [
|
|
"targetSandboxVersion-2.apk",
|
|
"targetSandboxVersion-2.apk",
|
|
"v1-only-with-cr-in-entry-name.apk",
|
|
"v1-only-with-lf-in-entry-name.apk",
|
|
"v1-only-with-nul-in-entry-name.apk",
|
|
"v1-only-with-rsa-1024-cert-not-der2.apk",
|
|
"v2-only-cert-and-public-key-mismatch.apk",
|
|
"v2-only-with-dsa-sha256-1024-sig-does-not-verify.apk",
|
|
"debuggable-boolean.apk",
|
|
"debuggable-resource.apk",
|
|
"mismatched-compression-method.apk",
|
|
]
|
|
|
|
v1_only_signed_attrs_fail = [
|
|
"v1-only-with-signed-attrs-missing-content-type.apk",
|
|
"v1-only-with-signed-attrs-missing-digest.apk",
|
|
"v1-only-with-signed-attrs-multiple-good-digests.apk",
|
|
"v1-only-with-signed-attrs-signerInfo1-missing-content-type-signerInfo2-good.apk",
|
|
"v1-only-with-signed-attrs-signerInfo1-missing-digest-signerInfo2-good.apk",
|
|
"v1-only-with-signed-attrs-signerInfo1-multiple-good-digests-signerInfo2-good.apk",
|
|
"v1-only-with-signed-attrs-signerInfo1-wrong-content-type-signerInfo2-good.apk",
|
|
"v1-only-with-signed-attrs-signerInfo1-wrong-digest-signerInfo2-good.apk",
|
|
"v1-only-with-signed-attrs-signerInfo1-wrong-signature-signerInfo2-good.apk",
|
|
"v1-only-with-signed-attrs-wrong-content-type.apk",
|
|
"v1-only-with-signed-attrs-wrong-digest.apk",
|
|
"v1-only-with-signed-attrs-wrong-signature.apk",
|
|
]
|
|
|
|
# Collect possible hashes for certificates
|
|
# Unfortunately, not all certificates are supplied...
|
|
for apath in os.listdir(root):
|
|
if apath in certfp:
|
|
with open(os.path.join(root, apath), "rb") as fp:
|
|
cert = x509.Certificate.load(pem.unarmor(fp.read())[2])
|
|
h = cert.sha256_fingerprint.replace(" ", "").lower()
|
|
self.assertEqual(h, certfp[apath])
|
|
self.assertIn(h, certfp.values())
|
|
|
|
for apath in os.listdir(root):
|
|
if apath.endswith(".apk"):
|
|
if apath in will_not_validate_correctly:
|
|
# These APKs are faulty (by design) and will return a not correct fingerprint.
|
|
# TODO: we need to check if we can prevent such errors...
|
|
continue
|
|
|
|
a = APK(os.path.join(root, apath))
|
|
|
|
self.assertIsInstance(a, APK)
|
|
|
|
# Test if the correct method returns True, while others return
|
|
# False
|
|
m_tests = {
|
|
'1': a.is_signed_v1,
|
|
'2': a.is_signed_v2,
|
|
'3': a.is_signed_v3,
|
|
}
|
|
|
|
# These APKs will raise an error
|
|
excluded = [
|
|
"v1v2v3-with-rsa-2048-lineage-3-signers-no-sig-block.apk",
|
|
"v2-only-apk-sig-block-size-mismatch.apk",
|
|
"v2-only-empty.apk",
|
|
"v2-only-wrong-apk-sig-block-magic.apk",
|
|
"v2-stripped.apk",
|
|
"v2-stripped-with-ignorable-signing-schemes.apk",
|
|
"v2v3-signed-v3-block-stripped.apk",
|
|
"v3-only-empty.apk",
|
|
"v3-only-with-ecdsa-sha512-p384-wrong-apk-sig-block-magic.apk",
|
|
"v3-only-with-rsa-pkcs1-sha512-4096-apk-sig-block-size-mismatch.apk",
|
|
"v3-stripped.apk",
|
|
]
|
|
if apath[0] == "v" and apath not in excluded:
|
|
methods = apath.split("-", 1)[0].split("v")[1:]
|
|
for m, f in m_tests.items():
|
|
if m in methods:
|
|
self.assertTrue(f())
|
|
else:
|
|
self.assertFalse(f())
|
|
|
|
# Special error cases
|
|
if apath == "v2-only-apk-sig-block-size-mismatch.apk":
|
|
with self.assertRaises(apk.BrokenAPKError):
|
|
a.is_signed_v2()
|
|
continue
|
|
elif apath == "v2-only-empty.apk":
|
|
with self.assertRaises(apk.BrokenAPKError):
|
|
a.is_signed_v2()
|
|
continue
|
|
elif (
|
|
apath
|
|
== "v3-only-with-rsa-pkcs1-sha512-4096-apk-sig-block-size-mismatch.apk"
|
|
):
|
|
with self.assertRaises(apk.BrokenAPKError):
|
|
a.is_signed_v3()
|
|
continue
|
|
|
|
if a.is_signed_v1():
|
|
if apath == "v1-only-with-rsa-1024-cert-not-der.apk":
|
|
for sig in a.get_signature_names():
|
|
c = a.get_certificate(sig)
|
|
h = c.sha256_fingerprint.replace(" ", "").lower()
|
|
self.assertNotIn(h, certfp.values())
|
|
# print([apath, h]) # I do not know, why put this file?
|
|
der = a.get_certificate_der(sig)
|
|
self.assertEqual(
|
|
hashlib.sha256(der).hexdigest(), h
|
|
)
|
|
elif apath in v1_only_signed_attrs_fail:
|
|
for sig in a.get_signature_names():
|
|
c = a.get_certificate_der(sig)
|
|
self.assertEqual(c, None)
|
|
else:
|
|
for sig in a.get_signature_names():
|
|
c = a.get_certificate(sig)
|
|
h = c.sha256_fingerprint.replace(" ", "").lower()
|
|
self.assertIn(h, certfp.values())
|
|
|
|
# Check that we get the same signature if we take the DER
|
|
der = a.get_certificate_der(sig)
|
|
self.assertEqual(
|
|
hashlib.sha256(der).hexdigest(), h
|
|
)
|
|
|
|
if a.is_signed_v2():
|
|
if apath == "weird-compression-method.apk":
|
|
with self.assertRaises(NotImplementedError):
|
|
a.get_certificates_der_v2()
|
|
elif (
|
|
apath
|
|
== "v2-only-with-rsa-pkcs1-sha256-1024-cert-not-der.apk"
|
|
):
|
|
# FIXME
|
|
# Not sure what this one should do... but the certificate fingerprint is weird
|
|
# as the hash over the DER is not the same when using the certificate
|
|
continue
|
|
else:
|
|
for c in a.get_certificates_der_v2():
|
|
cert = x509.Certificate.load(c)
|
|
h = cert.sha256_fingerprint.replace(
|
|
" ", ""
|
|
).lower()
|
|
self.assertIn(h, certfp.values())
|
|
# Check that we get the same signature if we take the DER
|
|
self.assertEqual(hashlib.sha256(c).hexdigest(), h)
|
|
|
|
if a.is_signed_v3():
|
|
if apath == "weird-compression-method.apk":
|
|
with self.assertRaises(NotImplementedError):
|
|
a.get_certificates_der_v3()
|
|
elif (
|
|
apath
|
|
== "v3-only-with-rsa-pkcs1-sha256-3072-sig-does-not-verify.apk"
|
|
or apath == "v3-only-cert-and-public-key-mismatch.apk"
|
|
):
|
|
cert = x509.Certificate.load(
|
|
a.get_certificates_der_v3()[0]
|
|
)
|
|
h = cert.sha256_fingerprint.replace(" ", "").lower()
|
|
self.assertNotIn(h, certfp.values())
|
|
else:
|
|
for c in a.get_certificates_der_v3():
|
|
cert = x509.Certificate.load(c)
|
|
h = cert.sha256_fingerprint.replace(
|
|
" ", ""
|
|
).lower()
|
|
self.assertIn(h, certfp.values())
|
|
# Check that we get the same signature if we take the DER
|
|
self.assertEqual(hashlib.sha256(c).hexdigest(), h)
|
|
|
|
def testMultipleCertsReturnTheCorrect(self):
|
|
sha256_fingerprint = (
|
|
'01e1999710a82c2749b4d50c445dc85d670b6136089d0a766a73827c82a1eac9'
|
|
)
|
|
a = APK(os.path.join(test_dir, 'data/APK/CertChain.apk'))
|
|
sig = a.get_signature_names()[0]
|
|
c = a.get_certificate(sig)
|
|
self.assertEqual(
|
|
hashlib.sha256(c.dump()).hexdigest(), sha256_fingerprint
|
|
)
|
|
|
|
def testAPKWrapperUnsigned(self):
|
|
a, d, dx = AnalyzeAPK(
|
|
os.path.join(test_dir, 'data/APK/TestActivity_unsigned.apk')
|
|
)
|
|
self.assertIsInstance(a, APK)
|
|
self.assertIsInstance(d[0], DEX)
|
|
self.assertIsInstance(dx, Analysis)
|
|
|
|
self.assertIsNone(a.get_signature_name())
|
|
self.assertEqual(a.get_signature_names(), [])
|
|
|
|
def testAPKManifest(self):
|
|
a = APK(
|
|
os.path.join(test_dir, 'data/APK/TestActivity.apk'), testzip=True
|
|
)
|
|
self.assertEqual(a.get_app_name(), "TestsAndroguardApplication")
|
|
self.assertEqual(a.get_app_icon(), "res/drawable-hdpi/icon.png")
|
|
self.assertEqual(
|
|
a.get_app_icon(max_dpi=120), "res/drawable-ldpi/icon.png"
|
|
)
|
|
self.assertEqual(
|
|
a.get_app_icon(max_dpi=160), "res/drawable-mdpi/icon.png"
|
|
)
|
|
self.assertEqual(
|
|
a.get_app_icon(max_dpi=240), "res/drawable-hdpi/icon.png"
|
|
)
|
|
self.assertIsNone(a.get_app_icon(max_dpi=1))
|
|
self.assertEqual(
|
|
a.get_main_activity(), "tests.androguard.TestActivity"
|
|
)
|
|
self.assertEqual(a.get_package(), "tests.androguard")
|
|
self.assertEqual(a.get_androidversion_code(), '1')
|
|
self.assertEqual(a.get_androidversion_name(), "1.0")
|
|
self.assertEqual(a.get_min_sdk_version(), "9")
|
|
self.assertEqual(a.get_target_sdk_version(), "16")
|
|
self.assertIsNone(a.get_max_sdk_version())
|
|
self.assertEqual(a.get_permissions(), [])
|
|
self.assertEqual(a.get_declared_permissions(), [])
|
|
self.assertTrue(a.is_valid_APK())
|
|
|
|
def testAPKPermissions(self):
|
|
a = APK(
|
|
os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True
|
|
)
|
|
|
|
self.assertEqual(a.get_package(), "a2dp.Vol")
|
|
self.assertListEqual(
|
|
sorted(a.get_permissions()),
|
|
sorted(
|
|
[
|
|
"android.permission.RECEIVE_BOOT_COMPLETED",
|
|
"android.permission.CHANGE_WIFI_STATE",
|
|
"android.permission.ACCESS_WIFI_STATE",
|
|
"android.permission.KILL_BACKGROUND_PROCESSES",
|
|
"android.permission.BLUETOOTH",
|
|
"android.permission.BLUETOOTH_ADMIN",
|
|
"com.android.launcher.permission.READ_SETTINGS",
|
|
"android.permission.RECEIVE_SMS",
|
|
"android.permission.MODIFY_AUDIO_SETTINGS",
|
|
"android.permission.READ_CONTACTS",
|
|
"android.permission.ACCESS_COARSE_LOCATION",
|
|
"android.permission.ACCESS_FINE_LOCATION",
|
|
"android.permission.ACCESS_LOCATION_EXTRA_COMMANDS",
|
|
"android.permission.WRITE_EXTERNAL_STORAGE",
|
|
"android.permission.READ_PHONE_STATE",
|
|
"android.permission.BROADCAST_STICKY",
|
|
"android.permission.GET_ACCOUNTS",
|
|
]
|
|
),
|
|
)
|
|
|
|
def testAPKActivitiesAreString(self):
|
|
a = APK(
|
|
os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True
|
|
)
|
|
activities = a.get_activities()
|
|
self.assertTrue(
|
|
isinstance(activities[0], str), 'activities[0] is not of type str'
|
|
)
|
|
|
|
def testAPKIntentFilters(self):
|
|
from androguard.util import set_log
|
|
|
|
set_log("ERROR")
|
|
a = APK(
|
|
os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True
|
|
)
|
|
activities = a.get_activities()
|
|
receivers = a.get_receivers()
|
|
services = a.get_services()
|
|
filter_list = []
|
|
for i in activities:
|
|
filters = a.get_intent_filters("activity", i)
|
|
if len(filters) > 0:
|
|
filter_list.append(filters)
|
|
self.assertEqual(
|
|
[
|
|
{
|
|
'action': ['android.intent.action.MAIN'],
|
|
'category': ['android.intent.category.LAUNCHER'],
|
|
}
|
|
],
|
|
filter_list,
|
|
)
|
|
filter_list = []
|
|
for i in receivers:
|
|
filters = a.get_intent_filters("receiver", i)
|
|
if len(filters) > 0:
|
|
filter_list.append(filters)
|
|
for expected in [
|
|
{
|
|
'action': [
|
|
'android.intent.action.BOOT_COMPLETED',
|
|
'android.intent.action.MY_PACKAGE_REPLACED',
|
|
],
|
|
'category': ['android.intent.category.HOME'],
|
|
},
|
|
{'action': ['android.appwidget.action.APPWIDGET_UPDATE']},
|
|
]:
|
|
assert expected in filter_list
|
|
filter_list = []
|
|
for i in services:
|
|
filters = a.get_intent_filters("service", i)
|
|
if len(filters) > 0:
|
|
filter_list.append(filters)
|
|
self.assertEqual(
|
|
filter_list,
|
|
[
|
|
{
|
|
'action': [
|
|
'android.service.notification.NotificationListenerService'
|
|
]
|
|
}
|
|
],
|
|
)
|
|
|
|
a = APK(
|
|
os.path.join(test_dir, 'data/APK/com.test.intent_filter.apk'),
|
|
testzip=True,
|
|
)
|
|
|
|
activities = a.get_activities()
|
|
receivers = a.get_receivers()
|
|
services = a.get_services()
|
|
filter_list = []
|
|
for i in activities:
|
|
filters = a.get_intent_filters("activity", i)
|
|
if len(filters) > 0:
|
|
filter_list.append(filters)
|
|
for expected in [
|
|
{
|
|
'action': ['android.intent.action.VIEW'],
|
|
'category': [
|
|
'android.intent.category.APP_BROWSER',
|
|
'android.intent.category.DEFAULT',
|
|
'android.intent.category.BROWSABLE',
|
|
],
|
|
'data': [
|
|
{
|
|
'scheme': 'testscheme',
|
|
'host': 'testhost',
|
|
'port': '0301',
|
|
'path': '/testpath',
|
|
'pathPattern': 'testpattern',
|
|
'mimeType': 'text/html',
|
|
}
|
|
],
|
|
},
|
|
{
|
|
'action': ['android.intent.action.MAIN'],
|
|
'category': ['android.intent.category.LAUNCHER'],
|
|
},
|
|
]:
|
|
assert expected in filter_list
|
|
filter_list = []
|
|
for i in receivers:
|
|
filters = a.get_intent_filters("receiver", i)
|
|
if len(filters) > 0:
|
|
filter_list.append(filters)
|
|
self.assertEqual(
|
|
filter_list,
|
|
[
|
|
{
|
|
'action': ['android.intent.action.VIEW'],
|
|
'category': [
|
|
'android.intent.category.DEFAULT',
|
|
'android.intent.category.BROWSABLE',
|
|
],
|
|
'data': [
|
|
{
|
|
'scheme': 'testhost',
|
|
'host': 'testscheme',
|
|
'port': '0301',
|
|
'path': '/testpath',
|
|
'pathPattern': 'testpattern',
|
|
'mimeType': 'text/html',
|
|
}
|
|
],
|
|
}
|
|
],
|
|
)
|
|
filter_list = []
|
|
for i in services:
|
|
filters = a.get_intent_filters("service", i)
|
|
if len(filters) > 0:
|
|
filter_list.append(filters)
|
|
self.assertEqual(
|
|
filter_list,
|
|
[
|
|
{
|
|
'action': ['android.intent.action.RESPOND_VIA_MESSAGE'],
|
|
'data': [
|
|
{
|
|
'scheme': 'testhost',
|
|
'host': 'testscheme',
|
|
'port': '0301',
|
|
'path': '/testpath',
|
|
'pathPattern': 'testpattern',
|
|
'mimeType': 'text/html',
|
|
},
|
|
{
|
|
'scheme': 'testscheme2',
|
|
'host': 'testhost2',
|
|
'port': '0301',
|
|
'path': '/testpath2',
|
|
'pathPattern': 'testpattern2',
|
|
'mimeType': 'image/png',
|
|
},
|
|
],
|
|
}
|
|
],
|
|
)
|
|
|
|
def testEffectiveTargetSdkVersion(self):
|
|
|
|
a = APK(os.path.join(test_dir, 'data/APK/app-prod-debug.apk'))
|
|
self.assertEqual(27, a.get_effective_target_sdk_version())
|
|
|
|
a = APK(os.path.join(test_dir, 'data/APK/Invalid.apk'))
|
|
self.assertEqual(15, a.get_effective_target_sdk_version())
|
|
|
|
a = APK(os.path.join(test_dir, 'data/APK/TC-debug.apk'))
|
|
self.assertEqual(1, a.get_effective_target_sdk_version())
|
|
|
|
a = APK(os.path.join(test_dir, 'data/APK/TCDiff-debug.apk'))
|
|
self.assertEqual(1, a.get_effective_target_sdk_version())
|
|
|
|
a = APK(os.path.join(test_dir, 'data/APK/TestActivity.apk'))
|
|
self.assertEqual(16, a.get_effective_target_sdk_version())
|
|
|
|
a = APK(os.path.join(test_dir, 'data/APK/TestActivity_unsigned.apk'))
|
|
self.assertEqual(16, a.get_effective_target_sdk_version())
|
|
|
|
a = APK(os.path.join(test_dir, 'data/APK/Test-debug.apk'))
|
|
self.assertEqual(1, a.get_effective_target_sdk_version())
|
|
|
|
a = APK(os.path.join(test_dir, 'data/APK/Test-debug-unaligned.apk'))
|
|
self.assertEqual(1, a.get_effective_target_sdk_version())
|
|
|
|
a = APK(os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'))
|
|
self.assertEqual(25, a.get_effective_target_sdk_version())
|
|
|
|
a = APK(os.path.join(test_dir, 'data/APK/hello-world.apk'))
|
|
self.assertEqual(25, a.get_effective_target_sdk_version())
|
|
|
|
a = APK(
|
|
os.path.join(
|
|
test_dir, 'data/APK/duplicate.permisssions_9999999.apk'
|
|
)
|
|
)
|
|
self.assertEqual(27, a.get_effective_target_sdk_version())
|
|
|
|
a = APK(os.path.join(test_dir, 'data/APK/com.politedroid_4.apk'))
|
|
self.assertEqual(3, a.get_effective_target_sdk_version())
|
|
|
|
def testUsesImpliedPermissions(self):
|
|
|
|
a = APK(os.path.join(test_dir, 'data/APK/app-prod-debug.apk'))
|
|
self.assertEqual(
|
|
[
|
|
['android.permission.READ_EXTERNAL_STORAGE', None],
|
|
],
|
|
a.get_uses_implied_permission_list(),
|
|
)
|
|
a = APK(os.path.join(test_dir, 'data/APK/Invalid.apk'))
|
|
self.assertEqual([], a.get_uses_implied_permission_list())
|
|
a = APK(os.path.join(test_dir, 'data/APK/TC-debug.apk'))
|
|
self.assertEqual(
|
|
[
|
|
['android.permission.WRITE_EXTERNAL_STORAGE', None],
|
|
['android.permission.READ_PHONE_STATE', None],
|
|
['android.permission.READ_EXTERNAL_STORAGE', None],
|
|
],
|
|
a.get_uses_implied_permission_list(),
|
|
)
|
|
a = APK(os.path.join(test_dir, 'data/APK/TCDiff-debug.apk'))
|
|
self.assertEqual(
|
|
[
|
|
['android.permission.WRITE_EXTERNAL_STORAGE', None],
|
|
['android.permission.READ_PHONE_STATE', None],
|
|
['android.permission.READ_EXTERNAL_STORAGE', None],
|
|
],
|
|
a.get_uses_implied_permission_list(),
|
|
)
|
|
a = APK(os.path.join(test_dir, 'data/APK/TestActivity.apk'))
|
|
self.assertEqual([], a.get_uses_implied_permission_list())
|
|
a = APK(os.path.join(test_dir, 'data/APK/TestActivity_unsigned.apk'))
|
|
self.assertEqual([], a.get_uses_implied_permission_list())
|
|
a = APK(os.path.join(test_dir, 'data/APK/Test-debug.apk'))
|
|
self.assertEqual(
|
|
[
|
|
['android.permission.WRITE_EXTERNAL_STORAGE', None],
|
|
['android.permission.READ_PHONE_STATE', None],
|
|
['android.permission.READ_EXTERNAL_STORAGE', None],
|
|
],
|
|
a.get_uses_implied_permission_list(),
|
|
)
|
|
a = APK(os.path.join(test_dir, 'data/APK/Test-debug-unaligned.apk'))
|
|
self.assertEqual(
|
|
[
|
|
['android.permission.WRITE_EXTERNAL_STORAGE', None],
|
|
['android.permission.READ_PHONE_STATE', None],
|
|
['android.permission.READ_EXTERNAL_STORAGE', None],
|
|
],
|
|
a.get_uses_implied_permission_list(),
|
|
)
|
|
a = APK(os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'))
|
|
self.assertEqual(
|
|
[
|
|
['android.permission.READ_EXTERNAL_STORAGE', None],
|
|
],
|
|
a.get_uses_implied_permission_list(),
|
|
)
|
|
a = APK(os.path.join(test_dir, 'data/APK/com.politedroid_4.apk'))
|
|
self.assertEqual(
|
|
[
|
|
['android.permission.WRITE_EXTERNAL_STORAGE', None],
|
|
['android.permission.READ_PHONE_STATE', None],
|
|
['android.permission.READ_EXTERNAL_STORAGE', None],
|
|
],
|
|
a.get_uses_implied_permission_list(),
|
|
)
|
|
a = APK(
|
|
os.path.join(
|
|
test_dir, 'data/APK/duplicate.permisssions_9999999.apk'
|
|
)
|
|
)
|
|
self.assertEqual(
|
|
[
|
|
['android.permission.READ_EXTERNAL_STORAGE', 18],
|
|
],
|
|
a.get_uses_implied_permission_list(),
|
|
)
|
|
a = APK(os.path.join(test_dir, 'data/APK/hello-world.apk'))
|
|
self.assertEqual([], a.get_uses_implied_permission_list())
|
|
|
|
a = APK(
|
|
os.path.join(
|
|
test_dir,
|
|
'data/APK/urzip-πÇÇπÇÇ现代汉语通用字-български-عربي1234.apk',
|
|
)
|
|
)
|
|
self.assertEqual([], a.get_uses_implied_permission_list())
|
|
|
|
def testNewZipWithoutModification(self):
|
|
a = APK(
|
|
os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True
|
|
)
|
|
with patch('zipfile.ZipFile') as zipFile:
|
|
mockZip = MagicMock()
|
|
zipFile.return_value = mockZip
|
|
a.new_zip("testout.apk")
|
|
self.assertEqual(mockZip.writestr.call_count, 48)
|
|
self.assertTrue(mockZip.close.called)
|
|
|
|
def testNewZipWithDeletedFile(self):
|
|
a = APK(
|
|
os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True
|
|
)
|
|
with patch('zipfile.ZipFile') as zipFile:
|
|
mockZip = MagicMock()
|
|
zipFile.return_value = mockZip
|
|
a.new_zip("testout.apk", deleted_files="res/menu/menu.xml")
|
|
self.assertEqual(mockZip.writestr.call_count, 47)
|
|
self.assertTrue(mockZip.close.called)
|
|
|
|
def testNewZipWithNewFile(self):
|
|
a = APK(
|
|
os.path.join(test_dir, 'data/APK/a2dp.Vol_137.apk'), testzip=True
|
|
)
|
|
with patch('zipfile.ZipFile') as zipFile:
|
|
mockZip = MagicMock()
|
|
zipFile.return_value = mockZip
|
|
a.new_zip(
|
|
"testout.apk", new_files={'res/menu/menu.xml': 'content'}
|
|
)
|
|
self.assertEqual(mockZip.writestr.call_count, 48)
|
|
self.assertTrue(mockZip.close.called)
|
|
|
|
def testFeatures(self):
|
|
a = APK(
|
|
os.path.join(
|
|
test_dir, 'data/APK/com.example.android.tvleanback.apk'
|
|
)
|
|
)
|
|
self.assertListEqual(
|
|
sorted(list(a.get_features())),
|
|
[
|
|
"android.hardware.microphone",
|
|
"android.hardware.touchscreen",
|
|
"android.software.leanback",
|
|
],
|
|
)
|
|
self.assertTrue(a.is_androidtv())
|
|
self.assertFalse(a.is_wearable())
|
|
self.assertTrue(a.is_leanback())
|
|
|
|
# Second Demo App
|
|
a = APK(
|
|
os.path.join(
|
|
test_dir,
|
|
'data/APK/com.example.android.wearable.wear.weardrawers.apk',
|
|
)
|
|
)
|
|
self.assertListEqual(
|
|
list(a.get_features()), ["android.hardware.type.watch"]
|
|
)
|
|
self.assertTrue(a.is_wearable())
|
|
self.assertFalse(a.is_leanback())
|
|
self.assertFalse(a.is_androidtv())
|
|
self.assertListEqual(
|
|
list(a.get_libraries()), ["com.google.android.wearable"]
|
|
)
|
|
|
|
def testAdaptiveIcon(self):
|
|
# See https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive.html
|
|
a = APK(
|
|
os.path.join(
|
|
test_dir, 'data/APK/com.android.example.text.styling.apk'
|
|
)
|
|
)
|
|
|
|
self.assertEqual(
|
|
a.get_app_icon(), "res/mipmap-anydpi-v26/ic_launcher.xml"
|
|
)
|
|
x = AXMLPrinter(a.get_file(a.get_app_icon())).get_xml().decode("UTF-8")
|
|
self.assertIn("adaptive-icon", x)
|
|
|
|
# * ldpi (low) ~120dpi
|
|
# * mdpi (medium) ~160dpi
|
|
# * hdpi (high) ~240dpi
|
|
# * xhdpi (extra-high) ~320dpi
|
|
# * xxhdpi (extra-extra-high) ~480dpi
|
|
# * xxxhdpi (extra-extra-extra-high) ~640dpi
|
|
self.assertIsNone(a.get_app_icon(max_dpi=120)) # No LDPI icon
|
|
self.assertIn("mdpi", a.get_app_icon(max_dpi=160))
|
|
self.assertIn("hdpi", a.get_app_icon(max_dpi=240))
|
|
self.assertIn("xhdpi", a.get_app_icon(max_dpi=320))
|
|
self.assertIn("xxhdpi", a.get_app_icon(max_dpi=480))
|
|
self.assertIn("xxxhdpi", a.get_app_icon(max_dpi=640))
|
|
|
|
self.assertIn(".png", a.get_app_icon(max_dpi=65533))
|
|
self.assertIn(".xml", a.get_app_icon(max_dpi=65534))
|
|
|
|
def testPartialSignature(self):
|
|
a = APK(
|
|
os.path.join(test_dir, 'data/APK/partialsignature.apk'),
|
|
skip_analysis=True,
|
|
)
|
|
|
|
self.assertIn("META-INF/CERT.RSA", a.get_files())
|
|
self.assertIn("META-INF/6AD89F48.RSA", a.get_files())
|
|
|
|
self.assertNotIn("META-INF/CERT.RSA", a.get_signature_names())
|
|
self.assertIn("META-INF/6AD89F48.RSA", a.get_signature_names())
|
|
|
|
def testFrameworkResAPK(self):
|
|
a = APK(
|
|
os.path.join(
|
|
test_dir, 'data/APK/lineageos_nexus5_framework-res.apk'
|
|
)
|
|
)
|
|
|
|
self.assertEqual(a.get_app_name(), 'Android System')
|
|
self.assertEqual(a.get_package(), 'android')
|
|
|
|
def testPermissionLoading(self):
|
|
"""Test if fallbacks for permission lists are working"""
|
|
import re
|
|
|
|
from androguard.core.androconf import (
|
|
CONF,
|
|
InvalidResourceError,
|
|
load_api_specific_resource_module,
|
|
)
|
|
from androguard.core.api_specific_resources import load_permissions
|
|
|
|
aosp_permissions = os.path.join(
|
|
test_dir,
|
|
'../androguard/core/api_specific_resources/aosp_permissions',
|
|
)
|
|
levels = filter(
|
|
lambda x: re.match(r'^permissions_\d+\.json$', x),
|
|
os.listdir(aosp_permissions),
|
|
)
|
|
levels = list(map(lambda x: int(x[:-5].split('_')[1]), levels))
|
|
|
|
min_level = min(levels)
|
|
max_level = max(levels)
|
|
|
|
self.assertGreater(min_level, 0)
|
|
self.assertGreater(max_level, 0)
|
|
|
|
self.assertNotEqual(load_permissions(min_level), {})
|
|
self.assertNotEqual(load_permissions(min_level, 'groups'), {})
|
|
self.assertNotEqual(load_permissions(max_level), {})
|
|
self.assertNotEqual(load_permissions(max_level, 'groups'), {})
|
|
|
|
self.assertNotEqual(load_permissions(max_level - 1), {})
|
|
self.assertNotEqual(load_permissions(max_level - 1, 'groups'), {})
|
|
|
|
self.assertNotEqual(load_permissions(min_level + 1), {})
|
|
self.assertNotEqual(load_permissions(min_level + 1, 'groups'), {})
|
|
|
|
self.assertEqual(
|
|
load_permissions(min_level - 1), load_permissions(min_level)
|
|
)
|
|
self.assertEqual(
|
|
load_permissions(max_level + 1), load_permissions(max_level)
|
|
)
|
|
|
|
self.assertEqual(load_permissions(0), load_permissions(min_level))
|
|
self.assertEqual(load_permissions(1337), load_permissions(max_level))
|
|
|
|
with self.assertRaises(ValueError):
|
|
load_permissions(23, 'foobar')
|
|
|
|
with self.assertRaises(InvalidResourceError):
|
|
load_api_specific_resource_module('blablabla')
|
|
|
|
self.assertEqual(
|
|
load_permissions(16),
|
|
load_api_specific_resource_module('aosp_permissions', 16),
|
|
)
|
|
self.assertEqual(
|
|
load_permissions(CONF['DEFAULT_API']),
|
|
load_api_specific_resource_module('aosp_permissions'),
|
|
)
|
|
|
|
for level in levels:
|
|
perm = load_permissions(level)
|
|
self.assertIn('android.permission.INTERNET', perm)
|
|
self.assertIsInstance(perm, dict)
|
|
self.assertIsInstance(perm['android.permission.INTERNET'], dict)
|
|
self.assertIn('description', perm['android.permission.INTERNET'])
|
|
self.assertIn('label', perm['android.permission.INTERNET'])
|
|
self.assertIn(
|
|
'protectionLevel', perm['android.permission.INTERNET']
|
|
)
|
|
self.assertIn(
|
|
'permissionGroup', perm['android.permission.INTERNET']
|
|
)
|
|
|
|
def testCustomPermissionProtectionLevel(self):
|
|
a = APK(
|
|
os.path.join(
|
|
test_dir, 'data/APK/com.example.android.tvleanback.apk'
|
|
)
|
|
)
|
|
self.assertEqual(
|
|
a.get_details_permissions()[
|
|
"com.example.android.tvleanback.ACCESS_VIDEO_DATA"
|
|
][0],
|
|
'signature',
|
|
)
|
|
|
|
def testShortNamesInManifest(self):
|
|
"""Test if shortnames are correctly handled"""
|
|
a = apk.APK(
|
|
os.path.join(test_dir, 'data/APK/AndroidManifest_ShortName.apk')
|
|
)
|
|
|
|
self.assertEqual(a.get_package(), 'com.android.galaxy4')
|
|
|
|
self.assertEqual(len(a.get_activities()), 1)
|
|
self.assertEqual(len(a.get_services()), 1)
|
|
|
|
self.assertEqual(a.get_activities()[0], 'com.android.galaxy4.Galaxy4')
|
|
self.assertEqual(
|
|
a.get_services()[0], 'com.android.galaxy4.Galaxy4Wallpaper'
|
|
)
|
|
|
|
self.assertEqual(
|
|
list(a.get_all_attribute_value("activity", "name"))[0],
|
|
'com.android.galaxy4.Galaxy4',
|
|
)
|
|
self.assertEqual(
|
|
list(
|
|
a.get_all_attribute_value(
|
|
"activity", "name", format_value=False
|
|
)
|
|
)[0],
|
|
'.Galaxy4',
|
|
)
|
|
|
|
# Test some formatting
|
|
self.assertEqual(a._format_value('foo'), 'com.android.galaxy4.foo')
|
|
self.assertEqual(a._format_value('.foo'), 'com.android.galaxy4.foo')
|
|
self.assertEqual(
|
|
a._format_value('com.android.galaxy4.foo'),
|
|
'com.android.galaxy4.foo',
|
|
)
|
|
self.assertEqual(a._format_value('bla.bar.foo'), 'bla.bar.foo')
|
|
self.assertEqual(a._format_value(None), None)
|
|
|
|
a.package = None
|
|
self.assertEqual(a._format_value('foo'), 'foo')
|
|
self.assertEqual(a._format_value('.foo'), '.foo')
|
|
self.assertEqual(
|
|
a._format_value('com.android.galaxy4.foo'),
|
|
'com.android.galaxy4.foo',
|
|
)
|
|
self.assertEqual(a._format_value('bla.bar.foo'), 'bla.bar.foo')
|
|
self.assertEqual(a._format_value(None), None)
|
|
|
|
def testMultipleLocaleAppName(self):
|
|
"""Test multiple locale appname"""
|
|
a = apk.APK(
|
|
os.path.join(test_dir, 'data/APK/multiple_locale_appname_test.apk')
|
|
)
|
|
self.assertEqual(a.get_app_name(), "values")
|
|
self.assertEqual(a.get_app_name(locale='en'), "values-en")
|
|
self.assertEqual(a.get_app_name(locale='zh-rCN'), "values-zh-rCN")
|
|
self.assertEqual(a.get_app_name(locale='zh-rTW'), "values-zh-rTW")
|
|
self.assertEqual(a.get_app_name(locale='ru-rRU'), "values-ru-rRU")
|
|
|
|
def testPublicKeysofApk(self):
|
|
a = APK(
|
|
os.path.join(
|
|
test_dir,
|
|
'data/APK/com.example.android.wearable.wear.weardrawers.apk',
|
|
)
|
|
)
|
|
pkeys = set(a.get_public_keys_der_v3() + a.get_public_keys_der_v2())
|
|
for public_key in pkeys:
|
|
from androguard.util import calculate_fingerprint, parse_public
|
|
|
|
parsed_key = parse_public(public_key)
|
|
self.assertEqual(parsed_key.algorithm, 'rsa')
|
|
self.assertEqual(parsed_key.bit_size, 2048)
|
|
self.assertEqual(
|
|
calculate_fingerprint(parsed_key).hex(),
|
|
'98917cd03c6277d73d58b661d614c442f2981a35a5aa122a61049215ba85c1d4',
|
|
)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main(failfast=True)
|