CVE-2020-27708: Electronic Arts (EA) Origin – Local Privilege Escalation

We recently assessed the security posture of Electronic Arts Origin Client and discovered a privilege escalation issue that would allow a low privilege attacker to elevate privileges to NT AUTHORTY\SYSTEM.  This has been recorded as CVE-2020-27708.

Origin is a digital distribution platform, by Electronic Arts, who own the brand EA Games.  They acquired the trademark Origin when it purchased Origin Systems in 1992. The platform allows some reported 39 million [1] users to download and install games by Electronic Arts.

An initial look with procmon

First, we used the free SysInternals Process Monitor tool (procmon) [2] to look for any low-hanging fruit.  Something immediately stood out; two system services looking for the directory C:\platforms, which they were not able to locate.


In Microsoft Windows, any user is by default able to create a directory in the root of the C drive. So, we proceeded to do just this.

We followed this with a second run of procmon.


As can be seen in the second procmon output, a directory listing takes place on the C:\platforms directory, which is interesting and something we made a note of.

A closer look with ProcessHacker

Our next course of action was to have a look at one of the service processes OriginWebHelperService.exe process using another free tool called ProcessHacker [3]. Something immediately stood out to us, which can be seen in the image below; OriginWebHelperService.exe is loading a DLL qwindows.dll from the directory C:\Program Files (x86)\Origin\platforms\.

Because of the similar names, C:\platforms and C:\Program Files (x86)\Origin\platforms\, we decided to copy the contents of the C:\Program Files (x86)\Origin\platforms\ directory into the C:\platforms directory.


We then ran ProcessHacker again to view the loaded modules within OriginWebHelperService.exe.

Surprisingly, this DLL was loaded directly into the OriginWebHelperService.exe process.

A bump in the road

The next step was to replace qwindows.dll with our own malicious DLL that would open a command prompt on behalf of a low level user. This is where we hit a slight bump in the road. We could see in a procmon log that our DLL was being read, however it was then closed and the original qwindows.dll was read from the Program Files path.

Using another free tool CFF Explorer [4] we took a look at qwindows.dll.


qwindows.dll has only two exported functions, qt_plugin_instance and qt_plugin_query_metadata.


Looking at the sections within the qwindows.dll there are two that stood out to us, .qtmetad and .gfids. What if the Origin Client executables are scanning the DLL’s in the C:\platforms directory and looking for these sections before loading the DLL?

We decided to find out and proceeded to copy the data from these two sections, adding the data to our own malicious DLL into sections with identical names.


Successful privilege escalation

The result was immediate; our DLL was loaded into the OriginWebHelperService.

The OriginWebHelperService runs as Local Service, which is a low privilege account and requires some further effort in order to gain full NT AUTHORITY\SYSTEM privileges.

A recent paper by Antonio Cocomazzi [7] details several ways to break out of Local Service accounts by abusing the SeImpersonatePrivilege.  We could have attempted to use the “Chimichurri Reloaded” technique, for example [8].

However there is another service included with Origin, “Origin Client Service” which runs under the account NT AUTHORITY\SYSTEM and shares the same DLL hijacking vulnerability as the OriginWebHelperService.

At this point we changed our focus to “Origin Client Service”.

Using the sdshow command of sc.exe, the Windows Service Control tool, it was possible to view the security permissions of the Origin Client Service:


The Security Descriptor Definition Language (SDDL) output from the sc sdshow command allows us to view the Security Descriptor, which suspiciously has an ACL for the well known SID string [5] “BU” is used which represents the BUILTIN\Users group.

More detail can be obtained using a PowerShell script [6]:

This allowed us to determine that any user is able to start and stop the OriginClientService.exe service process. This is an added bonus; we now don’t have to wait for reboot in order to execute our malicious payload; we can simply start the service and get as many elevated command prompts as we want:

While both the OriginWebHelperService and the OriginClientService were vulnerable to the issue, the path of least resistance was to exploit the OriginClientService gaining system privileges directly.

CVE-2020-16091 for EA Games Origin Client

We were initially issued CVE-2020-16091 by MITRE, which exclusively describes the vulnerability in this post.  Electronic Arts subsequently became a CNA and have issued a new CVE number, CVE-2020-27708, which merges a lower impact incarnation of this vulnerability with our original finding.  We have opted to lead with CVE-2020-27708, with a reference to CVE-2020-16091 noted here to avoid confusion.


  • 27 July 2020 – Initial discovery
  • 28 July 2020 – CVE-2020-16091 issued by MITRE
  • 8 September 2020 – Electronic Arts informed of vulnerability
  • 19 September 2020 – Electronic Arts granted CNA status
  • 28 October 2020 – Electronic Arts issued CVE-2020-27708
  • 29 October 2020 – Electronic Arts released patch
  • 3 November 2020 – Nettitude release vulnerability analysis


It takes a relatively low effort to audit for DLL path hijacks.  Tools such as process monitor are freely available and should be leveraged as part of a products testing cycle.

Developers should also assess if they really need a service to run as NT AUTHORITY\SYSTEM. For most practical purposes, running a service under the Local Service account is just as effective and more secure; the Local Service account has various privilege restrictions, although is not immune to further privilege escalation itself [7] [8].

