├── .gitignore ├── LICENSE ├── README.md ├── check-device-props.py ├── check-system-apps.py └── download-apks.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | *.bak 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android device check 2 | 3 | A set of scripts to check Android device security configuration. 4 | 5 | ## Device runtime configuration check 6 | 7 | The ```check-device-props.py``` script checks security configuration based on system properties 8 | and some basic system commands. 9 | 10 | ### Requirements 11 | 12 | Requires ADB connection. 13 | Set ```ANDROID_SERIAL``` and/or ```ADB_VENDOR_KEYS``` if more than one device is connected to host, 14 | or if ADB authentication is required. 15 | 16 | ### Major checks: 17 | 18 | * build type (userdebug, user, eng) 19 | * signing keys 20 | * SELinux availability and mode 21 | * debugging-related properties 22 | * Bluetooth configuration 23 | * USB/ADB configuration 24 | * 3G/telephony availability 25 | * enabled network interfaces 26 | * listening TCP services 27 | * ADB authentication 28 | * SUID binaries 29 | * AIDL services 30 | * disk encryption (FDE/FBE) availability 31 | * dm-verity availability and mode 32 | 33 | ### Usage 34 | 35 | 1. Connect to target device via ADB and run the script: 36 | 37 | ```bash 38 | ./check-device-props.py 39 | ``` 40 | 41 | 2. Report is output to stdout, redirect as needed. `WARN` messages mark potential configuration issues. 42 | 43 | ## System APK check 44 | 45 | ### Overview 46 | 47 | A simple script to check security configuration of system APKs for 48 | Android-based devices. Mainly targeted towards IoT-style devices, 49 | probably not that useful for phones/tablets. Not meant to be a 50 | replacement for CTS or other extensive test suites. 51 | 52 | Checks are focused on permissions, code signing and component configuration. 53 | This script does not attempt to perform static analysis of executable code. 54 | 55 | ### Assumptions 56 | 57 | The following assumptions are made: 58 | 59 | * device software is based on AOSP 60 | * device vendor components/apps all live under the same top-level package 61 | * system APKs from `system/` and `system-priv/` are accessible 62 | (either by downloading from live device or from build output) 63 | 64 | ### Major security checks 65 | 66 | The following security configuration is tested: 67 | 68 | * usage of shared user ID, esp. `android.uid.system` 69 | * whether 3rd-party (non-AOSP, not under top-level package) are running as `android.uid.system` 70 | * debuggable applications 71 | * whether custom (not defined in AOSP) permissions are signature-protected 72 | * whether protected broadcasts are used 73 | * whether APKs are signed with widely-known keys/certificates ('testkeys') 74 | * optionally prints all permissions and components declared in the APK (detailed mode) 75 | 76 | ### Requirements 77 | 78 | * Androguard >= 3.2.1 79 | * Python 2.7.x (for now) 80 | 81 | ### Usage 82 | 83 | 1. Obtain system APKs to test, usually all APKs under `/system/app` and `/system/priv-app` 84 | * if you can connect to a live device via ADB, you can use the `download-apks.py` helper script: 85 | ```bash 86 | $ ./download-apks.py apks/ 87 | ``` 88 | 2. Run the `check-system-apps.py` script against the APK directory from 1. 89 | * (optional) specify the `--show-apk-details` flag to show permissions and components declared in each APK. 90 | ```bash 91 | ./check-system-apps.py apks/ com.example.package 92 | ``` 93 | 3. Report is output to stdout, redirect as needed. -------------------------------------------------------------------------------- /check-device-props.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import re 5 | import socket 6 | import struct 7 | import subprocess 8 | 9 | SYS_PROP_RE = re.compile(r'^\[(\S+)\]: \[(\S+)\].*') 10 | NETSTAT_RE = re.compile(r'^\S+\s+\S+\s+\S+\s+(\S+)\s+.*') 11 | 12 | NET_IF_RE = re.compile(r'^(\S+)\s+Link encap:.*') 13 | IP_ADDR_RE = re.compile(r'^\s+inet addr:(\S+).*') 14 | 15 | SERVICE_RE = re.compile(r'\d+\s+(\S+): \[(\S+)\]') 16 | 17 | ADB_DEVICES_RE = re.compile(r'^(\S+)\s+(\S+)$') 18 | 19 | PRODUCT_PROP = 'ro.product' 20 | FINGERPRINT_PROP = 'ro.build.fingerprint' 21 | FLAVOR_PROP = 'ro.build.flavor' 22 | BUILD_PROP = 'ro.build.product' 23 | TAGS_PROP = 'ro.build.tags' 24 | TYPE_PROP = 'ro.build.type' 25 | FACTORY_MODE_PROP = 'ro.boot.factory_mode' 26 | SECURE_PROP = 'ro.secure' 27 | DEBUGGABLE_PROP = 'ro.debuggable' 28 | QCOM_BT_PROP = 'ro.qualcomm.bluetooth' 29 | USB_CONFIG_PROP = 'sys.usb.config' 30 | USB_STATE_PROP = 'sys.usb.state' 31 | GSM_NW_PROP = 'gsm.network.type' 32 | CRYPTO_STATE_PROP = 'ro.crypto.state' 33 | CRYPTO_PROPS = 'ro.crypto.' 34 | VERITY_MODE_PROP = 'ro.boot.veritymode' 35 | 36 | TYPE_USERDEBUG = 'userdebug' 37 | TYPE_ENG = 'eng' 38 | TEST_KEYS = 'test-keys' 39 | 40 | SELINUX_ENFORCING = 'Enforcing' 41 | VERITY_ENFORCING = 'enforcing' 42 | 43 | ADB_UNAUTHORIZED = 'unauthorized' 44 | ADB_AUTHORIZED = 'device' 45 | ADB_VENDOR_KEYS_ENV = 'ADB_VENDOR_KEYS' 46 | 47 | PRIVATE_NETS = ( 48 | ['127.0.0.0', '255.0.0.0'], 49 | ['192.168.0.0', '255.255.0.0'], 50 | ['172.16.0.0', '255.240.0.0'], 51 | ['10.0.0.0', '255.0.0.0'] 52 | ) 53 | 54 | # core services that don't have 'android' in the interface name only 55 | ANDROID_CORE_SERVICES = ('drm.drmManager', 'mount') 56 | 57 | 58 | def warn(msg, extra=None): 59 | log('WARN', msg, extra) 60 | 61 | 62 | def info(msg, extra=None): 63 | log('INFO', msg, extra) 64 | 65 | 66 | def err(msg, extra=None): 67 | log('ERR', msg, extra) 68 | 69 | 70 | def log(sev, msg, extra): 71 | print '%s: %s' % (sev, msg) 72 | if extra is not None: 73 | print '\t%s' % str(extra) 74 | 75 | 76 | def print_hr(): 77 | print '-' * 70 78 | 79 | 80 | def test_name(name): 81 | print '%s %s %s' % ('*' * 10, name, '*' * 10) 82 | 83 | 84 | def check_product(sys_props): 85 | product_props = {} 86 | for k in sys_props.keys(): 87 | if PRODUCT_PROP in k: 88 | product_props[k] = sys_props[k] 89 | 90 | info('Product info:') 91 | for k in product_props.keys(): 92 | info('\t%s=%s' % (k, product_props[k])) 93 | 94 | 95 | def check_build(sys_props): 96 | test_name('Build props check') 97 | 98 | build_type = sys_props[TYPE_PROP] 99 | build_flavor = sys_props[FLAVOR_PROP] 100 | 101 | if TYPE_USERDEBUG in build_type or TYPE_USERDEBUG in build_flavor: 102 | warn('userdebug build', (build_type, build_flavor)) 103 | 104 | if TYPE_ENG in build_type or TYPE_ENG in build_flavor: 105 | warn('eng build', (build_type, build_flavor)) 106 | 107 | 108 | def check_signing_keys(sys_props): 109 | test_name('Signing keys check') 110 | 111 | fingerprint = sys_props[FINGERPRINT_PROP] 112 | build_tags = sys_props[TAGS_PROP] 113 | 114 | if TEST_KEYS in fingerprint or TEST_KEYS in build_tags: 115 | warn('build is signed with test-keys', (fingerprint, build_tags)) 116 | 117 | 118 | def check_factory_mode(sys_props): 119 | test_name('Factory mode check') 120 | 121 | if FACTORY_MODE_PROP in sys_props.keys(): 122 | factory_mode = sys_props[FACTORY_MODE_PROP] 123 | if factory_mode == "1": 124 | warn("factory mode is on") 125 | 126 | 127 | def exec_command(cmd, ignore_err=False): 128 | p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 129 | res = p.communicate() 130 | if p.returncode != 0 and not ignore_err: 131 | err('Error executing [%s]: rc=%d, msg=%s' % (cmd, p.returncode, res[1])) 132 | if not ignore_err: 133 | return [] 134 | 135 | return res[0].splitlines() 136 | 137 | 138 | def check_selinux(): 139 | test_name('SELinux check') 140 | 141 | cmd = 'adb shell getenforce' 142 | p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 143 | res = p.communicate() 144 | if p.returncode != 0: 145 | print 'Error: rc=%d, msg=%s' % (p.returncode, res[1]) 146 | 147 | selinux_mode = res[0].splitlines()[0].strip() 148 | if selinux_mode != SELINUX_ENFORCING: 149 | warn('SELinux not in enforcing mode', selinux_mode) 150 | 151 | 152 | def check_debug(sys_props): 153 | test_name('Debuggable apps check') 154 | 155 | secure = sys_props[SECURE_PROP] 156 | debuggable = sys_props[DEBUGGABLE_PROP] 157 | 158 | if secure == '0': 159 | warn('Build is not secure', '%s=%s' % (SECURE_PROP, secure)) 160 | 161 | if debuggable == '1': 162 | warn('Build is debuggable', '%s=%s' % (DEBUGGABLE_PROP, debuggable)) 163 | 164 | 165 | def check_bt(sys_props): 166 | test_name('Bluetooth modes check') 167 | 168 | bt_props = {} 169 | for k in sys_props.keys(): 170 | if QCOM_BT_PROP in k: 171 | val = sys_props[k] 172 | if val == 'true': 173 | bt_props[k] = val 174 | 175 | if len(bt_props.keys()) > 0: 176 | warn('Bluetooth profiles are on by default', bt_props) 177 | 178 | 179 | def check_usb(sys_props): 180 | test_name('USB modes check') 181 | 182 | usb_config = sys_props[USB_CONFIG_PROP].split(',') 183 | usb_state = sys_props[USB_STATE_PROP].split(',') 184 | 185 | if len(usb_config) > 1 and usb_config[0] != 'adb': 186 | warn('Multiple USB modes configured.', '%s=%s' % (USB_CONFIG_PROP, usb_config)) 187 | 188 | if len(usb_state) > 1 and usb_state[0] != 'adb': 189 | warn('Multiple USB modes enabled.', '%s=%s' % (USB_STATE_PROP, usb_config)) 190 | 191 | 192 | def check_3g(sys_props): 193 | test_name('3G/LTE check') 194 | 195 | gsm_props = {} 196 | if GSM_NW_PROP in sys_props.keys(): 197 | gsm_nw = sys_props[GSM_NW_PROP] 198 | if gsm_nw != '': 199 | for k in sys_props.keys(): 200 | if 'gsm.' in k: 201 | gsm_props[k] = sys_props[k] 202 | 203 | if gsm_props: 204 | warn('3G/LET may be enabled', gsm_props) 205 | 206 | 207 | def is_private_ip(ipaddr): 208 | f = struct.unpack('!I', socket.inet_pton(socket.AF_INET, ipaddr))[0] 209 | for net in PRIVATE_NETS: 210 | mask = struct.unpack('!I', socket.inet_aton(net[1]))[0] 211 | p = struct.unpack('!I', socket.inet_aton(net[0]))[0] 212 | if (f & mask) == p: 213 | return True 214 | 215 | return False 216 | 217 | 218 | def check_net_ifs(): 219 | test_name('Network interface check') 220 | 221 | cmd = 'adb shell ifconfig' 222 | lines = exec_command(cmd) 223 | 224 | net_ifs = {} 225 | 226 | current_net_if = None 227 | for line in lines: 228 | m = NET_IF_RE.match(line) 229 | if m is not None: 230 | current_net_if = m.group(1) 231 | m = IP_ADDR_RE.match(line) 232 | if m is not None: 233 | ip_addr = m.group(1) 234 | if current_net_if is not None: 235 | net_ifs[current_net_if] = ip_addr 236 | 237 | for net_if in net_ifs.keys(): 238 | ip = net_ifs[net_if] 239 | if not is_private_ip(ip): 240 | warn('Found non-private IP address.', '%s: %s' % (net_if, ip)) 241 | info('Found network interface:', '%s: %s' % (net_if, ip)) 242 | 243 | 244 | def check_port_listen(): 245 | test_name('Listening TCP services check') 246 | 247 | services = [] 248 | cmd = 'adb shell "netstat -na|grep -i tcp|grep -i listen"' 249 | lines = exec_command(cmd, False) 250 | for line in lines: 251 | m = NETSTAT_RE.match(line) 252 | if m is not None: 253 | service = m.group(1) 254 | if '127.0.0.1' not in service: 255 | services.append(service) 256 | 257 | if services: 258 | warn('Non local TCP servers found', services) 259 | 260 | 261 | def check_adb_auth(): 262 | test_name('ADB authentication check') 263 | 264 | if ADB_VENDOR_KEYS_ENV not in os.environ.keys(): 265 | info('no ADB vendor key set') 266 | return 267 | 268 | vendor_keys = os.environ[ADB_VENDOR_KEYS_ENV] 269 | if vendor_keys: 270 | info('%s is set to %s, unsetting' % (ADB_VENDOR_KEYS_ENV, vendor_keys)) 271 | os.environ[ADB_VENDOR_KEYS_ENV] = '' 272 | 273 | try: 274 | cmd = 'adb kill-server' 275 | exec_command(cmd, True) 276 | cmd = 'adb devices' 277 | lines = exec_command(cmd) 278 | for line in lines: 279 | m = ADB_DEVICES_RE.match(line) 280 | if m is not None: 281 | device = m.group(1) 282 | state = m.group(2) 283 | if state != ADB_UNAUTHORIZED: 284 | warn('Device %s does not require ADB private key authentication' % device) 285 | finally: 286 | # try to restore env 287 | info('Restoring %s to %s. Reset manually if not successful.' % (ADB_VENDOR_KEYS_ENV, vendor_keys)) 288 | os.environ[ADB_VENDOR_KEYS_ENV] = vendor_keys 289 | cmd = 'adb kill-server' 290 | exec_command(cmd, True) 291 | 292 | 293 | def check_suid(): 294 | test_name('SUID binaries check') 295 | 296 | cmd = 'adb shell "find /system -xdev \( -perm -4000 -o -perm -2000 \)"' 297 | lines = exec_command(cmd, True) 298 | suid_files = [] 299 | for line in lines: 300 | if 'Permission denied' not in line: 301 | suid_files.append(line.strip()) 302 | 303 | if len(suid_files) > 0: 304 | warn("SUID binaries found", suid_files) 305 | 306 | 307 | def check_services(): 308 | test_name('AIDL services check') 309 | 310 | cmd = 'adb shell service list' 311 | lines = exec_command(cmd) 312 | 313 | services = {} 314 | for line in lines: 315 | m = SERVICE_RE.match(line) 316 | if m is not None: 317 | services[m.group(1).strip()] = m.group(2).strip() 318 | 319 | custom_services = {} 320 | for s in services.keys(): 321 | iface = services[s] 322 | if iface and 'android' not in iface: 323 | custom_services[s] = iface 324 | 325 | if custom_services: 326 | warn('Found custom Android services') 327 | for cs in custom_services.keys(): 328 | if cs not in ANDROID_CORE_SERVICES: 329 | warn('\t%s: [%s]' % (cs, custom_services[cs])) 330 | 331 | 332 | def check_fde(sys_props): 333 | test_name('Disk encryption (FDE) check') 334 | 335 | crypto_props = {} 336 | if CRYPTO_STATE_PROP in sys_props.keys(): 337 | state = sys_props[CRYPTO_STATE_PROP] 338 | for k in sys_props.keys(): 339 | if CRYPTO_PROPS in k: 340 | crypto_props[k] = sys_props[k] 341 | if state == 'encrypted': 342 | info('userdata is encrypted', crypto_props) 343 | else: 344 | warn('userdata is NOT encrypted', crypto_props) 345 | else: 346 | warn('userdata is NOT encrypted') 347 | 348 | 349 | def check_verity(sys_props): 350 | test_name('dm-verity check') 351 | 352 | if VERITY_MODE_PROP not in sys_props: 353 | warn('dm-verity is NOT enabled') 354 | else: 355 | verity_mode = sys_props[VERITY_MODE_PROP] 356 | if verity_mode == VERITY_ENFORCING: 357 | info('dm-verity is enabled and enforcing') 358 | else: 359 | warn('dm-verity is enabled but NOT enforcing', '%s=%s' % (VERITY_MODE_PROP, verity_mode)) 360 | 361 | 362 | def collect_sys_props(): 363 | cmd = 'adb shell getprop' 364 | p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 365 | res = p.communicate() 366 | if p.returncode != 0: 367 | print 'Error: rc=%d, msg=%s' % (p.returncode, res[1]) 368 | 369 | props = {} 370 | lines = res[0].splitlines() 371 | for line in lines: 372 | m = SYS_PROP_RE.match(line) 373 | if m is not None: 374 | props[m.group(1).strip()] = m.group(2).strip() 375 | 376 | return props 377 | 378 | 379 | def main(): 380 | sys_props = collect_sys_props() 381 | 382 | test_name('Starting device OS configuration check') 383 | check_product(sys_props) 384 | print_hr() 385 | check_build(sys_props) 386 | print_hr() 387 | check_signing_keys(sys_props) 388 | print_hr() 389 | check_factory_mode(sys_props) 390 | print_hr() 391 | check_debug(sys_props) 392 | print_hr() 393 | check_bt(sys_props) 394 | print_hr() 395 | check_usb(sys_props) 396 | print_hr() 397 | check_3g(sys_props) 398 | print_hr() 399 | check_fde(sys_props) 400 | print_hr() 401 | check_verity(sys_props) 402 | print_hr() 403 | 404 | check_selinux() 405 | print_hr() 406 | check_suid() 407 | print_hr() 408 | check_port_listen() 409 | print_hr() 410 | check_net_ifs() 411 | print_hr() 412 | check_services() 413 | print_hr() 414 | check_adb_auth() 415 | print_hr() 416 | print 417 | 418 | 419 | if __name__ == '__main__': 420 | main() 421 | -------------------------------------------------------------------------------- /check-system-apps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import glob 5 | import sys 6 | import os 7 | 8 | # needs Androguard 3.2.1 9 | import androguard.core.bytecodes.apk 10 | import androguard.misc 11 | 12 | NS_ANDROID_URI = 'http://schemas.android.com/apk/res/android' 13 | NS_ANDROID = '{http://schemas.android.com/apk/res/android}' 14 | 15 | ANDROID_SYSTEM_SERVICES = ('android', 'com.android', 'com.android.keychain', 'com.android.systemui', 16 | 'com.android.settings', 'com.android.providers.settings', 17 | 'com.android.inputdevices', 'com.android.location.fused') 18 | 19 | # media, networkstack, platform, shared, testkey 20 | TEST_CERT_SERIALS = ( 21 | 0xF2B98E6123572C4E, 22 | 0xFC6CB0D8A6FDD168, 23 | 0xB3998086D056CFFA, 24 | 0xF2A73396BD38767A, 25 | 0x936EACBE07F201DF 26 | ) 27 | 28 | CTS_SHIM_PKGS = ( 29 | 'com.android.cts.ctsshim', 30 | 'com.android.cts.priv.ctsshim' 31 | ) 32 | 33 | 34 | # from PermissionInfo.java in AOSP 35 | class Permission: 36 | PROTECTION_NORMAL = 0x0 37 | PROTECTION_DANGEROUS = 0x1 38 | PROTECTION_SIGNATURE = 0x2 39 | PROTECTION_SIGNATURE_OR_SYSTEM = 0x3 40 | 41 | PROTECTION_FLAG_PRIVILEGED = 0x10 42 | PROTECTION_FLAG_DEVELOPMENT = 0x20 43 | PROTECTION_FLAG_APPOP = 0x40 44 | PROTECTION_FLAG_PRE23 = 0x80 45 | PROTECTION_FLAG_INSTALLER = 0x100 46 | PROTECTION_FLAG_VERIFIER = 0x200 47 | PROTECTION_FLAG_PREINSTALLED = 0x400 48 | 49 | PROTECTION_MASK_BASE = 0xf 50 | PROTECTION_MASK_FLAGS = 0xff0 51 | 52 | PROT_LEVEL_DESC = { 53 | PROTECTION_NORMAL: 'normal', 54 | PROTECTION_DANGEROUS: 'dangerous', 55 | PROTECTION_SIGNATURE: 'signature', 56 | PROTECTION_SIGNATURE_OR_SYSTEM: 'signatureOrSystem' 57 | } 58 | 59 | def __init__(self, name, details): 60 | self.name = name 61 | self.prot_level = 0 62 | prot_level_str = details['protectionLevel'] 63 | if prot_level_str is not None and prot_level_str != 'None': 64 | self.prot_level = int(prot_level_str, 0) 65 | self.group = details['permissionGroup'] 66 | self.details = details 67 | 68 | @staticmethod 69 | def prot_level_to_str(prot_level): 70 | prot_level_strs = [] 71 | 72 | prot_level_str = Permission.PROT_LEVEL_DESC.get(prot_level & Permission.PROTECTION_MASK_BASE, 73 | 'invalid') 74 | prot_level_strs.append(prot_level_str) 75 | 76 | # flags 77 | if (prot_level & Permission.PROTECTION_FLAG_PRIVILEGED) != 0: 78 | prot_level_strs.append('|privileged') 79 | if (prot_level & Permission.PROTECTION_FLAG_DEVELOPMENT) != 0: 80 | prot_level_strs.append('|development') 81 | if (prot_level & Permission.PROTECTION_FLAG_APPOP) != 0: 82 | prot_level_strs.append('|appop') 83 | if (prot_level & Permission.PROTECTION_FLAG_PRE23) != 0: 84 | prot_level_strs.append('|pre23') 85 | if (prot_level & Permission.PROTECTION_FLAG_INSTALLER) != 0: 86 | prot_level_strs.append('|installer') 87 | if (prot_level & Permission.PROTECTION_FLAG_VERIFIER) != 0: 88 | prot_level_strs.append('|verifier') 89 | if (prot_level & Permission.PROTECTION_FLAG_PREINSTALLED) != 0: 90 | prot_level_strs.append('|preinstalled') 91 | 92 | return ''.join(prot_level_strs) 93 | 94 | def requires_signature(self): 95 | return (self.prot_level & Permission.PROTECTION_SIGNATURE) == \ 96 | Permission.PROTECTION_SIGNATURE 97 | 98 | def is_priviliged(self): 99 | return (self.prot_level & Permission.PROTECTION_FLAG_PRIVILEGED) == \ 100 | Permission.PROTECTION_FLAG_PRIVILEGED 101 | 102 | def print_perm(self, indent=0): 103 | print '%s %s' % ('' * indent, self.__str__()) 104 | 105 | def __str__(self): 106 | return '%s: [group: %s] [protectionLevel: %s (0x%04X)]' % \ 107 | (self.name, self.group, 108 | self.prot_level_to_str(self.prot_level), 109 | self.prot_level) 110 | 111 | 112 | class Package: 113 | 114 | def __init__(self, package_name, version_name, version_code, filename): 115 | self.name = package_name 116 | self.version_code = version_code 117 | self.version_name = version_name 118 | self.filename = filename 119 | self.dang_custom_perms = [] 120 | self.protected_broadcasts = [] 121 | self.is_system = False 122 | self.is_debuggable = False 123 | self.is_testkey_signed = False 124 | self.signer_dns = [] 125 | self.shared_user_id = None 126 | self._apk = None 127 | self.flag_reasons = [] 128 | 129 | def set_apk(self, apk): 130 | # Androguard APK 131 | self._apk = apk 132 | 133 | def check_permissions(self, top_level_pkg): 134 | dp = self._apk.get_declared_permissions_details() 135 | if dp: 136 | for name, details in dp.items(): 137 | perm = Permission(name, details) 138 | if perm.name.startswith(top_level_pkg) and not perm.requires_signature(): 139 | self.dang_custom_perms.append(perm) 140 | 141 | def check_prot_broadcasts(self): 142 | tag = self._apk.get_android_manifest_xml().findall('.//' + 'protected-broadcast') 143 | if len(tag) > 0: 144 | for item in tag: 145 | name = item.get(NS_ANDROID + 'name') 146 | self.protected_broadcasts.append(name) 147 | 148 | def get_signing_certs(self): 149 | return self._apk.get_certificates() 150 | 151 | def check_certs(self): 152 | for c in self.get_signing_certs(): 153 | self.signer_dns.append(c.subject.human_friendly) 154 | if c.serial_number in TEST_CERT_SERIALS and \ 155 | self.name not in CTS_SHIM_PKGS: 156 | self.is_testkey_signed = True 157 | 158 | def should_flag(self, top_level_pkg): 159 | if self.is_debuggable: 160 | self.flag_reasons.append('Debuggable package') 161 | 162 | self.check_permissions(top_level_pkg) 163 | has_dang_perms = len(self.dang_custom_perms) > 0 164 | if has_dang_perms: 165 | self.flag_reasons.append('Declares potentially dangerous permissions') 166 | 167 | is_3rd_party_sys_privs = self.is_system and \ 168 | self.name not in ANDROID_SYSTEM_SERVICES and \ 169 | not self.name.startswith('com.android') 170 | is_own_pkg = self.name.startswith(top_level_pkg) 171 | if is_3rd_party_sys_privs and not is_own_pkg: 172 | self.flag_reasons.append('3rd-party package running with system privileges') 173 | 174 | self.check_certs() 175 | if self.is_testkey_signed: 176 | self.flag_reasons.append('Signed with testkey: [%s]' % self.signer_dns) 177 | 178 | self.check_prot_broadcasts() 179 | if is_own_pkg and not self.is_system and len(self.protected_broadcasts) > 0: 180 | self.flag_reasons.append('Protected broadcasts in non-system package') 181 | 182 | return len(self.flag_reasons) > 0 183 | 184 | def print_potentially_dangerous_permissions(self): 185 | for perm in self.dang_custom_perms: 186 | perm.print_perm() 187 | print 188 | 189 | def print_apk_summary(self): 190 | print 'APK file: %s' % self.filename 191 | print 'Flag reasons: %s' % ', '.join(self.flag_reasons) 192 | print '=' * 40 193 | print 'package: %s' % self.name 194 | print 'versionCode=%s, versionName=%s' % (self.version_code, self.version_name) 195 | print 'sharedUserId=[%s]' % self.shared_user_id 196 | print 'debuggable: %s' % self.is_debuggable 197 | print 'signer cert DNs: %s' % self.signer_dns 198 | print 'signer cert serial: 0x%X' % self.get_signing_certs()[0].serial_number 199 | print 'signer cert SHA-256: %s' % self.get_signing_certs()[0].sha256.encode('hex') 200 | print 201 | if len(self.dang_custom_perms) > 0: 202 | print 'Potentially dangerous permissions: ' 203 | self.print_potentially_dangerous_permissions() 204 | print '-' * 40 205 | 206 | def print_apk_details(self): 207 | print 208 | print 'Permissions and components:' 209 | print '-' * 40 210 | dp = self._apk.get_declared_permissions_details() 211 | if dp: 212 | print 'DECLARED PERMISSIONS:' 213 | for name, details in dp.items(): 214 | perm = Permission(name, details) 215 | perm.print_perm() 216 | print 217 | 218 | rp = self._apk.get_permissions() 219 | if rp: 220 | print 'REQUESTED PERMISSIONS:' 221 | for p in rp: 222 | print ' [%s]' % p 223 | print 224 | 225 | main_activity = self._apk.get_main_activity() 226 | 227 | activities = self._apk.get_activities() 228 | if activities: 229 | print 'ACTIVITIES: ' 230 | for a in activities: 231 | filters = self._apk.get_intent_filters('activity', a) 232 | is_main = a == main_activity 233 | act = a 234 | if is_main: 235 | act = '** %s **' % a 236 | print ' %s' % act 237 | if filters: 238 | print ' filters: %s' % filters 239 | print 240 | 241 | services = self._apk.get_services() 242 | if services: 243 | print 'SERVICES: ' 244 | for s in services: 245 | filters = self._apk.get_intent_filters('service', s) 246 | print ' %s' % s 247 | if filters: 248 | print ' %s' % filters 249 | print 250 | 251 | receivers = self._apk.get_receivers() 252 | if receivers: 253 | print 'RECEIVERS: ' 254 | for r in receivers: 255 | filters = self._apk.get_intent_filters("receiver", r) 256 | print ' %s' % r 257 | if filters: 258 | print ' %s' % filters 259 | print 260 | 261 | if self._apk.get_providers(): 262 | print 'PROVIDERS: ' 263 | for p in self._apk.get_providers(): 264 | print ' %s' % p 265 | print 266 | 267 | if len(self.protected_broadcasts) > 0: 268 | print 'PROTECTED BROADCASTS' 269 | for pb in self.protected_broadcasts: 270 | print ' %s' % pb 271 | 272 | 273 | class Report: 274 | num_system_apps = None # type: int 275 | 276 | def __init__(self, top_level_pkg): 277 | self.top_level_pkg = top_level_pkg 278 | self.num_system_apps = 0 279 | self.num_debuggable_apps = 0 280 | self.issues = {} 281 | 282 | def add_issue(self, package): 283 | self.issues[package.name] = package 284 | 285 | def print_header(self): 286 | print 'Found %d system apps, %d debuggable apps, %d potentially problematic APKs' \ 287 | % (self.num_system_apps, self.num_debuggable_apps, len(self.issues)) 288 | print 289 | 290 | def print_summary(self): 291 | self.print_header() 292 | for name, pkg in self.issues.items(): 293 | pkg.print_apk_summary() 294 | 295 | def print_details(self): 296 | self.print_header() 297 | for name, pkg in self.issues.items(): 298 | pkg.print_apk_summary() 299 | pkg.print_apk_details() 300 | print 301 | 302 | 303 | class ApkChecker: 304 | 305 | def __init__(self): 306 | pass 307 | 308 | def check_apks(self, apks, top_level_pkg): 309 | print 'APKs to scan: %d' % len(apks) 310 | print 311 | 312 | report = Report(top_level_pkg) 313 | 314 | for a in apks: 315 | apk_path = os.path.abspath(a.strip()) 316 | # print 'path: %s' % apk_path 317 | apk = androguard.core.bytecodes.apk.APK(apk_path) 318 | axml = apk.get_android_manifest_xml() 319 | 320 | package_name = apk.package 321 | shared_user_id = axml.get(NS_ANDROID + 'sharedUserId') 322 | version_code = axml.get(NS_ANDROID + 'versionCode') 323 | version_name = axml.get(NS_ANDROID + 'versionName') 324 | debuggable = self.get_element(axml, 'application', 'debuggable') 325 | 326 | # a, d, dx = androguard.misc.AnalyzeAPK(apk_path) 327 | # print_apk_summary(apk) 328 | 329 | # if (shared_user_id is not None and 'system' in shared_user_id) or (debuggable is not None): 330 | apk_filename = os.path.basename(apk_path) 331 | pkg = Package(package_name, version_code, version_name, apk_filename) 332 | pkg.set_apk(apk) 333 | 334 | if shared_user_id is not None: 335 | pkg.shared_user_id = shared_user_id 336 | if 'android.uid.system' == shared_user_id: 337 | report.num_system_apps += 1 338 | pkg.is_system = True 339 | 340 | if debuggable is not None and debuggable == 'true': 341 | pkg.debuggable = True 342 | report.num_debuggable_apps += 1 343 | 344 | if pkg.should_flag(top_level_pkg): 345 | report.add_issue(pkg) 346 | 347 | return report 348 | 349 | @staticmethod 350 | def get_element(xml, tag_name, attribute, **attribute_filter): 351 | tag = xml.findall('.//' + tag_name) 352 | if len(tag) == 0: 353 | return None 354 | for item in tag: 355 | skip_this_item = False 356 | for attr, val in list(attribute_filter.items()): 357 | attr_val = item.get(NS_ANDROID + attr) 358 | if attr_val != val: 359 | skip_this_item = True 360 | break 361 | 362 | if skip_this_item: 363 | continue 364 | 365 | value = item.get(NS_ANDROID + attribute) 366 | 367 | if value is not None: 368 | return value 369 | return None 370 | 371 | 372 | def main(): 373 | help_msg = 'Checks security configuration of Android device system APKs and outputs report.' 374 | parser = argparse.ArgumentParser(description=help_msg) 375 | parser.add_argument('apk_dir', metavar='apk-dir', help='APK directory') 376 | parser.add_argument('top_level_pkg', metavar='top-level-pkg', help='Top-level package') 377 | parser.add_argument('--show-apk-details', action='store_true', 378 | required=False, help='Show components in each APK') 379 | 380 | args = parser.parse_args() 381 | 382 | apks = glob.glob('%s/*.apk' % args.apk_dir) 383 | 384 | checker = ApkChecker() 385 | report = checker.check_apks(apks, args.top_level_pkg) 386 | 387 | if args.show_apk_details: 388 | report.print_details() 389 | else: 390 | report.print_summary() 391 | 392 | 393 | if __name__ == '__main__': 394 | main() 395 | -------------------------------------------------------------------------------- /download-apks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import os 5 | import re 6 | import sys 7 | import subprocess 8 | 9 | APK_PATH_RE = re.compile(r'^\S+:(\S+)=.*') 10 | 11 | def collect_apk_paths(all_apks): 12 | apks = [] 13 | cmd = 'adb shell pm list packages -f' 14 | result = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) 15 | # filter system, system-priv? 16 | for line in result.stdout: 17 | m = APK_PATH_RE.match(line) 18 | if m is not None: 19 | path = m.group(1) 20 | if all_apks: 21 | apks.append(path) 22 | else: 23 | if '/system/app/' in path or '/system/priv-app/' in path: 24 | apks.append(path) 25 | 26 | return apks 27 | 28 | 29 | def download_apks(apk_paths, download_path): 30 | if not os.path.exists(download_path): 31 | os.makedirs(download_path) 32 | 33 | for apk in apk_paths: 34 | print 'Getting %s...' % apk 35 | cmd = 'adb pull %s %s/%s' % (apk, download_path, os.path.basename(apk)) 36 | p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 37 | result = p.communicate() 38 | if p.returncode != 0: 39 | print 'Error: rc=%d, msg=%s' % (p.returncode, result[1]) 40 | 41 | 42 | if __name__ == '__main__': 43 | help_msg = 'Downloads APKs from device over ADB. Defaults to system APKs only.' 44 | parser = argparse.ArgumentParser(description=help_msg) 45 | parser.add_argument('download_dir', metavar='apk-dir', help='APK directory') 46 | parser.add_argument('--all-apks', action='store_true', 47 | required=False, help='Download all APKs (system only unless specified)') 48 | 49 | args = parser.parse_args() 50 | 51 | apk_paths = collect_apk_paths(args.all_apks) 52 | download_apks(apk_paths, args.download_dir) 53 | 54 | --------------------------------------------------------------------------------