Thursday, December 12, 2024

A Case Study of Local Privilege Escalation from Vulnerability Discovery to Vulnerability Exploitation – Lenovo Dolby API

Background

This blog post is aimed to provide the reader the end-to-end explanation on how I conduct vulnerability research when given a whole new target – the scopes, the challenges, and the workarounds for the challenges I took along the way.

In this research, I’m focusing on the Windows system service running by default on Lenovo laptop, which is identified as Dolby DAX API service hosted by DAX3API.exe.

Vulnerable Target

The vulnerable target is known as Realtek Audio Driver which can be downloaded from Lenovo support page https://download.lenovo.com/pccbbs/mobiles/n3qa118w.exe. It consists of the checksum hash de32823d245c296e5d3ca24a824fa63468d7653c153a319ce0d92f343a5ab530

The DAX3API.exe used in the following analysis has version 3.30508.581.0 with SHA256 D59E6CDD07532B89EC7E575D85B15DAACFB7BBE66D54414924D09C3EC1DFC5FB

This vulnerability has been assigned with CVE-2024-44102 whilst the Lenovo's security advisory can be found here.

Scopes

Depending on the scopes you or the vendor defined, assuming if you are doing this for some bug bounty program vendor typically define the scopes, it is always a good idea to limit the scope so that you can list out the possible or the attack vectors that you are aware of on the target.