We identified this vulnerability in Electronic Arts Origin Windows client, version – 763270.  The vendor has patched this vulnerability in version of the client.


    1. Origin has 39 million users –
    2. Process Monitor –
    3. ProcessHacker –
    4. CFF Explorer –
    5. Well known SID strings –
    6. Using PowerShell to view service ACL’s –
    7. Windows Privilege Escalations: Still abusing Service Accounts to get SYSTEM privileges
    8. Chimichurri Reloaded

CVE-2017-18019: Privilege Escalation via a Kernel Pointer Dereference

A little while ago, I discovered a vulnerability, CVE-2017-18019, affecting a kernel driver of multiple K7 Computing security products, as well as the products of Defenx, both for Windows.  Both were affected because they were using the same anti virus engine, and both are now patched.

The proof of concept was based on an invalid kernel pointer dereference, which led to a blue screen of death.  That research and the subsequent coordinated disclosure process were, at the time, sponsored and handled by SecuriTeam.  It turns out that the proof of concept could be exploited further, and turned into local privilege escalation.  So, with the permission of SecuriTeam, I decided to create a write-up of that local privilege escalation development process.


This article targets the following 64-bit Windows versions: Windows 7 SP1 – Windows 10 v1809.

A Medium integrity level is required in order to exploit this vulnerability in the way that is demonstrated through this article. In order to exploit this from a Low integrity level, you will have to do extra work in order to leak some kernel pointers. This can be done either though other IOCTLs handlers of the target driver itself, or through other Windows driver kernel memory leak bugs.

Bug Analysis

The root cause of this issue is that the author of the following function trusts a pointer to read data from, originating from a user-supplied input buffer, as long as it references an address inside the kernel address space.

The vulnerable function fetches a pointer from the IOCTL’s input buffer and checks if it is greater or equal to nt!MmHighestUserAddress (0x00007ffffffeffff in x64). If that’s true, then the function will proceed by dereferencing that pointer and evaluating the first byte located at that memory address.

Clearly, the purpose (even though the implementation is buggy) of this check is to verify that the pointer address from where further information will be read resides in kernel memory of which, from the developer’s perspective, its virtual address and contents are not supposed to be known and controlled by the user. This, of course, is not entirely true because kernel object addresses may be leaked, and also they may reference directly or indirectly user-supplied data.

The following screenshot shows (in grouped nodes) what we described above.

Figure 1 – Verify it is a kernel pointer.

We can easily crash the host by supplying an arbitrary kernel pointer that references a non-allocated memory page.

The following image shows the output from Windbg the moment the memory access violation occurs.

Figure 2. Arbitrary kernel pointer dereference.

Further Analysis

What we know at this point is that we have a denial of service bug that can be triggered by any user in order to crash the host. So, we analysed this function further in order to find out if there is something more that we can do with it.

The following graph-view screenshot continues directly from what is shown in Figure 1.

Figure 3. Kernel memory buffer data checks.

Assuming that RCX points to a valid kernel address where the first byte is 0x4B, so that the previous check succeeds (cmp byte ptr [rcx], 4Bh), we arrive at the second part of the vulnerable function as shown above.

Here we notice further byte value checks, and specifically the second byte of the buffer referenced by RCX should be 0xFF in order to access the final part of our analysis.

Figure 4. Arbitrary Function Pointer Call.

A couple of pointer dereferences later, we see that the function is treating the last one as a function pointer. We also notice that the first and second parameters passed to RCX and RDX respectively can also be controlled.

To be more specific, the first parameter is taken from the buffer referenced by the arbitrary kernel pointer that we control, and the second one is pointing inside our user-input buffer that is defined through the call to DeviceIoControl function.

Setting things up

At this point, we have all the information we need in order to proceed with the exploitation of the vulnerability. To do that, we must know the address of a kernel object and also control its contents, to a certain extent. As we discussed, the initial pointer from where the rest of data is read leading to a function pointer called, must reference an address inside the kernel address space. This is also the developer’s assumption around the safety of that decision.

In a previous article we talked about Private Namespaces and the ability to insert user-defined data in the body of the associated kernel object. We will be using this type of objects in order to exploit the vulnerability, as they can be used reliably in this case as well.

In order to exploit this vulnerability, we will be using two kernel objects of the aforementioned type. The first object will be used for controlling the subsequent pointer dereferences that allow us to call an arbitrary function pointer, while the second object will be used in order to control the initial kernel pointer check that must reference a known kernel object in memory (first object).

Exploitation in Windows 7 SP1 x64

In the absence of exploitation mitigation such as SMEP (Supervisor Mode Execution Protection), taking advantage of this vulnerability is quite straight forward. We can execute our payload function in userland without taking any additional steps, such as temporarily disable SMEP. We just need to control the instruction pointer and that would be enough.

To start with, we will create a Private Namespace object using a random boundary name and we will use NtQuerySystemInformation function to leak its address.

Figure 5. 1st Object (Win7 SP1 x64).

Then, we will create another object of the same type with a crafted boundary name.

