Repository: yangchong211/YCRedDotView Branch: master Commit: 13d9c29702b0 Files: 53 Total size: 136.3 KB Directory structure: gitextract_x6e0zd28/ ├── .gitignore ├── LICENSE ├── NotCaptureLib/ │ ├── .gitignore │ ├── README.md │ ├── build.gradle │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── yc/ │ │ └── notcapturelib/ │ │ ├── encrypt/ │ │ │ ├── EncryptDecryptInterceptor.kt │ │ │ └── InterceptorHelper.kt │ │ ├── helper/ │ │ │ ├── CaptureConfig.java │ │ │ ├── EncryptDecryptListener.java │ │ │ └── NotCaptureHelper.java │ │ ├── proxy/ │ │ │ └── ProxyWifiUtils.java │ │ ├── sign/ │ │ │ └── SignGenerator.kt │ │ ├── ssl/ │ │ │ ├── HttpSslConfig.java │ │ │ ├── HttpSslFactory.java │ │ │ ├── TrustAllCertsManager.java │ │ │ ├── UnSafeHostnameVerifier.java │ │ │ └── UnSafeTrustManager.java │ │ ├── utils/ │ │ │ ├── NotCaptureUtils.java │ │ │ └── Rc4EncryptUtils.java │ │ └── xposed/ │ │ ├── CommandUtils.java │ │ ├── HackChecker.java │ │ ├── VirtualApkUtils.java │ │ ├── VirtualCallback.java │ │ └── XposedUtils.java │ └── res/ │ └── xml/ │ ├── network_security_config.xml │ └── network_security_config_debug.xml ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── yc/ │ │ └── toolapp/ │ │ ├── App.java │ │ └── MainActivity.kt │ └── res/ │ ├── drawable/ │ │ └── ic_launcher_background.xml │ ├── drawable-v24/ │ │ └── ic_launcher_foreground.xml │ ├── layout/ │ │ ├── activity_main.xml │ │ ├── component_banner.xml │ │ ├── fragment.xml │ │ └── main_tab_layout.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ └── values/ │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── yc.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Built application files *.apk *.ap_ # Files for the ART/Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ out/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # IntelliJ *.iml .idea/workspace.xml .idea/tasks.xml .idea/gradle.xml .idea/assetWizardSettings.xml .idea/dictionaries .idea/libraries .idea/caches # Keystore files # Uncomment the following line if you do not want to check your keystore files in. #*.jks # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild # Google Services (e.g. APIs or Firebase) google-services.json # Freeline freeline.py freeline/ freeline_project_description.json .idea/ # fastlane fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2017 yangchong211(github.com/yangchong211) 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 http://www.apache.org/licenses/LICENSE-2.0 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. ================================================ FILE: NotCaptureLib/.gitignore ================================================ /build ================================================ FILE: NotCaptureLib/README.md ================================================ # App防止抓包工具库 #### 目录介绍 - 01.基础概念介绍 - 02.常见思路和做法 - 03.Api调用说明 - 04.遇到的坑分析 - 05.其他问题说明 ### 01.基础概念说明 #### 1.1 为何不能抓Https - Android 7.0及以上为何不能轻易抓取到Https请求的明文数据? - 其实Charles上显示确实抓到了包,但是当看抓包的详细数据时会发现报错 You may need to configure your browser or application to trust the Charles Root Certificate. See SSL Proxying in the Help menu。 - Charles说手机端没有信任Charles的根证书,但是我们手机上已经安装了Charles根证书了,为什么会这样? - 原来在Android 7.0(API 24 ) ,有一个名为“Network Security Configuration”的新安全功能。这个新功能的目标是允许开发人员在不修改应用程序代码的情况下自定义他们的网络安全设置。 - 如果应用程序运行的系统版本高于或等于24,并且targetSdkVersion>=24,则只有系统(system)证书才会被信任。用户(user)导入的Charles根证书是不被信任的。 - Android 7.0 (api 24 ) 和 targetSdkVersion 对抓包的影响 - 1.抓自己开发的app的网络包;2.抓第三方app的网络包,比如微博客户端 - 这两种情况有什么区别的,第一种app是我们自己开发的,我们手里有源码,能够修改,能够做到像官方文档里面说的一样进行配置。第二种我们没有源码,要想做到像官方文档里面配置的话,只能反编译后,把配置文件添加进去然后重新打包,但是重新打包就会遇到很多坑,并不一定能成功,所以需要使用其他方式达到抓包目的。 - 引用官方文档一句话:默认情况下,来自所有应用的安全连接(使用 TLS 和 HTTPS 之类的协议)均信任预装的系统 CA,而面向 Android 6.0(API 级别 23)及更低版本的应用默认情况下还会信任用户添加的 CA 存储。应用可以使用 base-config(应用范围的自定义)或 domain-config(按域自定义)自定义自己的连接。 #### 1.2 业务背景说明 - https确保数据安全 - 现在几乎所有的api接口都使用https进行数据传输,客户端本身会对https证书的合法性进行校验,以确保数据传输的安全性。 - 存在抓包数据问题 - 第三方还是可以通过信任代理证书(charles)、安装插件绕过证书检测流程等方式,破解https证书安全校验,解密获取到传输数据。 #### 1.3 防止中间人拦截 - 怎么保证服务端在传输公钥给客户端时不被中间人代理拦截呢? - 这里就要引入第三方认证服务:数字证书认证机构(Certificate Authority,简称CA)。 - CA机构会根据申请方提供的公司信息生成一个数字证书,同时生成一对公钥和私钥。私钥由服务端保存,不可泄露。公钥则附带到数字证书的信息里,可以通过解密获取。 #### 1.4 信任的凭证 - 信任的凭证分为两种类型:系统和用户。 - 其中系统一列是随设备出厂内置的,并随系统版本更新同步更新,用户一列则是由用户自己安装并信任的证书。 - 证书的类型 - JKS:数字证书库。JKS里有KeyEntry和CertEntry,在库里的每个Entry都是靠别名(alias)来识别的。 - P12:是PKCS12的缩写。同样是一个存储私钥的证书库,由.jks文件导出的,用户在PC平台安装,用于标示用户的身份。 - CER:俗称数字证书,目的就是用于存储公钥证书,任何人都可以获取这个文件 。 - BKS:由于Android平台不识别.keystore和.jks格式的证书库文件,因此Android平台引入一种的证书库格式,BKS。 #### 1.7 证书校验API - SSLSocketFactory 或 SSLSocket - Android 使用的是 Java 的 API。那么 HTTPS 使用的 Socket 必然都是通过SSLSocketFactory 创建的 SSLSocket,当然自己实现了 TLS 协议除外。 - 此时使用的是默认的SSLSocketFactory(没有加载自己的证书),与下段代码使用的SSLContext是一致的: ``` private synchronized SSLSocketFactory getDefaultSSLSocketFactory() { try { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, null, null); return defaultSslSocketFactory = sslContext.getSocketFactory(); } catch (GeneralSecurityException e) { throw new AssertionError(); // The system has no TLS. Just give up. } } ``` ### 02.常见思路和做法 #### 2.1 App安全配置 - 添加配置文件 - android:networkSecurityConfig="@xml/network_security_config" - 在官方文档找到了答案: - 网络安全性配置特性让应用可以在一个安全的声明性配置文件中自定义其网络安全设置,而无需修改应用代码。可以针对特定域和特定应用配置这些设置。此特性的主要功能如下所示: - 自定义信任锚:针对应用的安全连接自定义哪些证书颁发机构 (CA) 值得信任。例如,信任特定的自签署证书或限制应用信任的公共 CA 集。 - 仅调试重写:在应用中以安全方式调试安全连接,而不会增加已安装用户的风险。 - 明文通信选择退出:防止应用意外使用明文通信。 - 证书固定:将应用的安全连接限制为特定的证书。 #### 2.5 网络数据加解密 - 网络数据加密的需求 - 为了项目数据安全性,对请求体和响应体加密,那肯定要知道请求体或响应体在哪里,然后才能加密,其实都一样不论是加密url里面的query内容还是加密body体里面的都一样。 - 对数据哪里进行加密和解密 - 目前对数据返回的data进行加解密。 ### 03.Api调用说明 ### 04.遇到的坑分析 ### 05.其他问题说明 ### 参考文章 - 全面总结 Android HTTPS 抓包 - https://mp.weixin.qq.com/s/M82R5YelhMAbdsjJ29gH9A - Android 高版本 HTTPS 抓包解决方案及问题分析! - https://mp.weixin.qq.com/s/lnv4-vvz4W8u5Qp_epEkkw - 还不懂 HTTP 代理吗? - https://mp.weixin.qq.com/s/H5H0LixgRY6CoRunBaLBAw - App 安全的HTTPS 通信 - https://developer.aliyun.com/article/64810?spm=a2c6h.13813017.content3.4.6f0b2fe20aa1S0 - Android 系统各个版本上https的抓包 - https://mp.weixin.qq.com/s?__biz=MzIxNzU1Nzk3OQ==&mid=2247486834&idx=1&sn=91850a5d1ac13953fcb869bf1f232aab&chksm=97f6b3c6a0813ad0bd3df0b09ff0cdbcbd8c85021592febed13f5f265b97cd3a8bbb32e5ca55&scene=38#wechat_redirect - 解决APP抓包问题「网络安全」 - https://baijiahao.baidu.com/s?id=1749021972862503189&wfr=spider&for=pc - Okhttp如何添加HTTPS自签名证书信任 - https://www.cnblogs.com/jiechao-zhang/p/15207246.html - 深度抓包好文 - https://zhuanlan.zhihu.com/p/465441201 - 关于HTTPS、TLS/SSL认证以及客户端证书导入方法 - http://t.zoukankan.com/blogs-of-lxl-p-10136582.html - 参考博客 - https://zhuanlan.zhihu.com/p/465441201 ================================================ FILE: NotCaptureLib/build.gradle ================================================ apply plugin: 'com.android.library' apply from: rootProject.projectDir.absolutePath + "/yc.gradle" apply plugin: 'kotlin-android' android { compileSdkVersion rootProject.ext.android["compileSdkVersion"] //buildToolsVersion rootProject.ext.android["buildToolsVersion"] defaultConfig { minSdkVersion rootProject.ext.android["minSdkVersion"] targetSdkVersion rootProject.ext.android["targetSdkVersion"] versionCode rootProject.ext.android["versionCode"] versionName rootProject.ext.android["versionName"] } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation rootProject.ext.dependencies["appcompat"] implementation rootProject.ext.dependencies["annotation"] implementation(rootProject.ext.dependencies["okhttp"]) implementation(rootProject.ext.dependencies["gson"]) //同上上报库 implementation 'com.github.yangchong211.YCCommonLib:EventUploadLib:1.4.3' //加解密库 //implementation project(path: ':AppEncryptLib') implementation 'com.github.yangchong211.YCCommonLib:AppEncryptLib:1.4.3' //通用组件接口库 implementation 'com.github.yangchong211.YCCommonLib:AppCommonInter:1.4.3' } ================================================ FILE: NotCaptureLib/src/main/AndroidManifest.xml ================================================ ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/encrypt/EncryptDecryptInterceptor.kt ================================================ package com.yc.notcapturelib.encrypt import com.google.gson.Gson import com.google.gson.JsonSyntaxException import com.google.gson.annotations.SerializedName import com.yc.eventuploadlib.LoggerReporter import com.yc.notcapturelib.helper.NotCaptureHelper import okhttp3.* import okhttp3.ResponseBody.Companion.toResponseBody import java.io.IOException /** * @author yangchong * GitHub : https://github.com/yangchong211/YCAppTool * time : 2020/11/30 * desc : 加解密数据 */ class EncryptDecryptInterceptor : Interceptor { private val gson = Gson() @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val request: Request = chain.request() val handleRequest = handleRequest(request) val response: Response = chain.proceed(handleRequest) val handleResponse = handleResponse(response) return handleResponse!! } private fun handleRequest(request: Request): Request { val headerIgnoreEncrypt = request.header(HEADER_IGNORE_ENCRYPT) val headerForceEncrypt = request.header(HEADER_FORCE_ENCRYPT) val ignoreEncrypt = "true" == headerIgnoreEncrypt val forceEncrypt = "true" == headerForceEncrypt val config = NotCaptureHelper.getInstance().config val configEncrypt = config.isEncrypt val headerEncrypt = !ignoreEncrypt || !forceEncrypt val newRequest : Request LoggerReporter.report("NotCaptureHelper", "handleRequest $configEncrypt , $headerEncrypt") if (configEncrypt && headerEncrypt) { when (request.method) { "GET" -> { newRequest = handleRequestGet(request) } "POST" -> { val requestBody = request.body newRequest = when { requestBody == null || requestBody.contentLength() == 0L || requestBody is FormBody -> { handleRequestPostFormBody(request) } requestBody is MultipartBody -> { handleRequestPostMultipartBody(request) } else -> { request } } } else -> { newRequest = request } } } else { newRequest = request } return newRequest.newBuilder() .removeHeader(HEADER_IGNORE_ENCRYPT) .removeHeader(HEADER_FORCE_ENCRYPT) .build() } private fun handleRequestPostMultipartBody(request: Request) : Request { val url = request.url val requestBody = request.body as MultipartBody LoggerReporter.report("NotCaptureHelper", "handleRequestPostMultipartBody url: $url") LoggerReporter.report("NotCaptureHelper", "handleRequestPostMultipartBody requestBody: $requestBody") val newParameterList = InterceptorHelper.buildNewParameterList(request) val newUrl = url.newBuilder() .encodedQuery(null) .encodedFragment(null) .apply { newParameterList.forEach { paramTriple -> if (paramTriple.first) { addQueryParameter(paramTriple.second, paramTriple.third) } } } .build() val newRequestBody = MultipartBody.Builder() .setType(requestBody.type) .apply { newParameterList.forEach { paramTriple -> if (!paramTriple.first) { addFormDataPart( paramTriple.second, paramTriple.third ?: "" ) } } requestBody.parts.forEach { part -> addPart(part) } } .build() LoggerReporter.report("NotCaptureHelper", "handleRequestPostMultipartBody newUrl: $newUrl") LoggerReporter.report("NotCaptureHelper", "handleRequestPostMultipartBody newRequestBody: $newRequestBody") return request.newBuilder() .url(newUrl) .post(newRequestBody) .build() } private fun handleRequestPostFormBody(request: Request) : Request { val url = request.url val requestBody = request.body val newParameterList = InterceptorHelper.buildNewParameterList(request) LoggerReporter.report("NotCaptureHelper", "handleRequestPostFormBody url: $url") LoggerReporter.report("NotCaptureHelper", "handleRequestPostFormBody requestBody: $requestBody") val newUrl = url.newBuilder() .encodedQuery(null) .encodedFragment(null) .apply { newParameterList.forEach { paramTriple -> if (paramTriple.first) { addQueryParameter(paramTriple.second, paramTriple.third) } } } .build() val newRequestBody = FormBody.Builder() .apply { newParameterList.forEach { paramTriple -> if (!paramTriple.first) { add(paramTriple.second, paramTriple.third ?: "") } } } .build() LoggerReporter.report("NotCaptureHelper", "handleRequestPostFormBody newUrl: $newUrl") LoggerReporter.report("NotCaptureHelper", "handleRequestPostFormBody newRequestBody: $newRequestBody") return request.newBuilder() .url(newUrl) .post(newRequestBody) .build() } private fun handleRequestGet(request: Request): Request { val url = request.url LoggerReporter.report("NotCaptureHelper", "handleRequestGet url: $url") val newParameterList = InterceptorHelper.buildNewParameterList(request) val newUrl = url.newBuilder() .encodedQuery(null) .encodedFragment(null) .apply { newParameterList.forEach { paramTriple -> //使用UTF-8编码查询参数并将其添加到此URL的查询字符串中 addQueryParameter(paramTriple.second, paramTriple.third) } } .build() LoggerReporter.report("NotCaptureHelper", "handleRequestGet newUrl: $newUrl") return request.newBuilder() .url(newUrl) .get() .build() } private fun handleResponse(response: Response): Response? { val responseBody = response.body return if (responseBody?.contentType()?.subtype == SUBTYPE_JSON) { val responseBodyStr = responseBody.string() val encryptResponseBody = try { gson.fromJson(responseBodyStr, EncryptResponseBody::class.java) } catch (e: JsonSyntaxException) { null } val encryptVersion = encryptResponseBody?.encryptVersion if (!encryptVersion.isNullOrEmpty()) { val result = encryptResponseBody.result ?: "" LoggerReporter.report("NotCaptureHelper", "handleResponse result: $result") val decryptResponseBodyStr = InterceptorHelper.decrypt(encryptVersion,result) ?: "" val toResponseBody = decryptResponseBodyStr.toResponseBody(responseBody.contentType()) LoggerReporter.report("NotCaptureHelper", "handleResponse toResponseBody: ${toResponseBody.string()}") response.newBuilder() .body(toResponseBody) .build() } else { val toResponseBody = responseBodyStr.toResponseBody(responseBody.contentType()) response.newBuilder() .body(toResponseBody) .build() } } else { response } } private class EncryptResponseBody( @SerializedName("ev") val encryptVersion: String? = null, @SerializedName("result") val result: String? = null ) companion object { const val KEY_ENCRYPT_VERSION = "ev" const val KEY_DATA = "data" const val ENCRYPT_VERSION_2 = "2" private const val SUBTYPE_JSON = "json" private const val HEADER_IGNORE_ENCRYPT = "Ignore-Encrypt" private const val HEADER_FORCE_ENCRYPT = "Force-Encrypt" } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/encrypt/InterceptorHelper.kt ================================================ package com.yc.notcapturelib.encrypt import android.annotation.SuppressLint import com.yc.eventuploadlib.LoggerReporter import com.yc.notcapturelib.helper.NotCaptureHelper import okhttp3.FormBody import okhttp3.Request /** * @author yangchong * GitHub : https://github.com/yangchong211/YCAppTool * time : 2020/11/30 * desc : 数据处理帮助类 */ object InterceptorHelper { @SuppressLint("LongLogTag") fun buildNewParameterList(request: Request, needEncode: Boolean = true): MutableList> { val parameterList = mutableListOf>() val url = request.url val requestBody = request.body //组装httpUrl数据 url.run { for (index in 0 until querySize) { parameterList.add(Pair(queryParameterName(index), queryParameterValue(index))) } } //组装requestBody数据 requestBody.run { if (this is FormBody) { for (index in 0 until size) { parameterList.add(Pair(name(index), value(index))) } } } val tempFormBody = FormBody.Builder().apply { parameterList.forEach { paramPair -> add(paramPair.first, paramPair.second ?: "") } }.build() val dataStrBuilder = StringBuilder() tempFormBody.run { if (needEncode) { for (index in 0 until size) { dataStrBuilder.append(encodedName(index)) dataStrBuilder.append("=") dataStrBuilder.append(encodedValue(index)) if (index < size - 1) { dataStrBuilder.append("&") } } } else { for (index in 0 until size) { dataStrBuilder.append(name(index)) dataStrBuilder.append("=") dataStrBuilder.append(value(index)) if (index < size - 1) { dataStrBuilder.append("&") } } } } val data = dataStrBuilder.toString() LoggerReporter.report("NotCaptureHelper", "buildNewParameterList data: $data") val reservedQueryParamNamesAndValues: MutableList> = mutableListOf() val reservedQueryParam = NotCaptureHelper.getInstance().config.reservedQueryParam reservedQueryParam?.let { tempFormBody.run { for (index in 0 until size) { if (name(index) in reservedQueryParam) { reservedQueryParamNamesAndValues.add(Triple(true, name(index), value(index))) } } } } val encryptVersion = NotCaptureHelper.getInstance().config.encryptVersion return mutableListOf>().apply { add(Triple(false, EncryptDecryptInterceptor.KEY_ENCRYPT_VERSION, encryptVersion)) add(Triple(false, EncryptDecryptInterceptor.KEY_DATA, encrypt(encryptVersion,data))) addAll(reservedQueryParamNamesAndValues) } } /** * 加密数据 */ fun encrypt(encryptVersion: String , data : String): String? { val encryptKey = NotCaptureHelper.getInstance().config.encryptKey val encryptDecryptListener = NotCaptureHelper.getInstance().encryptDecryptListener return when (encryptVersion) { EncryptDecryptInterceptor.ENCRYPT_VERSION_2 -> encryptDecryptListener.encryptData(encryptKey,data) else -> data } } /** * 解密数据 */ fun decrypt(encryptVersion: String , data : String): String? { val encryptKey = NotCaptureHelper.getInstance().config.encryptKey val encryptDecryptListener = NotCaptureHelper.getInstance().encryptDecryptListener return when (encryptVersion) { EncryptDecryptInterceptor.ENCRYPT_VERSION_2 -> encryptDecryptListener.decryptData(encryptKey, data) else -> null } } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/helper/CaptureConfig.java ================================================ package com.yc.notcapturelib.helper; import com.yc.appcommoninter.IMonitorToggle; import java.util.ArrayList; /** * @author yangchong * GitHub : https://github.com/yangchong211/YCAppTool * time : 2020/11/30 * desc : 配置类 */ public final class CaptureConfig { private CaptureConfig() { } /** * 是否是debug环境 */ private boolean isDebug; /** * 是否禁用代理 */ private boolean proxy; /** * 证书地址 */ private String cerPath; /** * 是否进行CA证书校验 */ private boolean isCaVerify; /** * 是否进行数据加密和解密 */ private boolean isEncrypt; /** * 加解密版本 */ private String encryptVersion = "2"; /** * 参数 */ private ArrayList reservedQueryParam; /** * 域名 */ private ArrayList url; /** * 加解密的key */ private String encryptKey; /** * 降级接口 */ private IMonitorToggle monitorToggle; public boolean isDebug() { return isDebug; } public boolean isProxy() { return proxy; } public String getCerPath() { return cerPath; } public boolean isCaVerify() { return isCaVerify; } public boolean isEncrypt() { return isEncrypt; } public String getEncryptVersion() { return encryptVersion; } public ArrayList getReservedQueryParam() { return reservedQueryParam; } public String getEncryptKey() { return encryptKey; } public ArrayList getUrl() { return url; } public IMonitorToggle getMonitorToggle() { return monitorToggle; } public static Builder builder() { return new Builder(); } public static class Builder { private final CaptureConfig config; private Builder() { config = new CaptureConfig(); } public Builder setDebug(boolean isDebug) { config.isDebug = isDebug; return this; } public Builder setProxy(boolean isProxy) { config.proxy = isProxy; return this; } public Builder setCerPath(String cerPath) { config.cerPath = cerPath; return this; } public Builder setCaVerify(boolean isCaVerify) { config.isCaVerify = isCaVerify; return this; } public Builder setEncrypt(boolean isEncrypt) { config.isEncrypt = isEncrypt; return this; } public Builder setEncryptVersion(String encryptVersion) { config.encryptVersion = encryptVersion; return this; } public Builder setReservedQueryParam(ArrayList reservedQueryParam) { config.reservedQueryParam = reservedQueryParam; return this; } public Builder setEncryptKey(String encryptKey) { config.encryptKey = encryptKey; return this; } public Builder setHostUrl(ArrayList url) { config.url = url; return this; } public Builder setMonitorToggle(IMonitorToggle monitorToggle) { config.monitorToggle = monitorToggle; return this; } public CaptureConfig build() { return config; } } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/helper/EncryptDecryptListener.java ================================================ package com.yc.notcapturelib.helper; /** * @author yangchong * GitHub : https://github.com/yangchong211/YCAppTool * time : 2020/11/30 * desc : 加密和解密自定义接口,外部开发者可以自由实现自己的加密和解密方式 */ public interface EncryptDecryptListener { /** * 加密数据 * * @param key key * @param data 加密内容 * @return */ String encryptData(String key, String data); /** * 解密数据 * * @param key key * @param data 加密内容 * @return */ String decryptData(String key, String data); } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/helper/NotCaptureHelper.java ================================================ package com.yc.notcapturelib.helper; import android.content.Context; import com.yc.appencryptlib.Rc4EncryptUtils; import com.yc.eventuploadlib.LoggerReporter; import com.yc.notcapturelib.encrypt.EncryptDecryptInterceptor; import com.yc.notcapturelib.proxy.ProxyWifiUtils; import com.yc.notcapturelib.ssl.HttpSslConfig; import com.yc.notcapturelib.ssl.HttpSslFactory; import com.yc.notcapturelib.utils.NotCaptureUtils; import java.net.Proxy; import okhttp3.OkHttpClient; /** * @author yangchong * GitHub : https://github.com/yangchong211/YCAppTool * time : 2020/11/30 * desc : 代理工具类 */ public final class NotCaptureHelper { private static volatile NotCaptureHelper notCaptureHelper; private CaptureConfig config; private EncryptDecryptListener encryptDecryptListener; private NotCaptureHelper(){ LoggerReporter.report("NotCaptureHelper" , "init once"); config = CaptureConfig.builder().build(); encryptDecryptListener = new EncryptDecryptListener() { @Override public String encryptData(String key, String data) { LoggerReporter.report("NotCaptureHelper" , "decryptData data : " + data); String encryptString = Rc4EncryptUtils.encryptString(data, key); if (encryptString != null && encryptString.length()>0){ LoggerReporter.report("NotCaptureHelper" , "encryptData : " + encryptString); return encryptString; } return data; } @Override public String decryptData(String key, String data) { LoggerReporter.report("NotCaptureHelper" , "decryptData data : " + data); String decryptString = Rc4EncryptUtils.decryptString(data, key); if (decryptString != null && decryptString.length()>0){ LoggerReporter.report("NotCaptureHelper" , "decryptData : " + decryptString); return decryptString; } return data; } }; } public static NotCaptureHelper getInstance(){ if (notCaptureHelper == null){ synchronized (NotCaptureHelper.class){ if (notCaptureHelper == null){ notCaptureHelper = new NotCaptureHelper(); } } } return notCaptureHelper; } public void setConfig(CaptureConfig config) { this.config = config; LoggerReporter.report("NotCaptureHelper" , "setConfig : " + config); } public CaptureConfig getConfig() { return config; } public EncryptDecryptListener getEncryptDecryptListener() { return encryptDecryptListener; } public void setEncryptDecryptListener(EncryptDecryptListener encryptDecryptListener) { this.encryptDecryptListener = encryptDecryptListener; } /** * 傻瓜式🤪式配置 * @param context 上下文 * @param builder okHttpBuilder * @return */ public OkHttpClient.Builder setOkHttp(Context context, OkHttpClient.Builder builder){ //判断是否代理 if (config.isProxy() && ProxyWifiUtils.isWifiProxy(context)){ //基于抓包原理的基础上,直接使用okHttp禁止代理,经过测试,可以避免第三方工具(比如charles)抓包 builder.proxy(Proxy.NO_PROXY); LoggerReporter.report("NotCaptureHelper" , "setOkHttp 设置避免代理"); } //证书路径检验 if (config.isCaVerify() && config.getCerPath() != null && config.getCerPath().length()>0){ HttpSslConfig httpSslConfig = HttpSslFactory.generateSslConfig(NotCaptureUtils.generateSsl(config.getCerPath(),context)); //设置ssl证书校验 builder.sslSocketFactory(httpSslConfig.getSslSocketFactory(),httpSslConfig.getTrustManager()); //自定义了HostnameVerifier。在握手期间,如果 URL 的主机名和服务器的标识主机名不匹配,则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。 builder.hostnameVerifier(HttpSslFactory.generateUnSafeHostnameVerifier()); LoggerReporter.report("NotCaptureHelper" , "setOkHttp 设置证书校验和域名校验"); } //设置数据加解密 if (config.isEncrypt()){ builder.addInterceptor(new EncryptDecryptInterceptor()); LoggerReporter.report("NotCaptureHelper" , "setOkHttp 设置数据加解密"); } return builder; } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/proxy/ProxyWifiUtils.java ================================================ package com.yc.notcapturelib.proxy; import android.content.Context; import android.os.Build; import android.text.TextUtils; /** * @author yangchong * GitHub : https://github.com/yangchong211/YCAppTool * time : 2020/11/30 * desc : 代理工具类 */ public final class ProxyWifiUtils { /** * 判断设备 是否使用代理上网 * @param context 上下文 * @return 设备是否链接代理 */ public static boolean isWifiProxy(Context context) { // 是否大于等于4.0 final boolean IS_ICS_OR_LATER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH; String proxyAddress; int proxyPort; if (IS_ICS_OR_LATER) { proxyAddress = System.getProperty("http.proxyHost"); String portStr = System.getProperty("http.proxyPort"); proxyPort = Integer.parseInt((portStr != null ? portStr : "-1")); } else { proxyAddress = android.net.Proxy.getHost(context); proxyPort = android.net.Proxy.getPort(context); } return (!TextUtils.isEmpty(proxyAddress)) && (proxyPort != -1); } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/sign/SignGenerator.kt ================================================ package com.yc.notcapturelib.sign import com.yc.appencryptlib.Base64Utils import com.yc.appencryptlib.Md5EncryptUtils import java.util.* /** * @author yangchong * GitHub : https://github.com/yangchong211/YCAppTool * time : 2020/11/30 * desc : sign 工具类 */ object SignGenerator { fun generate(params: HashMap?, secret: String?): String { val sb = StringBuilder() if (params == null || params.size == 0) { return "" } val entries: Set> = params.entries for ((key, value) in entries) { sb.append(key).append("=").append(value).append("&") } if (secret != null && secret.isNotEmpty()) { sb.append("secret=").append(secret) } val paramsString = sb.toString() val encodeParams = Base64Utils.encodeToStringWrap(paramsString.toByteArray()) return if (encodeParams != null && encodeParams.isNotEmpty()) { Md5EncryptUtils.encryptMD5ToString(encodeParams) } else Md5EncryptUtils.encryptMD5ToString(paramsString) } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/ssl/HttpSslConfig.java ================================================ package com.yc.notcapturelib.ssl; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; /** * @author yangchong * GitHub : https://github.com/yangchong211/YCAppTool * time : 2020/11/30 * desc : Https 配置类 */ public final class HttpSslConfig { private final SSLSocketFactory mSSLSocketFactory; private final X509TrustManager mTrustManager; HttpSslConfig(SSLSocketFactory factory, X509TrustManager manager) { mSSLSocketFactory = factory; mTrustManager = manager; } public SSLSocketFactory getSslSocketFactory() { return mSSLSocketFactory; } public X509TrustManager getTrustManager() { return mTrustManager; } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/ssl/HttpSslFactory.java ================================================ package com.yc.notcapturelib.ssl; import java.io.IOException; import java.io.InputStream; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; /** * @author yangchong * GitHub : https://github.com/yangchong211/YCAppTool * time : 2020/11/30 * desc : Https 证书校验工厂 */ public final class HttpSslFactory { private final static String KEYSTORE_TYPE = "BKS"; private final static String PROTOCOL_TYPE = "TLS"; private final static String CERTIFICATE_FORMAT = "X509"; /** * 生成信任任何证书的配置 */ public static HttpSslConfig generateSslConfig() { return generateSslConfigBase(null, null, null); } /** * https 单向认证 */ public static HttpSslConfig generateSslConfig(X509TrustManager trustManager) { return generateSslConfigBase(trustManager, null, null); } /** * https 单向认证 */ public static HttpSslConfig generateSslConfig(InputStream... certificates) { return generateSslConfigBase(null, null, null, certificates); } /** * https 双向认证 */ public static HttpSslConfig generateSslConfig(InputStream bksFile, String password, InputStream... certificates) { return generateSslConfigBase(null, bksFile, password, certificates); } /** * https 双向认证 */ public static HttpSslConfig generateSslConfig(InputStream bksFile, String password, X509TrustManager trustManager) { return generateSslConfigBase(trustManager, bksFile, password); } /** * 生成认证配置 * * @param trustManager 可以额外配置信任服务端的证书策略,否则默认是按CA证书去验证的,若不是CA可信任的证书,则无法通过验证 * @param bksFile 客户端使用 bks 证书校验服务端证书 * @param password 客户端的 bks 证书密码 * @param certificates 用含有服务端公钥的证书校验服务端证书 */ private static HttpSslConfig generateSslConfigBase(X509TrustManager trustManager, InputStream bksFile, String password, InputStream... certificates) { try { KeyManager[] keyManagers = prepareKeyManager(bksFile, password); TrustManager[] trustManagers = prepareTrustManager(certificates); X509TrustManager manager; if (trustManager != null) { // 优先使用用户自定义的 TrustManager manager = trustManager; } else if (trustManagers != null) { // 然后使用默认的 TrustManager manager = chooseTrustManager(trustManagers); } else { // 否则使用不安全的 TrustManager manager = new UnSafeTrustManager(); } // 创建 TLS 类型的 SsLContext 对象,使用我们的 TrustManager SSLContext sslContext = SSLContext.getInstance(PROTOCOL_TYPE); // 用上面得到的 TrustManagers 初始化 SsLContext,这样 SslContext 就会信任keyStore中的证书 // 第一个参数是授权的密钥管理器,用来授权验证,比如授权自签名的证书验证。第二个是被授权的证书管理器,用来验证服务器端的证书 sslContext.init(keyManagers, new TrustManager[]{manager}, null); // 通过 SslContext 获取 SSLSocketFactory 对象 SSLSocketFactory socketFactory = sslContext.getSocketFactory(); return new HttpSslConfig(socketFactory, manager); } catch (NoSuchAlgorithmException | KeyManagementException e) { throw new AssertionError(e); } } private static KeyManager[] prepareKeyManager(InputStream bksFile, String password) { try { if (bksFile == null || password == null) { return null; } KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); keyStore.load(bksFile, password.toCharArray()); String defaultAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); KeyManagerFactory factory = KeyManagerFactory.getInstance(defaultAlgorithm); factory.init(keyStore, password.toCharArray()); return factory.getKeyManagers(); } catch (IOException | CertificateException | UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException e) { e.printStackTrace(); return null; } } private static TrustManager[] prepareTrustManager(InputStream... certificates) { if (certificates == null || certificates.length <= 0) { return null; } try { //获取X.509格式的内置证书 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); // 创建一个默认类型的 KeyStore,存储我们信任的证书 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null); int index = 0; for (InputStream certStream : certificates) { String certificateAlias = Integer.toString(index++); // 证书工厂根据证书文件的流生成证书 Cert Certificate cert = certificateFactory.generateCertificate(certStream); // 将 Cert 作为可信证书放入到 KeyStore 中 keyStore.setCertificateEntry(certificateAlias, cert); try { if (certStream != null) { certStream.close(); } } catch (IOException e) { e.printStackTrace(); } } // 我们创建一个默认类型的 TrustManagerFactory String defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory factory = TrustManagerFactory.getInstance(defaultAlgorithm); // 用我们之前的 KeyStore 实例初始化 TrustManagerFactory,这样 tmf 就会信任 KeyStore 中的证书 factory.init(keyStore); // 通过 tmf 获取 TrustManager 数组,TrustManager 也会信任 KeyStore 中的证书 return factory.getTrustManagers(); } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { e.printStackTrace(); return null; } } private static X509TrustManager chooseTrustManager(TrustManager[] trustManagers) { for (TrustManager trustManager : trustManagers) { if (trustManager instanceof X509TrustManager) { return (X509TrustManager) trustManager; } } return null; } public static HostnameVerifier generateUnSafeHostnameVerifier() { return new UnSafeHostnameVerifier(); } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/ssl/TrustAllCertsManager.java ================================================ package com.yc.notcapturelib.ssl; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Arrays; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; /** * @author yangchong * GitHub : https://github.com/yangchong211/YCAppTool * time : 2020/11/30 * desc : 信任所有证书,校验请求地址 */ public class TrustAllCertsManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } public static SSLSocketFactory createSSLSocketFactory() { SSLSocketFactory ssfFactory = null; TrustAllCertsManager trustAllCertsManager = new TrustAllCertsManager(); SecureRandom secureRandom = new SecureRandom(); try { SSLContext sc = SSLContext.getInstance("TLS"); sc.init(null, new TrustManager[]{trustAllCertsManager}, secureRandom); ssfFactory = sc.getSocketFactory(); } catch (Exception e) { } return ssfFactory; } public static class TrustAllHostnameVerifier implements HostnameVerifier { private String serverUrl; public TrustAllHostnameVerifier(String hostname) { serverUrl = hostname; } @Override public boolean verify(String hostname, SSLSession session) { if (serverUrl.contains(hostname)) { return true; } return false; } } public static X509TrustManager createX509TrustManager() { TrustManagerFactory trustManagerFactory = null; X509TrustManager trustManager = null; String defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); try { trustManagerFactory = TrustManagerFactory.getInstance(defaultAlgorithm); trustManagerFactory.init((KeyStore) null); TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers)); } trustManager = (X509TrustManager) trustManagers[0]; } catch (NoSuchAlgorithmException e) { } catch (Exception e) { } return trustManager; } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/ssl/UnSafeHostnameVerifier.java ================================================ package com.yc.notcapturelib.ssl; import android.annotation.SuppressLint; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSession; /** * @author yangchong * GitHub : https://github.com/yangchong211/YCAppTool * time : 2020/11/30 * desc : 证书域名校验 */ public final class UnSafeHostnameVerifier implements HostnameVerifier { @SuppressLint("BadHostnameVerifier") @Override public boolean verify(String hostname, SSLSession session) { // 此类是用于主机名验证的基接口。 在握手期间,如果 URL 的主机名和服务器的标识主机名不匹配, // 则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。策略可以是基于证书的或依赖于其他验证方案。 // 当验证 URL 主机名使用的默认规则失败时使用这些回调。如果主机名是可接受的,则返回 true // 自定义判断逻辑:true-安全,false-不安全 return true; } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/ssl/UnSafeTrustManager.java ================================================ package com.yc.notcapturelib.ssl; import android.annotation.SuppressLint; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; /** * @author yangchong * GitHub : https://github.com/yangchong211/YCAppTool * time : 2020/11/30 * desc : 为了解决客户端不信任服务器数字证书的问题,网络上大部分的解决方案都是让客户端不对证书做任何检查,这是一种有很大安全漏洞的办法 */ public final class UnSafeTrustManager implements X509TrustManager { @SuppressLint("TrustAllX509TrustManager") @Override public void checkClientTrusted(X509Certificate[] chain, String authType) { } @SuppressLint("TrustAllX509TrustManager") @Override public void checkServerTrusted(X509Certificate[] chain, String authType) { //检查所有证书 /*try { TrustManagerFactory factory = TrustManagerFactory.getInstance("X509"); factory.init((KeyStore) null); for (TrustManager trustManager : factory.getTrustManagers()) { ((X509TrustManager) trustManager).checkServerTrusted(chain, authType); } } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (KeyStoreException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); }*/ //获取网络中的证书信息 if (chain!=null){ X509Certificate certificate = chain[0]; // 证书拥有者 String subject = certificate.getSubjectDN().getName(); // 证书颁发者 String issuer = certificate.getIssuerDN().getName(); } } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[]{}; } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/utils/NotCaptureUtils.java ================================================ package com.yc.notcapturelib.utils; import android.content.Context; import android.content.res.AssetManager; import java.io.InputStream; import java.util.LinkedList; /** * @author yangchong * GitHub : https://github.com/yangchong211/YCAppTool * time : 2020/11/30 * desc : 代理工具类 */ public final class NotCaptureUtils { /** * 获取证书数据 * @param fileName * @param context * @return */ public static InputStream[] generateSsl(String fileName, Context context){ LinkedList list = NotCaptureUtils.getFromAssets(fileName, context); //创建一个新的String类型数组 InputStream[] arr = new InputStream[list.size()]; //将LinkedList转换为字符串数组 list.toArray(arr); return arr; } /** * 获取asset文件下的资源文件信息 * @param fileName * @return */ public static LinkedList getFromAssets(String fileName, Context context) { LinkedList inputStreams = new LinkedList<>(); try { AssetManager assetManager = context.getApplicationContext().getAssets(); if (assetManager == null){ return inputStreams; } String[] list = assetManager.list(fileName); if (list != null && list.length > 0){ for (int i= 0 ; i * @author yangchong * email : yangchong211@163.com * time : 2018/7/10 * desc : rc4加解密 * revise: * */ public final class Rc4EncryptUtils { /** * RC4 对称密码算法的工作方式有四种: * 1.电子密码本(ECB, electronic codebook)方式 * 2.密码分组链接(CBC, cipherblock chaining)方式 * 3.密文反馈(CFB, cipher-feedback)方式 * 4.输出反馈(OFB, output-feedback)方式 * * RC4算法采用的是输出反馈工作方式,所以可以用一个短的密钥产生一个相对较长的密钥序列。 * 输出反馈工作方式 * 最大的优点是消息如果发生错误(这里指的是消息的某一位发生了改变,而不是消息的某一位丢失),错误不会传递到产生的密钥序列上; * 缺点是对插入攻击很敏感,并且对同步的要求比较高。 * */ /** * RC4加密,加密失败将返回空串。 */ public static String encryptString(String data, String secretKey) { try { return encryptToBase64(data.getBytes(),secretKey); } catch (Throwable e) { e.printStackTrace(); } return ""; } /** * RC4加密字节数据。 */ public static byte[] encryptToByte(byte[] data, String secretKey) { try { return convert(data, secretKey); } catch (Throwable e) { e.printStackTrace(); } return null; } /** * RC4加密base64编码数据,加密失败将返回空串。 */ public static String encryptToBase64(byte[] data, String secretKey) { try { byte[] convert = convert(data, secretKey); return encodeToString(convert); } catch (Throwable e) { e.printStackTrace(); } return ""; } /** * RC4解密,解密失败将返回NULL。 */ public static String decryptString(String base64, String secretKey) { try { byte[] decryptFromBase64 = decryptFromBase64(base64, secretKey); return new String(decryptFromBase64); } catch (Throwable e) { e.printStackTrace(); } return null; } public static byte[] decryptByte(byte[] data, String secretKey) { try { return convert(data, secretKey); } catch (Throwable e) { e.printStackTrace(); } return null; } /** * RC4解密,解密失败将返回NULL。 */ public static byte[] decryptFromBase64(String base64, String secretKey) { try { //将BASE64编码的字符串解码 byte[] decodeFromString = decodeFromString(base64); return convert(decodeFromString, secretKey); } catch (Throwable e) { e.printStackTrace(); } return null; } /** * RC4加解密,会自动识别传入的是密文还是明文。加解密失败将抛异常。 */ private static byte[] convert(byte[] data, String secretKey) throws IllegalArgumentException { if (data == null || data.length == 0) { throw new IllegalArgumentException("data cannot be empty"); } if (secretKey == null || secretKey.trim().length() == 0) { throw new IllegalArgumentException("Key cannot be empty"); } //初始化密钥 byte[] bkey = secretKey.getBytes(); if (bkey.length == 0 || bkey.length > 256) { throw new IllegalArgumentException("Key length must 1-256"); } byte[] key = new byte[256]; for (int i = 0; i < 256; i++) { key[i] = (byte) i; } int index1 = 0; int index2 = 0; for (int i = 0; i < 256; i++) { index2 = ((bkey[index1] & 0xff) + (key[i] & 0xff) + index2) & 0xff; byte tmp = key[i]; key[i] = key[index2]; key[index2] = tmp; index1 = (index1 + 1) % bkey.length; } //开始加解密 int x = 0; int y = 0; int xorIndex; byte[] result = new byte[data.length]; for (int i = 0; i < data.length; i++) { x = (x + 1) & 0xff; y = ((key[x] & 0xff) + y) & 0xff; byte tmp = key[x]; key[x] = key[y]; key[y] = tmp; xorIndex = ((key[x] & 0xff) + (key[y] & 0xff)) & 0xff; result[i] = (byte) (data[i] ^ key[xorIndex]); } return result; } /** * 将数据进行Base64编码,并转为可展示的ASCII字符串. * 编码失败不会抛出异常,编码失败会返回NULL. */ public static String encodeToString(byte[] data) { try { byte[] encode = Base64.encode(data, Base64.NO_WRAP); // 使用GBK及UTF-8字符集都是兼容ASCII字符集的 //noinspection CharsetObjectCanBeUsed return new String(encode, "UTF-8").trim(); } catch (Throwable e) { e.printStackTrace(); return null; } } /** * 将BASE64编码的字符串解码. * 解码失败不会抛出异常,解码失败会返回NULL. */ public static byte[] decodeFromString(String data) { try { byte[] bytes = data.trim().getBytes(); return Base64.decode(bytes,Base64.NO_WRAP); } catch (Throwable e) { e.printStackTrace(); return null; } } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/xposed/CommandUtils.java ================================================ package com.yc.notcapturelib.xposed; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; /** *
 *     @author yangchong
 *     email  : yangchong211@163.com
 *     time  : 2020/7/10
 *     desc  : 指令工具类
 *     revise:
 * 
