| Main |

dmg vulnerability debunked

On 20th of this month, lmh of MoKB fame posted a security advisory claiming that:

Mac OS X com.apple.AppleDiskImageController fails to properly handle corrupted DMG image structures, leading to an exploitable memory corruption condition with potential kernel-mode arbitrary code execution by unprivileged users.

(the bold highlighting is his, not mine, and is clearly intended to shock.)

A supposedly more complete description of the problem was posted here, and was subsequently “validated” by a third-party security company, Secunia, who rate it as a highly critical security flaw, allowing privilege escalation, denial of service and compromise of a vulnerable system. [Secunia tell me that they did not validate the issue, though they have been reported as having done so by others.]

This issue has been given a variety of numbers by various people, including:

Many of the above links are to organisations who people trust to provide authoritative security information. Indeed, so serious, apparently, is this bug, that it's even made the BBC News site, not once, but twice.

It is a shame, therefore, that lmh didn’t do his homework properly before writing his advisory. It is even more of a shame that all of these people trusted what was written by someone who couldn’t be bothered to analyse the kernel panic he’d triggered properly or to work out what the flaw was that he’d found.

I doubt this would normally even have been noticed. But lmh made a mistake when he accused me of being someone who “doesn’t bother reading, checking and… [being] willing to say something that doesn’t make sense at all” and then subsequently ignored my advice not to mistake me for someone who didn’t know what they were doing.

As as a result of his comments on his site, and with some cajoling from Matasano Security’s Thomas Ptacek, I decided to investigate further.

Guess what I found? Not only is lmh’s diagnosis completely incorrect, but the problem isn’t a security flaw at all, let alone a critical, highly critical, or warn-everyone-via-the-BBC type event.

Now, I should say, that I’m wary of suggesting that disk images are totally safe. There’s a lot of code involved in mounting and reading/writing a disk image, and quite a bit of that runs in kernel mode. But I am pretty peeved at the way that this issue has been so widely publicised, attracting a great deal of attention for lmh and MoKB, when in actual fact there is no such security flaw. I’m also upset that none of the security companies listed above, especially Secunia who put out a press release saying that they’d “verified” the problem [Secunia tell me that this is untrue.], actually did sufficient analysis to realise that. Hopefully in future they will do their jobs properly and actually go through the kernel code like I did.

“So&rdquo, you ask. “this dmg file… it does cause a kernel panic, right? So if lmh is wrong, where’s your evidence—after all, he’s published a very technical looking report with a lot of gobbledegook from gdb.”

Yes, he has, hasn’t he. Unfortunately he didn’t interpret it properly, which is where the problems started.

It’s taken me the best part of three days’ work to figure out what is really going on here, which gives some idea of how difficult it is (particularly without the source code for the disk image driver) to trace everything back through and come up with a proper, definitive explanation of the problem. lmh obviously wasn’t interested in doing that—after all, why do proper research when you get just as much publicity from guesswork and scaremongering?

What happens when you double-click this dmg file? Well, the first thing that happens is that the system runs a program called diskimages-helper. This program is responsible for a number of tasks related to disk image support in Mac OS X, one of which is mounting disk images.

(All of the discussion that follows is a slight simplification, because there are lots of different types of disk image and the handling differs for some of them. I’m talking specifically about raw read-write disk images, of the kind that lmh posted.)

diskimages-helper talks to the kernel by way of an IOUserClient implementation in the disk image driver, IOHDIXController.kext. When you try to mount a disk image, the helper sends a message to this component asking it to do the actual work.

Next, the kernel component instantiates a number of objects and attaches them to the IOKit’s I/O registry, which causes the IOKit to begin probing for partition maps. For this part, we have source code, in the form of the file IOApplePartitionScheme.cpp; the lines we are interested in are:

// Determine the official block size to use to scan the partition entries.

dpmeBlockSize = mediaBlockSize;                      // (natural block size)

if ( OSSwapBigToHostInt16(driverMap->sbSig) == BLOCK0_SIGNATURE )
{
    dpmeBlockSize = OSSwapBigToHostInt16(driverMap->sbBlkSize);

    // Increase the probe score when a driver map is detected, since we are
    // more confident in the match when it is present.  This will eliminate
    // conflicts with FDisk when it shares the same block as the driver map.

    *score += 2000;
}

This probably looks like gobbledegook, so let me explain what’s going on here. The IOApplePartitionScheme object is looking to see if there is a Block0 structure at the start of the image. If it finds one, then the partition map’s location depends on the block size stored in the Block0, rather than on the natural block size of the disk. It’s relevant at this point to note that disk images always have a natural block size of 512 bytes (this is hard-coded in the kernel).

