Skip to content

Commit 8730094

Browse files
committed
Add DEX signature verification and public key utility
Introduces signature verification for DEX files in WebUIConfigBaseLoader using a new WXUPublicKey utility. DEX files are now checked for a valid RSA signature, and loading is blocked if verification fails or if blocked packages are detected. Adds WXUPublicKey.kt to provide the public key for signature verification.
1 parent 6c6f5ae commit 8730094

File tree

2 files changed

+107
-12
lines changed

2 files changed

+107
-12
lines changed

webui/src/main/kotlin/com/dergoogler/mmrl/webui/model/Config.kt

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import com.dergoogler.mmrl.webui.R
3232
import com.dergoogler.mmrl.webui.__webui__adapters__
3333
import com.dergoogler.mmrl.webui.activity.WXActivity
3434
import com.dergoogler.mmrl.webui.interfaces.WXInterface
35-
import com.dergoogler.mmrl.webui.view.WebUIView
35+
import com.dergoogler.mmrl.webui.util.WXUPublicKey
3636
import com.squareup.moshi.Json
3737
import com.squareup.moshi.JsonClass
3838
import dalvik.system.BaseDexClassLoader
@@ -42,8 +42,10 @@ import kotlinx.coroutines.flow.StateFlow
4242
import org.jf.dexlib2.dexbacked.DexBackedDexFile
4343
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
4444
import java.io.InputStream
45-
import java.lang.System.console
4645
import java.nio.ByteBuffer
46+
import java.nio.ByteOrder
47+
import java.security.PublicKey
48+
import java.security.Signature
4749
import java.util.concurrent.ConcurrentHashMap
4850
import java.util.regex.Pattern
4951

