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:
topjohnwu 2020-08-31 03:39:20 -07:00
parent 38a34a7eeb
commit fbaf2bded6
6 changed files with 167 additions and 48 deletions

View File

@ -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)
} }

View File

@ -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

View 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)
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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"