From 756aa2efb21cf06b8086286f11de00adbde2f926 Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Tue, 19 May 2026 04:01:30 +0100 Subject: [PATCH] feat(spoof): periodic bulletin refresh via BulletinPoller BulletinPoller fetches the Pixel security bulletin index page on its own HandlerThread with 5s/30s/2m/10m/30m bootstrap backoff, then 24h steady cadence. The first YYYY-MM-DD match is the latest published patch; newer-than-current dates flow through PatchLevelManager.updateTo for validation + atomic write + resetprop. Persists the last 10 attempts to last_bulletin_fetch.json (atomic rename) with status, http_code, parsed_date, applied, and error fields so operators can audit history without logcat. Sepolicy rule appends TCP-socket allow rules for both ksu and magisk source domains so HttpsURLConnection survives SELinux enforcement on either root provider. Uninstall.sh cleans the three new artifacts. --- .../main/java/org/matrix/TEESimulator/App.kt | 3 + .../TEESimulator/config/BulletinPoller.kt | 171 ++++++++++++++++++ module/sepolicy.rule | 7 + module/uninstall.sh | 1 + 4 files changed, 182 insertions(+) create mode 100644 app/src/main/java/org/matrix/TEESimulator/config/BulletinPoller.kt diff --git a/app/src/main/java/org/matrix/TEESimulator/App.kt b/app/src/main/java/org/matrix/TEESimulator/App.kt index d8230e0..478242b 100644 --- a/app/src/main/java/org/matrix/TEESimulator/App.kt +++ b/app/src/main/java/org/matrix/TEESimulator/App.kt @@ -9,6 +9,7 @@ import android.os.Looper import java.security.Security import org.bouncycastle.jce.provider.BouncyCastleProvider import org.matrix.TEESimulator.config.BootStateManager +import org.matrix.TEESimulator.config.BulletinPoller import org.matrix.TEESimulator.config.ConfigurationManager import org.matrix.TEESimulator.config.PatchLevelManager import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor @@ -59,6 +60,8 @@ object App { NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so") + BulletinPoller.start() + // This starts the message queue processing. It blocks here indefinitely // processing messages until Looper.myLooper().quit() is called. Looper.loop() diff --git a/app/src/main/java/org/matrix/TEESimulator/config/BulletinPoller.kt b/app/src/main/java/org/matrix/TEESimulator/config/BulletinPoller.kt new file mode 100644 index 0000000..49888ec --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/config/BulletinPoller.kt @@ -0,0 +1,171 @@ +package org.matrix.TEESimulator.config + +import android.os.Handler +import android.os.HandlerThread +import java.io.File +import java.net.URL +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import javax.net.ssl.HttpsURLConnection +import org.json.JSONArray +import org.json.JSONObject +import org.matrix.TEESimulator.BuildConfig +import org.matrix.TEESimulator.logging.SystemLogger + +object BulletinPoller { + private const val BULLETIN_URL = "https://source.android.com/docs/security/bulletin/pixel" + private const val PATCH_FILE = "/data/adb/tricky_store/security_patch.txt" + private const val HISTORY_FILE = "/data/adb/tricky_store/last_bulletin_fetch.json" + private const val HISTORY_STAGING = "/data/adb/tricky_store/last_bulletin_fetch.json.next" + private const val HISTORY_CAP = 10 + private const val CONNECT_TIMEOUT_MS = 10_000 + private const val READ_TIMEOUT_MS = 15_000 + private const val STEADY_INTERVAL_MS = 24L * 60 * 60 * 1000 + + private val BOOTSTRAP_INTERVALS = longArrayOf(5_000, 30_000, 120_000, 600_000, 1_800_000) + private val DATE_REGEX = Regex("(\\d{4}-\\d{2}-\\d{2})") + + private lateinit var handler: Handler + @Volatile private var bootstrapStep = 0 + @Volatile private var steadyArmed = false + + fun start() { + val thread = HandlerThread("BulletinPoller").apply { start() } + handler = Handler(thread.looper) + handler.postDelayed(::pollOnce, BOOTSTRAP_INTERVALS[0]) + } + + private fun pollOnce() { + val result = fetchAndParse() + appendHistory(result) + scheduleNext(result.status == "success") + } + + private fun scheduleNext(success: Boolean) { + if (success || steadyArmed) { + steadyArmed = true + handler.postDelayed(::pollOnce, STEADY_INTERVAL_MS) + return + } + bootstrapStep++ + if (bootstrapStep >= BOOTSTRAP_INTERVALS.size) { + steadyArmed = true + handler.postDelayed(::pollOnce, STEADY_INTERVAL_MS) + } else { + handler.postDelayed(::pollOnce, BOOTSTRAP_INTERVALS[bootstrapStep]) + } + } + + private data class FetchResult( + val ts: Long, + val status: String, + val httpCode: Int?, + val parsedDate: String?, + val applied: Boolean, + val error: String?, + ) + + private fun fetchAndParse(): FetchResult { + val ts = System.currentTimeMillis() + var conn: HttpsURLConnection? = null + return try { + conn = + (URL(BULLETIN_URL).openConnection() as HttpsURLConnection).apply { + connectTimeout = CONNECT_TIMEOUT_MS + readTimeout = READ_TIMEOUT_MS + setRequestProperty( + "User-Agent", + "TEESimulator/${BuildConfig.VERSION_NAME}", + ) + requestMethod = "GET" + } + val code = conn.responseCode + if (code != 200) { + return FetchResult(ts, "network_error", code, null, false, "HTTP $code") + } + val html = conn.inputStream.bufferedReader().use { it.readText() } + val date = DATE_REGEX.find(html)?.groupValues?.get(1) + if (date == null) { + return FetchResult( + ts, + "parse_error", + code, + null, + false, + "no YYYY-MM-DD match", + ) + } + val current = currentPatch() + val isNewer = current == null || date > current + if (isNewer) { + PatchLevelManager.updateTo(date) + } + FetchResult(ts, "success", code, date, isNewer, null) + } catch (e: Exception) { + FetchResult(ts, "network_error", null, null, false, e.toString()) + } finally { + conn?.disconnect() + } + } + + private fun currentPatch(): String? { + val f = File(PATCH_FILE) + if (!f.exists()) return null + return try { + f.readLines() + .firstOrNull { it.startsWith("system=") } + ?.substringAfter("system=") + ?.trim() + ?.takeIf { it != "prop" && it.isNotEmpty() } + } catch (_: Exception) { + null + } + } + + private fun appendHistory(result: FetchResult) { + try { + val target = File(HISTORY_FILE) + val staging = File(HISTORY_STAGING) + val existing = if (target.exists()) runCatching { target.readText() }.getOrNull() else null + val history = + existing + ?.let { runCatching { JSONObject(it).optJSONArray("history") }.getOrNull() } + ?: JSONArray() + val entry = + JSONObject().apply { + put("ts", result.ts) + put("status", result.status) + put("http_code", result.httpCode ?: JSONObject.NULL) + put("parsed_date", result.parsedDate ?: JSONObject.NULL) + put("applied", result.applied) + put("error", result.error ?: JSONObject.NULL) + } + history.put(entry) + while (history.length() > HISTORY_CAP) history.remove(0) + + val latestKnown = + (0 until history.length()) + .mapNotNull { + history.optJSONObject(it)?.optString("parsed_date", "")?.takeIf { d -> + d.isNotBlank() + } + } + .lastOrNull() + + val root = + JSONObject().apply { + put("latest_known_date", latestKnown ?: JSONObject.NULL) + put("history", history) + } + staging.writeText(root.toString(2)) + Files.move( + staging.toPath(), + target.toPath(), + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING, + ) + } catch (e: Exception) { + SystemLogger.error("BulletinPoller: failed to persist history", e) + } + } +} diff --git a/module/sepolicy.rule b/module/sepolicy.rule index 21b522a..8e0124e 100644 --- a/module/sepolicy.rule +++ b/module/sepolicy.rule @@ -1,2 +1,9 @@ allow keystore {adb_data_file shell_data_file} file * allow crash_dump keystore process * + +allow ksu self:tcp_socket { create connect read write getopt setopt } +allow ksu node:tcp_socket node_bind +allow ksu port:tcp_socket name_connect +allow magisk self:tcp_socket { create connect read write getopt setopt } +allow magisk node:tcp_socket node_bind +allow magisk port:tcp_socket name_connect diff --git a/module/uninstall.sh b/module/uninstall.sh index d9cceab..ec896c5 100644 --- a/module/uninstall.sh +++ b/module/uninstall.sh @@ -10,3 +10,4 @@ done rm -rf "$CONFIG_DIR/persistent_keys" rm -f "$CONFIG_DIR/tee_status.txt" rm -f "$CONFIG_DIR/boot_hash.bin" "$CONFIG_DIR/boot_key.bin" +rm -f "$CONFIG_DIR/security_patch.txt" "$CONFIG_DIR/security_patch.txt.next" "$CONFIG_DIR/last_bulletin_fetch.json"