feat: security features

Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
rhunk 2024-07-06 16:47:45 +02:00
parent f9974b0e84
commit 8d17d55c83
19 changed files with 322 additions and 118 deletions

View File

@ -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")
}
}
}

View File

@ -78,6 +78,7 @@ class RemoteSideContext(
val tracker = RemoteTracker(this) val tracker = RemoteTracker(this)
val accountStorage = RemoteAccountStorage(this) val accountStorage = RemoteAccountStorage(this)
val locationManager = RemoteLocationManager(this) val locationManager = RemoteLocationManager(this)
private val remoteSharedLibraryManager = RemoteSharedLibraryManager(this)
//used to load bitmoji selfies and download previews //used to load bitmoji selfies and download previews
val imageLoader by lazy { val imageLoader by lazy {
@ -131,6 +132,9 @@ class RemoteSideContext(
messageLogger.purgeTrackerLogs(it) messageLogger.purgeTrackerLogs(it)
} }
} }
coroutineScope.launch {
remoteSharedLibraryManager.init()
}
} }
}.onFailure { }.onFailure {
log.error("Failed to load RemoteSideContext", it) log.error("Failed to load RemoteSideContext", it)
@ -212,6 +216,10 @@ class RemoteSideContext(
requirements = requirements or Requirements.MAPPINGS requirements = requirements or Requirements.MAPPINGS
} }
if (sharedPreferences.getString("sif", null) == null) {
requirements = requirements or Requirements.SIF
}
if (requirements == 0) return false if (requirements == 0) return false
val currentContext = activity ?: androidContext val currentContext = activity ?: androidContext

View File

@ -154,6 +154,9 @@ class HomeSettings : Routes.Route() {
RowAction(key = "change_language") { RowAction(key = "change_language") {
context.checkForRequirements(Requirements.LANGUAGE) context.checkForRequirements(Requirements.LANGUAGE)
} }
RowAction(key = "security_features") {
context.checkForRequirements(Requirements.SIF)
}
RowTitle(title = translation["message_logger_title"]) RowTitle(title = translation["message_logger_title"])
ShiftedRow { ShiftedRow {
Column( 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_feature_loading", text = "Disable Feature Loading")
PreferenceToggle(context.sharedPreferences, key = "disable_mapper", text = "Disable Auto Mapper") 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)) Spacer(modifier = Modifier.height(50.dp))

View File

@ -1,21 +1,11 @@
package me.rhunk.snapenhance.ui.setup package me.rhunk.snapenhance.ui.setup
object Requirements { object Requirements {
const val FIRST_RUN = 0b00001 const val FIRST_RUN = 0b000001
const val LANGUAGE = 0b00010 const val LANGUAGE = 0b000010
const val MAPPINGS = 0b00100 const val MAPPINGS = 0b000100
const val SAVE_FOLDER = 0b01000 const val SAVE_FOLDER = 0b001000
const val GRANT_PERMISSIONS = 0b10000 const val GRANT_PERMISSIONS = 0b010000
const val SIF = 0b100000
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"
}
}
} }

View File

