├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── NOTICE ├── acvtool ├── __init__.py ├── acvtool.py ├── cutter │ ├── __init__.py │ ├── basic_block.py │ ├── classes.py │ ├── cutter.py │ ├── invokes.py │ ├── label_block.py │ ├── methods.py │ ├── returns.py │ └── shrinker.py └── smiler │ ├── __init__.py │ ├── acv.py │ ├── cliparser.py │ ├── config.json │ ├── config.py │ ├── entities │ ├── __init__.py │ ├── coverage.py │ └── wd.py │ ├── granularity.py │ ├── instrumenting │ ├── __init__.py │ ├── acvpatcher.py │ ├── android_manifest.py │ ├── apkil │ │ ├── __init__.py │ │ ├── arraydatanode.py │ │ ├── classnode.py │ │ ├── codeblocknode.py │ │ ├── constants.py │ │ ├── fieldnode.py │ │ ├── insn35c.py │ │ ├── insn3rc.py │ │ ├── insnnode.py │ │ ├── labelnode.py │ │ ├── methodnode.py │ │ ├── smalitree.py │ │ ├── switchnode.py │ │ ├── trynode.py │ │ └── typenode.py │ ├── apktool.py │ ├── axml_manifest.py │ ├── baksmali.py │ ├── config.py │ ├── core │ │ ├── __init__.py │ │ ├── acv_classes.py │ │ ├── class_instrumenter.py │ │ └── method_instrumenter.py │ ├── manifest_instrumenter.py │ ├── smali_instrumenter.py │ ├── utils.py │ └── zipper.py │ ├── keystore │ ├── libs │ ├── __init__.py │ ├── jars │ │ ├── __init__.py │ │ ├── apktool_2.9.3.jar │ │ ├── baksmali-3.0.7-1c13925b-dirty-fat.jar │ │ └── smali-3.0.7-1c13925b-dirty-fat.jar │ └── libs.py │ ├── operations │ ├── __init__.py │ ├── adb.py │ ├── binaries.py │ ├── coverage.py │ └── terminal.py │ ├── reporting │ ├── __init__.py │ └── reporter.py │ ├── resources │ ├── __init__.py │ ├── html │ │ ├── .resources │ │ │ ├── NOTICE.txt │ │ │ ├── android.png │ │ │ ├── box.png │ │ │ ├── document.png │ │ │ ├── file.png │ │ │ ├── greenbar.png │ │ │ ├── highlight │ │ │ │ ├── LICENSE │ │ │ │ ├── highlight.pack.js │ │ │ │ └── styles │ │ │ │ │ └── default.css │ │ │ ├── index.js │ │ │ ├── redbar.png │ │ │ ├── report.css │ │ │ └── report.js │ │ ├── __init__.py │ │ └── templates │ │ │ ├── class.pt │ │ │ ├── index.pt │ │ │ ├── init_row.pt │ │ │ └── init_table.pt │ ├── instrumentation │ │ ├── __init__.py │ │ └── smali │ │ │ └── tool │ │ │ └── acv │ │ │ ├── AcvCalculator.smali │ │ │ ├── AcvFlushing.smali │ │ │ ├── AcvInstrumentation$1.smali │ │ │ ├── AcvInstrumentation.smali │ │ │ ├── AcvReceiver.smali │ │ │ ├── AcvReporterFields.smali │ │ │ └── AcvStoring.smali │ └── logging.yaml │ ├── serialisation │ ├── __init__.py │ ├── html_serialiser.py │ └── xml_serialiser.py │ └── smiler.py ├── readme.md ├── requirements.txt └── setup.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: pilgun 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .DS_Store 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | acvtool.egg-info/ 7 | build/ 8 | log.log 9 | times_log.csv 10 | wd*/ 11 | dist/ -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include NOTICE -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | ======================================================================= 2 | NOTICE 3 | ======================================================================= 4 | 5 | This file lists notices, copyright and licenses of ACVTool. 6 | 7 | ======================================================================= 8 | SUMMARY 9 | ======================================================================= 10 | 11 | ACVTool 2.0 MULTIDEX 12 | 13 | ACVTool Version 2.0 MULTIDEX represents a significant upgrade from the previously published ACVTool 0.2 and ACVCut 1.0. Originally developed by Aleksandr Pilgun at the University of Luxembourg, these tools are the result of his original research and can be accessed via the Zenodo repository and GitHub releases. The release of ACVTool 2.0 introduces new functionality and improvements that impact all files under the github.com/pilgun/acvtool URL. ACVTool 2.0 adheres to the Apache 2.0 license. 14 | 15 | The following changes apply to ACVTool 2.0, including but not limited to: 16 | - Shrinking functionality and other improvements initially released under the ACVCut name (https://github.com/pilgun/acvcut) 17 | - Updated Apkil Tree Structure 18 | - Multidex support in terms of instrumenting, report generation and shrinking 19 | - Multi-apk installation support 20 | - Instrumentation implementation improvements 21 | - Shrinking implementation improvements 22 | - Report generation improvements 23 | - Additional commands (snap, flush, calculate, shrink) 24 | - Major code refactoring 25 | 26 | The multidex smali instrumentation technique is protected by a patent. Users and contributors to ACVTool automatically receive a license for this patent within the scope of ACVTool, as governed by the Apache 2.0 license. However, other implementations, re-implementations, or translations into different programming languages may require separate patent licensing. 27 | 28 | Contributions to ACVTool are subject to the Contributer License Agreement (to be defined). 29 | 30 | ======================================================================= 31 | LICENSES 32 | ======================================================================= 33 | 34 | ACVTOOL MULTIDEX 35 | 36 | Copyright (c) 2024 Aleksandr Pilgun. All rights preserved. 37 | 38 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use ACVTool files from this repository except in compliance with the License. 39 | You may obtain a copy of the License at 40 | 41 | http://www.apache.org/licenses/LICENSE-2.0 42 | 43 | Unless required by applicable law or agreed to in writing, software 44 | distributed under the License is distributed on an "AS IS" BASIS, 45 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 46 | See the License for the specific language governing permissions and 47 | limitations under the License. 48 | 49 | ACVTOOL 0.2 50 | 51 | Copyright © 2018-2020 SnT, University of Luxembourg, Aleksandr Pilgun. All rights preserved. 52 | 53 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use the files under this repository except in compliance with the License. You may obtain a copy of the License at 54 | 55 | http://www.apache.org/licenses/LICENSE-2.0 56 | 57 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 58 | 59 | ACVCUT 1.0 60 | 61 | Copyright © 2020 University of Luxembourg, Aleksandr Pilgun. All rights preserved. 62 | 63 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use the files under this repository except in compliance with the License. You may obtain a copy of the License at 64 | 65 | http://www.apache.org/licenses/LICENSE-2.0 66 | 67 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 68 | 69 | APKIL 70 | 71 | https://github.com/kelwin/apkil 72 | 73 | APKIL is an APK Instrumentation Library. 74 | Supported by Google Summer of Code 2012 and The Honeynet Project. 75 | 76 | Author: Kun Yang 77 | Project Wiki: http://code.google.com/p/droidbox/wiki/APIMonitor 78 | 79 | BBOXTESTER 80 | 81 | ACVTool may include a few fully reworked files from the BBox Tester repository (e.g. android_manifest.py). Changes may be observed and understood by comparing them with the original repository. 82 | 83 | https://github.com/zyrikby/BBoxTester 84 | 85 | BBoxTester is distributed under Apache-2.0 license. The citation of the paper is highly appreciated. 86 | 87 | APKTOOL 88 | 89 | ACVTool includes apktool binary ditributed at https://ibotpeaches.github.io/Apktool/ 90 | Apktool source code is available at https://github.com/iBotPeaches/Apktool 91 | 92 | Copyright 2010 Ryszard Wiśniewski 93 | 94 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 95 | 96 | https://www.apache.org/licenses/LICENSE-2.0 97 | 98 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 99 | 100 | HTML REPORT ICONS 101 | 102 | Icons were downloaded from https://icons8.com under CC BY 4.0 License. 103 | 104 | HIGHLIGHT.JS 105 | 106 | Copyright (c) 2006, Ivan Sagalaev 107 | All rights reserved. 108 | Redistribution and use in source and binary forms, with or without 109 | modification, are permitted provided that the following conditions are met: 110 | 111 | * Redistributions of source code must retain the above copyright 112 | notice, this list of conditions and the following disclaimer. 113 | * Redistributions in binary form must reproduce the above copyright 114 | notice, this list of conditions and the following disclaimer in the 115 | documentation and/or other materials provided with the distribution. 116 | * Neither the name of highlight.js nor the names of its contributors 117 | may be used to endorse or promote products derived from this software 118 | without specific prior written permission. 119 | 120 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY 121 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 122 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 123 | DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY 124 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 125 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 126 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 127 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 128 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 129 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 130 | -------------------------------------------------------------------------------- /acvtool/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/__init__.py -------------------------------------------------------------------------------- /acvtool/acvtool.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import argparse 3 | from logging import config as logging_config 4 | from .smiler import acv, cliparser 5 | from .smiler.cliparser import AcvCommandParsers 6 | from .smiler.config import config 7 | 8 | 9 | def setup_logging(): 10 | with open(config.logging_yaml) as f: 11 | logging_config.dictConfig(yaml.safe_load(f.read())) 12 | 13 | def run_actions(parser, args=None): 14 | """ 15 | This function parses the arguments using the provided parser and run 16 | corresponding acvtool actions. 17 | 18 | Args: 19 | parser(argparse.ArgumentParser): ArgumentParser for this code 20 | """ 21 | 22 | if args is None: 23 | args = parser.parse_args() 24 | if args.subcmd in ["instrument", "install", "uninstall", "activate", "start", "stop", "snap", "flush", "calculate", "pull", "cover-pickles", "report", "shrink"] and args.device: 25 | config.adb_path = "{} -s {}".format(config.adb_path, args.device) 26 | if args.subcmd == "instrument": 27 | acv.instrument(args) 28 | elif args.subcmd == "install": 29 | acv.install(args) 30 | elif args.subcmd == "uninstall": 31 | acv.uninstall(args) 32 | elif args.subcmd == "activate": 33 | acv.activate(args) 34 | elif args.subcmd == "start": 35 | acv.start(args) 36 | elif args.subcmd == "stop": 37 | acv.stop(args) 38 | elif args.subcmd == "snap": 39 | acv.snap(args) 40 | elif args.subcmd == "flush": 41 | acv.flush(args) 42 | elif args.subcmd == "calculate": 43 | acv.calculate(args) 44 | elif args.subcmd == "pull": 45 | acv.pull(args) 46 | elif args.subcmd == "cover-pickles": 47 | acv.cover_pickles(args) 48 | elif args.subcmd == "report": 49 | acv.report(args) 50 | elif args.subcmd == "sign": 51 | acv.sign(args) 52 | elif args.subcmd == "build": 53 | acv.build(args) 54 | elif args.subcmd == "shrink": 55 | acv.shrink(args) 56 | else: 57 | parser.print_usage() 58 | return 59 | 60 | def get_parser(): 61 | parser = argparse.ArgumentParser(prog='acvtool.py', 62 | description='This tool is designed to measure code coverage of \ 63 | Android applications when its sources are not available.') 64 | parser.add_argument('--version', action='version', version=str(config.version)) 65 | subparsers = parser.add_subparsers(dest='subcmd', metavar="", 66 | help="acvtool commands") 67 | cli_cmd = AcvCommandParsers(subparsers) 68 | cli_cmd.instrument() 69 | cli_cmd.install() 70 | cli_cmd.uninstall() 71 | cli_cmd.activate() 72 | cli_cmd.start() 73 | cli_cmd.stop() 74 | cli_cmd.snap() 75 | cli_cmd.flush() 76 | cli_cmd.calculate() 77 | cli_cmd.pull() 78 | cli_cmd.cover_pickles() 79 | cli_cmd.report() 80 | cli_cmd.sign() 81 | cli_cmd.build() 82 | cli_cmd.shrink() 83 | return parser 84 | 85 | def main(): 86 | setup_logging() 87 | parser = get_parser() 88 | args = parser.parse_args() 89 | run_actions(parser, args) 90 | 91 | if __name__ == "__main__": 92 | main() 93 | -------------------------------------------------------------------------------- /acvtool/cutter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/cutter/__init__.py -------------------------------------------------------------------------------- /acvtool/cutter/classes.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def remove_not_covered(smalitree): 5 | references = get_references(smalitree) 6 | i = 0 7 | for cl in smalitree.classes: 8 | if "abstract" not in cl.access and "interface" not in cl.access and not methods_called(cl) and cl.name not in references: 9 | smalitree.classes.remove(cl) 10 | i += 1 11 | print("{} classes removed".format(i)) 12 | 13 | 14 | def methods_called(klass): 15 | return sum(m.called for m in klass.methods) 16 | 17 | check_cast_rx = r"^check-cast\s(v|p)\d+, (?P.*)" 18 | 19 | def get_references(smalitree): 20 | references = set() 21 | i = 0 22 | for cl in smalitree.classes: 23 | for m in cl.methods: 24 | for insn in m.insns: 25 | res = re.search(check_cast_rx, insn.buf) 26 | if res: 27 | references.add(res.group("invoke")) 28 | i += 1 29 | print("{} check-cast insns found".format(i)) 30 | print("{} references found".format(len(references))) 31 | return references 32 | 33 | ref_only = r"^(?:const-class|check-cast|instance-of|new-instance|new-array|filled-new-array|(?:iget|iput|sget|sput)(:?-[a-z]+)?|invoke-[a-z]+(?:/range)?)\s[\s{},vp0-9.]+,\s(?P.*)$" 34 | ref_ins_regex = r"^(?:const-class|check-cast|instance-of|new-instance|new-array|filled-new-array|(?:iget|iput|sget|sput)(:?-[a-z]+)?|invoke-[a-z]+(?:/range)?)\s[\s{},vp0-9.]+,\s(?P\[?(?P[^-]+)(:?->(?:(?P[\w<>]+\((?P[^-]*)\)[^-]+)|(?P\w+:.*)))?)$" #| 35 | #(?P.*;)(?:->(?P[a-zA-Z_$0-9]+\(.*\).*)|(?P[a-zA-Z_$0-9]+:.*))?$ 36 | 37 | def get_mentioned(smalitree, ins_name): 38 | classes = set() 39 | for cl in smalitree.classes: 40 | for m in cl.methods: 41 | for ins in m.insns: 42 | if ins.buf.startswith(ins_name): 43 | print(ins.buf) 44 | 45 | # todo: add @proto 46 | ref_ins_rx = r"^iget|iput|sget|sput|invoke" 47 | 48 | def get_all_refs(smalitree): 49 | refs = dict() 50 | for cl in smalitree.classes: 51 | for m in cl.methods: 52 | for ins in m.insns: 53 | if ins.buf.startswith(ins_name): 54 | print(ins.buf) 55 | 56 | 57 | #-instructions having references to fields/classes 58 | # const-string vAA, string@BBBB 59 | # const-string/jumbo vAA, string@BBBBBBBB 60 | # const-class vAA, type@BBBB 61 | # check-cast vAA, type@BBBB 62 | # instance-of vA, vB, type@CCCC 63 | # new-instance vAA, type@BBBB 64 | # new-array vA, vB, type@CCCC 65 | # filled-new-array {vC, vD, vE, vF, vG}, type@BBBB 66 | # filled-new-array/range {vCCCC .. vNNNN}, type@BBBB 67 | # iget* #iinstanceop vA, vB, field@CCCC 68 | # sget* #sstaticop vAA, field@BBBB 69 | # invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB 70 | # invoke-kind/range {vCCCC .. vNNNN}, meth@BBBB 71 | # invoke-polymorphic {vC, vD, vE, vF, vG}, meth@BBBB, proto@HHHH 72 | # invoke-polymorphic/range {vCCCC .. vNNNN}, meth@BBBB, proto@HHHH 73 | # invoke-custom {vC, vD, vE, vF, vG}, call_site@BBBB 74 | # invoke-custom/range {vCCCC .. vNNNN}, call_site@BBBB 75 | # const-method-handle vAA, method_handle@BBBB 76 | # const-method-type vAA, proto@BBBB -------------------------------------------------------------------------------- /acvtool/cutter/cutter.py: -------------------------------------------------------------------------------- 1 | from ..smiler.instrumenting.apkil.insnnode import InsnNode 2 | 3 | 4 | def remove_not_covered_instructions(smalitree): 5 | i = 0 6 | j = 0 7 | insns_amount = lambda classes: sum([sum([len(m.insns) for m in cl.methods]) for cl in classes]) 8 | insns_orig = insns_amount(smalitree.classes) 9 | print("insns: {}".format(insns_orig)) 10 | for cl in smalitree.classes: 11 | for m in cl.methods: 12 | for insn in m.insns: 13 | if insn.covered: 14 | m.insns.remove(insn) 15 | insns_new = insns_amount(smalitree.classes) 16 | print("insns: {}".format(insns_new)) 17 | 18 | print("left classes: {}".format(len(smalitree.classes))) 19 | print("removed {} classes".format(i)) 20 | print("removed {} methods, left {}".format(j, sum([len(cl.methods) for cl in smalitree.classes]))) 21 | 22 | 23 | def remove_methods_in_not_covered_classes(smalitree): 24 | i = 0 25 | j = 0 26 | 27 | for cl in smalitree.classes: 28 | if cl.not_covered(): 29 | print("{} {}".format(cl.name, cl.covered())) 30 | for m in cl.methods: 31 | if m.not_covered(): 32 | cl.methods.remove(m) 33 | j+=1 34 | #smalitree.classes.remove(cl) 35 | i += 1 36 | print("left classes: {}".format(len(smalitree.classes))) 37 | print("removed {} classes".format(i)) 38 | print("removed {} methods, left {}".format(j, sum([len(cl.methods) for cl in smalitree.classes]))) 39 | 40 | 41 | def get_all_method_invokes(smalitree): 42 | invokes = set() 43 | for cl in smalitree.classes: 44 | for m in cl.methods: 45 | for insn in m.insns: 46 | if insn.opcode_name.startswith("invoke"): 47 | invokes.add(insn.obj.method_desc) 48 | return invokes 49 | 50 | def get_all_methods_desc(smalitree): 51 | signatures = set() 52 | for cl in smalitree.classes: 53 | for m in cl.methods: 54 | signatures.add("{}->{}".format(cl.name, m.descriptor)) 55 | return signatures 56 | 57 | 58 | def remove_not_covered_if(smalitree): 59 | for cl in smalitree.classes: 60 | #if cl.name == "Landroid/support/v7/app/AppCompatDelegateImplBase;": 61 | if cl.name != "Landroid/support/constraint/ConstraintLayout;": 62 | continue 63 | # m = cl.methods[4] 64 | # print("method name: {}".format(m.descriptor)) 65 | for m in cl.methods[7:8]: 66 | print(m.descriptor) 67 | # if m.name != "setChildrenConstraints": 68 | # continue 69 | if not m.synchronized: 70 | start_ = 0 71 | block = False 72 | blocks = 0 73 | for i, insn in enumerate(m.insns): 74 | # if i == 18: 75 | # print(i) 76 | if not block and insn.opcode_name.startswith("if") and not insn.covered: # and insn.cover_code > -1 and not has_label_by_index(m.labels, i): 77 | lbl_index = insn.buf.rfind(':') 78 | m.insns[i] = InsnNode("goto {}".format(insn.buf[lbl_index:])) 79 | start_ = i 80 | block = True 81 | continue 82 | if block and has_label_by_index(m.labels, i): #(insn.covered or has_covered_lbl(m.labels, i)): 83 | end_ = i 84 | #print("\n".join([ins.buf for ins in m.insns[start_:end_]])) 85 | del m.insns[start_+1:end_] 86 | recalculate_label_indexes(m.labels, start_+1, end_) 87 | block = False 88 | blocks += 1 89 | if blocks == 6: 90 | break 91 | #break 92 | #break 93 | # break 94 | #for m in cl.methods: 95 | 96 | def recalculate_label_indexes(labels, start_, end_): 97 | d = end_ - start_ 98 | for (k, v) in labels.items(): 99 | if v.index >= end_: 100 | v.index -= d 101 | 102 | 103 | def has_label_by_index(labels, index): 104 | for (k, v) in labels.items(): 105 | if v.index == index: 106 | return True 107 | return False 108 | 109 | def has_covered_lbl(labels, index): 110 | for (k, v) in labels.items(): 111 | if v.index == index: 112 | return v.covered 113 | return False 114 | -------------------------------------------------------------------------------- /acvtool/cutter/invokes.py: -------------------------------------------------------------------------------- 1 | import re 2 | from ..smiler.instrumenting.apkil.constants import BASIC_TYPES 3 | 4 | rx = r"^invoke-(direct|super)" 5 | 6 | def get_invoke_non_virt_methods(smalitree): 7 | invokes = set() 8 | for cl in smalitree.classes: 9 | for m in cl.methods: 10 | for insn in m.insns: 11 | res = re.search(rx, insn.opcode_name) 12 | if res: 13 | segs = insn.buf.split() 14 | invokes.add(segs[-1]) 15 | return invokes 16 | 17 | 18 | def get_invoke_direct_methods(smalitree): 19 | invokes = set() 20 | for cl in smalitree.classes: 21 | for m in cl.methods: 22 | for insn in m.insns: 23 | if insn.buf.startswith("invoke-direct"): 24 | segs = insn.buf.split() 25 | invokes.add(segs[-1]) 26 | return invokes 27 | 28 | def get_invoke_static_methods(smalitree): 29 | invokes = set() 30 | for cl in smalitree.classes: 31 | for m in cl.methods: 32 | for insn in m.insns: 33 | if insn.buf.startswith("invoke-static"): 34 | segs = insn.buf.split() 35 | invokes.add(segs[-1]) 36 | return invokes 37 | 38 | def get_invoke_super_methods(smalitree): 39 | invokes = set() 40 | for cl in smalitree.classes: 41 | for m in cl.methods: 42 | for insn in m.insns: 43 | if insn.buf.startswith("invoke-super"): 44 | segs = insn.buf.split() 45 | invokes.add(segs[-1]) 46 | return invokes 47 | 48 | 49 | 50 | def get_method_descriptions(smalitree): 51 | descriptions = set() 52 | for cl in smalitree.classes: 53 | for m in cl.methods: 54 | full_name = "{}->{}".format(cl.name, m.descriptor) 55 | descriptions.add(full_name) 56 | return descriptions 57 | 58 | 59 | def get_class_method_dict(invokes): 60 | invokes_dict = {} 61 | for cm in invokes: 62 | c, m = cm.split('->') 63 | if c not in invokes_dict: 64 | invokes_dict[c] = [] 65 | invokes_dict[c].append(m) 66 | return invokes_dict 67 | 68 | def count_methods(smalitree): 69 | basics = 0 70 | objects_count = 0 71 | for cl in smalitree.classes: 72 | for m in cl.methods: 73 | if not m.called: 74 | index = m.descriptor.rfind(')')+1 75 | rtype = m.descriptor[index:] 76 | if rtype in BASIC_TYPES: 77 | basics += 1 78 | else: 79 | objects_count += 1 80 | print(cl.name+"->"+ m.descriptor) 81 | print("methods with basic types: {}".format(basics)) 82 | print("methods with object types: {}".format(objects_count)) 83 | 84 | def remove_methods_by_invokes(smalitree, to_remove_invokes): 85 | invokes_dict = get_class_method_dict(to_remove_invokes) 86 | i = 0 87 | for c in smalitree.classes: 88 | if c.name in invokes_dict: 89 | for m in c.methods[:]: 90 | if not m.is_constructor and m.descriptor in invokes_dict[c.name]: 91 | c.methods.remove(m) 92 | i += 1 93 | print("{} methods not anymore mentioned in invoke instructions removed".format(i)) 94 | -------------------------------------------------------------------------------- /acvtool/cutter/label_block.py: -------------------------------------------------------------------------------- 1 | 2 | class LBlock(object): 3 | 4 | def __init__(self, start_i, covered, labels, end_i=0, is_switch=False, is_array=False): 5 | self.start_i = start_i 6 | self.covered = covered 7 | self.labels = labels 8 | self.end_i = end_i 9 | self.is_switch = is_switch 10 | self.is_array = is_array 11 | #self.is_try_handler = is_try_handler 12 | self.monitor_exit = False 13 | self.is_try_start = False 14 | self.is_try_end = False 15 | self.is_catch = False 16 | self.is_catchall = False 17 | self.is_move_exception = False 18 | self.is_goto_monitor_exit = False 19 | #self.is_to_cut = False 20 | 21 | def __repr__(self): 22 | return "{}-{}: covered:{}, labels:{}, switch:{}, array:{}, monitor_exit:{}, try_start:{}, try_end:{}, catch:{}" \ 23 | .format(self.start_i, self.end_i, self.covered, ",".join(self.labels),self.is_switch, self.is_array, self.monitor_exit, self.is_try_start, self.is_try_end, self.is_catch) -------------------------------------------------------------------------------- /acvtool/cutter/methods.py: -------------------------------------------------------------------------------- 1 | import returns 2 | 3 | def clean_not_executed_methods(smalitree): 4 | stub_methods = set() 5 | for cl in smalitree.classes: 6 | for m in cl.methods: 7 | full_name = "{}->{}".format(cl.name, m.descriptor) 8 | if m.is_constructor and not m.called: 9 | stub(full_name, m) 10 | stub_methods.add(full_name) 11 | if not m.is_constructor and not m.called:# and not m.synchronized: 12 | ret_type = returns.get_return_type(m.descriptor) 13 | stub(full_name, m, ret_type) 14 | full_name = "{}->{}".format(cl.name, m.descriptor) 15 | stub_methods.add(full_name) 16 | return stub_methods 17 | 18 | 19 | def clean_not_exec_methods_range(smalitree, start, end): 20 | for i in range(start, end): 21 | cl = smalitree.classes[i] 22 | for m in cl.methods: 23 | full_name = "{}->{}".format(cl.name, m.descriptor) 24 | if m.is_constructor and not m.called: 25 | stub(full_name, m) 26 | if not m.is_constructor and not m.called:# and not m.synchronized: 27 | ret_type = returns.get_return_type(m.descriptor) 28 | stub(full_name, m, ret_type) 29 | 30 | 31 | def remove_static(smalitree): 32 | i = 0 33 | for cl in smalitree.classes: 34 | for m in cl.methods[:]: 35 | if not m.called and "static" in m.access: 36 | cl.methods.remove(m) 37 | i += 1 38 | print("{} static methods removed".format(i)) 39 | 40 | 41 | def stub(full_name, method, ret_type=None): 42 | method.labels = {} 43 | method.tries = [] 44 | method.insns = [] 45 | 46 | if ret_type: 47 | method.insns.extend(returns.get_return_insns(ret_type)) 48 | #returns.set_defaults(method, ret_type) 49 | else: 50 | method.insns.extend(returns.default_constructor_insns) 51 | #returns.set_default_constructor_for(method) 52 | method.ignore = True 53 | -------------------------------------------------------------------------------- /acvtool/cutter/returns.py: -------------------------------------------------------------------------------- 1 | from ..smiler.instrumenting.apkil.insnnode import InsnNode 2 | 3 | constructor_ins1 = InsnNode(r"invoke-direct/range {p0 .. p0}, Ljava/lang/Object;->()V") 4 | constructor_ins2 = InsnNode("return-void") 5 | default_constructor_insns = [constructor_ins1, constructor_ins2] 6 | 7 | ret_obj_ins1 = InsnNode(r"const/4 v0, 0x0") 8 | ret_obj_ins2 = InsnNode(r"return-object v0") 9 | default_ret_object_insns = [ret_obj_ins1, ret_obj_ins2] 10 | 11 | ret_zero_ins1 = InsnNode(r"const/4 v0, 0x0") 12 | ret_zero_ins2 = InsnNode(r"return v0") 13 | default_ret_zero_insns = [ret_zero_ins1, ret_zero_ins2] 14 | 15 | ret_wide_ins1 = InsnNode(r"const-wide v0, 0x0") 16 | ret_wide_ins2 = InsnNode(r"return-wide v0") 17 | default_ret_wide_insns = [ret_wide_ins1, ret_wide_ins2] 18 | 19 | 20 | basic_returns = { 21 | "V": [InsnNode("return-void")], 22 | 'Z': default_ret_zero_insns, 23 | 'B': default_ret_zero_insns, 24 | 'S': default_ret_zero_insns, 25 | 'C': default_ret_zero_insns, 26 | 'I': default_ret_zero_insns, 27 | 'J': default_ret_wide_insns, 28 | 'F': default_ret_zero_insns, 29 | 'D': default_ret_wide_insns 30 | } 31 | 32 | 33 | def set_defaults(method, ret_type): 34 | method.insns = get_return_insns(ret_type) 35 | method.labels = {} 36 | 37 | 38 | def set_default_constructor_for(method): 39 | method.insns = default_constructor_insns 40 | method.labels = {} 41 | 42 | 43 | def get_return_type(descriptor): 44 | index = descriptor.rfind(')')+1 45 | rtype = descriptor[index:] 46 | return rtype 47 | 48 | 49 | def get_return_insns(rtype): 50 | if rtype in basic_returns: 51 | return basic_returns[rtype] 52 | else: 53 | return default_ret_object_insns 54 | -------------------------------------------------------------------------------- /acvtool/cutter/shrinker.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from ..smiler.instrumenting.smali_instrumenter import Instrumenter 3 | from ..smiler.instrumenting.utils import Utils 4 | from ..smiler.operations import binaries 5 | from . import basic_block 6 | 7 | 8 | def shrink_smali(wd, pickle_files): 9 | '''Saves shrunk version of smali files. 10 | This implementation only removes not executed classes, methods and instructions in a dull way. 11 | The resulting code may not compile because some instructions still reference removed classes. 12 | ACVCut implementation was more sophisticated targeting to keep the shrunk app executable. 13 | ''' 14 | smali_dirs = Utils.get_smali_dirs(wd.unpacked_apk) 15 | if smali_dirs: 16 | for sd in smali_dirs: 17 | Utils.rm_if_exists(sd) 18 | min_pickle = min(pickle_files.keys()) 19 | max_pickle = max(pickle_files.keys()) 20 | for treeId in range(min_pickle, max_pickle+1): 21 | if treeId not in pickle_files: 22 | continue 23 | smalitree = binaries.load_smalitree(pickle_files[treeId]) 24 | shrink_smalitree(smalitree) 25 | smali_instrumenter = Instrumenter(smalitree, "", "instruction", wd.package) 26 | smali_instrumenter.save_instrumented_smalitree_by_class(smalitree, 0, instrument=False) 27 | logging.info("smali saved: {}".format(wd.unpacked_apk)) 28 | 29 | 30 | def shrink_smalitree(smalitree): 31 | remove_not_executed_methods_and_classes(smalitree) 32 | basic_block.remove_blocks_from_selected_method(smalitree) 33 | 34 | 35 | def remove_not_executed_methods_and_classes(smalitree): 36 | '''Remove not executed methods and classes (for visualisation only).''' 37 | for cl in smalitree.classes[:]: 38 | if cl.covered() == 0: 39 | print(cl.name) 40 | smalitree.classes.remove(cl) 41 | else: 42 | for m in cl.methods[:]: 43 | if m.covered() == 0: 44 | cl.methods.remove(m) -------------------------------------------------------------------------------- /acvtool/smiler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/__init__.py -------------------------------------------------------------------------------- /acvtool/smiler/acv.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from . import smiler 4 | from .config import config 5 | from ..cutter import shrinker 6 | from .operations import coverage 7 | from .instrumenting import apktool 8 | from .entities.wd import WorkingDirectory 9 | from .reporting.reporter import Reporter 10 | 11 | 12 | 13 | def instrument(args): 14 | if os.path.isdir(args.working_dir): 15 | if not args.force: 16 | print("The working directory exists and may contain data: {}".format(args.working_dir)) 17 | user_choice = input("Overwrite (y/n)? ") 18 | if user_choice.lower() in ["y", "yes"]: 19 | pass 20 | elif user_choice.lower() in ["n", "no"]: 21 | print("Aborting operation!") 22 | return 23 | else: 24 | print("Your choice is not correct! Exiting!") 25 | return 26 | package, apk_path, pickle_path = smiler.instrument_apk( 27 | apk_path=args.apk_path, 28 | result_dir=args.working_dir, 29 | dbg_start=args.dbg_start, 30 | dbg_end=args.dbg_end, 31 | installation=args.install or args.report, 32 | granularity=args.granularity, 33 | mem_stats=args.memstats, 34 | keep_unpacked=args.keepsources, 35 | ignore_filter=args.stubs, 36 | target_cl=args.target_class, 37 | target_mtd=args.method, 38 | target_dexs=[] if not args.dex else args.dex.split(",")) 39 | if args.report: 40 | # onstop is deprecated due to missing get_execution_results 41 | smiler.start_instrumenting(package, 42 | onstop=lambda: reporter.generate( 43 | package, 44 | pickle_path, 45 | output_dir=config.default_report_dir, 46 | granularity=args.granularity), 47 | timeout=int(args.timeout)) 48 | 49 | def install(args): 50 | smiler.install(args.apk_path) 51 | 52 | def uninstall(args): 53 | smiler.uninstall(args.package_name) 54 | 55 | def activate(args): 56 | smiler.activate(args.package) 57 | 58 | def start(args): 59 | onstop_report = None 60 | if args.report: 61 | # onstop is deprecated due to missing get_execution_results 62 | onstop_report = lambda: reporter.generate( 63 | args.package_name, 64 | args.pickle_path, 65 | output_dir=config.default_report_dir, 66 | granularity=args.granularity) 67 | smiler.start_instrumenting( 68 | args.package_name, 69 | args.q, 70 | onstop=onstop_report, 71 | timeout=int(args.timeout)) 72 | 73 | def stop(args): 74 | smiler.stop_instrumenting(args.package_name, int(args.timeout)) 75 | 76 | def snap(args): 77 | if args.repeat: 78 | smiler.save_ec_and_screen(args.package_name, int(args.throttle), args.output_dir) 79 | else: 80 | wd = WorkingDirectory(args.package_name, args.working_dir) if args.working_dir else "" 81 | output_dir = args.output_dir if args.output_dir else wd.ec_dir 82 | print("output_dir: {}".format(output_dir)) 83 | smiler.snap(args.package_name, 1, output_dir) 84 | 85 | def flush(args): 86 | smiler.flush(args.package_name) 87 | 88 | def calculate(args): 89 | smiler.calculate(args.package_name) 90 | 91 | def pull(args): 92 | wd = WorkingDirectory(args.package_name, args.working_dir) 93 | smiler.get_execution_results(args.package_name, wd.ec_dir, wd.images) 94 | 95 | def cover_pickles(args): 96 | wd = WorkingDirectory(args.package_name, args.working_dir) 97 | coverage.cover_pickles(wd) 98 | 99 | def report(args): 100 | wd = WorkingDirectory(args.package_name, args.working_dir) 101 | reporter = Reporter(args.package_name, wd.get_covered_pickles(), wd.images, wd.report) 102 | reporter.generate(html=args.html, xml=args.xml, 103 | granularity=args.granularity, 104 | ignore_filter=args.stubs, shrink=args.shrink) 105 | 106 | def sign(args): 107 | smiler.patch_align_sign(args.apk_path, "{0}.signed.apk".format(args.apk_path)) 108 | 109 | def build(args): 110 | smiler.build_dir(args.apktool_dir, args.result_dir, signature=args.s, installation=args.i) 111 | 112 | def shrink(args): 113 | wd = WorkingDirectory(args.package_name, args.working_dir) 114 | smiler.refresh_wd_no_smali(wd, args.apk_path) 115 | shrinker.shrink_smali(wd, wd.get_covered_pickles()) 116 | apktool.build(wd.unpacked_apk, wd.instrumented_package_path) 117 | smiler.patch_align_sign(wd.instrumented_package_path, wd.short_apk_path) 118 | logging.info("shrinked apk was saved to {}".format(wd.short_apk_path)) 119 | 120 | -------------------------------------------------------------------------------- /acvtool/smiler/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "AAPT": "aapt_path", 3 | "ZIPALIGN": "zipalign_path", 4 | "ADB": "adb_path", 5 | "APKSIGNER": "apksigner_path", 6 | "ACVPATCHER": "acvpatcher_path" 7 | } -------------------------------------------------------------------------------- /acvtool/smiler/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import shutil 4 | import json 5 | import logging 6 | from pathlib import Path 7 | 8 | from pkg_resources import resource_filename 9 | from os.path import expanduser 10 | 11 | 12 | class config(object): 13 | 14 | dir_path = os.path.join(expanduser("~"), 'acvtool') 15 | if not os.path.exists(dir_path): 16 | logging.info('Creating acvtool directory in the user home directory') 17 | os.makedirs(dir_path) 18 | config_path = os.path.join(dir_path, 'config.json') 19 | if not os.path.exists(config_path): 20 | shutil.copy(resource_filename("acvtool.smiler", "config.json"), config_path) 21 | logging.info("config.json config was created in the {}.".format(dir_path)) 22 | with open(config_path) as json_file: 23 | config_data = json.load(json_file) 24 | 25 | APKTOOL_JAVA_PATH = "java" # it is possible that some tools will require different versions of java 26 | APKTOOL_JAVA_OPTS = "-Xms512m -Xmx1024m" 27 | APKTOOL_QUITE = "True" 28 | 29 | INSTRUMENTING_NAME = "tool.acv.AcvInstrumentation" 30 | 31 | instrumenting_class_dir_path = resource_filename('acvtool.smiler.resources.instrumentation', 'smali') 32 | html_resources_dir_path = resource_filename('acvtool.smiler.resources.html', '.resources') 33 | templates_path = resource_filename('acvtool.smiler.resources.html', 'templates') 34 | keystore_path = resource_filename('acvtool.smiler', 'keystore') 35 | keystore_password = '123456' 36 | 37 | apksigner_path = Path(config_data["APKSIGNER"]) 38 | adb_path = Path(config_data["ADB"]) 39 | aapt_path = Path(config_data["AAPT"]) 40 | zipalign = Path(config_data["ZIPALIGN"]) 41 | acvpatcher = Path(config_data["ACVPATCHER"]) 42 | 43 | version = "2.3.2" 44 | logging_yaml = resource_filename('acvtool.smiler.resources', 'logging.yaml') 45 | 46 | default_working_dir = os.path.join(dir_path, "acvtool_working_dir") 47 | default_report_dir = os.path.join(default_working_dir, "report") 48 | default_onstop_timeout = 240 49 | 50 | throttle = 10 51 | 52 | # 65535 is the max number of methods in DEX, 11 is the number of methods acvtool adds to the app 53 | METHOD_LIMIT = 50000 # 65535-11 54 | 55 | @staticmethod 56 | def get_ec_dir(output_dir, package): 57 | return os.path.join(output_dir, package, "ec_files") 58 | 59 | 60 | @staticmethod 61 | def get_images_dir(output_dir, package): 62 | return os.path.join(output_dir, package, "images") 63 | 64 | @staticmethod 65 | def check_tools(): 66 | err = False 67 | if not os.path.exists(config.apksigner_path): 68 | err = True 69 | logging.error("apksigner was not found at {}".format(config.apksigner_path)) 70 | if not os.path.exists(config.adb_path): 71 | err = True 72 | logging.error("adb tool was not found at {}".format(config.adb_path)) 73 | if not os.path.exists(config.aapt_path): 74 | err = True 75 | logging.error("aapt tool was not found at {}".format(config.aapt_path)) 76 | if not os.path.exists(config.zipalign): 77 | err = True 78 | logging.error("zipalign was not found at {}".format(config.zipalign)) 79 | if not os.path.exists(config.acvpatcher): 80 | err = True 81 | logging.error("acvpatcher was not found at {}".format(config.acvpatcher)) 82 | if err: 83 | logging.error("\nCONFIGURATION ERROR: Please check paths at {} or/and install required software (consult README.md for details).\n".format(config.config_path)) 84 | sys.exit() 85 | 86 | config.check_tools() 87 | -------------------------------------------------------------------------------- /acvtool/smiler/entities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/entities/__init__.py -------------------------------------------------------------------------------- /acvtool/smiler/entities/coverage.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from ..granularity import Granularity 3 | 4 | 5 | class CoverageData(object): 6 | ''' Coverage data for method, class, package or app. ''' 7 | def __init__(self, lines=0, lines_missed=0, lines_covered=0, 8 | methods_covered=0, methods_missed=0, methods=0, classes=0, 9 | classes_missed=0, classes_covered=0): 10 | self.lines = lines 11 | self.lines_missed = lines_missed 12 | self.lines_covered = lines_covered 13 | self.methods_covered = methods_covered 14 | self.methods_missed = methods_missed 15 | self.methods = methods 16 | self.classes = classes 17 | self.classes_missed = classes_missed 18 | self.classes_covered = classes_covered 19 | 20 | @staticmethod 21 | def coverage(covered, coverable): 22 | if coverable == 0: 23 | return None 24 | return float(covered) / coverable 25 | 26 | def get_class_coverage(self): 27 | return CoverageData.coverage(self.classes_covered, self.classes) 28 | 29 | def get_method_coverage(self): 30 | return CoverageData.coverage(self.methods_covered, self.methods) 31 | 32 | def get_line_coverage(self): 33 | return CoverageData.coverage(self.lines_covered, self.lines) 34 | 35 | def get_coverage(self, granularity): 36 | if Granularity.is_instruction(granularity): 37 | return self.get_line_coverage() 38 | if Granularity.is_method(granularity): 39 | return self.get_method_coverage() 40 | return self.get_class_coverage() 41 | 42 | def update_coverage_for_single_class_from_methods(self): 43 | if self.methods > 0: 44 | self.classes = 1 45 | self.classes_missed = 1 if self.methods_missed == self.methods else 0 46 | classes=self.classes - self.classes_missed, 47 | else: 48 | self.classes = 0 49 | self.classes_covered = 0 50 | self.classes_missed = 0 51 | 52 | @staticmethod 53 | def format_coverage(result): 54 | return ("{:.5f}%".format(100 * result)) if result is not None else "-" 55 | 56 | def get_formatted_coverage(self, granularity): 57 | coverage = self.get_coverage(granularity) 58 | return CoverageData.format_coverage(coverage) 59 | 60 | def covered(self, granularity): 61 | if Granularity.is_instruction(granularity): 62 | return self.lines_covered 63 | if Granularity.is_method(granularity): 64 | return self.methods_covered 65 | return self.classes_covered 66 | 67 | def missed(self, granularity): 68 | if Granularity.is_instruction(granularity): 69 | return self.lines_missed 70 | if Granularity.is_method(granularity): 71 | return self.methods_missed 72 | return self.classes_missed 73 | 74 | def coverable(self, granularity): 75 | if Granularity.is_instruction(granularity): 76 | return self.lines 77 | if Granularity.is_method(granularity): 78 | return self.methods 79 | return self.classes 80 | 81 | def add_data(self, coverage_data, lines=0, methods=0, classes=0): 82 | self.lines += coverage_data.lines + lines 83 | self.lines_missed += coverage_data.lines_missed 84 | self.lines_covered += coverage_data.lines_covered 85 | self.methods_covered += coverage_data.methods_covered 86 | self.methods_missed += coverage_data.methods_missed 87 | self.methods += coverage_data.methods + methods 88 | self.classes += coverage_data.classes + classes 89 | self.classes_missed += coverage_data.classes_missed 90 | self.classes_covered += coverage_data.classes_covered 91 | 92 | 93 | def __sub__(self, coverage_data): 94 | diff = CoverageData( 95 | lines=self.lines - coverage_data.lines, 96 | lines_missed=self.lines_missed - coverage_data.lines_missed, 97 | lines_covered=self.lines_covered - coverage_data.lines_covered, 98 | methods_covered=self.methods_covered - coverage_data.methods_covered, 99 | methods_missed=self.methods_missed - coverage_data.methods_missed, 100 | methods=self.methods - coverage_data.methods, 101 | classes=self.classes - coverage_data.classes, 102 | classes_missed=self.classes_missed - coverage_data.classes_missed, 103 | classes_covered=self.classes_covered - coverage_data.classes_covered) 104 | return diff 105 | 106 | @staticmethod 107 | def log_coverage_difference(i, st_cov, new_st_cov): 108 | diff_cov = new_st_cov - st_cov 109 | logging.info("diff\tst {}: lines {}({}), methods {}({}), classes {}({}), coverage {}%".format( 110 | i, 111 | CoverageData.cred(diff_cov.lines_covered), st_cov.lines, 112 | CoverageData.cred(diff_cov.methods_covered), st_cov.methods, 113 | CoverageData.cred(diff_cov.classes_covered), st_cov.classes, 114 | 100*(new_st_cov.get_line_coverage()-st_cov.get_line_coverage()) 115 | ) 116 | ) 117 | return diff_cov 118 | 119 | @staticmethod 120 | def log_diff(diff): 121 | logging.info(" total diff: lines {}({}), methods {}({}), classes {}({}), coverage {}%".format( 122 | CoverageData.cred(diff.lines_covered), diff.lines, 123 | CoverageData.cred(diff.methods_covered), diff.methods, 124 | CoverageData.cred(diff.classes_covered), diff.classes, 125 | 100*diff.get_line_coverage() 126 | ) 127 | ) 128 | 129 | @staticmethod 130 | def cred(number): 131 | '''Highlight with color red if non zero''' 132 | if number == 0: 133 | return number 134 | return "\033[91m{}\033[0m".format(number) -------------------------------------------------------------------------------- /acvtool/smiler/entities/wd.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ..instrumenting.utils import Utils 4 | from ..instrumenting import config 5 | 6 | 7 | class WorkingDirectory(object): 8 | '''Describes the default working directory structure.''' 9 | 10 | def __init__(self, package, wd_path): 11 | if not wd_path: 12 | raise Exception("no working directory path given") 13 | self.wd_path = wd_path 14 | self.package = package 15 | self.unpacked_apk = os.path.join(wd_path, "apktool") # sources to be processed in this dir 16 | self.pickle_dir = os.path.join(wd_path, "pickles") 17 | self.instrumented_package_path = os.path.join(wd_path, self.package + ".apk") 18 | self.instrumented_apk_path = os.path.join(wd_path, "instr_{}.apk".format(self.package)) 19 | self.manifest_path = os.path.join(self.unpacked_apk, "AndroidManifest.xml") 20 | self.decompiled_apk = os.path.join(self.wd_path, "dec_apk") # sources to stay original in this dir 21 | self.ec_dir = os.path.join(wd_path, "ec_files") 22 | self.images = os.path.join(wd_path, "images") 23 | self.report = os.path.join(wd_path, "report") 24 | self.short_apk_path = os.path.join(wd_path, "short.apk") 25 | self.stub_dir = os.path.join(wd_path, "stubs") 26 | self.covered_pickle_dir = os.path.join(wd_path, "covered_pickles") 27 | self.shrunk_pickle_dir = os.path.join(wd_path, "shrunk_pickles") 28 | self.cha_pickle_dir = os.path.join(wd_path, "cha_pickles") 29 | self.cha_apk_path = os.path.join(wd_path, "cha.apk") 30 | 31 | @staticmethod 32 | def get_manifest_path(unpacked_path): 33 | return os.path.join(unpacked_path, "AndroidManifest.xml") 34 | 35 | 36 | def get_ecs(self): 37 | if not os.path.exists(self.ec_dir): 38 | raise Exception("No such directory: {}\nConsider: $ acv snap - to generate coverage .ec files".format(self.ec_dir)) 39 | ecs = {} 40 | for f in os.listdir(self.ec_dir): 41 | id = int(f.split("_")[2][:-3]) 42 | ec_path = os.path.join(self.ec_dir, f) 43 | if os.path.isfile(ec_path): 44 | if id not in ecs: 45 | ecs[id] = [] 46 | ecs[id].append(ec_path) 47 | return ecs 48 | #return {int(f.split("_")[1]): os.path.join(self.ec_dir, f) for f in os.listdir(self.ec_dir) if os.path.isfile(os.path.join(self.ec_dir, f))} 49 | 50 | def get_ecs_by_ts(self): 51 | ecs = {} 52 | for f in sorted(os.listdir(self.ec_dir)): 53 | ts, id = (lambda s: (int(s[1]), int(s[2][:-3])))(f.split("_")) 54 | ec_path = os.path.join(self.ec_dir, f) 55 | if os.path.isfile(ec_path): 56 | if ts not in ecs: 57 | ecs[ts] = [] 58 | ecs[ts].append(ec_path) 59 | return ecs 60 | 61 | 62 | def get_ecs_by_ts_by_dex(self): 63 | ''' 64 | Grupping ec files into ec[ts][dn] by timestamp (ts), 65 | then by dex number (dn) 66 | ''' 67 | ecs = {} 68 | for f in sorted(os.listdir(self.ec_dir)): 69 | ts, dn = (lambda s: (int(s[1]), int(s[2][:-3])))(f.split("_")) 70 | ec_path = os.path.join(self.ec_dir, f) 71 | if os.path.isfile(ec_path): 72 | if ts not in ecs: 73 | ecs[ts] = {} 74 | if dn not in ecs[ts]: 75 | ecs[ts][dn] = ec_path 76 | return ecs 77 | 78 | 79 | def __get_pickles(self, dir_path): 80 | if not os.path.exists(dir_path): 81 | raise Exception("No such directory: {}".format(dir_path)) 82 | return {int(f.split("_")[1][:-7]): os.path.join(dir_path, f) for f in os.listdir(dir_path) if os.path.isfile(os.path.join(dir_path, f))} 83 | 84 | def get_pickles(self): 85 | return self.__get_pickles(self.pickle_dir) 86 | 87 | def get_covered_pickles(self): 88 | if not os.path.exists(self.covered_pickle_dir): 89 | raise Exception("No such directory: {}\nConsider: $ acv cover-pickles ".format(self.covered_pickle_dir)) 90 | return self.__get_pickles(self.covered_pickle_dir) 91 | 92 | def get_shrunk_pickles(self): 93 | return self.__get_pickles(self.shrunk_pickle_dir) 94 | 95 | def get_smali_dirs(self, apk_dir): 96 | all_smali_dirs = Utils.get_smali_dirs(apk_dir) 97 | smali_dirs = {} 98 | smali_dirs[1] = os.path.join(apk_dir, config.smalidir_name) 99 | for i in range(2, len(all_smali_dirs)+1): 100 | smali_dirs[i] = os.path.join(apk_dir, config.smalidir_name + str(i)) 101 | return smali_dirs 102 | 103 | -------------------------------------------------------------------------------- /acvtool/smiler/granularity.py: -------------------------------------------------------------------------------- 1 | 2 | class Granularity(object): 3 | INSTRUCTION = 1 4 | METHOD = 2 5 | CLASS = 4 6 | GRANULARITIES = { 7 | "instruction": INSTRUCTION, 8 | "method": METHOD, 9 | "class": CLASS 10 | } 11 | reverseGranularityDict = {v: k for k, v in GRANULARITIES.items()} 12 | 13 | default = "instruction" 14 | 15 | def __init__(self, granularity="instruction"): 16 | self.granularity = Granularity.GRANULARITIES[granularity] 17 | 18 | 19 | @staticmethod 20 | def granularities(): 21 | return Granularity.GRANULARITIES.keys() 22 | 23 | @staticmethod 24 | def is_class(granularity): 25 | return granularity <= Granularity.CLASS 26 | 27 | @staticmethod 28 | def is_method(granularity): 29 | return granularity <= Granularity.METHOD 30 | 31 | @staticmethod 32 | def is_instruction(granularity): 33 | return granularity == Granularity.INSTRUCTION 34 | 35 | @staticmethod 36 | def get(granularity_key): 37 | return Granularity.reverseGranularityDict[granularity_key] 38 | 39 | class WrongGranularityValueException(Exception): 40 | pass -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/instrumenting/__init__.py -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/acvpatcher.py: -------------------------------------------------------------------------------- 1 | 2 | from ..config import config 3 | from ..operations import terminal 4 | 5 | def patch_apk(apk_path, dex_filepaths): 6 | classes_dex = ' '.join([f'"{d}"' for d in dex_filepaths]) 7 | cmd = '"{}" -c {} -p {} -i {} -r {} -a "{}"'.format(config.acvpatcher, 8 | classes_dex, 9 | "android.permission.WRITE_EXTERNAL_STORAGE", 10 | "tool.acv.AcvInstrumentation", 11 | "tool.acv.AcvReceiver:tool.acv.calculate tool.acv.AcvReceiver:tool.acv.snap tool.acv.AcvReceiver:tool.acv.flush", 12 | apk_path) 13 | terminal.request_pipe(cmd) -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/android_manifest.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | from xml.dom import minidom 3 | 4 | 5 | class XMLManifest(object): 6 | 7 | 8 | def __init__(self, manifest_path): 9 | self.manifest_path = manifest_path 10 | self.xml = minidom.parse(manifest_path) 11 | self.package = self.get_package_name() 12 | 13 | 14 | def add_instrumentation_tag(self): 15 | instrumentation = self.create_element( 16 | under_tag="manifest", 17 | tag="instrumentation", 18 | attributes={ 19 | "android:name" : "tool.acv.AcvInstrumentation", 20 | "android:targetPackage" : self.package 21 | } 22 | ) 23 | manifest = self.xml.getElementsByTagName("manifest")[0] 24 | manifest.appendChild(instrumentation) 25 | 26 | 27 | def add_broadcast_receiver(self): 28 | receiver = self.create_element( 29 | under_tag="application", 30 | tag="receiver", 31 | attributes={ 32 | "android:name": "tool.acv.AcvReceiver", 33 | "android:enabled": "true", 34 | "android:exported": "true" 35 | } 36 | ) 37 | intent_filter = self.create_element( 38 | under_tag="receiver", 39 | tag="intent-filter", 40 | attributes={} 41 | ) 42 | action1 = self.create_element( 43 | under_tag="intent-filter", 44 | tag="action", 45 | attributes={ 46 | "android:name": "tool.acv.calculate" 47 | } 48 | ) 49 | action2 = self.create_element( 50 | under_tag="intent-filter", 51 | tag="action", 52 | attributes={ 53 | "android:name": "tool.acv.snap" 54 | } 55 | ) 56 | action3 = self.create_element( 57 | under_tag="intent-filter", 58 | tag="action", 59 | attributes={ 60 | "android:name": "tool.acv.flush" 61 | } 62 | ) 63 | 64 | application = self.xml.getElementsByTagName("application")[0] 65 | application.appendChild(receiver) 66 | receiver.appendChild(intent_filter) 67 | intent_filter.appendChild(action3) 68 | intent_filter.appendChild(action2) 69 | intent_filter.appendChild(action1) 70 | 71 | 72 | def add_write_permission(self): 73 | write_permission = "android.permission.WRITE_EXTERNAL_STORAGE" 74 | permissions = self.xml.getElementsByTagName("uses-permission") 75 | for p in permissions: 76 | if p.getAttribute("android:name") == write_permission: 77 | return 78 | uses_write_permission = self.create_element( 79 | under_tag="manifest", 80 | tag="uses-permission", 81 | attributes={ 82 | "android:name": write_permission 83 | } 84 | ) 85 | manifest = self.xml.getElementsByTagName("manifest")[0] 86 | manifest.appendChild(uses_write_permission) 87 | 88 | 89 | def addUsesPermission(self, permName): 90 | if permName not in self.getUsesPermissions(): 91 | self.createElement("manifest", "uses-permission", {"android:name" : permName}) 92 | 93 | 94 | def save_xml(self): 95 | with codecs.open(self.manifest_path, "w", "utf-8") as f: 96 | self.xml.writexml(f) 97 | 98 | 99 | def get_package_name(self): 100 | return self.xml.getElementsByTagName("manifest")[0].getAttribute("package") 101 | 102 | 103 | def create_element(self, under_tag, tag, attributes): 104 | elem = self.xml.createElement(tag) 105 | if attributes: 106 | for entry in attributes.items(): 107 | elem.setAttribute(entry[0], entry[1]) 108 | return elem 109 | -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apkil/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/instrumenting/apkil/__init__.py -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apkil/arraydatanode.py: -------------------------------------------------------------------------------- 1 | class ArrayDataNode(object): 2 | 3 | def __init__(self, lines, label): 4 | self.buf = [] 5 | self.label = None 6 | 7 | self.__parse(lines, label) 8 | 9 | def __repr__(self): 10 | pass 11 | 12 | def __parse(self, lines, label): 13 | self.buf = lines 14 | self.label = label 15 | label.array_data = self 16 | 17 | def reload(self): 18 | pass 19 | 20 | -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apkil/classnode.py: -------------------------------------------------------------------------------- 1 | from .methodnode import MethodNode 2 | import sys 3 | import os 4 | from .fieldnode import FieldNode 5 | from .codeblocknode import CodeBlockNode 6 | 7 | class ClassNode(object): 8 | 9 | def __init__(self, filename=None, buf=None, folder=None): 10 | self.buf = [] 11 | self.file_path = "" 12 | self.folder = "" 13 | self.file_name = "" 14 | self.name = '' 15 | self.super_name = '' 16 | self.source = '' 17 | self.implements = [] 18 | self.access = [] 19 | self.interfaces = [] 20 | self.fields_comment = '' 21 | self.fields = [] 22 | self.methods_comment = '' 23 | self.methods = [] 24 | self.inner_classes = [] 25 | self.annotations_comment = '' 26 | self.annotations = [] 27 | self.debugs = [] 28 | self.meth_ref_dict = None 29 | self.ignore = False 30 | self.keep = False 31 | 32 | if filename or buf: 33 | self.__parse(filename, buf, folder) 34 | 35 | def __repr__(self): 36 | return "Class: %s %s << %s\n%s%s" % \ 37 | (' '.join(self.access), self.name, self.super_name, \ 38 | ''.join([repr(f) for f in self.fields]), \ 39 | ''.join([repr(m) for m in self.methods])) 40 | 41 | def __parse(self, file_path, buf, folder): 42 | if file_path: 43 | self.file_path = file_path 44 | f = open(self.file_path, 'r') 45 | elif buf: 46 | f = StringIO.StringIO(buf) 47 | else: 48 | return 49 | 50 | self.folder = folder 51 | full_folder, self.file_name = os.path.split(file_path) 52 | 53 | 54 | line = f.readline() 55 | while line: 56 | if line.isspace(): 57 | line = f.readline() 58 | continue 59 | line = line.strip() 60 | segs = line.split() 61 | # .source 62 | if segs[0] == ".source": 63 | self.source = segs[1] 64 | # .class 65 | elif segs[0] == ".class": 66 | self.name = segs[-1] 67 | # : public, final, super, interface, abstract 68 | self.access = segs[1:-1] 69 | # .super 70 | elif segs[0] == ".super": 71 | self.super_name = segs[1] 72 | elif segs[0] == ".interface": 73 | print("can't parse .interface") 74 | sys.exit(1) 75 | elif segs[0] == ".implements": 76 | self.implements.append(segs[1]) 77 | elif segs[0] == ".field": 78 | lines = [line] 79 | line = f.readline() 80 | if not line.isspace(): 81 | line = line.strip() 82 | annotation_lines = [] 83 | if line.startswith(".annotation"): 84 | while not line.startswith(".end field"): 85 | annotation_lines.append(line) 86 | line = f.readline() 87 | if line.isspace(): 88 | line = f.readline() 89 | line = line.strip() 90 | 91 | if line == ".end field": 92 | # annotations is inside a field 93 | lines.extend(annotation_lines) 94 | lines.append(line) 95 | else: 96 | if annotation_lines: 97 | # when annotations are not included inside the field 98 | self.annotations.append(CodeBlockNode(annotation_lines)) 99 | self.fields.append(FieldNode(lines)) 100 | continue 101 | elif segs[0] == ".method": 102 | lines = [line] 103 | line = f.readline() 104 | while line: 105 | if line.isspace(): 106 | line = f.readline() 107 | continue 108 | line = line.strip() 109 | lines.append(line) 110 | segs = line.split(None, 2) 111 | if segs[0] == ".end" and segs[1] == "method": 112 | break 113 | line = f.readline() 114 | self.methods.append(MethodNode(lines)) 115 | elif segs[0] == ".annotation": 116 | # there may be subannotations 117 | lines = [line] 118 | line = f.readline() 119 | while line: 120 | if line.isspace(): 121 | line = f.readline() 122 | continue 123 | line = line.strip() 124 | lines.append(line) 125 | segs = line.split(None, 2) 126 | if segs[0] == ".end" and segs[1] == "annotation": 127 | break 128 | line = f.readline() 129 | self.annotations.append(CodeBlockNode(lines)) 130 | elif segs[0] == '#': 131 | if len(segs) > 1 and segs[1] == 'annotations': 132 | self.annotations_comment = line 133 | if len(segs) > 2 and segs[2] == 'fields': 134 | self.fields_comment = line 135 | if len(segs) > 2 and segs[2] == 'methods': 136 | self.methods_comment = line 137 | pass 138 | line = f.readline() 139 | f.close() 140 | 141 | def get_class_description(self): 142 | buf = [] 143 | # .class 144 | buf.append(".class %s %s" % (' '.join(self.access), self.name)) 145 | # .super 146 | buf.append(".super %s" % (self.super_name,)) 147 | # .source 148 | if self.source: 149 | buf.append(".source %s" % (self.source,)) 150 | # .implements 151 | if self.implements: 152 | for imp in self.implements: 153 | buf.append(".implements %s" % (imp,)) 154 | # .interfaces 155 | 156 | return buf 157 | 158 | def get_annotations(self): 159 | buf = [] 160 | # .annotations 161 | if self.annotations: 162 | #buf.append(self.annotations_comment) 163 | for a in self.annotations: 164 | a.reload() 165 | buf.extend(a.buf) 166 | 167 | return buf 168 | 169 | def get_fields(self): 170 | buf = [] 171 | # .field 172 | if self.fields: 173 | #buf.append(self.fields_comment) 174 | for f in self.fields: 175 | f.reload() 176 | buf.extend(f.buf) 177 | 178 | return buf 179 | 180 | def reload(self): 181 | self.buf = [] 182 | 183 | # .class, .super, .source, .implements, .interfaces, .annotations, 184 | # .field 185 | self.buf.extend(self.get_class_description()) 186 | self.buf.extend(self.get_annotations()) 187 | self.buf.extend(self.get_fields()) 188 | 189 | # .method 190 | #self.buf.append(self.methods_comment) 191 | for m in self.methods: 192 | m.reload() 193 | self.buf.extend(m.buf) 194 | 195 | 196 | def set_name(self, name): 197 | self.name = name 198 | 199 | def add_access(self, access): 200 | if type(access) == list: 201 | self.access.extend(access) 202 | else: 203 | self.access.append(access) 204 | 205 | def set_super_name(self, super_name): 206 | self.super_name = super_name 207 | 208 | def add_field(self, field): 209 | self.fields.append(field) 210 | 211 | def add_method(self, method): 212 | if type(method) == list: 213 | self.methods.extend(method) 214 | else: 215 | self.methods.append(method) 216 | 217 | def save(self, new_foldername): 218 | self.reload() 219 | path, filename = os.path.split(self.name[1:-1]) 220 | filename += ".smali" 221 | path = os.path.join(new_foldername, path) 222 | if not os.path.exists(path): 223 | os.makedirs(path) 224 | filename = os.path.join(path, filename) 225 | f = open(filename, 'w') 226 | f.write('\n'.join(self.buf)) 227 | f.close() 228 | 229 | def coverable(self): 230 | if self.ignore: 231 | return 0 232 | return sum(m.coverable() for m in self.methods) 233 | 234 | def covered(self): 235 | if self.ignore: 236 | return 0 237 | return sum(m.covered() for m in self.methods) 238 | 239 | def not_covered(self): 240 | if self.ignore: 241 | return 0 242 | return sum(m.not_covered() for m in self.methods) 243 | 244 | def coverage(self): 245 | coverable = self.coverable() 246 | if coverable == 0: 247 | return None 248 | return float(self.covered()) / coverable 249 | 250 | def missed_methods(self): 251 | if self.ignore: 252 | return 0 253 | return sum(m.covered() == 0 for m in self.methods) 254 | 255 | def mtds_coverable(self): 256 | if self.ignore: 257 | return 0 258 | return sum(m.cover_code > -1 for m in self.methods) 259 | 260 | def is_coverable(self): 261 | if self.ignore: 262 | return False 263 | return any(m.cover_code > -1 for m in self.methods) 264 | 265 | def mtds_covered(self): 266 | if self.ignore: 267 | return 0 268 | return sum(m.called for m in self.methods) 269 | 270 | def mtds_not_covered(self): 271 | if self.ignore: 272 | return 0 273 | return self.mtds_coverable() - self.mtds_covered() 274 | 275 | def mtds_coverage(self): 276 | coverable = self.mtds_coverable() 277 | if coverable == 0: 278 | return None 279 | return float(self.mtds_covered()) / coverable 280 | 281 | def update_meth_ref_dict(self): 282 | self.meth_ref_dict = {} 283 | for m in self.methods: 284 | self.meth_ref_dict[m.descriptor] = m 285 | -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apkil/codeblocknode.py: -------------------------------------------------------------------------------- 1 | class CodeBlockNode(object): 2 | 3 | def __init__(self, lines): 4 | self.buf = [] 5 | 6 | self.__parse(lines) 7 | 8 | def __repr__(self): 9 | pass 10 | 11 | def __parse(self, lines): 12 | self.buf = lines 13 | 14 | def reload(self): 15 | pass 16 | 17 | def get_lines(self): 18 | return self.buf -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apkil/constants.py: -------------------------------------------------------------------------------- 1 | INSN_FMT = { 2 | "invoke-virtual": "35c", 3 | "invoke-super": "35c", 4 | "invoke-direct": "35c", 5 | "invoke-static": "35c", 6 | "invoke-interface": "35c", 7 | "invoke-virtual/range": "3rc", 8 | "invoke-super/range": "3rc", 9 | "invoke-direct/range": "3rc", 10 | "invoke-static/range": "3rc", 11 | "invoke-interface/range": "3rc", 12 | "filled-new-array": "35c", 13 | "invoke-custom": "35c", 14 | "invoke-custom/range": "3rc", 15 | "filled-new-array/range": "3rc" 16 | } 17 | 18 | BASIC_TYPES = { 19 | 'V': "void", 20 | 'Z': "boolean", 21 | 'B': "byte", 22 | 'S': 'short', 23 | 'C': "char", 24 | 'I': "int", 25 | 'J': "long", 26 | 'F': "float", 27 | 'D': "double" 28 | } 29 | 30 | BASIC_TYPES_BY_JAVA = { 31 | "void": 'V', 32 | "boolean": 'Z', 33 | "byte": 'B', 34 | 'short': 'S', 35 | "char": 'C', 36 | "int": 'I', 37 | "long": 'J', 38 | "float": 'F', 39 | "double": 'D' 40 | } -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apkil/fieldnode.py: -------------------------------------------------------------------------------- 1 | 2 | class FieldNode(object): 3 | 4 | def __init__(self, lines=None): 5 | self.buf = [] 6 | self.name = "" 7 | self.access = [] 8 | self.descriptor = "" 9 | self.value = None 10 | self.referred = False 11 | 12 | if lines: 13 | self.__parse(lines) 14 | 15 | def __repr__(self): 16 | return " Field: %s %s %s%s\n" % \ 17 | (' '.join(self.access), self.descriptor, self.name, \ 18 | self.value and "=" + self.value or "") 19 | 20 | # .field : [ = ] 21 | def __parse(self, lines): 22 | self.buf = lines 23 | 24 | i = self.buf[0].find('=') 25 | segs = [] 26 | if i > 0: 27 | segs = self.buf[0][:i].split() 28 | self.value = self.buf[0][i + 1:].strip() 29 | else: 30 | segs = self.buf[0].split() 31 | self.access = segs[1:-1] 32 | self.name, self.descriptor = segs[-1].split(':') 33 | 34 | def set_name(self, name): 35 | self.name = name 36 | 37 | def add_access(self, access): 38 | if type(access) == list: 39 | self.access.extend(access) 40 | else: 41 | self.access.append(access) 42 | 43 | def set_desc(self, desc): 44 | self.descriptor = desc 45 | 46 | def set_value(self, value): 47 | self.value = value 48 | 49 | def reload(self): 50 | self.buf[0] = "%s %s %s:%s" % \ 51 | (".field", ' '.join(self.access), self.name, \ 52 | self.descriptor) 53 | if self.value: self.buf[0] += " = %s" % self.value -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apkil/insn35c.py: -------------------------------------------------------------------------------- 1 | class Insn35c(object): 2 | 3 | def __init__(self, line): 4 | self.buf = "" 5 | self.opcode_name = "" 6 | self.registers = [] 7 | self.method_descriptor = "" 8 | 9 | self.__parse(line) 10 | 11 | def __repr__(self): 12 | return "%s\n" % self.buf 13 | 14 | def __parse(self, line): 15 | self.buf = line 16 | tmp = self.buf 17 | tmp = tmp.replace('{', '') 18 | tmp = tmp.replace('}', '') 19 | tmp = tmp.replace(',', '') 20 | segs = tmp.split() 21 | self.opcode_name = segs[0] 22 | self.registers = segs[1:-1] 23 | self.method_desc = segs[-1] 24 | 25 | def reload(self): 26 | self.buf = "%s {%s}, %s" % \ 27 | (self.opcode_name, ", ".join(self.registers), \ 28 | self.method_desc) 29 | 30 | def get_line(self, registers = None): 31 | if not registers: 32 | registers = self.registers 33 | return Insn35c.create_line(self.opcode_name, registers, self.method_desc) 34 | 35 | @staticmethod 36 | def create_line(opcode_name, registers, method_desc): 37 | return "%s {%s}, %s" % (opcode_name, ", ".join(registers), method_desc) 38 | 39 | def replace(self, opcode_name, method_desc): 40 | self.opcode_name = opcode_name 41 | self.method_desc = method_desc 42 | 43 | def set_regs(self, registers): 44 | self.registers = registers 45 | 46 | -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apkil/insn3rc.py: -------------------------------------------------------------------------------- 1 | class Insn3rc(object): 2 | 3 | def __init__(self, line=None, opcode_name='', reg_start='', reg_end='', method_desc=''): 4 | self.buf = "" 5 | self.opcode_name = opcode_name 6 | self.reg_start = reg_start 7 | self.reg_end = reg_end 8 | # self.reg_num = 0 9 | self.method_desc = method_desc 10 | 11 | if line: 12 | self.__parse(line) 13 | else: 14 | self.reload() 15 | 16 | def __repr__(self): 17 | return "%s\n" % self.buf 18 | 19 | def __parse(self, line): 20 | self.buf = line 21 | tmp = self.buf 22 | tmp = tmp.replace('{', '') 23 | tmp = tmp.replace('}', '') 24 | tmp = tmp.replace(',', '') 25 | tmp = tmp.replace("..", '') 26 | segs = tmp.split() 27 | self.opcode_name = segs[0] 28 | self.reg_start = segs[1] 29 | self.reg_end = segs[2] 30 | self.method_desc = segs[-1] 31 | 32 | def reload(self): 33 | self.buf = self.get_line() 34 | 35 | def get_line(self, registers=None): 36 | if not registers: 37 | reg_start = self.reg_start 38 | reg_end = self.reg_end 39 | else: 40 | reg_start = registers[0] 41 | reg_end = registers[1] 42 | 43 | return Insn3rc.create_line(self.opcode_name, reg_start, reg_end, 44 | self.method_desc) 45 | 46 | @staticmethod 47 | def create_line(opcode_name, reg_start, reg_end, method_desc): 48 | return "%s {%s .. %s}, %s" % (opcode_name,reg_start,reg_end, method_desc) 49 | 50 | def replace(self, opcode_name, method_desc): 51 | self.opcode_name = opcode_name 52 | self.method_desc = method_desc 53 | 54 | def set_reg_start(self, register): 55 | self.reg_start = register 56 | 57 | def set_reg_end(self, register): 58 | self.reg_end = register -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apkil/insnnode.py: -------------------------------------------------------------------------------- 1 | from .constants import INSN_FMT 2 | from .insn35c import Insn35c 3 | from .insn3rc import Insn3rc 4 | 5 | class InsnNode(object): 6 | 7 | def __init__(self, line=None): 8 | self.buf = "" 9 | self.opcode_name = "" 10 | self.fmt = "" 11 | self.obj = None 12 | self.cover_code = -1 13 | self.covered = False 14 | 15 | if line: 16 | self.__parse(line) 17 | 18 | def __repr__(self, line_number=""): 19 | return "%s\n" % \ 20 | (self.buf, ) 21 | 22 | def __parse(self, line): 23 | self.buf = line 24 | segs = self.buf.split() 25 | self.opcode_name = segs[0] 26 | if self.opcode_name in INSN_FMT: 27 | self.fmt = INSN_FMT[self.opcode_name] 28 | 29 | if self.fmt == "35c": 30 | self.obj = Insn35c(line) 31 | elif self.fmt == "3rc": 32 | self.obj = Insn3rc(line) 33 | 34 | def reload(self): 35 | if self.obj: 36 | self.obj.reload() 37 | self.buf = self.obj.buf 38 | else: 39 | pass 40 | 41 | def get_line(self, registers = None): 42 | if self.obj: 43 | return self.obj.get_line(registers) 44 | return self.buf 45 | -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apkil/labelnode.py: -------------------------------------------------------------------------------- 1 | 2 | class LabelNode(object): 3 | '''Consists of label line including tries, switches and array data if any.''' 4 | 5 | def __init__(self, line, index, lid): 6 | self.name = "" 7 | self.buf = "" 8 | self.index = -1 # id of the insn which is next to the label 9 | self.lid = None # number of the label in method 10 | self.tries = [] 11 | self.switch = None 12 | self.array_data = None 13 | self.cover_code = -1 14 | self.covered = False 15 | 16 | self.__parse(line, index, lid) 17 | 18 | def __repr__(self): 19 | return "Lable: %s\n" % \ 20 | (self.name, ) 21 | 22 | def __parse(self, line, index, lid): 23 | self.buf = line 24 | self.index = index 25 | self.name = self.buf[1:] 26 | self.lid = lid 27 | 28 | def reload(self): 29 | self.buf = self.get_line() 30 | 31 | def get_line(self): 32 | '''Returns single label line.''' 33 | return ":%s" % self.name 34 | 35 | def get_lines(self): 36 | ''' Returns labels including tries, switches, and arrays.''' 37 | lines = [":%s" % self.name] 38 | # for t in self.tries: 39 | # lines.append(t.buf) 40 | if self.switch: 41 | for sl in self.switch.buf: 42 | lines.append(sl) 43 | if self.array_data: 44 | for sl in self.array_data.buf: 45 | lines.append(sl) 46 | return lines 47 | -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apkil/smalitree.py: -------------------------------------------------------------------------------- 1 | import os 2 | import copy 3 | from .classnode import ClassNode 4 | 5 | class SmaliTree(object): 6 | 7 | def __init__(self, treeId, foldername): 8 | self.foldername = "" 9 | self.classes = [] 10 | self.class_ref_dict = None 11 | self.instrumented = False 12 | self.instrumented_method_number = None # go through method counter among all smali trees 13 | self.Id = treeId # smali dir number starting from 1 14 | 15 | self.__parse(foldername) 16 | 17 | def __repr__(self): 18 | return "Foldername: %s\n%s" % \ 19 | (self.foldername, \ 20 | "".join([repr(class_) for class_ in self.classes])) 21 | 22 | def __parse(self, foldername): 23 | print("parsing {}...".format(foldername)) 24 | self.foldername = foldername 25 | for (path, dirs, files) in os.walk(self.foldername): 26 | for f in files: 27 | name = os.path.join(path, f) 28 | rel_path = os.path.relpath(name, self.foldername) 29 | if rel_path.find("annotation") == 0: 30 | continue 31 | ext = os.path.splitext(name)[1] 32 | if ext != '.smali': continue 33 | folder, fn = os.path.split(rel_path) 34 | self.classes.append(ClassNode(filename=name, folder=folder)) 35 | 36 | def get_class(self, class_name): 37 | result = [c for c in self.classes if c.name == class_name] 38 | if result: 39 | return result[0] 40 | else: 41 | return None 42 | 43 | def add_class(self, class_node): 44 | if [c for c in self.classes if c.name == class_node.name]: 45 | print("Class {} alreasy exsits!".format(class_node.name)) 46 | return False 47 | else: 48 | self.classes.append(copy.deepcopy(class_node)) 49 | return True 50 | 51 | def remove_class(self, class_node): 52 | # self.classes.del() 53 | pass 54 | 55 | def update_class_ref_dict(self): 56 | self.class_ref_dict = {} 57 | for cl in self.classes: 58 | cl.update_meth_ref_dict() 59 | self.class_ref_dict[cl.name] = cl 60 | -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apkil/switchnode.py: -------------------------------------------------------------------------------- 1 | class SwitchNode(object): 2 | 3 | def __init__(self, lines, label): 4 | self.buf = [] 5 | self.type_ = "" 6 | self.packed_value = "" 7 | self.packed_labels = [] 8 | self.sparse_dict = {} 9 | self.label = None 10 | 11 | self.__parse(lines, label) 12 | 13 | def __repr__(self): 14 | return "Switch: %s" % ("".join(self.buf)) 15 | 16 | def __parse(self, lines, label): 17 | self.buf = lines 18 | self.label = label 19 | segs = self.buf[0].split() 20 | self.type_ = segs[0] 21 | if self.type_ == ".packed-switch": 22 | self.packed_value = segs[1] 23 | self.packed_labels = self.buf[1:-1] 24 | elif self.type_ == ".sparse-switch": 25 | for l in self.buf[1:-1]: 26 | v, arr, lbl = l.split() 27 | self.sparse_dict[v] = lbl[1:] 28 | label.switch = self 29 | 30 | def reload(self): 31 | self.buf = [] 32 | if self.type_ == ".packed-switch": 33 | self.buf.append("{} {}".format(self.type_, self.packed_value)) 34 | for l in self.packed_labels: 35 | #l.reload() 36 | self.buf.append(l) 37 | self.buf.append(".end packed-switch") 38 | elif self.type_ == ".sparse-switch": 39 | self.buf.append(".sparse-switch") 40 | for value in self.sparse_dict.keys(): 41 | label = self.sparse_dict[value] 42 | #label.reload() 43 | self.buf.append("{} -> :{}".format(value, label)) 44 | self.buf.append(".end sparse-switch") 45 | -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apkil/trynode.py: -------------------------------------------------------------------------------- 1 | class TryNode(object): 2 | 3 | def __init__(self, line, start, end, handler): 4 | self.buf = "" 5 | self.exception = "" 6 | self.start = None 7 | self.end = None 8 | self.handler = None 9 | 10 | self.__parse(line, start, end, handler) 11 | 12 | def __repr__(self): 13 | return "Try: %s {%s .. %s} %s" % \ 14 | (self.exception, start.index, end.index, handler.index) 15 | 16 | def __parse(self, line, start, end, handler): 17 | self.buf = line 18 | self.start = start 19 | self.end = end 20 | end.tries.append(self) 21 | self.handler = handler 22 | segs = self.buf.split() 23 | self.exception = segs[1] 24 | 25 | def reload(self): 26 | pass -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apkil/typenode.py: -------------------------------------------------------------------------------- 1 | from .constants import BASIC_TYPES, BASIC_TYPES_BY_JAVA 2 | 3 | class TypeNode(object): 4 | 5 | def __init__(self, desc=None): 6 | self.type_ = "" 7 | self.dim = 0 8 | self.basic = None 9 | self.void = None 10 | self.words = 1 11 | 12 | if desc: 13 | self.__parse(desc) 14 | 15 | def __parse(self, desc): 16 | self.dim = desc.rfind('[') + 1 17 | desc = desc[self.dim:] 18 | 19 | if desc[0] in BASIC_TYPES: 20 | self.type_ = desc[0] 21 | self.basic = True 22 | if self.type_ == 'V': 23 | self.void = True 24 | else: 25 | self.void = False 26 | if (self.type_ == 'J' or self.type_ == 'D') and self.dim == 0: 27 | self.words = 2 28 | elif desc[0] == 'L': 29 | self.type_ = desc 30 | self.basic = False 31 | 32 | def __repr__(self): 33 | return self.dim * '[' + self.type_ 34 | 35 | def load_java(self, java): 36 | self.dim = java.count("[]") 37 | java = java.replace("[]", '') 38 | if java in BASIC_TYPES_BY_JAVA: 39 | self.type_ = BASIC_TYPES_BY_JAVA[java] 40 | self.basic = True 41 | if self.type_ == 'V': 42 | self.void = True 43 | else: 44 | self.void = False 45 | if self.type_ == 'J' or self.type_ == 'D': 46 | self.words = 2 47 | else: 48 | self.type_ = 'L' + java.replace('.', '/') + ';' 49 | 50 | def get_desc(self): 51 | return self.dim * '[' + self.type_ 52 | 53 | def get_java(self): 54 | if self.basic: 55 | if self.void: 56 | return "" 57 | else: 58 | return constants.BASIC_TYPES[self.type_] + self.dim * "[]" 59 | else: 60 | return self.type_[1:-1].replace('/', '.') + self.dim * "[]" 61 | -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/apktool.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import shutil 3 | import os 4 | from ..operations import terminal 5 | from ..libs.libs import Libs 6 | 7 | def decode(apk_path, result_dir): 8 | # javaOpts = "-Xms512m -Xmx1024m" 9 | if os.path.exists(result_dir): 10 | shutil.rmtree(result_dir) 11 | cmd = 'java -jar "{}" d "{}" -o "{}"'.format(Libs.APKTOOL_PATH, apk_path, result_dir) 12 | terminal.request_pipe(cmd) 13 | 14 | def decode_no_res(apk_path, result_dir): 15 | if os.path.exists(result_dir): 16 | shutil.rmtree(result_dir) 17 | cmd = f'java -jar "{Libs.APKTOOL_PATH}" d --no-res "{apk_path}" -o "{result_dir}"' 18 | terminal.request_pipe(cmd) 19 | 20 | def unpack(apk_path, result_dir): 21 | cmd = 'java -jar "{}" -r -s d "{}" -o "{}"'.format(Libs.APKTOOL_PATH, apk_path, result_dir) 22 | terminal.request_pipe(cmd) 23 | 24 | def build(dir_path, output_apk): 25 | logging.info("building") 26 | cmd = 'java -jar "{}" --use-aapt2 b "{}" -o "{}"'.format(Libs.APKTOOL_PATH, dir_path, output_apk) 27 | terminal.request_pipe(cmd) 28 | -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/axml_manifest.py: -------------------------------------------------------------------------------- 1 | import pyaxml 2 | from typing import Set 3 | from lxml.etree import Element, SubElement 4 | from androguard.core import axml 5 | from androguard import util 6 | util.set_log("INFO") 7 | 8 | ANDROID_NAMESPACE_URI = "{http://schemas.android.com/apk/res/android}" 9 | 10 | class AxmlBinManifest(object): 11 | 12 | def __init__(self, manifest_path) -> None: 13 | self.manifest_path = manifest_path 14 | self.manifest = self._read_axml() 15 | self.package = self.manifest.get("package") 16 | 17 | def _read_axml(self): 18 | with open(self.manifest_path, "rb") as f: 19 | data = f.read() 20 | ap = axml.AXMLPrinter(data) 21 | return ap.root 22 | 23 | def _get_existing_children(self, child_tag: str) -> Set[str]: 24 | result = set() 25 | for element in self.manifest: 26 | if element.tag != child_tag: 27 | continue 28 | name_attributes = [attr for attr in element.attrib if attr == f"{ANDROID_NAMESPACE_URI}name"] 29 | # only adds permission identifier into the set 30 | if name_attributes: 31 | result.add(element.get(name_attributes[0])) 32 | return result 33 | 34 | @staticmethod 35 | def _add_attribute(element: SubElement, name: str, value: str): 36 | element.set(f"{ANDROID_NAMESPACE_URI}{name}", value) 37 | 38 | def add_instrumentation_tag(self): 39 | instr_element = SubElement(self.manifest, "instrumentation") 40 | self._add_attribute(instr_element, "name", "tool.acv.AcvInstrumentation") 41 | self._add_attribute(instr_element, "targetPackage", self.package) 42 | 43 | def add_broadcast_receiver(self): 44 | application_element = self.manifest.find("application") 45 | receiver = SubElement(application_element, "receiver") 46 | self._add_attribute(receiver, "name", "tool.acv.AcvReceiver") 47 | self._add_attribute(receiver, "enabled", "true") 48 | self._add_attribute(receiver, "exported", "true") 49 | intent_filter = SubElement(receiver, "intent-filter") 50 | actions = ["tool.acv.flush", "tool.acv.snap", "tool.acv.calculate"] 51 | for action in actions: 52 | action_element = SubElement(intent_filter, "action") 53 | self._add_attribute(action_element, "name", action) 54 | 55 | def add_write_permission(self): 56 | write_permission = "android.permission.WRITE_EXTERNAL_STORAGE" 57 | existing_permissions = self._get_existing_children("uses-permission") 58 | if write_permission in existing_permissions: 59 | return 60 | print(f"adding permission {write_permission}") 61 | perm_element = SubElement(self.manifest, "uses-permission") 62 | perm_element.set(f"{ANDROID_NAMESPACE_URI}name", write_permission) 63 | 64 | def save_xml(self): 65 | # Reencode XML to AXML 66 | axml_object = pyaxml.axml.AXML() 67 | axml_object.from_xml(self.manifest) 68 | # Rewrite the file 69 | with open(self.manifest_path, "wb") as f: 70 | f.write(axml_object.pack()) 71 | 72 | -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/baksmali.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from ..operations import terminal 4 | from ..libs.libs import Libs 5 | 6 | def decode(unpacked_apk, dex_filenames, remove_dex=False): 7 | smali_dirpaths = [] 8 | for dex in dex_filenames: 9 | smali_dir = dex.replace(".dex", "") 10 | smali_dirpaths.append(smali_dir) 11 | decode_single_dex(dex, smali_dir) 12 | if remove_dex: 13 | os.remove(dex) 14 | return smali_dirpaths 15 | 16 | def decode_single_dex(dex_path, smali_dir): 17 | os.makedirs(smali_dir, exist_ok=True) 18 | cmd = 'java -jar "{}" disassemble "{}" -o "{}" -l'.format(Libs.BAKSMALI_PATH, dex_path, smali_dir) 19 | terminal.request_pipe(cmd) 20 | return smali_dir 21 | 22 | def build(smali_dirs): 23 | dex_filepaths = [] 24 | for smali_dir_path in smali_dirs: 25 | dex_path = smali_dir_path + ".dex" 26 | if os.path.exists(dex_path): 27 | os.remove(dex_path) 28 | cmd = 'java -jar "{}" assemble "{}" -o "{}"'.format(Libs.SMALI_PATH, smali_dir_path, dex_path) 29 | terminal.request_pipe(cmd) 30 | logging.info("built dex file: {}".format(dex_path)) 31 | dex_filepaths.append(dex_path) 32 | return dex_filepaths -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/config.py: -------------------------------------------------------------------------------- 1 | 2 | SINT16_MAX = 32767 3 | 4 | smalidir_name = "classes" # classes in baksmali convention, smali_classes - apktool 5 | prefix_smdir_name_len = len(smalidir_name) -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/instrumenting/core/__init__.py -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/core/acv_classes.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ..config import SINT16_MAX 3 | 4 | 5 | class AcvCalculator(object): 6 | 7 | @staticmethod 8 | def add_reporter_calls(tree_number, smali_dir, package): 9 | code = AcvCalculator.get_smali_addons(tree_number) 10 | AcvCalculator.save(code, smali_dir, package) 11 | 12 | @staticmethod 13 | def get_smali_addons(tree_number): 14 | codes = [] 15 | for i in range(2, tree_number+1): 16 | codes.append(AcvCalculator.PRINTSUM_ADDON.format(i, i)) 17 | codes.append(AcvCalculator.PRINTSUM_ENDING) 18 | return "\n".join(codes) 19 | 20 | @staticmethod 21 | def save(code, smali_dir, package): 22 | path = os.path.join(smali_dir, "tool", "acv", "AcvCalculator.smali") 23 | with open(path, 'a') as f: 24 | f.write(code) 25 | 26 | PRINTSUM_ADDON = r''' 27 | const-class v0, Ltool/acv/AcvReporter{}; 28 | invoke-virtual {{v0}}, Ljava/lang/Class;->getFields()[Ljava/lang/reflect/Field; 29 | move-result-object v0 30 | const-string v1, "{}" 31 | invoke-static {{v0, v1}}, Ltool/acv/AcvCalculator;->printSum([Ljava/lang/reflect/Field;Ljava/lang/String;)V 32 | ''' 33 | PRINTSUM_ENDING = r''' 34 | # end of additional generation code 35 | return-void 36 | .end method 37 | ''' 38 | 39 | class AcvStoring(object): 40 | 41 | @staticmethod 42 | def add_reporter_calls(tree_number, smali_dir, package): 43 | code = AcvStoring.get_onreceive_smali_addons(tree_number) 44 | AcvStoring.save(code, smali_dir, package) 45 | 46 | @staticmethod 47 | def get_onreceive_smali_addons(tree_number): 48 | codes = [] 49 | for i in range(2, tree_number+1): 50 | codes.append(AcvStoring.ONRECEIVE_SMALI_ADDON.format(i)) 51 | codes.append(AcvStoring.ONRECEIVE_METHOD_ENDING) 52 | return "\n".join(codes) 53 | 54 | @staticmethod 55 | def save(code, smali_dir, package): 56 | path = os.path.join(smali_dir, "tool", "acv", "AcvStoring.smali") 57 | content = "" 58 | with open(path, 'r') as f: 59 | content = f.read().replace("app.debloat.instrapp", package) 60 | with open(path, 'w') as f: 61 | f.write(content) 62 | with open(path, 'a') as f: 63 | f.write(code) 64 | 65 | ONRECEIVE_SMALI_ADDON = r''' 66 | const-class v0, Ltool/acv/AcvReporter{}; 67 | invoke-virtual {{v0}}, Ljava/lang/Class;->getFields()[Ljava/lang/reflect/Field; 68 | move-result-object v0 69 | invoke-direct {{p0, v0}}, Ltool/acv/AcvStoring;->saveExternalPublicFile([Ljava/lang/reflect/Field;)V 70 | ''' 71 | 72 | ONRECEIVE_METHOD_ENDING = r''' 73 | # end of additional generation code 74 | return-void 75 | .end method 76 | ''' 77 | 78 | class AcvFlushing(object): 79 | 80 | @staticmethod 81 | def add_reporter_calls(tree_number, smali_dir, package): 82 | code = AcvFlushing.get_flush_smali_addons(tree_number) 83 | AcvFlushing.save(code, smali_dir, package) 84 | 85 | @staticmethod 86 | def get_flush_smali_addons(tree_number): 87 | codes = [] 88 | for i in range(2, tree_number+1): 89 | codes.append(AcvFlushing.FLUSH_SMALI_ADDON.format(i)) 90 | codes.append(AcvFlushing.FLUSH_METHOD_ENDING) 91 | return "\n".join(codes) 92 | 93 | @staticmethod 94 | def save(code, smali_dir, package): 95 | path = os.path.join(smali_dir, "tool", "acv", "AcvFlushing.smali") 96 | with open(path, 'a') as f: 97 | f.write(code) 98 | 99 | FLUSH_SMALI_ADDON = r''' 100 | const-class v0, Ltool/acv/AcvReporter{}; 101 | invoke-virtual {{v0}}, Ljava/lang/Class;->getFields()[Ljava/lang/reflect/Field; 102 | move-result-object v0 103 | invoke-static {{v0}}, Ltool/acv/AcvFlushing;->flushArrays([Ljava/lang/reflect/Field;)V 104 | ''' 105 | FLUSH_METHOD_ENDING = r''' 106 | # end of additional generation code 107 | return-void 108 | .end method 109 | ''' 110 | 111 | 112 | class AcvInstrumentation(object): 113 | 114 | @staticmethod 115 | def change_package(package, smali_dir): 116 | path = os.path.join(smali_dir, "tool", "acv", "AcvInstrumentation.smali") 117 | content = "" 118 | with open(path, 'r') as f: 119 | content = f.read().replace("app.debloat.instr", package) 120 | with open(path, 'w') as f: 121 | f.write(content) 122 | 123 | 124 | class AcvReporter(object): 125 | ''' Generates AcvReporter.smali classes. 126 | ''' 127 | 128 | def __init__(self, treeId, classes_info): 129 | self.treeId = treeId 130 | self.classes_info = classes_info 131 | self.number_of_fields = len(self.classes_info) 132 | return 133 | 134 | @staticmethod 135 | def get_reporter_field(treeId, class_name, class_number): 136 | field_name = Smali.get_reporting_field_name(class_name, class_number) 137 | return "Ltool/acv/AcvReporter{};->{}".format(treeId, field_name) 138 | 139 | @staticmethod 140 | def get_reporting_class(classes_info, treeId): 141 | fields = [] 142 | init_arrays = [] 143 | for name, length, number in classes_info: 144 | field_name = Smali.get_reporting_field_name(name, number) 145 | fields.append(Smali.get_acv_static_field(name, number)) 146 | init_block = Smali.get_clinit_array(length, treeId, field_name) 147 | init_arrays.append(init_block) 148 | reporter_fields = "\n".join(fields) 149 | init_arrays = "\n".join(init_arrays) 150 | reporter = Smali.get_reporter_smali(treeId, reporter_fields, init_arrays) 151 | # outdated getArray() (we use reflection now) 152 | # array_puts = AcvReporter.get_array_puts(classes_info) 153 | # reporter += Smali.get_array_method_smali(len(classes_info), array_puts) 154 | return reporter 155 | 156 | @staticmethod 157 | def get_array_puts(classes_info, treeId): 158 | arrays = [] 159 | i = 0 160 | for name, length, number in classes_info: 161 | arrays.append(Smali.get_array_put_smali(treeId, i, name, number)) 162 | i+=1 163 | all_arrays = "\n".join(arrays) 164 | return all_arrays 165 | 166 | @staticmethod 167 | def save_file(treeId, dir_path, reporter): 168 | reporter_dir = os.path.join(dir_path, 'tool', 'acv') 169 | reporter_path = os.path.join(reporter_dir, Smali.get_acvreporter_name(treeId)) 170 | if not os.path.isdir(reporter_dir): 171 | os.makedirs(reporter_dir) 172 | with open(reporter_path, 'w') as f_writer: 173 | f_writer.write(reporter) 174 | 175 | # (acv_classes_dir, tree_id, classes_info) 176 | @staticmethod 177 | def save(dir_path, treeId, classes_info): 178 | acv_reporter_code = AcvReporter.get_reporting_class(classes_info, treeId) 179 | AcvReporter.save_file(treeId, dir_path, acv_reporter_code) 180 | 181 | 182 | class Smali(object): 183 | ''' AcvReport specific smali manipulations. 184 | ''' 185 | 186 | ACVREPORTER_FILENAME = "AcvReporter{}.smali" 187 | 188 | @staticmethod 189 | def get_acvreporter_name(treeId): 190 | return Smali.ACVREPORTER_FILENAME.format(treeId) 191 | 192 | @staticmethod 193 | def get_reporting_field_name(class_name, class_number): 194 | return "{}_{}".format(''.join(class_name[:-1].split('/')), class_number) 195 | 196 | @staticmethod 197 | def get_array_put_smali(treeId, ind, class_name, class_number): 198 | field = Smali.get_reporting_field_name(class_name, class_number) 199 | if ind < SINT16_MAX: 200 | return Smali.PUT16_ARRAY_SMALI.format(index=hex(ind), treeId=treeId, field=field) 201 | return Smali.PUT_ARRAY_SMALI.format(index=hex(ind), treeId=treeId, field=field) 202 | 203 | @staticmethod 204 | def get_acv_static_field(class_name, class_number): 205 | field_name = Smali.get_reporting_field_name(class_name, class_number) 206 | return ".field public static {}:[Z".format(field_name) 207 | 208 | @staticmethod 209 | def get_clinit_array(array_len, treeId, field_name): 210 | if array_len < SINT16_MAX: 211 | return Smali.CLINIT16_SMALI.format(hex(array_len), treeId, field_name) 212 | return Smali.CLINIT_SMALI.format(hex(array_len), treeId, field_name) 213 | 214 | @staticmethod 215 | def get_reporter_smali(treeId, fields, field_inits): 216 | return Smali.REPORTER_SMALI.format(treeId=treeId, fields=fields, field_inits=field_inits) 217 | 218 | @staticmethod 219 | def get_array_method_smali(number_of_fields, arrays): 220 | return Smali.GET_ARRAY_SMALI.format(number=hex(number_of_fields), arrays=arrays) 221 | 222 | 223 | #const/4 v2, 0x0 224 | PUT_ARRAY_SMALI = r''' 225 | sget-object v1, Ltool/acv/AcvReporter{treeId};->{field}:[Z 226 | const v2, {index} 227 | aput-object v1, v0, v2 228 | ''' 229 | 230 | PUT16_ARRAY_SMALI = r''' 231 | sget-object v1, Ltool/acv/AcvReporter{treeId};->{field}:[Z 232 | const/16 v2, {index} 233 | aput-object v1, v0, v2 234 | ''' 235 | 236 | CLINIT_SMALI = r''' 237 | const v0, {} 238 | new-array v0, v0, [Z 239 | sput-object v0, Ltool/acv/AcvReporter{};->{}:[Z 240 | ''' 241 | CLINIT16_SMALI = r''' 242 | const/16 v0, {} 243 | new-array v0, v0, [Z 244 | sput-object v0, Ltool/acv/AcvReporter{};->{}:[Z 245 | ''' 246 | 247 | REPORTER_SMALI = r'''.class public Ltool/acv/AcvReporter{treeId}; 248 | .super Ljava/lang/Object; 249 | .source "AcvReporter{treeId}.java" 250 | # static fields 251 | {fields} 252 | 253 | # direct methods 254 | .method static constructor ()V 255 | .locals 1 256 | {field_inits} 257 | return-void 258 | .end method 259 | 260 | .method private constructor ()V 261 | .locals 1 262 | 263 | invoke-direct {{p0}}, Ljava/lang/Object;->()V 264 | 265 | return-void 266 | .end method 267 | 268 | ''' 269 | 270 | GET_ARRAY_SMALI = r''' 271 | 272 | .method public static getArray()[[Z 273 | .locals 3 274 | 275 | # array of arrays declaration 276 | const/16 v0, {number} 277 | 278 | new-array v0, v0, [[Z 279 | 280 | # start of putting arrays into the total array 281 | {arrays} 282 | # end of putting arrays into the total array 283 | 284 | return-object v0 285 | .end method 286 | 287 | ''' -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/core/class_instrumenter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .method_instrumenter import SmaliHelper 4 | 5 | class ClassInstrumenter: 6 | def __init__(self, methodInstr, target_sm_mtd=None): 7 | self.methodInstr = methodInstr 8 | self.target_sm_mtd = target_sm_mtd 9 | 10 | 11 | @staticmethod 12 | def get_dbg_instrument(instrument, method_number, dbg_start, dbg_end): 13 | return (instrument and dbg_start is None) \ 14 | or (instrument and dbg_start is not None and method_number >= dbg_start and method_number <= dbg_end) 15 | 16 | 17 | def instrument_class(self, treeId, smali_class, class_number, method_number=0, instrument=True, dbg_start=None, dbg_end=None): 18 | # method_number is app through counter 19 | class_lines = [] 20 | cover_index = 0 21 | entry_lines = [] 22 | entry_lines.extend(smali_class.get_class_description()) 23 | entry_lines.extend(smali_class.get_annotations()) 24 | entry_lines.extend(smali_class.get_fields()) 25 | 26 | method_lines = [] 27 | is_instrumented = False 28 | for meth in smali_class.methods: 29 | # if meth.descriptor != "i(Landroid/content/Context;Ljava/util/concurrent/Executor;Landroidx/profileinstaller/d$c;Z)V": 30 | # continue 31 | to_instrument = instrument and not meth.ignore and meth.registers < 253 and (SmaliHelper.len_paras(meth.paras)+meth.registers)<253 and (self.target_sm_mtd is None or self.target_sm_mtd == meth.descriptor) 32 | if instrument and not meth.ignore and SmaliHelper.len_paras(meth.paras) + meth.registers > 252: 33 | logging.info(f"Skipping method: {smali_class.name}->{meth.name}... Too many registers occupied: {meth.registers} registers + {SmaliHelper.len_paras(meth.paras)} parameter regs > 252") 34 | dbg_instrument = ClassInstrumenter.get_dbg_instrument(to_instrument, method_number, dbg_start, dbg_end) 35 | method_lines, cover_index, method_number, m_instrumented = self.methodInstr.instrument_method( 36 | treeId, 37 | meth, 38 | cover_index, smali_class.name, 39 | class_number, 40 | method_number, 41 | dbg_instrument) 42 | is_instrumented |= m_instrumented 43 | class_lines.extend(method_lines) 44 | class_lines[0:0] = entry_lines 45 | return ('\n'.join(class_lines), cover_index, method_number, is_instrumented) -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/manifest_instrumenter.py: -------------------------------------------------------------------------------- 1 | from .axml_manifest import AxmlBinManifest 2 | from .android_manifest import XMLManifest 3 | 4 | def instrument_manifest(manifest_path): 5 | manifest = None 6 | if is_xml(manifest_path): 7 | manifest = XMLManifest(manifest_path) 8 | else: 9 | manifest = AxmlBinManifest(manifest_path) 10 | manifest.add_instrumentation_tag() 11 | manifest.add_broadcast_receiver() 12 | manifest.add_write_permission() 13 | manifest.save_xml() 14 | 15 | def is_xml(file_path): 16 | with open(file_path, 'rb') as file: 17 | first_byte = file.read(1) 18 | return first_byte == b'<' 19 | 20 | if __name__ == "__main__": 21 | # $ acvtool/ python3 -m smiler.instrumenting.manifest_instrumenter path_to/AndroidManifest.xml 22 | import sys 23 | manifest_path = sys.argv[1] 24 | instrument_manifest(manifest_path) -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/smali_instrumenter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import logging 5 | 6 | from .apkil import constants 7 | from pkg_resources import resource_filename 8 | from ..operations import binaries 9 | from .utils import Utils 10 | from .core.acv_classes import AcvReporter 11 | from ..granularity import Granularity 12 | 13 | from .config import prefix_smdir_name_len 14 | from .config import smalidir_name 15 | from .core.method_instrumenter import MethodInstrumenter 16 | from .core.class_instrumenter import ClassInstrumenter 17 | 18 | 19 | class Instrumenter(object): 20 | ''' Instrumenter consists of instrumenting code to track smali instructions in 21 | smalitree.''' 22 | 23 | 24 | dir_path = sys.path[0] 25 | instrumentation_smali_path = resource_filename('acvtool.smiler.resources.instrumentation', 'smali') 26 | 27 | def __init__(self, smalitree, granularity, package, dbg_start=None, dbg_end=None, mem_stats=None, target_cl=None, target_mtd=None): 28 | self.smalitree = smalitree 29 | self.granularity = Granularity.GRANULARITIES[granularity] 30 | self.insns = [] 31 | self.class_traces = [] 32 | self.package = package 33 | self.dbg = dbg_start is not None 34 | self.dbg_start = dbg_start 35 | self.dbg_end = dbg_end 36 | self.mem_stats = mem_stats 37 | self.target_sm_cl = target_cl #Instrumenter.extract_target_sm_class(target_cl) if target_cl is not None else None 38 | self.target_sm_mtd = target_mtd #Instrumenter.extract_target_sm_mtd(target_cl, target_mtd) if target_mtd is not None and target_cl is not None else None 39 | self.class_info_list = [] 40 | self.methodInstr = MethodInstrumenter(self.granularity) 41 | self.classInstr = ClassInstrumenter(self.methodInstr, self.target_sm_mtd) 42 | 43 | 44 | @staticmethod 45 | def extract_target_sm_class(target_cl): 46 | '''Converts java-ish class name to smali representation.''' 47 | "android.support.multidex.a -> Landroid/support/multidex/a;" 48 | return "L{};".format(target_cl.replace(".", "/")) 49 | 50 | @staticmethod 51 | def extract_target_sm_mtd(target_cl, target_mtd): 52 | '''Converts java-ish method name to smali representation.''' 53 | "void android.support.multidex.a.g(android.content.Context) -> g(Landroid/content/Context;)V" 54 | ret_type, method_name = target_mtd.split() 55 | if ret_type in constants.BASIC_TYPES_BY_JAVA: 56 | ret_type = constants.BASIC_TYPES_BY_JAVA[ret_type] 57 | else: 58 | ret_type = "L" + ret_type.replace(".", "/") 59 | method_name = method_name[len(target_cl)+1:-1].replace("(", "(L", 1).replace(".", "/") + ";)" + ret_type 60 | return method_name 61 | 62 | 63 | def save_instrumented_smalitrees(self): 64 | '''- Inserts tracking probes into smali code trees. 65 | - Updates class_info_list for future covered code mapping. 66 | - Adds AcvReporter classes.''' 67 | method_number = 0 68 | #last_class_info = None 69 | classes_info = self.save_instrumented_smalitree_by_class(self.smalitree, method_number, instrument=True) 70 | return classes_info 71 | 72 | 73 | def save_instrumented_smalitree_by_class(self, tree, method_number=0, instrument=True): 74 | '''Saves instrumented smali to the specified directory/''' 75 | output_dir = tree.foldername 76 | logging.info("saving instrumented smali: {}".format(output_dir)) 77 | Utils.recreate_dir(output_dir) 78 | classes_info = [] 79 | class_number = 0 # to make array name unique 80 | # Helps to find specific method that cased a fail after the instrumentation. 81 | # See '# Debug purposes' below 82 | #method_number = method_number 83 | # dbg_ means specific part of the code defined by dbg_start-dbg_end 84 | # numbers will be instrumented 85 | dbg_instrument = instrument 86 | # if last_class_info: 87 | # cl_name, cover_index, class_number= last_class_info 88 | # class_number += 1 89 | #temp_class = self.smalitree.classes[4] 90 | #print(temp_class.methods[2].insns[0].cover_code) 91 | for class_ in tree.classes: 92 | code, cover_index, method_number, is_instrumented = self.classInstr.instrument_class( 93 | tree.Id, 94 | class_, 95 | class_number, 96 | method_number=method_number, 97 | instrument=dbg_instrument and not class_.ignore and (self.target_sm_cl is None or self.target_sm_cl == class_.name), 98 | dbg_start=self.dbg_start, 99 | dbg_end=self.dbg_end) 100 | tree.instrumented_method_number = method_number 101 | if dbg_instrument and class_.is_coverable(): # is_instrumented 102 | classes_info.append((class_.name, cover_index, class_number)) 103 | class_number += 1 104 | class_path = os.path.join(output_dir, class_.folder, class_.file_name) 105 | Instrumenter.save_class(class_path, code) 106 | if self.dbg and dbg_instrument and method_number > self.dbg_end: # Now leave other code not instrumented. 107 | dbg_instrument = False 108 | if self.dbg: 109 | print("Number of methods instrumented: {0}-{1} from {2}".format(self.dbg_start, self.dbg_end, method_number)) 110 | return classes_info 111 | 112 | 113 | def save_reporter_array_stats(self, classes_info, verbose=False): 114 | log_path = os.path.join("allocation_log.csv") 115 | csv_text = "" 116 | if self.mem_stats == "verbose": 117 | entries = ["{},{},{}".format(self.package, cl[0], cl[1]) for cl in classes_info] 118 | csv_text = "\n".join(entries) 119 | else: 120 | memory = sum(cl[1] for cl in classes_info) 121 | logging.info("{} bytes allocated in AcvReporter.smali".format(memory)) 122 | csv_text = "{},{}".format(self.package, memory) 123 | Utils.log_entry(log_path, csv_text+'\n') 124 | 125 | 126 | @staticmethod 127 | def save_class(output_path, smali_code): 128 | '''Save instrumented Smali class file.''' 129 | file_dir = os.path.dirname(output_path) 130 | if not os.path.exists(file_dir): 131 | os.makedirs(file_dir) 132 | 133 | with open(output_path, 'w') as f: 134 | f.write(smali_code) 135 | 136 | def save_pickle(self, pickle_dir): 137 | '''Saves source Smali code and links to the tracked instructions.''' 138 | if not os.path.exists(pickle_dir): 139 | os.makedirs(pickle_dir) 140 | pth = os.path.join(pickle_dir, "{}_{}.pickle".format(self.package, self.smalitree.Id)) 141 | binaries.save_pickle(self.smalitree, pth) 142 | -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import logging 4 | from time import time 5 | from datetime import datetime 6 | from . import config 7 | 8 | class Utils(object): 9 | ''' Static Helpers.''' 10 | 11 | @staticmethod 12 | def rm_tree(path): 13 | '''Removes all the files.''' 14 | if os.path.isdir(path): 15 | # if os.name == 'nt': 16 | # #Hack for Windows. Shutil can't remove files with a path longer than 260. 17 | # cmd = "rd '{0}' /s /q".format(path) 18 | # os.system(cmd) 19 | # else: 20 | shutil.rmtree(path) 21 | 22 | @staticmethod 23 | def rm_if_exists(path): 24 | if os.path.exists(path): 25 | logging.info("delete: " + path) 26 | if os.path.isdir(path): 27 | shutil.rmtree(path) 28 | else: 29 | os.remove(path) 30 | 31 | @staticmethod 32 | def recreate_dir(path): 33 | Utils.rm_tree(path) 34 | if os.path.isdir(path): 35 | raise Exception("shutil.rmtree didn't manage to remove dir in time: {}".format(path)) 36 | os.makedirs(path) 37 | 38 | @staticmethod 39 | def mkdirs(path): 40 | if not os.path.exists(path): 41 | os.makedirs(path) 42 | 43 | @staticmethod 44 | def get_groupped_classes(tree): 45 | # Group classes by relative path to generate index.html. 46 | groups = [] 47 | classes = [] 48 | if tree.classes: 49 | key = tree.classes[0].folder 50 | for i, s in enumerate(tree.classes): 51 | if tree.classes[i].folder == key: 52 | classes.append(tree.classes[i]) 53 | else: 54 | groups.append(classes) 55 | key = tree.classes[i].folder 56 | classes = [tree.classes[i]] 57 | groups.append(classes) 58 | return groups 59 | 60 | @staticmethod 61 | def get_package_name(smali_class_name): 62 | '''Returns the package name by given smali class name. 63 | Format: org/android/rock.''' 64 | package_name, f = os.path.split(smali_class_name) 65 | return package_name[1:] 66 | 67 | @staticmethod 68 | def get_standart_package_name(smali_class_name): 69 | '''Returns the package name by given smali class name. 70 | Format: org.android.rock.''' 71 | return Utils.get_package_name(smali_class_name).replace('/', '.') 72 | 73 | @staticmethod 74 | def is_in_ranges(i, ranges): 75 | '''Returns if i is in the intervals ranges. 76 | Ranges variable contains list of indexes intervals.''' 77 | for r in ranges: 78 | if i >= r[0]: 79 | if i < r[1]: 80 | return True 81 | return False 82 | return False 83 | 84 | @staticmethod 85 | def scan_synchronized_tries(method): 86 | '''Returns list of intervals of indexes where the insnsmust be throw safe. 87 | Otherwise, VerifyChecker recognizes the code as invalid.''' 88 | ranges = [] 89 | for tr in method.tries: 90 | if tr.handler.name.startswith("catchall"): 91 | start = tr.start.index 92 | end = tr.end.index 93 | if tr.handler.index < tr.end.index and tr.handler.index >= tr.start.index: 94 | end = tr.handler.index 95 | for i in range(start, end): 96 | if method.insns[i].opcode_name.startswith("monitor-exit"): 97 | ranges.append([i, end]) 98 | break 99 | return ranges 100 | 101 | @staticmethod 102 | def copytree(src, dst, symlinks=False, ignore=None): 103 | if not os.path.exists(dst): 104 | os.makedirs(dst) 105 | for item in os.listdir(src): 106 | s = os.path.join(src, item) 107 | d = os.path.join(dst, item) 108 | if os.path.isdir(s): 109 | Utils.copytree(s, d, symlinks, ignore) 110 | else: 111 | if not os.path.exists(d) or os.stat(s).st_mtime - os.stat(d).st_mtime > 1: 112 | shutil.copy2(s, d) 113 | 114 | @staticmethod 115 | def log_entry(log_path, entry, sep=','): 116 | if not os.path.exists(log_path): 117 | with open(log_path, 'w') as log_file: 118 | log_file.write("sep={}\n".format(sep)) 119 | log_file.flush() 120 | with open(log_path, 'a+') as log_file: 121 | log_file.write(entry) 122 | log_file.flush() 123 | 124 | 125 | @staticmethod 126 | def get_smali_dirs(unpacked_apk): 127 | i = 1 128 | smali_dirs = [] 129 | path = os.path.join(unpacked_apk, config.smalidir_name) 130 | while os.path.exists(path): 131 | smali_dirs.append(path) 132 | i += 1 133 | path = os.path.join(unpacked_apk, config.smalidir_name + str(i)) 134 | return smali_dirs 135 | 136 | 137 | def timeit(method): 138 | '''Measures the working time of the method.''' 139 | def wrapper(*args, **kwargs): 140 | start = time() 141 | result = method(*args, **kwargs) 142 | end = time() 143 | time_log_path = os.path.join("times_log.csv") 144 | args_str = ";".join(map(str,args)) 145 | entry = "{0};{1};{2};{3}\n".format(datetime.now(), end - start, method.__name__.lower(), args_str) 146 | Utils.log_entry(time_log_path, entry, sep=";") 147 | return result 148 | return wrapper 149 | -------------------------------------------------------------------------------- /acvtool/smiler/instrumenting/zipper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import zipfile 4 | 5 | class ZipReader (object): 6 | 7 | def __init__(self, zip_path, unpacked_dir): 8 | self.zip_path = zip_path 9 | self.unpacked_dir = unpacked_dir 10 | self.names = self._readfiles() 11 | self.dex_filenames = [name for name in self.names if name.endswith('.dex')] 12 | self.is_multidex = len(self.dex_filenames) > 1 13 | self.max_dex_number = self._get_max_dex_number() 14 | self.acv_classes_dir_name = "classes{}".format(self.max_dex_number + 1) 15 | self.acv_classes_dir = os.path.join(self.unpacked_dir, self.acv_classes_dir_name) 16 | self.acv_extra_dirs = [] 17 | self.extra_dirs = [] 18 | 19 | 20 | def _get_max_dex_number(self): 21 | match = re.compile(r'classes(\d+).dex').match 22 | dex_numbers = [] 23 | for dex_name in self.dex_filenames: 24 | res = match(dex_name) 25 | if res: 26 | dex_numbers.append(int(res.group(1))) 27 | else: 28 | dex_numbers.append(1) 29 | return max(dex_numbers) 30 | 31 | def _generate_next_classes_dir(self): # 1-indexed: prime acv_classes_dir 32 | next_dex_number = self.max_dex_number + 1 + len(self.acv_extra_dirs) + len(self.extra_dirs) + 1 33 | next_classes_dir_name = "classes{}".format(next_dex_number) 34 | next_classes_dir = os.path.join(self.unpacked_dir, next_classes_dir_name) 35 | return next_classes_dir 36 | 37 | def add_next_acv_classes_dir(self): 38 | next_classes_dir = self._generate_next_classes_dir() 39 | self.acv_extra_dirs.append(next_classes_dir) 40 | return next_classes_dir 41 | 42 | def add_next_classes_dir(self): 43 | next_classes_dir = self._generate_next_classes_dir() 44 | self.extra_dirs.append(next_classes_dir) 45 | return next_classes_dir 46 | 47 | def _readfiles(self): 48 | with zipfile.ZipFile(self.zip_path, 'r') as zip_ref: 49 | return zip_ref.namelist() 50 | 51 | def extract(self, output_dir, tgt_dexes=[]): 52 | extracted_files = [] 53 | dex_names_to_extract = tgt_dexes if tgt_dexes else self.dex_filenames 54 | with zipfile.ZipFile(self.zip_path, 'r') as zip_ref: 55 | for tgt_dex in dex_names_to_extract: 56 | extracted_file_path = os.path.join(output_dir, tgt_dex) 57 | zip_ref.extract(tgt_dex, output_dir) 58 | extracted_files.append(extracted_file_path) 59 | return extracted_files 60 | 61 | def get_acv_classes_dirs(self): 62 | return [self.acv_classes_dir] + self.acv_extra_dirs 63 | 64 | def get_extra_classes_dirs(self): 65 | return self.extra_dirs -------------------------------------------------------------------------------- /acvtool/smiler/keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/keystore -------------------------------------------------------------------------------- /acvtool/smiler/libs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/libs/__init__.py -------------------------------------------------------------------------------- /acvtool/smiler/libs/jars/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/libs/jars/__init__.py -------------------------------------------------------------------------------- /acvtool/smiler/libs/jars/apktool_2.9.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/libs/jars/apktool_2.9.3.jar -------------------------------------------------------------------------------- /acvtool/smiler/libs/jars/baksmali-3.0.7-1c13925b-dirty-fat.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/libs/jars/baksmali-3.0.7-1c13925b-dirty-fat.jar -------------------------------------------------------------------------------- /acvtool/smiler/libs/jars/smali-3.0.7-1c13925b-dirty-fat.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/libs/jars/smali-3.0.7-1c13925b-dirty-fat.jar -------------------------------------------------------------------------------- /acvtool/smiler/libs/libs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from pkg_resources import resource_filename 5 | 6 | class Libs: 7 | APKTOOL_PATH = resource_filename("acvtool.smiler.libs.jars", "apktool_2.9.3.jar") 8 | SMALI_PATH = resource_filename("acvtool.smiler.libs.jars", "smali-3.0.7-1c13925b-dirty-fat.jar") 9 | BAKSMALI_PATH = resource_filename("acvtool.smiler.libs.jars", "baksmali-3.0.7-1c13925b-dirty-fat.jar") 10 | -------------------------------------------------------------------------------- /acvtool/smiler/operations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/operations/__init__.py -------------------------------------------------------------------------------- /acvtool/smiler/operations/adb.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import time 4 | from . import terminal 5 | 6 | def install_multiple(apks): 7 | cmd = "adb install-multiple -r --no-incremental {}".format(" ".join(apks)) 8 | out = terminal.request_pipe(cmd) 9 | return out 10 | 11 | def input_text(text, sleep=0): 12 | os.system("adb shell input text {}".format(text)) 13 | if sleep > 0: 14 | time.sleep(sleep) 15 | 16 | 17 | def tap(x, y, sleep=0): 18 | os.system("adb shell input tap {} {}".format(x, y)) 19 | if sleep > 0: 20 | time.sleep(sleep) 21 | 22 | 23 | def clear_app_data(package): 24 | cmd = "adb shell pm clear {}".format(package) 25 | terminal.request_pipe(cmd) 26 | 27 | 28 | def send_broadcast(action, package): 29 | #adb shell am broadcast -a 'tool.acv.snap' -n /tool.acv.AcvReceiver 30 | #adb shell am broadcast -a 'tool.acv.snap' -p 31 | cmd = "adb shell am broadcast -a '{}' -p '{}'".format(action, package) 32 | terminal.request_pipe(cmd) 33 | 34 | 35 | def save_coverage(package): 36 | send_broadcast("tool.acv.snap", package) 37 | 38 | 39 | def delete_app_sdcard_dir(package): 40 | cmd = "adb shell rm -rf '/sdcard/Download/{}'".format(package) 41 | terminal.request_pipe(cmd) 42 | 43 | 44 | def create_app_sdcard_dir(package): 45 | cmd = "adb shell mkdir '/sdcard/Download/{}'".format(package) 46 | terminal.request_pipe(cmd) 47 | 48 | 49 | def sd_dir_exists(package): 50 | cmd = "adb shell ls '/sdcard/Download/{}'".format(package) 51 | try: 52 | terminal.request_pipe(cmd) 53 | return True 54 | except Exception as ex: 55 | return False 56 | 57 | 58 | def launch_app(package): 59 | cmd = "adb shell monkey -p '{}' 1".format(package) 60 | terminal.request_pipe(cmd) 61 | 62 | -------------------------------------------------------------------------------- /acvtool/smiler/operations/binaries.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import javaobj 4 | import pickle 5 | 6 | 7 | def read_ec(ec_path): 8 | pobj = '' 9 | with open(ec_path, mode='rb') as f: 10 | marshaller = javaobj.JavaObjectUnmarshaller(f) 11 | pobj = marshaller.readObject() 12 | return pobj 13 | 14 | 15 | def read_multiple_ec_per_tree(ec_pths): 16 | total_bin_coverage = [] 17 | for p in ec_pths: 18 | single_bin_coverage = read_ec(p) 19 | #logging.info(p) 20 | total_bin_coverage = sum_ec_bin_arrays(total_bin_coverage, single_bin_coverage) 21 | #coverage.calculate(total_bin_coverage) 22 | return total_bin_coverage 23 | 24 | 25 | 26 | def sum_ec_bin_arrays(total_bin_coverage, single_bin_coverage): 27 | if not total_bin_coverage: 28 | return single_bin_coverage 29 | for i in range(len(total_bin_coverage)): 30 | for j in range(len(total_bin_coverage[i])): 31 | total_bin_coverage[i][j] |= single_bin_coverage[i][j] 32 | return total_bin_coverage 33 | 34 | 35 | def save_pickle(smalitree, path): 36 | if os.path.exists(path): 37 | os.remove(path) 38 | dir_path = os.path.dirname(path) 39 | if not os.path.isdir(dir_path): 40 | os.makedirs(dir_path) 41 | if not os.path.exists(path): 42 | #clean_smalitree_buf(smalitree) 43 | with open(path, 'wb') as f: 44 | pickle.dump(smalitree, f, pickle.HIGHEST_PROTOCOL) 45 | logging.info('pickle file saved: {0}'.format(path)) 46 | 47 | 48 | def clean_smalitree_buf(smalitree): 49 | for cl in smalitree.classes: 50 | cl.buf = [] 51 | for m in cl.methods: 52 | m.buf = [] 53 | 54 | 55 | def load_smalitree(pickle_path): 56 | logging.info("deserialise: {}".format(pickle_path)) 57 | with open(pickle_path, 'rb') as f: 58 | st = pickle.load(f) 59 | return st 60 | 61 | 62 | def read_file(path): 63 | with open(path, 'r') as file: 64 | data = file.read() 65 | return data 66 | 67 | def save_list(path, entities_list): 68 | str_list = "\n".join(entities_list) 69 | with open(path, 'w') as f: 70 | f.write(str_list) 71 | -------------------------------------------------------------------------------- /acvtool/smiler/operations/coverage.py: -------------------------------------------------------------------------------- 1 | ''' 2 | `Cover`, `calculate`, `log` operations on SmaliTree. 3 | 4 | Input files: .pickle & .ec 5 | ''' 6 | 7 | import logging 8 | import os 9 | from ..entities.coverage import CoverageData 10 | from . import coverage 11 | from . import binaries 12 | 13 | 14 | def cover_pickles(wd): 15 | ecs = wd.get_ecs() 16 | if not ecs: 17 | logging.info("No coverage files found") 18 | return 19 | pkls = wd.get_pickles() 20 | covered_pkls = wd.get_covered_pickles() if os.path.exists(wd.covered_pickle_dir) else None 21 | total_cov_diff = CoverageData() 22 | for dex in ecs.keys(): 23 | pkl_path = covered_pkls[dex] if covered_pkls else pkls[dex] 24 | smalitree = binaries.load_smalitree(pkl_path) 25 | st_cov = get_coverage_data(smalitree) 26 | bin_coverage = binaries.read_multiple_ec_per_tree(ecs[dex]) 27 | prob_cov = calculate(bin_coverage) 28 | logging.info("ec files: {}({}), probes: {} out of {} total; {}% coverage".format(dex, len(ecs[dex]), prob_cov.covered, prob_cov.total, prob_cov.coverage())) 29 | log_coverage("", dex, st_cov.lines_covered, st_cov.lines, st_cov.get_line_coverage()) 30 | coverage.cover_tree(smalitree, bin_coverage) 31 | new_st_cov = get_coverage_data(smalitree) 32 | log_coverage("new", dex, new_st_cov.lines_covered, new_st_cov.lines, new_st_cov.get_line_coverage()) 33 | cov_diff = CoverageData.log_coverage_difference(dex, st_cov, new_st_cov) 34 | total_cov_diff.add_data(cov_diff, st_cov.lines, st_cov.methods, st_cov.classes) 35 | covered_pkl_pth = os.path.join(wd.covered_pickle_dir, os.path.basename(pkl_path)) 36 | if cov_diff.lines_covered > 0 or not os.path.exists(covered_pkl_pth): 37 | binaries.save_pickle(smalitree, covered_pkl_pth) 38 | CoverageData.log_diff(total_cov_diff) 39 | 40 | 41 | def log_coverage(prefix, i, lines_covered, lines, coverage): 42 | logging.info("{}\tst {}: {} out of {}, {}%".format( 43 | prefix, i, lines_covered, lines, 100*coverage) 44 | ) 45 | 46 | 47 | def get_coverage_data(smalitree): 48 | total_cd = CoverageData() 49 | for cl in smalitree.classes: 50 | coverage_data = CoverageData( 51 | lines=cl.coverable(), 52 | lines_missed=cl.not_covered(), 53 | lines_covered=cl.covered(), 54 | methods_covered=cl.mtds_covered(), 55 | methods_missed=cl.mtds_not_covered(), 56 | methods=cl.mtds_coverable() 57 | ) 58 | coverage_data.update_coverage_for_single_class_from_methods() 59 | total_cd.add_data(coverage_data) 60 | return total_cd 61 | 62 | def cover_tree(st, ec_coverage): 63 | cov_iterator = enumerate(ec_coverage) 64 | for cl in st.classes: 65 | if cl.is_coverable(): 66 | cov_class = next(cov_iterator)[1] 67 | for m in cl.methods: 68 | # print not executed methods 69 | # if m.cover_code > -1 and not m.called and cov_class[m.cover_code]: 70 | # print("{}->{}".format(cl.name, m.descriptor)) 71 | m.called = m.cover_code > -1 and (m.called or cov_class[m.cover_code]) 72 | for ins in m.insns: 73 | ins.covered = ins.cover_code > -1 and (ins.covered or cov_class[ins.cover_code]) 74 | for lbl in m.labels.values(): 75 | lbl.covered = lbl.cover_code > -1 and (lbl.covered or cov_class[lbl.cover_code]) 76 | 77 | 78 | def nullify_smalitree_coverage(st): 79 | for cl in st.classes: 80 | for m in cl.methods: 81 | m.called = False 82 | for ins in m.insns: 83 | ins.covered = False 84 | for lbl in m.labels.values(): 85 | lbl.covered = False 86 | 87 | 88 | def calculate(bin_coverage): 89 | covered_insns = 0 90 | total_insns = 0 91 | for cl in bin_coverage: 92 | class_covered_insns = sum(cl) 93 | class_insns = len(cl) 94 | covered_insns += class_covered_insns 95 | total_insns += class_insns 96 | return ProbesCoverage(covered_insns, total_insns) 97 | 98 | 99 | class ProbesCoverage(object): 100 | def __init__(self, covered=0, total=0): 101 | self.covered = covered 102 | self.total = total 103 | 104 | def coverage(self): 105 | return 100*float(self.covered)/self.total 106 | -------------------------------------------------------------------------------- /acvtool/smiler/operations/terminal.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | def request_pipe(cmd): 4 | pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 5 | out, err = pipe.communicate() 6 | 7 | res = out 8 | if not out: 9 | res = err 10 | 11 | if pipe.returncode > 0: 12 | raise Exception("----------------------------------------------------\n\ 13 | Out: {}\nError: {}".format(out.decode(), err.decode())) 14 | 15 | return res.decode() 16 | -------------------------------------------------------------------------------- /acvtool/smiler/reporting/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/reporting/__init__.py -------------------------------------------------------------------------------- /acvtool/smiler/reporting/reporter.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .. import smiler 4 | import logging 5 | from ..operations import binaries 6 | from ..granularity import Granularity 7 | from ..serialisation.html_serialiser import HtmlSerialiser 8 | from ..serialisation.xml_serialiser import XmlSerialiser 9 | from ...cutter import shrinker 10 | 11 | 12 | class Reporter(object): 13 | 14 | def __init__(self, package, pickle_files, images_dir, report_dir): 15 | self.package = package 16 | self.pickle_files = pickle_files # covered pickle files 17 | self.images_dir = images_dir 18 | self.report_dir = report_dir 19 | 20 | 21 | def generate(self, html=True, xml=True, granularity="instruction", ignore_filter=None, shrink=False): 22 | report_dir = self.report_dir 23 | if not os.path.exists(report_dir): 24 | os.makedirs(report_dir) 25 | logging.info("report generating...") 26 | self.save_reports(self.pickle_files, xml, html, granularity, ignore_filter, to_shrink=shrink) 27 | logging.info("reports saved to: {0}".format(report_dir)) 28 | 29 | 30 | def save_reports(self,pickle_files, xml, html, granularity, ignore_filter, to_shrink): 31 | dex_coverages = {} 32 | granularity = Granularity.GRANULARITIES[granularity] 33 | if html: 34 | htmlSerialiser = HtmlSerialiser(self.package, granularity, self.report_dir) 35 | min_pickle = min(pickle_files.keys()) 36 | max_pickle = max(pickle_files.keys()) 37 | for treeId in range(min_pickle, max_pickle+1): 38 | if treeId not in pickle_files: 39 | continue 40 | smalitree = binaries.load_smalitree(pickle_files[treeId]) 41 | if to_shrink: 42 | shrinker.shrink_smalitree(smalitree) 43 | if ignore_filter: 44 | smiler.apply_ignore_filter(smalitree, ignore_filter) 45 | if html: 46 | dex_coverage_data = htmlSerialiser.save_html(smalitree) 47 | dex_coverages[treeId] = dex_coverage_data 48 | if xml: 49 | logging.info("saving xml...") 50 | self.save_xml_report(treeId, smalitree, granularity) 51 | if dex_coverages: 52 | htmlSerialiser.save_dex_report(dex_coverages) 53 | 54 | 55 | def generate_xml(self, smalitree, app_name, granularity): 56 | serialiser = XmlSerialiser(smalitree, app_name, granularity) 57 | xml = serialiser.get_xml() 58 | return xml 59 | 60 | 61 | def save_xml_report(self, treeId, smalitree, granularity): 62 | xml = self.generate_xml(smalitree, self.package, granularity) 63 | path = os.path.join(self.report_dir, 'report_{}.xml'.format(treeId)) 64 | with open(path, 'w') as f: 65 | f.write(xml) 66 | -------------------------------------------------------------------------------- /acvtool/smiler/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/resources/__init__.py -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/.resources/NOTICE.txt: -------------------------------------------------------------------------------- 1 | All icons were downloaded from https://icons8.com under CC BY 4.0 License. -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/.resources/android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/resources/html/.resources/android.png -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/.resources/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/resources/html/.resources/box.png -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/.resources/document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/resources/html/.resources/document.png -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/.resources/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/resources/html/.resources/file.png -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/.resources/greenbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/resources/html/.resources/greenbar.png -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/.resources/highlight/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006, Ivan Sagalaev 2 | All rights reserved. 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of highlight.js nor the names of its contributors 12 | may be used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/.resources/highlight/highlight.pack.js: -------------------------------------------------------------------------------- 1 | /*! highlight.js v9.11.0 | BSD3 License | git.io/hljslicense */ 2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/&/g,"&").replace(//g,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return w(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||w(i))return i}function o(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){s+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var l=0,s="",f=[];e.length||r.length;){var g=i();if(s+=n(a.substring(l,g[0].offset)),l=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===l);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return s+n(a.substr(l))}function l(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(n){return o(e,{v:null},n)})),e.cached_variants||e.eW&&[o(e)]||[e]}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):x(a.k).forEach(function(e){u(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]),a.c=Array.prototype.concat.apply([],a.c.map(function(e){return l("self"===e?a:e)})),a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var c=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=c.length?t(c.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function l(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function p(e,n,t,r){var a=r?"":I.classPrefix,i='',i+n+o}function h(){var e,t,r,a;if(!E.k)return n(k);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(k);r;)a+=n(k.substring(t,r.index)),e=l(E,r),e?(B+=e[1],a+=p(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(k);return a+n(k.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!y[E.sL])return n(k);var t=e?f(E.sL,k,!0,x[E.sL]):g(k,E.sL.length?E.sL:void 0);return E.r>0&&(B+=t.r),e&&(x[E.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){L+=null!=E.sL?d():h(),k=""}function v(e){L+=e.cN?p(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(k+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?k+=n:(t.eB&&(k+=n),b(),t.rB||t.eB||(k=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?k+=n:(a.rE||a.eE||(k+=n),b(),a.eE&&(k=n));do E.cN&&(L+=C),E.skip||(B+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return k+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var R,E=i||N,x={},L="";for(R=E;R!==N;R=R.parent)R.cN&&(L=p(R.cN,"",!0)+L);var k="",B=0;try{for(var M,j,O=0;;){if(E.t.lastIndex=O,M=E.t.exec(t),!M)break;j=m(t.substring(O,M.index),M[0]),O=M.index+j}for(m(t.substr(O)),R=E;R.parent;R=R.parent)R.cN&&(L+=C);return{r:B,value:L,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function g(e,t){t=t||I.languages||x(y);var r={r:0,value:n(e)},a=r;return t.filter(w).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function p(e){return I.tabReplace||I.useBR?e.replace(M,function(e,n){return I.useBR&&"\n"===e?"
":I.tabReplace?n.replace(/\t/g,I.tabReplace):""}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function d(e){var n,t,r,o,l,s=i(e);a(s)||(I.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,l=n.textContent,r=s?f(s,l,!0):g(l),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),l)),r.value=p(r.value),e.innerHTML=r.value,e.className=h(e.className,s,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){I=o(I,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll("pre code");E.forEach.call(e,d)}}function m(){addEventListener("DOMContentLoaded",v,!1),addEventListener("load",v,!1)}function N(n,t){var r=y[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function R(){return x(y)}function w(e){return e=(e||"").toLowerCase(),y[e]||y[L[e]]}var E=[],x=Object.keys,y={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="
",I={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};return e.highlight=f,e.highlightAuto=g,e.fixMarkup=p,e.highlightBlock=d,e.configure=b,e.initHighlighting=v,e.initHighlightingOnLoad=m,e.registerLanguage=N,e.listLanguages=R,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("smali",function(t){var s=["add","and","cmp","cmpg","cmpl","const","div","double","float","goto","if","int","long","move","mul","neg","new","nop","not","or","rem","return","shl","shr","sput","sub","throw","ushr","xor"],e=["aget","aput","array","check","execute","fill","filled","goto/16","goto/32","iget","instance","invoke","iput","monitor","packed","sget","sparse"],r=["transient","constructor","abstract","final","synthetic","public","private","protected","static","bridge","system"];return{aliases:["smali"],c:[{cN:"string",b:'"',e:'"',r:0},t.C("#","$",{r:0}),{cN:"keyword",v:[{b:"\\s*\\.end\\s[a-zA-Z0-9]*"},{b:"^[ ]*\\.[a-zA-Z]*",r:0},{b:"\\s:[a-zA-Z_0-9]*",r:0},{b:"\\s("+r.join("|")+")"}]},{cN:"built_in",v:[{b:"\\s("+s.join("|")+")\\s"},{b:"\\s("+s.join("|")+")((\\-|/)[a-zA-Z0-9]+)+\\s",r:10},{b:"\\s("+e.join("|")+")((\\-|/)[a-zA-Z0-9]+)*\\s",r:10}]},{cN:"class",b:"L[^(;:\n]*;",r:0},{b:"[vp][0-9]+"}]}}); -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/.resources/highlight/styles/default.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original highlight.js style (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #F0F0F0; 12 | } 13 | 14 | 15 | /* Base color: saturation 0; */ 16 | 17 | .hljs, 18 | .hljs-subst { 19 | color: #444; 20 | } 21 | 22 | .hljs-comment { 23 | color: #888888; 24 | } 25 | 26 | .hljs-keyword, 27 | .hljs-attribute, 28 | .hljs-selector-tag, 29 | .hljs-meta-keyword, 30 | .hljs-doctag, 31 | .hljs-name { 32 | font-weight: bold; 33 | } 34 | 35 | 36 | /* User color: hue: 0 */ 37 | 38 | .hljs-type, 39 | .hljs-string, 40 | .hljs-number, 41 | .hljs-selector-id, 42 | .hljs-selector-class, 43 | .hljs-quote, 44 | .hljs-template-tag, 45 | .hljs-deletion { 46 | color: #880000; 47 | } 48 | 49 | .hljs-title, 50 | .hljs-section { 51 | color: #880000; 52 | font-weight: bold; 53 | } 54 | 55 | .hljs-regexp, 56 | .hljs-symbol, 57 | .hljs-variable, 58 | .hljs-template-variable, 59 | .hljs-link, 60 | .hljs-selector-attr, 61 | .hljs-selector-pseudo { 62 | color: #BC6060; 63 | } 64 | 65 | 66 | /* Language color: hue: 90; */ 67 | 68 | .hljs-literal { 69 | color: #78A960; 70 | } 71 | 72 | .hljs-built_in, 73 | .hljs-bullet, 74 | .hljs-code, 75 | .hljs-addition { 76 | color: #397300; 77 | } 78 | 79 | 80 | /* Meta color: hue: 200 */ 81 | 82 | .hljs-meta { 83 | color: #1f7199; 84 | } 85 | 86 | .hljs-meta-string { 87 | color: #4d99bf; 88 | } 89 | 90 | 91 | /* Misc effects */ 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/.resources/index.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | redbars = document.querySelectorAll('img.redbar') 3 | greenbars = document.querySelectorAll('img.greenbar') 4 | 5 | maxBarWidth(redbars, greenbars) 6 | 7 | })() 8 | 9 | function maxBarWidth(redbars, greenbars) { 10 | let width = (elem) => parseInt(elem.alt) 11 | let red_widths = Array.from(redbars).map(width) 12 | let green_widths = Array.from(greenbars).map(width) 13 | let max = Math.max.apply(Math,red_widths.map((x, i) => green_widths[i] + x)) 14 | let maxWidth = 120; 15 | for (i = 0; i < redbars.length; i++) { 16 | red_w = red_widths[i] / max * maxWidth 17 | green_w = green_widths[i] / max * maxWidth 18 | redbars[i].width = red_widths[i] > 0 && red_w < 1 ? 1 : red_w; 19 | greenbars[i].width = green_widths[i] > 0 && green_w < 1 ? 1 : green_w; 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/.resources/redbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/resources/html/.resources/redbar.png -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/.resources/report.css: -------------------------------------------------------------------------------- 1 | span.cov { 2 | background-color: #cddab9; 3 | } 4 | 5 | span.exec { 6 | background-color: #e6eac3; 7 | } 8 | 9 | span.ignore { 10 | background-color: grey; 11 | } 12 | 13 | span.missed { 14 | background-color: #f5ccb8; 15 | } 16 | 17 | body, td { 18 | font-family: sans-serif; 19 | font-size: 10pt; 20 | } 21 | 22 | h1 { 23 | font-weight: bold; 24 | font-size: 18pt; 25 | } 26 | 27 | ul.breadcrumb { 28 | padding: 10px 16px; 29 | list-style: none; 30 | background-color: #eee; 31 | } 32 | 33 | ul.breadcrumb li { 34 | display: inline; 35 | font-size: 18px; 36 | } 37 | 38 | ul.breadcrumb li+li:before { 39 | padding: 8px; 40 | color: black; 41 | content: "\003e"; 42 | } 43 | 44 | ul.breadcrumb li a, table a { 45 | color: #0275d8; 46 | text-decoration: none; 47 | } 48 | 49 | ul.breadcrumb li a:hover, table a:hover { 50 | color: #01447e; 51 | text-decoration: underline; 52 | } 53 | 54 | .left-margin { 55 | margin-left: 12px; 56 | } 57 | 58 | .ico { 59 | width: 24px; 60 | height: 24px; 61 | display: inherit; 62 | padding-left: 28px; 63 | background-repeat: no-repeat; 64 | background-position: left center; 65 | vertical-align: middle; 66 | } 67 | 68 | .ico-android { 69 | background-image: url(android.png); 70 | } 71 | 72 | .ico-package { 73 | background-image: url(box.png); 74 | } 75 | 76 | .ico-class { 77 | background-image: url(file.png); 78 | } 79 | 80 | .ico-doc { 81 | background-image: url(document.png); 82 | } 83 | 84 | table, th, td { 85 | border: 1px solid #dddddd; 86 | border-collapse: collapse; 87 | 88 | } 89 | 90 | table thead td { 91 | background-color: #06050530; 92 | padding: 6px 6px; 93 | } 94 | table tfoot td { 95 | padding: 6px 0px; 96 | } 97 | 98 | th, td { 99 | padding: 0px 6px; 100 | text-align: center; 101 | } 102 | 103 | table tr:hover { 104 | background-color: #f5f5f5; 105 | } 106 | 107 | table thead tr:hover { 108 | background-color: #06050530; 109 | } 110 | 111 | tr td:nth-child(2) { 112 | text-align: left; 113 | } 114 | 115 | .left { 116 | float: left; 117 | } 118 | 119 | .footer { 120 | padding: 26px 12px; 121 | } -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/.resources/report.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | if (typeof hljs != 'undefined') { 4 | hljs.initHighlighting() 5 | } 6 | 7 | })() 8 | -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/resources/html/__init__.py -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/templates/class.pt: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 |

granularity level: ${granularity}

16 |
17 | 		${code}
18 | 	
19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/templates/index.pt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 |

granularity level: ${granularity}

16 | 17 | ${table} 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/templates/init_row.pt: -------------------------------------------------------------------------------- 1 | 2 | ${elementname} 3 | 4 | ${progress_missed} 6 | ${progress_covered} 8 | 9 | 10 | ${coverage} 11 | ${coverage_data.lines_missed} 12 | ${coverage_data.lines} 13 | ${coverage_data.methods_missed} 14 | ${coverage_data.methods} 15 | ${coverage_data.classes_missed} 16 | ${coverage_data.classes} 17 | -------------------------------------------------------------------------------- /acvtool/smiler/resources/html/templates/init_table.pt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ${rows} 39 | 40 |
ElementRatioCov.MissedLinesMissedMethodsMissedClasses
Total${progress_covered} of ${progress_all}${total_coverage}${total_coverage_data.lines_missed}${total_coverage_data.lines}${total_coverage_data.methods_missed}${total_coverage_data.methods}${total_coverage_data.classes_missed}${total_coverage_data.classes}
-------------------------------------------------------------------------------- /acvtool/smiler/resources/instrumentation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/resources/instrumentation/__init__.py -------------------------------------------------------------------------------- /acvtool/smiler/resources/instrumentation/smali/tool/acv/AcvCalculator.smali: -------------------------------------------------------------------------------- 1 | .class public final Ltool/acv/AcvCalculator; 2 | .super Ljava/lang/Object; 3 | .source "AcvCalculator.java" 4 | 5 | 6 | # static fields 7 | .field private static final TAG:Ljava/lang/String; = "ACV" 8 | 9 | 10 | # direct methods 11 | .method public constructor ()V 12 | .locals 0 13 | 14 | .line 7 15 | invoke-direct {p0}, Ljava/lang/Object;->()V 16 | 17 | return-void 18 | .end method 19 | 20 | 21 | .method private static printSum([Ljava/lang/reflect/Field;Ljava/lang/String;)V 22 | .locals 7 23 | 24 | .line 16 25 | invoke-static {p0}, Ltool/acv/AcvReporterFields;->fieldsToArray([Ljava/lang/reflect/Field;)[[Z 26 | 27 | move-result-object p0 28 | 29 | const/4 v0, 0x0 30 | 31 | const/4 v1, 0x0 32 | 33 | const/4 v2, 0x0 34 | 35 | const/4 v3, 0x0 36 | 37 | .line 19 38 | :goto_0 39 | array-length v4, p0 40 | 41 | if-ge v1, v4, :cond_2 42 | 43 | .line 20 44 | aget-object v4, p0, v1 45 | 46 | array-length v4, v4 47 | 48 | add-int/2addr v3, v4 49 | 50 | const/4 v4, 0x0 51 | 52 | .line 21 53 | :goto_1 54 | aget-object v5, p0, v1 55 | 56 | array-length v6, v5 57 | 58 | if-ge v4, v6, :cond_1 59 | 60 | .line 22 61 | aget-boolean v5, v5, v4 62 | 63 | if-eqz v5, :cond_0 64 | 65 | add-int/lit8 v2, v2, 0x1 66 | 67 | :cond_0 68 | add-int/lit8 v4, v4, 0x1 69 | 70 | goto :goto_1 71 | 72 | :cond_1 73 | add-int/lit8 v1, v1, 0x1 74 | 75 | goto :goto_0 76 | 77 | .line 27 78 | :cond_2 79 | new-instance p0, Ljava/lang/StringBuilder; 80 | 81 | invoke-direct {p0}, Ljava/lang/StringBuilder;->()V 82 | 83 | invoke-virtual {p0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 84 | 85 | const-string p1, ": covered " 86 | 87 | invoke-virtual {p0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 88 | 89 | invoke-virtual {p0, v2}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; 90 | 91 | const-string p1, " out of " 92 | 93 | invoke-virtual {p0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 94 | 95 | invoke-virtual {p0, v3}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; 96 | 97 | invoke-virtual {p0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; 98 | 99 | move-result-object p0 100 | 101 | const-string p1, "ACV" 102 | 103 | invoke-static {p1, p0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 104 | 105 | return-void 106 | .end method 107 | 108 | 109 | .method public static calculateCoverage()V 110 | .locals 2 111 | 112 | .line 31 113 | const-class v0, Ltool/acv/AcvReporter1; 114 | 115 | invoke-virtual {v0}, Ljava/lang/Class;->getFields()[Ljava/lang/reflect/Field; 116 | 117 | move-result-object v0 118 | 119 | const-string v1, "1" 120 | 121 | .line 32 122 | invoke-static {v0, v1}, Ltool/acv/AcvCalculator;->printSum([Ljava/lang/reflect/Field;Ljava/lang/String;)V 123 | # additional code starts here 124 | -------------------------------------------------------------------------------- /acvtool/smiler/resources/instrumentation/smali/tool/acv/AcvFlushing.smali: -------------------------------------------------------------------------------- 1 | .class public final Ltool/acv/AcvFlushing; 2 | .super Ljava/lang/Object; 3 | .source "AcvFlushing.java" 4 | 5 | 6 | # direct methods 7 | .method public constructor ()V 8 | .locals 0 9 | 10 | .line 6 11 | invoke-direct {p0}, Ljava/lang/Object;->()V 12 | 13 | return-void 14 | .end method 15 | 16 | .method private static flushArrays([Ljava/lang/reflect/Field;)V 17 | .locals 3 18 | 19 | .line 10 20 | invoke-static {p0}, Ltool/acv/AcvReporterFields;->fieldsToArray([Ljava/lang/reflect/Field;)[[Z 21 | 22 | move-result-object p0 23 | 24 | const/4 v0, 0x0 25 | 26 | const/4 v1, 0x0 27 | 28 | .line 11 29 | :goto_0 30 | array-length v2, p0 31 | 32 | if-ge v1, v2, :cond_0 33 | 34 | .line 12 35 | aget-object v2, p0, v1 36 | 37 | invoke-static {v2, v0}, Ljava/util/Arrays;->fill([ZZ)V 38 | 39 | add-int/lit8 v1, v1, 0x1 40 | 41 | goto :goto_0 42 | 43 | :cond_0 44 | return-void 45 | .end method 46 | 47 | 48 | .method public static flush()V 49 | .locals 1 50 | 51 | .line 18 52 | const-class v0, Ltool/acv/AcvReporter1; 53 | 54 | invoke-virtual {v0}, Ljava/lang/Class;->getFields()[Ljava/lang/reflect/Field; 55 | 56 | move-result-object v0 57 | 58 | .line 19 59 | invoke-static {v0}, Ltool/acv/AcvFlushing;->flushArrays([Ljava/lang/reflect/Field;)V 60 | # additional code starts here 61 | -------------------------------------------------------------------------------- /acvtool/smiler/resources/instrumentation/smali/tool/acv/AcvInstrumentation$1.smali: -------------------------------------------------------------------------------- 1 | .class Ltool/acv/AcvInstrumentation$1; 2 | .super Landroid/content/BroadcastReceiver; 3 | .source "AcvInstrumentation.java" 4 | 5 | 6 | # annotations 7 | .annotation system Ldalvik/annotation/EnclosingClass; 8 | value = Ltool/acv/AcvInstrumentation; 9 | .end annotation 10 | 11 | .annotation system Ldalvik/annotation/InnerClass; 12 | accessFlags = 0x0 13 | name = null 14 | .end annotation 15 | 16 | 17 | # instance fields 18 | .field final synthetic this$0:Ltool/acv/AcvInstrumentation; 19 | 20 | 21 | # direct methods 22 | .method constructor (Ltool/acv/AcvInstrumentation;)V 23 | .locals 0 24 | 25 | .line 44 26 | iput-object p1, p0, Ltool/acv/AcvInstrumentation$1;->this$0:Ltool/acv/AcvInstrumentation; 27 | 28 | invoke-direct {p0}, Landroid/content/BroadcastReceiver;->()V 29 | 30 | return-void 31 | .end method 32 | 33 | 34 | # virtual methods 35 | .method public onReceive(Landroid/content/Context;Landroid/content/Intent;)V 36 | .locals 1 37 | 38 | .line 47 39 | new-instance p1, Ljava/lang/StringBuilder; 40 | 41 | invoke-direct {p1}, Ljava/lang/StringBuilder;->()V 42 | 43 | const-string p2, "BroadcastReceiver: saveCoverage: " 44 | 45 | invoke-virtual {p1, p2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 46 | 47 | invoke-static {}, Ljava/lang/Thread;->currentThread()Ljava/lang/Thread; 48 | 49 | move-result-object p2 50 | 51 | invoke-virtual {p2}, Ljava/lang/Thread;->getName()Ljava/lang/String; 52 | 53 | move-result-object p2 54 | 55 | invoke-virtual {p1, p2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 56 | 57 | const-string p2, " ---------" 58 | 59 | invoke-virtual {p1, p2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 60 | 61 | invoke-virtual {p1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; 62 | 63 | move-result-object p1 64 | 65 | const-string p2, "ACV" 66 | 67 | invoke-static {p2, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 68 | 69 | .line 48 70 | new-instance p1, Ltool/acv/AcvStoring; 71 | 72 | invoke-direct {p1}, Ltool/acv/AcvStoring;->()V 73 | 74 | .line 49 75 | invoke-virtual {p1}, Ltool/acv/AcvStoring;->saveCoverage()V 76 | 77 | .line 50 78 | iget-object p1, p0, Ltool/acv/AcvInstrumentation$1;->this$0:Ltool/acv/AcvInstrumentation; 79 | 80 | invoke-static {p1}, Ltool/acv/AcvInstrumentation;->access$000(Ltool/acv/AcvInstrumentation;)Landroid/os/Bundle; 81 | 82 | move-result-object p1 83 | 84 | const-string p2, "id" 85 | 86 | const-string v0, "ACV_Instrumentation" 87 | 88 | invoke-virtual {p1, p2, v0}, Landroid/os/Bundle;->putString(Ljava/lang/String;Ljava/lang/String;)V 89 | 90 | .line 51 91 | iget-object p1, p0, Ltool/acv/AcvInstrumentation$1;->this$0:Ltool/acv/AcvInstrumentation; 92 | 93 | invoke-static {p1}, Ltool/acv/AcvInstrumentation;->access$000(Ltool/acv/AcvInstrumentation;)Landroid/os/Bundle; 94 | 95 | move-result-object p2 96 | 97 | const/4 v0, -0x1 98 | 99 | invoke-virtual {p1, v0, p2}, Ltool/acv/AcvInstrumentation;->finish(ILandroid/os/Bundle;)V 100 | 101 | return-void 102 | .end method 103 | -------------------------------------------------------------------------------- /acvtool/smiler/resources/instrumentation/smali/tool/acv/AcvInstrumentation.smali: -------------------------------------------------------------------------------- 1 | .class public Ltool/acv/AcvInstrumentation; 2 | .super Landroid/app/Instrumentation; 3 | .source "AcvInstrumentation.java" 4 | 5 | 6 | # static fields 7 | .field private static final TAG:Ljava/lang/String; = "ACV" 8 | 9 | 10 | # instance fields 11 | .field private final receiverFinish:Landroid/content/BroadcastReceiver; 12 | 13 | .field private final resultsBundle:Landroid/os/Bundle; 14 | 15 | 16 | # direct methods 17 | .method public constructor ()V 18 | .locals 1 19 | 20 | .line 14 21 | invoke-direct {p0}, Landroid/app/Instrumentation;->()V 22 | 23 | .line 17 24 | new-instance v0, Landroid/os/Bundle; 25 | 26 | invoke-direct {v0}, Landroid/os/Bundle;->()V 27 | 28 | iput-object v0, p0, Ltool/acv/AcvInstrumentation;->resultsBundle:Landroid/os/Bundle; 29 | 30 | .line 44 31 | new-instance v0, Ltool/acv/AcvInstrumentation$1; 32 | 33 | invoke-direct {v0, p0}, Ltool/acv/AcvInstrumentation$1;->(Ltool/acv/AcvInstrumentation;)V 34 | 35 | iput-object v0, p0, Ltool/acv/AcvInstrumentation;->receiverFinish:Landroid/content/BroadcastReceiver; 36 | 37 | return-void 38 | .end method 39 | 40 | .method static synthetic access$000(Ltool/acv/AcvInstrumentation;)Landroid/os/Bundle; 41 | .locals 0 42 | 43 | .line 14 44 | iget-object p0, p0, Ltool/acv/AcvInstrumentation;->resultsBundle:Landroid/os/Bundle; 45 | 46 | return-object p0 47 | .end method 48 | 49 | 50 | # virtual methods 51 | .method public finish(ILandroid/os/Bundle;)V 52 | .locals 2 53 | 54 | .line 65 55 | new-instance v0, Ljava/lang/StringBuilder; 56 | 57 | invoke-direct {v0}, Ljava/lang/StringBuilder;->()V 58 | 59 | const-string v1, "finish: " 60 | 61 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 62 | 63 | invoke-static {}, Ljava/lang/Thread;->currentThread()Ljava/lang/Thread; 64 | 65 | move-result-object v1 66 | 67 | invoke-virtual {v1}, Ljava/lang/Thread;->getName()Ljava/lang/String; 68 | 69 | move-result-object v1 70 | 71 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 72 | 73 | const-string v1, " ---------" 74 | 75 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 76 | 77 | invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; 78 | 79 | move-result-object v0 80 | 81 | const-string v1, "ACV" 82 | 83 | invoke-static {v1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 84 | 85 | .line 66 86 | invoke-super {p0, p1, p2}, Landroid/app/Instrumentation;->finish(ILandroid/os/Bundle;)V 87 | 88 | return-void 89 | .end method 90 | 91 | .method public onCreate(Landroid/os/Bundle;)V 92 | .locals 4 93 | 94 | .line 21 95 | invoke-super {p0, p1}, Landroid/app/Instrumentation;->onCreate(Landroid/os/Bundle;)V 96 | 97 | const-string p1, "ACV" 98 | 99 | const-string v0, "------------------------------------------------------------" 100 | 101 | .line 22 102 | invoke-static {p1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 103 | 104 | .line 23 105 | new-instance v0, Ljava/lang/StringBuilder; 106 | 107 | invoke-direct {v0}, Ljava/lang/StringBuilder;->()V 108 | 109 | const-string v1, "onCreate: " 110 | 111 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 112 | 113 | invoke-static {}, Ljava/lang/Thread;->currentThread()Ljava/lang/Thread; 114 | 115 | move-result-object v1 116 | 117 | invoke-virtual {v1}, Ljava/lang/Thread;->getName()Ljava/lang/String; 118 | 119 | move-result-object v1 120 | 121 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 122 | 123 | const-string v1, " ---------" 124 | 125 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 126 | 127 | invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; 128 | 129 | move-result-object v0 130 | 131 | invoke-static {p1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 132 | 133 | .line 25 134 | new-instance v0, Ljava/io/File; 135 | 136 | const-string v2, "/sdcard/Download/app.debloat.instrapp" 137 | 138 | invoke-direct {v0, v2}, Ljava/io/File;->(Ljava/lang/String;)V 139 | 140 | invoke-virtual {v0}, Ljava/io/File;->mkdirs()Z 141 | 142 | move-result v0 143 | 144 | .line 26 145 | new-instance v2, Ljava/lang/StringBuilder; 146 | 147 | invoke-direct {v2}, Ljava/lang/StringBuilder;->()V 148 | 149 | const-string v3, "onCreate: mkdirs: " 150 | 151 | invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 152 | 153 | invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Z)Ljava/lang/StringBuilder; 154 | 155 | invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 156 | 157 | invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; 158 | 159 | move-result-object v0 160 | 161 | invoke-static {p1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 162 | 163 | .line 27 164 | new-instance v0, Landroid/content/IntentFilter; 165 | 166 | const-string v2, "tool.acv.finishtesting" 167 | 168 | invoke-direct {v0, v2}, Landroid/content/IntentFilter;->(Ljava/lang/String;)V 169 | 170 | .line 28 171 | invoke-virtual {p0}, Ltool/acv/AcvInstrumentation;->getContext()Landroid/content/Context; 172 | 173 | move-result-object v2 174 | 175 | iget-object v3, p0, Ltool/acv/AcvInstrumentation;->receiverFinish:Landroid/content/BroadcastReceiver; 176 | 177 | invoke-virtual {v2, v3, v0}, Landroid/content/Context;->registerReceiver(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent; 178 | 179 | .line 33 180 | new-instance v0, Ljava/lang/StringBuilder; 181 | 182 | invoke-direct {v0}, Ljava/lang/StringBuilder;->()V 183 | 184 | const-string v2, "onCreate end: " 185 | 186 | invoke-virtual {v0, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 187 | 188 | invoke-static {}, Ljava/lang/Thread;->currentThread()Ljava/lang/Thread; 189 | 190 | move-result-object v2 191 | 192 | invoke-virtual {v2}, Ljava/lang/Thread;->getName()Ljava/lang/String; 193 | 194 | move-result-object v2 195 | 196 | invoke-virtual {v0, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 197 | 198 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 199 | 200 | invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; 201 | 202 | move-result-object v0 203 | 204 | invoke-static {p1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 205 | 206 | return-void 207 | .end method 208 | 209 | .method public onDestroy()V 210 | .locals 2 211 | 212 | .line 77 213 | new-instance v0, Ljava/lang/StringBuilder; 214 | 215 | invoke-direct {v0}, Ljava/lang/StringBuilder;->()V 216 | 217 | const-string v1, "onDestroy: " 218 | 219 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 220 | 221 | invoke-static {}, Ljava/lang/Thread;->currentThread()Ljava/lang/Thread; 222 | 223 | move-result-object v1 224 | 225 | invoke-virtual {v1}, Ljava/lang/Thread;->getName()Ljava/lang/String; 226 | 227 | move-result-object v1 228 | 229 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 230 | 231 | const-string v1, " ---------" 232 | 233 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 234 | 235 | invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; 236 | 237 | move-result-object v0 238 | 239 | const-string v1, "ACV" 240 | 241 | invoke-static {v1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 242 | 243 | .line 78 244 | invoke-super {p0}, Landroid/app/Instrumentation;->onDestroy()V 245 | 246 | return-void 247 | .end method 248 | 249 | .method public onException(Ljava/lang/Object;Ljava/lang/Throwable;)Z 250 | .locals 2 251 | 252 | .line 71 253 | new-instance v0, Ljava/lang/StringBuilder; 254 | 255 | invoke-direct {v0}, Ljava/lang/StringBuilder;->()V 256 | 257 | const-string v1, "onException: " 258 | 259 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 260 | 261 | invoke-static {}, Ljava/lang/Thread;->currentThread()Ljava/lang/Thread; 262 | 263 | move-result-object v1 264 | 265 | invoke-virtual {v1}, Ljava/lang/Thread;->getName()Ljava/lang/String; 266 | 267 | move-result-object v1 268 | 269 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 270 | 271 | const-string v1, " ---------" 272 | 273 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 274 | 275 | invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; 276 | 277 | move-result-object v0 278 | 279 | const-string v1, "ACV" 280 | 281 | invoke-static {v1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 282 | 283 | .line 72 284 | invoke-super {p0, p1, p2}, Landroid/app/Instrumentation;->onException(Ljava/lang/Object;Ljava/lang/Throwable;)Z 285 | 286 | move-result p1 287 | 288 | return p1 289 | .end method 290 | -------------------------------------------------------------------------------- /acvtool/smiler/resources/instrumentation/smali/tool/acv/AcvReceiver.smali: -------------------------------------------------------------------------------- 1 | .class public Ltool/acv/AcvReceiver; 2 | .super Landroid/content/BroadcastReceiver; 3 | .source "AcvReceiver.java" 4 | 5 | 6 | # direct methods 7 | .method public constructor ()V 8 | .locals 0 9 | 10 | .line 11 11 | invoke-direct {p0}, Landroid/content/BroadcastReceiver;->()V 12 | 13 | return-void 14 | .end method 15 | 16 | 17 | # virtual methods 18 | .method public onReceive(Landroid/content/Context;Landroid/content/Intent;)V 19 | .locals 0 20 | 21 | .line 15 22 | invoke-virtual {p2}, Landroid/content/Intent;->getAction()Ljava/lang/String; 23 | 24 | move-result-object p1 25 | 26 | const-string p2, "ACV" 27 | 28 | .line 16 29 | invoke-static {p2, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 30 | 31 | const-string p2, "tool.acv.calculate" 32 | 33 | .line 17 34 | invoke-virtual {p1, p2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z 35 | 36 | move-result p2 37 | 38 | if-eqz p2, :cond_0 39 | 40 | .line 18 41 | invoke-static {}, Ltool/acv/AcvCalculator;->calculateCoverage()V 42 | 43 | goto :goto_0 44 | 45 | :cond_0 46 | const-string p2, "tool.acv.snap" 47 | 48 | if-ne p1, p2, :cond_1 49 | 50 | .line 21 51 | new-instance p1, Ljava/io/File; 52 | 53 | const-string p2, "/sdcard/Download/app.debloat.instrapp" 54 | 55 | invoke-direct {p1, p2}, Ljava/io/File;->(Ljava/lang/String;)V 56 | 57 | invoke-virtual {p1}, Ljava/io/File;->mkdirs()Z 58 | 59 | .line 22 60 | new-instance p1, Ltool/acv/AcvStoring; 61 | 62 | invoke-direct {p1}, Ltool/acv/AcvStoring;->()V 63 | 64 | .line 23 65 | invoke-virtual {p1}, Ltool/acv/AcvStoring;->saveCoverage()V 66 | 67 | goto :goto_0 68 | 69 | :cond_1 70 | const-string p2, "tool.acv.flush" 71 | 72 | if-ne p1, p2, :cond_2 73 | 74 | .line 25 75 | invoke-static {}, Ltool/acv/AcvFlushing;->flush()V 76 | 77 | :cond_2 78 | :goto_0 79 | return-void 80 | .end method 81 | -------------------------------------------------------------------------------- /acvtool/smiler/resources/instrumentation/smali/tool/acv/AcvReporterFields.smali: -------------------------------------------------------------------------------- 1 | .class public final Ltool/acv/AcvReporterFields; 2 | .super Ljava/lang/Object; 3 | .source "AcvReporterFields.java" 4 | 5 | 6 | # direct methods 7 | .method public constructor ()V 8 | .locals 0 9 | 10 | .line 7 11 | invoke-direct {p0}, Ljava/lang/Object;->()V 12 | 13 | return-void 14 | .end method 15 | 16 | .method public static extractNumber(Ljava/lang/String;)I 17 | .locals 5 18 | 19 | .line 32 20 | invoke-virtual {p0}, Ljava/lang/String;->length()I 21 | 22 | move-result v0 23 | 24 | const/4 v1, 0x1 25 | 26 | sub-int/2addr v0, v1 27 | 28 | const/4 v2, 0x0 29 | 30 | .line 33 31 | :goto_0 32 | invoke-virtual {p0, v0}, Ljava/lang/String;->charAt(I)C 33 | 34 | move-result v3 35 | 36 | const/16 v4, 0x5f 37 | 38 | if-eq v3, v4, :cond_0 39 | 40 | .line 34 41 | invoke-virtual {p0, v0}, Ljava/lang/String;->charAt(I)C 42 | 43 | move-result v3 44 | 45 | add-int/lit8 v3, v3, -0x30 46 | 47 | mul-int v3, v3, v1 48 | 49 | add-int/2addr v2, v3 50 | 51 | mul-int/lit8 v1, v1, 0xa 52 | 53 | add-int/lit8 v0, v0, -0x1 54 | 55 | goto :goto_0 56 | 57 | :cond_0 58 | return v2 59 | .end method 60 | 61 | .method public static fieldsToArray([Ljava/lang/reflect/Field;)[[Z 62 | .locals 5 63 | 64 | .line 15 65 | array-length v0, p0 66 | 67 | new-array v0, v0, [[Z 68 | 69 | const/4 v1, 0x0 70 | 71 | .line 16 72 | :goto_0 73 | array-length v2, p0 74 | 75 | if-ge v1, v2, :cond_0 76 | 77 | .line 18 78 | :try_start_0 79 | aget-object v2, p0, v1 80 | 81 | invoke-virtual {v2}, Ljava/lang/reflect/Field;->getName()Ljava/lang/String; 82 | 83 | move-result-object v2 84 | 85 | invoke-static {v2}, Ltool/acv/AcvReporterFields;->extractNumber(Ljava/lang/String;)I 86 | 87 | move-result v2 88 | 89 | .line 19 90 | aget-object v3, p0, v1 91 | 92 | const/4 v4, 0x0 93 | 94 | invoke-virtual {v3, v4}, Ljava/lang/reflect/Field;->get(Ljava/lang/Object;)Ljava/lang/Object; 95 | 96 | move-result-object v3 97 | 98 | check-cast v3, [Z 99 | 100 | aput-object v3, v0, v2 101 | :try_end_0 102 | .catch Ljava/lang/IllegalAccessException; {:try_start_0 .. :try_end_0} :catch_0 103 | 104 | goto :goto_1 105 | 106 | :catch_0 107 | move-exception v2 108 | 109 | .line 21 110 | invoke-virtual {v2}, Ljava/lang/IllegalAccessException;->printStackTrace()V 111 | 112 | .line 22 113 | new-instance v3, Ljava/lang/StringBuilder; 114 | 115 | invoke-direct {v3}, Ljava/lang/StringBuilder;->()V 116 | 117 | const-string v4, ":reflection: " 118 | 119 | invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 120 | 121 | invoke-virtual {v2}, Ljava/lang/IllegalAccessException;->getMessage()Ljava/lang/String; 122 | 123 | move-result-object v2 124 | 125 | invoke-virtual {v3, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 126 | 127 | invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; 128 | 129 | move-result-object v2 130 | 131 | const-string v3, "ACV" 132 | 133 | invoke-static {v3, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 134 | 135 | :goto_1 136 | add-int/lit8 v1, v1, 0x1 137 | 138 | goto :goto_0 139 | 140 | :cond_0 141 | return-object v0 142 | .end method 143 | -------------------------------------------------------------------------------- /acvtool/smiler/resources/instrumentation/smali/tool/acv/AcvStoring.smali: -------------------------------------------------------------------------------- 1 | .class public Ltool/acv/AcvStoring; 2 | .super Ljava/lang/Object; 3 | .source "AcvStoring.java" 4 | 5 | 6 | # instance fields 7 | .field private counter:I 8 | 9 | .field private snapTime:Ljava/lang/String; 10 | 11 | 12 | # direct methods 13 | .method public constructor ()V 14 | .locals 1 15 | 16 | .line 12 17 | invoke-direct {p0}, Ljava/lang/Object;->()V 18 | 19 | const/4 v0, 0x0 20 | 21 | .line 14 22 | iput v0, p0, Ltool/acv/AcvStoring;->counter:I 23 | 24 | return-void 25 | .end method 26 | 27 | .method private saveExternalPublicFile([Ljava/lang/reflect/Field;)V 28 | .locals 5 29 | 30 | .line 23 31 | invoke-static {p1}, Ltool/acv/AcvReporterFields;->fieldsToArray([Ljava/lang/reflect/Field;)[[Z 32 | 33 | move-result-object p1 34 | 35 | .line 24 36 | invoke-virtual {p0}, Ltool/acv/AcvStoring;->getCoverageFilename()Ljava/lang/String; 37 | 38 | move-result-object v0 39 | 40 | .line 25 41 | new-instance v1, Ljava/io/File; 42 | 43 | invoke-direct {v1, v0}, Ljava/io/File;->(Ljava/lang/String;)V 44 | 45 | const/4 v2, 0x1 46 | 47 | const/4 v3, 0x0 48 | 49 | .line 26 50 | invoke-virtual {v1, v2, v3}, Ljava/io/File;->setReadable(ZZ)Z 51 | 52 | const-string v2, "ACV" 53 | 54 | .line 29 55 | :try_start_0 56 | new-instance v3, Ljava/io/ObjectOutputStream; 57 | 58 | new-instance v4, Ljava/io/FileOutputStream; 59 | 60 | invoke-direct {v4, v1}, Ljava/io/FileOutputStream;->(Ljava/io/File;)V 61 | 62 | invoke-direct {v3, v4}, Ljava/io/ObjectOutputStream;->(Ljava/io/OutputStream;)V 63 | 64 | .line 30 65 | invoke-virtual {v3, p1}, Ljava/io/ObjectOutputStream;->writeObject(Ljava/lang/Object;)V 66 | 67 | .line 31 68 | invoke-virtual {v3}, Ljava/io/ObjectOutputStream;->flush()V 69 | 70 | .line 32 71 | invoke-virtual {v3}, Ljava/io/ObjectOutputStream;->close()V 72 | 73 | .line 33 74 | invoke-static {v2, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 75 | :try_end_0 76 | .catch Ljava/io/IOException; {:try_start_0 .. :try_end_0} :catch_0 77 | 78 | goto :goto_0 79 | 80 | :catch_0 81 | move-exception p1 82 | 83 | .line 35 84 | invoke-virtual {p1}, Ljava/io/IOException;->printStackTrace()V 85 | 86 | .line 36 87 | new-instance v0, Ljava/lang/StringBuilder; 88 | 89 | invoke-direct {v0}, Ljava/lang/StringBuilder;->()V 90 | 91 | const-string v1, ":saving: " 92 | 93 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 94 | 95 | invoke-virtual {p1}, Ljava/io/IOException;->getMessage()Ljava/lang/String; 96 | 97 | move-result-object p1 98 | 99 | invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 100 | 101 | invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; 102 | 103 | move-result-object p1 104 | 105 | invoke-static {v2, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 106 | 107 | :goto_0 108 | return-void 109 | .end method 110 | 111 | 112 | # virtual methods 113 | .method public getCoverageFilename()Ljava/lang/String; 114 | .locals 2 115 | 116 | .line 18 117 | iget v0, p0, Ltool/acv/AcvStoring;->counter:I 118 | 119 | add-int/lit8 v0, v0, 0x1 120 | 121 | iput v0, p0, Ltool/acv/AcvStoring;->counter:I 122 | 123 | .line 19 124 | new-instance v0, Ljava/lang/StringBuilder; 125 | 126 | invoke-direct {v0}, Ljava/lang/StringBuilder;->()V 127 | 128 | const-string v1, "/sdcard/Download/app.debloat.instrapp/coverage_" 129 | 130 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 131 | 132 | iget-object v1, p0, Ltool/acv/AcvStoring;->snapTime:Ljava/lang/String; 133 | 134 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 135 | 136 | const/16 v1, 0x5f 137 | 138 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder; 139 | 140 | iget v1, p0, Ltool/acv/AcvStoring;->counter:I 141 | 142 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; 143 | 144 | const-string v1, ".ec" 145 | 146 | invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; 147 | 148 | invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; 149 | 150 | move-result-object v0 151 | 152 | return-object v0 153 | .end method 154 | 155 | .method public saveCoverage()V 156 | .locals 2 157 | 158 | const/4 v0, 0x0 159 | 160 | .line 43 161 | iput v0, p0, Ltool/acv/AcvStoring;->counter:I 162 | 163 | .line 44 164 | invoke-static {}, Ljava/lang/System;->currentTimeMillis()J 165 | 166 | move-result-wide v0 167 | 168 | invoke-static {v0, v1}, Ljava/lang/String;->valueOf(J)Ljava/lang/String; 169 | 170 | move-result-object v0 171 | 172 | iput-object v0, p0, Ltool/acv/AcvStoring;->snapTime:Ljava/lang/String; 173 | 174 | .line 45 175 | const-class v0, Ltool/acv/AcvReporter1; 176 | 177 | invoke-virtual {v0}, Ljava/lang/Class;->getFields()[Ljava/lang/reflect/Field; 178 | 179 | move-result-object v0 180 | 181 | .line 46 182 | invoke-direct {p0, v0}, Ltool/acv/AcvStoring;->saveExternalPublicFile([Ljava/lang/reflect/Field;)V 183 | # additional code starts here 184 | -------------------------------------------------------------------------------- /acvtool/smiler/resources/logging.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | root: 3 | level: DEBUG 4 | handlers: [console, file] 5 | 6 | formatters: 7 | simple: 8 | format: '%(asctime)s - %(levelname)s - %(message)s' 9 | minimal: 10 | format: '%(message)s' 11 | 12 | handlers: 13 | console: 14 | level: INFO 15 | class: logging.StreamHandler 16 | formatter: minimal 17 | stream: ext://sys.stdout 18 | file: 19 | level: DEBUG 20 | class: logging.FileHandler 21 | formatter: simple 22 | filename: log.log 23 | 24 | -------------------------------------------------------------------------------- /acvtool/smiler/serialisation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilgun/acvtool/3e0ded4373902168379e14f5a85b1e4193ae2233/acvtool/smiler/serialisation/__init__.py -------------------------------------------------------------------------------- /acvtool/smiler/serialisation/html_serialiser.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import html 4 | from operator import attrgetter 5 | from chameleon import PageTemplateLoader 6 | from chameleon.utils import Markup 7 | from ..instrumenting.utils import Utils 8 | from ..config import config 9 | from ..granularity import Granularity 10 | from ..entities.coverage import CoverageData 11 | 12 | 13 | class HtmlSerialiser(object): 14 | 15 | not_instr_regex = re.compile("^(move-result|move-exception).*$") 16 | 17 | def __init__(self, package, granularity, output_dir): 18 | self.package = package 19 | self.granularity = granularity 20 | self.output_dir = output_dir 21 | self.templates = PageTemplateLoader(config.templates_path) 22 | self.class_template = self.templates["class.pt"] 23 | self.resources_dir = os.path.join(output_dir, '.resources') 24 | self.init_row = self.templates['init_row.pt'] 25 | self.init_table = self.templates['init_table.pt'] 26 | self.index_template = self.templates['index.pt'] 27 | 28 | 29 | @staticmethod 30 | def get_first_lbl_by_index(lables, index): 31 | i = 0 32 | while i < len(lables) and lables[i].index < index: 33 | i += 1 34 | if i < len(lables) and lables[i].index == index: 35 | return lables[i] 36 | return None 37 | 38 | def get_init_row(self, path, type, elementname, coverage_data, respath): 39 | coverage = coverage_data.get_formatted_coverage(self.granularity) 40 | return self.init_row(elementlink=path, type=type, elementname=elementname, 41 | coverage=coverage, 42 | respath=respath, coverage_data=coverage_data, 43 | is_instruction_level=Granularity.is_instruction(self.granularity), 44 | progress_covered=coverage_data.covered(self.granularity), 45 | progress_missed=coverage_data.missed(self.granularity)) 46 | 47 | def get_init_table(self, rows, coverage_data): 48 | total_coverage = coverage_data.get_formatted_coverage(self.granularity) 49 | table = self.init_table(rows=Markup("\n".join(rows)), 50 | total_coverage=total_coverage, 51 | total_coverage_data=coverage_data, 52 | is_instruction_level=Granularity.is_instruction(self.granularity), 53 | progress_covered=coverage_data.covered(self.granularity), 54 | progress_all=coverage_data.coverable(self.granularity)) 55 | return table 56 | 57 | def get_index_html(self, table, appname, title, package, respath, file_name): 58 | htmlpage = self.index_template(table=Markup(table), appname=appname, title=title, 59 | package=package, respath=respath, file_name=file_name, 60 | granularity=Granularity.get(self.granularity), version=config.version) 61 | return htmlpage 62 | 63 | def save_dex_report(self, dex_coverages): 64 | Utils.copytree(config.html_resources_dir_path, self.resources_dir) 65 | total_coverage_data = CoverageData() 66 | rows = [] 67 | for id, coverage_data in dex_coverages.items(): 68 | pth = os.path.join(str(id), "main_index.html") 69 | row = self.get_init_row(pth, "dex", str(id), coverage_data, "") 70 | rows.append(Markup(row)) 71 | total_coverage_data.add_data(coverage_data) 72 | table = self.get_init_table(rows, total_coverage_data) 73 | htmlpage = self.get_index_html(table, self.package, self.package, self.package, '', None) 74 | path = os.path.join(self.output_dir, "main_index.html") 75 | with open(path, 'w') as f: 76 | f.write(htmlpage) 77 | 78 | def save_html(self, smalitree): 79 | report_dir = os.path.join(self.output_dir, str(smalitree.Id)) 80 | if not os.path.exists(report_dir): 81 | os.makedirs(report_dir) 82 | for cl in smalitree.classes: 83 | self.save_class(cl, report_dir) 84 | dex_coverage_data = self.save_packaged_coverage(report_dir, smalitree) 85 | return dex_coverage_data 86 | 87 | def save_multidex_report(coverage_data): 88 | pass 89 | 90 | def save_class(self, cl, report_dir): 91 | dir = os.path.join(report_dir, cl.folder) 92 | if not os.path.exists(dir): 93 | os.makedirs(dir) 94 | class_path = os.path.join(dir, cl.file_name + '.html') 95 | buf = [Tag.Li(d) for d in cl.get_class_description()] 96 | buf.append(Tag.Li('')) 97 | buf.extend([Tag.Li(html.escape(a)) for a in cl.get_annotations()]) 98 | buf.append(Tag.Li('')) 99 | buf.extend([Tag.Li(f) for f in cl.get_fields()]) 100 | buf.append(Tag.Li('')) 101 | for m in cl.methods: 102 | ins_buf = [] 103 | labels = m.labels.values() 104 | labels = sorted(labels, key=attrgetter('index')) 105 | for i in range(len(m.insns)): 106 | ins = m.insns[i] 107 | if ins.covered: 108 | ins_buf.append(Tag.span_tab(html.escape(ins.buf), Tag.COV_CLASS)) 109 | else: 110 | if ins.buf.startswith("return"): 111 | lbl = HtmlSerialiser.get_first_lbl_by_index(labels, i) 112 | if lbl and lbl.covered or (not lbl and m.insns[i-1].covered): 113 | ins_buf.append(Tag.span_tab(ins.buf, Tag.EXEC_CLASS)) 114 | else: 115 | ins_buf.append(Tag.span_tab(ins.buf)) 116 | else: 117 | if m.insns[i].cover_code > -1 and not m.insns[i].covered: 118 | ins_buf.append(Tag.span_tab(html.escape(ins.buf), Tag.MISSED)) 119 | continue 120 | if i{}'.format(cl, txt) 240 | 241 | @staticmethod 242 | def span_tab(txt, cl=''): 243 | return Tag.span("\t{}".format(txt), cl) 244 | 245 | COV_CLASS = 'cov' #html class, ex: '' 246 | EXEC_CLASS = 'exec' 247 | IGNORE_TAG = 'ignore' 248 | MISSED = "missed" -------------------------------------------------------------------------------- /acvtool/smiler/serialisation/xml_serialiser.py: -------------------------------------------------------------------------------- 1 | import lxml 2 | from lxml import etree 3 | from lxml.etree import Element, SubElement 4 | from ..granularity import Granularity 5 | from ..instrumenting.utils import Utils as Utils2 6 | 7 | class XmlSerialiser(object): 8 | 9 | def __init__(self, smalitree, app_name, granularity): 10 | self.smalitree = smalitree 11 | self.app_name = app_name 12 | self.granularity = granularity 13 | 14 | def get_xml(self): 15 | report = Element("report") 16 | report.set("name", self.app_name) 17 | 18 | groups = Utils2.get_groupped_classes(self.smalitree) 19 | for g in groups: 20 | package = SubElement(report,"package") 21 | package.set("name", Utils2.get_package_name(g[0].name)) 22 | for cl in g: 23 | if (cl.is_coverable()): 24 | self.add_xml_class(package, cl) 25 | return etree.tostring(report, pretty_print=True) 26 | 27 | def add_xml_class(self, package, smali_class): 28 | class_insns_covered = 0 29 | class_insns_missed = 0 30 | xml_class = SubElement(package, "class") 31 | xml_class.set("name", smali_class.name[1:-1]) 32 | class_instructions_added = 0 33 | for m in smali_class.methods: 34 | if (m.cover_code > -1): # not abstract and not native method 35 | xml_method = self.create_xml_method(xml_class, m) 36 | if Granularity.is_instruction(self.granularity): 37 | lines_covered = m.covered() 38 | lines_missed = m.not_covered() 39 | self.add_xml_insn_counter(xml_method, lines_covered, lines_missed, "INSTRUCTION") 40 | class_insns_covered += lines_covered 41 | class_insns_missed += lines_missed 42 | class_instructions_added += 1 43 | if Granularity.is_method(self.granularity): 44 | self.add_xml_insn_counter(xml_method, int(m.called), 1-int(m.called), "METHOD") 45 | methods_covered = smali_class.mtds_covered() 46 | methods_missed = smali_class.mtds_coverable() - methods_covered 47 | if class_instructions_added: 48 | self.add_xml_insn_counter(xml_class, class_insns_covered, class_insns_missed, "INSTRUCTION") 49 | self.add_xml_insn_counter(xml_class, methods_covered, methods_missed, "METHOD") 50 | 51 | def create_xml_method(self, xml_class, smali_method): 52 | xml_method = SubElement(xml_class, "method") 53 | xml_method.set("name", smali_method.name) 54 | xml_method.set("desc", smali_method.get_method_argument_desc()) 55 | return xml_method 56 | 57 | def add_xml_insn_counter(self, xml_method, covered, missed, cover_type): 58 | xml_counter = SubElement(xml_method, "counter") 59 | xml_counter.set("covered", str(covered)) 60 | xml_counter.set("missed", str(missed)) 61 | xml_counter.set("type", cover_type) -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ACVTool 2.3.2 Multidex 2 | 3 | [![Python version](https://img.shields.io/pypi/pyversions/acvtool?color=%2300bf55)]() 4 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4060443.svg)](https://doi.org/10.5281/zenodo.4060443) 5 | [![Arsenal](https://github.com/toolswatch/badges/blob/master/arsenal/europe/2024.svg)](https://www.blackhat.com/eu-24/arsenal/schedule/index.html#acvtool--multidex-41926) 6 | 7 | ACVTool measures code coverage and highlights executed instructions in an Android app. ACVTool operates on the Smali representation of the bytecode. 8 | 9 | [Demonstration video of ACVTool](https://youtu.be/0GLyqFdxboQ). 10 | 11 | ## Prerequisites 12 | 1. `Windows`/`OSX`/`Ubuntu`. 13 | 3. `Java` version `1.8`. 14 | 2. `Android SDK`. 15 | 4. `Python 3`. 16 | 17 | ## Installation 18 | 1. Run the `pip` command to install dependencies: 19 | 20 | From PYPI 21 | ```shell 22 | $ pip install acvtool==2.3.2 23 | ``` 24 | Or from sources 25 | ```shell 26 | $ cd acvtool 27 | $ pip install -e . 28 | $ acv -h 29 | ``` 30 | 31 | When successfully installed, you will be able to execute `acv -h`. This command will create the working directory "\~/acvtool" and the configuration file "\~/acvtool/config.json". 32 | 33 | 1. Download the [ACVPatcher](https://github.com/pilgun/acvpatcher/releases) binary for your system. ACVPatcher replaces usage of Apktool, zipalign, apksigner. ACVPatcher is a separated binary since it was implemented with .NET Core. 34 | 35 | ACVPatcher needs to be trusted to work: 36 | - (OSX/Linux) `chmod +x acvpatcher` 37 | - Call the Context Menu, Tap Open, Open the App From Not Trusted Developer 38 | 39 | 2. Specify absolute paths to the Android tools at "~/acvtool/config.json" (%userprofile%\acvtool\config.json in Windows) for the following variables. 40 | * AAPT 41 | * ZIPALIGN 42 | * ADB 43 | * APKSIGNER 44 | * ACVPATCHER 45 | 46 | 3.1. Windows configuration example 47 | 48 | ```json 49 | { 50 | "AAPT": "C:/users/ME/appdata/local/android/sdk/build-tools/25.0.1/aapt2.exe", 51 | "ZIPALIGN": "C:/users/ME/appdata/local/android/sdk/build-tools/25.0.1/zipalign.exe", 52 | "ADB": "C:/users/ME/appdata/local/android/sdk/platform-tools/adb.exe", 53 | "APKSIGNER": "C:/users/ME/appdata/local/android/sdk/build-tools/24.0.3/apksigner.bat", 54 | "ACVPATCHER": "D:/distr/acvpatcher.exe" 55 | } 56 | ``` 57 | 3.2. OSX, Linux configuration example 58 | 59 | ```json 60 | { 61 | "AAPT": "[$HOME]/Library/Android/sdk/build-tools/25.0.3/aapt2", 62 | "ZIPALIGN": "[$HOME]/Library/Android/sdk/build-tools/25.0.3/zipalign", 63 | "ADB": "[$HOME]/Library/Android/sdk/platform-tools/adb", 64 | "APKSIGNER": "[$HOME]/Library/Android/sdk/build-tools/24.0.3/apksigner", 65 | "ACVPATCHER": "~/distr/ACVPatcher-osx/acvpatcher" 66 | } 67 | ``` 68 | 69 | ### Workflow 70 | 71 | Steps: 72 | 1. Instrument the original APK with ACVTool [instrument --wd ] 73 | 2. Install the instrumented APK in the Android emulator or device. [install ] 74 | 3. Activate the app for coverage measurement [activate ] (alternatively, [start ]) 75 | 4. Test the application (launch it!) 76 | 5. Make a snap [snap ] 77 | 6. Apply the extracted coverage data onto the smali code tree [cover-pickles --wd ] 78 | 6. Generate the code coverage report [report --wd ] 79 | 80 | ### Example: instruction coverage measurement 81 | 82 | 1. Instrument the original APK with ACVTool, and run the emulator: 83 | 84 | ```shell 85 | $ acv instrument test_apks/snake.apk --wd wd 86 | ``` 87 | 88 | ```shell 89 | $ ANDROID_HOME/tools/emulator -avd [device-name] 90 | ``` 91 | 92 | 2. Install the instrumented APK in the Android emulator or device: 93 | 94 | ```shell 95 | $ acv install ./wd/instr_snake.apk 96 | ``` 97 | 98 | 3. Activate the app: 99 | 100 | ```shell 101 | $ acv activate com.gnsdm.snake 102 | ``` 103 | 104 | 4. Test the application. 105 | 106 | Interact with the application. 107 | 108 | 5. Do the first coverage snapshot: 109 | 110 | ```shell 111 | $ acv snap com.gnsdm.snake --wd wd 112 | ``` 113 | 114 | 6. Apply coverage data to the Smali code tree: 115 | 116 | ```shell 117 | $ acv cover-pickles com.gnsdm.snake --wd wd 118 | ``` 119 | 120 | 7. Generate the code coverage report. 121 | 122 | ```shell 123 | $ acv report com.gnsdm.snake --wd wd 124 | ``` 125 | 126 | The code coverage report is available at "./wd/report/main_index.html" 127 | 128 | ### Example: App Shrinking 129 | 130 | - Make sure you got the `covered_pickles` files generated in previous steps. 131 | 132 | - Generate the shrunk smali coverage report. 133 | 134 | ```shell 135 | $ acv report com.gnsdm.snake --wd wd --shrink 136 | ``` 137 | 138 | - Generate the shrunk app. 139 | 140 | ```shell 141 | $ acv shrink --wd wd com.gnsdm.snake test_apks/snake.apk 142 | ``` 143 | 144 | The shrunk version is here ./wd/shrunk.apk. 145 | 146 | ### Full list of commands 147 | ```shell 148 | $ acv [-/--options] 149 | ``` 150 | 151 | positional arguments: 152 | 153 | | command | argument | description | options | 154 | | :----------- | :----------- | :--------------------------------------- | :----------------------------------- | 155 | | instrument | path_to_apk | Instruments an apk | --wd, --dbgstart, --dbgend, --r, --i | 156 | | install | path_to_apk | Installs an apk. | | 157 | | uninstall | path_to_apk | Uninstalls an apk. | | 158 | | activate | package_name | Prepared the app for measurements. | | 159 | | start | package_name | Starts runtime coverage data collection. | | 160 | | stop | - | Stops runtime coverage data collection. | | 161 | | snap | package_name | Saves coverage data. | --wd | 162 | | flush | package_name | Flushes runtime coverage information. | | 163 | | calculate | package_name | Logs current coverage into logcat. | | 164 | | report | package_name | Produces a report. | --wd, --shrink | 165 | | sign | apk_path | Signs and alignes an apk. | | 166 | | shrink | pkg, apk_pth | Generates shrunk code. | --wd | 167 | 168 | optional arguments: 169 | 170 | | option | argument | description | 171 | | :--------- | :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------ | 172 | | -h, --help | - | Shows this help message and exit. | 173 | | --version | - | Shows program's version number and exits. | 174 | | --wd | \ | Path to the directory where the working data is stored. Default: .\smiler\acvtool_working_dir. | 175 | | --dbgstart | \ | For troubleshooting purposes. The number of the first method to be instrumented. Only methods from DBGSTART to DBGEND will be instrumented. | 176 | | -r, --r | - | Working directory (--wd) will be overwritten without asking. | 177 | | -i, --i | - | Installs the application immidiately after instrumenting. | 178 | 179 | 180 | ## References 181 | 182 | Please cite our paper: 183 | ``` 184 | @article{pilgun2020acvtool, 185 | title={Fine-grained code coverage measurement in automated black-box Android testing}, 186 | author={Pilgun, Aleksandr and Gadyatskaya, Olga and Zhauniarovich, Yury and Dashevskyi, Stanislav and Kushniarou, Artsiom and Mauw, Sjouke}, 187 | journal={ACM Transactions on Software Engineering and Methodology (TOSEM)}, 188 | volume={29}, 189 | number={4}, 190 | pages={1--35}, 191 | year={2020}, 192 | publisher={ACM New York, NY, USA} 193 | } 194 | ``` 195 | ``` 196 | @inproceedings{pilgun2020acvcut, 197 | title={Don't Trust Me, Test Me: 100\% Code Coverage for a 3rd-party Android App}, 198 | author={Pilgun, Aleksandr}, 199 | booktitle={2020 27th Asia-Pacific Software Engineering Conference (APSEC)}, 200 | pages={375--384}, 201 | year={2020}, 202 | organization={IEEE} 203 | } 204 | ``` 205 | 206 | ## Contributions 207 | 208 | Contributions to ACVTool is a subject to Contributer License Agreement (to be defined). 209 | 210 | ## License 211 | 212 | Copyright (c) 2024 Aleksandr Pilgun 213 | 214 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use ACVTool files from this repository except in compliance with the License. 215 | You may obtain a copy of the License at 216 | 217 | http://www.apache.org/licenses/LICENSE-2.0 218 | 219 | Unless required by applicable law or agreed to in writing, software 220 | distributed under the License is distributed on an "AS IS" BASIS, 221 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 222 | See the License for the specific language governing permissions and 223 | limitations under the License. 224 | 225 | By sharing this code, the author of ACVTool does not grant or waive any patent rights beyond the scope of this ACVTool repository. 226 | 227 | ## Aknowledgement 228 | 229 | The initial development of ACVTool took place at the University of Luxembourg as part of the FNR DroidMod 2016-2020 research project. This project would not have been possible without the invaluable support of my coauthors: Dr. Olga Gadyatskaya, Dr. Yury Zhauniarovich, and Prof. Dr. Sjouke Mauw. 230 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML==6.0.1 2 | Chameleon==4.5.4 3 | javaobj-py3==0.4.4 4 | six==1.12.0 5 | androguard==4.0.2 6 | pyaxml==0.0.5 7 | typing_extensions==4.7.1 8 | setuptools==70.0.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='acvtool', 5 | version='2.3.2', 6 | author='Aleksandr Pilgun', 7 | author_email='alexand.pilgun@gmail.com', 8 | description="ACVTool is an instrumentation-based tool to measure and visualize instruction coverage for Android apps.", 9 | url='https://github.com/pilgun/acvtool', 10 | packages=find_packages(), 11 | package_data={ 12 | 'acvtool': [ 13 | 'smiler/libs/jars/*', 14 | 'smiler/resources/logging.yaml', 15 | 'smiler/resources/html/.resources/*', 16 | 'smiler/resources/html/.resources/highlight/*', 17 | 'smiler/resources/html/.resources/highlight/styles/*', 18 | 'smiler/resources/html/templates/*', 19 | 'smiler/resources/instrumentation/smali/tool/acv/*', 20 | ],}, 21 | install_requires=[ 22 | 'PyYAML==6.0.1', 23 | 'Chameleon==4.5.4', 24 | 'javaobj-py3==0.4.4', 25 | 'six==1.12.0', 26 | 'androguard==4.0.2', 27 | 'pyaxml==0.0.5', 28 | 'typing_extensions==4.7.1', 29 | 'setuptools==70.0.0' 30 | ], 31 | entry_points={ 32 | 'console_scripts': [ 33 | 'acv=acvtool.acvtool:main', 34 | ] 35 | }, 36 | classifiers=[ 37 | 'Development Status :: 5 - Production/Stable', 38 | 'Intended Audience :: Developers', 39 | 'License :: OSI Approved :: Apache Software License', 40 | 'Programming Language :: Python :: 3', 41 | 'Programming Language :: Python :: 3.6', 42 | 'Programming Language :: Python :: 3.7', 43 | 'Programming Language :: Python :: 3.8', 44 | ], 45 | ) --------------------------------------------------------------------------------