Fixes for issue described here appeared under CVE-2024-34740 / A-307288067:
- Bulletin
- Patch linked from bulletin
- Two other patches: 1 2
Inside Android system_server, many services store their state across reboots in XML files
$ adb shell su 0 find /data/system -name '*.xml' | sort
/data/system/appops_accesses.xml
/data/system/cachequota.xml
/data/system/device_policies.xml
/data/system/device_policy_state.xml
/data/system/display-manager-state.xml
/data/system/input-manager-state.xml
/data/system/inputmethod/subtypes.xml
/data/system/install_sessions.xml
/data/system/job/jobs_1000.xml
/data/system/job/jobs_10131.xml
/data/system/log-files.xml
/data/system/netpolicy.xml
/data/system/notification_policy.xml
/data/system/overlays.xml
/data/system/packages.xml
/data/system/package-watchdog.xml
/data/system/sensor_privacy_impl.xml
/data/system/sensor_privacy.xml
/data/system/shortcut_service.xml
/data/system/users/0/app_idle_stats.xml
/data/system/users/0/appwidgets.xml
/data/system/users/0/package-restrictions.xml
/data/system/users/0/settings_global.xml
/data/system/users/0/settings_secure.xml
/data/system/users/0/settings_system.xml
/data/system/users/0/wallpaper_info.xml
/data/system/users/0.xml
/data/system/users/userlist.xml
/data/system/watchlist_settings.xml
Historically these have been plain text XML files with indentation, which allowed developers easy reading of them, however in Android 12 new binary version of that format was introduced, citing 1.5% of all time spent by system_server being spent on these XML operations
It should be noted that this format is only used internally by system and has files with magic value "ABX\x00". It is different from format used inside APKs for AndroidManifest.xml, res/xml/*.xml, res/layout/*.xml, etc. which has no explicit "magic value", however usually starts 0300 0800 (which is header with type=RES_XML_TYPE and headerSize=8)
Whenever system reads one of these internal state XML files, it uses "ABX\0" magic value in file to choose either parser for Binary XML file or regular XML parser. Whenever these files are saved as Binary XML is controlled by system property and is enabled by default
When Binary XML files are in use, you can read their contents for example through adb shell su 0 abx2xml /data/system/packages.xml -
One of things this binary format does is offering typed accessors, so serializer offers attributeInt(String namespace, String name, int value) method, which writes value as binary integer, avoiding round-trip through String which would be new allocation and subsequent object for Garbage Collection
Another type that can be directly serialized is byte array
@Override
public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_BYTES_BASE64);
mOut.writeInternedUTF(name);
mOut.writeShort(value.length);
mOut.write(value);
return this;
}There's also similar method attributeBytesHex which only differs by TYPE_* tag written. That tag is used by abx2xml tool to convert byte array to appropriate String representation
mOut is instance of FastDataOutput, which provides functions of Java's DataOutputStream. writeByte/writeShort/writeInt/writeUTF/write use same format as standard DataOutputStream
Similarly to Parcel, if something mismatches during write/read subsequently read data will be taken from wrong offsets, however unlike Parcel, mistakes with usage of BinaryXmlSerializer/BinaryXmlPullParser don't give attacker ability to arbitrarily tamper read data (attacker cannot introduce new tag/attribute names/values in that case)
Mistakes within BinaryXmlSerializer class itself or in FastDataOutput however do
In above method if we'd try to write byte array with length 65536, we'll write length with writeShort(), which will effectively write 0, after which actual array contents will be written
In order to exploit that mismatch, we'll need to choose some file where we'll be able to inject arbitrary byte array to attributeBytesBase64 or attributeBytesHex as well as modification of that file will be valuable for attacker
PackageInstaller class offers ability to prepare package for installation. Without needing any permissions, any app can write new APK to be installed into temporary directory. Once everything necessary for installation has been written, installing app can commit() PackageInstaller.Session, which means it won't be able to do any further changes to installation files and Session is ready for either user approval or actual installation
State of these operations is stored in /data/system/install_sessions.xml. Installer app can for example download half of large APK into temporary directory created by Package Manager Service for its PackageInstaller.Session, then after reboot resume download, write remaining half and commit installation
One of possibilities is writing data into install_sessions.xml to mark session as staged, which means it'll be installed after next boot
Another one, presented here is changing path to temporary directory in which installation files are prepared, as openWrite()/openRead() accept any valid filename as long as there's no path traversal and place that file in directory pointed by stageDir field, which is read from XML
Now we need to actually get our controlled byte array into attributeBytesBase64()
PackageInstaller.Session offers setChecksums() method
On system_server side, provided Checksum-s are optionally verified against signature provided by caller and then put in mChecksums
When install_sessions.xml is written, checksum.getValue() is passed to writeByteArrayAttribute, which in turn passes it to attributeBytesBase64()
There are few events which trigger write of install_sessions.xml, one of which is creation of new Session, therefore this exploit after setting Checksum on one session creates new Session to ensure that first Session has been saved to file
Now we write byte array with length 65536, then after it is read its size is interpreted to be zero and contents of that array become raw data which BinaryXmlPullParser parses
There is no attribute count specified, each entry has tag byte which contains token. In lower nibble there's one of event types defined in XmlPullParser, such as START_TAG, END_TAG or END_DOCUMENT. In addition to these types, there's special ATTRIBUTE type, which isn't reported through next() but instead after seeing START_TAG token, parser peeks next tokens until it sees non-ATTRIBUTE token
Since there is no count attribute count specified, we can immediately proceed to closing current element through END_TAG token. Then we also close </session> as all interesting elements attributes are in <session> opening tag but we're past that point, however now we can open new <session> element and set them there
As noted above FastDataInput is compatible with Java's DataInputStream, except there's additional readInternedUTF() method, which can refer to past Strings. As we don't know what Strings were previously interned we always specify that previously-unseen String was written. This also adds newly read strings into pool, which could cause problem with reading data written after our injection point, however as part of injection I insert all ending tags and END_DOCUMENT token, so after my injection nothing else will be read from that file
Once system reads modified install_sessions.xml, we get PackageInstallerSession object with stageDir set to value controlled by us
My first idea was to set stageDir to /proc/self, then read maps and write mem, however these didn't work
When I tried using openRead() to open /proc/self/maps, system_server successfully opened the file, however passing that file to untrusted_app over Binder was blocked by SELinux
Writes however are done not by passing raw file descriptor to another process, but proxied through system_server, as system_server must be able to revoke write access once session is committed. Does that mean that we could write to /proc/self/mem? Turns out that while system_server can open that file, before writing anything it calls Os.chmod() on that file, which it cannot do on /proc/self/mem. So we cannot use that for exploitation here, although other than that system_server is able to open that file and perform writes at offsets specified by us and that file allows overwriting code pages, which would directly give us code execution
With that not being an option, I've tried next idea, replace contents of /data/system/packages.xml. This is file that contains state of PackageManagerService, most notably what apps are installed and what uids are assigned to them
It looks like system_server is not allowed to directly write to that file: instead whenever system writes that file it first writes to temporary file and then replaces packages.xml with that temporary file and enables protection on it
However when reading /data/system/packages.xml, system will first check if /data/system/packages-backup.xml file is present and if so it'll consider primary packages.xml to be corrupted and will read backup instead. During normal operation /data/system/packages-backup.xml file is not present and we can create one using crafted PackageInstallerSession with stageDir set to /data/system
Also system_server is allowed to send read-only file descriptor of /data/system/packages.xml when I use openRead(), so I can easily build patched file containing only my modifications without corrupting previous contents
In packages.xml I have definitions of installed applications registered, like:
<package name="com.android.settings" codePath="/system_ext/priv-app/Settings" ... sharedUserId="1000" ...>
<sigs count="1" schemeVersion="3">
<cert index="3" key="308204..." />
</sigs>
<proper-signing-keyset identifier="2" />
</package>Could we write new APK somewhere in /data/app (using another PackageInstallerSession) and add new <package> element to packages.xml and have it installed that way?
Yes, however we must provide in <cert> valid signature of our newly installed APK and system will check it against APK file during boot
Could we set userId (instead of sharedUserId to indicate APK without <manifest android:sharedUserId> attribute in AndroidManifest.xml) attribute to value we want?
Yes, however we must not use value that is already used by another package or sharedUserId
Could we set sharedUserId="1000" for our app?
If we do so, during boot system will validate that setting through canJoinSharedUserId()
In particular that method will use checkCapability() to check if signatures either match exactly or signature on one side matches one of past signatures on the other
These "past signatures" come from packages.xml, in particular when we have <sigs> element with <cert>, we can add <pastSigs> element under <sigs> to add new entries to SigningDetails.mPastSigningCertificates
In the end, our tampered <shared-user> element looks like this:
<shared-user name="android.uid.system" userId="1000">
<sigs count="1" schemeVersion="3">
<cert index="3" />
<pastSigs count="2" schemeVersion="3">
<cert index="19" flags="2" />
<cert index="19" flags="2" />
</pastSigs>
</sigs>
</shared-user><cert> element under <pastSigs> is inserted twice because last past signature is considered current and therefore isn't considered
flags="2" means that certificate is allowed for sharedUserId
Also <package sharedUserId="1000"> registration must be applied to app that declares android:sharedUserId="android.uid.system" in manifest, so it must be separate APK from one that performs the exploitation
While I was able register new certificate trusted for android:sharedUserId="android.uid.system", normally app signed with that certificate and declaring only sharedUserId in manifest wouldn't be able to start. When launching it we'll see following message in logcat:
signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
Abort message: 'JNI FatalError called: (com.example.abxoverflow.droppedapk) frameworks/base/core/jni/com_android_internal_os_Zygote.cpp:1976: selinux_android_setcontext(1000, 0, "default:privapp:targetSdkVersion=33:complete", "com.example.abxoverflow.droppedapk") failed'
This is because none of definitions in seapp_contexts file has matched
The user= rule in that file is mapped from uid (first argument of selinux_android_setcontext()), in our it'll be user=system, for normal apps this is user=_app
The other thing to match is seinfo= rule, which is taken from 3rd argument of selinux_android_setcontext() up to first colon. Originally that value comes from comparison of launched app signature against ones defined in /system/etc/selinux/plat_mac_permissions.xml
In the end our app tries to match user=system seinfo=default and there is no such rule in seapp_contexts
However, while process for our new android:sharedUserId="android.uid.system" app cannot be started, app can still be loaded into existing process if specified through android:process attribute. In particular, apps running under android.uid.system can specify android:process="system" to be loaded into system_server
In general app triggering crash of system_server is considered bug with Negligible Security Impact and here it is only worth noting because it is part of exploit chain that requires two system_server restarts
Anyway, we've got Parcelable chain:
IAlarmManager.set()AIDL method acceptsAlarmManager.AlarmClockInfoAlarmClockInfocalls deprecatedreadParcelable()without type argument (because it is in apex module and these weren't switched to new methods)- I specify
android.content.pm.PackageParser$ActivityasParcelableclass - Reading of that leads to invocation of any public constructor that accepts single
Parcelargument - I specify
android.os.PooledStringWriter, which callswriteInt(0)on providedParcel - That
writeInt()call was done onParcelreceived asdataargument ofonTransact(), which is backed by read-only memorymmap-ed from/dev/binder. Write to that causesSIGSEGV
Also of note, I've used PackageParser+PooledStringWriter combination as part of previous reports, for example for with CVE-2023-21098
This is what happens once you press "Do everything" button within app
RebootBackgroundRunneris started as separate process, which now will just usesetsid()to survive userspace reboot and after that will wait in background- New
PackageInstaller.Sessionis allocated and newChecksumobject is added to it. ThatChecksumobject contains byte array with size which will cause integer overflow during serialization and once its data are deserialized back system will seePackageInstaller.Session-s whose data were previouslyChecksumpayload. In particular, there are two sessions injected- One with
sessionStageDir="/data/system"andprepared="true"(meaning that stage directory is already ready and doesn't need to be created) - One with
sessionStageDir="/data/app/dropped_apk"andprepared="false"(meaning that directory will be created upon firstSession.openWrite())
- One with
- New
PackageInstaller.Sessionis allocated and then immediately destroyed. This triggers system to write updated contents toinstall_sessions.xml - After small delay, a
system_servercrash is triggered - During next
system_serverstart,install_sessions.xmlfile is read and nowPackageInstaller.Session-s which we injected can be used RebootBackgroundRunnerhas been waiting in background during userspace reboot and once it notices that system is back up and ready it performs next steps- Using one
PackageInstaller.Session, new APK is extracted from assets and written into with/data/app/dropped_apk/base.apk - Other session is used to read
/data/system/packages.xml, that file is patched to declare that newly dropped APK has been already installed and certificate used for it was previously used forandroid:sharedUserId="android.uid.system"and is still trusted for that purpose. Altered file is written as/data/system/packages-backup.xml - Another
system_servercrash is triggered - When
system_serverduring startup seespackages-backup.xml, it considers originalpackages.xmlto be corrupted and uses backup instead - As system has read modified
packages.xml, just dropped app is present and launches itself fromACTION_BOOT_COMPLETED. That new app runs withinsystem_serverbecause it has<manifest android:sharedUserId="android.uid.system">and<application android:process="system">inAndroidManifest.xml
Along with PoC app there is utils directory with few scripts
moveapk.shmoves compiled APK to drop intoassetsof dropper, to be run aftergradle :droppedapk:assembleReleasepeeksessions.shallows viewing current contents ofinstall_sessions.xml(requireseng/userdebugbuild of Android)wipesessions.shclears any presentPackageInstaller.Session-s and restarts system (requireseng/userdebugbuild of Android)
Not sure if this is related, but looking at history for possibly ABX-related bugs (cd frameworks/base ; git log -S ABX) I've found "Stop processing on IOException" commit, which includes addition of unit test with truncated ABX file. That commit was follow-up to "Ignore malformed shortcuts", which was described in bulletin as DoS
