Normal view

There are new articles available, click to refresh the page.
Before yesterdayZero Day Initiative - Blog

Exploiting a Flaw in Bitmap Handling in Windows User-Mode Printer Drivers

2 August 2023 at 16:09

In this guest blog from researcher Marcin Wiązowski, he details CVE-2023-21822 – a Use-After-Free (UAF) in win32kfull that could lead to a privilege escalation. The bug was reported through the ZDI program and later patched by Microsoft. Marcin has graciously provided this detailed write-up of the vulnerability, examines how it could be exploited, and a look at the patch Microsoft released to address the bug.


In the Windows kernel, there are three APIs intended for general use by device drivers for the purpose of creating bitmaps: EngCreateBitmap, EngCreateDeviceBitmap and EngCreateDeviceSurface. Each of these APIs return a bitmap handle. If the caller wants to perform some drawing operations on the bitmap, the caller must first lock the bitmap by passing its handle to EngLockSurface. EngLockSurface increases the bitmap’s reference counter and returns a pointer to a corresponding SURFOBJ record. SURFOBJ is a structure located in kernel memory containing all the information regarding the bitmap, such as the bitmap’s dimensions, pixel format, a pointer to the pixel buffer, and so forth. We’ll take a closer look at the SURFOBJ structure later. After calling EngLockSurface, the obtained SURFOBJ pointer can be passed to various drawing APIs such as EngLineTo and EngBitBlt. See winddi.h for the complete list of these drawing APIs. After the caller is finished with drawing operations, they should call EngUnlockSurface. At this point, the bitmap’s reference counter decreases to zero again, and the caller is no longer allowed to use the SURFOBJ pointer. Finally, the caller can delete the bitmap by calling EngDeleteSurface on its handle. Typical usage of these APIs is shown below:

All APIs discussed above are exported from win32k.sys kernel-mode module. Note, though, that the functions in win32k.sys are only wrappers, and the implementations are in win32kbase.sys and win32kfull.sys.

Many years ago, both display drivers and printer drivers worked in kernel mode, but since Windows Vista, printer drivers work only in user mode (hence User-Mode Printer Drivers, or UMPD). Two important facts emerge from this change:
       -- During printing operations, the kernel must now perform some callbacks to user mode to call the appropriate user-mode printer driver.
       -- To allow printer driver code to run in user mode, some kernel APIs have now been made available from user mode.

As a result, all the kernel APIs described above now have user-mode counterparts, exported from the gdi32.dll user-mode module. Let’s try to execute the same code shown above, but, this time, from user mode:

Note the reference counter values shown in the comments. The value is still zero after locking the bitmap. Why is this?

Kernel-mode code is always trusted, while user-mode code is always untrusted. So, now that printer drivers execute in user mode, they are considered untrusted and potentially malicious.

Suppose that the user-mode EngLockSurface call would increase the bitmap’s reference counter in the same way that the kernel-mode version does. An attacker, acting as a user-mode printer driver, could call EngLockSurface many times in a loop on a bitmap in order to overflow the bitmap’s reference counter, causing it to wrap around to zero. Then the bitmap could be deleted, leading to a use-after-free on the bitmap.

For this reason, the Windows kernel has implemented a different approach. The EngLockSurface API is expected to return a pointer to the bitmap’s SURFOBJ record – and it does, but, in user mode, this is a user-mode copy of the “true”, kernel-mode SURFOBJ record. We can reconstruct this user-mode data structure as follows:

The user-mode EngLockSurface implementation returns a pointer to the UMSO.so field, which is a copy of the true, kernel-mode SURFOBJ record, so that everything will work as expected. Internally, the user-mode EngLockSurface call jumps to its kernel-mode implementation win32kfull.sys!NtGdiEngLockSurface, where the user-mode UMSO record is allocated and filled in. In kernel mode, the “true”, kernel-mode EngLockSurface call is made on the bitmap, which is needed to access the bitmap’s SURFOBJ record so its data can be copied into the UMSO.so field. Afterwards, though, NtGdiEngLockSurface calls the kernel-mode EngUnlockSurface, which decreases the bitmap’s reference counter to zero again. This explains the observed reference counter values.