The first and second bytes must be 0x4B and 0xFF respectively (see Figures 1 and 3) to satisfy the byte value checks. Also, in the offset 0x0A (Figure 4 – first pointer dereference) of the crafted boundary name, we will be inserting the address of the first object + the distance in bytes (0x1a0) between that address and the location of the boundary name in that object + an arbitrary offset (0x1A) that contains a value that can be translated to a userland pointer, which satisfies the proof of concept for this version of Windows. Note that we take into account that at the result of the previous calculation, the value 0x0C will be added in order to reach the userland pointer value (Figure 4 – second pointer dereference).

Figure 6. 2nd Object (Win7 SP1 x64).

Let’s have a closer look at how these two objects are ‘inter-connected’.

Figure 7. Objects Interconnection (Win7 SP1 x64).

Finally, we can see the function pointer being called, in order to execute our payload at address 0x1010000.

Figure 8. Call Payload-Function Pointer (Win7 SP1 x64).

Exploitation in Windows 8.1 – 10 v1809 x64

In more recent Windows versions, exploiting a kernel driver bug is more challenging due to exploitation mitigations that have been added. In this case, we take control over the execution flow by calling an arbitrary function. However, due to the SMEP we are not able to directly execute code that resides in the user address space from kernel mode, so we will have to take another approach.

A common solution is to attempt to temporarily disable SMEP by clearing the 20th bit in CR4 register of a specific processor and lock our threads execution to only run on that one, so that we can execute our payload in userland as before. However, we would have to restore CR4 in order to avoid KPP (Kernel Patch Protection/PatchGuard) killing the host.

Another way, which we will be using in this write-up, is to take advantage of the execution flow control in order to turn it into a “write-what-where” primitive, which will enable us to modify arbitrary data in kernel memory. Once that is achieved, there are, again, two common ways of taking advantage of this in order to elevate our privileges.

The first method is to overwrite with a NULL value the SD (Security Descriptor) pointer in the object header of an elevated process running as SYSTEM. This will allow a non-privileged process to inject and execute malicious code in the same security context. However, this method will only work up to Windows 10 v1511 (Build 10586), as described in this article.

Another way to take advantage of a “write-what-where” primitive is to enable privileges in the primary token of a non-privileged process in order to enable it to again inject and execute code in the security context of a process running as SYSTEM. This method still works fine, but it requires a minor modification from Windows 10 v1709 (Build 15063) onwards, as described here. What we are about to describe here can also be used in Windows 7.

Going back to what we have described so far, we have noted that we are also able to control the first two parameters (see Figure 4) passed in RCX and RDX respectively, once our arbitrary function is called. We are going to take advantage of this capability in a moment.

In this case, we first need to leak the address of the primary token of our process, where will be enabling additional privileges. We will be using that address as the target of our exploitation primitive. As in Windows 7, NtQuerySystemInformation can be used for the same purpose from the standard ‘Medium Integrity’ of a user process in order to leak the kernel object and function addresses that we will be using.

We will then create our first Private Namespace object with a custom boundary name, where the first 8 bytes will be set to the kernel address that we will be using as our ‘gadget’ to modify arbitrary kernel data. So, instead of executing a payload in userland, we will be redirecting the execution to kernel function, nt!RtlCopyLuid that will enable us to modify arbitrary kernel data.

Figure 9. nt!RtlCopyLuid.

Since we control both the RCX and RDX registers, we can use this function to complete our “write-what-where” primitive.

We will be needing, again, a second Private Namespace object with a custom boundary name which at offset 0x0A of the name data (Figure 8 – first pointer dereference) must contain the address of the first object + the distance in bytes (0x1a0) between that address and the location of the boundary name in that object. Remember that at the first 8 bytes of the boundary name of the first Private Namespace object, we have inserted the address of nt!RtlCopyLuid. Note that as before, we must take into account that at the result of the previous calculation, the value 0x0C will be added in order to reach our arbitrary kernel function pointer value, loaded at the R10 register (Figure 8 – second pointer dereference).

So, this is how it should look:

*(ULONG_PTR*)(boundaryName + 0x0A) = customPrivNameSpaceAddress + boundaryNameOffsetInDireObject - 0x0C;

Then, we need to take control of the first two parameters.

The first parameter loaded in RCX is read again from our custom boundary name, at offset 2 (the first two bytes of our custom boundary name must be 0x4B,0xFF). So, we will be setting there the address of our process’ token object + the offset (0x40) to reach the nt!_SEP_TOKEN_PRIVILEGES structure member.


It should look as follows:

*(ULONG_PTR*)(boundaryName + 0x02) = tokenAddress + 0x40;

Finally, we can also control RDX since the value of R12 is copied over, which points at the address of our userland input buffer + 0x10 (see Figure 1 – 6th node). This is where we read the data from, to write into an arbitrary kernel address. In this case we will overwrite the ‘Enabled and ‘Present’ privileges members of the aforementioned structure (Figure 10).

It should look like this:

*(unsigned __int64*)(inputBuf + 0x10) = _ULLONG_MAX;

So, our exploit will have to reach the vulnerable function twice in order to complete the attack.

Figure 11. Objects Interconnection – Write-What-Where Primitive.

The image above shows how the two objects are ‘interconnected’ in order to complete our “write-what-where” primitive to finalize the exploit.


