Over the past few months, Adobe has patched several remote code execution bugs in Adobe Acrobat and Reader that were reported by researcher Mark Vincent Yason (@MarkYason) through our program. Two of these bugs, in particular, CVE-2021-28632 and CVE-2021-39840, are related Use-After-Free bugs even though they were patched months apart. Mark has graciously provided this detailed write-up of these vulnerabilities and their root cause.
This blog post describes two Adobe Reader use-after-free vulnerabilities that I submitted to ZDI: One from the June 2021 patch (CVE-2021-28632) and one from the September 2021 patch (CVE-2021-39840). An interesting aspect about these two bugs is that they are related – the first bug was discovered via fuzzing and the second bug was discovered by reverse engineering and then bypassing the patch for the first bug.
CVE-2021-28632: Understanding Field Locks
One early morning while doing my routine crash analysis, one Adobe Reader crash caught my attention:
After a couple of hours minimizing and cleaning up the fuzzer-generated PDF file, the resulting simplified proof-of-concept (PoC) was as follows:
PDF portion (important parts only):
The crash involved a use-after-free of CPDField
objects. CPDField
objects are internal AcroForm.api
C++ objects that represent text fields, button fields, etc. in interactive forms.
In the PDF portion above, two CPDField
objects are created to represent the two text fields named fieldParent
and fieldChild
. Note that the created objects have the type CTextField
, a subclass of CPDField
, which is used for text fields. To simplify the discussion, they will be referred to as CPDField
objects.
An important component for triggering the bug is that fieldChild
should be a descendant of fieldParent
by specifying it in the /Kids
key of the fieldParent
PDF object dictionary (see [A]
above) as documented in the PDF file format specification:
Another important concept relating to the bug is that to prevent a CPDField
object from being freed while it is in use, an internal property named LockFieldProp
is used. Internal properties of CPDField
objects are stored via a C++ map member variable.
If LockFieldProp
is not zero, it means that the CPDField
object is locked and can't be freed; if it is zero or is not set, it means that the CPDField
object is unlocked and can be freed. Below is the visual representation of the two CPDField
objects in the PoC before the field locking code (discussed later) is called: fieldParent
is unlocked (LockFieldProp
is 0
) and is in green, and fieldChild
is also unlocked (LockFieldProp
is not set) and is also in green:
On the JavaScript portion of the PoC, the code sets up a JavaScript callback so that when the “Format” event is triggered for fieldParent
, a custom JavaScript function callback()
will be executed [2]
. The JavaScript code then triggers a “Format” event by setting the textSize
property of fieldParent
[3]
. Internally, this executes the textSize
property setter of JavaScript Field
objects in AcroForm.api
.
One of the first actions of the textSize
property setter in AcroForm.api
is to call the following field locking code against fieldParent
:
The above code locks the CPDField
object passed to it by setting its LockFieldProp
property to 1
[AA]
.
After executing the field locking code, the lock state of fieldParent
(locked: in red) and fieldChild
(unlocked: in green) are as follows:
Note that in the later versions of Adobe Reader, the value of LockFieldProp
is a pointer to a counter instead of being set with the value 1
or 0
.
Next, the textSize
property setter in AcroForm.api
calls the following recursive CPDField
method where the use-after-free occurs:
On the first call to the above method, the this
pointer points to the locked fieldParent
CPDField
object. Because it has no associated widget [aa]
, the method performs a recursive call [cc]
with the this
pointer pointing to each of fieldParent
's children [bb]
.
Therefore, on the second call to the above method, the this
pointer points to the fieldChild
CPDField
object, and since it has an associated widget (see [B]
in the PDF portion of the PoC), a notification will be triggered [dd]
that results in the custom JavaScript callback()
function to be executed. As shown in the previous illustration, the locking code only locked fieldParent
while fieldChild
is left unlocked. Because fieldChild
is unlocked, the removeField("fieldChild")
call in the custom JavaScript callback()
function (see [1]
in the JavaScript portion of the PoC) succeeds in freeing the fieldChild
CPDField
object. This leads to the this
pointer in the recursive method to become a dangling pointer after the call in [dd]
. The dangling this
pointer is later dereferenced resulting in the crash.
This first vulnerability was patched in June 2021 by Adobe and assigned CVE-2021-28632.
CVE-2021-39840: Reversing Patch and Bypassing Locks
I was curious to see how Adobe patched CVE-2021-28632, so after the patch was released, I decided to look at the updated AcroForm.api
.
Upon reversing the updated field locking code, I noticed an addition of a call to a method that locks the passed field’s immediate descendants:
With the added code, both fieldParent
and fieldChild
will be locked and the PoC for the first bug will fail in freeing fieldChild
:
While assessing the updated code and thinking, I arrived at a thought: since the locking code only additionally locks the immediate descendants of the field, what if the field has a non-immediate descendant?... a grandchild field! I quickly modified the PoC for CVE-2021-28632 to the following:
PDF portion (important parts only):
And then loaded the updated PoC in Adobe Reader under a debugger, hit go... and crash!
The patch was bypassed, and Adobe Reader crashed at the same location in the previously discussed recursive method where the use-after-free originally occurred.
Upon further analysis, I confirmed that the illustration below was the state of the field locks when the recursive method was called. Notice that fieldGrandChild
is unlocked, and therefore, can be freed:
The recursive CPDField
method started with the this
pointer pointing to fieldParent
, and then called itself with the this
pointer pointing to fieldChild
, and then called itself again with the this
pointer pointing to fieldGrandChild
. Since fieldGrandChild
has an attached widget, the JavaScript callback()
function that frees fieldGrandChild
was executed, effectively making the this
pointer a dangling pointer.
This second vulnerability was patched in September 2021 by Adobe and assigned CVE-2021-39840.
Controlling Field Objects
Control of the freed CPDField
object is straightforward via JavaScript: after the CPDField
object is freed via the removeField()
call, the JavaScript code can spray the heap with similarly sized data or an object to replace the contents of the freed CPDField
object.
When I submitted my reports to ZDI, I included a second PoC that demonstrates full control of the CPDField
object and then dereferences a controlled, virtual function table pointer:
Conclusion
Implementation of object trees, particularly those in applications where the objects can be controlled and destroyed arbitrarily, is prone to use-after-free vulnerabilities. For developers, special attention must be made to the implementation of object reference tracking and object locking. For vulnerability researchers, they represent opportunities for uncovering interesting vulnerabilities.
Thanks again to Mark for providing this thorough write-up. He has contributed many bugs to the ZDI program over the last few years, and we certainly hope to see more submissions from him in the future. Until then, follow the team for the latest in exploit techniques and security patches.