Once we call the user-mode EngLockSurface, we are allowed to pass its result (which is a pointer to the copied SURFOBJ data) to various drawing functions, such as EngLineTo or EngBitBlt. When corresponding calls are made from kernel mode, it works in a straightforward manner, but when calling from user mode, an additional layer is needed to translate the user-mode SURFOBJ pointers into true, kernel-mode pointers. So, for example, if the user-mode code calls gdi32.dll!EngLineTo, this will jump to the kernel-mode win32kfull.sys!NtGdiEngLineTo wrapper. The wrapper will obtain the bitmap’s true kernel-mode SURFOBJ record, so the kernel-mode win32kfull.sys!EngLineTo drawing handler ultimately can be executed.

How does the kernel obtain the needed kernel-mode SURFOBJ record? A SURFOBJ record contains sensitive data such as the bitmap’s pixel buffer pointer, so the kernel never relies on the contents of SURFOBJ records coming from user mode. Otherwise, there would be a security risk from malicious user-mode code that tampers with the contents of UMSO.so structures. Instead, inside the wrapper function (such as win32kfull.sys!NtGdiEngLineTo in the example above), the kernel verifies the UMSO.magic value, and then uses the UMSO.hsurf bitmap handle value to lock the bitmap by calling EngLockSurface. In this way, the kernel safely obtains the requested bitmap’s kernel-mode SURFOBJ record, which it can then pass to the appropriate kernel-mode win32kfull.sys!EngXXX drawing function.

The Vulnerability

The user-mode EngLockSurface function performs some validation on the supplied bitmap handle, meaning that not every kind of bitmap can be passed successfully to this call (we will discuss this in more detail later). But malicious user-mode code can now bypass this in any of these ways:

1) After making the EngLockSurface call, we can delete the already-validated bitmap and create some other bitmap with the same handle value. We could choose to create a bitmap of a kind that couldn’t be successfully passed to EngLockSurface.

2) After making the EngLockSurface call, we receive a pointer to a user-mode SURFOBJ record, which, as we already know, is a part of a UMSO record. So, we can overwrite the UMSO.hsurf field, setting it to the handle of any bitmap that we want. We can choose to set it to the handle of a bitmap that couldn’t be successfully passed to EngLockSurface.

3) Simplest of all, we could prepare a UMSO record from scratch, without making any EngLockSurface call first. All we need to do is allocate some user-mode memory, set UMSO.magic to 0x554D534F, and set UMSO.hsurf to the handle of a bitmap of our choice. The remaining part of this record (the UMSO.so field, containing the SURFOBJ record under normal circumstances) can be zeroed, as it will be disregarded by the kernel in any event.

Each of the three possibilities above will allow us to bypass the bitmap validation performed by the user-mode version of the EngLockSurface API.

Now that we have seen that the validation can be bypassed, we must ask what is the purpose of that validation, and what ramifications does it have for security? To answer this question, we must look at the SURFOBJ record definition. Some fields are publicly documented, while others have been reconstructed, as shown below:

The bitmap’s flags field is undocumented, but it is known to contain some documented HOOK_XXX flags found in the winddi.h header file. These flags tell the win32k subsystem which drawing operations should be handled by win32k itself, and which should instead be directed to a specialized device driver. The device driver is indicated by the bitmap’s hdev field.

For example, suppose we want to draw a line on some bitmap. We’ll call EngLineTo, passing a pointer to the bitmap’s SURFOBJ record. Internally, the kernel will convert the requested line into a more general drawing construct known as a “path” (which can be a sequence of lines and curves). It will then check if the bitmap’s SURFOBJ.flags field has the HOOK_STROKEPATH flag set. If this flag is not present, it will use the generic code for drawing (“stroking”) paths provided by win32kfull. If HOOK_STROKEPATH is present, though, the kernel will direct the drawing request to the device driver specified by the SURFOBJ.hdev field. The latter case, where possible, offers improved performance, as it allows individual device drivers to take advantage of accelerations offered by the specific hardware. For example, a graphics adapter may offer hardware-accelerated path stroking. Similarly, printer devices have specialized acceleration for outputting text.

So, if we prepare a bitmap that has a screen-related SURFOBJ.hdev value, and also has the appropriate HOOK_XXX flag set, and we pass it to one of the EngXXX drawing APIs, there is the possibility of reaching an entry point of a specialized display driver, working in kernel mode. This could be cdd.dll!DrvXXX in the single-monitor case or win32kfull.sys!MulXXX in the multi-monitor case (though, there is not always a simple relationship between the requested functionality and the driver entry point ultimately called, as noted in the example above). The pointer to the bitmap’s SURFOBJ record will be passed as a parameter to the driver’s entry point.

