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.Upgrade
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.writeTo
import com.topjohnwu.superuser.Shell
@ -18,7 +18,7 @@ import java.io.File
private fun Context.patch(apk: File) {
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()
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.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.Info
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.ktx.get
import com.topjohnwu.magisk.ktx.writeTo
@ -24,8 +26,6 @@ import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.SecureRandom
object PatchAPK {
@ -36,13 +36,15 @@ object PatchAPK {
private const val APP_ID = "com.topjohnwu.magisk"
private const val APP_NAME = "Magisk Manager"
private fun genPackageName(prefix: String, length: Int): CharSequence {
val builder = StringBuilder(length)
builder.append(prefix)
val len = length - prefix.length
// Some arbitrary limit
const val MAX_LABEL_LENGTH = 32
private fun genPackageName(): CharSequence {
val random = SecureRandom()
val len = 5 + random.nextInt(15)
val builder = StringBuilder(len)
var next: Char
var prev = prefix[prefix.length - 1]
var prev = 0.toChar()
for (i in 0 until len) {
next = if (prev == '.' || i == len - 1) {
ALPHA[random.nextInt(ALPHA.length)]
@ -52,49 +54,30 @@ object PatchAPK {
builder.append(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
}
private fun findAndPatch(xml: ByteArray, from: CharSequence, to: CharSequence): Boolean {
if (to.length > from.length)
return false
val buf = ByteBuffer.wrap(xml).order(ByteOrder.LITTLE_ENDIAN).asCharBuffer()
val offList = mutableListOf<Int>()
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 {
fun patch(
context: Context,
apk: String, out: String,
pkg: CharSequence, label: CharSequence
): Boolean {
try {
val jar = JarMap.open(apk)
val je = jar.getJarEntry(Const.ANDROID_MANIFEST)
val xml = jar.getRawData(je)
val xml = AXML(jar.getRawData(je))
if (!findAndPatch(xml, APP_ID, pkg) ||
!findAndPatch(xml, APP_NAME, label))
if (!xml.findAndPatch(APP_ID to pkg.toString(), APP_NAME to label.toString()))
return false
// Write apk changes
jar.getOutputStream(je).write(xml)
val keys = Keygen(get())
jar.getOutputStream(je).write(xml.bytes)
val keys = Keygen(context)
SignApk.sign(keys.cert, keys.key, jar, FileOutputStream(out))
} catch (e: Exception) {
Timber.e(e)
@ -124,10 +107,10 @@ object PatchAPK {
// Generate a new random package name and signature
val repack = File(context.cacheDir, "patched.apk")
val pkg = genPackageName("com.", APP_ID.length)
val pkg = genPackageName()
Config.keyStoreRaw = ""
if (!patch(src, repack.path, pkg, label))
if (!patch(context, src, repack.path, pkg, label))
return false
// 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.Info
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.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.availableLocales
@ -88,9 +89,12 @@ object Hide : BaseSettingsItem.Input() {
var result = "Manager"
set(value) = set(value, field, { field = it }, BR.result, BR.error)
val maxLength
get() = PatchAPK.MAX_LABEL_LENGTH
@get:Bindable
val isError
get() = result.length > 14 || result.isBlank()
get() = result.length > maxLength || result.isBlank()
override fun getView(context: Context) = DialogSettingsAppNameBinding
.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.Subject
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.events.AddHomeIconEvent
import com.topjohnwu.magisk.events.RecreateEvent

View File

@ -25,7 +25,7 @@
android:hint="@string/settings_app_name_hint"
app:boxStrokeColor="?colorOnSurfaceVariant"
app:counterEnabled="true"
app:counterMaxLength="14"
app:counterMaxLength="@{data.maxLength}"
app:counterOverflowTextColor="?colorError"
app:error="@{data.error ? @string/settings_app_name_error : @string/empty}"
app:errorEnabled="true"