Dynamically generate component names at runtime

This commit is contained in:
topjohnwu 2022-08-26 06:20:10 -07:00
parent 71b0c8b42b
commit 357d913f18
5 changed files with 143 additions and 61 deletions

View File

@ -34,9 +34,11 @@ abstract class BaseActivity : AppCompatActivity() {
permissionCallback?.invoke(it)
permissionCallback = null
}
private var installCallback: ((Boolean) -> Unit)? = null
private val requestInstall = registerForActivityResult(RequestInstall()) {
permissionCallback?.invoke(it)
permissionCallback = null
installCallback?.invoke(it)
installCallback = null
}
private var contentCallback: ContentResultCallback? = null
@ -93,10 +95,11 @@ abstract class BaseActivity : AppCompatActivity() {
callback(true)
return
}
permissionCallback = callback
if (permission == REQUEST_INSTALL_PACKAGES) {
installCallback = callback
requestInstall.launch(Unit)
} else {
permissionCallback = callback
requestPermission.launch(permission)
}
}

View File

@ -101,15 +101,9 @@ class DownloadService : NotificationService() {
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
// Patch and install
val session = APKInstall.startSession(this)
session.openStream(this).use {
val label = applicationInfo.nonLocalizedLabel
if (!HideAPK.patch(this, apk, it, packageName, label)) {
throw IOException("HideAPK patch error")
}
}
subject.intent = HideAPK.upgrade(this, apk) ?:
throw IOException("HideAPK patch error")
apk.delete()
subject.intent = session.waitIntent()
} else {
ActivityTracker.foreground?.let {
// Relaunch the process if we are foreground

View File

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.annotation.WorkerThread
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.StubApk
@ -28,6 +29,7 @@ import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.security.SecureRandom
import kotlin.random.asKotlinRandom
object HideAPK {
@ -37,6 +39,7 @@ object HideAPK {
// Some arbitrary limit
const val MAX_LABEL_LENGTH = 32
const val PLACEHOLDER = "COMPONENT_PLACEHOLDER"
private fun genPackageName(): String {
val random = SecureRandom()
@ -61,7 +64,61 @@ object HideAPK {
return builder.toString()
}
fun patch(
private fun classNameGenerator() = sequence {
val c1 = mutableListOf<String>()
val c2 = mutableListOf<String>()
val c3 = mutableListOf<String>()
val random = SecureRandom()
val kRandom = random.asKotlinRandom()
fun <T> chain(vararg iters: Iterable<T>) = sequence {
iters.forEach { it.forEach { v -> yield(v) } }
}
for (a in chain('a'..'z', 'A'..'Z')) {
if (a != 'a' && a != 'A') {
c1.add("$a")
}
for (b in chain('a'..'z', 'A'..'Z', '0'..'9')) {
c2.add("$a$b")
for (c in chain('a'..'z', 'A'..'Z', '0'..'9')) {
c3.add("$a$b$c")
}
}
}
c1.shuffle(random)
c2.shuffle(random)
c3.shuffle(random)
fun notJavaKeyword(name: String) = when (name) {
"do", "if", "for", "int", "new", "try" -> false
else -> true
}
fun List<String>.process() = asSequence().filter(::notJavaKeyword)
val names = mutableListOf<String>()
names.addAll(c1)
names.addAll(c2.process().take(30))
names.addAll(c3.process().take(30))
while (true) {
val seg = 2 + random.nextInt(4)
val cls = StringBuilder()
for (i in 0 until seg) {
cls.append(names.random(kRandom))
if (i != seg - 1)
cls.append('.')
}
// Old Android does not support capitalized package names
// Check Android 7.0.0 PackageParser#buildClassName
cls[0] = cls[0].lowercaseChar()
yield(cls.toString())
}
}.distinct().iterator()
private fun patch(
context: Context,
apk: File, out: OutputStream,
pkg: String, label: CharSequence
@ -72,12 +129,15 @@ object HideAPK {
JarMap.open(apk, true).use { jar ->
val je = jar.getJarEntry(ANDROID_MANIFEST)
val xml = AXML(jar.getRawData(je))
val generator = classNameGenerator()
if (!xml.patchStrings {
for (i in it.indices) {
val s = it[i]
if (s.contains(APPLICATION_ID)) {
it[i] = s.replace(APPLICATION_ID, pkg)
} else if (s.contains(PLACEHOLDER)) {
it[i] = generator.next()
} else if (s == origLabel) {
it[i] = label.toString()
}
@ -193,4 +253,17 @@ object HideAPK {
}
if (!success) onFailure.run()
}
@WorkerThread
fun upgrade(context: Context, apk: File): Intent? {
val label = context.applicationInfo.nonLocalizedLabel
val pkg = context.packageName
val session = APKInstall.startSession(context)
session.openStream(context).use {
if (!patch(context, apk, it, pkg, label)) {
return null
}
}
return session.waitIntent()
}
}

View File

@ -12,6 +12,7 @@ import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.forEach
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
@ -22,12 +23,15 @@ import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
import com.topjohnwu.magisk.ktx.startAnimations
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Shortcuts
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
class MainViewModel : BaseViewModel()
@ -59,6 +63,7 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
setContentView()
showUnsupportedMessage()
askForHomeShortcut()
checkStubComponent()
// Ask permission to post notifications for background update check
if (Config.checkUpdate) {
@ -227,4 +232,22 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
}.show()
}
}
@SuppressLint("InlinedApi")
private fun checkStubComponent() {
if (intent.component?.className?.contains(HideAPK.PLACEHOLDER) == true) {
// The stub APK was not properly patched, re-apply our changes
withPermission(Manifest.permission.REQUEST_INSTALL_PACKAGES) { granted ->
if (granted) {
lifecycleScope.launch(Dispatchers.IO) {
val apk = File(applicationInfo.sourceDir)
HideAPK.upgrade(this@MainActivity, apk)?.let {
startActivity(it)
}
}
}
}
}
}
}

View File

@ -82,7 +82,7 @@ fun genStubManifest(srcDir: File, outDir: File): String {
cmpList.add(
"""
|<provider
| android:name="%s"
| android:name="x.COMPONENT_PLACEHOLDER_0"
| android:authorities="${'$'}{applicationId}.provider"
| android:directBootAware="true"
| android:exported="false"
@ -92,7 +92,7 @@ fun genStubManifest(srcDir: File, outDir: File): String {
cmpList.add(
"""
|<receiver
| android:name="%s"
| android:name="x.COMPONENT_PLACEHOLDER_1"
| android:exported="false">
| <intent-filter>
| <action android:name="android.intent.action.LOCALE_CHANGED" />
@ -111,7 +111,7 @@ fun genStubManifest(srcDir: File, outDir: File): String {
cmpList.add(
"""
|<activity
| android:name="%s"
| android:name="x.COMPONENT_PLACEHOLDER_2"
| android:exported="true">
| <intent-filter>
| <action android:name="android.intent.action.MAIN" />
@ -123,7 +123,7 @@ fun genStubManifest(srcDir: File, outDir: File): String {
cmpList.add(
"""
|<activity
| android:name="%s"
| android:name="x.COMPONENT_PLACEHOLDER_3"
| android:directBootAware="true"
| android:exported="false"
| android:taskAffinity=""
@ -138,56 +138,49 @@ fun genStubManifest(srcDir: File, outDir: File): String {
cmpList.add(
"""
|<service
| android:name="%s"
| android:name="x.COMPONENT_PLACEHOLDER_4"
| android:exported="false" />""".ind(2)
)
cmpList.add(
"""
|<service
| android:name="%s"
| android:name="x.COMPONENT_PLACEHOLDER_5"
| android:exported="false"
| android:permission="android.permission.BIND_JOB_SERVICE" />""".ind(2)
)
val names = mutableListOf<String>()
names.addAll(c1)
names.addAll(c2.subList(0, 10))
names.addAll(c3.subList(0, 10))
names.shuffle(RANDOM)
val classNameGenerator = sequence {
fun notJavaKeyword(name: String) = when (name) {
"do", "if", "for", "int", "new", "try" -> false
else -> true
}
val pkgNames = names
// Distinct by lower case to support case insensitive file systems
.distinctBy { it.toLowerCase(Locale.ROOT) }
// Old Android does not support capitalized package names
// Check Android 7.0.0 PackageParser#buildClassName
.map { it.decapitalize(Locale.ROOT) }
fun List<String>.process() = asSequence()
.filter(::notJavaKeyword)
// Distinct by lower case to support case insensitive file systems
.distinctBy { it.toLowerCase(Locale.ROOT) }
fun isJavaKeyword(name: String) = when (name) {
"do", "if", "for", "int", "new", "try" -> true
else -> false
}
val names = mutableListOf<String>()
names.addAll(c1)
names.addAll(c2.process().take(30))
names.addAll(c3.process().take(30))
names.shuffle(RANDOM)
val cmps = mutableListOf<String>()
val usedNames = mutableListOf<String>()
while (true) {
val cls = StringBuilder()
cls.append(names.random(kRANDOM))
cls.append('.')
cls.append(names.random(kRANDOM))
// Old Android does not support capitalized package names
// Check Android 7.0.0 PackageParser#buildClassName
cls[0] = cls[0].toLowerCase()
yield(cls.toString())
}
}.distinct().iterator()
fun genCmpName(): String {
var pkgName: String
do {
pkgName = pkgNames.random(kRANDOM)
} while (isJavaKeyword(pkgName))
var clzName: String
do {
clzName = names.random(kRANDOM)
} while (isJavaKeyword(clzName))
val cmp = "${pkgName}.${clzName}"
usedNames.add(cmp)
return cmp
}
fun genClass(type: String) {
val clzName = genCmpName()
fun genClass(type: String): String {
val clzName = classNameGenerator.next()
val (pkg, name) = clzName.split('.')
val pkgDir = File(outDir, pkg)
pkgDir.mkdirs()
@ -195,22 +188,18 @@ fun genStubManifest(srcDir: File, outDir: File): String {
it.println("package $pkg;")
it.println("public class $name extends com.topjohnwu.magisk.$type {}")
}
return clzName
}
// Generate 2 non redirect-able classes
genClass("DelegateComponentFactory")
genClass("DelegateApplication")
for (gen in cmpList) {
val name = genCmpName()
cmps.add(gen.format(name))
}
val factory = genClass("DelegateComponentFactory")
val app = genClass("DelegateApplication")
// Shuffle the order of the components
cmps.shuffle(RANDOM)
cmpList.shuffle(RANDOM)
val xml = File(srcDir, "AndroidManifest.xml").readText()
return xml.format(usedNames[0], usedNames[1], cmps.joinToString("\n\n"))
return xml.format(factory, app, cmpList.joinToString("\n\n"))
}
fun genEncryptedResources(res: InputStream, outDir: File) {