├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── .gitkeep ├── frida-skeleton.py ├── images └── logo.gif ├── lib ├── core │ ├── frida_thread.py │ ├── options.py │ ├── port_manager.py │ ├── project.py │ ├── settings.py │ ├── types.py │ └── watch_thread.py └── utils │ ├── adb.py │ ├── iptables.py │ └── shell.py ├── projects ├── .gitignore └── default │ ├── config.yaml │ └── main.js ├── requirements.txt ├── scripts ├── core │ ├── android_packages.js │ ├── common.js │ └── trace.js └── utils │ ├── bypass.js │ ├── conversion.js │ ├── format.js │ └── jav.js ├── tests └── AndroidToHook │ ├── .gitignore │ ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── io │ │ │ └── github │ │ │ └── margular │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── io │ │ │ │ └── github │ │ │ │ └── margular │ │ │ │ ├── AsyncRequest.java │ │ │ │ └── MainActivity.java │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ └── activity_main.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── test │ │ └── java │ │ └── io │ │ └── github │ │ └── margular │ │ └── ExampleUnitTest.java │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle └── thirdparty └── attrdict ├── __init__.py ├── default.py ├── dictionary.py ├── mapping.py ├── merge.py └── mixins.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | MANIFEST 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | .pytest_cache/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | db.sqlite3 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # Environments 84 | .env 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | env.bak/ 90 | venv.bak/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | 105 | # Built application files 106 | *.apk 107 | *.ap_ 108 | 109 | # Files for the ART/Dalvik VM 110 | *.dex 111 | 112 | # Java class files 113 | *.class 114 | 115 | # Generated files 116 | bin/ 117 | gen/ 118 | out/ 119 | 120 | # Gradle files 121 | .gradle/ 122 | 123 | # Local configuration file (sdk path, etc) 124 | local.properties 125 | 126 | # Proguard folder generated by Eclipse 127 | proguard/ 128 | 129 | # Log Files 130 | 131 | # Android Studio Navigation editor temp files 132 | .navigation/ 133 | 134 | # Android Studio captures folder 135 | captures/ 136 | 137 | # IntelliJ 138 | *.iml 139 | .idea/workspace.xml 140 | .idea/tasks.xml 141 | .idea/gradle.xml 142 | .idea/assetWizardSettings.xml 143 | .idea/dictionaries 144 | .idea/libraries 145 | .idea/caches 146 | 147 | # Keystore files 148 | # Uncomment the following line if you do not want to check your keystore files in. 149 | #*.jks 150 | 151 | # External native build folder generated in Android Studio 2.2 and later 152 | .externalNativeBuild 153 | 154 | # Google Services (e.g. APIs or Firebase) 155 | google-services.json 156 | 157 | # Freeline 158 | freeline.py 159 | freeline/ 160 | freeline_project_description.json 161 | 162 | # fastlane 163 | fastlane/report.xml 164 | fastlane/Preview.html 165 | fastlane/screenshots 166 | fastlane/test_output 167 | fastlane/readme.md 168 | 169 | # logs 170 | logs/ 171 | 172 | # certifications 173 | *.crt 174 | 175 | .idea/ 176 | release/ 177 | frida-server* 178 | *.swp 179 | .vscode 180 | js/ 181 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [3.3] - 2022-12-10 10 | 11 | ### Add 12 | 13 | * 新增--no-root选项,有的设备不支持adb root 14 | * frida-server使用随机端口绕过常规检测 15 | * 在初始化FridaThread时校验设备是否root 16 | 17 | ### Changed 18 | 19 | * 使用enumerate_applications替换enumerate_processes 20 | 21 | ### Fixed 22 | 23 | * 修复有的su不支持后面跟一个- 24 | * attrdict本地模块化以兼容3.10及以上版本 25 | 26 | ## [3.2.3] - 2021-06-24 27 | 28 | ### Add 29 | 30 | * 保存hook脚本的整个内容方便调试和查看 31 | 32 | ### Changed 33 | 34 | * 在调用Trace.javaClassByRegex的时候默认跳过hook系统函数,通常来说我们希望hook的是APP自身的函数。可以通过第二个参数传入true强制hook系统函数 35 | 36 | ### Fixed 37 | 38 | * 修复可能无法退出程序的BUG 39 | 40 | ## [3.2.2] - 2021-06-17 41 | 42 | ### Fixed 43 | 44 | * 修复frida-server启动之前就attach进程导致报错的BUG 45 | 46 | 47 | ## [3.2.1] - 2021-04-18 48 | 49 | ### Add 50 | 51 | * 新增指定设备hook的功能 52 | 53 | 54 | ## [3.2.0] - 2020-12-24 55 | 56 | ### Add 57 | 58 | * 新增Jav.describeObject API,很方便地打印java对象的函数和字段信息 59 | * 加入星链计划2.0 60 | * 添加logo 61 | 62 | ### Changed 63 | 64 | * 迁移wiki到github 65 | * 主程序帮助界面改为中文 66 | 67 | ## [3.1.0] - 2020-06-06 68 | 69 | ### Add 70 | 71 | * 新增spawn选项,现在可以根据选项全局spawn模式,或者在项目配置文件里面配置spawn为true 72 | * 新增项目优先级选项,数字越小越优先加载,用于hook有先后关系的场景,默认为0,默认工程为-100,为最优先 73 | 74 | ### Changed 75 | 76 | * 默认工程的bypass代码更新,现在可以根据新老设备自动切换hook脚本,老设备需要自行上传证书到/data/local/tmp/cert-der.crt 77 | * Common.impl函数大改,现在通过指定对象hook而不是字符串 78 | 79 | ## [3.0.0] - 2020-06-02 80 | 81 | ### Add 82 | 83 | * 新增项目概念,现在可以在projects目录下创建自己的项目 84 | * Javascript函数库新增namespace,每个内置函数库都有了自己的namespace,互不影响 85 | 86 | ### Changed 87 | 88 | * 代码结构变化,Javascript函数库分为内置函数库和用户自定义函数库,scripts下为内置函数库,projects目录下为用户自定义函数库 89 | 90 | ## [2.5.0] - 2020-05-28 91 | 92 | ### Add 93 | 94 | - 新增spawn模式,使用-s参数激活 95 | - 新增byte[]和hex格式之间的转化,并在implementationWrapper内部自动判断是否是byte[],如果是则以hex格式输出 96 | 97 | ### Changed 98 | 99 | - 日志输出格式优化,现在会标记是哪个设备的哪个apk打印的 100 | - 下载frida-server的时候显示百分比优化,现在固定xx.xx%的格式 101 | - PortManager在获取随机端口的时候会剔除本机已开启的端口 102 | 103 | ## [2.4.1] - 2020-03-16 104 | 105 | ### Changed 106 | 107 | - 修复没有-p参数时的找不到iptables的bug 108 | - 取消初始化的时候删除iptables,而在退出时再保证清除 109 | - 修复有的手机调用self.device.kill会报错的问题 110 | 111 | 112 | 113 | ## [2.4.0] - 2020-03-14 114 | 115 | ### Add 116 | 117 | - 解决有的设备通过frida.enumerate_devices()无法列出usb设备,改用frida.get_device_manager().add_remote_device()的方式 118 | 119 | 120 | 121 | ### Changed 122 | 123 | - 添加线程管理器使得子线程能够正常全部退出后再退出主线程 124 | 125 | 126 | 127 | ## [2.3.0] - 2020-03-11 128 | 129 | ### Add 130 | 131 | - 优雅地关闭frida-skeleton使其能够在退出的同时自动清除iptables并关闭frida-server以求对设备影响最小 132 | - 解决windows不能通过CTRL+C关闭frida-skeleton的bug 133 | 134 | 135 | 136 | ## [2.2.0] - 2019-12-22 137 | 138 | ### Changed 139 | 140 | - 优化日志格式 141 | - 代码架构调整  142 | 143 | 144 | 145 | ## [2.1.1] - 2019-12-21 146 | 147 | ### Changed 148 | 149 | - 使得手机流量能够被捕获 150 | 151 | 152 | 153 | ## [2.1.0] - 2019-12-21 154 | 155 | ### Changed 156 | 157 | - 更通用的iptables设置用以重定向TCP流量 158 | 159 | 160 | 161 | ## [2.0.0] - 2019-12-20 162 | 163 | ### Added 164 | 165 | - 多彩日志 166 | - 利用iptables和adb实现TCP流量重定向 167 | - 面向对象化 168 | - 捕获异常 169 | - 自动下载/安装/运行frida-server一条龙服务 170 | 171 | 172 | 173 | ## [1.1.0] - 2019-12-16 174 | 175 | ### Added 176 | 177 | - 一次性hook多个usb设备 178 | 179 | 180 | 181 | ## [1.0.0] - 2019-09-15 182 | ### Added 183 | - 通过正则表达式匹配包名 184 | - 自动打印日志 185 | - Frida-Skeleton独有的implementationWrapper更方便地写hook代码 186 | - 自动绕过证书绑定校验 187 | - 内置Java类hook方法 188 | - 内置jni函数hook方法 189 | - 良好的可扩展性 190 | 191 | [Unreleased]: https://github.com/Margular/frida-skeleton/compare/v3.3...HEAD 192 | [3.3]: https://github.com/Margular/frida-skeleton/compare/v3.2.3...v3.3 193 | [3.2.3]: https://github.com/Margular/frida-skeleton/compare/v3.2.2...v3.2.3 194 | [3.2.2]: https://github.com/Margular/frida-skeleton/compare/v3.2.1...v3.2.2 195 | [3.2.1]: https://github.com/Margular/frida-skeleton/compare/v3.2.0...v3.2.1 196 | [3.2.0]: https://github.com/Margular/frida-skeleton/compare/v3.1.0...v3.2.0 197 | [3.1.0]: https://github.com/Margular/frida-skeleton/compare/v3.0.0...v3.1.0 198 | [3.0.0]: https://github.com/Margular/frida-skeleton/compare/v2.5.0...v3.0.0 199 | [2.5.0]: https://github.com/Margular/frida-skeleton/compare/v2.4.1...v2.5.0 200 | [2.4.1]: https://github.com/Margular/frida-skeleton/compare/v2.4.0...v2.4.1 201 | [2.4.0]: https://github.com/Margular/frida-skeleton/compare/v2.3.0...v2.4.0 202 | [2.3.0]: https://github.com/Margular/frida-skeleton/compare/v2.2.0...v2.3.0 203 | [2.2.0]: https://github.com/Margular/frida-skeleton/compare/v2.1.1...v2.2.0 204 | [2.1.1]: https://github.com/Margular/frida-skeleton/compare/v2.1.0...v2.1.1 205 | [2.1.0]: https://github.com/Margular/frida-skeleton/compare/v2.0.0...v2.1.0 206 | [2.0.0]: https://github.com/Margular/frida-skeleton/compare/v1.1.0...v2.0.0 207 | [1.1.0]: https://github.com/Margular/frida-skeleton/compare/v1.0.0...v1.1.0 208 | [1.0.0]: https://github.com/Margular/frida-skeleton/releases/tag/v1.0.0 209 | 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Margular 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # frida-skeleton 4 | 5 | 6 | 7 | [![Contributors][contributors-shield]][contributors-url] 8 | [![Forks][forks-shield]][forks-url] 9 | [![Stargazers][stars-shield]][stars-url] 10 | [![Issues][issues-shield]][issues-url] 11 | [![MIT License][license-shield]][license-url] 12 | 13 | 14 |
15 | 16 |