Further note that some EngXXX APIs take not only one bitmap as a parameter, but rather two: a source bitmap and a destination bitmap. (Some optionally also take a mask bitmap, but that is not interesting for us). An example of such an API is EngBitBlt, which copies a rectangle of pixels from a source bitmap to a destination bitmap. APIs that work on two bitmaps use the SURFOBJ.flags and SURFOBJ.hdev values of the destination bitmap when determining the ultimate device driver to receive the call. Nonetheless, when the final driver’s entry point is called, both the source and destination bitmaps are passed to it.

Hence, a properly prepared, screen-related bitmap, when passed to some EngXXX API as the destination bitmap, allows us to reach a kernel-mode display driver, while also allowing an arbitrary bitmap of our choice to be passed as the source bitmap.

There is still no obvious security problem here, but let’s look at the SURFOBJ record definition once again. It contains a dhsurf field (not to be confused with the hsurf field discussed above). The win32k subsystem treats SURFOBJ.dhsurf as an opaque value. It is reserved for individual device drivers to use for their internal purposes. Setting this field on a new bitmap is easy: the EngCreateDeviceBitmap and EngCreateDeviceSurface bitmap creation APIs just take the dhsurf value as a parameter. Both the Canonical Display Driver (cdd.dll, used for single-monitor graphics output) and the multi-display driver (win32kfull.sys!MulXXX) expect to work only with their own bitmaps – bitmaps with SURFOBJ.dhsurf values set by that specific driver – rather than on arbitrary bitmaps created from user mode (or by other drivers). Internally, each of these drivers use the SURFOBJ.dhsurf value as a pointer to a block of kernel-mode memory, containing private data owned by that driver.

But we can reach a kernel-mode display driver by passing a properly prepared, destination bitmap to the EngXXX call, and we can also pass some arbitrary bitmap of our choice as the source bitmap to the same EngXXX call. This source bitmap can be an arbitrary bitmap we created, and its SURFOBJ.dhsurf value may point to arbitrary controllable memory. The kernel-mode display driver, such as the Canonical Display Driver, will work on this block of memory as if it were its own block of kernel-mode memory. This means “game over”.

For these reasons, the user-mode EngLockSurface implementation has validation to reject screen-related bitmaps that could be used to reach a kernel-mode display driver. But, thanks to the vulnerability described above, we can bypass this EngLockSurface validation easily. In fact, we can get away with not calling EngLockSurface at all, and just preparing the needed UMSO record from scratch instead, as we have explained.

Exploitation

We must first notice that user-mode EngXXX calls are intended to be used by user-mode printer drivers only, so most of these APIs will fail unless they are called during a callback from kernel to user-mode for a printing operation. But this doesn’t complicate things too much: the user-mode part of the callback is implemented as a gdi32.dll!GdiPrinterThunk function, which is a public export from gdi32.dll. It’s enough to hook or patch this function and perform our main exploitation there. This function receives four parameters (the input buffer, the input buffer size, the output buffer, and the output buffer size), but we don’t need the parameters during our exploitation at all. (However, if you are interested in more details, see Selecting Bitmaps into Mismatched Device Contexts. In particular, see sections titled “User-Mode Printer Drivers (UMPD)” and “Hooking the UMPD implementation”.)

We first need to get a callback from the kernel to our hooked gdi32.dll!GdiPrinterThunk function. To achieve this, we need to initiate some printing operation. First we must locate an installed printer. There is at least one virtual printer installed by default on every Windows machine. We can locate installed printers using a call to the user-mode winspool.drv!EnumPrintersA/W API. Then we must create a printer-related device context:

This call will go down to kernel mode, which will then perform several callbacks to user mode again – so our hooked gdi32.dll!GdiPrinterThunk function will be invoked, exactly as we need. Our main exploitation phase starts here.

First, we need to obtain a bitmap with a screen-related SURFOBJ.hdev value and a useful HOOK_XXX flag set in its SURFOBJ.flags field. To obtain such a bitmap, we can create a window with proper parameters, obtain the window’s device context, and grab the underlying bitmap. The obtained bitmap will act as our destination bitmap:

We also need a source bitmap, with its SURFOBJ.dhsurf field pointing to controlled user-mode memory (our FakeDhsurfBlock):

Now we can prepare two UMSO records, one for the destination bitmap and one for the source bitmap:

