Sanding the 64-bit-Acrobat’s Sandbox
Introduction
Through out the years, Adobe invested significantly in Acrobat’s security. One of their main security improvements was introducing sandboxing to Acrobat (Reader / Acrobat Pro).
No one can deny the significance of the sandbox introduced. It definitely made things more challenging from an attacker perspective. The sandbox itself is a big hurdle to bypass, thus forcing the attackers to jump directly to the kernel instead of looking for vulnerabilities in the sandbox.
The sandbox itself is nice challenge to tackle.
In a previous post, we covered how to enumerate the broker functions in the 32-bit version of Acrobat/Reader. Since the 64-bit version is out and about, we decided to migrate the scripts we wrote to enumerate the broker functions on the 64-bit version of Acrobat. Throughout this blog post, we’ll discuss how the migration went, hurdles we faced and the final outcome. We’ll also cover how we ported the 32-bit version of Sander, a tool used to communicate with the broker to 64-bit.
If you’d like to review the previous post please refer to our blog: Hunting adobe broker functions
Finding the Differences Between Adobe Reader and Acrobat
To make our IDAPython script operate on a 64-bit Acrobat version, we needed to verify the changes between 64-bit and 32-bit versions in IDA. Since we know that there is a broker function that calls “eula.exe”, we can start looking through strings for that specific function.
We can xreference that string to get to the broker function that is responsible for calling eula.exe, which we can then xreference to get to the functions database.
Here we see that the database, its very different than what we’re used to, when we first saw this, we had more questions than answers!
Where are the arguments?
Where is the function tag?
We knew the tags and arguments were in the rdata section, so we decided to skim through it for a similar structure (there's got to be a better way), (tag, function call, args). While skimming through the rdata section, we kept noticing the same bytes that were bundled and defined as 'xmmword' in the 32-bit version, so we decided to use our "cleaning()" function to undefine them.
Things began to make more sense after the packaged instructions were undefined.
Since the _text,### line appears to be pointing to a function, let's try to convert it to a QWORD since it's a 64-bit executable.
VOALLA! This appears to be exactly what we're looking for, a function pointer, a tag, and some arguments! To refresh our thoughts, The structure was made up of 1-byte tag, 52-bytes of arguments, and a function offset. Let's examine if it has the same structure.
We can see that the difference with the arguments is 4-bytes using simple math, and the structure in the 64-bit Acrobat is as follows: The tag is 1-byte long, the parameters are 56-bytes long, and the function offset is a QWORD rather than a DWORD.
Migrating our 32-bit IDAPython Script to 64-bit
We can now return to our IDAPython script from the previous blog and begin updating it using our new discoveries.
The first difference we notice is that the database for the functions appears to be different. We'll need to convert the byte that contains _text,### to QWORD, which should be simple to do using the IDAPyton create_qword(addr) function.
We're basically walking through the entire rdata section here. If we see '_text,140' on a line in the rdata section, we convert it to QWORD (140 because our base address for the executable in ida starts with 140).
The next difference is that the arguments are 56-bytes rather than 52-bytes. Since we already have the logic, all we need to do is modify the loop check condition from 52 to 56-bytes and the if condition to 57, which simply checks if the 57th byte instruction is a function pointer.
Sander 32-bit
To fuzz Adobe Reader or Acrobat, we need a tool that communicates with the broker to call a specified function. Bryan Alexander created a tool called sander for this purpose, which he mentions in his blog digging the adobe sandbox internals, but the problem is that the utility only works on the 32-bit version of Acrobat. We wanted to use the tool to fuzz the 64-bit version, thus we had to upgrade the sander tool to allow it to call the 64-bit-Acrobat functions.
The tool has 5 options, one to monitor IPC calls, dump all channels, trigger a test call, and capture IPC traffic.
The tool calls functions from the broker directly. We also built another method to initiate IPC calls from the of the renderer, which we won't go into the details in this blog.
We'll try to go over all the steps of how we went from a 32-bit to a 64-bit version.
Upgrading Sander to 64-bit
The sander was written in C++, and the first function was used to start the process and locate the shared memory map containing the function call channels.
The find_memory_map method simply scans the process memory for shared memory. Because the dwMap variable was DWORD, we had to convert it to DWORD64 to store 64-bit addresses.
To be able to contain 64-bit addresses, we had to change current address from UINT to SIZE_T, the return type from UINT to DWORD64, and memory block information casting from UINT to SIZE_T in the find memory map method.
Following the execution of this function, the correct shared memory containing the channels will be returned.
The next step is to build an object that holds all of the channels' data. How many channels are there, is the channel busy or not, what kind of information is on it, and so on.
Those methods that read the structures have a lot of offsets, which are likely to change a lot with the 64-bit version, so we'll need to run Acrobat and look at the structures to see what offsets have changed.
Setting a breakpoint after the find_memory_map function call in Visual Studio will tell us the shared memory address we need to investigate.
Here we can see the shared memory as well as all of the data we'll need to finish our job.
In this code, it just sets certain variables in the object because dwSharedMem was DWORD, we also had to modify it to DWORD64.
Digging inside the Unpack() function, we can see some offsets
The first is channel_count, which does not require any changes because it is the initial four bytes of shared memory.
The offset of the first channel in memory is stored in dwAddr, which we altered from 0x8 to 0x10 because in the 32 bit version all information was stored as 4 bytes each. However in the 64 bit version, all information are stored as 8 bytes each.
Let's have a look at the channel control now. The Unpack function retrieves information about each channel and stores it in its own object.
The state has been changed to 0x8, the ping event has been changed to 0x10, the pong event has been changed to 0x18, and the ipc tag has been changed to 0x18. This was simple because all 32-bit values were converted to 64-bit.
lets now check the crosscallparams Unpack function which retrieves argument information:
This is the channel buffer memory layout; there are 5 arguments, type 1 for the first argument, offset b0 for the first argument, and size 42.
Inside the loop, we go over all parameters in this channel and extract their information. We may jump to the first parameter type by using offset 0x68 from the beginning of channel_buffer, the size is channel_buffer + 8, and the offset is channel_buffer + 4. We'll keep multiplying I to 0xc (12 bytes) in each loop to go over all the parameters because each parameter information is 12 bytes.
Finally, using ReadProcessMemory and the offset of that specific buffer, we read the parameter buffer.
We won't go over every change we made to the 64-bit version, but basically, we compare the memory layout of the 64-bit version to the 32-bit version and make the necessary changes. We did the same thing with the pack functions, which are the contrary of unpack in that instead of reading information from the memory, we write our own tag and function information to the memory and then signal the broker function to trigger a specific function.
As a test, we triggered the Update Acrobat function with the tag "0xbf". Thanks to our IDAPython script, we know how many parameters it requires and what type of parameter it accepts.
Conclusion
We can now proceed with a fuzzing strategy to find bugs in the Acrobat sandbox.
This is only the first step. Stay tuned for more posts about how we ended up fuzzing the Acrobat Sandbox.
Until then, happy hunting!