feat: patch manager
- LSPatch v0.5.1-390
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
[versions]
|
||||
agp = "8.1.2"
|
||||
apksig = "8.0.2"
|
||||
guava = "32.1.3-jre"
|
||||
kotlin = "1.9.0"
|
||||
kotlinx-coroutines-android = "1.7.3"
|
||||
|
||||
@@ -33,6 +35,7 @@ androidx-material-ripple = { module = "androidx.compose.material:material-ripple
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
|
||||
androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "ui-tooling-preview" }
|
||||
androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "ui-tooling-preview" }
|
||||
apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" }
|
||||
bcprov-jdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bcprov-jdk18on" }
|
||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil-compose" }
|
||||
coil-video = { module = "io.coil-kt:coil-video", version.ref = "coil-compose" }
|
||||
@@ -40,6 +43,7 @@ coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-andro
|
||||
dexlib2 = { group = "org.smali", name = "dexlib2", version.ref = "dexlib2" }
|
||||
ffmpeg-kit = { group = "com.arthenica", name = "ffmpeg-kit-full-gpl", version.ref = "ffmpeg-kit" }
|
||||
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
|
||||
guava = { module = "com.google.guava:guava", version.ref = "guava" }
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
osmdroid-android = { group = "org.osmdroid", name = "osmdroid-android", version.ref = "osmdroid-android" }
|
||||
|
||||
1
manager/.gitignore
vendored
Normal file
1
manager/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
83
manager/build.gradle.kts
Normal file
83
manager/build.gradle.kts
Normal file
@@ -0,0 +1,83 @@
|
||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.kotlinAndroid)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = rootProject.ext["applicationId"].toString() + ".manager"
|
||||
compileSdk = 34
|
||||
|
||||
androidResources {
|
||||
noCompress += ".so"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
buildConfigField("String", "APPLICATION_ID", "\"${rootProject.ext["applicationId"]}\"")
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.2"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = rootProject.ext["applicationId"].toString() + ".manager"
|
||||
versionCode = 1
|
||||
versionName = "1.0.0"
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
multiDexEnabled = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles += file("proguard-rules.pro")
|
||||
}
|
||||
debug {
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
outputs.map { it as BaseVariantOutputImpl }.forEach { outputVariant ->
|
||||
outputVariant.outputFileName = "manager.apk"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
all {
|
||||
resolutionStrategy {
|
||||
exclude(group = "com.google.guava", module = "listenablefuture")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||
implementation(libs.guava)
|
||||
implementation(libs.apksig)
|
||||
implementation(libs.gson)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.activity.ktx)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.material.icons.core)
|
||||
implementation(libs.androidx.material.ripple)
|
||||
implementation(libs.androidx.material.icons.extended)
|
||||
}
|
||||
BIN
manager/libs/ManifestEditor-1.0.2.jar
Normal file
BIN
manager/libs/ManifestEditor-1.0.2.jar
Normal file
Binary file not shown.
BIN
manager/libs/apkzlib.jar
Normal file
BIN
manager/libs/apkzlib.jar
Normal file
Binary file not shown.
2
manager/proguard-rules.pro
vendored
Normal file
2
manager/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
-dontwarn com.google.errorprone.annotations.**
|
||||
-dontwarn com.google.auto.value.**
|
||||
20
manager/src/main/AndroidManifest.xml
Normal file
20
manager/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:label="SE Manager"
|
||||
android:icon="@android:drawable/ic_input_add">
|
||||
<activity android:name=".MainActivity" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
BIN
manager/src/main/assets/lspatch/dexes/loader.dex
Normal file
BIN
manager/src/main/assets/lspatch/dexes/loader.dex
Normal file
Binary file not shown.
BIN
manager/src/main/assets/lspatch/dexes/metaloader.dex
Normal file
BIN
manager/src/main/assets/lspatch/dexes/metaloader.dex
Normal file
Binary file not shown.
BIN
manager/src/main/assets/lspatch/keystore.jks
Normal file
BIN
manager/src/main/assets/lspatch/keystore.jks
Normal file
Binary file not shown.
BIN
manager/src/main/assets/lspatch/so/arm64-v8a/liblspatch.so
Normal file
BIN
manager/src/main/assets/lspatch/so/arm64-v8a/liblspatch.so
Normal file
Binary file not shown.
BIN
manager/src/main/assets/lspatch/so/armeabi-v7a/liblspatch.so
Normal file
BIN
manager/src/main/assets/lspatch/so/armeabi-v7a/liblspatch.so
Normal file
Binary file not shown.
1
manager/src/main/assets/lspatch/version.txt
Normal file
1
manager/src/main/assets/lspatch/version.txt
Normal file
@@ -0,0 +1 @@
|
||||
0.5.1
|
||||
@@ -0,0 +1,109 @@
|
||||
package me.rhunk.snapenhance.manager
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.manager.lspatch.LSPatch
|
||||
import java.io.File
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
MaterialTheme(
|
||||
colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (isSystemInDarkTheme()) dynamicDarkColorScheme(LocalContext.current)
|
||||
else dynamicLightColorScheme(LocalContext.current)
|
||||
} else MaterialTheme.colorScheme
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val logs = remember { mutableStateListOf<String>() }
|
||||
fun printLog(data: Any) {
|
||||
when (data) {
|
||||
is Throwable -> {
|
||||
logs += data.message.toString()
|
||||
logs += StringWriter().apply {
|
||||
data.printStackTrace(PrintWriter(this))
|
||||
}.toString()
|
||||
}
|
||||
else -> logs += data.toString()
|
||||
}
|
||||
}
|
||||
|
||||
val scrollState = rememberLazyListState(0)
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
modifier = Modifier.padding(10.dp)
|
||||
) {
|
||||
Text(text = "SE Manager")
|
||||
|
||||
Button(onClick = {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val lspatch = LSPatch(
|
||||
context,
|
||||
mapOf(
|
||||
BuildConfig.APPLICATION_ID to File(context.packageManager.getPackageInfo(BuildConfig.APPLICATION_ID, 0).applicationInfo.sourceDir)
|
||||
)
|
||||
) { printLog(it) }
|
||||
lspatch.patch(
|
||||
File(context.packageManager.getPackageInfo("com.snapchat.android", 0).applicationInfo.sourceDir),
|
||||
File(context.filesDir, "patched.apk")
|
||||
)
|
||||
}.onFailure { printLog(it) }
|
||||
}
|
||||
}) {
|
||||
Text(text = "Test patch apk")
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
state = scrollState,
|
||||
modifier = Modifier.fillMaxWidth().padding(5.dp).height(500.dp).border(1.dp, color = Color.Black),
|
||||
content = {
|
||||
items(logs) {
|
||||
Text(text = it, modifier = Modifier.padding(2.dp))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(logs.size) {
|
||||
scrollState.scrollToItem((logs.size - 1).coerceAtLeast(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package me.rhunk.snapenhance.manager.lspatch
|
||||
|
||||
import android.content.Context
|
||||
import com.android.tools.build.apkzlib.sign.SigningExtension
|
||||
import com.android.tools.build.apkzlib.sign.SigningOptions
|
||||
import com.android.tools.build.apkzlib.zip.AlignmentRules
|
||||
import com.android.tools.build.apkzlib.zip.ZFile
|
||||
import com.android.tools.build.apkzlib.zip.ZFileOptions
|
||||
import com.google.gson.Gson
|
||||
import com.wind.meditor.core.ManifestEditor
|
||||
import com.wind.meditor.property.AttributeItem
|
||||
import com.wind.meditor.property.ModificationProperty
|
||||
import me.rhunk.snapenhance.manager.lspatch.config.Constants.ORIGINAL_APK_ASSET_PATH
|
||||
import me.rhunk.snapenhance.manager.lspatch.config.Constants.PROXY_APP_COMPONENT_FACTORY
|
||||
import me.rhunk.snapenhance.manager.lspatch.config.PatchConfig
|
||||
import me.rhunk.snapenhance.manager.lspatch.util.ApkSignatureHelper
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.zip.ZipFile
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
|
||||
//https://github.com/LSPosed/LSPatch/blob/master/patch/src/main/java/org/lsposed/patch/LSPatch.java
|
||||
class LSPatch(
|
||||
private val context: Context,
|
||||
private val modules: Map<String, File>, //packageName -> file
|
||||
private val printLog: (Any) -> Unit
|
||||
) {
|
||||
companion object {
|
||||
private val Z_FILE_OPTIONS = ZFileOptions().setAlignmentRule(
|
||||
AlignmentRules.compose(
|
||||
AlignmentRules.constantForSuffix(".so", 4096),
|
||||
AlignmentRules.constantForSuffix(ORIGINAL_APK_ASSET_PATH, 4096)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun patchManifest(data: ByteArray, lspatchMetadata: String): ByteArray {
|
||||
val property = ModificationProperty()
|
||||
|
||||
property.addApplicationAttribute(AttributeItem("appComponentFactory", PROXY_APP_COMPONENT_FACTORY))
|
||||
property.addMetaData(ModificationProperty.MetaData("lspatch", lspatchMetadata))
|
||||
|
||||
return ByteArrayOutputStream().apply {
|
||||
ManifestEditor(ByteArrayInputStream(data), this, property).processManifest()
|
||||
flush()
|
||||
close()
|
||||
}.toByteArray()
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
private fun patchApk(inputApkFile: File, outputFile: File) {
|
||||
printLog("Patching ${inputApkFile.absolutePath} to ${outputFile.absolutePath}")
|
||||
val dstZFile = ZFile.openReadWrite(outputFile, Z_FILE_OPTIONS)
|
||||
val sourceApkFile = dstZFile.addNestedZip({ ORIGINAL_APK_ASSET_PATH }, inputApkFile, false)
|
||||
|
||||
val patchConfig = PatchConfig(
|
||||
useManager = false,
|
||||
debuggable = false,
|
||||
overrideVersionCode = false,
|
||||
sigBypassLevel = 2,
|
||||
originalSignature = ApkSignatureHelper.getApkSignInfo(inputApkFile.absolutePath),
|
||||
appComponentFactory = "androidx.core.app.CoreComponentFactory"
|
||||
).let { Gson().toJson(it) }
|
||||
|
||||
// sign apk
|
||||
runCatching {
|
||||
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||
keyStore.load(context.assets.open("lspatch/keystore.jks"), "android".toCharArray())
|
||||
val key = keyStore.getEntry("androiddebugkey", KeyStore.PasswordProtection("android".toCharArray())) as KeyStore.PrivateKeyEntry
|
||||
SigningExtension(
|
||||
SigningOptions.builder().apply {
|
||||
setMinSdkVersion(28)
|
||||
setV2SigningEnabled(true)
|
||||
setCertificates(*(key.certificateChain as Array<X509Certificate>))
|
||||
setKey(key.privateKey)
|
||||
}.build()
|
||||
).apply {
|
||||
register(dstZFile)
|
||||
}
|
||||
}.onFailure {
|
||||
throw Exception("Failed to sign apk", it)
|
||||
}
|
||||
|
||||
printLog("Patching manifest")
|
||||
|
||||
val originalManifestEntry = sourceApkFile.get("AndroidManifest.xml") ?: throw Exception("No original manifest found")
|
||||
originalManifestEntry.open().use { inputStream ->
|
||||
val patchedManifestData = patchManifest(inputStream.readBytes(), Base64.encode(patchConfig.toByteArray()))
|
||||
dstZFile.add("AndroidManifest.xml", patchedManifestData.inputStream())
|
||||
}
|
||||
|
||||
//add config
|
||||
printLog("Adding config")
|
||||
dstZFile.add("assets/lspatch/config.json", ByteArrayInputStream(patchConfig.toByteArray()))
|
||||
|
||||
|
||||
// add loader dex
|
||||
printLog("Adding dex files")
|
||||
dstZFile.add("classes.dex", context.assets.open("lspatch/dexes/metaloader.dex"))
|
||||
dstZFile.add("assets/lspatch/loader.dex", context.assets.open("lspatch/dexes/loader.dex"))
|
||||
|
||||
//add natives
|
||||
printLog("Adding natives")
|
||||
context.assets.list("lspatch/so")?.forEach { native ->
|
||||
dstZFile.add("assets/lspatch/so/$native/liblspatch.so", context.assets.open("lspatch/so/$native/liblspatch.so"), false)
|
||||
}
|
||||
|
||||
|
||||
//embed modules
|
||||
printLog("embedding modules")
|
||||
modules.forEach { (packageName, module) ->
|
||||
dstZFile.add("assets/lspatch/modules/$packageName.apk", module.inputStream())
|
||||
}
|
||||
|
||||
|
||||
// link apk entries
|
||||
printLog("Linking apk entries")
|
||||
|
||||
for (entry in sourceApkFile.entries()) {
|
||||
val name = entry.centralDirectoryHeader.name
|
||||
if (name.startsWith("classes") && name.endsWith(".dex")) continue
|
||||
if (dstZFile[name] != null) continue
|
||||
if (name == "AndroidManifest.xml") continue
|
||||
if (name.startsWith("META-INF") && (name.endsWith(".SF") || name.endsWith(".MF") || name.endsWith(
|
||||
".RSA"
|
||||
))
|
||||
) continue
|
||||
sourceApkFile.addFileLink(name, name)
|
||||
}
|
||||
|
||||
dstZFile.realign()
|
||||
dstZFile.close()
|
||||
printLog("Done")
|
||||
}
|
||||
|
||||
fun patch(input: File, outputFile: File) {
|
||||
//check if input apk is already patched
|
||||
var isAlreadyPatched = false
|
||||
var inputFile = input
|
||||
|
||||
// extract origin
|
||||
printLog("Extracting origin apk")
|
||||
ZipFile(input).use { zipFile ->
|
||||
zipFile.getEntry("assets/lspatch/origin.apk")?.apply {
|
||||
inputFile = File.createTempFile("origin", ".apk")
|
||||
inputFile.outputStream().use {
|
||||
zipFile.getInputStream(this).copyTo(it)
|
||||
}
|
||||
isAlreadyPatched = true
|
||||
}
|
||||
}
|
||||
|
||||
if (outputFile.exists()) outputFile.delete()
|
||||
|
||||
printLog("Patching apk")
|
||||
runCatching {
|
||||
patchApk(inputFile, outputFile)
|
||||
}.onFailure {
|
||||
if (isAlreadyPatched) {
|
||||
inputFile.delete()
|
||||
}
|
||||
outputFile.delete()
|
||||
printLog("Failed to patch")
|
||||
printLog(it)
|
||||
}.onSuccess {
|
||||
outputFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package me.rhunk.snapenhance.manager.lspatch.config
|
||||
|
||||
//https://github.com/LSPosed/LSPatch/blob/master/share/java/src/main/java/org/lsposed/lspatch/share/Constants.java
|
||||
object Constants {
|
||||
const val ORIGINAL_APK_ASSET_PATH = "assets/lspatch/origin.apk"
|
||||
const val PROXY_APP_COMPONENT_FACTORY =
|
||||
"org.lsposed.lspatch.metaloader.LSPAppComponentFactoryStub"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package me.rhunk.snapenhance.manager.lspatch.config
|
||||
|
||||
data class PatchConfig(
|
||||
val useManager: Boolean = false,
|
||||
val debuggable: Boolean = false,
|
||||
val overrideVersionCode: Boolean = false,
|
||||
val sigBypassLevel: Int = 0,
|
||||
val originalSignature: String? = null,
|
||||
val appComponentFactory: String? = null,
|
||||
val lspConfig: LSPConfig? = LSPConfig()
|
||||
) {
|
||||
data class LSPConfig(
|
||||
var API_CODE: Int = 93,
|
||||
var VERSION_CODE: Int = 360,
|
||||
var VERSION_NAME: String = "0.5.1",
|
||||
var CORE_VERSION_CODE: Int = 6649,
|
||||
var CORE_VERSION_NAME: String = "1.8.5",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package me.rhunk.snapenhance.manager.lspatch.util;
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.RandomAccessFile
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.security.cert.Certificate
|
||||
import java.util.Enumeration
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarFile
|
||||
|
||||
|
||||
//https://github.com/LSPosed/LSPatch/blob/master/patch/src/main/java/org/lsposed/patch/util/ApkSignatureHelper.java
|
||||
object ApkSignatureHelper {
|
||||
private val APK_V2_MAGIC = charArrayOf('A', 'P', 'K', ' ', 'S', 'i', 'g', ' ',
|
||||
'B', 'l', 'o', 'c', 'k', ' ', '4', '2')
|
||||
|
||||
private fun toChars(mSignature: ByteArray): CharArray {
|
||||
val N = mSignature.size
|
||||
val N2 = N * 2
|
||||
val text = CharArray(N2)
|
||||
for (j in 0 until N) {
|
||||
val v = mSignature[j]
|
||||
var d = v.toInt() shr 4 and 0xf
|
||||
text[j * 2] = (if (d >= 10) 'a'.code + d - 10 else '0'.code + d).toChar()
|
||||
d = v.toInt() and 0xf
|
||||
text[j * 2 + 1] = (if (d >= 10) 'a'.code + d - 10 else '0'.code + d).toChar()
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
private fun loadCertificates(
|
||||
jarFile: JarFile,
|
||||
je: JarEntry?,
|
||||
readBuffer: ByteArray
|
||||
): Array<Certificate?>? {
|
||||
try {
|
||||
val `is` = jarFile.getInputStream(je)
|
||||
while (`is`.read(readBuffer, 0, readBuffer.size) != -1) {
|
||||
}
|
||||
`is`.close()
|
||||
return je?.certificates as Array<Certificate?>?
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getApkSignInfo(apkFilePath: String): String? {
|
||||
return try {
|
||||
getApkSignV2(apkFilePath)
|
||||
} catch (e: Exception) {
|
||||
getApkSignV1(apkFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkSignV1(apkFilePath: String?): String? {
|
||||
val readBuffer = ByteArray(8192)
|
||||
var certs: Array<Certificate?>? = null
|
||||
try {
|
||||
val jarFile = JarFile(apkFilePath)
|
||||
val entries: Enumeration<*> = jarFile.entries()
|
||||
while (entries.hasMoreElements()) {
|
||||
val je = entries.nextElement() as JarEntry
|
||||
if (je.isDirectory) {
|
||||
continue
|
||||
}
|
||||
if (je.name.startsWith("META-INF/")) {
|
||||
continue
|
||||
}
|
||||
val localCerts = loadCertificates(jarFile, je, readBuffer)
|
||||
if (certs == null) {
|
||||
certs = localCerts
|
||||
} else {
|
||||
for (i in certs.indices) {
|
||||
var found = false
|
||||
for (j in localCerts!!.indices) {
|
||||
if (certs[i] != null && certs[i] == localCerts[j]) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!found || certs.size != localCerts.size) {
|
||||
jarFile.close()
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jarFile.close()
|
||||
return if (certs != null) String(toChars(certs[0]!!.encoded)) else null
|
||||
} catch (ignored: Throwable) {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun getApkSignV2(apkFilePath: String): String {
|
||||
RandomAccessFile(apkFilePath, "r").use { apk ->
|
||||
val buffer = ByteBuffer.allocate(0x10)
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN)
|
||||
apk.seek(apk.length() - 0x6)
|
||||
apk.readFully(buffer.array(), 0x0, 0x6)
|
||||
val offset = buffer.getInt()
|
||||
if (buffer.getShort().toInt() != 0) {
|
||||
throw UnsupportedEncodingException("no zip")
|
||||
}
|
||||
apk.seek((offset - 0x10).toLong())
|
||||
apk.readFully(buffer.array(), 0x0, 0x10)
|
||||
if (!buffer.array().contentEquals(APK_V2_MAGIC.map { it.code.toByte() }.toByteArray())) {
|
||||
throw UnsupportedEncodingException("no apk v2")
|
||||
}
|
||||
|
||||
// Read and compare size fields
|
||||
apk.seek((offset - 0x18).toLong())
|
||||
apk.readFully(buffer.array(), 0x0, 0x8)
|
||||
buffer.rewind()
|
||||
var size = buffer.getLong().toInt()
|
||||
val block = ByteBuffer.allocate(size + 0x8)
|
||||
block.order(ByteOrder.LITTLE_ENDIAN)
|
||||
apk.seek((offset - block.capacity()).toLong())
|
||||
apk.readFully(block.array(), 0x0, block.capacity())
|
||||
if (size.toLong() != block.getLong()) {
|
||||
throw UnsupportedEncodingException("no apk v2")
|
||||
}
|
||||
while (block.remaining() > 24) {
|
||||
size = block.getLong().toInt()
|
||||
if (block.getInt() == 0x7109871a) {
|
||||
// signer-sequence length, signer length, signed data length
|
||||
block.position(block.position() + 12)
|
||||
size = block.getInt() // digests-sequence length
|
||||
|
||||
// digests, certificates length
|
||||
block.position(block.position() + size + 0x4)
|
||||
size = block.getInt() // certificate length
|
||||
break
|
||||
} else {
|
||||
block.position(block.position() + size - 0x4)
|
||||
}
|
||||
}
|
||||
val certificate = ByteArray(size)
|
||||
block[certificate]
|
||||
return String(toChars(certificate))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,4 +21,5 @@ include(":common")
|
||||
include(":core")
|
||||
include(":app")
|
||||
include(":mapper")
|
||||
include(":native")
|
||||
include(":native")
|
||||
include(":manager")
|
||||
Reference in New Issue
Block a user