mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-29 21:10:20 +02:00
feat: security features
Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
parent
f9974b0e84
commit
8d17d55c83
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -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() {
|
||||||
|
@ -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")
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
54
native/jni/src/hooks/linker_hook.h
Normal file
54
native/jni/src/hooks/linker_hook.h
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
)
|
)
|
@ -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)
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user