At this point, we have everything that we need to make a malicious EngXXX call with our bitmaps. Our screen-related, destination bitmap will have all the defined HOOK_XXX flags set, so we are free to choose any of the EngXXX APIs that accept two bitmaps:

Through reverse engineering the Canonical Display Driver or multi-display driver internals, we can learn how to prepare the user-mode FakeDhsurfBlock so that the call to the display driver yields exploitable memory primitives.

The Patch

As discussed earlier, each of the user-mode EngXXX drawing APIs (such as EngLineTo and EngBitBlt) calls its corresponding kernel-mode win32kfull.sys!NtGdiEngXXX wrapper, where, amongst other things, user-mode SURFOBJ pointers are converted to kernel-mode SURFOBJ pointers. Afterwards, a kernel-mode win32kfull.sys!EngXXX driver endpoint is called to perform the requested drawing operation.

Although it’s not related to our vulnerability, it’s worth mentioning that, for the duration of the gdi32.dll!GdiPrinterThunk user-mode callback, the kernel maintains a mapping of known user-mode SURFOBJ records to kernel-mode SURFOBJ records. When a user-mode printer driver passes a user-mode SURFOBJ pointer to some user-mode EngXXX call, the kernel will try to use the mapping to find the corresponding kernel-mode SURFOBJ pointer so it can be passed to the corresponding kernel-mode EngXXX call.

The mapping is prepared before the user-mode GdiPrinterThunk callback begins. This is because some bitmaps may be passed to the callback as parameters (though, during our exploitation, we made no use of the GdiPrinterThunk input data). However, this means that bitmaps “locked” later, that is, by calls to EngLockSurface made from inside the callback, are not present in the mapping.

Whenever some win32kfull.sys!NtGdiEngXXX receives a user-mode SURFOBJ pointer as a parameter and is not able to find it in the mapping, it assumes that the received SURFOBJ record is contained in an UMSO record (as its UMSO.so field).

Before the patch, such cases were directed to the internal win32kfull.sys!UMPDSURFOBJ::GetLockedSURFOBJ function, where the UMSO.magic value would be verified against the 0x554D534F value, and then the kernel-mode EngLockSurface call would be made on the UMSO.hsurf handle value, yielding the needed pointer to the “true”, kernel-mode SURFOBJ record, as discussed earlier.

As you may have noticed, the name GetLockedSURFOBJ is misleading, as it suggests that the bitmap is already locked. In reality, when coming from user mode, a bitmap’s reference counter is still zero. And as we saw above, a malicious user-mode printer driver may not have called EngLockSurface at all, but instead just prepared the needed UMSO record from scratch.

After the patch, the function name was changed to GetLockableSURFOBJ. A user-mode printer driver can still perform all the manipulations described above, but now GetLockableSURFOBJ considers the received bitmap handle (UMSO.hsurf) as untrusted. After using the UMSO.hsurf value to lock the bitmap in kernel mode, GetLockableSURFOBJ now once again performs the same bitmap validation that is performed when calling the user-mode EngLockSurface API. This validation is performed by calling win32kfull.sys!IsSurfaceLockable. In this way, screen-related bitmaps that could be used to reach the kernel-mode display driver from within the user-mode printer driver are now rejected by GetLockableSURFOBJ.


Thanks again to Marcin for providing this thorough write-up. He has contributed multiple bugs to the ZDI program over the last few years, and we certainly hope to see more submissions from them in the future. Until then, follow the team on Twitter, Mastodon, LinkedIn, or Instagram for the latest in exploit techniques and security patches.

CVE-2022-29844: A Classic Buffer Overflow on the Western Digital My Cloud Pro Series PR4100

20 April 2023 at 15:00

This post covers an exploit chain demonstrated by Luca Moro (@johncool__) during Pwn2Own Toronto 2022. At the contest, he used a classic buffer overflow to gain code execution on the My Cloud Pro Series PR4100 Network Attached Storage (NAS) device. He also displayed a nifty message on the device. Luca’s successful entry earned him $40,000 and 4 points towards Master of Pwn. All Pwn2Own entries are accompanied by a full whitepaper describing the vulnerabilities being used and how they were exploited. The following blog is an excerpt from that whitepaper detailing CVE-2022-29844 with minimal modifications.


Prior to being patched by Western Digital, a memory corruption vulnerability existed in the FTP service of the My Cloud Pro Series PR4100. It allowed unauthenticated attackers to read arbitrary files and, in certain cases, write them. This vulnerability could allow the full compromise of the NAS and gives remote command execution capabilities.

