package com.example.modifier import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Build import android.util.Log import androidx.core.content.ContextCompat import com.example.modifier.data.BackupItem import com.example.modifier.data.BackupItemDao import com.example.modifier.enums.RcsConfigureState import com.example.modifier.http.KtorClient import com.example.modifier.model.TelephonyConfig import com.example.modifier.service.ModifierService import com.example.modifier.ui.shellRun import com.example.modifier.utils.RcsHackTool import com.google.gson.Gson import io.ktor.client.request.head import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.apache.commons.io.FileUtils import org.apache.commons.io.IOUtils import org.apache.commons.lang3.RandomStringUtils import org.apache.commons.lang3.StringUtils import java.io.File import java.io.FileWriter import java.nio.file.Files import java.time.ZoneId import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.util.Base64 import java.util.Date import java.util.Locale import kotlin.time.Duration.Companion.minutes object Global { @JvmField var serverUrl: String = "" @JvmField var name: String? = "" @JvmField var telephonyConfig: TelephonyConfig = TelephonyConfig("", "", "", "", "", "", "", "") private const val TAG = "Modifier" @JvmStatic fun load() { val context = Utils.getContext() val prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE) serverUrl = prefs.getString("server", "http://47.98.225.28") ?: "" name = prefs.getString("name", Build.DEVICE) try { val file = File(ContextCompat.getDataDir(context), "config.json") if (file.exists()) { val gson = Gson() val json = FileUtils.readFileToString(file, "UTF-8") telephonyConfig = gson.fromJson(json, TelephonyConfig::class.java) } } catch (e: Exception) { e.printStackTrace() } } @JvmStatic val servers: MutableSet get() { val context = Utils.getContext() val defServers: MutableSet = HashSet() defServers.add("http://192.168.6.215:3000") defServers.add("http://192.168.50.135:3000") defServers.add("http://47.98.225.28") val prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE) return HashSet(prefs.getStringSet("servers", defServers)) } @JvmStatic fun saveServer(server: String, name: String?) { serverUrl = server Global.name = name val context = Utils.getContext() val prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE) val servers = servers servers.add(server) prefs.edit().putStringSet("servers", servers) .putString("server", server) .putString("name", name) .apply() } @JvmStatic fun save(telephonyConfig: TelephonyConfig, suspend: Boolean? = true) { val context = Utils.getContext() Global.telephonyConfig.mcc = telephonyConfig.mcc Global.telephonyConfig.mnc = telephonyConfig.mnc Global.telephonyConfig.number = telephonyConfig.number Global.telephonyConfig.country = telephonyConfig.country Global.telephonyConfig.areaCode = telephonyConfig.areaCode Global.telephonyConfig.iccid = telephonyConfig.iccid Global.telephonyConfig.imei = telephonyConfig.imei Global.telephonyConfig.imsi = telephonyConfig.imsi Global.telephonyConfig.mock = telephonyConfig.mock try { if (suspend == true) { suspend(gms = true, sms = true) } val file = File(ContextCompat.getDataDir(context), "config.json") val gson = Gson() val json = gson.toJson(telephonyConfig) try { val writer = FileWriter(file) writer.write(json) writer.close() } catch (e: Exception) { e.printStackTrace() } Utils.runAsRoot( "setprop persist.spoof.mcc ${telephonyConfig.mcc}", "setprop persist.spoof.mnc ${telephonyConfig.mnc}", "setprop persist.spoof.number ${telephonyConfig.number}", "setprop persist.spoof.country ${telephonyConfig.country}", "setprop persist.spoof.iccid ${telephonyConfig.iccid}", "setprop persist.spoof.imei ${telephonyConfig.imei}", "setprop persist.spoof.imsi ${telephonyConfig.imsi}", ) if (suspend == true) { unsuspend(gms = true, sms = true) } } catch (e: Exception) { e.printStackTrace() } } private fun isOldVersion(): Boolean { val info = Utils.getContext().packageManager.getPackageInfo("com.google.android.apps.messaging", 0) var oldVersion = false if (info != null) { if (info.versionCode < 170545910) { oldVersion = true } } return oldVersion } fun saveMock() { if (isOldVersion()) { val content = Utils.getContext().assets.open("us_numbers.txt").bufferedReader().use { it.readText() } // get random number content.split("\n") .filter { it.isNotBlank() } .shuffled() .firstOrNull() ?.let { save( TelephonyConfig( number = it, mcc = "310", mnc = "150", iccid = genICCID("310", "1"), "310150" + RandomStringUtils.randomNumeric(9), Utils.generateIMEI(), "us", "1", mock = true ) ) } } else { val content = Utils.getContext().assets.open("us_numbers.txt").bufferedReader().use { it.readText() } // get random number content.split("\n") .filter { it.isNotBlank() } .shuffled() .firstOrNull() ?.let { save( TelephonyConfig( number = it, mcc = "310", mnc = "240", iccid = genICCID("310", "1"), "310240" + RandomStringUtils.randomNumeric(9), Utils.generateIMEI(), "us", "1", mock = true ) ) } } } @JvmStatic fun clear(gsf: Boolean, gms: Boolean, sms: Boolean) { try { suspend(gsf, gms, sms) val cmds: MutableList = ArrayList() // suspend if (gsf) { cmds.add("pm suspend com.google.android.gsf") cmds.add("am force-stop com.google.android.gsf") cmds.add("echo 'gsf suspended'") } if (gms) { cmds.add("pm suspend com.google.android.gms") cmds.add("am force-stop com.google.android.gms") cmds.add("echo 'gms suspended'") } if (sms) { cmds.add("pm suspend com.google.android.apps.messaging") cmds.add("am force-stop com.google.android.apps.messaging") cmds.add("echo 'sms suspended'") } cmds.add("sleep 1") // clear if (gsf) { cmds.add("pm clear com.google.android.gsf") cmds.add("echo 'cleared gsf'") } if (gms) { cmds.add("pm clear com.google.android.gms") cmds.add("echo 'cleared gms'") } if (sms) { cmds.add("pm clear com.google.android.apps.messaging") cmds.add("echo 'cleared sms'") } cmds.add("sleep 1") // unsuspend if (gsf) { cmds.add("pm unsuspend com.google.android.gsf") cmds.add("echo 'gsf unsuspend'") } if (gms) { cmds.add("pm unsuspend com.google.android.gms") cmds.add("echo 'gms unsuspend'") } if (sms) { cmds.add("pm unsuspend com.google.android.apps.messaging") cmds.add("echo 'sms unsuspend'") } cmds.add("sleep 1") Utils.runAsRoot(*cmds.toTypedArray()) } catch (e: Exception) { e.printStackTrace() } } // @JvmStatic // fun stop(gsf: Boolean? = false, gms: Boolean? = false, sms: Boolean? = false) { // try { // val cmds: MutableList = ArrayList() // if (gsf == true) { // cmds.add("am force-stop com.google.android.gsf") // cmds.add("echo 'stopped gsf'") // } // if (gms == true) { // cmds.add("am force-stop com.google.android.gms") // cmds.add("echo 'stopped gms'") // Thread.sleep(1000) // } // if (sms == true) { // cmds.add("am force-stop com.google.android.apps.messaging") // cmds.add("echo 'stopped sms'") // } // Utils.runAsRoot(*cmds.toTypedArray()) // } catch (e: Exception) { // e.printStackTrace() // } // } @JvmStatic fun suspend(gsf: Boolean? = false, gms: Boolean? = false, sms: Boolean? = false) { try { val cmds: MutableList = ArrayList() if (gsf == true) { cmds.add("pm suspend com.google.android.gsf") cmds.add("am force-stop com.google.android.gsf") cmds.add("echo 'gsf suspended'") } if (gms == true) { cmds.add("pm suspend com.google.android.gms") cmds.add("am force-stop com.google.android.gms") cmds.add("echo 'gms suspended'") } if (sms == true) { cmds.add("pm suspend com.google.android.apps.messaging") cmds.add("am force-stop com.google.android.apps.messaging") cmds.add("echo 'sms suspended'") } cmds.add("sleep 1") Utils.runAsRoot(*cmds.toTypedArray()) } catch (e: Exception) { e.printStackTrace() } } @JvmStatic fun unsuspend(gsf: Boolean? = false, gms: Boolean? = false, sms: Boolean? = false) { try { val cmds: MutableList = ArrayList() if (gsf == true) { cmds.add("pm unsuspend com.google.android.gsf") cmds.add("echo 'gsf unsuspend'") } if (gms == true) { cmds.add("pm unsuspend com.google.android.gms") cmds.add("echo 'gms unsuspend'") } if (sms == true) { cmds.add("pm unsuspend com.google.android.apps.messaging") cmds.add("echo 'sms unsuspend'") } cmds.add("sleep 1") Utils.runAsRoot(*cmds.toTypedArray()) } catch (e: Exception) { e.printStackTrace() } } fun sqlite3path(): String { val context = Utils.getContext() val dataDir = ContextCompat.getDataDir(context) val binDir = File(dataDir, "bin") val dbDir = File(dataDir, "providerDB") if (!binDir.exists()) { Utils.copyAssetFolder(context.assets, "bin", binDir.path) Utils.copyAssetFolder(context.assets, "providerDB", dbDir.path) binDir.listFiles()?.forEach { it.setExecutable(true) } } Log.i("Modifier", "arch: " + Build.SUPPORTED_ABIS.joinToString(", ")) val file = File(dataDir, "bin/sqlite3.arm") file.setExecutable(true) return file.path } fun copyDB(): File { val context = Utils.getContext() val dataDir = ContextCompat.getDataDir(context) val dbDir = File(dataDir, "providerDB") if (!dbDir.exists()) { Utils.copyAssetFolder(context.assets, "providerDB", dbDir.path) } return dbDir } @JvmStatic suspend fun clearConv() { val context = Utils.getContext() if (context.getSharedPreferences("settings", Context.MODE_PRIVATE) .getBoolean("do_not_clean", false) ) { return } val model = Build.MODEL.replace(" ", "_") if (!(model == "SM-F707N" || model == "SM-F711B" || model == "Pixel_5")) { return } try { val sqlite3 = sqlite3path() suspend(sms = true) val dataDir = ContextCompat.getDataDir(context) val providerDir = File(dataDir, "telephony_provider/$model") if (!providerDir.exists()) { Utils.copyAssetFolder(context.assets, "telephony_provider/$model", providerDir.path) } val cmds = mutableListOf() fun copyToData(file: File) { val relative = file.path.replace(providerDir.path, "") if (file.isDirectory) { file.listFiles()?.forEach { copyToData(it) } } else { val dest = File("/data/data/com.android.providers.telephony/$relative") cmds.addAll( listOf( "cp -f ${file.path} ${dest.path}", "chmod 660 ${dest.path}", "chown radio:radio ${dest.path}" ) ) } } copyToData(providerDir) shellRun( "$sqlite3 /data/data/com.google.android.apps.messaging/databases/bugle_db \"DELETE FROM conversations;\"", "$sqlite3 /data/data/com.google.android.apps.messaging/databases/bugle_db \"DELETE FROM messages;\"", *cmds.toTypedArray(), ) unsuspend(sms = true) } catch (e: Exception) { e.printStackTrace() } } @JvmStatic suspend fun resetAll() { try { clearConv() shellRun( CMD_SUSPEND_MESSAGING_APP, CMD_KILL_MESSAGING_APP, CMD_CLEAR_MESSAGING_APP, CMD_CLEAR_GSF, CMD_CLEAR_GMS, "sleep 1", CMD_START_PLAY_STORE, "sleep 1", CMD_HOME, "sleep 10", CMD_CLEAR_GMS, CMD_RESUME_MESSAGING_APP, CMD_MESSAGING_APP ) } catch (e: Exception) { e.printStackTrace() } } @JvmStatic suspend fun killPhoneProcess(force: Boolean = false): Boolean { try { if (!force) { if (shellRun("getprop phonekilled")["output"]!!.contains("yes")) { return true } } run kill@{ repeat(3) { val pid = shellRun("pidof com.android.phone")["output"]!!.trim() if (!Regex("[0-9]+").matches(pid)) { Log.e(TAG, "killPhoneProcess: pid not found") return true } Log.i(TAG, "killPhoneProcess: pid=$pid") shellRun("kill -9 $pid") delay(1000) val pidNew = shellRun("pidof com.android.phone")["output"]!!.trim() if (pidNew == pid) { Log.e(TAG, "killPhoneProcess: failed to kill phone process") } else { Log.i(TAG, "killPhoneProcess: success, new pid: $pidNew") shellRun("kill -9 $pid", "setprop phonekilled yes") return true } } } } catch (e: Exception) { Log.e(TAG, "Error Kill Phone", e) } return false } @JvmStatic suspend fun sendSmsFrida(sender: String, msg: String) { val context = Utils.getContext() try { val dataDir = ContextCompat.getDataDir(context) Utils.copyAssetFolder(context.assets, "bin", File(dataDir, "bin").path) val binPath = File(dataDir, "bin/frida-inject-16.3.3-android-arm64").path val pduBase64 = String(Base64.getEncoder().encode(RcsHackTool.createFakeSms(sender, msg))) Log.i("Modifier", "pduBase64: $pduBase64") val script = IOUtils.toString(context.assets.open("scripts/sms.js"), "UTF-8") .replace("{pduBase64}", pduBase64) val tmpFile: File withContext(Dispatchers.IO) { tmpFile = File.createTempFile("script", ".js") FileUtils.writeStringToFile(tmpFile, script, "UTF-8") } val pid = Utils.runAsRoot("pidof com.android.phone").trim() if (!Regex("[0-9]+").matches(pid)) { return } Log.i("Modifier", "sendSms: $binPath -p $pid -s $tmpFile") val p = withContext(Dispatchers.IO) { Runtime.getRuntime().exec("su -M") } p.outputStream.bufferedWriter().use { it.write("chmod +x $binPath") it.newLine() it.flush() it.write("$binPath -p $pid -s $tmpFile") it.newLine() it.flush() } coroutineScope { launch { p.errorStream.bufferedReader().useLines { lines -> lines.forEach { Log.e("Modifier", it) } } } launch { p.inputStream .bufferedReader() .useLines { lines -> lines.forEach { Log.i("Modifier", it) if (it == "OK") { p.inputStream.close() p.errorStream.close() p.destroy() } } } } } withContext(Dispatchers.IO) { p.waitFor() } } catch (e: Exception) { e.printStackTrace() } } @JvmStatic fun sendSmsIntent(sender: String, msg: String) { val intent = Intent() intent.setAction("com.example.modifier.sms") intent.putExtra("sender", sender) intent.putExtra( "message", msg ) val context = Utils.getContext() context.sendBroadcast(intent) } @JvmStatic suspend fun syncTime() { try { Log.i("Modifier", "syncTime: start") val response = KtorClient.head("http://www.baidu.com") val dateHeader = response.headers["Date"] val date = ZonedDateTime.parse( dateHeader, DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH) ) // convert to Asia/Shanghai val dateInZone = date.withZoneSameInstant(ZoneId.of("Asia/Shanghai")) Log.i( TAG, "CurrentTime from Baidu: ${dateInZone.format(DateTimeFormatter.ISO_DATE_TIME)}" ) shellRun( "settings put system time_12_24 24", "settings put global auto_time 0", "settings put global auto_time_zone 0", "setprop persist.sys.timezone Asia/Shanghai", "date \"${dateInZone.format(DateTimeFormatter.ofPattern("MMddHHmmyyyy.ss"))}\"" ) } catch (e: Exception) { Log.e(TAG, "Error SyncTime", e) } } @JvmStatic suspend fun hasRoot(): Boolean { val hasRoot = run checkRoot@{ repeat(5) { if (Utils.hasRootAccess()) { return@checkRoot true } delay(500) } return@checkRoot false } return hasRoot } @SuppressLint("DefaultLocale") @JvmStatic fun genICCID(mnc: String, areaCode: String): String { val prefix = String.format("89%02d%s", areaCode.toInt(), mnc) return prefix + RandomStringUtils.randomNumeric(20 - prefix.length) } @JvmStatic suspend fun rebooted(): Boolean { if (shellRun("getprop rebooted")["output"]!!.contains("yes")) { return false } shellRun("setprop rebooted yes") return true } @SuppressLint("SdCardPath") @JvmStatic suspend fun backup( backupItemDao: BackupItemDao, type: String, sendCount: Int, fresh: Boolean = false ): BackupItem { clearConv() // ModifierService.instance!!.toggleRcsSwitch(false) val context = Utils.getContext() val dest = File( ContextCompat.getExternalFilesDirs(context, "backup")[0], System.currentTimeMillis().toString() ) dest.mkdirs() val file = File(ContextCompat.getDataDir(context), "config.json") if (!file.exists()) { throw Exception("Config file not found") } withContext(Dispatchers.IO) { IOUtils.copy( Files.newInputStream(file.toPath()), Files.newOutputStream(File(dest, "config.json").toPath()) ) } val dataDir = File(dest, "data") dataDir.mkdirs() val packages = mutableListOf( "com.google.android.apps.messaging", "com.google.android.gms", "com.google.android.gsf", ) packages.forEach { File(dataDir, it).mkdirs() } val cmds = mutableListOf() cmds.addAll(packages.flatMap { mutableListOf("pm suspend $it", "am force-stop $it") }) for (pkg in packages) { if (!shellRun("ls /data/data/$pkg")["error"]!!.contains("No such file or directory")) { cmds.add("cp -r /data/data/$pkg $dataDir/$pkg/data") } if (!shellRun("ls /data/user_de/0/$pkg")["error"]!!.contains("No such file or directory")) { cmds.add("cp -r /data/user_de/0/$pkg $dataDir/$pkg/user_de") } if (!shellRun("ls /sdcard/Android/data/$pkg")["error"]!!.contains("No such file or directory")) { cmds.add("cp -r /sdcard/Android/data/$pkg $dataDir/$pkg/external") } } cmds.addAll(packages.reversed().map { "pm unsuspend $it" }) shellRun(*cmds.toTypedArray()) val backup = BackupItem( createdAt = Date().time, number = telephonyConfig.number, code = if (telephonyConfig.areaCode == null) "" else telephonyConfig.areaCode, country = telephonyConfig.country, mcc = telephonyConfig.mcc, mnc = telephonyConfig.mnc, imei = telephonyConfig.imei, imsi = telephonyConfig.imsi, iccid = telephonyConfig.iccid, path = dest.path, sendCount = sendCount, lastUse = Date().time, type = type, fresh = fresh ) backupItemDao.findBackupForNumber(telephonyConfig.country, telephonyConfig.number)?.let { File(it.path).deleteRecursively() backupItemDao.delete(it) backup.sendCount += it.sendCount } backup.id = backupItemDao.insert(backup).toInt() return backup } @JvmStatic suspend fun restore(backup: BackupItem): Boolean { save( TelephonyConfig( number = backup.number, mcc = backup.mcc, mnc = backup.mnc, country = backup.country, areaCode = backup.code, iccid = backup.iccid, imei = backup.imei, imsi = backup.imsi, ), false ) val packages = mutableListOf( "com.google.android.apps.messaging", "com.google.android.gms", "com.google.android.gsf", ) val cmds = mutableListOf() cmds.addAll(packages.flatMap { mutableListOf("pm suspend $it", "am force-stop $it") }) val packageManager = Utils.getContext().packageManager for (pkg in packages) { val uid = packageManager.getApplicationInfo(pkg, 0).uid if (File("${backup.path}/data/$pkg/data").exists()) { cmds.add("rm -rf /data/data/$pkg/*") cmds.add("cp -r ${backup.path}/data/$pkg/data/* /data/data/$pkg") cmds.add("chown -R $uid:$uid /data/data/$pkg") } if (File("${backup.path}/data/$pkg/user_de").exists()) { cmds.add("rm -rf /data/user_de/0/$pkg/*") cmds.add("cp -r ${backup.path}/data/$pkg/user_de/* /data/user_de/0/$pkg") cmds.add("chown -R $uid:$uid /data/user_de/0/$pkg") } if (File("${backup.path}/data/$pkg/external").exists()) { if (File("${backup.path}/data/$pkg/external").listFiles()?.isNotEmpty() == true) { cmds.add("rm -rf /sdcard/Android/data/$pkg/*") cmds.add("cp -r ${backup.path}/data/$pkg/external/* /sdcard/Android/data/$pkg") cmds.add("chown -R $uid:$uid /sdcard/Android/data/$pkg") } } } cmds.addAll(packages.reversed().map { "pm unsuspend $it" }) cmds.addAll(listOf(CMD_MESSAGING_APP)) shellRun(*cmds.toTypedArray()) delay(2000) ModifierService.instance!!.toggleRcsSwitch(false) delay(1000) ModifierService.instance!!.toggleRcsSwitch(true) var success = false ModifierService.instance!!.waitForRcsState(arrayOf(RcsConfigureState.CONFIGURED), 2.minutes) .let { success = it == RcsConfigureState.CONFIGURED } // shellRun("am start -a android.intent.action.SENDTO -d sms:+18583199738 --es sms_body \"Test\" --ez exit_on_sent false") return success } @JvmStatic suspend fun optimize() { val context = Utils.getContext() val packageManager = context.packageManager val info = packageManager.getApplicationInfo("com.google.android.gms", 0) shellRun( "dumpsys deviceidle whitelist +com.google.android.apps.messaging", "dumpsys deviceidle whitelist +${BuildConfig.APPLICATION_ID}", "cmd netpolicy add restrict-background-blacklist ${info.uid}" ) } @JvmStatic suspend fun setupSystem() { syncTime() optimize() } }