diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSharedLibraryManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSharedLibraryManager.kt new file mode 100644 index 00000000..2baf50a1 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSharedLibraryManager.kt @@ -0,0 +1,78 @@ +package me.rhunk.snapenhance + +import me.rhunk.snapenhance.common.bridge.InternalFileHandleType +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.File + +class RemoteSharedLibraryManager( + private val remoteSideContext: RemoteSideContext +) { + private val okHttpClient = OkHttpClient() + + private fun getVersion(): String? { + return runCatching { + okHttpClient.newCall( + Request.Builder() + .url("https://raw.githubusercontent.com/SnapEnhance/resources/main/sif/version") + .build() + ).execute().use { response -> + if (!response.isSuccessful) { + return null + } + response.body.string() + } + }.getOrNull() + } + + private fun downloadLatest(outputFile: File): Boolean { + val request = Request.Builder() + .url("https://raw.githubusercontent.com/SnapEnhance/resources/main/sif/libsif.so") + .build() + runCatching { + okHttpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + return false + } + response.body.byteStream().use { input -> + outputFile.outputStream().use { output -> + input.copyTo(output) + } + } + return true + } + }.onFailure { + remoteSideContext.log.error("Failed to download latest sif", it) + } + return false + } + + fun init() { + val libraryFile = InternalFileHandleType.SIF.resolve(remoteSideContext.androidContext) + val currentVersion = remoteSideContext.sharedPreferences.getString("sif", null)?.trim() + if (currentVersion == null || currentVersion == "false") { + libraryFile.takeIf { it.exists() }?.delete() + remoteSideContext.log.info("sif can't be loaded due to user preference") + return + } + val latestVersion = getVersion()?.trim() ?: run { + remoteSideContext.log.warn("Failed to get latest sif version") + return + } + + if (currentVersion == latestVersion) { + remoteSideContext.log.info("sif is up to date ($currentVersion)") + return + } + + remoteSideContext.log.info("Updating sif from $currentVersion to $latestVersion") + if (downloadLatest(libraryFile)) { + remoteSideContext.sharedPreferences.edit().putString("sif", latestVersion).apply() + remoteSideContext.log.info("sif updated to $latestVersion") + // force restart snapchat + remoteSideContext.bridgeService?.stopSelf() + } else { + remoteSideContext.log.warn("Failed to download latest sif") + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt index 39134414..be61d155 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -78,6 +78,7 @@ class RemoteSideContext( val tracker = RemoteTracker(this) val accountStorage = RemoteAccountStorage(this) val locationManager = RemoteLocationManager(this) + private val remoteSharedLibraryManager = RemoteSharedLibraryManager(this) //used to load bitmoji selfies and download previews val imageLoader by lazy { @@ -131,6 +132,9 @@ class RemoteSideContext( messageLogger.purgeTrackerLogs(it) } } + coroutineScope.launch { + remoteSharedLibraryManager.init() + } } }.onFailure { log.error("Failed to load RemoteSideContext", it) @@ -212,6 +216,10 @@ class RemoteSideContext( requirements = requirements or Requirements.MAPPINGS } + if (sharedPreferences.getString("sif", null) == null) { + requirements = requirements or Requirements.SIF + } + if (requirements == 0) return false val currentContext = activity ?: androidContext diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt index bab9d044..7046a0ac 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt @@ -154,6 +154,9 @@ class HomeSettings : Routes.Route() { RowAction(key = "change_language") { context.checkForRequirements(Requirements.LANGUAGE) } + RowAction(key = "security_features") { + context.checkForRequirements(Requirements.SIF) + } RowTitle(title = translation["message_logger_title"]) ShiftedRow { Column( @@ -284,7 +287,7 @@ class HomeSettings : Routes.Route() { ) { PreferenceToggle(context.sharedPreferences, key = "disable_feature_loading", text = "Disable Feature Loading") PreferenceToggle(context.sharedPreferences, key = "disable_mapper", text = "Disable Auto Mapper") - PreferenceToggle(context.sharedPreferences, key = "force_native_load", text = "Force Native Load") + PreferenceToggle(context.sharedPreferences, key = "disable_sif", text = "Disable Security Features") } } Spacer(modifier = Modifier.height(50.dp)) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/Requirements.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/Requirements.kt index 04235b78..4d81c030 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/Requirements.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/Requirements.kt @@ -1,21 +1,11 @@ package me.rhunk.snapenhance.ui.setup object Requirements { - const val FIRST_RUN = 0b00001 - const val LANGUAGE = 0b00010 - const val MAPPINGS = 0b00100 - const val SAVE_FOLDER = 0b01000 - const val GRANT_PERMISSIONS = 0b10000 - - fun getName(requirement: Int): String { - return when (requirement) { - FIRST_RUN -> "FIRST_RUN" - LANGUAGE -> "LANGUAGE" - MAPPINGS -> "MAPPINGS" - SAVE_FOLDER -> "SAVE_FOLDER" - GRANT_PERMISSIONS -> "GRANT_PERMISSIONS" - else -> "UNKNOWN" - } - } + const val FIRST_RUN = 0b000001 + const val LANGUAGE = 0b000010 + const val MAPPINGS = 0b000100 + const val SAVE_FOLDER = 0b001000 + const val GRANT_PERMISSIONS = 0b010000 + const val SIF = 0b100000 } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt index 85a4e661..d4942a0b 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt @@ -29,10 +29,7 @@ import androidx.navigation.compose.rememberNavController import me.rhunk.snapenhance.SharedContextHolder import me.rhunk.snapenhance.common.ui.AppMaterialTheme import me.rhunk.snapenhance.ui.setup.screens.SetupScreen -import me.rhunk.snapenhance.ui.setup.screens.impl.MappingsScreen -import me.rhunk.snapenhance.ui.setup.screens.impl.PermissionsScreen -import me.rhunk.snapenhance.ui.setup.screens.impl.PickLanguageScreen -import me.rhunk.snapenhance.ui.setup.screens.impl.SaveFolderScreen +import me.rhunk.snapenhance.ui.setup.screens.impl.* class SetupActivity : ComponentActivity() { @@ -69,6 +66,9 @@ class SetupActivity : ComponentActivity() { if (isFirstRun || hasRequirement(Requirements.MAPPINGS)) { add(MappingsScreen().apply { route = "mappings" }) } + if (isFirstRun || hasRequirement(Requirements.SIF)) { + add(SecurityScreen().apply { route = "security" }) + } } // If there are no required screens, we can just finish the activity diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SecurityScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SecurityScreen.kt new file mode 100644 index 00000000..43d90b82 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SecurityScreen.kt @@ -0,0 +1,82 @@ +package me.rhunk.snapenhance.ui.setup.screens.impl + +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.WarningAmber +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import me.rhunk.snapenhance.ui.setup.screens.SetupScreen + +class SecurityScreen : SetupScreen() { + @Composable + override fun Content() { + Icon( + imageVector = Icons.Default.WarningAmber, + contentDescription = null, + modifier = Modifier.padding(16.dp).size(30.dp), + ) + + DialogText( + "Since Snapchat has implemented additional security measures against third-party applications such as SnapEnhance, we offer a non-opensource solution that reduces the risk of banning and prevents Snapchat from detecting SnapEnhance. " + + "\nPlease note that this solution does not provide a ban bypass or spoofer for anything, and does not take any personal data or communicate with the network." + + "\nWe also encourage you to use official signed builds to avoid compromising the security of your account." + + "\nIf you're having trouble using the solution, or are experiencing crashes, join the Telegram Group for help: https://t.me/snapenhance_chat" + ) + + var denyDialog by remember { mutableStateOf(false) } + + if (denyDialog) { + AlertDialog( + onDismissRequest = { + denyDialog = false + }, + text = { + Text("Are you sure you don't want to use this solution? You can always change this later in the settings in the SnapEnhance app.") + }, + dismissButton = { + Button(onClick = { + denyDialog = false + }) { + Text("Go back") + } + }, + confirmButton = { + Button(onClick = { + context.sharedPreferences.edit().putString("sif", "false").apply() + goNext() + }) { + Text("Yes, I'm sure") + } + } + ) + } + + Column ( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Button( + onClick = { + context.sharedPreferences.edit().putString("sif", "").apply() + goNext() + } + ) { + Text("Accept and continue", fontSize = 18.sp, fontWeight = FontWeight.Bold) + } + Button( + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), + onClick = { + denyDialog = true + } + ) { + Text("I don't want to use this solution") + } + } + } +} \ No newline at end of file diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 5bf26156..a90e0789 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -299,6 +299,10 @@ "name": "Change Language", "description": "Change the language of SnapEnhance" }, + "security_features": { + "name": "Security Features", + "description": "Access security features" + }, "file_imports": { "name": "File Imports", "description": "Import files for use in Snapchat" diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/BridgeFiles.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/BridgeFiles.kt index 2bd5f035..164eb84e 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/BridgeFiles.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/BridgeFiles.kt @@ -32,8 +32,8 @@ enum class InternalFileHandleType( CONFIG("config", "config.json"), MAPPINGS("mappings", "mappings.json"), MESSAGE_LOGGER("message_logger", "message_logger.db", isDatabase = true), - PINNED_BEST_FRIEND("pinned_best_friend", "pinned_best_friend.txt"); - + PINNED_BEST_FRIEND("pinned_best_friend", "pinned_best_friend.txt"), + SIF("sif", "libsif.so"); fun resolve(context: Context): File = if (isDatabase) { context.getDatabasePath(fileName) diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt index c0976884..f986d4b7 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt @@ -31,7 +31,7 @@ class Experimental : ConfigContainer() { val composerLogs = boolean("composer_logs") } - class NativeHooks : ConfigContainer(hasGlobalState = true) { + class NativeHooks : ConfigContainer() { val composerHooks = container("composer_hooks", ComposerHooksConfig()) { requireRestart() } val disableBitmoji = boolean("disable_bitmoji") val customEmojiFont = string("custom_emoji_font") { @@ -40,7 +40,6 @@ class Experimental : ConfigContainer() { addFlags(ConfigFlag.USER_IMPORT) filenameFilter = { it.endsWith(".ttf") } } - val remapExecutable = boolean("remap_executable") { requireRestart(); addNotices(FeatureNotice.INTERNAL_BEHAVIOR, FeatureNotice.UNSTABLE) } } class E2EEConfig : ConfigContainer(hasGlobalState = true) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt index df2af9ec..e1bee5df 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt @@ -153,13 +153,11 @@ class ModContext( } fun reloadNativeConfig() { - if (config.experimental.nativeHooks.globalState != true) return native.loadNativeConfig( NativeConfig( disableBitmoji = config.experimental.nativeHooks.disableBitmoji.get(), disableMetrics = config.global.disableMetrics.get(), composerHooks = config.experimental.nativeHooks.composerHooks.globalState == true, - remapExecutable = config.experimental.nativeHooks.remapExecutable.get(), customEmojiFontPath = getCustomEmojiFontPath(this) ) ) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt index c6481d60..6b450552 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -4,7 +4,8 @@ import android.app.Activity import android.content.Context import android.content.res.Resources import android.os.Build -import dalvik.system.BaseDexClassLoader +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Cancel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -14,6 +15,9 @@ import me.rhunk.snapenhance.bridge.SyncCallback import me.rhunk.snapenhance.common.Constants import me.rhunk.snapenhance.common.ReceiversConfig import me.rhunk.snapenhance.common.action.EnumAction +import me.rhunk.snapenhance.common.bridge.FileHandleScope +import me.rhunk.snapenhance.common.bridge.InternalFileHandleType +import me.rhunk.snapenhance.common.bridge.toWrapper import me.rhunk.snapenhance.common.data.FriendStreaks import me.rhunk.snapenhance.common.data.MessagingFriendInfo import me.rhunk.snapenhance.common.data.MessagingGroupInfo @@ -27,7 +31,6 @@ import me.rhunk.snapenhance.core.util.LSPatchUpdater import me.rhunk.snapenhance.core.util.hook.HookAdapter import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook -import me.rhunk.snapenhance.core.util.hook.hookConstructor import kotlin.reflect.KClass import kotlin.system.exitProcess import kotlin.system.measureTimeMillis @@ -166,6 +169,8 @@ class SnapEnhance { } } + private var safeMode = false + private fun onActivityCreate(activity: Activity) { measureTimeMillis { with(appContext) { @@ -173,6 +178,10 @@ class SnapEnhance { inAppOverlay.onActivityCreate(activity) scriptRuntime.eachModule { callFunction("module.onSnapMainActivityCreate", activity) } actionManager.onActivityCreate() + + if (safeMode) { + appContext.inAppOverlay.showStatusToast(Icons.Outlined.Cancel, "Failed to load security features! Snapchat may not work properly.", durationMs = 5000) + } } }.also { time -> appContext.log.verbose("onActivityCreate took $time") @@ -180,36 +189,58 @@ class SnapEnhance { } private fun initNative() { - // don't initialize native when not logged in - if ( - !appContext.isLoggedIn() && - appContext.bridgeClient.getDebugProp("force_native_load", null) != "true" - ) return - if (appContext.config.experimental.nativeHooks.globalState != true) return + val lateInit = appContext.native.initOnce { + nativeUnaryCallCallback = { request -> + appContext.event.post(NativeUnaryCallEvent(request.uri, request.buffer)) { + request.buffer = buffer + request.canceled = canceled + } + } + appContext.reloadNativeConfig() + } + + if (appContext.bridgeClient.getDebugProp("disable_sif", "false") != "true") { + runCatching { + appContext.native.loadSharedLibrary( + appContext.fileHandlerManager.getFileHandle(FileHandleScope.INTERNAL.key, InternalFileHandleType.SIF.key) + .toWrapper() + .readBytes() + .takeIf { + it.isNotEmpty() + } ?: throw IllegalStateException("buffer is empty") + ) + appContext.log.verbose("loaded sif") + }.onFailure { + safeMode = true + appContext.log.error("Failed to load sif", it) + } + } else { + appContext.log.warn("sif is disabled") + } - lateinit var unhook: () -> Unit Runtime::class.java.declaredMethods.first { it.name == "loadLibrary0" && it.parameterTypes.contentEquals( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) arrayOf(Class::class.java, String::class.java) else arrayOf(ClassLoader::class.java, String::class.java) ) - }.hook(HookStage.AFTER) { param -> - val libName = param.arg(1) - if (libName != "client") return@hook - unhook() - appContext.native.initOnce { - nativeUnaryCallCallback = { request -> - appContext.event.post(NativeUnaryCallEvent(request.uri, request.buffer)) { - request.buffer = buffer - request.canceled = canceled - } + }.apply { + if (safeMode) { + hook(HookStage.BEFORE) { param -> + if (param.arg(1) != "scplugin") return@hook + appContext.log.warn("Can't load scplugin in safe mode") + Thread.sleep(Long.MAX_VALUE) } - appContext.reloadNativeConfig() } - BaseDexClassLoader::class.java.hookConstructor(HookStage.AFTER) { - appContext.native.hideAnonymousDexFiles() - } - }.also { unhook = { it.unhook() } } + + lateinit var unhook: () -> Unit + hook(HookStage.AFTER) { param -> + val libName = param.arg(1) + if (libName != "client") return@hook + unhook() + appContext.log.verbose("libclient lateInit") + lateInit() + }.also { unhook = { it.unhook() } } + } } private fun initConfigListener() { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/DeviceSpooferHook.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/DeviceSpooferHook.kt index 30d85c57..0cdb0b07 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/DeviceSpooferHook.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/DeviceSpooferHook.kt @@ -11,7 +11,7 @@ import me.rhunk.snapenhance.core.util.LSPatchUpdater import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook -class DeviceSpooferHook: Feature("device_spoofer") { +class DeviceSpooferHook: Feature("Device Spoofer") { private fun hookInstallerPackageName() { context.androidContext.packageManager::class.java.hook("getInstallerPackageName", HookStage.BEFORE) { param -> param.setResult("com.android.vending") diff --git a/native/jni/src/common.h b/native/jni/src/common.h index aae2ff2f..cab6ac38 100644 --- a/native/jni/src/common.h +++ b/native/jni/src/common.h @@ -13,7 +13,6 @@ typedef struct { bool disable_bitmoji; bool disable_metrics; bool composer_hooks; - bool remap_executable; char custom_emoji_font_path[256]; } native_config_t; diff --git a/native/jni/src/dobby_helper.h b/native/jni/src/dobby_helper.h index 608b94a8..fa6b45dc 100644 --- a/native/jni/src/dobby_helper.h +++ b/native/jni/src/dobby_helper.h @@ -9,8 +9,5 @@ static pthread_mutex_t hook_mutex = PTHREAD_MUTEX_INITIALIZER; static void inline SafeHook(void *addr, void *hook, void **original) { pthread_mutex_lock(&hook_mutex); DobbyHook(addr, hook, original); - if (common::native_config->remap_executable) { - mprotect((void *)((uintptr_t) *original & PAGE_MASK), PAGE_SIZE, PROT_EXEC); - } pthread_mutex_unlock(&hook_mutex); } \ No newline at end of file diff --git a/native/jni/src/hooks/linker_hook.h b/native/jni/src/hooks/linker_hook.h new file mode 100644 index 00000000..4295b312 --- /dev/null +++ b/native/jni/src/hooks/linker_hook.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +namespace LinkerHook { + static auto linker_openat_hooks = std::map>(); + + void JNICALL addLinkerSharedLibrary(JNIEnv *env, jobject, jstring path, jbyteArray content) { + const char *path_str = env->GetStringUTFChars(path, nullptr); + jsize content_len = env->GetArrayLength(content); + jbyte *content_ptr = env->GetByteArrayElements(content, nullptr); + + auto allocated_content = (jbyte *) malloc(content_len); + memcpy(allocated_content, content_ptr, content_len); + linker_openat_hooks[path_str] = std::make_pair((uintptr_t) allocated_content, content_len); + + LOGD("added linker hook for %s, size=%d", path_str, content_len); + + env->ReleaseStringUTFChars(path, path_str); + env->ReleaseByteArrayElements(content, content_ptr, JNI_ABORT); + } + + HOOK_DEF(int, linker_openat, int dirfd, const char *pathname, int flags, mode_t mode) { + for (const auto &item: linker_openat_hooks) { + if (strstr(pathname, item.first.c_str())) { + LOGD("found openat hook for %s", pathname); + static auto memfd_create = (int (*)(const char *, unsigned int)) DobbySymbolResolver("libc.so", "memfd_create"); + auto fd = memfd_create("me.rhunk.snapenhance", 0); + LOGD("memfd created: %d", fd); + + if (fd == -1) { + LOGE("memfd_create failed: %d", errno); + return -1; + } + if (write(fd, (void *) item.second.first, item.second.second) == -1) { + LOGE("write failed: %d", errno); + return -1; + } + lseek(fd, 0, SEEK_SET); + + free((void *) item.second.first); + linker_openat_hooks.erase(item.first); + + LOGD("memfd written"); + return fd; + } + } + return linker_openat_original(dirfd, pathname, flags, mode); + } + + void init() { + DobbyHook((void *) DobbySymbolResolver(ARM64 ? "linker64" : "linker", "__dl___openat"), (void *) linker_openat, (void **) &linker_openat_original); + } +} \ No newline at end of file diff --git a/native/jni/src/library.cpp b/native/jni/src/library.cpp index c0635784..e25bdb2e 100644 --- a/native/jni/src/library.cpp +++ b/native/jni/src/library.cpp @@ -7,6 +7,7 @@ #include "logger.h" #include "common.h" #include "dobby_helper.h" +#include "hooks/linker_hook.h" #include "hooks/unary_call.h" #include "hooks/fstat_hook.h" #include "hooks/sqlite_mutex.h" @@ -17,9 +18,6 @@ bool JNICALL init(JNIEnv *env, jobject clazz) { LOGD("Initializing native"); using namespace common; - util::remap_sections([](const std::string &line, size_t size) { - return line.find(BUILD_PACKAGE) != std::string::npos; - }, native_config->remap_executable); native_lib_object = env->NewGlobalRef(clazz); client_module = util::get_module("libclient.so"); @@ -66,7 +64,6 @@ void JNICALL load_config(JNIEnv *env, jobject, jobject config_object) { native_config->disable_bitmoji = GET_CONFIG_BOOL("disableBitmoji"); native_config->disable_metrics = GET_CONFIG_BOOL("disableMetrics"); native_config->composer_hooks = GET_CONFIG_BOOL("composerHooks"); - native_config->remap_executable = GET_CONFIG_BOOL("remapExecutable"); memset(native_config->custom_emoji_font_path, 0, sizeof(native_config->custom_emoji_font_path)); auto custom_emoji_font_path = env->GetObjectField(config_object, env->GetFieldID(native_config_clazz, "customEmojiFontPath", "Ljava/lang/String;")); @@ -97,15 +94,6 @@ void JNICALL lock_database(JNIEnv *env, jobject, jstring database_name, jobject } } -void JNICALL hide_anonymous_dex_files(JNIEnv *, jobject) { - util::remap_sections([](const std::string &line, size_t size) { - return ( - (common::native_config->remap_executable && size == PAGE_SIZE && line.find("r-xp 00000000 00") != std::string::npos && line.find("[v") == std::string::npos) || - line.find("dalvik-DEX") != std::string::npos || - line.find("dalvik-classes") != std::string::npos - ); - }, common::native_config->remap_executable); -} extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *_) { common::java_vm = vm; @@ -118,7 +106,9 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *_) { methods.push_back({"lockDatabase", "(Ljava/lang/String;Ljava/lang/Runnable;)V", (void *)lock_database}); methods.push_back({"setComposerLoader", "(Ljava/lang/String;)V", (void *) ComposerHook::setComposerLoader}); methods.push_back({"composerEval", "(Ljava/lang/String;)Ljava/lang/String;",(void *) ComposerHook::composerEval}); - methods.push_back({"hideAnonymousDexFiles", "()V", (void *)hide_anonymous_dex_files}); + methods.push_back({"addLinkerSharedLibrary", "(Ljava/lang/String;[B)V", (void *) LinkerHook::addLinkerSharedLibrary}); + + LinkerHook::init(); env->RegisterNatives(env->FindClass(std::string(BUILD_NAMESPACE "/NativeLib").c_str()), methods.data(), methods.size()); return JNI_VERSION_1_6; diff --git a/native/jni/src/util.h b/native/jni/src/util.h index 484a3681..1cdb3d41 100644 --- a/native/jni/src/util.h +++ b/native/jni/src/util.h @@ -52,46 +52,6 @@ namespace util { return { start_offset, end_offset - start_offset }; } - static void remap_sections(std::function filter, bool remove_read_permission) { - char buff[256]; - auto maps = fopen("/proc/self/maps", "rt"); - - while (fgets(buff, sizeof buff, maps) != NULL) { - int len = strlen(buff); - if (len > 0 && buff[len - 1] == '\n') buff[--len] = '\0'; - - size_t start, end, offset; - char flags[4]; - - if (sscanf(buff, "%zx-%zx %c%c%c%c %zx", &start, &end, - &flags[0], &flags[1], &flags[2], &flags[3], &offset) != 7) continue; - - if (!filter(buff, end - start)) continue; - - auto section_size = end - start; - auto section_ptr = mmap(0, section_size, PROT_READ | PROT_EXEC | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - - if (section_ptr == MAP_FAILED) { - LOGE("mmap failed: %s", strerror(errno)); - break; - } - - memcpy(section_ptr, (void *)start, section_size); - - if (mremap(section_ptr, section_size, section_size, MREMAP_MAYMOVE | MREMAP_FIXED, start) == MAP_FAILED) { - LOGE("mremap failed: %s", strerror(errno)); - break; - } - - auto new_prot = (flags[0] == 'r' ? PROT_READ : 0) | (flags[1] == 'w' ? PROT_WRITE : 0) | (flags[2] == 'x' ? PROT_EXEC : 0); - if (remove_read_permission && flags[0] == 'r' && flags[2] == 'x') { - new_prot &= ~PROT_READ; - } - mprotect((void *)start, section_size, new_prot); - } - fclose(maps); - } - static uintptr_t find_signature(uintptr_t module_base, uintptr_t size, const std::string &pattern, int offset = 0) { std::vector bytes; std::vector mask; diff --git a/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeConfig.kt b/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeConfig.kt index aaa6795d..a41e64c1 100644 --- a/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeConfig.kt +++ b/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeConfig.kt @@ -8,7 +8,5 @@ data class NativeConfig( @JvmField val composerHooks: Boolean = false, @JvmField - val remapExecutable: Boolean = false, - @JvmField val customEmojiFontPath: String? = null, ) \ No newline at end of file diff --git a/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt b/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt index baa24849..0ae1cf72 100644 --- a/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt +++ b/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt @@ -1,6 +1,9 @@ package me.rhunk.snapenhance.nativelib +import android.annotation.SuppressLint import android.util.Log +import kotlin.math.absoluteValue +import kotlin.random.Random @Suppress("KotlinJniMissingFunction") class NativeLib { @@ -11,19 +14,21 @@ class NativeLib { private set } - fun initOnce(callback: NativeLib.() -> Unit) { + fun initOnce(callback: NativeLib.() -> Unit): () -> Unit { if (initialized) throw IllegalStateException("NativeLib already initialized") - runCatching { + return runCatching { System.loadLibrary(BuildConfig.NATIVE_NAME) initialized = true callback(this) - if (!init()) { - throw IllegalStateException("NativeLib init failed. Check logcat for more info") + return@runCatching { + if (!init()) { + throw IllegalStateException("NativeLib init failed. Check logcat for more info") + } } }.onFailure { initialized = false Log.e("SnapEnhance", "NativeLib init failed", it) - } + }.getOrThrow() } @Suppress("unused") @@ -54,10 +59,18 @@ class NativeLib { } } + @SuppressLint("UnsafeDynamicallyLoadedCode") + fun loadSharedLibrary(content: ByteArray) { + if (!initialized) throw IllegalStateException("NativeLib not initialized") + val generatedPath = "/data/app/${Random.nextLong().absoluteValue.toString(16)}.so" + addLinkerSharedLibrary(generatedPath, content) + System.load(generatedPath) + } + private external fun init(): Boolean private external fun loadConfig(config: NativeConfig) private external fun lockDatabase(name: String, callback: Runnable) external fun setComposerLoader(code: String) external fun composerEval(code: String): String? - external fun hideAnonymousDexFiles() + private external fun addLinkerSharedLibrary(path: String, content: ByteArray) } \ No newline at end of file