The exploitation requires the FTP service to be activated. While the ability to read arbitrary files is always possible, writing needs at least one share to be “Public” and accessible via FTP. Such setting is the default configuration when a share is made available on FTP, so it should be a common case.

Here is Network Services panel showing FTP Access enabled:

Figure 1 - Network Services Panel

Here is the control panel for configuring FTP shares. Note that the needed settings are enabled by default when sharing a folder via FTP.

Figure 2 - Control panel dialog for setting up FTP shares

Technical Analysis

The issue was discovered by reverse engineering the firmware and source code auditing. The firmware used during this event can be downloaded here. The archive can be carved to extract a SquashFS filesystem. The GPL source code and the modifications made by Western Digital can be downloaded here.

The FTP service used by the NAS is based on the Pure-FTPd open source implementation with a few custom patches made by Western Digital. After extracting the GPL tar archive, one can find the custom modification in the WDMyCloud_PR4100_GPL_v5.24.108_20220826/open-source-packages/pure-ftpd/pure-ftpd-1.0.47/patch/ path within the archive listed above.

To apply the WD patches, run the following commands:

One of the modifications concerns the function douser(), which is called when a user issues the ftp command “user ” in order to log in. In the next code example, the content pointed by the argument const char \*username is user controlled.

We can spot a buffer overflow vulnerability in the first strncpy. When username is larger than 2048 bytes an overwrite occurs after auth_name which is located in the .bss section. The overwritten data is fully controlled by the user. The only requirement is that the data should not contain any NULL bytes beside the finishing one.

Here are some of the relevant global variables located after auth_name:

The global variable loggedin is especially useful as it represents whether a user was authenticated. The string wd is also valuable as it represents the path of the working directory. By overwriting and abusing both of these variables, it is possible to achieve an arbitrary file read and write by using the FTP commands cwd, retr, and stor.

Exploitation

Given the previous analysis, the overall strategy for getting command execution would be to read or write an arbitrary file in order to install a backdoor served by the NAS webserver.

The first step would be to find a way to authenticate without valid credentials. This section explains how to do so given the vulnerability.

Then, to read or write a file, it seems reasonable to try to use the FTP features and commands. This would require changing the current working directory of the FTP session by using the command “cwd ”. This step needs some attention because of various access control checks detailed in this section. After a better understanding on how “cwd” is implemented, we will see how the exploit manage to achieve a file read and write.

Step One: The Authentication Bypass

As expected, the attacker does not have any credential on the NAS. Moreover, we must assume the FTP service is not accessible to anonymous user. Therefore, it is necessary to find a way to bypass the authentication.

Usually, authentication is achieved in two ftp commands:

         -- 'user ': This specifies the username we want to use to log on. It is implemented in douser().
         -- 'pass ': This gives the password for the previous username. This is implemented in dopass().

It is worth mentioning that without first authenticating, it is impossible to use other FTP commands to fetch or store files.

A simplified description of the dopass() implementation is to check the user password and set the global variable loggedin to 1 and eventually to use setuid() to change the user ID of the running process.

As previously seen, the vulnerability allows the attacker to overflow the variable loggedin. It is possible to make the server think that the user has already logged in by issuing only a malicious user command with a name long enough so that loggedin becomes something other than 0, which is sufficient.

One side effect is that the process will not use setuid() so the user root will remain the user of the running process. This will have some consequences later.

Step Two: Access control

An Overview of the implementation

Before retrieving or storing files, we need to change the current working directory of the FTP session by using the cwd path command. This command handling is implemented by the function docwd(), which was patched by Western Digital to include access control checks. Here are some of the relevant parts:

The main access control check revolves around the call to check_allowed(wd_tmp, allowed), where wd_tmp is the concatenation of the global string wd and the user-controlled argument dir. The general idea is that check_allowed() returns something greater than 0 if the user is allowed to enter the directory.

In a similar fashion, the function check_allowed() is called when the user tries to read (retr) or write a file (stor). To provide some additional detail, check_allowed() is specific to Western Digital and is implemented in the closed source /wd/usr/lib/libftp_allow.so as seen in the example below:

This function checks whether the current user (using getuid() and getpwuid()) has access to the required path. The function relies on the Get_Share_Permission() function to determine the permissions over a path representing a share.