Then, further down in the same file, the code loops through the entries in the partition table, as follows:

// Scan the media for Apple partition entries.

for ( dpmeID = 1, dpmeCount = 1; dpmeID <= dpmeCount; dpmeID++ ) 
{
    UInt32 partitionBlockSize = dpmeBlockSize;

    // Determine whether we've exhausted the current buffer of entries.

    if ( dpmeID * dpmeBlockSize + sizeof(dpme) > bufferReadAt + bufferSize )
    {
        // Read the next partition entry into our buffer.

        bufferReadAt = dpmeID * dpmeBlockSize;

        status = media->read(this, bufferReadAt, buffer);
        if ( status != kIOReturnSuccess )  goto scanErr;
    }

Notice how it’s using the dpmeBlockSize variable from above to work out where to read from. In lmh’s broken disk image, the start of the image looks like this:

00000000: 4552 023d 0000 2f00 0000 0000 0000 0000  ER.=../.........
00000010: 0000 0000 0000 9c00 0000 0000 0000 0000  ................

The second pair of bytes, 023d, are the interesting ones here; they tell the system that the block size for this image (that is, the value in dpmeBlockSize) is not 512, but 573 bytes. Therefore, the first partition map entry should be at byte offset 573 into the image, which is what bufferReadAt gets set to, and it’s at that offset that the IOApplePartitionScheme code tries to read a partition map entry.

Now, normally, partition map entries are at integer multiples of the device block size, so the various bits of code in IOStorageFamily.kext (which I won’t bore you with, though I went through them to work this out) would simply pass the buffer created by the IOApplePartitionScheme object through to the code that actually does the read.

However, in this case, the read wasn’t aligned on a block boundary. The system, of course, has code to cope with this situation. What it does is it creates an IODeblocker object, whose job is to read the data in chunks and then copy the correct bits of it into the buffer it’s supposed to read into.

It is this IODeblocker object that finally makes its way to the IOHDIXHDDriveInKernel object’s kernelIOThread() method. (IOHDIXHDDriveInKernel is an object instantiated by the in-kernel disk image code to handle I/O for the disk image itself.) Normally at this point, it would see an IOBufferMemoryDescriptor object instead, but because of the misalignment of the read request, it gets the IODeblocker.

The first thing that the IOHDIXHDDriveInKernel object tries to do with the buffer when it gets it is to map it into the kernel’s memory space (it doesn’t know at this point, but the buffer is already in the kernel’s memory map because this particular read request came from the kernel itself).

To do this, IOHDIXHDDriveInKernel calls the buffer object’s IOMemoryDescriptor::map() method, which, in turn calls IOMemoryDescriptor::makeMapping(). That function checks for an existing mapping, decides in this case that there isn’t one, and therefore creates a new _IOMemoryMap object.

(You can see all of this in IOMemoryDescriptor.cpp, which you can find in the xnu sources at xnu/iokit/Kernel.)

This object is then initialised using its initWithDescriptor() method, which in turn calls the buffer object’s doMap() method to actually do the work of updating the virtual memory map with the new mappings.

The doMap() method eventually finds its way to a call to IOMemoryDescriptorMapAlloc():

logical = *atAddress;
if( options & kIOMapAnywhere) 
    // vm_map looks for addresses above here, even when VM_FLAGS_ANYWHERE
    ref.mapped = 0;
else {
    ref.mapped = trunc_page_32( logical );
    if( (logical - ref.mapped) != pageOffset) {
        err = kIOReturnVMError;
        continue;
    }
}
    
if( ref.sharedMem && (addressMap == kernel_map) && (kIOMemoryBufferPageable & _flags))
    err = IOIteratePageableMaps( ref.size, &IOMemoryDescriptorMapAlloc, &ref );
else
    err = IOMemoryDescriptorMapAlloc( addressMap, &ref );

which fills-in the address at which the memory is mapped in the target task (in this case, the kernel), in the ref.mapped member of the IOMemoryDescriptorMapAllocRef structure, ref. (for those who are wondering, options does indeed contain the flag kIOMapAnywhere at this point.)

Now this is important: we don’t have a normal IOBufferMemoryDescriptor at this point. We have an IODeblocker instead. An IODeblocker has more than one buffer, so when the code in IOMemoryDescriptorMapAlloc() asks for the address of the buffer, it is given the address of the buffer for the first chunk.

All of this is fine, and at the bottom of the function, it makes a call to handleFault():

if( !ref.sharedMem || pager )
    err = handleFault( pager, addressMap, ref.mapped, sourceOffset, length, options );

The important thing to note here is that the address argument to handleFault() points at the start of the buffer for the first chunk in our IODeblocker.

Now, we look at the handleFault() method. It clearly needs to be able to handle situations where the data isn’t contiguous from the kernel’s perspective, and indeed if you look at it, you’ll see that it is written as a big loop:

IOReturn IOMemoryDescriptor::handleFault(
        void *			_pager,
	vm_map_t		addressMap,
	IOVirtualAddress	address,
	IOByteCount		sourceOffset,
	IOByteCount		length,
        IOOptionBits		options )
{
    ...
    physAddr = getPhysicalSegment64( sourceOffset, &segLen );
    assert( physAddr );
    pageOffset = physAddr - trunc_page_64( physAddr );
    pagerOffset = sourceOffset;

    size = length + pageOffset;
    physAddr -= pageOffset;

    segLen += pageOffset;
    bytes = size;
    do {
	...
	sourceOffset += segLen - pageOffset;
	address += segLen;
	bytes -= segLen;
	pageOffset = 0;

    } while( bytes
	&& (physAddr = getPhysicalSegment64( sourceOffset, &segLen )));

    if( bytes)
        err = kIOReturnBadArgument;

    return( err );
}

Anyone spot the mistake? If handleFault() were guaranteed to be called with multi-segment buffers that were actually a single chunk in user-land, everything would be fine. But here, we’re calling it with a multi-segment buffer (in our case an IODeblocker) that started off in the kernel. So the value in the address variable on the second pass through this loop will be wrong.

That wouldn’t have mattered, as it turns out, except that, because of a bug in the nVidia video driver, the following code was added as a “Temporary Workaround”:

/*  *** ALERT *** */
/*  *** Temporary Workaround *** */

/* This call to vm_fault causes an early pmap level resolution	*/
/* of the mappings created above.  Need for this is in absolute	*/
/* violation of the basic tenet that the pmap layer is a cache.	*/
/* Further, it implies a serious I/O architectural violation on	*/
/* the part of some user of the mapping.  As of this writing, 	*/
/* the call to vm_fault is needed because the NVIDIA driver 	*/
/* makes a call to pmap_extract.  The NVIDIA driver needs to be	*/
/* fixed as soon as possible.  The NVIDIA driver should not 	*/
/* need to query for this info as it should know from the doMap	*/
/* call where the physical memory is mapped.  When a query is 	*/
/* necessary to find a physical mapping, it should be done 	*/
/* through an iokit call which includes the mapped memory 	*/
/* handle.  This is required for machine architecture independence.*/

if(!(kIOMemoryRedirected & _flags)) {
	vm_fault(addressMap, 
		 (vm_map_offset_t)address, 
		 VM_PROT_READ|VM_PROT_WRITE, 
		 FALSE, THREAD_UNINT, NULL, 
		 (vm_map_offset_t)0);
}

/*  *** Temporary Workaround *** */
/*  *** ALERT *** */

Because of this, on the second pass through this loop, vm_fault() gets called with address set to point to a non-aligned address inside the first buffer held by the IODeblocker.

Note that it isn’t possible for it to point outside of this buffer, because the segment length, held in segLen, has to be in the range 0x1segLen &le 0x1ff as it’s set up by the IODeblocker to buffer the misaligned part at the beginning of the read. The buffer itself is larger than that, so there’s no chance of any mischief because the ability of a potential attacker to set the address at which vm_fault() gets called is extremely restricted.

Anyway, it is the call to vm_fault() with a non-aligned address that is causing the kernel panic.

So, what have we learned:

  1. It is not a memory overwrite bug.

  2. It is not exploitable, except in that you can kernel panic a machine if you can persuade a user to double-click a damaged dmg file.

  3. It is not, therefore, possible to use this bug for privilege elevation or to execute arbitrary code in the kernel.

In fact, all lmh has found here is a bug that causes a kernel panic. Not a security flaw. Not a memory corruption bug. Just a completely orderly kernel panic. There aren’t even any processor exceptions involved; the path to the panic is perfectly normal non-exceptional code using ordinary function calls. (All of the preceding not withstanding, you should still turn off Safari’s “Open ‘safe’ files after downloading” option; that way, you can’t be tricked into clicking a link that could kernel panic your machine, or indeed fall foul of any other problems with the algorithm that determines how safe a file might be.)

The moral of this story? Well, to my mind there are a few:

  1. Security research needs to be done properly and responsibly. It isn’t good enough to panic the user community over something if you haven’t done your research properly. Investigating this bug took me three days. What lmh appears to have done would have taken minutes, and is no way to verify a security flaw.

  2. Don’t “confirm” or “verify” the findings of others without actually investigating. Particularly if you’re going to release press releases telling everyone about it. That means you, Secunia. [Secunia inform me that in fact, though they were reported by others as having verified the issue, they have not done so, and did not issue any press releases.]

  3. Don’t accuse random people on the Internet of not knowing their stuff, like lmh did with me. Every so often you’ll find someone who really does, and you’re going to look very silly.

Credit is due to Thomas Ptacek of Matasano Security for casting an eye over my findings before I wrote this post.

Trackbacks

TrackBack URL for this entry:
http://alastairs-place.net/movabletype/mt-tb.cgi/112.

Comments

This was an excellent read. Personally I wish that Apple would have it's engineers post this sort of article after they issue patches for issues like this. However they have even taken to putting "potential to execute arbitrary code" on everything, and this may therefore lead to them not even doing this depth of analysis.

Your analysis is also a little bit off. This issue IS a security problem, regardless of the ability of any DMG image to execute arbitrary code when mounted.

DMGs open automatically in Safari and many other applications. This means that the kernel panic can be triggered remotely.

Further, unprivileged local users can mount these DMGs, enabling them to potentially crash the system as well.

The system should NOT crash when faced with any request from an unprivileged user or a remote party, period. If it does, be it a kernel panic, an access violation, or arbitrary code execution, that's a security issue.

dmg files don't open automatically in “many other applications”. The issue there is mainly Safari, and its “Open safe files after downloading” option.

What’s more, as far as I’m aware, the kernel panic cannot be triggered remotely—it always requires the assistance of a local user. You have to click on a link in order to open a dmg file even if you have Safari’s “Open safe files” option enabled. I suppose you could, if you’re feeling foolish, set a broken dmg file as your home page, and then running Safari would panic your machine until such time as you deleted its preferences. Pushing it a bit, though, I think.

And while I agree with the rest of what you wrote to some extent, I would point out that the only thing that you lose by panicking your machine this way is a minute or two rebooting it. Most Macs are single-user systems, and end users are unlikely to trigger the problem more than once (unless they’re particularly stupid, or just trying to make a point).

If we were talking about Linux, Solaris, *BSD, AIX or another system that was commonly deployed as a multi-user platform, I’d agree with you wholeheartedly. But my non-technical conclusions are really aimed at ordinary users of Mac OS X; what they want to know is whether it is a security problem for them, and I think the answer there is “No”. Yes, if they download a disk image from a dodgy source, they might panic their machine. How does that affect their security? It doesn’t. How does it affect the security of their machine? Again, it doesn’t. Nobody can break into their system as a result. Nobody can steal their identity, copy their credit card details, or install spyware, adware or trojans on their system using this bug.

So I stand by my remarks. For the overwhelming majority of Mac OS X machines today, this isn’t a security vulnerability, it’s a bug.

Anyone using Mac OS X in other ways (e.g. as a server platform) will, of course, need to think more carefully, and in that sense you make a good point.

" ... dmg files don't open automatically in “many other applications” ..."

Wrong, Stuffit Expander by instance does the same automatic mounting, when you receive a .dmg.sit file. (Even without the .dmg prefix)

But thanks for your time. I really appreciate your work.
--
Steve

OK, but for Stuffit Expander to do that, you have to open the dmg.sit file, right? So it’s still really Safari’s “Open ‘safe’ files automatically” option that’s the problem.

(Also—assuming we count Stuffit Expander—that makes two, which still isn’t “many”.)

Excellent analysis, thanks. Since it's not a remote exploit, what would an attacker do?

- get the user to install an .app contained in a good .dmg
- make a corrupt .dmg very visible thanks to a (relatively) harmless kernel panic

In the former case the app will install itself somewhere in the system, with the hope of surviving a reboot, in the latter the .dmg will be tossed in the trashcan by an annoyed user on the next boot.

it always requires the assistance of a local user. You have to click on a link in order to open a dmg file even if you have Safari’s “Open safe files” option enabled.

Well, this is a very weak sense of "assistance". Just visiting a web page can trigger a dmg download. Clicking on a link should be considered a "safe" action for a user.

Agreed, clicking a link should be safe, and that’s why Safari’s “Open ‘safe’ files automatically” option should be removed.

However, I don’t personally consider the fact that Safari has a security flaw in it to be sufficient to make this disk image bug into a “security issue”. Even given that it’s possible to construct a link or a web page such that it downloads a damaged dmg file, and even if that dmg is mounted automatically by Safari and causes a kernel panic, the end user still had to go to that web-page or click that link. (And, don’t forget, they still had to be running Safari in order to get that far!)

I think, in order to be called a security issue, a bug has to have some actual security implications. It has to put the system or its data at risk of being stolen, misused, or otherwise manipulated, or it has to prevent the use of the system on an ongoing basis or such that an attacker could trigger a crash on demand.

None of those things are true here, and it worries me that by using a broader definition of a security issue than this, just about anything could be argued to be a security issue. That dilutes the public perception of security problems, and makes it harder for both the problems themselves and the people who research them to be taken seriously.

I really appreciated your post.
It's one of kind for the accuracy and the deepness.
And, above all, it finally debunk the equation (or "logic" chain):
bug that can trigger a kernel panic => bug that can lead to code execution => zero-day exploit.

George Ou should really read this very carefully.

You refer to "address" in the handler fault code, but it's unclear what the "address" argument has to do with much of anything... at least in the code that you show. Could you be more clear the connection to "address" and the bug. It seems like the bug is that the loop is just wrong (assuming the multi-block buffers). If that is indeed the bug, I don't see a particularly more vexing attack vector at the moment, but I've only looked at the code for a couple of minutes. I wouldn't at all be surprsied if there weren't something quite insidious here though.

The bug is that the code assumes that a multi-segment IOMemoryDescriptor represents contiguous address space in the target process, but in this case the target process is the kernel, so it's discontiguous. As a result, address doesn’t point at the start of a mapped block of memory in addressMap, so passing it to vm_fault() triggers a kernel panic.

An interesting analysis. I am now in a better position to understand (and laugh at) FAQ item 2 on the Month of Apple Bugs page. :-)

