According to Adobe Security Bulletin APSB16-14, this vulnerability was reported to Adobe by Pier-Luc Maltais of COSIG, and shortly after the release of this bulletin, a proof-of-concept exploit was published on Packet Storm. This PoC is in the form of a PDF document that crashes the Reader when opened. (By the way, it was apparently created with single-byte raw file fuzzing; we were able to compare it to the original PDF file we found on the Internet.)
This was enough for us to start analyzing the vulnerability. To keep this blog post reasonably short, we will omit the vulnerability analysis process, which is nicely described in this detailed post by Fortinet's
So let's fast-forward to the point where we know exactly what the flaw is. We have a use-after-free issue, as shown in the vulnerable code below.
|Vulnerable code: after freeing an object, reference to this object is not removed|
The above image shows three relevant code blocks somewhere in Acrobat Reader's implementation of deflate operation on an XObject (an element in a PDF document), where the code is obviously doing some clean-up by freeing an object it no longer needs. The pointer to this object is retrieved in register ecx, then the test ecx, ecx instruction effectively checks if the pointer is NULL, and this determines whether the execution will continue on the red branch (ecx not NULL) or the green branch (ecx is NULL). In the red branch, pointer to object's free function is copied from object's vtable (free is the first function in vtable in this case) to register eax, then a call to free is made.
The relevant code can be represented by this simple pseudo-code:
Note that after c is deleted, its reference (pointer to it) in b->some_objectC is not removed. And sure enough, at some point later in the execution, b->some_objectC is accessed again. But this time c points to an already deallocated memory address, and access violation occurs.
Now let's see how we would fix the above pseudo-code. It's trivial, we only need to write NULL to b->some_objectC after deleting c:
So we have a pseudo-code patch, now let's translate it to machine code. First look at the vulnerable code in a "text" view:
The patch code must come after call dword ptr [eax], which is a call to object's free. We need the address where the pointer to the object is stored, so we can put a NULL there.This address was [edi+3Ch]+C8h just a few instructions earlier, and we know edi is not corrupted by the call to free as it is still being used later in the code without being assigned a new value. (This is easy to check with IDA as it highlights all uses of a register when you click on one.)
When creating a micropatch, we need to be careful about a few things:
- Injecting a micropatch works by overwriting one or more existing instructions with a 5-byte JMP instruction to some nearby-allocated memory block, where the overwritten instructions are added at the end of our patch code, followed by a JMP back to the place in the original code right after them. Of the overwritten instructions, the first one may be a destination point of some CALL or JMP, but others may not be, as that would certainly lead to an error in execution. Fortunately, IDA makes it really easy to verify this with its graphical representation of code.
- We should not cause any unwanted side-effects, like corrupting values of registers or stack-based variables that subsequent code may still be using. When in doubt, we should PUSH/POP the registers we use or otherwise preserve them.
- We should not merely assume that a register value will survive a function call (e.g., that edi will survive a call to free in our case) - we must prove it by either reviewing this function (and all functions it calls etc.) and showing that the value is reliably preserved, or showing that the original code after the function call relies on the fact that the register hasn't changed.
- Our code should be as easy as possible to review. On one hand, this means using as few instructions as possible, and on the other hand it means making it easy for a reviewer to verify our claims upon which the patch code is constructed (e.g., that "patch code can safely change eax because...", or "at this point, edx will always point to..."). Sometimes, these goals are in contradiction: for example, in our patch code below, we could simply use eax instead of edi and avoid having to PUSH/POP edi, but it would make code review harder.
First, let's find a suitable location for our patch. There's an obvious candidate immediately after call dword ptr [eax] at location 6056E29B (AcroRd32.dll + 0x56E29B). The instructions there are:
Remember, we need 5 bytes for our "JMP to patch", so we'll have to relocate all these three instructions to the space right after our patch code. Note that the third instruction is a relative CALL, which means that its offset has to be recalculated when it is moved. (Don't worry, 0patch agent does this automatically.)
As for the patch code, let's try this:
This code just uses a single register (edi), making it trivial for a reviewer to see that it has no side effects while writing a NULL where the original code forgot to do it. As already noted, we could make it even shorter if we used eax instead of edi, as we wouldn't have to preserve eax's value. However, proving that we don't have to preserve eax's value would require reviewing quite some additional code after our patch location, including a function that is called there. This would consume patch developer's as well as patch reviewer's time, and increase the risk of error for both of them.
One last check: Can we be absolutely sure that edi will point to the same object a at address 6056E29B as it did a couple of instructions back at address 6056E289? There are two execution paths between these two addresses (as IDA nicely shows). One goes through the aforementioned green branch, and the other goes through the red branch. The "green branch" path is easy to prove: there is simply no instruction there that would change edi, so it must still have the same value at the end. The "red branch" path, however, includes a call to free from object's vtable. Proving that this function does not alter edi could be a daunting task, especially as IDA can't show you where the code behind this dynamically-set free is. (In fact, this function could be different for various types of objects being processed here, and only the original developers of this code know what objects that could possibly be.) So we'll use another approach: we look at the code after our patch location to see whether it relies on edi being the same as before. And in fact, we can see that it does (but that's beyond the scope of this article).
We have all we need, but how do we turn this into an actual 0patch? We use 0patch Factory. 0patch Factory is a simple tool we wrote, which takes a 0patch source file as input, and turns it into a 0patch that can be immediately applied on any computer with our 0patch Agent. Let's take a look at a 0patch source file for this micropatch:
Some explanation is probably in order:
- Each 0patch consists of one or more patchlets; a patchlet is a set of machine code instructions that are to be injected at a specific offset from the module's base.
- MODULE_PATH is full path to the module (binary file) we're patching. 0patch Factory needs this for two reasons: (1) calculating the module's hash, as the patch should only be applied to this exact binary, and (2) extracting the original bytes from all locations where we're injecting our patchlets, as any patchlet should only be applied when the bytes found in the "live" module on its injection location exactly match these original bytes.
- PATCH_ID is a unique ID of the patch in the 0patch database. It can be arbitrary during patch development, but a patch will get a suitable unique ID before it gets signed and uploaded to the server.
- PATCH_FORMAT_VER is the version of the format in which the patch is written. 2 is the only supported value at this time.
- VULN_ID is the ID of the vulnerability (in the 0patch database) fixed by this patch.
- PLATFORM is either Win32 or Win64 at this time, specifying the format of machine code in this patch's patchlets. Acrobat Reader DC we're patching here is a 32-bit application.
- patchlet_start and patchlet_end mark beginning and end of an individual patchlet.
- PATCHLET_ID is a unique ID of a patchlet inside a patch, usually iterative from 1 upwards.
- PATCHLET_TYPE is the type of the patchlet, currently the only supported type is type 2 (which means "code injection patchlet"). In the future, we'll support other types of patchlets.
- PATCHLET_OFFSET is the offset from the module's base where the patchlet code should be injected. In our case, this offset is 0x56e29b.
- N_ORIGINALBYTES is the number of original bytes that we should check when applying the patchlet. The default is 5, which means all 5 bytes that we will overwrite with the "JMP to patch" instruction.
- code_start and code_end mark beginning and end of patchlet's code, which will be compiled to binary code by 0patch Factory.
We store this 0patch source file in ZP-245.0pp and build the patch using 0patch Factory on a computer with 0patch Agent installed:
|0patch Factory makes it easy to build a micropatch: just |
right-click a 0pp file and select "Build Patch"
This both compiles and installs the new 0patch, making it ready for immediate application to a running vulnerable Acrobat Reader. Without further ado, let's open POC_ZP-245.pdf to test our patch.
|0patch Agent notifies you that patch 245 just|
got applied to the running Acrobat Reader
Acrobat Reader gets launched, the "Patch Applied" pop-up is shown notifying us that patch 245 was applied to AcroRd32.exe, and... no crash. The document seems to be displayed correctly. We scroll down and get the "A drawing error occurred." error message from the Reader, but this is expected, as there is still a corrupt graphical element in the PDF document, and Reader correctly notifies us about that. (Latest version of Reader also shows this message.) We can continue to use Acrobat Reader as if nothing has happened.
There you go, we've just created a simple micropatch for a remotely executable memory corruption vulnerability. It consists of just four machine code instructions and can be instantaneously applied to a running Acrobat Reader while a user is reading some document with it.
Should you like to experiment with this micropatch on your own, create a free 0patch account and download 0patch Agent for Windows if you haven't already. (If you have, than patch ZP-245 has probably already been downloaded by your agent and is waiting for you to run the vulnerable Acrobat Reader.)
|Patch 245 is now in the "Patches" list in the 0patch Console, where you can enable or disable it|
Once you have 0patch Agent installed and registered, install Acrobat Reader DC 15.010.20060 and use it to open POC_ZP-245.pdf, first with patch 245 enabled, and then disabled. Notice the difference? ;)
If you'd like to write your own 0patches, and we sure hope many of you would, give us some time to polish our 0patch Factory for you. It's nothing fancy, but you deserve a decently tested tool with useful instructions. In the mean time, please send us an email at email@example.com to express your interest. We look forward to our collaboration in changing the way vulnerabilities are getting fixed.