When one corruption is not enough: Analyzing an Adobe Pwn2Own Exploit
December 12, 2018 | Abdul-Aziz HaririSitting in an exit seat on a plane to Vancouver which was my first leg to Pwn2Own Tokyo – I decided to open my laptop and start analyzing an exploit that we got at a different Pwn2Own a while ago. The JavaScript on the screen seems to have attracted the eyes of the guy sitting next to me. “Is that JavaScript?” he said. “Yes,” I answered back. “Are you trying to read the code?” he replied. “Well, I’m learning,” I said. I must admit that it was a partially troll-answer. I finished commenting a function, closed my laptop and decided to relax. Then I thought about my “I’m learning” comment. It was absolutely true that I’m learning. Learning how other people think while writing an exploit.
The exploit that I decided to analyze was the Adobe Reader PDF exploit written by the 360 Codesafe Team in 2017. A few reasons that made me go back and analyze this exploit. First, it exploited a heap-based buffer overflow vulnerability (ZDI-17-280) in JPEG2000 parsing. Second, it lacked documentation and comments. In fact, not even the JavaScript code that was used to trigger/exploit the bug was plainly described. I had to extract the code from the PDF exploit they gave us. And finally, I’m a huge fan of Adobe Reader – not a bad thing, right?
Throughout this blogpost, I’ll go over the exploitation technique used by 360 to exploit the heap-based buffer overflow in Adobe Reader. For their original submission, they used a kernel exploit to escape the sandbox. This blog post won’t cover the kernel portion of the exploit and focuses solely on the Reader portion.
The Details
The version of Reader I used to analyze the exploit is 2015.023.20070 and can be grabbed from Adobe’s FTP server.
The first step I took to start the analysis was the obvious: extract the JavaScript code used to exploit the vulnerability from the PDF. This task can be accomplished in multiple ways. It’s not too involved to write a Python script to achieve the goal – in fact, this script works quite well. If you’re allergic to scripts and Python, you can always extract it with Foxit Phantom PDF. *wink* *wink*.
Once we extract the code, we’ll end with a large set of JavaScript code. I ended up stripping out the shellcode to make things more readable. The start code calls the following instructions:
Nothing too fancy here. Setup a couple of variables then call memory1
then more variables and then call memory2
.
So, what the heck is memory1
and memory2
? Let’s start with memory1
.
The function allocates an Array
of size len2
and sets the first element (I call it the marker element) to 0x11223344. Then fills the Array
with ArrayBuffers
of size len1
. Finally, it enters a second loop that pokes holes in the array. It looks close to something like this:
Want to verify that in a debugger? No problem:
But why? Well, first, this is a heap overflow. The logical reason for this is to setup the heap in a way to make sure that the vulnerable object we’re overflowing is thrown in one of the holes poked. In this case, we’re overflowing an object of size 0x1200:
Next, memory2
:
Let’s cover the obvious first. It starts off by getting a reference to a button. If successful, then it loops through the array and starts closing some of the holes poked earlier. It leaves some holes unfilled. It then sets btt1.display
to display.visible
. Then calls memory3
.
The reason for filling the holes and keeping a bit unfilled is to make sure that the vulnerable object, when allocated, fills one of the holes. In my opinion, the most interesting part in this function is “btt1.display = display.visible
”. Why ?
Well, I had to wrap my head around this for a bit. Turns out that if you have an Icon
set to a Button
, and it was initially set to be hidden
, you can force it to be parsed through JavaScript - specifically when setting the display
variable of the Button
to display.visible
. So in fact, what they did was basically create a Button
object, attach an Icon to it (JPEG2000 image in this case) and set it to hidden. When display is set to display.visible
, the following happens:
1. An object of size 0x1200 is created
2. The overflow is triggered
The fascinating part about the vulnerability is that after it ends up overflowing the next chunk, it also ends up writing a controlled WORD value. In our case, and if everything was aligned properly, it’ll end up overwriting the size of the next ArrayBuffer chunk.
To make this clearer, here’s the ArrayBuffer before the size got overwritten:
And after the size was overwritten:
Since the bug allows us to overwrite the length of the ArrayBuffer object next by a WORD value, the 360 guys logically went with the max they could - 0xFFFF. Then memory3
is called:
We did overwrite the byteLength
with0xFFFF
but that does not give a lot of read/write freedom. The function loops through the Array to look for the ArrayBuffer
with the corrupted byteLength
. Once found, it created a DataView object for that
ArrayBuffer`.
What happened next? They used the DataView
of the corrupted ArrayBuffer
to corrupt the size of the next ArrayBuffer
with a bigger byteLength
(0x77777777).
The last loop is to find the newly corrupted ArrayBuffer
and create a DataView
object for it.
The rest of the code is mostly to leak memory and gain RCE using the new DataView
.
To sum up the whole story:
1. The vulnerability was only triggered once.
2. The bug is triggered when the JPEG2000 image is parsed.
3. The bug was triggered through JavaScript, specifically by attaching the image as an Icon for a Button which is set initially to the hidden state.
4. The Button display mode is changed to visible then the image parsing starts and the bug is triggered.
5. They sprayed a bunch of ArrayBuffers of size 0x1200 and poked holes.
6. They filled some holes then triggered the bug. Note that when the bug is triggered, the vulnerable allocation is created before the parsing happens.
7. The overflow allows overwriting the size of the next chunk with 1 controlled WORD, 0xFFFF.
8. This is not enough for a read/write, so they used the corrupted chunk (with size 0xFFFF) to corrupt the size of the next chunk with size 0x77777777.
9. They looped through the elements in the array until they found the newly corrupted chunk and creates a DataView for it to give them read/write freedom.
Again, this only covers the Adobe Reader portion of their entire exploit chain. Back in 2017, the team combined this heap overflow with a Windows kernel info leak, and an RCE through an uninitialized buffer in the Windows kernel to win $50,000 USD. Of course, Adobe patched this particular issue some time ago. Still, we see similar techniques used in malware leveraging PDFs to this day, and Acrobat and Reader continue to have a broad attack surface. There’s no doubt this will continue to be a fruitful area of research both for myself and security researchers submitting to the ZDI.
You can find me on Twitter at @AbdHariri, and follow the team for the latest in exploit techniques and security patches.