This was an interesting bug to examine and exploit, as it shows once more that no input data should ever be blindly trusted. From the developer’s perspective, trusting a kernel pointer to read data from, presumably out of user’s control, was a ‘safe’ decision to take. However, it turned out to become a serious vulnerability in multiple products of two different vendors that use the same SDK.

CVE-2018-5240: Symantec Management Agent (Altiris) Privilege Escalation

During a recent red team exercise, we discovered a vulnerability within the latest versions of the Symantec Management Agent (Altiris), that allowed us to escalate our privileges.


When the Altiris agent performs an inventory scan, e.g. software inventory scan, the SYSTEM level service re-applies the permissions on both the NSI and Outbox folders after the scan is completed.

  • C:\Program Files\Altiris\Inventory\Outbox
  • C:\Program Files\Altiris\Inventory\NSI

The permissions applied grant the ‘Everyone’ group full control over both folders, allowing any standard user to create a junction to an alternative folder. Thus, the ‘Everyone’ permission is placed on the junction folder, enforcing inheritance on each file or folder within this structure.

This allows a low privilege user to elevate their privileges on any endpoint that has Symantec Management Agent v7.6, v8.0 or v8.1 RU7 installed.

Analysis – Discovery

When performing red team engagements, it is common to come across different types of third party endpoint software installed on a host. This type of software is always of interest, as it could be a point of escalation on the host, or potentially across the environment.

One example of endpoint management software we’ve often seen is Altiris by Symantec. This software is an endpoint management framework that allows an organisation to centrally administer their estate to ensure the latest operating system patches are applied, to deliver software, to make configuration changes based on a user’s role or group, and to perform an inventory asset register across the entire estate.

The version that this was tested by Nettitude was version 7.6, as shown throughout this release, however it was confirmed by Symantec on 12 June 2018 that all versions prior to the patched version are affected by the same issue.

We noticed that folders within the Altiris file structure had the ‘Everyone – Full Control’ permission applied. These folders seemed to contain fairly benign content, such as scan configuration files and XML files, from what we believed to be the inventory scan or output from a recent task. These folder and file permissions were found using a simple PowerShell one liner which allowed us to perform an ACL review on any Windows host, using only the tools on that host. An example of this one liner is as follows:

Get-ChildItem C:\ -Recurse -ErrorAction SilentlyContinue | ForEach-Object {try {Get-Acl -Path $_.FullName | Select-Object pschildname,pspath,accesstostring} catch{}}|Export-Csv C:\temp\acl.csv -NoTypeInformation


When reviewing the timestamp on these folders, it appeared there was activity happening once a day within this folder. After doing further research into the folders, we concluded that these files were likely modified after a system or software inventory scan. Now, depending on the relevant organisations configuration and appetite for inventory management, this may happen more or less than once per day.

Here’s where the fun begins. Having ‘Everyone – Full Control’ permissions on a folder can be of great interest, but sometimes you can go down a rabbit hole that leads to nowhere, other than access to the files themselves. Nevertheless, we jumped headfirst down that rabbit hole.

It’s worth noting that once we found this behaviour, we went back to a recent vulnerability disclosure against Cylance (awesome post by Ryan Hanson incoming) to see if this type of attack would be possible here:

Here is the folder permissions that we identified on the ‘NSI’ folder. These permissions were also the same on the ‘Outbox’ folder.

