feat: implement smali patching

Apply the smali patch bundles while patching the APK
This commit is contained in:
rushiiMachine 2024-12-22 14:35:47 -08:00
parent 7a8d434119
commit 562826c2d7
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
12 changed files with 209 additions and 4 deletions

View file

@ -137,7 +137,10 @@ dependencies {
implementation(libs.bouncycastle) implementation(libs.bouncycastle)
implementation(libs.binaryResources) implementation(libs.binaryResources)
implementation(libs.coil) implementation(libs.coil)
implementation(libs.diff)
implementation(libs.microg) implementation(libs.microg)
implementation(libs.smali)
implementation(libs.baksmali)
implementation(variantOf(libs.zip) { artifactType("aar") }) implementation(variantOf(libs.zip) { artifactType("aar") })
} }

View file

@ -2,6 +2,7 @@ package com.aliucord.manager.manager
import android.content.Context import android.content.Context
import android.os.Environment import android.os.Environment
import com.aliucord.manager.network.utils.SemVer
import java.io.File import java.io.File
/** /**
@ -58,7 +59,7 @@ class PathManager(context: Context) {
/** /**
* Resolve a specific path for a cached injector. * Resolve a specific path for a cached injector.
*/ */
fun cachedInjectorDex(aliucordHash: String) = externalCacheDir fun cachedInjectorDex(aliucordHash: SemVer) = externalCacheDir
.resolve("injector").apply { mkdirs() } .resolve("injector").apply { mkdirs() }
.resolve("$aliucordHash.dex") .resolve("$aliucordHash.dex")
@ -69,6 +70,13 @@ class PathManager(context: Context) {
.resolve("aliuhook").apply { mkdirs() } .resolve("aliuhook").apply { mkdirs() }
.resolve("$version.aar") .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 * Singular Kotlin file of the most up-to-date version
* since the stdlib is backwards compatible. * since the stdlib is backwards compatible.

View file

@ -1,5 +1,6 @@
package com.aliucord.manager.network.utils package com.aliucord.manager.network.utils
import androidx.compose.runtime.Immutable
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind 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]) * Parses a Semantic version in the format of `v1.0.0` (v[major].[minor].[patch])
*/ */
@Immutable
@Serializable(SemVer.Serializer::class) @Serializable(SemVer.Serializer::class)
data class SemVer( data class SemVer(
val major: Int, val major: Int,

View file

@ -33,4 +33,9 @@ data class InstallMetadata(
* Version of the injector build that was injected into the APK. * Version of the injector build that was injected into the APK.
*/ */
val injectorVersion: SemVer, val injectorVersion: SemVer,
/**
* Version of the smali patches that were applied onto the APK.
*/
val patchesVersion: SemVer,
) )

View file

@ -22,9 +22,11 @@ class KotlinPatchRunner(options: PatchOptions) : StepRunner() {
DownloadInjectorStep(), DownloadInjectorStep(),
DownloadAliuhookStep(), DownloadAliuhookStep(),
DownloadKotlinStep(), DownloadKotlinStep(),
DownloadPatchesStep(),
CopyDependenciesStep(), CopyDependenciesStep(),
// Patch // Patch
SmaliPatchStep(),
ReplaceIconStep(options), ReplaceIconStep(options),
PatchManifestStep(options), PatchManifestStep(options),
ReorganizeDexStep(), ReorganizeDexStep(),

View file

@ -83,6 +83,7 @@ abstract class DownloadStep : Step(), KoinComponent {
context.showToast(R.string.installer_dl_verify_fail) context.showToast(R.string.installer_dl_verify_fail)
} }
targetFile.delete()
throw t throw t
} }
} }

View file

@ -3,6 +3,7 @@ package com.aliucord.manager.patcher.steps.download
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.manager.PathManager import com.aliucord.manager.manager.PathManager
import com.aliucord.manager.network.utils.SemVer
import com.aliucord.manager.patcher.StepRunner import com.aliucord.manager.patcher.StepRunner
import com.aliucord.manager.patcher.steps.base.DownloadStep import com.aliucord.manager.patcher.steps.base.DownloadStep
import com.aliucord.manager.patcher.steps.base.IDexProvider 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]) * This is populated right before the download starts (ref: [execute])
*/ */
lateinit var targetVersion: String lateinit var targetVersion: SemVer
private set private set
override val localizedName = R.string.patch_step_dl_injector override val localizedName = R.string.patch_step_dl_injector
@ -32,7 +33,7 @@ class DownloadInjectorStep : DownloadStep(), IDexProvider, KoinComponent {
override suspend fun execute(container: StepRunner) { override suspend fun execute(container: StepRunner) {
targetVersion = container.getStep<FetchInfoStep>() targetVersion = container.getStep<FetchInfoStep>()
.data.injectorVersion.toString() .data.injectorVersion
super.execute(container) super.execute(container)
} }

View file

@ -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<FetchInfoStep>()
.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"
}
}

