by Mitja Kolsek, the 0patch Team
Three days ago, Cisco Talos published a post about a code execution vulnerability in LabVIEW, whereby opening a malformed VI file with LabVIEW results in writing NULL bytes at chosen memory locations. This can most likely be used for executing arbitrary code by carefully placing NULLs in various data structures or stack. Nothing unusual so far.
According to Talos' post, the producer of LabVIEW, National Instruments, initially* refused to patch this vulnerability, stating that "National Instruments does not consider that this issue constitutes a vulnerability in their product, since any .exe like file format can be modified to replace legitimate content with malicious."
(* Subsequently, National Instruments stated that they would produce a patch.)
A VI file is not a Windows executable that would run on any Windows computer. However, if you have LabVIEW installed, a VI file will get opened by it, and can be made to automatically run its embedded code. This code is very powerful and by design has ability to access your file system and launch native executables. So a malicious VI file, say, received via email or found on the Internet, could attack your computer if opened in LabVIEW - even without the vulnerability described here.
This is not entirely different from, say, a Microsoft Word document, which is also not an executable file, but can contain powerful damaging macros. (Although Word does warn you about macros and you have to explicitly allow their execution.)
National Instruments provides Security Best Practices stating that you should exercise the same precautions with a VI file as you would with a EXE or DLL file. This makes sense - if an attacker can get you to open his malicious VI file, he can simply put malicious VI code in it that will attack you, just as if he could get you to open a malicious EXE. Importantly, he does not gain any additional benefit from a memory corruption issue described here, as he would still need you to open his VI file - and in contrast to Word and macros, LabVIEW does not ask your permission to execute VI code.
However, the Security Best Practices document further states that if you want to safely inspect a suspect VI before running it, you should add that VI as a sub-VI to a blank VI, and inspect its code before running it.
In this case, however, there is a difference between a legitimately-formatted VI with malicious VI code (which does not get executed as a sub-VI) and a malformed VI causing memory corruption when loaded (which executes malicious code even if loaded as a sub-VI).
This vulnerability therefore allows an attacker to mount an attack with a malicious VI file against a user following National Instruments' Security Best Practices. Since the vendor initially stated that they would not issue a fix (it's still not available at the time of this writing), we decided to make one ourselves.
Analysis
In order to fix this vulnerability, we needed to first understand it. We started with a sample VI file.
A .VI file (example shown above) is a data file in a publicly undocumented format. It gets opened with labview.exe, which, among other things, parses the file's RSRC segments into in-memory RSRC data structures. You can see one RSRC segment at the beginning of the file above, but there can be others further down in a file.
Talos' detailed vulnerability report provided useful details on where their malformed .VI file caused a crash. Apparently, a method called ClearAllDataHdls (yes, the affected DLL comes with some symbols) walks through an array of what we can assume are "data handles". Each data handle has an offset to its own array of some 20-byte objects, and the count of these objects. The code simply walks through all objects of all handles, and writes a NULL to each one of them. Manipulating the said offset allows for writing one or more NULLs at arbitrarily chosen locations in memory.
It was trivial to create a malformed .VI file from a sample file based on this information. And, as expected, it crashed LabVIEW with an access violation. However, it did not crash it in ClearAllDataHdls, but in a method called StandardizeAndSanityChkRsrcMap (actually in a small helper function called by it). What happened? Was our POC different, did we find another bug?
It turned out we were using LabVIEW 2017, while Talos did their testing on version 2016. It appears that in version 2017, LabVIEW added some RSRC sanitization code, and in fact looking at this method revealed some sanity checks are being done on the RSRC data, whereby a .VI file is rejected if these checks fail. Unfortunately, these checks are not for the malformed data in question; in fact, StandardizeAndSanityChkRsrcMap also performs initialization of above-mentioned 20-byte objects by reversing their byte order to little-endian format, and this very action is what resulted in our crash due to accessing an invalid memory address.
It was time to take a closer look at StandardizeAndSanityChkRsrcMap and understand the RSRC data structure. The following image shows the most important part of StandardizeAndSanityChkRsrcMap, where the outer loop walks through all the handles, and the inner loop walks through all objects of a given handle and byte-reverses them.
Now let's look at a sample RSRC structure in the memory, after all the values have been byte-reversed.
52 53 52 43 0d 0a 03 00 4c 56 49 4e 4c 42 56 57 RSRC....LVINLBVW
c4 28 00 00 50 03 00 00 20 00 00 00 a4 28 00 00 .(..P... ....(..
00 00 00 00 00 00 00 00 20 00 00 00 34 00 00 00 ........ ...4...
38 03 00 00 17 00 00 00 4c 56 53 52 00 00 00 00 8.......LVSR....
24 01 00 00 52 54 53 47 00 00 00 00 38 01 00 00 $...RTSG....8...
76 65 72 73 00 00 00 00 4c 01 00 00 43 4f 4e 50 vers....L...CONP
00 00 00 00 60 01 00 00 4c 49 76 69 00 00 00 00 ....`...LIvi....
74 01 00 00 42 44 50 57 00 00 00 00 88 01 00 00 t...BDPW........
49 43 4f 4e 00 00 00 00 9c 01 00 00 69 63 6c 38 ICON........icl8
00 00 00 00 b0 01 00 00 54 49 54 4c 00 00 00 00 ........TITL....
c4 01 00 00 43 50 43 32 00 00 00 00 d8 01 00 00 ....CPC2........
4c 49 66 70 00 00 00 00 ec 01 00 00 46 50 48 62 LIfp........FPHb
00 00 00 00 00 02 00 00 46 50 53 45 00 00 00 00 ........FPSE....
14 02 00 00 56 50 44 50 00 00 00 00 28 02 00 00 ....VPDP....(...
4c 49 62 64 00 00 00 00 3c 02 00 00 42 44 48 62 LIbd....<...BDHb
00 00 00 00 50 02 00 00 42 44 53 45 00 00 00 00 ....P...BDSE....
64 02 00 00 56 49 54 53 00 00 00 00 78 02 00 00 d...VITS....x...
44 54 48 50 00 00 00 00 8c 02 00 00 4d 55 49 44 DTHP........MUID
00 00 00 00 a0 02 00 00 48 49 53 54 00 00 00 00 ........HIST....
b4 02 00 00 50 52 54 20 00 00 00 00 c8 02 00 00 ....PRT ........
56 43 54 50 00 00 00 00 dc 02 00 00 46 54 41 42 VCTP........FTAB
00 00 00 00 f0 02 00 00 00 00 00 00 00 00 00 00 ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
ff ff ff ff 00 00 00 00 a4 00 00 00 00 00 00 00 ................
04 00 00 00 ff ff ff ff 00 00 00 00 b8 00 00 00 ................
00 00 00 00 00 00 00 00 ff ff ff ff 00 00 00 00 ................
c8 00 00 00 00 00 00 00 00 00 00 00 ff ff ff ff ................
00 00 00 00 d0 00 00 00 00 00 00 00 00 00 00 00 ................
ff ff ff ff 00 00 00 00 84 01 00 00 00 00 00 00 ................
00 00 00 00 ff ff ff ff 00 00 00 00 b8 01 00 00 ................
00 00 00 00 00 00 00 00 ff ff ff ff 00 00 00 00 ................
3c 02 00 00 00 00 00 00 00 00 00 00 ff ff ff ff <...............
00 00 00 00 40 06 00 00 00 00 00 00 00 00 00 00 ....@...........
ff ff ff ff 00 00 00 00 5c 06 00 00 00 00 00 00 ........\.......
00 00 00 00 ff ff ff ff 00 00 00 00 64 06 00 00 ............d...
00 00 00 00 00 00 00 00 ff ff ff ff 00 00 00 00 ................
74 06 00 00 00 00 00 00 00 00 00 00 ff ff ff ff t...............
00 00 00 00 50 09 00 00 00 00 00 00 00 00 00 00 ....P...........
ff ff ff ff 00 00 00 00 58 09 00 00 00 00 00 00 ........X.......
00 00 00 00 ff ff ff ff 00 00 00 00 60 09 00 00 ............`...
00 00 00 00 00 00 00 00 ff ff ff ff 00 00 00 00 ................
84 0a 00 00 00 00 00 00 00 00 00 00 ff ff ff ff ................
00 00 00 00 08 24 00 00 00 00 00 00 00 00 00 00 .....$..........
ff ff ff ff 00 00 00 00 10 24 00 00 00 00 00 00 .........$......
00 00 00 00 ff ff ff ff 00 00 00 00 9c 24 00 00 .............$..
00 00 00 00 00 00 00 00 ff ff ff ff 00 00 00 00 ................
a4 24 00 00 00 00 00 00 00 00 00 00 ff ff ff ff .$..............
00 00 00 00 ac 24 00 00 00 00 00 00 00 00 00 00 .....$..........
ff ff ff ff 00 00 00 00 d8 24 00 00 00 00 00 00 .........$......
00 00 00 00 ff ff ff ff 00 00 00 00 5c 25 00 00 ............\%..
00 00 00 00 80 00 00 00 ff ff ff ff 00 00 00 00 ................
38 28 00 00 00 00 00 00 8(......
The structure begins with a 30h-byte header (purple), followed by a DWORD structure length (blue), which is the size of the entire structure as shown - in our case 338h. After that, a DWORD handle count (green), 17h, tells us that there are 23 handles in the handle array that follows (red). Each handle consists of three DWORDs: some seemingly user-readable keyword, count of handle's objects (subtracted by 1, so 0 means 1 object), and the offset of its first object; the offset is meant from the handle count (green). Finally, the rest of the structure is object data area (black). Each object takes 20 bytes, and if a handle has n objects, they take n * 20 consecutive bytes at the specified offset.
Clearly, a valid RSRC structure would have all handles' objects located neatly inside the object data area. But a malformed RSRC structure can specify an arbitrary offset, and thus tamper with chosen memory locations.
Patching
Our goal at this point was to add the missing sanity check to the original code: we should not allow accessing any object data outside the object data area.
We needed to find a good location for injecting the patch, and we chose one right after a handle's offset is obtained, at which point we had all information available to implement the sanitiy check. The following image shows the location of our patch.
We have the following information available at the patch injection point:
- esi holds the offset of the current handle's first object
- dword [ebp+10h] holds the number of objects for this handle (reduced by 1)
- dword [ebp-4] holds the address of the handle count value, which is right next to the structure length value in memory.
In pseudo-code, this is what we needed to do:
- if offset of the current handle is negative or ridiculously large, we return with error 6
- if the number of objects for the current handle is negative or ridiculously large, we return with error 6
- multiply the number of objects with 20 to get the size of the object array
- add offset to the size of object array to get the offset immediately after the array
- calculate the maximum allowed offset by subtracting 34h (offset of handle count) from the structure length
- if the last byte of object array is beyond the maximum allowed offset, return with error 6
- Otherwise continue
This is the source code of the actual patch:
MODULE_PATH "C:\Program Files (x86)\National Instruments\LabVIEW 2017\resource\mgcore_SH_17_0.dll"
PATCH_ID 276
PATCH_FORMAT_VER 2
VULN_ID 2892
PLATFORM win32
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x30c94
N_ORIGINALBYTES 5
PIT mgcore_SH_17_0!0x30c09
code_start
; esi is offset of the handle's object data
test esi, 0FFF00000h ; is offset negative or too huge?
jnz error ; if so, exit with error
mov eax, dword [ebp+10h] ; eax = number of objects in this handle (-1)
inc eax ; eax = actual number of objects in this handle
test eax, 0FFF00000h ; is number of objects negative or too huge?
jnz error
imul eax, 14h ; size of object data for this handle
; (1 object is 14h bytes)
add eax, esi ; eax = offset right after this handle's
; last object
mov edx, dword [ebp-4] ; stored address of handles_num
mov edx, [edx-4] ; structure length is stored right before
; handles_num
sub edx, 34h ; edx is the maximum allowed offset
cmp eax, edx ; are we out of bounds?
jg error ; if so, exit with error
jmp continue
error:
call PIT_ExploitBlocked
jmp PIT_0x30c09 ; jmp to epilogue with error code 6
continue:
code_end
patchlet_end
PATCH_ID 276
PATCH_FORMAT_VER 2
VULN_ID 2892
PLATFORM win32
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x30c94
N_ORIGINALBYTES 5
PIT mgcore_SH_17_0!0x30c09
code_start
; esi is offset of the handle's object data
test esi, 0FFF00000h ; is offset negative or too huge?
jnz error ; if so, exit with error
mov eax, dword [ebp+10h] ; eax = number of objects in this handle (-1)
inc eax ; eax = actual number of objects in this handle
test eax, 0FFF00000h ; is number of objects negative or too huge?
jnz error
imul eax, 14h ; size of object data for this handle
; (1 object is 14h bytes)
add eax, esi ; eax = offset right after this handle's
; last object
mov edx, dword [ebp-4] ; stored address of handles_num
mov edx, [edx-4] ; structure length is stored right before
; handles_num
sub edx, 34h ; edx is the maximum allowed offset
cmp eax, edx ; are we out of bounds?
jg error ; if so, exit with error
jmp continue
error:
call PIT_ExploitBlocked
jmp PIT_0x30c09 ; jmp to epilogue with error code 6
continue:
code_end
patchlet_end
Our micropatch has been published and distributed to all installed 0patch Agents yesterday (two days after Talos published vulnerability details), and you can see it in action in this video.
The benefits of micropatching
This story is a common one: a software vendor creates a product, many users use it, then someone finds a vulnerability. The vendor is notified but it's expensive for them to create and distribute a patch outside their schedule. Even with an updating mechanism in place, the so-called "fat updates" (updates that replace huge chunks of the product) are risky; many things can go wrong and expensive full-blown testing has to be done. And then the update has to be delivered to users, who have to waste their precious time with updating. And all that just for a single vulnerability? Understandably, vendors are inclined to try postponing such unwanted updates and bundle them with scheduled ones, often buying their time by downplaying the issue. When that happens, the security community likes to drop the details ("hey, if the vendor says it's not an issue, there's no harm in publishing"), and that usually pushes the vendor to issue a fix after all. They do it under pressure, and the risk of error is higher than usual. Finally, since un-updating is not really a thing, a botched fix could mean a nightmare for users to just get back to the vulnerable functional state.
In contrast, in-memory micropatching can fix a vulnerability with minimal and extremely controlled code modification (usually a dozen or so machine instructions) with no unwanted side effects. In addition, a micropatch can be applied to a product instantly, while the product is running, and just as instantly removed if suspected to be causing problems. All this allows the testing to be less rigorous, and only focused on the modified code - therefore cheaper.
Now imagine National Instruments had micropatching integrated in LabVIEW. It would be inexpensive to create and distribute a highly reliable micropatch for a vulnerability like this - especially with their intimate knowledge of the product -, and they could stay on their original release schedule while users would get their LabVIEW installations micropatched without even knowing it. No PR mess, no unhappy users, and very little disruption of business. What's not to like?
Software vendors are welcome to approach us about saving money, grief, and their users' time with micropatching.
If you have 0patch Agent installed (it's free!), this micropatch is already on your computer and is getting automatically applied whenever you launch LabVIEW 2017.
@mkolsek
@0patch
No comments:
Post a Comment