From 562826c2d7dcf7e59f100f784d9d45705a9d57f9 Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:35:47 -0800 Subject: [PATCH] feat: implement smali patching Apply the smali patch bundles while patching the APK --- app/build.gradle.kts | 3 + .../aliucord/manager/manager/PathManager.kt | 10 +- .../aliucord/manager/network/utils/SemVer.kt | 2 + .../manager/patcher/InstallMetadata.kt | 5 + .../manager/patcher/KotlinPatchRunner.kt | 2 + .../patcher/steps/base/DownloadStep.kt | 1 + .../steps/download/DownloadInjectorStep.kt | 5 +- .../steps/download/DownloadPatchesStep.kt | 45 ++++++ .../patcher/steps/patch/SaveMetadataStep.kt | 4 +- .../patcher/steps/patch/SmaliPatchStep.kt | 129 ++++++++++++++++++ app/src/main/res/values/strings.xml | 2 + gradle/libs.versions.toml | 5 + 12 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadPatchesStep.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SmaliPatchStep.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6b2baa4..d00a3a3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -137,7 +137,10 @@ dependencies { implementation(libs.bouncycastle) implementation(libs.binaryResources) implementation(libs.coil) + implementation(libs.diff) implementation(libs.microg) + implementation(libs.smali) + implementation(libs.baksmali) implementation(variantOf(libs.zip) { artifactType("aar") }) } diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt index 8de3741..8d70353 100644 --- a/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt @@ -2,6 +2,7 @@ package com.aliucord.manager.manager import android.content.Context import android.os.Environment +import com.aliucord.manager.network.utils.SemVer import java.io.File /** @@ -58,7 +59,7 @@ class PathManager(context: Context) { /** * Resolve a specific path for a cached injector. */ - fun cachedInjectorDex(aliucordHash: String) = externalCacheDir + fun cachedInjectorDex(aliucordHash: SemVer) = externalCacheDir .resolve("injector").apply { mkdirs() } .resolve("$aliucordHash.dex") @@ -69,6 +70,13 @@ class PathManager(context: Context) { .resolve("aliuhook").apply { mkdirs() } .resolve("$version.aar") + /** + * Resolve a specific path for a versioned smali patches archive. + */ + fun cachedSmaliPatches(version: SemVer) = externalCacheDir + .resolve("patches").apply { mkdirs() } + .resolve("$version.zip") + /** * Singular Kotlin file of the most up-to-date version * since the stdlib is backwards compatible. diff --git a/app/src/main/kotlin/com/aliucord/manager/network/utils/SemVer.kt b/app/src/main/kotlin/com/aliucord/manager/network/utils/SemVer.kt index 87492dc..315c2cd 100644 --- a/app/src/main/kotlin/com/aliucord/manager/network/utils/SemVer.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/utils/SemVer.kt @@ -1,5 +1,6 @@ package com.aliucord.manager.network.utils +import androidx.compose.runtime.Immutable import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -10,6 +11,7 @@ import kotlinx.serialization.encoding.Encoder /** * Parses a Semantic version in the format of `v1.0.0` (v[major].[minor].[patch]) */ +@Immutable @Serializable(SemVer.Serializer::class) data class SemVer( val major: Int, diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/InstallMetadata.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/InstallMetadata.kt index 92bcd2f..40806fe 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/InstallMetadata.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/InstallMetadata.kt @@ -33,4 +33,9 @@ data class InstallMetadata( * Version of the injector build that was injected into the APK. */ val injectorVersion: SemVer, + + /** + * Version of the smali patches that were applied onto the APK. + */ + val patchesVersion: SemVer, ) diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/KotlinPatchRunner.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/KotlinPatchRunner.kt index f04c1a4..fe19524 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/KotlinPatchRunner.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/KotlinPatchRunner.kt @@ -22,9 +22,11 @@ class KotlinPatchRunner(options: PatchOptions) : StepRunner() { DownloadInjectorStep(), DownloadAliuhookStep(), DownloadKotlinStep(), + DownloadPatchesStep(), CopyDependenciesStep(), // Patch + SmaliPatchStep(), ReplaceIconStep(options), PatchManifestStep(options), ReorganizeDexStep(), diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt index 7986eac..8922acd 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt @@ -83,6 +83,7 @@ abstract class DownloadStep : Step(), KoinComponent { context.showToast(R.string.installer_dl_verify_fail) } + targetFile.delete() throw t } } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadInjectorStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadInjectorStep.kt index 69f6291..ba6d121 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadInjectorStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadInjectorStep.kt @@ -3,6 +3,7 @@ package com.aliucord.manager.patcher.steps.download import androidx.compose.runtime.Stable import com.aliucord.manager.R import com.aliucord.manager.manager.PathManager +import com.aliucord.manager.network.utils.SemVer import com.aliucord.manager.patcher.StepRunner import com.aliucord.manager.patcher.steps.base.DownloadStep import com.aliucord.manager.patcher.steps.base.IDexProvider @@ -22,7 +23,7 @@ class DownloadInjectorStep : DownloadStep(), IDexProvider, KoinComponent { /** * This is populated right before the download starts (ref: [execute]) */ - lateinit var targetVersion: String + lateinit var targetVersion: SemVer private set override val localizedName = R.string.patch_step_dl_injector @@ -32,7 +33,7 @@ class DownloadInjectorStep : DownloadStep(), IDexProvider, KoinComponent { override suspend fun execute(container: StepRunner) { targetVersion = container.getStep() - .data.injectorVersion.toString() + .data.injectorVersion super.execute(container) } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadPatchesStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadPatchesStep.kt new file mode 100644 index 0000000..d825ab7 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadPatchesStep.kt @@ -0,0 +1,45 @@ +package com.aliucord.manager.patcher.steps.download + +import androidx.compose.runtime.Stable +import com.aliucord.manager.R +import com.aliucord.manager.manager.OverlayManager +import com.aliucord.manager.manager.PathManager +import com.aliucord.manager.network.utils.SemVer +import com.aliucord.manager.patcher.StepRunner +import com.aliucord.manager.patcher.steps.base.DownloadStep +import com.aliucord.manager.patcher.steps.prepare.FetchInfoStep +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +/** + * Download a zip of all the smali patches to be applied to the APK during patching. + */ +@Stable +class DownloadPatchesStep : DownloadStep(), KoinComponent { + private val paths: PathManager by inject() + private val overlays: OverlayManager by inject() + + /** + * This is populated right before the download starts (ref: [execute]) + */ + lateinit var targetVersion: SemVer + private set + + override val localizedName = R.string.patch_step_dl_smali + override val targetUrl get() = URL + override val targetFile get() = paths.cachedSmaliPatches(targetVersion) + + override suspend fun execute(container: StepRunner) { + targetVersion = container.getStep() + .data.patchesVersion + + super.execute(container) + } + + private companion object { + const val ORG = "Aliucord" + const val MAIN_REPO = "Aliucord" + + const val URL = "https://raw.githubusercontent.com/$ORG/$MAIN_REPO/builds/patches.zip" + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SaveMetadataStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SaveMetadataStep.kt index f557fa9..d4afb64 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SaveMetadataStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SaveMetadataStep.kt @@ -31,13 +31,15 @@ class SaveMetadataStep(private val options: PatchOptions) : Step() { val apk = container.getStep().patchedApk val aliuhook = container.getStep() val injector = container.getStep() + val patches = container.getStep() val metadata = InstallMetadata( options = options, customManager = IS_CUSTOM_BUILD, managerVersion = SemVer.parse(BuildConfig.VERSION_NAME), aliuhookVersion = aliuhook.targetVersion, - injectorVersion = SemVer.parse(injector.targetVersion), + injectorVersion = injector.targetVersion, + patchesVersion = patches.targetVersion, ) ZipWriter(apk, /* append = */ true).use { diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SmaliPatchStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SmaliPatchStep.kt new file mode 100644 index 0000000..ad3d9ac --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SmaliPatchStep.kt @@ -0,0 +1,129 @@ +package com.aliucord.manager.patcher.steps.patch + +import com.aliucord.manager.R +import com.aliucord.manager.manager.PathManager +import com.aliucord.manager.patcher.StepRunner +import com.aliucord.manager.patcher.steps.StepGroup +import com.aliucord.manager.patcher.steps.base.IDexProvider +import com.aliucord.manager.patcher.steps.base.Step +import com.aliucord.manager.patcher.steps.download.CopyDependenciesStep +import com.aliucord.manager.patcher.steps.download.DownloadPatchesStep +import com.android.tools.smali.baksmali.Baksmali +import com.android.tools.smali.baksmali.BaksmaliOptions +import com.android.tools.smali.dexlib2.Opcodes +import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile +import com.android.tools.smali.smali.Smali +import com.android.tools.smali.smali.SmaliOptions +import com.github.diamondminer88.zip.ZipReader +import com.github.difflib.DiffUtils +import com.github.difflib.UnifiedDiffUtils +import com.github.difflib.patch.Patch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import java.io.FileNotFoundException +import kotlin.io.path.Path +import kotlin.io.path.writeLines + +class SmaliPatchStep : Step(), IDexProvider, KoinComponent { + private val paths: PathManager by inject() + + override val group = StepGroup.Patch + override val localizedName = R.string.patch_step_patch_smali + + private val coreCount = Runtime.getRuntime().availableProcessors() + private val smaliDir = paths.patchingWorkingDir().resolve("smali") + private val outDex = smaliDir.resolve("patched.dex") + + override suspend fun execute(container: StepRunner) { + val apk = container.getStep().patchedApk + val patchesZip = container.getStep().targetFile + + val patches = mutableListOf() + + // Load and parse all the patches from the smali patch archive + ZipReader(patchesZip).use { zip -> + for (patchFile in zip.entryNames) { + if (!patchFile.endsWith(".patch")) continue + + val lines = zip.openEntry(patchFile)!!.read() + .decodeToString() + .split('\n') + + try { + val patch = LoadedPatch( + fullClassName = patchFile.removeSuffix(".patch"), + patch = UnifiedDiffUtils.parseUnifiedDiff(lines), + ) + patches.add(patch) + } catch (t: Throwable) { + throw Error("Failed to parse patch file $patchFile", t) + } + } + } + + // Disassemble all the classes we have patches for from all the dex files + ZipReader(apk).use { zip -> + for (file in zip.entryNames) { + if (!file.endsWith(".dex")) continue + + val dexFile = try { + DexBackedDexFile( + /* opcodes = */ Opcodes.getDefault(), + /* buf = */ zip.openEntry(file)!!.read(), + ) + } catch (t: Throwable) { + throw Error("Failed to parse dex $file", t) + } + + val result = try { + Baksmali.disassembleDexFile( + /* dexFile = */ dexFile, + /* outputDir = */ smaliDir, + /* jobs = */ coreCount - 1, + /* options = */ BaksmaliOptions(), + /* classes = */ patches.map { "L${it.fullClassName};" }, + ) + } catch (t: Throwable) { + throw Error("Failed to disassemble dex $file", t) + } + + assert(result) { "Failed to disassemble dex $file (unknown reason)" } + } + } + + // Apply all the patches to the smali files + for ((fullClassName, patch) in patches) { + val smaliFile = smaliDir.resolve("$fullClassName.smali") + if (!smaliFile.exists()) { + throw FileNotFoundException("Target smali file $fullClassName.smali not found for patching!") + } + + val patched = try { + DiffUtils.patch(smaliFile.readText().split('\n'), patch) + } catch (t: Throwable) { + throw Error("Failed to smali patch $fullClassName", t) + } + + Path(smaliFile.absolutePath).writeLines(patched) + } + + // Assemble the patched classes back into a single dex + smaliDir.mkdir() + Smali.assemble( + SmaliOptions().apply { + this.jobs = coreCount - 1 + this.outputDexFile = outDex.absolutePath + }, + listOf(smaliDir.absolutePath), + ) + } + + override val dexPriority = 2 + override val dexCount = 1 + override fun getDexFiles() = listOf(outDex.readBytes()) +} + +private data class LoadedPatch( + val fullClassName: String, + val patch: Patch, +) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 94269b6..f89e261 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -109,11 +109,13 @@ Downloading Kotlin Stdlib Downloading Aliucord Injector Downloading Aliuhook library + Downloading smali patches Copying dependencies Patching apk manifests Patching app icon Adding Aliucord injector Adding Aliuhook library + Applying smali patches Adding .dex files to APK Store metadata Aligning APKs diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 66ed93e..70dc188 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,12 +14,14 @@ compose = "1.5.4" compose-compiler = "1.5.8" compose-tracing = "1.0.0-beta01" compose-material3 = "1.2.0-rc01" +diff = "4.12" koin = "3.5.3" kotlin = "1.9.22" kotlinx-immutable = "0.3.7" kotlinx-serialization = "1.5.0" ktor = "2.3.7" microg = "0.3.2.240913" +smali = "3.0.7" voyager = "1.0.0" zip = "2.1.1" @@ -73,7 +75,10 @@ axml = { module = "com.aliucord:axml", version.ref = "axml" } binaryResources = { module = "com.aliucord:binary-resources", version.ref = "binary-resources" } bouncycastle = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bouncycastle" } coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" } +diff = { module = "io.github.java-diff-utils:java-diff-utils", version.ref = "diff" } microg = { module = "org.microg.gms:play-services-safetynet", version.ref = "microg" } +smali = { module = "com.android.tools.smali:smali", version.ref = "smali" } +baksmali = { module = "com.android.tools.smali:smali-baksmali", version.ref = "smali" } zip = { module = "io.github.diamondminer88:zip-android", version.ref = "zip" } # Only use the "aar" artifact [bundles]