Yes, and FAQ items 7 and 9 are also aimed at me or at comments I’ve written (at least, I think they are).

I don’t know why, but LMH seems to want to take every opportunity to insult me. I can’t say it exactly promotes the kind of professional image that he seems to want to project.

MoAB (superficially) looks to be better researched thus far than MoKB was, though I’m not quite sure what a bug in VLC is doing on the Month of Apple Bugs. But hey, it’s LMH’s month of bugs, not mine. I still think he should be reporting these to the vendor and allowing time for them to be fixed. Not that I ever expected him to do that, of course.

Hi Folks

I am working on a storage driver.Now i want to know about the DMG in its mounted state.We all know that when a DMG file mounts it is treated like disk or like a virtual disk it takes an entry in the /dev folder as rdisk1 if rdisk0 is your primary harddisk.

Now if u try to read the partition map of this virtual disk by using pdisk the output would be

/dev/rdisk1 map block size=512
#: type name length base ( size )
1: Apple_partition_map Apple 63 @ 1
2: Apple_HFS disk image 2097072 @ 64 (1024.0M)
3: Apple_Free 0+@ 2097136

These offsets which are displayed are relative to /disk1 but /disk1 is a virtual disk since its physical existaence is on /disk0 hence there must me some offset to these which is relative to disk0.Now my doubt is that how can we find out this offset.

