Cache reflection results using lazy fields

This commit is contained in:
世界
2026-01-17 03:16:53 +08:00
parent 0fe4a3b6a1
commit 0cb6f1fb7d
11 changed files with 312 additions and 112 deletions

View File

@@ -19,30 +19,67 @@ import java.io.IOException
object PrivilegedServiceUtils {
private val iPackageManagerStubClass by lazy { Class.forName("android.content.pm.IPackageManager\$Stub") }
private val asInterfaceMethod by lazy { iPackageManagerStubClass.getMethod("asInterface", IBinder::class.java) }
private val iPackageManagerClass by lazy { Class.forName("android.content.pm.IPackageManager") }
private val getInstalledPackagesMethodLong by lazy {
iPackageManagerClass.getMethod(
"getInstalledPackages",
Long::class.javaPrimitiveType,
Int::class.javaPrimitiveType
)
}
private val getInstalledPackagesMethodInt by lazy {
iPackageManagerClass.getMethod(
"getInstalledPackages",
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType
)
}
private val getPackageInstallerMethod by lazy { iPackageManagerClass.getMethod("getPackageInstaller") }
private val packageInstallerCtorS by lazy {
PackageInstaller::class.java.getConstructor(
IPackageInstaller::class.java,
String::class.java,
String::class.java,
Int::class.javaPrimitiveType
)
}
private val packageInstallerCtorPre by lazy {
PackageInstaller::class.java.getConstructor(
IPackageInstaller::class.java,
String::class.java,
Int::class.javaPrimitiveType
)
}
private val sessionCtor by lazy {
PackageInstaller.Session::class.java.getConstructor(IPackageInstallerSession::class.java)
}
private val intentSenderCtor by lazy {
IntentSender::class.java.getConstructor(IIntentSender::class.java)
}
private val installFlagsField by lazy {
PackageInstaller.SessionParams::class.java.getDeclaredField("installFlags").apply { isAccessible = true }
}
private val getListMethod by lazy {
Class.forName("android.content.pm.ParceledListSlice").getMethod("getList")
}
private fun getPackageManager(): Any {
val binder = SystemServiceHelperCompat.getSystemService("package") ?: throw IllegalStateException("package service not available")
val stubClass = Class.forName("android.content.pm.IPackageManager\$Stub")
val asInterface = stubClass.getMethod("asInterface", IBinder::class.java)
return asInterface.invoke(null, binder) ?: throw IllegalStateException("IPackageManager is null")
val binder = SystemServiceHelperCompat.getSystemService("package")
?: throw IllegalStateException("package service not available")
return asInterfaceMethod.invoke(null, binder)
?: throw IllegalStateException("IPackageManager is null")
}
fun getInstalledPackages(flags: Int, userId: Int): List<PackageInfo> {
val iPackageManager = getPackageManager()
val iPackageManagerClass = Class.forName("android.content.pm.IPackageManager")
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val method = iPackageManagerClass.getMethod(
"getInstalledPackages",
Long::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
)
method.invoke(iPackageManager, flags.toLong(), userId)
getInstalledPackagesMethodLong.invoke(iPackageManager, flags.toLong(), userId)
} else {
val method = iPackageManagerClass.getMethod(
"getInstalledPackages",
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
)
method.invoke(iPackageManager, flags, userId)
getInstalledPackagesMethodInt.invoke(iPackageManager, flags, userId)
}
return extractPackageList(result)
}
@@ -62,9 +99,6 @@ object PrivilegedServiceUtils {
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
params.setAppPackageName(BuildConfig.APPLICATION_ID)
// Set INSTALL_REPLACE_EXISTING flag (value = 2)
val installFlagsField = PackageInstaller.SessionParams::class.java.getDeclaredField("installFlags")
installFlagsField.isAccessible = true
installFlagsField.setInt(params, installFlagsField.getInt(params) or 2)
val sessionId = packageInstaller.createSession(params)
@@ -106,9 +140,7 @@ object PrivilegedServiceUtils {
private fun getPackageInstaller(): IPackageInstaller {
val iPackageManager = getPackageManager()
val iPackageManagerClass = Class.forName("android.content.pm.IPackageManager")
val method = iPackageManagerClass.getMethod("getPackageInstaller")
val installer = method.invoke(iPackageManager) as IPackageInstaller
val installer = getPackageInstallerMethod.invoke(iPackageManager) as IPackageInstaller
return IPackageInstaller.Stub.asInterface(installer.asBinder())
}
@@ -119,23 +151,14 @@ object PrivilegedServiceUtils {
userId: Int,
): PackageInstaller {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PackageInstaller::class.java.getConstructor(
IPackageInstaller::class.java,
String::class.java,
String::class.java,
Int::class.javaPrimitiveType,
).newInstance(installer, installerPackageName, installerAttributionTag, userId)
packageInstallerCtorS.newInstance(installer, installerPackageName, installerAttributionTag, userId)
} else {
PackageInstaller::class.java.getConstructor(
IPackageInstaller::class.java,
String::class.java,
Int::class.javaPrimitiveType,
).newInstance(installer, installerPackageName, userId)
packageInstallerCtorPre.newInstance(installer, installerPackageName, userId)
}
}
private fun createSession(session: IPackageInstallerSession): PackageInstaller.Session {
return PackageInstaller.Session::class.java.getConstructor(IPackageInstallerSession::class.java).newInstance(session)
return sessionCtor.newInstance(session)
}
private fun createIntentSender(onResult: (Intent) -> Unit): IntentSender {
@@ -152,13 +175,12 @@ object PrivilegedServiceUtils {
onResult(intent)
}
}
return IntentSender::class.java.getConstructor(IIntentSender::class.java).newInstance(sender)
return intentSenderCtor.newInstance(sender)
}
@Suppress("UNCHECKED_CAST")
private fun extractPackageList(parceledListSlice: Any?): List<PackageInfo> {
if (parceledListSlice == null) return emptyList()
val getListMethod = parceledListSlice.javaClass.getMethod("getList")
val list = getListMethod.invoke(parceledListSlice) as? List<*>
return list?.filterIsInstance<PackageInfo>() ?: emptyList()
}