View file

@ -31,13 +31,15 @@ class SaveMetadataStep(private val options: PatchOptions) : Step() {
val apk = container.getStep<CopyDependenciesStep>().patchedApk val apk = container.getStep<CopyDependenciesStep>().patchedApk
val aliuhook = container.getStep<DownloadAliuhookStep>() val aliuhook = container.getStep<DownloadAliuhookStep>()
val injector = container.getStep<DownloadInjectorStep>() val injector = container.getStep<DownloadInjectorStep>()
val patches = container.getStep<DownloadPatchesStep>()
val metadata = InstallMetadata( val metadata = InstallMetadata(
options = options, options = options,
customManager = IS_CUSTOM_BUILD, customManager = IS_CUSTOM_BUILD,
managerVersion = SemVer.parse(BuildConfig.VERSION_NAME), managerVersion = SemVer.parse(BuildConfig.VERSION_NAME),
aliuhookVersion = aliuhook.targetVersion, aliuhookVersion = aliuhook.targetVersion,
injectorVersion = SemVer.parse(injector.targetVersion), injectorVersion = injector.targetVersion,
patchesVersion = patches.targetVersion,
) )
ZipWriter(apk, /* append = */ true).use { ZipWriter(apk, /* append = */ true).use {

View file

@ -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<CopyDependenciesStep>().patchedApk
val patchesZip = container.getStep<DownloadPatchesStep>().targetFile
val patches = mutableListOf<LoadedPatch>()
// 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<String>,
)

View file

@ -109,11 +109,13 @@
<string name="patch_step_dl_kotlin">Downloading Kotlin Stdlib</string> <string name="patch_step_dl_kotlin">Downloading Kotlin Stdlib</string>
<string name="patch_step_dl_injector">Downloading Aliucord Injector</string> <string name="patch_step_dl_injector">Downloading Aliucord Injector</string>
<string name="patch_step_dl_aliuhook">Downloading Aliuhook library</string> <string name="patch_step_dl_aliuhook">Downloading Aliuhook library</string>
<string name="patch_step_dl_smali">Downloading smali patches</string>
<string name="patch_step_copy_deps">Copying dependencies</string> <string name="patch_step_copy_deps">Copying dependencies</string>
<string name="patch_step_patch_manifests">Patching apk manifests</string> <string name="patch_step_patch_manifests">Patching apk manifests</string>
<string name="patch_step_patch_icon">Patching app icon</string> <string name="patch_step_patch_icon">Patching app icon</string>
<string name="patch_step_add_injector">Adding Aliucord injector</string> <string name="patch_step_add_injector">Adding Aliucord injector</string>
<string name="patch_step_add_aliuhook">Adding Aliuhook library</string> <string name="patch_step_add_aliuhook">Adding Aliuhook library</string>
<string name="patch_step_patch_smali">Applying smali patches</string>
<string name="patch_step_reorganize_dex">Adding .dex files to APK</string> <string name="patch_step_reorganize_dex">Adding .dex files to APK</string>
<string name="patch_step_save_metadata">Store metadata</string> <string name="patch_step_save_metadata">Store metadata</string>
<string name="patch_step_alignment">Aligning APKs</string> <string name="patch_step_alignment">Aligning APKs</string>

View file

@ -14,12 +14,14 @@ compose = "1.5.4"
compose-compiler = "1.5.8" compose-compiler = "1.5.8"
compose-tracing = "1.0.0-beta01" compose-tracing = "1.0.0-beta01"
compose-material3 = "1.2.0-rc01" compose-material3 = "1.2.0-rc01"
diff = "4.12"
koin = "3.5.3" koin = "3.5.3"
kotlin = "1.9.22" kotlin = "1.9.22"
kotlinx-immutable = "0.3.7" kotlinx-immutable = "0.3.7"
kotlinx-serialization = "1.5.0" kotlinx-serialization = "1.5.0"
ktor = "2.3.7" ktor = "2.3.7"
microg = "0.3.2.240913" microg = "0.3.2.240913"
smali = "3.0.7"
voyager = "1.0.0" voyager = "1.0.0"
zip = "2.1.1" 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" } binaryResources = { module = "com.aliucord:binary-resources", version.ref = "binary-resources" }
bouncycastle = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bouncycastle" } bouncycastle = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bouncycastle" }
coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" } 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" } 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 zip = { module = "io.github.diamondminer88:zip-android", version.ref = "zip" } # Only use the "aar" artifact
[bundles] [bundles]