verify-permissions.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. #!/usr/bin/python3
  2. #
  3. # Copyright (C) 2021 Paul Keith <[email protected]>
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License version 2 and
  7. # only version 2 as published by the Free Software Foundation.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. import errno
  14. from glob import glob
  15. import os
  16. import re
  17. import subprocess
  18. import sys
  19. from xml.etree import ElementTree
  20. # Get external packages
  21. try:
  22. import requests
  23. except ImportError:
  24. print('Please install the "requests" package via pip3.')
  25. exit(errno.ENOPKG)
  26. # Change working directory to the location of this script
  27. # This fixes relative path references when calling this script from
  28. # outside of the directory containing it
  29. os.chdir(sys.path[0])
  30. # Definitions for privileged permissions
  31. ANDROID_MANIFEST_XML = \
  32. 'https://raw.githubusercontent.com/LineageOS/android_frameworks_base/lineage-19.0/core/res/AndroidManifest.xml'
  33. ANDROID_XML_NS = '{http://schemas.android.com/apk/res/android}'
  34. privileged_permissions = set()
  35. privileged_permission_mask = {'privileged', 'signature'}
  36. # Get AndroidManifest.xml
  37. req = requests.get(ANDROID_MANIFEST_XML)
  38. # Parse AndroidManifest.xml to get signature|privileged permissions
  39. root = ElementTree.fromstring(req.text)
  40. for perm in root.findall('permission'):
  41. # Get name of permission
  42. name = perm.get(f'{ANDROID_XML_NS}name')
  43. # Get the protection levels on the permission
  44. levels = set(
  45. perm.get(f'{ANDROID_XML_NS}protectionLevel').split('|'))
  46. # Check if the protections include signature and privileged
  47. levels_masked = levels & privileged_permission_mask
  48. if len(levels_masked) == len(privileged_permission_mask):
  49. privileged_permissions.add(name)
  50. # Definitions for privapp-permissions
  51. # Dictionary with structure:
  52. # package_name : (set(allowed_permissions), set(requested_permissions))
  53. privapp_permissions_dict = {}
  54. # Definitions for privapp-permission allowlists
  55. GLOB_XML_STR = '../*/proprietary/*/etc/permissions/privapp-permissions*.xml'
  56. # Parse allowlists to extract allowed privileged permissions
  57. for allowlist in glob(GLOB_XML_STR):
  58. # Get root of XML
  59. tree = ElementTree.parse(allowlist)
  60. root = tree.getroot()
  61. # Loop through and find packages
  62. for package in root.findall('privapp-permissions'):
  63. name = package.get('package')
  64. # Create empty entry if it's not in the dictionary
  65. if name not in privapp_permissions_dict:
  66. privapp_permissions_dict[name] = (set(), set())
  67. # Get all permissions and add them to dictionary
  68. for permission in package.findall('permission'):
  69. privapp_permissions_dict[name][0].add(permission.get('name'))
  70. for permission in package.findall('deny-permission'):
  71. privapp_permissions_dict[name][0].add(permission.get('name'))
  72. # Definitions for parsing APKs
  73. GLOB_APK_STR = '../*/proprietary/*/priv-app/*/*.apk'
  74. AAPT_CMD = ['aapt', 'd', 'permissions']
  75. # Extract requested privileged permissions from all priv-app APKs
  76. for apk in glob(GLOB_APK_STR):
  77. # Run 'aapt d permissions' on APK
  78. aapt_output = subprocess.check_output(AAPT_CMD + [apk],
  79. stderr=subprocess.STDOUT).decode(encoding='UTF-8')
  80. lines = aapt_output.split('\n')
  81. # Extract package name from the output
  82. # Output looks like:
  83. # package: my.package.name
  84. matches = re.search(r'package: ([\S]+)', lines[0])
  85. package_name = matches.group(1)
  86. # Create empty entry if package is not in dic
  87. if package_name not in privapp_permissions_dict:
  88. privapp_permissions_dict[package_name] = (set(), set())
  89. # Extract 'uses-permission' lines from the rest of the output
  90. # Relevant output looks like:
  91. # uses-permission: name='permission'
  92. for line in lines[1:]:
  93. if line.startswith('uses-permission'):
  94. # Extract permission name and add it to the dictionary if it's
  95. # one of the privileged permissions we extracted earlier
  96. matches = re.search(r"uses-permission.*: name='([\S]+)'", line)
  97. perm_name = matches.group(1)
  98. if perm_name in privileged_permissions:
  99. privapp_permissions_dict[package_name][1].add(perm_name)
  100. # Keep track of exit code
  101. rc = 0
  102. # Loop through all the packages and compare permission sets
  103. for package in privapp_permissions_dict:
  104. # Get the sets of permissions
  105. # Format is (allowed, requested)
  106. perm_sets = privapp_permissions_dict[package]
  107. # Compute the set difference requested - allowed
  108. # This gives us all the permissions requested that were not allowed
  109. perm_diff = perm_sets[1] - perm_sets[0]
  110. # If any permissions are left, set exit code to EPERM and print output
  111. if len(perm_diff) > 0:
  112. rc = errno.EPERM
  113. sys.stderr.write(f"Package {package} is missing these permissions:\n")
  114. for perm in perm_diff:
  115. sys.stderr.write(f" - {perm}\n")
  116. # Exit program
  117. exit(rc)