17 | 18 | Logo 19 | 20 |

21 | 探索本项目的文档 » 22 |
23 |
24 | 报告Bug 25 | · 26 | 提出新特性 27 |

28 | 29 |

30 | 31 | ## 目录 32 | 33 | - [简介](#简介) 34 | - [上手指南](#上手指南) 35 | - [开发前的配置要求](#开发前的配置要求) 36 | - [安装步骤](#安装步骤) 37 | - [文件目录说明](#文件目录说明) 38 | - [如何参与开源项目](#如何参与开源项目) 39 | - [版本控制](#版本控制) 40 | - [版权说明](#版权说明) 41 | - [鸣谢](#鸣谢) 42 | 43 | ### 简介 44 | 45 | `frida-skeleton`是基于frida的安卓hook框架,提供了很多frida自身不支持的功能,将hook安卓变成简单便捷,人人都会的事情,主要有: 46 | 47 | - 根据正则表达式批量hook安卓应用,支持多线程,可同时hook多个设备互不影响 48 | - 针对不同的应用可以同时加载不同的hook脚本,且支持优先级配置 49 | - 自动将手机上的所有TCP流量重定向到PC上的抓包工具如BurpSuite,无需手动配置,且自动绕过证书绑定机制 50 | - 丰富的日志记录功能,让你的hook历史永不丢失 51 | - 自动识别当前使用的frida版本并下载对应版本的frida-server到/data/local/tmp运行 52 | - 提供封装好的实用API以减少日常工作中的重复劳动 53 | 54 | ### 上手指南 55 | 56 | ###### 开发前的配置要求 57 | 58 | - Python3 59 | 60 | ###### 安装步骤 61 | 62 | 1. 克隆本项目到本地 63 | 64 | ```sh 65 | git clone https://github.com/Margular/frida-skeleton.git 66 | ``` 67 | 68 | 2. 安装第三方依赖库 69 | 70 | ```sh 71 | pip install -r requirements.txt 72 | ``` 73 | 74 | ###### 查看说明 75 | 76 | ```sh 77 | python frida-skeleton.py -h 78 | ``` 79 | 80 | 详细说明请移步[WIKI](https://github.com/Margular/frida-skeleton/wiki) 81 | 82 | ### 文件目录说明 83 | 84 | ``` 85 | 文件目录 86 | ├── CHANGELOG.md 项目改动记录 87 | ├── LICENSE 许可证 88 | ├── README.md 本文档 89 | ├── /assets/ 下载的frida-server存放的位置 90 | ├── frida-skeleton.py 项目入口 91 | ├── /images/ 本项目用到的图像资源文件 92 | ├── /lib/ Python库文件,frida-skeleton核心实现部分 93 | ├── /logs/ hook日志记录文件夹 94 | ├── /projects/ hook脚本存放的文件夹,以目录区分项目 95 | ├── requirements.txt 三方库需求列表 96 | ├── /scripts/ 封装好的实用API 97 | └── /tests/ 提供测试的安卓项目 98 | ``` 99 | 100 | ### 如何参与开源项目 101 | 102 | 贡献使开源社区成为一个学习、激励和创造的绝佳场所。你所作的任何贡献都是**非常感谢**的。 103 | 104 | 105 | 1. Fork本项目 106 | 2. 创建开发分支 (`git checkout -b dev`) 107 | 3. 提交更改 (`git commit -m 'Add something'`) 108 | 4. 推送到分支 (`git push origin dev`) 109 | 5. 提[Pull Request](https://github.com/Margular/frida-skeleton/compare) 110 | 111 | ### 版本控制 112 | 113 | 该项目使用Git进行版本管理。您可以在repository参看当前可用版本。 114 | 115 | ### 版权说明 116 | 117 | 该项目签署了MIT 授权许可,详情请参阅 [LICENSE](https://github.com/Margular/frida-skeleton/blob/master/LICENSE) 118 | 119 | ### 鸣谢 120 | 121 | - [frida](https://frida.re/) 122 | - [frida-snippets](https://github.com/iddoeldor/frida-snippets) 123 | - [Img Shields](https://shields.io) 124 | - [Choose an Open Source License](https://choosealicense.com) 125 | 126 | 127 | 128 | [contributors-shield]: https://img.shields.io/github/contributors/Margular/frida-skeleton.svg?style=flat-square 129 | [contributors-url]: https://github.com/Margular/frida-skeleton/graphs/contributors 130 | [forks-shield]: https://img.shields.io/github/forks/Margular/frida-skeleton.svg?style=flat-square 131 | [forks-url]: https://github.com/Margular/frida-skeleton/network/members 132 | [stars-shield]: https://img.shields.io/github/stars/Margular/frida-skeleton.svg?style=flat-square 133 | [stars-url]: https://github.com/Margular/frida-skeleton/stargazers 134 | [issues-shield]: https://img.shields.io/github/issues/Margular/frida-skeleton.svg?style=flat-square 135 | [issues-url]: https://img.shields.io/github/issues/Margular/frida-skeleton.svg 136 | [license-shield]: https://img.shields.io/github/license/Margular/frida-skeleton.svg?style=flat-square 137 | [license-url]: https://github.com/Margular/frida-skeleton/blob/master/LICENSE 138 | 139 | # 404StarLink 2.0 - Galaxy 140 | ![](https://github.com/knownsec/404StarLink-Project/raw/master/logo.png) 141 | 142 | frida-skeleton 是 404Team [星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy)中的一环,如果对frida-skeleton 有任何疑问又或是想要找小伙伴交流,可以参考星链计划的加群方式。 143 | 144 | - [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community) 145 | 146 | -------------------------------------------------------------------------------- /assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Margular/frida-skeleton/82db1b25bd7134cd5917cc95628d057ae28e48a0/assets/.gitkeep -------------------------------------------------------------------------------- /frida-skeleton.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | import os 6 | import signal 7 | import sys 8 | import time 9 | 10 | import coloredlogs 11 | import urllib3 12 | 13 | from lib.core.options import options 14 | from lib.core.settings import LOG_DIR, LOG_FILENAME 15 | from lib.core.watch_thread import WatchThread 16 | from lib.utils.adb import Adb 17 | 18 | # disable ssl warnings 19 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 20 | 21 | 22 | class MainExit(Exception): 23 | pass 24 | 25 | 26 | class FridaSkeleton: 27 | 28 | def __init__(self): 29 | self.log = logging.getLogger(self.__class__.__name__) 30 | 31 | def start(self): 32 | try: 33 | if options.list: 34 | for device in Adb.devices().out.split('\n')[1:]: 35 | print(device) 36 | sys.exit(0) 37 | 38 | level = logging.DEBUG if options.verbose else logging.INFO 39 | coloredlogs.install(level=level) 40 | 41 | # set log 42 | os.makedirs(LOG_DIR, mode=0o700, exist_ok=True) 43 | log_file = open(os.path.join(LOG_DIR, LOG_FILENAME), 'a', encoding='utf-8') 44 | coloredlogs.install(level=level, stream=log_file) 45 | 46 | # set handling interrupt exceptions 47 | signal.signal(signal.SIGTERM, self.shutdown) 48 | signal.signal(signal.SIGINT, self.shutdown) 49 | 50 | Adb.start_server() 51 | 52 | watch_thread = WatchThread() 53 | 54 | try: 55 | watch_thread.start() 56 | while True: 57 | time.sleep(1) 58 | except MainExit: 59 | while True: 60 | try: 61 | self.log.info('shutdown command received, wait for clean up please...') 62 | watch_thread.terminate() 63 | while watch_thread.is_alive(): 64 | time.sleep(1) 65 | break 66 | except MainExit: 67 | pass 68 | except (KeyboardInterrupt, InterruptedError): 69 | pass 70 | 71 | self.log.info('thank you for using, bye!') 72 | 73 | def shutdown(self, signum, frame): 74 | if signum == signal.SIGINT: 75 | self.log.debug('keyboard interrupt event detected') 76 | elif signum == signal.SIGTERM: 77 | self.log.debug('termination event detected') 78 | else: 79 | self.log.warning('unknown event detected') 80 | 81 | raise MainExit 82 | 83 | 84 | if __name__ == '__main__': 85 | skeleton = FridaSkeleton() 86 | skeleton.start() 87 | -------------------------------------------------------------------------------- /images/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Margular/frida-skeleton/82db1b25bd7134cd5917cc95628d057ae28e48a0/images/logo.gif -------------------------------------------------------------------------------- /lib/core/frida_thread.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | import lzma 6 | import os 7 | import re 8 | import sys 9 | import threading 10 | import time 11 | from concurrent.futures.thread import ThreadPoolExecutor 12 | 13 | import frida 14 | import requests 15 | 16 | from lib.core.options import options 17 | from lib.core.port_manager import port_manager 18 | from lib.core.project import Project 19 | from lib.core.settings import ROOT_DIR, FRIDA_SERVER_DEFAULT_PORT 20 | from lib.core.types import FakeDevice 21 | from lib.utils.adb import Adb 22 | from lib.utils.iptables import Iptables 23 | 24 | __lock__ = threading.Lock() 25 | 26 | 27 | class FridaThread(threading.Thread): 28 | 29 | def __init__(self, device): 30 | super().__init__( 31 | name="{}-{}".format(self.__class__.__name__, device.id)) 32 | self.thread_tag = self.name 33 | 34 | self.server_executor = ThreadPoolExecutor(max_workers=1) 35 | self.log = logging.getLogger(self.thread_tag) 36 | 37 | self.frida_server_port = FRIDA_SERVER_DEFAULT_PORT 38 | 39 | if options.random_port: 40 | self.frida_server_port = port_manager.acquire_port( 41 | excludes=[FRIDA_SERVER_DEFAULT_PORT]) 42 | self.log.info("pick random port {} for frida-server".format( 43 | self.frida_server_port)) 44 | 45 | if options.random_port or device.type == FakeDevice.type: 46 | # init remote device 47 | self.log.debug('device {} does not support get_usb_device, ' 48 | 'changing to get_remote_device method' 49 | .format(device.id)) 50 | 51 | self.forward_port = port_manager.acquire_port( 52 | excludes=[options.port]) 53 | 54 | self.device = frida.get_device_manager().add_remote_device( 55 | '127.0.0.1:{}'.format(self.forward_port)) 56 | 57 | self.device.id = device.id 58 | else: 59 | self.device = device 60 | 61 | self.adb = Adb(self.device.id) 62 | 63 | self.log.info("frida-server port: {}".format(self.frida_server_port)) 64 | if options.random_port or device.type == FakeDevice.type: 65 | result = self.adb.forward(self.forward_port, self.frida_server_port) 66 | # port has been used 67 | if result.err: 68 | port_manager.release_port(self.forward_port) 69 | raise RuntimeError('port {} has been used'.format( 70 | self.forward_port)) 71 | 72 | if options.port: 73 | self.iptables = Iptables(self.adb, options.port) 74 | 75 | self.arch = self.adb.unsafe_shell("getprop ro.product.cpu.abi").out 76 | # maybe get 'arm64-v8a', 'arm-v7a' ... 77 | if 'arm64' in self.arch: 78 | self.arch = 'arm64' 79 | elif 'arm' in self.arch: 80 | self.arch = 'arm' 81 | elif 'x86_64' in self.arch: 82 | self.arch = 'x86_64' 83 | elif 'x86' in self.arch: 84 | self.arch = 'x86' 85 | else: 86 | raise RuntimeError('unknown arch: ' + self.arch) 87 | 88 | self.server_name = 'frida-server-{}-android-{}'.format( 89 | frida.__version__, self.arch) 90 | 91 | self._terminate = False 92 | 93 | def run(self) -> None: 94 | self.log.info("start with hook device: id={}, name={}, type={}".format( 95 | self.device.id, self.device.name, self.device.type)) 96 | 97 | try: 98 | self.prepare() 99 | self.hook_apps() 100 | except Exception as e: 101 | self.log.error('device {}: {}'.format(self.device.id, e)) 102 | 103 | try: 104 | self.shutdown() 105 | except Exception as e: 106 | self.log.error( 107 | 'unexpected error occurred when shutdown device {}: {}' 108 | .format(self.device.id, e)) 109 | 110 | self.log.debug('device {} exit'.format(self.device.id)) 111 | 112 | # prepare for starting hook 113 | def prepare(self): 114 | if self._terminate: 115 | return 116 | 117 | # get root 118 | if not options.no_root: 119 | self.adb.root() 120 | 121 | # close selinux 122 | self.adb.unsafe_shell('setenforce 0', root=True) 123 | 124 | # install iptables and reverse tcp port 125 | if options.port: 126 | # enable tcp connections between frida server and binding 127 | self.iptables.install() 128 | self.adb.reverse(options.port) 129 | 130 | if options.install: 131 | self.install_frida_server() 132 | 133 | self.kill_frida_servers() 134 | self.run_frida_server() 135 | 136 | def download(self, url, file_path): 137 | # get total size of file 138 | r1 = requests.get(url, stream=True, verify=False) 139 | total_size = int(r1.headers['Content-Length']) 140 | 141 | # check downloaded size 142 | if os.path.exists(file_path): 143 | temp_size = os.path.getsize(file_path) 144 | else: 145 | temp_size = 0 146 | 147 | if temp_size == total_size: 148 | self.log.info('{} has downloaded completely'.format(file_path)) 149 | return 150 | 151 | if temp_size > total_size: 152 | self.log.error( 153 | '{} has corrupted, download it again'.format(file_path)) 154 | os.remove(file_path) 155 | return self.download(url, file_path) 156 | 157 | self.log.debug('{} of {} needs to be download'.format( 158 | total_size - temp_size, total_size)) 159 | 160 | # download from temp size to end 161 | headers = {'Range': 'bytes={}-'.format(temp_size)} 162 | 163 | r = requests.get(url, stream=True, verify=False, headers=headers) 164 | 165 | with open(file_path, "ab") as f: 166 | for chunk in r.iter_content(chunk_size=1024): 167 | if self._terminate: 168 | break 169 | 170 | if chunk: 171 | temp_size += len(chunk) 172 | f.write(chunk) 173 | f.flush() 174 | 175 | # download progress 176 | done = int(50 * temp_size / total_size) 177 | sys.stdout.write( 178 | "\r[{}{}] {:.2f}%".format( 179 | '█' * done, 180 | ' ' * (50 - done), 181 | 100 * temp_size / total_size) 182 | ) 183 | sys.stdout.flush() 184 | 185 | sys.stdout.write(os.linesep) 186 | 187 | def install_frida_server(self): 188 | server_path = os.path.join(ROOT_DIR, 'assets', self.server_name) 189 | server_path_xz = server_path + '.xz' 190 | 191 | # if not exist frida server then install it 192 | if not self.adb.unsafe_shell( 193 | "ls /data/local/tmp/" + self.server_name).out: 194 | self.log.info( 195 | 'download {} from github ...'.format(self.server_name)) 196 | 197 | with __lock__: 198 | self.download( 199 | 'https://github.com/frida/frida/releases/download/{}/{}.xz' 200 | .format(frida.__version__, self.server_name), 201 | server_path_xz 202 | ) 203 | 204 | # extract frida server 205 | with open(server_path, 'wb') as f: 206 | with lzma.open(server_path_xz) as xz: 207 | f.write(xz.read()) 208 | 209 | # upload frida server 210 | self.log.info("pushing frida-server-{}...".format(self.server_name)) 211 | self.adb.push(server_path, '/data/local/tmp/') 212 | 213 | def kill_frida_servers(self): 214 | try: 215 | apps = self.device.enumerate_processes() 216 | except (frida.ServerNotRunningError, frida.TransportError, 217 | frida.InvalidOperationError): 218 | # frida server has not been started, no need to start 219 | return 220 | 221 | for app in apps: 222 | if app.name == self.server_name: 223 | self.log.debug("killing {}...".format(self.server_name)) 224 | self.adb.unsafe_shell( 225 | 'kill -9 {}'.format(app.pid), root=True, quiet=True) 226 | time.sleep(0.5) 227 | 228 | def run_frida_server(self): 229 | self.adb.unsafe_shell('chmod +x /data/local/tmp/' + self.server_name) 230 | self.server_executor.submit( 231 | self.adb.unsafe_shell, 232 | '/data/local/tmp/{} -l 127.0.0.1:{} -D'.format( 233 | self.server_name, self.frida_server_port 234 | ), 235 | True 236 | ) 237 | 238 | # waiting for frida server 239 | max_try = 100 240 | while True: 241 | try: 242 | if max_try < 0: 243 | self.log.info( 244 | "{} can't run frida server ".format(self.thread_tag)) 245 | self.terminate() 246 | return 247 | time.sleep(0.5) 248 | if not self._terminate: 249 | self.device.enumerate_processes() 250 | self.log.info("frida-server connected") 251 | break 252 | except (frida.ServerNotRunningError, frida.TransportError, 253 | frida.InvalidOperationError): 254 | max_try -= 1 255 | continue 256 | 257 | def hook_apps(self): 258 | apps = set() 259 | 260 | self.log.info("filter: {}".format(options.regexps)) 261 | # monitor apps 262 | while True: 263 | if self._terminate: 264 | break 265 | 266 | time.sleep(0.1) 267 | 268 | new_apps = set('{}:{}'.format(p.pid, p.identifier) for p in 269 | self.device.enumerate_applications()) 270 | 271 | if not new_apps: 272 | continue 273 | 274 | incremental_apps = new_apps - apps 275 | decremental_apps = apps - new_apps 276 | 277 | for incremental_app in incremental_apps: 278 | pid, name = incremental_app.split(':', 1) 279 | 280 | if self._terminate: 281 | break 282 | 283 | for regexp in options.regexps: 284 | if re.search(regexp, name): 285 | # waiting for app startup completely 286 | time.sleep(0.1) 287 | 288 | try: 289 | self.hook(int(pid), name) 290 | except Exception as e: 291 | self.log.error( 292 | 'error occurred when hook {}@{}: {}'.format( 293 | name, pid, e)) 294 | finally: 295 | break 296 | 297 | for decremental_app in decremental_apps: 298 | pid, name = decremental_app.split(':', 1) 299 | 300 | if self._terminate: 301 | break 302 | 303 | for regexp in options.regexps: 304 | if re.search(regexp, name): 305 | self.log.info('{}[pid:{}] has died'.format(name, pid)) 306 | break 307 | 308 | apps = new_apps 309 | 310 | def hook(self, pid, name): 311 | if self._terminate: 312 | return 313 | 314 | js = Project.preload() 315 | spawn = options.spawn 316 | projects = [] 317 | mode = "attach" if not spawn else "spawn" 318 | self.log.info('hook {}[pid={}] mode: {}'.format(name, pid, mode)) 319 | 320 | for project in Project.scan(os.path.join(ROOT_DIR, 'projects')): 321 | projects.append(project) 322 | 323 | projects.sort(key=lambda p: p.priority) 324 | 325 | for project in projects: 326 | if project.enable: 327 | # if app match regexp 328 | if not re.search(project.regexp, name): 329 | continue 330 | 331 | js += project.load(name) 332 | if project.spawn: 333 | spawn = True 334 | 335 | js += Project.postload() 336 | 337 | # save js content 338 | os.makedirs(os.path.join(ROOT_DIR, 'js'), exist_ok=True) 339 | open(os.path.join(ROOT_DIR, 'js', name + ".js"), 'w').write(js) 340 | 341 | while True: 342 | try: 343 | if spawn: 344 | process = self.device.attach(self.device.spawn(name)) 345 | else: 346 | process = self.device.attach(pid) 347 | break 348 | except frida.ServerNotRunningError: 349 | if self._terminate: 350 | return 351 | 352 | self.log.warning("frida server not running, wait one second") 353 | time.sleep(1) 354 | 355 | # wait for the app to start otherwise it will not hook the java function 356 | time.sleep(1) 357 | 358 | script = process.create_script(js) 359 | script.on('message', self.on_message(name)) 360 | script.load() 361 | 362 | if spawn: 363 | self.device.resume(pid) 364 | 365 | def on_message(self, app: str): 366 | app_log = logging.getLogger( 367 | '{}|{}|{}'.format(self.__class__.__name__, self.device.id, app)) 368 | 369 | def on_message_inner(message, data): 370 | try: 371 | if message['type'] == 'error': 372 | text = message['description'].strip() 373 | 374 | if not text: 375 | return 376 | 377 | app_log.error(text) 378 | else: 379 | if message['type'] == 'send': 380 | text = message['payload'].strip() 381 | else: 382 | text = message.strip() 383 | 384 | if not text: 385 | return 386 | 387 | app_log.info(text) 388 | except Exception as e: 389 | app_log.error(e) 390 | 391 | return on_message_inner 392 | 393 | def terminate(self): 394 | self._terminate = True 395 | 396 | def shutdown(self): 397 | self.log.debug('shutdown device ' + self.device.id) 398 | 399 | if self.device.type == 'remote': 400 | port_manager.release_port(self.forward_port) 401 | self.adb.clear_forward(self.forward_port) 402 | 403 | if options.port: 404 | self.iptables.uninstall() 405 | self.adb.clear_reverse(options.port) 406 | 407 | self.kill_frida_servers() 408 | self.server_executor.shutdown() 409 | -------------------------------------------------------------------------------- /lib/core/options.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import argparse 5 | 6 | 7 | class Options: 8 | def __init__(self): 9 | parser = argparse.ArgumentParser(description='基于frida的安卓hook框架,提供了很多frida自身不支持的功能,' 10 | '将hook安卓变成简单便捷,人人都会的事情,项目地址:' 11 | 'https://github.com/Margular/frida-skeleton') 12 | 13 | parser.add_argument('regexps', nargs='*', default=[r'^com\.'], 14 | help=r'根据你指定的正则表达式去匹配包名hook对应的程序,支持多个正则表达式') 15 | parser.add_argument('-l', '--list', action='store_true', help='显示设备列表') 16 | parser.add_argument('-d', '--devices', type=str, help='指定hook的设备,多个设备逗号隔开') 17 | parser.add_argument('-i', '--install', action='store_true', 18 | help='自动从github安装对应版本和架构的frida-server到assets目录下,支持断点续传,下载完后自动运行') 19 | parser.add_argument('-p', '--port', type=int, 20 | help='自动利用iptables和adb将所有的TCP流量重定向到PC的指定端口,这样就可以在本机监听该端口来抓包了') 21 | parser.add_argument('-n', '--no-root', action='store_true', help='不尝试使用adb root获取root权限,默认尝试') 22 | parser.add_argument('-s', '--spawn', action='store_true', 23 | help='开启frida的spawn模式并忽略项目配置文件中的spawn选项,开启此选项会导致被hook的进程自动重启') 24 | parser.add_argument("-r", "--random-port",help="随机生成frida-server监听端口,若不开启则使用默认端口27042", 25 | action='store_true') 26 | parser.add_argument('-v', '--verbose', action='store_true', help='输出调试信息') 27 | 28 | args = parser.parse_args() 29 | 30 | self.list = args.list 31 | self.devices = args.devices 32 | self.regexps = args.regexps 33 | self.install = args.install 34 | self.port = args.port 35 | self.no_root = args.no_root 36 | self.spawn = args.spawn 37 | self.random_port = args.random_port 38 | self.verbose = args.verbose 39 | 40 | 41 | options = Options() 42 | -------------------------------------------------------------------------------- /lib/core/port_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import secrets 5 | import threading 6 | 7 | __lock__ = threading.Lock() 8 | 9 | 10 | class PortManager: 11 | def __init__(self): 12 | self.port_map = {} 13 | 14 | @classmethod 15 | def secure_rand_port(cls): 16 | while True: 17 | port = secrets.randbits(16) 18 | if port < 1024: 19 | continue 20 | return port 21 | 22 | def acquire_port(self, excludes=None): 23 | with __lock__: 24 | while True: 25 | port = PortManager.secure_rand_port() 26 | 27 | if port in self.port_map.keys() or port in excludes: 28 | continue 29 | 30 | self.port_map[port] = True 31 | return port 32 | 33 | def release_port(self, port): 34 | with __lock__: 35 | assert port in self.port_map.keys() 36 | self.port_map.pop(port) 37 | 38 | 39 | port_manager = PortManager() 40 | -------------------------------------------------------------------------------- /lib/core/project.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import logging 4 | import os 5 | import threading 6 | 7 | import yaml 8 | 9 | from lib.core.settings import ROOT_DIR, PROJECT_CONFIG_FILENAME 10 | from thirdparty.attrdict import AttrDict 11 | 12 | __lock__ = threading.Lock() 13 | 14 | 15 | class Project: 16 | 17 | def __init__(self, path: str, enable:bool, name: str, regexp: str, spawn: bool, priority: int): 18 | self.log = logging.getLogger(self.__class__.__name__ + '|' + name) 19 | 20 | self.path = path 21 | self.enable = enable 22 | self.name = name 23 | self.regexp = regexp 24 | self.spawn = spawn 25 | self.priority = priority 26 | 27 | @classmethod 28 | def logger(cls): 29 | with __lock__: 30 | if hasattr(cls, 'log'): 31 | return cls.log 32 | else: 33 | return logging.getLogger(cls.__name__) 34 | 35 | @classmethod 36 | def preload(cls) -> str: 37 | js = 'Java.perform(function() {\n' 38 | 39 | # recursively load js files in the script directory 40 | for (dirpath, dirnames, filenames) in os.walk(os.path.join(ROOT_DIR, 'scripts')): 41 | for filename in filenames: 42 | js += open(os.path.join(dirpath, filename), encoding="utf-8").read() + '\n' 43 | 44 | return js 45 | 46 | @classmethod 47 | def postload(cls) -> str: 48 | return '});' 49 | 50 | @classmethod 51 | def scan(cls, path: str): 52 | for entry in os.scandir(path): 53 | if entry.is_dir(): 54 | try: 55 | config = AttrDict(yaml.safe_load(open(os.path.join(entry.path, PROJECT_CONFIG_FILENAME)))) 56 | if config.regexp: 57 | yield Project(entry.path, 58 | config.enable if 'enable' in config.keys() else True, 59 | os.path.basename(entry.path), 60 | config.regexp, 61 | config.spawn if 'spawn' in config.keys() else False, 62 | config.priority if 'priority' in config.keys() else 0) 63 | except yaml.YAMLError as e: 64 | cls.logger().error('error in configuration file: {}'.format(e)) 65 | 66 | def load(self, app: str) -> str: 67 | self.log.debug('loading script for {}...'.format(app)) 68 | 69 | js = '' 70 | 71 | # recursively load js files 72 | for (dirpath, dirnames, filenames) in os.walk(self.path): 73 | for filename in filenames: 74 | if os.path.splitext(filename)[1] != '.js': 75 | continue 76 | js += open(os.path.join(dirpath, filename), encoding="utf-8").read() + '\n' 77 | 78 | return js 79 | 80 | -------------------------------------------------------------------------------- /lib/core/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | import time 7 | 8 | import coloredlogs 9 | 10 | # The name of the operating system dependent module imported. 11 | # The following names have currently been registered: 'posix', 'nt', 'mac', 'os2', 'ce', 'java', 'riscos' 12 | PLATFORM = os.name 13 | IS_WIN = PLATFORM == "nt" 14 | ROOT_DIR = os.path.dirname(sys.argv[0]) 15 | LOG_DIR = os.path.join(ROOT_DIR, 'logs') 16 | LOG_FILENAME = time.strftime('%Y-%m-%d_%H-%M-%S.log') 17 | FRIDA_SERVER_DEFAULT_PORT = 27042 18 | PROJECT_CONFIG_FILENAME = 'config.yaml' 19 | 20 | # coloredlogs 21 | coloredlogs.DEFAULT_LOG_FORMAT = "[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s" 22 | -------------------------------------------------------------------------------- /lib/core/types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class FakeDevice: 6 | id = 'fake id' 7 | type = 'fake' 8 | -------------------------------------------------------------------------------- /lib/core/watch_thread.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | import threading 6 | import time 7 | 8 | import frida 9 | 10 | from lib.core.frida_thread import FridaThread 11 | from lib.core.options import options 12 | from lib.core.types import FakeDevice 13 | from lib.utils.adb import Adb 14 | 15 | 16 | class WatchThread(threading.Thread): 17 | 18 | def __init__(self): 19 | super().__init__() 20 | 21 | self.log = logging.getLogger(self.__class__.__name__) 22 | 23 | self.frida_threads = [] 24 | self._terminate = False 25 | 26 | def run(self) -> None: 27 | self.log.debug('{} start'.format(self.__class__.__name__)) 28 | 29 | while True: 30 | if self._terminate: 31 | break 32 | 33 | devices = frida.enumerate_devices() 34 | 35 | # usb devices from frida api 36 | usb_devices = [device for device in devices if device.type == 'usb'] 37 | usb_devices_ids = [device.id for device in usb_devices] 38 | 39 | # devices strings from "adb devices" 40 | adb_devices_strings = Adb.devices().out.split('\n')[1:] 41 | adb_devices_strings = [_.split('\t')[0] for _ in adb_devices_strings] 42 | 43 | # we need to access these devices remotely 44 | remote_devices_strings = set(adb_devices_strings) - set(usb_devices_ids) 45 | remote_devices = [] 46 | 47 | for _ in remote_devices_strings: 48 | new_device = FakeDevice() 49 | new_device.id = _ 50 | remote_devices.append(new_device) 51 | 52 | for device in usb_devices + remote_devices: 53 | # only hook specified devices 54 | if options.devices and device.id not in options.devices: 55 | continue 56 | 57 | duplicated = False 58 | 59 | for t in self.frida_threads: 60 | if t.device.id == device.id: 61 | if not t.is_alive(): 62 | self.frida_threads.remove(t) 63 | break 64 | 65 | duplicated = True 66 | break 67 | 68 | if duplicated: 69 | continue 70 | 71 | try: 72 | frida_thread = FridaThread(device) 73 | except RuntimeError as e: 74 | self.log.error('error occurred when init frida thread: {}'.format(e)) 75 | else: 76 | frida_thread.start() 77 | self.frida_threads.append(frida_thread) 78 | 79 | time.sleep(0.1) 80 | 81 | self.shutdown() 82 | self.log.debug('watch thread exit') 83 | 84 | def terminate(self): 85 | self._terminate = True 86 | 87 | def shutdown(self): 88 | for frida_thread in self.frida_threads: 89 | if frida_thread.is_alive(): 90 | self.log.debug('waiting for {}'.format(frida_thread.device)) 91 | frida_thread.terminate() 92 | 93 | for frida_thread in self.frida_threads: 94 | while frida_thread.is_alive(): 95 | time.sleep(1) 96 | -------------------------------------------------------------------------------- /lib/utils/adb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from lib.utils.shell import Shell 5 | 6 | 7 | class Adb(Shell): 8 | def __init__(self, serial): 9 | super().__init__() 10 | 11 | self.serial = serial 12 | # if we are root shell 13 | self.is_root = False 14 | self.check_root() 15 | 16 | @classmethod 17 | def start_server(cls): 18 | return Shell().exec('adb start-server', supress_error=True) 19 | 20 | @classmethod 21 | def devices(cls): 22 | return Shell().exec('adb devices', quiet=True) 23 | 24 | def check_root(self): 25 | if self.unsafe_shell('whoami').out == 'root': 26 | self.is_root = True 27 | 28 | def root(self): 29 | self.exec('adb -s "{}" root'.format(self.serial)) 30 | self.check_root() 31 | 32 | def unsafe_shell(self, command, root=False, quiet=False): 33 | return self.exec(r'''adb -s "{}" shell "{}{}"'''.format( 34 | self.serial, 'su -c ' if root and not self.is_root else '', command), quiet) 35 | 36 | def push(self, src, dst): 37 | return self.exec('adb -s "{}" push "{}" "{}"'.format(self.serial, src, dst)) 38 | 39 | def reverse(self, port): 40 | return self.exec('adb -s "{0}" reverse tcp:{1} tcp:{1}'.format(self.serial, port)) 41 | 42 | def clear_reverse(self, remote_port): 43 | return self.exec('adb -s "{}" reverse --remove tcp:{}'.format(self.serial, remote_port)) 44 | 45 | def forward(self, local_port, remote_port): 46 | return self.exec('adb -s "{}" forward tcp:{} tcp:{}'.format(self.serial, local_port, remote_port)) 47 | 48 | def clear_forward(self, local_port): 49 | return self.exec('adb -s "{}" forward --remove tcp:{}'.format(self.serial, local_port)) 50 | -------------------------------------------------------------------------------- /lib/utils/iptables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class Iptables: 6 | 7 | def __init__(self, adb, port): 8 | self.adb = adb 9 | self.port = port 10 | self.install_cmd = 'iptables -t nat -A OUTPUT -p tcp -o lo -j RETURN' 11 | self.install_cmd2 = 'iptables -t nat -A OUTPUT -p tcp -j DNAT --to-destination 127.0.0.1:{}'.format(port) 12 | self.uninstall_cmd = 'iptables -t nat -D OUTPUT -p tcp -o lo -j RETURN' 13 | self.uninstall_cmd2 = 'iptables -t nat -D OUTPUT -p tcp -j DNAT --to-destination 127.0.0.1:{}'.format(port) 14 | 15 | def uninstall(self): 16 | self.adb.unsafe_shell(self.uninstall_cmd, root=True) 17 | self.adb.unsafe_shell(self.uninstall_cmd2, root=True) 18 | 19 | def install(self): 20 | self.adb.unsafe_shell(self.install_cmd, root=True) 21 | self.adb.unsafe_shell(self.install_cmd2, root=True) 22 | -------------------------------------------------------------------------------- /lib/utils/shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | from subprocess import Popen, PIPE 6 | 7 | from thirdparty.attrdict import AttrDict 8 | 9 | 10 | class Shell: 11 | def __init__(self): 12 | self.log = logging.getLogger(self.__class__.__name__) 13 | 14 | def exec(self, cmd: str, quiet=False, supress_error=False) -> AttrDict: 15 | ret = AttrDict() 16 | 17 | if not quiet: 18 | self.log.debug(cmd) 19 | 20 | p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, close_fds=True) 21 | 22 | # output processing 23 | out = p.stdout.read().decode().strip() 24 | ret.out = out 25 | err = p.stderr.read().decode().strip() 26 | ret.err = err 27 | 28 | output = '{} {}'.format(cmd, out if out else 'Nothing') 29 | 30 | if err and not supress_error: 31 | output += ' ' + err 32 | 33 | if not quiet: 34 | self.log.debug(output) 35 | 36 | return ret 37 | -------------------------------------------------------------------------------- /projects/.gitignore: -------------------------------------------------------------------------------- 1 | */ 2 | !default/ 3 | -------------------------------------------------------------------------------- /projects/default/config.yaml: -------------------------------------------------------------------------------- 1 | regexp: '.*' 2 | priority: -100 3 | -------------------------------------------------------------------------------- /projects/default/main.js: -------------------------------------------------------------------------------- 1 | try { 2 | Bypass.universal(); 3 | } catch (err) { 4 | Bypass.byCert(); 5 | } 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six 2 | coloredlogs 3 | frida 4 | PyYAML 5 | requests 6 | urllib3 7 | -------------------------------------------------------------------------------- /scripts/core/android_packages.js: -------------------------------------------------------------------------------- 1 | var AndroidPackages = [ 2 | 'com.android.', 3 | 'com.sun.', 4 | 'com.google.android.', 5 | 'org.ccil.cowan.tagsoup.', 6 | 'org.w3c.', 7 | 'org.apache.', 8 | 'android.', 9 | 'libcore.', 10 | 'org.xml.', 11 | 'org.xmlpull.', 12 | 'org.json.', 13 | 'sun.', 14 | 'java.', 15 | 'javax.', 16 | 'dalvik.', 17 | 'kotlin.', 18 | 'androidx.' 19 | ]; 20 | -------------------------------------------------------------------------------- /scripts/core/common.js: -------------------------------------------------------------------------------- 1 | var Common = { 2 | impl: function (method, func) { 3 | // the function itself rather than overloads 4 | if ('_o' in method && !('_p' in method)) { 5 | var overloadCount = method.overloads.length; 6 | send(JSON.stringify({ 7 | tracing: method.holder.$className + '.' + method.methodName, 8 | overloaded: overloadCount 9 | })); 10 | for (var i = 0; i < overloadCount; i++) { 11 | Common.impl(method.overloads[i], func); 12 | } 13 | return; 14 | } 15 | 16 | // return if this is not a overload 17 | if (!('_p' in method)) { 18 | send('not a overload: ' + JSON.stringify(Common.entries(method))); 19 | return; 20 | } 21 | 22 | // store method to global, make _method_xxx_ === method, xxx is stored in _method_index_ 23 | // and automatically increments by 1 24 | // ex: _method_0_ = 25 | // _method_1_ = 26 | // _method_2_ = 27 | // ...... 28 | if (global.hasOwnProperty("_method_index_")) 29 | global["_method_index_"]++; 30 | else 31 | global["_method_index_"] = 0; 32 | 33 | var method_index = global["_method_index_"]; 34 | var method_name = "_method_" + method_index + "_"; 35 | global[method_name] = method; 36 | 37 | // store func to global, same as method 38 | var func_name = method_name; 39 | 40 | if (func !== undefined) { 41 | if (global.hasOwnProperty("_func_index_")) 42 | global["_func_index_"]++; 43 | else 44 | global["_func_index_"] = 0; 45 | 46 | var func_index = global["_func_index_"]; 47 | func_name = "_func_" + func_index + "_"; 48 | global[func_name] = func; 49 | } 50 | 51 | // thanks Zack Argyle 52 | // https://medium.com/@zackargyle/es6-like-template-strings-in-10-lines-a88caca6f36c 53 | var impl = '${method_obj}.implementation = function () {\n' + 54 | '\tvar sendString = "${method_full_name}(";\n' + 55 | '\tfor (var i = 0; i < arguments.length; i++) {\n' + 56 | '\t\tvar arg = arguments[i];\n' + 57 | '\t\tsendString += arg;\n' + 58 | '\t\tvar prettyArg = Format.pretty(arg);\n' + 59 | '\t\tif (prettyArg !== arg)\n' + 60 | '\t\t\tsendString += "|" + prettyArg;\n' + 61 | '\t\tif (i < arguments.length - 1)\n' + 62 | '\t\t\tsendString += ", ";\n' + 63 | '\t}\n' + 64 | '\tsendString += ")";\n' + 65 | '\tsend(sendString);\n' + 66 | '\tvar ret = ${func_obj}.apply(this, arguments);\n' + 67 | '\tvar prettyRet = Format.pretty(ret);\n' + 68 | '\tsendString += " => " + ret;\n' + 69 | '\tif (ret !== prettyRet)\n' + 70 | '\t\tsendString += "|" + prettyRet;\n' + 71 | '\tsend(sendString);\n' + 72 | '\treturn ret;\n' + 73 | '};'; 74 | 75 | impl = impl.replace(/\${(.*?)}/g, function (_, code) { 76 | var scoped = code.replace(/(["'.\w$]+)/g, function (match) { 77 | return /["']/.test(match[0]) ? match : 'scope.' + match; 78 | }); 79 | 80 | try { 81 | return new Function('scope', 'return ' + scoped)({ 82 | method_obj: method_name, 83 | method_full_name: method._p[1].$className + "." + method._p[0], 84 | func_obj: func_name 85 | }); 86 | } catch (e) { 87 | return ''; 88 | } 89 | }); 90 | 91 | eval(impl); 92 | }, 93 | 94 | printBacktrace: function () { 95 | var android_util_Log = Java.use('android.util.Log'); 96 | var java_lang_Exception = Java.use('java.lang.Exception'); 97 | // getting stacktrace by throwing an exception 98 | send(android_util_Log.getStackTraceString(java_lang_Exception.$new())); 99 | }, 100 | 101 | // remove duplicates from array 102 | uniqBy: function (array, key) { 103 | var seen = {}; 104 | return array.filter(function (item) { 105 | var k = key(item); 106 | return seen.hasOwnProperty(k) ? false : (seen[k] = true); 107 | }); 108 | }, 109 | 110 | keys: function (obj) { 111 | var o = obj; 112 | var keys = []; 113 | 114 | while (o.__proto__ !== Object.prototype) { 115 | keys.push(Object.keys(o)); 116 | o = o.__proto__; 117 | } 118 | 119 | return keys; 120 | }, 121 | 122 | values: function (obj) { 123 | var o = obj; 124 | var values = []; 125 | while (o.__proto__ !== Object.prototype) { 126 | var keys = Object.keys(o); 127 | for (var i = 0; i < keys.length; i++) { 128 | values.push(obj[keys[i]]); 129 | } 130 | o = o.__proto__; 131 | } 132 | 133 | return values; 134 | }, 135 | 136 | entries: function (obj) { 137 | var o = obj; 138 | var entries = {}; 139 | while (o.__proto__ !== Object.prototype) { 140 | var keys = Object.keys(o); 141 | for (var i = 0; i < keys.length; i++) { 142 | var key = keys[i]; 143 | entries[key] = obj[key]; 144 | } 145 | o = o.__proto__; 146 | } 147 | 148 | return entries; 149 | }, 150 | 151 | entriesJson: function (obj) { 152 | return JSON.stringify(this.entries(obj)); 153 | }, 154 | 155 | propertyNames: function (obj) { 156 | var o = obj; 157 | var propertyNames = []; 158 | 159 | while (true) { 160 | Object.getOwnPropertyNames(o).forEach(function (p) { 161 | propertyNames.push(p); 162 | }); 163 | 164 | if (o.__proto__ === Object.prototype) 165 | break; 166 | o = o.__proto__; 167 | } 168 | 169 | return propertyNames; 170 | } 171 | }; 172 | -------------------------------------------------------------------------------- /scripts/core/trace.js: -------------------------------------------------------------------------------- 1 | // thanks iddoeldor: https://github.com/iddoeldor/frida-snippets 2 | 3 | var Trace = { 4 | javaClassMethod: function (targetClassMethod) { 5 | var delim = targetClassMethod.lastIndexOf('.'); 6 | if (delim === -1) 7 | return; 8 | 9 | var targetClass = targetClassMethod.slice(0, delim); 10 | var targetMethod = targetClassMethod.slice(delim + 1, targetClassMethod.length); 11 | 12 | var hook = Java.use(targetClass); 13 | try { 14 | var overloadCount = hook[targetMethod].overloads.length; 15 | } catch (e) { 16 | // cannot read property 'overloads' of undefined 17 | send(e.message) 18 | } 19 | 20 | send(JSON.stringify({tracing: targetClassMethod, overloaded: overloadCount})); 21 | 22 | for (var i = 0; i < overloadCount; i++) { 23 | Common.impl(hook[targetMethod].overloads[i]); 24 | } 25 | 26 | return overloadCount; 27 | }, 28 | 29 | javaClassName: function (targetClass) { 30 | // skip primitive types 31 | if (['byte', 'char', 'double', 'float', 'int', 'long', 'short', 'void', 'boolean'].includes(targetClass)) { 32 | return {class: targetClass, methodCount: 0, overloadCount: 0}; 33 | } 34 | 35 | try { 36 | var hook = Java.use(targetClass); 37 | } catch (e) { 38 | send("invalid target class: " + targetClass); 39 | return {class: targetClass, methodCount: 0, overloadCount: 0}; 40 | } 41 | 42 | var methods = hook.class.getDeclaredMethods(); 43 | hook.$dispose(); 44 | 45 | var parsedMethods = []; 46 | methods.forEach(function (method) { 47 | var methodStr = method.toString(); 48 | var methodReplace = methodStr.replace(targetClass + ".", "TOKEN") 49 | .match(/\sTOKEN(.*)\(/)[1]; 50 | parsedMethods.push(methodReplace); 51 | }); 52 | 53 | var countJson = {class: targetClass, methodCount: parsedMethods.length, overloadCount: 0}; 54 | 55 | Common.uniqBy(parsedMethods, JSON.stringify).forEach(function (targetMethod) { 56 | countJson.overloadCount += Trace.javaClassMethod(targetClass + '.' + targetMethod); 57 | }); 58 | 59 | send(JSON.stringify(countJson)); 60 | 61 | return countJson; 62 | }, 63 | 64 | javaClassNames: function (classes) { 65 | classes.forEach(this.javaClassName); 66 | }, 67 | 68 | javaClassByRegex: function (regexp, forceSystem) { 69 | var info = {regexp: regexp.toString(), classCount: 0, methodCount: 0, overloadCount: 0}; 70 | 71 | Java.enumerateLoadedClasses({ 72 | "onMatch": function (className) { 73 | if (className.startsWith('[')) { 74 | return; 75 | } 76 | 77 | // skip system packages 78 | if (!forceSystem) { 79 | for (var i = 0; i < AndroidPackages.length; i++) { 80 | if (className.startsWith(AndroidPackages[i])) { 81 | return; 82 | } 83 | } 84 | } 85 | 86 | if (className.match(regexp)) { 87 | var countJson = Trace.javaClassName(className); 88 | info.classCount++; 89 | info.methodCount += countJson.methodCount; 90 | info.overloadCount += countJson.overloadCount; 91 | } 92 | }, 93 | "onComplete": function () { 94 | } 95 | }); 96 | 97 | send(JSON.stringify(info)); 98 | }, 99 | 100 | jniName: function (mName) { 101 | Module.enumerateExports(mName, { 102 | onMatch: function (e) { 103 | if (e.type === 'function') { 104 | send("Intercepting jni function: " + e.name + "(" + e.address + "|" + 105 | e.address.sub(Module.findBaseAddress(mName)) + ")"); 106 | try { 107 | Interceptor.attach(e.address, { 108 | onEnter: function (args) { 109 | this.sendString = e.name + "(addr: " + e.address + "|" + 110 | e.address.sub(Module.findBaseAddress(mName)) + ", args: {"; 111 | 112 | var i = 0; 113 | while (1) { 114 | try { 115 | this.sendString += args[i].readUtf8String(); 116 | } catch (error) { 117 | // can not convert to string 118 | this.sendString += " "; 119 | } finally { 120 | if (i) { 121 | this.sendString += ", "; 122 | } 123 | } 124 | 125 | i++; 126 | if (i > 20) break; 127 | } 128 | 129 | this.sendString += "}) called from: { " + 130 | Thread.backtrace(this.context, Backtracer.ACCURATE) 131 | .map(DebugSymbol.fromAddress).join(', '); 132 | }, 133 | onLeave: function (retVal) { 134 | this.sendString += " } -> " + retVal; 135 | send(this.sendString); 136 | } 137 | }); 138 | } catch (error) { 139 | console.error(error); 140 | } 141 | } 142 | }, 143 | onComplete: function () { 144 | } 145 | }); 146 | }, 147 | 148 | jniNames: function (mNames) { 149 | mNames.forEach(this.jniName); 150 | }, 151 | 152 | propertyGet: function () { 153 | Interceptor.attach(Module.findExportByName(null, '__system_property_get'), { 154 | onEnter: function (args) { 155 | this._name = args[0].readCString(); 156 | this._value = args[1]; 157 | }, 158 | onLeave: function (retval) { 159 | send(JSON.stringify({ 160 | result_length: retval, 161 | name: this._name, 162 | val: this._value.readCString() 163 | })); 164 | } 165 | }); 166 | }, 167 | 168 | hiddenNativeMethods: function () { 169 | var pSize = Process.pointerSize; 170 | var env = Java.vm.getEnv(); 171 | // search "215" @ https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html 172 | var RegisterNatives = 215, FindClassIndex = 6; 173 | var jclassAddress2NameMap = {}; 174 | 175 | function getNativeAddress(idx) { 176 | return env.handle.readPointer().add(idx * pSize).readPointer(); 177 | } 178 | 179 | // intercepting FindClass to populate Map 180 | Interceptor.attach(getNativeAddress(FindClassIndex), { 181 | onEnter: function (args) { 182 | jclassAddress2NameMap[args[0]] = args[1].readCString(); 183 | } 184 | }); 185 | 186 | // RegisterNative(jClass*, .., JNINativeMethod *methods[nMethods], uint nMethods) 187 | // https://android.googlesource.com/platform/libnativehelper/+/master/include_jni/jni.h#977 188 | Interceptor.attach(getNativeAddress(RegisterNatives), { 189 | onEnter: function (args) { 190 | for (var i = 0, nMethods = parseInt(args[3]); i < nMethods; i++) { 191 | /* 192 | https://android.googlesource.com/platform/libnativehelper/+/master/include_jni/jni.h#129 193 | typedef struct { 194 | const char* name; 195 | const char* signature; 196 | void* fnPtr; 197 | } JNINativeMethod; 198 | */ 199 | var structSize = pSize * 3; // = sizeof(JNINativeMethod) 200 | var methodsPtr = ptr(args[2]); 201 | var signature = methodsPtr.add(i * structSize + pSize).readPointer(); 202 | var fnPtr = methodsPtr.add(i * structSize + (pSize * 2)).readPointer(); // void* fnPtr 203 | var jClass = jclassAddress2NameMap[args[0]].split('/'); 204 | send(JSON.stringify({ 205 | // https://www.frida.re/docs/javascript-api/#debugsymbol 206 | module: DebugSymbol.fromAddress(fnPtr)['moduleName'], 207 | package: jClass.slice(0, -1).join('.'), 208 | class: jClass[jClass.length - 1], 209 | method: methodsPtr.readPointer().readCString(), // char* name 210 | // char* signature TODO Java bytecode signature parser { Z: 'boolean', B: 'byte', C: 'char', S: 'short', I: 'int', J: 'long', F: 'float', D: 'double', L: 'fully-qualified-class;', '[': 'array' } https://github.com/skylot/jadx/blob/master/jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java 211 | signature: signature.readCString(), 212 | address: fnPtr 213 | })); 214 | } 215 | } 216 | }); 217 | } 218 | }; 219 | -------------------------------------------------------------------------------- /scripts/utils/bypass.js: -------------------------------------------------------------------------------- 1 | var Bypass = { 2 | /** 3 | * Universal Android SSL Pinning Bypass #2 4 | * https://techblog.mediaservice.net/2018/11/universal-android-ssl-pinning-bypass-2/ 5 | */ 6 | universal: function () { 7 | var array_list = Java.use("java.util.ArrayList"); 8 | var ApiClient = Java.use('com.android.org.conscrypt.TrustManagerImpl'); 9 | 10 | ApiClient.checkTrustedRecursive.implementation = function (a1, a2, a3, a4, a5, a6) { 11 | send('Bypassing SSL Pinning'); 12 | return array_list.$new(); 13 | }; 14 | }, 15 | 16 | /** 17 | * Universal Android SSL Pinning bypass with Frida 18 | * https://techblog.mediaservice.net/2017/07/universal-android-ssl-pinning-bypass-with-frida/ 19 | * 20 | * UPDATE 20191605: Fixed undeclared var. Thanks to @oleavr and @ehsanpc9999 ! 21 | * */ 22 | byCert: function () { 23 | send("[.] Cert Pinning Bypass/Re-Pinning"); 24 | 25 | var CertificateFactory = Java.use("java.security.cert.CertificateFactory"); 26 | var FileInputStream = Java.use("java.io.FileInputStream"); 27 | var BufferedInputStream = Java.use("java.io.BufferedInputStream"); 28 | var X509Certificate = Java.use("java.security.cert.X509Certificate"); 29 | var KeyStore = Java.use("java.security.KeyStore"); 30 | var TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory"); 31 | var SSLContext = Java.use("javax.net.ssl.SSLContext"); 32 | 33 | // Load CAs from an InputStream 34 | send("[+] Loading our CA...") 35 | var cf = CertificateFactory.getInstance("X.509"); 36 | 37 | try { 38 | var fileInputStream = FileInputStream.$new("/data/local/tmp/cert-der.crt"); 39 | } catch (err) { 40 | send("[o] " + err); 41 | return; 42 | } 43 | 44 | var bufferedInputStream = BufferedInputStream.$new(fileInputStream); 45 | var ca = cf.generateCertificate(bufferedInputStream); 46 | bufferedInputStream.close(); 47 | 48 | var certInfo = Java.cast(ca, X509Certificate); 49 | send("[o] Our CA Info: " + certInfo.getSubjectDN()); 50 | 51 | // Create a KeyStore containing our trusted CAs 52 | send("[+] Creating a KeyStore for our CA..."); 53 | var keyStoreType = KeyStore.getDefaultType(); 54 | var keyStore = KeyStore.getInstance(keyStoreType); 55 | keyStore.load(null, null); 56 | keyStore.setCertificateEntry("ca", ca); 57 | 58 | // Create a TrustManager that trusts the CAs in our KeyStore 59 | send("[+] Creating a TrustManager that trusts the CA in our KeyStore..."); 60 | var tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); 61 | var tmf = TrustManagerFactory.getInstance(tmfAlgorithm); 62 | tmf.init(keyStore); 63 | send("[+] Our TrustManager is ready..."); 64 | 65 | send("[+] Hijacking SSLContext methods now...") 66 | send("[-] Waiting for the app to invoke SSLContext.init()...") 67 | 68 | SSLContext.init.overload( 69 | "[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom") 70 | .implementation = function (a, b, c) { 71 | send("[o] App invoked javax.net.ssl.SSLContext.init..."); 72 | SSLContext.init.overload( 73 | "[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom") 74 | .call(this, a, tmf.getTrustManagers(), c); 75 | send("[+] SSLContext initialized with our custom TrustManager!"); 76 | } 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /scripts/utils/conversion.js: -------------------------------------------------------------------------------- 1 | var Conversion = { 2 | /** 3 | * Convert hex string to byte array 4 | * @example 5 | * // returns [156,255,90,67] 6 | * JSON.stringify(Conversion.hex2bytes('9CFF5A43')); 7 | * @param {String} hex 8 | * @returns {[]} 9 | */ 10 | hex2bytes: function (hex) { 11 | for (var bytes = [], c = 0; c < hex.length; c += 2) 12 | bytes.push(parseInt(hex.substr(c, 2), 16)); 13 | return bytes; 14 | }, 15 | 16 | /** 17 | * Convert byte array to hex string 18 | * @example 19 | * // returns 9CFF5A43 20 | * Conversion.bytes2hex([156, -1, 90, 67]); 21 | * @param {[]} bytes 22 | * @returns {string} hex string 23 | */ 24 | bytes2hex: function (bytes) { 25 | for (var hex = [], i = 0; i < bytes.length; i++) { 26 | hex.push(((bytes[i] >>> 4) & 0xF).toString(16).toUpperCase()); 27 | hex.push((bytes[i] & 0xF).toString(16).toUpperCase()); 28 | } 29 | return hex.join(""); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /scripts/utils/format.js: -------------------------------------------------------------------------------- 1 | var Format = { 2 | /** 3 | * Convert byte array to hex string 4 | * @param {string} obj 5 | * @example 6 | * // returns hello 7 | * Format.pretty('hello'); 8 | * @param {Object} obj 9 | * @example 10 | * // returns 010203 11 | * Format.pretty(Java.array('byte', [1, 2, 3])); 12 | * @returns {string} 13 | */ 14 | pretty: function (obj) { 15 | if (obj === null || obj === undefined || typeof obj !== 'object') return obj; 16 | 17 | // byte array 18 | if (obj.__proto__ === Java.array('byte', []).__proto__) { 19 | var str = Java.use('java.lang.String').$new(obj).toString(); 20 | var validStr = str.replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, ''); 21 | // need fix: the regexp can not work perfectly 22 | if (str.length > validStr.length) 23 | return Conversion.bytes2hex(obj); 24 | else 25 | return validStr; 26 | } 27 | 28 | return JSON.stringify(Jav.describeObject(obj)); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /scripts/utils/jav.js: -------------------------------------------------------------------------------- 1 | var Jav = { 2 | describeObject: function (obj) { 3 | if (typeof(obj) !== 'object' || !('class' in obj)) { 4 | return obj; 5 | } 6 | 7 | var describe = {class: obj.class.getName(), methods: [], fields: []}; 8 | 9 | // search methods 10 | Common.propertyNames(obj).map(function (propertyName) { 11 | return obj[propertyName]; 12 | }).filter(function (property) { 13 | return typeof (property) !== 'string' && 'overloads' in property; 14 | }).forEach(function (method) { 15 | method.overloads.forEach(function (overload) { 16 | // ret method(args) 17 | describe.methods.push(overload.returnType.className + ' ' + overload.methodName + '(' 18 | + overload.argumentTypes.map(function (argumentType) {return argumentType.className;}).join() 19 | + ')'); 20 | }); 21 | }); 22 | 23 | // search fields 24 | Common.propertyNames(obj).filter(function (propertyName) { 25 | var property = obj[propertyName]; 26 | return typeof (property) !== 'string' && 'fieldType' in property; 27 | }).forEach(function (fieldName) { 28 | var field = obj[fieldName]; 29 | // type name = value 30 | describe.fields.push(field.fieldReturnType.className + ' ' + fieldName + ' = ' + field.value); 31 | }); 32 | 33 | return describe; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /tests/AndroidToHook/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | -------------------------------------------------------------------------------- /tests/AndroidToHook/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /tests/AndroidToHook/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "io.github.margular" 7 | minSdkVersion 24 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | compileOptions { 20 | sourceCompatibility = 1.8 21 | targetCompatibility = 1.8 22 | } 23 | buildToolsVersion = '28.0.3' 24 | } 25 | 26 | dependencies { 27 | implementation fileTree(dir: 'libs', include: ['*.jar']) 28 | implementation 'com.android.support:appcompat-v7:28.0.0' 29 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 30 | testImplementation 'junit:junit:4.12' 31 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 32 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 33 | } 34 | -------------------------------------------------------------------------------- /tests/AndroidToHook/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /tests/AndroidToHook/app/src/androidTest/java/io/github/margular/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package io.github.margular; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("io.github.margular", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/AndroidToHook/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/AndroidToHook/app/src/main/java/io/github/margular/AsyncRequest.java: -------------------------------------------------------------------------------- 1 | package io.github.margular; 2 | 3 | import android.os.AsyncTask; 4 | 5 | import java.io.IOException; 6 | import java.net.HttpURLConnection; 7 | import java.net.URL; 8 | 9 | class AsyncRequest extends AsyncTask { 10 | 11 | @Override 12 | protected void onPreExecute() { 13 | //display progress dialog. 14 | 15 | } 16 | 17 | @Override 18 | protected Void doInBackground(Void... params) { 19 | try { 20 | URL url = new URL("https://www.baidu.com/"); 21 | 22 | HttpURLConnection con = (HttpURLConnection) url.openConnection(); 23 | 24 | System.out.println(con.getResponseCode() + " " + con.getResponseMessage() + "\n"); 25 | } catch (IOException e) { 26 | e.printStackTrace(); 27 | } 28 | 29 | return null; 30 | } 31 | 32 | @Override 33 | protected void onPostExecute(Void result) { 34 | // dismiss progress dialog and update ui 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/AndroidToHook/app/src/main/java/io/github/margular/MainActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.margular; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.text.method.ScrollingMovementMethod; 6 | import android.view.View; 7 | import android.widget.Button; 8 | import android.widget.TextView; 9 | import android.widget.Toast; 10 | 11 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_main); 17 | 18 | Button btnSpeak = findViewById(R.id.btnSpeak); 19 | btnSpeak.setOnClickListener(this); 20 | 21 | TextView txtHttp = findViewById(R.id.txtHttp); 22 | txtHttp.setMovementMethod(new ScrollingMovementMethod()); 23 | } 24 | 25 | @Override 26 | public void onClick(View v) { 27 | switch (v.getId()) { 28 | case R.id.btnSpeak: 29 | Toast.makeText(getApplicationContext(), getBestLanguage("Python") + " is the best" 30 | + " programming language of the world!", Toast.LENGTH_SHORT).show(); 31 | new AsyncRequest().execute(); 32 | break; 33 | } 34 | } 35 | 36 | private String getBestLanguage(String lang) { 37 | return lang; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/AndroidToHook/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /tests/AndroidToHook/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /tests/AndroidToHook/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |