package me.eternal.purrfect.core.features.impl.experiments import android.annotation.SuppressLint import android.location.Location import android.net.ConnectivityManager import android.net.LinkAddress import android.net.LinkProperties import android.net.Network import android.net.NetworkCapabilities import android.os.Build import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Wifi import me.eternal.purrfect.core.features.Feature import me.eternal.purrfect.core.util.LSPatchUpdater import me.eternal.purrfect.core.util.hook.HookStage import me.eternal.purrfect.core.util.hook.hook import java.lang.reflect.Modifier import java.net.InetAddress import java.security.SecureRandom import java.util.Locale import java.util.TimeZone import me.eternal.purrfect.common.config.ModConfig class DeviceSpooferHook : Feature("Device Spoofer") { private var spoofedAndroidId: String? = null private var spoofedDeviceInfo: DeviceInfo? = null private var spoofedFingerprint: String? = null private var randomizedProfile: RandomizedDeviceProfile? = null private fun spoofPrefs() = context.androidContext.getSharedPreferences("purrfect_spoof", 0) private fun generateAndroidId(): String { val customId = context.config.experimental.spoof.spoofDeviceId.customAndroidId.getNullable() if (!customId.isNullOrEmpty()) { val normalizedId = customId.lowercase().trim() if (normalizedId.length == 16 && normalizedId.all { it in '0'..'9' || it in 'a'..'f' }) { if (spoofedAndroidId != normalizedId) { spoofedAndroidId = normalizedId context.log.info("Using custom Android ID: $spoofedAndroidId") } return spoofedAndroidId!! } else { context.log.warn("Invalid custom Android ID format (must be 16 hex chars), generating new one") } } val sharedPrefs = spoofPrefs() val storedId = sharedPrefs.getString("android_id", null) if (storedId == null || storedId.length != 16) { spoofedAndroidId = DeviceSpoofer.generateAndroidId() sharedPrefs.edit().putString("android_id", spoofedAndroidId).apply() context.log.info("Generated new Android ID: $spoofedAndroidId") } else if (spoofedAndroidId != storedId) { spoofedAndroidId = storedId context.log.info("Using stored Android ID: $spoofedAndroidId") } return spoofedAndroidId!! } private fun getDeviceInfo(modelName: String): DeviceInfo? = DeviceSpoofer.getDeviceInfo(modelName) private fun getSpoofedDeviceInfo(): DeviceInfo? { val selectedModel = context.config.experimental.spoof.deviceModel.getNullable() ?: return null if (selectedModel == "random") { val sharedPrefs = spoofPrefs() val randomDevice = sharedPrefs.getString("random_device", null) if (randomDevice == null) { val availableDevices = DeviceSpoofer.getAvailableDevices() val newRandomDevice = availableDevices.random() sharedPrefs.edit().putString("random_device", newRandomDevice).apply() context.log.info("Randomly selected device: $newRandomDevice") spoofedDeviceInfo = getDeviceInfo(newRandomDevice) } else { context.log.info("Using stored random device: $randomDevice") spoofedDeviceInfo = getDeviceInfo(randomDevice) } return spoofedDeviceInfo } if (selectedModel == "none" || selectedModel == "null") return null spoofedDeviceInfo = getDeviceInfo(selectedModel) return spoofedDeviceInfo } private fun getSpoofedFingerprint(deviceInfo: DeviceInfo): String { val sharedPrefs = spoofPrefs() val storedFingerprint = sharedPrefs.getString("device_fingerprint", null) if (storedFingerprint == null) { val buildVersion = Build.VERSION.RELEASE spoofedFingerprint = DeviceSpoofer.generateFingerprint(deviceInfo, buildVersion) sharedPrefs.edit().putString("device_fingerprint", spoofedFingerprint).apply() context.log.info("Generated new device fingerprint: $spoofedFingerprint") } else { spoofedFingerprint = storedFingerprint context.log.info("Using stored device fingerprint: $spoofedFingerprint") } return spoofedFingerprint!! } private fun getRandomGsfId(): String { val sharedPrefs = spoofPrefs() val savedGsfId = sharedPrefs.getString("gsf_id", null) if (savedGsfId != null) return savedGsfId val random = SecureRandom() val gsfId = (1..16).map { "0123456789abcdef"[random.nextInt(16)] }.joinToString("") sharedPrefs.edit().putString("gsf_id", gsfId).apply() return gsfId } private fun generateRandomUUID(): String { val sharedPrefs = spoofPrefs() val savedUuid = sharedPrefs.getString("advertising_id", null) if (savedUuid != null) return savedUuid val random = SecureRandom() val uuid = "%08x-%04x-%04x-%04x-%012x".format( random.nextInt(), random.nextInt() and 0xFFFF, (random.nextInt() and 0x0FFF) or 0x4000, (random.nextInt() and 0x3FFF) or 0x8000, random.nextLong() and 0xFFFFFFFFFFFFL ) sharedPrefs.edit().putString("advertising_id", uuid).apply() return uuid } private fun generateRandomMacAddress(): String { val sharedPrefs = spoofPrefs() val savedMac = sharedPrefs.getString("bluetooth_address", null) if (savedMac != null) return savedMac val random = SecureRandom() val mac = (1..6).map { "%02x".format(random.nextInt(256)) }.joinToString(":") sharedPrefs.edit().putString("bluetooth_address", mac).apply() return mac } private fun hookInstallerPackageName() { context.androidContext.packageManager::class.java.hook("getInstallerPackageName", HookStage.BEFORE) { param -> param.setResult("com.android.vending") } } private fun getRandomizedProfile(): RandomizedDeviceProfile { if (randomizedProfile != null) return randomizedProfile!! val spoofConfig = context.config.experimental.spoof.randomizeDeviceProfile val generationToken = spoofConfig.profileGenerationToken.getNullable() val configProfileJson = spoofConfig.profileData.getNullable() // Priority 1: Use the profile data explicitly saved in ModConfig (Manager is the Single Source of Truth) if (!configProfileJson.isNullOrBlank()) { runCatching { val profile = RandomizedDeviceProfile.fromJson(configProfileJson) val storedToken = context.androidContext.getSharedPreferences("purrfect_spoof", 0) .getString("randomized_device_profile_token", "") ?: "" // If the Manager generated a new profile, we sync the token locally so we know we absorbed it. if (storedToken != (generationToken ?: "")) { context.androidContext.getSharedPreferences("purrfect_spoof", 0) .edit() .putString("randomized_device_profile_token", generationToken) .apply() } randomizedProfile = profile return profile }.onFailure { context.log.warn("Failed to parse device profile from config: ${it.message}") } } // Priority 2: Fallback to generation if ModConfig is somehow empty but the feature is enabled return RandomizedDeviceProfileStore .getOrCreate(context.androidContext, context.log, generationToken) .also { profile -> randomizedProfile = profile } } private fun persistRandomizedProfileSnapshot(profile: RandomizedDeviceProfile) { // This function is now deprecated for the Hook process to prevent synchronization resets. // The Manager app will handle its own persistence. } private fun androidIdAsLong(androidId: String): Long { return runCatching { androidId.toULong(16).toLong() }.getOrElse { androidId.hashCode().toLong() } } private fun getEffectiveRandomizedLocale(profile: RandomizedDeviceProfile): Locale { val forcedLanguage = context.config.experimental.spoof.randomizeDeviceProfile.persistentAppLanguage.getNullable() return forcedLanguage?.let(Locale::forLanguageTag) ?: profile.locale() } private data class RandomizedProfileToggleState( val buildProperties: Boolean, val locale: Boolean, val telephony: Boolean, val settings: Boolean, val network: Boolean, val identifiers: Boolean ) private data class RandomizedBuildToggleState( val deviceIdentity: Boolean, val abiLists: Boolean, val systemProperties: Boolean, val fingerprint: Boolean, val display: Boolean, val host: Boolean, val bootloader: Boolean, val buildTime: Boolean ) private data class RandomizedLocaleToggleState( val locale: Boolean, val timeZone: Boolean, val autoTime: Boolean, val autoTimeZone: Boolean ) private data class RandomizedTelephonyToggleState( val mmsUserAgent: Boolean, val networkIdentity: Boolean, val simIdentity: Boolean, val phoneCapabilities: Boolean ) private data class RandomizedSettingsToggleState( val secure: Boolean, val system: Boolean, val global: Boolean ) private data class RandomizedNetworkToggleState( val wifi: Boolean, val dns: Boolean, val captivePortal: Boolean, val ipAddress: Boolean ) private data class RandomizedIdentifierToggleState( val androidId: Boolean, val advertisingId: Boolean, val hardwareAddresses: Boolean ) private fun getRandomizedProfileToggleState(): RandomizedProfileToggleState { val config = context.config.experimental.spoof.randomizeDeviceProfile return RandomizedProfileToggleState( buildProperties = config.buildProperties.globalState == true, locale = config.localeOptions.globalState == true, telephony = config.telephonyOptions.globalState == true, settings = config.settingsOptions.globalState == true, network = config.networkOptions.globalState == true || config.randomizeIpAddress.get(), identifiers = config.identifierOptions.globalState == true ) } private fun getRandomizedBuildToggleState(): RandomizedBuildToggleState { val config = context.config.experimental.spoof.randomizeDeviceProfile.buildProperties val buildVersion = config.buildVersion val deviceIdentity = config.deviceIdentity val abiLists = config.abiLists val systemProperties = config.systemProperties return RandomizedBuildToggleState( deviceIdentity = deviceIdentity.globalState == true && (deviceIdentity.manufacturerModel.get() || deviceIdentity.brandProduct.get() || deviceIdentity.hardwareBoard.get()), abiLists = abiLists.globalState == true && (abiLists.combinedAbis.get() || abiLists.splitAbis.get()), systemProperties = systemProperties.globalState == true && (systemProperties.build.get() || systemProperties.locale.get() || systemProperties.telephony.get()), fingerprint = buildVersion.globalState == true && buildVersion.fingerprint.get(), display = buildVersion.globalState == true && buildVersion.display.get(), host = buildVersion.globalState == true && buildVersion.host.get(), bootloader = buildVersion.globalState == true && buildVersion.bootloader.get(), buildTime = buildVersion.globalState == true && buildVersion.buildTime.get() ) } private fun getRandomizedLocaleToggleState(): RandomizedLocaleToggleState { val config = context.config.experimental.spoof.randomizeDeviceProfile.localeOptions val locale = config.locale val time = config.time return RandomizedLocaleToggleState( locale = locale.globalState == true && (locale.language.get() || locale.region.get()), timeZone = time.globalState == true && (time.timeZoneId.get() || time.timeZoneDisplayName.get()), autoTime = time.globalState == true && time.autoTime.get(), autoTimeZone = time.globalState == true && time.autoTimeZone.get() ) } private fun getRandomizedTelephonyToggleState(): RandomizedTelephonyToggleState { val config = context.config.experimental.spoof.randomizeDeviceProfile.telephonyOptions val mms = config.mms val networkIdentity = config.networkIdentity val simIdentity = config.simIdentity val phoneCapabilities = config.phoneCapabilities return RandomizedTelephonyToggleState( mmsUserAgent = mms.globalState == true && mms.userAgent.get(), networkIdentity = networkIdentity.globalState == true && (networkIdentity.networkType.get() || networkIdentity.operatorNumeric.get() || networkIdentity.operatorName.get() || networkIdentity.countryIso.get()), simIdentity = simIdentity.globalState == true && (simIdentity.countryIso.get() || simIdentity.operatorNumeric.get() || simIdentity.operatorName.get() || simIdentity.simState.get() || simIdentity.hasIccCard.get()), phoneCapabilities = phoneCapabilities.globalState == true && (phoneCapabilities.phoneCount.get() || phoneCapabilities.hearingAid.get() || phoneCapabilities.tty.get() || phoneCapabilities.worldPhone.get() || phoneCapabilities.roaming.get() || phoneCapabilities.smsVoice.get() || phoneCapabilities.phoneType.get()) ) } private fun getRandomizedSettingsToggleState(): RandomizedSettingsToggleState { val config = context.config.experimental.spoof.randomizeDeviceProfile.settingsOptions val secure = config.secureSettings val system = config.systemSettings val global = config.globalSettings return RandomizedSettingsToggleState( secure = secure.globalState == true && (secure.base.get() || secure.tts.get()), system = system.globalState == true && (system.base.get() || system.bluetooth.get()), global = global.globalState == true && global.base.get() ) } private fun getRandomizedNetworkToggleState(): RandomizedNetworkToggleState { val config = context.config.experimental.spoof.randomizeDeviceProfile.networkOptions val wifi = config.wifi val dns = config.dns val captivePortal = config.captivePortal return RandomizedNetworkToggleState( wifi = wifi.globalState == true && (wifi.ssid.get() || wifi.rssi.get()), dns = dns.globalState == true && (dns.servers.get() || dns.searchDomains.get() || dns.privateDns.get()), captivePortal = captivePortal.globalState == true && captivePortal.capability.get(), ipAddress = context.config.experimental.spoof.randomizeDeviceProfile.randomizeIpAddress.get() ) } private fun getRandomizedIdentifierToggleState(): RandomizedIdentifierToggleState { val config = context.config.experimental.spoof.randomizeDeviceProfile.identifierOptions val androidId = config.androidId val advertisingId = config.advertisingId val hardwareAddresses = config.hardwareAddresses return RandomizedIdentifierToggleState( androidId = androidId.globalState == true && (androidId.stringValue.get() || androidId.longValue.get()), advertisingId = advertisingId.globalState == true && (advertisingId.settingsValue.get() || advertisingId.playServices.get()), hardwareAddresses = hardwareAddresses.globalState == true && (hardwareAddresses.wifiMac.get() || hardwareAddresses.bluetoothMac.get()) ) } private fun applyBuildFieldOverrides( manufacturer: String, model: String, brand: String, device: String, product: String, hardware: String, board: String, bootloader: String, display: String, host: String, fingerprint: String, buildTime: Long, overrideDeviceIdentity: Boolean = true, overrideAbiLists: Boolean = true, overrideFingerprint: Boolean = true, overrideDisplay: Boolean = true, overrideHost: Boolean = true, overrideBootloader: Boolean = true, overrideBuildTime: Boolean = true, buildIncremental: String? = null, buildRelease: String? = null, supportedAbis: List? = null, supported32BitAbis: List? = null, supported64BitAbis: List? = null ) { val safeSupportedAbis = supportedAbis?.takeIf { it.isNotEmpty() } ?: (Build.SUPPORTED_ABIS?.toList() ?: emptyList()) val safeSupported32BitAbis = supported32BitAbis?.takeIf { it.isNotEmpty() } ?: (Build.SUPPORTED_32_BIT_ABIS?.toList() ?: emptyList()) val safeSupported64BitAbis = supported64BitAbis?.takeIf { it.isNotEmpty() } ?: (Build.SUPPORTED_64_BIT_ABIS?.toList() ?: emptyList()) Build::class.java.fields.forEach { field -> if (!field.isAccessible) field.isAccessible = true runCatching { val modifiersField = java.lang.reflect.Field::class.java.getDeclaredField("modifiers") modifiersField.isAccessible = true modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv()) } when (field.name) { "MANUFACTURER" -> if (overrideDeviceIdentity) field.set(null, manufacturer) "MODEL" -> if (overrideDeviceIdentity) field.set(null, model) "BRAND" -> if (overrideDeviceIdentity) field.set(null, brand) "DEVICE" -> if (overrideDeviceIdentity) field.set(null, device) "PRODUCT" -> if (overrideDeviceIdentity) field.set(null, product) "HARDWARE" -> if (overrideDeviceIdentity) field.set(null, hardware) "FINGERPRINT" -> if (overrideFingerprint) field.set(null, fingerprint) "BOARD" -> if (overrideDeviceIdentity) runCatching { field.set(null, board) } "BOOTLOADER" -> if (overrideBootloader) runCatching { field.set(null, bootloader) } "DISPLAY" -> if (overrideDisplay) runCatching { field.set(null, display) } "HOST" -> if (overrideHost) runCatching { field.set(null, host) } "TIME" -> if (overrideBuildTime) runCatching { field.setLong(null, buildTime) } "SUPPORTED_ABIS" -> if (overrideAbiLists) runCatching { field.set(null, safeSupportedAbis.toTypedArray()) } "SUPPORTED_32_BIT_ABIS" -> if (overrideAbiLists) runCatching { field.set(null, safeSupported32BitAbis.toTypedArray()) } "SUPPORTED_64_BIT_ABIS" -> if (overrideAbiLists) runCatching { field.set(null, safeSupported64BitAbis.toTypedArray()) } } } Build.VERSION::class.java.fields.forEach { field -> if (!field.isAccessible) field.isAccessible = true runCatching { val modifiersField = java.lang.reflect.Field::class.java.getDeclaredField("modifiers") modifiersField.isAccessible = true modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv()) } when (field.name) { "RELEASE" -> {} "INCREMENTAL" -> {} } } } private fun installSystemPropertyHooks( profile: RandomizedDeviceProfile, buildToggles: RandomizedBuildToggleState, localeToggles: RandomizedLocaleToggleState, telephonyToggles: RandomizedTelephonyToggleState ) { val effectiveLocale = getEffectiveRandomizedLocale(profile) runCatching { findClass("android.os.SystemProperties").hook("get", HookStage.BEFORE) { param -> when (val key = param.argNullable(0) ?: return@hook) { "ro.product.manufacturer" -> if (buildToggles.deviceIdentity) param.setResult(profile.deviceInfo.manufacturer) else return@hook "ro.product.model" -> if (buildToggles.deviceIdentity) param.setResult(profile.deviceInfo.model) else return@hook "ro.product.brand" -> if (buildToggles.deviceIdentity) param.setResult(profile.deviceInfo.brand) else return@hook "ro.product.device" -> if (buildToggles.deviceIdentity) param.setResult(profile.deviceInfo.device) else return@hook "ro.product.name" -> if (buildToggles.deviceIdentity) param.setResult(profile.deviceInfo.product) else return@hook "ro.product.board" -> if (buildToggles.deviceIdentity) param.setResult(profile.deviceInfo.board) else return@hook "ro.hardware" -> if (buildToggles.deviceIdentity) param.setResult(profile.deviceInfo.hardware) else return@hook "ro.build.fingerprint" -> if (buildToggles.fingerprint) param.setResult(profile.buildFingerprint) else return@hook "ro.build.id" -> if (buildToggles.display) param.setResult(profile.buildDisplayId) else return@hook "ro.build.display.id" -> if (buildToggles.display) param.setResult(profile.buildDisplayId) else return@hook "ro.build.version.incremental" -> return@hook "ro.build.version.release" -> return@hook "ro.bootloader" -> if (buildToggles.bootloader) param.setResult(profile.deviceInfo.bootloader) else return@hook "persist.sys.locale" -> if (localeToggles.locale) param.setResult(effectiveLocale.toLanguageTag()) else return@hook "ro.product.locale" -> if (localeToggles.locale) param.setResult(effectiveLocale.toLanguageTag()) else return@hook "ro.product.locale.language" -> if (localeToggles.locale) param.setResult(effectiveLocale.language) else return@hook "ro.product.locale.region" -> if (localeToggles.locale) param.setResult(effectiveLocale.country) else return@hook "persist.sys.timezone" -> if (localeToggles.timeZone) param.setResult(profile.timeZoneId) else return@hook "gsm.operator.alpha" -> if (telephonyToggles.networkIdentity) param.setResult(profile.networkOperatorName) else return@hook "gsm.operator.numeric" -> if (telephonyToggles.networkIdentity) param.setResult(profile.networkOperator) else return@hook "gsm.sim.operator.alpha" -> if (telephonyToggles.simIdentity) param.setResult(profile.simOperatorName) else return@hook "gsm.sim.operator.numeric" -> if (telephonyToggles.simIdentity) param.setResult(profile.simOperator) else return@hook "gsm.sim.operator.iso-country" -> if (telephonyToggles.simIdentity) param.setResult(profile.simCountryIso) else return@hook else -> return@hook } if (key.startsWith("gsm.") || key.startsWith("persist.sys.") || key.startsWith("ro.")) { context.log.verbose("Random profile SystemProperties override: $key") } } context.log.info("Randomized profile SystemProperties hooks installed") }.onFailure { context.log.warn("Failed to hook randomized SystemProperties: ${it.message}") } } private fun installLocaleHooks(profile: RandomizedDeviceProfile, toggles: RandomizedLocaleToggleState) { val effectiveLocale = getEffectiveRandomizedLocale(profile) runCatching { if (toggles.locale) { Locale::class.java.hook("getDefault", HookStage.BEFORE) { param -> param.setResult(effectiveLocale) } } if (toggles.timeZone) { TimeZone::class.java.hook("getDefault", HookStage.BEFORE) { param -> param.setResult(profile.timeZone()) } } context.log.info("Randomized profile locale/timezone hooks installed") }.onFailure { context.log.warn("Failed to hook locale/timezone for randomized profile: ${it.message}") } } private fun installTelephonyHooks(profile: RandomizedDeviceProfile, toggles: RandomizedTelephonyToggleState) { runCatching { findClass("android.telephony.TelephonyManager").apply { if (toggles.mmsUserAgent) { hook("getMmsUserAgent", HookStage.BEFORE) { it.setResult(profile.mmsUserAgent) } } if (toggles.networkIdentity) { hook("getNetworkType", HookStage.BEFORE) { it.setResult(profile.networkType) } hook("getNetworkOperator", HookStage.BEFORE) { it.setResult(profile.networkOperator) } hook("getNetworkOperatorName", HookStage.BEFORE) { it.setResult(profile.networkOperatorName) } hook("getNetworkCountryIso", HookStage.BEFORE) { it.setResult(profile.networkCountryIso) } } if (toggles.simIdentity) { hook("getSimCountryIso", HookStage.BEFORE) { it.setResult(profile.simCountryIso) } hook("getSimOperator", HookStage.BEFORE) { it.setResult(profile.simOperator) } hook("getSimOperatorName", HookStage.BEFORE) { it.setResult(profile.simOperatorName) } hook("getSimState", HookStage.BEFORE) { it.setResult(profile.simState) } hook("hasIccCard", HookStage.BEFORE) { it.setResult(profile.hasIccCard) } } if (toggles.phoneCapabilities) { hook("getPhoneCount", HookStage.BEFORE) { it.setResult(profile.phoneCount) } hook("isHearingAidCompatibilitySupported", HookStage.BEFORE) { it.setResult(profile.isHearingAidCompatibilitySupported) } hook("isTtyModeSupported", HookStage.BEFORE) { it.setResult(profile.isTtySupported) } hook("isWorldPhone", HookStage.BEFORE) { it.setResult(profile.isWorldPhone) } hook("isNetworkRoaming", HookStage.BEFORE) { it.setResult(profile.isNetworkRoaming) } hook("isSmsCapable", HookStage.BEFORE) { it.setResult(profile.isSmsCapable) } hook("isVoiceCapable", HookStage.BEFORE) { it.setResult(profile.isVoiceCapable) } hook("getPhoneType", HookStage.BEFORE) { it.setResult(profile.phoneType) } } } context.log.info("Randomized profile telephony hooks installed") }.onFailure { context.log.warn("Failed to hook telephony for randomized profile: ${it.message}") } } private fun installSettingsHooks( profile: RandomizedDeviceProfile, settingToggles: RandomizedSettingsToggleState, identifierToggles: RandomizedIdentifierToggleState, localeToggles: RandomizedLocaleToggleState ) { fun hookStringSettings(className: String, values: Map) { findClass(className).hook("getString", HookStage.BEFORE) { param -> val key = param.argNullable(1) ?: return@hook values[key]?.let { param.setResult(it) } } } fun hookIntSettings(className: String, values: Map) { findClass(className).hook("getInt", HookStage.BEFORE) { param -> val key = param.argNullable(1) ?: return@hook values[key]?.let { param.setResult(it) } } } runCatching { if (settingToggles.secure || identifierToggles.androidId || identifierToggles.advertisingId || identifierToggles.hardwareAddresses) { val secureStrings = buildMap { if (settingToggles.secure) putAll(profile.secureStringSettings) if (identifierToggles.androidId) put("android_id", profile.androidId) if (identifierToggles.advertisingId) put("advertising_id", profile.advertisingId) if (identifierToggles.hardwareAddresses) put("bluetooth_address", profile.bluetoothMacAddress) } hookStringSettings("android.provider.Settings\$Secure", secureStrings) } if (settingToggles.secure) { hookIntSettings("android.provider.Settings\$Secure", profile.secureIntSettings) } if (identifierToggles.androidId) { findClass("android.provider.Settings\$Secure").hook("getLong", HookStage.BEFORE) { param -> val key = param.argNullable(1) ?: return@hook if (key == "android_id") { param.setResult(androidIdAsLong(profile.androidId)) } } } if (settingToggles.system) { hookStringSettings("android.provider.Settings\$System", profile.systemStringSettings) hookIntSettings("android.provider.Settings\$System", profile.systemIntSettings) } val globalStrings = buildMap { if (settingToggles.global) { putAll(profile.globalStringSettings.filterKeys { it != "auto_time" && it != "auto_time_zone" }) } if (localeToggles.autoTime) { put("auto_time", profile.globalStringSettings["auto_time"] ?: "1") } if (localeToggles.autoTimeZone) { put("auto_time_zone", profile.globalStringSettings["auto_time_zone"] ?: "1") } } if (globalStrings.isNotEmpty()) { hookStringSettings("android.provider.Settings\$Global", globalStrings) } if (settingToggles.global) { hookIntSettings("android.provider.Settings\$Global", profile.globalIntSettings) } context.log.info("Randomized profile settings hooks installed") }.onFailure { context.log.warn("Failed to hook settings for randomized profile: ${it.message}") } } private fun installNetworkHooks( profile: RandomizedDeviceProfile, networkToggles: RandomizedNetworkToggleState, identifierToggles: RandomizedIdentifierToggleState ) { if (networkToggles.wifi || identifierToggles.hardwareAddresses) { runCatching { findClass("android.net.wifi.WifiInfo").apply { if (networkToggles.wifi) { hook("getSSID", HookStage.BEFORE) { it.setResult("\"${profile.wifiSsid}\"") } hook("getRssi", HookStage.BEFORE) { it.setResult(profile.wifiRssi) } } if (identifierToggles.hardwareAddresses) { hook("getMacAddress", HookStage.BEFORE) { it.setResult(profile.wifiMacAddress) } } } } } if (identifierToggles.hardwareAddresses) { runCatching { findClass("android.bluetooth.BluetoothAdapter").hook("getAddress", HookStage.BEFORE) { it.setResult(profile.bluetoothMacAddress) } } } if (networkToggles.dns) { runCatching { LinkProperties::class.java.apply { hook("getDnsServers", HookStage.BEFORE) { param -> param.setResult(profile.dnsServers.map { InetAddress.getByName(it) }) } hook("getDomains", HookStage.BEFORE) { param -> param.setResult(profile.dnsSearchDomains) } hook("isPrivateDnsActive", HookStage.BEFORE) { param -> param.setResult(profile.privateDnsActive) } hook("getPrivateDnsServerName", HookStage.BEFORE) { param -> param.setResult(profile.privateDnsServerName) } } } } if (networkToggles.ipAddress) { runCatching { val spoofedAddress = InetAddress.getByName(profile.ipAddress) val spoofedLinkAddress = runCatching { LinkAddress::class.java.getConstructor(String::class.java) .newInstance("${profile.ipAddress}/24") }.getOrNull() LinkProperties::class.java.apply { hook("getAddresses", HookStage.BEFORE) { param -> param.setResult(listOf(spoofedAddress)) } spoofedLinkAddress?.let { linkAddress -> hook("getLinkAddresses", HookStage.BEFORE) { param -> param.setResult(listOf(linkAddress)) } } } context.log.info("Randomized profile IP override active: ${profile.ipAddress}") }.onFailure { context.log.warn("Failed to hook randomized IP address: ${it.message}") } } if (networkToggles.captivePortal) { runCatching { NetworkCapabilities::class.java.hook("hasCapability", HookStage.BEFORE) { param -> val capability = param.argNullable(0) ?: return@hook if (capability == NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL) { param.setResult(profile.hasCaptivePortal) } } } } if (identifierToggles.advertisingId) { val advertisingInfoClass = runCatching { findClass("com.google.android.gms.ads.identifier.AdvertisingIdClient\$Info") }.getOrNull() if (advertisingInfoClass != null) { advertisingInfoClass.apply { hook("getId", HookStage.BEFORE) { it.setResult(profile.advertisingId) } hook("isLimitAdTrackingEnabled", HookStage.BEFORE) { it.setResult(false) } } context.log.info("Randomized profile network/identifier hooks installed") } } } private fun installRandomizedProfileHooks(profile: RandomizedDeviceProfile) { val toggles = getRandomizedProfileToggleState() val buildToggles = getRandomizedBuildToggleState() val localeToggles = getRandomizedLocaleToggleState() val telephonyToggles = getRandomizedTelephonyToggleState() val settingsToggles = getRandomizedSettingsToggleState() val networkToggles = getRandomizedNetworkToggleState() val identifierToggles = getRandomizedIdentifierToggleState() if (toggles.buildProperties) { applyBuildFieldOverrides( manufacturer = profile.deviceInfo.manufacturer, model = profile.deviceInfo.model, brand = profile.deviceInfo.brand, device = profile.deviceInfo.device, product = profile.deviceInfo.product, hardware = profile.deviceInfo.hardware, board = profile.deviceInfo.board, bootloader = profile.deviceInfo.bootloader, display = profile.buildDisplayId, host = profile.buildHost, fingerprint = profile.buildFingerprint, buildTime = profile.buildTime, overrideDeviceIdentity = buildToggles.deviceIdentity, overrideAbiLists = buildToggles.abiLists, overrideFingerprint = buildToggles.fingerprint, overrideDisplay = buildToggles.display, overrideHost = buildToggles.host, overrideBootloader = buildToggles.bootloader, overrideBuildTime = buildToggles.buildTime, buildIncremental = profile.buildIncremental, buildRelease = profile.androidRelease, supportedAbis = profile.supportedAbis, supported32BitAbis = profile.supported32BitAbis, supported64BitAbis = profile.supported64BitAbis ) if (buildToggles.systemProperties || toggles.locale || toggles.telephony) { installSystemPropertyHooks(profile, buildToggles, localeToggles, telephonyToggles) } } if (toggles.locale && (localeToggles.locale || localeToggles.timeZone)) { installLocaleHooks(profile, localeToggles) } if (toggles.telephony) { installTelephonyHooks(profile, telephonyToggles) } if (toggles.settings || toggles.identifiers) { installSettingsHooks(profile, settingsToggles, identifierToggles, localeToggles) } if (toggles.network || toggles.identifiers) { installNetworkHooks(profile, networkToggles, identifierToggles) } context.log.info( "Randomized profile active: ${profile.deviceInfo.manufacturer} ${profile.deviceInfo.model}, " + "androidId=${profile.androidId}, operator=${profile.simOperatorName}, locale=${profile.localeTag}" ) } @SuppressLint("MissingPermission") override fun init() { val randomizeDeviceProfile by context.config.experimental.spoof.randomizeDeviceProfile val spoofDevice by context.config.experimental.spoof.spoofDevice val randomizeDeviceProfileEnabled = randomizeDeviceProfile == true if (!randomizeDeviceProfileEnabled && spoofDevice) { val deviceInfo = getSpoofedDeviceInfo() if (deviceInfo != null) { getSpoofedFingerprint(deviceInfo) context.log.info("Device spoofing initialized: ${deviceInfo.manufacturer} ${deviceInfo.model}") } } if (LSPatchUpdater.HAS_LSPATCH) { hookInstallerPackageName() } if (context.config.experimental.spoof.globalState != true) return if (randomizeDeviceProfileEnabled) { val profile = getRandomizedProfile() installRandomizedProfileHooks(profile) if (context.config.experimental.spoof.randomizeDeviceProfile.showActivationOverlay.get()) { onNextActivityCreate { context.inAppOverlay.showStatusToast( icon = Icons.Filled.CheckCircle, text = "Randomized device profile active", durationMs = 3200 ) } } } val removeMockLocationFlag by context.config.experimental.spoof.removeMockLocationFlag val overridePlayStoreInstallerPackageName by context.config.experimental.spoof.overridePlayStoreInstallerPackageName val removeVpnTransportFlag by context.config.experimental.spoof.removeVpnTransportFlag val forceWifiTransportFlag by context.config.experimental.spoof.forceWifiTransportFlag val networkOptimization by context.config.experimental.networkOptimization val spoofAndroidId by context.config.experimental.spoof.spoofDeviceId.spoofAndroidId if (overridePlayStoreInstallerPackageName) { hookInstallerPackageName() } if (removeMockLocationFlag) { Location::class.java.hook("isMock", HookStage.BEFORE) { param -> param.setResult(false) } } if (removeVpnTransportFlag) { ConnectivityManager::class.java.hook("getAllNetworks", HookStage.AFTER) { param -> val instance = param.thisObject() as? ConnectivityManager ?: return@hook val networks = param.getResult() as? Array<*> ?: return@hook param.setResult(networks.filterIsInstance().filter { network -> val capabilities = instance.getNetworkCapabilities(network) ?: return@filter false !capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) }.toTypedArray()) } } if (forceWifiTransportFlag) { val connectivityManager = context.androidContext.getSystemService(ConnectivityManager::class.java) val activeNetwork = connectivityManager?.activeNetwork val initialCapabilities = activeNetwork?.let { connectivityManager.getNetworkCapabilities(it) } val isReallyOnWifi = initialCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true onNextActivityCreate { if (isReallyOnWifi) { context.inAppOverlay.showStatusToast( icon = Icons.Filled.Wifi, text = "Connected to Real WiFi", durationMs = 3000 ) } else { context.inAppOverlay.showStatusToast( icon = Icons.Filled.Wifi, text = "WiFi Spoofing Active", durationMs = 3000 ) } } NetworkCapabilities::class.java.apply { hook("hasTransport", HookStage.BEFORE) { param -> val transportType = param.args().getOrNull(0) as? Int val actuallyHasWifi = runCatching { param.invokeOriginal() as Boolean }.getOrDefault(false) if (actuallyHasWifi && transportType == NetworkCapabilities.TRANSPORT_WIFI) { return@hook } if (transportType == NetworkCapabilities.TRANSPORT_WIFI) { param.setResult(true) } else if (transportType == NetworkCapabilities.TRANSPORT_CELLULAR) { param.setResult(false) } } hook("hasCapability", HookStage.BEFORE) { param -> val capability = param.args().getOrNull(0) as? Int if (capability == NetworkCapabilities.NET_CAPABILITY_NOT_VPN) { param.setResult(true) } } } findClass("android.net.NetworkInfo").apply { hook("getType", HookStage.BEFORE) { param -> val originalType = runCatching { param.invokeOriginal() as Int }.getOrDefault(-1) if (originalType == 1) { return@hook } param.setResult(1) } hook("getTypeName", HookStage.BEFORE) { param -> val originalTypeName = runCatching { param.invokeOriginal() as String }.getOrNull() if (originalTypeName?.equals("WIFI", ignoreCase = true) == true) { return@hook } param.setResult("WIFI") } hook("isConnected", HookStage.BEFORE) { param -> param.setResult(true) } } findClass("org.chromium.base.RadioUtils").hook("isWifiConnected", HookStage.BEFORE) { param -> val actuallyOnWifi = runCatching { val connectivityMgr = context.androidContext.getSystemService(ConnectivityManager::class.java) val activeNet = connectivityMgr?.activeNetwork val caps = activeNet?.let { connectivityMgr.getNetworkCapabilities(it) } caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true }.getOrDefault(false) if (!actuallyOnWifi) { param.setResult(true) } } } if (networkOptimization) { findClass("java.net.Socket").apply { hook("setSendBufferSize", HookStage.BEFORE) { param -> val size = param.arg(0) if (size < 1024 * 1024) param.setArg(0, 1024 * 1024) } hook("setReceiveBufferSize", HookStage.BEFORE) { param -> val size = param.arg(0) if (size < 1024 * 1024) param.setArg(0, 1024 * 1024) } } } if (!randomizeDeviceProfileEnabled && spoofAndroidId) { val gsfId = getRandomGsfId() val wifiMac = generateRandomMacAddress() findClass("android.provider.Settings\$Secure").hook("getString", HookStage.BEFORE) { param -> val settingName = param.argNullable(1) when (settingName) { "android_id" -> param.setResult(generateAndroidId()) "advertising_id" -> param.setResult(generateRandomUUID()) "bluetooth_address" -> param.setResult(wifiMac) "gsf_id" -> param.setResult(gsfId) else -> return@hook } context.log.verbose("Manual Android ID spoof override: $settingName") } findClass("android.provider.Settings\$Secure").hook("getLong", HookStage.BEFORE) { param -> val settingName = param.argNullable(1) if (settingName == "android_id") { param.setResult(androidIdAsLong(generateAndroidId())) } } runCatching { findClass("android.net.wifi.WifiInfo").hook("getMacAddress", HookStage.BEFORE) { param -> param.setResult(wifiMac) } } runCatching { findClass("android.bluetooth.BluetoothAdapter").hook("getAddress", HookStage.BEFORE) { param -> param.setResult(wifiMac) } } } if (!randomizeDeviceProfileEnabled && spoofDevice) { val deviceInfo = getSpoofedDeviceInfo() if (deviceInfo != null) { val fingerprint = getSpoofedFingerprint(deviceInfo) context.log.info("Device spoofing active: ${deviceInfo.manufacturer} ${deviceInfo.model}") applyBuildFieldOverrides( manufacturer = deviceInfo.manufacturer, model = deviceInfo.model, brand = deviceInfo.brand, device = deviceInfo.device, product = deviceInfo.product, hardware = deviceInfo.hardware, board = deviceInfo.board, bootloader = deviceInfo.bootloader, display = deviceInfo.display, host = deviceInfo.host, fingerprint = fingerprint, buildTime = System.currentTimeMillis() - (30L..180L).random() * 24L * 60L * 60L * 1000L ) runCatching { findClass("android.os.SystemProperties").hook("get", HookStage.BEFORE) { param -> when (param.argNullable(0) ?: return@hook) { "ro.product.manufacturer" -> param.setResult(deviceInfo.manufacturer) "ro.product.model" -> param.setResult(deviceInfo.model) "ro.product.brand" -> param.setResult(deviceInfo.brand) "ro.product.device" -> param.setResult(deviceInfo.device) "ro.product.name" -> param.setResult(deviceInfo.product) "ro.product.board" -> param.setResult(deviceInfo.board) "ro.hardware" -> param.setResult(deviceInfo.hardware) "ro.build.fingerprint" -> param.setResult(fingerprint) "ro.bootloader" -> param.setResult(deviceInfo.bootloader) "ro.build.display.id" -> param.setResult(deviceInfo.display) } } context.log.info("SystemProperties hooks installed successfully") }.onFailure { context.log.warn("Failed to hook SystemProperties: ${it.message}") } } } } }