Closed Bug 547143 (CVE-2010-0164) Opened 15 years ago Closed 15 years ago

libpr0n imgContainer Bits-Per-Pixel Change Remote Code Execution Vulnerability (ZDI-CAN-693)

Categories

(Core :: Graphics: ImageLib, defect)

1.9.1 Branch
x86
Windows XP
defect
Not set
critical

Tracking

()

RESOLVED FIXED
Tracking Status
blocking2.0 --- beta1+
blocking1.9.2 --- .2+
status1.9.2 --- .2-fixed
status1.9.1 --- unaffected

People

(Reporter: dveditz, Assigned: joe)

Details

(Keywords: verified1.9.2, Whiteboard: [sg:moderate])

Attachments

(1 file, 1 obsolete file)

Attached file zdi's PoC
ZDI-CAN-693: Mozilla Firefox libpr0n imgContainer Bits-Per-Pixel Change Remote Code Execution Vulnerability -- ABSTRACT ------------------------------------------------------------ TippingPoint has identified a vulnerability affecting the following products: Mozilla Firefox 3.5.x -- VULNERABILITY DETAILS ----------------------------------------------- This vulnerability allows remote attackers to execute arbitrary code on vulnerable installations of Mozilla Firefox. User interaction is required to exploit this vulnerability in that the target must visit a malicious page or open a malicious file. The specific flaw exists within the libpr0n library which is responsible for handling image caching and animation and is due to the way the application handle animations received from the server via the multipart/x-mixed-replace mime. During a case where the bits-per-pixel changes, the application will free a pointer and then can be made to reuse the freed pointer later. This can lead to code execution under the context of the application. Mozilla utilizes libpr0n as it's image renderer. libpr0n is responsible for providing image encoding/decoding along with image caching and canvas reuse to the application. This library includes support for multipart-x-mixed-replace which can be utilized for streaming images or animation from a server over the http protocol. The vulnerability exists due to the library's ability to reuse a frame for animations where a new frame has a different bpp than the old frame. This leads to a pointer that gets freed, which can then be used in order to further an attacker into control of the process. The actual dangling pointer is located in the following code in the 'frame' variable. It occurs when the previous frame can not be reused for the next frame that gets added. The case can be hit when frame->GetFormat() != aformat. modules/libpr0n/src/imgContainer.cpp:539 imgFrame *frame = GetImgFrame(aFrameNum); if (!frame) return InternalAddFrame(aFrameNum, aX, aY, aWidth, aHeight, aFormat, /* aPaletteDepth = */ 0, imageData, imageLength, /* aPaletteData = */ nsnull, /* aPaletteLength = */ nsnull); // See if we can re-use the frame that already exists. nsIntRect rect = frame->GetRect(); if (rect.x != aX || rect.y != aY || rect.width != aWidth || rect.height != aHeight || frame->GetFormat() != aFormat) { delete frame; // XXX: pointer we want control of return InternalAddFrame(aFrameNum, aX, aY, aWidth, aHeight, aFormat, /* aPaletteDepth = */ 0, imageData, imageLength, /* aPaletteData = */ nsnull, /* aPaletteLength = */ nsnull); } Upon parsing an image, the decoder for the image will be initialized with the object responsible for loading. This code will later call AppendFrame to append an image to the current list. /* void init (in imgILoad aLoad); */ NS_IMETHODIMP nsJPEGDecoder::Init(imgILoad *aLoad) { mImageLoad = aLoad; mObserver = do_QueryInterface(aLoad); /* We set up the normal JPEG error routines, then override error_exit. */ mInfo.err = jpeg_std_error(&mErr.pub); /* mInfo.err = jpeg_std_error(&mErr.pub); */ mErr.pub.error_exit = my_error_exit; /* Establish the setjmp return context for my_error_exit to use. */ if (setjmp(mErr.setjmp_buffer)) { /* If we get here, the JPEG code has signaled an error. * We need to clean up the JPEG object, close the input file, and return. */ return NS_ERROR_FAILURE; } AppendFrame is relaly just a wrapper around InternalAddFrame. modules/libpr0n/src/imgContainer.cpp:479 NS_IMETHODIMP imgContainer::AppendFrame(PRInt32 aX, PRInt32 aY, PRInt32 aWidth, PRInt32 aHeight, gfxASurface::gfxImageFormat aFormat, PRUint8 **imageData, PRUint32 *imageLength) { NS_ENSURE_ARG_POINTER(imageData); NS_ENSURE_ARG_POINTER(imageLength); return InternalAddFrame(mNumFrames, aX, aY, aWidth, aHeight, aFormat, /* aPaletteDepth = */ 0, imageData, imageLength, /* aPaletteData = */ nsnull, /* aPaletteLength = */ nsnull); } InternalAddFrame will check if the frame length is 2, and then call StartAnimation as there's more than 1 frame to flip through. modules/libpr0n/src/imgContainer.cpp:421 nsresult imgContainer::InternalAddFrame(PRUint32 framenum, PRInt32 aX, PRInt32 aY, PRInt32 aWidth, PRInt32 aHeight, gfxASurface::gfxImageFormat aFormat, PRUint8 aPaletteDepth, PRUint8 **imageData, PRUint32 *imageLength, PRUint32 **paletteData, PRUint32 *paletteLength) { if (framenum > PRUint32(mNumFrames)) return NS_ERROR_INVALID_ARG; nsAutoPtr<imgFrame> frame(new imgFrame()); NS_ENSURE_TRUE(frame, NS_ERROR_OUT_OF_MEMORY); nsresult rv = frame->Init(aX, aY, aWidth, aHeight, aFormat, aPaletteDepth); NS_ENSURE_SUCCESS(rv, rv); if (mFrames.Length() == 0) { return InternalAddFrameHelper(framenum, frame.forget(), imageData, imageLength, paletteData, paletteLength); } ... // If this is our second frame (We've just added our second frame above), // count should now be 2. This must be called after we AppendObject // because StartAnimation checks for > 1 frames if (mFrames.Length() == 2) StartAnimation(); // XXX return rv; } StartAnimation will then fetch the current image frame, and then start a timeout for animating. modules/libpr0n/src/imgContainer.cpp:699 NS_IMETHODIMP imgContainer::StartAnimation() { if (mAnimationMode == kDontAnimMode || (mAnim && (mAnim->timer || mAnim->animating))) return NS_OK; if (mNumFrames > 1) { if (!ensureAnimExists()) return NS_ERROR_OUT_OF_MEMORY; // Default timeout to 100: the timer notify code will do the right // thing, so just get that started. PRInt32 timeout = 100; imgFrame *currentFrame = GetCurrentImgFrame(); // XXX: calls ::GetImgFrame if (currentFrame) { timeout = currentFrame->GetTimeout(); if (timeout <= 0) // -1 means display this frame forever return NS_OK; } Once reaching GetImgFrame, RestoreDiscardedData will get called, this will later called ReloadImages. modules/libpr0n/src/imgContainer.cpp:192 imgFrame *imgContainer::GetImgFrame(PRUint32 framenum) { nsresult rv = RestoreDiscardedData(); // XXX NS_ENSURE_SUCCESS(rv, nsnull); modules/libpr0n/src/imgContainer.cpp:933 NS_IMETHODIMP imgContainer::Notify(nsITimer *timer) { // Note that as long as the image is animated, it will not be discarded, // so this should never happen... nsresult rv = RestoreDiscardedData(); // XXX modules/libpr0n/src/imgContainer.cpp:1616 nsresult imgContainer::RestoreDiscardedData(void) { // mRestoreDataDone = PR_TRUE means that we want to timeout and then discard the image frames // So, we only need to restore, if mRestoreDataDone is true, and then only when the frames are discarded... if (!mRestoreDataDone) return NS_OK; // Reset timer, as the frames are accessed nsresult rv = ResetDiscardTimer(); NS_ENSURE_SUCCESS(rv, rv); if (!mDiscarded) return NS_OK; int num_expected_frames = mNumFrames; // To prevent that ReloadImages is called multiple times, reset the flag before reloading mDiscarded = PR_FALSE; rv = ReloadImages(); // XXX Upon reaching ReloadImages, the library will ask the image decoder to initialize another image, which it will then write data to via the WriteFrom method of the decoder. modules/libpr0n/src/imgContainer.cpp:1830 nsresult result = decoder->Init(loader); if (NS_FAILED(result)) { PR_LOG(gCompressedImageAccountingLog, PR_LOG_WARNING, ("CompressedImageAccounting: imgContainer::ReloadImages() image container %p " "failed to initialize the decoder (%s)", this, mDiscardableMimeType.get())); return result; } ... // |WriteFrom()| may fail if the original data is broken. PRUint32 written; (void)decoder->WriteFrom(stream, mRestoreData.Length(), &written); // XXX modules/libpr0n/decoders/jpeg/nsJPEGDecoder.cpp:259 /* unsigned long writeFrom (in nsIInputStream inStr, in unsigned long count); */ NS_IMETHODIMP nsJPEGDecoder::WriteFrom(nsIInputStream *inStr, PRUint32 count, PRUint32 *writeCount) { NS_ENSURE_ARG_POINTER(inStr); NS_ENSURE_ARG_POINTER(writeCount); /* necko doesn't propagate the errors from ReadDataOut */ nsresult rv = inStr->ReadSegments(ReadDataOut, this, count, writeCount); // XXX modules/libpr0n/decoders/jpeg/nsJPEGDecoder.cpp:240 static NS_METHOD ReadDataOut(nsIInputStream* in, void* closure, const char* fromRawSegment, PRUint32 toOffset, PRUint32 count, PRUint32 *writeCount) { nsJPEGDecoder *decoder = static_cast<nsJPEGDecoder*>(closure); nsresult rv = decoder->ProcessData(fromRawSegment, count, writeCount); //XXX modules/libpr0n/decoders/jpeg/nsJPEGDecoder.cpp:276 nsresult nsJPEGDecoder::ProcessData(const char *data, PRUint32 count, PRUint32 *writeCount) { LOG_SCOPE_WITH_PARAM(gJPEGlog, "nsJPEGDecoder::ProcessData", "count", count); Upon parsing all useful data for the jpeg image, the library will then call EnsureCleanFrame, to allocate a frame at a particular index. modules/libpr0n/decoders/jpeg/nsJPEGDecoder.cpp:458 /* verify that the width and height of the image are the same as * the container we're about to put things in to. * XXX it might not matter maybe we should just resize the image. */ PRInt32 width, height; mImage->GetWidth(&width); mImage->GetHeight(&height); if (width == 0 && height == 0) { mImage->Init(mInfo.image_width, mInfo.image_height, mObserver); } else if ((width != (PRInt32)mInfo.image_width) || (height != (PRInt32)mInfo.image_height)) { mState = JPEG_ERROR; return NS_ERROR_UNEXPECTED; } mImage->Init(mInfo.image_width, mInfo.image_height, mObserver); mObserver->OnStartContainer(nsnull, mImage); // Use EnsureCleanFrame so we don't create a new frame if we're being // reused for e.g. multipart/x-replace PRUint32 imagelength; if (NS_FAILED(mImage->EnsureCleanFrame(0, 0, 0, mInfo.image_width, mInfo.image_height, gfxASurface::ImageFormatRGB24, &mImageData, &imagelength))) { // XXX EnsureCleanFrame will then get called. This will allocate a new frame via InternalAddFrame at the specified Index..Unless the format is different. modules/libpr0n/src/imgContainer.cpp:515 /* [noscript] void ensureCleanFrame(in unsigned long aFramenum, in PRInt32 aX, in PRInt32 aY, in PRInt32 aWidth, in PRInt32 aHeight, in gfxImageFormat aFormat, [array, size_is(imageLength)] out PRUint8 imageData, out unsigned long imageLength); */ NS_IMETHODIMP imgContainer::EnsureCleanFrame(PRUint32 aFrameNum, PRInt32 aX, PRInt32 aY, PRInt32 aWidth, PRInt32 aHeight, gfxASurface::gfxImageFormat aFormat, PRUint8 **imageData, PRUint32 *imageLength) { NS_ENSURE_ARG_POINTER(imageData); NS_ENSURE_ARG_POINTER(imageLength); if (aFrameNum > PRUint32(mNumFrames)) return NS_ERROR_INVALID_ARG; // Adding a frame that doesn't already exist. if (aFrameNum == PRUint32(mNumFrames)) return InternalAddFrame(aFrameNum, aX, aY, aWidth, aHeight, aFormat, /* aPaletteDepth = */ 0, imageData, imageLength, /* aPaletteData = */ nsnull, /* aPaletteLength = */ nsnull); imgFrame *frame = GetImgFrame(aFrameNum); if (!frame) return InternalAddFrame(aFrameNum, aX, aY, aWidth, aHeight, aFormat, /* aPaletteDepth = */ 0, imageData, imageLength, /* aPaletteData = */ nsnull, /* aPaletteLength = */ nsnull); // See if we can re-use the frame that already exists. nsIntRect rect = frame->GetRect(); if (rect.x != aX || rect.y != aY || rect.width != aWidth || rect.height != aHeight || frame->GetFormat() != aFormat) { delete frame; return InternalAddFrame(aFrameNum, aX, aY, aWidth, aHeight, aFormat, /* aPaletteDepth = */ 0, imageData, imageLength, /* aPaletteData = */ nsnull, /* aPaletteLength = */ nsnull); } Due to the format being different, the deleted frame will still remain in the list of containers at which point an attacker will need to figure out how to get the frame to be utilized in the current execution path. One example would be upon image container destruction. modules/libpr0n/src/imgContainer.cpp:90 imgContainer::~imgContainer() { if (mAnim) delete mAnim; for (unsigned int i = 0; i < mFrames.Length(); ++i) delete mFrames[i]; Version(s) tested: Mozilla Firefox 3.6 Platform(s) tested: Windows XP SP3 -- CREDIT -------------------------------------------------------------- This vulnerability was discovered by: * regenrecht
Whiteboard: [sg:critical?]
Blocking for the next security updates. Probably needs to block 3.0.19 as well since that will probably be the last release from that line.
blocking1.9.1: ? → .9+
blocking1.9.2: ? → .2+
Flags: wanted1.9.0.x?
Flags: blocking1.9.0.19?
I am 99% sure that there is no bug here. InternalAddFrame calls InternalAddFrameHelper which changes the pointer held in mFrames, so we don't hold on to a deleted pointer. This is further borne out by my inability to reproduce any problems in Valgrind or by running the PoC myself. However, InternalAddFrame *does* have failure states, which, though unlikely, could conceivably leave us with a dangling pointer. I can also conceive of cases where we could be left with a leak. I'll whip up a patch to fix those cases. Also, this doesn't affect 3.0, because imagelib code there is very different there.
Flags: wanted1.9.0.x?
Flags: blocking1.9.0.19?
Whoops, I had forgotten that this code wasn't in 3.5 *either*.
blocking1.9.1: .9+ → ---
(In reply to comment #0) > TippingPoint has identified a vulnerability affecting the following > products: > > Mozilla Firefox 3.5.x ... > Version(s) tested: Mozilla Firefox 3.6 ~o ~o One of these things is not like the others, One of these things just doesn't belong, Can you tell which thing is not like the others By the time I finish my song? ~o ~o
There are two cases where we could do the wrong thing here. Both of them are purely theoretical, because they can occur only when we call EnsureCleanFrame() with aFrameNum != 0, which we never do because EnsureCleanFrame() is only called from the JPEG decoder. The first is bad. Before this patch, if we call EnsureCleanFrame() on an animated image, GetImgFrame() will return mAnim->compositingFrame, which we delete but DO NOT null out. We should never delete mAnim->compositingFrame on our own, because it's purely for display, not storage. The second is a leak. In the above case, since we delete mAnim->compositingFrame and not mFrames[aFrameNum], InternalAddFrameHelper will unconditionally overwrite mFrames[aFrameNum], and since mFrames is just an nsTArray, overwriting does not imply deletion. This patch fixes both of these issues.
Attachment #427850 - Flags: superreview?(vladimir)
Attachment #427850 - Flags: review?(jmuizelaar)
Attachment #427850 - Flags: review?(bobbyholley+bmo)
This patch also fixes the case where InternalAddFrame or InternalAddFrameHelper returns before it sets mFrame[index] - for example, due to new returning null.
Comment on attachment 427850 [details] [diff] [review] Add DeleteImgFrame, which deletes and nulls out a frame in mFrames the condition is poor
Attachment #427850 - Flags: review?(jmuizelaar) → review-
Attachment #427850 - Attachment is obsolete: true
Attachment #428868 - Flags: superreview?(vladimir)
Attachment #428868 - Flags: review?(jmuizelaar)
Attachment #428868 - Flags: review?(bobbyholley+bmo)
Attachment #427850 - Flags: superreview?(vladimir)
Attachment #427850 - Flags: review?(bobbyholley+bmo)
ZDI will get back to us about our inability to see bad stuff from the PoC, might take a week or two they said which I interpret as meaning they have to get in touch with the original reporter.
Comment on attachment 428868 [details] [diff] [review] the condition is better I talked with Joe a bunch about this in person and it looks ok to me. We should get test for x-mulitpart-replace jpegs and Joe will file a bug about cleaning this code up so we're less likely to get ourselves in trouble in the future.
Attachment #428868 - Flags: review?(jmuizelaar) → review+
Attachment #428868 - Flags: superreview?(vladimir) → superreview?(roc)
Attachment #428868 - Flags: superreview?(roc) → superreview+
Does this still need review from bholley or do should it be landed on trunk and put up for branch approval?
For all my bugs, you can ignore bholley review requests. I r? him so that he knows what's going on for when he returns from school.
OK, so is this ready for approval?
Whiteboard: [sg:critical?] → [sg:critical?][needs trunk landing, branch approval requests]
I'm working on a multipart/x-mixed-replace JPEG test right now. Once that's ready, I'll check it in to m-c, and then I'll request branch approval.
OK, tap me on the shoulder when you do. This blocks our code freeze tomorrow.
Attachment #428868 - Flags: review?(bobbyholley+bmo) → feedback?(bobbyholley+bmo)
Status: NEW → RESOLVED
Closed: 15 years ago
Resolution: --- → FIXED
Attachment #428868 - Flags: approval1.9.2.2?
Attachment #428868 - Flags: approval1.9.1.9?
Attachment #428868 - Flags: approval1.9.0.19?
Whiteboard: [sg:critical?][needs trunk landing, branch approval requests] → [sg:critical?][needs branch approval]
Comment on attachment 428868 [details] [diff] [review] the condition is better Whoops; 3.0 is unaffected by this.
Attachment #428868 - Flags: approval1.9.0.19?
(In reply to comment #16) > http://hg.mozilla.org/mozilla-central/rev/81e60d14dd49 for the tests; Why are we committing tests for a security bug that specifically mention important details such as the multipart/x-mixed-replace mime-type? We usually hold off on committing tests for security bugs until we've shipped the fix to users.
Attachment #428868 - Flags: approval1.9.2.2?
Attachment #428868 - Flags: approval1.9.2.2+
Attachment #428868 - Flags: approval1.9.1.9?
Attachment #428868 - Flags: approval1.9.1.9+
Comment on attachment 428868 [details] [diff] [review] the condition is better a=beltzner for branches
Whiteboard: [sg:critical?][needs branch approval] → [sg:critical?]
Attachment #428868 - Flags: approval1.9.1.9+
Comment on attachment 428868 [details] [diff] [review] the condition is better This doesn't need to get into 1.9.1.
blocking2.0: ? → beta1
verified FIXED on builds (using the PoC attached to this bug): Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 and Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2
Status: RESOLVED → VERIFIED
Changing this to resolved fixed and adding verified1.9.2 keyword since this was only verified for 1.9.2.
Status: VERIFIED → RESOLVED
Closed: 15 years ago15 years ago
Keywords: verified1.9.2
Alias: CVE-2010-0165
Alias: CVE-2010-0165 → CVE-2010-0164
security received this from ZDI last week: Because it's a bug related to reusing a pointer that has been previously freed and is very likely to contain the same data as before, one would have to get an allocation to happen within that code path or hope the OS unmaps the page between the free and the reuse, so it's kind of sketchy to trigger with that poc. A crash in most cases will happen after the fact due to it being possibly minimal heap corruption, so to catch it one could breakpoint at the location the pointer is expected used and check to see if windows has marked any of those pointers as being previously freed. The analyst hasn't written a more reliable poc yet, however, if you reload the firefox page a few times after each frame has been rendered (1 seconds per frame iirc), the crash should reveal itself upon destruction of the surface that everything is being rendered on. It took the analyst 3 wait/reloads before anything happened. This was also done with fullpageheap enabled. Users don't run with fullpageheap or other freaky gflags settings. That coupled with the lack of valgrind warnings (comment 2) seems to lower the actual user-facing risk considerably.
Whiteboard: [sg:critical?] → [sg:moderate]
Group: core-security
Attachment #428868 - Flags: feedback?(bobbyholley+bmo) → feedback+
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: