mirror of
https://github.com/topjohnwu/Magisk.git
synced 2024-12-26 18:27:38 +00:00
Patch AndroidManifest.xml properly
Parse and rebuild the string pool of the AXML format for patching string in AndroidManifest.xml
This commit is contained in:
parent
38a34a7eeb
commit
fbaf2bded6
@ -10,7 +10,7 @@ import com.topjohnwu.magisk.core.Info
|
|||||||
import com.topjohnwu.magisk.core.download.Action.APK.Restore
|
import com.topjohnwu.magisk.core.download.Action.APK.Restore
|
||||||
import com.topjohnwu.magisk.core.download.Action.APK.Upgrade
|
import com.topjohnwu.magisk.core.download.Action.APK.Upgrade
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.core.utils.PatchAPK
|
import com.topjohnwu.magisk.core.tasks.PatchAPK
|
||||||
import com.topjohnwu.magisk.ktx.relaunchApp
|
import com.topjohnwu.magisk.ktx.relaunchApp
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.ktx.writeTo
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
@ -18,7 +18,7 @@ import java.io.File
|
|||||||
|
|
||||||
private fun Context.patch(apk: File) {
|
private fun Context.patch(apk: File) {
|
||||||
val patched = File(apk.parent, "patched.apk")
|
val patched = File(apk.parent, "patched.apk")
|
||||||
PatchAPK.patch(apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
|
PatchAPK.patch(this, apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
|
||||||
apk.delete()
|
apk.delete()
|
||||||
patched.renameTo(apk)
|
patched.renameTo(apk)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.tasks
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
@ -8,6 +8,8 @@ import com.topjohnwu.magisk.core.Config
|
|||||||
import com.topjohnwu.magisk.core.Const
|
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.utils.AXML
|
||||||
|
import com.topjohnwu.magisk.core.utils.Keygen
|
||||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.ktx.get
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.ktx.writeTo
|
||||||
@ -24,8 +26,6 @@ import timber.log.Timber
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
|
||||||
object PatchAPK {
|
object PatchAPK {
|
||||||
@ -36,13 +36,15 @@ object PatchAPK {
|
|||||||
private const val APP_ID = "com.topjohnwu.magisk"
|
private const val APP_ID = "com.topjohnwu.magisk"
|
||||||
private const val APP_NAME = "Magisk Manager"
|
private const val APP_NAME = "Magisk Manager"
|
||||||
|
|
||||||
private fun genPackageName(prefix: String, length: Int): CharSequence {
|
// Some arbitrary limit
|
||||||
val builder = StringBuilder(length)
|
const val MAX_LABEL_LENGTH = 32
|
||||||
builder.append(prefix)
|
|
||||||
val len = length - prefix.length
|
private fun genPackageName(): CharSequence {
|
||||||
val random = SecureRandom()
|
val random = SecureRandom()
|
||||||
|
val len = 5 + random.nextInt(15)
|
||||||
|
val builder = StringBuilder(len)
|
||||||
var next: Char
|
var next: Char
|
||||||
var prev = prefix[prefix.length - 1]
|
var prev = 0.toChar()
|
||||||
for (i in 0 until len) {
|
for (i in 0 until len) {
|
||||||
next = if (prev == '.' || i == len - 1) {
|
next = if (prev == '.' || i == len - 1) {
|
||||||
ALPHA[random.nextInt(ALPHA.length)]
|
ALPHA[random.nextInt(ALPHA.length)]
|
||||||
@ -52,49 +54,30 @@ object PatchAPK {
|
|||||||
builder.append(next)
|
builder.append(next)
|
||||||
prev = next
|
prev = next
|
||||||
}
|
}
|
||||||
|
if (!builder.contains('.')) {
|
||||||
|
// Pick a random index and set it as dot
|
||||||
|
val idx = random.nextInt(len - 1)
|
||||||
|
builder[idx] = '.'
|
||||||
|
}
|
||||||
return builder
|
return builder
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findAndPatch(xml: ByteArray, from: CharSequence, to: CharSequence): Boolean {
|
fun patch(
|
||||||
if (to.length > from.length)
|
context: Context,
|
||||||
return false
|
apk: String, out: String,
|
||||||
val buf = ByteBuffer.wrap(xml).order(ByteOrder.LITTLE_ENDIAN).asCharBuffer()
|
pkg: CharSequence, label: CharSequence
|
||||||
val offList = mutableListOf<Int>()
|
): Boolean {
|
||||||
var i = 0
|
|
||||||
loop@ while (i < buf.length - from.length) {
|
|
||||||
for (j in from.indices) {
|
|
||||||
if (buf.get(i + j) != from[j]) {
|
|
||||||
++i
|
|
||||||
continue@loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
offList.add(i)
|
|
||||||
i += from.length
|
|
||||||
}
|
|
||||||
if (offList.isEmpty())
|
|
||||||
return false
|
|
||||||
|
|
||||||
val toBuf = to.toString().toCharArray().copyOf(from.length)
|
|
||||||
for (off in offList) {
|
|
||||||
buf.position(off)
|
|
||||||
buf.put(toBuf)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun patch(apk: String, out: String, pkg: CharSequence, label: CharSequence): Boolean {
|
|
||||||
try {
|
try {
|
||||||
val jar = JarMap.open(apk)
|
val jar = JarMap.open(apk)
|
||||||
val je = jar.getJarEntry(Const.ANDROID_MANIFEST)
|
val je = jar.getJarEntry(Const.ANDROID_MANIFEST)
|
||||||
val xml = jar.getRawData(je)
|
val xml = AXML(jar.getRawData(je))
|
||||||
|
|
||||||
if (!findAndPatch(xml, APP_ID, pkg) ||
|
if (!xml.findAndPatch(APP_ID to pkg.toString(), APP_NAME to label.toString()))
|
||||||
!findAndPatch(xml, APP_NAME, label))
|
|
||||||
return false
|
return false
|
||||||
|
|
||||||
// Write apk changes
|
// Write apk changes
|
||||||
jar.getOutputStream(je).write(xml)
|
jar.getOutputStream(je).write(xml.bytes)
|
||||||
val keys = Keygen(get())
|
val keys = Keygen(context)
|
||||||
SignApk.sign(keys.cert, keys.key, jar, FileOutputStream(out))
|
SignApk.sign(keys.cert, keys.key, jar, FileOutputStream(out))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
@ -124,10 +107,10 @@ object PatchAPK {
|
|||||||
|
|
||||||
// Generate a new random package name and signature
|
// Generate a new random package name and signature
|
||||||
val repack = File(context.cacheDir, "patched.apk")
|
val repack = File(context.cacheDir, "patched.apk")
|
||||||
val pkg = genPackageName("com.", APP_ID.length)
|
val pkg = genPackageName()
|
||||||
Config.keyStoreRaw = ""
|
Config.keyStoreRaw = ""
|
||||||
|
|
||||||
if (!patch(src, repack.path, pkg, label))
|
if (!patch(context, src, repack.path, pkg, label))
|
||||||
return false
|
return false
|
||||||
|
|
||||||
// Install the application
|
// Install the application
|
132
app/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt
Normal file
132
app/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder.LITTLE_ENDIAN
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class AXML(b: ByteArray) {
|
||||||
|
|
||||||
|
var bytes = b
|
||||||
|
private set
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CHUNK_SIZE_OFF = 4
|
||||||
|
private const val STRING_INDICES_OFF = 7 * 4
|
||||||
|
private val UTF_16LE = Charset.forName("UTF-16LE")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String pool header:
|
||||||
|
* 0: 0x1C0001
|
||||||
|
* 1: chunk size
|
||||||
|
* 2: number of strings
|
||||||
|
* 3: number of styles (assert as 0)
|
||||||
|
* 4: flags
|
||||||
|
* 5: offset to string data
|
||||||
|
* 6: offset to style data (assert as 0)
|
||||||
|
*
|
||||||
|
* Followed by an array of uint32_t with size = number of strings
|
||||||
|
* Each entry points to an offset into the string data
|
||||||
|
*/
|
||||||
|
fun findAndPatch(vararg patterns: Pair<String, String>): Boolean {
|
||||||
|
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
|
||||||
|
|
||||||
|
fun findStringPool(): Int {
|
||||||
|
var offset = 8
|
||||||
|
while (offset < bytes.size) {
|
||||||
|
if (buffer.getInt(offset) == 0x1C0001)
|
||||||
|
return offset
|
||||||
|
offset += buffer.getInt(offset + CHUNK_SIZE_OFF)
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
var patch = false
|
||||||
|
val start = findStringPool()
|
||||||
|
if (start < 0)
|
||||||
|
return false
|
||||||
|
|
||||||
|
// Read header
|
||||||
|
buffer.position(start + 4)
|
||||||
|
val intBuf = buffer.asIntBuffer()
|
||||||
|
val size = intBuf.get()
|
||||||
|
val count = intBuf.get()
|
||||||
|
intBuf.get()
|
||||||
|
intBuf.get()
|
||||||
|
val dataOff = start + intBuf.get()
|
||||||
|
intBuf.get()
|
||||||
|
|
||||||
|
val strings = ArrayList<String>(count)
|
||||||
|
// Read and patch all strings
|
||||||
|
loop@ for (i in 0 until count) {
|
||||||
|
val off = dataOff + intBuf.get()
|
||||||
|
val len = buffer.getShort(off)
|
||||||
|
val str = String(bytes, off + 2, len * 2, UTF_16LE)
|
||||||
|
for ((from, to) in patterns) {
|
||||||
|
if (str.contains(from)) {
|
||||||
|
strings.add(str.replace(from, to))
|
||||||
|
patch = true
|
||||||
|
continue@loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strings.add(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!patch)
|
||||||
|
return false
|
||||||
|
|
||||||
|
// Write everything before string data, will patch values later
|
||||||
|
val baos = RawByteStream()
|
||||||
|
baos.write(bytes, 0, dataOff)
|
||||||
|
|
||||||
|
val strList = IntArray(count)
|
||||||
|
for (i in 0 until count) {
|
||||||
|
strList[i] = baos.size() - dataOff
|
||||||
|
val str = strings[i]
|
||||||
|
baos.write(str.length.toShortBytes())
|
||||||
|
baos.write(str.toByteArray(UTF_16LE))
|
||||||
|
// Null terminate
|
||||||
|
baos.write(0)
|
||||||
|
baos.write(0)
|
||||||
|
}
|
||||||
|
baos.align()
|
||||||
|
|
||||||
|
val sizeDiff = baos.size() - start - size
|
||||||
|
val newBuffer = ByteBuffer.wrap(baos.buf).order(LITTLE_ENDIAN)
|
||||||
|
|
||||||
|
// Patch XML size
|
||||||
|
newBuffer.putInt(CHUNK_SIZE_OFF, buffer.getInt(CHUNK_SIZE_OFF) + sizeDiff)
|
||||||
|
// Patch string pool size
|
||||||
|
newBuffer.putInt(start + CHUNK_SIZE_OFF, size + sizeDiff)
|
||||||
|
// Patch index table
|
||||||
|
newBuffer.position(start + STRING_INDICES_OFF)
|
||||||
|
val newStrList = newBuffer.asIntBuffer()
|
||||||
|
for (idx in strList)
|
||||||
|
newStrList.put(idx)
|
||||||
|
|
||||||
|
// Write the rest of the chunks
|
||||||
|
val nextOff = start + size
|
||||||
|
baos.write(bytes, nextOff, bytes.size - nextOff)
|
||||||
|
|
||||||
|
bytes = baos.toByteArray()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.toShortBytes(): ByteArray {
|
||||||
|
val b = ByteBuffer.allocate(2).order(LITTLE_ENDIAN)
|
||||||
|
b.putShort(this.toShort())
|
||||||
|
return b.array()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RawByteStream : ByteArrayOutputStream() {
|
||||||
|
val buf get() = buf
|
||||||
|
|
||||||
|
fun align(alignment: Int = 4) {
|
||||||
|
val newCount = (count + alignment - 1) / alignment * alignment
|
||||||
|
for (i in 0 until (newCount - count))
|
||||||
|
write(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ import com.topjohnwu.magisk.core.Config
|
|||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.UpdateCheckService
|
import com.topjohnwu.magisk.core.UpdateCheckService
|
||||||
|
import com.topjohnwu.magisk.core.tasks.PatchAPK
|
||||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.core.utils.availableLocales
|
import com.topjohnwu.magisk.core.utils.availableLocales
|
||||||
@ -88,9 +89,12 @@ object Hide : BaseSettingsItem.Input() {
|
|||||||
var result = "Manager"
|
var result = "Manager"
|
||||||
set(value) = set(value, field, { field = it }, BR.result, BR.error)
|
set(value) = set(value, field, { field = it }, BR.result, BR.error)
|
||||||
|
|
||||||
|
val maxLength
|
||||||
|
get() = PatchAPK.MAX_LABEL_LENGTH
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
val isError
|
val isError
|
||||||
get() = result.length > 14 || result.isBlank()
|
get() = result.length > maxLength || result.isBlank()
|
||||||
|
|
||||||
override fun getView(context: Context) = DialogSettingsAppNameBinding
|
override fun getView(context: Context) = DialogSettingsAppNameBinding
|
||||||
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
|
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
|
||||||
|
@ -19,7 +19,7 @@ import com.topjohnwu.magisk.core.download.Action
|
|||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.core.download.DownloadService
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.core.utils.PatchAPK
|
import com.topjohnwu.magisk.core.tasks.PatchAPK
|
||||||
import com.topjohnwu.magisk.data.database.RepoDao
|
import com.topjohnwu.magisk.data.database.RepoDao
|
||||||
import com.topjohnwu.magisk.events.AddHomeIconEvent
|
import com.topjohnwu.magisk.events.AddHomeIconEvent
|
||||||
import com.topjohnwu.magisk.events.RecreateEvent
|
import com.topjohnwu.magisk.events.RecreateEvent
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
android:hint="@string/settings_app_name_hint"
|
android:hint="@string/settings_app_name_hint"
|
||||||
app:boxStrokeColor="?colorOnSurfaceVariant"
|
app:boxStrokeColor="?colorOnSurfaceVariant"
|
||||||
app:counterEnabled="true"
|
app:counterEnabled="true"
|
||||||
app:counterMaxLength="14"
|
app:counterMaxLength="@{data.maxLength}"
|
||||||
app:counterOverflowTextColor="?colorError"
|
app:counterOverflowTextColor="?colorError"
|
||||||
app:error="@{data.error ? @string/settings_app_name_error : @string/empty}"
|
app:error="@{data.error ? @string/settings_app_name_error : @string/empty}"
|
||||||
app:errorEnabled="true"
|
app:errorEnabled="true"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user