mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-13 05:37:48 +02:00
feat: LSPatch obfuscation
This commit is contained in:
@ -64,6 +64,10 @@ class SnapEnhance {
|
||||
}
|
||||
runCatching {
|
||||
LSPatchUpdater.onBridgeConnected(appContext, bridgeClient)
|
||||
}.onFailure {
|
||||
logCritical("Failed to init LSPatchUpdater", it)
|
||||
}
|
||||
runCatching {
|
||||
measureTimeMillis {
|
||||
runBlocking {
|
||||
init(this)
|
||||
|
@ -5,8 +5,8 @@ import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.Hooker
|
||||
|
||||
class DeviceSpooferHook: Feature("device_spoofer", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||
override fun asyncOnActivityCreate() {
|
||||
class DeviceSpooferHook: Feature("device_spoofer", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
override fun init() {
|
||||
if (context.config.experimental.spoof.globalState != true) return
|
||||
|
||||
val fingerprint by context.config.experimental.spoof.device.fingerprint
|
||||
|
@ -18,11 +18,24 @@ object LSPatchUpdater {
|
||||
}
|
||||
|
||||
fun onBridgeConnected(context: ModContext, bridgeClient: BridgeClient) {
|
||||
val obfuscatedModulePath by lazy {
|
||||
(runCatching {
|
||||
context::class.java.classLoader?.loadClass("org.lsposed.lspatch.share.Constants")
|
||||
}.getOrNull())?.declaredFields?.firstOrNull { it.name == "MANAGER_PACKAGE_NAME" }?.also {
|
||||
it.isAccessible = true
|
||||
}?.get(null) as? String
|
||||
}
|
||||
|
||||
val embeddedModule = context.androidContext.cacheDir
|
||||
.resolve("lspatch")
|
||||
.resolve(BuildConfig.APPLICATION_ID).let { moduleDir ->
|
||||
if (!moduleDir.exists()) return@let null
|
||||
moduleDir.listFiles()?.firstOrNull { it.extension == "apk" }
|
||||
} ?: obfuscatedModulePath?.let { path ->
|
||||
context.androidContext.cacheDir.resolve(path).let dir@{ moduleDir ->
|
||||
if (!moduleDir.exists()) return@dir null
|
||||
moduleDir.listFiles()?.firstOrNull { it.extension == "apk" }
|
||||
} ?: return
|
||||
} ?: return
|
||||
|
||||
context.log.verbose("Found embedded SE at ${embeddedModule.absolutePath}", TAG)
|
||||
|
@ -80,6 +80,7 @@ dependencies {
|
||||
implementation(libs.libsu)
|
||||
implementation(libs.guava)
|
||||
implementation(libs.apksig)
|
||||
implementation(libs.dexlib2)
|
||||
implementation(libs.gson)
|
||||
implementation(libs.jsoup)
|
||||
implementation(libs.okhttp)
|
||||
|
@ -21,4 +21,7 @@ class SharedConfig(
|
||||
|
||||
var useRootInstaller get() = sharedPreferences.getBoolean("useRootInstaller", false)
|
||||
set(value) = sharedPreferences.edit().putBoolean("useRootInstaller", value).apply()
|
||||
|
||||
var obfuscateLSPatch get() = sharedPreferences.getBoolean("obfuscateLSPatch", false)
|
||||
set(value) = sharedPreferences.edit().putBoolean("obfuscateLSPatch", value).apply()
|
||||
}
|
@ -10,7 +10,6 @@ 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
|
||||
@ -22,28 +21,22 @@ import java.security.cert.X509Certificate
|
||||
import java.util.zip.ZipFile
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
//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 obfuscate: Boolean,
|
||||
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 {
|
||||
private fun patchManifest(data: ByteArray, lspatchMetadata: Pair<String, String>): ByteArray {
|
||||
val property = ModificationProperty()
|
||||
|
||||
property.addApplicationAttribute(AttributeItem("appComponentFactory", PROXY_APP_COMPONENT_FACTORY))
|
||||
property.addMetaData(ModificationProperty.MetaData("lspatch", lspatchMetadata))
|
||||
property.addMetaData(ModificationProperty.MetaData(lspatchMetadata.first, lspatchMetadata.second))
|
||||
|
||||
return ByteArrayOutputStream().apply {
|
||||
ManifestEditor(ByteArrayInputStream(data), this, property).processManifest()
|
||||
@ -70,7 +63,7 @@ class LSPatch(
|
||||
|
||||
private fun resignApk(inputApkFile: File, outputFile: File) {
|
||||
printLog("Resigning ${inputApkFile.absolutePath} to ${outputFile.absolutePath}")
|
||||
val dstZFile = ZFile.openReadWrite(outputFile, Z_FILE_OPTIONS)
|
||||
val dstZFile = ZFile.openReadWrite(outputFile, ZFileOptions())
|
||||
val inZFile = ZFile.openReadOnly(inputApkFile)
|
||||
|
||||
inZFile.entries().forEach { entry ->
|
||||
@ -90,12 +83,42 @@ class LSPatch(
|
||||
printLog("Done")
|
||||
}
|
||||
|
||||
private fun uniqueHash(): String {
|
||||
return Random.nextBytes(Random.nextInt(5, 10)).joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
@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 obfuscationCacheFolder = File(context.cacheDir, "lspatch").apply {
|
||||
if (exists()) deleteRecursively()
|
||||
mkdirs()
|
||||
}
|
||||
val lspatchObfuscation = LSPatchObfuscation(obfuscationCacheFolder) { printLog(it) }
|
||||
val dexObfuscationConfig = if (obfuscate) DexObfuscationConfig(
|
||||
packageName = uniqueHash(),
|
||||
metadataManifestField = uniqueHash(),
|
||||
metaLoaderFilePath = uniqueHash(),
|
||||
configFilePath = uniqueHash(),
|
||||
loaderFilePath = uniqueHash(),
|
||||
libNativeFilePath = mapOf(
|
||||
"arm64-v8a" to uniqueHash() + ".so",
|
||||
"armeabi-v7a" to uniqueHash() + ".so",
|
||||
),
|
||||
originApkPath = uniqueHash(),
|
||||
cachedOriginApkPath = uniqueHash(),
|
||||
openAtApkPath = uniqueHash(),
|
||||
assetModuleFolderPath = uniqueHash(),
|
||||
) else null
|
||||
|
||||
val dstZFile = ZFile.openReadWrite(outputFile, ZFileOptions().setAlignmentRule(
|
||||
AlignmentRules.compose(
|
||||
AlignmentRules.constantForSuffix(".so", 4096),
|
||||
AlignmentRules.constantForSuffix("assets/" + (dexObfuscationConfig?.originApkPath ?: "lspatch/origin.apk"), 4096)
|
||||
)
|
||||
))
|
||||
|
||||
val patchConfig = PatchConfig(
|
||||
useManager = false,
|
||||
@ -115,32 +138,37 @@ class LSPatch(
|
||||
|
||||
printLog("Patching manifest")
|
||||
|
||||
val sourceApkFile = dstZFile.addNestedZip({ "assets/" + (dexObfuscationConfig?.originApkPath ?: "lspatch/origin.apk") }, inputApkFile, false)
|
||||
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()))
|
||||
val patchedManifestData = patchManifest(inputStream.readBytes(), (dexObfuscationConfig?.metadataManifestField ?: "lspatch") to Base64.encode(patchConfig.toByteArray()))
|
||||
dstZFile.add("AndroidManifest.xml", patchedManifestData.inputStream())
|
||||
}
|
||||
|
||||
//add config
|
||||
printLog("Adding config")
|
||||
dstZFile.add("assets/lspatch/config.json", ByteArrayInputStream(patchConfig.toByteArray()))
|
||||
dstZFile.add("assets/" + (dexObfuscationConfig?.configFilePath ?: "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"))
|
||||
printLog("Adding loader dex")
|
||||
context.assets.open("lspatch/dexes/loader.dex").use { inputStream ->
|
||||
dstZFile.add("assets/" + (dexObfuscationConfig?.loaderFilePath ?: "lspatch/loader.dex"), dexObfuscationConfig?.let {
|
||||
lspatchObfuscation.obfuscateLoader(inputStream, it).inputStream()
|
||||
} ?: inputStream)
|
||||
}
|
||||
|
||||
//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)
|
||||
dstZFile.add("assets/${dexObfuscationConfig?.libNativeFilePath?.get(native) ?: "lspatch/so/$native/liblspatch.so"}", context.assets.open("lspatch/so/$native/liblspatch.so"), false)
|
||||
}
|
||||
|
||||
//embed modules
|
||||
printLog("Embedding modules")
|
||||
modules.forEach { (packageName, module) ->
|
||||
printLog("- $packageName")
|
||||
dstZFile.add("assets/lspatch/modules/$packageName.apk", module.inputStream())
|
||||
val obfuscatedPackageName = dexObfuscationConfig?.packageName ?: packageName
|
||||
printLog("- $obfuscatedPackageName")
|
||||
dstZFile.add("assets/${dexObfuscationConfig?.assetModuleFolderPath ?: "lspatch/modules"}/$obfuscatedPackageName.apk", module.inputStream())
|
||||
}
|
||||
|
||||
// link apk entries
|
||||
@ -148,7 +176,7 @@ class LSPatch(
|
||||
|
||||
for (entry in sourceApkFile.entries()) {
|
||||
val name = entry.centralDirectoryHeader.name
|
||||
if (name.startsWith("classes") && name.endsWith(".dex")) continue
|
||||
if (dexObfuscationConfig == null && 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(
|
||||
@ -158,8 +186,20 @@ class LSPatch(
|
||||
sourceApkFile.addFileLink(name, name)
|
||||
}
|
||||
|
||||
printLog("Adding meta loader dex")
|
||||
context.assets.open("lspatch/dexes/metaloader.dex").use { inputStream ->
|
||||
dstZFile.add(dexObfuscationConfig?.let { "classes9.dex" } ?: "classes.dex", dexObfuscationConfig?.let {
|
||||
lspatchObfuscation.obfuscateMetaLoader(inputStream, it).inputStream()
|
||||
} ?: inputStream)
|
||||
}
|
||||
|
||||
printLog("Writing apk")
|
||||
dstZFile.realign()
|
||||
dstZFile.close()
|
||||
sourceApkFile.close()
|
||||
|
||||
printLog("Cleaning obfuscation cache")
|
||||
obfuscationCacheFolder.deleteRecursively()
|
||||
printLog("Done")
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,107 @@
|
||||
package me.rhunk.snapenhance.manager.lspatch
|
||||
|
||||
import org.jf.dexlib2.Opcodes
|
||||
import org.jf.dexlib2.dexbacked.DexBackedDexFile
|
||||
import org.jf.dexlib2.iface.reference.StringReference
|
||||
import org.jf.dexlib2.writer.io.FileDataStore
|
||||
import org.jf.dexlib2.writer.pool.DexPool
|
||||
import org.jf.dexlib2.writer.pool.StringPool
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
data class DexObfuscationConfig(
|
||||
val packageName: String,
|
||||
val metadataManifestField: String? = null,
|
||||
val metaLoaderFilePath: String? = null,
|
||||
val configFilePath: String? = null,
|
||||
val loaderFilePath: String? = null,
|
||||
val originApkPath: String? = null,
|
||||
val cachedOriginApkPath: String? = null,
|
||||
val openAtApkPath: String? = null,
|
||||
val assetModuleFolderPath: String? = null,
|
||||
val libNativeFilePath: Map<String, String> = mapOf(),
|
||||
)
|
||||
|
||||
class LSPatchObfuscation(
|
||||
private val cacheFolder: File,
|
||||
private val printLog: (String) -> Unit = { println(it) }
|
||||
) {
|
||||
private fun obfuscateDexFile(dexStrings: Map<String, String?>, inputStream: InputStream): File {
|
||||
val dexFile = DexBackedDexFile.fromInputStream(Opcodes.forApi(29), BufferedInputStream(inputStream))
|
||||
|
||||
val dexPool = object: DexPool(dexFile.opcodes) {
|
||||
override fun getSectionProvider(): SectionProvider {
|
||||
val dexPool = this
|
||||
return object: DexPoolSectionProvider() {
|
||||
override fun getStringSection() = object: StringPool(dexPool) {
|
||||
private val cacheMap = mutableMapOf<String, String>()
|
||||
|
||||
override fun intern(string: CharSequence) {
|
||||
dexStrings[string.toString()]?.let {
|
||||
cacheMap[string.toString()] = it
|
||||
printLog("mapping $string to $it")
|
||||
super.intern(it)
|
||||
return
|
||||
}
|
||||
super.intern(string)
|
||||
}
|
||||
|
||||
override fun getItemIndex(key: CharSequence): Int {
|
||||
return cacheMap[key.toString()]?.let {
|
||||
internedItems[it]
|
||||
} ?: super.getItemIndex(key)
|
||||
}
|
||||
|
||||
override fun getItemIndex(key: StringReference): Int {
|
||||
return cacheMap[key.toString()]?.let {
|
||||
internedItems[it]
|
||||
} ?: super.getItemIndex(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dexFile.classes.forEach { dexBackedClassDef ->
|
||||
dexPool.internClass(dexBackedClassDef)
|
||||
}
|
||||
val outputFile = File.createTempFile("obf", ".dex", cacheFolder)
|
||||
dexPool.writeTo(FileDataStore(outputFile))
|
||||
return outputFile
|
||||
}
|
||||
|
||||
|
||||
fun obfuscateMetaLoader(inputStream: InputStream, config: DexObfuscationConfig): File {
|
||||
return obfuscateDexFile(mapOf(
|
||||
"assets/lspatch/config.json" to "assets/${config.configFilePath}",
|
||||
"assets/lspatch/loader.dex" to "assets/${config.loaderFilePath}",
|
||||
) + (config.libNativeFilePath.takeIf { it.isNotEmpty() }?.let {
|
||||
mapOf(
|
||||
"!/assets/lspatch/so/" to "!/assets/",
|
||||
"assets/lspatch/so/" to "assets/",
|
||||
"/liblspatch.so" to "",
|
||||
"arm64-v8a" to config.libNativeFilePath["arm64-v8a"],
|
||||
"armeabi-v7a" to config.libNativeFilePath["armeabi-v7a"],
|
||||
"x86" to config.libNativeFilePath["x86"],
|
||||
"x86_64" to config.libNativeFilePath["x86_64"],
|
||||
)
|
||||
} ?: mapOf()), inputStream)
|
||||
}
|
||||
|
||||
fun obfuscateLoader(inputStream: InputStream, config: DexObfuscationConfig): File {
|
||||
return obfuscateDexFile(mapOf(
|
||||
"assets/lspatch/config.json" to config.configFilePath?.let { "assets/$it" },
|
||||
"assets/lspatch/loader.dex" to config.loaderFilePath?.let { "assets/$it" },
|
||||
"assets/lspatch/metaloader.dex" to config.metaLoaderFilePath?.let { "assets/$it" },
|
||||
"assets/lspatch/origin.apk" to config.originApkPath?.let { "assets/$it" },
|
||||
"/lspatch/origin/" to config.cachedOriginApkPath?.let { "/$it/" }, // context.getCacheDir() + ==> "/lspatch/origin/" <== + sourceFile.getEntry(ORIGINAL_APK_ASSET_PATH).getCrc() + ".apk";
|
||||
"/lspatch/" to config.cachedOriginApkPath?.let { "/$it/" }, // context.getCacheDir() + "/lspatch/" + packageName + "/"
|
||||
"cache/lspatch/origin/" to config.cachedOriginApkPath?.let { "cache/$it" }, //LSPApplication => Path originPath = Paths.get(appInfo.dataDir, "cache/lspatch/origin/");
|
||||
"assets/lspatch/modules/" to config.assetModuleFolderPath?.let { "assets/$it/" }, // Constants.java => EMBEDDED_MODULES_ASSET_PATH
|
||||
"lspatch/modules" to config.assetModuleFolderPath, // LocalApplicationService.java => context.getAssets().list("lspatch/modules"),
|
||||
"lspatch/modules/" to config.assetModuleFolderPath?.let { "$it/" }, // LocalApplicationService.java => try (var is = context.getAssets().open("lspatch/modules/" + name)) {
|
||||
"lspatch" to config.metadataManifestField, // SigBypass.java => "lspatch",
|
||||
"org.lsposed.lspatch" to config.cachedOriginApkPath?.let { "$it/${config.packageName}/" }, // Constants.java => "org.lsposed.lspatch", (Used in LSPatchUpdater.kt)
|
||||
), inputStream)
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ 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"
|
||||
}
|
@ -145,6 +145,11 @@ class SettingsTab : Tab("settings", isPrimary = true, icon = Icons.Default.Setti
|
||||
setValue = { sharedConfig.useRootInstaller = it },
|
||||
label = "Use root installer"
|
||||
)
|
||||
ConfigBooleanRow(
|
||||
getValue = { sharedConfig.obfuscateLSPatch },
|
||||
setValue = { sharedConfig.obfuscateLSPatch = it },
|
||||
label = "Obfuscate LSPatch (experimental)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.manager.data.APKMirror
|
||||
import me.rhunk.snapenhance.manager.data.DownloadItem
|
||||
@ -85,7 +86,7 @@ class LSPatchTab : Tab("lspatch") {
|
||||
sharedConfig.snapEnhancePackageName to module,
|
||||
), printLog = {
|
||||
log("[LSPatch] $it")
|
||||
})
|
||||
}, obfuscate = sharedConfig.obfuscateLSPatch)
|
||||
|
||||
log("== Patching apk ==")
|
||||
val outputFiles = lsPatch.patchSplits(listOf(apkFile!!))
|
||||
@ -138,6 +139,12 @@ class LSPatchTab : Tab("lspatch") {
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
fun triggerInstallation(shouldUninstall: Boolean) {
|
||||
|
Reference in New Issue
Block a user