We can sum up the check_allowed() method as follows:

         -- If the path is /, then check_allowed() returns 1, which means the path is readable.
         -- If path looks like /<some_path>, then check_allowed() returns the value of Get_Share_Permission(some_path, pw_name).
         -- Otherwise, check_allowed() returns its second argument perm, which is the global variable allowed that was passed along in the call docwd().

The last case is interesting because it means that for paths that do not start with /, no check is really enforced. Indeed, in that case, the access control check only relies on the global variable allowed. This in peculiar will be used later.

The Get_Share_Permission() function is also closed source and specific to Western Digital. Because of this, we won’t dig deeper here. The gist of that function is that it opens the file /etc/NAS_CFG/ftp.xml, which specifies the authorizations on the share.

The Get_Share_Permission() function returns 1 for readable shares for a given username and 2 for readable and writable files and 0 for other states. The file /etc/NAS_CFG/ftp.xml maps the user’s permissions and is an image of what is configured in the “Shares” tab of the administration web page. Here is an example of this file:

In that example, Get_Share_Permission() would:

         -- return 2 for the path “share1” for any account.
         -- return 2 for the path “share2” for the account user2.
         -- return 1 for the path “share2” for the account admin.
         -- return 0 for the path “share2” for all other accounts.

In our context with the previous authentication bypass, the user that is being passed to the Get_Share_Permission() function is root. This may seem ideal, but it is not in the context of the access control. That is because root is different than the default administrator account, which is admin. Additionally, the user root does not makes sense in the ACL context. Consequently, it is likely that Get_Share_Permission() does not give permission to our user root.

That being said, in the previous example, “share1” would still return 2 (making it writable) for the user root. That is because of the parameter #@allaccount# appearing in its write_list. This is because share1 represents a share configured as “Public” (see Figure 1 above). This means that everyone with valid credentials can access this share via FTP. In fact, this is the default configuration for FTP shares.

Please note that Public in this context is a different concept than anonymous access since you would still require a username and password to write or read anything on share1. In that sense, it is fair to expect some kind of security for Public shares.

Step Three: Reaching Arbitrary Directories to Read or Write

At this point, we already bypassed the FTP authentication, but we cannot yet use the cwd command on an arbitrary directory as the check_allowed() function is preventing that command for most folders that are not a Share. For example, we cannot reach the /etc/ directory.

As seen earlier, check_allowed() is called with the variable wd_tmp (path) and allowed (int). Additionally, when the user given path dir does not start with '/', the content of wd_tmp is the concatenation of the global path wd and dir. This is interesting because using the vulnerability, we can rewrite wd_tmp. To summarize, the path argument of check_allowed() can be fully chosen by the attacker.

Using the peculiarities of the check_allowed() function with the control of the first argument, the following strategy allows reaching arbitrary folders with write access:

  1. Use the user vulnerability to rewrite loggedin and bypass authentication.
  2. Use cwd share1, which is writable by root. Therefore, this sets allowed to 2.
  3. Use the user vulnerability to rewrite and erase wd so that wd_tmp[0] == 0. As explained previously, this will prevent check_allowed() from checking anything and forces it to return its second argument (allowed).
  4. Use cwd to any path without starting slash (e.g., etc/). Because of the previous step, this will be allowed, and the server will think that the user has write permissions to the folder.

After that, it is possible to use the stor command to achieve an arbitrary file write. In accordance with the FTP protocol, this command is to be preceded by a port command so that the server will connect to the client on the given port (FTP active mode).

However, there is one limitation with the previous strategy. The attacker must know a valid and writable (i.e., Public) share name. One way to get around this is to first obtain an arbitrary file read capability in order to read the /etc/NAS_CFG/ftp.xml file. With the contents of that file, the attacker can find valid share names to use in the write strategy.

By noticing that the check_allowed() function always returns 1 when called with / as path, we can construct a similar strategy to achieve arbitrary file read:

  1. Use the user vulnerability to rewrite loggedin and bypass the authentication.
  2. Use the cwd / command, which is always “readable”, therefore setting allowed to 1.
  3. Use the user vulnerability to rewrite and erase wd_tmp so that wd_tmp[0] == 0. As explained previously, this will prevent check_allowed() from checking anything. It will return its second argument (allowed).
  4. Use the cwd command to any path without starting slash (for example etc/NAS_CFG/). Because of the previous step, this will be allowed, and the server will think that the user has read permissions on the folder.

After that, it is possible to use the retr command to achieve an arbitrary file read. Here again, one must use the port command per FTP standards.