Hi Folks

I am working on a storage driver.Now i want to know about the DMG in its mounted state.We all know that when a DMG file mounts it is treated like disk or like a virtual disk it takes an entry in the /dev folder as rdisk1 if rdisk0 is your primary harddisk.

Now if u try to read the partition map of this virtual disk by using pdisk the output would be

/dev/rdisk1 map block size=512
#: type name length base ( size )
1: Apple_partition_map Apple 63 @ 1
2: Apple_HFS disk image 2097072 @ 64 (1024.0M)
3: Apple_Free 0+@ 2097136

These offsets which are displayed are relative to /disk1 but /disk1 is a virtual disk since its physical existaence is on /disk0 hence there must me some offset to these which is relative to disk0.Now my doubt is that how can we find out this offset.

Although late to the party, I want to express my appreciation and admiration for your analysis. FWIW, I quite agree that characterizing a user-activated kernel panic as a security breach strays well outside the conventional conception of the latter. I suppose that in some context or another the disabling of a computer, however transient, might be regarded as a security threat, but that such effect cannot be remotely deployed would greatly limit the scale of mischief resulting. This seems no more than an annoying bug of the family Kernelpanic.

Post a comment

If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thank-you for your patience.

(Your e-mail address will not be displayed or included in any pages served on this site; nor will you get any spam as a result.)

A live preview of your comment will be displayed below. It should refresh automatically when you stop typing, but if not then the “Preview” button above will update it.

Live Comment Preview