@ -29,10 +29,7 @@ import androidx.navigation.compose.rememberNavController
import me.rhunk.snapenhance.SharedContextHolder import me.rhunk.snapenhance.SharedContextHolder
import me.rhunk.snapenhance.common.ui.AppMaterialTheme import me.rhunk.snapenhance.common.ui.AppMaterialTheme
import me.rhunk.snapenhance.ui.setup.screens.SetupScreen 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.*
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
class SetupActivity : ComponentActivity() { class SetupActivity : ComponentActivity() {
@ -69,6 +66,9 @@ class SetupActivity : ComponentActivity() {
if (isFirstRun || hasRequirement(Requirements.MAPPINGS)) { if (isFirstRun || hasRequirement(Requirements.MAPPINGS)) {
add(MappingsScreen().apply { route = "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 // If there are no required screens, we can just finish the activity

View File

@ -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")
}
}
}
}

View File

@ -299,6 +299,10 @@
"name": "Change Language", "name": "Change Language",
"description": "Change the language of SnapEnhance" "description": "Change the language of SnapEnhance"
}, },
"security_features": {
"name": "Security Features",
"description": "Access security features"
},
"file_imports": { "file_imports": {
"name": "File Imports", "name": "File Imports",
"description": "Import files for use in Snapchat" "description": "Import files for use in Snapchat"

View File

@ -32,8 +32,8 @@ enum class InternalFileHandleType(
CONFIG("config", "config.json"), CONFIG("config", "config.json"),
MAPPINGS("mappings", "mappings.json"), MAPPINGS("mappings", "mappings.json"),
MESSAGE_LOGGER("message_logger", "message_logger.db", isDatabase = true), 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) { fun resolve(context: Context): File = if (isDatabase) {
context.getDatabasePath(fileName) context.getDatabasePath(fileName)

View File

@ -31,7 +31,7 @@ class Experimental : ConfigContainer() {
val composerLogs = boolean("composer_logs") val composerLogs = boolean("composer_logs")
} }
class NativeHooks : ConfigContainer(hasGlobalState = true) { class NativeHooks : ConfigContainer() {
val composerHooks = container("composer_hooks", ComposerHooksConfig()) { requireRestart() } val composerHooks = container("composer_hooks", ComposerHooksConfig()) { requireRestart() }
val disableBitmoji = boolean("disable_bitmoji") val disableBitmoji = boolean("disable_bitmoji")
val customEmojiFont = string("custom_emoji_font") { val customEmojiFont = string("custom_emoji_font") {
@ -40,7 +40,6 @@ class Experimental : ConfigContainer() {
addFlags(ConfigFlag.USER_IMPORT) addFlags(ConfigFlag.USER_IMPORT)
filenameFilter = { it.endsWith(".ttf") } filenameFilter = { it.endsWith(".ttf") }
} }
val remapExecutable = boolean("remap_executable") { requireRestart(); addNotices(FeatureNotice.INTERNAL_BEHAVIOR, FeatureNotice.UNSTABLE) }
} }
class E2EEConfig : ConfigContainer(hasGlobalState = true) { class E2EEConfig : ConfigContainer(hasGlobalState = true) {

View File

@ -153,13 +153,11 @@ class ModContext(
} }
fun reloadNativeConfig() { fun reloadNativeConfig() {
if (config.experimental.nativeHooks.globalState != true) return
native.loadNativeConfig( native.loadNativeConfig(
NativeConfig( NativeConfig(
disableBitmoji = config.experimental.nativeHooks.disableBitmoji.get(), disableBitmoji = config.experimental.nativeHooks.disableBitmoji.get(),
disableMetrics = config.global.disableMetrics.get(), disableMetrics = config.global.disableMetrics.get(),
composerHooks = config.experimental.nativeHooks.composerHooks.globalState == true, composerHooks = config.experimental.nativeHooks.composerHooks.globalState == true,
remapExecutable = config.experimental.nativeHooks.remapExecutable.get(),
customEmojiFontPath = getCustomEmojiFontPath(this) customEmojiFontPath = getCustomEmojiFontPath(this)
) )
) )

View File

@ -4,7 +4,8 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.os.Build 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch 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.Constants
import me.rhunk.snapenhance.common.ReceiversConfig import me.rhunk.snapenhance.common.ReceiversConfig
import me.rhunk.snapenhance.common.action.EnumAction 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.FriendStreaks
import me.rhunk.snapenhance.common.data.MessagingFriendInfo import me.rhunk.snapenhance.common.data.MessagingFriendInfo
import me.rhunk.snapenhance.common.data.MessagingGroupInfo 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.HookAdapter
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.hook.hookConstructor
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.system.exitProcess import kotlin.system.exitProcess
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
@ -166,6 +169,8 @@ class SnapEnhance {
} }
} }
private var safeMode = false
private fun onActivityCreate(activity: Activity) { private fun onActivityCreate(activity: Activity) {
measureTimeMillis { measureTimeMillis {
with(appContext) { with(appContext) {
@ -173,6 +178,10 @@ class SnapEnhance {
inAppOverlay.onActivityCreate(activity) inAppOverlay.onActivityCreate(activity)
scriptRuntime.eachModule { callFunction("module.onSnapMainActivityCreate", activity) } scriptRuntime.eachModule { callFunction("module.onSnapMainActivityCreate", activity) }
actionManager.onActivityCreate() actionManager.onActivityCreate()
if (safeMode) {
appContext.inAppOverlay.showStatusToast(Icons.Outlined.Cancel, "Failed to load security features! Snapchat may not work properly.", durationMs = 5000)
}
} }
}.also { time -> }.also { time ->
appContext.log.verbose("onActivityCreate took $time") appContext.log.verbose("onActivityCreate took $time")
@ -180,36 +189,58 @@ class SnapEnhance {
} }
private fun initNative() { private fun initNative() {
// don't initialize native when not logged in val lateInit = appContext.native.initOnce {
if ( nativeUnaryCallCallback = { request ->
!appContext.isLoggedIn() && appContext.event.post(NativeUnaryCallEvent(request.uri, request.buffer)) {
appContext.bridgeClient.getDebugProp("force_native_load", null) != "true" request.buffer = buffer
) return request.canceled = canceled
if (appContext.config.experimental.nativeHooks.globalState != true) return }
}
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 { Runtime::class.java.declaredMethods.first {
it.name == "loadLibrary0" && it.parameterTypes.contentEquals( it.name == "loadLibrary0" && it.parameterTypes.contentEquals(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) arrayOf(Class::class.java, String::class.java) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) arrayOf(Class::class.java, String::class.java)
else arrayOf(ClassLoader::class.java, String::class.java) else arrayOf(ClassLoader::class.java, String::class.java)
) )
}.hook(HookStage.AFTER) { param -> }.apply {
val libName = param.arg<String>(1) if (safeMode) {
if (libName != "client") return@hook hook(HookStage.BEFORE) { param ->
unhook() if (param.arg<String>(1) != "scplugin") return@hook
appContext.native.initOnce { appContext.log.warn("Can't load scplugin in safe mode")
nativeUnaryCallCallback = { request -> Thread.sleep(Long.MAX_VALUE)
appContext.event.post(NativeUnaryCallEvent(request.uri, request.buffer)) {
request.buffer = buffer
request.canceled = canceled
}
} }
appContext.reloadNativeConfig()
} }
BaseDexClassLoader::class.java.hookConstructor(HookStage.AFTER) {
appContext.native.hideAnonymousDexFiles() lateinit var unhook: () -> Unit
} hook(HookStage.AFTER) { param ->
}.also { unhook = { it.unhook() } } val libName = param.arg<String>(1)
if (libName != "client") return@hook
unhook()
appContext.log.verbose("libclient lateInit")
lateInit()
}.also { unhook = { it.unhook() } }
}
} }
private fun initConfigListener() { private fun initConfigListener() {

View File

@ -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.HookStage
import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hook
class DeviceSpooferHook: Feature("device_spoofer") { class DeviceSpooferHook: Feature("Device Spoofer") {
private fun hookInstallerPackageName() { private fun hookInstallerPackageName() {
context.androidContext.packageManager::class.java.hook("getInstallerPackageName", HookStage.BEFORE) { param -> context.androidContext.packageManager::class.java.hook("getInstallerPackageName", HookStage.BEFORE) { param ->
param.setResult("com.android.vending") param.setResult("com.android.vending")

View File

@ -13,7 +13,6 @@ typedef struct {
bool disable_bitmoji; bool disable_bitmoji;
bool disable_metrics; bool disable_metrics;
bool composer_hooks; bool composer_hooks;
bool remap_executable;
char custom_emoji_font_path[256]; char custom_emoji_font_path[256];
} native_config_t; } native_config_t;

View File

@ -9,8 +9,5 @@ static pthread_mutex_t hook_mutex = PTHREAD_MUTEX_INITIALIZER;
static void inline SafeHook(void *addr, void *hook, void **original) { static void inline SafeHook(void *addr, void *hook, void **original) {
pthread_mutex_lock(&hook_mutex); pthread_mutex_lock(&hook_mutex);
DobbyHook(addr, hook, original); 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); pthread_mutex_unlock(&hook_mutex);
} }

View File

@ -0,0 +1,54 @@
#pragma once
#include <map>
namespace LinkerHook {
static auto linker_openat_hooks = std::map<std::string, std::pair<uintptr_t, size_t>>();
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);
}
}

View File

@ -7,6 +7,7 @@
#include "logger.h" #include "logger.h"
#include "common.h" #include "common.h"
#include "dobby_helper.h" #include "dobby_helper.h"
#include "hooks/linker_hook.h"
#include "hooks/unary_call.h" #include "hooks/unary_call.h"
#include "hooks/fstat_hook.h" #include "hooks/fstat_hook.h"
#include "hooks/sqlite_mutex.h" #include "hooks/sqlite_mutex.h"
@ -17,9 +18,6 @@
bool JNICALL init(JNIEnv *env, jobject clazz) { bool JNICALL init(JNIEnv *env, jobject clazz) {
LOGD("Initializing native"); LOGD("Initializing native");
using namespace common; 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); native_lib_object = env->NewGlobalRef(clazz);
client_module = util::get_module("libclient.so"); 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_bitmoji = GET_CONFIG_BOOL("disableBitmoji");
native_config->disable_metrics = GET_CONFIG_BOOL("disableMetrics"); native_config->disable_metrics = GET_CONFIG_BOOL("disableMetrics");
native_config->composer_hooks = GET_CONFIG_BOOL("composerHooks"); 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)); 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;")); 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 *_) { extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *_) {
common::java_vm = vm; 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({"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({"setComposerLoader", "(Ljava/lang/String;)V", (void *) ComposerHook::setComposerLoader});
methods.push_back({"composerEval", "(Ljava/lang/String;)Ljava/lang/String;",(void *) ComposerHook::composerEval}); 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()); env->RegisterNatives(env->FindClass(std::string(BUILD_NAMESPACE "/NativeLib").c_str()), methods.data(), methods.size());
return JNI_VERSION_1_6; return JNI_VERSION_1_6;

View File

@ -52,46 +52,6 @@ namespace util {
return { start_offset, end_offset - start_offset }; return { start_offset, end_offset - start_offset };
} }
static void remap_sections(std::function<bool(const std::string &, size_t)> 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) { static uintptr_t find_signature(uintptr_t module_base, uintptr_t size, const std::string &pattern, int offset = 0) {
std::vector<char> bytes; std::vector<char> bytes;
std::vector<char> mask; std::vector<char> mask;

View File

@ -8,7 +8,5 @@ data class NativeConfig(
@JvmField @JvmField
val composerHooks: Boolean = false, val composerHooks: Boolean = false,
@JvmField @JvmField
val remapExecutable: Boolean = false,
@JvmField
val customEmojiFontPath: String? = null, val customEmojiFontPath: String? = null,
) )

View File

@ -1,6 +1,9 @@
package me.rhunk.snapenhance.nativelib package me.rhunk.snapenhance.nativelib
import android.annotation.SuppressLint
import android.util.Log import android.util.Log
import kotlin.math.absoluteValue
import kotlin.random.Random
@Suppress("KotlinJniMissingFunction") @Suppress("KotlinJniMissingFunction")
class NativeLib { class NativeLib {
@ -11,19 +14,21 @@ class NativeLib {
private set private set
} }
fun initOnce(callback: NativeLib.() -> Unit) { fun initOnce(callback: NativeLib.() -> Unit): () -> Unit {
if (initialized) throw IllegalStateException("NativeLib already initialized") if (initialized) throw IllegalStateException("NativeLib already initialized")
runCatching { return runCatching {
System.loadLibrary(BuildConfig.NATIVE_NAME) System.loadLibrary(BuildConfig.NATIVE_NAME)
initialized = true initialized = true
callback(this) callback(this)
if (!init()) { return@runCatching {
throw IllegalStateException("NativeLib init failed. Check logcat for more info") if (!init()) {
throw IllegalStateException("NativeLib init failed. Check logcat for more info")
}
} }
}.onFailure { }.onFailure {
initialized = false initialized = false
Log.e("SnapEnhance", "NativeLib init failed", it) Log.e("SnapEnhance", "NativeLib init failed", it)
} }.getOrThrow()
} }
@Suppress("unused") @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 init(): Boolean
private external fun loadConfig(config: NativeConfig) private external fun loadConfig(config: NativeConfig)
private external fun lockDatabase(name: String, callback: Runnable) private external fun lockDatabase(name: String, callback: Runnable)
external fun setComposerLoader(code: String) external fun setComposerLoader(code: String)
external fun composerEval(code: String): String? external fun composerEval(code: String): String?
external fun hideAnonymousDexFiles() private external fun addLinkerSharedLibrary(path: String, content: ByteArray)
} }