View File

@@ -1,5 +1,5 @@
package io.nekohasekai.sfa.xposed
object HookModuleVersion {
const val CURRENT = 2
const val CURRENT = 3
}

View File

@@ -3,8 +3,7 @@ package io.nekohasekai.sfa.xposed
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Process
import de.robv.android.xposed.XposedHelpers
import io.nekohasekai.sfa.BuildConfig
import java.lang.reflect.Method
import java.util.concurrent.ConcurrentHashMap
object PrivilegeChecker {
@@ -21,6 +20,13 @@ object PrivilegeChecker {
private val exemptCache = ConcurrentHashMap<Int, Boolean>()
private val privilegedCache = ConcurrentHashMap<Int, Boolean>()
private val appGlobalsClass by lazy { Class.forName("android.app.AppGlobals") }
private val getPackageManagerMethod by lazy { appGlobalsClass.getMethod("getPackageManager") }
private var getPackagesForUidMethod: Method? = null
private var checkUidPermissionMethod: Method? = null
private var getApplicationInfoMethodLong: Method? = null
private var getApplicationInfoMethodInt: Method? = null
fun isPrivilegedUid(uid: Int): Boolean {
if (uid < Process.FIRST_APPLICATION_UID) {
return true
@@ -44,9 +50,16 @@ object PrivilegeChecker {
return true
}
}
val checkMethod = checkUidPermissionMethod ?: run {
pm.javaClass.getMethod(
"checkUidPermission",
String::class.java,
Int::class.javaPrimitiveType
).also { checkUidPermissionMethod = it }
}
for (permission in privilegedPermissions) {
val result = try {
XposedHelpers.callMethod(pm, "checkUidPermission", permission, uid) as? Int
checkMethod.invoke(pm, permission, uid) as? Int
} catch (_: Throwable) {
null
}
@@ -83,7 +96,11 @@ object PrivilegeChecker {
private fun getPackagesForUid(uid: Int): List<String> {
val pm = getPackageManager() ?: return emptyList()
return try {
val method = pm.javaClass.getMethod("getPackagesForUid", Int::class.javaPrimitiveType)
val method = getPackagesForUidMethod ?: run {
pm.javaClass.getMethod("getPackagesForUid", Int::class.javaPrimitiveType).also {
getPackagesForUidMethod = it
}
}
val result = method.invoke(pm, uid)
when (result) {
is Array<*> -> result.filterIsInstance<String>()
@@ -97,9 +114,7 @@ object PrivilegeChecker {
private fun getPackageManager(): Any? {
return try {
val appGlobals = Class.forName("android.app.AppGlobals")
val method = appGlobals.getMethod("getPackageManager")
method.invoke(null)
getPackageManagerMethod.invoke(null)
} catch (_: Throwable) {
null
}
@@ -107,10 +122,26 @@ object PrivilegeChecker {
private fun getApplicationInfo(pm: Any, pkg: String, userId: Int): ApplicationInfo? {
return try {
XposedHelpers.callMethod(pm, "getApplicationInfo", pkg, 0, userId) as? ApplicationInfo
val method = getApplicationInfoMethodInt ?: run {
pm.javaClass.getMethod(
"getApplicationInfo",
String::class.java,
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType
).also { getApplicationInfoMethodInt = it }
}
method.invoke(pm, pkg, 0, userId) as? ApplicationInfo
} catch (_: Throwable) {
try {
XposedHelpers.callMethod(pm, "getApplicationInfo", pkg, 0L, userId) as? ApplicationInfo
val method = getApplicationInfoMethodLong ?: run {
pm.javaClass.getMethod(
"getApplicationInfo",
String::class.java,
Long::class.javaPrimitiveType,
Int::class.javaPrimitiveType
).also { getApplicationInfoMethodLong = it }
}
method.invoke(pm, pkg, 0L, userId) as? ApplicationInfo
} catch (_: Throwable) {
null
}

View File

@@ -1,8 +1,8 @@
package io.nekohasekai.sfa.xposed
import java.io.File
import java.lang.reflect.Method
import java.util.concurrent.ConcurrentHashMap
import io.nekohasekai.sfa.xposed.HookErrorStore
object PrivilegeSettingsStore {
private const val SETTINGS_DIR = "/data/system/sing-box"
@@ -17,6 +17,10 @@ object PrivilegeSettingsStore {
private var interfacePrefix = "en"
private val uidCache = ConcurrentHashMap<Int, Boolean>()
private val appGlobalsClass by lazy { Class.forName("android.app.AppGlobals") }
private val getPackageManagerMethod by lazy { appGlobalsClass.getMethod("getPackageManager") }
private var getPackagesForUidMethod: Method? = null
fun update(
enabled: Boolean,
packages: Set<String>,
@@ -110,7 +114,11 @@ object PrivilegeSettingsStore {
private fun getPackagesForUid(uid: Int): List<String> {
val pm = getPackageManager() ?: return emptyList()
return try {
val method = pm.javaClass.getMethod("getPackagesForUid", Int::class.javaPrimitiveType)
val method = getPackagesForUidMethod ?: run {
pm.javaClass.getMethod("getPackagesForUid", Int::class.javaPrimitiveType).also {
getPackagesForUidMethod = it
}
}
val result = method.invoke(pm, uid)
when (result) {
is Array<*> -> result.filterIsInstance<String>()
@@ -125,9 +133,7 @@ object PrivilegeSettingsStore {
private fun getPackageManager(): Any? {
return try {
val appGlobals = Class.forName("android.app.AppGlobals")
val method = appGlobals.getMethod("getPackageManager")
method.invoke(null)
getPackageManagerMethod.invoke(null)
} catch (e: Throwable) {
HookErrorStore.e("PrivilegeSettingsStore", "getPackageManager failed", e)
null

View File

@@ -7,6 +7,7 @@ import android.content.pm.PackageManager
import android.os.Binder
import android.os.SystemClock
import io.nekohasekai.sfa.BuildConfig
import java.lang.reflect.Method
import java.util.concurrent.ConcurrentHashMap
object VpnAppStore {
@@ -20,6 +21,16 @@ object VpnAppStore {
private val uidVpnCache = ConcurrentHashMap<Int, CacheEntry<Boolean>>()
private val uidPackagesCache = ConcurrentHashMap<Int, CacheEntry<List<String>>>()
private val appGlobalsClass by lazy { Class.forName("android.app.AppGlobals") }
private val getPackageManagerMethod by lazy { appGlobalsClass.getMethod("getPackageManager") }
@Volatile
private var pmClass: Class<*>? = null
private var getPackagesForUidMethod: Method? = null
private var getInstalledPackagesMethodLong: Method? = null
private var getInstalledPackagesMethodInt: Method? = null
private var getListMethod: Method? = null
fun isVpnUid(uid: Int): Boolean {
val now = SystemClock.uptimeMillis()
val cached = uidVpnCache[uid]
@@ -57,7 +68,11 @@ object VpnAppStore {
val result = binderLocalScope {
val pm = getPackageManager() ?: return@binderLocalScope emptyList<String>()
try {
val method = pm.javaClass.getMethod("getPackagesForUid", Int::class.javaPrimitiveType)
val method = getPackagesForUidMethod ?: run {
pm.javaClass.getMethod("getPackagesForUid", Int::class.javaPrimitiveType).also {
getPackagesForUidMethod = it
}
}
when (val raw = method.invoke(pm, uid)) {
is Array<*> -> raw.filterIsInstance<String>()
is List<*> -> raw.filterIsInstance<String>()
@@ -108,19 +123,23 @@ object VpnAppStore {
private fun getInstalledPackagesCompat(pm: Any, flags: Long, userId: Int): List<PackageInfo> {
val result = try {
val method = pm.javaClass.getMethod(
"getInstalledPackages",
Long::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
)
val method = getInstalledPackagesMethodLong ?: run {
pm.javaClass.getMethod(
"getInstalledPackages",
Long::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
).also { getInstalledPackagesMethodLong = it }
}
method.invoke(pm, flags, userId)
} catch (_: Throwable) {
try {
val method = pm.javaClass.getMethod(
"getInstalledPackages",
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
)
val method = getInstalledPackagesMethodInt ?: run {
pm.javaClass.getMethod(
"getInstalledPackages",
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
).also { getInstalledPackagesMethodInt = it }
}
method.invoke(pm, flags.toInt(), userId)
} catch (e: Throwable) {
HookErrorStore.e("VpnAppStore", "getInstalledPackages failed", e)
@@ -137,9 +156,7 @@ object VpnAppStore {
private fun getPackageManager(): Any? {
return try {
val appGlobals = Class.forName("android.app.AppGlobals")
val method = appGlobals.getMethod("getPackageManager")
method.invoke(null)
getPackageManagerMethod.invoke(null)
} catch (e: Throwable) {
HookErrorStore.e("VpnAppStore", "getPackageManager failed", e)
null
@@ -161,7 +178,9 @@ object VpnAppStore {
return raw.filterIsInstance<PackageInfo>()
}
return try {
val method = raw.javaClass.getMethod("getList")
val method = getListMethod ?: run {
raw.javaClass.getMethod("getList").also { getListMethod = it }
}
val list = method.invoke(raw)
if (list is List<*>) {
list.filterIsInstance<PackageInfo>()

View File

@@ -3,6 +3,7 @@ package io.nekohasekai.sfa.xposed
import android.net.LinkProperties
import android.net.NetworkCapabilities
import android.net.NetworkInfo
import android.net.ProxyInfo
import android.os.Build
import android.os.Parcel
import android.os.Process
@@ -14,6 +15,22 @@ object VpnSanitizer {
"tun",
)
private val getStackedLinksMethod by lazy {
LinkProperties::class.java.getMethod("getStackedLinks")
}
private val removeStackedLinkMethod by lazy {
LinkProperties::class.java.getMethod("removeStackedLink", String::class.java)
}
private val setHttpProxyMethod by lazy {
LinkProperties::class.java.getMethod("setHttpProxy", ProxyInfo::class.java)
}
private val removeTransportTypeMethod by lazy {
NetworkCapabilities::class.java.getMethod("removeTransportType", Int::class.javaPrimitiveType)
}
private val addCapabilityMethod by lazy {
NetworkCapabilities::class.java.getMethod("addCapability", Int::class.javaPrimitiveType)
}
fun shouldHide(uid: Int): Boolean {
if (!PrivilegeSettingsStore.shouldHideUid(uid)) {
return false
@@ -47,13 +64,13 @@ object VpnSanitizer {
lp.setInterfaceName(null)
}
@Suppress("UNCHECKED_CAST")
val stacked = XposedHelpers.callMethod(lp, "getStackedLinks") as? List<LinkProperties>
val stacked = getStackedLinksMethod.invoke(lp) as? List<LinkProperties>
if (!stacked.isNullOrEmpty()) {
for (link in stacked) {
clearHttpProxy(link)
val name = link.interfaceName
if (isVpnInterface(name)) {
XposedHelpers.callMethod(lp, "removeStackedLink", name)
val iface = link.interfaceName
if (iface != null && isVpnInterface(iface)) {
removeStackedLinkMethod.invoke(lp, iface)
}
}
}
@@ -65,8 +82,7 @@ object VpnSanitizer {
return true
}
@Suppress("UNCHECKED_CAST")
val stacked = XposedHelpers.callMethod(lp, "getStackedLinks") as? List<LinkProperties>
?: return false
val stacked = getStackedLinksMethod.invoke(lp) as? List<LinkProperties> ?: return false
return stacked.any { isVpnInterface(it.interfaceName) }
}
@@ -77,8 +93,8 @@ object VpnSanitizer {
}
private fun sanitizeTransport(caps: NetworkCapabilities) {
XposedHelpers.callMethod(caps, "removeTransportType", NetworkCapabilities.TRANSPORT_VPN)
XposedHelpers.callMethod(caps, "addCapability", NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
removeTransportTypeMethod.invoke(caps, NetworkCapabilities.TRANSPORT_VPN)
addCapabilityMethod.invoke(caps, NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
}
private fun clearUnderlyingNetworks(caps: NetworkCapabilities) {
@@ -110,7 +126,7 @@ object VpnSanitizer {
}
private fun clearHttpProxy(lp: LinkProperties) {
XposedHelpers.callMethod(lp, "setHttpProxy", null)
setHttpProxyMethod.invoke(lp, null as ProxyInfo?)
}
fun cloneLinkProperties(source: LinkProperties): LinkProperties {

View File

@@ -15,6 +15,10 @@ class XposedInit(
param: XposedModuleInterface.ModuleLoadedParam,
) : XposedModule(base, param) {
private val activityThreadClass by lazy { Class.forName("android.app.ActivityThread") }
private val currentActivityThreadMethod by lazy { activityThreadClass.getMethod("currentActivityThread") }
private val getSystemContextMethod by lazy { activityThreadClass.getMethod("getSystemContext") }
override fun onSystemServerLoaded(param: XposedModuleInterface.SystemServerLoadedParam) {
val systemContext = resolveSystemContext()
HookErrorStore.i("XposedInit", "handleSystemServerLoaded")
@@ -45,9 +49,8 @@ class XposedInit(
private fun resolveSystemContext(): Context? {
return try {
val activityThread = Class.forName("android.app.ActivityThread")
val currentThread = activityThread.getMethod("currentActivityThread").invoke(null)
activityThread.getMethod("getSystemContext").invoke(currentThread) as? Context
val currentThread = currentActivityThreadMethod.invoke(null)
getSystemContextMethod.invoke(currentThread) as? Context
} catch (e: Throwable) {
HookErrorStore.e("XposedInit", "resolveSystemContext failed", e)
null

View File

@@ -14,6 +14,7 @@ import io.nekohasekai.sfa.xposed.VpnAppStore
import io.nekohasekai.sfa.xposed.VpnSanitizer
import io.nekohasekai.sfa.xposed.hooks.SafeMethodHook
import io.nekohasekai.sfa.xposed.hooks.XHook
import java.lang.reflect.Method
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
@@ -33,6 +34,17 @@ class ConnectivityServiceHookHelper(private val classLoader: ClassLoader) : XHoo
lateinit var cls: Class<*>
private set
private val serviceManagerClass by lazy { Class.forName("android.os.ServiceManager") }
private val checkServiceMethod by lazy { serviceManagerClass.getMethod("checkService", String::class.java) }
private var getVpnForUidMethod: Method? = null
private lateinit var getVpnUnderlyingNetworksMethod: Method
private lateinit var getNetworkAgentInfoForNetworkMethod: Method
private var getFilteredNetworkInfoMethod: Method? = null
private lateinit var getDefaultNetworkMethod: Method
private lateinit var isVPNMethod: Method
private var networkMethod: Method? = null
override fun injectHook() {
val foundClass = findConnectivityServiceClass()
if (foundClass != null) {
@@ -50,6 +62,7 @@ class ConnectivityServiceHookHelper(private val classLoader: ClassLoader) : XHoo
}
this.cls = cls
connectivityClassLoader = cls.classLoader ?: classLoader
initMethodCache()
HookErrorStore.i(
SOURCE,
"Installing ConnectivityService hooks ($source) cls=${cls.name} loader=${connectivityClassLoader.javaClass.name}",
@@ -72,6 +85,38 @@ class ConnectivityServiceHookHelper(private val classLoader: ClassLoader) : XHoo
HookErrorStore.i(SOURCE, "Hooked ConnectivityService ($source) cls=${cls.name}")
}
private fun initMethodCache() {
val intType = Int::class.javaPrimitiveType!!
val booleanType = Boolean::class.javaPrimitiveType!!
val naiClass = resolveConnectivityModuleClass("NetworkAgentInfo", "connectivity")
if (sdkInt >= 31) {
getVpnForUidMethod = findDeclaredMethod(cls, "getVpnForUid", intType)
if (getVpnForUidMethod == null) {
HookErrorStore.w(SOURCE, "getVpnForUid not found; falling back to underlying networks")
}
}
getVpnUnderlyingNetworksMethod = requireDeclaredMethod(cls, "getVpnUnderlyingNetworks", intType)
getNetworkAgentInfoForNetworkMethod = requireDeclaredMethod(cls, "getNetworkAgentInfoForNetwork", Network::class.java)
if (sdkInt >= 31) {
getFilteredNetworkInfoMethod = findDeclaredMethod(
cls,
"getFilteredNetworkInfo",
naiClass,
intType,
booleanType
)
if (getFilteredNetworkInfoMethod == null) {
HookErrorStore.w(SOURCE, "getFilteredNetworkInfo not found; network info sanitization disabled")
}
}
getDefaultNetworkMethod = requireDeclaredMethod(cls, "getDefaultNetwork")
isVPNMethod = requireDeclaredMethod(naiClass, "isVPN")
networkMethod = findDeclaredMethod(naiClass, "network")
if (networkMethod == null) {
HookErrorStore.w(SOURCE, "NetworkAgentInfo.network() not found; falling back to field access")
}
}
// region Service Discovery
private fun findConnectivityServiceClass(): Class<*>? {
@@ -229,9 +274,7 @@ class ConnectivityServiceHookHelper(private val classLoader: ClassLoader) : XHoo
private fun tryHookFromServiceManager() {
if (hooked.get()) return
val binder = try {
val serviceManager = Class.forName("android.os.ServiceManager")
val checkService = serviceManager.getMethod("checkService", String::class.java)
checkService.invoke(null, Context.CONNECTIVITY_SERVICE) as? IBinder
checkServiceMethod.invoke(null, Context.CONNECTIVITY_SERVICE) as? IBinder
} catch (_: Throwable) {
null
}
@@ -365,56 +408,88 @@ class ConnectivityServiceHookHelper(private val classLoader: ClassLoader) : XHoo
fun hasVpnForUid(connectivityService: Any, uid: Int): Boolean {
if (sdkInt >= 31) {
return XposedHelpers.callMethod(connectivityService, "getVpnForUid", uid) != null
val vpnForUidMethod = getVpnForUidMethod
if (vpnForUidMethod != null) {
return vpnForUidMethod.invoke(connectivityService, uid) != null
}
}
@Suppress("UNCHECKED_CAST")
val networks = XposedHelpers.callMethod(connectivityService, "getVpnUnderlyingNetworks", uid)
as? Array<Network>
val networks = getVpnUnderlyingNetworksMethod.invoke(connectivityService, uid) as? Array<Network>
return networks != null && networks.isNotEmpty()
}
fun isVpnNetwork(connectivityService: Any, network: Network): Boolean {
val nai = XposedHelpers.callMethod(connectivityService, "getNetworkAgentInfoForNetwork", network)
?: return false
val nai = getNetworkAgentInfoForNetworkMethod.invoke(connectivityService, network) ?: return false
return isVpnNai(nai)
}
fun isVpnNai(nai: Any): Boolean {
return XposedHelpers.callMethod(nai, "isVPN") as Boolean
return isVPNMethod.invoke(nai) as Boolean
}
fun getUnderlyingNetwork(connectivityService: Any, uid: Int): Network? {
val nai = getUnderlyingNai(connectivityService, uid) ?: return null
return XposedHelpers.callMethod(nai, "network") as Network?
val method = networkMethod
return if (method != null) {
method.invoke(nai) as Network?
} else {
XposedHelpers.getObjectField(nai, "network") as? Network
}
}
fun getUnderlyingLinkProperties(connectivityService: Any, uid: Int): LinkProperties? {
val nai = getUnderlyingNai(connectivityService, uid) ?: return null
val lp = XposedHelpers.getObjectField(nai, "linkProperties") as LinkProperties?
?: return null
val lp = XposedHelpers.getObjectField(nai, "linkProperties") as LinkProperties? ?: return null
return VpnSanitizer.cloneLinkProperties(lp)
}
fun getUnderlyingNetworkInfo(connectivityService: Any, uid: Int): NetworkInfo? {
val nai = getUnderlyingNai(connectivityService, uid) ?: return null
return XposedHelpers.callMethod(connectivityService, "getFilteredNetworkInfo", nai, uid, false)
as NetworkInfo?
val method = getFilteredNetworkInfoMethod
if (method != null) {
return method.invoke(connectivityService, nai, uid, false) as NetworkInfo?
}
return XposedHelpers.getObjectField(nai, "networkInfo") as? NetworkInfo
}
fun getUnderlyingNai(connectivityService: Any, uid: Int): Any? {
@Suppress("UNCHECKED_CAST")
val networks = XposedHelpers.callMethod(connectivityService, "getVpnUnderlyingNetworks", uid)
as? Array<Network>
val networks = getVpnUnderlyingNetworksMethod.invoke(connectivityService, uid) as? Array<Network>
if (networks != null && networks.isNotEmpty()) {
return XposedHelpers.callMethod(connectivityService, "getNetworkAgentInfoForNetwork", networks[0])
return getNetworkAgentInfoForNetworkMethod.invoke(connectivityService, networks[0])
}
val defaultNai = XposedHelpers.callMethod(connectivityService, "getDefaultNetwork")
val defaultNai = getDefaultNetworkMethod.invoke(connectivityService)
if (defaultNai != null && !isVpnNai(defaultNai)) {
return defaultNai
}
return null
}
private fun findDeclaredMethod(
target: Class<*>,
name: String,
vararg parameterTypes: Class<*>,
): Method? {
var current: Class<*>? = target
while (current != null) {
try {
return current.getDeclaredMethod(name, *parameterTypes).apply { isAccessible = true }
} catch (_: NoSuchMethodException) {
current = current.superclass
}
}
return null
}
private fun requireDeclaredMethod(
target: Class<*>,
name: String,
vararg parameterTypes: Class<*>,
): Method {
return findDeclaredMethod(target, name, *parameterTypes)
?: throw NoSuchMethodException("${target.name}#$name")
}
/**
* Resolves a class from the Connectivity module, handling APEX package rewriting.
*

View File

@@ -17,6 +17,25 @@ class HookNetworkCapabilitiesWriteToParcel : XHook {
private const val SOURCE = "HookNCWriteToParcel"
}
private val copyCtor by lazy {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
NetworkCapabilities::class.java.getDeclaredConstructor(
NetworkCapabilities::class.java,
Long::class.javaPrimitiveType
).apply { isAccessible = true }
} else {
NetworkCapabilities::class.java.getDeclaredConstructor(
NetworkCapabilities::class.java
).apply { isAccessible = true }
}
}
private val removeTransportTypeMethod by lazy {
NetworkCapabilities::class.java.getMethod("removeTransportType", Int::class.javaPrimitiveType)
}
private val addCapabilityMethod by lazy {
NetworkCapabilities::class.java.getMethod("addCapability", Int::class.javaPrimitiveType)
}
private val inWrite = ThreadLocal.withInitial { false }
override fun injectHook() {
@@ -58,22 +77,15 @@ class HookNetworkCapabilitiesWriteToParcel : XHook {
private fun copyNetworkCapabilities(caps: NetworkCapabilities): NetworkCapabilities {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
val ctor = NetworkCapabilities::class.java.getDeclaredConstructor(
NetworkCapabilities::class.java,
Long::class.javaPrimitiveType!!
)
ctor.isAccessible = true
ctor.newInstance(caps, 0L)
copyCtor.newInstance(caps, 0L) as NetworkCapabilities
} else {
val ctor = NetworkCapabilities::class.java.getDeclaredConstructor(NetworkCapabilities::class.java)
ctor.isAccessible = true
ctor.newInstance(caps)
copyCtor.newInstance(caps) as NetworkCapabilities
}
}
private fun sanitizeNetworkCapabilities(caps: NetworkCapabilities) {
XposedHelpers.callMethod(caps, "removeTransportType", NetworkCapabilities.TRANSPORT_VPN)
XposedHelpers.callMethod(caps, "addCapability", NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
removeTransportTypeMethod.invoke(caps, NetworkCapabilities.TRANSPORT_VPN)
addCapabilityMethod.invoke(caps, NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
clearVpnTransportInfo(caps)
clearUnderlyingNetworks(caps)
clearOwnerUid(caps)

View File

@@ -31,6 +31,11 @@ class HookNetworkInterfaceGetName(private val classLoader: ClassLoader) : XHook
private const val IFF_UP = 0x1
}
private val netlinkSocketAddressClass by lazy { Class.forName("android.system.NetlinkSocketAddress") }
private val netlinkSocketAddressCtor by lazy {
netlinkSocketAddressClass.getConstructor(Int::class.javaPrimitiveType, Int::class.javaPrimitiveType)
}
private val seq = AtomicInteger(1)
override fun injectHook() {
@@ -223,9 +228,7 @@ class HookNetworkInterfaceGetName(private val classLoader: ClassLoader) : XHook
}
private fun buildNetlinkAddress(): SocketAddress {
val cls = Class.forName("android.system.NetlinkSocketAddress")
val ctor = cls.getConstructor(Int::class.javaPrimitiveType, Int::class.javaPrimitiveType)
return ctor.newInstance(0, 0) as SocketAddress
return netlinkSocketAddressCtor.newInstance(0, 0) as SocketAddress
}
private fun buildLinkMessage(

View File

@@ -13,6 +13,7 @@ import io.nekohasekai.sfa.xposed.PrivilegeSettingsStore
import io.nekohasekai.sfa.xposed.VpnAppStore
import io.nekohasekai.sfa.xposed.hooks.SafeMethodHook
import io.nekohasekai.sfa.xposed.hooks.XHook
import java.lang.reflect.Method
class HookPackageManagerGetInstalledPackages(private val classLoader: ClassLoader) : XHook {
private companion object {
@@ -20,6 +21,10 @@ class HookPackageManagerGetInstalledPackages(private val classLoader: ClassLoade
private const val PER_USER_RANGE = 100000
}
@Volatile
private var lastPackageNameClass: Class<*>? = null
private var getPackageNameMethod: Method? = null
override fun injectHook() {
val hooked = ArrayList<String>()
val sdk = Build.VERSION.SDK_INT
@@ -238,7 +243,15 @@ class HookPackageManagerGetInstalledPackages(private val classLoader: ClassLoade
private fun extractPackageName(arg: Any?): String? {
if (arg == null) return null
try {
val method = arg.javaClass.getMethod("getPackageName")
val argClass = arg.javaClass
val method = if (lastPackageNameClass == argClass && getPackageNameMethod != null) {
getPackageNameMethod!!
} else {
argClass.getMethod("getPackageName").also {
lastPackageNameClass = argClass
getPackageNameMethod = it
}
}
val result = method.invoke(arg) as String?
if (!result.isNullOrEmpty()) {
return result