We then attempted to redirect the folder to another location using James Forshaw’s symboliclink-testing-tools to create a mount point to another folder and see if those files were written, which was successful. It was also possible to use the junction tools from sysinternals ( The only problem with the sysinternals junction tool is that it requires the source folder to not exist, whereas in our case the folder was already there with ‘Everyone’ permissions. An example of this is shown below:

If we were to completely delete this folder we would not have the correct permissions to recreate this attack. James Forshaw’s toolkit allows the existing folder to be overwritten rather than starting from scratch, as shown below:

Another tool that could be used for this type of attack is called mklink.exe from Windows, but this requires elevated privileges, which would not have been possible in this situation (the point is that we’re attempting to gain elevated privileges).

To completely understand what process was overwriting these permissions, we uploaded Process Monitor from sysinternals ( to see what was going on under the hood. As you can see from the output below, all files and folders were getting a DACL applied by the AeXNSAgent.exe.

Analysis – Weaponisation

So how can we weaponise this? There are multiple ways you could choose to make this vulnerability exploitable, but the path of least resistance was trying to override the entire root Altiris folder (“C:\Program Files\Altiris\Alritis Agent\”) permissions so that we could modify the service binary running under the SYSTEM account, namely AeNXSAgent.exe.

The following screenshots show the permissions applied to the ‘Altiris Agent’ folder and the AeNXSAgent.exe service binary, before modifying the mount point to overwrite the permissions:

We then created a mountpoint which points to the folder ‘Altiris Agent’. It’s worth noting that this folder must be empty for the redirection to be possible. Since we have full permissions over every file, this was trivial to complete. The mount point was created and verified using James Forshaw’s symboliclink-testing-tools.

We then waited for the next scan to run, which was the following morning, and the next screenshot shows the outcome. As we expected the ‘Everyone – Full Control’ permission was applied to the root folder and everything under it, including the AeNXSAgent.exe.

Once we had full control over AeXNSAgent.exe we could then replace the service binary and reboot the host to obtain SYSTEM level privileges. It is worth noting that privilege escalation vulnerabilities in symlinks are fairly common and James Forshaw himself has found well over twenty as shown here:


This vulnerability affected all versions of Altiris Management Agent, namely up to v7.6, v8.0 and 8.1 RU7. We strongly recommend you apply all patches immediately.

If you have more ideas about exploiting this or similar vulnerabilities on run-time and/or in other ways, then please share them with the community and let us know your thoughts.

Disclosure Timeline

  • Vendor contacted – 31 May 2018
  • Vendor assigned Tracking ID – 31 May 2018
  • Vendor confirmed 60 day disclosure – 31 May 2018
  • Vendor acknowledged vulnerability in v7.6, 8.0, 8.1 RU7 – 12 June 2018
  • Vendor confirmed fix for all four releases – 16 July 2018
  • CVE issued by participating CNA – 23 July 2018
  • Vendor publicly disclosed ( – 25 July 2018
  • Nettitude release further information – 12 September 2018

CVE-2019-9702: Symantec Encryption Desktop Local Privilege Escalation – Exploiting an Arbitrary Hard Disk Read/Write Vulnerability Over NTFS

Note: These vulnerabilities remain unpatched at the point of publication.  We have been working with Symantec to try and help them to fix this since our initial private disclosure in July 2017 (full timeline at the end of this article), however no patch has yet been released.  Consequently, we are at the point of publishing the findings publicly.  We will continue to work with Symantec to help them to produce an effective patch.

Update – Thursday 20 June 2019

Symantec assigned this CVE-2019-9702.

This vulnerability affected both Symantec Endpoint Encryption and Symantec Encryption Desktop.

Symantec has produced a patch for Symantec Endpoint Encryption as of version 11.3.0 but not for Symantec Encryption Desktop.

While there is no plan to produce a patch for Symantec Encryption Desktop, the Symantec Security and Development teams have recommendations to mitigate the risks involved.  For more information, see the following SYMC Advisory:

In this article we discuss various approaches to exploiting a vulnerability in a kernel driver, PGPwded.sys, which is part of Symantec Encryption Desktop [1]. These vulnerabilities allow an attacker to attain arbitrary hard disk read and write access at sector level, and subsequently infect the target and gain low level persistence (MBR/VBR). They also allow the attacker to execute code in the context of the built-in SYSTEM user account, without requiring a reboot.

Since many of the exploitation techniques that we come across rely on memory corruption, we thought that demonstrating exploitation of this type of flaw would be interesting and informative.

We will provide a short overview of the discovery and nature of the vulnerability. We will then discuss how access control to file and directory objects is enforced by NTFS, attack methods, problems, possible solutions to complete the exploit, and their limitations.

But first, here is a video demonstration of the vulnerability being exploited in the latest Windows 10 v1709 64-bit.

Affected products

  • Symantec Encryption Desktop suite version 10.4.1 MP2HF1 and earlier.
    • Module: PGPwded.sys v10.4.1
  • Symantec Endpoint Encryption version v11.1.3 MP1 and earlier.
    • Module: eedDiskEncryptionDriver.sys v11.1.3

Vulnerability discovery

Before discussing the two interesting input/output control requests (IOCTLs) and some associated code snippets, we need to focus on the practice that ultimately allows any user to take advantage of the disk read/write capabilities of the kernel driver under examination.

While we were going through the exposed named device objects by the kernel drivers installed, we noticed something interesting. To start with, by using DeviceTree by OSR [2], we could see that PGPwded.sys exposed a device object named PGPwdef.

According to its security attributes, all users should be able to access that object.

However, by using WinObj from Sysinternals [3], even with full administrative privileges, we were immediately receiving an access denied error. In other words, even an administrator was blocked from accessing that device object. That was interesting enough by itself to make us start digging a bit deeper.

Before opening Windbg, we decided to perform a fast check over all running processes, in order to find out if at that point there was any process with an open handle to that device object. The results of that check were even more interesting. In fact, there was a process called PGPtray.exe that had an open handle to the aforementioned device object. That process was running with Medium Integrity Level [4], though.

Our attempts to access that device object were blocked, even with full administrative privileges (High Integrity Level), but at the same time a process running with medium integrity had managed to obtain a handle with Read/Write access, and thus it was able to send I/O requests to the driver under examination.

It was about time to bring in Windbg and start examining that kernel driver further.

Our first target was the function that handles the IRP_MJ_CREATE [5] request, because that IRP is normally sent when attempting to access a named device object via the  CreateFile function.

Shortly after we started stepping through the code, we found the check that was blocking our attempts to access that device object.

So let’s see what those two unicode strings are all about…

In other words, if the application that attempts to access that device object resides in the installation directory (the first one) then access is granted. Otherwise, access is denied. Of course, that directory is only writable by administrators.

However, there is nothing that stops us from either injecting code into another process of an executable that is already placed there, such as PGPtray.exe, or just starting a new process of one of the executables installed there by default, and inject our DLL module which will run the exploit code.

At this point, we had managed to start interacting with this driver of the target application and we just had to start digging deeper.

The fact that the developers of this application had tried to block access to that device object, albeit in an ineffective manner, made the associated driver immediately a lot more attractive.

Getting raw read/write access to the hard disk

With the assumption that this driver might have some interesting functionality that a normal user shouldn’t be allowed to access, we started looking at the various IOCTL handlers.

The first interesting IOCTL code was 0x8002206C, which allowed us to obtain raw read access to the hard disk. This by itself can have an interesting impact in the context of security, as it allows a user to access data that they normally wouldn’t be allowed to. In other words, a standard user could use that IOCTL to read the contents of a file that they normally cannot access, or obtain other sensitive data from portions of kernel memory that have been paged out.

The second interesting and even more dangerous IOCTL code was 0x80022070, which allowed us to perform arbitrary raw writes on the hard disk. This, of course, has an even more serious impact.

Combined, these two IOCTLs permit the attacker to parse and modify data at will.  The impact ranges from abusing the MBR [6] to install a bootkit, to destroying and/or altering data, and finally – as we are about to demonstrate – to elevating privileges on runtime and executing code as the SYSTEM user account.

In order to read and write to the hard disk, the driver calls the following two functions in the same order:

  1. nt!IoBuildSynchronousFsdRequest
  2. nt!IofCallDriver

The following two images show the read and write operations respectively.

The StartingOffset parameter is a pointer to a LARGE_INTEGER structure (this provides a 64-bit signed integer) that specifies the starting disk offset for the read/write operations.

In both cases, all the parameters are fully controlled by the user. This means that we can access any offset of the disk and read/write arbitrary data at will. The Length parameter must be a multiple of the size of the sector of the disk we interact with.

At this point we knew we had full access to the main hard disk, so we had to do something meaningful with it.

Of course, obtaining disk access at that level opens many doors for attacking the affected host, but since we wanted something with immediate effect, without requiring a reboot of the system, we went for the privilege escalation on run-time attack vector.

In this case, we can’t manipulate any objects or data in memory, but we can do so for anything on disk, and for that reason the idea was to manipulate a file or a directory in a way that will allow us to perform some actions that will result to privilege escalation.

NTFS – objects security model

We will now provide a high level overview of how NTFS object security is enforced in order to assist the reader with understanding the concepts upon which our exploitation approaches were based.

In earlier versions of NTFS, and more specifically from version 1.2, each  file record entry of the Master File Table (MFT) [7], or in other words each file (directories are stored as file record entries as well), had its own copy of the $SECURITY_DESCRIPTOR  attribute [8] which contained the access control list (ACL) that is required in order to enforce the security permissions for each object.

Recent versions of NTFS, version 3.0 upwards, make use of a metadata file called $Secure (MFT Record number 9) [9] which has a named data stream called Security Descriptor Stream ($SDS) that contains a list of all the security descriptors on the volume with the associated ACL.

That being said, each file no longer has its own $SECURITY_DESCRIPTOR attribute. Instead, security descriptors are shared among files that require the same access permissions.

In other words, each MFT file record references a Security Id through its $STANDARD_INFORMATION attribute [10]. That Security Id is a unique ‘key’ reference to another table, the Security Id Index ($SII), of which the corresponding entry contains information for the associated Security Descriptor entry in the $SDS.

By using the Security Id to locate the associated entry in the $SII, the filesystem can then locate the correct Security Descriptor in the $SDS by extracting its relative offset in that data stream from the $SII entry, and perform the security check.

Since many files in a system require the same access permissions, it is more efficient to have a unique copy of a Security Descriptor for each group of files that have the same security enforcement requirements.

When a new file or folder is created, a 32-bit hash of the required Security Descriptor is calculated and is used as an index for another table called Security Descriptor Hash index which, similarly to the $SII, is used to map those hashes instead to security descriptors in the $SDS stream. If a match is found, which means that there is already a Security Descriptor that satisfies the security requirements for the new object, then the Security Id is extracted and stored in the $STANDARD_INFORMATION attribute of the corresponding MFT record.

On the other hand, if a matching Security Descriptor does not exist, then a unique Security Id will be assigned and new entries will be added to the $SDS stream, and the $SII and $SDH tables.

Note that, during the aforementioned checks, both the 32-bit hash and the security descriptors have to match in order to use the same Security Id for the new file as well. This is done in order to avoid collisions where different security descriptors could result in the same 32-bit hash identifier.

A closer look at a $Secure file

In this section we will examine an instance of a $Secure file in order to manually look up the information that we mentioned in the previous section.

Each MFT Record contains a set of attributes and each one of those provides information about the associated file on disk. Here we provide some information about a few of those.

For example, the $STANDARD_INFORMATION (0x10) attribute provides information about file creation, modification, access times, Security Id etc.

On the other hand, the  $FILE_NAME (0x30) attribute [11] provides information about the parent directory, file flags (directory, hidden, read-only etc…), filename length, filename namespace, file name in Unicode, etc.

In addition, the $DATA (0x80) attribute [12] contains the data of the file or information on where to find the data of the file if it doesn’t fit inside the MFT record itself, as usually happens.

Another example of an attribute that we are about to meet is the $INDEX_ALLOCATION (0xA0) [13] which provides information about the storage location of an index table, such as the $SII and $SDH tables that we mentioned earlier.

In the following image we have highlighted some interesting parts of the MFT record under examination.

Apart from the highlighted areas related to Data Runs [14] which are part of a different attribute in each case and the Security Id that belongs to the $STANDARD_INFORMATION attribute, you’ll notice that the first 4 bytes ( DWORD) represent the id of an attribute, while the second DWORD refers to the size of the attribute, including its header.

In order to understand how to find the associated data, we need to explain how Data Runs are interpreted.

The low nibble of the first byte refers to the number of bytes used to represent the amount of clusters reserved for this run of data. The high nibble refers to the number of bytes used to represent the cluster number where the associated data starts from, passed the number of bytes previously discussed.

Example for $SDS Data Run: 0x31 0x46 0xC1 0xBB 0x0B

  • 0x31
    • 1 byte to represent amount of clusters – 0x46 clusters reserved
    • 3 bytes to represent the cluster number – 0xBBBC1

In our case, each cluster consists of 8 sectors, and each sector occupies 0x200 (512) bytes. This information is taken from the BIOS parameter block (BPB) of the boot sector of the volume [15].

Furthermore, in this example we see only one data run sequence. If there were more, because the data might be fragmented, then the rules change a bit in order to find the next data sequence. In practice, the next sequence, instead of having the starting cluster number, would have an offset (signed number) relative to the previous run, and so on.

Also, note that since the vulnerability gives raw access to the hard disk, it means that in our calculations for the exploit we also need to take into consideration the space between the very first sector of the disk and the boot sector of the partition of the running instance of the OS.

In this case the boot sector is located at offset 0x100000. So, whatever calculations we do, we need to add that number in order to get the raw offset of the data on disk.

Offset of the $SDS data = (0xBBBC1 * 8 * 0x200) + 0x100000 = 0xBBCC1000.

Decoding a security descriptor

In the image above we show the first two security descriptors. Keep in mind that these are always 16 bytes aligned. So, even though the first Security Descriptor [16] reports to be 0x78 bytes long, the next one starts at relative offset 0x80 ( 0xBBCC1080).

Let’s decode the first one in order to understand their layout.


  • Security Descriptor’s Hash: 0xCBC6FE32
  • Security Id: 0x100
  • Security Descriptor’s offset in the SDS stream: 0x00
  • Size of Security Descriptor: 0x78
  • Revision: 0x01
  • Padding: 0x00
  • ACL control flags: 0x8004
  • Offset to User SID: 0x48
  • Offet to Group SID: 0x54
  • Offset to SACL: 0x00 (not present)
  • Offset to DACL: 0x14


  • Revision: 0x02
  • Padding: 0x00
  • ACL size: 0x34
  • ACE count: 0x02
  • Padding: 0x00

ACE #1

  • ACE Access Allowed type: 0x00
  • ACE Flags: 0x00
  • ACE Size: 0x14
  • Access Mask: 0x00120089
  • SID: S-1-5-18

ACE #2

  • ACE Access Allowed type: 0x00
  • ACE Flags: 0x00
  • ACE Size: 0x18
  • Access Mask: 0x00120089
  • SID: S-1-5-32-544

User SID


Group SID


From Security Id to Security Descriptor

In this section we will locate the Security Descriptor that is associated with the Security Id of an MFT file record by using the $INDEX_ALLOCATION attribute corresponding to the $SII.

We can use the MFT entry for the $Secure file as shown in the image above.

The Security Id in this case is 0x101, and the data runs correspond to the sequence: 0x41 0x02 0x41 0xC9 0x30 0x01.

In other words, we have two clusters reserved starting at cluster 0x130C941.

Disk offset of the $SII table = (0x130C941 * 8 * 0x200) + 0x100000 = 0x130CA41000

We can see the corresponding entry for this Security Id, which gives us the offset ( 0x80) of the Security Descriptor inside the $SDS stream. In this case it’s the second entry. Notice also that the hash of the Security Descriptor taken from the $SII entry matches the hash in the $SDS entry.

Exploitation approaches

Approach A

Search the disk for a registry key associated with a service that we can start as a standard user and point it to our own binary by directly modifying the data.

This was our first approach. It looked quite straight forward, although it could take several seconds to locate the correct data on disk. It seems that registry hives are cached and for a non-admin user, this is a problem. We didn’t find a straight forward way to flush the cache without doing some really resource-intensive stuff that would normally result in freezing the host for several seconds. This method worked well from any user account level, but it required a reboot of the host.

Approach B

Parse the $SDS stream, find all interesting Security Descriptors and modify them to give full access to everyone on the protected file objects.

However, caching kicks in again. According to Windows Internals, the 32 most recently accessed Security Descriptors are cached. Now, by remembering the fact that these are shared among files with the same security access requirements and that the OS by itself keeps accessing interesting files in Windows and System32 directories, this failed again to work on run-time. However, as in the previous case, it worked perfectly after reboot.

Approach C

Instead of trying to corrupt Security Descriptors, locate the MFT record of the target binary, such as a Service DLL, and change the Security Id to a new one. This is also the method we used in our exploit.

However, there are a few details to take care of.

First, we need to know which Security Id to use to replace the original one. Of course, we need one that corresponds to a Security Descriptor that allows us to do whatever we want.

We can solve this problem by creating a new file and setting up a custom Security Descriptor for it, which will cause the system to either create a new Security Id for it, or assign an existing one that corresponds to a Security Descriptor that allows us to have full access to that object. We can then do a quick search for our file per MFT record, extract the Security Id and use it to replace the one of an interesting binary.

This approach worked quite well, giving us the ability to overwrite a system service DLL and elevate our privileges on run-time.

We noticed that this method doesn’t have an immediate effect for any system binary. For example, if we attempt to do this for a binary in use, again the security settings will not change until reboot. However, there are plenty of options and we did find various DLLs that we could use for privilege escalation. These were DLLs that were not loaded or recently accessed by another process.

Finally, another important detail that we need to address is to ensure that the MFT record of a file with the same name it actually corresponds to the file we are targeting.

In other words, only taking into consideration the file name entry of the  $FILE_NAME (0x30) attribute is not enough for the obvious reason that we might be looking into the wrong file.

For example, in 64-bit Windows it is common to have duplicates of the same system files in the  System32 and SysWOW64 directories, with those in the latter being the 32-bit builds that we are not interested in.

To solve this problem, we can take into consideration the reference to the MFT record of the parent directory, again through the $FILE_NAME (0x30) attribute, and parse the tree backwards to the MFT record of the root directory named as . (dot).

In other words, we know that if it’s the right target file, then the first parent directory reference (going backwards) is System32, the following is Windows, and the third and final is a reference to the MFT record of the root directory, which by default corresponds to the MFT record number 5 [17].

Approach D

Since you managed to arrive at this point we would like to share another experimental idea with you, which we haven’t tried.

The concept behind this one is around allocating some kernel paged pool objects, waiting for them to be paged out, and then corrupting them on disk before attempting to re-use them from the exploit process.

This might not be even possible, or at least practical, but if you are adventurous enough to try this out, please do let us know if you get any interesting results.

Exploitation Steps

So, let’s sum up the various stages of the exploit by using the third method as described above.

  1. Locate Boot Sector and read physical parameters of the volume from BPB.
  2. Create a new file with a custom Security Descriptor.
  3. Search the disk for our file using information from step 1 and extract the Security Id from the  MFT record that corresponds to the file that we created in step 2.
  4. Search the disk for the target binary file to overwrite and replace the Security Id in its MFT record.
  5. Save its original contents and overwrite or patch the binary with our code.
  6. Trigger the system Service/Task to elevate privileges.
  7. Restore original binary.
  8. w00t!


If the disk is fully encrypted, then by attempting to perform raw disk access we will just read back encrypted data that will make no sense. That being said, the File Share Encryption and Desktop Email Encryption products of the Symantec Encryption Desktop suite are the main targets of this vulnerability for direct privilege escalation, without requiring a reboot.

Regarding the case of Symantec Endpoint Encryption, the driver is still affected by the same security issue, but it does not allow the attacker to set arbitrary disk offset to read/write from. The offset has to be in the range of the first two disk sectors with regards to IOCTLs 0x8002206C and 0x80022070. However, we can still control the size of the data and so it can potentially be abused in the same way. By digging a bit further, we also discovered that IOCTL 0x8002208C for the same driver would allow us to perform fully controlled read operations starting from any disk offset. However, since this product is used to fully encrypt the disk, we would still read junk data if we tried to to access the disk in that way to read arbitrary data.

What is important to mention is that for all the aforementioned products, the vulnerability allows the attacker to perform further low level attacks on the disk, such as modifying the MBR, which is not encrypted, and can be used to install a bootkit and thus become abused by an APT that will be then able to execute itself with highest privileges, or a Ransomware threat (Petya) [18] as we have seen happening a lot recently.

Of course, destroying data is also an attack scenario, and it’s possible both with encryption being present or not.


Exploiting a vulnerability that is different from a memory corruption bug was a really interesting journey. One of the most interesting parts of this process was trying to trick and bypass the caching behavior that caused our first exploitation methods to fail on runtime.

At one point we actually thought that we had to stick with rebooting the host to complete the exploitation process, but then the idea of modifying the Security Id of a target MFT record came up and we managed to win this battle.

If you have more ideas about exploiting this or similar vulnerabilities on run-time and/or in other ways, then please share them with the community and let us know your thoughts.

Disclosure Timeline

  • Vendor contacted – 12 July 2017.
  • Vendor assigned a Tracking ID – 12 July 2017.
  • Asked vendor for patch release confirmation – 11 October 2017.
  • Vendor replied all issues under same Tracking ID are fixed – 11 October 2017.
  • Contacted vendor to report the vulnerability is still there in the latest version – 11 October 2017.
  • Vendor sent new builds that include latest hotfixes for various issues – 12 October 2017.
  • Vendor reported the vulnerability is still not fixed and postponed – 12 October 2017.
  • Contacted vendor to confirm that the vulnerability is still not fixed – 13 October 2017.
  • Public Disclosure – 28 November 2017.
  • Vendor (as a CNA) Assigns CVE Number CVE-2019-9702 – 20 June 2019.
  • Vendor patches Symantec Endpoint Encryption in version 11.3.0.  No plans to patch Symantec Desktop Encryption at this time – 20 June 2019.