This time we'll only go over the vulnerability, with no exploit, since I don't personally have any device which is vulnerable to this issue, and therefore couldn't write an exploit. However, we'll dream up an exploit together, which should be pretty simple to implement.
Before we start, I'd like to point out that this vulnerability has been responsibly disclosed to Qualcomm, and it has since been fixed (see "Timeline" below). It should be noted that this vulnerability was present in all Qualcomm-based devices based on the following chipsets:
- APQ 8064 (Snapdragon S4 Pro)
- MSM 8960 (Snapdragon S4)
- MSM 8660 (Snapdragon S3)
- MSM 8x30
- MSM 7x30
Let's get to it
Today we'll take a look at the "mdp" display driver. There are slight variations of this driver, depending on the SoC. However, both the MDP22 and MDP303 versions (which correspond to the SoCs listed above) are vulnerable.
Normally, users may access the display driver in order to modify the display's properties, and perhaps even in order to retrieve the current frame-buffer (that is, take a screenshot).
Since these operations are somewhat sensitive, they are usually restricted so that only processes with the "graphics" group-ID may perform them. This is facilitated by setting the permissions on the device files appropriately:
Naturally, the process in charge of compositing surfaces on Android (surfaceflinger) is a member of this group. However! The shell user is also a member of the graphics group - meaning, it can interact freely with the "mdp" driver (and therefore the vulnerability is also locally exploitable).
shell's user-ID and group-IDs
Diving into the code
The "mdp" driver is extremely complex, supporting a wide range of commands; from IOCTLs, to memory mapping the device, etc.
Funnily, though, there was no need to go too deeply, since the second IOCTL command turned out to be vulnerable :)
The "mdp" driver allows a user to change the colour map lookup table used by the display, by means of a special IOCTL called "MSMFB_SET_LUT". The actual implementation of this IOCTL is deferred to a simple call to an internal function pointer, which is initialized to point to the actual implementation based on the MDP platform which is compiled into the kernel.
Both of these functions call the "mdp_lut_hw_update" function directly in order to update the lookup-table, without performing any validations of their own on the user-controlled "fb_cmap" structure.
Let's take a good look at the "fb_cmap" structure:
Alarm bells should be ringing right about now:
- This structure contains a large (32-bit) length field
- There's a "start" field which is not only large (32-bit), but whose name indicates that it might actually be treated as a pointer, even though its type is an unsigned integer
- All the pointers in the structures aren't marked as "tainted" (using "__user")!
Finally - let's keep our fingers crossed and take a look at the "mdp_lut_hw_update" function:
First, the function iterates "len" times. Then, for each iteration, the function reads the red, green and blue values from the "fb_cmap" (safely, using "copy_from_user"). But here comes the scary part:
Still non the wiser. What does "outpdw" do?
For those who haven't come across it before, "writel" simply writes the value in "val" into the address at "port", using a memory write barrier beforehand. This is usually used in order to write to memory mapped registers, in order to make sure the write itself remains coherent.
Regardless, this means that the function above writes the concatenated value of the red, green and blue parameters (which are fully user controlled), into an address which is built from fully known, constant values and a fully controlled 32-bit value which is not validated in any way, since:
- "MDP_BASE" is a macro which is defined to a constant memory mapped address (one for each SoC)
- 0x93800 is a constant number and therefore also known in advance
- "mdp_lut_i" is actually a flag which is set alternately to either 0 or 1, on each call to MDSSFB_SET_LUT. This means that the value of 0x400*mdp_lut_i is either 0 or 0x400
- Since we can set cmap->len to 1, the index "i" will therefore be zero in the single iteration performed, meaning we can ignore i*4 (since it will equal zero)
- cmap->start is fully user-controlled and never validated
Dreaming up an exploit
First, as with all exploits, we'd like to neatly package the write-what-where primitive into a single function. Let's imagine we've done that, and that it's called write_value, and it accepts a 24-bit value and a 32-bit address to which this value should be written.
In order to make exploitation fully reliable, we'd need to know the current value of "mdp_lut_i". This can be done by mapping a sterile buffer within user-space which is more than 0x400 bytes large. Then, we can simply trigger to overwrite vulnerability with a cmap->start value so that the destination address will either correspond to the beginning of this mapped buffer, or to its end:
Now that we know all the values from which the destination address is built, we can freely overwrite any address within the kernel's virtual address space. From here on, we can simply overwrite a kernel function pointer and redirect it to a function stub allocated within user-space.
This is actually identical to the exploitation method covered in a previous blog post - in which we overwrote a function pointer within "pppolac_proto_ops" and triggered it by closing a PPP_OLAC socket.
And there you have it. A fully imaginary exploit, just waiting to be written :) If you do happen to write this exploit, please let me know!
- 27.09.14 - Vulnerability disclosed
- 29.09.14 - Initial response from QC
- 02.10.14 - Issue confirmed by QC
- 13.11.14 - QC publishes notification to customers
- 27.11.14 - QC publishes notification to carriers
- 11.12.14 - Issue closed, CAF advisory issued