From fbaf2bded6a4e94c6a0d927139b710e6fcec913e Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Mon, 31 Aug 2020 03:39:20 -0700 Subject: [PATCH] Patch AndroidManifest.xml properly Parse and rebuild the string pool of the AXML format for patching string in AndroidManifest.xml --- .../magisk/core/download/ManagerHandler.kt | 4 +- .../magisk/core/{utils => tasks}/PatchAPK.kt | 69 ++++----- .../com/topjohnwu/magisk/core/utils/AXML.kt | 132 ++++++++++++++++++ .../magisk/ui/settings/SettingsItems.kt | 6 +- .../magisk/ui/settings/SettingsViewModel.kt | 2 +- .../res/layout/dialog_settings_app_name.xml | 2 +- 6 files changed, 167 insertions(+), 48 deletions(-) rename app/src/main/java/com/topjohnwu/magisk/core/{utils => tasks}/PatchAPK.kt (69%) create mode 100644 app/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerHandler.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerHandler.kt index d9ef99b47..516553616 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerHandler.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerHandler.kt @@ -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) } diff --git a/app/src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/PatchAPK.kt similarity index 69% rename from app/src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt rename to app/src/main/java/com/topjohnwu/magisk/core/tasks/PatchAPK.kt index 7c1e6fd4d..9b8b7d0ae 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/PatchAPK.kt @@ -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() - 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt new file mode 100644 index 000000000..39469c332 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt @@ -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): 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(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) + } + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt index ffef1f644..3111453a3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt @@ -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 diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt index 424819b21..eead2a620 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt @@ -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 diff --git a/app/src/main/res/layout/dialog_settings_app_name.xml b/app/src/main/res/layout/dialog_settings_app_name.xml index eb2266465..dc0b830e2 100644 --- a/app/src/main/res/layout/dialog_settings_app_name.xml +++ b/app/src/main/res/layout/dialog_settings_app_name.xml @@ -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"