Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions daemon/src/main/jni/obfuscation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ namespace {

std::once_flag init_flag;

// What is rewritten, and the whole of what is rewritten: the string table of a dex. A module that
// names one of these prefixes anywhere a dex writer cannot reach keeps the original name and stops
// matching the one its classes now carry. A native library is the case that occurs: the JNI
// signature strings compiled into it still read Lde/robv/android/xposed/..., so RegisterNatives
// finds no method of that shape and the module loads without its native entry points. Nothing here
// can fix that -- the library is the module's own build output -- which is why
// `enable_dex_obfuscate` is a setting a user can turn off rather than a build type.
std::map<std::string, std::string> signatures = {
{"Lde/robv/android/xposed/", ""}, {"Landroid/app/AndroidApp", ""},
{"Landroid/content/res/XRes", ""}, {"Landroid/content/res/XModule", ""},
Expand Down
11 changes: 10 additions & 1 deletion daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,16 @@ class ScopeCommand {
}
}

@Command(name = "config", description = ["Manage daemon preferences natively"])
@Command(
name = "config",
description =
[
"Manage daemon preferences natively",
"Keys: status-notification, verbose-log, dex-obfuscation.",
"dex-obfuscation renames the legacy Xposed API classes in the framework dex",
"and in every module dex; a module whose native code names those classes",
"cannot bind its methods while it is on. Takes effect on the next boot.",
])
class ConfigCommand {
@ParentCommand lateinit var parent: Cli

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ object ConfigCache {
}
}
applySqliteHelperWorkaround()

// After the workaround, which has to be in place before the configuration database is opened,
// and this read may well be what opens it. Caught, because a daemon that cannot read its own
// preference must still start: the fallback in `DaemonState` is the build-type default this
// setting used to be.
//
// Read once and held, not consulted per use. `FileSystem.getPreloadDex` caches the framework
// dex the first time it is asked for, so the answer that produced it is the only one the rest
// of this process may act on; a module loaded with the other answer would look for class names
// the framework does not have. Writing the preference therefore takes effect on the next boot.
runCatching { PreferenceStore.isDexObfuscateEnabled() }
.onSuccess { synchronized(this) { state = state.copy(isDexObfuscateEnabled = it) } }
.onFailure {
Log.w(TAG, "Cannot read the dex obfuscation preference; using the default", it)
}
}

private fun ensureCacheReady() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ data class ProcessScope(val processName: String, val uid: Int)
* and atomically swap the reference.
*/
data class DaemonState(
// State non configurable for users
/**
* Whether the legacy API classes are renamed, seeded from
* [PreferenceStore.isDexObfuscateEnabled] when [ConfigCache] is first touched.
*
* The default here is only what a daemon that cannot read its own configuration falls back to.
* It used to be the entire setting, which is why it was marked as not configurable: the
* framework dex and every module dex were obfuscated in a release build and not in a debug
* one, with nothing able to say otherwise.
*/
val isDexObfuscateEnabled: Boolean = !BuildConfig.DEBUG,
// States initialized after system services are ready
val isCacheReady: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.matrix.vector.daemon.data
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import org.apache.commons.lang3.SerializationUtilsX
import org.matrix.vector.daemon.BuildConfig

private const val TAG = "VectorPreferenceStore"

Expand Down Expand Up @@ -97,6 +98,21 @@ object PreferenceStore {
fun setVerboseLog(enabled: Boolean) =
updateModulePref("lspd", 0, "config", "enable_verbose_log", enabled)

/**
* Whether the legacy Xposed API classes are renamed in the framework dex and in every module
* dex, under the key LSPosed wrote and this daemon has only ever read a build-type default for.
*
* A release build obfuscates and a debug build does not, which is the default below and was the
* whole of it: the row existed in the database, nothing read it, and the manager and the CLI
* offered no way to write it. The difference is not cosmetic — it decides whether a module whose
* native code names those classes can bind its methods at all — so it is a setting.
*/
fun isDexObfuscateEnabled(): Boolean =
getModulePrefs("lspd", 0, "config")["enable_dex_obfuscate"] as? Boolean ?: !BuildConfig.DEBUG

fun setDexObfuscate(enabled: Boolean) =
updateModulePref("lspd", 0, "config", "enable_dex_obfuscate", enabled)

fun isScopeRequestBlocked(pkg: String): Boolean =
(getModulePrefs("lspd", 0, "config")["scope_request_blocked"] as? Set<*>)?.contains(pkg) ==
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ object CliHandler {
when (key) {
"status-notification" -> ManagerService.isStatusNotificationEnabled()
"verbose-log" -> ManagerService.isVerboseLogEnabled()
// What the stored preference says, which after a write is not yet what this daemon
// is doing. `ConfigCache.state.isDexObfuscateEnabled` is what it is doing, and the
// two disagree until the next boot; reporting the setting is what lets a reader see
// that their write landed.
"dex-obfuscation" -> PreferenceStore.isDexObfuscateEnabled()
else -> throw IllegalArgumentException("Unknown config key: $key")
}
mapOf("KEY" to key, "VALUE" to value)
Expand All @@ -200,9 +205,15 @@ object CliHandler {
when (key) {
"status-notification" -> ManagerService.setStatusNotificationEnabled(value)
"verbose-log" -> ManagerService.setVerboseLogEnabled(value)
"dex-obfuscation" -> PreferenceStore.setDexObfuscate(value)
else -> throw IllegalArgumentException("Unknown config key: $key")
}
"Successfully set $key to $value."
if (key == "dex-obfuscation" && value != ConfigCache.state.isDexObfuscateEnabled) {
"Set $key to $value. The framework dex this daemon handed out names its classes the " +
"other way, so reboot to apply."
} else {
"Successfully set $key to $value."
}
}
else -> throw IllegalArgumentException("Unknown config action: ${request.action}")
}
Expand Down
10 changes: 9 additions & 1 deletion zygisk/src/main/cpp/ipc_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <elf/symbol_cache.h>
#include <unistd.h>

#include <algorithm>
#include <atomic>
#include <string_view>
#include <thread>
Expand Down Expand Up @@ -469,7 +470,14 @@ std::map<std::string, std::string> IPCBridge::FetchObfuscationMap(JNIEnv *env, j
result_map[key_str.get()] = val_str.get();
}

LOGV("Fetched obfuscation map with {} entries.", result_map.size());
// Whether the names actually differ, and not merely how many arrived. The daemon answers with
// every prefix either way and maps each to itself when obfuscation is off, so a count alone
// reads the same in both configurations -- which is the one thing a reader looking at a module
// that will not bind its native methods needs to tell apart.
const bool renamed = std::any_of(result_map.begin(), result_map.end(),
[](const auto &entry) { return entry.first != entry.second; });
LOGV("Fetched obfuscation map with {} entries, dex obfuscation {}.", result_map.size(),
renamed ? "on" : "off");
return result_map;
}

Expand Down
Loading