From f20fd140753a7af7a9142fe30bbd25292174a27a Mon Sep 17 00:00:00 2001 From: DonnchaC <3081375+DonnchaC@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:37:07 +0000 Subject: [PATCH 1/3] Add new iOS versions and build numbers --- src/mvt/ios/data/ios_versions.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/mvt/ios/data/ios_versions.json b/src/mvt/ios/data/ios_versions.json index d8a93c91..8cea66c2 100644 --- a/src/mvt/ios/data/ios_versions.json +++ b/src/mvt/ios/data/ios_versions.json @@ -1275,5 +1275,9 @@ { "version": "26.6.2", "build": "23G90" + }, + { + "version": "27.0", + "build": "24A437" } ] \ No newline at end of file From 935747519ff89b4390e2f391c7d1237fa7bb35ef Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Fri, 18 Sep 2026 10:22:21 +0200 Subject: [PATCH 2/3] Bump iphone_backup_decrypt to 0.10.0 --- pyproject.toml | 2 +- src/mvt/ios/decrypt.py | 2 +- uv.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9f423d2e..29c605c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "simplejson==4.1.1", "packaging==26.3", "appdirs==1.4.4", - "iphone_backup_decrypt==0.9.0", + "iphone_backup_decrypt==0.10.0", "pycryptodome>=3.20.0", "adb-shell[usb]==0.4.4", "libusb1==3.4.0", diff --git a/src/mvt/ios/decrypt.py b/src/mvt/ios/decrypt.py index dc30d9c9..ffc7bfa0 100644 --- a/src/mvt/ios/decrypt.py +++ b/src/mvt/ios/decrypt.py @@ -52,7 +52,7 @@ class MVTEncryptedBackup(EncryptedBackup): NOTE: This subclass relies on internal APIs of iphone_backup_decrypt (specifically _read_and_unlock_keybag, _keybag, and the Keybag class - internals). Pinned to iphone_backup_decrypt==0.9.0. + internals). Pinned to iphone_backup_decrypt==0.10.0. """ def __init__(self, *, backup_directory, passphrase=None, derived_key=None): diff --git a/uv.lock b/uv.lock index a1d370e7..8e4255c3 100644 --- a/uv.lock +++ b/uv.lock @@ -625,14 +625,14 @@ wheels = [ [[package]] name = "iphone-backup-decrypt" -version = "0.9.0" +version = "0.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycryptodome" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/e7/bcdacdec21d628122ba240e7f742ab2175149e58672be63af55ff37a0f28/iphone_backup_decrypt-0.9.0.tar.gz", hash = "sha256:13b18fef3c8e3af627914f8c1a429bbc5555dfb0505239ba49efe99984cc0c96", size = 16125, upload-time = "2024-09-18T15:50:12.179Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/b8/a2ec2b10d792403d36bb6661fea6357179c31ece1ed3285c442e636d6335/iphone_backup_decrypt-0.10.0.tar.gz", hash = "sha256:5f2799e58f908cce28d7929dd82231abc8acd57c25b37bce6a2d6a82e018d343", size = 20000, upload-time = "2026-09-17T21:46:44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/94/64a31be93f72e0a254bde68e4cf7d24aef37a0a985754a197fa1b028a665/iphone_backup_decrypt-0.9.0-py3-none-any.whl", hash = "sha256:55b5adfafac757f58aa6444b83a4cc2c20cdd699c6ff1d2f4b549936a5dad92c", size = 15767, upload-time = "2024-09-18T15:50:10.537Z" }, + { url = "https://files.pythonhosted.org/packages/89/10/349b2a720e012921d1457e1d1c2b921b16300a5f3570c4c64d0d3787539f/iphone_backup_decrypt-0.10.0-py3-none-any.whl", hash = "sha256:1ac4345a07b104a3eacab20932b8621cd1e0164e0f03d913e66eed6ea5ee173e", size = 17400, upload-time = "2026-09-17T21:46:42Z" }, ] [[package]] @@ -1068,7 +1068,7 @@ requires-dist = [ { name = "betterproto2", specifier = "==0.10.0" }, { name = "click", specifier = "==8.4.2" }, { name = "cryptography", specifier = "==50.0.0" }, - { name = "iphone-backup-decrypt", specifier = "==0.9.0" }, + { name = "iphone-backup-decrypt", specifier = "==0.10.0" }, { name = "libusb1", specifier = "==3.4.0" }, { name = "nskeyedunarchiver", specifier = "==1.5.2" }, { name = "packaging", specifier = "==26.3" }, From d0fc0379b94fbe89e5f8561591d5b42cf370f770 Mon Sep 17 00:00:00 2001 From: va-resident Date: Fri, 18 Sep 2026 18:43:06 +0300 Subject: [PATCH 3/3] Fix mountinfo read-write detection across both option layers /proc/PID/mountinfo carries two option sets with different meaning: fields[5] are the per-mount (VFS) flags, the field after the "-" separator belongs to the superblock. A mount is writable only if both allow it. parse_mountinfo() merged both into one list and set is_read_write = "rw" in options, so a "rw" VFS mount over a read-only superblock was reported as writable. On stock Xiaomi-family builds that made every read-only mi_ext customisation overlay a HIGH "system partition is mounted as read-write". Require "rw" in both layers, and let "rw" count as a suspicious mount option only when the mount is actually writable; remount, noatime and nodiratime keep their current meaning in either layer. mount_options and options_list still carry both layers, so nothing downstream loses data. Measured on 50 bug reports carrying mountinfo - the 28 where the rule changes the output plus 22 controls, 14 brands, Android 10-16: HIGH 94 -> 0, MEDIUM 119 -> 25, the 22 controls identical, and 36233 mount entries parsed either way. Every removed alert is a read-only superblock under a "rw" VFS mount; no report gains an alert. A partition that really is writable still raises the HIGH, which the new test asserts explicitly. Fixes #936 Co-Authored-By: Claude Opus 5 (1M context) --- src/mvt/android/artifacts/mounts.py | 15 ++++++++-- tests/android/test_artifact_mounts.py | 43 +++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 tests/android/test_artifact_mounts.py diff --git a/src/mvt/android/artifacts/mounts.py b/src/mvt/android/artifacts/mounts.py index 5a5537b7..0380a785 100644 --- a/src/mvt/android/artifacts/mounts.py +++ b/src/mvt/android/artifacts/mounts.py @@ -136,6 +136,10 @@ class Mounts(AndroidArtifact): mount_options = fields[5].split(",") super_options = fields[separator + 3].split(",") options = list(dict.fromkeys(mount_options + super_options)) + # fields[5] are the per-mount (VFS) flags, the field after the + # separator the superblock's; a mount is writable only if both + # allow it. The merged list made every read-only overlay a HIGH. + is_read_write = "rw" in mount_options and "rw" in super_options mount_point = fields[4].replace("\\040", " ") device = fields[separator + 2].replace("\\040", " ") filesystem_type = fields[separator + 1] @@ -156,7 +160,7 @@ class Mounts(AndroidArtifact): "options_list": options, "optional_fields": fields[6:separator], "is_system_partition": is_system, - "is_read_write": "rw" in options, + "is_read_write": is_read_write, "process_ids": [process_id], } ) @@ -191,7 +195,14 @@ class Mounts(AndroidArtifact): ) # Check for other suspicious mount options - suspicious_opts = [opt for opt in options if opt in SUSPICIOUS_OPTIONS] + # `rw` sitting in the merged option list is not evidence of + # writability (see is_read_write in parse_mountinfo); every other + # suspicious option is meaningful in either list. + suspicious_opts = [ + opt + for opt in options + if opt in SUSPICIOUS_OPTIONS and (opt != "rw" or mount["is_read_write"]) + ] if suspicious_opts and mount["is_system_partition"]: if ( "noatime" in mount["mount_options"] diff --git a/tests/android/test_artifact_mounts.py b/tests/android/test_artifact_mounts.py new file mode 100644 index 00000000..6b7d1faf --- /dev/null +++ b/tests/android/test_artifact_mounts.py @@ -0,0 +1,43 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2026 The MVT Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ +"""A mount is writable only if both mountinfo option layers allow it.""" + +from mvt.android.artifacts.mounts import Mounts +from mvt.common.alerts import AlertLevel + +# fields[5] is the per-mount (VFS) layer, the field after "-" the superblock. +RO_OVERLAY = ( + "304 166 0:130 / /product/usr rw,relatime shared:63 - overlay overlay " + "ro,seclabel,lowerdir=/mnt/vendor/ext/product/usr:/product/usr" +) +RW_SYSTEM = ( + "305 166 0:131 / /system rw,relatime shared:64 - ext4 /dev/block/dm-1 " + "rw,seclabel,errors=panic" +) + + +def _run(line): + mounts = Mounts() + mounts.results = Mounts.parse_mountinfo(line, 1) + mounts.check_indicators() + return mounts + + +class TestMountsReadWriteLayers: + def test_rw_vfs_over_ro_superblock_is_not_writable(self): + mounts = _run(RO_OVERLAY) + assert mounts.results[0]["is_read_write"] is False + assert mounts.alertstore.count(AlertLevel.HIGH) == 0 + assert mounts.alertstore.count(AlertLevel.MEDIUM) == 0 + + def test_rw_in_both_layers_still_alerts(self): + mounts = _run(RW_SYSTEM) + assert mounts.results[0]["is_read_write"] is True + assert mounts.alertstore.count(AlertLevel.HIGH) == 1 + + def test_both_option_layers_are_still_reported(self): + mounts = _run(RO_OVERLAY) + options = mounts.results[0]["options_list"] + assert "rw" in options and "ro" in options