@@ -194,25 +196,33 @@ open class WebUIConfigBaseLoader() {
194196
): BaseDexClassLoader? {
195197
val file = SuFile(modId.webrootDir, dexPath)
196198

197-
if (!file.isFile || file.extension != "dex") {
198-
Log.e(TAG, "Provided path is not a valid .dex file: ${file.path}")
199+
if (!file.isFile || !file.extension.equals("dex", ignoreCase = true)) {
200+
Log.e(TAG, "Invalid .dex file: ${file.path}")
199201
return null
200202
}
201203

202-
// Using InMemoryDexClassLoader is efficient if DEX files are not excessively large.
203-
val dexFileBytes = file.readBytes()
204-
val str = SuFileInputStream(file).use { it.buffered() }
205-
if (isBlocked(str)) {
206-
return null
204+
val dexFile = loadSignedDex(file, WXUPublicKey)
205+
206+
if (!dexFile.official) {
207+
SuFileInputStream(file).use { stream ->
208+
if (isBlocked(stream.buffered())) {
209+
Log.w(TAG, "Blocked dex loading: ${file.path}")
210+
return null
211+
}
212+
}
207213
}
208214

209-
return InMemoryDexClassLoader(ByteBuffer.wrap(dexFileBytes), context.classLoader)
215+
return InMemoryDexClassLoader(
216+
ByteBuffer.wrap(dexFile.dexBytes),
217+
context.classLoader
218+
)
210219
}
211220

212221
@Throws(Exception::class)
213-
fun isBlocked(stream: InputStream): Boolean {
222+
private fun isBlocked(stream: InputStream): Boolean {
214223
val blockedPackages = listOf(
215-
"(Lcom/dergoogler/mmrl/platform/)?(.+)?/?KsuNative"
224+
"(Lcom/dergoogler/mmrl/platform/)?(.+)?/?KsuNative",
225+
"Lcom/dergoogler/mmrl/webui/util/WXUPublicKeyKt"
216226
)
217227

218228
val dexFile = DexBackedDexFile.fromInputStream(null, stream)
@@ -237,6 +247,71 @@ open class WebUIConfigBaseLoader() {
237247
return false
238248
}
239249

250+
private data class VerifiedDex(
251+
val dexBytes: ByteArray,
252+
val official: Boolean,
253+
) {
254+
override fun equals(other: Any?): Boolean {
255+
if (this === other) return true
256+
if (javaClass != other?.javaClass) return false
257+
258+
other as VerifiedDex
259+
260+
if (official != other.official) return false
261+
if (!dexBytes.contentEquals(other.dexBytes)) return false
262+
263+
return true
264+
}
265+
266+
override fun hashCode(): Int {
267+
var result = official.hashCode()
268+
result = 31 * result + dexBytes.contentHashCode()
269+
return result
270+
}
271+
}
272+
273+
private fun loadSignedDex(file: SuFile, publicKey: PublicKey): VerifiedDex {
274+
val allBytes = file.readBytes()
275+
val totalSize = allBytes.size
276+
277+
if (totalSize < 8) { // must fit signature size + some bytes
278+
Log.e(TAG, "File too small to contain signature: ${file.path}")
279+
return VerifiedDex(allBytes, false)
280+
}
281+
282+
return try {
283+
val sigSizeBytes = allBytes.copyOfRange(totalSize - 4, totalSize)
284+
val sigSize = ByteBuffer.wrap(sigSizeBytes)
285+
.order(ByteOrder.BIG_ENDIAN)
286+
.int
287+
288+
if (sigSize <= 0 || sigSize > totalSize - 4) {
289+
Log.e(TAG, "Invalid signature size=$sigSize for ${file.path}")
290+
return VerifiedDex(allBytes, false)
291+
}
292+
293+
val sigStart = totalSize - 4 - sigSize
294+
val sigBytes = allBytes.copyOfRange(sigStart, totalSize - 4)
295+
val dexBytes = allBytes.copyOfRange(0, sigStart)
296+
297+
val signature = Signature.getInstance("SHA256withRSA")
298+
signature.initVerify(publicKey)
299+
signature.update(dexBytes)
300+
301+
val isOfficial = try {
302+
signature.verify(sigBytes)
303+
} catch (e: Exception) {
304+
Log.e(TAG, "Error verifying signature", e)
305+
false
306+
}
307+
308+
VerifiedDex(dexBytes, isOfficial)
309+
} catch (e: Exception) {
310+
Log.e(TAG, "Unable to verify signature for ${file.path}", e)
311+
VerifiedDex(allBytes, false)
312+
}
313+
}
314+
240315
/**
241316
* Creates a ClassLoader for a class within an installed APK.
242317
*/
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
@file:Suppress("FunctionName")
2+
3+
package com.dergoogler.mmrl.webui.util
4+
5+
import java.security.KeyFactory
6+
import java.security.PublicKey
7+
import java.security.spec.X509EncodedKeySpec
8+
import kotlin.io.encoding.Base64
9+
import kotlin.io.encoding.ExperimentalEncodingApi
10+
11+
@OptIn(ExperimentalEncodingApi::class)
12+
val WXUPublicKey
13+
get(): PublicKey {
14+
val publicKeyPEM =
15+
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkT7SQr9BKze1uuZxZjVFosWrVfJw0g3ulgt/N8TqYPaYXchkaKU3l8MubAOgVrYRfAcWPxFaZpeGbw5MRQkPwBqgXut8uUTI4/YfhDO6HBn4k9tFIMYTJiIJHTRJ+dXFHbfj8IKr53LQGs40rwfD83DZvEUxT6Cn7/a77oMVSMPSP9TDULRK8tvnmWJJbQACHz/bHxYkqo3DZNre09GHFOZiD3fxoaWBwGO3a0wUIkNkHpoSSX0itmcdPCXH0Sq8y/nWP6MSjTBzstTEJgMW1/Ej8FBnjvjCSZvWLmG3aVfoulig5o+rywKxLYsKLO4xnZb2+mCmHFa/zYOglNnQtwIDAQAB"
16+
val encoded: ByteArray = Base64.decode(publicKeyPEM)
17+
val keySpec = X509EncodedKeySpec(encoded)
18+
val keyFactory = KeyFactory.getInstance("RSA")
19+
return keyFactory.generatePublic(keySpec)
20+
}

0 commit comments

Comments
 (0)