verify-permissions.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  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 subprocess
  17. import sys
  18. from xml.etree import ElementTree
  19. # Get external packages
  20. try:
  21. from parse import parse
  22. except ImportError:
  23. print('Please install the "parse" package via pip3.')
  24. exit(errno.ENOPKG)
  25. try:
  26. import requests
  27. except ImportError:
  28. print('Please install the "requests" package via pip3.')
  29. exit(errno.ENOPKG)
  30. # Change working directory to the location of this script
  31. # This fixes relative path references when calling this script from
  32. # outside of the directory containing it
  33. os.chdir(sys.path[0])
  34. # Definitions for privileged permissions
  35. ANDROID_MANIFEST_XML = \
  36. 'https://raw.githubusercontent.com/LineageOS/android_frameworks_base/lineage-21.0/core/res/AndroidManifest.xml'
  37. ANDROID_XML_NS = '{http://schemas.android.com/apk/res/android}'
  38. privileged_permissions = set()
  39. privileged_permission_mask = {'privileged', 'signature'}
  40. # Get AndroidManifest.xml
  41. req = requests.get(ANDROID_MANIFEST_XML)
  42. # Parse AndroidManifest.xml to get signature|privileged permissions
  43. root = ElementTree.fromstring(req.text)
  44. for perm in root.findall('permission'):
  45. # Get name of permission
  46. name = perm.get(f'{ANDROID_XML_NS}name')
  47. # Get the protection levels on the permission
  48. levels = set(
  49. perm.get(f'{ANDROID_XML_NS}protectionLevel').split('|'))
  50. # Check if the protections include signature and privileged
  51. levels_masked = levels & privileged_permission_mask
  52. if len(levels_masked) == len(privileged_permission_mask):
  53. privileged_permissions.add(name)
  54. # List of partitions to check priv-app permissions on
  55. partitions = ['product', 'system_ext']
  56. # Definitions for privapp-permissions
  57. # Dictionary with structure:
  58. # partition: permissions_dictionary
  59. # Where permissions_dictionary has the structure:
  60. # package_name : (set(allowed_permissions), set(requested_permissions))
  61. privapp_permissions_dict = {x: {} for x in partitions}
  62. # Definitions for privapp-permission allowlists
  63. GLOB_XML_STR = '../*/proprietary/{}/etc/permissions/privapp-permissions*.xml'
  64. # Parse allowlists to extract allowed privileged permissions
  65. for partition in partitions:
  66. # Get pointer to permissions_dictionary for the partition
  67. perm_dict = privapp_permissions_dict[partition]
  68. # Loop over all the XMLs in the partition we want
  69. for allowlist in glob(GLOB_XML_STR.format(partition)):
  70. # Get root of XML
  71. tree = ElementTree.parse(allowlist)
  72. root = tree.getroot()
  73. # Loop through and find packages
  74. for package in root.findall('privapp-permissions'):
  75. name = package.get('package')
  76. # Create empty entry if it's not in the dictionary
  77. if name not in perm_dict:
  78. perm_dict[name] = (set(), set())
  79. # Get all permissions and add them to dictionary
  80. for permission in package.findall('permission'):
  81. perm_dict[name][0].add(permission.get('name'))
  82. for permission in package.findall('deny-permission'):
  83. perm_dict[name][0].add(permission.get('name'))
  84. # Definitions for parsing APKs
  85. GLOB_APK_STR = '../*/proprietary/{}/priv-app/*/*.apk'
  86. AAPT_CMD = ['aapt', 'd', 'permissions']
  87. # Extract requested privileged permissions from all priv-app APKs
  88. for partition in partitions:
  89. # Get pointer to permissions_dictionary for the partition
  90. perm_dict = privapp_permissions_dict[partition]
  91. # Loop over all the APKs in the partition we want
  92. for apk in glob(GLOB_APK_STR.format(partition)):
  93. # Run 'aapt d permissions' on APK
  94. aapt_output = subprocess.check_output(AAPT_CMD + [apk],
  95. stderr=subprocess.STDOUT).decode(encoding='UTF-8')
  96. lines = aapt_output.splitlines()
  97. # Extract package name from the output
  98. # Output looks like:
  99. # package: my.package.name
  100. package_name = parse('package: {}', lines[0])[0]
  101. # Create empty entry if package is not in dict
  102. if package_name not in perm_dict:
  103. perm_dict[package_name] = (set(), set())
  104. # Extract 'uses-permission' lines from the rest of the output
  105. # Relevant output looks like:
  106. # uses-permission: name='permission'
  107. for line in lines[1:]:
  108. # Extract permission name and add it to the dictionary if it's
  109. # one of the privileged permissions we extracted earlier
  110. if perm_name := parse('uses-permission: name=\'{}\'', line):
  111. if perm_name[0] in privileged_permissions:
  112. perm_dict[package_name][1].add(perm_name[0])
  113. # Keep track of exit code
  114. rc = 0
  115. # Find all the missing permissions
  116. for partition in partitions:
  117. # Get pointer to permissions_dictionary for the partition
  118. perm_dict = privapp_permissions_dict[partition]
  119. # Loop through all the packages and compare permission sets
  120. for package in perm_dict:
  121. # Get the sets of permissions
  122. # Format is (allowed, requested)
  123. perm_sets = perm_dict[package]
  124. # Compute the set difference requested - allowed
  125. # This gives us all the permissions requested that were not allowed
  126. perm_diff = perm_sets[1] - perm_sets[0]
  127. # If any permissions are left, set exit code to EPERM and print output
  128. if len(perm_diff) > 0:
  129. rc = errno.EPERM
  130. sys.stderr.write(
  131. f"Package {package} on partition {partition} is missing these permissions:\n")
  132. for perm in perm_diff:
  133. sys.stderr.write(f" - {perm}\n")
  134. # Exit program
  135. exit(rc)