By Luka Treiber, 0patch Team.
Among many other things, last Patch Tuesday brought a fix for an RCE vulnerability named "Windows Uniscribe font processing heap-based memory corruption in USP10!MergeLigRecords". A couple of days later Mateusz Jurczyk of Google Project Zero published a report with PoC attached. Out of the eight vulnerabilities reported at the same time in that Windows library I chose to patch this one for being the most severe.
Reproduction
Opening font files from the PoC.zip package in Windows Font Viewer displayed the "quick brown fox text" in weird constellations but without a crash. With no exact PoC instructions given this was the procedure that worked for me on Windows 7 x64:
- unpack the PoC
- install signal_sigsegv_313372b5_210_42111ccffd2e10aba8b5e6ba45289ef3.ttf (or any other TTF from the package) on the system
- run Notepad
- select Format->Font...>[Font] 4000 Tall and [Script] Arabic
(1410.378): Unknown exception - code c000041d (!!! second chance !!!)
msvcrt!memcpy+0x1c0:
000007fe`fd651444 8a040a mov al,byte ptr [rdx+rcx] ds:00000000`025230e8=??
The callstack: it matches the one from report
msvcrt!memcpy+0x1c0
USP10!MergeLigRecords+0x7f
USP10!LoadTTOArabicShapeTables+0x5f8
USP10!LoadArabicShapeTables+0x148
USP10!ArabicLoadTbl+0xf0
USP10!UpdateCache+0xf5
Analysis of the PoC
It is usually worth to take a look at PoC and try to understand its payload first before diving into debugging, and thus save a lot of time by knowing what data patterns to look for in the application's memory. So I searched the Internet for the original TTF file the PoC was derived from. I quickly found one and started diffing the two files in order to locate malformed font attributes. Comparison showed a common structure of the files. However, it became clear that an automatic fuzzer was used and too many attributes were polluted with suspicious values to sift through by hand. That task was definitely not a time-saver so I had to give it up.
Analysis of the official patch
Extracting and analyzing the official patch is the next reasonable thing to do after a Patch Tuesday. So let's see how this goes.
The vulnerable version 1.626.7601.23688 of usp10.dll got replaced by the patched 1.626.7601.23807. The files are both of approximately the same size (788KB) and when compared using BinDiff a match of 733 functions is found. Among those are also the ones from the crash call stack above. It makes sense to check differences in those functions first as the patch is often a sanity check on input data inserted before a vulnerable call. The first one - USP10!MergeLigRecords from the immediate crash context - is identical but the next one - USP10!LoadTTOArabicShapeTables - shows some interesting changes in the function graph. Left is the old (vulnerable) and right is the new (patched) version of usp10.dll
We can see that the vulnerable call MergeLigRecords is enclosed in a loop.
Within that loop the only significant change in the patched version is the gray cmp-jnz block that has been added to the patched DLL version. Could this be the patch? With limited analysis done so far it is hard to say what the data in comparison (rsp+var_9C) means, but it is clear that by introduction of that block the criteria to reach the problematic call is more refined than in the old version. This is also a behavior I would expect of a patch.
The only way to make sure is to debug. So we start WinDbg and attach it to notepad.exe on a vulnerable system. We set three breakpoints. One at the vulnerable MergeLigRecords, one before the call to ttoGetTableInfo, and one after. The ttoGetTableInfo call is interesting because it takes two struct parameters - TTOOutput and TTOInput. The gray if block that follows right after, checks if one of the TTOOutput attributes (rsp+var_9C) is 4 and in case of a mismatch exits the loop. So what we're interested in is where/if rsp+var_9C changes inside ttoGetTableInfo and how it is related to the crash inside MergeLigRecords. Before we hit [F5] in WinDbg and click-through the PoC we also need to locate a match for the rsp+var_9C attribute from the patched version in the vulnerable code.
We see that, conveniently, a reference to the same attribute can be found in the old LoadTTOArabicShapeTables named as rsp+var_A4 (it resolves to @rsp+34 in the snippets below).
Now we can observe our checkpoints in WinDbg
0:002> bu0 @!"usp10"+1e32f "dw @rsp+34 L1";
0:002> bu1 @!"usp10"+1e334 "dw @rsp+34 L1";
0:002> bu2 @!"usp10"+1e3f3;
0:002> g
In the first two passes, the value of var_A4 is 0004 and does not change. Also the USP10!MergeLigRecords call executes without a crash.
USP10!LoadTTOArabicShapeTables+0x52f:
000007fe`fd59e32f e8bcfd0000 call USP10!ttoGetTableInfo (000007fe`fd5ae0f0)
0:000> g
00000000`001edd14 0004
USP10!LoadTTOArabicShapeTables+0x534:
000007fe`fd59e334 85c0 test eax,eax
0:000> g
Breakpoint 2 hit
USP10!LoadTTOArabicShapeTables+0x5f3:
000007fe`fd59e3f3 e8b8010000 call USP10!MergeLigRecords (000007fe`fd59e5b0)
0:000> g
00000000`001edd14 0004
USP10!LoadTTOArabicShapeTables+0x52f:
000007fe`fd59e32f e8bcfd0000 call USP10!ttoGetTableInfo (000007fe`fd5ae0f0)
0:000> g
00000000`001edd14 0004
USP10!LoadTTOArabicShapeTables+0x534:
000007fe`fd59e334 85c0 test eax,eax
0:000> g
Breakpoint 2 hit
USP10!LoadTTOArabicShapeTables+0x5f3:
000007fe`fd59e3f3 e8b8010000 call USP10!MergeLigRecords (000007fe`fd59e5b0)
0:000> g
In the third pass, var_A4 gets changed by the ttoGetTableInfo call from 0004 to 0001 and MergeLigRecords crashes the app.
00000000`001edd14 0004
USP10!LoadTTOArabicShapeTables+0x52f:
000007fe`fd59e32f e8bcfd0000 call USP10!ttoGetTableInfo (000007fe`fd5ae0f0)
0:000> g
00000000`001edd14 0001
USP10!LoadTTOArabicShapeTables+0x534:
000007fe`fd59e334 85c0 test eax,eax
0:000> g
Breakpoint 2 hit
USP10!LoadTTOArabicShapeTables+0x5f3:
000007fe`fd59e3f3 e8b8010000 call USP10!MergeLigRecords (000007fe`fd59e5b0)
0:000> p
(14a8.189c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
msvcrt!memcpy+0x1c0:
000007fe`fd651444 8a040a mov al,byte ptr [rdx+rcx] ds:00000000`03bf5018=??
At this point we can conclude that the gray cmp-jnz block would've prevented the crash so we found a patch candidate and can make a 0patch of it.
Patching
So far we've assembled all required pieces to build one - it's all in the gray block in above diff. And below is the resulting .0pp file I made.
MODULE_PATH "C:\Windows\System32\usp10.dll"
PATCH_ID 273
PATCH_FORMAT_VER 2
VULN_ID 2536
PLATFORM win64
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x0001e32f
PIT USP10!0x2e0f0 ; import USP10!ttoGetTableInfo
JUMPOVERBYTES 5
N_ORIGINALBYTES 1
code_start
call PIT_0x2e0f0 ; call USP10!ttoGetTableInfo
cmp word[rsp+34h], 4 ; var_A4
je skip ; inverse condition from original patch
or eax, 01h ; set piggyback condition
skip:
code_end
patchlet_end
PATCH_ID 273
PATCH_FORMAT_VER 2
VULN_ID 2536
PLATFORM win64
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x0001e32f
PIT USP10!0x2e0f0 ; import USP10!ttoGetTableInfo
JUMPOVERBYTES 5
N_ORIGINALBYTES 1
code_start
call PIT_0x2e0f0 ; call USP10!ttoGetTableInfo
cmp word[rsp+34h], 4 ; var_A4
je skip ; inverse condition from original patch
or eax, 01h ; set piggyback condition
skip:
code_end
patchlet_end
When turning the official patch into a 0patch I used a "jump condition piggy-backing" technique already described in a previous post (only this time It is the quick brown fox I'm piggy-backing). Conveniently the official patch is right next to a jump (jnz) that exits the problematic loop so this is also the location for our 0patch. I placed it before the original jnz. Due to lack of space (jnz is only 2 bytes long while we always need 5 to jump to our patch code) I had to place it before the ttoGetTableInfo call that 0patch Agent will substitute with the 5 byte jump. The first instruction in the patch is therefore the substituted original call to ttoGetTableInfo. What follows it is the check from the official patch. First var_A4 is compared to 4 to check if the loop can continue, otherwise eax that holds the result of ttoGetTableInfo, is set to 1 so the test eax,eax instruction from original code that follows the patch will set a jump flag (zf=0) for the jnz that will exit the loop.
After compiling this .0pp file with 0patch Builder, the patch gets applied and the PoC crashes no more. Our team tested it also against the other TTF files from the PoC.zip package and all of them seem to be disarmed. It is worth noting, however, that this patch only covers the execution route discovered in the Reproduction section above. In the diff of LoadTTOArabicShapeTablesthere there is one more cmp-jnz gray block added to the patched usp10.dll that exits a similar loop to the one covered above. But since there is no public PoC available that would trigger that branch of execution we won't include it in a 0patch. The only safe way for 0patch to work is to cover testable execution paths.
Now, if you haven't so far, install 0patch Agent and check the PoC to test it for yourself. Next time you see a quick brown fox jumping, make sure that you have 0patch Agent enabled.
No comments:
Post a Comment