To summarize, the vulnerability is used to rewrite a global stored path in order to control an argument of check_allowed(). This allows the attacker to circumvent the check and force check_allowed() to return the value of its previous invocation, stored in the value allowed. By using the cwd command in a valid directory then using this attack, it is possible to reach any folder with the permissions of the valid directory. Since the root directory (/) is readable, we can reach /etc/NAS_CFG/ with read permissions to get a read access to the ftp.xml file and learn a writable directory name. From there, we apply the same strategy to access any location with writable permissions.

Getting Remote Code Execution

At that point, we have both arbitrary file read and write access on the NAS. It is the time to investigate getting remote code execution (RCE).

It is worth mentioning that an arbitrary file read might be sufficient to achieve command execution of a sort. For instance, one might find ways to steal user passwords, session cookies, or other secrets stored on the filesystem. However, getting RCE with the file write is easier and only adds a small requirement since FTP shares are Public by default. Therefore, the exploit uses the file write technique, though we know that RCE might be possible without it.

The command execution is achieved in a couple of steps. To begin, a PHP web shell is uploaded to /var/www/cmd.php. This makes the web server serve the url /cmd.php and executes the GET argument cmd. While this might be sufficient, a proper reverse Python shell is uploaded in /tmp/python_shell.py and is executed thanks to the web shell. The Python shell connects back to the attacker device and provides a root shell.

Conclusion

This vulnerability was patched by Western Digital in firmware version 5.26.119 and assigned CVE-2022-29844. Based on the writeup from the vendor, the vulnerability was addressed by correcting the memory corruption condition that could allow an attacker to read and write arbitrary files. During the Pwn2Own contest, Luca was able to demonstrate success both with a reverse shell and a special light show on the device:


Thanks again to Luca Moro for providing this write-up and for his participation in Pwn2Own Toronto. He has participated in multiple events, and we certainly hope to see more submissions from him in the future. Until then, follow the team on Twitter, Mastodon, LinkedIn, or Instagram for the latest in exploit techniques and security patches.

CVE-2021-28632 & CVE-2021-39840: Bypassing Locks in Adobe Reader

21 October 2021 at 16:12

Over the past few months, Adobe has patched several remote code execution bugs in Adobe Acrobat and Reader that were reported by researcher Mark Vincent Yason (@MarkYason) through our program. Two of these bugs, in particular, CVE-2021-28632 and CVE-2021-39840, are related Use-After-Free bugs even though they were patched months apart. Mark has graciously provided this detailed write-up of these vulnerabilities and their root cause.


This blog post describes two Adobe Reader use-after-free vulnerabilities that I submitted to ZDI: One from the June 2021 patch (CVE-2021-28632) and one from the September 2021 patch (CVE-2021-39840). An interesting aspect about these two bugs is that they are related – the first bug was discovered via fuzzing and the second bug was discovered by reverse engineering and then bypassing the patch for the first bug.

CVE-2021-28632: Understanding Field Locks

One early morning while doing my routine crash analysis, one Adobe Reader crash caught my attention:

After a couple of hours minimizing and cleaning up the fuzzer-generated PDF file, the resulting simplified proof-of-concept (PoC) was as follows:

PDF portion (important parts only):

JavaScript portion:

The crash involved a use-after-free of CPDField objects. CPDField objects are internal AcroForm.api C++ objects that represent text fields, button fields, etc. in interactive forms.

In the PDF portion above, two CPDField objects are created to represent the two text fields named fieldParent and fieldChild. Note that the created objects have the type CTextField, a subclass of CPDField, which is used for text fields. To simplify the discussion, they will be referred to as CPDField objects.

An important component for triggering the bug is that fieldChild should be a descendant of fieldParent by specifying it in the /Kids key of the fieldParent PDF object dictionary (see [A] above) as documented in the PDF file format specification:

img01.jpg

Another important concept relating to the bug is that to prevent a CPDField object from being freed while it is in use, an internal property named LockFieldProp is used. Internal properties of CPDField objects are stored via a C++ map member variable.

If LockFieldProp is not zero, it means that the CPDField object is locked and can't be freed; if it is zero or is not set, it means that the CPDField object is unlocked and can be freed. Below is the visual representation of the two CPDField objects in the PoC before the field locking code (discussed later) is called: fieldParent is unlocked (LockFieldProp is 0) and is in green, and fieldChild is also unlocked (LockFieldProp is not set) and is also in green:

