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.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") })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 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 {
|
||||||
|
|
|
||||||
|
|
@ -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_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>
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue