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

View File

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

View File

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.widget.Toast import android.widget.Toast
import androidx.annotation.WorkerThread
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.StubApk import com.topjohnwu.magisk.StubApk
@ -28,6 +29,7 @@ import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
import java.security.SecureRandom import java.security.SecureRandom
import kotlin.random.asKotlinRandom
object HideAPK { object HideAPK {
@ -37,6 +39,7 @@ object HideAPK {
// Some arbitrary limit // Some arbitrary limit
const val MAX_LABEL_LENGTH = 32 const val MAX_LABEL_LENGTH = 32
const val PLACEHOLDER = "COMPONENT_PLACEHOLDER"
private fun genPackageName(): String { private fun genPackageName(): String {
val random = SecureRandom() val random = SecureRandom()
@ -61,7 +64,61 @@ object HideAPK {
return builder.toString() 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, context: Context,
apk: File, out: OutputStream, apk: File, out: OutputStream,
pkg: String, label: CharSequence pkg: String, label: CharSequence
@ -72,12 +129,15 @@ object HideAPK {
JarMap.open(apk, true).use { jar -> JarMap.open(apk, true).use { jar ->
val je = jar.getJarEntry(ANDROID_MANIFEST) val je = jar.getJarEntry(ANDROID_MANIFEST)
val xml = AXML(jar.getRawData(je)) val xml = AXML(jar.getRawData(je))
val generator = classNameGenerator()
if (!xml.patchStrings { if (!xml.patchStrings {
for (i in it.indices) { for (i in it.indices) {
val s = it[i] val s = it[i]
if (s.contains(APPLICATION_ID)) { if (s.contains(APPLICATION_ID)) {
it[i] = s.replace(APPLICATION_ID, pkg) it[i] = s.replace(APPLICATION_ID, pkg)
} else if (s.contains(PLACEHOLDER)) {
it[i] = generator.next()
} else if (s == origLabel) { } else if (s == origLabel) {
it[i] = label.toString() it[i] = label.toString()
} }
@ -193,4 +253,17 @@ object HideAPK {
} }
if (!success) onFailure.run() 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.forEach
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import com.topjohnwu.magisk.MainDirections import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R 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.Info
import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.model.module.LocalModule 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.databinding.ActivityMainMd2Binding
import com.topjohnwu.magisk.ktx.startAnimations import com.topjohnwu.magisk.ktx.startAnimations
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.magisk.view.Shortcuts
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File import java.io.File
class MainViewModel : BaseViewModel() class MainViewModel : BaseViewModel()
@ -59,6 +63,7 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
setContentView() setContentView()
showUnsupportedMessage() showUnsupportedMessage()
askForHomeShortcut() askForHomeShortcut()
checkStubComponent()
// Ask permission to post notifications for background update check // Ask permission to post notifications for background update check
if (Config.checkUpdate) { if (Config.checkUpdate) {
@ -227,4 +232,22 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
}.show() }.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( cmpList.add(
""" """
|<provider |<provider
| android:name="%s" | android:name="x.COMPONENT_PLACEHOLDER_0"
| android:authorities="${'$'}{applicationId}.provider" | android:authorities="${'$'}{applicationId}.provider"
| android:directBootAware="true" | android:directBootAware="true"
| android:exported="false" | android:exported="false"
@ -92,7 +92,7 @@ fun genStubManifest(srcDir: File, outDir: File): String {
cmpList.add( cmpList.add(
""" """
|<receiver |<receiver
| android:name="%s" | android:name="x.COMPONENT_PLACEHOLDER_1"
| android:exported="false"> | android:exported="false">
| <intent-filter> | <intent-filter>
| <action android:name="android.intent.action.LOCALE_CHANGED" /> | <action android:name="android.intent.action.LOCALE_CHANGED" />
@ -111,7 +111,7 @@ fun genStubManifest(srcDir: File, outDir: File): String {
cmpList.add( cmpList.add(
""" """
|<activity |<activity
| android:name="%s" | android:name="x.COMPONENT_PLACEHOLDER_2"
| android:exported="true"> | android:exported="true">
| <intent-filter> | <intent-filter>
| <action android:name="android.intent.action.MAIN" /> | <action android:name="android.intent.action.MAIN" />
@ -123,7 +123,7 @@ fun genStubManifest(srcDir: File, outDir: File): String {
cmpList.add( cmpList.add(
""" """
|<activity |<activity
| android:name="%s" | android:name="x.COMPONENT_PLACEHOLDER_3"
| android:directBootAware="true" | android:directBootAware="true"
| android:exported="false" | android:exported="false"
| android:taskAffinity="" | android:taskAffinity=""
@ -138,56 +138,49 @@ fun genStubManifest(srcDir: File, outDir: File): String {
cmpList.add( cmpList.add(
""" """
|<service |<service
| android:name="%s" | android:name="x.COMPONENT_PLACEHOLDER_4"
| android:exported="false" />""".ind(2) | android:exported="false" />""".ind(2)
) )
cmpList.add( cmpList.add(
""" """
|<service |<service
| android:name="%s" | android:name="x.COMPONENT_PLACEHOLDER_5"
| android:exported="false" | android:exported="false"
| android:permission="android.permission.BIND_JOB_SERVICE" />""".ind(2) | android:permission="android.permission.BIND_JOB_SERVICE" />""".ind(2)
) )
val names = mutableListOf<String>() val classNameGenerator = sequence {
names.addAll(c1) fun notJavaKeyword(name: String) = when (name) {
names.addAll(c2.subList(0, 10)) "do", "if", "for", "int", "new", "try" -> false
names.addAll(c3.subList(0, 10)) else -> true
names.shuffle(RANDOM) }
val pkgNames = names fun List<String>.process() = asSequence()
// Distinct by lower case to support case insensitive file systems .filter(::notJavaKeyword)
.distinctBy { it.toLowerCase(Locale.ROOT) } // Distinct by lower case to support case insensitive file systems
// Old Android does not support capitalized package names .distinctBy { it.toLowerCase(Locale.ROOT) }
// Check Android 7.0.0 PackageParser#buildClassName
.map { it.decapitalize(Locale.ROOT) }
fun isJavaKeyword(name: String) = when (name) { val names = mutableListOf<String>()
"do", "if", "for", "int", "new", "try" -> true names.addAll(c1)
else -> false names.addAll(c2.process().take(30))
} names.addAll(c3.process().take(30))
names.shuffle(RANDOM)
val cmps = mutableListOf<String>() while (true) {
val usedNames = mutableListOf<String>() 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 { fun genClass(type: String): String {
var pkgName: String val clzName = classNameGenerator.next()
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()
val (pkg, name) = clzName.split('.') val (pkg, name) = clzName.split('.')
val pkgDir = File(outDir, pkg) val pkgDir = File(outDir, pkg)
pkgDir.mkdirs() pkgDir.mkdirs()
@ -195,22 +188,18 @@ fun genStubManifest(srcDir: File, outDir: File): String {
it.println("package $pkg;") it.println("package $pkg;")
it.println("public class $name extends com.topjohnwu.magisk.$type {}") it.println("public class $name extends com.topjohnwu.magisk.$type {}")
} }
return clzName
} }
// Generate 2 non redirect-able classes // Generate 2 non redirect-able classes
genClass("DelegateComponentFactory") val factory = genClass("DelegateComponentFactory")
genClass("DelegateApplication") val app = genClass("DelegateApplication")
for (gen in cmpList) {
val name = genCmpName()
cmps.add(gen.format(name))
}
// Shuffle the order of the components // Shuffle the order of the components
cmps.shuffle(RANDOM) cmpList.shuffle(RANDOM)
val xml = File(srcDir, "AndroidManifest.xml").readText() 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) { fun genEncryptedResources(res: InputStream, outDir: File) {