In this case, I’m intended to look for an exploitable bug that I’m comfortable to exploit with. Because of my prior experience, I have exploited local privilege escalation for logical bugs caused by access control issues – more specifically the security issue that allows low privileged users to use symbolic link/hard link to attack a world-writable folder that will be manipulated by a system service. Just a side-note, creation hard link is disallowed for non-admin users starting on Windows 11 (technically some later version of Windows 10 has this mitigation in placed, which I lost track. That is also the reason why I stopped looking for arbitrary file creation/deletion vulnerability using hard link attack when this feature was implemented in Insider Preview as it was no longer a security issue and therefore no more bounty from MSRC for all these low-hanging fruit issues.

To my surprised, I revisit the development of symlink attack and realized that arbitrary file deletion is still a thing. One of the reasons is because the development of the generic exploitation method shared by researcher Abdelhamid Naceri. So, I decided to look for this class of vulnerability as a start for the scope of this research.

Who Doesn't Like Debug Messages?

When reverse-engineering a binary, one common difficulty that most of the researchers faced is probably the lack of debug symbol that provides names for the function or variables in the binary. Without the debug symbol you will need to either guess what the particular function is trying to do or spend time to do the debugging to understand its functionality.

Fortunately, the target binary in question has the debug messages available including the name of the function.

 

Obviously, this is going to be helpful for reverse engineers to have the context of the inspecting function. As a perceptive reader, you have probably realized that this is the vulnerable function where local privilege escalation will occur. I do not realize this function directly at first. As I mentioned earlier, this blog post is aimed to give the reader end-to-end insight on how I define the scope and the attack vector of the vulnerability for a whole new target therefore I will walk through the steps I took to identify the attack vector.

From Arbitrary File Delete to Arbitrary File Execution

If this is not the first local privilege escalation vulnerability analysis blog post that you have seen, you should be familiar with how the researchers typically identify the potential arbitrary file delete using dynamic analysis tools like Process Monitor, which is an easy approach even for those who do not know how to do reverse engineering. Apparently, the drawback of this approach is that if the target application requires certain criteria, for example, some configuration found in some specific configuration file, then only trigger the vulnerable code, dynamic analysis tools will not be able to capture anything interesting before these criteria are fulfilled. So, I decided to go for static-analysis approach.

In static analysis, the common strategy to look for attack vectors for vulnerability is to locate the user-controlled input that will be ingested and manipulated by the target binary. When it comes to the logical bugs related to access control issues, one of the plausible user-controlled input is the file name or folder name. Having said that, I directly navigate to the import table and look for the relevant APIs like CreateFile, CreateDirectory and etc.

 


When looking into the caller functions from the screenshots above, these appear to be C++ runtime routines that will eventually call the Windows APIs. Further examination reveals these C++ runtime routines are imported from std::filesystem. Following the caller tree leads us to the runtime routine that is responsible to create directory from a given file path. The proximity view from IDA Pro below shows the code path from the offending function to the CreateDirectoryW API function where we started our analysis at first.

 

At this point, I was able to narrow down the target function from a pool of functions and then only focus and reverse engineer on the target function, PluginManager::StartSoundRadarHost, which reveals a few interesting facts about the vulnerability:

  1. Determine if the host machine is compatible with the supported SKUs as defined in the following:
  2. ·         Gaming

    ·       Mainstream

    ·         SnG

    ·         DolbyAudio

    ·         DolbyAudioPremium

    ·         DolbyAtmosSpeakerSystem

    ·         DolbyAtmosSpeakerSystemForGaming

  3. Determine if DSRHost.exe has been executed and running on the host machine

  4. If no existing DSRHost.exe process is found, it attempts to locate and find Radar Host package which turns out to be bundled with UWP applications in Dolby Access and Dolby Atoms for Gaming that can be downloaded and installed from Microsoft Store. To determine the existence of these UWP applications, it checks from the registry HKEY_CLASSES_ROOT\Local Settins\Software\Microsoft\Windows\CurrentVersion\AppModel\PackageRepository\Packages. For example, the Path value contains the location of where the UWP is installed on the file system.
  5. In my host machine, the Radar Host package can be found under C:\Program Files\WindowsApps\DolbyLaboratories.DolbyAccess_3.23.1378.0_x64__rz1tebttyb220\Assets\RadarHost\Res.dat. The file itself is a ZIP archive with the following contents including DSRHost.exe, which is what the function routine intended to launch later.



  6. A new directory will be created under C:\ProgramData\Dolby\DAX3\RADARHOST if it does not exist. If the folder already exists, the folder and its contents will be removed before creating a new one. Once the folder has been setup, Res.dat will be copied to this folder as Res.zip and the contents in the ZIP archive will be extracted to this folder.

  7. Res.zip will be deleted before it proceeds to launch C:\ProgramData\Dolby\DAX3\RADARHOST\DSRHost.exe

For astute readers, you can see where the low-hanging fruit from the workflows stated above. Since C:\ProgramData is a world-writable directory, i.e. any non-admin users could modify the contents in this directory and its sub-directories, it poses security issue if a system process, in this context DAX3API.exe, attempts to manipulate files under this directory without careful scrutiny.

 

There are two attack vectors from the workflow #6. If you have followed this blog from the beginning, the subtitle said it all, we are not going to exploit the arbitrary file delete even if there is a generic method to turn arbitrary file delete into local privilege escalation. I decided to go for arbitrary file execution by replacing the DSRHost.exe to some other contents under our control. Even though we are changing our focus to arbitrary file execution, the exploitation steps do not differ much. The overview of the exploitation workflow should look like this:

  1. Create an exclusive file handle (i.e. specifying null value for the dwShareMode parameter as per CreateFile documentation) under the directory C:\ProgramData\Dolby\DAX3\RADARHOST to prevent workflow #5 from removing the contents inside this folder because we do not want oplock file that we are going to create in the next step gets deleted.

  2. Identify the last file or any file that will be created after DSRHost.exe is extracted. This is because we want the oplock handler to be triggered when the I/O operation happens on the target oplock file. The oplock handler will replace the already extracted DSRHost.exe with our controlled contents.

That’s it, the exploitation steps are simple and straight forward. However, things become not straight forward when I attempted to trigger the vulnerable code path. This is because the vulnerable code path will *ONLY* be triggered during a user login. This is going to be an issue as we are unable to trigger the vulnerability on-demand.

Before I showcase the complete POC, I’m going to share both of my successful and failed attempts when attempting to trigger the vulnerable code path manually – after all, this blog post is meant to share all the obstacles that I encountered and the taken workarounds to tackle those obstacles.

 

 

After dissecting the user logon routine implemented under CDAXService::OnSessionChanged with the event type WTS_SESSION_LOGON, the event object with the name User-Logon-{FBC2B3C4-B045-40C2-8363-872A91F1C21D} will be signaled. When the “User Logon” event handle is signaled the PluginManager::UserLogonMonitorLoop will break the wait state in this infinite loop routine controlled by WaitForMultipleObjects API and then launch Radar Host through the offending function.

Having gained a comprehensive understanding of the workflow leading to the vulnerable code, I decided to explore ways to manually trigger the vulnerability that would allow me to exploit the vulnerability successfully.

Manual signaling of event User-Logon-{FBC2B3C4-B045-40C2-8363-872A91F1C21D}

It turns out that using CreateEvent API directly can successfully return the named event object handle. However, the created event object is spawned under \Sessions\X\BasedNameObjects\ object directory while the one created by the system service, in our case, DAX3API.exe, found under the root of \BasedNamedObjects.

 

 

I did not attempt to reverse engineer how CreateEvent API works internally, but It seems that the API appends the current user logon session information, for example the session ID, and place the specified named event object under designated \BasedNamedObjects of the session branch.

To verify my hypothesis, I attempted to “bypass” the implementation of CreateEvent API and use native NT API to get the handle of the target event object. Here is the code snippet:


Code Snippet
  1. int NativeCreateEvent()
  2. {
  3.     NTSTATUS status;
  4.     HANDLE hEvent = NULL;
  5.     WCHAR eventName[] = L"User-Logon-{FBC2B3C4-B045-40C2-8363-872A91F1C21D}";
  6.     WCHAR baseNamedObjects[] = L"\\BaseNamedObjects";
  7.     UNICODE_STRING directoryName;
  8.     OBJECT_ATTRIBUTES oa;
  9.     UNICODE_STRING eventDirectoryName;
  10.  
  11.     memset(&oa, 0, sizeof(OBJECT_ATTRIBUTES));
  12.     memset(&eventDirectoryName, 0, sizeof(eventDirectoryName));
  13.     memset(&directoryName, 0, sizeof(directoryName));
  14.  
  15.     pfnNtOpenEvent = (NTOPENEVENT)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtOpenEvent");
  16.  
  17.     if (!pfnNtOpenEvent)
  18.     {
  19.         printf("[-] GetProcAddress(NtOpenEvent) failed (0x%x)\n", GetLastError());
  20.         return 0;
  21.     }
  22.  
  23.     pfnNtOpenDirectoryObject = (NTOPENDIRECTORYOBJECT)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtOpenDirectoryObject");
  24.  
  25.     if (!pfnNtOpenDirectoryObject)
  26.     {
  27.         printf("[-] GetProcAddress(NtOpenDirectoryObject) failed (0x%x)\n", GetLastError());
  28.         return 0;
  29.     }
  30.  
  31.     pfnNtClose = (NTCLOSE)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtClose");
  32.  
  33.     if (!pfnNtClose)
  34.     {
  35.         printf("[-] GetProcAddress(NtClose) failed (0x%x)\n", GetLastError());
  36.         return 0;
  37.     }
  38.  
  39.     RtlInitUnicodeString = (RTLINITUNICODESTRING)GetProcAddress(GetModuleHandleA("ntdll.dll"), "RtlInitUnicodeString");
  40.  
  41.     if (!RtlInitUnicodeString)
  42.     {
  43.         printf("[-] GetProcAddress(RtlInitUnicodeString) failed (0x%x)\n", GetLastError());
  44.         return 0;
  45.     }
  46.  
  47.     RtlInitUnicodeString(&directoryName, L"\\BaseNamedObjects");
  48.     InitializeObjectAttributes(&oa, &directoryName, OBJ_CASE_INSENSITIVE, nullptr, nullptr);
  49.  
  50.     HANDLE hDirectory = NULL;
  51.     status = pfnNtOpenDirectoryObject(&hDirectory, DIRECTORY_QUERY, &oa);
  52.  
  53.     if (status < 0)
  54.     {
  55.         wprintf(L"[-] Cannot get the directory object for \"%ws\" (0x%x)\n", baseNamedObjects, status);
  56.         return 0;
  57.     }
  58.  
  59.     RtlInitUnicodeString(&eventDirectoryName, eventName);
  60.  
  61.     memset(&oa, 0, sizeof(OBJECT_ATTRIBUTES));
  62.     InitializeObjectAttributes(&oa, &eventDirectoryName, OBJ_CASE_INSENSITIVE, hDirectory, nullptr);
  63.  
  64.     status = pfnNtOpenEvent(&hEvent, EVENT_MODIFY_STATE, &oa);
  65.  
  66.     if (status < 0)
  67.     {
  68.         wprintf(L"[-] Cannot open the event for \"%ws\\%ws\" (0x%x)\n", baseNamedObjects, eventName, status);
  69.         return 0;
  70.     }
  71.  
  72.     printf("[+] Opened hEvent: 0x%llx\n", (UINT64)hEvent);
  73.     return 1;
  74. }

However, an access denied returns upon executing the code snippet above. As per the API documentation, when a NULL value is specified in lpEventAttributes parameter, the default security descriptor will be inherited from the creator of the event object. The following screenshot explains why access denied was returned. The event object has 2 ACLs, SYSTEM and Administrators, assigned. For further hardening, the Administrators group does not have Modify State permission enable, which means even an administrative user is not able to signal this event object.

 

Nevertheless, using administrator will defeat the purpose of privilege escalation. So, I decided to move on to the next approach.

Chaining DoS to arbitrary file execution

Understadning the vulnerable code path will be executed once before entering into the wait state in the function PluginManager::UserLogonMonitorLoop,  it gave me the idea that if I could deliberately crash the target service and at the same time perform the exploitation steps that I outlined previously, I might be able to exploit the privilege escalation.

Without spending too much time, I found a way to crash the target service deliberately from the vulnerable function. The workflow stated in #5 within the vulnerable function indicates that C:\ProgramData\Dolby\DAX3\RADARHOST will be removed prior to extracting DSRHost.exe and other files into this directory. It seems that it is the custom C++ function that makes use the std::filesystem library that performs that sanity check to ensure that the specified directory does not contain FILE_ATTRIBUTE_REPARSE_POINT, a filesystem attribute assigned to junction/symlink file or directory. The DoS can be triggered if a junction/symlink directory was found. The custom C++ function will throw an access violation exception that will effectively terminate the target service.

 

Now I can deliberately crash the target service, what is next? I was expecting the service will recover automatically, as I had prior experience with other Windows services before which will restart automatically after the crash, but it turns out this mechanism must be enabled explicitly during the service creation through the SERVICE_CONFIG_FAILURE_ACTIONS option in ChangeServiceConfig2 API. 

Looking at the code snippet for the service creation routine of the target binary, I knew that the recovery mechanism is not enabled for our target service:

Code Snippet
  1. CreateService(L"DolbyDAXAPI", L"Dolby DAX API Service", 2u, &Dependencies, 0LL, 0LL);
  2. hScManager = OpenSCManagerW(0LL, 0LL, 2u);
  3. if (hScManager)
  4. {
  5.     hService = OpenServiceW(hScManager, L"DolbyDAXAPI", 2u);
  6.     if (hService)
  7.     {
  8.         *&Info = L"Dolby DAX API Service is used by Dolby DAX applications to control Dolby Atmos components in the system.";
  9.         ChangeServiceConfig2W(hService, 1u, &Info);
  10.         CloseServiceHandle(hService);
  11.         CloseServiceHandle(hScManager);
  12.     }
  13.     ...
  14.     ...
  15.     ...

This setting can be found in the recover tab under the service control panel:

 

Unfortunately, we cannot take advantage of the DoS that we discovered to chain it to arbitrary file execution. Though it might worth mentioning that this is a persistent DoS that could yield the target service unrecoverable even after restart the machine until the RADARHOST symlink/junction directory was removed.

Multi-user login using terminal services

When finding some workarounds to have two users to login at the same time, I came across Windows Enterprise multi-session, which is available only on Azure Virtual Desktop service. From the FAQ, it came to my attention that RDS on Windows Server is able to achieve that. However, both options are not favourable to us.

At the end, I found this convenient open source tool, SuperRDP, that allows concurrent user login on a normal Windows platform with RDP service enable. NOTE: The author of SuperRDP claims that there is supersede version that allows patching of RDP automatically, however it is closed source. It is important to take note that I do not endorse the use of SuperRDP2 and it is the reader of this blog post responsibility to take any chance if the tool could be implanted with suspicious code.

Nevertheless, I decided to use the SuperRDP in which the core component is based on the original rdpwrap and it seems more trustworthy. I also took an in-depth look into SuperRDP source code, which seems to contain most of the front-end code and does not seem to do anything malicious. The catch is that you will need to place the right configuration for the version of termsrv.dll on your Windows platform into rdpwrap.ini – this requires a fair bit of effort to retrieve the information from the binary through disassembler though.

To no surprise, this approach does not trigger the vulnerable code path because the WTS_REMOTE_CONNECT event is triggered rather than the WTS_SESSION_LOGON which is the only event that CDAXService::OnSessionChanged looks for.

So, I have to figure out another approach.

Fast user switching

This is the definition extracted from Wikipedia

Fast user switching is a feature of a multi-user operating system which allows users to switch between user accounts without quitting applications and logging out.

which turns out to be the workaround that I have been looking for!

Though the fast user switching cannot be fully automated, you can call WTSDisconnectSession to disconnect the current logon session without logging out and show the Windows switching screen which is similar to clicking the switch user button. This could be a practical scenario to exploit this local privilege escalation.

Putting all these together – a way to trigger the vulnerable code and exploitation steps discussed previously – the step-by-step on how to reproduce and exploit this vulnerability is outlined below:

  1. On a fresh Lenovo laptop without Dolby Access or Dolby Atoms for Gamers UWP app installed, this folder C:\ProgramData\Dolby\DAX3\RADARHOST should not exist.
  2. Login to Administrator account and create two standard user accounts (non-administrator), for example:
      • User1
      • User2
  3. Sign out the Administrator account or the User2 account if you are trying to re-run the POC as indicated in the Side Note. Then, login to User1 account:
    1. Download the POC and compile the provided source codes. Upon successful compilation, it should generate dax3apipoc.exe and FakeDSRHost.exe. Ensure the default.xml is placed in the same folder as other executables. (Note: The POC will be made available after the patch is released to all affected models)
    2. Open the command prompt and run dax3apipoc.exe
    3. The expected output is shown in the following screenshot:
    4. Proceed to restart the machine.
    5. After restart, login to User1 account and run dax3apipoc.exe again.
    6. Once you see the message “[+] Setting up oplock: C:\ProgramData\Dolby\DAX3\RADARHOST\HookManager64.dll”, the machine has been set and ready to be exploited with the local privilege escalation vulnerability.
    7. DO NOT LOG OUT User1 account yet but you should switch to User2 account. To do that, click on the Windows icon and then click on the user name as indicated in the arrow in the screenshot:
    8. Select User2 account as indicated in the screenshot below and proceed to sign in using this user account:
  4. You will be presented with the Windows switching screen, enter the password and login to User2 account:
    1. Install Dolby Access from Microsoft Store.
    2. Sign out User2 account. Login to User2 account again.
    3. Upon login, you should see the following screen immediately if the POC works properly. If the exploitation is successful, a dummy file will be created under C:\pwned.txt at the same time. The expected output:



      Noticed that the fake DSRHost.exe is spawned under our target DAX3API.exe process.

Side Note

In the event you would like to re-run the POC on the same affected machine after you have taken all the steps above, you are required to follow the outlined instructions below:

Login to Administrator and use Process Explorer or other similar tool to determine if DSRHost.exe process is running or not. Terminate DSRHost.exe process if it exists.

  1. Login to Administrator and use Process Explorer or other similar tool to determine if DSRHost.exe process is running or not. Terminate DSRHost.exe process if it exists.
  2. Remove all the files/folders under C:\ProgramData\Dolby\DAX3\RADARHOST
  3. Repeat the Step (3) – (4) outlined above. You can perhaps skip the instructions where it asks for installing Dolby Access UWP app.

 

Mitigation

For the affected Lenovo models where patches are not available yet, the users could temporarily disable the service and change the service's start type from Automatic to Manual.

Conclusion

In this post, I have walked through the complete process of vulnerability research, from identifying attack vectors to executing the exploitation. The goal is to equip readers with a structured approach to tackling vulnerability research on new targets. Along the way, I have shared both successful and unsuccessful case studies to highlight key strategies and potential pitfalls. To conclude, I demonstrate a local privilege escalation exploit that requires user interaction. I hope you found this post insightful and enjoyable, just as I did when exploiting this vulnerability.

The views expressed in this blog are solely those of the author and do not necessarily reflect the views of my employer. Any resemblance to actual events or locales or persons, living or dead, is entirely coincidental.

 

Disclosure Timeline

 

17/07/2024: Reported the vulnerability to Lenovo PSIRT

18/07/2024: Lenovo PSIRT responded the team was liaising with Dolby to continue the investigation

03/08/2024: Lenovo PSIRT responded Dolby team acknowledged the issue

13/08/2024: Lenovo PSIRT responded that the security advisory and the patch will be released on November 12th

01/10/2024: Lenovo PSIRT requested to extend the release date to 14 January 2025. Asked for public disclosure on 10 December

05/10/2024: Lenovo PSIRT agreed with the final public disclosure date

10/12/2024: Lenovo published the advisory and released the fix to certain affected models