By Mitja Kolsek, the 0patch team
A bit of introduction: last week we could all witness a familiar "It's a vuln - no it's not" dance, this time featuring Zero Day Initiative and Foxit Software. In short, ZDI reported to Foxit two security issues in its Reader and PhantomPDF products discovered by Steven Seeley and Ariele Caltabiano. Foxit said they would not fix these issues as their exploitation requires the user to disable Secure Mode and thus allow unsafe JavaScript code to execute. ZDI then moved to publish proof-of-concept details, which resulted in Foxit deciding to address these issues anyway. Finally,
Foxit stated that they would "add an additional guard in PhantomPDF/Reader code where when opening a PDF document contains these powerful ( and thus potentially insecure) JavaScript functions, the software will check if the document is digitally signed by a verifiable/trustworthy person of entity. Only certified documents can run these powerful JS functions even when “Safe Reading Mode” is turned off."
They also announced their plan to "release a Reader/PhantomPDF 8.3.2 patch update this week (ETA Aug 25th) with additional guard against misuse of powerful (potentially insecure) JavaScript functions — this will make Foxit software equivalent to what Adobe does."
We at 0patch like a challenge as much as the next guy. While we're usually patching memory corruption bugs (most critical remotely exploitable vulns are of that sort), we're happy to demonstrate that in-memory micrpatching can just as well be used for fixing logical bugs - at least temporarily, until the official vendor fix is applied.
So we set upon creating a micropatch for CVE-2017-10952, allowing a script inside a PDF document to use the saveAs function to save itself to an arbitrarily chosen location on user's computer, using an arbitrarily chosen file extension. For example, a PDF document containing a <html> block with some script anywhere in it could simply save itself as an HTA file (locally executable HTML file) in user's StartUp folder like this:
this.saveAs("/c/Users/”+ identity.loginName + ”/AppData/Roaming/Microsoft/Windows/STARTM~1/Programs/Startup/si.hta");
As a result, when the user logged in to Windows the next time, this HTA file would get executed.
Reproducing the issue
Reproducing the issue was simple: we downloaded and installed the latest version of Foxit Reader (8.3.1.21155) and put the above code into a sample PDF file to get our POC.
Opening the POC in Foxit Reader with default/recommended configuration resulted in the Safe Reading Mode warning. Pressing "OK" was enough to disable Safe Reading Mode and get our code executed. Indeed, si.hta got created in the StartUp folder.
Analysis
It's generally not trivial to find code that implements a JavaScript function, especially if the result of the POC is not a crash. However, in this case the saveAs function writes a file to disk so it can be intercepted at a call to one of the disk-writing Windows API functions. While the ZDI article mentions the WriteFile function, putting a breakpoint on it resulted in way too many hits. So we went with CreateFile.
However, CreateFile also got called constantly with some BMP file that Foxit Reader seems to be loading all the time for some reason. Making the breakpoint conditional did the trick by simply checking two letters of the file path and not breaking if they matched the said BMP file path:
bp kernel32!CreateFileW "j poi(poi(esp+4)+6)!=0x00730055 ''; 'gc'"
Bingo! The first break occurred right in our saveAs call, confirmed by our HTA path being passed to CreateFile. The call stack tail looked like this:
0030eba8 01703c5e kernel32!CreateFileW
0030ebdc 016f7d4f FOXITREADER+0x823c5e
0030ebfc 011c5a22 FOXITREADER+0x817d4f
0030efac 010bd155 FOXITREADER+0x2e5a22
0030f068 0238ad23 FOXITREADER+0x1dd155
0030f230 02377492 FOXITREADER+0x14aad23
0030f2e4 012f17c7 FOXITREADER+0x1497492
0030f31c 0217568e FOXITREADER+0x4117c7
Time for IDA. As FOXITREADER.EXE is a 54MB beast, it took IDA quite a while to sort it all out. After that, looking at the functions in the call stack revealed that the one containing address FOXITREADER+0x14aad23 holds the bulk of saveAs implementation. There are references to all saveAs arguments there (cPath, cConvID, cFS, bCopy and bPromptToOverwrite), and one can see the logic of bPromptToOverwrite value, which, if true, calls PathFileExistsW and sets a local variable we called allowed_to_save_file to 0 if the user declines the overwrite.
The image below shows the place where allowed_to_save_file is set to 0, and a bit further down where allowed_to_save_file is checked, whereby a bunch of code is bypassed if it is 0. The bypassed code includes the green block where the execution proceeds towards the CreateFile call.
Patching
It would be very difficult for us to do exactly what Foxit is about to do to address this issue (only allowing properly signed documents to use dangerous functions like saveAs) because digging down deeper to find a way to the data on document signatures could easily take us days. So we decided to simplify the fix in a way to still allow saveAs, but not in a RCE-style dangerous way.
We noticed (and tested) that in contrast to Adobe's products, Foxit Reader's saveAs function doesn't seem to support document conversion. Consequently, it makes no sense to allow a document to save itself with any other extension than ".pdf". So our logic would be to check the file name passed to saveAs, and only allow files with a ".pdf" extension to proceed, while silently blocking all other extensions.
Having this patching logic in place, we already knew how to get the file path in the function shown above, so we only needed a way to sabotage the file creation for disallowed extensions without causing any unwanted side effects.
The implementation of bPromptToOverwrite logic came in handy as we saw that when the user doesn't allow overwriting, the execution simply bypasses a code block that otherwise leads to CreateFile, and that surely has no unwanted side effects.
So we did a similar thing: We decided to inject our patch code at the beginning of the green block and do the following there:
1) Get the address of the last "." in cPath_string (if any).
2) If no dot, jump out of the block
3) Make a case-insensitive _wcsicmp with ".pdf"
4) If we don't have a match, jump out of the block
5) Otherwise continue
IDA was really helpful in identifying implementations of _wcsrchr and _wcsicmp inside FOXITREADER.EXE for us so we didn't have to implement them in the patch.
Furthermore, we decided to identify any attempt to save a file with some other file extension as an exploit attempt, which results in an "Exploit Attempt Blocked" popup.
This is the micropatch that came out of this:
; target: Foxit Reader 8.3.1.21155
MODULE_PATH "C:\Program Files (x86)\Foxit Software\Foxit Reader\FoxitReader.exe"
PATCH_ID 275
PATCH_FORMAT_VER 2
VULN_ID 2891
PLATFORM win32
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PIT FoxitReader.exe!0x5CA2A1,FoxitReader.exe!0x5C9A22,FoxitReader.exe!0x14AAD23
PATCHLET_OFFSET 0x14AACF2
N_ORIGINALBYTES 5
code_start
mov edx, dword [ebp-1A0h]
push '.'
push edx
call PIT_0x5CA2A1 ; (_wcsrchr) find the last dot in cPath
add esp, 8 ; restore esp
test eax, eax ; was a dot found?
jz Abort ; no dot found, we don't allow that
call GetLocalPdfString ; a trick to get the address of a local
; string on stack
dw __utf16__(".pdf"),0
GetLocalPdfString: ; at this point, the address of string
; ".pdf" is on the stack
push eax ; eax points to the found dot in cPath
call PIT_0x5C9A22 ; (_wcsicmp) compare the two strings,
; case insensitive
add esp, 8 ; restore esp
test eax, eax ; do strings match?
jz Continue ; they do match, we allow saveAs to continue
Abort:
call PIT_ExploitBlocked ; show the "Exploit Blocked" popup
jmp PIT_0x14AAD23 ; jmp out of the block, sabotaging saveAs
Continue:
code_end
patchlet_end
MODULE_PATH "C:\Program Files (x86)\Foxit Software\Foxit Reader\FoxitReader.exe"
PATCH_ID 275
PATCH_FORMAT_VER 2
VULN_ID 2891
PLATFORM win32
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PIT FoxitReader.exe!0x5CA2A1,FoxitReader.exe!0x5C9A22,FoxitReader.exe!0x14AAD23
PATCHLET_OFFSET 0x14AACF2
N_ORIGINALBYTES 5
code_start
mov edx, dword [ebp-1A0h]
push '.'
push edx
call PIT_0x5CA2A1 ; (_wcsrchr) find the last dot in cPath
add esp, 8 ; restore esp
test eax, eax ; was a dot found?
jz Abort ; no dot found, we don't allow that
call GetLocalPdfString ; a trick to get the address of a local
; string on stack
dw __utf16__(".pdf"),0
GetLocalPdfString: ; at this point, the address of string
; ".pdf" is on the stack
push eax ; eax points to the found dot in cPath
call PIT_0x5C9A22 ; (_wcsicmp) compare the two strings,
; case insensitive
add esp, 8 ; restore esp
test eax, eax ; do strings match?
jz Continue ; they do match, we allow saveAs to continue
Abort:
call PIT_ExploitBlocked ; show the "Exploit Blocked" popup
jmp PIT_0x14AAD23 ; jmp out of the block, sabotaging saveAs
Continue:
code_end
patchlet_end
Micropatch in Action
We made a video to quickly show you how 0patch Agent applies this micropatch to a running Foxit Reader, effectively patching it without disturbing the user.
Closing Remarks
We made this micropatch to show how logical vulnerabilities can also be patched, and to demonstrate the amount of effort required for creating a micropatch. It took us 6 man-hours from installing the vulnerable Foxit Reader to having a working micropatch. Mind you, the majority of time was spent on analyzing the vulnerability and Reader's code, and deciding on a good way to patch. The original product developers already know the product inside out and would skip most of this process.
We believe that with Foxit's intimate knowledge of their product, and our knowledge of micropatching, a high-quality micropatch could be made in less than an hour. With our distribution system, it would be on everyone's computer within another hour, and applied automatically. Even users using Foxit Reader at that time would not notice anything - Reader would instantly get from "vulnerable" to "fixed" while they would scroll the pages of some document. And that's how we believe most software vulnerabilities should (and could) be fixed.
If you have 0patch Agent installed (it's free!), this micropatch is already on your computer and is getting automatically applied when you launch the vulnerable Foxit Reader. You can use this POC to play with it. Have fun, write some micropatches and if you're a software vendor interested in equipping your products with self-micropatching ability, ping us at support@0patch.com.
@mkolsek
@0patch
Update 8/29/2017: The latest Foxit Reader (8.3.2.25013) fixes CVE-2017-10951, CVE-2017-10952, and both vulnerabilities reported by