Analyzing an Out-of-Bounds read in a TTF font file
This vulnerability is a user-mode out-of-bounds read in Microsoft DirectWrite function dwrite.dll!sfac_GetSbitBitmap while processing a TTF font file.
Our starting point is a first recording of the crash, using the TTF loader available on github, also kindly provided by M. Jurczyk once again. We reduced the recording to a total of 256M of instructions. Regarding the needs of the analysis, we then recorded other traces, for example one using the Microsoft Edge browser.
First, we will precisely identify which bytes from the file are responsible for the crash. Then in the second part, we want to analyze the displayed pixels: what is displayed and how we can interpret it as leaked memory. Most of these operations use our powerful backward and forward taint engine, seamlessly integrated with REVEN. During this post we also demonstrate some nice features REVEN provides, such as synchronization with Ghidra/IDA and Kernel WinDbg. This is really nice to cross information so that one get the most of each tool.
Forewords on the recording
For the first record, we used a mechanism of REVEN to start and stop the record more accurately than with a manual record: the ASM stub technique. Basically, we can introduce a four instructions stub in the C/C++ code that will trigger the start/stop/commit of a record, to allow the capture of a specific function or portion of code. In this TTF loader case, we put the start_record stub before the handling of the file, and the stop_record in an exception handler, assuming that the code would trigger an access violation.
Crash to bytes: Tainting our way to the file
For this first part, we analyzed the first available PoC (/1/poc.ttf in the available archive), triggered with the TTF loader.
The first step is to locate the crash; we can simply look for calls to KiExceptionDispatch, and reach the KiPageFault associated:
The page fault occurs when trying to dereference r13=0x2c527014000. Having a look at the memory in this area confirms that it hasn’t been mapped.
By analyzing the code around, we see a simple loop applying an or, byte by byte from a source memory chunk on a zeroed destination. We can synchronize REVEN and Ghidra, and get a view on this simple loop:
Mostly this is a memcpy since target area is 0.
Each round of the loop, the destination address is compared against a low boundary and a high boundary, but the origin address isn’t. The low boundary (0x2c5270e0d90) isn’t interesting to us as the fetch of memory goes upward. The upper boundary (0x2c5270e2d70) on the other hand is too big, as the access violation occurs on the origin address, even though the boundary isn’t reached.
This upper boundary is stored in r9 for comparison.
With REVEN, we can taint r9 (the upper boundary) backward, and follow where it comes from:
From the first results in the backward taint, we see that the value 0x2c5270e2d70 is computed from two values: 0x2c5270e0d90 and rcx=0x1fe0. This is also visible with Ghidra and we can easily analyze the values: the first one is the low boundary found previously, so we can assume that the problem comes from a classic scheme base_address+size with the size being too big, hence the out-of-bound read.
Now, our objective is to know where this size comes from: again we can use the backward taint against this 0x1ef0 value only. This way, taint isn’t “polluted” with the base_address and we get a small amount of results that we can follow step by step. In the following video, we go fast through each taint transfer, and point out when the value is modified:
We conclude that 0x1fe0 is the result of the multiplication of two values: 0x20 and 0xff, and that these values come directly from what seems to be the raw TTF file in memory (a simple grep of surrounding bytes confirms it).
By listing operations performed to obtain these values we see that 0xff remains unchanged until the end of the taint, and the modification to obtain 0x20 are performed in dwrite.dll!ROWBYTESLONG:
The taint shows that the value in cx comes from the second byte of (0xff,0xff), highlighted in the end of the previous video.
The way they are handled shows why the total result may lead to the overflow of the memory block. As a matter of fact, we can tweak these two bytes manually and assert that the overflow doesn’t occur if the result isn’t high enough. For example, setting the first byte to 0x50 instead of 0xff doesn’t lead to a crash.
In this part, we wanted to analyze where the data from pixels came from. To do so, we recorded once again the same Proof of Concept, but we tweaked the two bytes so that no overflow occurs. This way, we ensure that the pixels displayed on screen come from the glyph and not from raw memory.
First pixel analysis
With a simple dichotomy search and the framebuffer, we chose a pixel that belong to a zone with some entropy. We chose one with the value 0xff141414:
We can start by following the memory history and see where this value comes from:
We reach some JIT code (hence the “unknown” binary name) that performs some packing/unpacking on the pixel value. Tainting is probably a better idea at this point. Using it, we even reach the wmain function of the TTF loader:
Here we’re in the wmain of the loader that we used to load the TTF file. To better understand what we are looking at, we can connect WinDbg to REVEN, and have a look at the source code synchronisation from the TTF loader:
We can see the arguments passed to the function, and especially the forged color. So, the 0x14 value is actually a color and “not” memory as is. A question remains: why is the memory displayed this way? An assumption we want to verify is that the “leaked” -some-sort-of- memory is displayed through frequency of bits; long story short: it is.
Recording the first PoC using Microsoft Edge
In the first part, - the trace recorded with the TTF loader -, we can see that the crash occurs quite fast as the OOB read reaches an unallocated block. Since there wasn’t much data after the Glyph and before the next block, we decided to perform another record using Microsoft Edge and a HTML PoC to observe how the bits are transformed/displayed. This way, the memory located behind the Glyph is readable most of the time, - explaining the fact that it doesn’t crash, as mentioned in the Chromium issue -, and we will see how it is read and displayed.
In this recording we used the first PoC available (/1/poc.html), and triggered the OOB Read using Microsoft Edge. Our objective is once again to study the origins of a pixel, and more precisely, how they are displayed.
We want to choose a pixel that looks interesting, so we want to look at pixels that come after the Glyph (from file) data. From the pattern and what we saw earlier, we can guess that the first part of the pattern is the glyph whilst the second is the memory behind it. With a simple dichotomy in the trace’s timeline, looking at the framebuffer, we decided to follow a specific byte using the taint once, the same way we did before, that isn’t 0xffffffff. This time, its value is 0x000000ff.
As the video shows, we quickly reach the moment where a check is performed to decide whether or not the “color” should be applied. Here is the code responsible for this decision:
This code takes each bit of the byte value pointed by rdx + rsi and if the bit at position cl is set, the RGB color is or’ed to the target memory and the target memory is later written on screen through the basic display driver.
This byte value is responsible for the information displayed on the screen. So let’s use the taint or the memory history: we first reach fs_ContourScan which modifies the memory byte by byte, then we reach the vulnerable code in sfac_GetSbitBitmap (as explained in the first part of this post). To put this OOB mechanism in the right order: sfac_GetSbitBitmap copied the memory from the glyph and beyond to a buffer Bytes from this buffer are modified by fs_ContourScan Each bit from these bytes is tested in TDrawGlyphRun1x1, to decide whether to apply the color to screen or not: if a bit is set to 1, then the color is applied.
The remaining question is how fs_ContourScan modified the bytes. The short answer is it only reversed the bits in each bytes. Here is the code:
It seems complicated at first. Synchronizing the trace with Ghidra, we get the following decompiled code:
With more analysis and/or some basic Z3 solving, we find that this code simply reverses the order of bits in a byte.
Two small questions that we can answer in less than a minute
Where the color 0xff000000 comes from when Edge is used?
With the taint once again, we can simply taint a byte, and in the end, we see that it comes from the function GetSysColor:
What memory is leaked?
In the last example with the PoC in Edge, we followed a pixel that led to value 0x4a, which was before bit-reversing the value 0x52. This value is part of a pointer: 0x00000167abe2520d. If we taint this pointer, we can see that it is actually passed as a first parameter to
public: long int __cdecl Windows::UI::Composition::CompositionVirtualDrawingSurface::RuntimeClassInitialize(class Windows::UI::Composition::Compositor * __ptr64, class Windows::UI::Composition::CompositionGraphicsDevice * __ptr64, struct Windows::Graphics::SizeInt32, enum Windows::Graphics::DirectX::DirectXPixelFormat, enum Windows::Graphics::DirectX::DirectXAlphaMode) __ptr64
So it points to a class Windows::UI::Composition::Compositor.
There may be other interesting pointers in memory but this is not the subject of this analysis.
This analysis focused on tracking data through the execution of an OOB read. With the help of the backward taint analysis, we pointed out bytes from the original file that were defining the faulty size, leading to the read overflow. Next we followed the leaked data itself, and gave an explanation on how to “reconstruct” the original memory from displayed pixels.
Generally speaking, the Taint engine provided with REVEN is a very powerful tool that allows to quickly follow data through billions of instructions, and dealing with behavior analysis becomes very straightforward.
To go further
For more information about REVEN, have a look at the following:
This blog entry from Tetrane, showing how to analyze a Use-After-Free in VLC
This blog entry from Cisco Talos, showing how to prove or disprove exploitability of a crash
This blog entry from Thanh Dinh TA, reversing a deeply obfuscated challenge
Or directly contact us at firstname.lastname@example.org!