├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── xposed_init │ ├── cpp │ ├── CMakeLists.txt │ ├── art.cpp │ ├── art.hpp │ ├── classloader.cpp │ ├── classloader.h │ ├── elf_parser │ │ ├── CMakeLists.txt │ │ ├── elf_parser.cc │ │ ├── include │ │ │ └── elf_parser.hpp │ │ └── xz-embedded │ │ │ ├── xz.h │ │ │ ├── xz_config.h │ │ │ ├── xz_crc32.c │ │ │ ├── xz_crc64.c │ │ │ ├── xz_dec_lzma2.c │ │ │ ├── xz_dec_stream.c │ │ │ ├── xz_lzma2.h │ │ │ ├── xz_private.h │ │ │ └── xz_stream.h │ ├── include │ │ └── logging.h │ ├── jvmti │ │ ├── jvmti.h │ │ └── stethox_jvmti.cpp │ ├── maps_scan │ │ ├── CMakeLists.txt │ │ ├── include │ │ │ └── maps_scan.hpp │ │ └── maps_scan.cpp │ ├── reflection.cpp │ ├── reflection.hpp │ ├── stethox.cpp │ ├── utils.cpp │ └── utils.h │ ├── java │ └── io │ │ └── github │ │ └── a13e300 │ │ └── tools │ │ ├── DexKitWrapper.java │ │ ├── DexUtils.java │ │ ├── InitMethods.java │ │ ├── Logger.java │ │ ├── NativeUtils.java │ │ ├── StethoxAppInterceptor.java │ │ ├── SuspendProvider.java │ │ ├── Utils.java │ │ ├── deobfuscation │ │ ├── MutableNameMap.java │ │ └── NameMap.java │ │ ├── network │ │ ├── httpurl │ │ │ └── HttpURLConnectionInterceptor.java │ │ └── okhttp3 │ │ │ ├── DefaultOkHttp3Helper.java │ │ │ ├── DeobfuscationUtils.kt │ │ │ ├── OkHttp3Helper.java │ │ │ └── StethoOkHttp3ProxyInterceptor.java │ │ ├── node │ │ └── Node.java │ │ └── objects │ │ ├── FindStackTraceFunction.java │ │ ├── GetStackTraceFunction.java │ │ ├── HookFunction.java │ │ ├── HookParam.java │ │ ├── JArrayFunction.java │ │ ├── OkHttpInterceptorObject.java │ │ ├── PrintStackTraceFunction.java │ │ ├── RunOnHandlerFunction.java │ │ └── UnhookFunction.java │ └── res │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ └── strings.xml ├── build.gradle.kts ├── copyright ├── debugstub ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ └── AndroidManifest.xml ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── hidden-api ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── a13e300 │ │ └── hidden_api │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── android │ │ ├── app │ │ ├── ActivityThread.java │ │ ├── IActivityManager.java │ │ └── IApplicationThread.java │ │ └── os │ │ ├── ServiceManager.java │ │ └── SystemProperties.java │ └── test │ └── java │ └── io │ └── github │ └── a13e300 │ └── hidden_api │ └── ExampleUnitTest.java └── settings.gradle.kts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | tags: [ v* ] 8 | pull_request: 9 | merge_group: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | submodules: "recursive" 20 | fetch-depth: 0 21 | path: StethoX 22 | 23 | - name: Write key 24 | if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/master' ) || github.ref_type == 'tag' }} 25 | run: | 26 | if [ ! -z "${{ secrets.KEY_STORE }}" ]; then 27 | echo androidStorePassword='${{ secrets.KEY_STORE_PASSWORD }}' >> gradle.properties 28 | echo androidKeyAlias='${{ secrets.ALIAS }}' >> gradle.properties 29 | echo androidKeyPassword='${{ secrets.KEY_PASSWORD }}' >> gradle.properties 30 | echo androidStoreFile='key.jks' >> gradle.properties 31 | echo ${{ secrets.KEY_STORE }} | base64 --decode > key.jks 32 | fi 33 | 34 | - name: Checkout rhino 35 | uses: actions/checkout@v4 36 | with: 37 | repository: 5ec1cff/rhino 38 | path: rhino 39 | 40 | - name: Checkout stetho 41 | uses: actions/checkout@v4 42 | with: 43 | repository: 5ec1cff/stetho 44 | path: stetho 45 | 46 | - name: Setup Java 47 | uses: actions/setup-java@v4 48 | with: 49 | distribution: temurin 50 | java-version: 17 51 | 52 | - name: Setup Gradle 53 | uses: gradle/actions/setup-gradle@v4 54 | 55 | - name: Build rhino 56 | working-directory: rhino 57 | run: | 58 | echo 'org.gradle.caching=true' >> ~/.gradle/gradle.properties 59 | echo 'org.gradle.parallel=true' >> ~/.gradle/gradle.properties 60 | echo 'org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC' >> ~/.gradle/gradle.properties 61 | echo 'android.native.buildOutput=verbose' >> ~/.gradle/gradle.properties 62 | ./gradlew publishToMavenLocal 63 | 64 | - name: Build stetho 65 | working-directory: stetho 66 | run: | 67 | echo 'org.gradle.caching=true' >> ~/.gradle/gradle.properties 68 | echo 'org.gradle.parallel=true' >> ~/.gradle/gradle.properties 69 | echo 'org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC' >> ~/.gradle/gradle.properties 70 | echo 'android.native.buildOutput=verbose' >> ~/.gradle/gradle.properties 71 | ./gradlew publishToMavenLocal 72 | 73 | - name: Build with Gradle 74 | working-directory: StethoX 75 | run: | 76 | ./gradlew app:assemble 77 | 78 | - name: Upload release 79 | uses: actions/upload-artifact@v4 80 | with: 81 | name: "StethoX-release-ci.apk" 82 | path: "./StethoX/app/build/outputs/apk/release/app-release.apk" 83 | 84 | - name: Upload debug 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: "StethoX-debug-ci.apk" 88 | path: "./StethoX/app/build/outputs/apk/debug/app-debug.apk" 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | /keystore.properties 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | StethoX 2 | ======= 3 | 4 | ## Description 5 | 6 | Original Project: https://gitlab.com/derSchabi/Stethox 7 | 8 | This is a [Xposed](http://repo.xposed.info/module/de.robv.android.xposed.installer) Module 9 | that enables [Stetho](https://github.com/5ec1cff/stetho) ([origin](https://facebook.github.io/stetho/)) for every application on your phone, 10 | allows you inspect any application with Chrome Remote Devtools. 11 | 12 | ## Usage 13 | 14 | 1. Install Xposed and this module. 15 | 2. Enable this module for apps you need. 16 | 3. Connect to your PC via USB Debugging (ADB). 17 | 4. Open chrome://inspect , select your app to inspect. 18 | 5. Have fun ! 19 | 20 | ## Features 21 | 22 | - View the View tree like an element (supports Activity, Dialog or other Window). 23 | - Screen mirroring and visual element selection (you need to select the required view in Elements to mirror). 24 | - Use Javascript in the console to access Java objects and hook Java methods (type `hook.help()` to view help). 25 | - Support for suspending process at startup 26 | 27 | ## Suspend process 28 | 29 | StethoX supports suspending the App process when it starts and displays the "Waiting for Debugger" dialog. When the user connects with Devtools, he can enter `cont` at the console to keep the process running. 30 | 31 | Before the process continues to run, the process can also be accessed through Devtools, such as executing Javascript and other operations. 32 | 33 | **NOTE: You may need to turn off StethoX's battery optimization for this feature to work properly. ** 34 | 35 | You can configure suspend using the `content` command in the adb shell: 36 | 37 | ```shell 38 | # Suspend once (take effect at next startup) 39 | content call --uri content://io.github.a13e300.tools.stethox.suspend --method suspend --arg ${processName} 40 | #Hang all the time 41 | content call --uri content://io.github.a13e300.tools.stethox.suspend --method suspend_forever --arg ${processName} 42 | # Clear configuration (clear all without specifying arg) 43 | content call --uri content://io.github.a13e300.tools.stethox.suspend --method clear [--arg ${processName}] 44 | # List configuration 45 | content call --uri content://io.github.a13e300.tools.stethox.suspend --method list 46 | ``` 47 | 48 | You can also configure it by prop or file: 49 | 50 | ``` 51 | # Separate with newline 52 | setprop debug.stethox.suspend [,...] 53 | # clear 54 | setprop debug.stethox.suspend '' 55 | 56 | # Separate with newline 57 | echo '' > /data/local/tmp/stethox_suspend 58 | # clear 59 | rm /data/local/tmp/stethox_suspend 60 | ``` 61 | 62 | ## ATTENTION 63 | 64 | Never leave this Module enabled or installed on day to day use. 65 | __THIS IS A SECURITY RISK__. Only enable this for Development. 66 | 67 | --- 68 | 69 | ## 描述 70 | 71 | 原项目: https://gitlab.com/derSchabi/Stethox 72 | 73 | 这是一个能够为任意应用启用 [Stetho](https://github.com/5ec1cff/stetho) ([原 facebook 项目](https://facebook.github.io/stetho/)) 74 | 的 [Xposed](http://repo.xposed.info/module/de.robv.android.xposed.installer) 模块,你可以借助它通过 Chrome 开发者工具检查任意 App 。 75 | 76 | ## 用法 77 | 78 | 1. 安装 Xposed 及该模块。 79 | 2. 为你需要的应用启用模块。 80 | 3. 通过 USB 调试 (ADB) 连接到电脑。 81 | 4. 打开 chrome://inspect ,选择要检查的 App 。 82 | 5. 祝你玩得开心! 83 | 84 | ## 功能 85 | 86 | - 像审查元素一样查看 View 树(支持 Activity、Dialog 或其他 Window)。 87 | - 画面镜像和可视的元素选择(需要在 Elements 中选中需要的 view 以镜像)。 88 | - 在控制台使用 Javascript 访问 Java 对象和 hook Java 方法(输入 `hook.help()` 查看帮助)。 89 | - 支持启动时挂起进程 90 | 91 | ## 挂起进程 92 | 93 | StethoX 支持在 App 进程启动时挂起进程,并显示「等待调试器」对话框。当用户使用 Devtools 连接后,可以在控制台输入 `cont` 让进程继续运行。 94 | 95 | 在进程继续运行之前,也可以通过 Devtools 访问进程,如执行 Javascript 等操作。 96 | 97 | **注意:你可能需要关闭 StethoX 的电池优化确保该功能正常工作。** 98 | 99 | 你可以在 adb shell 使用 `content` 命令配置挂起: 100 | 101 | ```shell 102 | # 挂起一次(下次启动时生效) 103 | content call --uri content://io.github.a13e300.tools.stethox.suspend --method suspend --arg ${processName} 104 | # 一直挂起 105 | content call --uri content://io.github.a13e300.tools.stethox.suspend --method suspend_forever --arg ${processName} 106 | # 清除配置(不指定 arg 则清除所有) 107 | content call --uri content://io.github.a13e300.tools.stethox.suspend --method clear [--arg ${processName}] 108 | # 列出配置 109 | content call --uri content://io.github.a13e300.tools.stethox.suspend --method list 110 | ``` 111 | 112 | 你也可以使用 prop 或者文件配置: 113 | 114 | ``` 115 | # Separate with newline 116 | setprop debug.stethox.suspend [,...] 117 | # clear 118 | setprop debug.stethox.suspend '' 119 | 120 | # Separate with newline 121 | echo '' > /data/local/tmp/stethox_suspend 122 | # clear 123 | rm /data/local/tmp/stethox_suspend 124 | ``` 125 | 126 | ## 注意 127 | 128 | 日常使用请勿启用该模块,这样有安全风险,请只在开发时启用。 129 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.cxx -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import java.io.BufferedReader 4 | import java.io.FileInputStream 5 | import java.io.FileReader 6 | import java.io.FileWriter 7 | import java.util.Properties 8 | 9 | plugins { 10 | alias(libs.plugins.agp.app) 11 | alias(libs.plugins.kotlin) 12 | } 13 | 14 | val keystorePropertiesFile = rootProject.file("keystore.properties") 15 | val keystoreProperties = if (keystorePropertiesFile.exists() && keystorePropertiesFile.isFile) { 16 | Properties().apply { 17 | load(FileInputStream(keystorePropertiesFile)) 18 | } 19 | } else null 20 | 21 | android { 22 | namespace = "io.github.a13e300.tools" 23 | compileSdk = 35 24 | signingConfigs { 25 | if (keystoreProperties != null) { 26 | create("release") { 27 | keyAlias = keystoreProperties["keyAlias"] as String 28 | keyPassword = keystoreProperties["keyPassword"] as String 29 | storeFile = file(keystoreProperties["storeFile"] as String) 30 | storePassword = keystoreProperties["storePassword"] as String 31 | } 32 | } 33 | } 34 | buildFeatures { 35 | buildConfig = true 36 | prefab = true 37 | } 38 | defaultConfig { 39 | applicationId = "io.github.a13e300.tools.stethox" 40 | minSdk = 24 41 | targetSdk = 35 42 | versionCode = 2 43 | versionName = "1.0.2" 44 | externalNativeBuild { 45 | cmake { 46 | cppFlags("-std=c++20", "-fno-rtti", "-fno-exceptions") 47 | arguments += "-DANDROID_STL=none" 48 | } 49 | } 50 | } 51 | buildTypes { 52 | release { 53 | isMinifyEnabled = true 54 | isShrinkResources = true 55 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") 56 | signingConfig = signingConfigs.findByName("release") ?: signingConfigs["debug"] 57 | } 58 | } 59 | compileOptions { 60 | sourceCompatibility = JavaVersion.VERSION_11 61 | targetCompatibility = JavaVersion.VERSION_11 62 | } 63 | externalNativeBuild { 64 | cmake { 65 | path = file("src/main/cpp/CMakeLists.txt") 66 | version = "3.22.1" 67 | } 68 | } 69 | kotlinOptions { 70 | jvmTarget = "11" 71 | } 72 | } 73 | 74 | task("generateDefaultOkHttp3Helper") { 75 | group = "StethoX" 76 | doFirst { 77 | runCatching { 78 | val okioTypes = listOf("BufferedSink", "Sink", "BufferedSource", "Okio", "Source", "BufferedSource") 79 | fun String.toOkhttpClass() = 80 | if (this.contains("_")) this.replace("_", ".") 81 | else if (this in okioTypes) "okio.$this" 82 | else "okhttp3.$this" 83 | val f = 84 | project.file("src/main/java/io/github/a13e300/tools/network/okhttp3/OkHttp3Helper.java") 85 | FileWriter(project.file("src/main/java/io/github/a13e300/tools/network/okhttp3/DefaultOkHttp3Helper.java")).use { writer -> 86 | BufferedReader(FileReader(f)).use { reader -> 87 | val cstrContent = StringBuilder() 88 | for (l in reader.lines()) { 89 | if (l.startsWith("package") || l.startsWith("import")) { 90 | writer.write(l) 91 | } else if (l.startsWith("public interface")) { 92 | writer.write("import java.lang.reflect.*;\n") 93 | writer.write("import io.github.a13e300.tools.deobfuscation.NameMap;\n") 94 | writer.write("public class DefaultOkHttp3Helper implements OkHttp3Helper {") 95 | } else { 96 | val trimmed = l.trim() 97 | if (trimmed.startsWith("}") || trimmed.startsWith("//")) continue 98 | val r = Regex("\\s*(.*)\\((.*)\\)(.*);").matchEntire(l) ?: continue 99 | val typeAndName = r.groupValues[1].split(" ") 100 | val arguments = r.groupValues[2] 101 | val exceptions = r.groupValues[3].trim().let { 102 | if (!it.startsWith("throws")) emptyList() 103 | else it.removePrefix("throws").trim().split(Regex("\\s+,\\s+")) 104 | } 105 | var isStatic: Boolean = false 106 | var returnType: String? = null 107 | var className: String? = null 108 | var methodName: String? = null 109 | var functionName: String? = null 110 | var isMethodGetter = false 111 | var isClassGetter = false 112 | var isFieldGetter = false 113 | for (token in typeAndName) { 114 | if (token == "/*static*/") { 115 | isStatic = true 116 | continue 117 | } 118 | if (returnType == null) { 119 | returnType = token 120 | if (token == "Method") isMethodGetter = true 121 | if (token == "Class") isClassGetter = true 122 | if (token == "Field") isFieldGetter = true 123 | } else { 124 | if (isClassGetter) { 125 | className = token.toOkhttpClass() 126 | functionName = token 127 | } else Regex("(.*)_\\d*(.*?)").matchEntire(token)?.let { 128 | className = it.groupValues[1].toOkhttpClass() 129 | methodName = it.groupValues[2] 130 | functionName = token 131 | } 132 | } 133 | } 134 | val argTypesAndNames = arguments.let { 135 | if (isMethodGetter) it.removePrefix("/*").removeSuffix("*/") 136 | else it 137 | }.split(Regex(",\\s*")).mapNotNull { token -> 138 | if (token.isEmpty()) return@mapNotNull null 139 | token.split(" ").let { 140 | it[it.lastIndex - 1].let { t -> 141 | Regex("/\\*(.*)\\*/").matchEntire(t)?.groupValues?.get(1) 142 | ?.let { t_ -> 143 | t_.toOkhttpClass() to false 144 | } ?: (t to true) 145 | } to it.last() 146 | } 147 | } 148 | if (isClassGetter) { 149 | writer.write("private Class class_$functionName;\n") 150 | writer.write("@Override public") 151 | writer.write(l.removeSuffix(";") + " {\nreturn class_$functionName;\n}\n") 152 | cstrContent.append("class_$functionName = classLoader.loadClass(nameMap.getClass(\"$className\"));\n") 153 | continue 154 | } else if (isFieldGetter) { 155 | writer.write("private Field field_$functionName;\n") 156 | writer.write("@Override public") 157 | writer.write(l.removeSuffix(";") + " {\nreturn field_$functionName;\n}\n") 158 | cstrContent.append("field_$functionName = classLoader.loadClass(nameMap.getClass(\"$className\")).getDeclaredField(nameMap.getField(\"$className\", \"$methodName\"));\n") 159 | cstrContent.append("field_$functionName.setAccessible(true);\n") 160 | continue 161 | } 162 | writer.write("private Method method_$functionName;\n") 163 | writer.write("@Override public ") 164 | writer.write(l.removeSuffix(";") + " {\n") 165 | if (isMethodGetter) { 166 | writer.write("return method_$functionName;\n}") 167 | } else { 168 | writer.write("try {\n${if (returnType == "void") "" else "return ($returnType)"}method_$functionName.invoke(") 169 | val argList = mutableListOf() 170 | if (isStatic) argList.add("null") 171 | for ((_, name) in argTypesAndNames) { 172 | argList.add(name) 173 | } 174 | writer.write(argList.joinToString(", ")) 175 | writer.write(");\n}") 176 | if (exceptions.isNotEmpty()) { 177 | writer.write("catch (InvocationTargetException ex) {\n") 178 | writer.write(exceptions.map { ex -> 179 | "if (ex.getCause() instanceof $ex) { throw ($ex) ex.getCause(); }\n" 180 | }.joinToString(" else ")) 181 | writer.write(" else throw new RuntimeException(ex);\n}") 182 | } 183 | writer.write("catch (Throwable t) { throw new RuntimeException(t); }\n}") 184 | } 185 | cstrContent.append("method_$functionName = classLoader.loadClass(nameMap.getClass(\"$className\"))") 186 | .append(".getDeclaredMethod(nameMap.getMethod(\"$className\", \"$methodName\"") 187 | .apply { 188 | var first = true 189 | for ((t, _) in argTypesAndNames) { 190 | if (!isStatic && first) { 191 | first = false 192 | continue 193 | } 194 | val (type, _) = t 195 | append(", \"") 196 | if (type == "String") append("java.lang.String") 197 | else append(type) 198 | append("\"") 199 | } 200 | append(")") 201 | } 202 | var first = true 203 | for ((t, _) in argTypesAndNames) { 204 | val (type, known) = t 205 | if (!isStatic && first) { 206 | first = false 207 | continue 208 | } 209 | cstrContent.append(",") 210 | if (known) 211 | cstrContent.append("$type.class") 212 | else 213 | cstrContent.append("classLoader.loadClass(\"$type\")") 214 | } 215 | cstrContent.append(");\nmethod_$functionName.setAccessible(true);\n") 216 | } 217 | writer.write("\n") 218 | } 219 | writer.write("") 220 | writer.write("public DefaultOkHttp3Helper(ClassLoader classLoader, NameMap nameMap) {\n") 221 | writer.write("try {\n") 222 | writer.write(cstrContent.toString()) 223 | writer.write("} catch (Throwable t) {\nthrow new RuntimeException(t);\n}\n}\n}") 224 | } 225 | } 226 | }.onFailure { it.printStackTrace() } 227 | } 228 | } 229 | 230 | dependencies { 231 | implementation(libs.annotation) 232 | compileOnly(libs.api) 233 | compileOnly(project(":hidden-api")) 234 | implementation(libs.stetho) 235 | implementation(libs.stetho.js.rhino) 236 | implementation(libs.stetho.urlconnection) 237 | implementation(libs.rhino) 238 | implementation(libs.dexmaker) 239 | implementation(libs.dexkit) 240 | implementation(libs.cxx) 241 | } 242 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -keep class io.github.a13e300.tools.StethoxAppInterceptor 23 | -keep class io.github.a13e300.tools.NativeUtils { *; } 24 | # stetho 25 | -keep class com.facebook.stetho.** { *; } 26 | 27 | # rhino (javascript) 28 | -dontwarn org.mozilla.javascript.** 29 | -dontwarn org.mozilla.classfile.** 30 | -keep class org.mozilla.classfile.** { *; } 31 | -keep class org.mozilla.javascript.* { *; } 32 | -keep class org.mozilla.javascript.annotations.** { *; } 33 | -keep class org.mozilla.javascript.ast.** { *; } 34 | -keep class org.mozilla.javascript.commonjs.module.** { *; } 35 | -keep class org.mozilla.javascript.commonjs.module.provider.** { *; } 36 | -keep class org.mozilla.javascript.debug.** { *; } 37 | -keep class org.mozilla.javascript.jdk13.** { *; } 38 | -keep class org.mozilla.javascript.jdk15.** { *; } 39 | -keep class org.mozilla.javascript.jdk18.** { *; } 40 | -keep class org.mozilla.javascript.json.** { *; } 41 | -keep class org.mozilla.javascript.optimizer.** { *; } 42 | -keep class org.mozilla.javascript.regexp.** { *; } 43 | -keep class org.mozilla.javascript.serialize.** { *; } 44 | -keep class org.mozilla.javascript.typedarrays.** { *; } 45 | -keep class org.mozilla.javascript.v8dtoa.** { *; } 46 | -keep class org.mozilla.javascript.xml.** { *; } 47 | -keep class org.mozilla.javascript.xmlimpl.** { *; } 48 | 49 | -keep class io.github.a13e300.tools.objects.** { *; } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 19 | 20 | 23 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | io.github.a13e300.tools.StethoxAppInterceptor -------------------------------------------------------------------------------- /app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.22.1) 2 | project("stethox") 3 | 4 | include_directories(include) 5 | 6 | find_package(cxx REQUIRED CONFIG) 7 | link_libraries(cxx::cxx) 8 | 9 | add_library(${CMAKE_PROJECT_NAME} SHARED stethox.cpp classloader.cpp utils.cpp jvmti/stethox_jvmti.cpp art.cpp reflection.cpp) 10 | 11 | target_link_libraries(${CMAKE_PROJECT_NAME} log elf_parser maps_scan) 12 | add_subdirectory(elf_parser) 13 | add_subdirectory(maps_scan) 14 | -------------------------------------------------------------------------------- /app/src/main/cpp/art.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "art.hpp" 4 | #include "logging.h" 5 | 6 | #include "utils.h" 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "art.hpp" 14 | 15 | namespace art { 16 | void (*ClassLinker::visit_class_loader_)(void *, ClassLoaderVisitor *) = nullptr; 17 | 18 | bool ClassLinker::Init(elf_parser::Elf &art) { 19 | auto VisitClassLoaders = reinterpret_cast( 20 | art.getSymbAddress("_ZNK3art11ClassLinker17VisitClassLoadersEPNS_18ClassLoaderVisitorE") 21 | ); 22 | LOGD("VisitClassLoaders: %p", VisitClassLoaders); 23 | if (VisitClassLoaders == nullptr) return false; 24 | visit_class_loader_ = VisitClassLoaders; 25 | return true; 26 | } 27 | 28 | void ClassLinker::VisitClassLoaders(ClassLoaderVisitor *clv) { 29 | if (visit_class_loader_ != nullptr) { 30 | visit_class_loader_(this, clv); 31 | } 32 | } 33 | 34 | // https://github.com/LSPosed/LSPlant/blob/08b65e436f766066f7aff1e35309aee0c656e3ba/lsplant/src/main/jni/art/runtime/runtime.cxx#L65 35 | 36 | // prevent from deopt 37 | // https://cs.android.com/android/platform/superproject/main/+/main:art/openjdkjvmti/deopt_manager.cc;l=145;drc=6f07c0cc2811994b8b9c350a46534138be423479 38 | // https://cs.android.com/android/platform/superproject/+/android-13.0.0_r3:art/openjdkjvmti/deopt_manager.cc;l=152;drc=c618f7c01e83409e57f3e74bdc1fd4bdc6a828b5 39 | enum class RuntimeDebugState { 40 | // This doesn't support any debug features / method tracing. This is the expected state 41 | // usually. 42 | kNonJavaDebuggable, 43 | // This supports method tracing and a restricted set of debug features (for ex: redefinition 44 | // isn't supported). We transition to this state when method tracing has started or when the 45 | // debugger was attached and transition back to NonDebuggable once the tracing has stopped / 46 | // the debugger agent has detached.. 47 | kJavaDebuggable, 48 | // The runtime was started as a debuggable runtime. This allows us to support the extended 49 | // set 50 | // of debug features (for ex: redefinition). We never transition out of this state. 51 | kJavaDebuggableAtInit 52 | }; 53 | 54 | inline static unsigned int class_linker_offset_; 55 | inline static size_t java_debuggable_offset = -1; 56 | inline static size_t debug_state_offset = -1; 57 | 58 | Runtime *Runtime::instance_ = nullptr; 59 | bool Runtime::Init(JNIEnv *env, elf_parser::Elf &art) { 60 | // https://github.com/frida/frida-java-bridge/blob/58030ace413a9104b8bf67f7396b22bf5d889e43/lib/android.js#L586 61 | #ifdef __LP64__ 62 | constexpr auto start_offset = 48; 63 | #else 64 | constexpr auto start_offset = 50; 65 | #endif 66 | constexpr auto end_offset = start_offset + 100; 67 | constexpr auto std_string_size = 3; 68 | auto sdk_int = GetAndroidApiLevel(); 69 | 70 | bool success = true; 71 | 72 | auto instance = *(Runtime**) art.getSymbAddress("_ZN3art7Runtime9instance_E"); 73 | if (instance == nullptr) { 74 | LOGE("not found: art::Runtime::instance_"); 75 | return false; 76 | } 77 | LOGD("instance %p", instance); 78 | instance_ = instance; 79 | 80 | // get classLinker 81 | 82 | JavaVM *vm; 83 | env->GetJavaVM(&vm); 84 | if (vm) { 85 | auto class_linker_offset = 0u; 86 | for (auto offset = start_offset; offset != end_offset; offset++) { 87 | if (*((void **) instance + offset) == vm) { 88 | if (sdk_int >= __ANDROID_API_T__) { 89 | class_linker_offset = offset - 4; 90 | } else if (sdk_int >= __ANDROID_API_R__) { 91 | auto try_class_linker = [&](int class_linker_offset) { 92 | auto intern_table_offset = class_linker_offset - 1; 93 | constexpr auto start_offset = 25; 94 | constexpr auto end_offset = start_offset + 100; 95 | auto class_linker = *(void **) ((void **) instance + 96 | class_linker_offset); 97 | auto intern_table = *(void **) ((void **) instance + 98 | intern_table_offset); 99 | if (!is_pointer_valid(class_linker)) return false; 100 | for (auto i = start_offset; i != end_offset; i++) { 101 | auto p = (void **) class_linker + i; 102 | if (is_pointer_valid(p) && (*p) == intern_table) return true; 103 | } 104 | return false; 105 | }; 106 | if (try_class_linker(offset - 3)) { 107 | class_linker_offset = offset - 3; 108 | } else if (try_class_linker(offset - 4)) { 109 | class_linker_offset = offset - 4; 110 | } else { 111 | success = false; 112 | break; 113 | } 114 | } else if (sdk_int >= __ANDROID_API_Q__) { 115 | class_linker_offset = offset - 2; 116 | } else if (sdk_int >= __ANDROID_API_O_MR1__) { 117 | class_linker_offset = offset - std_string_size - 3; 118 | } else { 119 | class_linker_offset = offset - std_string_size - 2; 120 | } 121 | } 122 | } 123 | if (class_linker_offset == 0u) { 124 | success = false; 125 | } 126 | class_linker_offset_ = class_linker_offset; 127 | LOGD("class_linker_offset_ %u", class_linker_offset); 128 | success &= ClassLinker::Init(art); 129 | } else { 130 | success = false; 131 | } 132 | 133 | // debuggable 134 | 135 | if (auto fn = art.getSymbAddress( 136 | "_ZN3art7Runtime20SetRuntimeDebugStateENS0_17RuntimeDebugStateE"); fn) { 137 | static constexpr size_t kLargeEnoughSizeForRuntime = 4096; 138 | std::array code{}; 139 | static_assert(static_cast(RuntimeDebugState::kJavaDebuggable) != 0); 140 | static_assert(static_cast(RuntimeDebugState::kJavaDebuggableAtInit) != 0); 141 | code.fill(uint8_t{0}); 142 | auto *const fake_runtime = reinterpret_cast(code.data()); 143 | reinterpret_cast(fn)(fake_runtime, 144 | RuntimeDebugState::kJavaDebuggable); 145 | for (size_t i = 0; i < kLargeEnoughSizeForRuntime; ++i) { 146 | if (*reinterpret_cast( 147 | reinterpret_cast(fake_runtime) + i) == 148 | RuntimeDebugState::kJavaDebuggable) { 149 | LOGD("found debug_state at offset %zu", i); 150 | debug_state_offset = i; 151 | break; 152 | } 153 | } 154 | } 155 | 156 | if (auto fn = art.getSymbAddress("_ZN3art7Runtime17SetJavaDebuggableEb"); fn) { 157 | static constexpr size_t kLargeEnoughSizeForRuntime = 4096; 158 | std::array code{}; 159 | static_assert(static_cast(RuntimeDebugState::kJavaDebuggable) != 0); 160 | static_assert(static_cast(RuntimeDebugState::kJavaDebuggableAtInit) != 0); 161 | code.fill(uint8_t{0}); 162 | auto *const fake_runtime = reinterpret_cast(code.data()); 163 | reinterpret_cast(fn)(fake_runtime, true); 164 | for (size_t i = 0; i < kLargeEnoughSizeForRuntime; ++i) { 165 | if (*reinterpret_cast( 166 | reinterpret_cast(fake_runtime) + i)) { 167 | LOGD("found java_debuggable at offset %zu", i); 168 | java_debuggable_offset = i; 169 | break; 170 | } 171 | } 172 | } 173 | 174 | if (java_debuggable_offset == -1 && debug_state_offset == -1) { 175 | LOGE("not java_debuggable_offset nor debug_state_offset found"); 176 | success = false; 177 | } 178 | 179 | return success; 180 | } 181 | 182 | ClassLinker* Runtime::getClassLinker() { 183 | return *((ClassLinker**)this + class_linker_offset_); 184 | } 185 | 186 | int SetDebuggableValue(int value) { 187 | if (debug_state_offset != -1) { 188 | auto addr = reinterpret_cast(reinterpret_cast(Runtime::Current()) + debug_state_offset); 189 | int orig = static_cast(*addr); 190 | *addr = static_cast(value); 191 | return orig; 192 | } 193 | 194 | if (java_debuggable_offset != -1) { 195 | auto addr = reinterpret_cast(reinterpret_cast(Runtime::Current()) + java_debuggable_offset); 196 | int orig = *addr; 197 | *addr = true; 198 | return orig; 199 | } 200 | 201 | return 0; 202 | } 203 | 204 | int SetDebuggable(bool allow) { 205 | if (debug_state_offset != -1) { 206 | return SetDebuggableValue(static_cast(allow ? RuntimeDebugState::kJavaDebuggable : RuntimeDebugState::kNonJavaDebuggable)); 207 | } else if (java_debuggable_offset != -1) { 208 | return SetDebuggableValue(allow ? 1 : 0); 209 | } 210 | return 0; 211 | } 212 | 213 | void (*symSetJdwpAllowed)(bool) = nullptr; 214 | 215 | bool Init(JNIEnv *env, elf_parser::Elf &art) { 216 | bool success = true; 217 | 218 | symSetJdwpAllowed = reinterpret_cast(art.getSymbAddress("_ZN3art3Dbg14SetJdwpAllowedEb")); 219 | if (symSetJdwpAllowed) { 220 | LOGE("not found: art::Dbg::SetJdwpAllowed"); 221 | success = false; 222 | } 223 | 224 | success &= Runtime::Init(env, art); 225 | 226 | return success; 227 | } 228 | 229 | void SetJdwpAllowed() { 230 | symSetJdwpAllowed(true); 231 | } 232 | } 233 | 234 | extern "C" 235 | JNIEXPORT jint JNICALL 236 | Java_io_github_a13e300_tools_NativeUtils_nativeSetJavaDebug(JNIEnv *, jclass, 237 | jboolean allow) { 238 | return art::SetDebuggable(allow == JNI_TRUE); 239 | } 240 | 241 | extern "C" 242 | JNIEXPORT void JNICALL 243 | Java_io_github_a13e300_tools_NativeUtils_nativeRestoreJavaDebug(JNIEnv *, jclass, jint orig) { 244 | art::SetDebuggableValue(orig); 245 | } 246 | 247 | extern "C" 248 | JNIEXPORT void JNICALL 249 | Java_io_github_a13e300_tools_NativeUtils_nativeAllowDebugging(JNIEnv *env, jclass clazz) { 250 | art::SetJdwpAllowed(); 251 | LOGD("allow debug"); 252 | } 253 | -------------------------------------------------------------------------------- /app/src/main/cpp/art.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "elf_parser.hpp" 3 | 4 | #ifndef NDEBUG 5 | #define ALWAYS_INLINE 6 | #else 7 | #define ALWAYS_INLINE __attribute__ ((always_inline)) 8 | #endif 9 | 10 | #define DCHECK(...) 11 | 12 | #define OVERRIDE override 13 | 14 | #define ATTRIBUTE_UNUSED __attribute__((__unused__)) 15 | 16 | #define REQUIRES_SHARED(...) 17 | 18 | #define MANAGED PACKED(4) 19 | #define PACKED(x) __attribute__ ((__aligned__(x), __packed__)) 20 | 21 | namespace art { 22 | namespace mirror { 23 | 24 | class Object { 25 | 26 | }; 27 | 28 | class MANAGED ClassLoader : public Object { 29 | }; 30 | 31 | template 32 | class ObjPtr { 33 | 34 | public: 35 | MirrorType *Ptr() const { 36 | return nullptr; 37 | } 38 | 39 | }; 40 | 41 | template 42 | class PtrCompression { 43 | public: 44 | // Compress reference to its bit representation. 45 | static uint32_t Compress(MirrorType *mirror_ptr) { 46 | uintptr_t as_bits = reinterpret_cast(mirror_ptr); 47 | return static_cast(kPoisonReferences ? -as_bits : as_bits); 48 | } 49 | 50 | // Uncompress an encoded reference from its bit representation. 51 | static MirrorType *Decompress(uint32_t ref) { 52 | uintptr_t as_bits = kPoisonReferences ? -ref : ref; 53 | return reinterpret_cast(as_bits); 54 | } 55 | 56 | // Convert an ObjPtr to a compressed reference. 57 | static uint32_t Compress(ObjPtr ptr) REQUIRES_SHARED(Locks::mutator_lock_) { 58 | return Compress(ptr.Ptr()); 59 | } 60 | }; 61 | 62 | 63 | // Value type representing a reference to a mirror::Object of type MirrorType. 64 | template 65 | class MANAGED ObjectReference { 66 | private: 67 | using Compression = PtrCompression; 68 | 69 | public: 70 | MirrorType *AsMirrorPtr() const { 71 | return Compression::Decompress(reference_); 72 | } 73 | 74 | void Assign(MirrorType *other) { 75 | reference_ = Compression::Compress(other); 76 | } 77 | 78 | void Assign(ObjPtr ptr) REQUIRES_SHARED(Locks::mutator_lock_); 79 | 80 | void Clear() { 81 | reference_ = 0; 82 | DCHECK(IsNull()); 83 | } 84 | 85 | bool IsNull() const { 86 | return reference_ == 0; 87 | } 88 | 89 | uint32_t AsVRegValue() const { 90 | return reference_; 91 | } 92 | 93 | static ObjectReference 94 | FromMirrorPtr(MirrorType *mirror_ptr) 95 | REQUIRES_SHARED(Locks::mutator_lock_) { 96 | return ObjectReference(mirror_ptr); 97 | } 98 | 99 | protected: 100 | explicit ObjectReference(MirrorType *mirror_ptr) REQUIRES_SHARED(Locks::mutator_lock_) 101 | : reference_(Compression::Compress(mirror_ptr)) { 102 | } 103 | 104 | // The encoded reference to a mirror::Object. 105 | uint32_t reference_; 106 | }; 107 | 108 | // Standard compressed reference used in the runtime. Used for StackReference and GC roots. 109 | template 110 | class MANAGED CompressedReference : public mirror::ObjectReference { 111 | public: 112 | CompressedReference() REQUIRES_SHARED(Locks::mutator_lock_) 113 | : mirror::ObjectReference(nullptr) {} 114 | }; 115 | } 116 | 117 | class ClassLoaderVisitor { 118 | public: 119 | virtual ~ClassLoaderVisitor() {} 120 | 121 | virtual void Visit(mirror::ClassLoader *class_loader) 122 | REQUIRES_SHARED(Locks::classlinker_classes_lock_, Locks::mutator_lock_) = 0; 123 | }; 124 | 125 | class RootInfo { 126 | }; 127 | 128 | class RootVisitor { 129 | public: 130 | virtual ~RootVisitor() {} 131 | 132 | // Single root version, not overridable. 133 | ALWAYS_INLINE void VisitRoot(mirror::Object **root, const RootInfo &info) 134 | REQUIRES_SHARED(Locks::mutator_lock_) { 135 | VisitRoots(&root, 1, info); 136 | } 137 | 138 | // Single root version, not overridable. 139 | ALWAYS_INLINE void VisitRootIfNonNull(mirror::Object **root, const RootInfo &info) 140 | REQUIRES_SHARED(Locks::mutator_lock_) { 141 | if (*root != nullptr) { 142 | VisitRoot(root, info); 143 | } 144 | } 145 | 146 | virtual void VisitRoots(mirror::Object ***roots, size_t count, const RootInfo &info) 147 | REQUIRES_SHARED(Locks::mutator_lock_) = 0; 148 | 149 | virtual void 150 | VisitRoots(mirror::CompressedReference **roots, size_t count, 151 | const RootInfo &info) 152 | REQUIRES_SHARED(Locks::mutator_lock_) = 0; 153 | }; 154 | 155 | // Only visits roots one at a time, doesn't handle updating roots. Used when performance isn't 156 | // critical. 157 | class SingleRootVisitor : public RootVisitor { 158 | private: 159 | void VisitRoots(mirror::Object ***roots, size_t count, const RootInfo &info) OVERRIDE 160 | REQUIRES_SHARED(Locks::mutator_lock_) { 161 | for (size_t i = 0; i < count; ++i) { 162 | VisitRoot(*roots[i], info); 163 | } 164 | } 165 | 166 | void VisitRoots(mirror::CompressedReference **roots, size_t count, 167 | const RootInfo &info) OVERRIDE 168 | REQUIRES_SHARED(Locks::mutator_lock_) { 169 | for (size_t i = 0; i < count; ++i) { 170 | VisitRoot(roots[i]->AsMirrorPtr(), info); 171 | } 172 | } 173 | 174 | virtual void VisitRoot(mirror::Object *root, const RootInfo &info) = 0; 175 | }; 176 | 177 | class IsMarkedVisitor { 178 | public: 179 | virtual ~IsMarkedVisitor() {} 180 | 181 | // Return null if an object is not marked, otherwise returns the new address of that object. 182 | // May return the same address as the input if the object did not move. 183 | virtual mirror::Object *IsMarked(mirror::Object *obj) = 0; 184 | }; 185 | 186 | class ClassLinker { 187 | private: 188 | static void (*visit_class_loader_)(void *, ClassLoaderVisitor *); 189 | 190 | public: 191 | static bool Init(elf_parser::Elf &art); 192 | 193 | void VisitClassLoaders(ClassLoaderVisitor *clv); 194 | }; 195 | 196 | class Runtime { 197 | private: 198 | static Runtime *instance_; 199 | public: 200 | static bool Init(JNIEnv *env, elf_parser::Elf &art); 201 | 202 | ClassLinker* getClassLinker(); 203 | inline static Runtime *Current() { return instance_; } 204 | }; 205 | 206 | bool Init(JNIEnv *env, elf_parser::Elf &art); 207 | } 208 | -------------------------------------------------------------------------------- /app/src/main/cpp/classloader.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "classloader.h" 5 | #include "logging.h" 6 | #include "art.hpp" 7 | #include 8 | #include 9 | #include 10 | 11 | struct JNIEnvExt { 12 | private: 13 | static inline jobject (*symNewLocalRef)(JNIEnvExt *, art::mirror::Object *) = nullptr; 14 | static inline void (*symDeleteLocalRef)(JNIEnvExt *, jobject) = nullptr; 15 | public: 16 | static bool Init(elf_parser::Elf &art) { 17 | bool success = true; 18 | symNewLocalRef = reinterpret_cast(art.getSymbAddress("_ZN3art9JNIEnvExt11NewLocalRefEPNS_6mirror6ObjectE")); 19 | if (!symNewLocalRef) { 20 | success = false; 21 | LOGE("not found: art::JNIEnvExt::NewLocalRef"); 22 | } 23 | symDeleteLocalRef = reinterpret_cast(art.getSymbAddress("_ZN3art9JNIEnvExt14DeleteLocalRefEP8_jobject")); 24 | if (!symDeleteLocalRef) { 25 | success = false; 26 | LOGE("not found: art::JNIEnvExt::DeleteLocalRef"); 27 | } 28 | return success; 29 | } 30 | 31 | inline jobject NewLocalRef(art::mirror::Object* object) { 32 | return symNewLocalRef(this, object); 33 | } 34 | 35 | inline void DeleteLocalRef(jobject object) { 36 | symDeleteLocalRef(this, object); 37 | } 38 | 39 | static inline JNIEnvExt* From(JNIEnv* env) { 40 | return reinterpret_cast(env); 41 | } 42 | }; 43 | 44 | struct JavaVMExt { 45 | private: 46 | static inline void (*symVisitRoots)(JavaVMExt*, art::RootVisitor*) = nullptr; 47 | static inline void (*symSweepJniWeakGlobals)(JavaVMExt*, art::IsMarkedVisitor*) = nullptr; 48 | 49 | public: 50 | static bool Init(elf_parser::Elf &art) { 51 | bool success = true; 52 | symVisitRoots = reinterpret_cast(art.getSymbAddress("_ZN3art9JavaVMExt10VisitRootsEPNS_11RootVisitorE")); 53 | if (!symVisitRoots) { 54 | success = false; 55 | LOGE("not found: art::JavaVMExt::VisitRoots"); 56 | } 57 | symSweepJniWeakGlobals = reinterpret_cast(art.getSymbAddress("_ZN3art9JavaVMExt19SweepJniWeakGlobalsEPNS_15IsMarkedVisitorE")); 58 | if (!symSweepJniWeakGlobals) { 59 | success = false; 60 | LOGE("not found: art::JavaVMExt::SweepJniWeakGlobals"); 61 | } 62 | return success; 63 | } 64 | 65 | inline void VisitRoots(art::RootVisitor* visitor) { 66 | if (symVisitRoots) symVisitRoots(this, visitor); 67 | } 68 | 69 | inline void SweepJniWeakGlobals(art::IsMarkedVisitor* visitor) { 70 | if (symSweepJniWeakGlobals) symSweepJniWeakGlobals(this, visitor); 71 | } 72 | 73 | static inline JavaVMExt* From(JavaVM* vm) { 74 | return reinterpret_cast(vm); 75 | } 76 | }; 77 | 78 | using Callback = std::function; 79 | 80 | class ClassLoaderVisitor : public art::SingleRootVisitor { 81 | private: 82 | Callback callback_; 83 | jmethodID toStringMid; 84 | public: 85 | ClassLoaderVisitor(JNIEnv *env, jclass classLoader, Callback callback) : env_(env), classLoader_(classLoader), callback_(std::move(callback)) { 86 | auto objectClass = env_->FindClass("java/lang/Class"); 87 | toStringMid = env_->GetMethodID(objectClass, "getName", "()Ljava/lang/String;"); 88 | } 89 | 90 | void VisitRoot(art::mirror::Object *root, const art::RootInfo &info ATTRIBUTE_UNUSED) final { 91 | jobject object = JNIEnvExt::From(env_)->NewLocalRef(root); 92 | if (object != nullptr) { 93 | auto s = (jstring) env_->CallObjectMethod(env_->GetObjectClass(object), toStringMid); 94 | auto c = env_->GetStringUTFChars(s, nullptr); 95 | env_->ReleaseStringUTFChars(s, c); 96 | LOGD("object name %s", c); 97 | if (env_->IsInstanceOf(object, classLoader_)) { 98 | callback_(root); 99 | } 100 | JNIEnvExt::From(env_)->DeleteLocalRef(object); 101 | } 102 | } 103 | 104 | private: 105 | JNIEnv *env_; 106 | jclass classLoader_; 107 | }; 108 | 109 | class MyClassLoaderVisitor : public art::ClassLoaderVisitor { 110 | public: 111 | MyClassLoaderVisitor(JNIEnv *env, Callback callback) : env_(env), callback_(std::move(callback)) { 112 | } 113 | 114 | void Visit(art::mirror::ClassLoader *class_loader) override { 115 | callback_(class_loader); 116 | } 117 | 118 | private: 119 | JNIEnv *env_; 120 | Callback callback_; 121 | }; 122 | 123 | jobjectArray visitClassLoaders(JNIEnv *env) { 124 | jclass class_loader_class = env->FindClass("java/lang/ClassLoader"); 125 | std::vector class_loaders; 126 | auto callback = [&](art::mirror::Object* o) { 127 | auto r = JNIEnvExt::From(env)->NewLocalRef(reinterpret_cast(o)); 128 | class_loaders.push_back(r); 129 | }; 130 | MyClassLoaderVisitor v(env, callback); 131 | art::Runtime::Current()->getClassLinker()->VisitClassLoaders(&v); 132 | auto arr = env->NewObjectArray(static_cast(class_loaders.size()), class_loader_class, nullptr); 133 | for (auto i = 0; i < class_loaders.size(); i++) { 134 | auto o = class_loaders[i]; 135 | env->SetObjectArrayElement(arr, i, o); 136 | JNIEnvExt::From(env)->DeleteLocalRef(o); 137 | } 138 | return arr; 139 | } 140 | 141 | class WeakClassLoaderVisitor : public art::IsMarkedVisitor { 142 | Callback callback_; 143 | jmethodID toStringMid; 144 | public : 145 | WeakClassLoaderVisitor(JNIEnv *env, jclass classLoader, Callback callback) : env_(env), classLoader_(classLoader), callback_(std::move(callback)) { 146 | auto objectClass = env_->FindClass("java/lang/Class"); 147 | toStringMid = env_->GetMethodID(objectClass, "getName", "()Ljava/lang/String;"); 148 | } 149 | 150 | art::mirror::Object *IsMarked(art::mirror::Object *obj) override { 151 | jobject object = JNIEnvExt::From(env_)->NewLocalRef(obj); 152 | if (object != nullptr) { 153 | if (env_->IsInstanceOf(object, classLoader_)) { 154 | auto s = (jstring) env_->CallObjectMethod(env_->GetObjectClass(object), toStringMid); 155 | auto c = env_->GetStringUTFChars(s, nullptr); 156 | env_->ReleaseStringUTFChars(s, c); 157 | LOGD("object name %s", c); 158 | callback_(obj); 159 | } 160 | JNIEnvExt::From(env_)->DeleteLocalRef(object); 161 | } 162 | return obj; 163 | } 164 | 165 | private: 166 | JNIEnv *env_; 167 | jclass classLoader_; 168 | }; 169 | 170 | jobjectArray visitClassLoadersByRootVisitor(JNIEnv *env) { 171 | JavaVM *jvm; 172 | env->GetJavaVM(&jvm); 173 | jclass class_loader_class = env->FindClass("java/lang/ClassLoader"); 174 | std::vector class_loaders; 175 | auto callback = [&](art::mirror::Object* o) { 176 | class_loaders.push_back(JNIEnvExt::From(env)->NewLocalRef(o)); 177 | }; 178 | { 179 | ClassLoaderVisitor visitor(env, class_loader_class, callback); 180 | JavaVMExt::From(jvm)->VisitRoots(&visitor); 181 | } 182 | WeakClassLoaderVisitor visitor(env, class_loader_class, callback); 183 | JavaVMExt::From(jvm)->SweepJniWeakGlobals(&visitor); 184 | auto arr = env->NewObjectArray(class_loaders.size(), class_loader_class, nullptr); 185 | for (auto i = 0; i < class_loaders.size(); i++) { 186 | auto o = class_loaders[i]; 187 | env->SetObjectArrayElement(arr, i, o); 188 | JNIEnvExt::From(env)->DeleteLocalRef(o); 189 | } 190 | return arr; 191 | } 192 | 193 | bool InitClassLoaders(elf_parser::Elf &art) { 194 | bool success = true; 195 | if (!JNIEnvExt::Init(art)) { 196 | LOGE("JNIEnvExt init failed"); 197 | success = false; 198 | } 199 | if (JavaVMExt::Init(art)) { 200 | LOGE("JavaVMExt init failed"); 201 | success = false; 202 | } 203 | return success; 204 | } 205 | 206 | extern "C" 207 | JNIEXPORT jobjectArray JNICALL 208 | Java_io_github_a13e300_tools_NativeUtils_getClassLoaders(JNIEnv *env, jclass clazz) { 209 | return visitClassLoaders(env); 210 | } 211 | 212 | extern "C" 213 | JNIEXPORT jobjectArray JNICALL 214 | Java_io_github_a13e300_tools_NativeUtils_getClassLoaders2(JNIEnv *env, jclass clazz) { 215 | return visitClassLoadersByRootVisitor(env); 216 | } 217 | 218 | struct GlobalRefVisitor : public art::SingleRootVisitor { 219 | private: 220 | Callback callback_; 221 | public: 222 | explicit GlobalRefVisitor(Callback&& callback) : callback_(callback) {} 223 | 224 | void VisitRoot(art::mirror::Object *root, const art::RootInfo &info) final { 225 | callback_(root); 226 | } 227 | }; 228 | 229 | extern "C" 230 | JNIEXPORT jobjectArray JNICALL 231 | Java_io_github_a13e300_tools_NativeUtils_getGlobalRefs(JNIEnv *env, jclass, jclass clazz) { 232 | JavaVM *jvm; 233 | env->GetJavaVM(&jvm); 234 | 235 | std::vector objects; 236 | GlobalRefVisitor visitor{[&](art::mirror::Object* o) { 237 | auto obj = JNIEnvExt::From(env)->NewLocalRef(o); 238 | if (!clazz || env->IsInstanceOf(obj, clazz)) 239 | objects.push_back(obj); 240 | else 241 | JNIEnvExt::From(env)->DeleteLocalRef(obj); 242 | }}; 243 | 244 | JavaVMExt::From(jvm)->VisitRoots(&visitor); 245 | 246 | if (clazz == nullptr) { 247 | clazz = env->FindClass("java/lang/Object"); 248 | } 249 | 250 | auto arr = env->NewObjectArray(static_cast(objects.size()), clazz, nullptr); 251 | for (auto i = 0; i < objects.size(); i++) { 252 | auto o = objects[i]; 253 | env->SetObjectArrayElement(arr, i, o); 254 | JNIEnvExt::From(env)->DeleteLocalRef(o); 255 | } 256 | return arr; 257 | } 258 | -------------------------------------------------------------------------------- /app/src/main/cpp/classloader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "elf_parser.hpp" 5 | 6 | bool InitClassLoaders(elf_parser::Elf &art); 7 | -------------------------------------------------------------------------------- /app/src/main/cpp/elf_parser/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(elf_parser) 2 | 3 | add_library(elf_parser STATIC 4 | elf_parser.cc 5 | xz-embedded/xz_crc32.c 6 | xz-embedded/xz_crc64.c 7 | xz-embedded/xz_dec_lzma2.c 8 | xz-embedded/xz_dec_stream.c 9 | ) 10 | target_include_directories(elf_parser PUBLIC include) 11 | target_include_directories(elf_parser PRIVATE xz-embedded) 12 | -------------------------------------------------------------------------------- /app/src/main/cpp/elf_parser/include/elf_parser.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace elf_parser { 13 | class Elf { 14 | bool valid_{false}; 15 | bool for_dynamic_{false}; 16 | 17 | uintptr_t load_base_{0}; 18 | uintptr_t parse_base_{0}; 19 | uintptr_t vaddr_min_{UINTPTR_MAX}; 20 | size_t parse_size_{0}; 21 | ElfW(Ehdr) *header_ = nullptr; 22 | ElfW(Addr) dynamic_off_{0}; 23 | size_t dynamic_size_{0}; 24 | 25 | const char *dynstr_ = nullptr; 26 | ElfW(Sym) *dynsym_ = nullptr; 27 | ElfW(Sym) *symtab_ = nullptr; 28 | ElfW(Off) symtab_count_ = 0; 29 | ElfW(Off) symstr_ = 0; 30 | 31 | uint32_t nbucket_{}; 32 | uint32_t *bucket_ = nullptr; 33 | uint32_t *chain_ = nullptr; 34 | 35 | uint32_t gnu_nbucket_{}; 36 | uint32_t gnu_symndx_{}; 37 | uint32_t gnu_bloom_size_{}; 38 | uint32_t gnu_shift2_{}; 39 | uintptr_t *gnu_bloom_filter_{}; 40 | uint32_t *gnu_bucket_{}; 41 | uint32_t *gnu_chain_{}; 42 | 43 | // for plt 44 | bool is_use_rela_ = false; 45 | 46 | ElfW(Addr) rel_plt_ = 0; //.rel.plt or .rela.plt 47 | ElfW(Word) rel_plt_size_ = 0; 48 | 49 | ElfW(Addr) rel_dyn_ = 0; //.rel.dyn or .rela.dyn 50 | ElfW(Word) rel_dyn_size_ = 0; 51 | 52 | ElfW(Addr) rel_android_ = 0; // android compressed rel or rela 53 | ElfW(Word) rel_android_size_ = 0; 54 | 55 | std::unique_ptr> gnu_debugdata_{nullptr}; 56 | std::unique_ptr gnu_debugdata_elf_{nullptr}; 57 | 58 | mutable std::map symtabs_; 59 | 60 | std::string path; 61 | 62 | constexpr inline static uint32_t GnuHash(std::string_view name) { 63 | constexpr uint32_t kInitialHash = 5381; 64 | constexpr uint32_t kHashShift = 5; 65 | uint32_t hash = kInitialHash; 66 | for (unsigned char chr: name) { 67 | hash += (hash << kHashShift) + chr; 68 | } 69 | return hash; 70 | } 71 | 72 | constexpr inline static uint32_t ElfHash(std::string_view name) { 73 | constexpr uint32_t kHashMask = 0xf0000000; 74 | constexpr uint32_t kHashShift = 24; 75 | uint32_t hash = 0; 76 | for (unsigned char chr: name) { 77 | hash = (hash << 4) + chr; 78 | uint32_t tmp = hash & kHashMask; 79 | hash ^= tmp; 80 | hash ^= tmp >> kHashShift; 81 | } 82 | return hash; 83 | } 84 | 85 | bool Init(uintptr_t load_base, uintptr_t parse_base, size_t size); 86 | 87 | bool InitFromData(std::vector &&data); 88 | 89 | ElfW(Sym)* getSym(std::string_view name, uint32_t gnu_hash, 90 | uint32_t elf_hash) const; 91 | 92 | ElfW(Sym)* ElfLookup(std::string_view name, uint32_t hash) const; 93 | 94 | ElfW(Sym)* GnuLookup(std::string_view name, uint32_t hash) const; 95 | 96 | uint32_t ElfLookupIdx(std::string_view name, uint32_t hash) const; 97 | 98 | uint32_t GnuLookupIdx(std::string_view name, uint32_t hash) const; 99 | 100 | void MayInitLinearMap() const; 101 | 102 | ElfW(Sym)* LinearLookup(std::string_view name) const; 103 | 104 | uint32_t LinearLookupForDyn(std::string_view name) const; 105 | 106 | std::vector LinearRangeLookup(std::string_view name) const; 107 | 108 | ElfW(Addr) PrefixLookupFirst(std::string_view prefix) const; 109 | 110 | ElfW(Sym)* PrefixLookupFirstSym(std::string_view prefix) const; 111 | bool LoadSymbolsForDynamic(); 112 | 113 | bool LoadSymbolsForFull(); 114 | 115 | public: 116 | Elf() = default; 117 | 118 | bool InitFromFile(std::string_view so_path, uintptr_t base_addr = 0, bool init_sym = false); 119 | 120 | bool InitFromMemory(void* addr, bool init_sym = false); 121 | 122 | bool LoadSymbols(); 123 | 124 | std::vector FindPltAddr(std::string_view name) const; 125 | 126 | ElfW(Sym)* getSym(std::string_view name) const; 127 | 128 | ElfW(Sym)* getSymByPrefix(std::string_view name) const; 129 | 130 | void forEachSymbols(std::function &&fn) const; 131 | 132 | ~Elf(); 133 | 134 | constexpr bool IsValid() const { return valid_; } 135 | 136 | inline auto GetLoadBias() const { 137 | return static_cast(load_base_ - vaddr_min_); 138 | } 139 | 140 | template 141 | requires(std::is_pointer_v) 142 | constexpr T getSymbAddress(std::string_view name) const { 143 | auto sym = getSym(name, GnuHash(name), ElfHash(name)); 144 | if (sym == nullptr) return nullptr; 145 | auto offset = sym->st_value; 146 | return reinterpret_cast( 147 | static_cast(GetLoadBias() + offset)); 148 | } 149 | 150 | template 151 | requires(std::is_pointer_v) 152 | constexpr T getSymbPrefixFirstAddress(std::string_view prefix) const { 153 | auto offset = PrefixLookupFirst(prefix); 154 | if (offset > 0 && load_base_ != 0) { 155 | return reinterpret_cast( 156 | static_cast(GetLoadBias() + offset)); 157 | } else { 158 | return nullptr; 159 | } 160 | } 161 | 162 | template 163 | requires(std::is_pointer_v) 164 | std::vector getAllSymbAddress(std::string_view name) const { 165 | auto offsets = LinearRangeLookup(name); 166 | std::vector res; 167 | res.reserve(offsets.size()); 168 | for (const auto &offset: offsets) { 169 | res.emplace_back(reinterpret_cast( 170 | static_cast(GetLoadBias() + offset))); 171 | } 172 | return res; 173 | } 174 | 175 | inline uintptr_t GetLoadBase() const { 176 | return load_base_; 177 | } 178 | 179 | inline void SetLoadBase(uintptr_t base) { 180 | load_base_ = base; 181 | } 182 | 183 | inline std::string GetPath() const { 184 | return path; 185 | } 186 | 187 | inline ElfW(Ehdr) *GetElfHeader() const { 188 | return header_; 189 | } 190 | 191 | inline uintptr_t GetParseBase() const { 192 | return parse_base_; 193 | } 194 | 195 | inline bool IsDynamic() const { 196 | if (!valid_) return false; 197 | return header_->e_type == ET_DYN; 198 | } 199 | }; 200 | } // namespace elf_parser 201 | -------------------------------------------------------------------------------- /app/src/main/cpp/elf_parser/xz-embedded/xz_config.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Private includes and definitions for userspace use of XZ Embedded 3 | * 4 | * Author: Lasse Collin 5 | * 6 | * This file has been put into the public domain. 7 | * You can do whatever you want with this file. 8 | */ 9 | 10 | #ifndef XZ_CONFIG_H 11 | #define XZ_CONFIG_H 12 | 13 | /* Uncomment to enable building of xz_dec_catrun(). */ 14 | /* #define XZ_DEC_CONCATENATED */ 15 | 16 | /* Uncomment to enable CRC64 support. */ 17 | #define XZ_USE_CRC64 18 | 19 | /* Uncomment as needed to enable BCJ filter decoders. */ 20 | /* #define XZ_DEC_X86 */ 21 | /* #define XZ_DEC_ARM */ 22 | /* #define XZ_DEC_ARMTHUMB */ 23 | /* #define XZ_DEC_ARM64 */ 24 | /* #define XZ_DEC_POWERPC */ 25 | /* #define XZ_DEC_IA64 */ 26 | /* #define XZ_DEC_SPARC */ 27 | 28 | /* 29 | * MSVC doesn't support modern C but XZ Embedded is mostly C89 30 | * so these are enough. 31 | */ 32 | #ifdef _MSC_VER 33 | typedef unsigned char bool; 34 | # define true 1 35 | # define false 0 36 | # define inline __inline 37 | #else 38 | # include 39 | #endif 40 | 41 | #include 42 | #include 43 | 44 | #include "xz.h" 45 | 46 | #define kmalloc(size, flags) malloc(size) 47 | #define kfree(ptr) free(ptr) 48 | #define vmalloc(size) malloc(size) 49 | #define vfree(ptr) free(ptr) 50 | 51 | #define memeq(a, b, size) (memcmp(a, b, size) == 0) 52 | #define memzero(buf, size) memset(buf, 0, size) 53 | 54 | #ifndef min 55 | # define min(x, y) ((x) < (y) ? (x) : (y)) 56 | #endif 57 | #define min_t(type, x, y) min(x, y) 58 | 59 | /* 60 | * Some functions have been marked with __always_inline to keep the 61 | * performance reasonable even when the compiler is optimizing for 62 | * small code size. You may be able to save a few bytes by #defining 63 | * __always_inline to plain inline, but don't complain if the code 64 | * becomes slow. 65 | * 66 | * NOTE: System headers on GNU/Linux may #define this macro already, 67 | * so if you want to change it, you need to #undef it first. 68 | */ 69 | #ifndef __always_inline 70 | # ifdef __GNUC__ 71 | # define __always_inline \ 72 | inline __attribute__((__always_inline__)) 73 | # else 74 | # define __always_inline inline 75 | # endif 76 | #endif 77 | 78 | /* Inline functions to access unaligned unsigned 32-bit integers */ 79 | #ifndef get_unaligned_le32 80 | static inline uint32_t get_unaligned_le32(const uint8_t *buf) 81 | { 82 | return (uint32_t)buf[0] 83 | | ((uint32_t)buf[1] << 8) 84 | | ((uint32_t)buf[2] << 16) 85 | | ((uint32_t)buf[3] << 24); 86 | } 87 | #endif 88 | 89 | #ifndef get_unaligned_be32 90 | static inline uint32_t get_unaligned_be32(const uint8_t *buf) 91 | { 92 | return (uint32_t)(buf[0] << 24) 93 | | ((uint32_t)buf[1] << 16) 94 | | ((uint32_t)buf[2] << 8) 95 | | (uint32_t)buf[3]; 96 | } 97 | #endif 98 | 99 | #ifndef put_unaligned_le32 100 | static inline void put_unaligned_le32(uint32_t val, uint8_t *buf) 101 | { 102 | buf[0] = (uint8_t)val; 103 | buf[1] = (uint8_t)(val >> 8); 104 | buf[2] = (uint8_t)(val >> 16); 105 | buf[3] = (uint8_t)(val >> 24); 106 | } 107 | #endif 108 | 109 | #ifndef put_unaligned_be32 110 | static inline void put_unaligned_be32(uint32_t val, uint8_t *buf) 111 | { 112 | buf[0] = (uint8_t)(val >> 24); 113 | buf[1] = (uint8_t)(val >> 16); 114 | buf[2] = (uint8_t)(val >> 8); 115 | buf[3] = (uint8_t)val; 116 | } 117 | #endif 118 | 119 | /* 120 | * Use get_unaligned_le32() also for aligned access for simplicity. On 121 | * little endian systems, #define get_le32(ptr) (*(const uint32_t *)(ptr)) 122 | * could save a few bytes in code size. 123 | */ 124 | #ifndef get_le32 125 | # define get_le32 get_unaligned_le32 126 | #endif 127 | 128 | #endif 129 | -------------------------------------------------------------------------------- /app/src/main/cpp/elf_parser/xz-embedded/xz_crc32.c: -------------------------------------------------------------------------------- 1 | /* 2 | * CRC32 using the polynomial from IEEE-802.3 3 | * 4 | * Authors: Lasse Collin 5 | * Igor Pavlov 6 | * 7 | * This file has been put into the public domain. 8 | * You can do whatever you want with this file. 9 | */ 10 | 11 | /* 12 | * This is not the fastest implementation, but it is pretty compact. 13 | * The fastest versions of xz_crc32() on modern CPUs without hardware 14 | * accelerated CRC instruction are 3-5 times as fast as this version, 15 | * but they are bigger and use more memory for the lookup table. 16 | */ 17 | 18 | #include "xz_private.h" 19 | 20 | /* 21 | * STATIC_RW_DATA is used in the pre-boot environment on some architectures. 22 | * See for details. 23 | */ 24 | #ifndef STATIC_RW_DATA 25 | # define STATIC_RW_DATA static 26 | #endif 27 | 28 | STATIC_RW_DATA uint32_t xz_crc32_table[256]; 29 | 30 | XZ_EXTERN void xz_crc32_init(void) 31 | { 32 | const uint32_t poly = 0xEDB88320; 33 | 34 | uint32_t i; 35 | uint32_t j; 36 | uint32_t r; 37 | 38 | for (i = 0; i < 256; ++i) { 39 | r = i; 40 | for (j = 0; j < 8; ++j) 41 | r = (r >> 1) ^ (poly & ~((r & 1) - 1)); 42 | 43 | xz_crc32_table[i] = r; 44 | } 45 | 46 | return; 47 | } 48 | 49 | XZ_EXTERN uint32_t xz_crc32(const uint8_t *buf, size_t size, uint32_t crc) 50 | { 51 | crc = ~crc; 52 | 53 | while (size != 0) { 54 | crc = xz_crc32_table[*buf++ ^ (crc & 0xFF)] ^ (crc >> 8); 55 | --size; 56 | } 57 | 58 | return ~crc; 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/cpp/elf_parser/xz-embedded/xz_crc64.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: 0BSD 2 | 3 | /* 4 | * CRC64 using the polynomial from ECMA-182 5 | * 6 | * This file is similar to xz_crc32.c. See the comments there. 7 | * 8 | * Authors: Lasse Collin 9 | * Igor Pavlov 10 | */ 11 | 12 | #include "xz_private.h" 13 | 14 | #ifndef STATIC_RW_DATA 15 | # define STATIC_RW_DATA static 16 | #endif 17 | 18 | STATIC_RW_DATA uint64_t xz_crc64_table[256]; 19 | 20 | XZ_EXTERN void xz_crc64_init(void) 21 | { 22 | /* 23 | * The ULL suffix is needed for -std=gnu89 compatibility 24 | * on 32-bit platforms. 25 | */ 26 | const uint64_t poly = 0xC96C5795D7870F42ULL; 27 | 28 | uint32_t i; 29 | uint32_t j; 30 | uint64_t r; 31 | 32 | for (i = 0; i < 256; ++i) { 33 | r = i; 34 | for (j = 0; j < 8; ++j) 35 | r = (r >> 1) ^ (poly & ~((r & 1) - 1)); 36 | 37 | xz_crc64_table[i] = r; 38 | } 39 | 40 | return; 41 | } 42 | 43 | XZ_EXTERN uint64_t xz_crc64(const uint8_t *buf, size_t size, uint64_t crc) 44 | { 45 | crc = ~crc; 46 | 47 | while (size != 0) { 48 | crc = xz_crc64_table[*buf++ ^ (crc & 0xFF)] ^ (crc >> 8); 49 | --size; 50 | } 51 | 52 | return ~crc; 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/cpp/elf_parser/xz-embedded/xz_lzma2.h: -------------------------------------------------------------------------------- 1 | /* 2 | * LZMA2 definitions 3 | * 4 | * Authors: Lasse Collin 5 | * Igor Pavlov 6 | * 7 | * This file has been put into the public domain. 8 | * You can do whatever you want with this file. 9 | */ 10 | 11 | #ifndef XZ_LZMA2_H 12 | #define XZ_LZMA2_H 13 | 14 | /* Range coder constants */ 15 | #define RC_SHIFT_BITS 8 16 | #define RC_TOP_BITS 24 17 | #define RC_TOP_VALUE (1 << RC_TOP_BITS) 18 | #define RC_BIT_MODEL_TOTAL_BITS 11 19 | #define RC_BIT_MODEL_TOTAL (1 << RC_BIT_MODEL_TOTAL_BITS) 20 | #define RC_MOVE_BITS 5 21 | 22 | /* 23 | * Maximum number of position states. A position state is the lowest pb 24 | * number of bits of the current uncompressed offset. In some places there 25 | * are different sets of probabilities for different position states. 26 | */ 27 | #define POS_STATES_MAX (1 << 4) 28 | 29 | /* 30 | * This enum is used to track which LZMA symbols have occurred most recently 31 | * and in which order. This information is used to predict the next symbol. 32 | * 33 | * Symbols: 34 | * - Literal: One 8-bit byte 35 | * - Match: Repeat a chunk of data at some distance 36 | * - Long repeat: Multi-byte match at a recently seen distance 37 | * - Short repeat: One-byte repeat at a recently seen distance 38 | * 39 | * The symbol names are in from STATE_oldest_older_previous. REP means 40 | * either short or long repeated match, and NONLIT means any non-literal. 41 | */ 42 | enum lzma_state { 43 | STATE_LIT_LIT, 44 | STATE_MATCH_LIT_LIT, 45 | STATE_REP_LIT_LIT, 46 | STATE_SHORTREP_LIT_LIT, 47 | STATE_MATCH_LIT, 48 | STATE_REP_LIT, 49 | STATE_SHORTREP_LIT, 50 | STATE_LIT_MATCH, 51 | STATE_LIT_LONGREP, 52 | STATE_LIT_SHORTREP, 53 | STATE_NONLIT_MATCH, 54 | STATE_NONLIT_REP 55 | }; 56 | 57 | /* Total number of states */ 58 | #define STATES 12 59 | 60 | /* The lowest 7 states indicate that the previous state was a literal. */ 61 | #define LIT_STATES 7 62 | 63 | /* Indicate that the latest symbol was a literal. */ 64 | static inline void lzma_state_literal(enum lzma_state *state) 65 | { 66 | if (*state <= STATE_SHORTREP_LIT_LIT) 67 | *state = STATE_LIT_LIT; 68 | else if (*state <= STATE_LIT_SHORTREP) 69 | *state -= 3; 70 | else 71 | *state -= 6; 72 | } 73 | 74 | /* Indicate that the latest symbol was a match. */ 75 | static inline void lzma_state_match(enum lzma_state *state) 76 | { 77 | *state = *state < LIT_STATES ? STATE_LIT_MATCH : STATE_NONLIT_MATCH; 78 | } 79 | 80 | /* Indicate that the latest state was a long repeated match. */ 81 | static inline void lzma_state_long_rep(enum lzma_state *state) 82 | { 83 | *state = *state < LIT_STATES ? STATE_LIT_LONGREP : STATE_NONLIT_REP; 84 | } 85 | 86 | /* Indicate that the latest symbol was a short match. */ 87 | static inline void lzma_state_short_rep(enum lzma_state *state) 88 | { 89 | *state = *state < LIT_STATES ? STATE_LIT_SHORTREP : STATE_NONLIT_REP; 90 | } 91 | 92 | /* Test if the previous symbol was a literal. */ 93 | static inline bool lzma_state_is_literal(enum lzma_state state) 94 | { 95 | return state < LIT_STATES; 96 | } 97 | 98 | /* Each literal coder is divided in three sections: 99 | * - 0x001-0x0FF: Without match byte 100 | * - 0x101-0x1FF: With match byte; match bit is 0 101 | * - 0x201-0x2FF: With match byte; match bit is 1 102 | * 103 | * Match byte is used when the previous LZMA symbol was something else than 104 | * a literal (that is, it was some kind of match). 105 | */ 106 | #define LITERAL_CODER_SIZE 0x300 107 | 108 | /* Maximum number of literal coders */ 109 | #define LITERAL_CODERS_MAX (1 << 4) 110 | 111 | /* Minimum length of a match is two bytes. */ 112 | #define MATCH_LEN_MIN 2 113 | 114 | /* Match length is encoded with 4, 5, or 10 bits. 115 | * 116 | * Length Bits 117 | * 2-9 4 = Choice=0 + 3 bits 118 | * 10-17 5 = Choice=1 + Choice2=0 + 3 bits 119 | * 18-273 10 = Choice=1 + Choice2=1 + 8 bits 120 | */ 121 | #define LEN_LOW_BITS 3 122 | #define LEN_LOW_SYMBOLS (1 << LEN_LOW_BITS) 123 | #define LEN_MID_BITS 3 124 | #define LEN_MID_SYMBOLS (1 << LEN_MID_BITS) 125 | #define LEN_HIGH_BITS 8 126 | #define LEN_HIGH_SYMBOLS (1 << LEN_HIGH_BITS) 127 | #define LEN_SYMBOLS (LEN_LOW_SYMBOLS + LEN_MID_SYMBOLS + LEN_HIGH_SYMBOLS) 128 | 129 | /* 130 | * Maximum length of a match is 273 which is a result of the encoding 131 | * described above. 132 | */ 133 | #define MATCH_LEN_MAX (MATCH_LEN_MIN + LEN_SYMBOLS - 1) 134 | 135 | /* 136 | * Different sets of probabilities are used for match distances that have 137 | * very short match length: Lengths of 2, 3, and 4 bytes have a separate 138 | * set of probabilities for each length. The matches with longer length 139 | * use a shared set of probabilities. 140 | */ 141 | #define DIST_STATES 4 142 | 143 | /* 144 | * Get the index of the appropriate probability array for decoding 145 | * the distance slot. 146 | */ 147 | static inline uint32_t lzma_get_dist_state(uint32_t len) 148 | { 149 | return len < DIST_STATES + MATCH_LEN_MIN 150 | ? len - MATCH_LEN_MIN : DIST_STATES - 1; 151 | } 152 | 153 | /* 154 | * The highest two bits of a 32-bit match distance are encoded using six bits. 155 | * This six-bit value is called a distance slot. This way encoding a 32-bit 156 | * value takes 6-36 bits, larger values taking more bits. 157 | */ 158 | #define DIST_SLOT_BITS 6 159 | #define DIST_SLOTS (1 << DIST_SLOT_BITS) 160 | 161 | /* Match distances up to 127 are fully encoded using probabilities. Since 162 | * the highest two bits (distance slot) are always encoded using six bits, 163 | * the distances 0-3 don't need any additional bits to encode, since the 164 | * distance slot itself is the same as the actual distance. DIST_MODEL_START 165 | * indicates the first distance slot where at least one additional bit is 166 | * needed. 167 | */ 168 | #define DIST_MODEL_START 4 169 | 170 | /* 171 | * Match distances greater than 127 are encoded in three pieces: 172 | * - distance slot: the highest two bits 173 | * - direct bits: 2-26 bits below the highest two bits 174 | * - alignment bits: four lowest bits 175 | * 176 | * Direct bits don't use any probabilities. 177 | * 178 | * The distance slot value of 14 is for distances 128-191. 179 | */ 180 | #define DIST_MODEL_END 14 181 | 182 | /* Distance slots that indicate a distance <= 127. */ 183 | #define FULL_DISTANCES_BITS (DIST_MODEL_END / 2) 184 | #define FULL_DISTANCES (1 << FULL_DISTANCES_BITS) 185 | 186 | /* 187 | * For match distances greater than 127, only the highest two bits and the 188 | * lowest four bits (alignment) is encoded using probabilities. 189 | */ 190 | #define ALIGN_BITS 4 191 | #define ALIGN_SIZE (1 << ALIGN_BITS) 192 | #define ALIGN_MASK (ALIGN_SIZE - 1) 193 | 194 | /* Total number of all probability variables */ 195 | #define PROBS_TOTAL (1846 + LITERAL_CODERS_MAX * LITERAL_CODER_SIZE) 196 | 197 | /* 198 | * LZMA remembers the four most recent match distances. Reusing these 199 | * distances tends to take less space than re-encoding the actual 200 | * distance value. 201 | */ 202 | #define REPS 4 203 | 204 | #endif 205 | -------------------------------------------------------------------------------- /app/src/main/cpp/elf_parser/xz-embedded/xz_private.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Private includes and definitions 3 | * 4 | * Author: Lasse Collin 5 | * 6 | * This file has been put into the public domain. 7 | * You can do whatever you want with this file. 8 | */ 9 | 10 | #ifndef XZ_PRIVATE_H 11 | #define XZ_PRIVATE_H 12 | 13 | #ifdef __KERNEL__ 14 | # include 15 | # include 16 | # include 17 | /* XZ_PREBOOT may be defined only via decompress_unxz.c. */ 18 | # ifndef XZ_PREBOOT 19 | # include 20 | # include 21 | # include 22 | # ifdef CONFIG_XZ_DEC_X86 23 | # define XZ_DEC_X86 24 | # endif 25 | # ifdef CONFIG_XZ_DEC_POWERPC 26 | # define XZ_DEC_POWERPC 27 | # endif 28 | # ifdef CONFIG_XZ_DEC_IA64 29 | # define XZ_DEC_IA64 30 | # endif 31 | # ifdef CONFIG_XZ_DEC_ARM 32 | # define XZ_DEC_ARM 33 | # endif 34 | # ifdef CONFIG_XZ_DEC_ARMTHUMB 35 | # define XZ_DEC_ARMTHUMB 36 | # endif 37 | # ifdef CONFIG_XZ_DEC_SPARC 38 | # define XZ_DEC_SPARC 39 | # endif 40 | # ifdef CONFIG_XZ_DEC_ARM64 41 | # define XZ_DEC_ARM64 42 | # endif 43 | # ifdef CONFIG_XZ_DEC_MICROLZMA 44 | # define XZ_DEC_MICROLZMA 45 | # endif 46 | # define memeq(a, b, size) (memcmp(a, b, size) == 0) 47 | # define memzero(buf, size) memset(buf, 0, size) 48 | # endif 49 | # define get_le32(p) le32_to_cpup((const uint32_t *)(p)) 50 | #else 51 | /* 52 | * For userspace builds, use a separate header to define the required 53 | * macros and functions. This makes it easier to adapt the code into 54 | * different environments and avoids clutter in the Linux kernel tree. 55 | */ 56 | # include "xz_config.h" 57 | #endif 58 | 59 | /* If no specific decoding mode is requested, enable support for all modes. */ 60 | #if !defined(XZ_DEC_SINGLE) && !defined(XZ_DEC_PREALLOC) \ 61 | && !defined(XZ_DEC_DYNALLOC) 62 | # define XZ_DEC_SINGLE 63 | # define XZ_DEC_PREALLOC 64 | # define XZ_DEC_DYNALLOC 65 | #endif 66 | 67 | /* 68 | * The DEC_IS_foo(mode) macros are used in "if" statements. If only some 69 | * of the supported modes are enabled, these macros will evaluate to true or 70 | * false at compile time and thus allow the compiler to omit unneeded code. 71 | */ 72 | #ifdef XZ_DEC_SINGLE 73 | # define DEC_IS_SINGLE(mode) ((mode) == XZ_SINGLE) 74 | #else 75 | # define DEC_IS_SINGLE(mode) (false) 76 | #endif 77 | 78 | #ifdef XZ_DEC_PREALLOC 79 | # define DEC_IS_PREALLOC(mode) ((mode) == XZ_PREALLOC) 80 | #else 81 | # define DEC_IS_PREALLOC(mode) (false) 82 | #endif 83 | 84 | #ifdef XZ_DEC_DYNALLOC 85 | # define DEC_IS_DYNALLOC(mode) ((mode) == XZ_DYNALLOC) 86 | #else 87 | # define DEC_IS_DYNALLOC(mode) (false) 88 | #endif 89 | 90 | #if !defined(XZ_DEC_SINGLE) 91 | # define DEC_IS_MULTI(mode) (true) 92 | #elif defined(XZ_DEC_PREALLOC) || defined(XZ_DEC_DYNALLOC) 93 | # define DEC_IS_MULTI(mode) ((mode) != XZ_SINGLE) 94 | #else 95 | # define DEC_IS_MULTI(mode) (false) 96 | #endif 97 | 98 | /* 99 | * If any of the BCJ filter decoders are wanted, define XZ_DEC_BCJ. 100 | * XZ_DEC_BCJ is used to enable generic support for BCJ decoders. 101 | */ 102 | #ifndef XZ_DEC_BCJ 103 | # if defined(XZ_DEC_X86) || defined(XZ_DEC_POWERPC) \ 104 | || defined(XZ_DEC_IA64) \ 105 | || defined(XZ_DEC_ARM) || defined(XZ_DEC_ARMTHUMB) \ 106 | || defined(XZ_DEC_SPARC) || defined(XZ_DEC_ARM64) 107 | # define XZ_DEC_BCJ 108 | # endif 109 | #endif 110 | 111 | /* 112 | * Allocate memory for LZMA2 decoder. xz_dec_lzma2_reset() must be used 113 | * before calling xz_dec_lzma2_run(). 114 | */ 115 | XZ_EXTERN struct xz_dec_lzma2 *xz_dec_lzma2_create(enum xz_mode mode, 116 | uint32_t dict_max); 117 | 118 | /* 119 | * Decode the LZMA2 properties (one byte) and reset the decoder. Return 120 | * XZ_OK on success, XZ_MEMLIMIT_ERROR if the preallocated dictionary is not 121 | * big enough, and XZ_OPTIONS_ERROR if props indicates something that this 122 | * decoder doesn't support. 123 | */ 124 | XZ_EXTERN enum xz_ret xz_dec_lzma2_reset(struct xz_dec_lzma2 *s, 125 | uint8_t props); 126 | 127 | /* Decode raw LZMA2 stream from b->in to b->out. */ 128 | XZ_EXTERN enum xz_ret xz_dec_lzma2_run(struct xz_dec_lzma2 *s, 129 | struct xz_buf *b); 130 | 131 | /* Free the memory allocated for the LZMA2 decoder. */ 132 | XZ_EXTERN void xz_dec_lzma2_end(struct xz_dec_lzma2 *s); 133 | 134 | #ifdef XZ_DEC_BCJ 135 | /* 136 | * Allocate memory for BCJ decoders. xz_dec_bcj_reset() must be used before 137 | * calling xz_dec_bcj_run(). 138 | */ 139 | XZ_EXTERN struct xz_dec_bcj *xz_dec_bcj_create(bool single_call); 140 | 141 | /* 142 | * Decode the Filter ID of a BCJ filter. This implementation doesn't 143 | * support custom start offsets, so no decoding of Filter Properties 144 | * is needed. Returns XZ_OK if the given Filter ID is supported. 145 | * Otherwise XZ_OPTIONS_ERROR is returned. 146 | */ 147 | XZ_EXTERN enum xz_ret xz_dec_bcj_reset(struct xz_dec_bcj *s, uint8_t id); 148 | 149 | /* 150 | * Decode raw BCJ + LZMA2 stream. This must be used only if there actually is 151 | * a BCJ filter in the chain. If the chain has only LZMA2, xz_dec_lzma2_run() 152 | * must be called directly. 153 | */ 154 | XZ_EXTERN enum xz_ret xz_dec_bcj_run(struct xz_dec_bcj *s, 155 | struct xz_dec_lzma2 *lzma2, 156 | struct xz_buf *b); 157 | 158 | /* Free the memory allocated for the BCJ filters. */ 159 | #define xz_dec_bcj_end(s) kfree(s) 160 | #endif 161 | 162 | #endif 163 | -------------------------------------------------------------------------------- /app/src/main/cpp/elf_parser/xz-embedded/xz_stream.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Definitions for handling the .xz file format 3 | * 4 | * Author: Lasse Collin 5 | * 6 | * This file has been put into the public domain. 7 | * You can do whatever you want with this file. 8 | */ 9 | 10 | #ifndef XZ_STREAM_H 11 | #define XZ_STREAM_H 12 | 13 | #if defined(__KERNEL__) && !XZ_INTERNAL_CRC32 14 | # include 15 | # undef crc32 16 | # define xz_crc32(buf, size, crc) \ 17 | (~crc32_le(~(uint32_t)(crc), buf, size)) 18 | #endif 19 | 20 | /* 21 | * See the .xz file format specification at 22 | * https://xz.tukaani.org/format/xz-file-format.txt 23 | * to understand the container format. 24 | */ 25 | 26 | #define STREAM_HEADER_SIZE 12 27 | 28 | #define HEADER_MAGIC "\3757zXZ" 29 | #define HEADER_MAGIC_SIZE 6 30 | 31 | #define FOOTER_MAGIC "YZ" 32 | #define FOOTER_MAGIC_SIZE 2 33 | 34 | /* 35 | * Variable-length integer can hold a 63-bit unsigned integer or a special 36 | * value indicating that the value is unknown. 37 | * 38 | * Experimental: vli_type can be defined to uint32_t to save a few bytes 39 | * in code size (no effect on speed). Doing so limits the uncompressed and 40 | * compressed size of the file to less than 256 MiB and may also weaken 41 | * error detection slightly. 42 | */ 43 | typedef uint64_t vli_type; 44 | 45 | #define VLI_MAX ((vli_type)-1 / 2) 46 | #define VLI_UNKNOWN ((vli_type)-1) 47 | 48 | /* Maximum encoded size of a VLI */ 49 | #define VLI_BYTES_MAX (sizeof(vli_type) * 8 / 7) 50 | 51 | /* Integrity Check types */ 52 | enum xz_check { 53 | XZ_CHECK_NONE = 0, 54 | XZ_CHECK_CRC32 = 1, 55 | XZ_CHECK_CRC64 = 4, 56 | XZ_CHECK_SHA256 = 10 57 | }; 58 | 59 | /* Maximum possible Check ID */ 60 | #define XZ_CHECK_MAX 15 61 | 62 | #endif 63 | -------------------------------------------------------------------------------- /app/src/main/cpp/include/logging.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | #define TAG "StethoX" 11 | 12 | #define LOGI(...) (__android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)) 13 | #define LOGW(...) (__android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)) 14 | #define LOGE(...) (__android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)) 15 | #define PLOGE(fmt, ...) (__android_log_print(ANDROID_LOG_ERROR, TAG, "failed with %d %s: " fmt, errno, strerror(errno) __VA_OPT__(,) __VA_ARGS__)) 16 | 17 | #ifdef NDEBUG 18 | #define LOGV(...) 19 | #define LOGD(...) 20 | #define debug(...) 21 | #else 22 | #define DEBUG 1 23 | #define LOGV(...) (__android_log_print(ANDROID_LOG_VERBOSE, TAG, __VA_ARGS__)) 24 | #define LOGD(...) (__android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)) 25 | 26 | #endif 27 | 28 | #ifdef __cplusplus 29 | } 30 | #endif 31 | -------------------------------------------------------------------------------- /app/src/main/cpp/maps_scan/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(maps_scan) 2 | 3 | add_library(maps_scan STATIC maps_scan.cpp) 4 | target_include_directories(maps_scan PUBLIC include) 5 | -------------------------------------------------------------------------------- /app/src/main/cpp/maps_scan/include/maps_scan.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace maps_scan { 11 | struct MapInfo; 12 | using Callback = std::function; 13 | using Filter = Callback; 14 | /// \struct MapInfo 15 | /// \brief An entry that describes a line in /proc/self/maps. You can obtain a list of these entries 16 | /// by calling #Scan(). 17 | struct MapInfo { 18 | /// \brief The start address of the memory region. 19 | uintptr_t start; 20 | /// \brief The end address of the memory region. 21 | uintptr_t end; 22 | /// \brief The permissions of the memory region. This is a bit mask of the following values: 23 | /// - PROT_READ 24 | /// - PROT_WRITE 25 | /// - PROT_EXEC 26 | uint8_t perms; 27 | /// \brief Whether the memory region is private. 28 | bool is_private; 29 | /// \brief The offset of the memory region. 30 | uintptr_t offset; 31 | /// \brief The device number of the memory region. 32 | /// Major can be obtained by #major() 33 | /// Minor can be obtained by #minor() 34 | dev_t dev; 35 | /// \brief The inode number of the memory region. 36 | ino_t inode; 37 | /// \brief The path of the memory region. 38 | std::string path; 39 | 40 | /// \brief Scans /proc/self/maps and returns a list of \ref MapInfo entries. 41 | /// This is useful to find out the inode of the library to hook. 42 | /// \param[in] pid The process id to scan. This is "self" by default. 43 | /// \return A list of \ref MapInfo entries. 44 | static std::vector Scan(std::string_view pid = "self", std::optional filter = std::nullopt); 45 | 46 | static void ForEach(const Callback &callback, std::string_view pid = "self"); 47 | 48 | static inline std::vector ScanSelf(std::optional filter = std::nullopt) { 49 | return Scan("self", filter); 50 | } 51 | 52 | inline bool InRange(uintptr_t addr) const { 53 | return addr >= start && addr < end; 54 | } 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/cpp/maps_scan/maps_scan.cpp: -------------------------------------------------------------------------------- 1 | #include "maps_scan.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace maps_scan { 12 | 13 | [[maybe_unused]] void MapInfo::ForEach(const Callback &callback, std::string_view pid) { 14 | constexpr static auto kPermLength = 5; 15 | constexpr static auto kMapEntry = 7; 16 | auto path = "/proc/" + std::string{pid} + "/maps"; 17 | auto maps = std::unique_ptr{fopen(path.c_str(), "r"), &fclose}; 18 | if (maps) { 19 | char *line = nullptr; 20 | size_t len = 0; 21 | ssize_t read; 22 | while ((read = getline(&line, &len, maps.get())) > 0) { 23 | line[read - 1] = '\0'; 24 | uintptr_t start = 0; 25 | uintptr_t end = 0; 26 | uintptr_t off = 0; 27 | ino_t inode = 0; 28 | unsigned int dev_major = 0; 29 | unsigned int dev_minor = 0; 30 | std::array perm{'\0'}; 31 | int path_off; 32 | if (sscanf(line, "%" PRIxPTR "-%" PRIxPTR " %4s %" PRIxPTR " %x:%x %lu %n%*s", &start, 33 | &end, perm.data(), &off, &dev_major, &dev_minor, &inode, 34 | &path_off) != kMapEntry) { 35 | continue; 36 | } 37 | while (path_off < read && isspace(line[path_off])) path_off++; 38 | uint8_t perms = 0; 39 | if (perm[0] == 'r') perms |= PROT_READ; 40 | if (perm[1] == 'w') perms |= PROT_WRITE; 41 | if (perm[2] == 'x') perms |= PROT_EXEC; 42 | MapInfo mi{start, end, perms, perm[3] == 'p', off, 43 | static_cast(makedev(dev_major, dev_minor)), 44 | inode, line + path_off}; 45 | 46 | if (!callback(mi)) break; 47 | } 48 | free(line); 49 | } 50 | } 51 | 52 | [[maybe_unused]] std::vector MapInfo::Scan(std::string_view pid, std::optional filter) { 53 | std::vector info; 54 | 55 | ForEach([&](const auto &map) -> auto { 56 | if (!filter.has_value() || filter->operator()(map)) 57 | info.emplace_back(map); 58 | return true; 59 | }, pid); 60 | 61 | return info; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/cpp/reflection.cpp: -------------------------------------------------------------------------------- 1 | #include "reflection.hpp" 2 | 3 | #include "logging.h" 4 | 5 | 6 | namespace Reflection { 7 | // I, J, S, B, C, Z, F, D 8 | // L, V 9 | jmethodID valueOfMethods[8]; 10 | jmethodID valueMethods[8]; 11 | jclass primitiveClasses[8]; 12 | jmethodID invocationTargetExceptionConstructor; 13 | jclass invocationTargetExceptionClass; 14 | 15 | bool Init(JNIEnv *env) { 16 | #define CHECK_JNI(cond, ...) \ 17 | if ((cond)) { \ 18 | LOGE(__VA_ARGS__); \ 19 | env->ExceptionDescribe(); \ 20 | env->ExceptionClear(); \ 21 | return JNI_ERR; \ 22 | } 23 | 24 | #define FIND_CLASS(className, name) \ 25 | jclass class_##name = env->FindClass(className); \ 26 | CHECK_JNI(!class_##name || env->ExceptionCheck(), "class java.lang.%s not found", #className) 27 | 28 | #define FIND_PRIMITIVE_CLASS(idx, cn) \ 29 | FIND_CLASS("java/lang/" #cn, cn) \ 30 | primitiveClasses[idx] = reinterpret_cast(env->NewGlobalRef(class_##cn)); 31 | 32 | FIND_PRIMITIVE_CLASS(0, Integer) 33 | FIND_PRIMITIVE_CLASS(1, Long) 34 | FIND_PRIMITIVE_CLASS(2, Short) 35 | FIND_PRIMITIVE_CLASS(3, Byte) 36 | FIND_PRIMITIVE_CLASS(4, Character) 37 | FIND_PRIMITIVE_CLASS(5, Boolean) 38 | FIND_PRIMITIVE_CLASS(6, Float) 39 | FIND_PRIMITIVE_CLASS(7, Double) 40 | 41 | FIND_CLASS("java/lang/reflect/InvocationTargetException", InvocationTargetException) 42 | invocationTargetExceptionClass = reinterpret_cast(env->NewGlobalRef(class_InvocationTargetException)); 43 | invocationTargetExceptionConstructor = env->GetMethodID(class_InvocationTargetException, "", "(Ljava/lang/Throwable;)V"); 44 | CHECK_JNI(!invocationTargetExceptionConstructor || env->ExceptionCheck(), "constructor InvocationTargetException(Throwable) not found") 45 | 46 | #undef FIND_PRIMITIVE_CLASS 47 | #undef FIND_CLASS 48 | 49 | #define FIND_VALUEOF_METHOD(idx, clazz, type) \ 50 | valueOfMethods[idx] = env->GetStaticMethodID(class_##clazz, "valueOf", "(" #type ")Ljava/lang/" #clazz ";"); \ 51 | CHECK_JNI(!valueOfMethods[idx] || env->ExceptionCheck(), "method %s.valueOf() not found", #clazz) 52 | 53 | FIND_VALUEOF_METHOD(0, Integer, I) 54 | FIND_VALUEOF_METHOD(1, Long, J) 55 | FIND_VALUEOF_METHOD(2, Short, S) 56 | FIND_VALUEOF_METHOD(3, Byte, B) 57 | FIND_VALUEOF_METHOD(4, Character, C) 58 | FIND_VALUEOF_METHOD(5, Boolean, Z) 59 | FIND_VALUEOF_METHOD(6, Float, F) 60 | FIND_VALUEOF_METHOD(7, Double, D) 61 | #undef FIND_VALUEOF_METHOD 62 | 63 | #define FIND_VALUE_METHOD(idx, clazz, type, prefix) \ 64 | valueMethods[idx] = env->GetMethodID(class_##clazz, #prefix "Value", "()" #type); \ 65 | CHECK_JNI(!valueMethods[idx] || env->ExceptionOccurred(), "method %s.%sValue() not found", #clazz, #prefix) 66 | 67 | FIND_VALUE_METHOD(0, Integer, I, int) 68 | FIND_VALUE_METHOD(1, Long, J, long) 69 | FIND_VALUE_METHOD(2, Short, S, short) 70 | FIND_VALUE_METHOD(3, Byte, B, byte) 71 | FIND_VALUE_METHOD(4, Character, C, char) 72 | FIND_VALUE_METHOD(5, Boolean, Z, boolean) 73 | FIND_VALUE_METHOD(6, Float, F, float) 74 | FIND_VALUE_METHOD(7, Double, D, double) 75 | #undef FIND_VALUE_METHOD 76 | 77 | #undef CHECK_JNI 78 | return JNI_VERSION_1_4; 79 | } 80 | 81 | jobject invokeNonVirtualMethod(JNIEnv *env, jobject method, jclass clazz, jbyteArray types, jobject thiz, jobjectArray argArr) { 82 | auto mid = env->FromReflectedMethod(method); 83 | auto len = env->GetArrayLength(types); 84 | auto typeIds = env->GetByteArrayElements(types, nullptr); 85 | 86 | jvalue argValues[len - 1]; 87 | for (int i = 1; i < len; i++) { 88 | auto arg = env->GetObjectArrayElement(argArr, i - 1); 89 | auto typeId = typeIds[i]; 90 | switch (typeId) { 91 | case 0: 92 | argValues[i].i = env->CallIntMethod(arg, valueMethods[0]); 93 | break; 94 | case 1: 95 | argValues[i].j = env->CallLongMethod(arg, valueMethods[1]); 96 | break; 97 | case 2: 98 | argValues[i].s = env->CallShortMethod(arg, valueMethods[2]); 99 | break; 100 | case 3: 101 | argValues[i].b = env->CallByteMethod(arg, valueMethods[3]); 102 | break; 103 | case 4: 104 | argValues[i].c = env->CallCharMethod(arg, valueMethods[4]); 105 | break; 106 | case 5: 107 | argValues[i].z = env->CallBooleanMethod(arg, valueMethods[5]); 108 | break; 109 | case 6: 110 | argValues[i].f = env->CallFloatMethod(arg, valueMethods[6]); 111 | break; 112 | case 7: 113 | argValues[i].d = env->CallDoubleMethod(arg, valueMethods[7]); 114 | break; 115 | default: 116 | argValues[i].l = arg; 117 | break; 118 | } 119 | } 120 | auto retTypeId = typeIds[0]; 121 | env->ReleaseByteArrayElements(types, typeIds, JNI_ABORT); 122 | 123 | jvalue retVal; 124 | 125 | switch (retTypeId) { 126 | case 0: 127 | retVal.i = env->CallNonvirtualIntMethodA(thiz, clazz, mid, argValues); 128 | break; 129 | case 1: 130 | retVal.j = env->CallNonvirtualLongMethodA(thiz, clazz, mid, argValues); 131 | break; 132 | case 2: 133 | retVal.s = env->CallNonvirtualShortMethodA(thiz, clazz, mid, argValues); 134 | break; 135 | case 3: 136 | retVal.b = env->CallNonvirtualByteMethodA(thiz, clazz, mid, argValues); 137 | break; 138 | case 4: 139 | retVal.c = env->CallNonvirtualCharMethodA(thiz, clazz, mid, argValues); 140 | break; 141 | case 5: 142 | retVal.z = env->CallNonvirtualBooleanMethodA(thiz, clazz, mid, argValues); 143 | break; 144 | case 6: 145 | retVal.f = env->CallNonvirtualFloatMethodA(thiz, clazz, mid, argValues); 146 | break; 147 | case 7: 148 | retVal.d = env->CallNonvirtualDoubleMethodA(thiz, clazz, mid, argValues); 149 | break; 150 | case 8: 151 | retVal.l = env->CallNonvirtualObjectMethodA(thiz, clazz, mid, argValues); 152 | break; 153 | case 9: 154 | env->CallNonvirtualIntMethodA(thiz, clazz, mid, argValues); 155 | break; 156 | } 157 | 158 | if (auto exception = env->ExceptionOccurred(); exception) { 159 | env->ExceptionClear(); 160 | auto throwable = reinterpret_cast(env->NewObject(invocationTargetExceptionClass, invocationTargetExceptionConstructor, exception)); 161 | env->Throw(throwable); 162 | return nullptr; 163 | } 164 | 165 | jobject ret; 166 | 167 | if (retTypeId >= 0 && retTypeId < 8) { 168 | ret = env->CallStaticObjectMethodA(primitiveClasses[retTypeId], valueOfMethods[retTypeId], &retVal); 169 | } else if (retTypeId == 8) { 170 | ret = retVal.l; 171 | } else { 172 | ret = nullptr; 173 | } 174 | 175 | return ret; 176 | } 177 | } 178 | 179 | extern "C" 180 | JNIEXPORT jobject JNICALL 181 | Java_io_github_a13e300_tools_NativeUtils_invokeNonVirtualInternal( 182 | JNIEnv *env, jclass, 183 | jobject method, jclass clazz, 184 | jbyteArray types, jobject thiz, 185 | jobjectArray argArr 186 | ) { 187 | return Reflection::invokeNonVirtualMethod(env, method, clazz, types, thiz, argArr); 188 | } 189 | 190 | extern "C" 191 | JNIEXPORT jobject JNICALL 192 | Java_io_github_a13e300_tools_NativeUtils_getCLInit(JNIEnv *env, jclass, jclass target) { 193 | auto method = env->GetStaticMethodID(target, "", "()V"); 194 | if (method == nullptr || env->ExceptionOccurred()) { 195 | env->ExceptionClear(); 196 | return nullptr; 197 | } else { 198 | return env->ToReflectedMethod(target, method, true); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /app/src/main/cpp/reflection.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace Reflection { 5 | bool Init(JNIEnv *env); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/cpp/stethox.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "classloader.h" 4 | 5 | #include "elf_parser.hpp" 6 | #include "maps_scan.hpp" 7 | #include "logging.h" 8 | 9 | #include "art.hpp" 10 | #include "reflection.hpp" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | static bool initArt(JNIEnv *env) { 18 | elf_parser::Elf art{}; 19 | uintptr_t art_base; 20 | std::string art_path; 21 | maps_scan::MapInfo::ForEach([&](auto &map) -> bool { 22 | if (map.path.ends_with("/libart.so") && map.offset == 0) { 23 | art_base = map.start; 24 | art_path = map.path; 25 | return false; 26 | } 27 | return true; 28 | }); 29 | if (!art.InitFromFile(art_path, art_base, true)) { 30 | LOGE("init art"); 31 | return false; 32 | } 33 | bool success = true; 34 | if (!art::Init(env, art)) { 35 | LOGE("ArtDebugging init failed"); 36 | success = false; 37 | } 38 | success &= InitClassLoaders(art); 39 | return success; 40 | } 41 | 42 | extern "C" 43 | JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { 44 | JNIEnv *env; 45 | vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_4); 46 | bool success = true; 47 | success &= Reflection::Init(env); 48 | success &= initArt(env); 49 | return success ? JNI_VERSION_1_4 : JNI_ERR; 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/cpp/utils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "utils.h" 5 | 6 | // https://stackoverflow.com/a/68051325 7 | bool is_pointer_valid(void *p) { 8 | /* get the page size */ 9 | size_t page_size = sysconf(_SC_PAGESIZE); 10 | /* find the address of the page that contains p */ 11 | void *base = (void *)((((size_t)p) / page_size) * page_size); 12 | /* call msync, if it returns non-zero, return false */ 13 | int ret = msync(base, page_size, MS_ASYNC) != -1; 14 | return ret ? ret : errno != ENOMEM; 15 | } -------------------------------------------------------------------------------- /app/src/main/cpp/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | inline auto GetAndroidApiLevel() { 7 | static auto kApiLevel = []() { 8 | std::array prop_value; 9 | __system_property_get("ro.build.version.sdk", prop_value.data()); 10 | int base = atoi(prop_value.data()); 11 | __system_property_get("ro.build.version.preview_sdk", prop_value.data()); 12 | return base + atoi(prop_value.data()); 13 | }(); 14 | return kApiLevel; 15 | } 16 | 17 | bool is_pointer_valid(void *p); 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/DexKitWrapper.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import org.luckypray.dexkit.DexKitBridge; 6 | 7 | import java.util.Objects; 8 | 9 | public class DexKitWrapper { 10 | 11 | static { 12 | try { 13 | System.loadLibrary("dexkit"); 14 | } catch (Throwable t) { 15 | Logger.e("failed to load dexkit", t); 16 | } 17 | } 18 | 19 | @NonNull 20 | public static DexKitBridge create(String apkPath) { 21 | return Objects.requireNonNull(DexKitBridge.create(apkPath), "dexkit failed to create"); 22 | } 23 | 24 | @NonNull 25 | public static DexKitBridge create(ClassLoader cl) { 26 | return Objects.requireNonNull(DexKitBridge.create(cl, true), "dexkit failed to create"); 27 | } 28 | 29 | @NonNull 30 | public static DexKitBridge create(ClassLoader cl, boolean useMemory) { 31 | return Objects.requireNonNull(DexKitBridge.create(cl, useMemory), "dexkit failed to create"); 32 | } 33 | 34 | @NonNull 35 | public static DexKitBridge create(byte[][] dex) { 36 | return Objects.requireNonNull(DexKitBridge.create(dex), "dexkit failed to create"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/DexUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools; 2 | 3 | 4 | import android.util.Log; 5 | 6 | import com.android.dx.DexMaker; 7 | import com.android.dx.TypeId; 8 | 9 | import java.lang.reflect.Modifier; 10 | import java.nio.ByteBuffer; 11 | 12 | import dalvik.system.InMemoryDexClassLoader; 13 | 14 | public class DexUtils { 15 | public static void log(String msg) { 16 | Logger.d("DexUtils log: " + msg); 17 | } 18 | public static Object make(ClassLoader classLoader, String name, String superName) { 19 | var maker = new DexMaker(); 20 | var superType = TypeId.get(superName); // LNeverCall$AB; 21 | var myType = TypeId.get(name); // LMyAB; 22 | maker.declare(myType, "Generated", Modifier.PUBLIC, superType); 23 | var printMethod = myType.getMethod(TypeId.VOID, "print", TypeId.STRING); 24 | var cstr = maker.declare(myType.getConstructor(), Modifier.PUBLIC); 25 | var superCstr = superType.getConstructor(); 26 | cstr.invokeDirect(superCstr, null, cstr.getThis(myType)); 27 | cstr.returnVoid(); 28 | var code = maker.declare(printMethod, Modifier.PUBLIC); 29 | var tag = code.newLocal(TypeId.STRING); 30 | code.loadConstant(tag, "DexUtils"); 31 | var p0 = code.getParameter(0, TypeId.STRING); 32 | var logI = TypeId.get(Log.class).getMethod(TypeId.INT, "i", TypeId.STRING, TypeId.STRING); 33 | var myLog = TypeId.get(DexUtils.class).getMethod(TypeId.VOID, "log", TypeId.STRING); 34 | code.invokeStatic(logI, null, tag, p0); 35 | code.invokeStatic(myLog, null, p0); 36 | code.returnVoid(); 37 | var dex = maker.generate(); 38 | try { 39 | if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) return null; 40 | var loader = new InMemoryDexClassLoader(ByteBuffer.wrap(dex), new HybridClassLoader(classLoader)); 41 | var clazz = loader.loadClass("MyAB"); 42 | Logger.d("class loader=" + clazz.getClassLoader()); 43 | var superClass = classLoader.loadClass("NeverCall$AB"); 44 | var inst = clazz.newInstance(); 45 | var m = superClass.getDeclaredMethod("call", superClass, String.class); 46 | m.setAccessible(true); 47 | m.invoke(null, inst, "test"); 48 | return inst; 49 | } catch (Throwable t) { 50 | Logger.e("dexutils load and call", t); 51 | } 52 | return null; 53 | } 54 | 55 | static class HybridClassLoader extends ClassLoader { 56 | private final ClassLoader mBase; 57 | public HybridClassLoader(ClassLoader parent) { 58 | mBase = parent; 59 | } 60 | 61 | @Override 62 | protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { 63 | if (name.startsWith("io.github.a13e300.tools")) { 64 | return getClass().getClassLoader().loadClass(name); 65 | } 66 | try { 67 | return mBase.loadClass(name); 68 | } catch (ClassNotFoundException e) { 69 | return super.loadClass(name, resolve); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/InitMethods.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools; 2 | 3 | import java.util.List; 4 | 5 | public class InitMethods { 6 | public static final String INIT_ATTACH = "attach"; 7 | public static final String INIT_ATTACH_BASE_CONTEXT = "attach_base_context"; 8 | public static final String INIT_MAKE_APPLICATION = "make_application"; 9 | public static final String INIT_DEFAULT = INIT_MAKE_APPLICATION; 10 | 11 | public static List METHODS = List.of(INIT_ATTACH, INIT_ATTACH_BASE_CONTEXT, INIT_MAKE_APPLICATION); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/Logger.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools; 2 | 3 | import android.util.Log; 4 | 5 | public class Logger { 6 | private static final String TAG = "StethoX"; 7 | public static void d(String msg) { 8 | Log.d(TAG, msg); 9 | } 10 | 11 | public static void e(String msg) { 12 | Log.e(TAG, msg); 13 | } 14 | 15 | public static void e(String msg, Throwable t) { 16 | Log.e(TAG, msg, t); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/NativeUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools; 2 | 3 | import android.os.Build; 4 | import android.os.Debug; 5 | import android.util.Log; 6 | 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.lang.reflect.Member; 9 | import java.lang.reflect.Method; 10 | import java.lang.reflect.Modifier; 11 | 12 | public class NativeUtils { 13 | static { 14 | try { 15 | System.loadLibrary("stethox"); 16 | } catch (Throwable t) { 17 | Log.e("StethoX", "failed to load native library", t); 18 | } 19 | } 20 | 21 | private static boolean jvmtiAttached = false; 22 | 23 | public static native ClassLoader[] getClassLoaders(); 24 | 25 | public static native ClassLoader[] getClassLoaders2(); 26 | 27 | private static native void nativeAllowDebugging(); 28 | private static native int nativeSetJavaDebug(boolean allow); 29 | private static native void nativeRestoreJavaDebug(int orig); 30 | 31 | private static void ensureJvmTi() { 32 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { 33 | throw new UnsupportedOperationException("JVMTI is unsupported!"); 34 | } 35 | synchronized (NativeUtils.class) { 36 | if (jvmtiAttached) return; 37 | nativeAllowDebugging(); 38 | var orig = nativeSetJavaDebug(true); 39 | try { 40 | Debug.attachJvmtiAgent("libstethox.so", "", NativeUtils.class.getClassLoader()); 41 | } catch (Throwable t) { 42 | Logger.e("load jvmti", t); 43 | throw new UnsupportedOperationException("load failed", t); 44 | } finally { 45 | nativeRestoreJavaDebug(orig); 46 | } 47 | jvmtiAttached = true; 48 | } 49 | } 50 | 51 | public static Object[] getObjects(Class clazz, boolean child) { 52 | ensureJvmTi(); 53 | return nativeGetObjects(clazz, child); 54 | } 55 | 56 | public static Class[] getAssignableClasses(Class clazz, ClassLoader loader) { 57 | ensureJvmTi(); 58 | return nativeGetAssignableClasses(clazz, loader); 59 | } 60 | 61 | private static native Object[] nativeGetObjects(Class clazz, boolean child); 62 | 63 | private static native Class[] nativeGetAssignableClasses(Class clazz, ClassLoader loader); 64 | 65 | private static native void dumpThread(); 66 | 67 | public static class FrameVar { 68 | String name; 69 | String sig; 70 | int slot; 71 | 72 | Object lvalue; 73 | long jvalue; 74 | int ivalue; 75 | short svalue; 76 | char cvalue; 77 | byte bvalue; 78 | boolean zvalue; 79 | float fvalue; 80 | double dvalue; 81 | 82 | public FrameVar() {} 83 | } 84 | 85 | private static native FrameVar[] getFrameVarsNative(int nframe); 86 | 87 | public static FrameVar[] getFrameVars(int nframe) { 88 | ensureJvmTi(); 89 | return getFrameVarsNative(nframe); 90 | } 91 | 92 | public static native Object[] getGlobalRefs(Class clazz); 93 | 94 | public static native Member getCLInit(Class clazz); 95 | 96 | private static native Object invokeNonVirtualInternal(Method method, Class target, byte[] types, Object thiz, Object[] args) throws InvocationTargetException; 97 | 98 | private static byte typeId(Class type) { 99 | if (type == int.class) { 100 | return 0; 101 | } else if (type == long.class) { 102 | return 1; 103 | } else if (type == short.class) { 104 | return 2; 105 | } else if (type == byte.class) { 106 | return 3; 107 | } else if (type == char.class) { 108 | return 4; 109 | } else if (type == boolean.class) { 110 | return 5; 111 | } else if (type == float.class) { 112 | return 6; 113 | } else if (type == double.class) { 114 | return 7; 115 | } else if (type == Void.class) { 116 | return 9; 117 | } 118 | return 8; 119 | } 120 | 121 | public static Object invokeNonVirtual(Method method, Object thiz, Object ...args) throws InvocationTargetException { 122 | if (thiz == null) throw new NullPointerException("this == null"); 123 | var modifier = method.getModifiers(); 124 | if (Modifier.isStatic(modifier)) throw new IllegalArgumentException("expected instance method, got " + method); 125 | if (Modifier.isAbstract(modifier)) throw new IllegalArgumentException("cannot invoke abstract method " + method); 126 | var clazz = method.getDeclaringClass(); 127 | if (!clazz.isInstance(thiz)) throw new IllegalArgumentException(thiz + " is not an instance of class " + clazz); 128 | 129 | Class retType = method.getReturnType(); 130 | Class[] pTypes = method.getParameterTypes(); 131 | byte[] types; 132 | types = new byte[pTypes.length + 1]; 133 | types[0] = typeId(retType); 134 | for (int i = 0; i < pTypes.length; i++) { 135 | types[i + 1] = typeId(pTypes[i]); 136 | } 137 | return invokeNonVirtualInternal(method, clazz, types, thiz, args); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/StethoxAppInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.ActivityThread; 5 | import android.app.Application; 6 | import android.app.IActivityManager; 7 | import android.app.IApplicationThread; 8 | import android.app.Instrumentation; 9 | import android.content.Context; 10 | import android.content.ContextWrapper; 11 | import android.net.Uri; 12 | import android.os.Handler; 13 | import android.os.Looper; 14 | import android.os.ServiceManager; 15 | import android.os.SystemProperties; 16 | 17 | import com.facebook.stetho.Stetho; 18 | import com.facebook.stetho.rhino.JsRuntimeReplFactoryBuilder; 19 | 20 | import org.mozilla.javascript.BaseFunction; 21 | import org.mozilla.javascript.Scriptable; 22 | import org.mozilla.javascript.ScriptableObject; 23 | 24 | import java.io.File; 25 | import java.io.FileInputStream; 26 | import java.lang.reflect.InvocationTargetException; 27 | import java.lang.reflect.Method; 28 | import java.nio.charset.StandardCharsets; 29 | import java.util.Arrays; 30 | 31 | import de.robv.android.xposed.IXposedHookZygoteInit; 32 | import de.robv.android.xposed.XC_MethodHook; 33 | import de.robv.android.xposed.XposedBridge; 34 | import de.robv.android.xposed.XposedHelpers; 35 | import io.github.a13e300.tools.objects.FindStackTraceFunction; 36 | import io.github.a13e300.tools.objects.GetStackTraceFunction; 37 | import io.github.a13e300.tools.objects.HookFunction; 38 | import io.github.a13e300.tools.objects.HookParam; 39 | import io.github.a13e300.tools.objects.JArrayFunction; 40 | import io.github.a13e300.tools.objects.OkHttpInterceptorObject; 41 | import io.github.a13e300.tools.objects.PrintStackTraceFunction; 42 | import io.github.a13e300.tools.objects.RunOnHandlerFunction; 43 | import io.github.a13e300.tools.objects.UnhookFunction; 44 | 45 | public class StethoxAppInterceptor implements IXposedHookZygoteInit { 46 | 47 | private boolean initialized = false; 48 | private boolean mWaiting = false; 49 | public static ClassLoader mClassLoader = null; 50 | 51 | private synchronized void cont(ScriptableObject ignore) { 52 | if (mWaiting) { 53 | notify(); 54 | Stetho.setSuspend(false); 55 | } 56 | } 57 | 58 | private synchronized void initializeStetho(Context context) throws InterruptedException { 59 | if (initialized) return; 60 | var packageName = context.getPackageName(); 61 | var a = (IApplicationThread) ActivityThread.currentActivityThread().getApplicationThread(); 62 | var am = IActivityManager.Stub.asInterface(ServiceManager.getService("activity")); 63 | if (BuildConfig.APPLICATION_ID.equals(packageName) || BuildConfig.APPLICATION_ID.equals(Utils.getProcessName())) { 64 | initialized = true; 65 | return; 66 | } 67 | 68 | var processName = Utils.getProcessName(); 69 | Logger.d("Install Stetho: " + packageName + " proc=" + processName); 70 | var suspendRequested = false; 71 | 72 | try { 73 | var packages = SystemProperties.get("debug.stethox.suspend"); 74 | if (Arrays.asList(packages.split(",")).contains(processName)) { 75 | suspendRequested = true; 76 | Logger.d("suspend requested b prop"); 77 | } 78 | } catch (Throwable t) { 79 | Logger.e("get suspend prop", t); 80 | } 81 | 82 | if (!suspendRequested) { 83 | try { 84 | var file = new File("/data/local/tmp/stethox_suspend"); 85 | if (file.canRead()) { 86 | var bytes = new byte[(int) file.length()]; 87 | try (var is = new FileInputStream(file)) { 88 | is.read(bytes); 89 | } 90 | var str = new String(bytes, StandardCharsets.UTF_8); 91 | if (Arrays.asList(str.split("\n")).contains(processName)) { 92 | suspendRequested = true; 93 | Logger.d("suspend requested by file"); 94 | } 95 | } 96 | } catch (Throwable t) { 97 | Logger.e("get suspend file", t); 98 | } 99 | } 100 | 101 | if (!suspendRequested) { 102 | try { 103 | var r = context.getContentResolver().call( 104 | Uri.parse("content://io.github.a13e300.tools.stethox.suspend"), 105 | "process_started", processName, null 106 | ); 107 | if (r != null && r.getBoolean("suspend", false)) { 108 | suspendRequested = true; 109 | Logger.d("suspend requested by provider"); 110 | } 111 | } catch (IllegalArgumentException e) { 112 | Logger.e("failed to find suspend provider, please ensure module process without restriction", e); 113 | } 114 | } 115 | 116 | if (suspendRequested) { 117 | mWaiting = true; 118 | Stetho.setSuspend(true); 119 | } 120 | 121 | try { 122 | mClassLoader = context.getClassLoader(); 123 | } catch (Throwable t) { 124 | Logger.e("failed to get classloader for context", t); 125 | } 126 | Stetho.initialize(Stetho.newInitializerBuilder(context) 127 | .enableWebKitInspector( 128 | () -> new Stetho.DefaultInspectorModulesBuilder(context).runtimeRepl( 129 | new JsRuntimeReplFactoryBuilder(context) 130 | .addFunction("getStackTrace", new GetStackTraceFunction()) 131 | .addFunction("printStackTrace", new PrintStackTraceFunction()) 132 | .addFunction("findStackTrace", new FindStackTraceFunction()) 133 | .addFunction("runOnUiThread", new RunOnHandlerFunction(new Handler(Looper.getMainLooper()))) 134 | .addFunction("runOnHandler", new RunOnHandlerFunction()) 135 | .addFunction("JArray", new JArrayFunction(null)) 136 | .addFunction("IntArray", new JArrayFunction(int.class)) 137 | .addFunction("LongArray", new JArrayFunction(long.class)) 138 | .addFunction("ByteArray", new JArrayFunction(byte.class)) 139 | .addFunction("BooleanArray", new JArrayFunction(boolean.class)) 140 | .addFunction("CharArray", new JArrayFunction(char.class)) 141 | .addFunction("FloatArray", new JArrayFunction(float.class)) 142 | .addFunction("DoubleArray", new JArrayFunction(double.class)) 143 | .addFunction("ShortArray", new JArrayFunction(short.class)) 144 | .addVariable("int", int.class) 145 | .addVariable("long", long.class) 146 | .addVariable("double", double.class) 147 | .addVariable("float", float.class) 148 | .addVariable("byte", byte.class) 149 | .addVariable("boolean", boolean.class) 150 | .addVariable("char", char.class) 151 | .addVariable("short", short.class) 152 | .importPackage("java.lang") 153 | .importClass(NativeUtils.class) 154 | .addVariable("xposed_bridge", XposedBridge.class) 155 | .addVariable("xposed_helper", XposedHelpers.class) 156 | .onInitScope((jsContext, scope) -> { 157 | try { 158 | scope.defineProperty("activities", null, Utils.class.getDeclaredMethod("getActivities", ScriptableObject.class), null, ScriptableObject.READONLY); 159 | scope.defineProperty("current", null, Utils.class.getDeclaredMethod("getCurrentActivity", ScriptableObject.class), null, ScriptableObject.READONLY); 160 | scope.defineProperty("fragments", null, Utils.class.getDeclaredMethod("getFragments", ScriptableObject.class), null, ScriptableObject.READONLY); 161 | ScriptableObject.defineClass(scope, HookFunction.class); 162 | ScriptableObject.defineClass(scope, UnhookFunction.class); 163 | ScriptableObject.defineClass(scope, HookParam.class); 164 | ScriptableObject.defineClass(scope, OkHttpInterceptorObject.class); 165 | scope.defineProperty("hook", new HookFunction(scope), ScriptableObject.DONTENUM | ScriptableObject.CONST); 166 | scope.defineProperty("okhttp3", new OkHttpInterceptorObject(scope), ScriptableObject.DONTENUM | ScriptableObject.CONST); 167 | synchronized (StethoxAppInterceptor.this) { 168 | if (mWaiting) { 169 | var method = StethoxAppInterceptor.class.getDeclaredMethod("cont", ScriptableObject.class); 170 | method.setAccessible(true); 171 | scope.defineProperty("cont", StethoxAppInterceptor.this, method, null, ScriptableObject.READONLY); 172 | } 173 | } 174 | jsContext.evaluateString(scope, 175 | "let st, pst, fst; st = pst = printStackTrace; fst = findStackTrace;" 176 | + "intv=java.lang.Integer.valueOf; longv=java.lang.Long.valueOf; " 177 | + "boolv=java.lang.Boolean.valueOf; bytev=java.lang.Byte.valueOf; " 178 | + "charv=java.lang.Char.valueOf; shortv=java.lang.Short.valueOf; " 179 | + "floatv=java.lang.Float.valueOf; doublev=java.lang.Double.valueOf; ", 180 | "initializer", 1, null); 181 | } catch (NoSuchMethodException | 182 | InvocationTargetException | 183 | IllegalAccessException | 184 | InstantiationException e) { 185 | throw new RuntimeException(e); 186 | } 187 | }) 188 | .onFinalize(scope -> { 189 | ((HookFunction) ScriptableObject.getProperty(scope, "hook")).clearHooks(); 190 | ((OkHttpInterceptorObject) ScriptableObject.getProperty(scope, "okhttp3")).stop(true); 191 | }) 192 | // .importClass(DexUtils.class) 193 | // .importClass(StethoOkHttp3ProxyInterceptor.class) 194 | // .importClass(MutableNameMap.class) 195 | // .importClass(DexKitWrapper.class) 196 | // .importPackage("org.luckypray.dexkit.query") 197 | // .importPackage("org.luckypray.dexkit.query.matchers") 198 | .addFunction("MYCL", new BaseFunction() { 199 | @Override 200 | public Object call(org.mozilla.javascript.Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { 201 | return StethoxAppInterceptor.class.getClassLoader(); 202 | } 203 | }) 204 | .build() 205 | ).finish() 206 | ).build() 207 | ); 208 | initialized = true; 209 | if (mWaiting) { 210 | try { 211 | am.showWaitingForDebugger(a, true); 212 | } catch (Throwable t) { 213 | Logger.e("failed to show wait for debugger", t); 214 | } 215 | wait(); 216 | mWaiting = false; 217 | try { 218 | am.showWaitingForDebugger(a, false); 219 | } catch (Throwable t) { 220 | Logger.e("failed to close wait for debugger", t); 221 | } 222 | } 223 | } 224 | 225 | @SuppressLint("PrivateApi") 226 | @Override 227 | public void initZygote(StartupParam startupParam) throws Throwable { 228 | XposedBridge.log("initZygote"); 229 | // initWithMakeApplication(); 230 | initWithAttach(); 231 | } 232 | 233 | private void initWithAttachBaseContext() { 234 | XposedHelpers.findAndHookMethod(ContextWrapper.class, "attachBaseContext", Context.class, new XC_MethodHook() { 235 | @Override 236 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 237 | if (!(param.thisObject instanceof Application)) return; 238 | var context = (Application) param.thisObject; 239 | XposedBridge.log("context " + context); 240 | if (context == null) return; 241 | initializeStetho(context); 242 | } 243 | }); 244 | } 245 | 246 | private void initWithAttach() { 247 | XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() { 248 | @Override 249 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 250 | XposedBridge.log("after attach " + param.thisObject); 251 | var context = (Application) param.thisObject; 252 | XposedBridge.log("context " + context); 253 | if (context == null) return; 254 | initializeStetho(context); 255 | } 256 | }); 257 | } 258 | 259 | private void initWithMakeApplication() { 260 | Method makeApplication; 261 | try { 262 | makeApplication = XposedHelpers.findMethodExact( 263 | "android.app.LoadedApk", 264 | ClassLoader.getSystemClassLoader(), 265 | "makeApplicationInner", 266 | boolean.class, 267 | Instrumentation.class 268 | ); 269 | } catch (NoSuchMethodError e) { 270 | makeApplication = XposedHelpers.findMethodExact( 271 | "android.app.LoadedApk", 272 | ClassLoader.getSystemClassLoader(), 273 | "makeApplication", 274 | boolean.class, 275 | Instrumentation.class 276 | ); 277 | } 278 | XposedBridge.hookMethod(makeApplication, 279 | new XC_MethodHook() { 280 | @Override 281 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 282 | var context = (Application) param.getResult(); 283 | XposedBridge.log("context " + context); 284 | if (context == null) return; 285 | initializeStetho(context); 286 | } 287 | } 288 | ); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/SuspendProvider.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.ContentProvider; 5 | import android.content.ContentValues; 6 | import android.content.Context; 7 | import android.content.SharedPreferences; 8 | import android.database.Cursor; 9 | import android.net.Uri; 10 | import android.os.Bundle; 11 | import android.util.Log; 12 | 13 | import java.util.Objects; 14 | 15 | public class SuspendProvider extends ContentProvider { 16 | 17 | private SharedPreferences mSp; 18 | public SuspendProvider() { 19 | } 20 | 21 | @Override 22 | public int delete(Uri uri, String selection, String[] selectionArgs) { 23 | throw new UnsupportedOperationException("Not yet implemented"); 24 | } 25 | 26 | @Override 27 | public String getType(Uri uri) { 28 | throw new UnsupportedOperationException("Not yet implemented"); 29 | } 30 | 31 | @Override 32 | public Uri insert(Uri uri, ContentValues values) { 33 | throw new UnsupportedOperationException("Not yet implemented"); 34 | } 35 | 36 | @Override 37 | public boolean onCreate() { 38 | var ctx = getContext(); 39 | if (ctx == null) return false; 40 | mSp = ctx.createDeviceProtectedStorageContext().getSharedPreferences("suspend", Context.MODE_PRIVATE); 41 | return true; 42 | } 43 | 44 | @Override 45 | public Cursor query(Uri uri, String[] projection, String selection, 46 | String[] selectionArgs, String sortOrder) { 47 | throw new UnsupportedOperationException("Not yet implemented"); 48 | } 49 | 50 | @Override 51 | public int update(Uri uri, ContentValues values, String selection, 52 | String[] selectionArgs) { 53 | throw new UnsupportedOperationException("Not yet implemented"); 54 | } 55 | 56 | private void checkPermission() { 57 | Objects.requireNonNull(getContext()).enforceCallingPermission("android.permission.INTERACT_ACROSS_USERS", "permission denied"); 58 | } 59 | 60 | @SuppressLint("ApplySharedPref") 61 | @Override 62 | public Bundle call(String method, String arg, Bundle extras) { 63 | Log.d("suspend", "called method " + method); 64 | var result = new Bundle(); 65 | if ("process_started".equals(method)) { 66 | if (arg == null) { 67 | throw new IllegalArgumentException("process name is none"); 68 | } 69 | var mode = mSp.getString(arg, null); 70 | boolean suspend = false; 71 | if ("oneshot".equals(mode)) { 72 | mSp.edit().remove(arg).commit(); 73 | suspend = true; 74 | } else if ("always".equals(mode)) { 75 | suspend = true; 76 | } 77 | result.putBoolean("suspend", suspend); 78 | } else if ("suspend".equals(method)) { 79 | checkPermission(); 80 | if (arg == null) { 81 | throw new IllegalArgumentException("process name is none"); 82 | } 83 | mSp.edit().putString(arg, "oneshot").commit(); 84 | } else if ("suspend_forever".equals(method)) { 85 | checkPermission(); 86 | if (arg == null) { 87 | throw new IllegalArgumentException("process name is none"); 88 | } 89 | mSp.edit().putString(arg, "always").commit(); 90 | } else if ("clear".equals(method)) { 91 | checkPermission(); 92 | if (arg == null) { 93 | mSp.edit().clear().commit(); 94 | } else { 95 | mSp.edit().remove(arg).commit(); 96 | } 97 | } else if ("list".equals(method)) { 98 | checkPermission(); 99 | mSp.getAll().forEach((k, v) -> { result.putString(k, (String) v); }); 100 | } 101 | return result; 102 | } 103 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/Utils.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools; 2 | 3 | import android.app.Activity; 4 | import android.app.ActivityThread; 5 | 6 | import org.mozilla.javascript.Context; 7 | import org.mozilla.javascript.ScriptableObject; 8 | 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.lang.reflect.Method; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | import de.robv.android.xposed.XposedHelpers; 16 | import io.github.a13e300.tools.node.Node; 17 | 18 | public class Utils { 19 | public static ActivityThread getActivityThread() { 20 | return ActivityThread.currentActivityThread(); 21 | } 22 | 23 | public static String getProcessName() { 24 | try { 25 | return getActivityThread().getProcessName(); 26 | } catch (NullPointerException e) { 27 | return null; 28 | } 29 | } 30 | 31 | public static Activity[] getActivities(ScriptableObject ignore) { 32 | var activities = (Map) XposedHelpers.getObjectField(getActivityThread(), "mActivities"); 33 | return activities.values().stream().map(v -> (Activity) XposedHelpers.getObjectField(v, "activity")).toArray(Activity[]::new); 34 | } 35 | 36 | public static Activity getCurrentActivity(ScriptableObject ignore) { 37 | var activities = (Map) XposedHelpers.getObjectField(getActivityThread(), "mActivities"); 38 | var acr = activities.values().stream().filter(v -> 39 | !XposedHelpers.getBooleanField(v, "paused") 40 | ).findFirst(); 41 | return acr.map(o -> (Activity) XposedHelpers.getObjectField(o, "activity")).orElse(null); 42 | } 43 | 44 | public static Object getSupportFragmentManager(ScriptableObject ignore) { 45 | var currentActivity = getCurrentActivity(ignore); 46 | if (currentActivity == null) return null; 47 | 48 | var clazz = XposedHelpers.findClass("androidx.fragment.app.FragmentActivity", currentActivity.getClassLoader()); 49 | Method method = XposedHelpers.findMethodExactIfExists(clazz, "getSupportFragmentManager"); 50 | try { 51 | return method.invoke(currentActivity); 52 | } catch (InvocationTargetException | IllegalAccessException e) { 53 | return null; 54 | } 55 | } 56 | 57 | public static ArrayList> getFragments(ScriptableObject ignore) { 58 | Object manager = getSupportFragmentManager(ignore); 59 | if (manager == null) return null; 60 | 61 | var rootNodes = new ArrayList>(); 62 | var nodeStack = new ArrayList>>(); 63 | nodeStack.add(rootNodes); 64 | 65 | var fragmentMgrStack = new ArrayList<>(); 66 | fragmentMgrStack.add(manager); 67 | 68 | var loader = manager.getClass().getClassLoader(); 69 | var fragmentMgrClazz = XposedHelpers.findClass("androidx.fragment.app.FragmentManager", loader); 70 | var fragmentClazz = XposedHelpers.findClass("androidx.fragment.app.Fragment", loader); 71 | 72 | while (!fragmentMgrStack.isEmpty()) { 73 | Object currentManager = fragmentMgrStack.remove(fragmentMgrStack.size() - 1); 74 | List> currentNode = nodeStack.remove(nodeStack.size() - 1); 75 | // Object currentManager = fragmentMgrStack.remove(0); 76 | // List> currentNode = nodeStack.remove(0); 77 | 78 | try { 79 | Method method = XposedHelpers.findMethodExactIfExists(fragmentMgrClazz, "getFragments"); 80 | var fragments = (List) method.invoke(currentManager); 81 | 82 | for (int i = 0; i < fragments.size(); i++) { 83 | Object fragment = fragments.get(i); 84 | 85 | var childNode = new Node<>(); 86 | childNode.index = i; 87 | childNode.value = fragment; 88 | childNode.children = new ArrayList<>(); 89 | currentNode.add(childNode); 90 | 91 | Method childMethod = XposedHelpers.findMethodExactIfExists(fragmentClazz, "getChildFragmentManager"); 92 | Object childManager = childMethod.invoke(fragment); 93 | if (childManager == null) continue; 94 | 95 | fragmentMgrStack.add(childManager); 96 | nodeStack.add(childNode.children); 97 | } 98 | 99 | } catch (InvocationTargetException | IllegalAccessException e) { 100 | // Ignore: the exception and continue with the next fragment 101 | } 102 | } 103 | 104 | return rootNodes; 105 | } 106 | 107 | public static String getStackTrace(boolean hide) { 108 | var sb = new StringBuilder(); 109 | var st = Thread.currentThread().getStackTrace(); 110 | for (int i = 3; i < st.length; i++) { 111 | if (!hide && st[i].getClassName().startsWith("org.mozilla.javascript")) continue; 112 | sb.append(st[i].toString()).append("\n"); 113 | } 114 | return sb.toString(); 115 | } 116 | 117 | public static Context enterJsContext() { 118 | final Context jsContext = Context.enter(); 119 | 120 | // If we cause the context to throw a runtime exception from this point 121 | // we need to make sure that exit the context. 122 | try { 123 | jsContext.setLanguageVersion(Context.VERSION_1_8); 124 | 125 | // We can't let Rhino to optimize the JS and to use a JIT because it would generate JVM bytecode 126 | // and android runs on DEX bytecode. Instead we need to go in interpreted mode. 127 | jsContext.setOptimizationLevel(-1); 128 | } catch (RuntimeException e) { 129 | // Something bad happened to the javascript context but it might still be usable. 130 | // The first thing to do is to exit the context and then propagate the error. 131 | Context.exit(); 132 | throw e; 133 | } 134 | 135 | return jsContext; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/deobfuscation/MutableNameMap.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.deobfuscation; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public class MutableNameMap implements NameMap { 9 | private static class ClassNode { 10 | String name; 11 | final Map, String>> methodMap = new HashMap<>(); 12 | final Map fieldMap = new HashMap<>(); 13 | } 14 | private final Map mClassMap = new HashMap<>(); 15 | @Override 16 | public String getClass(String name) { 17 | var node = mClassMap.get(name); 18 | if (node == null) return name; 19 | return node.name; 20 | } 21 | 22 | public void putClass(String name, String obfName) { 23 | mClassMap.computeIfAbsent(name, k -> new ClassNode()).name = obfName; 24 | } 25 | 26 | @Override 27 | public String getMethod(String className, String methodName, String... argTypes) { 28 | var node = mClassMap.get(className); 29 | if (node == null) return methodName; 30 | var mm = node.methodMap.get(methodName); 31 | if (mm == null) return methodName; 32 | var n = mm.get(Arrays.asList(argTypes)); 33 | if (n == null) return methodName; 34 | return n; 35 | } 36 | 37 | public void putMethod(String className, String methodName, String obfName, String... argTypes) { 38 | mClassMap.computeIfAbsent(className, k -> new ClassNode()) 39 | .methodMap.computeIfAbsent(methodName, k -> new HashMap<>()) 40 | .put(Arrays.asList(argTypes), obfName); 41 | } 42 | 43 | @Override 44 | public String getField(String className, String fieldName) { 45 | var node = mClassMap.get(className); 46 | if (node == null) return fieldName; 47 | var n = node.fieldMap.get(fieldName); 48 | if (n == null) return fieldName; 49 | return n; 50 | } 51 | 52 | public void putField(String className, String fieldName, String obfName) { 53 | mClassMap.computeIfAbsent(className, k -> new ClassNode()) 54 | .fieldMap.put(fieldName, obfName); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/deobfuscation/NameMap.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.deobfuscation; 2 | 3 | public interface NameMap { 4 | NameMap sDefaultMap = new NameMap() { 5 | @Override 6 | public String getClass(String name) { 7 | return name; 8 | } 9 | 10 | @Override 11 | public String getMethod(String className, String methodName, String... argTypes) { 12 | return methodName; 13 | } 14 | 15 | @Override 16 | public String getField(String className, String fieldName) { 17 | return fieldName; 18 | } 19 | }; 20 | String getClass(String name); 21 | String getMethod(String className, String methodName, String... argTypes); 22 | String getField(String className, String fieldName); 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/network/httpurl/HttpURLConnectionInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.network.httpurl; 2 | 3 | import com.facebook.stetho.urlconnection.StethoURLConnectionManager; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.net.HttpURLConnection; 10 | import java.net.ProtocolException; 11 | import java.net.URL; 12 | import java.security.Permission; 13 | 14 | import de.robv.android.xposed.XC_MethodHook; 15 | import de.robv.android.xposed.XposedHelpers; 16 | 17 | public class HttpURLConnectionInterceptor { 18 | private static XC_MethodHook.Unhook sUnhook; 19 | public static synchronized void enable() { 20 | sUnhook = XposedHelpers.findAndHookMethod(URL.class, "openConnection", new XC_MethodHook() { 21 | @Override 22 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 23 | var connection = param.getResult(); 24 | if (!(connection instanceof HttpURLConnection)) return; 25 | 26 | } 27 | }); 28 | } 29 | 30 | public static synchronized void disable() { 31 | if (sUnhook != null) { 32 | sUnhook.unhook(); 33 | sUnhook = null; 34 | } 35 | } 36 | 37 | private class HttpURLConnectionWrapper extends HttpURLConnection { 38 | private final HttpURLConnection mWrapped; 39 | private final StethoURLConnectionManager mManager; 40 | private ByteArrayOutputStream mOutputStream; 41 | HttpURLConnectionWrapper(HttpURLConnection wrapped) { 42 | super(wrapped.getURL()); 43 | mWrapped = wrapped; 44 | mManager = new StethoURLConnectionManager(null); 45 | } 46 | 47 | @Override 48 | public void connect() throws IOException { 49 | 50 | } 51 | 52 | @Override 53 | public boolean getDoOutput() { 54 | return mWrapped.getDoOutput(); 55 | } 56 | 57 | @Override 58 | public void setDoOutput(boolean dooutput) { 59 | mWrapped.setDoOutput(dooutput); 60 | } 61 | 62 | @Override 63 | public OutputStream getOutputStream() throws IOException { 64 | // TODO 65 | return mWrapped.getOutputStream(); 66 | } 67 | 68 | public String getHeaderFieldKey(int n) { 69 | return mWrapped.getHeaderFieldKey(n); 70 | } 71 | 72 | public void setFixedLengthStreamingMode(int contentLength) { 73 | mWrapped.setFixedLengthStreamingMode(contentLength); 74 | } 75 | 76 | public void setFixedLengthStreamingMode(long contentLength) { 77 | mWrapped.setFixedLengthStreamingMode(contentLength); 78 | } 79 | 80 | public void setChunkedStreamingMode(int chunklen) { 81 | mWrapped.setChunkedStreamingMode(chunklen); 82 | } 83 | 84 | public String getHeaderField(int n) { 85 | return mWrapped.getHeaderField(n); 86 | } 87 | 88 | public void setInstanceFollowRedirects(boolean followRedirects) { 89 | mWrapped.setInstanceFollowRedirects(followRedirects); 90 | } 91 | 92 | public boolean getInstanceFollowRedirects() { 93 | return mWrapped.getInstanceFollowRedirects(); 94 | } 95 | 96 | public void setRequestMethod(String method) throws ProtocolException { 97 | mWrapped.setRequestMethod(method); 98 | } 99 | 100 | public String getRequestMethod() { 101 | return mWrapped.getRequestMethod(); 102 | } 103 | 104 | public int getResponseCode() throws IOException { 105 | return mWrapped.getResponseCode(); 106 | } 107 | 108 | public String getResponseMessage() throws IOException { 109 | return mWrapped.getResponseMessage(); 110 | } 111 | 112 | public long getHeaderFieldDate(String name, long Default) { 113 | return mWrapped.getHeaderFieldDate(name, Default); 114 | } 115 | 116 | public void disconnect() { 117 | mWrapped.disconnect(); 118 | } 119 | 120 | public boolean usingProxy() { 121 | return mWrapped.usingProxy(); 122 | } 123 | 124 | public Permission getPermission() throws IOException { 125 | return mWrapped.getPermission(); 126 | } 127 | 128 | public InputStream getErrorStream() { 129 | return mWrapped.getErrorStream(); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/network/okhttp3/DefaultOkHttp3Helper.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.network.okhttp3; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.lang.reflect.Field; 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.lang.reflect.Method; 10 | 11 | import io.github.a13e300.tools.deobfuscation.NameMap; 12 | 13 | public class DefaultOkHttp3Helper implements OkHttp3Helper { 14 | private Method method_Interceptor$Chain_request; 15 | 16 | @Override 17 | public Object /*Request*/ Interceptor$Chain_request(Object chain) { 18 | try { 19 | return (Object) method_Interceptor$Chain_request.invoke(chain); 20 | } catch (Throwable t) { 21 | throw new RuntimeException(t); 22 | } 23 | } 24 | 25 | private Method method_Interceptor$Chain_proceed; 26 | 27 | @Override 28 | public Object /*Response*/ Interceptor$Chain_proceed(Object chain, Object /*Request*/ request) throws IOException { 29 | try { 30 | return (Object) method_Interceptor$Chain_proceed.invoke(chain, request); 31 | } catch (InvocationTargetException ex) { 32 | if (ex.getCause() instanceof IOException) { 33 | throw (IOException) ex.getCause(); 34 | } else throw new RuntimeException(ex); 35 | } catch (Throwable t) { 36 | throw new RuntimeException(t); 37 | } 38 | } 39 | 40 | private Method method_Interceptor$Chain_connection; 41 | 42 | @Override 43 | public Object /*Connection*/ Interceptor$Chain_connection(Object chain) { 44 | try { 45 | return (Object) method_Interceptor$Chain_connection.invoke(chain); 46 | } catch (Throwable t) { 47 | throw new RuntimeException(t); 48 | } 49 | } 50 | 51 | private Method method_Response_body; 52 | 53 | @Override 54 | public Object /*ResponseBody*/ Response_body(Object response) { 55 | try { 56 | return (Object) method_Response_body.invoke(response); 57 | } catch (Throwable t) { 58 | throw new RuntimeException(t); 59 | } 60 | } 61 | 62 | private Method method_Response_header; 63 | 64 | @Override 65 | public String Response_header(Object response, String name) { 66 | try { 67 | return (String) method_Response_header.invoke(response, name); 68 | } catch (Throwable t) { 69 | throw new RuntimeException(t); 70 | } 71 | } 72 | 73 | private Method method_Response_headers; 74 | 75 | @Override 76 | public Object /*Headers*/ Response_headers(Object response) { 77 | try { 78 | return (Object) method_Response_headers.invoke(response); 79 | } catch (Throwable t) { 80 | throw new RuntimeException(t); 81 | } 82 | } 83 | 84 | private Method method_Response_cacheResponse; 85 | 86 | @Override 87 | public Object /*Response*/ Response_cacheResponse(Object response) { 88 | try { 89 | return (Object) method_Response_cacheResponse.invoke(response); 90 | } catch (Throwable t) { 91 | throw new RuntimeException(t); 92 | } 93 | } 94 | 95 | private Method method_Response_newBuilder; 96 | 97 | @Override 98 | public Object /*Response$Builder*/ Response_newBuilder(Object response) { 99 | try { 100 | return (Object) method_Response_newBuilder.invoke(response); 101 | } catch (Throwable t) { 102 | throw new RuntimeException(t); 103 | } 104 | } 105 | 106 | private Method method_Response$Builder_build; 107 | 108 | @Override 109 | public Object /*Response*/ Response$Builder_build(Object builder) { 110 | try { 111 | return (Object) method_Response$Builder_build.invoke(builder); 112 | } catch (Throwable t) { 113 | throw new RuntimeException(t); 114 | } 115 | } 116 | 117 | private Method method_ResponseBody_create; 118 | 119 | @Override 120 | public /*static*/ Object /*ResponseBody*/ ResponseBody_create(Object /*MediaType*/ contentType, long contentLength, Object /*BufferedSource*/ content) { 121 | try { 122 | return (Object) method_ResponseBody_create.invoke(null, contentType, contentLength, content); 123 | } catch (Throwable t) { 124 | throw new RuntimeException(t); 125 | } 126 | } 127 | 128 | private Method method_Response$Builder_body; 129 | 130 | @Override 131 | public Object /*Response$Builder*/ Response$Builder_body(Object builder, Object /*ResponseBody*/ body) { 132 | try { 133 | return (Object) method_Response$Builder_body.invoke(builder, body); 134 | } catch (Throwable t) { 135 | throw new RuntimeException(t); 136 | } 137 | } 138 | 139 | private Method method_ResponseBody_contentType; 140 | 141 | @Override 142 | public Object /*MediaType*/ ResponseBody_contentType(Object body) { 143 | try { 144 | return (Object) method_ResponseBody_contentType.invoke(body); 145 | } catch (Throwable t) { 146 | throw new RuntimeException(t); 147 | } 148 | } 149 | 150 | private Method method_ResponseBody_byteStream; 151 | 152 | @Override 153 | public InputStream ResponseBody_byteStream(Object body) { 154 | try { 155 | return (InputStream) method_ResponseBody_byteStream.invoke(body); 156 | } catch (Throwable t) { 157 | throw new RuntimeException(t); 158 | } 159 | } 160 | 161 | private Method method_ResponseBody_contentLength; 162 | 163 | @Override 164 | public long ResponseBody_contentLength(Object body) { 165 | try { 166 | return (long) method_ResponseBody_contentLength.invoke(body); 167 | } catch (Throwable t) { 168 | throw new RuntimeException(t); 169 | } 170 | } 171 | 172 | private Method method_Request_url; 173 | 174 | @Override 175 | public Object /*HttpUrl*/ Request_url(Object request) { 176 | try { 177 | return (Object) method_Request_url.invoke(request); 178 | } catch (Throwable t) { 179 | throw new RuntimeException(t); 180 | } 181 | } 182 | 183 | private Method method_Request_method; 184 | 185 | @Override 186 | public String Request_method(Object request) { 187 | try { 188 | return (String) method_Request_method.invoke(request); 189 | } catch (Throwable t) { 190 | throw new RuntimeException(t); 191 | } 192 | } 193 | 194 | private Method method_Request_body; 195 | 196 | @Override 197 | public Object /*RequestBody*/ Request_body(Object request) { 198 | try { 199 | return (Object) method_Request_body.invoke(request); 200 | } catch (Throwable t) { 201 | throw new RuntimeException(t); 202 | } 203 | } 204 | 205 | private Method method_Request_headers; 206 | 207 | @Override 208 | public Object /*Headers*/ Request_headers(Object request) { 209 | try { 210 | return (Object) method_Request_headers.invoke(request); 211 | } catch (Throwable t) { 212 | throw new RuntimeException(t); 213 | } 214 | } 215 | 216 | private Method method_Request_header; 217 | 218 | @Override 219 | public String Request_header(Object request, String name) { 220 | try { 221 | return (String) method_Request_header.invoke(request, name); 222 | } catch (Throwable t) { 223 | throw new RuntimeException(t); 224 | } 225 | } 226 | 227 | private Method method_RequestBody_writeTo; 228 | 229 | @Override 230 | public void RequestBody_writeTo(Object body, Closeable /*BufferedSink*/ bufferedSink) throws IOException { 231 | try { 232 | method_RequestBody_writeTo.invoke(body, bufferedSink); 233 | } catch (InvocationTargetException ex) { 234 | if (ex.getCause() instanceof IOException) { 235 | throw (IOException) ex.getCause(); 236 | } else throw new RuntimeException(ex); 237 | } catch (Throwable t) { 238 | throw new RuntimeException(t); 239 | } 240 | } 241 | 242 | private Method method_Headers_size; 243 | 244 | @Override 245 | public int Headers_size(Object headers) { 246 | try { 247 | return (int) method_Headers_size.invoke(headers); 248 | } catch (Throwable t) { 249 | throw new RuntimeException(t); 250 | } 251 | } 252 | 253 | private Method method_Headers_name; 254 | 255 | @Override 256 | public String Headers_name(Object headers, int index) { 257 | try { 258 | return (String) method_Headers_name.invoke(headers, index); 259 | } catch (Throwable t) { 260 | throw new RuntimeException(t); 261 | } 262 | } 263 | 264 | private Method method_Headers_value; 265 | 266 | @Override 267 | public String Headers_value(Object headers, int index) { 268 | try { 269 | return (String) method_Headers_value.invoke(headers, index); 270 | } catch (Throwable t) { 271 | throw new RuntimeException(t); 272 | } 273 | } 274 | 275 | private Method method_Response_code; 276 | 277 | @Override 278 | public int Response_code(Object response) { 279 | try { 280 | return (int) method_Response_code.invoke(response); 281 | } catch (Throwable t) { 282 | throw new RuntimeException(t); 283 | } 284 | } 285 | 286 | private Method method_Response_message; 287 | 288 | @Override 289 | public String Response_message(Object response) { 290 | try { 291 | return (String) method_Response_message.invoke(response); 292 | } catch (Throwable t) { 293 | throw new RuntimeException(t); 294 | } 295 | } 296 | 297 | private Method method_Okio_sink; 298 | 299 | @Override 300 | public /*static*/ Closeable /*Sink*/ Okio_sink(OutputStream out) { 301 | try { 302 | return (Closeable) method_Okio_sink.invoke(null, out); 303 | } catch (Throwable t) { 304 | throw new RuntimeException(t); 305 | } 306 | } 307 | 308 | private Method method_Okio_buffer; 309 | 310 | @Override 311 | public /*static*/ Closeable /*BufferedSink*/ Okio_buffer(Closeable /*Sink*/ sink) { 312 | try { 313 | return (Closeable) method_Okio_buffer.invoke(null, sink); 314 | } catch (Throwable t) { 315 | throw new RuntimeException(t); 316 | } 317 | } 318 | 319 | private Method method_Okio_source; 320 | 321 | @Override 322 | public /*static*/ Closeable /*Source*/ Okio_source(InputStream in) { 323 | try { 324 | return (Closeable) method_Okio_source.invoke(null, in); 325 | } catch (Throwable t) { 326 | throw new RuntimeException(t); 327 | } 328 | } 329 | 330 | private Method method_Okio_1buffer; 331 | 332 | @Override 333 | public /*static*/ Closeable /*BufferedSource*/ Okio_1buffer(Closeable /*Source*/ source) { 334 | try { 335 | return (Closeable) method_Okio_1buffer.invoke(null, source); 336 | } catch (Throwable t) { 337 | throw new RuntimeException(t); 338 | } 339 | } 340 | 341 | private Class class_Interceptor; 342 | 343 | @Override 344 | public Class Interceptor() { 345 | return class_Interceptor; 346 | } 347 | 348 | private Class class_okhttp3_internal_http_RealInterceptorChain; 349 | 350 | @Override 351 | public Class okhttp3_internal_http_RealInterceptorChain() { 352 | return class_okhttp3_internal_http_RealInterceptorChain; 353 | } 354 | 355 | private Field field_okhttp3_internal_http_RealInterceptorChain_interceptors; 356 | 357 | @Override 358 | public Field okhttp3_internal_http_RealInterceptorChain_interceptors() { 359 | return field_okhttp3_internal_http_RealInterceptorChain_interceptors; 360 | } 361 | 362 | public DefaultOkHttp3Helper(ClassLoader classLoader, NameMap nameMap) { 363 | try { 364 | method_Interceptor$Chain_request = classLoader.loadClass(nameMap.getClass("okhttp3.Interceptor$Chain")).getDeclaredMethod(nameMap.getMethod("okhttp3.Interceptor$Chain", "request")); 365 | method_Interceptor$Chain_request.setAccessible(true); 366 | method_Interceptor$Chain_proceed = classLoader.loadClass(nameMap.getClass("okhttp3.Interceptor$Chain")).getDeclaredMethod(nameMap.getMethod("okhttp3.Interceptor$Chain", "proceed", "okhttp3.Request"), classLoader.loadClass("okhttp3.Request")); 367 | method_Interceptor$Chain_proceed.setAccessible(true); 368 | method_Interceptor$Chain_connection = classLoader.loadClass(nameMap.getClass("okhttp3.Interceptor$Chain")).getDeclaredMethod(nameMap.getMethod("okhttp3.Interceptor$Chain", "connection")); 369 | method_Interceptor$Chain_connection.setAccessible(true); 370 | method_Response_body = classLoader.loadClass(nameMap.getClass("okhttp3.Response")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response", "body")); 371 | method_Response_body.setAccessible(true); 372 | method_Response_header = classLoader.loadClass(nameMap.getClass("okhttp3.Response")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response", "header", "java.lang.String"), String.class); 373 | method_Response_header.setAccessible(true); 374 | method_Response_headers = classLoader.loadClass(nameMap.getClass("okhttp3.Response")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response", "headers")); 375 | method_Response_headers.setAccessible(true); 376 | method_Response_cacheResponse = classLoader.loadClass(nameMap.getClass("okhttp3.Response")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response", "cacheResponse")); 377 | method_Response_cacheResponse.setAccessible(true); 378 | method_Response_newBuilder = classLoader.loadClass(nameMap.getClass("okhttp3.Response")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response", "newBuilder")); 379 | method_Response_newBuilder.setAccessible(true); 380 | method_Response$Builder_build = classLoader.loadClass(nameMap.getClass("okhttp3.Response$Builder")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response$Builder", "build")); 381 | method_Response$Builder_build.setAccessible(true); 382 | method_ResponseBody_create = classLoader.loadClass(nameMap.getClass("okhttp3.ResponseBody")).getDeclaredMethod(nameMap.getMethod("okhttp3.ResponseBody", "create", "okhttp3.MediaType", "long", "okio.BufferedSource"), classLoader.loadClass("okhttp3.MediaType"), long.class, classLoader.loadClass("okio.BufferedSource")); 383 | method_ResponseBody_create.setAccessible(true); 384 | method_Response$Builder_body = classLoader.loadClass(nameMap.getClass("okhttp3.Response$Builder")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response$Builder", "body", "okhttp3.ResponseBody"), classLoader.loadClass("okhttp3.ResponseBody")); 385 | method_Response$Builder_body.setAccessible(true); 386 | method_ResponseBody_contentType = classLoader.loadClass(nameMap.getClass("okhttp3.ResponseBody")).getDeclaredMethod(nameMap.getMethod("okhttp3.ResponseBody", "contentType")); 387 | method_ResponseBody_contentType.setAccessible(true); 388 | method_ResponseBody_byteStream = classLoader.loadClass(nameMap.getClass("okhttp3.ResponseBody")).getDeclaredMethod(nameMap.getMethod("okhttp3.ResponseBody", "byteStream")); 389 | method_ResponseBody_byteStream.setAccessible(true); 390 | method_ResponseBody_contentLength = classLoader.loadClass(nameMap.getClass("okhttp3.ResponseBody")).getDeclaredMethod(nameMap.getMethod("okhttp3.ResponseBody", "contentLength")); 391 | method_ResponseBody_contentLength.setAccessible(true); 392 | method_Request_url = classLoader.loadClass(nameMap.getClass("okhttp3.Request")).getDeclaredMethod(nameMap.getMethod("okhttp3.Request", "url")); 393 | method_Request_url.setAccessible(true); 394 | method_Request_method = classLoader.loadClass(nameMap.getClass("okhttp3.Request")).getDeclaredMethod(nameMap.getMethod("okhttp3.Request", "method")); 395 | method_Request_method.setAccessible(true); 396 | method_Request_body = classLoader.loadClass(nameMap.getClass("okhttp3.Request")).getDeclaredMethod(nameMap.getMethod("okhttp3.Request", "body")); 397 | method_Request_body.setAccessible(true); 398 | method_Request_headers = classLoader.loadClass(nameMap.getClass("okhttp3.Request")).getDeclaredMethod(nameMap.getMethod("okhttp3.Request", "headers")); 399 | method_Request_headers.setAccessible(true); 400 | method_Request_header = classLoader.loadClass(nameMap.getClass("okhttp3.Request")).getDeclaredMethod(nameMap.getMethod("okhttp3.Request", "header", "java.lang.String"), String.class); 401 | method_Request_header.setAccessible(true); 402 | method_RequestBody_writeTo = classLoader.loadClass(nameMap.getClass("okhttp3.RequestBody")).getDeclaredMethod(nameMap.getMethod("okhttp3.RequestBody", "writeTo", "okio.BufferedSink"), classLoader.loadClass("okio.BufferedSink")); 403 | method_RequestBody_writeTo.setAccessible(true); 404 | method_Headers_size = classLoader.loadClass(nameMap.getClass("okhttp3.Headers")).getDeclaredMethod(nameMap.getMethod("okhttp3.Headers", "size")); 405 | method_Headers_size.setAccessible(true); 406 | method_Headers_name = classLoader.loadClass(nameMap.getClass("okhttp3.Headers")).getDeclaredMethod(nameMap.getMethod("okhttp3.Headers", "name", "int"), int.class); 407 | method_Headers_name.setAccessible(true); 408 | method_Headers_value = classLoader.loadClass(nameMap.getClass("okhttp3.Headers")).getDeclaredMethod(nameMap.getMethod("okhttp3.Headers", "value", "int"), int.class); 409 | method_Headers_value.setAccessible(true); 410 | method_Response_code = classLoader.loadClass(nameMap.getClass("okhttp3.Response")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response", "code")); 411 | method_Response_code.setAccessible(true); 412 | method_Response_message = classLoader.loadClass(nameMap.getClass("okhttp3.Response")).getDeclaredMethod(nameMap.getMethod("okhttp3.Response", "message")); 413 | method_Response_message.setAccessible(true); 414 | method_Okio_sink = classLoader.loadClass(nameMap.getClass("okio.Okio")).getDeclaredMethod(nameMap.getMethod("okio.Okio", "sink", "OutputStream"), OutputStream.class); 415 | method_Okio_sink.setAccessible(true); 416 | method_Okio_buffer = classLoader.loadClass(nameMap.getClass("okio.Okio")).getDeclaredMethod(nameMap.getMethod("okio.Okio", "buffer", "okio.Sink"), classLoader.loadClass("okio.Sink")); 417 | method_Okio_buffer.setAccessible(true); 418 | method_Okio_source = classLoader.loadClass(nameMap.getClass("okio.Okio")).getDeclaredMethod(nameMap.getMethod("okio.Okio", "source", "InputStream"), InputStream.class); 419 | method_Okio_source.setAccessible(true); 420 | method_Okio_1buffer = classLoader.loadClass(nameMap.getClass("okio.Okio")).getDeclaredMethod(nameMap.getMethod("okio.Okio", "buffer", "okio.Source"), classLoader.loadClass("okio.Source")); 421 | method_Okio_1buffer.setAccessible(true); 422 | class_Interceptor = classLoader.loadClass(nameMap.getClass("okhttp3.Interceptor")); 423 | class_okhttp3_internal_http_RealInterceptorChain = classLoader.loadClass(nameMap.getClass("okhttp3.internal.http.RealInterceptorChain")); 424 | field_okhttp3_internal_http_RealInterceptorChain_interceptors = classLoader.loadClass(nameMap.getClass("okhttp3.internal.http.RealInterceptorChain")).getDeclaredField(nameMap.getField("okhttp3.internal.http.RealInterceptorChain", "interceptors")); 425 | field_okhttp3_internal_http_RealInterceptorChain_interceptors.setAccessible(true); 426 | } catch (Throwable t) { 427 | throw new RuntimeException(t); 428 | } 429 | } 430 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/network/okhttp3/DeobfuscationUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.network.okhttp3 2 | 3 | import io.github.a13e300.tools.DexKitWrapper 4 | import io.github.a13e300.tools.deobfuscation.MutableNameMap 5 | import io.github.a13e300.tools.deobfuscation.NameMap 6 | 7 | fun deobfOkhttp3(cl: ClassLoader): NameMap { 8 | val map = MutableNameMap() 9 | DexKitWrapper.create(cl).use { 10 | val classRealInterceptorChain = it.findClass { 11 | matcher { 12 | usingStrings = listOf("must call proceed() exactly once", "returned a response with no body") 13 | } 14 | } 15 | require(classRealInterceptorChain.size == 1) { "require only one RealInterceptorChain, found ${classRealInterceptorChain.size}" } 16 | val fieldInterceptors = classRealInterceptorChain[0].getFields().findField { 17 | matcher { 18 | type = "java.util.List" 19 | } 20 | } 21 | require(fieldInterceptors.size == 1) { "require only one Interceptors field, found ${fieldInterceptors.size}" } 22 | map.putClass("okhttp3.internal.http.RealInterceptorChain", classRealInterceptorChain[0].className) 23 | map.putField("okhttp3.internal.http.RealInterceptorChain", "interceptors", fieldInterceptors[0].name) 24 | } 25 | return map 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/network/okhttp3/OkHttp3Helper.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.network.okhttp3; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.lang.reflect.Field; 8 | 9 | public interface OkHttp3Helper { 10 | Object /*Request*/ Interceptor$Chain_request(Object chain); 11 | Object /*Response*/ Interceptor$Chain_proceed(Object chain, Object /*Request*/ request) throws IOException; 12 | Object /*Connection*/ Interceptor$Chain_connection(Object chain); 13 | 14 | 15 | Object /*ResponseBody*/ Response_body(Object response); 16 | String Response_header(Object response, String name); 17 | Object /*Headers*/ Response_headers(Object response); 18 | Object /*Response*/ Response_cacheResponse(Object response); 19 | Object /*Response$Builder*/ Response_newBuilder(Object response); 20 | Object /*Response*/ Response$Builder_build(Object builder); 21 | /*static*/ Object /*ResponseBody*/ ResponseBody_create(Object /*MediaType*/ contentType, long contentLength, Object /*BufferedSource*/ content); 22 | Object /*Response$Builder*/ Response$Builder_body(Object builder, Object /*ResponseBody*/ body); 23 | 24 | 25 | Object /*MediaType*/ ResponseBody_contentType(Object body); 26 | InputStream ResponseBody_byteStream(Object body); 27 | long ResponseBody_contentLength(Object body); 28 | 29 | Object /*HttpUrl*/ Request_url(Object request); 30 | String Request_method(Object request); 31 | Object /*RequestBody*/ Request_body(Object request); 32 | Object /*Headers*/ Request_headers(Object request); 33 | String Request_header(Object request, String name); 34 | 35 | void RequestBody_writeTo(Object body, Closeable /*BufferedSink*/ bufferedSink) throws IOException; 36 | 37 | int Headers_size(Object headers); 38 | String Headers_name(Object headers, int index); 39 | String Headers_value(Object headers, int index); 40 | 41 | int Response_code(Object response); 42 | String Response_message(Object response); 43 | 44 | /*static*/ Closeable /*Sink*/ Okio_sink(OutputStream out); 45 | /*static*/ Closeable /*BufferedSink*/ Okio_buffer(Closeable /*Sink*/ sink); 46 | /*static*/ Closeable /*Source*/ Okio_source(InputStream in); 47 | /*static*/ Closeable /*BufferedSource*/ Okio_1buffer(Closeable /*Source*/ source); 48 | 49 | 50 | 51 | Class Interceptor(); 52 | 53 | Class okhttp3_internal_http_RealInterceptorChain(); 54 | Field okhttp3_internal_http_RealInterceptorChain_interceptors(); 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/network/okhttp3/StethoOkHttp3ProxyInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.network.okhttp3; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import com.facebook.stetho.inspector.network.DefaultResponseHandler; 6 | import com.facebook.stetho.inspector.network.NetworkEventReporter; 7 | import com.facebook.stetho.inspector.network.NetworkEventReporterImpl; 8 | import com.facebook.stetho.inspector.network.RequestBodyHelper; 9 | 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.io.OutputStream; 13 | import java.lang.reflect.Proxy; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.Set; 17 | 18 | import de.robv.android.xposed.XC_MethodHook; 19 | import de.robv.android.xposed.XposedBridge; 20 | import io.github.a13e300.tools.Logger; 21 | import io.github.a13e300.tools.deobfuscation.NameMap; 22 | 23 | 24 | 25 | public class StethoOkHttp3ProxyInterceptor { 26 | /*public static XC_MethodHook.Unhook startHookClientBuild(ClassLoader classLoader) { 27 | OkHttp3Helper helper = new DefaultOkHttp3Helper(classLoader, NameMap.sDefaultMap); 28 | var interceptor = new StethoOkHttp3ProxyInterceptor(helper); 29 | var proxy = Proxy.newProxyInstance(classLoader, new Class[]{helper.Interceptor()}, 30 | (o, method, objects) -> { 31 | if ("intercept".equals(method.getName())) 32 | return interceptor.intercept(objects[0]); 33 | try { 34 | return method.invoke(o, objects); 35 | } catch (InvocationTargetException e) { 36 | if (e.getCause() != null) throw e.getCause(); 37 | throw new RuntimeException(e); 38 | } 39 | } 40 | ); 41 | return XposedBridge.hookMethod(helper.OkHttpClient$Builder_getNetworkInterceptors$okhttp(), new XC_MethodHook() { 42 | @Override 43 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 44 | var list = (List) param.getResult(); 45 | if (list == null) list = new ArrayList(); 46 | if (list.contains(proxy)) return; 47 | list.add(proxy); 48 | param.setResult(list); 49 | } 50 | }); 51 | return null; 52 | }*/ 53 | public static Set start(ClassLoader classLoader) { 54 | try { 55 | return start(classLoader, false); 56 | } catch (Throwable t) { 57 | Logger.e("try deobfuscation for okhttp"); 58 | return start(classLoader, true); 59 | } 60 | } 61 | 62 | public static Set start(ClassLoader classLoader, boolean useDeObf) { 63 | if (useDeObf) { 64 | return start(classLoader, DeobfuscationUtilsKt.deobfOkhttp3(classLoader)); 65 | } else { 66 | return start(classLoader, NameMap.sDefaultMap); 67 | } 68 | } 69 | 70 | public static Set start(ClassLoader classLoader, NameMap nameMap) { 71 | OkHttp3Helper helper = new DefaultOkHttp3Helper(classLoader, nameMap); 72 | var interceptor = new StethoOkHttp3ProxyInterceptor(helper); 73 | var proxy = Proxy.newProxyInstance(classLoader, new Class[]{helper.Interceptor()}, 74 | (o, method, objects) -> { 75 | if ("intercept".equals(method.getName())) { 76 | return interceptor.intercept(objects[0]); 77 | } else if ("equals".equals(method.getName())) { 78 | return o == objects[0]; 79 | } else if ("hashCode".equals(method.getName())) { 80 | return System.identityHashCode(o); 81 | } else if ("toString".equals(method.getName())) { 82 | return o.getClass().getName() + "{" + System.identityHashCode(o) + "}"; 83 | } else { 84 | throw new IllegalArgumentException("unknown method " + method + " to proxy!"); 85 | } 86 | } 87 | ); 88 | return XposedBridge.hookAllConstructors(helper.okhttp3_internal_http_RealInterceptorChain(), new XC_MethodHook() { 89 | @Override 90 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 91 | // TODO: avoid using proxy 92 | var oldList = (List) helper.okhttp3_internal_http_RealInterceptorChain_interceptors().get(param.thisObject); 93 | if (oldList != null) { 94 | if (oldList.contains(proxy)) return; 95 | var list = new ArrayList(); 96 | list.addAll(oldList); 97 | list.add(list.size() - 1, proxy); 98 | Logger.d("add " + proxy + " to " + list); 99 | helper.okhttp3_internal_http_RealInterceptorChain_interceptors().set(param.thisObject, list); 100 | var updatedList = helper.okhttp3_internal_http_RealInterceptorChain_interceptors().get(param.thisObject); 101 | Logger.d("updated=" + updatedList); 102 | } 103 | } 104 | }); 105 | } 106 | 107 | private final NetworkEventReporter mEventReporter = NetworkEventReporterImpl.get(); 108 | 109 | private final OkHttp3Helper mHelper; 110 | 111 | public StethoOkHttp3ProxyInterceptor(OkHttp3Helper helper) { 112 | mHelper = helper; 113 | } 114 | 115 | private Object /*Response*/ intercept(Object /*Chain*/ chain) throws IOException { 116 | Logger.d("intercept " + chain); 117 | String requestId = mEventReporter.nextRequestId(); 118 | 119 | Object /*Request*/ request = mHelper.Interceptor$Chain_request(chain); 120 | 121 | RequestBodyHelper requestBodyHelper = null; 122 | if (mEventReporter.isEnabled()) { 123 | requestBodyHelper = new RequestBodyHelper(mEventReporter, requestId); 124 | OkHttpInspectorRequest inspectorRequest = 125 | new OkHttpInspectorRequest(mHelper, requestId, request, requestBodyHelper); 126 | mEventReporter.requestWillBeSent(inspectorRequest); 127 | } 128 | 129 | Object /*Response*/ response; 130 | try { 131 | response = mHelper.Interceptor$Chain_proceed(chain, request); 132 | } catch (IOException e) { 133 | if (mEventReporter.isEnabled()) { 134 | mEventReporter.httpExchangeFailed(requestId, e.toString()); 135 | } 136 | throw e; 137 | } 138 | 139 | if (mEventReporter.isEnabled()) { 140 | if (requestBodyHelper != null && requestBodyHelper.hasBody()) { 141 | requestBodyHelper.reportDataSent(); 142 | } 143 | 144 | Object /*Connection*/ connection = mHelper.Interceptor$Chain_connection(chain); 145 | if (connection == null) { 146 | throw new IllegalStateException( 147 | "No connection associated with this request; " + 148 | "did you use addInterceptor instead of addNetworkInterceptor?"); 149 | } 150 | mEventReporter.responseHeadersReceived( 151 | new OkHttpInspectorResponse( 152 | mHelper, 153 | requestId, 154 | request, 155 | response, 156 | connection)); 157 | 158 | Object /*ResponseBody*/ body = mHelper.Response_body(response); 159 | Object /*MediaType*/ contentType = null; 160 | InputStream responseStream = null; 161 | if (body != null) { 162 | contentType = mHelper.ResponseBody_contentType(body); 163 | responseStream = mHelper.ResponseBody_byteStream(body); 164 | } 165 | 166 | responseStream = mEventReporter.interpretResponseStream( 167 | requestId, 168 | contentType != null ? contentType.toString() : null, 169 | mHelper.Response_header(response, "Content-Encoding"), 170 | responseStream, 171 | new DefaultResponseHandler(mEventReporter, requestId)); 172 | if (responseStream != null) { 173 | var responseBody = mHelper.ResponseBody_create( 174 | mHelper.ResponseBody_contentType(body), 175 | mHelper.ResponseBody_contentLength(body), 176 | mHelper.Okio_1buffer(mHelper.Okio_source(responseStream)) 177 | ); 178 | response = mHelper.Response$Builder_build( 179 | mHelper.Response$Builder_body( 180 | mHelper.Response_newBuilder(response), responseBody 181 | ) 182 | ); 183 | } 184 | } 185 | 186 | return response; 187 | } 188 | 189 | private static class OkHttpInspectorRequest implements NetworkEventReporter.InspectorRequest { 190 | private final String mRequestId; 191 | private final Object /*Request*/ mRequest; 192 | private RequestBodyHelper mRequestBodyHelper; 193 | private final OkHttp3Helper mHelper; 194 | 195 | public OkHttpInspectorRequest( 196 | OkHttp3Helper helper, 197 | String requestId, 198 | Object /*Request*/ request, 199 | RequestBodyHelper requestBodyHelper) { 200 | mHelper = helper; 201 | mRequestId = requestId; 202 | mRequest = request; 203 | mRequestBodyHelper = requestBodyHelper; 204 | } 205 | 206 | @Override 207 | public String id() { 208 | return mRequestId; 209 | } 210 | 211 | @Override 212 | public String friendlyName() { 213 | // Hmm, can we do better? tag() perhaps? 214 | return null; 215 | } 216 | 217 | @Nullable 218 | @Override 219 | public Integer friendlyNameExtra() { 220 | return null; 221 | } 222 | 223 | @Override 224 | public String url() { 225 | return mHelper.Request_url(mRequest).toString(); 226 | } 227 | 228 | @Override 229 | public String method() { 230 | return mHelper.Request_method(mRequest); 231 | } 232 | 233 | @Nullable 234 | @Override 235 | public byte[] body() throws IOException { 236 | Object /*RequestBody*/ body = mHelper.Request_body(mRequest); 237 | if (body == null) { 238 | return null; 239 | } 240 | OutputStream out = mRequestBodyHelper.createBodySink(firstHeaderValue("Content-Encoding")); 241 | try (var bufferedSink = mHelper.Okio_buffer(mHelper.Okio_sink(out))) { 242 | mHelper.RequestBody_writeTo(body, bufferedSink); 243 | } 244 | return mRequestBodyHelper.getDisplayBody(); 245 | } 246 | 247 | @Override 248 | public int headerCount() { 249 | return mHelper.Headers_size(mHelper.Request_headers(mRequest)); 250 | } 251 | 252 | @Override 253 | public String headerName(int index) { 254 | return mHelper.Headers_name(mHelper.Request_headers(mRequest), index); 255 | } 256 | 257 | @Override 258 | public String headerValue(int index) { 259 | return mHelper.Headers_value(mHelper.Request_headers(mRequest), index); 260 | } 261 | 262 | @Nullable 263 | @Override 264 | public String firstHeaderValue(String name) { 265 | return mHelper.Request_header(mRequest, name); 266 | } 267 | } 268 | 269 | private static class OkHttpInspectorResponse implements NetworkEventReporter.InspectorResponse { 270 | private final String mRequestId; 271 | private final Object /*Request*/ mRequest; 272 | private final Object /*Response*/ mResponse; 273 | private @Nullable 274 | final Object /*Connection*/ mConnection; 275 | private final OkHttp3Helper mHelper; 276 | 277 | public OkHttpInspectorResponse( 278 | OkHttp3Helper helper, 279 | String requestId, 280 | Object /*Request*/ request, 281 | Object /*Response*/ response, 282 | @Nullable Object /*Connection*/ connection) { 283 | mHelper = helper; 284 | mRequestId = requestId; 285 | mRequest = request; 286 | mResponse = response; 287 | mConnection = connection; 288 | } 289 | 290 | @Override 291 | public String requestId() { 292 | return mRequestId; 293 | } 294 | 295 | @Override 296 | public String url() { 297 | return mHelper.Request_url(mRequest).toString(); 298 | } 299 | 300 | @Override 301 | public int statusCode() { 302 | return mHelper.Response_code(mResponse); 303 | } 304 | 305 | @Override 306 | public String reasonPhrase() { 307 | return mHelper.Response_message(mResponse); 308 | } 309 | 310 | @Override 311 | public boolean connectionReused() { 312 | // Not sure... 313 | return false; 314 | } 315 | 316 | @Override 317 | public int connectionId() { 318 | return mConnection == null ? 0 : mConnection.hashCode(); 319 | } 320 | 321 | @Override 322 | public boolean fromDiskCache() { 323 | return mHelper.Response_cacheResponse(mResponse) != null; 324 | } 325 | 326 | @Override 327 | public int headerCount() { 328 | return mHelper.Headers_size(mHelper.Response_headers(mResponse)); 329 | } 330 | 331 | @Override 332 | public String headerName(int index) { 333 | return mHelper.Headers_name(mHelper.Response_headers(mResponse), index); 334 | } 335 | 336 | @Override 337 | public String headerValue(int index) { 338 | return mHelper.Headers_value(mHelper.Response_headers(mResponse), index); 339 | } 340 | 341 | @Nullable 342 | @Override 343 | public String firstHeaderValue(String name) { 344 | return mHelper.Response_header(mResponse, name); 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/node/Node.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.node; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.util.List; 6 | 7 | public class Node { 8 | public int index; 9 | public T value; 10 | public List> children; 11 | 12 | public String getValueName() { 13 | return value == null ? "" : value.getClass().getName(); 14 | } 15 | 16 | @NonNull 17 | @Override 18 | public String toString() { 19 | // json format 20 | return "{" 21 | + "\"index\":" + index 22 | + ",\"name\":\"" + getValueName() + "\"" 23 | + ",\"value\":\"" + value + "\"" 24 | + ",\"children\":" + children 25 | + "}"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/objects/FindStackTraceFunction.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.objects; 2 | 3 | import android.app.Activity; 4 | import android.util.Log; 5 | import android.view.View; 6 | import android.view.Window; 7 | 8 | import com.facebook.stetho.rhino.JsConsole; 9 | 10 | import org.mozilla.javascript.BaseFunction; 11 | import org.mozilla.javascript.Context; 12 | import org.mozilla.javascript.Scriptable; 13 | import org.mozilla.javascript.Wrapper; 14 | 15 | import de.robv.android.xposed.XposedHelpers; 16 | 17 | public class FindStackTraceFunction extends BaseFunction { 18 | @Override 19 | public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { 20 | if (args.length == 1) { 21 | var obj = args[0]; 22 | Throwable t; 23 | if (obj instanceof Wrapper) { 24 | obj = ((Wrapper) obj).unwrap(); 25 | } 26 | if (obj instanceof Throwable) { 27 | t = (Throwable) obj; 28 | } else { 29 | if (obj instanceof Activity) { 30 | obj = ((Activity) obj).getWindow(); 31 | } 32 | if (obj instanceof Window) { 33 | obj = ((Window) obj).getDecorView(); 34 | } 35 | if (obj instanceof View) { 36 | t = (Throwable) XposedHelpers.getObjectField( 37 | XposedHelpers.callMethod(obj, "getViewRootImpl"), 38 | "mLocation" 39 | ); 40 | } else { 41 | throw new IllegalArgumentException("required a view or throwable!"); 42 | } 43 | } 44 | JsConsole.fromScope(scope).log(Log.getStackTraceString(t)); 45 | return null; 46 | } 47 | throw new IllegalArgumentException("must be 1 view or throwable arg !"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/objects/GetStackTraceFunction.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.objects; 2 | 3 | import org.mozilla.javascript.BaseFunction; 4 | import org.mozilla.javascript.Context; 5 | import org.mozilla.javascript.Scriptable; 6 | 7 | import io.github.a13e300.tools.Utils; 8 | 9 | public class GetStackTraceFunction extends BaseFunction { 10 | @Override 11 | public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { 12 | var hide = args.length >= 1 && args[0] == Boolean.TRUE; 13 | return Utils.getStackTrace(hide); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/objects/HookParam.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.objects; 2 | 3 | import org.mozilla.javascript.Context; 4 | import org.mozilla.javascript.Function; 5 | import org.mozilla.javascript.ScriptRuntime; 6 | import org.mozilla.javascript.Scriptable; 7 | import org.mozilla.javascript.ScriptableObject; 8 | import org.mozilla.javascript.annotations.JSFunction; 9 | import org.mozilla.javascript.annotations.JSGetter; 10 | import org.mozilla.javascript.annotations.JSSetter; 11 | 12 | import java.lang.reflect.InvocationTargetException; 13 | import java.lang.reflect.Member; 14 | 15 | import de.robv.android.xposed.XC_MethodHook; 16 | import de.robv.android.xposed.XposedBridge; 17 | 18 | public class HookParam extends ScriptableObject { 19 | XC_MethodHook.MethodHookParam mParam; 20 | private boolean mInvoked = false; 21 | 22 | public HookParam() {} 23 | 24 | public HookParam(Scriptable scope) { 25 | setParentScope(scope); 26 | Object ctor = ScriptRuntime.getTopLevelProp(scope, getClassName()); 27 | if (ctor instanceof Scriptable) { 28 | Scriptable scriptable = (Scriptable) ctor; 29 | setPrototype((Scriptable) scriptable.get("prototype", scriptable)); 30 | } 31 | } 32 | 33 | void setParam(XC_MethodHook.MethodHookParam param) { 34 | mParam = param; 35 | } 36 | 37 | synchronized Object invoke() throws Throwable { 38 | Object mOriginalResult; 39 | try { 40 | mOriginalResult = XposedBridge.invokeOriginalMethod(mParam.method, mParam.thisObject, mParam.args); 41 | if (!mInvoked) mParam.setResult(mOriginalResult); 42 | } catch (Throwable t) { 43 | var realT = t; 44 | if (realT instanceof InvocationTargetException) realT = ((InvocationTargetException) realT).getTargetException(); 45 | if (!mInvoked) mParam.setThrowable(realT); 46 | throw realT; 47 | } finally { 48 | mInvoked = true; 49 | } 50 | return mOriginalResult; 51 | } 52 | 53 | @JSFunction 54 | public static Object invoke(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws Throwable { 55 | var f = (HookParam) thisObj; 56 | return f.invoke(); 57 | } 58 | 59 | boolean isInvoked() { 60 | return mInvoked; 61 | } 62 | 63 | @Override 64 | public String getClassName() { 65 | return "HookParam"; 66 | } 67 | 68 | @JSGetter 69 | public static Object getResult(Scriptable thisObj) { 70 | return ((HookParam) thisObj).mParam.getResult(); 71 | } 72 | 73 | @JSSetter 74 | public static void setResult(Scriptable thisObj, Object value) { 75 | var p = (HookParam) thisObj; 76 | p.mParam.setResult(value); 77 | p.mInvoked = true; 78 | } 79 | 80 | @JSGetter 81 | public static Throwable getThrowable(Scriptable thisObj) { 82 | return ((HookParam) thisObj).mParam.getThrowable(); 83 | } 84 | 85 | @JSSetter 86 | public static void setThrowable(Scriptable thisObj, Throwable value) { 87 | var p = (HookParam) thisObj; 88 | p.mParam.setThrowable(value); 89 | p.mInvoked = true; 90 | } 91 | 92 | @JSGetter 93 | public static Object[] getArgs(Scriptable thisObj) { 94 | return ((HookParam) thisObj).mParam.args; 95 | } 96 | 97 | @JSSetter 98 | public static void setArgs(Scriptable thisObj, Object[] value) { 99 | ((HookParam) thisObj).mParam.args = value; 100 | } 101 | 102 | @JSGetter 103 | public static Object getThisObject(Scriptable thisObj) { 104 | return ((HookParam) thisObj).mParam.thisObject; 105 | } 106 | 107 | @JSSetter 108 | public static void setThisObject(Scriptable thisObj, Object[] value) { 109 | ((HookParam) thisObj).mParam.thisObject = value; 110 | } 111 | 112 | @JSGetter 113 | public static Member getMethod(Scriptable thisObj) { 114 | return ((HookParam) thisObj).mParam.method; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/objects/JArrayFunction.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.objects; 2 | 3 | import org.mozilla.javascript.BaseFunction; 4 | import org.mozilla.javascript.Context; 5 | import org.mozilla.javascript.Scriptable; 6 | import org.mozilla.javascript.ScriptableObject; 7 | import org.mozilla.javascript.Wrapper; 8 | 9 | import java.lang.reflect.Array; 10 | 11 | public class JArrayFunction extends BaseFunction { 12 | private final Class mType; 13 | 14 | public JArrayFunction(Class type) { 15 | mType = type; 16 | } 17 | 18 | @Override 19 | public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { 20 | Class type = null; 21 | int length = -1; 22 | if (mType != null) { 23 | type = mType; 24 | if (args.length != 1 || !(args[0] instanceof Number)) throw new IllegalArgumentException("required 1 numeric arg: length"); 25 | length = ((Number) args[0]).intValue(); 26 | } else { 27 | if (args.length == 2) { 28 | Object a0 = args[0]; 29 | if (a0 instanceof Wrapper) a0 = ((Wrapper) a0).unwrap(); 30 | if (a0 instanceof String) { 31 | Object hook = ScriptableObject.getProperty(scope, "hook"); 32 | if (hook instanceof HookFunction) { 33 | try { 34 | type = ((HookFunction) hook).getClassLoader().loadClass((String) a0); 35 | } catch (ClassNotFoundException e) { 36 | throw new IllegalArgumentException(e); 37 | } 38 | } else { 39 | try { 40 | type = Class.forName((String) a0); 41 | } catch (ClassNotFoundException e) { 42 | throw new IllegalArgumentException(e); 43 | } 44 | } 45 | } else if (a0 instanceof Class) { 46 | type = (Class) a0; 47 | } 48 | if (args[1] instanceof Number) length = ((Number) args[1]).intValue(); 49 | } 50 | if (type == null || length == -1) throw new IllegalArgumentException("2 args required: class (String|Class), length"); 51 | } 52 | return Array.newInstance(type, length); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/objects/OkHttpInterceptorObject.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.objects; 2 | 3 | import com.facebook.stetho.rhino.JsConsole; 4 | 5 | import org.mozilla.javascript.Context; 6 | import org.mozilla.javascript.Function; 7 | import org.mozilla.javascript.ScriptRuntime; 8 | import org.mozilla.javascript.Scriptable; 9 | import org.mozilla.javascript.ScriptableObject; 10 | import org.mozilla.javascript.Wrapper; 11 | import org.mozilla.javascript.annotations.JSFunction; 12 | 13 | import java.util.Set; 14 | 15 | import de.robv.android.xposed.XC_MethodHook; 16 | import io.github.a13e300.tools.Logger; 17 | import io.github.a13e300.tools.network.okhttp3.StethoOkHttp3ProxyInterceptor; 18 | 19 | public class OkHttpInterceptorObject extends ScriptableObject { 20 | private Set unhook = null; 21 | 22 | public OkHttpInterceptorObject() {} 23 | 24 | public OkHttpInterceptorObject(Scriptable scope) { 25 | setParentScope(scope); 26 | Object ctor = ScriptRuntime.getTopLevelProp(scope, getClassName()); 27 | if (ctor instanceof Scriptable) { 28 | Scriptable scriptable = (Scriptable) ctor; 29 | setPrototype((Scriptable) scriptable.get("prototype", scriptable)); 30 | } 31 | } 32 | 33 | @Override 34 | public String getClassName() { 35 | return "OkHttpInterceptorObject"; 36 | } 37 | 38 | private static final int USE_DEOBF_AUTO = 0; 39 | private static final int USE_DEOBF_YES = 1; 40 | private static final int USE_DEOBF_NO = 2; 41 | 42 | public synchronized void start(ClassLoader classLoader, int useDeObf) { 43 | if (unhook == null) { 44 | var console = JsConsole.fromScope(getParentScope()); 45 | if (useDeObf == USE_DEOBF_AUTO) { 46 | try { 47 | unhook = StethoOkHttp3ProxyInterceptor.start(classLoader, false); 48 | } catch (Throwable t) { 49 | Logger.e("Okhttp3Interceptor start with useDeObf=false failed, try useDeObf=true", t); 50 | console.log("Okhttp3Interceptor start with useDeObf=false failed, try useDeObf=true"); 51 | unhook = StethoOkHttp3ProxyInterceptor.start(classLoader, true); 52 | } 53 | } else { 54 | unhook = StethoOkHttp3ProxyInterceptor.start(classLoader, useDeObf == USE_DEOBF_YES); 55 | } 56 | Logger.d("OkHttp3Interceptor started!"); 57 | console.log("Okhttp3Interceptor started!"); 58 | } else { 59 | throw new IllegalStateException("Okhttp3Interceptor has already started!"); 60 | } 61 | } 62 | 63 | public synchronized void stop(boolean finalize) { 64 | if (unhook != null) { 65 | for (var h: unhook) { 66 | h.unhook(); 67 | } 68 | unhook = null; 69 | Logger.d("OkHttp3Interceptor stopped!"); 70 | if (!finalize) { 71 | JsConsole.fromScope(getParentScope()).log("Okhttp3Interceptor stopped!"); 72 | } 73 | } else if (!finalize) { 74 | throw new IllegalStateException("Okhttp3Interceptor has not yet started!"); 75 | } 76 | } 77 | 78 | private static void usage() { 79 | throw new IllegalArgumentException("usage: start([useObf] [, classLoader])"); 80 | } 81 | 82 | @JSFunction 83 | public static void start(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws Throwable { 84 | var obj = (OkHttpInterceptorObject) thisObj; 85 | var hook = (HookFunction) ScriptableObject.getProperty(obj.getParentScope(), "hook"); 86 | ClassLoader cl = null; 87 | int useDeObf; 88 | if (args.length == 0) { 89 | cl = hook.getClassLoader(); 90 | useDeObf = USE_DEOBF_AUTO; 91 | } else { 92 | if (!(args[0] instanceof Boolean)) usage(); 93 | useDeObf = ((Boolean) args[0]) ? USE_DEOBF_YES : USE_DEOBF_NO; 94 | if (args.length == 2) { 95 | var c = args[1]; 96 | if (c instanceof Wrapper) c = ((Wrapper) c).unwrap(); 97 | if (c instanceof ClassLoader) cl = (ClassLoader) c; 98 | else usage(); 99 | } else usage(); 100 | } 101 | obj.start(cl, useDeObf); 102 | } 103 | 104 | @JSFunction 105 | public static void stop(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws Throwable { 106 | var obj = (OkHttpInterceptorObject) thisObj; 107 | obj.stop(false); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/objects/PrintStackTraceFunction.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.objects; 2 | 3 | import com.facebook.stetho.rhino.JsConsole; 4 | 5 | import org.mozilla.javascript.BaseFunction; 6 | import org.mozilla.javascript.Context; 7 | import org.mozilla.javascript.Scriptable; 8 | 9 | import io.github.a13e300.tools.Utils; 10 | 11 | public class PrintStackTraceFunction extends BaseFunction { 12 | @Override 13 | public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { 14 | var hide = args.length >= 1 && args[0] == Boolean.TRUE; 15 | JsConsole.fromScope(scope).log(Utils.getStackTrace(hide)); 16 | return null; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/objects/RunOnHandlerFunction.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.objects; 2 | 3 | import static io.github.a13e300.tools.Utils.enterJsContext; 4 | 5 | import android.os.Handler; 6 | 7 | import com.facebook.stetho.rhino.JsConsole; 8 | 9 | import org.mozilla.javascript.BaseFunction; 10 | import org.mozilla.javascript.Context; 11 | import org.mozilla.javascript.Function; 12 | import org.mozilla.javascript.Scriptable; 13 | import org.mozilla.javascript.Wrapper; 14 | 15 | public class RunOnHandlerFunction extends BaseFunction { 16 | private final Handler mHandler; 17 | public RunOnHandlerFunction(Handler handler) { 18 | mHandler = handler; 19 | } 20 | 21 | public RunOnHandlerFunction() { 22 | mHandler = null; 23 | } 24 | 25 | @Override 26 | public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { 27 | Handler handler; 28 | if (args.length < 1 || !(args[0] instanceof Function)) { 29 | throw new IllegalArgumentException("arg0 must be a function"); 30 | } 31 | handler = mHandler; 32 | var fn = (Function) args[0]; 33 | if (handler == null && args.length == 2) { 34 | var arg1 = args[1]; 35 | if (arg1 instanceof Wrapper) arg1 = ((Wrapper) arg1).unwrap(); 36 | if (!(arg1 instanceof Handler)) throw new IllegalArgumentException("arg1 must be a handler"); 37 | handler = (Handler) arg1; 38 | } 39 | handler.post(() -> { 40 | var context = enterJsContext(); 41 | try { 42 | fn.call(context, scope, null, new Object[0]); 43 | } catch (Throwable t) { 44 | JsConsole.fromScope(scope).error("error at handler", t); 45 | } finally { 46 | Context.exit(); 47 | } 48 | }); 49 | return null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/a13e300/tools/objects/UnhookFunction.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.tools.objects; 2 | 3 | import org.mozilla.javascript.Context; 4 | import org.mozilla.javascript.Function; 5 | import org.mozilla.javascript.ScriptRuntime; 6 | import org.mozilla.javascript.Scriptable; 7 | import org.mozilla.javascript.ScriptableObject; 8 | import org.mozilla.javascript.annotations.JSFunction; 9 | 10 | import java.util.List; 11 | 12 | import de.robv.android.xposed.XC_MethodHook; 13 | 14 | public class UnhookFunction extends ScriptableObject { 15 | public UnhookFunction() { 16 | } 17 | 18 | List mUnhooks; 19 | 20 | public UnhookFunction(Scriptable scope) { 21 | setParentScope(scope); 22 | Object ctor = ScriptRuntime.getTopLevelProp(scope, getClassName()); 23 | if (ctor instanceof Scriptable) { 24 | Scriptable scriptable = (Scriptable) ctor; 25 | setPrototype((Scriptable) scriptable.get("prototype", scriptable)); 26 | } 27 | } 28 | 29 | void setUnhooks(List unhooks) { 30 | mUnhooks = unhooks; 31 | } 32 | 33 | @Override 34 | public String getClassName() { 35 | return "UnhookFunction"; 36 | } 37 | 38 | @JSFunction 39 | public static String toString(Context cx, Scriptable thisObj, Object[] args, Function funObj) { 40 | var f = (UnhookFunction) thisObj; 41 | var sb = new StringBuilder("Unhook of ") 42 | .append(f.mUnhooks.size()) 43 | .append(" methods"); 44 | for (var m: f.mUnhooks) { 45 | sb.append("\n"); 46 | sb.append(m); 47 | } 48 | return sb.toString(); 49 | } 50 | 51 | @JSFunction 52 | public static void unhook(Context cx, Scriptable thisObj, Object[] args, Function funObj) { 53 | var f = (UnhookFunction) thisObj; 54 | for (var m: f.mUnhooks) { 55 | m.unhook(); 56 | } 57 | f.mUnhooks = null; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/StethoX/97de270ae2e3c2979c07f6582f6ca178966c8e49/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/StethoX/97de270ae2e3c2979c07f6582f6ca178966c8e49/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/StethoX/97de270ae2e3c2979c07f6582f6ca178966c8e49/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/StethoX/97de270ae2e3c2979c07f6582f6ca178966c8e49/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/StethoX/97de270ae2e3c2979c07f6582f6ca178966c8e49/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Stethox 3 | 4 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.agp.app) apply false 3 | alias(libs.plugins.agp.lib) apply false 4 | alias(libs.plugins.kotlin) apply false 5 | } 6 | 7 | task("clean", type = Delete::class) { 8 | delete(rootProject.buildDir) 9 | } 10 | -------------------------------------------------------------------------------- /copyright: -------------------------------------------------------------------------------- 1 | Copyright: 2018 Christian Schabesberger 2 | 3 | License: GPL-3.0+ 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /debugstub/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /debugstub/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.agp.app) 3 | } 4 | 5 | android { 6 | namespace = "io.github.a13e300.demo" 7 | compileSdk = 35 8 | 9 | defaultConfig { 10 | // rename to the debug target 11 | // or android studio will not recognize 12 | applicationId = "io.github.a13e300.demo" 13 | minSdk = 27 14 | targetSdk = 35 15 | versionCode = 1 16 | versionName = "1.0" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | isMinifyEnabled = false 22 | proguardFiles( 23 | getDefaultProguardFile("proguard-android-optimize.txt"), 24 | "proguard-rules.pro" 25 | ) 26 | } 27 | } 28 | compileOptions { 29 | sourceCompatibility = JavaVersion.VERSION_1_8 30 | targetCompatibility = JavaVersion.VERSION_1_8 31 | } 32 | } 33 | 34 | dependencies { 35 | // this breaks native code indexing, use carefully ! 36 | // implementation(project(":app")) 37 | } -------------------------------------------------------------------------------- /debugstub/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /debugstub/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | android.useAndroidX=true 15 | android.defaults.buildfeatures.buildconfig=true 16 | android.nonTransitiveRClass=false 17 | android.nonFinalResIds=false 18 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.7.2" 3 | annotation = "1.9.1" 4 | api = "82" 5 | dexkit = "2.0.0-rc4" 6 | dexmaker = "2.28.3" 7 | kotlin = "2.0.20" 8 | rhino = "1.7.15-SNAPSHOT" 9 | stetho = "1.0-alpha-1" 10 | 11 | [plugins] 12 | agp-app = { id = "com.android.application", version.ref = "agp" } 13 | agp-lib = { id = "com.android.library", version.ref = "agp" } 14 | kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 15 | 16 | [libraries] 17 | annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } 18 | api = { module = "de.robv.android.xposed:api", version.ref = "api" } 19 | cxx = { module = "org.lsposed.libcxx:libcxx", version = "27.0.12077973" } 20 | dexkit = { module = "org.luckypray:dexkit", version.ref = "dexkit" } 21 | dexmaker = { module = "com.linkedin.dexmaker:dexmaker", version.ref = "dexmaker" } 22 | rhino = { module = "org.mozilla:rhino", version.ref = "rhino" } 23 | stetho = { module = "com.github.5ec1cff.stetho:stetho", version.ref = "stetho" } 24 | stetho-js-rhino = { module = "com.github.5ec1cff.stetho:stetho-js-rhino", version.ref = "stetho" } 25 | stetho-urlconnection = { module = "com.github.5ec1cff.stetho:stetho-urlconnection", version.ref = "stetho" } 26 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/StethoX/97de270ae2e3c2979c07f6582f6ca178966c8e49/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Mar 09 17:29:28 CST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /hidden-api/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /hidden-api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.agp.lib) 3 | } 4 | 5 | android { 6 | namespace = "io.github.a13e300.hidden_api" 7 | compileSdk = 35 8 | 9 | defaultConfig { 10 | minSdk = 24 11 | 12 | consumerProguardFiles("consumer-rules.pro") 13 | } 14 | 15 | buildTypes { 16 | release { 17 | isMinifyEnabled = false 18 | proguardFiles( 19 | getDefaultProguardFile("proguard-android-optimize.txt"), 20 | "proguard-rules.pro" 21 | ) 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility = JavaVersion.VERSION_1_8 26 | targetCompatibility = JavaVersion.VERSION_1_8 27 | } 28 | } 29 | 30 | dependencies { 31 | 32 | } -------------------------------------------------------------------------------- /hidden-api/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/StethoX/97de270ae2e3c2979c07f6582f6ca178966c8e49/hidden-api/consumer-rules.pro -------------------------------------------------------------------------------- /hidden-api/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /hidden-api/src/androidTest/java/io/github/a13e300/hidden_api/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.hidden_api; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import android.content.Context; 6 | 7 | import androidx.test.ext.junit.runners.AndroidJUnit4; 8 | import androidx.test.platform.app.InstrumentationRegistry; 9 | 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("io.github.a13e300.hidden_api.test", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /hidden-api/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/app/ActivityThread.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | public class ActivityThread { 4 | public String getProcessName() { 5 | throw new RuntimeException("STUB"); 6 | } 7 | 8 | public ApplicationThread getApplicationThread() { 9 | throw new RuntimeException("STUB"); 10 | } 11 | 12 | public static ActivityThread currentActivityThread() { 13 | throw new RuntimeException("STUB"); 14 | } 15 | 16 | private static class ApplicationThread implements IApplicationThread {} 17 | } 18 | -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/app/IActivityManager.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | import android.os.IBinder; 4 | 5 | public interface IActivityManager { 6 | void showWaitingForDebugger(IApplicationThread who, boolean waiting); 7 | 8 | class Stub { 9 | public static IActivityManager asInterface(IBinder b) { 10 | throw new RuntimeException(""); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/app/IApplicationThread.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | public interface IApplicationThread { 4 | } 5 | -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/os/ServiceManager.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public class ServiceManager { 4 | public static IBinder getService(String name) { 5 | throw new RuntimeException(""); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/os/SystemProperties.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public class SystemProperties { 4 | public native static String get(String key); 5 | } 6 | -------------------------------------------------------------------------------- /hidden-api/src/test/java/io/github/a13e300/hidden_api/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package io.github.a13e300.hidden_api; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | } 14 | 15 | dependencyResolutionManagement { 16 | repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS 17 | repositories { 18 | mavenLocal() 19 | google() 20 | mavenCentral() 21 | maven("https://jitpack.io") 22 | maven("https://api.xposed.info/") 23 | } 24 | } 25 | 26 | rootProject.name = "StethoX" 27 | 28 | include(":app") 29 | include(":hidden-api") 30 | include(":debugstub") 31 | --------------------------------------------------------------------------------