Mask direct settings-provider reads (ContentResolver.call and query)
An app could skip Settings.Secure/Global and read the settings provider straight through ContentResolver, getting the real accessibility, ADB and developer-options state. Two routes, both now masked for the same keys the Settings hooks cover: - ContentResolver.call(content://settings/<table>, "GET_<table>", key): the returned Bundle's "value" is rewritten to null / "0". - ContentResolver.query on settings/secure and settings/global: the cursor is replaced with a MatrixCursor with the value masked. The key is read from the "name" column, or, when a caller projects only "value", from the URI path or a single selection arg. Cursors that touch no masked key are left as they were. Verified on device with Veil Probe: call(), query() with name=?, and the settings/global/<key> path form all report HIDDEN for accessibility, adb_enabled and development_settings_enabled, and a scoped app still starts. Co-Authored-By: agent <agent@bdeshi.space>
This commit is contained in:
1 file changed
+121
@@ -4,7 +4,13 @@ import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.VersionedPackage
|
||||
import android.content.ContentResolver
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.CancellationSignal
|
||||
import de.robv.android.xposed.IXposedHookLoadPackage
|
||||
import de.robv.android.xposed.XC_MethodHook
|
||||
import de.robv.android.xposed.XC_MethodReplacement
|
||||
@@ -44,6 +50,7 @@ class VeilHook : IXposedHookLoadPackage {
|
||||
hookAccessibilityManager(lpparam)
|
||||
hookSettingsClass("android.provider.Settings\$Secure", lpparam, SECURE_NULL_STRING_KEYS, SECURE_ZERO_VALUE_KEYS)
|
||||
hookSettingsClass("android.provider.Settings\$Global", lpparam, emptySet(), GLOBAL_ZERO_VALUE_KEYS)
|
||||
hookSettingsProviderAccess(lpparam)
|
||||
hookPackageManager(lpparam, Features.HIDDEN_PACKAGES)
|
||||
spoofBuildTags()
|
||||
}
|
||||
@@ -138,6 +145,120 @@ class VeilHook : IXposedHookLoadPackage {
|
||||
}
|
||||
}
|
||||
|
||||
// The Settings classes above end up in the settings provider; an app can skip them and reach
|
||||
// it directly, either through ContentResolver.call(content://settings/<table>, "GET_<table>",
|
||||
// key), whose Bundle carries the answer under "value", or through ContentResolver.query,
|
||||
// whose cursor carries name/value rows. Mask both. The Settings classes' own cache goes
|
||||
// through IContentProvider, not ContentResolver, so nothing is masked twice.
|
||||
private fun hookSettingsProviderAccess(lpparam: LoadPackageParam) {
|
||||
val resolverClass = XposedHelpers.findClassIfExists("android.content.ContentResolver", lpparam.classLoader)
|
||||
?: return
|
||||
|
||||
val maskValue = object : XC_MethodHook() {
|
||||
override fun afterHookedMethod(param: MethodHookParam) {
|
||||
val authority = when (val target = param.args.getOrNull(0)) {
|
||||
is Uri -> target.authority
|
||||
is String -> target
|
||||
else -> return
|
||||
}
|
||||
if (authority != "settings") return
|
||||
val method = param.args.getOrNull(1) as? String ?: return
|
||||
val key = param.args.getOrNull(2) as? String ?: return
|
||||
val result = param.result as? Bundle ?: return
|
||||
when (method) {
|
||||
"GET_secure" -> when (key) {
|
||||
in SECURE_NULL_STRING_KEYS -> result.putString("value", null)
|
||||
in SECURE_ZERO_VALUE_KEYS -> result.putString("value", "0")
|
||||
}
|
||||
"GET_global" -> if (key in GLOBAL_ZERO_VALUE_KEYS) result.putString("value", "0")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every query() overload funnels into this one.
|
||||
val maskCursor = object : XC_MethodHook() {
|
||||
override fun afterHookedMethod(param: MethodHookParam) {
|
||||
val uri = param.args.getOrNull(0) as? Uri ?: return
|
||||
if (uri.authority != "settings") return
|
||||
val table = uri.pathSegments?.firstOrNull() ?: return
|
||||
if (table != "secure" && table != "global") return
|
||||
val cursor = param.result as? Cursor ?: return
|
||||
val nameIndex = cursor.getColumnIndex("name")
|
||||
val valueIndex = cursor.getColumnIndex("value")
|
||||
if (valueIndex < 0) return
|
||||
// A caller can project just "value". The key is then only in the URI path
|
||||
// (settings/global/<key>) or in the selection args (name=?), so take it from there.
|
||||
val queriedName = if (nameIndex >= 0) null else {
|
||||
uri.pathSegments.getOrNull(1)
|
||||
?: (param.args.getOrNull(2) as? Bundle)
|
||||
?.getStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS)?.singleOrNull()
|
||||
}
|
||||
if (nameIndex < 0 && queriedName == null) return
|
||||
|
||||
fun masked(name: String?): Pair<Boolean, String?> = when (table) {
|
||||
"secure" -> when (name) {
|
||||
in SECURE_NULL_STRING_KEYS -> true to null
|
||||
in SECURE_ZERO_VALUE_KEYS -> true to "0"
|
||||
else -> false to null
|
||||
}
|
||||
"global" -> if (name in GLOBAL_ZERO_VALUE_KEYS) true to "0" else false to null
|
||||
else -> false to null
|
||||
}
|
||||
|
||||
val rows = mutableListOf<Array<Any?>>()
|
||||
var changed = false
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
val row = arrayOfNulls<Any?>(cursor.columnCount)
|
||||
for (i in row.indices) {
|
||||
row[i] = when (cursor.getType(i)) {
|
||||
Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(i)
|
||||
Cursor.FIELD_TYPE_FLOAT -> cursor.getDouble(i)
|
||||
Cursor.FIELD_TYPE_STRING -> cursor.getString(i)
|
||||
Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(i)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
val (hide, value) = masked(if (nameIndex >= 0) row[nameIndex] as? String else queriedName)
|
||||
if (hide) {
|
||||
row[valueIndex] = value
|
||||
changed = true
|
||||
}
|
||||
rows.add(row)
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
if (!changed) {
|
||||
cursor.moveToPosition(-1)
|
||||
return
|
||||
}
|
||||
val replacement = MatrixCursor(cursor.columnNames, rows.size)
|
||||
rows.forEach { replacement.addRow(it) }
|
||||
cursor.close()
|
||||
param.result = replacement
|
||||
}
|
||||
}
|
||||
|
||||
val stringType = String::class.java
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
resolverClass, "query", Uri::class.java, Array<String>::class.java,
|
||||
Bundle::class.java, CancellationSignal::class.java, maskCursor,
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
// Not present on this API level.
|
||||
}
|
||||
for (signature in listOf(
|
||||
arrayOf<Class<*>>(Uri::class.java, stringType, stringType, Bundle::class.java),
|
||||
arrayOf<Class<*>>(stringType, stringType, stringType, Bundle::class.java),
|
||||
)) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(resolverClass, "call", *signature, maskValue)
|
||||
} catch (t: Throwable) {
|
||||
// Not present with this signature on this API level.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hookPackageManager(lpparam: LoadPackageParam, hiddenPackages: Set<String>) {
|
||||
val clazz = XposedHelpers.findClassIfExists(
|
||||
"android.app.ApplicationPackageManager",
|
||||
|
||||
Reference in new issue
Block a user