Posted by Mateusz Jurczyk, Project Zero
This post is the fourth of a multi-part series capturing my journey from discovering a vulnerable little-known Samsung image codec, to completing a remote zero-click MMS attack that worked on the latest Samsung flagship devices. New posts will be published as they are completed and will be linked here when complete.
- [this post]
In Part 3 of the series, I chose one of the 174 obvious Qmage memory corruption crashes reported in Issue #2002 for exploitation. It was a linear heap buffer overflow in RLE decompression with an arbitrary allocation size, overflow size, and overflow data. By carefully adjusting the bitmap dimensions (which control the heap region size), we managed to place the pixel storage buffer directly before the associated android::Bitmap object in memory, allowing us to reliably corrupt it. From there, we constructed some potential RCE primitives, as well as a memory oracle that triggers a control flow-neutral read from a chosen memory area, triggering a crash or not depending on whether the address range is mapped and readable. In terms of low-level capabilities, this is a satisfying set of options to continue working with.
To make further progress in the exploit development, we finally have to get familiar with the MMS protocol that we'll be using as the medium of our attack. Specifically, we need to find a way to remotely leak information about the crash of the target Messages app, or lack thereof, to complete the ASLR oracle and build a more complex ASLR bypass logic on top of it. This is not completely trivial considering the unidirectional nature of MMS, but ultimately possible thanks to the little used feature of delivery reports. However, first things first – let's start with learning more about the protocol itself, and how we can move from sending test messages from a smartphone, to programmatically running experiments from a more comfortable environment of one's workstation.
Setting up a test environment
In order to be able to test MMS effectively, we need an easy way to deliver them to the target device from our PC. There are various methods to achieve this, for example Joshua Drake suggested two ways to send MMS without carriers in his Stagefright Black Hat presentation in 2015 (slides 84-85). However, I decided to take a more practical approach and send all messages through carriers, to be able to observe fully accurate results and spot any real-life issues related to conducting such an attack in practice.
To that end, I purchased two SIM cards for sending and receiving messages, and enabled an "unlimited MMS" package on the sender one to avoid excessive costs. Then, I found and licensed the NowSMS Windows software, which is a powerful solution for sending, receiving, and processing SMS/MMS. It may serve as an SMS server, MMS server, WAP Push Proxy Gateway and Multimedia Messaging Center (MMSC), and has a number of advanced features that are beyond the scope of our use case. Most importantly, it can be used to send messages through a locally connected GSM modem, or an Android phone acting as one. This is precisely the functionality we need, and it's available even in the most basic Now SMS & MMS Lite package. Notably, the service can be used in a number of ways: via a local web interface, through an HTTP API, and through developer API made available for technologies such as PHP, Java and .NET (C#, Visual Basic). The vendor also maintains an extensive documentation regarding both the product and relevant mobile protocols, and hosts an active user forum. All in all, NowSMS proved extremely helpful in my research by making interactions with SMS/MMS easily accessible on a PC, both manually and programmatically.
The screenshot below shows how the MMS sending page looks like in the Web UI (in Developer Mode). We can immediately spot a number of new and unfamiliar settings which are not available to the user when sending a message on a typical mobile phone. It looks like we have gained a much more fine grained control over what is transmitted over the cellular network:
The Android phone acting as a modem may operate in three modes: "Local WiFi", "Remote Direct" and "Remote via Cloud". In my case, I used the Remote Direct mode, and connected the sender phone to the local network via an ethernet cable, to prevent any disruptions related to wireless connectivity. At the same time, I connected the victim phone to my workstation via a USB cable for command line access and screen capturing. The structure of my setup is illustrated below:
I used a Samsung Galaxy A50 as the modem, and Samsung Galaxy Note 10+ as the receiver. In addition to having them connected to the PC for data transfer, it was obviously necessary to keep them charged throughout the testing, and to ensure that they were placed in a spot with strong cellular signal.
Crafting a raw MMS PDU
Now that we have a solid testing environment setup on a PC, we can dig deeper into MMS itself to better understand how it works. MMS is a relatively old technology dating back to circa 2001-2002, and since its inner workings are relevant mostly to mobile network operators, it is not documented as well as many other technologies and protocols seen in widespread use today. However, throughout this project, I have dug up a number of comprehensive books, articles, presentation slides and other educational materials on the subject. They are listed below for your convenience:
- As a start, the Wikipedia article on MMS and MMS architecture (and their references).
- Books listed at https://www.smssolutions.net/books/smsmms/. I found the following two especially informative:
- Mobile Messaging Technologies and Services: SMS, EMS and MMS by Gwenaël Le Bodic
- MMS: Technologies, Usage and Business Models by Daniel Ralph and Paul Graham
- Documentation available on the NowSMS homepage (How MMS Works), and the MMSC Training Slides.
- Gwenaël Le Bodic's slide deck: Mobile Messaging - Part 5 - MMS Architecture And Transactions
The volume of these resources may seem overwhelming, but in fact, we are only interested in a small subset of the MMS architecture, namely the MM1 protocol used between mobile devices and the MMSC (Multimedia Messaging Service Centre). The Phone to MMSC Protocol (MM1) slides from NowSMS are a highly recommended read to get a good overview of its design. In essence, we can view an MMS message as a self-contained binary file of MIME type application/vnd.wap.mms-message. It contains a number of headers (some of them required, some optional), followed by optional Multipart objects – the actual multimedia content of the message (images, audio, video, etc.). The details of the MMS binary encoding are defined by the MMS Encapsulation Protocol, and the list of headers compatible with the M-Send.req request can be found in that document in section "6.1.1 Send Request" on page 17.
An example source file of an MMS message is shown below:
Lines 1-3 specify mandatory headers, lines 4-6 specify optional headers, and lines 7-8 contain NowSMS-specific headers that point to the multimedia files to include in the message, and indicate their respective MIME types. Such MMS source can be converted to its binary form with NowSMS mmscomp command line utility:
The first 128 bytes of the message.MMS file are shown below; the rest are just the remainder of the JPEG image contents:
In this blob, we can see the binary-encoded headers (for example the two initial 0x8c 0x80 bytes encode "X-Mms-Message-Type: m-send-req"), as well as a number of plaintext strings corresponding to the header values, attachment file names, and data of the embedded files themselves. Such a raw MMS file can be sent via NowSMS, and will be delivered in a very similar form to the recipient device.
As a side note, correctly formatted MMS messages are also expected to contain SMIL (Synchronized Multimedia Integration Language) resources, which define how the multimedia and text should be presented to the user. If you are interested in more details on how they're used in MMS, there is a good tutorial by NowSMS on the subject. However, the SMIL markup seems to be optional in practice, and client apps such as Samsung Messages will correctly display MMS without it. When it comes to file attachments in general, what matters the most for us is that their MIME types are specified explicitly and separately in the encoded message, which enables us to freely send Qmage files marked as image/jpeg (or some other image type) and have them automatically loaded as bitmaps.
MMS delivery reports
Delivery reports have been part of the MMS specification since version 1.0. They enable the sender of a message to request a confirmation of its successful delivery to the recipient. It's one of the very few ways for the sender to receive any kind of (indirect) feedback from the target phone, and it is what we intend to use to complete our ASLR oracle mechanism.
When composing the MMS PDU, a delivery report can be requested by setting the X-Mms-Delivery-Report header to "yes", which is expressed as 0x86 0x81 in binary. Here's how the header is described in Gwenaël Le Bodic's book:
Request for a delivery report. This parameter indicates whether or not delivery report(s) are to be generated for the submitted message. Two values can be assigned to this parameter: 'yes' (delivery report is to be generated) or 'no' (no delivery report requested). If the message class is 'auto', then this parameter is present in the submission PDU and is set to 'no'.
Quite frankly, I have never legitimately used this feature of MMS before. Even though it's part of the protocol, the option to request delivery reports is missing in some common apps such as Google Messages (it only supports SMS delivery reports). However, Samsung Messages does support it, so we can enable the reports under Settings > More settings > Multimedia messages > Show when delivered, and test it out:
The option does indeed work as advertised. Let's take a deeper look at how it is implemented in the protocol and in the client app, and how we can make use of it in our exploit.
A closer look at MM1
Once again, let me start by emphasizing that Gwenaël's slides 24-41 and the entire NowSMS MM1 slide deck explain the MM1 protocol and data flows in great detail. In our case, let's analyze the transactions involved in sending and receiving an MMS in an environment with a few assumptions:
- The originator and recipient are both in the same network, so there is no inter-operator communication taking place. Whether this is true or not shouldn't make any practical difference for us, as the data exchange between them happens seamlessly over the MM4 protocol and doesn't have any observable side effects (that I know of).
- The recipient has the auto-retrieval of MMS enabled, which I understand to be the default on a majority or all of Samsung devices.
- The recipient has good enough connectivity to be able to download the message.
- The delivery report is requested by the originator.
Under these conditions, the message exchange between two mobile phones and the MMSC is illustrated in the following diagram:
In a typical scenario, the sender initiates an M-send.req HTTP POST transaction to the carrier. Once the MMS is transmitted in full, the MMSC sends a WAP PUSH notification to the recipient to announce that a message is awaiting. In the case of auto-retrieve, the client app immediately sends an HTTP GET request, and receives the serialized MMS data in response. Finally, it acknowledges the receipt of the message with a M-notifyresp.ind POST request, and that information is forwarded back to the sender in the form of an M-delivery.ind transaction. This concludes the communication between the participating parties.
The biggest problem shown in the diagram is the fact that the Samsung Messages app parses the incoming MMS before finalizing communication with the MMSC through the M-notifyresp.ind PDU. Ideally, any processing of external data should only take place once the connection with MMSC is closed. Otherwise, if the app crashes during the processing of a corrupted media file, the final M-notifyresp.ind message is never transmitted, which causes the MMSC to classify the MMS delivery as unsuccessful and prevents it from sending the delivery receipt to the originator. This creates a very easily observable side channel, revealing whether Samsung Messages crashed on the victim phone or not.
Coupled with the powerful memory-probing primitive constructed in Part 3, the side channel enables an attacker to remotely query the readability of arbitrary address ranges, with no user interaction required. Such a capability is enormously useful on Android due to the Zygote design, and the fact that the location of code and data in the address space is persistent across program crashes. Consequently, even though the ASLR oracle output only carries 1 bit of information at a time, the overall attack can be broken down into multiple steps, and their results combined to determine complete 64-bit addresses of the necessary gadgets.
We can confirm the behavior by checking the logcat logs on the target device. When we send a regular MMS message, we can see both the WSP/HTTP GET.req and M-notifyresp.ind (POST) requests being made:
The time span between receiving the full message from the MMSC and sending the acknowledgement is around 1.5 seconds. On the other hand, when we send a malformed Qmage file, only the WSP/HTTP GET.req request is visible in the logs:
Before M-notifyresp.ind can be sent, the process crashes after ~1.3 seconds of reading the HTTP response:
This confirms the insecure behavior on the client app side. How does it look from the perspective of an attacker? When the M-delivery.ind PDU is received by NowSMS, it is decoded and saved in a text file with a .HDR extension in the "C:\Program Files (x86)\NowSMS\MMS-IN" directory, for example C0B04508.HDR:
The status is indicated as "retrieved", and the report can be associated with the original message through the value of the Message-id header. Otherwise, if the original MMS crashes the target phone, we don't see any immediate return messages in the MMS-IN directory. Depending on the MMS expiry period (specified in the headers or defined by the operator's default setting), the carrier may retry to deliver the message, and if that fails, it eventually expires and the sender is notified about it too:
The carriers I have experimented with have a default expiration period of 48 hours, and it can be manually adjusted with the X-Mms-Expiry header to values between 1 minute and 48 hours. In my exploit, I didn't use the expiration aspect at all, and simply assumed that Samsung Messages crashed if the delivery report was not received within 30 seconds of sending the message. This completes the construction of a functional MMS-based ASLR oracle, which is an essential building block of a generic ASLR bypass logic discussed in the next blog post in the series.
Further thoughts on oracle reliability
The reliability of the presented ASLR oracle scheme is generally high, provided that both the sender and recipient devices maintain good connectivity with the MMSC. The weakest link is by far the android::Bitmap memory corruption primitive, which relies on two subsequent 160-byte jemalloc allocations being adjacent in memory. This generally holds true, but we have no guarantee that the condition will be always met, especially since the relevant jemalloc bin (chunks between 129-160 bytes in size) is not particularly quiet and is also utilized for other unrelated objects by the Samsung Messages app. Needless to say, any ASLR bypass logic we devise will most likely assume 100% accuracy of the oracle output, so we have to put some extra effort to make sure that the oracle can be indeed relied upon.
One simple technique we can use to improve the reliability of the attack is to have each oracle MMS processed with a relatively clean state of the heap. This can be accomplished by unconditionally crashing the client app with a malformed Qmage file, causing the com.samsung.android.messaging process to be killed and restarted from scratch when the next message arrives on the phone. Of course the ASLR oracle output false already implies a crash taking place, so extra artificial crashes are only needed at the very beginning of the attack (before the first oracle query), and after each query returning true. The type of the artificial crash doesn't matter as long as it always reproduces; it can be a huge out-of-bounds read/write, a NULL pointer dereference, assertion failure, or any condition that doesn't depend on the existing state of the process to trigger a crash. In my exploit, I used the signal_sigsegv_400357fc6c_7014_c1d4fedf1cbcdd583e0f331f32df1f72.qmg sample from crash 39b052a01c99f60982ec92f8d01a5401, which accesses a NULL pointer returned by a malloc call with a negative integer passed as the size.
This one trick allowed me to achieve an oracle accuracy rate of more than 99% (loose estimate) on my Galaxy Note 10+ test device. In my case, it was sufficient to completely rely on each single measurement to successfully defeat ASLR without making any mistakes during the process, but your mileage may vary depending on the device model, Android version, existing history of messages in the SMS app, or even specific options enabled on the phone (such as WiFi) during the attack. If the oracle accuracy drops below a certain threshold, it may be necessary to introduce redundant memory probes sent to the target for each tested address range, and only return the output value to the higher levels of the exploit code once there is enough confidence about its validity.
ActivityManager and crash rate limiting on Android
Based on what we know so far, we can assume that any potential attack will involve a number of crashes of Samsung Messages on the victim phone, some of them carrying address space information and some triggered simply to reset the heap. The ability to continuously crash and have an app restarted on a remote device is a crucial requirement, so we should verify that this is actually possible on Android. If we send corrupted Qmages via MMS twice in a short span of time, we will observe two crashes, as expected:
If we then send a third message, Samsung Messages won't be spawned to handle it. Instead, we'll see the following message:
At this point, we (as the attacker) are cut off from the device and cannot reach or interact with the remote Qmage attack surface anymore. In fact, the victim won't be able to receive SMS/MMS from anyone until they manually start the Messages app again. So what happened here, and does it mean that all our efforts up to this point were in vain?
When I first saw the warning, I immediately went looking for clues at cs.android.com. It was easy to locate the culprit based on the "process is bad" string: it is printed out when a call to mService.startProcessLocked fails in BroadcastQueue.java. This may generally only happen when mService.mAppErrors.isBadProcessLocked returns true for the app in question:
There is a list of bad processes in the system, but how does an app end up on that list? The answer can be found in the handleAppCrashLocked method in AppErrors.java, and specifically in the following lines (slightly reformatted for readability):
In the above snippet, now is the current timestamp and crashTime is the time of the last crash of the app. Accordingly, the logic checks if two crashes in a single app have occurred in a short period of time, and if so, it bans the process indefinitely from future restarts. How short is short? Let's look up the MIN_CRASH_INTERVAL constant in ProcessList.java:
It's 60 seconds. From the attacker's point of view, this is certainly not perfect, but also not terribly bad. This logic of the ActivityManager service means that at no point in time, should we trigger two crashes of the Messages app within one minute, or the attack will be halted. In the context of our ASLR oracle, it limits the probing rate to one query a minute, which may be acceptable or not depending on how many queries are required to break the ASLR. For example, if we consider a realistic attack to be carried out during the night, that leaves us with a maximum of 8 hours × 60 minutes ~= 480 queries. The good news (for exploitation) is, that there is no absolute limit of crashes for one app, and we can interact with the MMS client indefinitely as long as we slow down the communication to meet the crash interval condition.
The diagram below illustrates the high-level process of safely sending two ASLR oracle queries to a target phone, taking the mandatory cooldown period into account. The first query returns true and takes two MMS to complete (one probe and one unconditional crash), and the second one returns false. Note how there is always a guaranteed 60 second gap between two subsequent crashes on the recipient device:
On a closing note, there is one more important detail to consider in the crash handling logic. If we look closely at the source code of the handleAppCrashLocked method, we can notice that the timestamp is obtained through the SystemClock.uptimeMillis() API:
As the documentation states, this is not exactly the wall clock time we have assumed it to be:
uptimeMillis() is counted in milliseconds since the system was booted. This clock stops when the system enters deep sleep (CPU off, display dark, device waiting for external input), but is not affected by clock scaling, idle, or other power saving mechanisms. This is the basis for most interval timing such as Thread#sleep(long), Object#wait(long), and System#nanoTime. This clock is guaranteed to be monotonic, and is suitable for interval timing when the interval does not span device sleep. Most methods that accept a timestamp value currently expect the uptimeMillis() clock.
According to my experimentation on the Galaxy Note 10+ device, when the phone is in an inactive state (e.g. set aside on a bedside table with the display off), the clock indeed doesn't progress. This makes practical zero-click exploitation even more challenging, as it is not enough to just wait for 60 seconds before sending the next MMS. Instead, the attacker has to keep the target phone somehow occupied for those 60 seconds, while not triggering any vibration/notification sounds at the same time. The most obvious way to achieve this is through the cellular network, and I have identified at least three techniques that could be used to silently and remotely keep a phone busy:
- By sending an MMS with empty text (i.e. an empty text/plain MIME file), a few seconds can be wasted while the phone receives and processes the message. In the end, the empty text leads to an unhandled Java exception being thrown in the Messages app, preventing it from showing any notification to the user. I abused this behavior in my exploit to send an initial "ping" to quietly verify that the recipient phone is responsive (see 0:43-0:56 in the exploit demo). It has been fixed in Samsung Messages since version 184.108.40.206.
- By sending an MMS with very long text (at least around 140 kB in my testing), a few seconds can be similarly wasted by the victim phone. In this corner case, the misbehavior is slightly different and varies between devices, as the unhandled Java exception is thrown in the midst of generating a user notification, when it is already displayed on the screen, but before the notification sound rings. As such, it also qualifies as a (literally) silent CPU cycle burning trick.
- By sending a very long SMS of 5180+ characters, which is divided into 34 segments of 140 characters each. The target phone starts receiving the SMS segments, roughly one per second, but for some reason (I didn't investigate this deeply) it stops reassembling the message after the 33rd part, and abandons it completely without generating any notifications. During the process of receiving and saving the initial portions of the SMS in the internal database, the uptimeMillis clock progresses by around 35 seconds in my test setup.
These are some basic ideas for ways to transmit data to the phone such that it has to spend cycles processing it, but fails at some point before notifying the owner. I am sure many more similar techniques exist, and specialized software such as NowSMS certainly helps put the relevant mobile apps to the test against very unusual conditions. All in all, the nature of the uptimeMillis clock is not a fundamental barrier in remote Android exploitation, but it is an annoying aspect that needs to be addressed with the use of additional techniques, and it may extend the overall attack time and impair its reliability. With 60 seconds of active CPU time required between each ASLR oracle query, we might also start being concerned about the extent of power consumption induced by the exploit on target phones with low battery levels… :)
In this episode, we set up an environment to programmatically send MMS messages from a Windows PC, and learned the basics of the client ⟷ MMSC MM1 protocol and its encapsulation encoding. This enabled us to specify the X-Mms-Delivery-Report header in outgoing messages, and abuse the delivery report feature to establish a 1-bit side channel indicating if the recipient's Messages app crashed while processing our malformed Qmage image or not. Based on this capability and the address-probing primitive built in Part 3, we now have a fully functional (albeit somewhat slow) ASLR oracle at our disposal. We are getting close to defeating ASLR and finally executing arbitrary code.
To make further progress in the research, we have to face a few remaining questions:
- What types of addresses are we interested in leaking? Which libraries will be needed to achieve RCE, and do we also need to disclose any data locations?
- How do we find any regions in memory at all, starting with absolutely no initial insight as to where they might be located?
- Finally, how do we achieve this in a relatively small number of steps (preferably low hundreds), such that the attack has a realistic execution time?
All of these matters will be discussed in detail in the upcoming Part 5. Stay tuned!
Đăng nhận xét