*/ public final class CommandUtils { public static String getProperty(String propName) { String value = null; Object roSecureObj; try { Class aClass = Class.forName("android.os.SystemProperties"); roSecureObj = aClass.getMethod("get", String.class).invoke(null, propName); if (roSecureObj != null){ value = (String) roSecureObj; } } catch (Exception e) { value = null; } finally { return value; } } public static String exec(String command) { BufferedOutputStream bufferedOutputStream = null; BufferedInputStream bufferedInputStream = null; Process process = null; try { process = Runtime.getRuntime().exec("sh"); bufferedOutputStream = new BufferedOutputStream(process.getOutputStream()); bufferedInputStream = new BufferedInputStream(process.getInputStream()); bufferedOutputStream.write(command.getBytes()); bufferedOutputStream.write('\n'); bufferedOutputStream.flush(); bufferedOutputStream.close(); process.waitFor(); String outputStr = getStrFromBufferInputSteam(bufferedInputStream); return outputStr; } catch (Exception e) { return null; } finally { if (bufferedOutputStream != null) { try { bufferedOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (bufferedInputStream != null) { try { bufferedInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (process != null) { process.destroy(); } } } /** * 读取流转变化为字符串 * @param bufferedInputStream 流 * @return */ private static String getStrFromBufferInputSteam(BufferedInputStream bufferedInputStream) { if (null == bufferedInputStream) { return ""; } int BUFFER_SIZE = 512; byte[] buffer = new byte[BUFFER_SIZE]; StringBuilder result = new StringBuilder(); try { while (true) { int read = bufferedInputStream.read(buffer); if (read > 0) { result.append(new String(buffer, 0, read)); } if (read < BUFFER_SIZE) { break; } } } catch (Exception e) { e.printStackTrace(); } return result.toString(); } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/xposed/HackChecker.java ================================================ package com.yc.notcapturelib.xposed; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; /** *
 *     @author yangchong
 *     email  : yangchong211@163.com
 *     time  : 2020/7/10
 *     desc  : 判断是否root
 *     revise:
 * 
*/ public final class HackChecker { /** * 检查当前设备是否root * @return */ public static boolean hasRoot() { return checkDeviceDebuggable() || checkSuperuserApk() || suFileExist() || whichSu(); } /** * 检查是否是测试版android系统 * @return */ private static boolean checkDeviceDebuggable(){ String buildTags = android.os.Build.TAGS; if (buildTags != null && buildTags.contains("test-keys")) { return true; } return false; } /** * 检查Superuser apk是否存在 * @return */ private static boolean checkSuperuserApk(){ try { File file = new File("/system/app/Superuser.apk"); if (file.exists()) { return true; } } catch (Exception e) { } return false; } /** * 检查su文件是否存在 * @return */ private static boolean suFileExist() { File f = null; final String kSuSearchPaths[] = {"/system/bin/", "/system/xbin/", "/system/sbin/", "/sbin/", "/vendor/bin/"}; try { for (int i = 0; i < kSuSearchPaths.length; i++) { f = new File(kSuSearchPaths[i] + "su"); if (f != null && f.exists()) { return true; } } } catch (Exception e) { e.printStackTrace(); } return false; } private static boolean whichSu() { String[] strCmd = new String[] {"/system/xbin/which","su"}; ArrayList execResult = executeCommand(strCmd); if (execResult != null){ return true; }else{ return false; } } private static ArrayList executeCommand(String[] shellCmd){ String line = null; ArrayList fullResponse = new ArrayList(); Process localProcess = null; try { localProcess = Runtime.getRuntime().exec(shellCmd); } catch (Exception e) { return null; } BufferedReader in = new BufferedReader(new InputStreamReader(localProcess.getInputStream())); try { while ((line = in.readLine()) != null) { fullResponse.add(line); } } catch (Exception e) { e.printStackTrace(); } finally { try { in.close(); } catch (IOException e) { } } return fullResponse; } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/xposed/VirtualApkUtils.java ================================================ package com.yc.notcapturelib.xposed; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.LocalServerSocket; import android.text.TextUtils; import android.util.Log; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.BindException; import java.net.ConnectException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Random; /** *
 *     @author yangchong
 *     email  : yangchong211@163.com
 *     time  : 2020/7/10
 *     desc  : VirtualApk 工具类
 *     revise:
 * 
*/ public final class VirtualApkUtils { private static volatile VirtualApkUtils singleInstance; private static final String TAG = "VirtualApkUtils"; private VirtualApkUtils() { } public static VirtualApkUtils getSingleInstance() { if (singleInstance == null) { synchronized (VirtualApkUtils.class) { if (singleInstance == null) { singleInstance = new VirtualApkUtils(); } } } return singleInstance; } /** * 维护一份市面多开应用的包名列表 */ private String[] virtualPkgs = { "com.bly.dkplat",//多开分身本身的包名 //"dkplugin.pke.nnp",//多开分身克隆应用的包名会随机变换 "com.by.chaos",//chaos引擎 "com.lbe.parallel",//平行空间 "com.excelliance.dualaid",//双开助手 "com.lody.virtual",//VirtualXposed,VirtualApp "com.qihoo.magic"//360分身大师 }; /** * 通过检测app私有目录,多开后的应用路径会包含多开软件的包名 * * @param context * @param callback * @return */ public boolean checkByPrivateFilePath(Context context, VirtualCallback callback) { String path = context.getFilesDir().getPath(); for (String virtualPkg : virtualPkgs) { if (path.contains(virtualPkg)) { if (callback != null) { callback.findSuspect(); } return true; } } return false; } /** * 检测原始的包名,多开应用会hook处理getPackageName方法 * 顺着这个思路,如果在应用列表里出现了同样的包,那么认为该应用被多开了 * * @param context * @param callback * @return */ public boolean checkByOriginApkPackageName(Context context, VirtualCallback callback) { try { if (context == null){ throw new IllegalArgumentException("you have to set context first"); } int count = 0; String packageName = context.getPackageName(); PackageManager pm = context.getPackageManager(); List pkgs = pm.getInstalledPackages(0); for (PackageInfo info : pkgs) { if (packageName.equals(info.packageName)) { count++; } } if (count > 1 && callback != null){ callback.findSuspect(); } return count > 1; } catch (Exception ignore) { } return false; } /** * 运行被克隆的应用,该应用会加载多开应用的so库 * 检测已经加载的so里是否包含这些应用的包名 * * @param callback * @return */ public boolean checkByMultiApkPackageName(VirtualCallback callback) { BufferedReader bufr = null; try { bufr = new BufferedReader(new FileReader("/proc/self/maps")); String line; while ((line = bufr.readLine()) != null) { for (String pkg : virtualPkgs) { if (line.contains(pkg)) { if (callback != null) { callback.findSuspect(); } return true; } } } } catch (Exception ignore) { //ignore.printStackTrace(); } finally { if (bufr != null) { try { bufr.close(); } catch (IOException e) { e.printStackTrace(); } } } return false; } /** * Android系统一个app一个uid * 如果同一uid下有两个进程对应的包名,在"/data/data"下有两个私有目录,则该应用被多开了 * * @param callback * @return */ public boolean checkByHasSameUid(VirtualCallback callback) { String filter = getUidStrFormat(); if (TextUtils.isEmpty(filter)) { return false; } String result = CommandUtils.exec("ps"); if (TextUtils.isEmpty(result)) { return false; } String[] lines = new String[0]; if (result != null) { lines = result.split("\n"); } if (lines.length <= 0){ return false; } int exitDirCount = 0; for (int i = 0; i < lines.length; i++) { if (filter != null && lines[i].contains(filter)) { int pkgStartIndex = lines[i].lastIndexOf(" "); String processName = lines[i].substring(pkgStartIndex <= 0 ? 0 : pkgStartIndex + 1); File dataFile = new File(String.format("/data/data/%s", processName, Locale.CHINA)); if (dataFile.exists()) { exitDirCount++; } } } if (exitDirCount > 1 && callback != null){ callback.findSuspect(); } return exitDirCount > 1; } private String getUidStrFormat() { String filter = CommandUtils.exec("cat /proc/self/cgroup"); if (filter == null || filter.length() == 0) { return null; } int uidStartIndex = filter.lastIndexOf("uid"); int uidEndIndex = filter.lastIndexOf("/pid"); if (uidStartIndex < 0) { return null; } if (uidEndIndex <= 0) { uidEndIndex = filter.length(); } filter = filter.substring(uidStartIndex + 4, uidEndIndex); try { String strUid = filter.replaceAll("\n", ""); if (isNumber(strUid)) { int uid = Integer.valueOf(strUid); filter = String.format("u0_a%d", uid - 10000); return filter; } return null; } catch (Exception e) { e.printStackTrace(); return null; } } private boolean isNumber(String str) { if (str == null || str.length() == 0) { return false; } for (int i = 0; i < str.length(); i++) { if (!Character.isDigit(str.charAt(i))) { return false; } } return true; } /** * 端口监听,先扫一遍已开启的端口并连接, * 如果发现能通信且通信信息一致, * 则认为之前有一个相同的自己打开了(也就是被多开了) * 如果没有,则开启监听 * 这个方法没有 checkByCreateLocalServerSocket 方法简单,不推荐使用 * * @param secret * @param callback */ @Deprecated public void checkByPortListening(String secret, VirtualCallback callback) { startClient(secret); new ServerThread(secret, callback).start(); } //此时app作为secret的接收方,也就是server角色 private class ServerThread extends Thread { String secret; VirtualCallback callback; private ServerThread(String secret, VirtualCallback callback) { this.secret = secret; this.callback = callback; } @Override public void run() { super.run(); startServer(secret, callback); } } //找一个没被占用的端口开启监听 //如果监听到有连接,开启读线程 private void startServer(String secret, VirtualCallback callback) { Random random = new Random(); ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(); int port = random.nextInt(55534)+ 10000; InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", port); serverSocket.bind(inetSocketAddress); while (true) { Socket socket = serverSocket.accept(); ReadThread readThread = new ReadThread(secret, socket, callback); readThread.start(); //serverSocket.close(); } } catch (BindException e) { //may be loop forever startServer(secret, callback); } catch (IOException e) { e.printStackTrace(); } } //读线程读流信息,如果包含secret则认为被广义多开 private class ReadThread extends Thread { private ReadThread(String secret, Socket socket, VirtualCallback callback) { InputStream inputStream = null; try { inputStream = socket.getInputStream(); byte buffer[] = new byte[1024 * 4]; int temp = 0; while ((temp = inputStream.read(buffer)) != -1) { String result = new String(buffer, 0, temp); if (result.contains(secret) && callback != null) { callback.findSuspect(); } } inputStream.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } //读文件扫描已开启的端口,放入端口列表,每个端口都尝试连接一次 private void startClient(String secret) { String tcp6 = CommandUtils.exec("cat /proc/net/tcp6"); if (TextUtils.isEmpty(tcp6)) return; String[] lines = tcp6.split("\n"); ArrayList portList = new ArrayList<>(); for (int i = 0, len = lines.length; i < len; i++) { int localHost = lines[i].indexOf("0100007F:");//127.0.0.1: if (localHost < 0) { continue; } String singlePort = lines[i].substring(localHost + 9, localHost + 13); Integer port = Integer.parseInt(singlePort, 16); portList.add(port); } if (portList.isEmpty()) { return; } for (int port : portList) { new ClientThread(secret, port).start(); } } //app此时作为secret的发送方(也就是client角色),发送完毕就结束 private static class ClientThread extends Thread { String secret; int port; private ClientThread(String secret, int port) { this.secret = secret; this.port = port; } @Override public void run() { super.run(); try { Socket socket = new Socket("127.0.0.1", port); socket.setSoTimeout(2000); OutputStream outputStream = socket.getOutputStream(); outputStream.write((secret + "\n").getBytes("utf-8")); outputStream.flush(); socket.shutdownOutput(); InputStream inputStream = socket.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); String info = null; while ((info = bufferedReader.readLine()) != null) { Log.i(TAG,"ClientThread: " + info); } bufferedReader.close(); inputStream.close(); socket.close(); } catch (ConnectException e) { Log.i(TAG,port + "port refused"); } catch (SocketException e) { e.printStackTrace(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } /** * 如issue25讨论 * https://github.com/lamster2018/EasyProtector/issues/25 * 感谢https://github.com/wangkunlin提供 * * @param uniqueMsg * @param callback * @return */ private volatile LocalServerSocket localServerSocket; public boolean checkByCreateLocalServerSocket(String uniqueMsg, VirtualCallback callback) { if (localServerSocket != null) { return false; } try { localServerSocket = new LocalServerSocket(uniqueMsg); return false; } catch (IOException e) { if (callback != null) { callback.findSuspect(); } return true; } } } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/xposed/VirtualCallback.java ================================================ package com.yc.notcapturelib.xposed; public interface VirtualCallback { void findSuspect(); } ================================================ FILE: NotCaptureLib/src/main/java/com/yc/notcapturelib/xposed/XposedUtils.java ================================================ package com.yc.notcapturelib.xposed; import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.List; import dalvik.system.BaseDexClassLoader; /** *
 *     @author yangchong
 *     email  : yangchong211@163.com
 *     time  : 2020/7/10
 *     desc  : Xposed环境 工具类
 *     revise:
 * 
*/ public final class XposedUtils { private static final String XPOSED_HELPERS = "de.robv.android.xposed.XposedHelpers"; private static final String XPOSED_BRIDGE = "de.robv.android.xposed.XposedBridge"; private static final String XPOSED_INSTALLER = "de.robv.android.xposed.installer"; /** * 第一种:获取当前设备所有运行的APP,根据安装包名对应用进行检测判断是否有Xposed环境。 * * @return */ public boolean isXposedExistsApk(Context context) { PackageManager packageManager = context.getPackageManager(); @SuppressLint("QueryPermissionsNeeded") List applicationInfoList = packageManager .getInstalledApplications(PackageManager.GET_META_DATA); for (ApplicationInfo item : applicationInfoList) { if (item.packageName.equals(XPOSED_INSTALLER)) { return true; } } return false; } /** * 第三种:通过ClassLoader检查是否已经加载了XposedBridge类和XposedHelpers类来检测 * * @return */ @Deprecated public boolean isXposedExists() { try { ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); //调用 loadClass 加载类 Class aClass = systemClassLoader.loadClass(XPOSED_HELPERS); Object xpHelperObj = aClass.newInstance(); } catch (InstantiationException e) { e.printStackTrace(); return true; } catch (IllegalAccessException e) { e.printStackTrace(); return true; } catch (ClassNotFoundException e) { e.printStackTrace(); return false; } try { ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); //调用 loadClass 加载类 Class aClass = systemClassLoader.loadClass(XPOSED_BRIDGE); Object xpBridgeObj = aClass.newInstance(); } catch (InstantiationException e) { e.printStackTrace(); return true; } catch (IllegalAccessException e) { e.printStackTrace(); return true; } catch (ClassNotFoundException e) { e.printStackTrace(); return false; } return true; } /** * 第二种:通过主动抛出异常,检查堆栈信息来判断是否存在XP框架 * * @return */ public static boolean isXposedExistByThrow() { try { throw new Exception("gg"); } catch (Throwable e) { return isXposedExists(e); } } /** * 第二种:判断是否有Xposed环境 * * @param thr 异常 * @return */ public static boolean isXposedExists(Throwable thr) { StackTraceElement[] stackTraces = thr.getStackTrace(); for (StackTraceElement stackTrace : stackTraces) { final String clazzName = stackTrace.getClassName(); if (clazzName != null && clazzName.contains(XPOSED_BRIDGE)) { return true; } } return false; } /** * 第三种:尝试关闭XP框架 * 先通过isXposedExistByThrow判断有没有XP框架 * 有的话先hookXP框架的全局变量disableHooks *

* 漏洞在,如果XP框架先hook了isXposedExistByThrow的返回值,那么后续就没法走了 * 现在直接先hookXP框架的全局变量disableHooks * * 1.如果不想自己的APP被Xposed框架修改,可以在应用内部关闭Xposed框架Hook的总开关,使其无法对应用程序进行Hook。 * 2.这个所谓的“总开关”实质上为XposedBridge.java文件中的disableHooks变量 * 3.disableHooks变量默认为false,若该值为true则表示关闭了Xposed框架Hook的总开关。 * 4.可以通过反射的方法去获取disableHooks变量 * * @return 是否关闭成功的结果 */ public static boolean tryShutdownXposed() { Field xpdisabledHooks; try { ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); Class aClass = systemClassLoader.loadClass(XPOSED_BRIDGE); xpdisabledHooks = aClass.getDeclaredField("disableHooks"); xpdisabledHooks.setAccessible(true); xpdisabledHooks.set(null, Boolean.TRUE); return true; } catch (NoSuchFieldException e) { e.printStackTrace(); return false; } catch (ClassNotFoundException e) { e.printStackTrace(); return false; } catch (IllegalAccessException e) { e.printStackTrace(); return false; } } /** * 第四种:获取DEX加载列表,判断其中是否包含XposedBridge.jar等字符串。 * * @return */ public static boolean isXposedByJar() { boolean z = false; try { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); Class cls = Class.forName("dalvik.system.DexPathList"); Method method = Class.forName("dalvik.system.DexPathList$Element").getMethod("toString", new Class[0]); Field declaredField = cls.getDeclaredField("dexElements"); declaredField.setAccessible(true); Field declaredField2 = BaseDexClassLoader.class.getDeclaredField("pathList"); declaredField2.setAccessible(true); Object[] objArr = (Object[]) declaredField.get(declaredField2.get(classLoader)); for (Object obj : objArr) { try { String str2 = (String) method.invoke(obj, new Object[0]); if (str2 != null && str2.contains("XposedBridge.jar")) { z = true; } } catch (Exception e2) { z = false; } } }catch(Exception e3){ z = false; } return z; } } ================================================ FILE: NotCaptureLib/src/main/res/xml/network_security_config.xml ================================================ ================================================ FILE: NotCaptureLib/src/main/res/xml/network_security_config_debug.xml ================================================ ================================================ FILE: README.md ================================================ #### 目录介绍 - 01.整体概述介绍 - 1.1 项目背景 - 1.2 思考问题 - 1.3 设计目标 - 1.4 收益分析 - 02.市面抓包的分析 - 2.1 Https三要素 - 2.2 抓包核心原理 - 2.3 搞定CA证书 - 2.4 突破CA证书校验 - 2.5 如何搞定加解密 - 2.6 Charles原理 - 2.7 抓包原理图 - 2.8 抓包核心流程 - 03.防止抓包思路 - 3.1 先看如何抓包 - 3.2 设置配置文件 - 3.3 数据加密处理 - 3.4 避免黑科技抓包 - 04.防抓包实践开发 - 4.1 App安全配置 - 4.2 关闭代理 - 4.3 证书校验 - 4.4 双向认证 - 4.5 防止挂载抓包 - 4.6 数据加解密 - 4.7 证书锁定 - 4.8 Sign签名 - 4.9 其他的方式 - 05.架构设计说明 - 5.1 整体架构设计 - 5.2 关键流程图 - 5.3 稳定性设计 - 5.4 降级设计 - 5.5 异常设计说明 - 06.防抓包功能自测 - 6.1 网络请求测试 - 6.2 抓包测试 - 6.3 黑科技挂载测试 - 6.4 逆向破解测试 ### 01.整体概述介绍 #### 1.1 项目背景 - 通讯安全是App安全检测过程中非常重要的一项 - 针对该项的主要检测手段就是使用中间人代理机制对网络传输数据进行抓包、拦截和篡改,以检验App在核心链路上是否有安全漏洞。 - 保证数据安全 - 通过charles等工具可以对app的网络请求进行抓包,这样这些信息就会被清除的提取出来,会被不法分子进行利用。 - 不想被竞争对手逆向抓包 - 不想自身App的数据被别人轻而易举地抓包获取到,从而进行类似业务或数据分析、爬虫或网络攻击等破坏性行为。 #### 1.2 思考问题 - 开发项目的时候,都需要抓包,很多情况下即使是Https也能正常抓包正常。那么问题来了: - 抓包的原理是?任何Https的 app 都能抓的到吗?如果不能,哪些情况下可以抓取,哪些情况下抓取不到? - 什么叫做中间人攻击? - 使用HTTPS协议进行通信时,客户端需要对服务器身份进行完整性校验,以确认服务器是真实合法的目标服务器。 - 如果没有校验,客户端可能与仿冒的服务器建立通信链接,即“中间人攻击”。 #### 1.3 设计目标 - 防止App被各种方式抓包 - 做好各种防抓包安全措施,避免各种黑科技抓包。 - 沉淀为技术库复用 - 目前只是针对App端有需要做防抓包措施,后期其他业务线可能也有这个需要。因此下沉为工具库,傻瓜式调用很有必要。 - 该库终极设计目标如下所示 - 第一点:必须是低入侵性,对原有代码改动少,最简单的加入是一行代码设置即可。完全解耦合。 - 第二点:可以动态灵活配置,支持配置禁止代理,支持配置是否证书校验,支持配置域名合法性过滤,支持拦截器加解密数据。 - 第三点:可以检测App是否在双开,挂载,Xposed攻击环境 - 第四点:可以灵活设置加解密的key,可以灵活替换加解密方式,比如目前采用RC4,另一个项目想用DES,可以灵活更换。 #### 1.4 收益分析 - 抓包库收益 - 提高产品App的数据安全,必须对数据传输做好安全保护措施和完整性校验,以防止自身数据在网络传输中裸奔,甚至是被三方恶意利用或攻击。 - 技能的收益 - 下沉为功能基础库,可以方便各个产品线使用,提高开发的效率。避免跟业务解耦合。傻瓜式调用,低成本接入! ### 02.市面抓包的分析 #### 2.1 Https三要素 - 要清楚HTTPS抓包的原理,首先需要先说清楚 HTTPS 实现数据安全传输的工作原理,主要分为三要素和三阶段。 - Http传输数据目前存在的问题 - 1.通信使用明文,内容可能被窃听;2.不验证通信方的身份,因此可能遭遇伪装;3.无法证明报文的完整性,所以有可能遭到篡改。 - ![image](https://img-blog.csdnimg.cn/e83ebd1f1ca94f87b3148fb15dc8a5f7.png) - Https三要素分别是: - 1.加密:通过对称加密算法实现。 - 2.认证:通过数字签名实现。(因为私钥只有 “合法的发送方” 持有,其他人伪造的数字签名无法通过验证) - 3.报文完整性:通过数字签名实现。(因为数字签名中使用了消息摘要,其他人篡改的消息无法通过验证) - Https三阶段分别是: - 1.CA 证书校验:CA 证书校验发生在 TLS 的前两次握手,客户端和服务端通过报文获得服务端 CA 证书,客户端验证 CA 证书合法性,从而确认 CA 证书中的公钥合法性(大多数场景不会做双向认证,即服务端不会认证客户端合法性,这里先不考虑)。 - 2.密钥协商:密钥协商发生在 TLS 的后两次握手,客户端和服务端分别基于公钥和私钥进行非对称加密通信,协商获得 Master Secret 对称加密私钥(不同算法的协商过程细节略有不同)。 - 3.数据传输:数据传输发生在 TLS 握手之后,客户端和服务端基于协商的对称密钥进行对称加密通信。 - Https流程图如下 - ![image](https://img-blog.csdnimg.cn/d714c2bb30a444b7b1422038b7eb7f25.png) #### 2.2 抓包核心原理 - HTTPS抓包原理 - Fiddler、Charles等抓包工具,其实都是采用了中间人攻击的方案: 将客户端的网络流量代理到MITM(中间人)主机,再通过一系列的面板或工具将网络请求结构化地呈现出来。 - 抓包Https有两个突破点 - CA证书校验是否合法;数据传递过程中的加密和解密。如果是要抓包,则需要突破这两点的技术,无非就是MITM(中间人)伪造证书和使用自己的加解密方式。 - 抓包的工作流程如下 - 中间人截获客户端向发起的HTTPS请求,佯装客户端,向真实的服务器发起请求; - 中间人截获真实服务器的返回,佯装真实服务器,向客户端发送数据; - 中间人获取了用来加密服务器公钥的非对称秘钥和用来加密数据的对称秘钥,处理数据加解密。 #### 2.3 搞定CA证书 - Https抓包核心CA证书 - HTTPS抓包的原理还是挺简单的,简单来说,就是Charles作为“中间人代理”,拿到了服务器证书公钥和HTTPS连接的对称密钥。 - 前提是客户端选择信任并安装Charles的CA证书,否则客户端就会“报警”并中止连接。这样看来,HTTPS还是很安全的。 - 安装CA证书到手机中必须洗白 - 抓包应用内置的 CA 证书要洗白,必须安装到系统中。而 Android 系统将 CA 证书又分为两种:用户 CA 证书和系统 CA 证书(必要Root权限)。 - Android从7.0开始限制CA证书 - 只有系统(system)证书才会被信任。用户(user)导入的Charles根证书是不被信任的。相当于可以理解Android系统增加了安全校验! - 如何绕过CA证书这种限制呢?已知有以下四种方式 - 第一种方式:AndroidManifest 中配置 networkSecurityConfig,App 信任用户 CA 证书,让系统对用户 CA 证书的校验给予通过。 - 第二种方式:调低 targetSdkVersion < 24,不过这种方式谷歌市场有限制,意味着抓 HTTPS 的包越来越难操作。 - 第三种方式:挂载App抓包,VirtualApp 这种多开应用可以作为宿主系统来运行其它应用,利用xposed避开CA证书校验。 - 第四种方式:Root手机,把 CA 证书安装到系统 CA 证书目录中,那这个假 CA 证书就是真正洗白了,难度较大。 #### 2.4 突破CA证书校验 - App版本如何让证书校验安全 - 1.设置targetSdkVersion大于24,去掉清单文件中networkSecurityConfig文件中的system和user配置,设置不信任用户证书。 - 2.公钥证书固定。指 Client 端内置 Server 端真正的公钥证书。在 HTTPS 请求时,Server 端发给客户端的公钥证书必须与 Client 端内置的公钥证书一致,请求才会成功。 - 证书固定的一般做法是,将公钥证书(.crt 或者 .cer 等格式)内置到 App 中,然后创建 TrustManager 时将公钥证书加进去。 - 那么如何突破CA证书校验 - 第一种:JustTrustMe 破解证书固定。Xposed 和 Magisk 都有相应的模块,用来破解证书固定,实现正常抓包。破解的原理大致是,Hook 创建 SSLContext 等涉及 TrustManager 相关的方法,将固定的证书移除。 - 第二种:基于 VirtualApp 的 Hook 机制破解证书固定。在 VirtualApp 中加入 Hook 代码,然后利用 VirtualApp 打开目标应用进行抓包。具体看:[VirtualHook](https://github.com/PAGalaxyLab/VirtualHook) #### 2.5 如何搞定加解密 - 目前使用对称加密和解密请求和响应数据 - 加密和解密都是用相同密钥。只有一把密钥,如果密钥暴露,内容就会暴露。但是这一块逆向破解有些难度。而破解解密方式就是用密钥逆向解密,或者中间人冒充使用自己的加解密方式! - 加密后数据镇兼顾了安全性吗 - 不一定安全。中间人伪造自己的公钥和私钥,然后拦截信息,进行篡改。 #### 2.6 Charles原理 - Charles类似代理服务器 - Charles 通过将软件本身设置成系统的网络访问代理服务器,使得所有的网络请求都会走一遍 Charles 代理,从而 Charles 可以截取经过它的请求,然后我们就可以对其进行网络包的分析。 - 截取设备网络封包数据 - Charles对应设置:将代理功能打开,并设置一个固定的端口。默认情况下,端口号为:8888 。 - 移动设备设置:在手机上设置 WIFI 的 HTTP 代理。注意这里的前提是,Phone 和 Charles 代理设备链接的是同一网络(同一个ip地址和端口号)。 - 截取Https的网络封包 - 正常情况下,Charles 是不能截取Https的网络包的,这涉及到 Https 的证书问题。 #### 2.7 抓包原理图 - Charles抓包原理图 - ![image](https://img-blog.csdnimg.cn/20200921192339473.png) - Android上的网络抓包原来是这样工作的 - [Charles抓包](https://mp.weixin.qq.com/s/kqMUbHl59V75w8xBxHbXkA) #### 2.8 抓包核心流程 - 抓包核心流程关键节点 - 第一步,客户端向服务器发起HTTPS请求,charles截获客户端发送给服务器的HTTPS请求,charles伪装成客户端向服务器发送请求进行握手 。 - 第二步,服务器发回相应,charles获取到服务器的CA证书,用根证书(这里的根证书是CA认证中心给自己颁发的证书)公钥进行解密,验证服务器数据签名,获取到服务器CA证书公钥。然后charles伪造自己的CA证书(这里的CA证书,也是根证书,只不过是charles伪造的根证书),冒充服务器证书传递给客户端浏览器。 - 第三步,与普通过程中客户端的操作相同,客户端根据返回的数据进行证书校验、生成密码Pre_master、用charles伪造的证书公钥加密,并生成HTTPS通信用的对称密钥enc_key。 - 第四步,客户端将重要信息传递给服务器,又被charles截获。charles将截获的密文用自己伪造证书的私钥解开,获得并计算得到HTTPS通信用的对称密钥enc_key。charles将对称密钥用服务器证书公钥加密传递给服务器。 - 第五步,与普通过程中服务器端的操作相同,服务器用私钥解开后建立信任,然后再发送加密的握手消息给客户端。 - 第六步,charles截获服务器发送的密文,用对称密钥解开,再用自己伪造证书的私钥加密传给客户端。 - 第七步,客户端拿到加密信息后,用公钥解开,验证HASH。握手过程正式完成,客户端与服务器端就这样建立了”信任“。 - 在之后的正常加密通信过程中,charles如何在服务器与客户端之间充当第三者呢? - 服务器—>客户端:charles接收到服务器发送的密文,用对称密钥解开,获得服务器发送的明文。再次加密, 发送给客户端。 - 客户端—>服务端:客户端用对称密钥加密,被charles截获后,解密获得明文。再次加密,发送给服务器端。由于charles一直拥有通信用对称密钥enc_key,所以在整个HTTPS通信过程中信息对其透明。 ### 03.防止抓包思路 #### 3.1 先看如何抓包 - 使用Charles需要做哪些操作 - **1.电脑上需要安装证书**。这个主要是让Charles充当中间人,颁布自己的CA证书。 - **2.手机上需要安装证书**。这个是访问Charles获取手机证书,然后安装即可。 - **3.Android项目代码设置兼容**。Google 推出更加严格的安全机制,应用默认不信任用户证书(手机里自己安装证书),自己的app可以通过配置解决,相当于信任证书的一种操作! - 尤其可知抓包的突破口集中以下几点 - 第一点:必须链接代理,且跟Charles要具有相同ip。**思路:客户端是否可以判断网络是否被代理了**。 - 第二点:CA证书,这一块避免使用黑科技hook证书校验代码,或者拥有修改CA证书权限。**思路:集中在可以判断是否挂载**。 - 第三点:冒充中间人CA证书,在客户端client和服务端server之间篡改拦截数据。**思路:可以做CA证书校验**。 - 第四点:为了可以在7.0上抓包,App往往配置清单文件networkSecurityConfig。**思路:线上环境去掉该配置**。 #### 3.2 设置配置文件 - 一个是CA证书配置文件 - debug包为了能够抓包,需要配置networkSecurityConfig清单文件的system和user权限,只有这样才会信任用户证书。 - 一个是检验证书配置 - 不论是权威机构颁发的证书还是自签名的,打包一份到 app 内部,比如存放在 asset 里。然后用这个KeyStore去引导生成的TrustManager来提供证书验证。 - 一个是检验域名合法性 - Android允许开发者重定义证书验证方法,使用HostnameVerifier类检查证书中的主机名与使用该证书的服务器的主机名是否一致。 - 如果重写的HostnameVerifier不对服务器的主机名进行验证,即验证失败时也继续与服务器建立通信链接,存在发生“中间人攻击”的风险。 - 如何查看CA证书的数据 - [证书验证网站](https://www.myssl.cn/tools/downloadchain.html) ;[SSL配置检查网站](https://www.geocerts.com/ssl-checker) #### 3.3 数据加密处理 - 网络数据加密的需求 - 为了项目数据安全性,对请求体和响应体加密,那肯定要知道请求体或响应体在哪里,然后才能加密,其实都一样不论是加密url里面的query内容还是加密body体里面的都一样。 - 对数据哪里进行加密和解密 - 目前对数据返回的data进行加解密。那么如何做数据加密呢?目前项目中采用RC4加密和解密数据。 - 抓取到的内容为乱码 - 有的APP为了防止抓取,在返回的内容上做了层加密,所以从Charles上看到的内容是乱码。这种情况下也只能反编译APP,研究其加密解密算法进行解密。难度极大! #### 3.4 避免黑科技抓包 - 基于Xposed(或者)黑科技破解证书校验 - 这种方式可以检查是否有Xposed环境,大概的思路是使用ClassLoader去加载固定包名的xp类,或者手动抛出异常然后捕获去判断是否包含Xposed环境。 - 基于VirtualApp挂载App突破证书访问权限 - 这个VirtualApp相当于是一个宿主App(可以把它想像成桌面级App),它突破证书校验。然后再实现挂载App的抓包。判断是否是双开环境! ### 04.防抓包实践开发 #### 4.1 App安全配置 - 添加配置文件 - android:networkSecurityConfig="@xml/network_security_config" - 配置networkSecurityConfig抓包说明 - 中间人代理之所有能够获取到加密密钥就是因为我们手机上安装并信任了其代理证书,这类证书安装后都会被归结到用户证书一类,而不是系统证书。 - 那我们可以选择只信任系统内置的系统证书,而屏蔽掉用户证书(Android7.0以后就默认是只信任系统证书了),就可以防止数据被解密了。 - 实现App防抓包安全配置方式有两种: - 一种是Android官方提供的网络安全配置;另一种也可以通过设置网络框架实现(以okhttp为例)。 - 第一种:具体可以看清单配置文件,相当于base-config标签下去掉 这组标签。 - 第二种:需要给okhttpClient配置 X509TrustManager 来监听校验服务端证书有效性。遍历设备上信任的证书,通过证书别名将用户证书(别名中含有user字段)过滤掉,只将系统证书添加到验证列表中。 - 该方案优点和缺点分析说明 - 优点:network_security_config配置简单,对整个app网络生效,无需修改代码;代码实现对通过该网络框架请求的生效,能兼容7.0以前系统。 - 缺陷:network_security_config配置方式,7.0以前的系统配置不生效,依然可以通过代理工具进行抓包。okhttp配置的方式只能对使用该网络框架进行数据传输的接口生效,并不能对整个app生效。 - 破解:将手机进行root,然后将代理证书放置到系统证书列表内,就可以绕过代码或配置检查了。 #### 4.2 关闭代理 - charles 和 fiddler 都使用代理来进行抓包,对网络客户端使用无代理模式即可防止抓包,如 ``` java OkHttpClient.Builder() .proxy(Proxy.NO_PROXY) .build() ``` - no_proxy实际上就是type属性为direct的一个proxy对象,这个type有三种 - direct,http,socks。这样因为是直连,所以不走代理。所以charles等工具就抓不到包了,这样一定程度上保证了数据的安全,这种方式只是通过代理抓不到包。 - 通常情况下上述的办法有用,但是无法防住使用 VPN 导流进行的抓包 - 使用VPN抓包的原理是,先将手机请求导到VPN,再对VPN的网络进行Charles的代理,绕过了对App的代理。 - 该方案优点和缺点分析说明 - 优点:实现简单方便,无系统版本兼容问题。 - 缺陷:该方案比较粗暴,将一切代理都切断了,对于有合理诉求需要使用网络代理的场景无法满足。 - 破解:使用ProxyDroid全局代理工具通过iptables对请求进行强制转发,可以有效绕过代理检测。 #### 4.3 证书校验(单向认证) - 下载服务器端公钥证书 - 为了防止上面方案可能导致的“中间人攻击”,可以下载服务器端公钥证书,然后将公钥证书编译到Android应用中一般在assets文件夹保存,由应用在交互过程中去验证证书的合法性。 - 如何设置证书校验 - 通过OkHttp的API方法 sslSocketFactory(sslSocketFactory,trustManager) 设置SSL证书校验。 - 如何设置域名合法性校验 - 通过OkHttp的API方法 hostnameVerifier(hostnameVerifier) 设置域名合法性校验。 - 证书校验的原理分析 - 按CA证书去验证的,若不是CA可信任的证书,则无法通过验证。 - 单向认证流程图 - ![image](https://img-blog.csdnimg.cn/888b3c81230d47a8bf073fbbff97d046.png) - 该方案优点和缺点分析说明 - 优点:安全性比较高,单向认证校验证书在代码中是方便的,安全性相对较高。 - 缺陷:CA证书存在过期的问题,证书升级。 - 破解:证书锁定破解比较复杂,比如老牌的JustTrustMe插件,通过hook各网络框架的证书校验方法,替换原有逻辑,使校验失效。 #### 4.4 双向认证 - 什么叫做双向认证 - SSL/TLS 协议提供了双向认证的功能,即除了 Client 需要校验 Server 的真实性,Server 也需要校验 Client 的真实性。 - 双向认证的原理 - 双向认证需要 Server 支持,Client 必须内置一套公钥证书 + 私钥。在 SSL/TLS 握手过程中,Server 端会向 Client 端请求证书,Client 端必须将内置的公钥证书发给 Server,Server 验证公钥证书的真实性。 - 用于双向认证的公钥证书和私钥代表了 Client 端身份,所以其是隐秘的,一般都是用 .p12 或者 .bks 文件 + 密钥进行存放。 - 代码层面如何做双向认证 - 双向校验就是自定义生成客户端证书,保存在服务端和客户端,当客户端发起请求时在服务端也校验客户端的证书合法性,如果不是可信任的客户端发送的请求,则拒绝响应。 - 服务端根据自身使用语言和网络框架配置相应证书校验机制即可。 - 双向认证流程图 - ![image](https://img-blog.csdnimg.cn/7e52929766224f86a23dee5c83fa24e6.png) - 该方案优点和缺点分析说明 - 优点:安全性非常高,使用三方工具不易破解。 - 缺陷:服务端需要存储客户端证书,一般服务端会对应多个客户端,就需要分别存储和校验客户端证书,增加校验成本,降低响应速度。该方案比较适合对安全等级要求比较高的业务(如金融类业务)。 - 破解:由于在服务端也做校验,在服务端安全的情况下很难被攻破。 #### 4.5 防止挂载抓包 - Xposed是一个牛逼的黑科技 - Xposed + JustTrustMe 可以破解绕过校验CA证书。那么这样CA证书的校验就形同虚设了,对App的危险性也很大。 - App多开运行在多个环境上 - 多开App的原理类似,都是以新进程运行被多开的App,并hook各类系统函数,使被多开的App认为自己是一个正常的App在运行。 - 一种是从多开App中直接加载被多开的App,如平行空间、VirtualApp等,另一种是让用户新安装一个App,但这个App本质上就是一个壳,用来加载被多开的App。 - VirtualApp是一个牛逼的黑科技 - 它破坏了Android 系统本身的隔离措施,可以进行免root hook和其他黑科技操作,你可以用这个做很多在原来APP里做不到事情,于此同时Virtual App的安全威胁也不言而喻。 - 如何判断是否具有Xposed环境 - 第一种方式:获取当前设备所有运行的APP,根据安装包名对应用进行检测判断是否有Xposed环境。 - 第二种方式:通过自造异常来检测堆栈信息,判断异常堆栈中是否包含Xposed等字符串。 - 第三种方式:通过ClassLoader检查是否已经加载了XposedBridge类和XposedHelpers类来检测。 - 第四种方式:获取DEX加载列表,判断其中是否包含XposedBridge.jar等字符串。 - 第五种方式:检测Xposed相关文件,通过读取/proc/self/maps文件,查找Xposed相关jar或者so文件来检测。 - 如何判断是否是双开环境 - 第一种方式:通过检测app私有目录,多开后的应用路径会包含多开软件的包名。还有一种思路遍历应用列表如果出现同样的包名,则被认为双开了。 - 第二种方式:如果同一uid下有两个进程对应的包名,在"/data/data"下有两个私有目录,则该应用被多开了。 - 判断了具有xposed或者多开环境怎么处理App - 目前使用VirtualApp挂载,或者Xposed黑科技去hook,前期可以先用埋点统计。测试学而思App发现挂载在VA上是推出App。 #### 4.5 数据加解密 - 针对数据加解密入口 - 目前在网络请求类里添加拦截器,然后在拦截器中处理request请求和response响应数据的加密和解密操作。 - 主要是加密什么数据 - 在request请求数据阶段,如果是get请求加密url数据,如果是post请求则加密url数据和requestBody数据。 - 在response响应数据阶段, - 如何进行加密:发起请求(加密) - 第一步:获取请求的数据。主要是获取请求url和requestBody,这一块需要对数据一块处理。 - 第二步:对请求数据进行加密。采用RC4加密数据 - 第三步:根据不同的请求方式构造新的request。使用 key 和 result 生成新的 RequestBody 发起网络请求 - 如何进行解密:接收返回(解密) - 第一步:常规解析得到 result ,然后使用RC4工具,传入key去解密数据得到解密后的字符串 - 第二步:将解密的字符串组装成ResponseBody数据传入到body对象中 - 第三步:利用response对象去构造新的response,然后最后返回给App #### 4.7 证书锁定 - 证书锁定是Google官方比较推荐的一种校验方式 - 原理是在客户端中预先设置好证书信息,握手时与服务端返回的证书进行比较,以确保证书的真实性和有效性。 - 如何实现证书锁定 - 有两种实现方式:一种通过network_security_config.xml配置,另一种通过代码设置; ``` java //第一种方式:配置文件 api.zuoyebang.cn 38JpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhK90= 9k1a0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM90K= //第二种方式:代码设置 fun sslPinning(): OkHttpClient { val builder = OkHttpClient.Builder() val pinners = CertificatePinner.Builder() .add("api.zuoyebang.cn", "sha256//89KpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRh00L=") .add("api.zuoyebang.com", "sha256//a8za0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1o=09") .build() builder.apply { certificatePinner(pinners) } return builder.build() } ``` - 该方案优点和缺点分析说明 - 优点:安全性高,配置方式也比较简单,并能实现动态更新配置。 - 缺陷:网络安全配置无法实现证书证书的动态更新,另外该配置也受Android系统影响,对7.0以前的系统不支持。代码配置相对灵活些。 - 破解:证书锁定破解比较复杂,比如老牌的JustTrustMe插件,通过hook各网络框架的证书校验方法,替换原有逻辑,使校验失效 #### 4.8 Sign签名 - 先说一下背景和问题 - http://api.test.com/getbanner?key1=value1&key2=value2&key3=value3 - 这种方式简单粗暴,通过调用getbanner方法即可获取轮播图列表信息,但是这样的方式会存在很严重的安全性问题,没有进行任何的验证,大家都可以通过这个方法获取到数据,导致产品信息泄露。 - 在写开放的API接口时是如何保证数据的安全性的? - 请求来源(身份)是否合法?请求参数被篡改?请求的唯一性(不可复制)? - 问题的解决方案设想 - 解决方案:为了保证数据在通信时的安全性,我们可以采用参数签名的方式来进行相关验证。 - 最终决定的解决方案 - 调用接口之前需要验证签名和有效时间,要生成一个sign签名。先拼接-后转码-再加密-再发请求! - sign签名校验实践 - 需要对请求参数进行签名验证,签名方式如下:key1=value1&key2=value2&key3=value3&secret=yc 。对这个字符串进行md5一下。 - 然后被sign后的接口就变成了:http://api.test.com/getbanner?key1=value1&key2=value2&key3=value3&sign=xxx - 为什么在获取sign的时候建议使用secret参数?secret仅作加密使用,添加在参数中主要是md5,为了保证数据安全请不要在请求参数中使用。 - 服务端对sign校验 - 这样请求的时候就需要合法正确签名sign才可以获取数据。这样就解决了身份验证和防止参数篡改问题,如果请求参数被人拿走,没事,他们永远也拿不到secret,因为secret是不传递的。再也无法伪造合法的请求。 - 如何保证请求的唯一性 - http://api.test.com/getbanner?key1=value1&key2=value2&key3=value3&sign=xxx&stamp=201803261407 - 通过stamp时间戳用来验证请求是否过期。这样就算被人拿走完整的请求链接也是无效的。 - Sign签名安全性分析: - 通过上面的案例,安全的关键在于参与签名的secret,整个过程中secret是不参与通信的,所以只要保证secret不泄露,请求就不会被伪造。 ### 05.架构设计说明 #### 5.1 整体架构设计 #### 5.2 关键流程图 #### 5.3 稳定性设计 - 对于请求和响应的数据加解密要注意 - 在网络上交换数据(网络请求数据)时,可能会遇到不可见字符,不同的设备对字符的处理方式有一些不同。 - Base64对数据内容进行编码来适合传输。准确说是把一些二进制数转成普通字符用于网络传输。统统变成可见字符,这样出错的可能性就大降低了。 #### 5.4 降级设计 - 可以一键配置AB测试开关 ``` .setMonitorToggle(object : IMonitorToggle { override fun isOpen(): Boolean { //todo 是否降级,如果降级,则不使用该功能。留给AB测试开关 return false } }) ``` #### 5.5 异常设计说明 - base64加密和解密导致错误问题 - Android 有自带的Base64实现,flag要选Base64.NO_WRAP,不然末尾会有换行影响服务端解码。导致解码失败。 #### 5.6 Api文档 - 关于初始化配置 ``` java NotCaptureHelper.getInstance().config = CaptureConfig.builder() //设置debug模式 .setDebug(true) //设置是否禁用代理 .setProxy(false) //设置是否进行数据加密和解密, .setEncrypt(true) //设置cer证书路径 .setCerPath("") //设置是否进行CA证书校验 .setCaVerify(false) //设置加密和解密key .setEncryptKey(key) //设置参数 .setReservedQueryParam(OkHttpBuilder.RESERVED_QUERY_PARAM_NAMES) .setMonitorToggle(object : IMonitorToggle { override fun isOpen(): Boolean { //todo 是否降级,如果降级,则不使用该功能。留给AB测试开关 return false } }) .build() ``` - 设置okHttp配置 ``` java NotCaptureHelper.getInstance().setOkHttp(app,okHttpBuilder) ``` - 如何设置自己的加解密方式 ``` java NotCaptureHelper.getInstance().encryptDecryptListener = object : EncryptDecryptListener { /** * 外部实现自定义加密数据 */ override fun encryptData(key: String, data: String): String { LoggerReporter.report("NotCaptureHelper", "encryptData data : $data") val str = data.encryptWithRC4(key) ?: "" LoggerReporter.report("NotCaptureHelper", "encryptData str : $str") return str } /** * 外部实现自定义解密数据 */ override fun decryptData(key: String, data: String): String { LoggerReporter.report("NotCaptureHelper", "decryptData data : $data") val str = data.decryptWithRC4(key) ?: "" LoggerReporter.report("NotCaptureHelper", "decryptData str : $str") return str } } ``` #### 5.7 防抓包功能自测 - 网络请求测试 - 正常请求,测试网络功能是否正常 - 抓包测试 - 配置fiddler,charles等工具 - 手机上设置代理 - 手机上安装证书 - 单向认证测试:进行网络请求,会提示SSLHandshakeException即ssl握手失败的错误提示,即表示app端的单向认证成功。 - 数据加解密:进行网络请求,看一下请求参数和响应body数据是否加密,如果看不到实际json实体则表示加密成功。 ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply from: rootProject.projectDir.absolutePath + "/yc.gradle" android { compileSdkVersion rootProject.ext.android["compileSdkVersion"] //buildToolsVersion rootProject.ext.android["buildToolsVersion"] defaultConfig { applicationId "com.ycbjie.ycreddotview" minSdkVersion rootProject.ext.android["minSdkVersion"] targetSdkVersion rootProject.ext.android["targetSdkVersion"] versionCode rootProject.ext.android["versionCode"] versionName rootProject.ext.android["versionName"] } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation rootProject.ext.dependencies["appcompat"] implementation rootProject.ext.dependencies["annotation"] implementation(rootProject.ext.dependencies["okhttp"]) implementation(rootProject.ext.dependencies["gson"]) implementation project(path: ':NotCaptureLib') //同上上报库 implementation 'com.github.yangchong211.YCCommonLib:EventUploadLib:1.4.3' //通用组件接口库 implementation 'com.github.yangchong211.YCCommonLib:AppCommonInter:1.4.3' //加解密库 //implementation project(path: ':AppEncryptLib') implementation 'com.github.yangchong211.YCCommonLib:AppEncryptLib:1.4.3' } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/yc/toolapp/App.java ================================================ package com.yc.toolapp; import android.app.Application; public class App extends Application { @Override public void onCreate() { super.onCreate(); } } ================================================ FILE: app/src/main/java/com/yc/toolapp/MainActivity.kt ================================================ package com.yc.toolapp import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.yc.appcommoninter.IMonitorToggle import com.yc.appencryptlib.AesEncryptUtils import com.yc.eventuploadlib.LoggerReporter import com.yc.notcapturelib.helper.CaptureConfig import com.yc.notcapturelib.helper.EncryptDecryptListener import com.yc.notcapturelib.helper.NotCaptureHelper import okhttp3.OkHttpClient import java.util.concurrent.TimeUnit class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } private fun init(){ NotCaptureHelper.getInstance().config = CaptureConfig.builder() //设置debug模式 .setDebug(true) //设置是否禁用代理 .setProxy(false) //设置是否进行数据加密和解密, .setEncrypt(true) //设置cer证书路径 .setCerPath("") //设置是否进行CA证书校验 .setCaVerify(false) //设置加密和解密key .setEncryptKey("key") .setMonitorToggle(object : IMonitorToggle { override fun isOpen(): Boolean { //todo 是否降级,如果降级,则不使用该功能。留给AB测试开关 return false } }) .build() NotCaptureHelper.getInstance().encryptDecryptListener = object : EncryptDecryptListener { /** * 外部实现自定义加密数据 */ override fun encryptData(key: String, data: String): String { LoggerReporter.report("NotCaptureHelper", "encryptData data : $data") val str = AesEncryptUtils.encrypt(data, key) LoggerReporter.report("NotCaptureHelper", "encryptData str : $str") return str } /** * 外部实现自定义解密数据 */ override fun decryptData(key: String, data: String): String { LoggerReporter.report("NotCaptureHelper", "decryptData data : $data") val str = AesEncryptUtils.decrypt(data, key) LoggerReporter.report("NotCaptureHelper", "decryptData str : $str") return str } } } private fun setOkHttp() { val builder = OkHttpClient.Builder() val okHttpClient = builder .connectTimeout(15, TimeUnit.SECONDS) .writeTimeout(20, TimeUnit.SECONDS) .readTimeout(20, TimeUnit.SECONDS) .build() NotCaptureHelper.getInstance().setOkHttp(this,builder) } } ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/component_banner.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment.xml ================================================ ================================================ FILE: app/src/main/res/layout/main_tab_layout.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #3F51B5 #303F9F #FF4081 #ffe935 ================================================ FILE: app/src/main/res/values/strings.xml ================================================ YCRedDotView ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: build.gradle ================================================ apply from: "yc.gradle" buildscript { apply from: 'yc.gradle' repositories { google() jcenter() maven { url 'https://maven.google.com/' name 'Google' } //添加阿里云镜像 maven { url "https://maven.aliyun.com/repository/public" } maven { url "https://maven.aliyun.com/repository/google" } maven { url "https://maven.aliyun.com/repository/jcenter" } maven { url 'https://jitpack.io' } mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:4.1.0' //classpath 'com.android.tools.build:gradle:3.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" //jitpack classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' //jetpack classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.0.0" //自定义插件 //classpath 'com.github.yangchong211.YCAppTool:ServiceLoaderPlugin:1.4.3' //classpath 'com.github.yangchong211.YCAppTool:BuildTimeCostPlugin:1.4.3' } } allprojects { repositories { google() jcenter() maven { url 'https://maven.google.com/' name 'Google' } maven { url 'https://jitpack.io' } //添加阿里云镜像 maven { url "https://maven.aliyun.com/repository/public" } maven { url "https://maven.aliyun.com/repository/google" } maven { url "https://maven.aliyun.com/repository/jcenter" } mavenCentral() } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true android.enableAapt2=false android.enableJetifier=true android.useAndroidX=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle ================================================ include ':app' include ':NotCaptureLib' ================================================ FILE: yc.gradle ================================================ ext { isApplication = false //false:作为Lib组件存在, true:作为application存在,这个不建议改 isJetpackApplication = true isOtherApplication = false //其他模块开关,false:作为Lib组件存在, true:作为application存在 isAnimApplication = false android = [ compileSdkVersion: 29, buildToolsVersion: "29.0.0", minSdkVersion : 17, targetSdkVersion : 29, versionCode : 22, versionName : "1.8.2" //必须是int或者float,否则影响线上升级 ] //AndroidX系列 appcompatVersion = '1.2.0' annotationVersion = '1.2.0' cardviewVersion = '1.0.0' mediaVersion = '1.0.1' swiperefreshlayoutVersion = '1.0.0' materialVersion = '1.0.0-rc01' coordinatorlayoutVersion = '1.0.0' constraintlayoutVersion = '1.1.3' recyclerviewVersion = '1.0.0' multidexVersion = '1.0.2' viewpagerVersion = '1.0.0' //AndroidX系列ktx activityKtx = '1.4.0' //kotlin系列 kotlin_version = '1.4.31' kotlinxVersion = '1.0.1' //jetpack系列 coreVersion = '1.0.0' databindingVersion = '3.2.1' archLifecycleVersion = '2.2.0' roomVersion = '2.0.0' workVersion = '2.0.0' //第三方库系列 retrofitSdkVersion = "2.4.0" glideSdkVersion = "4.9.0" okhttpVersion = "4.7.2" gsonVersion = "2.8.5" permissionsVersion = "1.0.1" dependencies = [ //AndroidX系列 appcompat : "androidx.appcompat:appcompat:${appcompatVersion}", annotation : "androidx.annotation:annotation:${annotationVersion}", constraintlayout : "androidx.constraintlayout:constraintlayout:${constraintlayoutVersion}", coordinatorlayout : "androidx.coordinatorlayout:coordinatorlayout:${coordinatorlayoutVersion}", cardview : "androidx.cardview:cardview:${cardviewVersion}", recyclerview : "androidx.recyclerview:recyclerview:${recyclerviewVersion}", media : "androidx.media:media:${mediaVersion}", material : "com.google.android.material:material:${materialVersion}", swiperefreshlayout : "androidx.swiperefreshlayout:swiperefreshlayout:${swiperefreshlayoutVersion}", "multidex" : "com.android.support:multidex:$multidexVersion", viewpager2 : "androidx.viewpager2:viewpager2:${viewpagerVersion}", //jetpack系列 core : "androidx.core:core:${coreVersion}", coreKtx : "androidx.core:core-ktx:${coreVersion}", roomRuntime : "androidx.room:room-runtime:${roomVersion}", roomCompiler : "androidx.room:room-compiler:${roomVersion}", databinding : "androidx.databinding:databinding-common:${databindingVersion}", lifecycle : "androidx.lifecycle:lifecycle-extensions:${archLifecycleVersion}", lifecycleCompiler : "androidx.lifecycle:lifecycle-common:${archLifecycleVersion}", lifecycleRuntime : "androidx.lifecycle:lifecycle-runtime:${archLifecycleVersion}", livedataCore : "androidx.lifecycle:lifecycle-livedata-core:${archLifecycleVersion}", workKtx : "androidx.work:work-runtime-ktx:${workVersion}", activityKtx : "androidx.activity:activity-ktx:${activityKtx}", navigationFragment : "androidx.navigation:navigation-fragment:${archLifecycleVersion}", navigationFragmentKtx : "androidx.navigation:navigation-fragment-ktx:${archLifecycleVersion}", navigationUiKtx : "androidx.navigation:navigation-ui-ktx:${archLifecycleVersion}", activityKtx : "androidx.activity:activity-ktx:${activityKtx}", datastore : "androidx.datastore:datastore-preferences:${coreVersion}", //将 Kotlin 协程与生命周期感知型组件一起使用 //https://developer.android.com/topic/libraries/architecture/coroutines lifecycleKtx : "androidx.lifecycle:lifecycle-runtime-ktx:${archLifecycleVersion}", livedataKtx : "androidx.lifecycle:lifecycle-livedata-ktx:${archLifecycleVersion}", viewmodelKtx : "androidx.lifecycle:lifecycle-viewmodel-ktx:${archLifecycleVersion}", //kotlin kotlinxCoroutinesCore : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxVersion", kotlinxCoroutinesAndroid : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxVersion", kotlinxJdk : "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version", //network retrofit : "com.squareup.retrofit2:retrofit:$retrofitSdkVersion", "retrofit-converter-gson" : "com.squareup.retrofit2:converter-gson:$retrofitSdkVersion", "retrofit-adapter-rxjava2": "com.squareup.retrofit2:adapter-rxjava2:$retrofitSdkVersion", okhttp : "com.squareup.okhttp3:okhttp:$okhttpVersion", gson : "com.google.code.gson:gson:$gsonVersion", glide : "com.github.bumptech.glide:glide:$glideSdkVersion", "glide-compiler" : "com.github.bumptech.glide:compiler:$glideSdkVersion", //widget库 //https://github.com/yangchong211/YCWidgetLib "ShadowConfig" : "com.github.yangchong211.YCWidgetLib:ShadowConfig:1.0.5", "RedDotView" : "com.github.yangchong211.YCWidgetLib:RedDotView:1.0.5", "ExpandPager" : "com.github.yangchong211.YCWidgetLib:ExpandPager:1.0.5", "ExpandLib" : "com.github.yangchong211.YCWidgetLib:ExpandLib:1.0.5", "CardViewLib" : "com.github.yangchong211.YCWidgetLib:CardViewLib:1.0.5", "RoundCorners" : "com.github.yangchong211.YCWidgetLib:RoundCorners:1.0.5", //线程池 //https://github.com/yangchong211/YCThreadPool "ThreadPoolLib" : "com.github.yangchong211.YCThreadPool:ThreadPoolLib:1.3.7", "ThreadTaskLib" : "com.github.yangchong211.YCThreadPool:ThreadTaskLib:1.3.7", "EasyExecutor" : "com.github.yangchong211.YCThreadPool:EasyExecutor:1.3.8", //效率优化 //https://github.com/yangchong211/YCEfficient "AppStartLib" : "com.github.yangchong211.YCEfficient:AppStartLib:1.3.1", "AppProcessLib" : "com.github.yangchong211.YCEfficient:AppProcessLib:1.3.1", "AutoCloserLib" : "com.github.yangchong211.YCEfficient:AutoCloserLib:1.3.1", //tools "easypermissions" : "pub.devrel:easypermissions:$permissionsVersion", ] }