img02.jpg

On the JavaScript portion of the PoC, the code sets up a JavaScript callback so that when the “Format” event is triggered for fieldParent, a custom JavaScript function callback() will be executed [2]. The JavaScript code then triggers a “Format” event by setting the textSize property of fieldParent [3]. Internally, this executes the textSize property setter of JavaScript Field objects in AcroForm.api.

One of the first actions of the textSize property setter in AcroForm.api is to call the following field locking code against fieldParent:

The above code locks the CPDField object passed to it by setting its LockFieldProp property to 1 [AA].

After executing the field locking code, the lock state of fieldParent (locked: in red) and fieldChild (unlocked: in green) are as follows:

img03.jpg

Note that in the later versions of Adobe Reader, the value of LockFieldProp is a pointer to a counter instead of being set with the value 1 or 0.

Next, the textSize property setter in AcroForm.api calls the following recursive CPDField method where the use-after-free occurs:

On the first call to the above method, the this pointer points to the locked fieldParent CPDField object. Because it has no associated widget [aa], the method performs a recursive call [cc] with the this pointer pointing to each of fieldParent's children [bb].

Therefore, on the second call to the above method, the this pointer points to the fieldChild CPDField object, and since it has an associated widget (see [B] in the PDF portion of the PoC), a notification will be triggered [dd] that results in the custom JavaScript callback() function to be executed. As shown in the previous illustration, the locking code only locked fieldParent while fieldChild is left unlocked. Because fieldChild is unlocked, the removeField("fieldChild") call in the custom JavaScript callback() function (see [1] in the JavaScript portion of the PoC) succeeds in freeing the fieldChild CPDField object. This leads to the this pointer in the recursive method to become a dangling pointer after the call in [dd]. The dangling this pointer is later dereferenced resulting in the crash.

This first vulnerability was patched in June 2021 by Adobe and assigned CVE-2021-28632.

CVE-2021-39840: Reversing Patch and Bypassing Locks

I was curious to see how Adobe patched CVE-2021-28632, so after the patch was released, I decided to look at the updated AcroForm.api.

Upon reversing the updated field locking code, I noticed an addition of a call to a method that locks the passed field’s immediate descendants:

With the added code, both fieldParent and fieldChild will be locked and the PoC for the first bug will fail in freeing fieldChild:

img04.jpg

While assessing the updated code and thinking, I arrived at a thought: since the locking code only additionally locks the immediate descendants of the field, what if the field has a non-immediate descendant?... a grandchild field! I quickly modified the PoC for CVE-2021-28632 to the following:

PDF portion (important parts only):

JavaScript portion:

And then loaded the updated PoC in Adobe Reader under a debugger, hit go... and crash!

The patch was bypassed, and Adobe Reader crashed at the same location in the previously discussed recursive method where the use-after-free originally occurred.

Upon further analysis, I confirmed that the illustration below was the state of the field locks when the recursive method was called. Notice that fieldGrandChild is unlocked, and therefore, can be freed:

img05.jpg

The recursive CPDField method started with the this pointer pointing to fieldParent, and then called itself with the this pointer pointing to fieldChild, and then called itself again with the this pointer pointing to fieldGrandChild. Since fieldGrandChild has an attached widget, the JavaScript callback() function that frees fieldGrandChild was executed, effectively making the this pointer a dangling pointer.

This second vulnerability was patched in September 2021 by Adobe and assigned CVE-2021-39840.

Controlling Field Objects

Control of the freed CPDField object is straightforward via JavaScript: after the CPDField object is freed via the removeField() call, the JavaScript code can spray the heap with similarly sized data or an object to replace the contents of the freed CPDField object.

When I submitted my reports to ZDI, I included a second PoC that demonstrates full control of the CPDField object and then dereferences a controlled, virtual function table pointer:

Conclusion

Implementation of object trees, particularly those in applications where the objects can be controlled and destroyed arbitrarily, is prone to use-after-free vulnerabilities. For developers, special attention must be made to the implementation of object reference tracking and object locking. For vulnerability researchers, they represent opportunities for uncovering interesting vulnerabilities.


Thanks again to Mark for providing this thorough write-up. He has contributed many bugs to the ZDI program over the last few years, and we certainly hope to see more submissions from him in the future. Until then, follow the team for the latest in exploit techniques and security patches.

CVE-2021-28632 & CVE-2021-39840: Bypassing Locks in Adobe Reader

❌
❌