feat: implement smali patching
Apply the smali patch bundles while patching the APK
This commit is contained in:
parent
7a8d434119
commit
562826c2d7
12 changed files with 209 additions and 4 deletions
|
|
@ -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") })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ class KotlinPatchRunner(options: PatchOptions) : StepRunner() {
|
|||
DownloadInjectorStep(),
|
||||
DownloadAliuhookStep(),
|
||||
DownloadKotlinStep(),
|
||||
DownloadPatchesStep(),
|
||||
CopyDependenciesStep(),
|
||||
|
||||
// Patch
|
||||
SmaliPatchStep(),
|
||||
ReplaceIconStep(options),
|
||||
PatchManifestStep(options),
|
||||
ReorganizeDexStep(),
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ abstract class DownloadStep : Step(), KoinComponent {
|
|||
context.showToast(R.string.installer_dl_verify_fail)
|
||||
}
|
||||
|
||||
targetFile.delete()
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FetchInfoStep>()
|
||||
.data.injectorVersion.toString()
|
||||
.data.injectorVersion
|
||||
|
||||
super.execute(container)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -31,13 +31,15 @@ class SaveMetadataStep(private val options: PatchOptions) : Step() {
|
|||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
||||
val aliuhook = container.getStep<DownloadAliuhookStep>()
|
||||
val injector = container.getStep<DownloadInjectorStep>()
|
||||
val patches = container.getStep<DownloadPatchesStep>()
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -109,11 +109,13 @@
|
|||
<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_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_patch_manifests">Patching apk manifests</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_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_save_metadata">Store metadata</string>
|
||||
<string name="patch_step_alignment">Aligning APKs</string>
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue